-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathfeed.rss
More file actions
1310 lines (1267 loc) · 106 KB
/
feed.rss
File metadata and controls
1310 lines (1267 loc) · 106 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<?xml version="1.0" encoding="utf-8"?>
<rss xmlns:content="http://purl.org/rss/1.0/modules/content/" version="2.0">
<channel>
<title>@devlead - Mattias Karlsson's Blog</title>
<link>https://www.devlead.se/</link>
<description />
<copyright>© Mattias Karlsson 2026</copyright>
<pubDate>Mon, 18 May 2026 09:56:30 GMT</pubDate>
<lastBuildDate>Mon, 18 May 2026 09:56:30 GMT</lastBuildDate>
<item>
<title>A quarter of a billion NuGet downloads</title>
<link>https://www.devlead.se/posts/2026/2026-03-22-quarter-billion-nuget-downloads</link>
<description>A personal milestone and a short reflection on NuGet's history and what it has enabled for open source and the community</description>
<author>devlead</author>
<enclosure url="https://cdn.devlead.se/clipimg-vscode/2026/03/22/019d14ed-ff79-7c53-b9ed-40e27779409a.png?sv=2025-07-05&spr=https&st=2026-03-22T05%253A31%253A00Z&se=2036-03-23T09%253A31%253A00Z&sr=b&sp=r&sig=cL%252FseHDMzerFGyf5a0w0H2JNWWfTytZ5tOqcVCBrueY%253D" length="0" type="image" />
<guid>https://www.devlead.se/posts/2026/2026-03-22-quarter-billion-nuget-downloads</guid>
<pubDate>Sun, 22 Mar 2026 00:00:00 GMT</pubDate>
<content:encoded><p>The total downloads of my packages on <a href="https://www.nuget.org/profiles/devlead">NuGet.org</a> have crossed a quarter of a billion. It is a vanity number, but it got me thinking less about the count and more about the platform that made it possible. So here is a short look back at NuGet, the positive bits, and what it has enabled for open source and the community.</p>
<h2 id="nuget-in-context">NuGet in context</h2>
<p>An essential tool for any modern development platform is a way for developers to create, share, and consume useful libraries. For .NET, that mechanism is <a href="https://www.nuget.org/policies/About">NuGet</a>. It started out as NuPack, was <a href="https://haacked.com/archive/2010/10/29/nupack-is-now-nuget.aspx/">renamed to NuGet</a> in 2010, and became one of Microsoft's early forays into open source package management under the OuterCurve Foundation. Later, and still today, NuGet is part of the <a href="https://dotnetfoundation.org/">.NET Foundation</a>. The <a href="https://blog.davidebbo.com/2011/01/introducing-nuget-gallery.html">NuGet Gallery</a> followed in 2011, and the rest is history.</p>
<p>In May 2016, <a href="https://devblogs.microsoft.com/dotnet/the-1st-billion-1/">NuGet.org reached its first billion downloads</a>. Today the gallery has passed 895 billion package downloads, over 11 million package versions, and over half a million unique packages. The ecosystem has grown enormously around that: .NET Framework, .NET Core and the unified .NET platform, better tooling, and a gallery that keeps improving. What has stayed constant is that the <a href="https://www.nuget.org/policies/About">NuGet Gallery is open source</a>, licensed under the Apache 2 License on GitHub. The NuGet team and a long list of community contributors have built and maintained the site that powers NuGet.org. That openness is something worth celebrating.</p>
<h2 id="what-nuget-has-enabled">What NuGet has enabled</h2>
<p>For open source, NuGet has become the place where .NET libraries and tools get discovered and reused. Maintainers can publish once and reach millions of developers. Projects like <a href="https://cakebuild.net/">Cake</a> and countless libraries, tools, and templates are easily available because there is a single, trusted place to host and consume packages. That has made it far easier for teams and individuals to adopt .NET open source.</p>
<p>For the community, NuGet has evolved beyond the basics. Package signing, two-factor authentication, <a href="https://devblogs.microsoft.com/dotnet/nugetaudit-2-0-elevating-security-and-trust-in-package-management/">NuGetAudit</a>, and <a href="https://devblogs.microsoft.com/dotnet/enhanced-security-is-here-with-the-new-trust-publishing-on-nuget-org/">Trusted Publishing</a> have raised security and trust. <a href="https://devblogs.microsoft.com/dotnet/announcing-sponsorship-on-nugetdotorg-for-maintainer-appreciation/">Sponsorship on NuGet.org</a> gives users a way to support maintainers. The gallery itself has improved with better search, <a href="https://devblogs.microsoft.com/dotnet/introducing-central-package-management/">Central Package Management</a> support, dark mode, and clearer package details. None of that happens without a platform that the team and community keep investing in. Development does not always move as fast as users would like, but that is the trade-off when you are also operating and maintaining a service that so many developers depend on every day.</p>
<p>For me, NuGet has been the channel for publishing and maintaining Cake-related packages and .NET tools such as <a href="https://www.nuget.org/packages/Cake.Tool">Cake.Tool</a>, <a href="https://www.nuget.org/packages/Cake.Git">Cake.Git</a> and latest being <a href="https://www.nuget.org/packages/Cake.Sdk">Cake.Sdk</a>, along with several other tools and libraries like <a href="https://www.nuget.org/packages/LitJson">LitJSON</a>, <a href="https://www.nuget.org/packages/DPI">DPI</a>, <a href="https://www.nuget.org/packages/ARI">ARI</a>, <a href="https://www.nuget.org/packages/BRI">BRI</a>, <a href="https://www.nuget.org/packages/Blobify">Blobify</a>, and <a href="https://www.nuget.org/packages/UnpackDacPac">UnpackDacPac</a>, etc. It has also given me the chance to give back. The gallery is open for contributions, and I was able to add <a href="https://github.com/NuGet/NuGetGallery/pull/10277">Central Package Management and multiple package manager commands to the NuGet.org command palette</a>. That kind of improvement is possible because the gallery is open source and the team welcomes community pull requests.</p>
<h2 id="thanks">Thanks</h2>
<p>Having reached a quarter of a billion downloads is not something one does alone. Thank you to everyone who uses these packages, to my projects co-maintainers, and to the NuGet team and everyone who contributes to the gallery and the ecosystem. NuGet has come a long way since 2010, and I am looking forward to seeing how it continues to improve and support maintainers and the .NET community.</p>
</content:encoded>
<comments xmlns="http://purl.org/rss/1.0/modules/slash/">0</comments>
</item>
<item>
<title>Mend Renovate Now Supports C# File-Based Apps and Cake.Sdk</title>
<link>https://www.devlead.se/posts/2026/2026-02-26-renovate-csharp-file-based-apps</link>
<description>Renovate can now update dependencies in C# single-file scripts and Cake.Sdk build scripts with #:sdk, #:package, and InstallTool.</description>
<author>devlead</author>
<enclosure url="https://cdn.devlead.se/clipimg-vscode/2026/02/26/a39f98b5-b854-48a8-a44b-2affb1b8d043.png?sp=r&st=2026-02-26T22:10:14Z&se=2036-02-27T06:25:14Z&spr=https&sv=2024-11-04&sr=b&sig=FIy9HlptIfMEwPF9kW3iJoNqz6iN%252BMMqX%252BezOGZ9DNM%253D" length="0" type="image" />
<guid>https://www.devlead.se/posts/2026/2026-02-26-renovate-csharp-file-based-apps</guid>
<pubDate>Thu, 26 Feb 2026 00:00:00 GMT</pubDate>
<content:encoded><p><a href="https://github.com/renovatebot/renovate">Mend Renovate</a> automates dependency updates by opening pull requests when newer versions of your dependencies are available. Until recently, if you used .NET single-file scripts or <a href="https://cakebuild.net/docs/running-builds/runners/cake-sdk">Cake.Sdk</a> build scripts written in C# (e.g. <code>cake.cs</code> or <code>build.cs</code>), Renovate did not look inside those files. Two merged changes fix that: the NuGet manager now understands <code>#:sdk</code> and <code>#:package</code> directives in C# files, and the Cake manager can extract and update packages from <a href="https://cakebuild.net/docs/writing-builds/sdk/tools"><code>InstallTool</code></a> and <code>InstallTools</code> calls. In this post I'll summarize what was added and how to enable it in your repo.</p>
<h2 id="what-changed">What Changed</h2>
<p>.NET 10 introduced <a href="https://devblogs.microsoft.com/dotnet/announcing-dotnet-run-app/">file-based execution</a> for single-file C# apps. In that model, dependencies are declared with <code>#:sdk</code> and <code>#:package</code> at the top of the file instead of in a <code>.csproj</code>. Cake.Sdk uses the same mechanism for build scripts, and tools are installed via <code>InstallTool()</code> or <code>InstallTools()</code> rather than the old <code>#tool</code> directive. Renovate's default file patterns only include project and config files (e.g. <code>.csproj</code>, <code>global.json</code>, <code>dotnet-tools.json</code>), so it did not previously scan plain <code>.cs</code> files. That meant dependencies in <code>cake.cs</code> or other single-file C# scripts were never considered for updates.</p>
<p>Two pull requests extend Renovate to support these formats:</p>
<ol>
<li><p><strong><a href="https://github.com/renovatebot/renovate/pull/40040">feat(manager/nuget): Support single file package directives</a></strong> (merged in <a href="https://github.com/renovatebot/renovate/releases/tag/43.26.0">v43.26.0</a>)<br />
The NuGet manager can now extract and update dependencies from C# files that use <code>#:sdk</code> and <code>#:package</code>. Because the target files are regular source files, they are not part of the default <code>managerFilePatterns</code>. You must add the patterns you want (e.g. <code>/\\.cs$/</code> or something narrower) in your Renovate config.</p>
</li>
<li><p><strong><a href="https://github.com/renovatebot/renovate/pull/40070">feat(manager/cake): Support extracting nuget packages from InstallTools helper methods</a></strong> (merged in <a href="https://github.com/renovatebot/renovate/releases/tag/43.41.0">v43.41.0</a>)<br />
The Cake manager gains support for single-file Cake.Sdk build scripts. It can extract NuGet package references from <code>InstallTool()</code> and <code>InstallTools()</code> method parameters (e.g. <code>InstallTool(&quot;dotnet:https://api.nuget.org/v3/index.json?package=DPI&amp;version=2025.12.17.349&quot;);</code>). By default the Cake manager still only matches <code>*.cake</code> files, so to have Renovate process C# Cake scripts you need to extend the Cake manager's file patterns in config.</p>
</li>
</ol>
<p>So in practice: use the <strong>NuGet</strong> manager for <code>#:sdk</code> and <code>#:package</code> in any C# file, and use the <strong>Cake</strong> manager for Cake-specific constructs (including <code>InstallTool(s)</code>) in C# files. For a typical <code>cake.cs</code> you will often enable both and let each manager handle the directives it understands.</p>
<h2 id="what-renovate-can-update">What Renovate Can Update</h2>
<p>In a C# single-file script or Cake.Sdk build file, Renovate can now update:</p>
<ul>
<li><strong><code>#:sdk</code></strong> – e.g. <code>#:sdk Cake.Sdk&#64;6.0.0</code></li>
<li><strong><code>#:package</code></strong> – e.g. <code>#:package Cake.Core&#64;6.0.0</code></li>
<li><strong><a href="https://cakebuild.net/docs/writing-builds/sdk/tools">InstallTool() / InstallTools()</a></strong> – e.g. <code>InstallTool(&quot;dotnet:https://api.nuget.org/v3/index.json?package=DPI&amp;version=2025.12.17.349&quot;);</code></li>
</ul>
<p>The <code>#:sdk</code> and <code>#:package</code> directives are handled by the <a href="https://docs.renovatebot.com/modules/manager/nuget/">NuGet manager</a>. The <a href="https://docs.renovatebot.com/modules/manager/cake/">Cake manager</a> handles the Cake-specific tool installation methods. Both managers need to be told to look at <code>.cs</code> files (or the specific paths you use) via configuration.</p>
<h2 id="enabling-it-in-your-repository">Enabling It in Your Repository</h2>
<p>Renovate reads config from your repo (e.g. <code>renovate.json</code> or <code>renovate.json5</code>) and merges it with <a href="https://docs.renovatebot.com/config-overview/">default and inherited config</a>. To have Renovate consider your C# single-file scripts and Cake.Sdk build files, add the corresponding <code>managerFilePatterns</code> (or the <code>fileMatch</code>-style option your preset uses) so that the NuGet and Cake managers include those files.</p>
<p>Example addition to your Renovate config (e.g. in <code>renovate.json</code>):</p>
<pre><code class="language-json">{
&quot;$schema&quot;: &quot;https://docs.renovatebot.com/renovate-schema.json&quot;,
&quot;nuget&quot;: {
&quot;managerFilePatterns&quot;: [&quot;/\\.cs$/&quot;]
},
&quot;cake&quot;: {
&quot;managerFilePatterns&quot;: [&quot;/\\.cs$/&quot;]
}
}
</code></pre>
<p>If you only want to include specific script names (e.g. only <code>cake.cs</code> and <code>build.cs</code>), you can narrow the pattern:</p>
<pre><code class="language-json">{
&quot;nuget&quot;: {
&quot;managerFilePatterns&quot;: [&quot;/(^|/)(cake|build)\\.cs$/&quot;]
},
&quot;cake&quot;: {
&quot;managerFilePatterns&quot;: [&quot;/(^|/)(cake|build)\\.cs$/&quot;]
}
}
</code></pre>
<p>After this, Renovate will detect dependencies in those C# files and open PRs when updates are available. You can combine this with your existing presets (e.g. <code>config:recommended</code>) and other options like <code>packageRules</code> or scheduling as needed.</p>
<h2 id="example-repositories-and-prs">Example Repositories and PRs</h2>
<p>Real-world examples of Renovate running against Cake.Sdk-style repos:</p>
<ul>
<li><strong><a href="https://github.com/devlead/Devlead.Console">devlead/Devlead.Console</a></strong> – <a href="https://github.com/devlead/Devlead.Console/pull/239">PR #239</a> shows Renovate updating dependencies in a C# build script.</li>
<li><strong><a href="https://github.com/azurevoodoo/renovate-cake-sdk-test">azurevoodoo/renovate-cake-sdk-test</a></strong> – A small test repo used to validate the feature; e.g. <a href="https://github.com/azurevoodoo/renovate-cake-sdk-test/pull/4">PR #4</a>, <a href="https://github.com/azurevoodoo/renovate-cake-sdk-test/pull/8">PR #8</a>, <a href="https://github.com/azurevoodoo/renovate-cake-sdk-test/pull/9">PR #9</a>.</li>
</ul>
<p>Running Renovate locally against that test repo with a config that includes <code>(^|/)(cake|build)\\.cs$</code> for the Cake manager produces PRs for outdated packages in those files without issues.</p>
<h2 id="summary">Summary</h2>
<p>If you use C# single-file apps or Cake.Sdk build scripts with <code>#:sdk</code>, <code>#:package</code>, and <code>InstallTool(s)</code>, you can now keep those dependencies up to date with Mend Renovate. Add the appropriate <code>managerFilePatterns</code> for the NuGet and Cake managers so they scan your <code>.cs</code> files (or the specific paths you use), and Renovate will open pull requests for available updates. For more on the managers and config, see the <a href="https://docs.renovatebot.com/modules/manager/nuget/">NuGet manager docs</a>, the <a href="https://docs.renovatebot.com/modules/manager/cake/">Cake manager docs</a>, and the <a href="https://docs.renovatebot.com/config-overview/">Renovate configuration overview</a>.</p>
</content:encoded>
<comments xmlns="http://purl.org/rss/1.0/modules/slash/">0</comments>
</item>
<item>
<title>Migrating to Cake.Sdk</title>
<link>https://www.devlead.se/posts/2025/2025-07-28-migrating-to-cake-sdk</link>
<description>Taking your build.cake to cake.cs</description>
<author>devlead</author>
<enclosure url="https://cdn.devlead.se/clipimg-vscode/2025/07/28/aba66765-b6fb-4c2a-1553-71ec0c766405.png?sv=2025-01-05&st=2025-07-27T20%253A09%253A59Z&se=2035-07-28T20%253A09%253A59Z&sr=b&sp=r&sig=FsI7ji92JZB3VCtCDhCl5ujmhxlCLoi0xsw1ukh0nFw%253D" length="0" type="image" />
<guid>https://www.devlead.se/posts/2025/2025-07-28-migrating-to-cake-sdk</guid>
<pubDate>Mon, 28 Jul 2025 00:00:00 GMT</pubDate>
<content:encoded><p>The Cake team recently announced <a href="https://cakebuild.net/blog/2025/07/dotnet-cake-cs">Cake.Sdk</a>, a new way to get the Cake tool scripting experience in regular .NET console applications. This brings the stellar experience of the new &quot;dotnet run app.cs&quot; feature (requires .NET 10), while also working seamlessly with .NET 8 and 9 for regular csproj projects.</p>
<p>In this post, I'll walk you through migrating from a traditional Cake .NET Tool build script to the new Cake.Sdk single file approach.</p>
<h2 id="whats-changing">What's Changing</h2>
<p>The migration involves converting from a <code>build.cake</code> file with <code>#addin</code> and <code>#tool</code> directives to a <code>cake.cs</code> file with <code>#:sdk</code> and <code>#:package</code> directives. The <code>#tool</code> directive is replaced with the <code>InstallTool()</code> method call. The new approach leverages .NET 10's file-based execution while maintaining all the familiar Cake functionality.</p>
<h2 id="migration-steps">Migration Steps</h2>
<h3 id="update-global.json">1. Update global.json</h3>
<p>First, add the Cake.Sdk to your <code>global.json</code> to pin the version:</p>
<pre><code class="language-json">{
&quot;sdk&quot;: {
&quot;version&quot;: &quot;10.0.100-preview.6.25358.103&quot;,
&quot;allowPrerelease&quot;: false,
&quot;rollForward&quot;: &quot;latestMajor&quot;
},
&quot;msbuild-sdks&quot;: {
&quot;Cake.Sdk&quot;: &quot;5.0.25198.49-beta&quot;
}
}
</code></pre>
<h3 id="rename-and-update-build-script">2. Rename and Update Build Script</h3>
<p>Rename your <code>build.cake</code> to <code>cake.cs</code> and update the directives:</p>
<p><strong>Example before (build.cake):</strong></p>
<pre><code class="language-csharp">#addin nuget:?package=Cake.FileHelpers&amp;version=7.0.0
#addin nuget:?package=Newtonsoft.Json&amp;version=13.0.3
#tool dotnet:?package=GitVersion.Tool&amp;version=5.12.0
// ... existing code ...
</code></pre>
<p><strong>Example after (cake.cs):</strong></p>
<pre><code class="language-csharp">#:sdk Cake.Sdk
#:package Cake.FileHelpers
#:package Newtonsoft.Json
InstallTool(&quot;dotnet:https://api.nuget.org/v3/index.json?package=GitVersion.Tool&amp;version=5.12.0&quot;);
// ... existing code ...
</code></pre>
<h3 id="update-package-management">3. Update Package Management</h3>
<p>For Central Package Management (CPM), add the packages to your <code>Directory.Packages.props</code>:</p>
<pre><code class="language-xml">&lt;ItemGroup&gt;
&lt;PackageVersion Include=&quot;Cake.FileHelpers&quot; Version=&quot;7.0.0&quot; /&gt;
&lt;PackageVersion Include=&quot;Newtonsoft.Json&quot; Version=&quot;13.0.3&quot; /&gt;
&lt;!-- ... existing packages ... --&gt;
&lt;/ItemGroup&gt;
</code></pre>
<p>Alternatively, you can specify versions directly in the package directives:</p>
<pre><code class="language-csharp">#:package Cake.FileHelpers&#64;7.0.0
#:package Newtonsoft.Json&#64;13.0.3
</code></pre>
<h3 id="update-build-command">4. Update Build Command</h3>
<p>The build command changes from:</p>
<pre><code class="language-bash">dotnet cake build.cake
</code></pre>
<p>To:</p>
<pre><code class="language-bash">dotnet cake.cs
</code></pre>
<h2 id="key-differences">Key Differences</h2>
<h3 id="package-references">Package References</h3>
<p><strong>Old way:</strong></p>
<pre><code class="language-csharp">#addin nuget:?package=Cake.FileHelpers&amp;version=7.0.0
#addin nuget:?package=Newtonsoft.Json&amp;version=13.0.3
</code></pre>
<p><strong>New way:</strong></p>
<pre><code class="language-csharp">#:package Cake.FileHelpers
#:package Newtonsoft.Json
</code></pre>
<h3 id="tool-installation">Tool Installation</h3>
<p><strong>Old way:</strong></p>
<pre><code class="language-csharp">#tool dotnet:?package=GitVersion.Tool&amp;version=5.12.0
</code></pre>
<p><strong>New way:</strong></p>
<pre><code class="language-csharp">InstallTool(&quot;dotnet:https://api.nuget.org/v3/index.json?package=GitVersion.Tool&amp;version=5.12.0&quot;);
</code></pre>
<h2 id="requirements">Requirements</h2>
<ul>
<li><strong>File-based approach</strong>: .NET 10 Preview 6 or later</li>
<li><strong>Project-based approach</strong>: .NET 8.0 or later</li>
</ul>
<h2 id="benefits">Benefits</h2>
<ol>
<li><strong>Simplified Setup</strong>: No need for wrapper scripts or tool installation</li>
<li><strong>Better IDE Support</strong>: Full IntelliSense and debugging capabilities</li>
<li><strong>Centralized Package Management</strong>: Works seamlessly with CPM</li>
<li><strong>Standard NuGet Auth Support</strong>: Uses your existing NuGet configuration and credentials</li>
<li><strong>.NET SDK Tooling</strong>: Leverages standard .NET tooling and build processes</li>
<li><strong>Directory.Build.props/targets Support</strong>: Integrates with MSBuild's directory-level customization for build settings</li>
</ol>
<h2 id="converting-to-project-based">Converting to Project-Based</h2>
<p>If you prefer a traditional project-based approach, you can convert your <code>cake.cs</code> file to a full .NET project using:</p>
<pre><code class="language-bash">dotnet project convert cake.cs
</code></pre>
<p>This command creates a new directory named for your file, scaffolds a <code>.csproj</code> file, moves your code into the new directory as <code>cake.cs</code>, and translates any <code>#:</code> directives into MSBuild properties and references.</p>
<p><strong>Before (cake.cs):</strong></p>
<pre><code class="language-csharp">#:sdk Cake.Sdk
#:package Cake.FileHelpers
#:package Newtonsoft.Json
#:property ProjectType=Test
// ... existing code ...
</code></pre>
<p><strong>After (cake/cake.csproj):</strong></p>
<pre><code class="language-xml">&lt;Project Sdk=&quot;Cake.Sdk&quot;&gt;
&lt;PropertyGroup&gt;
&lt;OutputType&gt;Exe&lt;/OutputType&gt;
&lt;TargetFramework&gt;net10.0&lt;/TargetFramework&gt;
&lt;ImplicitUsings&gt;enable&lt;/ImplicitUsings&gt;
&lt;Nullable&gt;enable&lt;/Nullable&gt;
&lt;PublishAot&gt;true&lt;/PublishAot&gt;
&lt;/PropertyGroup&gt;
&lt;PropertyGroup&gt;
&lt;ProjectType&gt;Test&lt;/ProjectType&gt;
&lt;/PropertyGroup&gt;
&lt;ItemGroup&gt;
&lt;PackageReference Include=&quot;Cake.FileHelpers&quot; /&gt;
&lt;PackageReference Include=&quot;Newtonsoft.Json&quot; /&gt;
&lt;/ItemGroup&gt;
&lt;/Project&gt;
</code></pre>
<p>The project-based approach works with .NET 8, 9, and 10, while the file-based approach requires .NET 10.</p>
<h2 id="real-world-example">Real-World Example</h2>
<p>The <a href="https://github.com/App-vNext/Polly">Polly</a> project recently migrated from Cake .NET Tool to Cake.Sdk in their <a href="https://github.com/App-vNext/Polly/pull/2676">dotnet-vnext branch</a>. The migration involved:</p>
<ul>
<li>Renaming <code>build.cake</code> to <code>cake.cs</code> and adding SDK directives</li>
<li>Updating <code>build.ps1</code> to use <code>dotnet cake.cs</code> instead of <code>dotnet cake</code></li>
<li>Adding <code>Cake.Sdk</code> to <code>global.json</code> <code>msbuild-sdks</code> section</li>
<li>Moving <code>Cake.FileHelpers</code> and <code>Newtonsoft.Json</code> from <code>#addin</code> to <code>#package</code> directives</li>
<li>Adding <code>ProjectType=Test</code> property for analyzer support</li>
</ul>
<p>This real-world example demonstrates how straightforward the migration process is, even for large, complex projects like Polly.</p>
<h2 id="conclusion">Conclusion</h2>
<p>The migration to Cake.Sdk is straightforward and brings significant improvements to the development experience. The new approach maintains all existing functionality while providing better tooling support, simplified project structure, and enhanced IDE integration.</p>
<p>For more information, check out the <a href="https://cakebuild.net/blog/2025/07/dotnet-cake-cs">official announcement</a> and the <a href="https://github.com/cake-build/cakesdk-example">example repository</a>.</p>
</content:encoded>
<comments xmlns="http://purl.org/rss/1.0/modules/slash/">0</comments>
</item>
<item>
<title>Introducing AZDOI</title>
<link>https://www.devlead.se/posts/2025/2025-03-22-introducing-azdoi</link>
<description>A DevOps tool to document a Azure DevOps organization</description>
<author>devlead</author>
<enclosure url="https://cdn.devlead.se/clipimg-vscode/2025/03/21/70d7054b-ef89-cdf6-88ba-45a98ccc5d6f.png?sv=2025-01-05&st=2025-03-20T18%253A45%253A56Z&se=2035-03-21T18%253A45%253A56Z&sr=b&sp=r&sig=7cf9wdEo49wQF3T9ZJjMkEmLCOeukU6xHW9bQID019Q%253D" length="0" type="image" />
<guid>https://www.devlead.se/posts/2025/2025-03-22-introducing-azdoi</guid>
<pubDate>Sat, 22 Mar 2025 00:00:00 GMT</pubDate>
<content:encoded><p>For the last couple of months, I've had the pleasure of mentoring two talented .NET students, <a href="https://se.linkedin.com/in/andreassiggelin">Andreas Siggelin</a> and <a href="https://se.linkedin.com/in/elie-bou-absi-5b722123a">Elie Bou Absi</a>. They've been working on various internal and customer projects, gaining practical experience in real-world software development. One project we've been collaborating on is AZDOI, a tool designed to document an Azure DevOps organization. After seeing its value in our work, we've decided to open source AZDOI, and in this blog post, I'll walk you through what it is and how you can use it.</p>
<h1 id="what-azdoi-does">What AZDOI Does</h1>
<p>AZDOI (Azure DevOps Inventory) is a .NET tool designed to create documentation for Azure DevOps organizations. The tool connects to the Azure DevOps REST API and iterates over all projects in an organization it has access to, using either personal access token or Azure Entra ID authentication i.e. using a service principle.</p>
<p>For each project, AZDOI systematically inventories all repositories, fetching useful data including default branch, size, and URIs. It goes deeper by also collecting each repository's branches, tags, and README content.</p>
<div class="mermaid">graph LR
Projects[Iterate Projects]
Repos[Iterate Repositories]
Branches[Iterate Branches]
Tags[Iterate Tags]
Tag[Fetch tag annotation]
Readme[Fetch README.md]
Markdown[Generate Markdown]
Projects --> Repos
Repos --> Branches --> Markdown
Repos --> Tags --> Tag --> Markdown
Repos --> Readme --> Markdown
</div>
<p>The output is a well-organized set of Markdown files structured at the organization, project, and repository levels. These generated Markdown files can then serve as source input for a static site generator to create comprehensive, easily navigable, searchable documentation of your entire Azure DevOps organization.</p>
<p>An example of the end result published to GitHub Pages can be found at <a href="https://wcomab.github.io/AZDOI/AZDOI/">https://wcomab.github.io/AZDOI/AZDOI/</a></p>
<p>While AZDOI currently focuses on documenting Azure Repos, future versions may expand to cover other aspects of Azure DevOps like work items, pipelines, and release definitions to provide a more complete organizational view.</p>
<h2 id="getting-started">Getting Started</h2>
<p>AZDOI is a .NET tool distributed via <a href="https://www.nuget.org/packages/AZDOI/">NuGet.org</a>. You can install it either as a globally available tool or as a local tool using a tool manifest.</p>
<h3 id="global-installation">Global Installation</h3>
<p>To install AZDOI as a global tool, run the following command:</p>
<pre><code class="language-bash">dotnet tool install --global AZDOI
</code></pre>
<p>Once installed globally, you can invoke AZDOI simply by using the command:</p>
<pre><code class="language-bash">azdoi
</code></pre>
<h3 id="local-installation">Local Installation</h3>
<p>If you prefer to install AZDOI locally (per repository), you can set up a tool manifest and install it with the following commands:</p>
<pre><code class="language-bash"># Create tool manifest (only needed first time when setting up local/repo-versioned tools)
dotnet new tool-manifest
# Install tool into manifest
dotnet tool install --local AZDOI
</code></pre>
<p>After installing, make sure to commit the manifest file located at <code>.config/dotnet-tools.json</code> into your repository. This allows anyone cloning the repo or using a DevOps pipeline to restore all versioned tools by executing:</p>
<pre><code class="language-bash">dotnet tool restore
</code></pre>
<p>As a local tool, you can invoke AZDOI using:</p>
<pre><code class="language-bash">dotnet azdoi
</code></pre>
<h2 id="usage">Usage</h2>
<p>The basic usage of AZDOI is as follows:</p>
<pre><code class="language-bash">azdoi inventory repositories &lt;devopsorg&gt; &lt;outputpath&gt; [OPTIONS]
</code></pre>
<p>For example:</p>
<pre><code class="language-bash">azdoi inventory repositories AZDOI /path/to/output
</code></pre>
<h3 id="options">Options</h3>
<p>Here are some of the available options you can use with AZDOI inventory repositories command:</p>
<table>
<thead>
<tr>
<th>Parameter</th>
<th>Description</th>
<th>Default Value</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>--help</code></td>
<td>Used to get help with parameters</td>
<td></td>
</tr>
<tr>
<td><code>--pat</code></td>
<td>Personal Access Token for authentication</td>
<td>Environment variable: AZDOI_PAT</td>
</tr>
<tr>
<td><code>--entra-id-auth</code></td>
<td>Use Entra ID for Azure DevOps Authentication</td>
<td>False</td>
</tr>
<tr>
<td><code>--azure-tenant-id</code></td>
<td>Entra Azure Tenant ID for authentication</td>
<td>Environment variable: AZURE_TENANT_ID</td>
</tr>
<tr>
<td><code>--include-project</code></td>
<td>Include specific projects</td>
<td></td>
</tr>
<tr>
<td><code>--exclude-project</code></td>
<td>Exclude specific projects</td>
<td></td>
</tr>
<tr>
<td><code>--include-repository</code></td>
<td>Include specific repositories</td>
<td></td>
</tr>
<tr>
<td><code>--exclude-repository</code></td>
<td>Exclude specific repositories</td>
<td></td>
</tr>
<tr>
<td><code>--include-repository-readme</code></td>
<td>Include specific repository README</td>
<td></td>
</tr>
<tr>
<td><code>--exclude-repository-readme</code></td>
<td>Exclude specific repository README</td>
<td></td>
</tr>
<tr>
<td><code>--run-in-parallel</code></td>
<td>Enable parallel processing of projects</td>
<td>False</td>
</tr>
</tbody>
</table>
<p>When the <code>--entra-id-auth</code> option is specified, AZDOI will attempt to authenticate using the <a href="https://learn.microsoft.com/en-us/dotnet/api/azure.identity.defaultazurecredential?view=azure-dotnet">DefaultAzureCredential</a>, which tries to authorize in the following order based on your environment:</p>
<ol>
<li><a href="https://learn.microsoft.com/en-us/dotnet/api/azure.identity.environmentcredential?view=azure-dotnet">EnvironmentCredential</a></li>
<li><a href="https://learn.microsoft.com/en-us/dotnet/api/azure.identity.workloadidentitycredential?view=azure-dotnet">WorkloadIdentityCredential</a></li>
<li><a href="https://learn.microsoft.com/en-us/dotnet/api/azure.identity.managedidentitycredential?view=azure-dotnet">ManagedIdentityCredential</a></li>
<li><a href="https://learn.microsoft.com/en-us/dotnet/api/azure.identity.sharedtokencachecredential?view=azure-dotnet">SharedTokenCacheCredential</a></li>
<li><a href="https://learn.microsoft.com/en-us/dotnet/api/azure.identity.visualstudiocredential?view=azure-dotnet">VisualStudioCredential</a></li>
<li><a href="https://learn.microsoft.com/en-us/dotnet/api/azure.identity.visualstudiocodecredential?view=azure-dotnet">VisualStudioCodeCredential</a></li>
<li><a href="https://learn.microsoft.com/en-us/dotnet/api/azure.identity.azureclicredential?view=azure-dotnet">AzureCliCredential</a></li>
<li><a href="https://learn.microsoft.com/en-us/dotnet/api/azure.identity.azurepowershellcredential?view=azure-dotnet">AzurePowerShellCredential</a></li>
<li><a href="https://learn.microsoft.com/en-us/dotnet/api/azure.identity.azuredeveloperclicredential?view=azure-dotnet">AzureDeveloperCliCredential</a></li>
<li><a href="https://learn.microsoft.com/en-us/dotnet/api/azure.identity.interactivebrowsercredential?view=azure-dotnet">InteractiveBrowserCredential</a></li>
</ol>
<h2 id="devops-pipeline">DevOps Pipeline</h2>
<p>Below is a fairly minimal Azure Pipeline that runs daily to generate an inventory of Azure DevOps repositories. This pipeline uses Azure CLI authentication and uploads the results as a pipeline artifact.</p>
<pre><code class="language-yaml"># azure-pipelines.yml
trigger:
- main
schedules:
- cron: &quot;0 22 * * *&quot;
displayName: &quot;Daily build at 22:00&quot;
branches:
include:
- main
always: true
pool:
vmImage: 'ubuntu-latest'
steps:
- task: AzureCLI&#64;2
displayName: 'Generate Azure DevOps Inventory'
inputs:
azureSubscription: 'azure-devops-inventory-tool'
scriptType: 'bash'
scriptLocation: 'inlineScript'
inlineScript: |
URL=&quot;$(System.CollectionUri)&quot;
ORG_NAME=${URL#https://dev.azure.com/}
ORG_NAME=${ORG_NAME%/}
echo &quot;Organization name is: $ORG_NAME&quot;
dotnet tool restore \
&amp;&amp; dotnet AZDOI inventory repositories $ORG_NAME &quot;$(Build.ArtifactStagingDirectory)&quot; --entra-id-auth --run-in-parallel \
&amp;&amp; echo '##vso[artifact.upload artifactname=AzureDevOpsDocs]$(Build.ArtifactStagingDirectory)'
</code></pre>
<h3 id="pipeline-configuration-explanation">Pipeline Configuration Explanation</h3>
<p>The Azure Pipeline configuration above does the following:</p>
<h4 id="trigger">Trigger</h4>
<ul>
<li>The pipeline runs automatically when changes are pushed to the <code>main</code> branch.</li>
<li>It is scheduled to run daily at 22:00 (10 PM) UTC through a cron schedule.</li>
<li>The schedule is set to always run, even if there are no code changes.</li>
</ul>
<h4 id="environment">Environment</h4>
<ul>
<li>It uses the latest version of Ubuntu as the build agent.</li>
</ul>
<h4 id="steps">Steps</h4>
<p>The pipeline has a single step using the Azure CLI task that:</p>
<ol>
<li>Extracts the Azure DevOps organization name from the collection URI.</li>
<li>Runs the AZDOI tool to generate an inventory of repositories by:
<ul>
<li>Restoring .NET tools.</li>
<li>Running the <code>inventory repositories</code> command for the organization.</li>
<li>Using Entra ID (formerly Azure AD) authentication.</li>
<li>Running operations in parallel for better performance.</li>
<li>Saving output to the build artifacts directory.</li>
</ul>
</li>
<li>Uploads the generated documentation as a build artifact named 'AzureDevOpsDocs'.</li>
</ol>
<p>The pipeline requires an Azure service connection named 'azure-devops-inventory-tool' with appropriate permissions to access Azure DevOps resources.</p>
<h2 id="conclusion">Conclusion</h2>
<p>In this post, we introduced AZDOI, a .NET tool designed to document Azure DevOps organizations by generating a set of Markdown files. You can find the code for AZDOI at <a href="https://github.com/WCOMAB/AZDOI/">GitHub</a>, and the tool is available for installation via <a href="https://www.nuget.org/packages/AZDOI/">NuGet</a>.</p>
<p>An example of the generated documentation can be viewed at <a href="https://wcomab.github.io/AZDOI/AZDOI/">AZDOI Example Documentation</a>.</p>
<p>Take it for a spin, and feel free to let us know what you think!</p>
</content:encoded>
<comments xmlns="http://purl.org/rss/1.0/modules/slash/">0</comments>
</item>
<item>
<title>Static Web App WASM Search</title>
<link>https://www.devlead.se/posts/2025/2025-03-12-static-web-app-wasm-search</link>
<description>Discover how to add blazing-fast, bandwidth-friendly search to your static website using Pagefind - no backend required!</description>
<author>devlead</author>
<enclosure url="https://cdn.devlead.se/clipimg-vscode/2025/03/12/1a8c12b5-f5c2-9a13-1296-879a0995898a.png?sv=2025-01-05&st=2025-03-11T06%253A09%253A49Z&se=2035-03-12T06%253A09%253A49Z&sr=b&sp=r&sig=tyhiRLHe1gghBnxLo45yhnSUsSiaUbuMQbHAM1pUNG8%253D" length="0" type="image" />
<guid>https://www.devlead.se/posts/2025/2025-03-12-static-web-app-wasm-search</guid>
<pubDate>Wed, 12 Mar 2025 00:00:00 GMT</pubDate>
<content:encoded><p>Static web apps excel at delivering pre-rendered content quickly, efficiently, and cost-effectively.
However, implementing search functionality with these same benefits has traditionally required compromising the static nature by using a backend or third-party service for a good experience. In this post, I'll demonstrate how to add powerful client-side search to a static website using WebAssembly. For my blog, I'm using the <a href="https://www.statiq.dev/">Statiq</a> as my static site generator together with GitHub Pages as my hosting provider, but the approach described should work across most static site generators and hosting platforms.</p>
<h2 id="static-web-app-model">Static web app model</h2>
<p>At its core, static web apps follow a simple yet powerful pattern. Content (the model) typically exists as markdown documents with metadata, while templates (the view) define how that content should be presented. The static site generator acts as the orchestrator, processing this separation of concerns by applying the templates to the content. The result is pre-generated static HTML files for every possible route on the site, ready to be served efficiently to visitors without any runtime processing.</p>
<div class="mermaid">graph LR
subgraph Input
direction LR
I_M[Markdown]
I_IMG[Images]
I_CSS[CSS]
I_JS[JavaScript]
I_HTML[Dynamic&lt;br/>HTML Templates]
end
subgraph Generator
direction LR
S[Static Site Generator]
end
subgraph Output
direction LR
O_IMG[Images]
O_CSS[CSS]
O_JS[JavaScript]
O_HTML[HTML]
end
subgraph Hosting
direction LR
H[Static Hosting Provider]
end
Input --> Generator
Generator --> Output
Output --> Hosting
</div>
<h2 id="adding-static-search">Adding static search</h2>
<p>In this post, we'll implement search functionality by introducing an indexing step that runs after the static site generator has completed its work but before deployment to the hosting provider. This additional step analyzes the generated content and creates a search index that gets included with the site's static assets.</p>
<div class="mermaid">graph LR
subgraph Input
direction LR
I_M[Markdown]
I_IMG[Images]
I_CSS[CSS]
I_JS[JavaScript]
I_HTML[Dynamic&lt;br/>HTML Templates]
end
subgraph Generator
direction LR
S[Static Site Generator]
end
subgraph Indexer
direction LR
I[Search Indexer]
end
subgraph Output
direction LR
O_IMG[Images]
O_CSS[CSS]
O_JS[JavaScript]
O_HTML[HTML]
O_INDEX[Search index]
end
subgraph Hosting
direction LR
H[Static Hosting Provider]
end
Input --> Generator
Generator --> Indexer
Indexer --> Output
Output --> Hosting
</div>
<h2 id="pagefind">Pagefind</h2>
<p>For this implementation, we'll use Pagefind - a powerful static search solution written in Rust. Pagefind works by analyzing your static HTML content, generating a WebAssembly-powered search index, and providing a JavaScript API for seamless integration.</p>
<h3 id="installing">Installing</h3>
<p>Easiest way to obtain Pagefind is via <a href="https://www.npmjs.com/package/pagefind">NPM</a> (<em>binaries also available via its GitHub <a href="https://github.com/CloudCannon/pagefind/releases">releases</a> and wrapper package through <a href="https://pypi.org/project/pagefind/">pypi</a></em>).</p>
<pre><code class="language-bash">npm install -g pagefind
</code></pre>
<h3 id="creating-index">Creating index</h3>
<p>Pagefind has several parameters and configuration options available, but to get started you can simply point it to your static site's generated content. The basic command is:</p>
<pre><code class="language-bash">pagefind --site &quot;path to generated site&quot;
</code></pre>
<p>This will create a <code>pagefind</code> folder in your site output directory containing:</p>
<ul>
<li>Compressed WebAssembly search indexes</li>
<li>JavaScript API files</li>
<li>CSS styles for the default UI components</li>
</ul>
<h3 id="adding-to-your-site">Adding to your site</h3>
<p>To add the search functionality to your site, you'll need to:</p>
<ol>
<li>Reference the Pagefind CSS stylesheet</li>
<li>Include the Pagefind JavaScript file</li>
<li>Add a placeholder <code>&lt;div&gt;</code> element where the search UI will be rendered</li>
<li>Initialize the Pagefind UI when the page loads</li>
</ol>
<p>Here's an example of the minimal HTML code needed:</p>
<pre><code class="language-html">&lt;link href=&quot;/pagefind/pagefind-ui.css&quot; rel=&quot;stylesheet&quot;&gt;
&lt;script src=&quot;/pagefind/pagefind-ui.js&quot;&gt;&lt;/script&gt;
&lt;div id=&quot;search&quot;&gt;&lt;/div&gt;
&lt;script&gt;
window.addEventListener('DOMContentLoaded', (event) =&gt; {
new PagefindUI({ element: &quot;#search&quot;, showSubResults: true });
});
&lt;/script&gt;
</code></pre>
<h2 id="integrating-with-statiq">Integrating with Statiq</h2>
<p>Statiq has support for executing <a href="https://www.statiq.dev/guide/web/external-processes">External Processes</a>, these can be executed during</p>
<ol>
<li>Initialization: The process should be started only once before other processes on the first engine execution.</li>
<li>BeforeExecution: The process should be started before each pipeline execution.</li>
<li>AfterExecution: The process should be started after all pipelines have executed.</li>
<li>BeforeDeployment: The process should be started after normal pipelines are executed and before deployment</li>
</ol>
<p>Looking at my <a href="https://github.com/devlead/devlead.se/blob/c58f61579cac0ad5700ba44440d75c69534fcca7/src/DevLead/Program.cs#L12C1-L21C6">Program.cs</a> the integration was just a matter of adding two process steps:</p>
<ol>
<li>Install Pagefind during initialization</li>
<li>Execute Pagefind before deployment</li>
</ol>
<pre><code class="language-csharp"> .AddProcess(
ProcessTiming.Initialization,
launcher =&gt; new ProcessLauncher(&quot;npm&quot;, &quot;install&quot;, &quot;-g&quot;, &quot;pagefind&quot;) {
ContinueOnError = true
}
)
.AddProcess(
ProcessTiming.BeforeDeployment,
launcher =&gt; new ProcessLauncher(&quot;pagefind&quot;, &quot;--site&quot;, $&quot;\&quot;{launcher.FileSystem.OutputPath.FullPath}\&quot;&quot;)
)
</code></pre>
<p>Then I added the CSS to my theme's <a href="https://github.com/devlead/devlead.se/blob/c58f61579cac0ad5700ba44440d75c69534fcca7/src/DevLead/input/_head.cshtml#L12">header template</a></p>
<pre><code class="language-html">&lt;link href=&quot;/pagefind/pagefind-ui.css&quot; rel=&quot;stylesheet&quot;&gt;
</code></pre>
<p>Followed by placholder div to my theme's <a href="https://github.com/devlead/devlead.se/blob/c58f61579cac0ad5700ba44440d75c69534fcca7/src/DevLead/input/_footer.cshtml#L12">footer template</a> where I wanted the search UI to appear.</p>
<pre><code class="language-html"> &lt;div id=&quot;search&quot;&gt;&lt;/div&gt;
</code></pre>
<p>Finally added the pagefind script and init code to my theme's <a href="https://github.com/devlead/devlead.se/blob/c58f61579cac0ad5700ba44440d75c69534fcca7/src/DevLead/input/_scripts.cshtml#L2-L7">scripts template</a></p>
<pre><code class="language-html">&lt;script src=&quot;/pagefind/pagefind-ui.js&quot;&gt;&lt;/script&gt;
&lt;script&gt;
window.addEventListener('DOMContentLoaded', (event) =&gt; {
new PagefindUI({ element: &quot;#search&quot;, showSubResults: true });
});
&lt;/script&gt;
</code></pre>
<p>And voilá, my site is indexed and searchable, even locally in preview mode🏆</p>
<p><img src="https://cdn.devlead.se/clipimg-vscode/2025/03/12/1f36ee83-1f85-ab43-221c-68249a605612.png?sv=2025-01-05&amp;st=2025-03-11T10%3A21%3A50Z&amp;se=2035-03-12T10%3A21%3A50Z&amp;sr=b&amp;sp=r&amp;sig=OipymFsUbYq2wNpcIGeYBFPe1WJgqgds233ZMRLeErY%3D" alt="Screenshot of search UI" /></p>
<h2 id="whats-indexed">What's indexed?!</h2>
<p>By default, Pagefind will index all content it finds in your static site, which can sometimes lead to duplicate results if you have content repeated across multiple pages (like tag pages or summaries). However, Pagefind provides several HTML attributes that give you fine-grained control over what gets indexed:</p>
<ul>
<li><p><code>data-pagefind-body</code>: When added to an element, only content within elements with this attribute will be indexed. If used anywhere on your site, pages without this attribute are excluded entirely.</p>
</li>
<li><p><code>data-pagefind-ignore</code>: Add this to any element you want to exclude from indexing. This works even if the element is inside a <code>data-pagefind-body</code> section.</p>
</li>
<li><p><code>data-pagefind-index-attrs</code>: Allows you to specify HTML attributes to index, like image alt text or link titles.</p>
</li>
</ul>
<p>These attributes gave me precise control over my search index. For example, to avoid duplicate results from tag pages and summaries, I added <code>data-pagefind-body</code> only to the main content of my blog posts in my <a href="https://github.com/devlead/devlead.se/blob/c58f61579cac0ad5700ba44440d75c69534fcca7/src/DevLead/input/_layout.cshtml#L90">layout template</a></p>
<pre><code class="language-c#"> &lt;div id=&quot;content&quot; class=&quot;col-md-12&quot; &#64;(Document.GetBool(&quot;IsPost&quot;) ? &quot;data-pagefind-body&quot; : &quot;&quot;) &gt;
</code></pre>
<h2 id="potential-gotchas">Potential gotchas</h2>
<h3 id="cdn">CDN</h3>
<p>Images in search results are HTML encoded, which worked with local images but caused issues with my CDN. Fortunately, the Pagefind JS API provides a <code>processResult</code> callback so you can post-process the search result data model before it's returned to the search result UI. I modified my <a href="https://github.com/devlead/devlead.se/blob/0b464f8f8abfbcdb0f478955bcefbed1f046d9c1/src/DevLead/input/_scripts.cshtml#L8-L11">scripts template</a> in the Pagefind initialization to unencode the characters causing issues with my CDN.</p>
<pre><code class="language-html">&lt;script&gt;
window.addEventListener('DOMContentLoaded', (event) =&gt; {
new PagefindUI({
element: &quot;#search&quot;,
showSubResults: true,
processResult: function (result) {
result.meta.image = result.meta.image.replaceAll(&quot;&amp;amp;&quot;, &quot;&amp;&quot;);
return result;
}
});
});
&lt;/script&gt;
</code></pre>
<h3 id="compression">Compression</h3>
<p>Pagefind compresses its index files during indexing so they can be served as-is without the need to be compressed by the HTTP server. This generally works seamlessly with hosting providers like GitHub Pages and Azure Static Web Apps. However, in some situations, depending on your configuration, you might need to either:</p>
<ul>
<li>Opt-out of compression for these already compressed files, or</li>
<li>Add headers to indicate that they're compressed / should be served</li>
</ul>
<p>For example, with Azure App Service, your <code>web.config</code> needs to add the MIME types and headers to indicate compression. Here's how that configuration might look:</p>
<pre><code class="language-xml">&lt;configuration&gt;
&lt;system.webServer&gt;
&lt;staticContent&gt;
&lt;mimeMap fileExtension=&quot;.pf_fragment&quot; mimeType=&quot;application/pf_fragment&quot; /&gt;
&lt;mimeMap fileExtension=&quot;.pf_index&quot; mimeType=&quot;application/pf_index&quot; /&gt;
&lt;mimeMap fileExtension=&quot;.pf_meta&quot; mimeType=&quot;application/pf_meta&quot; /&gt;
&lt;mimeMap fileExtension=&quot;.pagefind&quot; mimeType=&quot;application/pagefind&quot; /&gt;
&lt;/staticContent&gt;
&lt;rewrite&gt;
&lt;outboundRules&gt;
&lt;rule name=&quot;Add gzip Content-Encoding for specific extensions&quot;&gt;
&lt;match serverVariable=&quot;RESPONSE_Content-Encoding&quot; pattern=&quot;.*&quot; /&gt;
&lt;conditions logicalGrouping=&quot;MatchAny&quot;&gt;
&lt;add input=&quot;{REQUEST_FILENAME}&quot; pattern=&quot;\.pf_fragment$&quot; /&gt;
&lt;add input=&quot;{REQUEST_FILENAME}&quot; pattern=&quot;\.pf_index$&quot; /&gt;
&lt;add input=&quot;{REQUEST_FILENAME}&quot; pattern=&quot;\.pf_meta$&quot; /&gt;
&lt;add input=&quot;{REQUEST_FILENAME}&quot; pattern=&quot;\.pagefind$&quot; /&gt;
&lt;/conditions&gt;
&lt;action type=&quot;Rewrite&quot; value=&quot;gzip&quot; /&gt;
&lt;/rule&gt;
&lt;/outboundRules&gt;
&lt;/rewrite&gt;
&lt;/system.webServer&gt;
&lt;/configuration&gt;
</code></pre>
<h2 id="conclusion">Conclusion</h2>
<p>Adding search functionality to a static website no longer requires compromising its static nature. <a href="https://pagefind.app/">Pagefind</a> provides a straightforward solution that maintains the benefits of static sites through its WebAssembly-powered approach. With excellent <a href="https://pagefind.app/">documentation</a> and an active open-source <a href="https://github.com/cloudcannon/pagefind">project</a>, implementing powerful client-side search has never been more accessible.</p>
<p>By following the steps outlined in this post, you can add robust search functionality to your static site while maintaining its performance, efficiency, and cost-effectiveness.</p>
<p>So Pagefind is worth taking for a spin, and feel free to let me know what you think!</p>
</content:encoded>
<comments xmlns="http://purl.org/rss/1.0/modules/slash/">0</comments>
</item>
<item>
<title>SLNX Finally here📄</title>
<link>https://www.devlead.se/posts/2025/2025-02-24-slnx-finally-here</link>
<description>The new .NET solution format has evolved from being messy and bloated to being focused and clean</description>
<author>devlead</author>
<enclosure url="https://cdn.devlead.se/clipimg-vscode/2025/02/24/b83c06ea-69c7-be32-31cd-da3c5e6a5173.png?sv=2025-01-05&st=2025-02-23T06%253A49%253A41Z&se=2035-02-24T06%253A49%253A41Z&sr=b&sp=r&sig=B0uwPpXawEe%252BfbdTgsiNHrULc3f6zv%252BUgHR%252Fdk3tlQA%253D" length="0" type="image" />
<guid>https://www.devlead.se/posts/2025/2025-02-24-slnx-finally-here</guid>
<pubDate>Mon, 24 Feb 2025 00:00:00 GMT</pubDate>
<content:encoded><p>The Visual Studio solution files have long been an explicit and messy format, with lots of configuration that could be inferred from conventions. However, with the release of the latest .NET 9 SDK (9.0.200) earlier this month, things have changed. The new XML-based solution format, SLNX, is now out of preview, bringing clean, convention-based defaults while still allowing for explicit configuration when needed.</p>
<h2 id="what-has-changed">What has changed?</h2>
<p>Let's look at a simple &quot;Hello World&quot; example to illustrate the difference between the old and new formats.</p>
<h3 id="traditional.sln-file-helloworld.sln">Traditional .sln file - HelloWorld.sln</h3>
<pre><code class="language-ini">Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project(&quot;{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}&quot;) = &quot;HelloWorld&quot;, &quot;HelloWorld\HelloWorld.csproj&quot;, &quot;{979C8E48-A2EA-4647-A3A1-8647AB5F20C6}&quot;
EndProject
Project(&quot;{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}&quot;) = &quot;HelloWorld.Tests&quot;, &quot;HelloWorld.Tests\HelloWorld.Tests.csproj&quot;, &quot;{3B7AB8F1-88C0-4303-856B-A1E1EDE0A736}&quot;
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{979C8E48-A2EA-4647-A3A1-8647AB5F20C6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{979C8E48-A2EA-4647-A3A1-8647AB5F20C6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{979C8E48-A2EA-4647-A3A1-8647AB5F20C6}.Debug|x64.ActiveCfg = Debug|Any CPU
{979C8E48-A2EA-4647-A3A1-8647AB5F20C6}.Debug|x64.Build.0 = Debug|Any CPU
{979C8E48-A2EA-4647-A3A1-8647AB5F20C6}.Debug|x86.ActiveCfg = Debug|Any CPU
{979C8E48-A2EA-4647-A3A1-8647AB5F20C6}.Debug|x86.Build.0 = Debug|Any CPU
{979C8E48-A2EA-4647-A3A1-8647AB5F20C6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{979C8E48-A2EA-4647-A3A1-8647AB5F20C6}.Release|Any CPU.Build.0 = Release|Any CPU
{979C8E48-A2EA-4647-A3A1-8647AB5F20C6}.Release|x64.ActiveCfg = Release|Any CPU
{979C8E48-A2EA-4647-A3A1-8647AB5F20C6}.Release|x64.Build.0 = Release|Any CPU
{979C8E48-A2EA-4647-A3A1-8647AB5F20C6}.Release|x86.ActiveCfg = Release|Any CPU
{979C8E48-A2EA-4647-A3A1-8647AB5F20C6}.Release|x86.Build.0 = Release|Any CPU
{3B7AB8F1-88C0-4303-856B-A1E1EDE0A736}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3B7AB8F1-88C0-4303-856B-A1E1EDE0A736}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3B7AB8F1-88C0-4303-856B-A1E1EDE0A736}.Debug|x64.ActiveCfg = Debug|Any CPU
{3B7AB8F1-88C0-4303-856B-A1E1EDE0A736}.Debug|x64.Build.0 = Debug|Any CPU
{3B7AB8F1-88C0-4303-856B-A1E1EDE0A736}.Debug|x86.ActiveCfg = Debug|Any CPU
{3B7AB8F1-88C0-4303-856B-A1E1EDE0A736}.Debug|x86.Build.0 = Debug|Any CPU
{3B7AB8F1-88C0-4303-856B-A1E1EDE0A736}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3B7AB8F1-88C0-4303-856B-A1E1EDE0A736}.Release|Any CPU.Build.0 = Release|Any CPU
{3B7AB8F1-88C0-4303-856B-A1E1EDE0A736}.Release|x64.ActiveCfg = Release|Any CPU
{3B7AB8F1-88C0-4303-856B-A1E1EDE0A736}.Release|x64.Build.0 = Release|Any CPU
{3B7AB8F1-88C0-4303-856B-A1E1EDE0A736}.Release|x86.ActiveCfg = Release|Any CPU
{3B7AB8F1-88C0-4303-856B-A1E1EDE0A736}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
EndGlobal
</code></pre>
<h3 id="new-slnx-file-helloworld.slnx">New SLNX file - HelloWorld.slnx</h3>
<pre><code class="language-xml">&lt;Solution&gt;
&lt;Project Path=&quot;HelloWorld/HelloWorld.csproj&quot; /&gt;
&lt;Project Path=&quot;HelloWorld.Tests/HelloWorld.Tests.csproj&quot; /&gt;
&lt;/Solution&gt;
</code></pre>
<p>The code speaks for itself. The SLNX file is much cleaner and easier to read, with sensible defaults mean you only add exceptions when needed.</p>
<h2 id="getting-started">Getting started</h2>
<h3 id="prerequisites">Prerequisites</h3>
<p>To get started with the new SLNX format, you need to update your .NET SDK to version <code>9.0.200</code> or later. You can verify your current version by running the following command:</p>
<pre><code class="language-bash">dotnet --version
</code></pre>
<p>If you need to update, you can download the latest version from <a href="https://get.dot.net/">https://get.dot.net/</a> or update using the Visual Studio installer.</p>
<p>For Visual Studio you might still need to enable the new format under <code>Tools</code> -&gt; <code>Options</code> -&gt; <code>Environment</code> -&gt; <code>Preview Features</code> -&gt; <code>Use Solution File Persistence Model</code>.</p>
<p><img src="https://cdn.devlead.se/clipimg-vscode/2025/02/24/a21f4139-6e77-b134-9d45-efefaced787e.png?sv=2025-01-05&amp;st=2025-02-23T07%3A22%3A06Z&amp;se=2035-02-24T07%3A22%3A06Z&amp;sr=b&amp;sp=r&amp;sig=IPOH7kSyA1xh2njkGtiwEA6OPmalNNGKd2pJz348ZKw%3D" alt="Visual Studio Tools Options - Enable SLNX" /></p>
<h3 id="create-a-new-slnx-file">Create a new SLNX file</h3>
<p>To create a new <code>SLNX</code> file, you can use the following command:</p>
<pre><code class="language-bash">dotnet new sln --format slnx
</code></pre>
<p>This will create a new solution file with the SLNX format.</p>
<h3 id="convert-an-existing.sln-file-to-slnx">Convert an existing <code>.sln</code> file to SLNX</h3>
<p>To convert an existing <code>.sln</code> file to SLNX, you can use the following command:</p>
<pre><code class="language-bash">dotnet sln migrate &lt;SLN_FILE&gt;
</code></pre>
<p>i.e.</p>
<pre><code class="language-bash">dotnet sln migrate HelloWorld.sln
</code></pre>
<p>This will create a new solution file with the SLNX format based on the existing <code>.sln</code> file.</p>
<h2 id="working-with-slnx-programmatically">Working with SLNX programmatically</h2>
<p>Microsoft provides the <a href="https://www.nuget.org/packages/Microsoft.VisualStudio.SolutionPersistence">Microsoft.VisualStudio.SolutionPersistence</a> NuGet package, which provides a clean API for working with both traditional <code>.sln</code> and new <code>.slnx</code> files programmatically.</p>
<p>The entry point to serializers can be found on the <code>SolutionSerializers</code> static class. This has the helper <code>GetSerializerByMoniker</code> that can pick the serializer for a file extension, or a specific serializers can be used.</p>
<p>Here's a simple example of how to read and write SLNX files:</p>
<pre><code class="language-csharp">using Microsoft.VisualStudio.SolutionPersistence.Serializer;
// Open and deserialize the SLNX file
var solution = await SolutionSerializers.SlnXml.OpenAsync(&quot;HelloWorld.slnx&quot;, cancellationToken);
// Iterate through all projects in the solution
foreach (var project in solution.SolutionProjects)
{
// Print the file path of each project
Console.WriteLine(project.FilePath);
}
</code></pre>
<p>More examples can be found in the project GitHub wiki at <a href="https://github.com/microsoft/vs-solutionpersistence/wiki/Samples">github.com/microsoft/vs-solutionpersistence/wiki/Samples</a>.</p>
<h2 id="conclusion">Conclusion</h2>
<p>The new SLNX format is a great step forward, bringing clean, convention-based defaults while still allowing explicit configuration when needed. I belive this longterm will improve tooling and maintainability of solution files. It will also improve the developer experience by simplifying authoring and maintenance, reducing merge conflicts, and making it easier to sort them when they occur.</p>
<p>So if you haven't already, give it a try and let me know what you think!</p>
</content:encoded>
<comments xmlns="http://purl.org/rss/1.0/modules/slash/">0</comments>
</item>
<item>
<title>Long paths in Git on Windows</title>
<link>https://www.devlead.se/posts/2025/2025-02-19-git-windows-long-paths</link>
<description>How to deal with long paths in Git on Windows</description>
<author>devlead</author>
<enclosure url="https://cdn.devlead.se/clipimg-vscode/2025/02/19/d6040277-0b3f-4d2d-96be-356c4b5e8625.png?sv=2023-01-03&st=2025-02-19T19%253A10%253A42Z&se=2035-01-19T19%253A10%253A00Z&sr=b&sp=r&sig=f7JFWYHcFzUGKxOb4ihKJe9rt2bP%252BbIm%252Bk5LfJaTH6E%253D" length="0" type="image" />
<guid>https://www.devlead.se/posts/2025/2025-02-19-git-windows-long-paths</guid>
<pubDate>Wed, 19 Feb 2025 00:00:00 GMT</pubDate>
<content:encoded><p>On Windows, it's not unlikely that you'll encounter issues where you either have a repo that won't clone or files that won't commit. One common scenario that causes this is when doing snapshot testing, particularly with parameterized tests. These tests often generate snapshot files with names that include the test parameters, resulting in very long filenames.
One workaround is to move folders into the root of drives or create shorter names, but ultimately, this will cause issues sooner or later.</p>
<p>Fortunately, Windows can handle long files, but it's opt-in for legacy and compatibility reasons.</p>
<h2 id="enabling-long-paths-in-git">Enabling Long Paths in Git</h2>
<p>To enable long paths in Git, you can set the following system configuration:</p>
<pre><code class="language-bash">git config --system core.longpaths true
</code></pre>
<h2 id="enabling-long-paths-in-visual-studio">Enabling Long Paths in Visual Studio</h2>
<p>For Visual Studio, you need to enable long paths by setting a Windows Registry Key. You can do this using PowerShell with the following command:</p>
<pre><code class="language-PowerShell">New-ItemProperty `
-Path &quot;HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem&quot; `
-Name &quot;LongPathsEnabled&quot; `
-Value 1 `
-PropertyType DWORD `
-Force
</code></pre>
<p>By following these steps, you can avoid the common pitfalls associated with long file and directory names in your Git repositories on Windows.</p>
<h3 id="disclaimer">Disclaimer</h3>
<p>This isn't a silver bullet. While enabling long paths in Windows and Git helps in most scenarios, it's not a complete solution for all situations. Some workloads in Visual Studio can still encounter issues, particularly when paths are passed to other processes like legacy .NET Framework-based tools. If this is your process, you might want to consider using the <a href="https://www.nuget.org/packages/Pri.LongPath">Pri.LongPath NuGet package</a>, which provides drop-in replacements for System.IO APIs that handle long paths correctly.</p>
</content:encoded>
<comments xmlns="http://purl.org/rss/1.0/modules/slash/">0</comments>
</item>
<item>
<title>Introducing Devlead.Testing.MockHttp</title>
<link>https://www.devlead.se/posts/2025/2025-02-16-introducing-mockhttp-testing</link>
<description>An opinionated .NET source package for mocking HTTP client requests</description>
<author>devlead</author>
<enclosure url="https://cdn.devlead.se/clipimg-vscode/2025/02/15/48b2ec90-3c5d-4bfb-9d12-838867e50011.png?sv=2023-01-03&st=2025-02-15T20%253A33%253A23Z&se=2031-02-16T20%253A33%253A00Z&sr=b&sp=r&sig=OqeViFR%252B2YKVMXEVKWn6wC0g%252B3NtrxfUcfDS%252FASFOjY%253D" length="0" type="image" />
<guid>https://www.devlead.se/posts/2025/2025-02-16-introducing-mockhttp-testing</guid>
<pubDate>Sun, 16 Feb 2025 00:00:00 GMT</pubDate>
<content:encoded><p>There are undoubtedly many sophisticated and comprehensive solutions out there for mocking HTTP requests in .NET applications. However, I found myself with a very specific need: I wanted a lightweight, low-friction way to mock third-party HTTP APIs within my unit tests, without a lot of ceremony or complexity. I needed something that was &quot;good enough&quot; for my use case, providing in-memory request/response simulation that would let me validate my HTTP client interactions.</p>
<p>That's why I created Devlead.Testing.MockHttp. It's not trying to be the most feature-complete or elegant solution; it simply aims to solve this specific testing scenario in a straightforward way, with a minimum of ceremony, mimicking how the tested code would be used in a real application. If you have similar needs for basic HTTP mocking in your unit tests, this might be useful for you too.</p>
<p>Even though it has a fairly limited scope, it still enables testing of a wide range of scenarios, i.e., validating request headers, status codes, authentication, and even a small state machine for testing retry logic/throttling and requiring certain requests to be made before others.</p>
<h2 id="overview-diagram">Overview diagram</h2>
<div class="mermaid">graph TB
subgraph unit["Unit test"]
direction TB
Test1 &lt;--"GET Index.html"--> testioc
Test2 &lt;--"POST Api.json"--> testioc
Test3 &lt;--"PUT SecretApi.json"--> testioc
end
subgraph testioc["ServiceProviderFixture&amp;nbsp;(Test&amp;nbsp;Inversion&amp;nbsp;of&amp;nbsp;Control&amp;nbsp;Container)"]
direction TB
MyService{{"MyService(HttpClient&amp;nbsp;client)"}}
Mock{{"MockHttpClient:HttpClient&lt;br/>MockHttpClientFactory:IHttpClientFactory&lt;br/>MockHttpMessageHandlerFactory:IHttpMessageHandlerFactory"}}
MyService &lt;--> Mock
end
subgraph mockhttp["Router"]
direction TB
Router&#64;{ shape: procs, label: "HttpRequestMessage&lt;br/>to&lt;br/>HttpResponseMessage"}
end
subgraph assembly["Assembly&amp;nbsp;Embedded&amp;nbsp;Resources"]
direction BT
Routes["Routes.json"]
Index["Index.html"]
Api&#64;{ shape: doc, label: "Api.json"}
SecretApi&#64;{ shape: doc, label: "SecretApi.json"}
Index --> Routes
Api --> Routes
SecretApi --> Routes
end
testioc &lt;--> mockhttp
mockhttp &lt;-- Routes&lt;/br>configuration --> assembly
</div>
<h2 id="dependency-injection-in-unit-tests">Dependency injection in unit tests?</h2>
<p>My goal was to ensure that the unit test for constructing fixtures and object code closely resembles how they are used in real applications. For instance, if resolving your HTTP client is simply done with <code>AddHttpClient&lt;MyService&gt;()</code> in the application, then that should be all that's needed for it to be resolved in tests. By using an IOC container to resolve the services, it ensures that the same logic to construct the services is used in the tests as in the application. It also makes tests more resilient to changes in the service construction logic, especially if you use common extension methods to configure the services.</p>
<p>Let's walk through an example of that.</p>
<h3 id="example-service">Example Service</h3>
<p>The service is a simple class that uses an <code>HttpClient</code> to fetch data from a couple of different endpoints.</p>
<pre><code class="language-csharp">public class MyService(HttpClient httpClient)
{
public async Task&lt;string&gt; GetData()
{
var response = await httpClient.GetAsync(&quot;https://example.com/index.txt&quot;);
return await response.Content.ReadAsStringAsync();
}
public async Task&lt;User?&gt; GetSecret()
{
return await httpClient.GetFromJsonAsync&lt;User&gt;(&quot;https://example.com/login/secret.json&quot;);
}
}
</code></pre>
<h3 id="example-unit-test">Example Unit Test</h3>
<p>The unit tests below are NUnit tests that verify the behavior of the <code>MyService</code> class using <code>Devlead.Testing.MockHttp.Tests</code> provided <code>ServiceProviderFixture</code> to resolve the service. Methods are called as usual, and the results are snapshotted using <a href="https://github.com/VerifyTests/Verify">Verify</a>. Any exceptions or changes in the response are asserted and will fail the test.</p>
<pre><code class="language-csharp">public class MyServiceTests
{
[Test]
public async Task GetData()
{
// Given
var myService = ServiceProviderFixture.GetRequiredService&lt;MyService&gt;();
// When
var result = await myService.GetData();
// Then
await Verify(result);
}
[Test]
public async Task GetUnauthorizedSecret()
{
// Given
var myService = ServiceProviderFixture.GetRequiredService&lt;MyService&gt;();
// When
var result = Assert.CatchAsync&lt;HttpRequestException&gt;(myService.GetSecret);
// Then
await Verify(result);
}
[Test]
public async Task GetSecret()
{
// Given
var myService = ServiceProviderFixture.GetRequiredService&lt;MyService&gt;(
configure =&gt; configure.ConfigureMockHttpClient&lt;Constants&gt;(
client =&gt; client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue(
&quot;Bearer&quot;,
&quot;AccessToken&quot;
)
)
);
// When
var result = await myService.GetSecret();
// Then
await Verify(result);
}
}
</code></pre>
<h3 id="registering-the-service">Registering the service</h3>
<p>The project provides a <code>ServiceProviderFixture</code> partial class, which contains IOC helpers and a partial method <code>InitServiceProvider</code> for you to implement to register your own registrations and any of their dependencies. In theory, your project could have multiple mocked routes within the same project, and <code>.AddMockHttpClient&lt;Constants&gt;()</code> below picks up the routes from the assembly embedded resources relative to the type parameter <code>Constants</code>.</p>
<pre><code class="language-csharp">public static partial class ServiceProviderFixture
{
static partial void InitServiceProvider(IServiceCollection services)
{
services.AddHttpClient&lt;MyService&gt;()
.AddMockHttpClient&lt;Constants&gt;();
}
}
</code></pre>
<p>Services and instances registered in the <code>InitServiceProvider</code> method will be available for all unit tests and can be resolved using the <code>ServiceProviderFixture.GetRequiredService&lt;T&gt;()</code> method, as shown in the example unit test above. You can also register instances/services for a specific test or even configure the registrations using a delegate, as shown in the <code>GetSecret</code> test.</p>
<p><code>GetRequiredService</code> can resolve up to seven different types by passing one to seven type arguments, and each call to <code>GetRequiredService</code> will create a new separate isolated IOC container instance, ensuring that each test can have its own isolated dependencies.</p>
<pre><code class="language-csharp">var myService = ServiceProviderFixture
.GetRequiredService&lt;MyService&gt;();
var (
myService,
myService2
) = ServiceProviderFixture
.GetRequiredService&lt;MyService, MyService2&gt;();
...
var (
myService,
myService2,
myService3,
myService4,
myService5,
myService6,
myService7
) = ServiceProviderFixture
.GetRequiredService&lt;MyService, MyService2, MyService3, MyService4, MyService5, MyService6, MyService7&gt;();
</code></pre>
<p>This makes it easy to resolve multiple services in a single test or even resolve the same service multiple times with different configurations.</p>
<p>For example, resolving a <code>TimeProvider</code> for testing date/time-related functionality and your service that uses it.</p>
<pre><code class="language-csharp">// Given
var (
timeProvider,
myService
) = ServiceProviderFixture
.GetRequiredService&lt;FakeTimeProvider, MyService&gt;();
// When
var result = myService.GetData();
var cachedResult = myService.GetData();
timeProvider.Advance(TimeSpan.FromDays(1));
var uncachedResult = myService.GetData();
// Then
await Verify(
new {
result,
cachedResult,
uncachedResult
}
);
</code></pre>
<h3 id="registering-routes">Registering routes</h3>
<p>Routes are configured using the <code>Routes.json</code> file, which is embedded as an assembly resource. The file is used to configure the <code>Router</code>, which is responsible for matching incoming requests to the correct response. The file is placed in a &quot;Routes&quot; folder relative to the type parameter used in <code>.AddMockHttpClient&lt;T&gt;()</code>.</p>
<p>The example routes file below provides two endpoints available via GET requests. The secret endpoint requires an access token in the Authorization header, while the index endpoint does not.</p>
<pre><code class="language-json">[
{
&quot;Request&quot;: {
&quot;Methods&quot;: [
{
&quot;Method&quot;: &quot;GET&quot;
}