<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
    <channel>
        <title><![CDATA[Damir's Corner]]></title>
        <description><![CDATA[Notes from Daily Encounters with Technology]]></description>
        <link>https://www.damirscorner.com</link>
        <generator>RSS for Node</generator>
        <lastBuildDate>Fri, 11 Sep 2026 06:16:10 GMT</lastBuildDate>
        <atom:link href="https://www.damirscorner.com/blog/posts/rss.xml" rel="self" type="application/rss+xml"/>
        <author><![CDATA[Damir Arh]]></author>
        <pubDate>Fri, 11 Sep 2026 06:14:14 GMT</pubDate>
        <item>
            <title><![CDATA[Indexer extension members in C# 15]]></title>
            <description><![CDATA[<p>Last year, <a href="https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/extension-methods">extension members</a> were the biggest new feature of C# 14. I wrote about them in detail <a href="20250829-ExtensionMembersInCs14.html">in</a> <a href="20250905-GenericExtensionMembersInCs14.html">multiple</a> <a href="20250912-DisambiguationOfExtensionMembersInCs14.html">blogposts</a>. This year, <a href="https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-15.0/extension-indexers">extension indexers</a> are being added to C# 15 which didn&#39;t make it into C# 14.</p>
<p>Extension methods have been added to C# as a part of LINQ a long time ago. In essence, they were static methods which could be called as if they were members of the type of their first parameter:</p>
<pre class="highlight"><code class="hljs csharp"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">string</span>? FirstCharToUpper(<span class="hljs-keyword">this</span> <span class="hljs-keyword">string</span>? receiver)
{
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">string</span>.IsNullOrEmpty(receiver)
        ? receiver
        : <span class="hljs-keyword">string</span>.Concat(receiver[.<span class="hljs-number">.1</span>].ToUpper(), receiver.AsSpan(<span class="hljs-number">1</span>));
}
</code></pre>
<p>They had to be placed in a static class and the first parameter had to be introduced with <code>this</code> keyword. Such a method could be called as a member method of its first parameter which therefore didn&#39;t have to be passed in as a parameter:</p>
<pre class="highlight"><code class="hljs csharp"><span class="hljs-string">"me"</span>.FirstCharToUpper()
</code></pre>
<p>The extension members feature of C# 14 added support for adding other member types as extensions. To make this possible, the syntax for declaring them had to be different from the existing extension method syntax. Their syntax is more similar to regular type members, but they have to be inside an <code>extension</code> block, in a static class. The old syntax is still supported for extension instance methods, but a new one was added to match the syntax of other extension member types:</p>
<pre class="highlight"><code class="hljs csharp">extension(<span class="hljs-keyword">string</span>? receiver)
{
    <span class="hljs-keyword">public</span> <span class="hljs-keyword">string</span>? FirstCharToUpper()
    {
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">string</span>.IsNullOrEmpty(receiver)
            ? receiver
            : <span class="hljs-keyword">string</span>.Concat(receiver[.<span class="hljs-number">.1</span>].ToUpper(), receiver.AsSpan(<span class="hljs-number">1</span>));
    }
}
</code></pre>
<p>Other supported member types were:</p>
<ul>
<li>instance properties:<pre class="highlight"><code class="hljs csharp">extension(<span class="hljs-keyword">string</span>? receiver)
{
    <span class="hljs-keyword">public</span> <span class="hljs-keyword">bool</span> IsEmptyField =&gt;
        <span class="hljs-keyword">string</span>.IsNullOrEmpty(receiver) || receiver == <span class="hljs-string">"N/A"</span>;
}
</code></pre>
</li>
<li><p>static methods:</p>
<pre class="highlight"><code class="hljs csharp">extension(<span class="hljs-keyword">string</span>?)
{
    <span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">string</span>? Create(<span class="hljs-keyword">string</span>? pattern, <span class="hljs-keyword">int</span> count)
    {
        <span class="hljs-keyword">if</span> (pattern == <span class="hljs-keyword">null</span>)
        {
            <span class="hljs-keyword">return</span> <span class="hljs-keyword">null</span>;
        }

        <span class="hljs-keyword">if</span> (pattern.Length == <span class="hljs-number">0</span> || count &lt;= <span class="hljs-number">0</span>)
        {
            <span class="hljs-keyword">return</span> <span class="hljs-keyword">string</span>.Empty;
        }

        <span class="hljs-keyword">var</span> builder = <span class="hljs-keyword">new</span> StringBuilder(pattern.Length * count);
        <span class="hljs-keyword">for</span> (<span class="hljs-keyword">var</span> i = <span class="hljs-number">0</span>; i &lt; count; i++)
        {
            builder.Append(pattern);
        }

        <span class="hljs-keyword">return</span> builder.ToString();
    }
}
</code></pre>
</li>
<li><p>static properties:</p>
<pre class="highlight"><code class="hljs csharp">extension(<span class="hljs-keyword">string</span>?)
{
    <span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">string</span> NotAvailable =&gt; <span class="hljs-string">"N/A"</span>;
}
</code></pre>
</li>
<li>operators:<pre class="highlight"><code class="hljs csharp">extension(<span class="hljs-keyword">string</span>?)
{
    <span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">string</span>? <span class="hljs-keyword">operator</span> *(<span class="hljs-keyword">string</span>? pattern, <span class="hljs-keyword">int</span> count)
    {
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">string</span>.Create(pattern, count);
    }
}
</code></pre>
</li>
</ul>
<p>In addition to all these extension member types, C# 15 now also supports extension indexers. The syntax follows the pattern of all other extension members:</p>
<pre class="highlight"><code class="hljs csharp">extension(IEnumerable&lt;<span class="hljs-keyword">int</span>&gt; sequence)
{
    <span class="hljs-keyword">public</span> <span class="hljs-keyword">int</span> <span class="hljs-keyword">this</span>[<span class="hljs-keyword">int</span> index] =&gt; sequence.ElementAt(index);
}
</code></pre>
<p>Just like other extension member types, extension indexers are only considered by the compiler when there are no matching actual member indexers. Even <a href="https://learn.microsoft.com/en-us/dotnet/csharp/tutorials/ranges-indexes#type-support-for-indices-and-ranges">implicit <code>Index</code> and <code>Range</code> indexers</a> have precedence over extension indexers with the same type. E.g., if a type has an <code>int</code> indexer and a <code>Length</code> or <code>Count</code> property, then the <code>Index</code> indexer is implicitly supported and an extension <code>Index</code> indexer will be ignored even if you implement it.</p>
<p>In addition to all that, extension indexers for arrays and strings will be ignored altogether as documented at the very bottom in the collapsed section of <a href="https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-15.0/extension-indexers#open-issues">the feature specification</a>:</p>
<blockquote>
<p>The current spec and baseline rules for <a href="https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/expressions.md#128122-array-access">array access</a> and <a href="https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/expressions.md#128123-string-access">string access</a> mean that extension indexers don&#39;t work on arrays or strings.<br>Yet the declaration of such extension indexers is permitted.</p>
</blockquote>
<p>Extension indexers are currently a preview feature and might still change. To use them in <a href="https://devblogs.microsoft.com/dotnet/dotnet-11-preview-7/">.NET 11 preview 7</a>, you must set the language version to preview in your project:</p>
<pre class="highlight"><code class="hljs xml"><span class="hljs-tag">&lt;<span class="hljs-title">PropertyGroup</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-title">LangVersion</span>&gt;</span>preview<span class="hljs-tag">&lt;/<span class="hljs-title">LangVersion</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-title">PropertyGroup</span>&gt;</span>
</code></pre>
<p>A sample project with all supported extension member types (the ones from C# 14 and the indexers newly added in C# 15) is available in my <a href="https://github.com/DamirsCorner/20260911-cs15-extension-indexers">GitHub repository</a>. Feel free to clone it and try out the feature yourself.</p>
<p>With the addition of extension indexers in C# 15, the originally planned scope of extension members from C# 14 has been completed. The only remaining member type without extension support are conversion operators which aren&#39;t even planned for now as stated in <a href="https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-14.0/extension-operators">the feature specification</a>:</p>
<blockquote>
<p>Does not cover user-defined implicit and explicit conversion operators, which are not yet designed or planned.</p>
</blockquote>
]]></description>
            <link>https://www.damirscorner.com/blog/posts/20260911-IndexerExtensionMembersInCs15.html</link>
            <guid isPermaLink="true">https://www.damirscorner.com/blog/posts/20260911-IndexerExtensionMembersInCs15.html</guid>
            <dc:creator><![CDATA[Damir Arh]]></dc:creator>
            <pubDate>Fri, 11 Sep 2026 00:00:00 GMT</pubDate>
        </item>
        <item>
            <title><![CDATA[Collection expressions with arguments in C# 15]]></title>
            <description><![CDATA[<p><a href="https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/collection-expressions">Collection expressions</a> were introduced in C# 12. I wrote <a href="20231020-BenefitsOfCollectionExpressions.html">a blog post</a> about them at that time. In C# 15, the syntax is being extended with the ability to provide additional arguments to the underlying collection constructor or builder.</p>
<p>Let&#39;s start with a quick reminder, what collection expressions are. Since the early days of C#, collection types could be initialized using the collection initializer syntax. For example, an array could be initialized with:</p>
<pre class="highlight"><code class="hljs csharp"><span class="hljs-keyword">var</span> oldSyntax = <span class="hljs-keyword">new</span>[] { <span class="hljs-number">1</span>, <span class="hljs-number">2</span>, <span class="hljs-number">3</span> };
</code></pre>
<p>Collection expressions provide an alternative syntax (notice how the variable has to be explicitly typed so that the compiler knows which type of collection to instantiate):</p>
<pre class="highlight"><code class="hljs csharp"><span class="hljs-keyword">int</span>[] newSyntax = [<span class="hljs-number">1</span>, <span class="hljs-number">2</span>, <span class="hljs-number">3</span>];
</code></pre>
<p>The new syntax also added support for the spread element to initialize a new collection as a concatenation of elements from multiple existing collections, inspired by the Javascript <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax">spread syntax</a>:</p>
<pre class="highlight"><code class="hljs csharp"><span class="hljs-keyword">int</span>[] array = [<span class="hljs-number">1</span>, <span class="hljs-number">2</span>];
Span&lt;<span class="hljs-keyword">int</span>&gt; span = [<span class="hljs-number">3</span>, <span class="hljs-number">4</span>];
List&lt;<span class="hljs-keyword">int</span>&gt; list = [<span class="hljs-number">5</span>, <span class="hljs-number">6</span>];
<span class="hljs-keyword">int</span>[] finalArray = [.. array, .. span, .. list];
</code></pre>
<p>However, the new collection expression wasn&#39;t a full replacement for all use cases. Some collection constructors have parameters which tweak how the collection gets created. For example, a list can be created with a larger initial size, which is useful if we know in advance that more items will be added to it:</p>
<pre class="highlight"><code class="hljs csharp"><span class="hljs-keyword">var</span> oldSyntax = <span class="hljs-keyword">new</span> List&lt;<span class="hljs-keyword">int</span>&gt;(<span class="hljs-number">10</span>) { <span class="hljs-number">1</span>, <span class="hljs-number">2</span>, <span class="hljs-number">3</span> };
</code></pre>
<p>Before C# 15, the same final result couldn&#39;t be achieved using collection expression syntax. The addition of so-called <a href="https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/collection-expressions#collection-expression-arguments">collection expression arguments</a> in C# 15 makes it possible to specify values for the collection constructor parameters. The following syntax is equivalent to the one above:</p>
<pre class="highlight"><code class="hljs csharp">List&lt;<span class="hljs-keyword">int</span>&gt; newSyntax = [with(<span class="hljs-number">10</span>), <span class="hljs-number">1</span>, <span class="hljs-number">2</span>, <span class="hljs-number">3</span>];
</code></pre>
<p>The constructor parameters are specified using the <code>with</code> keyword which has to be the first element in the collection expression. Its parameters must match the parameters of a constructor of the collection type being initialized.</p>
<p>Collection initializers can&#39;t be used with all collections. <a href="https://learn.microsoft.com/en-us/dotnet/api/system.collections.immutable">Immutable collections</a> are such an example. The closest alternative without using collection expressions are the builder methods with <code>params</code> arguments. This means that the following call invokes the <code>Create&lt;string&gt;(params ReadOnlySpan&lt;string&gt; items)</code> method (and called <code>Create&lt;string&gt;(params string[] items)</code> before <a href="https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/classes#15624-parameter-collections">parameter collections</a> from C# 13 added <code>params</code> support to other types of collections):</p>
<pre class="highlight"><code class="hljs csharp"><span class="hljs-keyword">var</span> oldSyntax = ImmutableHashSet.Create(<span class="hljs-string">"a"</span>, <span class="hljs-string">"b"</span>, <span class="hljs-string">"c"</span>);
</code></pre>
<p>Collection expression can invoke the same method using the syntax identical to other types of collections which support collection initializers:</p>
<pre class="highlight"><code class="hljs csharp">ImmutableHashSet&lt;<span class="hljs-keyword">string</span>&gt; newSyntax = [<span class="hljs-string">"a"</span>, <span class="hljs-string">"b"</span>, <span class="hljs-string">"c"</span>];
</code></pre>
<p>With the addition of collection expression arguments in C# 15, collection builders with other arguments in addition to the span or array for specifying the elements to be added to the collection can now supported by the collection expression syntax. For example:</p>
<pre class="highlight"><code class="hljs csharp">ImmutableHashSet&lt;<span class="hljs-keyword">string</span>&gt; newSyntax =
[
  with(StringComparer.OrdinalIgnoreCase),
  <span class="hljs-string">"a"</span>,
  <span class="hljs-string">"A"</span>
];
</code></pre>
<p>This new syntax is equivalent to the old one without collection expressions:</p>
<pre class="highlight"><code class="hljs csharp"><span class="hljs-keyword">var</span> oldSyntax = ImmutableHashSet.Create(StringComparer.OrdinalIgnoreCase, <span class="hljs-string">"a"</span>, <span class="hljs-string">"A"</span>);
</code></pre>
<p>As you can deduce from the given example, in the case of builder methods the collection expression arguments map to the builder method parameters placed before the span <code>items</code> parameter.</p>
<p>Just like all other C# 15 features, collection expression arguments are still in preview and might change before the November release. To use them in <a href="https://devblogs.microsoft.com/dotnet/dotnet-11-preview-7/">.NET 11 preview 7</a> you have to set the language version to preview:</p>
<pre class="highlight"><code class="hljs xml"><span class="hljs-tag">&lt;<span class="hljs-title">PropertyGroup</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-title">LangVersion</span>&gt;</span>preview<span class="hljs-tag">&lt;/<span class="hljs-title">LangVersion</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-title">PropertyGroup</span>&gt;</span>
</code></pre>
<p>I created a sample project with all the code from this blog post and pushed it to <a href="https://github.com/DamirsCorner/20260904-cs15-collection-expression-arguments">my GitHub repository</a>. It&#39;s an easy way to try out this feature yourself.</p>
<p>Collection expression arguments are in essence only syntactic sugar. But so were collection expressions themselves and I still see them used quite regularly nowadays. WIth this new addition, collection expressions can now be used in a few more cases where they couldn&#39;t be before.</p>
]]></description>
            <link>https://www.damirscorner.com/blog/posts/20260904-CollectionExpressionsWithArgumentsInCs15.html</link>
            <guid isPermaLink="true">https://www.damirscorner.com/blog/posts/20260904-CollectionExpressionsWithArgumentsInCs15.html</guid>
            <dc:creator><![CDATA[Damir Arh]]></dc:creator>
            <pubDate>Fri, 04 Sep 2026 00:00:00 GMT</pubDate>
        </item>
        <item>
            <title><![CDATA[Custom union types in C# 15]]></title>
            <description><![CDATA[<p>In <a href="20260821-UnionTypesInCs15.html">the previous blog post</a> I explored what union types bring to C#. But what does the compiler do under the hood? And do I need to know about that to fully take advantage of union types?</p>
<p>Each union type implements the <code>IUnion</code> interface with a single <code>Value</code> property:</p>
<pre class="highlight"><code class="hljs csharp"><span class="hljs-keyword">public</span> <span class="hljs-keyword">interface</span> <span class="hljs-title">IUnion</span>
{
    <span class="hljs-keyword">object</span>? Value { <span class="hljs-keyword">get</span>; }
}
</code></pre>
<p>To initialize the <code>Value</code> property, a constructor is generated for each case type, e.g.:</p>
<pre class="highlight"><code class="hljs csharp"><span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">Shape</span><span class="hljs-params">(Square square)</span>
</span>{
    Value = square;
}
</code></pre>
<p>This constructor is used by the implicit conversion operator which allows us to directly assign an instance of any case type to the union type:</p>
<pre class="highlight"><code class="hljs csharp">Shape shape = <span class="hljs-keyword">new</span> Square(<span class="hljs-number">2</span>);
</code></pre>
<p>Since the union type is just a regular type, we can add custom members to it:</p>
<pre class="highlight"><code class="hljs csharp"><span class="hljs-function"><span class="hljs-keyword">public</span> union <span class="hljs-title">Shape</span><span class="hljs-params">(Square, Circle)</span>
</span>{
    <span class="hljs-keyword">public</span> <span class="hljs-keyword">double</span> Area =&gt;
        <span class="hljs-keyword">this</span> <span class="hljs-keyword">switch</span>
        {
            Square square =&gt; Math.Pow(square.Length, <span class="hljs-number">2</span>),
            Circle circle =&gt; Math.PI * Math.Pow(circle.Radius, <span class="hljs-number">2</span>)
        };
}
</code></pre>
<p>By following the requirements for a union type we can even implement our own custom union type which supports exhaustive <code>switch</code> expressions just like the autogenerated one. It has to:</p>
<ul>
<li>implement the <code>IUnion</code> interface</li>
<li>have a constructor for each case type with its instance as the only parameter</li>
<li>return that case type instance via the <code>Value</code> property</li>
<li>be annotated with the <code>[Union]</code> attribute</li>
</ul>
<pre class="highlight"><code class="hljs csharp">[Union]
<span class="hljs-keyword">public</span> <span class="hljs-keyword">struct</span> CustomShape : IUnion
{
    <span class="hljs-keyword">public</span> <span class="hljs-keyword">object</span>? Value { <span class="hljs-keyword">get</span>; }

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">CustomShape</span><span class="hljs-params">(Square square)</span>
    </span>{
        Value = square;
    }

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">CustomShape</span><span class="hljs-params">(Circle circle)</span>
    </span>{
        Value = circle;
    }

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">implicit</span> <span class="hljs-keyword">operator</span> <span class="hljs-title">CustomShape</span><span class="hljs-params">(Square square)</span> </span>=&gt; <span class="hljs-keyword">new</span>(square);

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">implicit</span> <span class="hljs-keyword">operator</span> <span class="hljs-title">CustomShape</span><span class="hljs-params">(Circle circle)</span> </span>=&gt; <span class="hljs-keyword">new</span>(circle);
}
</code></pre>
<p>The implicit conversion operators are optional. If we don&#39;t implement them, the compiler will generate them for us.</p>
<p>But why would we even want to implement our own custom union type when the compiler can do it for us? It could be for performance reasons. The compiler-generated union type always stores the case type instance as an <code>object</code> just like the struct above. If our case types are value types, this means that they are boxed every time they are stored in the union type and unboxed every time they are read from it. We can avoid this with a custom union type:</p>
<pre class="highlight"><code class="hljs csharp">[Union]
<span class="hljs-keyword">public</span> <span class="hljs-keyword">readonly</span> <span class="hljs-keyword">struct</span> IntOrDouble : IUnion
{
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">enum</span> ValueType : <span class="hljs-keyword">byte</span>
    {
        None,
        Int,
        Double,
    }

    <span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> <span class="hljs-keyword">int</span> _intValue;
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> <span class="hljs-keyword">double</span> _doubleValue;
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> ValueType _type = ValueType.None;

    <span class="hljs-keyword">public</span> <span class="hljs-keyword">object</span>? Value =&gt;
        _type <span class="hljs-keyword">switch</span>
        {
            ValueType.Int =&gt; _intValue,
            ValueType.Double =&gt; _doubleValue,
            _ =&gt; <span class="hljs-keyword">null</span>,
        };

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">IntOrDouble</span><span class="hljs-params">(<span class="hljs-keyword">int</span> <span class="hljs-keyword">value</span>)</span>
    </span>{
        _type = ValueType.Int;
        _intValue = <span class="hljs-keyword">value</span>;
    }

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">IntOrDouble</span><span class="hljs-params">(<span class="hljs-keyword">double</span> <span class="hljs-keyword">value</span>)</span>
    </span>{
        _type = ValueType.Double;
        _doubleValue = <span class="hljs-keyword">value</span>;
    }

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">bool</span> <span class="hljs-title">TryGetValue</span><span class="hljs-params">(<span class="hljs-keyword">out</span> <span class="hljs-keyword">int</span> <span class="hljs-keyword">value</span>)</span>
    </span>{
        <span class="hljs-keyword">value</span> = _intValue;
        <span class="hljs-keyword">return</span> _type == ValueType.Int;
    }

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">bool</span> <span class="hljs-title">TryGetValue</span><span class="hljs-params">(<span class="hljs-keyword">out</span> <span class="hljs-keyword">double</span> <span class="hljs-keyword">value</span>)</span>
    </span>{
        <span class="hljs-keyword">value</span> = _doubleValue;
        <span class="hljs-keyword">return</span> _type == ValueType.Double;
    }
}
</code></pre>
<p>Notice the two <code>TryGetValue</code> methods in this custom union type. When they are present, the compiler uses them in favor of the <code>Value</code> property when the union type is used with type matching, e.g.,</p>
<pre class="highlight"><code class="hljs csharp"><span class="hljs-keyword">if</span> (union <span class="hljs-keyword">is</span> <span class="hljs-keyword">int</span> <span class="hljs-keyword">value</span>)
{
    <span class="hljs-comment">// ...</span>
}
</code></pre>
<p>These methods make sure that boxing and unboxing can be completely avoided. Of course, if the <code>Value</code> property is accessed directly, the value will still be boxed, as there is no other way to return a value type as an <code>object</code>.</p>
<p>As already mentioned in the previous blog post, union types are still in preview and can change before the final release, but they are already available to try out in <a href="https://devblogs.microsoft.com/dotnet/dotnet-11-preview-7/">.NET 11 preview 7</a>. You have to set the language version to preview in your project for them to work:</p>
<pre class="highlight"><code class="hljs xml"><span class="hljs-tag">&lt;<span class="hljs-title">PropertyGroup</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-title">LangVersion</span>&gt;</span>preview<span class="hljs-tag">&lt;/<span class="hljs-title">LangVersion</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-title">PropertyGroup</span>&gt;</span>
</code></pre>
<p>You can find a sample project with all the code from this blog post and more in <a href="https://github.com/DamirsCorner/20260828-cs15-custom-union-types">my GitHub repository</a>. Feel free to clone it and play around with it.</p>
<p>Although you can use union types as soon as you understand their basic syntax, it&#39;s interesting to learn what the compiler does for you. And while it&#39;s unlikely that you&#39;ll need to implement your own custom union type, it&#39;s good to know that it&#39;s possible.</p>
]]></description>
            <link>https://www.damirscorner.com/blog/posts/20260828-CustomUnionTypesInCs15.html</link>
            <guid isPermaLink="true">https://www.damirscorner.com/blog/posts/20260828-CustomUnionTypesInCs15.html</guid>
            <dc:creator><![CDATA[Damir Arh]]></dc:creator>
            <pubDate>Fri, 28 Aug 2026 00:00:00 GMT</pubDate>
        </item>
        <item>
            <title><![CDATA[Union types in C# 15]]></title>
            <description><![CDATA[<p>Continuing with the topic of <code>switch</code> expression exhaustiveness in C# 15, I chose <a href="https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-15#union-types">union types</a> as the next new language feature to explore.</p>
<p>Before C# 15, there were several ways to return multiple different types from a method or store them in a single variable. You can use the type of the common ancestor of your types. This gives you two options for your types</p>
<ol>
<li><p>They have to implement the same interface. If the only purpose of the interface is to provide a common ancestry (i.e., it doesn&#39;t define any members to implement), such an interface is called a marker interface:</p>
<pre class="highlight"><code class="hljs csharp"><span class="hljs-keyword">public</span> <span class="hljs-keyword">interface</span> <span class="hljs-title">IShape</span> { }

<span class="hljs-function"><span class="hljs-keyword">public</span> record <span class="hljs-title">Square</span><span class="hljs-params">(<span class="hljs-keyword">double</span> Length)</span> : IShape</span>;

<span class="hljs-function"><span class="hljs-keyword">public</span> record <span class="hljs-title">Circle</span><span class="hljs-params">(<span class="hljs-keyword">double</span> Radius)</span> : IShape</span>;
</code></pre>
</li>
<li><p>They have to derive from the same base class (usually abstract if it&#39;s only used to provide common ancestry):</p>
<pre class="highlight"><code class="hljs csharp"><span class="hljs-keyword">public</span> <span class="hljs-keyword">abstract</span> record Shape { }

<span class="hljs-function"><span class="hljs-keyword">public</span> record <span class="hljs-title">Square</span><span class="hljs-params">(<span class="hljs-keyword">double</span> Length)</span> : Shape</span>;

<span class="hljs-function"><span class="hljs-keyword">public</span> record <span class="hljs-title">Circle</span><span class="hljs-params">(<span class="hljs-keyword">double</span> Radius)</span> : Shape</span>;
</code></pre>
</li>
</ol>
<p>In both cases, a catch-all <a href="https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/patterns#discard-pattern">discard pattern</a> has to be added to <a href="https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/switch-expression">a type-based <code>switch</code> expression</a>:</p>
<pre class="highlight"><code class="hljs csharp"><span class="hljs-keyword">return</span> shape <span class="hljs-keyword">switch</span>
{
    Square square =&gt; Math.Pow(square.Length, <span class="hljs-number">2</span>),
    Circle circle =&gt; Math.PI * Math.Pow(circle.Radius, <span class="hljs-number">2</span>),
    _ =&gt; <span class="hljs-function"><span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-title">NotSupportedException</span><span class="hljs-params">($<span class="hljs-string">"{shape.GetType()} is not supported."</span>)</span>,
}</span>;
</code></pre>
<p>Otherwise, the following warning is generated by the compiler:</p>
<blockquote>
<p>Warning CS8509 : The switch expression does not handle all possible values of its input type (it is not exhaustive). For example, the pattern <code>_</code> is not covered.</p>
</blockquote>
<p>Unfortunately that same discard pattern which resolves the warning also means that the compiler can&#39;t generate a warning if you add another sibling type at a later time. That type will already be covered by the discard pattern as far as the compiler is concerned.</p>
<p>For the abstract base class approach, C# 15 changes this with the introduction of closed class hierarchies which I explored in <a href="20260814-ClosedClassHierarchiesInCs15.html">my previous blog post</a>.</p>
<p>But what if you can&#39;t derive your types from a common ancestor (e.g., because you are using types which you haven&#39;t defined yourself)?</p>
<pre class="highlight"><code class="hljs csharp"><span class="hljs-function"><span class="hljs-keyword">public</span> record <span class="hljs-title">Square</span><span class="hljs-params">(<span class="hljs-keyword">double</span> Length)</span></span>;

<span class="hljs-function"><span class="hljs-keyword">public</span> record <span class="hljs-title">Circle</span><span class="hljs-params">(<span class="hljs-keyword">double</span> Radius)</span></span>;
</code></pre>
<p>The only obvious choice before C# 15 was to use <code>object</code> as the return type or variable type. Since the <code>object</code> type doesn&#39;t constraint the type of the assigned value in any way, the compiler of course can&#39;t reason about the exhaustiveness of a <code>switch</code> expression and always requires a discard pattern to be added.</p>
<p><a href="https://github.com/mcintyre321/OneOf">The <code>OneOf</code> library</a> provides one possible solution for this challenge. It introduces a generic <code>OneOf</code> type which you can use to combine multiple unrelated types into one. You can even give a custom name to that type by taking advantage of the using alias directives:</p>
<pre class="highlight"><code class="hljs csharp"><span class="hljs-keyword">using</span> Shape = OneOf&lt;Square, Circle&gt;;
</code></pre>
<p>This doesn&#39;t change how <code>switch</code> expressions treat these type in any way. However, the <code>OneOf</code> type comes with a <code>Match</code> method which you can use instead:</p>
<pre class="highlight"><code class="hljs csharp"><span class="hljs-keyword">return</span> shape.Match(
    square =&gt; Math.Pow(square.Length, <span class="hljs-number">2</span>),
    circle =&gt; Math.PI * Math.Pow(circle.Radius, <span class="hljs-number">2</span>)
);
</code></pre>
<p>Although the syntax is different, the functionality is equivalent. But if you add another type to your <code>OneOf</code> type alias:</p>
<pre class="highlight"><code class="hljs csharp"><span class="hljs-keyword">using</span> Shape = OneOf&lt;Square, Circle, Rectangle&gt;;
</code></pre>
<p>The code won&#39;t build anymore:</p>
<blockquote>
<p>Error CS7036 : There is no argument given that corresponds to the required parameter <code>f2</code> of <code>OneOf&lt;Square, Circle, Rectangle&gt;.Match&lt;TResult&gt;(Func&lt;Square, TResult&gt;, Func&lt;Circle, TResult&gt;, Func&lt;Rectangle, TResult&gt;)</code></p>
</blockquote>
<p>The number of parameters of the <code>Match</code> method matches the number of generic type arguments in your <code>OneOf</code> type declaration. Therefore, when you add another type argument, you must also add another method parameter or the code won&#39;t compile. Your code won&#39;t behave incorrectly because you forgot to do that.</p>
<p>With C# 15 you have an alternative to the <code>OneOf</code> type which is built into the language: union types. Just like the <code>OneOf</code> type, it allows you to specify which (unrelated) types you want to work with:</p>
<pre class="highlight"><code class="hljs csharp"><span class="hljs-function"><span class="hljs-keyword">public</span> union <span class="hljs-title">Shape</span><span class="hljs-params">(Square, Circle)</span></span>;
</code></pre>
<p>Unlike the <code>OneOf</code> type, you can now use these types in a <code>switch</code> expression without any warnings even if you don&#39;t include a discard pattern:</p>
<pre class="highlight"><code class="hljs csharp"><span class="hljs-keyword">return</span> shape <span class="hljs-keyword">switch</span>
{
    Square square =&gt; Math.Pow(square.Length, <span class="hljs-number">2</span>),
    Circle circle =&gt; Math.PI * Math.Pow(circle.Radius, <span class="hljs-number">2</span>)
};
</code></pre>
<p>And when you add another type to your union:</p>
<pre class="highlight"><code class="hljs csharp"><span class="hljs-function"><span class="hljs-keyword">public</span> union <span class="hljs-title">Shape</span><span class="hljs-params">(Square, Circle, Rectangle/)</span></span>;
</code></pre>
<p>The compiler will generate a warning if you don&#39;t handle that new type in the <code>switch</code> expression:</p>
<blockquote>
<p>Warning CS8509 : The switch expression does not handle all possible values of its input type (it is not exhaustive). For example, the pattern <code>Cs15UnionTypes.Union.Rectangle</code> is not covered.</p>
</blockquote>
<p>It&#39;s the same behavior as when using closed class hierarchies.</p>
<p>Of course, the feature is still in preview and it can change before the final release. You can already try it out in its current state with <a href="https://devblogs.microsoft.com/dotnet/dotnet-11-preview-7/">.NET 11 preview 7</a>. To enable it, you have to set the language version to preview in your project:</p>
<pre class="highlight"><code class="hljs xml"><span class="hljs-tag">&lt;<span class="hljs-title">PropertyGroup</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-title">LangVersion</span>&gt;</span>preview<span class="hljs-tag">&lt;/<span class="hljs-title">LangVersion</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-title">PropertyGroup</span>&gt;</span>
</code></pre>
<p>I created a sample project with all five approaches from this post and put it in <a href="https://github.com/DamirsCorner/20260821-cs15-union-types">my GitHub repository</a>. You can use it as a starting point to explore this feature on your own.</p>
<p>Union types are a complement to closed class hierarchies in C# 15. They both improve the experience when using type-based <code>switch</code> expressions, each for its own use case. Where closed class hierarchies address exhaustiveness for types with a common base type, unions do it for unrelated types. And give you a way to declare a predefined set of unrelated types to return from a method or assign to a variable or parameter.</p>
]]></description>
            <link>https://www.damirscorner.com/blog/posts/20260821-UnionTypesInCs15.html</link>
            <guid isPermaLink="true">https://www.damirscorner.com/blog/posts/20260821-UnionTypesInCs15.html</guid>
            <dc:creator><![CDATA[Damir Arh]]></dc:creator>
            <pubDate>Fri, 21 Aug 2026 00:00:00 GMT</pubDate>
        </item>
        <item>
            <title><![CDATA[Closed class hierarchies in C# 15]]></title>
            <description><![CDATA[<p>The release of C# 15 is getting closer and its new features started to show up in the .NET 11 previews. That&#39;s a good time for me to take a closer look at what to expect. I&#39;m starting with <a href="https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-15#closed-hierarchies">closed class hierarchies</a>.</p>
<p>Class hierarchies are referring to a base class and the classes derived from it. For example:</p>
<pre class="highlight"><code class="hljs csharp"><span class="hljs-keyword">public</span> <span class="hljs-keyword">abstract</span> record OpenShape { }

<span class="hljs-function"><span class="hljs-keyword">public</span> record <span class="hljs-title">OpenSquare</span><span class="hljs-params">(<span class="hljs-keyword">double</span> Length)</span> : OpenShape </span>{ }

<span class="hljs-function"><span class="hljs-keyword">public</span> record <span class="hljs-title">OpenCircle</span><span class="hljs-params">(<span class="hljs-keyword">double</span> Radius)</span> : OpenShape </span>{ }
</code></pre>
<p><a href="https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/switch-expression">A <code>switch</code> expression</a> can be used to handle each type differently:</p>
<pre class="highlight"><code class="hljs csharp"><span class="hljs-keyword">var</span> area = shape <span class="hljs-keyword">switch</span>
{
    OpenSquare square =&gt; Math.Pow(square.Length, <span class="hljs-number">2</span>),
    OpenCircle circle =&gt; Math.PI * Math.Pow(circle.Radius, <span class="hljs-number">2</span>),
};
</code></pre>
<p>Although the code covers all the classes, currently derived from <code>OpenShape</code>, the code above still generates a compiler warning:</p>
<blockquote>
<p>Warning CS8509 : The switch expression does not handle all possible values of its input type (it is not exhaustive). For example, the pattern <code>_</code> is not covered.</p>
</blockquote>
<p>That&#39;s because the compiler cannot be sure that the code won&#39;t encounter additional classes derived from <code>OpenShape</code> at runtime. Such a class could be defined in a different assembly and passed into code from the original assembly which was compiled without the knowledge of this new class. Such a class hierarchy is called an open class hierarchy because it&#39;s open for adding new classes to it. To fix the warning, a catch-all <a href="https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/patterns#discard-pattern">discard pattern</a> has to be added to the <code>switch</code> expression which will handle any classes that are not handled explicitly:</p>
<pre class="highlight"><code class="hljs csharp"><span class="hljs-keyword">var</span> area = shape <span class="hljs-keyword">switch</span>
{
    OpenSquare square =&gt; Math.Pow(square.Length, <span class="hljs-number">2</span>),
    OpenCircle circle =&gt; Math.PI * Math.Pow(circle.Radius, <span class="hljs-number">2</span>),
    _ =&gt; <span class="hljs-function"><span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-title">NotSupportedException</span><span class="hljs-params">($<span class="hljs-string">"{shape.GetType()} is not supported."</span>)</span>,
}</span>;
</code></pre>
<p>Even before C# 15, there is a way to prevent new classes from being derived from the base class in a different assembly, with access modifiers making the base class constructor inaccessible from other assemblies:</p>
<ul>
<li>Use <code>internal</code>, if you want the constructor publicly accessible in the original assembly.</li>
<li>Use <code>private protected</code>, if you want the constructor to be protected in the original assembly, i.e., only accessible from derived classes.</li>
</ul>
<pre class="highlight"><code class="hljs csharp"><span class="hljs-keyword">public</span> <span class="hljs-keyword">abstract</span> record PrivateShape
{
    <span class="hljs-function"><span class="hljs-keyword">private</span> <span class="hljs-keyword">protected</span> <span class="hljs-title">PrivateShape</span><span class="hljs-params">()</span> </span>{ }
}

<span class="hljs-function"><span class="hljs-keyword">public</span> record <span class="hljs-title">PrivateSquare</span><span class="hljs-params">(<span class="hljs-keyword">double</span> Length)</span> : PrivateShape </span>{ }

<span class="hljs-function"><span class="hljs-keyword">public</span> record <span class="hljs-title">PrivateCircle</span><span class="hljs-params">(<span class="hljs-keyword">double</span> Radius)</span> : PrivateShape </span>{ }
</code></pre>
<p>Trying to derive from <code>PrivateShape</code> in a different assembly will generate the following error:</p>
<blockquote>
<p>Error CS7036 : There is no argument given that corresponds to the required parameter &#39;original&#39; of &#39;PrivateShape.PrivateShape(PrivateShape)&#39;</p>
</blockquote>
<p>This is caused by the fact that there is now no parameterless constructor accessible. The constructor mentioned in the error message is the <a href="https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/record#nondestructive-mutation">automatically generated copy constructor for a record</a>.</p>
<p>Although this effectively ensures that no class can be derived from <code>PrivateShape</code> in a different assembly, the compiler will still generate a warning for a <code>switch</code> expression without the discard pattern.</p>
<p>However, C# 15 introduces a new keyword <code>closed</code> which can be used on a base class to disallow deriving from it in a different assembly:</p>
<pre class="highlight"><code class="hljs csharp"><span class="hljs-keyword">public</span> closed record ClosedShape { }

<span class="hljs-function"><span class="hljs-keyword">public</span> record <span class="hljs-title">ClosedSquare</span><span class="hljs-params">(<span class="hljs-keyword">double</span> Length)</span> : ClosedShape </span>{ }

<span class="hljs-function"><span class="hljs-keyword">public</span> record <span class="hljs-title">ClosedCircle</span><span class="hljs-params">(<span class="hljs-keyword">double</span> Radius)</span> : ClosedShape </span>{ }
</code></pre>
<p>When trying to do so, the following error will be generated:</p>
<blockquote>
<p>Error CS9382 : &#39;ClosedRectangle&#39;: cannot use a closed type &#39;ClosedShape&#39; from another assembly as a base type.</p>
</blockquote>
<p>A class hierarchy with a <code>closed</code> class as its base is treated by the compiler as a closed class hierarchy, meaning that it is closed for adding new classes. A <code>switch</code> expression over such a class hierarchy doesn&#39;t require a discard catch-all pattern. The following block of code will compile without warnings:</p>
<pre class="highlight"><code class="hljs csharp"><span class="hljs-keyword">var</span> area = shape <span class="hljs-keyword">switch</span>
{
    ClosedSquare square =&gt; Math.Pow(square.Length, <span class="hljs-number">2</span>),
    ClosedCircle circle =&gt; Math.PI * Math.Pow(circle.Radius, <span class="hljs-number">2</span>),
};
</code></pre>
<p>You might wonder what is the big deal about having or not having to add a discard pattern to a switch expression. After all, it&#39;s only a single line of code. But this line of code has an important impact on what will happen when you add another class to the hierarchy, e.g.:</p>
<pre class="highlight"><code class="hljs csharp"><span class="hljs-function"><span class="hljs-keyword">public</span> record <span class="hljs-title">ClosedRectangle</span><span class="hljs-params">(<span class="hljs-keyword">double</span> Length, <span class="hljs-keyword">double</span> Width)</span> : ClosedShape </span>{ }
</code></pre>
<p>If there is a discard pattern in the <code>switch</code> expression, no new warnings will be generated when you add such a class. Only at run time will you notice that an exception is thrown because you forgot to handle the new class. However, without the discard pattern, a warning will be generated when you add a class to the hierarchy, notifying you immediately that you need to properly handle this new class:</p>
<blockquote>
<p>Warning CS8509 : The switch expression does not handle all possible values of its input type (it is not exhaustive). For example, the pattern &#39;ClosedClassHierarchies.CoreLib.ClosedRectangle&#39; is not covered.</p>
</blockquote>
<p>You might also argue that you can still extend a closed class hierarchy indirectly by deriving from a derived class instead of the base class:</p>
<pre class="highlight"><code class="hljs csharp"><span class="hljs-function"><span class="hljs-keyword">public</span> record <span class="hljs-title">ClosedColoredSquare</span><span class="hljs-params">(Color Color, <span class="hljs-keyword">double</span> Length)</span>
    : <span class="hljs-title">ClosedSquare</span><span class="hljs-params">(Length)</span> </span>{ }
</code></pre>
<p>Such a class doesn&#39;t affect the exhaustiveness of a <code>switch</code> expression, though. The new class will be handled by the existing <code>ClosedSquare</code> case.</p>
<p>As already mentioned, the feature is still in preview, so it can change before the final release. You can already try it out in its current state with <a href="https://devblogs.microsoft.com/dotnet/dotnet-11-preview-6/">.NET 11 preview 6</a>. To enable it, you have to set the language version to preview in your project:</p>
<pre class="highlight"><code class="hljs xml"><span class="hljs-tag">&lt;<span class="hljs-title">PropertyGroup</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-title">LangVersion</span>&gt;</span>preview<span class="hljs-tag">&lt;/<span class="hljs-title">LangVersion</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-title">PropertyGroup</span>&gt;</span>
</code></pre>
<p>You can find a small sample with all the code from this article and more in <a href="https://github.com/DamirsCorner/20260814-cs15-closed-class-hierarchies">my GitHub repository</a>. Feel free to clone it and use it as a basis for experimenting with this new feature yourself.</p>
<p>The closed class hierarchies with the <code>closed</code> keywords make the type-based <code>switch</code> expressions safer to use. As long as you don&#39;t ignore the warnings in your project you should never again forget to modify an existing <code>switch</code> expression when extending a class hierarchy with a new class.</p>
]]></description>
            <link>https://www.damirscorner.com/blog/posts/20260814-ClosedClassHierarchiesInCs15.html</link>
            <guid isPermaLink="true">https://www.damirscorner.com/blog/posts/20260814-ClosedClassHierarchiesInCs15.html</guid>
            <dc:creator><![CDATA[Damir Arh]]></dc:creator>
            <pubDate>Fri, 14 Aug 2026 00:00:00 GMT</pubDate>
        </item>
        <item>
            <title><![CDATA[Switching restic backup from NFS to SFTP]]></title>
            <description><![CDATA[<p><a href="20250711-BackingUpLinuxHomeServerUsingRestic.html">For over a year</a>, I&#39;ve been using restic to back up my home lab server to a repository on a Synology NAS mounted locally using NFS. All this time, it worked for me without any issues whatsoever. Only when I configured the repository the same way on my new Linux-based home computer, I noticed that something was off.</p>
<p>The problem was that I could only access the repository with root privileges. That was a given on my home lab server where the backups where always created by a scheduled script running in root context. On my home computer, I want to interact with the restic repository as myself without having to elevate privileges.</p>
<p>As I investigated further, I noticed that the files created in the repository were owned by an unknown user, i.e., a user with UID 1000 which didn&#39;t even exist on Synology. It turned out that it was the UID of my home lab server user. I haven&#39;t encountered any issues before only because the NFS mount was always used as root on my home server and therefore permissions hadn&#39;t been checked.</p>
<p>I started playing around with NFS security settings on Synology but I couldn&#39;t find a way to interact with the repository as my Synology user. As stated in <a href="https://kb.synology.com/en-global/DSM/help/DSM/AdminCenter/file_share_privilege_nfs?version=7">the Synology documentation</a>:</p>
<blockquote>
<p>If <strong>AUTH_SYS</strong> security flavor is implemented: The client must have exactly the same numerical UID (user identifier) and GID (group identifier) on the NFS client and Synology NAS, or else the client will be assigned the permissions of <strong>others</strong> when accessing the shared folder. To avoid any permissions conflicts, you can select <strong>Map all users to admin</strong> from <strong>Squash</strong> or give &quot;Everyone&quot; permissions to the shared folder.</p>
</blockquote>
<p>Of course, the user and group identifiers weren&#39;t the same across Synology, my home lab server and my home computer. And I had no interest in trying to make them the same. The available squash options in Synology UI were also rather limited: map root to admin/guest or map all users to admin/guest. As such, they didn&#39;t give me an option to map the NFS user to my Synology user without manually tinkering with the <code>etc/exports</code> file which I also didn&#39;t want to do.</p>
<p>I took a step back and looked at other ways to access the repository on my Synology NAS. I liked SFTP best because:</p>
<ul>
<li>It&#39;s built into both Synology and restic.</li>
<li>I already used SSH to access Synology remotely.</li>
</ul>
<p>The process of switching from NFS to SFTP was very straightforward.</p>
<p>First, I enabled SFTP access on my Synology. This required two configuration changes in the <strong>Control Panel</strong>.</p>
<ul>
<li><strong>Enable SSH service</strong> in <strong>Terminal &amp; SNMP</strong> &gt; <strong>Terminal</strong>. Optionally change the <strong>Port</strong> as well. I had this one enabled already.
<img src="img/20260807-SynologyEnableSsh.png" alt="Enable SSH in Synology Control Panel"></li>
<li><strong>Enable SFTP service</strong> in <strong>File Services</strong> &gt; <strong>FTP</strong> &gt; <strong>SFTP</strong>.
<img src="img/20260807-SynologyEnableSftp.png" alt="Enable SFTP in Synology Control Panel"></li>
</ul>
<p>With Synology configured correctly, I was ready to configure the client. Since I already used SSH, I could skip some of the following steps, but I&#39;m listing them all anyway:</p>
<ul>
<li>I added an entry for my Synology to <code>~/.ssh/config</code> so that I didn&#39;t have to specify all the details every time I wanted to connect to it:<pre class="highlight"><code class="hljs nginx"><span class="hljs-title">Host</span> synology
    HostName synology
    Port <span class="hljs-number">2222</span>
    User damir
</code></pre></li>
<li>I created an SSH key so that I didn&#39;t have to enter the password every time:<pre class="highlight"><code class="hljs mathematica">ssh-keygen -t ed25519 -<span class="hljs-keyword">C</span> <span class="hljs-string">"me@mycomputer"</span>
</code></pre>
</li>
<li>I added the key to the authorized keys on Synology. This was the only time I had to enter my Synology password to connect to it. I didn&#39;t have to specify the username and port thanks to the <code>~/.ssh/config</code> entry above.<pre class="highlight"><code class="hljs stylus">ssh-copy-id -<span class="hljs-tag">i</span> ~/.ssh/id_ed25519<span class="hljs-class">.pub</span> synology
</code></pre>
</li>
<li>I changed my <code>RESTIC_REPOSITORY</code> to <code>sftp:synology:/restic</code> (again no username or port needed thanks to my SSH configuration).</li>
</ul>
<p>I followed the same steps on my home server with minor modifications because I had to do it for the root user:</p>
<ul>
<li>Add the SSH configuration entry to <code>/root/.ssh/config</code>.</li>
<li>Run all commands with <code>sudo</code>.</li>
<li>Set no passphrase for the SSH key so that it can be used from the backup script non-interactively.</li>
</ul>
<p>Since the ownership and permissions of existing files in the restic repository were in complete disarray, I had to fix those, too:</p>
<pre class="highlight"><code class="hljs groovy">sudo chown -R <span class="hljs-string">damir:</span>users <span class="hljs-regexp">/volume1/</span>restic/
sudo chmod -R a-rwx,u+rwX,g+rwX <span class="hljs-regexp">/volume1/</span>restic/
</code></pre>
<p>This was enough to get everything working. The backup created files as my Synology user (the one I used for SSH login). And I could access the restic repository from my home computer without root privileges.</p>
<p>As the final step, I got rid of everything NFS related because I didn&#39;t need it anymore:</p>
<ul>
<li>I unmounted the NFS share and deleted the mount directory on the client:<pre class="highlight"><code class="hljs bash"><span class="hljs-built_in">sudo</span> umount /mnt/restic
<span class="hljs-built_in">sudo</span> rm -rf /mnt/restic
</code></pre>
</li>
<li>Also on the client, I removed the entry for this mount from <code>etc/fstab/</code>.</li>
<li>I deleted the <strong>NFS Permissions</strong> entry for the <code>restic</code> <strong>Shared Folder</strong> in Synology <strong>Control Panel</strong>.</li>
</ul>
<p>Although at first NFS seemed the simplest option for accessing the restic repository on my Synology NAS, SFTP turned out a much better choice in the end. It only took me a year to realize that.</p>
]]></description>
            <link>https://www.damirscorner.com/blog/posts/20260807-SwitchingResticBackupFromNfsToSftp.html</link>
            <guid isPermaLink="true">https://www.damirscorner.com/blog/posts/20260807-SwitchingResticBackupFromNfsToSftp.html</guid>
            <dc:creator><![CDATA[Damir Arh]]></dc:creator>
            <pubDate>Fri, 07 Aug 2026 00:00:00 GMT</pubDate>
        </item>
        <item>
            <title><![CDATA[Installing preview .NET SDK in Arch Linux]]></title>
            <description><![CDATA[<p>I&#39;m in the process of switching from Windows 11 to <a href="https://cachyos.org">CachyOS</a> on my home machine. Of course, my setup also includes .NET development tooling. And with the upcoming .NET 11 release, I want to have access to its latest preview as well. However, the installation initially failed because the <a href="https://aur.archlinux.org/packages/dotnet-sdk-preview-bin">dotnet-sdk-preview-bin</a> package from the user repository was in conflict with the <a href="https://archlinux.org/packages/extra/x86_64/dotnet-sdk-10.0/">dotnet-sdk-10.0</a> package from the official repository I already had installed.</p>
<p>Fortunately, there&#39;s <a href="https://wiki.archlinux.org/title/.NET">a detailed .NET page in Arch Linux Wiki</a> which proved very helpful for resolving my issue. According to it, I could choose between two approaches to get the .NET 11 SDK preview installed:</p>
<ul>
<li>do it manually using the <a href="https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-install-script">dotnet-install script</a>, or</li>
<li>use <a href="https://wiki.archlinux.org/title/.NET#Install_multiple_versions_via_AUR">user repository packages exclusively</a> for installing .NET.</li>
</ul>
<p>I chose the second approach. I first uninstalled the <code>dotnet-sdk-10.0</code> package and started experimenting with AUR packages to get everything working. Even after reading the documentation, it took me a few tries before I succeeded.</p>
<p>In the end, I learned the following:</p>
<ul>
<li>I need to have either <code>dotnet-host-bin</code> or <code>dotnet-host-preview-bin</code> package installed. The former only works with stable versiosn, so the latter is required to be able to also install the preview versions.</li>
<li>For each SDK version, three packages have to be installed: <code>dotnet-sdk-bin</code>, <code>dotnet-runtime-bin</code> and <code>aspnet-runtime-bin</code>. These three install the latest stable release, i.e., .NET 10 at the time of writing. The preview version adds <code>preview</code> to the package names (e.g., <code>dotnet-sdk-preview-bin</code>) and the older stable versions add their version number to the package names (e.g., <code>dotnet-sdk-9.0-bin</code> for .NET 9).</li>
</ul>
<p>This meant that to install the SDKs for the preview version and all still supported stable versions, I had to install the following packages:</p>
<ul>
<li>common host package: <code>dotnet-host-preview-bin</code></li>
<li>.NET 11 SDK preview: <code>dotnet-sdk-preview-bin</code>, <code>dotnet-runtime-preview-bin</code>, <code>aspnet-runtime-preview-bin</code></li>
<li>.NET 10 SDK: <code>dotnet-sdk-bin</code>, <code>dotnet-runtime-bin</code>, <code>aspnet-runtime-bin</code></li>
<li>.NET 9 SDK: <code>dotnet-sdk-9.0-bin</code>, <code>dotnet-runtime-9.0-bin</code>, <code>aspnet-runtime-9.0-bin</code></li>
<li>.NET 8 SDK: <code>dotnet-sdk-8.0-bin</code>, <code>dotnet-runtime-8.0-bin</code>, <code>aspnet-runtime-8.0-bin</code></li>
</ul>
<p>After installing all the packages listed above, I ended up with the following SDKs on my machine:</p>
<pre class="highlight"><code class="hljs elixir">❯ dotnet --list-sdks
<span class="hljs-number">8.0</span>.<span class="hljs-number">423</span> [<span class="hljs-regexp">/usr/share</span><span class="hljs-regexp">/dotnet/sdk</span>]
<span class="hljs-number">9.0</span>.<span class="hljs-number">316</span> [<span class="hljs-regexp">/usr/share</span><span class="hljs-regexp">/dotnet/sdk</span>]
<span class="hljs-number">10.0</span>.<span class="hljs-number">301</span> [<span class="hljs-regexp">/usr/share</span><span class="hljs-regexp">/dotnet/sdk</span>]
<span class="hljs-number">11.0</span>.<span class="hljs-number">100</span>-preview.<span class="hljs-number">6.26359</span>.<span class="hljs-number">118</span> [<span class="hljs-regexp">/usr/share</span><span class="hljs-regexp">/dotnet/sdk</span>]
</code></pre><p>I assume that by simply updating the already installed packages, these SDKs should be updated to their respective latest versions. I expect that I&#39;ll only have to install additional packages when .NET 11 gets released because <code>dotnet-sdk-bin</code> will then install .NET 11 instead and I&#39;ll have to install <code>dotnet-sdk-10.0-bin</code> for .NET 10.</p>
<p>I might also uninstall the preview packages altogether at that time since I won&#39;t be all that interested in .NET 12 until a few months before release. And of course, I&#39;ll eventually uninstall .NET 8 and .NET 9 after they <a href="https://dotnet.microsoft.com/en-us/platform/support/policy/dotnet-core">aren&#39;t supported anymore</a>.</p>
]]></description>
            <link>https://www.damirscorner.com/blog/posts/20260731-InstallingPreviewDotNetSdkInArchLinux.html</link>
            <guid isPermaLink="true">https://www.damirscorner.com/blog/posts/20260731-InstallingPreviewDotNetSdkInArchLinux.html</guid>
            <dc:creator><![CDATA[Damir Arh]]></dc:creator>
            <pubDate>Fri, 31 Jul 2026 00:00:00 GMT</pubDate>
        </item>
        <item>
            <title><![CDATA[Manually trigger workflow from non-main branch]]></title>
            <description><![CDATA[<p>As a part of migrating my blog to Azure Static Web Apps, I also had to modify <a href="20220128-BlogPostPublishingWithGitHubActions.html">my GitHub Actions deployment workflow</a>. Usually, I only want the deployment to get triggered from the <code>main</code> branch but during development I wanted it to run from the feature branch so that I could test it. Adding <a href="https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows#workflow_dispatch">a <code>workflow-dispatch</code> trigger</a> seemed the least invasive approach to achieve that as I could trigger it manually when I wanted to test it. Unfortunately, it didn&#39;t work as I expected.</p>
<p>A <code>workflow-dispatch</code> trigger allows a workflow to be triggered manually. When such a trigger is present in a workflow, GitHub adds <a href="https://docs.github.com/en/actions/how-tos/manage-workflow-runs/manually-run-a-workflow">a <strong>Run workflow</strong> button</a> to its web page. In the button dropdown, the branch to run the workflow on can be selected and the values for all the workflow inputs can be set.</p>
<p><img src="img/20260702-GitHubActionsRunWorkflow.png" alt="Run workflow from its web page"></p>
<p>However, I had no such button available for my workflow even after I added a <code>workflow-dispatch</code> trigger to it. As I learned later, it was because I only had the <code>workflow-dispatch</code> trigger on the feature branch and not on the <code>main</code> branch, and button only show up when the trigger is present in the main branch. Of course, I didn&#39;t want to add it to the <code>main</code> branch, as I wanted to test everything in the feature branch first.</p>
<p>Fortunately, a workflow with a <code>workflow-dispatch</code> trigger on a non-<code>main</code> branch can still be triggered for that branch in other ways:</p>
<ul>
<li>with <a href="https://docs.github.com/en/rest/actions/workflows?apiVersion=2026-03-10#create-a-workflow-dispatch-event">a REST call</a>, or</li>
<li>with <a href="https://cli.github.com/manual/gh_workflow_run">GitHub CLI</a>.</li>
</ul>
<p>Since I&#39;m already using GitHub CLI to <a href="20211008-SimplifyCommonOperationsWithGitHubCli.html">create repositories for sample code for my blog posts</a>, I liked the second option better. And it was indeed very easy to use. Since my GitHub CLI was already <a href="https://cli.github.com/manual/gh_auth_login">authenticated</a>, I could simply invoke <code>gh workflow run</code> from my local repository folder and specify the workflow file, the branch and the input values, e.g.:</p>
<pre class="highlight"><code class="hljs bash">gh workflow run build.yml --ref feature/azure-static-web-app <span class="hljs-operator">-f</span> deploy=<span class="hljs-literal">true</span>
</code></pre>
<p>It turned out that once you know how, it&#39;s even easier to manually trigger a workflow with GitHub CLI than via the web page. I&#39;m likely going to use it now even when I could run it from the web page just because it&#39;s more convenient.</p>
]]></description>
            <link>https://www.damirscorner.com/blog/posts/20260703-ManuallyTriggerWorkflowFromNonMainBranch.html</link>
            <guid isPermaLink="true">https://www.damirscorner.com/blog/posts/20260703-ManuallyTriggerWorkflowFromNonMainBranch.html</guid>
            <dc:creator><![CDATA[Damir Arh]]></dc:creator>
            <pubDate>Fri, 03 Jul 2026 00:00:00 GMT</pubDate>
        </item>
        <item>
            <title><![CDATA[Apex domain redirect for Azure Static Web Apps]]></title>
            <description><![CDATA[<p>After <a href="20260612-RegexBasedRedirectsForStaticWebApps.html">reimplementing redirects in Azure Static Web App using managed Azure Functions</a>, there was one last redirect I couldn&#39;t implement in this way and had to find another solution for: redirecting all apex domain requests to their counterparts with <code>www</code> subdomain.</p>
<p>In IIS, I used <a href="https://learn.microsoft.com/en-us/iis/extensions/url-rewrite-module/url-rewrite-module-configuration-reference#using-back-references-in-rewrite-rules">URL rewrite module</a> to implement it:</p>
<pre class="highlight"><code class="hljs xml"><span class="hljs-tag">&lt;<span class="hljs-title">rule</span> <span class="hljs-attribute">name</span>=<span class="hljs-value">"Redirect to www"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-title">match</span> <span class="hljs-attribute">url</span>=<span class="hljs-value">".*"</span> /&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-title">conditions</span> <span class="hljs-attribute">logicalGrouping</span>=<span class="hljs-value">"MatchAny"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-title">add</span> <span class="hljs-attribute">input</span>=<span class="hljs-value">"{HTTP_HOST}"</span> <span class="hljs-attribute">pattern</span>=<span class="hljs-value">"^(www\.)(.*)$"</span> <span class="hljs-attribute">negate</span>=<span class="hljs-value">"true"</span> /&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-title">conditions</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-title">action</span> <span class="hljs-attribute">type</span>=<span class="hljs-value">"Redirect"</span> <span class="hljs-attribute">url</span>=<span class="hljs-value">"https://www.{HTTP_HOST}/{R:0}"</span> <span class="hljs-attribute">redirectType</span>=<span class="hljs-value">"Permanent"</span>/&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-title">rule</span>&gt;</span>
</code></pre>
<p>For Azure Static Web App, I ended up doing it as part of my custom domain setup. First, I had set up both custom domains: <code>www</code> and apex.</p>
<p>The simper of the two was <a href="https://learn.microsoft.com/en-us/azure/static-web-apps/custom-domain-external">the <code>www</code> subdomain</a>:</p>
<ul>
<li>In my DNS configuration I had to add a <code>CNAME</code> entry pointing at the autogenerated <code>azurestaticapps.net</code> URL of my Azure Static Web App listed on its <strong>Overview</strong> page in Azure Portal.</li>
<li>On the <strong>Custom domains</strong> page of the Azure Static Web App in Azure Portal, I had to add an entry for my <code>www</code> domain with <code>CNAME</code> type. After it was validated, the web page started responding at my <code>www</code> subdomain.</li>
</ul>
<p>Setting up the apex domain was a bit more involved. Since my domain registrar doesn&#39;t support <code>ALIAS</code> records or domain forwarding, I had to go <a href="https://learn.microsoft.com/en-us/azure/static-web-apps/apex-domain-external#set-up-with-an-a-record">the <code>A</code> record route</a>:</p>
<ul>
<li>To validate the domain, I had to add an entry on the <strong>Custom domains</strong> page for my apex domain with type <code>TXT</code>. This generated a code for me which I had to enter as the value for the corresponding <code>TXT</code> entry in my DNS configuration.</li>
<li>Once the <code>TXT</code> entry was validated, it was time to add an <code>A</code> entry for the apex domain in my DNS configuration. I got the value for it, i.e., the IP, in the <code>stableInboundIP</code> field of the JSON view on the <strong>Overview</strong> page of my Azure Static Web App in Azure Portal.</li>
</ul>
<p>After all of this was successfully set up, it was time for the very last step: configuring the redirect from the apex domain to the <code>www</code> subdomain. I could achieve this by setting my <code>www</code> subdomain entry on the <strong>Custom domains</strong> page in Azure Portal <a href="https://learn.microsoft.com/en-us/azure/static-web-apps/custom-domain-default#set-a-default-domain">as default</a>. As a side effect, this also redirects any requests from the autogenerated <code>azurestaticapps.net</code> domain to my <code>www</code> custom subdomain. I don&#39;t expect (m)any such requests, but it&#39;s still nice to have this redirect in place.</p>
]]></description>
            <link>https://www.damirscorner.com/blog/posts/20260626-ApexDomainRedirectForAzureStaticWebApps.html</link>
            <guid isPermaLink="true">https://www.damirscorner.com/blog/posts/20260626-ApexDomainRedirectForAzureStaticWebApps.html</guid>
            <dc:creator><![CDATA[Damir Arh]]></dc:creator>
            <pubDate>Fri, 26 Jun 2026 00:00:00 GMT</pubDate>
        </item>
        <item>
            <title><![CDATA[Writing tests for HTTP-triggered Azure Functions]]></title>
            <description><![CDATA[<p>As I was implementing <a href="20260612-RegexBasedRedirectsForStaticWebApps.html">redirects in Azure Functions for my blog</a>, I wanted to be able to test them as efficiently as possible. Although you can run and test an Azure function locally, and I did, I also implemented automated unit tests to systematically cover all the cases I care about.</p>
<p>I chose <a href="https://jestjs.io">Jest</a> as my testing framework or <a href="https://kulshekhar.github.io/ts-jest/docs">ts-jest</a> to be precise since I used Typescript. I set it up in my <code>api</code> folder by simply following <a href="https://kulshekhar.github.io/ts-jest/docs/getting-started/installation">the instructions</a>:</p>
<pre class="highlight"><code class="hljs bash">npm install --save-dev jest ts-jest @types/jest
npx ts-jest config:init
</code></pre>
<p>I modified my <code>package.json</code> to invoke <code>jest</code> when running <code>npm test</code>:</p>
<pre class="highlight"><code class="hljs json">{
  "<span class="hljs-attribute">scripts</span>": <span class="hljs-value">{
    "<span class="hljs-attribute">test</span>": <span class="hljs-value"><span class="hljs-string">"jest"</span>
  </span>}
</span>}
</code></pre>
<p>And I added <code>jest</code> types to my <code>tsconfig.json</code> so that I didn&#39;t have to import <code>test</code>, <code>expect</code> and other Jest symbols:</p>
<pre class="highlight"><code class="hljs json">{
  "<span class="hljs-attribute">compilerOptions</span>": <span class="hljs-value">{
    "<span class="hljs-attribute">types</span>": <span class="hljs-value">[<span class="hljs-string">"node"</span>, <span class="hljs-string">"jest"</span>]
  </span>}
</span>}
</code></pre>
<p>Since an HTTP-triggered Azure Function accepts <code>HttpRequest</code> and <code>InvocationContext</code> as parameters, I had to mock these two first to make my tests simpler.</p>
<p>For <code>InvocationContext</code>, I created a minimal mock I could use with every call. Because I prefer writing TypeScript in <a href="https://www.typescriptlang.org/tsconfig/#strict">strict mode</a>, I had to set all non-optional fields even if I don&#39;t need them in my code:</p>
<pre class="highlight"><code class="hljs typescript"><span class="hljs-keyword">class</span> MockContext <span class="hljs-keyword">implements</span> InvocationContext {
  invocationId: <span class="hljs-built_in">string</span> = <span class="hljs-string">""</span>;
  functionName: <span class="hljs-built_in">string</span> = <span class="hljs-string">""</span>;
  extraInputs: InvocationContextExtraInputs = {
    <span class="hljs-keyword">get</span>: <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-params">()</span>: <span class="hljs-title">unknown</span> </span>{
      <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">"Function not implemented."</span>);
    },
    <span class="hljs-keyword">set</span>: <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-params">(inputOrName: FunctionInput | string, value: unknown)</span>: <span class="hljs-title">void</span> </span>{
      <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">"Function not implemented."</span>);
    },
  };
  extraOutputs: InvocationContextExtraOutputs = {
    <span class="hljs-keyword">set</span>: <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-params">(output: HttpOutput)</span>: <span class="hljs-title">void</span> </span>{
      <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">"Function not implemented."</span>);
    },
    <span class="hljs-keyword">get</span>: <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-params">(outputOrName: FunctionOutput | string)</span>: <span class="hljs-title">unknown</span> </span>{
      <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">"Function not implemented."</span>);
    },
  };
  log(...args: <span class="hljs-built_in">any</span>[]): <span class="hljs-built_in">void</span> {
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"[test] log:"</span>, ...args);
  }
  trace(...args: <span class="hljs-built_in">any</span>[]): <span class="hljs-built_in">void</span> {
    <span class="hljs-built_in">console</span>.trace(<span class="hljs-string">"[test] trace:"</span>, ...args);
  }
  debug(...args: <span class="hljs-built_in">any</span>[]): <span class="hljs-built_in">void</span> {
    <span class="hljs-built_in">console</span>.debug(<span class="hljs-string">"[test] debug:"</span>, ...args);
  }
  info(...args: <span class="hljs-built_in">any</span>[]): <span class="hljs-built_in">void</span> {
    <span class="hljs-built_in">console</span>.info(<span class="hljs-string">"[test] info:"</span>, ...args);
  }
  warn(...args: <span class="hljs-built_in">any</span>[]): <span class="hljs-built_in">void</span> {
    <span class="hljs-built_in">console</span>.warn(<span class="hljs-string">"[test] warn:"</span>, ...args);
  }
  error(...args: <span class="hljs-built_in">any</span>[]): <span class="hljs-built_in">void</span> {
    <span class="hljs-built_in">console</span>.error(<span class="hljs-string">"[test] error:"</span>, ...args);
  }
  retryContext?: RetryContext;
  traceContext?: TraceContext;
  triggerMetadata?: TriggerMetadata;
  options: EffectiveFunctionOptions = {
    trigger: {
      type: <span class="hljs-string">""</span>,
      name: <span class="hljs-string">""</span>,
    },
    extraInputs: [],
    extraOutputs: [],
  };
}
</code></pre>
<p>The <code>HttpRequest</code> contains the custom <code>x-ms-original-url</code> header which serves as the only relevant input to my function, so I simply create the mocked <code>HttpRequest</code> instance inside the helper function I use to invoke the function under test:</p>
<pre class="highlight"><code class="hljs typescript"><span class="hljs-keyword">const</span> baseUrl = <span class="hljs-string">"https://www.myserver.com"</span>;

async <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">invokeRedirect</span><span class="hljs-params">(route: string)</span>: <span class="hljs-title">Promise</span>&lt;<span class="hljs-title">HttpResponseInit</span>&gt; </span>{
  <span class="hljs-keyword">const</span> context = <span class="hljs-keyword">new</span> MockContext();
  <span class="hljs-keyword">const</span> request = <span class="hljs-keyword">new</span> HttpRequest({
    url: <span class="hljs-string">"http://localhost/api/redirect"</span>,
    method: <span class="hljs-string">"GET"</span>,
    headers: { <span class="hljs-string">"x-ms-original-url"</span>: `${baseUrl}${route}` },
  });

  <span class="hljs-keyword">return</span> await redirect(request, context);
}
</code></pre>
<p>For a successful redirect, the most important part of the response is the <code>location</code> header which contains the URL to redirect to. The <code>headers</code> field in the response is a <a href="https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#union-types">union type</a>, but since I know the exact type my function returns, I simplified the assertion code by always casting the value to that type:</p>
<pre class="highlight"><code class="hljs typescript"><span class="hljs-keyword">const</span> response = await invokeRedirect(route);

expect(response.status).toBe(<span class="hljs-number">301</span>);
<span class="hljs-keyword">const</span> location = (response.headers as Record&lt;<span class="hljs-built_in">string</span>, <span class="hljs-built_in">string</span>&gt; | <span class="hljs-literal">undefined</span>)
  ?.location;
expect(location).toBe(expected);
</code></pre>
<p>Since I want to test the function for many supported inputs, <a href="https://jestjs.io/docs/api#testeachtablename-fn-timeout">data-driven tests</a> are a good fit:</p>
<pre class="highlight"><code class="hljs typescript">test.each`
  route                   | expected
  ${<span class="hljs-string">"/categories/dotnet"</span>} | ${<span class="hljs-string">"/tags/dotnet.html"</span>}
  ${<span class="hljs-string">"/categories/csharp"</span>} | ${<span class="hljs-string">"/tags/csharp.html"</span>}
`(
  <span class="hljs-string">"redirects to $route -&gt; $expected"</span>,
  async ({ route, expected }: { route: <span class="hljs-built_in">string</span>; expected: <span class="hljs-built_in">string</span> }) =&gt; {
    <span class="hljs-comment">// ...</span>
  },
);
</code></pre>
<p>To test the behavior for URLs without a matching redirect, I had to <a href="https://jestjs.io/docs/mock-functions">mock</a> the <code>fetch</code> function which my function uses to read the response body:</p>
<pre class="highlight"><code class="hljs typescript"><span class="hljs-keyword">const</span> notFoundPageText = <span class="hljs-string">"404 Not Found"</span>;

global.fetch = jest.fn().mockResolvedValue({
  ok: <span class="hljs-literal">true</span>,
  status: <span class="hljs-number">200</span>,
  text: () =&gt; Promise.resolve(notFoundPageText),
});
</code></pre>
<p>To be as thorough as possible, I decided to also verify the <code>fetch</code> invocation, not only the response of the function under test:</p>
<pre class="highlight"><code class="hljs typescript">expect(global.fetch).toHaveBeenCalledTimes(<span class="hljs-number">1</span>);
expect(global.fetch).toHaveBeenCalledWith(`${baseUrl}/errors/<span class="hljs-number">404.</span>html`);

expect(response.status).toBe(<span class="hljs-number">404</span>);
expect(response.body).toBe(notFoundPageText);
<span class="hljs-keyword">const</span> contentType = (response.headers as Record&lt;<span class="hljs-built_in">string</span>, <span class="hljs-built_in">string</span>&gt; | <span class="hljs-literal">undefined</span>)?.[
  <span class="hljs-string">"Content-Type"</span>
];
expect(contentType).toBe(<span class="hljs-string">"text/html"</span>);
</code></pre>
<p>To start each test with a clean state, I clear the mocks after every test:</p>
<pre class="highlight"><code class="hljs typescript">afterEach(() =&gt; {
  jest.clearAllMocks();
});
</code></pre>
<p>You can find a sample project in my <a href="https://github.com/DamirsCorner/20260619-ts-http-azure-function-tests">GitHub repository</a>. I started off with the sample project from <a href="20260612-RegexBasedRedirectsForStaticWebApps.html">my previous blog post</a> and added tests to it. The last commit adds all the tests as well as the extra setup to make them work.</p>
<p>Although the unit tests are great for thoroughly testing all the use cases for the Azure function, you shouldn&#39;t completely rely on them. Before deploying the function, it&#39;s a good idea to <a href="https://learn.microsoft.com/en-us/azure/azure-functions/functions-run-local?tabs=windows%2Cisolated-process%2Cnode-v4%2Cpython-v2%2Chttp-trigger%2Ccontainer-apps&amp;pivots=programming-language-typescript">run it locally</a> and invoke it using <code>curl</code>, for example:</p>
<pre class="highlight"><code class="hljs bash">curl -H <span class="hljs-string">"x-ms-original-url: http://localhost:7071/categories/dotnet"</span> -v http://localhost:<span class="hljs-number">7071</span>/api/redirect
</code></pre>
<p>This way the function will at least run in a simulated Azure Functions runtime, not only in a Node runtime provided by Jest.</p>
<p>Of course, the final and ultimate test will be when the Azure Function is deployed and invoked in Azure. You should have a small set of test cases prepared to run whenever you deploy any changes to your Azure Function. Only then can you be certain that it works as expected.</p>
]]></description>
            <link>https://www.damirscorner.com/blog/posts/20260619-WritingTestsForHttpTriggeredAzureFunctions.html</link>
            <guid isPermaLink="true">https://www.damirscorner.com/blog/posts/20260619-WritingTestsForHttpTriggeredAzureFunctions.html</guid>
            <dc:creator><![CDATA[Damir Arh]]></dc:creator>
            <pubDate>Fri, 19 Jun 2026 00:00:00 GMT</pubDate>
        </item>
    </channel>
</rss>