<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>rendle.dev</title>
    <link>https://rendle.dev/</link>
    <description>Recent content on rendle.dev</description>
    <generator>Hugo -- gohugo.io</generator>
    <language>en-us</language>
    <managingEditor>rendle@rendlelabs.com (Rendle)</managingEditor>
    <webMaster>rendle@rendlelabs.com (Rendle)</webMaster>
    <copyright>© 2025 Rendle</copyright>
    <lastBuildDate>Mon, 24 Feb 2025 11:00:00 +0000</lastBuildDate>
    <atom:link href="https://rendle.dev/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Back Once Again</title>
      <link>https://rendle.dev/blog/back-once-again/</link>
      <pubDate>Mon, 24 Feb 2025 11:00:00 +0000</pubDate><author>rendle@rendlelabs.com (Rendle)</author>
      <guid>https://rendle.dev/blog/back-once-again/</guid>
      <description>&lt;p&gt;I should really start blogging again. So I&amp;rsquo;m gonna.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s been a bunch of stuff on my mind lately, some of it about
software engineering and architecture, some about life and mental
health and stuff I&amp;rsquo;m going through, that kind of thing.
So I&amp;rsquo;m spinning up Hugo, sorting out
some hosting and CI/CD pipelines and all that so I can get it all out
of my head and on the web so you can read it. If you want to keep up
to date there&amp;rsquo;s an &lt;a href=&#34;https://rendle.dev/index.xml&#34;&gt;RSS feed&lt;/a&gt; which I&amp;rsquo;m hoping will go
&lt;em&gt;ping&lt;/em&gt; at least a couple of times a week.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>I should really start blogging again. So I&rsquo;m gonna.</p>
<p>There&rsquo;s been a bunch of stuff on my mind lately, some of it about
software engineering and architecture, some about life and mental
health and stuff I&rsquo;m going through, that kind of thing.
So I&rsquo;m spinning up Hugo, sorting out
some hosting and CI/CD pipelines and all that so I can get it all out
of my head and on the web so you can read it. If you want to keep up
to date there&rsquo;s an <a href="/index.xml">RSS feed</a> which I&rsquo;m hoping will go
<em>ping</em> at least a couple of times a week.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Rendle Talks</title>
      <link>https://rendle.dev/talks/</link>
      <pubDate>Fri, 07 Feb 2025 09:30:17 +0000</pubDate><author>rendle@rendlelabs.com (Rendle)</author>
      <guid>https://rendle.dev/talks/</guid>
      <description>&lt;h2 id=&#34;highlights&#34;&gt;Highlights&lt;/h2&gt;
&lt;p&gt;In no particular order&amp;hellip;&lt;/p&gt;
&lt;h3 id=&#34;programmings-greatest-mistakes&#34;&gt;Programming&amp;rsquo;s Greatest Mistakes&lt;/h3&gt;
&lt;p&gt;An ever-evolving look at some of the mistakes in software engineering
over the years that have led to explosions, bankruptcies, world-wide
Windows outages, or in one case just a severe telling-off from my boss :)&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://www.youtube.com/watch?v=C9YQLzSybU8&#34;&gt;From Devoxx Belgium 2024&lt;/a&gt;&lt;/p&gt;
&lt;h3 id=&#34;how-javascript-happened&#34;&gt;How JavaScript Happened&lt;/h3&gt;
&lt;p&gt;A history of programming languages, but limited to the ones that
introduced features, concepts or syntax that led to or influenced
JavaScript.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<h2 id="highlights">Highlights</h2>
<p>In no particular order&hellip;</p>
<h3 id="programmings-greatest-mistakes">Programming&rsquo;s Greatest Mistakes</h3>
<p>An ever-evolving look at some of the mistakes in software engineering
over the years that have led to explosions, bankruptcies, world-wide
Windows outages, or in one case just a severe telling-off from my boss :)</p>
<p><a href="https://www.youtube.com/watch?v=C9YQLzSybU8">From Devoxx Belgium 2024</a></p>
<h3 id="how-javascript-happened">How JavaScript Happened</h3>
<p>A history of programming languages, but limited to the ones that
introduced features, concepts or syntax that led to or influenced
JavaScript.</p>
<p><a href="https://www.youtube.com/watch?v=GMsm9-1WHns">From Øredev 2024</a></p>
<h3 id="the-albatross-project">The Albatross Project</h3>
<p>The opposite of
<a href="https://www.goodreads.com/book/show/17255186-the-phoenix-project">The Phoenix Project</a>,
basically. I&rsquo;ve started doing it as a one-person play now,
which is a lot of fun.</p>
<p><a href="https://www.youtube.com/watch?v=-PGIeiivuZk">From NDC Porto 2024</a></p>
<h3 id="the-worst-programming-language-ever">The Worst Programming Language Ever</h3>
<p>People always ask &ldquo;Is it JavaScript?&rdquo; but this is so much worse. It
takes all the bizarre and inexplicable design decisions from a wide
range of languages and munges them all together in one unusable
monstrosity. Also leaves time at the end for audience members to
share their own ideas (and has an amazing comment section on YouTube).</p>
<p><a href="https://www.youtube.com/watch?v=vcFBwt1nu2U">From NDC Oslo 2021</a></p>
<h3 id="how-simple-is-as-simple-as-possible">How Simple Is &ldquo;As Simple As Possible&rdquo;</h3>
<p>I do also do (mostly) serious talks sometimes. This one is about
unnecessary complexity in software architecture and development,
from microservices (do you really need them) to front-end JavaScript
frameworks (do they really need to be <strong>16 times</strong> the size of
the original Quake?).</p>
<p><a href="https://www.youtube.com/watch?v=WfUQHpcLKF4">From NDC Porto 2024</a></p>
<h2 id="my-speaking-journey">My Speaking Journey</h2>
<p>I have done a <em>lot</em> of talks over the last 15 years, from local user
groups to international conferences. Here&rsquo;s how I got to where I am.</p>
<p>In the 90s (yes, I&rsquo;m old) I did
a bit of stand-up comedy, from open spots in pubs to the Edinburgh
Fringe, eventually winning the <em>Jongleurs/Times Metro New Act of the
Year</em> competition in 1999 and going professional for a couple of years.
Bits of being funny for a living were fun, but a lot of bits
weren&rsquo;t, and I eventually ended up back in the software engineering
world (although I did get to work as a writer for a BBC comedy show
and I have
<a href="https://www.imdb.com/name/nm1068222/">the IMDb entry</a>
to prove it).</p>
<p>The urge to get up on stage and show off never really went away
though, so when I discovered user groups and meetups and conferences
I had to get up there and talk about something. My first talk was
<em>Functional Alchemy</em> and was about some of the interesting and
extremely hacky things you could do with the functional features in
C# 3.0. I did it at DDD Southwest
(which is <a href="https://dddsouthwest.com/">still going</a>) in 2011, and then
DDD Reading at the Microsoft campus (which is sadly no more afaict).
In Reading, a certain Jon Skeet was in the audience and really enjoyed
himself; I think he lasted about 15 minutes before joining in and
suggesting things. We ended up pairing on a couple of ideas after the
talk and I have to admit, I was more than a little starstruck.</p>
<p>Jon had also been tweeting during the talk, so I came out to find
a couple of hundred new Twitter followers (remember Twitter?), which
was nice. And then he recommended me to a couple of international
conferences,
<a href="https://oredev.org/">Øredev</a> and
<a href="https://codemash.org/">Code Mash</a>, giving me a welcome boost into
the wider speaking circuit. At Øredev I met Greg Young and
mentioned having done stand-up, and he invited me to do a talk at
<a href="https://www.buildstuff.events/">Build Stuff</a> but also asked me to
MC the attendee party: tell some jokes, do some crowd work&hellip;</p>
<blockquote>
<p>&ldquo;What language do you use?&rdquo;</p>
<p>&ldquo;Ruby.&rdquo;</p>
<p>&ldquo;I&rsquo;m sorry?&rdquo;</p>
<p>&ldquo;RUBY.&rdquo;</p>
<p>&ldquo;No, I heard what you said, I&rsquo;m just sorry.&rdquo;</p>
</blockquote>
<p>&hellip;and introduce the musical acts, which chance I leapt at.
That was a fun night.</p>
<p>There was a closing keynote that year, I forget the details, but it
was a very dry, very academic talk and after three days of the kind
of hardcore technical content you get at Build Stuff, it was tough
going for the audience. At some point, somebody tweeted at Greg
<em>&ldquo;Next year let Rendle do the closing keynote&rdquo;</em>; he asked if I would
do something funny and I said &ldquo;hell yes&rdquo; and spent a year trying to
think of something technical <em>and</em> funny. Eventually I came up with
<em>The Worst Programming Language Ever</em>, which has been preserved for
posterity on
<a href="https://www.infoq.com/presentations/worst-programming-language/">InfoQ</a>.</p>
<p>That talk went very well and I started getting invited to do it, and any
others I could come up with, at other conferences, and to build a bit of
a reputation for them. Fast-forward to today and I&rsquo;m practically the
resident &ldquo;locknote&rdquo; speaker for the
<a href="https://ndcconferences.com/">NDC conferences</a>, and get invited to speak
at other events outside of my .NET bubble, which is amazing. I&rsquo;m
incredibly grateful to be in this position, and very aware of the
privilege and luck that (along with a lot of hard work) has led me
to this point.</p>
<p>Anyway, that&rsquo;s the story so far. If you&rsquo;re interested in getting into
speaking I&rsquo;m always happy to share whatever I&rsquo;ve learned along this
journey. You can reach out on
<a href="https://www.linkedin.com/in/rendledotdev/">LinkedIn</a>
or <a href="https://bsky.app/profile/rendle.dev">Bluesky</a>, say hi, and ask
for advice or help with the speaking itself, writing abstracts, or
anything else.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Funding OSS maintainers like YouTube funds creators</title>
      <link>https://rendle.dev/blog/oss-funding-package-managers/</link>
      <pubDate>Fri, 01 Jul 2022 15:30:00 +0100</pubDate><author>rendle@rendlelabs.com (Rendle)</author>
      <guid>https://rendle.dev/blog/oss-funding-package-managers/</guid>
      <description>&lt;p&gt;&lt;a href=&#34;https://twitter.com/dustinmoris&#34;&gt;Dustin Moris Gorski&lt;/a&gt; yesterday published
&lt;a href=&#34;https://dusted.codes/fund-oss-through-package-managers&#34;&gt;Fund OSS through package managers&lt;/a&gt;
on his blog over &lt;a href=&#34;https://dusted.codes/&#34;&gt;Dusted Codes&lt;/a&gt;. In it, he suggests adding payment
mechanisms to package managers such as &lt;a href=&#34;https://nuget.org&#34;&gt;NuGet&lt;/a&gt; or
&lt;a href=&#34;https://www.npmjs.com&#34;&gt;NPM&lt;/a&gt;, and that&amp;rsquo;s certainly an option. But it involves creators
choosing how much to charge for their packages, and potentially developers having to raise
purchase requests every time they want to adopt a new package, and potentially have to
pay different creators for different packages on an annual basis, and so on.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p><a href="https://twitter.com/dustinmoris">Dustin Moris Gorski</a> yesterday published
<a href="https://dusted.codes/fund-oss-through-package-managers">Fund OSS through package managers</a>
on his blog over <a href="https://dusted.codes/">Dusted Codes</a>. In it, he suggests adding payment
mechanisms to package managers such as <a href="https://nuget.org">NuGet</a> or
<a href="https://www.npmjs.com">NPM</a>, and that&rsquo;s certainly an option. But it involves creators
choosing how much to charge for their packages, and potentially developers having to raise
purchase requests every time they want to adopt a new package, and potentially have to
pay different creators for different packages on an annual basis, and so on.</p>
<p>As I was reading Dustin&rsquo;s post, a possible alternative model occurred to me: something like
YouTube Premium&rsquo;s subscription model.</p>
<h2 id="how-does-youtube-premium-work">How does YouTube Premium work?</h2>
<p>My understanding, and please correct <a href="https://twitter.com/markrendle">me on Twitter</a> if
I&rsquo;m wrong, is that when you buy a YouTube premium subscription, your money is divided
between the creators of the videos you watch, based on the total amount of time you
spent watching their videos. If I spend three hours watching people building computers on
<a href="https://www.youtube.com/c/LinusTechTips">Linus Tech Tips</a> and one hour watching Hot Wheels
cars being raced on <a href="https://www.youtube.com/c/3Dbotmaker">3Dbotmaker</a>, then Linus gets 75%
of my subscription, and 3D gets 25%. Or something like that.</p>
<p>There&rsquo;s also a <strong>Subscribe</strong> button which YouTubers are extremely keen for you to click,
so I don&rsquo;t know if that increases the percentage of your money that they get, but I&rsquo;m
going to imagine it might.</p>
<p>Of course, if you don&rsquo;t have a YouTube Premium subscription you can still watch all those
videos but you&rsquo;ll get shown ads, and YouTube pay a share of that ad revenue to the
creators. Unless you have a YouTube-aware ad-blocker, in which case you&rsquo;ll enjoy the hard
work of all those content creators and they won&rsquo;t get any money. Which is why I have a
Premium subscription and I turn off my ad-blocker on YouTube.</p>
<h2 id="how-would-that-work-for-package-managers">How would that work for Package Managers?</h2>
<p>First of all, I&rsquo;m not suggesting that Package Managers should show you ads every time
you install a package. I don&rsquo;t even know how that would work.</p>
<p>But what if, instead, I could get a NuGet Premium subscription? And I pay $9.99 a month
for that subscription, and I get a token which I can add to my feed URL, something like
<code>https://api.nuget.org/v3/index.json?npt=9au3efoj9as3ofh9</code>.
Which I can configure at machine level for NuGet and <code>dotnet</code>, and in my CI
pipelines, and so on. And the people who maintain those NuGet packages can opt-in to
monetization, and then every month, a percentage of my Premium subscription money
goes to each of those creators based on how often their packages are downloaded using
that token? (Hopefully Microsoft would <em>not</em> choose to opt-in to monetization so there&rsquo;d
be some money left for everybody else.)</p>
<p>If there&rsquo;s a package or a creator I particularly want to support, I can &ldquo;subscribe&rdquo; to
them, which would increase their share of my subscription.</p>
<p>And if you don&rsquo;t have a Premium subscription, maybe there could be a not-too-obtrusive
message every now and then, in the output of <code>dotnet restore</code> or in one of those little
message bars Visual Studio sometimes puts at the top of the document well, saying &ldquo;hey,
you could support the creator of <em>X</em> package by subscribing to NuGet Premium for as little
as $4.99 a month!&rdquo;</p>
<h2 id="nobody-would-do-this">Nobody would do this</h2>
<p>I&rsquo;d do it. And I think a lot of people might, if it was as easy as setting up a monthly
payment and changing a setting in Visual Studio or Rider or <code>dotnet</code>. And you could have
Personal and Corporate subscriptions, and maybe have leaderboards to show who&rsquo;s the most
generous.</p>
<p>I don&rsquo;t know if it would generate enough revenue for people to live off their OSS work,
but if it could be sending a few people a few bucks a month, without them having to set
up invoicing or PayPal or Ko-Fi donation pages, maybe it would be worth it.</p>
]]></content:encoded>
    </item>
    <item>
      <title>My 20 Years with .NET</title>
      <link>https://rendle.dev/blog/my-20-years-with-dotnet/</link>
      <pubDate>Sun, 13 Feb 2022 14:25:17 +0100</pubDate><author>rendle@rendlelabs.com (Rendle)</author>
      <guid>https://rendle.dev/blog/my-20-years-with-dotnet/</guid>
      <description>&lt;p&gt;It&amp;rsquo;s .NET&amp;rsquo;s 20th birthday: 20 years since Microsoft released v1.0 of
their managed runtime, a Base Class Library, a couple of application
frameworks, and the C# programming language. So I thought I&amp;rsquo;d do a
retrospective of my 20 years working with .NET, because I&amp;rsquo;m
extraordinarily vain and I assume people want to read stuff like this.&lt;/p&gt;
&lt;h2 id=&#34;10-beta&#34;&gt;1.0 beta&lt;/h2&gt;
&lt;p&gt;It was some time in late 2001 when Microsoft released the beta version
of their new framework. I&amp;rsquo;d just returned to programming after a brief
stint as a professional stand-up comic, which I tried for a couple of
years before deciding it wasn&amp;rsquo;t for me.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>It&rsquo;s .NET&rsquo;s 20th birthday: 20 years since Microsoft released v1.0 of
their managed runtime, a Base Class Library, a couple of application
frameworks, and the C# programming language. So I thought I&rsquo;d do a
retrospective of my 20 years working with .NET, because I&rsquo;m
extraordinarily vain and I assume people want to read stuff like this.</p>
<h2 id="10-beta">1.0 beta</h2>
<p>It was some time in late 2001 when Microsoft released the beta version
of their new framework. I&rsquo;d just returned to programming after a brief
stint as a professional stand-up comic, which I tried for a couple of
years before deciding it wasn&rsquo;t for me.</p>
<p>I was working for a company who shall remain nameless, developing and
maintaining a CRM system for non-profits that was written in
<a href="https://en.wikipedia.org/wiki/Gupta_Technologies">Gupta Team Developer</a>. It was a dying platform, and we needed something
to replace it. We were using Visual C++ and VB 6.0 to create ActiveX
components, and Classic ASP for our web application interface, so
Microsoft&rsquo;s new hotness was an obvious line of investigation.</p>
<p>For the first beta, there was no Visual Studio, which made life
interesting. No IntelliSense, and no real documentation either. The
first program I wrote was a console app that reflected over every
public type in the .NET Framework assemblies and generated a very
basic set of HTML pages, one per type, divided into a directory
per namespace. It didn&rsquo;t extract the XML doc comments, it just
dumped out all the methods, properties, constructors, etc for
every type. I wrote the app in Vim, and compiled it from the
command line with <code>csc</code>. And then I went browsing around my HTML
pages, seeing what was there, comparing it with VB 6.0 and MFC.</p>
<h2 id="10">1.0</h2>
<p>By the time .NET 1.0 and Visual Studio .Net were released in February
2002, I&rsquo;d written a handful of Windows Forms experiments and was
convinced that this was the way forward. It was more fully formed
out of the box than Java, fairly easy to learn if you were already
familiar with the Microsoft ecosystem, and had some impressive
features. ADO.NET! Data-binding! A WYSIWYG window designer! And C#!
A language that felt like C++ but without having to remember to
<code>delete</code> objects when you were finished with them!
I started using it to write utilities for internal use,
and lobbying to rewrite our product in C#. But that first version
was very rough around the edges. The only resizable collection type
was <code>ArrayList</code>, and the only dictionary was <code>HashTable</code>, neither of
which were strongly typed. If you wanted a resizable list of <code>int</code>,
you had to box them to add them to an <code>ArrayList</code>, and cast them
back to <code>int</code> when you accessed them. And if you wanted to pass
them to a method, that method had to know the <code>ArrayList</code> had <code>int</code>s
in it. It was practically a dynamically typed environment. Microsoft
had already announced that .NET and C# 2.0 would be getting &ldquo;generics&rdquo;,
a magical new language feature that would fix this problem, so we
decided to wait before starting the big porting project.</p>
<h2 id="20">2.0</h2>
<p>Four years passed between the releases of .NET 1.0 and 2.0, which is
mad to think about now that we get a new version every year. It came
with Visual Studio 2005, and the new generics feature was
ground-breaking at the time. It was similar to C++ templates, but
much easier to work with. It was also implemented properly, thanks to
the Microsoft Research team who were working on a functional
programming language for .NET. Generics in .NET were reified, which
meant they were supported by the runtime and not just the compiler.
Using reflection, you could create new generic objects and methods
at runtime.</p>
<p>I finally got permission to start a project to rewrite our core
product in C#. It was, in the best tradition of 1990s line-of-business
applications, mostly just a collection of windows with too many
textboxes and comboboxes, providing basic CRUD functionality but
with an advanced &ldquo;query-by-example&rdquo; feature inspired by Informix 4GL.
So rather than doing a screen-by-screen rewrite, I was building my
own mini-framework to provide the same functionality.</p>
<p>At the time, .NET had a feature that was supposed to make working with
complex database schema less of a chore:
<a href="https://docs.microsoft.com/en-us/dotnet/framework/data/adonet/dataset-datatable-dataview/generating-strongly-typed-datasets">Strongly Typed DataSets</a>.
At the time, writing plain old C# objects was a chore. C# 2.0 did not
have automatic properties like this:</p>






<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-c#" data-lang="c#"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">1</span><span><span style="color:#f38ba8">public</span> <span style="color:#cba6f7">class</span> <span style="color:#f9e2af">Member</span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">2</span><span>{
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">3</span><span>    <span style="color:#f38ba8">public</span> <span style="color:#f38ba8">int</span> Id { <span style="color:#cba6f7">get</span>; <span style="color:#cba6f7">set</span>; }
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">4</span><span>}</span></span></code></pre></div>
<p>You had to provide your own backing field and implement the <code>get</code> and
<code>set</code> manually, like this:</p>






<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-c#" data-lang="c#"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 1</span><span><span style="color:#f38ba8">public</span> <span style="color:#cba6f7">class</span> <span style="color:#f9e2af">Member</span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 2</span><span>{
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 3</span><span>    <span style="color:#f38ba8">private</span> <span style="color:#f38ba8">int</span> _id;
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 4</span><span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 5</span><span>    <span style="color:#f38ba8">public</span> <span style="color:#f38ba8">int</span> Id
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 6</span><span>    {
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 7</span><span>        <span style="color:#cba6f7">get</span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 8</span><span>        {
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 9</span><span>            <span style="color:#cba6f7">return</span> _id;
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">10</span><span>        }
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">11</span><span>        <span style="color:#cba6f7">set</span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">12</span><span>        {
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">13</span><span>            _id = <span style="color:#cba6f7">value</span>;
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">14</span><span>        }
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">15</span><span>    }
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">16</span><span>}</span></span></code></pre></div>
<p>Multiply that by a database with some 200 tables averaging 30 columns
each. And then implementing the code to map from a <code>SqlDataReader</code> to
those objects. <a href="https://en.wikipedia.org/wiki/NHibernate">NHibernate</a>
was in early development, but it was scary &ldquo;Open Source&rdquo; and we
weren&rsquo;t allowed to use it. So Strongly Typed DataSets, which you could
pretty much just generate from your database and start using, were
The Way.</p>
<p>Except they were terrible, and performance was horrible, and they were
awful to work with, and data-binding to them from Windows Forms was
hideous. But I persisted, and made progress, and had the core part of
our core product running on .NET some time in 2006.</p>
<p>And then Microsoft released .NET 3.0.</p>
<h2 id="30">3.0</h2>
<p>Just two years after .NET 2.0, in November 2006, Microsoft released
3.0, at the same time
as Windows Vista. The ambition for these two releases had been huge; a
new file-system that was part database was one of the many features
that never made it to the final code. But we did get Windows
Presentation Foundation, a brand new desktop application framework
that improved on Windows Forms in many, many ways. The data-binding was
far better, for a start, and the layout was declared in XAML instead
of hundreds of lines of C# generated by the WinForms designer. And
applications developed with WPF <em>looked</em> modern.</p>
<p>So of course I decided to rewrite my rewrite in WPF.</p>
<p>At this point I had a junior developer working with me on the rewrite
project, but there were three senior developers and three juniors
still hard at work on the old Gupta Team Developer codebase, adding
features that customers asked for and polishing the UI as best they
could. And this junior developer and I were trying to catch up to them
and get to a point where we had a new product with the same
functionality as the old one. So throwing out thousands of lines of
code and starting again with a brand new framework, which we had to
learn as we went along, was probably one of the worse decisions I&rsquo;ve
made in my career.</p>
<h2 id="35">3.5</h2>
<p>Less than a year after 3.0, along came 3.5, bringing with it Language
Integrated Query, or LINQ. This included LINQ-to-SQL, which allowed
you to generate proper C# classes from your database and query against
it right in your code. This was much better than Strongly Typed
DataSets, so out they went and in came LINQ. More thousands of lines
of code ditched and replaced with the new hotness.</p>
<p>By the end of 2008 my attempts to keep up with Microsoft&rsquo;s new .NET
features were seriously damaging my ability to deliver the promised
rewrite. .NET just kept getting more and more awesome, and I couldn&rsquo;t
help chasing after every new feature they added. Except WCF. I never
liked WCF.</p>
<p>I ended up burning myself out, and the company scrapped my
project in favour of a VB.NET application from a company they&rsquo;d
acquired in the meantime. I didn&rsquo;t want to do VB, so I quit and
got a job at .NET Solutions, which sounded like my kind of place.</p>
<h2 id="net-solutions">.NET Solutions</h2>
<p>My new employers were a Microsoft Gold Partner and we got to work
on some amazing projects. Getting excited about new features was
pretty much part of the job, and projects were shorter so if
something new landed while you were working on something, you could
just finish that and then jump into a new one using the latest
version. When I joined, they were just finishing up a .NET Micro
Framework project that ran on a tiny circuit board connected to a
fridge.</p>
<p>You know how during the World Cup Final there&rsquo;s a spike in demand for
electricity at half time? People think it&rsquo;s all the kettles being
boiled, but it&rsquo;s not. It&rsquo;s all the fridges being opened and warming
up while people put milk in their teas and coffees, and then kicking
in the refridgeration unit to cool down again. It spikes so much we
have to buy extra electricity from France to cope with it.</p>
<p>The idea behind this project was that all the fridges would be able
to talk to a web API that would say whether or not they could turn on.
So when everybody put the milk back and closed the door, the fridges
could coordinate and avoid all demanding extra electricity at once.</p>
<p>It was a fun project. The .NET Micro Framework didn&rsquo;t have an
<code>HttpClient</code> built in, so we had to implement our own over the <code>Socket</code>
types. And there were inexplicable bugs which turned out to be due to
voltage fluctuations causing problems with the custom ASIC on the
board or some such thing.</p>
<p>.NET Solutions also had a bonus scheme that rewarded, among other
things, blogging and speaking at user groups, so that was when I
started doing that stuff. The first talk I did was about the
LINQ-related features in .NET 3.5, particularly lambdas and
extension methods, which allowed you to do some funky things with
C#. For example, catching more than one exception type&hellip;</p>






<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-c#" data-lang="c#"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 1</span><span><span style="color:#f38ba8">public</span> <span style="color:#f38ba8">static</span> <span style="color:#cba6f7">void</span> Try&lt;TException1, TException2&gt;(
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 2</span><span>    Action action,
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 3</span><span>    Action&lt;Exception&gt; catchAction)
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 4</span><span>{
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 5</span><span>    <span style="color:#cba6f7">try</span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 6</span><span>    {
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 7</span><span>        action();
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 8</span><span>    }
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 9</span><span>    <span style="color:#cba6f7">catch</span> (TException1 ex)
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">10</span><span>    {
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">11</span><span>        catchAction(ex);
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">12</span><span>    }
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">13</span><span>    <span style="color:#cba6f7">catch</span> (TException2 ex)
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">14</span><span>    {
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">15</span><span>        catchAction(ex);
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">16</span><span>    }
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">17</span><span>}</span></span></code></pre></div>
<p>The third time I gave that talk was at the Developer Developer
Developer conference at the Microsoft Campus in Reading. I was
setting up when one of the other speakers came up to me and said
&ldquo;you know Jon Skeet&rsquo;s in the audience?&rdquo; So that was terrifying.
But he loved it, and couldn&rsquo;t help joining in and making suggestions
for ways to push things even further, and I ended up pairing with him
once the talk was finished, when I also discovered he&rsquo;d been tweeting
about it all the way through. That was the day I passed 1,000 followers
on Twitter, thanks to .NET, C# and Jon.</p>
<h2 id="aspnet-mvc">ASP.NET MVC</h2>
<p>In 2007, Scott Guthrie announced
<a href="https://en.wikipedia.org/wiki/ASP.NET_MVC">ASP.NET MVC</a>, a .NET
implementation of the Model/View/Controller pattern for building
web applications. Up to then, the way to create web apps with
.NET had been WebForms, an ambitious framework which tried to let
developers pretend they were building a desktop application
without having to worry about things like the HTTP protocol
or round-trips to the browser. Unfortunately this required some
ugly hacks, most notably ViewState, a potentially enormous chunk of
data that was sent back and forth between browser and server
on every request.</p>
<p>MVC did away with all of this, replacing it with Controllers that
could handle HTTP requests and render Views, embracing the HTTP
paradigm. It also came with Razor, a new way of combining C# with
HTML to render pages from models.</p>
<p>But maybe the most interesting thing about ASP.NET MVC was that it
was open source, with all the source code available on Microsoft&rsquo;s
CodePlex site. Before this, Microsoft, under Steve Ballmer, had
been the sworn enemy of open source software; ASP.NET MVC was the
first sign that this was starting to change.</p>
<h2 id="40">4.0</h2>
<p>.NET 4.0 and C# 4.0 were a weird release. The headline feature was
the Dynamic Language Runtime, or DLR, and the <code>dynamic</code> keyword in
C#. I think the main reason for it was to improve Office interop,
but there were more interesting things you could do with it.</p>
<p>Around the same time that 4.0 came out, I&rsquo;d been learning Ruby, and
using an ORM called <a href="https://github.com/datamapper/dm-core">DataMapper</a>
which used Ruby&rsquo;s <code>method_missing</code> feature to map dynamic properties
and methods to database calls. There was a new base class in .NET 4.0,
<a href="https://docs.microsoft.com/en-us/dotnet/api/system.dynamic.dynamicobject?view=net-6.0">DynamicObject</a>,
that let you do pretty much the same thing, so as an experiment I wrote
a simple library that let you have a <code>dynamic</code> object and call
properties and methods on it to do database things, like this:</p>






<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-c#" data-lang="c#"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">1</span><span><span style="color:#f38ba8">dynamic</span> db = <span style="color:#cba6f7">new</span> Database(connectionString);
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">2</span><span><span style="color:#f38ba8">var</span> order = db.Orders.FindById(id);
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">3</span><span>Console.WriteLine(order.Description);</span></span></code></pre></div>
<p>I called it Simple.Data, wrote some blogs about it and announced it on
Twitter, and to my surprise it became quite popular. It&rsquo;s had 800K
downloads from NuGet, which isn&rsquo;t bad. But as people asked for more
and more features, and I added them, it became quite complicated and
difficult to maintain. Eventually what it really needed was a ground-up
rewrite, but by then I had a five year old daughter and a baby son and
time was a scarce resource, so I wound the project down. Sorry if you
liked it.</p>
<h2 id="45">4.5</h2>
<p>The big thing in .NET 4.5 was the <code>Task</code>-based asynchronous
programming model. Instead of methods with <code>Begin</code> and <code>End</code>
prefixes, we had methods that returned a <code>Task</code> which could
trigger a callback when the underlying operation, usually
some form of IO, completed. It was easier to use than the
<code>Begin/End</code> model, but still resulted in code like this:</p>






<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-c#" data-lang="c#"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">1</span><span>client.GetAsync(uri)
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">2</span><span>    .ContinueWith(r =&gt; r.ReadBodyAsStringAsync()
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">3</span><span>        .ContinueWith(body =&gt; ...))</span></span></code></pre></div>
<p>The levels of callback nesting in a chain with a lot of
<code>Task</code>-returning methods quickly got completely out of control.</p>
<h2 id="c-50">C# 5.0</h2>
<p>This was hugely improved with the release of C# 5.0, with the new
<code>async/await</code> keywords, which made this kind of asynchronous
programming much easier. Now we could just write code like this:</p>






<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-c#" data-lang="c#"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">1</span><span><span style="color:#f38ba8">var</span> response = <span style="color:#cba6f7">await</span> client.GetAsync(uri);
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">2</span><span><span style="color:#f38ba8">var</span> body = <span style="color:#cba6f7">await</span> response.ReadBodyAsStringAsync();
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">3</span><span>...</span></span></code></pre></div>
<p>This was much easier to understand and reason about, although there
was an enormous amount of bike-shedding about what the actual
keywords should be, particularly <code>await</code>, which many people thought
sounded too much like <code>Wait</code> and implied that the current thread
would be blocked waiting for the result, when what actually
happened was that the current thread was returned to the thread
pool and another one would be used when the <code>Task</code> completed.</p>
<p>I remember spending a fair amount of time going through code
using either <code>Task.ContinueWith</code> or the old <code>Begin/End</code> methods
and replacing it with <code>async/await</code> around this time, and also
wishing there were an <code>IAsyncEnumerable</code> type.</p>
<h2 id="46-47-48">4.6, 4.7, 4.8</h2>
<p>After 4.5, .NET seemed to stagnate for a while. New features were
added, but nothing that merited a major version bump. But the C#
team were working on something completely new.</p>
<h2 id="c-60">C# 6.0</h2>
<p>Up to this point, the C# and VB.Net compilers had been written in
C++. With C# 6.0, the compiler teams embarked on a multi-year project,
codenamed Roslyn, to rewrite the compilers in their own languages.
So the C# compiler was rewritten in C#, and the VB.Net compiler was
rewritten in VB.Net. This would make it easier to add new features,
although the only changes in C# 6.0 itself were some syntactic
sugar for string interpolation and the like.</p>
<p>The really revolutionary thing about Roslyn, though, was that it
represented a move to a Compiler-as-a-Service. The Roslyn packages
were published on NuGet, and you could use them to parse, modify,
analyse and generated C# code at runtime. It became possible to
write your own code analysers and fixes and publish them as
Visual Studio extensions or NuGet packages. More on this later&hellip;</p>
<h2 id="project-k">Project K</h2>
<p>In late 2015, on the ASP.Net Insiders mailing list, which I was lucky
enough to be on,
<a href="https://twitter.com/damianedwards">Damian Edwards</a> and
<a href="https://twitter.com/davidfowl">David Fowler</a> announced a new project
they&rsquo;d been working on: a cut-down, light-weight and, most importantly,
<em>cross-platform</em> version of .NET. It would run on Linux and Mac as
well as Windows, and was optimised for writing web applications,
without all the baggage of the full-fat, Windows-only .NET Framework.
No Windows Forms, no WebForms, no WPF or WCF, just the minimum
necessary to run ASP.NET MVC applications.</p>
<p>Moreover, Project K would be properly open source, developed in
the open on GitHub, and accepting contributions from the
community.</p>
<p>Obviously I downloaded and started playing with this right away,
and I loved it. OK, so a huge chunk of the Base Class Library was
missing, but it was C# and ASP.NET MVC and Web API working on
Linux.</p>
<p>At the time I was a working at a place where the new Linux container
technology, Docker, was being enthusiastically embraced. Apps were
written in Node.js and Python to run in a cluster using an
orchestration platform that I don&rsquo;t remember the name of, but it
wasn&rsquo;t Kubernetes.</p>
<p>Project K meant that we could use .NET in this environment, and I
found an actual use for it. I was working on a &ldquo;web bug&rdquo; tracking
system that would follow users through the site and then off to
external links to verify that they purchased something so we
could send them a stuffed animal. This required generating a lot
of UUIDs, and under heavy load. And it turns out that generating
UUIDs is a compute intensive task, something that Node.js is not
particularly good at.</p>
<p>So I wrote the simplest possible microservice in Project K,
although it might have been called ASP.NET 5 by that point,
which had a single endpoint that just returned a <code>Guid</code> as
plain text. The Node.js app could call this service asynchronously,
which is something that Node is <em>very</em> good at, and suddenly the
system scaled. I don&rsquo;t know if this was the first production use
of the new .NET outside of Microsoft, but it must have been
pretty close.</p>
<h2 id="net-core-10">.NET Core 1.0</h2>
<p>Project K became ASP.NET 5 became .NET Core 1.0. Along the way
the promised new <code>project.json</code> project files were scrapped,
which upset a lot of people, but the <code>csproj</code> files for Core
applications were far simpler than those for .NET and despite
being XML are mostly pretty simple to work with and edit manually
when the need arises.</p>
<p>I remember watching a talk given by Damian and David at NDC Oslo
where they talked about performance in .NET Core. Damian shared
an anecdote wherein <a href="https://twitter.com/kellabyte">Kelly Sommers</a>
benchmarked the new Kestrel web server that was the engine of
ASP.NET Core against her own Haywire engine, written in C.
Kestrel was not fast: something like 70,000 requests per second
compared to Haywire&rsquo;s 1 million requests per second. Per
Damian, he asked the Kestrel team why it was so slow, to which
they responded &ldquo;you didn&rsquo;t tell us you wanted it to be fast.&rdquo;</p>
<p>I don&rsquo;t know how much of that is apocryphal, but there was a
concerted effort to optimise the hell out of Kestrel, with a lot
of contributions from <a href="https://twitter.com/ben_a_adams">Ben Adams</a>,
who had a vested interest in the project as his company, Ilyiad Games,
make a game that runs on .NET on the server. By the time 1.0 was
released, .NET Core was actually becoming competitive on the
famous
<a href="https://www.techempower.com/benchmarks/">TechEmpower benchmarks</a>.</p>
<p>Annoyingly, when .NET Core 1.0 launched, I was working at a place
where new stuff was frowned upon so I had to continue working with
.NET 4.something and WebAPI.</p>
<h2 id="net-core-21">.NET Core 2.1</h2>
<p>There was a 2.0 release, which was mainly concerned with adding back
a lot of missing APIs from the .NET Framework, but the big release
was .NET Core 2.1, which was when performance really became a feature
of the new framework. 2.1 introduced the new <code>Span&lt;T&gt;</code> and <code>Memory&lt;T&gt;</code>
types, which provided a better way of working with arrays or other
chunks of memory, including unmanaged memory. This came with C# 7.2,
which added a <code>ref struct</code> modifier that reduced copies when passing
<code>struct</code> types between methods and is, I&rsquo;m pretty sure, only really
used for <code>Span&lt;T&gt;</code>; I&rsquo;ve certainly never used it in my own code.</p>
<p>At this time I was working for one of the big investment banks,
with a remit to extract functionality from a behemoth of a WPF
application into shared services, and we <em>were</em> allowed to use
.NET Core 2.1, so I got to play with it. But the biggest thing I
actually achieved there was to use some of the .NET Core
packages that were backward-compatible with .NET 4.x to add
telemetry to the WPF application.</p>
<p>My favourite thing was a custom-written client for
<a href="https://www.influxdata.com/products/influxdb/">InfluxDB</a>. The
official client for .NET was quite inefficient: it allocated
a <em>lot</em> of <code>Dictionary&lt;string,string&gt;</code> or <code>Dictionary&lt;string,object&gt;</code>
objects for every line of metrics, and was too slow for our
purposes. So I got to write an alternative client which used
large byte arrays and wrote metrics passed through the new
<code>System.Diagnostics.DiagnosticSource</code> model directly to
<code>Span&lt;byte&gt;</code>s. My only regret was that we weren&rsquo;t allowed to
open source that library. If I can find the time, I might recreate
it as an InfluxDB target for the new OpenTelemetry framework.</p>
<h2 id="net-core-31">.NET Core 3.1</h2>
<p>Again, there was a 3.0 release, but at this time Microsoft were
using the <code>.1</code> versions of .NET Core as the &ldquo;LTS&rdquo; (Long Term Servicing)
releases. The big thing with 3.1 was that Windows Forms and WPF
were ported over to Core, so you could build desktop Windows apps
with it. Sadly this did not extend to making them cross-platform,
which would be a huge effort since both WinForms and WPF are
fairly thin wrappers over the underlying Windows APIs.</p>
<p>At the time .NET Core 3.1 was released,
<a href="https://twitter.com/coolcsh">Scott Hunter</a> announced that the next
version would be .NET 5.0. .NET Core was becoming .NET; this would
be where Microsoft invested time and energy in moving the platform
forwards, and .NET 4.x would go into long term support, getting
security updates and continuing to work on new versions of Windows,
but no longer being improved or enhanced.</p>
<p>With this announcement came the news that the work of migrating
.NET APIs to .NET Core was finished; if something hadn&rsquo;t been
migrated across now, it wasn&rsquo;t going to be. This included Windows
Communication Foundation, the framework that had been added way
back in .NET 3.0 for building Service-Oriented Applications,
usually using SOAP over HTTP as the protocol. The official
Microsoft recommendation was to migrate WCF services to gRPC,
the current standard for building distributed systems and
microservices using the RPC pattern. gRPC had been added to
ASP.NET Core as a first-class citizen, with a brand new implementation
that was fast and efficient and worked with the rest of the
ASP.NET Core stack.</p>
<p>As someone who had failed a Microsoft certification twice on the
WCF test, and had fought against it on a variety of projects, I was
delighted at this news, but it made a lot of people very angry. And
I can understand why. I started playing with the new gRPC framework
and was impressed, and realised that it mapped pretty easily to
WCF&rsquo;s RPC concepts. In fact, it mapped closely enough that I thought
it might be possible to use Roslyn to convert WCF code to use gRPC
instead.</p>
<p>I spent a week learning the Roslyn SDK and managed to get it doing
a very basic conversion of a simple WCF service to gPRC, and could
see that it was possible to do more complicated things as well, and
<a href="https://visualrecode.com">Visual ReCode</a> was born. Although I
briefly considered making it open source, my wife would have killed
me so I partnered with
<a href="https://gibraltarsoftware.com">Gibraltar Software</a> to sell it as
a commercial product. I&rsquo;m still working on it today.</p>
<h2 id="unity">Unity</h2>
<p>This is not exactly a .NET version, but Unity is a game engine that
uses C# as its scripting language, and provides most of the framework
classes for building games. I put it here because it was around this
time that I learned it. I used it to build an homage to the
Club Penguin games with my son; he would draw the &ldquo;graphics&rdquo; in felt
tip pen, and we&rsquo;d scan it in and import it into the game, with
me writing C# code and teaching him as we went along. I&rsquo;ve also
made a silly little game called <em>Cheese Times Tables</em> and actually
released it on the various mobile App Stores. And I could do all of
this with the fundamental knowledge of C# and .NET that I&rsquo;ve picked
up over the years writing line-of-business apps and web sites.
I think that&rsquo;s awesome. If I could find the time I&rsquo;d be working
on <em>Extreme Croquet</em>&hellip;</p>
<h2 id="50-and-60">5.0 and 6.0</h2>
<p>And that brings us to today. I&rsquo;ve been a .NET developer for 20 years,
I&rsquo;ve built things with WinForms, WebForms, WPF, WCF, MVC, the Micro
Framework, Silverlight, Project K, .NET Core and .NET 6.0. For most
of that time Microsoft have innovated on the framework and the C#
language and compiler platform, and that innovation has accelerated
since the Core era began. We now have Blazor, an amazing new
web app framework that can run over a WebSocket between a browser
and server, or be compiled to WebAssembly to run .NET code right
in modern browsers. It&rsquo;s easier to work with than WebForms ever
was, while providing a lot of the same benefits such as simplified
event handling and session state. And we&rsquo;ve very nearly got MAUI,
the new desktop and mobile application framework that is the
evolution of Xamarin and a replacement for WPF, WinForms and UWP
that also runs on Macs and might, one day, support Linux. There&rsquo;s even
a hybrid MAUI+Blazor model that wraps a Blazor Server application in
a MAUI app, letting you target desktop, mobile and web with a single
codebase.</p>
<p>With every release, .NET gets better performance across ASP.NET Core,
Entity Framework Core, and all parts of the framework. C#, now on
version 10 (with version 11 being loudly discussed on GitHub and
<a href="https://twitter.com/markrendle/status/1492470230588829696">Twitter</a>),
continues to evolve, offering ways of writing more
expressive code with more optimal and safer runtime behaviour.
The first previews of .NET 7.0 and C# 11 are likely to land any
day now, and I&rsquo;m looking forward to seeing what&rsquo;s new there.</p>
<p>I don&rsquo;t know if I&rsquo;ll still be coding professionally in another 20
years; I&rsquo;m getting on a bit and ideally I&rsquo;ll be retired by then.
But if there&rsquo;s a celebration of 40 years of .NET in 2042, I hope to
be there for the party.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Running a .NET 5 web app on Railway</title>
      <link>https://rendle.dev/blog/deploying-to-railway-with-dotnet/</link>
      <pubDate>Thu, 13 May 2021 17:30:17 +0100</pubDate><author>rendle@rendlelabs.com (Rendle)</author>
      <guid>https://rendle.dev/blog/deploying-to-railway-with-dotnet/</guid>
      <description>&lt;p&gt;I&amp;rsquo;ve been working on a fun little project on
&lt;a href=&#34;https://twitch.tv/markrendle&#34;&gt;my stream&lt;/a&gt; recently. It&amp;rsquo;s an app
called &lt;em&gt;StreamBadger&lt;/em&gt; that adds
overlay images and plays sounds on the stream through OBS Studio.
It uses the Twitch API so it needs to authenticate with Twitch, and as it&amp;rsquo;s
a desktop application I wanted to build that authentication flow where you
log in through the browser and the app sits and waits and then magically
gets connected.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>I&rsquo;ve been working on a fun little project on
<a href="https://twitch.tv/markrendle">my stream</a> recently. It&rsquo;s an app
called <em>StreamBadger</em> that adds
overlay images and plays sounds on the stream through OBS Studio.
It uses the Twitch API so it needs to authenticate with Twitch, and as it&rsquo;s
a desktop application I wanted to build that authentication flow where you
log in through the browser and the app sits and waits and then magically
gets connected.</p>
<p>This is pretty straightforward: open a browser page, passing some kind of
unique key, then poll an endpoint on the server until it returns the mystic
runes you seek. So this needs to be an ASP.NET Core MVC app, and it needs
some transient external storage like, say, a Redis instance. And this is
an open source side-project and I really don&rsquo;t want to be paying for
hosting for an essential component of it.</p>
<p>A couple of weeks earlier, <a href="https://twitter.com/buhakmeh">Khalid</a> tweeted
a link to a .NET app running on a <code>.railway.app</code> sub-domain, and I&rsquo;d been
intrigued and checked out what that was. Turns out,
<a href="https://railway.app">Railway</a> is a cloud platform that provides really,
really simple hosting and deployment, as well as plug-ins for things like
Redis, MongoDB and Postgres. The good bit is that it&rsquo;s <strong>free</strong> for up
to three projects. So I thought I&rsquo;d give it a try and last night,
on the stream, I went for it.</p>
<h2 id="creating-a-railway-project">Creating a Railway project</h2>
<p>I used the Railway web UI to create the project and added the Redis plugin
to it. This sets some variables like <code>REDISHOST</code> and <code>REDISPASSWORD</code> which
are available as environment variables at runtime. I also added my Twitch
app details as custom variables here.</p>
<h2 id="installing-the-railway-cli">Installing the Railway CLI</h2>
<p>Railway provides a CLI that lets you set up projects, run them locally
and deploy them to the cloud with simple commands like <code>railway run</code>.
Unfortunately it doesn&rsquo;t work very well on Windows, but that&rsquo;s no problem
these days because Windows runs Linux, and it works perfectly there.
I initially tried to install it using NPM, but that didn&rsquo;t work. That&rsquo;s not
Railway&rsquo;s fault: NPM just didn&rsquo;t work.</p>
<p>Luckily for me Jake from Railway was in the stream chat and he
suggested using the Curl installation, and very helpfully pasted the
command into the chat. So I copied and pasted it into my WSL terminal,
because running Linux commands from my Twitch chat with <code>sudo</code> is how I roll.</p>
<p>Then I <code>cd</code>&rsquo;d into the Windows project directory from my WSL shell
(<code>cd /mnt/d/Twitch/StreamBadger</code>). Technically there&rsquo;s a tiny performance
overhead from working with Windows directories in WSL, but in this case
it made no noticeable difference.</p>
<h2 id="logging-in">Logging in</h2>
<p>First up I had to do <code>railway login</code> which, funnily enough, took me through the
exact same process I&rsquo;m trying to create for StreamBadger: it opened a
browser page and, because I was already logged in, just magically
authenticated. Now my WSL session was connected to my Railway account.</p>
<h2 id="linking-the-project">Linking the project</h2>
<p>On the Railway dashboard page from my project there was yet another
command I could just copy and paste into my terminal to link the current
directory with the Railway project.  So I did that, but not with <code>sudo</code>
this time because that was not necessary. I linked from the project root
directory where the <code>.sln</code> and <code>Dockerfile</code> files were.</p>
<h2 id="adding-a-dockerfile">Adding a Dockerfile</h2>
<p>Railway runs a bunch of different things like Node.js, Python and Ruby,
but it will also just run Docker images, which means it can handle pretty
much anything. If there&rsquo;s a Dockerfile in your directory, it will just use
it. So I created a standard Dockerfile for my web app:</p>






<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-docker" data-lang="docker"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 1</span><span><span style="color:#cba6f7">FROM</span><span style="color:#a6e3a1"> mcr.microsoft.com/dotnet/sdk:5.0 AS build-env</span><span style="color:#f38ba8">
</span></span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 2</span><span><span style="color:#f38ba8"></span><span style="color:#cba6f7">WORKDIR</span><span style="color:#a6e3a1"> /app</span><span style="color:#f38ba8">
</span></span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 3</span><span><span style="color:#f38ba8">
</span></span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 4</span><span><span style="color:#f38ba8"></span><span style="color:#6c7086;font-style:italic"># Copy csproj and restore as distinct layers</span><span style="color:#f38ba8">
</span></span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 5</span><span><span style="color:#f38ba8"></span><span style="color:#cba6f7">COPY</span> ./StreamBadger.sln .<span style="color:#f38ba8">
</span></span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 6</span><span><span style="color:#f38ba8"></span><span style="color:#cba6f7">COPY</span> ./src/StreamBadger/StreamBadger.csproj ./src/StreamBadger/<span style="color:#f38ba8">
</span></span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 7</span><span><span style="color:#f38ba8"></span><span style="color:#cba6f7">COPY</span> ./src/StreamBadgerDesktop/StreamBadgerDesktop.csproj ./src/StreamBadgerDesktop/<span style="color:#f38ba8">
</span></span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 8</span><span><span style="color:#f38ba8"></span><span style="color:#cba6f7">COPY</span> ./src/StreamBadgerLogin/StreamBadgerLogin.csproj ./src/StreamBadgerLogin/<span style="color:#f38ba8">
</span></span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 9</span><span><span style="color:#f38ba8"></span><span style="color:#cba6f7">RUN</span> dotnet restore<span style="color:#f38ba8">
</span></span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">10</span><span><span style="color:#f38ba8">
</span></span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">11</span><span><span style="color:#f38ba8"></span><span style="color:#6c7086;font-style:italic"># Copy everything else and build</span><span style="color:#f38ba8">
</span></span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">12</span><span><span style="color:#f38ba8"></span><span style="color:#cba6f7">COPY</span> . .<span style="color:#f38ba8">
</span></span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">13</span><span><span style="color:#f38ba8"></span><span style="color:#cba6f7">RUN</span> dotnet publish --no-restore -c Release -o out ./src/StreamBadgerLogin<span style="color:#f38ba8">
</span></span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">14</span><span><span style="color:#f38ba8">
</span></span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">15</span><span><span style="color:#f38ba8"></span><span style="color:#6c7086;font-style:italic"># Build runtime image</span><span style="color:#f38ba8">
</span></span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">16</span><span><span style="color:#f38ba8"></span><span style="color:#cba6f7">FROM</span><span style="color:#a6e3a1"> mcr.microsoft.com/dotnet/aspnet:5.0</span><span style="color:#f38ba8">
</span></span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">17</span><span><span style="color:#f38ba8"></span><span style="color:#cba6f7">WORKDIR</span><span style="color:#a6e3a1"> /app</span><span style="color:#f38ba8">
</span></span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">18</span><span><span style="color:#f38ba8"></span><span style="color:#cba6f7">COPY</span> --from<span style="color:#89dceb;font-weight:bold">=</span>build-env /app/out .<span style="color:#f38ba8">
</span></span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">19</span><span><span style="color:#f38ba8"></span><span style="color:#cba6f7">ENTRYPOINT</span> [<span style="color:#a6e3a1">&#34;./StreamBadgerLogin&#34;</span>]</span></span></code></pre></div>
<p>I also added a <code>.dockerignore</code> file to ignore <code>bin</code> and <code>obj</code> directories.</p>






<pre tabindex="0"><code>**/bin/
**/obj/</code></pre>
<p>This is important if you&rsquo;re developing on Windows because the <code>obj</code> directory
contains a file called <code>project.assets.json</code>, which is like a lock file
for Nuget packages and such. If you copy this into the Docker image, which
is running Linux, then the <code>dotnet restore</code> build step will explode and kill
your cat.</p>
<h2 id="running-the-project-locally">Running the project locally</h2>
<p>You can test your code straight away, pointing at the actual resources in
the cloud, like my Redis plugin. I just had to tweak the code to read the
connection details from the environment variables mentioned earlier. Then
it&rsquo;s just <code>railway run</code> and it builds the Docker image and runs your app
on <code>localhost</code>. Slightly weird thing, though: it arbitrarily chooses a port
to bind to. In my case it chose 4411. This is also exposed as an
environment variable, <code>PORT</code>, which I believe is quite a common practice in
the land of script. Jake from Railway pointed this out and I added some code
to my app to use this port:</p>






<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-c#" data-lang="c#"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 1</span><span>    <span style="color:#f38ba8">public</span> <span style="color:#f38ba8">static</span> IHostBuilder CreateHostBuilder(<span style="color:#f38ba8">string</span>[] args) =&gt;
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 2</span><span>        Host.CreateDefaultBuilder(args)
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 3</span><span>            .ConfigureWebHostDefaults(webBuilder =&gt;
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 4</span><span>            {
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 5</span><span>                webBuilder.UseStartup&lt;Startup&gt;();
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 6</span><span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 7</span><span>                <span style="color:#6c7086;font-style:italic">// For running in Railway</span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 8</span><span>                <span style="color:#f38ba8">var</span> portVar = Environment.GetEnvironmentVariable(<span style="color:#a6e3a1">&#34;PORT&#34;</span>);
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 9</span><span>                <span style="color:#cba6f7">if</span> (portVar <span style="color:#cba6f7">is</span> {Length: &gt;<span style="color:#fab387">0</span>} &amp;&amp; <span style="color:#f38ba8">int</span>.TryParse(portVar, <span style="color:#cba6f7">out</span> <span style="color:#f38ba8">int</span> port))
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">10</span><span>                {
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">11</span><span>                    webBuilder.ConfigureKestrel(options =&gt;
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">12</span><span>                    {
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">13</span><span>                        options.ListenAnyIP(port);
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">14</span><span>                    });
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">15</span><span>                }
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">16</span><span>            });</span></span></code></pre></div>
<p>Then I did <code>railway run</code> again and there was my app, happily running in
Docker and connecting to the Railway Redis instance.</p>
<h2 id="deploying-to-the-cloud">Deploying to the cloud</h2>
<p>The theory here is that if <code>railway run</code> works on your machine, then it
should work in the cloud too. To deploy it, you just use <code>railway up</code>.
That&rsquo;s it. Under the covers it builds your Docker image, pushes it to
Railway&rsquo;s registry, spins up the container and connects it to your sub-domain
(in this instance
<a href="https://streambadger-production.up.railway.app/">streambadger-production.up.railway.app/</a>). And it just worked. Took a couple of minutes and there it
was.</p>
<h2 id="connecting-an-actual-domain">Connecting an actual domain</h2>
<p>I&rsquo;ve bought <a href="https://streambadger.com">streambadger.com</a> because of course
I have, so I wanted to get that hooked up as well. I do pretty much all my
DNS through Cloudflare because they&rsquo;re awesome and do magic SSL and such,
so I just needed to set up a CNAME pointing to the Railway sub-domain and&hellip;
no wait, infinite redirect loop. I&rsquo;d missed a helpful bit in the
<a href="https://docs.railway.app/deployment/custom-domains#provider-specific-instructions">Provider Specific Instructions</a>
that tells you to use the <em>Full</em> SSL/TLS encryption mode with Cloudflare.
This is because Railway just won&rsquo;t serve your site over plain old <code>HTTP</code>, but Cloudflare&rsquo;s <em>Flexible</em> encryption mode only proxies to <code>HTTP</code>.</p>
<h2 id="in-summary-then">In summary, then</h2>
<p>I&rsquo;m super impressed with <a href="https://railway.app">Railway</a>.
They&rsquo;ve obviously worked very hard on the
user experience, and it shows. I had Jake in the stream chat to help me out
so I wasn&rsquo;t having to refer to
<a href="https://docs.railway.app/">the docs</a>, but they&rsquo;re really good. The one
small hiccup was the Windows issues with the CLI, which isn&rsquo;t great, but
I&rsquo;d expect any developer in 2021 to be running WSL anyway.</p>
<p>You get three projects with two plugins per project on the Free plan, with
a couple of environments (e.g. Staging and Production) as well. The next
step up is the $20/month &ldquo;Early Adopter&rdquo; plan, which is just unlimited
everything: projects, plugins, environments, deploys. And access to something
called Teams, which I don&rsquo;t know what that is.</p>
<p>I&rsquo;ve got a few things I run online, and I&rsquo;m pretty sure if I moved them all
over to Railway on that $20 plan I&rsquo;d be saving money over the various cloud
platforms I&rsquo;m using at the moment. It won&rsquo;t replace Azure for more complex
stuff with a wider range of platform services, but for side projects and
simple sites or APIs I think Railway looks great.</p>
<h2 id="disclaimer">Disclaimer</h2>
<p>This post was <strong>not</strong> sponsored or requested by Railway. I used it
and liked it and just wanted to share.</p>
<h2 id="twitch">Twitch</h2>
<p>If you want to watch me learn more stuff like this in real-time, stop by
<a href="https://twitch.tv/markrendle">my Twitch channel</a> some time.
I&rsquo;m usually on at 7pm UK (6pm UTC) on Tuesdays and Thursdays.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Upgrading ASP.NET projects one page at a time</title>
      <link>https://rendle.dev/blog/upgrading-aspnet-projects/</link>
      <pubDate>Fri, 08 Jan 2021 18:30:17 +0100</pubDate><author>rendle@rendlelabs.com (Rendle)</author>
      <guid>https://rendle.dev/blog/upgrading-aspnet-projects/</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; I&amp;rsquo;ve come up with a way to gradually migrate old ASP.NET WebForms (or MVC)
applications to .NET 5.0, and you can watch me figure it out in real-time on
&lt;a href=&#34;https://twitch.tv/markrendle&#34;&gt;my brand new Twitch channel&lt;/a&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;I&amp;rsquo;ve been working on Visual ReCode for a while now. It&amp;rsquo;s a plug-in for Visual Studio 2019
that helps developers migrate old .NET 4.x code to new .NET 5.0 or .NET Core 3.1.
&lt;a href=&#34;https://visualrecode.com/blog/Visual-ReCode-2-Announcment/&#34;&gt;We just released version 2.0&lt;/a&gt;
, with a new feature called
&lt;a href=&#34;https://visualrecode.com/blog/Guided-Project-Upgrades-with-ReCode/&#34;&gt;Guided Project Upgrades&lt;/a&gt;
that helps you upgrade your solution one project at a time,
switching to the new SDK-style &lt;code&gt;csproj&lt;/code&gt; format, and adding modern target frameworks like
&lt;code&gt;net5.0&lt;/code&gt; or &lt;code&gt;netstandard2.0&lt;/code&gt;. It checks your NuGet packages and upgrades them if
necessary, and makes sure you upgrade projects in the right order.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p><strong>TL;DR:</strong> I&rsquo;ve come up with a way to gradually migrate old ASP.NET WebForms (or MVC)
applications to .NET 5.0, and you can watch me figure it out in real-time on
<a href="https://twitch.tv/markrendle">my brand new Twitch channel</a>.</p>
<hr>
<p>I&rsquo;ve been working on Visual ReCode for a while now. It&rsquo;s a plug-in for Visual Studio 2019
that helps developers migrate old .NET 4.x code to new .NET 5.0 or .NET Core 3.1.
<a href="https://visualrecode.com/blog/Visual-ReCode-2-Announcment/">We just released version 2.0</a>
, with a new feature called
<a href="https://visualrecode.com/blog/Guided-Project-Upgrades-with-ReCode/">Guided Project Upgrades</a>
that helps you upgrade your solution one project at a time,
switching to the new SDK-style <code>csproj</code> format, and adding modern target frameworks like
<code>net5.0</code> or <code>netstandard2.0</code>. It checks your NuGet packages and upgrades them if
necessary, and makes sure you upgrade projects in the right order.</p>
<p>Visual ReCode also provides a way to migrate WCF applications, which are not supported in
the new .NET, to gRPC, which is very much supported (in fact, it&rsquo;s the best-performing
gRPC implementation there is). And right now I&rsquo;m working on providing the same
functionality for ASP.NET WebAPI, migrating it to ASP.NET
Core MVC, and looking at a WCF ReST to ASP.NET Core MVC feature.</p>
<p>That still leaves a couple of old frameworks on the table, though. <strong>ASP.NET
MVC</strong> is a possible target for future investigation, although there are quite a lot of
differences that mean it would be more of a &ldquo;helper&rdquo; than an automated migrator. But
<strong>ASP.NET WebForms</strong> is a whole other thing. It&rsquo;s been around for 20 years now;
it&rsquo;s really, really complicated; it&rsquo;s completely unlike MVC or Blazor or anything else
in the modern .NET stack. There&rsquo;s really no way to automate the migration of WebForms
applications to ASP.NET Core.</p>
<p>And the chances are, if you&rsquo;ve got a WebForms application in
production, it&rsquo;s a massive behemoth of code that&rsquo;s built up over years, and rewriting
it would be a huge undertaking. Having worked on rewrite projects in the past, I know
from experience that they&rsquo;re a nightmare, and get derailed all the time by the
maintenance requirements of the existing code.</p>
<h2 id="so-what-to-do">So what to do?</h2>
<p>The big problem is that there&rsquo;s no way to gradually upgrade WebForms (or MVC) to
ASP.NET Core. You can&rsquo;t run Core on .NET 4.x, and you can&rsquo;t run WebForms
on Core. But what if you could run two separate applications and make it look as if
they&rsquo;re the same one?</p>
<p>Enter <strong>YARP</strong>.</p>
<p>YARP stands for Yet Another Reverse Proxy, and it&rsquo;s a Microsoft project to build a
reverse proxy server on top of the blazing-fast Kestrel HTTP server. It&rsquo;s not a
stand-alone application; it&rsquo;s a package that runs in an ASP.NET Core app. You just
reference the
<a href="https://www.nuget.org/packages/Microsoft.ReverseProxy/1.0.0-preview.7.20562.2">Microsoft.ReverseProxy</a>
package and add it as an endpoint in your <code>Startup</code> class.</p>






<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 1</span><span><span style="color:#f38ba8">public</span> <span style="color:#cba6f7">void</span> ConfigureServices(IServiceCollection services)
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 2</span><span>{
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 3</span><span>    services.AddReverseProxy() 
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 4</span><span>        .LoadFromConfig(Configuration.GetSection(<span style="color:#a6e3a1">&#34;ReverseProxy&#34;</span>));
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 5</span><span>}
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 6</span><span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 7</span><span><span style="color:#f38ba8">public</span> <span style="color:#cba6f7">void</span> Configure(IApplicationBuilder app, IWebHostEnvironment env)
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 8</span><span>{
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 9</span><span>    app.UseRouting();
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">10</span><span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">11</span><span>    app.UseEndpoints(endpoints =&gt;
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">12</span><span>    {
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">13</span><span>        endpoints.MapReverseProxy();
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">14</span><span>    });
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">15</span><span>}</span></span></code></pre></div>
<p>Being just part of the ASP.NET Core pipeline means that you can do all sorts of
creative things with your reverse proxy by adding more middleware. And of course,
one of the pieces of middleware you could add in there is ASP.NET Core MVC.</p>






<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 1</span><span><span style="color:#f38ba8">public</span> <span style="color:#cba6f7">void</span> ConfigureServices(IServiceCollection services)
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 2</span><span>{
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 3</span><span>    services.AddControllersWithViews();
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 4</span><span>    services.AddReverseProxy() 
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 5</span><span>        .LoadFromConfig(Configuration.GetSection(<span style="color:#a6e3a1">&#34;ReverseProxy&#34;</span>));
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 6</span><span>}
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 7</span><span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 8</span><span><span style="color:#f38ba8">public</span> <span style="color:#cba6f7">void</span> Configure(IApplicationBuilder app, IWebHostEnvironment env)
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 9</span><span>{
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">10</span><span>    app.UseRouting();
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">11</span><span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">12</span><span>    app.UseEndpoints(endpoints =&gt;
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">13</span><span>    {
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">14</span><span>        endpoints.MapControllerRoute(
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">15</span><span>            name: <span style="color:#a6e3a1">&#34;default&#34;</span>,
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">16</span><span>            pattern: <span style="color:#a6e3a1">&#34;{controller=Home}/{action=Index}/{id?}&#34;</span>);
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">17</span><span>        
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">18</span><span>        endpoints.MapReverseProxy();
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">19</span><span>    });
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">20</span><span>}</span></span></code></pre></div>
<p>Now, let&rsquo;s say the home page for your WebForms application is <code>~/Default.aspx</code>. If
you add an MVC action with that route to your Core application, the routing will hit
that first. Any routes that aren&rsquo;t hit in your application, though, will be
transparently proxied through to the old app. Now you can rewrite individual pages
in your site however you want, while continuing to support and maintain the old
application, until eventually everything is migrated and the old code can be retired.</p>
<h2 id="the-complexity">The complexity</h2>
<p>If only it were just that easy. There are a couple of things in the way of this approach:
session state and authentication. WebForms applications in particular tend to be heavy
users of server-side session, whereas in ASP.NET Core it&rsquo;s much less used. And
authentication is very different, too.</p>
<p>I&rsquo;m pretty sure I&rsquo;ve figured out a way to address these issues, though. And as a new
thing for 2021, I wanted to try my hand at streaming. So I&rsquo;m combining these two things:
you can join me on <a href="https://twitch.tv/markrendle">my Twitch stream</a> and watch as I try
to gradually upgrade the
<a href="https://github.com/Project6/Wingtip-Toys">Wingtip Toys sample application</a>
to ASP.NET Core 5.0 using this approach. I&rsquo;ve done two streams to date, and so far
I&rsquo;ve got the home page to look roughly the same, and implemented basic session sharing
between the two applications (that even works with &ldquo;in-proc&rdquo; session on the WebForms
side), as well as copying and pasting a bunch of Entity Framework 6.0 classes and
finding out that they work perfectly in EF Core 5.0.</p>
<p>Once I&rsquo;ve got everything working in this test application, I&rsquo;m going to package up the
various components as open-source NuGet packages so that everyone can try this approach.</p>
<p>If you want to catch up on the streams, I&rsquo;ve archived them on YouTube:
<a href="https://www.youtube.com/watch?v=1DEXDKQguK8">Part 1</a> and
<a href="https://www.youtube.com/watch?v=2V-ntu0rtq4">Part 2</a>. Or if you just want to browse
around the code, you can <a href="https://github.com/VisualReCode/Facade">find it on GitHub</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Nullable References in Enumerables</title>
      <link>https://rendle.dev/blog/where-not-null/</link>
      <pubDate>Sat, 05 Dec 2020 13:30:17 +0100</pubDate><author>rendle@rendlelabs.com (Rendle)</author>
      <guid>https://rendle.dev/blog/where-not-null/</guid>
      <description>&lt;p&gt;I&amp;rsquo;m a big fan of the &lt;a href=&#34;https://docs.microsoft.com/en-us/dotnet/csharp/nullable-references&#34;&gt;Nullable Reference Types&lt;/a&gt;
feature added in C# 8.0. It lets the compiler catch a bunch of potential runtime errors,
and I guarantee if you turn it on you&amp;rsquo;ll get a bunch of warnings. As of .NET 5.0, the
entire BCL is now covered with nullable annotations, and a lot of the extended ecosystem
supports them too.&lt;/p&gt;
&lt;p&gt;But there are situations where the compiler is unable to work out that you&amp;rsquo;ve done a null
check, meaning you either have to use the &amp;ldquo;null-forgiving operator&amp;rdquo; (where you append a
bang to the identifier, like &lt;code&gt;item!.Name&lt;/code&gt;), disable the null checking, or just ignore the
warning.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>I&rsquo;m a big fan of the <a href="https://docs.microsoft.com/en-us/dotnet/csharp/nullable-references">Nullable Reference Types</a>
feature added in C# 8.0. It lets the compiler catch a bunch of potential runtime errors,
and I guarantee if you turn it on you&rsquo;ll get a bunch of warnings. As of .NET 5.0, the
entire BCL is now covered with nullable annotations, and a lot of the extended ecosystem
supports them too.</p>
<p>But there are situations where the compiler is unable to work out that you&rsquo;ve done a null
check, meaning you either have to use the &ldquo;null-forgiving operator&rdquo; (where you append a
bang to the identifier, like <code>item!.Name</code>), disable the null checking, or just ignore the
warning.</p>
<p>One of these that I run across a lot is in LINQ chains. For example, I&rsquo;m just writing
some code that enumerates through the <code>MetadataReferences</code> property of a Roslyn
<code>Project</code> to find the file paths of references:</p>






<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">1</span><span><span style="color:#f38ba8">var</span> files = project.MetadataReferences
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">2</span><span>    .OfType&lt;PortableExecutableReference&gt;
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">3</span><span>    .Select(p =&gt; p.FilePath);</span></span></code></pre></div>
<p>In the Roslyn library, <code>PortableExecutableReference.FilePath</code> is declared as <code>string?</code>,
because apparently you can have a <code>PortableExecutableReference</code> that isn&rsquo;t actually a
file. So the type of this expression is <code>IEnumerable&lt;string?&gt;</code>.</p>
<p>Now, I can filter out the null file paths with a simple <code>Where</code> call:</p>






<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">1</span><span><span style="color:#f38ba8">var</span> files = project.MetadataReferences
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">2</span><span>    .OfType&lt;PortableExecutableReference&gt;
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">3</span><span>    .Select(p =&gt; p.FilePath)
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">4</span><span>    .Where(f =&gt; f <span style="color:#cba6f7">is</span> not <span style="color:#fab387">null</span>);</span></span></code></pre></div>
<p>But the compiler isn&rsquo;t actually clever enough to work out what that <code>Where</code> does, so the
type of the expression is <em>still</em> <code>IEnumerable&lt;string?&gt;</code>. Vexing. There&rsquo;s actually no
easy way to deal with this within a chain of LINQ methods (that I can think of, at
least).</p>
<p>So, I&rsquo;ve added an extension method to my common library of &ldquo;stuff that&rsquo;s not in
the BCL&rdquo; that filters out nulls and changes the expression type accordingly.</p>






<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">1</span><span><span style="color:#f38ba8">public</span> <span style="color:#f38ba8">static</span> IEnumerable&lt;T&gt; WhereNotNull&lt;T&gt;(<span style="color:#cba6f7">this</span> IEnumerable&lt;T?&gt; source) <span style="color:#cba6f7">where</span> T : <span style="color:#cba6f7">class</span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">2</span><span>{
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">3</span><span>    <span style="color:#cba6f7">foreach</span> (<span style="color:#f38ba8">var</span> item <span style="color:#cba6f7">in</span> source)
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">4</span><span>    {
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">5</span><span>        <span style="color:#cba6f7">if</span> (item <span style="color:#cba6f7">is</span> not <span style="color:#fab387">null</span>) <span style="color:#cba6f7">yield</span> <span style="color:#cba6f7">return</span> item;
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">6</span><span>    }
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">7</span><span>}</span></span></code></pre></div>
<p>And I thought that might be useful for other people so I&rsquo;m sharing it here.</p>
<h2 id="advertisement">Advertisement</h2>
<p>I ran into this while working on <a href="https://visualrecode.com">Visual ReCode</a>. It&rsquo;s an
extension for Visual Studio 2019 that helps you migrate code from .NET 4.x to
.NET Core 3.1 and .NET 5.0, including a WCF to gRPC conversion to upgrade your
services to the modern, cross-platform standard for RPC.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Creating a dotnet tool</title>
      <link>https://rendle.dev/blog/creating-a-dotnet-tool/</link>
      <pubDate>Fri, 14 Aug 2020 13:30:17 +0100</pubDate><author>rendle@rendlelabs.com (Rendle)</author>
      <guid>https://rendle.dev/blog/creating-a-dotnet-tool/</guid>
      <description>&lt;p&gt;One of the great features of .NET Core is that it gives you a simple
way to create and distribute CLI tools via NuGet. You just create a Console
application, add a few entries to the &lt;code&gt;.csproj&lt;/code&gt; file, and publish it to
NuGet. Then other people can install it with the &lt;code&gt;dotnet tool install&lt;/code&gt;
command. I&amp;rsquo;ve published a couple of tools this way before, but I&amp;rsquo;ve just
published another one so I thought I&amp;rsquo;d take time to describe the steps
involved, as well as a handful of neat NuGet packages that I used along
the way.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>One of the great features of .NET Core is that it gives you a simple
way to create and distribute CLI tools via NuGet. You just create a Console
application, add a few entries to the <code>.csproj</code> file, and publish it to
NuGet. Then other people can install it with the <code>dotnet tool install</code>
command. I&rsquo;ve published a couple of tools this way before, but I&rsquo;ve just
published another one so I thought I&rsquo;d take time to describe the steps
involved, as well as a handful of neat NuGet packages that I used along
the way.</p>
<h2 id="nuke-from-orbit">nuke-from-orbit</h2>
<p>You know how doing <code>dotnet clean</code> (or right-click &gt; Clean solution in Visual Studio)
doesn&rsquo;t actually delete all the <code>bin</code> and <code>obj</code> directories and the stuff in them?
And you know how sometimes you really, really have to do that, because something
hiding in one of those directories has got itself into such a muddle that the only
way to fix it is to nuke the site from orbit? Yeah. That&rsquo;s why I wrote this.</p>
<p>The tool is published as <a href="https://www.nuget.org/packages/RendleLabs.NukeFromOrbit/">RendleLabs.NukeFromOrbit</a>,
so you can install it by running:</p>






<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">1</span><span>dotnet tool install -g RendleLabs.NukeFromOrbit</span></span></code></pre></div>
<p>The <code>-g</code> tells dotnet to install the tool globally so you can use it from anywhere.</p>
<p>This should output:</p>






<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">1</span><span>You can invoke the tool using the following command: nuke-from-orbit
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">2</span><span>Tool &#39;rendlelabs.nukefromorbit&#39; (version &#39;1.0.3&#39;) was successfully installed.</span></span></code></pre></div>
<p>Which helpfully tells you that you can now call it as <code>nuke-from-orbit</code>. If you run it
with the <code>--help</code> argument, it will helpfully tell you how to use it:</p>






<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 1</span><span>NukeFromOrbit:
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 2</span><span>  Dust off and nuke bin and obj directories from orbit. It&#39;s the only way to be sure.
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 3</span><span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 4</span><span>Usage:
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 5</span><span>  NukeFromOrbit [options] [&lt;workingDirectory&gt;]
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 6</span><span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 7</span><span>Arguments:
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 8</span><span>  &lt;workingDirectory&gt;    [default: D:\blog]
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 9</span><span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">10</span><span>Options:
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">11</span><span>  -y, --yes         Don&#39;t ask for confirmation, just nuke it. [default: False]
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">12</span><span>  -n, --dry-run     List items that will be nuked but don&#39;t nuke them. [default: False]
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">13</span><span>  --version         Show version information
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">14</span><span>  -?, -h, --help    Show help and usage information</span></span></code></pre></div>
<p>That little feature comes from a NuGet package called
<a href="https://www.nuget.org/packages/System.CommandLine">System.CommandLine</a>,
which is pre-release at the moment but dead useful for this kind of thing.</p>
<h2 id="systemcommandline">System.CommandLine</h2>
<p>This package provides a command line argument parser which makes it easy to handle
flags, options and arguments, and even sub-commands if you&rsquo;re building something big.
You just create a <code>Command</code>, assign a <code>Handler</code> to it, and then <code>Invoke</code> it with
the <code>args</code> parameter from your <code>Main</code> method.</p>
<p>For <code>nuke-from-orbit</code> the command looks like this:</p>






<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 1</span><span><span style="color:#f38ba8">var</span> command = <span style="color:#cba6f7">new</span> RootCommand
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 2</span><span>{
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 3</span><span>    <span style="color:#cba6f7">new</span> Option&lt;<span style="color:#f38ba8">bool</span>&gt;(<span style="color:#cba6f7">new</span>[]{<span style="color:#a6e3a1">&#34;--yes&#34;</span>, <span style="color:#a6e3a1">&#34;-y&#34;</span>}, () =&gt; <span style="color:#fab387">false</span>,
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 4</span><span>        <span style="color:#a6e3a1">&#34;Don&#39;t ask for confirmation, just nuke it.&#34;</span>),
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 5</span><span>    <span style="color:#cba6f7">new</span> Option&lt;<span style="color:#f38ba8">bool</span>&gt;(<span style="color:#cba6f7">new</span>[]{<span style="color:#a6e3a1">&#34;--dry-run&#34;</span>, <span style="color:#a6e3a1">&#34;-n&#34;</span>}, () =&gt; <span style="color:#fab387">false</span>,
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 6</span><span>        <span style="color:#a6e3a1">&#34;List items that will be nuked but don&#39;t nuke them.&#34;</span>),
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 7</span><span>    <span style="color:#cba6f7">new</span> Argument&lt;<span style="color:#f38ba8">string</span>&gt;(<span style="color:#a6e3a1">&#34;workingDirectory&#34;</span>, () =&gt; Environment.CurrentDirectory)
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 8</span><span>};
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 9</span><span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">10</span><span>command.Description = <span style="color:#a6e3a1">&#34;Dust off and nuke bin and obj directories from orbit. It&#39;s the only way to be sure.&#34;</span>;</span></span></code></pre></div>
<p>It&rsquo;s using C#&rsquo;s list initializer syntax so you can add as many <code>Option</code>s and <code>Argument</code>s
as you need. The <code>Argument</code>s will be <em>positional</em> if there&rsquo;s more than one, in the order
that they&rsquo;re added in the list. The <code>Option</code>s can be anywhere, before or after the
<code>Argument</code>s.</p>
<p>For each option, you can specify an array of <em>aliases</em>, like <code>--yes</code> or <code>-y</code>. You can
also specify a default value for both <code>Option</code>s and <code>Argument</code>s by using a <code>Func&lt;T&gt;</code>
that returns the default; if you specify this, the <code>Option</code> or <code>Argument</code> will be
optional.</p>
<p>The <code>Description</code> property is output in the Help, just under the name of the command.</p>
<p>To add a handler, we just create a <code>CommandHandler</code> using an <code>Action</code> or <code>Func</code>, like this:</p>






<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">1</span><span>command.Handler = CommandHandler.Create&lt;<span style="color:#f38ba8">bool</span>, <span style="color:#f38ba8">bool</span>, <span style="color:#f38ba8">string</span>&gt;(
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">2</span><span>    <span style="color:#f38ba8">async</span> (yes, dryRun, workingDirectory) =&gt;
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">3</span><span>    {
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">4</span><span>        <span style="color:#cba6f7">await</span> Nuke(yes, dryRun, workingDirectory);
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">5</span><span>    }
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">6</span><span>);</span></span></code></pre></div>
<p><code>System.CommandLine</code> uses the names of the parameters on the delegate to match to the
<code>Option</code> and <code>Argument</code> names; <code>Option</code>s with hyphens are converted to <code>camelCase</code> so
<code>--dry-run</code> matches <code>dryRun</code>.</p>
<blockquote>
<p><strong>Note:</strong> at the time of writing System.CommandLine is still in preview, so you&rsquo;ll
need to enable pre-release packages to use it in your project.</p>
</blockquote>
<h2 id="finding-bin-and-obj-folders">Finding bin and obj folders</h2>
<p>Have you ever written an application that worked with files or directories and muttered
dark curses at whichever API designer decided that the <code>File</code> and <code>Directory</code> classes
should be <code>static</code>, so they&rsquo;re impossible to test against? You are not alone.
Rejoice! for there is a NuGet package that fixes this, too:
<a href="https://www.nuget.org/packages/System.IO.Abstractions">System.IO.Abstractions</a> provides
interfaces for all the static <code>System.IO</code> types, along with default implementations,
so you can write proper tests without contaminating the file system of whatever
machine the tests run on.</p>






<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">1</span><span><span style="color:#cba6f7">foreach</span> (<span style="color:#f38ba8">var</span> directory <span style="color:#cba6f7">in</span> _fileSystem.Directory.EnumerateDirectories(currentDirectory))
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">2</span><span>{
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">3</span><span>    <span style="color:#f38ba8">var</span> name = _fileSystem.Path.GetFileName(directory);
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">4</span><span>    <span style="color:#cba6f7">if</span> (name <span style="color:#cba6f7">is</span> <span style="color:#fab387">null</span>) <span style="color:#cba6f7">continue</span>;</span></span></code></pre></div>
<p>It even allows you to mock <code>Path</code> so you can change things like <code>DirectorySeparatorChar</code>
and simulate Linux or Windows in your tests.</p>
<p>Here&rsquo;s an example using <a href="https://www.nuget.org/packages/NSubstitute/">NSubstitute</a>
to mock the <code>IDirectory</code> interface:</p>






<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 1</span><span>        <span style="color:#f38ba8">private</span> <span style="color:#f38ba8">static</span> IDirectory FakeDirectory(<span style="color:#f38ba8">string</span>[] directories, IPath path, <span style="color:#f38ba8">string</span>[] files)
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 2</span><span>        {
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 3</span><span>            <span style="color:#f38ba8">var</span> directory = Substitute.For&lt;IDirectory&gt;();
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 4</span><span>            
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 5</span><span>            directory.EnumerateDirectories(Arg.Any&lt;<span style="color:#f38ba8">string</span>&gt;())
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 6</span><span>                .Returns(c =&gt; directories.Where(d =&gt; path.GetDirectoryName(d) == c.Arg&lt;<span style="color:#f38ba8">string</span>&gt;()).Distinct());
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 7</span><span>            
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 8</span><span>            directory.EnumerateFiles(Arg.Any&lt;<span style="color:#f38ba8">string</span>&gt;())
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 9</span><span>                .Returns(c =&gt; files.Where(f =&gt; path.GetDirectoryName(f) == c.Arg&lt;<span style="color:#f38ba8">string</span>&gt;()).Distinct());
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">10</span><span>            
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">11</span><span>            directory.Exists(Arg.Any&lt;<span style="color:#f38ba8">string</span>&gt;()).Returns(<span style="color:#fab387">true</span>);
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">12</span><span>            
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">13</span><span>            <span style="color:#cba6f7">return</span> directory;
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">14</span><span>        }</span></span></code></pre></div>
<p>The other really nice thing about <code>System.IO.Abstractions</code> is that you can add extension
methods to the interfaces, like this one to detect whether the file system is case
sensitive:</p>






<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 1</span><span><span style="color:#f38ba8">public</span> <span style="color:#f38ba8">static</span> <span style="color:#cba6f7">class</span> <span style="color:#f9e2af">FileSystemExtensions</span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 2</span><span>{
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 3</span><span>    <span style="color:#f38ba8">public</span> <span style="color:#f38ba8">static</span> <span style="color:#f38ba8">bool</span> IsCaseSensitive(<span style="color:#cba6f7">this</span> IFileSystem fileSystem, <span style="color:#f38ba8">string</span> directory)
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 4</span><span>    {
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 5</span><span>        <span style="color:#cba6f7">if</span> (!fileSystem.Directory.Exists(directory))
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 6</span><span>            <span style="color:#cba6f7">throw</span> <span style="color:#cba6f7">new</span> InvalidOperationException(<span style="color:#a6e3a1">&#34;Directory does not exist.&#34;</span>);
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 7</span><span>        
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 8</span><span>        <span style="color:#cba6f7">if</span> (directory.Where(<span style="color:#f38ba8">char</span>.IsLetter).Any(<span style="color:#f38ba8">char</span>.IsLower))
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 9</span><span>        {
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">10</span><span>            <span style="color:#cba6f7">if</span> (fileSystem.Directory.Exists(directory.ToUpper())) <span style="color:#cba6f7">return</span> <span style="color:#fab387">false</span>;
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">11</span><span>        }
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">12</span><span>        <span style="color:#cba6f7">else</span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">13</span><span>        {
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">14</span><span>            <span style="color:#cba6f7">if</span> (fileSystem.Directory.Exists(directory.ToLower())) <span style="color:#cba6f7">return</span> <span style="color:#fab387">false</span>;
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">15</span><span>        }
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">16</span><span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">17</span><span>        <span style="color:#cba6f7">return</span> <span style="color:#fab387">true</span>;
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">18</span><span>    }
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">19</span><span>}</span></span></code></pre></div>
<h2 id="not-deleting-git-controlled-files">Not deleting Git-controlled files</h2>
<p>There are bad people in the world. People who hate puppies, eat your last Jaffa Cake, and
add files in their <code>bin</code> and <code>obj</code> directories to source control. I&rsquo;m sure they have very
good reasons for doing that, but they don&rsquo;t. Let&rsquo;s humour them anyway, and check whether
and <code>bin</code> or <code>obj</code> files are in Git before deleting them.</p>
<p>From the command line, you can get a complete list of files that are version controlled
using the <code>git ls-files</code> command. So we can run that, find any entries with <code>/bin/</code> or
<code>/obj/</code> in them, and add them to some kind of protected list.</p>
<p>I don&rsquo;t know if you&rsquo;ve ever written the code to run a process and capture its output, but
it&rsquo;s more complicated than it should be. You have to remember to redirect standard
output, and not use ShellExecute (or is it <em>use</em> ShellExecute? I can never remember),
and then handle the events and everything. It&rsquo;s just annoying.</p>
<p>Enter another great NuGet package: <a href="https://www.nuget.org/packages/CliWrap/">CliWrap</a>.
This package wraps all that <code>ProcessInfo</code> and <code>Process.Start</code> business up in a really
well-designed fluent API and takes care of remembering the hard stuff for you.</p>
<p>Here&rsquo;s a call to <code>git ls-files</code> using <code>CliWrap</code>:</p>






<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 1</span><span><span style="color:#f38ba8">private</span> <span style="color:#f38ba8">async</span> Task&lt;HashSet&lt;<span style="color:#f38ba8">string</span>&gt;&gt; ListFileAsync()
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 2</span><span>{
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 3</span><span>    <span style="color:#f38ba8">var</span> <span style="color:#cba6f7">set</span> = <span style="color:#cba6f7">new</span> HashSet&lt;<span style="color:#f38ba8">string</span>&gt;(_stringComparer);
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 4</span><span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 5</span><span>    <span style="color:#f38ba8">var</span> result = <span style="color:#cba6f7">await</span> Cli.Wrap(<span style="color:#a6e3a1">&#34;git&#34;</span>)
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 6</span><span>        .WithArguments(<span style="color:#a6e3a1">&#34;ls-files&#34;</span>)
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 7</span><span>        .WithWorkingDirectory(_workingDirectory)
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 8</span><span>        .WithValidation(CommandResultValidation.None)
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 9</span><span>        .WithStandardOutputPipe(PipeTarget.ToDelegate(line =&gt;
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">10</span><span>        {
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">11</span><span>            line = _fileSystem.Path.Normalize(line);
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">12</span><span>            
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">13</span><span>            <span style="color:#cba6f7">if</span> (line.Contains(Bin) || line.Contains(Obj))
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">14</span><span>            {
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">15</span><span>                <span style="color:#cba6f7">set</span>.Add(Path.Combine(_workingDirectory, line));
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">16</span><span>            }
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">17</span><span>        }))
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">18</span><span>        .ExecuteAsync();
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">19</span><span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">20</span><span>    <span style="color:#cba6f7">if</span> (result.ExitCode != <span style="color:#fab387">0</span>)
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">21</span><span>    {
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">22</span><span>        <span style="color:#cba6f7">set</span>.Clear();
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">23</span><span>    }
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">24</span><span>    <span style="color:#cba6f7">return</span> <span style="color:#cba6f7">set</span>;
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">25</span><span>}</span></span></code></pre></div>
<p>That <code>WithStandardOutputPipe</code> call works with <code>PipeTarget</code> objects: there are
implementations built-in for delegates, streams and <code>StringBuilder</code> and you
can implement your own, too.</p>
<p>Note the <code>.WithValidation(CommandResultValidation.None)</code> line: the default behaviour
for CliWrap is to throw an exception if the command returns a non-zero exit code, and
we&rsquo;re expecting that might happen so we want to avoid an exception. With
<code>CommandValidation.None</code>, if <code>git</code> isn&rsquo;t installed, or the working directory is not a
<code>git</code> repo, then <code>result.ExitCode</code> will be non-zero and we can just return an
empty <code>HashSet&lt;string&gt;</code>.</p>
<h2 id="making-it-a-tool">Making it a tool</h2>
<p>A <code>dotnet</code> tool is just a NuGet package with a bit of extra metadata specified in the
project file:</p>






<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-xml" data-lang="xml"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">1</span><span>  <span style="color:#cba6f7">&lt;PropertyGroup&gt;</span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">2</span><span>    <span style="color:#cba6f7">&lt;PackAsTool&gt;</span>true<span style="color:#cba6f7">&lt;/PackAsTool&gt;</span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">3</span><span>    <span style="color:#cba6f7">&lt;ToolCommandName&gt;</span>nuke-from-orbit<span style="color:#cba6f7">&lt;/ToolCommandName&gt;</span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">4</span><span>    <span style="color:#cba6f7">&lt;PackageId&gt;</span>RendleLabs.NukeFromOrbit<span style="color:#cba6f7">&lt;/PackageId&gt;</span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">5</span><span>  <span style="color:#cba6f7">&lt;/PropertyGroup&gt;</span></span></span></code></pre></div>
<p>The <code>PackAsTool</code> property sets the <code>tool</code> flag in the NuGet package, and the
<code>ToolCommandName</code> is the name used to invoke the command. I&rsquo;d normally go for something
a bit shorter, but I&rsquo;m generally so annoyed by the time I need this one that I want
the satisfaction of typing <code>nuke-from-orbit</code> and smashing the <em>Enter</em> key.</p>
<h2 id="publishing-to-nuget">Publishing to NuGet</h2>
<p>There are a bunch of ways to publish NuGet packages these days, but I wanted to try
GitHub Actions. This involves adding a <em>workflow</em> file to your git repo which is, of
course, YAML. This is the workflow for NukeFromOrbit, in a file at
<code>.github/workflows/dotnet-core.yml</code>:</p>






<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yml" data-lang="yml"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 1</span><span><span style="color:#cba6f7">name</span>: .NET Core
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 2</span><span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 3</span><span><span style="color:#cba6f7">on</span>:
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 4</span><span>  <span style="color:#cba6f7">push</span>:
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 5</span><span>    <span style="color:#cba6f7">branches</span>: [ master ]
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 6</span><span>  <span style="color:#cba6f7">pull_request</span>:
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 7</span><span>    <span style="color:#cba6f7">branches</span>: [ master ]
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 8</span><span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 9</span><span><span style="color:#cba6f7">jobs</span>:
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">10</span><span>  <span style="color:#cba6f7">build</span>:
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">11</span><span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">12</span><span>    <span style="color:#cba6f7">runs-on</span>: windows-latest
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">13</span><span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">14</span><span>    <span style="color:#cba6f7">steps</span>:
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">15</span><span>    - <span style="color:#cba6f7">uses</span>: actions/checkout@v2
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">16</span><span>      <span style="color:#cba6f7">with</span>:
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">17</span><span>        <span style="color:#cba6f7">fetch-depth</span>: <span style="color:#fab387">0</span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">18</span><span>    - <span style="color:#cba6f7">name</span>: Setup .NET Core
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">19</span><span>      <span style="color:#cba6f7">uses</span>: actions/setup-dotnet@v1
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">20</span><span>      <span style="color:#cba6f7">with</span>:
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">21</span><span>        <span style="color:#cba6f7">dotnet-version</span>: <span style="color:#fab387">3.1.301</span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">22</span><span>    - <span style="color:#cba6f7">name</span>: Install dependencies
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">23</span><span>      <span style="color:#cba6f7">run</span>: dotnet restore
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">24</span><span>    - <span style="color:#cba6f7">name</span>: Nerdbank.GitVersioning
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">25</span><span>      <span style="color:#cba6f7">id</span>: nbgv
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">26</span><span>      <span style="color:#cba6f7">uses</span>: dotnet/nbgv@v0.3.1
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">27</span><span>    - <span style="color:#cba6f7">name</span>: Build
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">28</span><span>      <span style="color:#cba6f7">run</span>: dotnet build --configuration Release --no-restore
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">29</span><span>    - <span style="color:#cba6f7">name</span>: Test
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">30</span><span>      <span style="color:#cba6f7">run</span>: dotnet test --configuration Release --no-build --verbosity normal test\NukeFromOrbit.Tests
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">31</span><span>    - <span style="color:#cba6f7">name</span>: Pack
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">32</span><span>      <span style="color:#cba6f7">run</span>: dotnet pack --configuration Release --no-build -p:PackageVersion=${{ steps.nbgv.outputs.SimpleVersion }} --output . src\NukeFromOrbit\NukeFromOrbit.csproj
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">33</span><span>    - <span style="color:#cba6f7">name</span>: Push
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">34</span><span>      <span style="color:#cba6f7">run</span>: dotnet nuget push *.nupkg -s https://api.nuget.org/v3/index.json -k ${{ secrets.NUGET_API_KEY }} --skip-duplicate</span></span></code></pre></div>
<p>This is mostly the default .NET Core workflow template, with a couple of additions.</p>
<h3 id="nerdbankgitversioning">NerdBank.GitVersioning</h3>
<p>This step takes care of assigning version numbers to assemblies, using the &ldquo;height&rdquo; of
the git history (i.e. the number of commits) as the revision number, and taking the major
and minor version numbers from a <code>version.json</code> file in the root of the repo.</p>






<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-json" data-lang="json"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">1</span><span>{
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">2</span><span>  <span style="color:#cba6f7">&#34;$schema&#34;</span>: <span style="color:#a6e3a1">&#34;https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json&#34;</span>,
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">3</span><span>  <span style="color:#cba6f7">&#34;version&#34;</span>: <span style="color:#a6e3a1">&#34;1.0&#34;</span>,
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">4</span><span>  <span style="color:#cba6f7">&#34;assemblyVersion&#34;</span>: {
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">5</span><span>    <span style="color:#cba6f7">&#34;precision&#34;</span>: <span style="color:#a6e3a1">&#34;revision&#34;</span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">6</span><span>  }
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">7</span><span>}</span></span></code></pre></div>
<p>The <code>assemblyVersion</code> property tells NBGV to include the <code>revision</code> in the
<code>AssemblyVersion</code> value; without this the default is just <code>{version}.0.0</code>. I want it
to include the revision because <code>System.CommandLine</code> automatically adds a <code>--version</code>
flag that prints the <code>AssemblyVersion</code> value.</p>
<p>The workflow uses an <em>output variable</em> from the NBGV step to set the package version
in the <code>dotnet pack</code> step.</p>
<p>NerdBank.GitVersioning is in the GitHub Actions Marketplace, pulled in with the
<code>uses: dotnet/nbgv@v0.3.1</code> line.</p>
<h3 id="nuget_api_key">NUGET_API_KEY</h3>
<p>GitHub Actions provides a <em>Secrets</em> store where you can put API keys, passwords and the
like. You can store Secrets at the repo level, or at the organisation level, where you
can specify which repos the secrets are available to.</p>
<h2 id="thats-it">That&rsquo;s it!</h2>
<p>Hopefully this post has shown you how easy it is to publish your own <code>dotnet</code> tool,
from <code>dotnet new console</code> to CI/CD. If you have any simple tools lying around then
why not share them? And if you do, be sure to drop by
<a href="https://github.com/natemcmaster/dotnet-tools">github.com/natemcmaster/dotnet-tools</a>
and submit a PR to let people know about it.</p>
<p>The source code for the tool, including the workflow stuff, is at
<a href="https://github.com/RendleLabs/NukeFromOrbit">github.com/RendleLabs/NukeFromOrbit</a>.
Let me know if you like it, or open an issue or submit a PR if it needs fixing.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Loading old-style projects with Roslyn in .NET Core</title>
      <link>https://rendle.dev/blog/working-with-old-projects/</link>
      <pubDate>Tue, 11 Aug 2020 17:08:08 +0100</pubDate><author>rendle@rendlelabs.com (Rendle)</author>
      <guid>https://rendle.dev/blog/working-with-old-projects/</guid>
      <description>&lt;p&gt;First up, let me just say that &lt;a href=&#34;https://github.com/dotnet/roslyn&#34;&gt;Roslyn&lt;/a&gt;,
the compiler platform for C# and .NET, is amazing and I love it.
I couldn&amp;rsquo;t have built &lt;a href=&#34;https://visualrecode.com&#34;&gt;Visual ReCode&lt;/a&gt; without it.
But I have run into an issue with the &lt;code&gt;MSBuildWorkspace&lt;/code&gt; type that, ostensibly,
lets you load .NET solutions quickly and easily.&lt;/p&gt;
&lt;p&gt;The problem is, when you use &lt;code&gt;MSBuildWorkspace&lt;/code&gt; in a .NET 4.x project, it uses the
old, full-fat .NET version of MSBuild.
But when you use it in a .NET Core 3.1 project, it uses .NET Core&amp;rsquo;s version of MSBuild,
and they&amp;rsquo;re not compatible.
Specifically, because the &lt;code&gt;MSBuildWorkspace&lt;/code&gt; internals fully parse the project file
and resolve all the targets and everything, when .NET Core&amp;rsquo;s MSBuild tries to load
the .NET assemblies used in old projects&amp;rsquo; targets, it all kind of explodes.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>First up, let me just say that <a href="https://github.com/dotnet/roslyn">Roslyn</a>,
the compiler platform for C# and .NET, is amazing and I love it.
I couldn&rsquo;t have built <a href="https://visualrecode.com">Visual ReCode</a> without it.
But I have run into an issue with the <code>MSBuildWorkspace</code> type that, ostensibly,
lets you load .NET solutions quickly and easily.</p>
<p>The problem is, when you use <code>MSBuildWorkspace</code> in a .NET 4.x project, it uses the
old, full-fat .NET version of MSBuild.
But when you use it in a .NET Core 3.1 project, it uses .NET Core&rsquo;s version of MSBuild,
and they&rsquo;re not compatible.
Specifically, because the <code>MSBuildWorkspace</code> internals fully parse the project file
and resolve all the targets and everything, when .NET Core&rsquo;s MSBuild tries to load
the .NET assemblies used in old projects&rsquo; targets, it all kind of explodes.</p>






<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">1</span><span>Msbuild failed when processing the file &#39;D:\ReCode\Samples\Hotel\Hotel\Hotel.csproj&#39; with message: 
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">2</span><span>C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\MSBuild\Current\Bin\Microsoft.Common.CurrentVersion.targets: (1489, 5): 
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">3</span><span>The &#34;AssignProjectConfiguration&#34; task could not be instantiated from 
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">4</span><span>&#34;Microsoft.Build.Tasks.Core, Version=15.1.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a&#34;.
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">5</span><span>Method not found: &#39;Byte[] System.AppDomainSetup.GetConfigurationBytes()&#39;.</span></span></code></pre></div>
<h2 id="why-this-is-annoying-me">Why this is annoying me</h2>
<p>This is annoying me because I love .NET Core and I want to use it for
everything. Right now, the Visual ReCode engine is all in the same solution as the
Visual Studio extension, which means it&rsquo;s written in C# 7.3 for .NET 4.7.2, and
there&rsquo;s a whole world of useful stuff I don&rsquo;t have access to: nullable reference types,
proper <code>Span</code> support, <code>IAsyncEnumerable</code>, and even a bunch of new APIs that I&rsquo;d
forgotten were new to .NET Core like <code>Path.GetFullPath</code>.</p>
<p>So I&rsquo;m extracting the engine from the extension, with the plan to run it as an
out-of-process server, using (of course) gRPC to talk to the VS extension. Doing this
also solves a bunch of other problems like getting out of Visual Studio&rsquo;s <em>weird</em> Task
scheduler, running in a 32-bit process alongside whatever other extensions are installed,
and having to spin up a whole new VS instance to do a little debugging.</p>
<p>But my .NET Core 3.1 gRPC app can&rsquo;t use <code>MSBuildWorkspace</code> to load old-style projects,
like, oh, <em>WCF projects</em>, which ReCode kinda needs to work with.</p>
<h2 id="a-solution">A solution</h2>
<p>There is another <code>Workspace</code> implementation: <code>AdhocWorkspace</code>. This is a much more
tolerant and forgiving workspace, but it takes a little more effort to use. I already
use it extensively in unit tests, where I create solutions, projects and documents
in memory from strings. The tests happily pass when running in .NET Core 3.1, so I
thought, why don&rsquo;t I just create an <code>AdhocWorkspace</code> from an actual solution and
its projects? All I need to do is parse the <code>.sln</code> file and the <code>.csproj</code> files
and build the thing up manually.</p>
<p>It&rsquo;s a well-known fact that the Visual Studio solution file is weird and hard to
parse, but if all you&rsquo;re doing is looking for projects it&rsquo;s actually not that bad.
A Project entry looks like this:</p>






<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">1</span><span>Project(&#34;&#34;{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}&#34;&#34;) = &#34;&#34;Hotel&#34;&#34;, &#34;&#34;Hotel\Hotel.csproj&#34;&#34;, &#34;&#34;{0AB9EB14-38A1-40FD-B093-B756E9679FE5}&#34;&#34;
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">2</span><span>EndProject</span></span></code></pre></div>
<p>Se we can just look for lines that start with <code>Project</code>, skip past the <code>=</code> symbol and grab
the next two quoted values, which are the name of the project and the path relative to the
solution file.</p>
<p>With the path to the <code>csproj</code> file we can open that up and parse it using <code>XDocument</code>.
If the project is the old-style <code>csproj</code>, the top-level <code>&lt;Project&gt;</code> element will have an
<code>xmlns</code> attribute, which we&rsquo;ll need to use to find elements.</p>






<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">1</span><span><span style="color:#f38ba8">var</span> itemGroupName = XName.Get(<span style="color:#a6e3a1">&#34;ItemGroup&#34;</span>, xmlns);
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">2</span><span><span style="color:#f38ba8">var</span> compileName = XName.Get(<span style="color:#a6e3a1">&#34;Compile&#34;</span>, xmlns);</span></span></code></pre></div>
<p>All the C# files (or &ldquo;Documents&rdquo; as Roslyn calls them) will be in <code>&lt;Compile&gt;</code> elements</p>






<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-xml" data-lang="xml"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">1</span><span><span style="color:#cba6f7">&lt;ItemGroup&gt;</span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">2</span><span>  <span style="color:#cba6f7">&lt;Compile</span> <span style="color:#89b4fa">Include=</span><span style="color:#a6e3a1">&#34;&#34;</span><span style="color:#f38ba8">HotelService.svc.cs&#34;&#34;</span><span style="color:#cba6f7">&gt;</span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">3</span><span>    <span style="color:#cba6f7">&lt;DependentUpon&gt;</span>HotelService.svc<span style="color:#cba6f7">&lt;/DependentUpon&gt;</span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">4</span><span>  <span style="color:#cba6f7">&lt;/Compile&gt;</span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">5</span><span>  <span style="color:#cba6f7">&lt;Compile</span> <span style="color:#89b4fa">Include=</span><span style="color:#a6e3a1">&#34;&#34;</span><span style="color:#f38ba8">IHotelService.cs&#34;&#34;</span> <span style="color:#cba6f7">/&gt;</span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">6</span><span>  <span style="color:#cba6f7">&lt;Compile</span> <span style="color:#89b4fa">Include=</span><span style="color:#a6e3a1">&#34;&#34;</span><span style="color:#f38ba8">Properties\AssemblyInfo.cs&#34;&#34;</span> <span style="color:#cba6f7">/&gt;</span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">7</span><span><span style="color:#cba6f7">&lt;/ItemGroup&gt;</span></span></span></code></pre></div>
<p>We also need to handle references, both to other projects in the solution, and to
.NET assemblies which might be in a <code>packages</code> folder, the Global Assembly Cache, or
just stored somewhere on disk. Project references are in <code>&lt;ProjectReference&gt;</code> elements,
and all the others, including to NuGet packages, are in <code>&lt;Reference&gt;</code> elements, with a
<code>&lt;HintPath&gt;</code> element if they&rsquo;re not in the GAC.</p>
<p>Loading these into an <code>AdhocWorkspace</code> is done using Roslyn&rsquo;s <code>*Info</code> classes, which are
like builders for Solutions, Projects and Documents. For each document, we create a
<code>DocumentInfo</code> object:</p>






<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">1</span><span><span style="color:#f38ba8">var</span> di = DocumentInfo.Create(DocumentId.CreateNewId(projectId), documentSource.Name,
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">2</span><span>    filePath: documentSource.Path,
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">3</span><span>    loader: <span style="color:#cba6f7">new</span> FileTextLoader(documentSource.Path, <span style="color:#fab387">null</span>));</span></span></code></pre></div>
<p>With a list of all the documents, as well as the Project and Assembly references we
got from the project file, we can create the <code>ProjectInfo</code> object:</p>






<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">1</span><span><span style="color:#cba6f7">return</span> ProjectInfo.Create(projectId, VersionStamp.Default, projectSource.Name, projectSource.Name, LanguageNames.CSharp,
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">2</span><span>    projectSource.Path,
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">3</span><span>    projectReferences: projectReferences,
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">4</span><span>    metadataReferences: metadataReferences,
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">5</span><span>    documents: documents);</span></span></code></pre></div>
<p>And with all our <code>ProjectInfo</code> objects we can create a <code>SolutionInfo</code> and add it to our
<code>AdhocWorkspace</code>:</p>






<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">1</span><span><span style="color:#f38ba8">var</span> workspace = <span style="color:#cba6f7">new</span> AdhocWorkspace();
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">2</span><span><span style="color:#f38ba8">var</span> solutionInfo = SolutionInfo.Create(SolutionId.CreateNewId(),
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">3</span><span>                    VersionStamp.Default, solutionFilePath, projectInfos);
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">4</span><span>workspace.AddSolution(solutionInfo);</span></span></code></pre></div>
<p>This will turn all those <code>Info</code>s into actual Roslyn projects and documents that we can poke
around in using <code>SyntaxNode</code>s and <code>Symbol</code>s in our .NET Core 3.1 application.</p>
<h3 id="new-sdk-style-projects">New &ldquo;SDK-style&rdquo; Projects</h3>
<p>Some projects in the solution might be the new SDK-style that came out of .NET Core but can
be used for .NET 4.x projects as well. These are much simpler than the old-style projects,
but actually harder to work with in this instance because all the default references
(e.g. mscorlib, System.Runtime, etc) are inferred from the SDK. In this instance you can
actually use <code>MSBuildWorkspace</code> to load the project up and then just grab the <code>Document</code>,
<code>ProjectReference</code> and <code>MetadataReference</code> info and copy it into the <code>AdhocWorkspace</code> so
everything works together.</p>
<h2 id="its-on-nuget">It&rsquo;s on NuGet</h2>
<p>I&rsquo;ve wrapped all this up in a NuGet package,
<a href="https://www.nuget.org/packages/RendleLabs.LegacyWorkspaceLoader/">RendleLabs.LegacyWorkspaceLoader</a>,
and the source code is at
<a href="https://github.com/RendleLabs/LegacyWorkspaceLoader">github.com/RendleLabs/LegacyWorkspaceLoader</a>.</p>
<p>In its current state, it works for my use case, but I&rsquo;m sure there are edge cases that it
doesn&rsquo;t catch. If I run into any while using it in <a href="https://visualrecode.com">Visual ReCode</a>
I&rsquo;ll fix them up and update the package, but if you take it for a spin and run into problems,
please create an Issue on the GitHub repo or send a pull request.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Using Channel Like IAsyncEnumerable</title>
      <link>https://rendle.dev/blog/using-channel-like-iasyncenumerable/</link>
      <pubDate>Wed, 29 Jul 2020 12:13:25 +0100</pubDate><author>rendle@rendlelabs.com (Rendle)</author>
      <guid>https://rendle.dev/blog/using-channel-like-iasyncenumerable/</guid>
      <description>&lt;p&gt;Ironically, as I am creating a Visual Studio extension that migrates .NET 4.x code to .NET Core 3.1, I find myself mainly coding with .NET 4.7.2 and C# 7.3. While it is technically possible to use a subset of C# 8 features outside of .NET Core 3.x, the VS SDK is a difficult beast and introducing it to new things often makes it unhappy. I&amp;rsquo;ve lost an afternoon tracking down a reference to &lt;code&gt;System.Collections.Immutable&lt;/code&gt; that was upsetting it.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>Ironically, as I am creating a Visual Studio extension that migrates .NET 4.x code to .NET Core 3.1, I find myself mainly coding with .NET 4.7.2 and C# 7.3. While it is technically possible to use a subset of C# 8 features outside of .NET Core 3.x, the VS SDK is a difficult beast and introducing it to new things often makes it unhappy. I&rsquo;ve lost an afternoon tracking down a reference to <code>System.Collections.Immutable</code> that was upsetting it.</p>
<p>One of the big things from C# 8 that I&rsquo;m missing is <code>IAsyncEnumerable&lt;T&gt;</code> and <code>await foreach</code>. These would be incredibly useful for getting a stream of values from a background thread and updating the UI. But I can&rsquo;t use them, so instead I turned to <code>System.Threading.Channels</code>, which scratched a similar itch before C# 8 came along.</p>
<p>If you&rsquo;re not familiar, <code>System.Threading.Channels</code> is a new-ish feature of .NET designed for inter-thread messaging. It&rsquo;s a really nice abstraction compared to things like <code>BlockingCollection</code>.</p>
<p><a href="https://twitter.com/stevejgordon">Steve Gordon</a> wrote a great <a href="https://www.stevejgordon.co.uk/an-introduction-to-system-threading-channels">Introduction to Channels</a> article that you should read if you want to know more about them.</p>
<p>The fun thing I&rsquo;ve found is that you can treat a <code>ChannelReader&lt;T&gt;</code> almost exactly like an <code>IAsyncEnumerable&lt;T&gt;</code>, and write useful extension methods over it. For example, while writing unit tests for a method that returns a <code>ChannelReader&lt;Thing&gt;</code>, I wanted to collate all the values it returned into a <code>List&lt;Thing&gt;</code>. The code was so obviously generic that I refactored it into this extension method:</p>






<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 1</span><span><span style="color:#f38ba8">internal</span> <span style="color:#f38ba8">static</span> <span style="color:#cba6f7">class</span> <span style="color:#f9e2af">ChannelReaderExtensions</span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 2</span><span>{
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 3</span><span>    <span style="color:#f38ba8">public</span> <span style="color:#f38ba8">static</span> <span style="color:#f38ba8">async</span> Task&lt;List&lt;T&gt;&gt; ToListAsync&lt;T&gt;(<span style="color:#cba6f7">this</span> ChannelReader&lt;T&gt; channelReader, CancellationToken cancellationToken = <span style="color:#cba6f7">default</span>)
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 4</span><span>    {
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 5</span><span>        <span style="color:#f38ba8">var</span> list = <span style="color:#cba6f7">new</span> List&lt;T&gt;();
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 6</span><span>        <span style="color:#cba6f7">while</span> (<span style="color:#cba6f7">await</span> channelReader.WaitToReadAsync(cancellationToken))
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 7</span><span>        {
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 8</span><span>            <span style="color:#cba6f7">while</span> (channelReader.TryRead(<span style="color:#cba6f7">out</span> <span style="color:#f38ba8">var</span> item))
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 9</span><span>            {
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">10</span><span>                list.Add(item);
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">11</span><span>            }
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">12</span><span>        }
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">13</span><span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">14</span><span>        <span style="color:#cba6f7">return</span> list;
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">15</span><span>    }
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">16</span><span>}</span></span></code></pre></div>
<p>You could pretty much write any of the <code>IEnumerable&lt;T&gt;</code> extension methods over <code>ChannelReader&lt;T&gt;</code>, although you&rsquo;d be creating new <code>Channel</code>s and piling things on thread pools a lot, so, you know&hellip; <em>caveat developer</em>. But anyway, here&rsquo;s a <code>Where</code> extension method as an example.</p>






<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 1</span><span><span style="color:#f38ba8">public</span> <span style="color:#f38ba8">static</span> ChannelReader&lt;T&gt; Where&lt;T&gt;(<span style="color:#cba6f7">this</span> ChannelReader&lt;T&gt; channelReader,
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 2</span><span>    Func&lt;T, <span style="color:#f38ba8">bool</span>&gt; predicate, CancellationToken token)
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 3</span><span>{
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 4</span><span>    <span style="color:#f38ba8">var</span> output = Channel.CreateUnbounded&lt;T&gt;(<span style="color:#cba6f7">new</span> UnboundedChannelOptions {SingleWriter = <span style="color:#fab387">true</span>});
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 5</span><span>    <span style="color:#f38ba8">var</span> writer = output.Writer;
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 6</span><span>    Task.Run(<span style="color:#f38ba8">async</span> () =&gt;
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 7</span><span>    {
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 8</span><span>        <span style="color:#cba6f7">while</span> (<span style="color:#cba6f7">await</span> channelReader.WaitToReadAsync(token))
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 9</span><span>        {
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">10</span><span>            <span style="color:#cba6f7">while</span> (channelReader.TryRead(<span style="color:#cba6f7">out</span> <span style="color:#f38ba8">var</span> item))
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">11</span><span>            {
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">12</span><span>                <span style="color:#cba6f7">if</span> (predicate(item))
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">13</span><span>                {
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">14</span><span>                    <span style="color:#cba6f7">if</span> (!writer.TryWrite(item))
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">15</span><span>                    {
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">16</span><span>                        <span style="color:#cba6f7">await</span> writer.WriteAsync(item, token);
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">17</span><span>                    }
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">18</span><span>                }
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">19</span><span>            }
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">20</span><span>        }
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">21</span><span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">22</span><span>        writer.TryComplete();
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">23</span><span>    });
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">24</span><span>    <span style="color:#cba6f7">return</span> output.Reader;
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">25</span><span>}</span></span></code></pre></div>
]]></content:encoded>
    </item>
  </channel>
</rss>
