<?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" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Yoshifumi Kawai on Medium]]></title>
        <description><![CDATA[Stories by Yoshifumi Kawai on Medium]]></description>
        <link>https://medium.com/@neuecc?source=rss-61fea4eebf07------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/0*5p7GOVtYN8PbMKTw.jpg</url>
            <title>Stories by Yoshifumi Kawai on Medium</title>
            <link>https://medium.com/@neuecc?source=rss-61fea4eebf07------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Tue, 16 Jun 2026 09:03:05 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@neuecc/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com" rel="hub"/>
        <item>
            <title><![CDATA[ToonEncoder — A JSON-Compatible Format Encoder for C# and LLMs]]></title>
            <link>https://neuecc.medium.com/toonencoder-a-json-compatible-format-encoder-for-c-and-llms-53c096dfca2f?source=rss-61fea4eebf07------2</link>
            <guid isPermaLink="false">https://medium.com/p/53c096dfca2f</guid>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[csharp]]></category>
            <category><![CDATA[toon]]></category>
            <dc:creator><![CDATA[Yoshifumi Kawai]]></dc:creator>
            <pubDate>Thu, 25 Dec 2025 07:36:33 GMT</pubDate>
            <atom:updated>2025-12-25T07:36:33.199Z</atom:updated>
            <content:encoded><![CDATA[<h3>ToonEncoder — A JSON-Compatible Format Encoder for C# and LLMs</h3><p>I’ve created a serializer (encode-only) for <a href="https://github.com/toon-format/toon">Token-Oriented Object Notation (TOON)</a>, a JSON-compatible format. When used appropriately, TOON has the potential to significantly reduce token consumption when interacting with LLMs. While this is a compact library, internally it processes everything in UTF8, and it’s equipped with all the essential features of a modern library, including IBufferWriter&lt;byte&gt; support and Source Generator-based serializer generation.</p><ul><li><a href="https://github.com/Cysharp/ToonEncoder">GitHub — Cysharp/ToonEncoder</a></li></ul><p>Of course, compared to competitors, the performance and memory efficiency are overwhelmingly better.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/993/0*41BdChp81REiiHb6.png" /><figcaption>Top 3 variationa are all ToonEncoder</figcaption></figure><p>I’m simply too experienced in serializer design at this point (<a href="https://github.com/MessagePack-CSharp/MessagePack-CSharp">MessagePack-CSharp</a>, <a href="https://github.com/Cysharp/MemoryPack">MemoryPack</a>, <a href="https://github.com/neuecc/Utf8Json">Utf8Json</a>, etc…) with plenty of track record and know-how! Given the nature of this field, there are many libraries out there that seem to have been thrown together with AI to “just make it work,” but they’re no match for this. After all, this is warm, handcrafted code! Hyper-handmade craft coding. Honestly, I believe current AI-generated code is still far from top-tier quality. Sure, it can produce working code, and that’s impressive, but still.</p><p>Now, let me briefly explain TOON. The following JSON data:</p><pre>{<br>  &quot;context&quot;: {<br>    &quot;task&quot;: &quot;Our favorite hikes together&quot;,<br>    &quot;location&quot;: &quot;Boulder&quot;,<br>    &quot;season&quot;: &quot;spring_2025&quot;<br>  },<br>  &quot;friends&quot;: [&quot;ana&quot;, &quot;luis&quot;, &quot;sam&quot;],<br>  &quot;hikes&quot;: [<br>    {<br>      &quot;id&quot;: 1,<br>      &quot;name&quot;: &quot;Blue Lake Trail&quot;,<br>      &quot;distanceKm&quot;: 7.5,<br>      &quot;elevationGain&quot;: 320,<br>      &quot;companion&quot;: &quot;ana&quot;,<br>      &quot;wasSunny&quot;: true<br>    },<br>    {<br>      &quot;id&quot;: 2,<br>      &quot;name&quot;: &quot;Ridge Overlook&quot;,<br>      &quot;distanceKm&quot;: 9.2,<br>      &quot;elevationGain&quot;: 540,<br>      &quot;companion&quot;: &quot;luis&quot;,<br>      &quot;wasSunny&quot;: false<br>    },<br>    {<br>      &quot;id&quot;: 3,<br>      &quot;name&quot;: &quot;Wildflower Loop&quot;,<br>      &quot;distanceKm&quot;: 5.1,<br>      &quot;elevationGain&quot;: 180,<br>      &quot;companion&quot;: &quot;sam&quot;,<br>      &quot;wasSunny&quot;: true<br>    }<br>  ]<br>}</pre><p>Can be expressed in TOON as follows, resulting in a much smaller representation:</p><pre>context:<br>  task: Our favorite hikes together<br>  location: Boulder<br>  season: spring_2025<br>  friends[3]: ana,luis,sam<br>  hikes[3]{id,name,distanceKm,elevationGain,companion,wasSunny}:<br>    1,Blue Lake Trail,7.5,320,ana,true<br>    2,Ridge Overlook,9.2,540,luis,false<br>    3,Wildflower Loop,5.1,180,sam,true</pre><p>Rather than JSON, it’s more like a hybrid of YAML and CSV. In particular, arrays of objects containing only primitive elements — which can be represented as tables (CSV) — are output in a CSV-like format, significantly reducing data size. This reduction translates to token savings for LLMs, which has garnered some attention. You might ask, “Why not just use CSV instead of some obscure format?” Well, CSV alone only handles tables and can’t include accompanying metadata, making it impractical. TOON offers better usability in this regard. Additionally, since the specification maintains mutual compatibility with JSON, it can serve as a drop-in replacement for JSON — another selling point.</p><p>My personal take is that TOON is not human-readable. Because TOON prioritizes efficiency, there are three different ways to represent arrays. In ToonEncoder, I call them TabularArray, InlineArray, and NonUniformArray, but honestly, having three types makes it hard to read. Moreover, when TabularArray and NonUniformArray are combined with nested objects, the indentation becomes utterly confusing. While LLMs seem to somehow correctly interpret human-readable formats even if they’re unfamiliar, I’m concerned whether they can properly understand such broken-looking structures.</p><p>Therefore, rather than replacing all JSON, I think the sweet spot — in terms of token efficiency, LLM comprehension, and human readability — is to apply TOON to CSV-like tables (TabularArray) or flat objects with TabularArray appended at the end. ToonEncoder is tuned to deliver optimal performance for exactly this kind of usage, and it integrates with Microsoft.Extensions.AI to enable selective TOON conversion for specific types only.</p><h3>Using with Microsoft.Extensions.AI</h3><p>Download from <a href="https://www.nuget.org/packages/ToonEncoder">NuGet/ToonEncoder</a> to get the core library and Source Generator bundled together. Note that the minimum target platform is .NET 10.</p><p>Basically, you can use Encode to convert either JsonElement or T value.</p><pre>using Cysharp.AI;<br><br>var users = new User[]<br>{<br>    new (1, &quot;Alice&quot;, &quot;admin&quot;),<br>    new (2, &quot;Bob&quot;, &quot;user&quot;),<br>};<br><br>// simply encode<br>string toon = ToonEncoder.Encode(users);<br><br>// [2]{Id,Name,Role}:<br>//   1,Alice,admin<br>//   2,Bob,user<br>Console.WriteLine(toon);<br><br>public record User(int Id, string Name, string Role);</pre><p>In this case, since we have an array of objects containing only primitive elements, it’s serialized as a tabular layout (TabularArray).</p><p>For practical usage, when applying this to Function Calling with <a href="https://learn.microsoft.com/en-us/dotnet/ai/microsoft-extensions-ai">Microsoft.Extensions.AI</a>, you can prepare a JsonSerializerOptions configured with converters for the target types and pass it to the options. The Source Generator creates efficient JsonConverters. Usage is simple—just apply [GenerateToonTabularArrayConverter] to your target type!</p><pre>public IEnumerable&lt;AIFunction&gt; GetAIFunctions()<br>{<br>    var jsonSerializerOptions = new JsonSerializerOptions<br>    {<br>        Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,<br>        WriteIndented = false,<br>        DefaultIgnoreCondition = JsonIgnoreCondition.Never,<br>        Converters =<br>        {<br>            // setup generated converter<br>            new Cysharp.AI.Converters.CodeDiagnosticTabularArrayConverter(),<br>        }<br>    };<br>    jsonSerializerOptions.MakeReadOnly(true); // need MakeReadOnly(true) or setup converter to TypeInfoResolve<br><br>    var factoryOptions = new AIFunctionFactoryOptions<br>    {<br>        SerializerOptions = jsonSerializerOptions<br>    };<br><br>    yield return AIFunctionFactory.Create(GetDiagnostics, factoryOptions);<br>}<br><br>[Description(&quot;Get error diagnostics of the target project.&quot;)]<br>public CodeDiagnostic[] GetDiagnostics(string projectName)<br>{<br>    // ...<br>}<br><br>// Trigger of Source Generator<br>[GenerateToonTabularArrayConverter]<br>public class CodeDiagnostic<br>{<br>    public string Code { get; set; }<br>    public string Description { get; set; }<br>    public string FilePath { get; set; }<br>    public int LocationStart { get; set; }<br>    public int LocationLength { get; set; }<br>}</pre><p>In this example, when the number of CodeDiagnostic[] items is large, there&#39;s a significant difference in token consumption between JSON and TOON, giving TOON a clear advantage. However, since TOON has its strengths and weaknesses, I recommend evaluating the characteristics of your data to decide whether to apply TOON (add a Converter) or leave it as-is (JSON).</p><p>For generating flat objects (containing primitives, arrays of primitives, or arrays of objects composed only of primitives), a different attribute [GenerateToonSimpleObjectConverter] handles scenarios with TabularArray plus additional metadata.</p><pre>var item = new Item<br>{<br>    Status = &quot;active&quot;,<br>    Users = [new(1, &quot;Alice&quot;, &quot;Admin&quot;), new(2, &quot;Bob&quot;, &quot;User&quot;)]<br>};<br><br>var toon = Cysharp.AI.Converters.ItemSimpleObjectConverter.Encode(item);<br><br>// Status: active<br>// Users[2]{Id,Name,Role}:<br>//   1,Alice,Admin<br>//   2,Bob,User<br>Console.WriteLine(toon);<br><br>[GenerateToonSimpleObjectConverter]<br>public record Item<br>{<br>    public required string Status { get; init; }<br>    public required User[] Users { get; init; }<br>}</pre><h3>Conclusion</h3><p>I created ToonEncoder as a component for <a href="https://github.com/Cysharp/CompilerBrain">Cysharp/CompilerBrain</a>, a C# Coding Agent that’s still very much a work in progress. Since it handles a lot of data, I wanted to save on tokens. So, early next year I’ll be focusing on CompilerBrain… probably!</p><p>To be completely honest, I don’t think TOON itself is a particularly good format. In fact, I’d say it’s quite rough around the edges. However, the marketing appeal of “JSON-compatible drop-in replacement” seems to have resonated, and since CSV alone is indeed limiting, having an actual specification to work with makes it a reasonable compromise choice.</p><p>My lack of intention to serialize complex data is reflected in [GenerateToonTabularArrayConverter] and [GenerateToonSimpleObjectConverter]. These also function as Analyzers—they produce compile errors when you try to include unsupported nested properties, essentially creating a pseudo-subset of TOON. Of course, if you call the methods via JsonElement, nested properties will be serialized properly. The library passes all the official test suite cases (except for intentionally unsupported features).</p><p>Also, as the library name suggests, it only supports Encoding. There’s no Decode. Since this is meant for sending data to LLMs, decoding isn’t really necessary.</p><p>While there are various shortcuts that make this a compact library, it’s still quite practical, so if you’re interested, please give it a try!</p><h3>2025 — Work, Life, OSS</h3><p>Since it’s the end of the year, let me also reflect on this year’s situation, though it’s unrelated to the main topic.</p><p>My company <a href="https://cysharp.co.jp/en/">Cysharp</a>’s parent company <a href="https://www.cygames.co.jp/en/">Cygames</a> released <a href="https://shadowverse-wb.com/en/">Shadowverse: Worlds Beyond</a> this year. Cysharp contributed to the C#-related portions. Some details are explained in a Japanese presentation: <a href="https://speakerdeck.com/cygames/cygames_202512_udaytokyo2025">Cygames Approach to Technical Design for the Latest Smartphone Games: The Challenge of Architectural Redesign in “Shadowverse: Worlds Beyond”</a>.</p><p>The main card battle component and the lobby space where many users gather are built with Cysharp’s network framework <a href="https://github.com/Cysharp/MagicOnion">MagicOnion</a>, running on C# servers. As someone who strongly wishes to see C# thrive not just in the enterprise world, and not just within Unity, but in consumer-facing applications, I’m delighted that we could deliver this worldwide.</p><p>While work went very well, unfortunately I significantly damaged my health from mid-year onward. Because of this, I intentionally reduced the time I spend on OSS maintenance. Since it was difficult to find extended periods of concentration, I focused my time on only a few things like <a href="https://github.com/Cysharp/ConsoleAppFramework">ConsoleAppFramework</a>. Additionally, being frustrated by Microsoft (employees) was also part of the reason I stepped back a bit. As for my health, I’m on the path to recovery, so I plan to gradually get back into things next year.</p><p>As you can see from the many OSS projects provided at <a href="https://github.com/cysharp/">GitHub/Cysharp</a>, the OSS and money issues that made waves this year are certainly not irrelevant to me. My personal opinion is that seeking money is the right thing to do. For a truly sustainable environment, money cannot be separated from the equation and must be addressed seriously. I’m very grateful to everyone sponsoring me at <a href="https://github.com/sponsors/neuecc">sponsors/neuecc</a>. Thank you!</p><p>I hope to continue delivering results that surprise everyone in the C# world next year. Happy New Year!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=53c096dfca2f" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[“ZLinq”, a Zero-Allocation LINQ Library for .NET]]></title>
            <link>https://neuecc.medium.com/zlinq-a-zero-allocation-linq-library-for-net-1bb0a3e5c749?source=rss-61fea4eebf07------2</link>
            <guid isPermaLink="false">https://medium.com/p/1bb0a3e5c749</guid>
            <category><![CDATA[csharp]]></category>
            <category><![CDATA[linq]]></category>
            <category><![CDATA[unity]]></category>
            <dc:creator><![CDATA[Yoshifumi Kawai]]></dc:creator>
            <pubDate>Thu, 15 May 2025 09:44:43 GMT</pubDate>
            <atom:updated>2025-05-19T03:20:18.283Z</atom:updated>
            <content:encoded><![CDATA[<h3>“ZLinq”, a Zero-Allocation LINQ Library for .NET</h3><p>I’ve released <a href="https://github.com/Cysharp/ZLinq">ZLinq</a> v1 last month! By building on structs and generics, it achieves zero allocations. It includes extensions like LINQ to Span, LINQ to SIMD, LINQ to Tree (FileSystem, JSON, GameObject, etc.), a drop-in replacement Source Generator for arbitrary types, and support for multiple platforms including .NET Standard 2.0, Unity, and Godot. It has now exceeded 2000 GitHub stars.</p><ul><li><a href="https://github.com/Cysharp/ZLinq">https://github.com/Cysharp/ZLinq</a></li></ul><p>Struct-based LINQ itself isn’t particularly rare, and many implementations have attempted this approach over the years. However, none have been truly practical until now. They’ve typically suffered from extreme assembly size bloat, insufficient operator coverage, or performance issues due to inadequate optimization, never evolving beyond experimental status. With ZLinq, we aimed to create something practical by implementing 100% coverage of all methods and overloads in .NET 10 (including new ones like Shuffle, RightJoin, LeftJoin), ensuring 99% behavior compatibility, and implementing optimizations beyond just allocation reduction, including SIMD support, to outperform in most scenarios.</p><p>This was possible because of my extensive experience implementing LINQ. In April 2009, I released <a href="https://github.com/neuecc/linq.js/">linq.js</a>, a LINQ to Objects library for JavaScript (it’s wonderful to see that linq.js is still being maintained by someone who forked it!). I’ve also implemented the widely-used Reactive Extensions library <a href="https://github.com/neuecc/UniRx">UniRx</a> for Unity, and recently released its evolution, <a href="https://github.com/Cysharp/R3">R3</a>. I’ve created variants like <a href="https://assetstore.unity.com/packages/tools/integration/linq-to-gameobject-24256">LINQ to GameObject</a>, <a href="https://github.com/neuecc/LINQ-to-BigQuery">LINQ to BigQuery</a>, and <a href="https://github.com/Cysharp/SimdLinq/">SimdLinq</a>. By combining these experiences with knowledge from zero-allocation related libraries (<a href="https://github.com/Cysharp/ZString">ZString</a>, <a href="https://github.com/Cysharp/ZLogger">ZLogger</a>) and high-performance serializers (<a href="https://github.com/MessagePack-CSharp/MessagePack-CSharp/">MessagePack-CSharp</a>, <a href="https://github.com/Cysharp/MemoryPack">MemoryPack</a>), we achieved the ambitious goal of creating a superior alternative to the standard library.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/674/0*SDn0WRgt7viYsqrV.jpg" /></figure><p>This simple benchmark shows that while normal LINQ allocations increase as you chain more methods (Where, Where.Take, Where.Take.Select), ZLinq remains at zero.</p><p>Performance varies depending on the source, quantity, element type, and method chaining. To confirm that ZLinq performs better in most cases, we’ve prepared various benchmark scenarios that run on GitHub Actions: <a href="https://github.com/Cysharp/ZLinq/actions/workflows/benchmark.yaml">ZLinq/actions/Benchmark</a>. While there are cases where ZLinq structurally can’t win, it outperforms in most practical scenarios.</p><p>For extreme differences in benchmarks, consider repeatedly calling Select multiple times. Neither System.LINQ nor ZLinq apply special optimizations in this case, but ZLinq shows a significant performance advantage:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/281/1*_pyu2LwhltriPXPlbNvg4w.png" /></figure><p>(Memory measurement 1B is BenchmarkDotNet MemoryDiagnoser errors. The documentation clearly states that <a href="https://benchmarkdotnet.org/articles/configs/diagnosers.html#restrictions">MemoryDiagnoser has an accuracy of 99.5%</a>, which means slight measurement errors can occur.)</p><p>In simple cases, operations that require intermediate buffers like Distinct or OrderBy show large differences because aggressive pooling significantly reduces allocations (ZLinq uses somewhat aggressive pooling since it’s primarily based on ref struct, which is expected to be short-lived):</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/379/1*AmIHnJJT-svEvUCL72Hwuw.png" /></figure><p>LINQ applies special optimizations based on method call patterns, so reducing allocations alone isn’t enough to always outperform it. For operator chain optimizations, such as those introduced in .NET 9 and described in <a href="https://devblogs.microsoft.com/dotnet/performance-improvements-in-net-9/">Performance Improvements in .NET 9</a>, ZLinq implements all these optimizations to achieve even higher performance:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/563/1*fdpvCMz_vCFG_Pz7l1Ht2w.png" /></figure><p>A great benefit of ZLinq is that these LINQ evolution optimizations become available to all .NET generations (including .NET Framework), not just the latest versions.</p><p>Usage is simple — just add an AsValueEnumerable() call. Since all operators are 100% covered, replacing existing code works without issues:</p><pre>using ZLinq;<br><br>var seq = source<br>    .AsValueEnumerable() // only add this line<br>    .Where(x =&gt; x % 2 == 0)<br>    .Select(x =&gt; x * 3);<br>foreach (var item in seq) { }</pre><p>To ensure behavior compatibility, ZLinq ports System.Linq.Tests from dotnet/runtime and continuously runs them at <a href="https://github.com/Cysharp/ZLinq/tree/main/tests/System.Linq.Tests">ZLinq/System.Linq.Tests</a>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/559/0*g2Y4revC1BE8Pd70.png" /></figure><p>9000 test cases guarantee behavior (Skip cases are due to ref struct limitations where identical test code can’t be run, etc.)</p><p>Additionally, ZLinq provides a Source Generator for Drop-In Replacement that can optionally eliminate even the need for AsValueEnumerable():</p><pre>[assembly: ZLinq.ZLinqDropInAttribute(&quot;&quot;, ZLinq.DropInGenerateTypes.Everything)]</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/326/0*L96lrxd9neATfZMF.jpg" /></figure><p>This mechanism allows you to freely control the scope of the Drop-In Replacement. ZLinq/System.Linq.Tests itself uses Drop-In Replacement to run existing test code with ZLinq without changing the tests.</p><h3>ValueEnumerable Architecture and Optimization</h3><p>For usage, please refer to the ReadMe. Here, I’ll delve deeper into optimization. The architectural distinction goes beyond simply implementing lazy sequence execution, containing many innovations compared to collection processing libraries in other languages.</p><p>The definition of ValueEnumerable&lt;T&gt;, which forms the basis of chaining, looks like this:</p><pre>public readonly ref struct ValueEnumerable&lt;TEnumerator, T&gt;(TEnumerator enumerator)<br>    where TEnumerator : struct, IValueEnumerator&lt;T&gt;, allows ref struct // allows ref struct only in .NET 9 or later<br>{<br>    public readonly TEnumerator Enumerator = enumerator;<br>}<br><br>public interface IValueEnumerator&lt;T&gt; : IDisposable<br>{<br>    bool TryGetNext(out T current); // as MoveNext + Current<br>    // Optimization helper<br>    bool TryGetNonEnumeratedCount(out int count);<br>    bool TryGetSpan(out ReadOnlySpan&lt;T&gt; span);<br>    bool TryCopyTo(scoped Span&lt;T&gt; destination, Index offset);<br>}</pre><p>Based on this, operators like Where chain as follows:</p><pre>public static ValueEnumerable&lt;Where&lt;TEnumerator, TSource&gt;, TSource&gt; Where&lt;TEnumerator, TSource&gt;(this ValueEnumerable&lt;TEnumerator, TSource&gt; source, Func&lt;TSource, Boolean&gt; predicate)<br>    where TEnumerator : struct, IValueEnumerator&lt;TSource&gt;, allows ref struct</pre><p>We chose this approach rather than using IValueEnumerable&lt;T&gt; because with a definition like (this TEnumerable source) where TEnumerable : struct, IValueEnumerable&lt;TSource&gt;, type inference for TSource would fail. This is due to a C# language limitation where type inference doesn&#39;t work from type parameter constraints (<a href="https://github.com/dotnet/csharplang/discussions/6930">dotnet/csharplang#6930</a>). If implemented with that definition, it would require defining instance methods for a vast number of combinations. <a href="https://github.com/kevin-montrose/LinqAF">LinqAF</a> took that approach, resulting in <a href="https://kevinmontrose.com/2018/01/17/linqaf-replacing-linq-and-not-allocating/">100,000+ methods and massive assembly sizes</a>, which wasn&#39;t ideal.</p><p>In LINQ, all implementation is in IValueEnumerator&lt;T&gt;, and since all Enumerators are structs, I realized that instead of using GetEnumerator(), we could simply copy-pass the common Enumerator, allowing each Enumerator to process with its independent state. This led to the final structure of wrapping IValueEnumerator&lt;T&gt; with ValueEnumerable&lt;TEnumerator, T&gt;. This way, types appear in type declarations rather than constraints, avoiding type inference issues.</p><h3>TryGetNext</h3><p>Let’s examine MoveNext, the core of iteration, in more detail:</p><pre>// Traditional interface<br>public interface IEnumerator&lt;out T&gt; : IDisposable<br>{<br>    bool MoveNext();<br>    T Current { get; }<br>}<br><br>// iterate example<br>while (e.MoveNext())<br>{<br>    var item = e.Current; // invoke get_Current()<br>}<br>// ZLinq interface<br>public interface IValueEnumerator&lt;T&gt; : IDisposable<br>{<br>    bool TryGetNext(out T current);<br>}<br>// iterate example<br>while (e.TryGetNext(out var item))<br>{<br>}</pre><p>C#’s foreach expands to MoveNext() + Current, which presents two issues. First, each iteration requires two method calls: MoveNext and get_Current. Second, Current requires holding a variable. Therefore, I combined them into bool TryGetNext(out T current). This reduces method calls to one per iteration, improving performance.</p><p>This bool TryGetNext(out T current) approach is also used in <a href="https://doc.rust-lang.org/std/iter/trait.Iterator.html">Rust&#39;s iterator</a>:</p><pre>pub trait Iterator {<br>    type Item;<br>    // Required method<br>    fn next(&amp;mut self) -&gt; Option&lt;Self::Item&gt;;<br>}</pre><p>To understand the variable holding issue, let’s look at the Select implementation:</p><pre>public sealed class LinqSelect&lt;TSource, TResult&gt;(IEnumerator&lt;TSource&gt; source, Func&lt;TSource, TResult&gt; selector) : IEnumerator&lt;TResult&gt;<br>{<br>    // Three fields<br>    IEnumerator&lt;TSource&gt; source = source;<br>    Func&lt;TSource, TResult&gt; selector = selector;<br>    TResult current = default!;<br><br>    public TResult Current =&gt; current;<br><br>    public bool MoveNext()<br>    {<br>        if (source.MoveNext())<br>        {<br>            current = selector(source.Current);<br>            return true;<br>        }<br>        return false;<br>    }<br>}<br><br>public ref struct ZLinqSelect&lt;TEnumerator, TSource, TResult&gt;(TEnumerator source, Func&lt;TSource, TResult&gt; selector) : IValueEnumerator&lt;TResult&gt;<br>    where TEnumerator : struct, IValueEnumerator&lt;TSource&gt;, allows ref struct<br>{<br>    // Two fields<br>    TEnumerator source = source;<br>    Func&lt;TSource, TResult&gt; selector = selector;<br>    public bool TryGetNext(out TResult current)<br>    {<br>        if (source.TryGetNext(out var value))<br>        {<br>            current = selector(value);<br>            return true;<br>        }<br>        current = default!;<br>        return false;<br>    }<br>}</pre><p>IEnumerator&lt;T&gt; requires a current field because it advances with MoveNext() and returns with Current. However, ZLinq advances and returns values simultaneously, eliminating the need to store the field. This makes a significant difference in ZLinq&#39;s struct-based architecture. Since ZLinq embraces a structure where each method chain encompasses the previous struct entirely (TEnumerator being a struct), struct size grows with each method chain. While performance remains acceptable within reasonable method chain lengths, smaller structs mean lower copy costs and better performance. The adoption of TryGetNext was essential to minimize struct size.</p><p>A drawback of TryGetNext is that it cannot support covariance and contravariance. However, I believe iterators and arrays should abandon covariance/contravariance support altogether. They’re incompatible with Span&lt;T&gt;, making them outdated concepts when weighing pros and cons. For example, array Span conversion can fail at runtime without compile-time detection:</p><pre>// Due to generic variance, Derived[] is accepted by Base[]<br>Base[] array = new Derived[] { new Derived(), new Derived() };<br><br>// In this case, casting to Span&lt;T&gt; or using AsSpan() causes a runtime error!<br>// System.ArrayTypeMismatchException: Attempted to access an element as a type incompatible with the array.<br>Span&lt;Base&gt; foo = array;<br>class Base;<br>class Derived : Base;</pre><p>While this behavior exists because these features were added before Span&lt;T&gt;, it&#39;s problematic in modern .NET where Span is widely used, making features that can cause runtime errors practically unusable.</p><h3>TryGetNonEnumeratedCount / TryGetSpan / TryCopyTo</h3><p>Naively enumerating everything doesn’t maximize performance. For example, when calling ToArray, if the size doesn’t change (e.g., array.Select().ToArray()), we can create a fixed-length array with new T[count]. System.LINQ internally uses an Iterator&lt;T&gt; type for such optimizations, but since the parameter is IEnumerable&lt;T&gt;, code like if (source is Iterator&lt;TSource&gt; iterator) is always needed.</p><p>Since ZLinq is designed specifically for LINQ from the start, we’ve prepared for these optimizations. To avoid assembly size bloat, we’ve carefully selected the minimal set of definitions that provide maximum effect, resulting in these three methods.</p><p>TryGetNonEnumeratedCount(out int count) succeeds when the original source has a finite count and no filtering methods (Where, Distinct, etc., though Take and Skip are calculable) intervene. This benefits ToArray and methods requiring intermediate buffers like OrderBy and Shuffle.</p><p>TryGetSpan(out ReadOnlySpan&lt;T&gt; span) potentially delivers dramatic performance improvements when the source can be accessed as contiguous memory, enabling SIMD operations or Span-based loop processing for aggregation performance.</p><p>TryCopyTo(scoped Span&lt;T&gt; destination, Index offset) enhances performance through internal iterators. To explain external vs. internal iterators, consider that List&lt;T&gt; offers both foreach and ForEach:</p><pre>// external iterator<br>foreach (var item in list) { Do(item); }<br><br>// internal iterator<br>list.ForEach(Do);</pre><p>They look similar but perform differently. Breaking down the implementations:</p><pre>// external iterator<br>List&lt;T&gt;.Enumerator e = list.GetEnumerator();<br>while (e.MoveNext())<br>{<br>    var item = e.Current;<br>    Do(item);<br>}<br><br>// internal iterator<br>for (int i = 0; i &lt; _size; i++)<br>{<br>    action(_items[i]);<br>}</pre><p>This becomes a competition between delegate call overhead (+ delegate creation allocation) vs. iterator MoveNext + Current calls. The iteration speed itself is faster with internal iterators. In some cases, delegate calls may be lighter, making internal iterators potentially advantageous in benchmarks.</p><p>Of course, this varies case by case, and since lambda captures and normal control flow (like continue, break, await, etc…) aren’t available, I personally believe ForEach shouldn&#39;t be used, nor should custom extension methods be defined to mimic it. However, this structural difference exists.</p><p>TryCopyTo(scoped Span&lt;T&gt; destination, Index offset) achieves limited internal iteration by accepting a Span rather than a delegate.</p><p>Using Select as an example, for ToArray when Count is available, it passes a Span for internal iteration:</p><pre>public ref struct Select<br>{<br>    public bool TryCopyTo(Span&lt;TResult&gt; destination, Index offset)<br>    {<br>        if (source.TryGetSpan(out var span))<br>        {<br>            if (EnumeratorHelper.TryGetSlice(span, offset, destination.Length, out var slice))<br>            {<br>                // loop inlining<br>                for (var i = 0; i &lt; slice.Length; i++)<br>                {<br>                    destination[i] = selector(slice[i]);<br>                }<br>                return true;<br>            }<br>        }<br>        return false;<br>    }<br>}<br><br>// ------------------<br><br>// ToArray<br>if (enumerator.TryGetNonEnumeratedCount(out var count))<br>{<br>    var array = GC.AllocateUninitializedArray&lt;TSource&gt;(count);<br>    // try internal iterator<br>    if (enumerator.TryCopyTo(array.AsSpan(), 0))<br>    {<br>        return array;<br>    }<br>    // otherwise, use external iterator<br>    var i = 0;<br>    while (enumerator.TryGetNext(out var item))<br>    {<br>        array[i] = item;<br>        i++;<br>    }<br>    return array;<br>}</pre><p>Thus, while Select can’t create a Span, if the original source can, processing as an internal iterator accelerates loop processing.</p><p>TryCopyTo differs from regular CopyTo by including an Index offset and allowing destination to be smaller than the source (normal .NET CopyTo fails if destination is smaller). This enables ElementAt representation when destination size is 1 - index 0 becomes First, ^1 becomes Last. Adding First, Last, ElementAt directly to IValueEnumerator&lt;T&gt; would create redundancy in class definitions (affecting assembly size), but combining small destinations with Index allows one method to cover more optimization cases:</p><pre>public static TSource ElementAt&lt;TEnumerator, TSource&gt;(this ValueEnumerable&lt;TEnumerator, TSource&gt; source, Index index)<br>    where TEnumerator : struct, IValueEnumerator&lt;TSource&gt;, allows ref struct<br>{<br>    using var enumerator = source.Enumerator;<br>    var value = default(TSource)!;<br>    var span = new Span&lt;T&gt;(ref value); // create single span<br>    if (enumerator.TryCopyTo(span, index))<br>    {<br>        return value;<br>    }<br>    // else...<br>}</pre><h3>LINQ to Span</h3><p>In .NET 9 and above, ZLinq allows chaining all LINQ operators on Span&lt;T&gt; and ReadOnlySpan&lt;T&gt;:</p><pre>using ZLinq;<br><br>// Can also be applied to Span (only in .NET 9/C# 13 environments that support allows ref struct)<br>Span&lt;int&gt; span = stackalloc int[5] { 1, 2, 3, 4, 5 };<br>var seq1 = span.AsValueEnumerable().Select(x =&gt; x * x);<br>// If enables Drop-in replacement, you can call LINQ operator directly.<br>var seq2 = span.Select(x =&gt; x);</pre><p>While some libraries claim to support LINQ for Spans, they typically only define extension methods for Span&lt;T&gt; without a generic mechanism. They offer limited operators due to language constraints that previously prevented receiving Span&lt;T&gt; as a generic parameter. Generic processing became possible with the introduction of allows ref struct in .NET 9.</p><p>In ZLinq, there’s no distinction between IEnumerable&lt;T&gt; and Span&lt;T&gt; - they&#39;re treated equally.</p><p>However, since allows ref struct requires language/runtime support, while ZLinq supports all .NET versions from .NET Standard 2.0 up, Span support is limited to .NET 9 and above. This means in .NET 9+, all operators are ref struct, which differs from earlier versions.</p><h3>LINQ to SIMD</h3><p>System.Linq accelerates certain aggregation methods with SIMD. For example, calling Sum or Max directly on primitive type arrays provides faster processing than using a for loop. However, being based on IEnumerable&lt;T&gt;, applicable types are limited. ZLinq makes this more generic through IValueEnumerator.TryGetSpan, targeting collections where Span&lt;T&gt; can be obtained (including direct Span&lt;T&gt; application).</p><p>Supported methods include:</p><ul><li><strong>Range</strong> to ToArray/ToList/CopyTo/etc…</li><li><strong>Repeat</strong> for unmanaged struct and size is power of 2 to ToArray/ToList/CopyTo/etc...</li><li><strong>Sum</strong> for sbyte, short, int, long, byte, ushort, uint, ulong, double</li><li><strong>SumUnchecked</strong> for sbyte, short, int, long, byte, ushort, uint, ulong, double</li><li><strong>Average</strong> for sbyte, short, int, long, byte, ushort, uint, ulong, double</li><li><strong>Max</strong> for byte, sbyte, short, ushort, int, uint, long, ulong, nint, nuint, Int128, UInt128</li><li><strong>Min</strong> for byte, sbyte, short, ushort, int, uint, long, ulong, nint, nuint, Int128, UInt128</li><li><strong>Contains</strong> for byte, sbyte, short, ushort, int, uint, long, ulong, bool, char, nint, nuint</li><li><strong>SequenceEqual</strong> for byte, sbyte, short, ushort, int, uint, long, ulong, bool, char, nint, nuint</li></ul><p>Sum checks for overflow, which adds overhead. We&#39;ve added a custom SumUnchecked method that&#39;s faster:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/417/1*G2sYFpJBXZqB-YLSpDWc2Q.png" /></figure><p>Since these methods apply implicitly when conditions match, understanding the internal pipeline is necessary to target SIMD application. Therefore, for T[], Span&lt;T&gt;, or ReadOnlySpan&lt;T&gt;, we provide the .AsVectorizable() method to explicitly call SIMD-applicable operations like Sum, SumUnchecked, Average, Max, Min, Contains, and SequenceEqual (though these fall back to normal processing when Vector.IsHardwareAccelerated &amp;&amp; Vector&lt;T&gt;.IsSupported is false).</p><p>int[] or Span&lt;int&gt; gain the VectorizedFillRange method, which performs the same operation as ValueEunmerable.Range().CopyTo(), filling with sequential numbers using SIMD acceleration. This is much faster than filling with a for loop when needed:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/253/1*Ri8uzpiLAVhDMmxbsr8r4g.png" /></figure><h3>Vectorizable Methods</h3><p>Handwriting SIMD loop processing requires practice and effort. We’ve provided helpers that take Func arguments for casual use. While these incur delegate overhead and perform worse than inline code, they’re convenient for casual SIMD processing. They accept Func&lt;Vector&lt;T&gt;, Vector&lt;T&gt;&gt; vectorFunc and Func&lt;T, T&gt; func, processing with Vector&lt;T&gt; where possible and handling remainder with Func&lt;T&gt;.</p><p>T[] and Span&lt;T&gt; offer the VectorizedUpdate method:</p><pre>using ZLinq.Simd; // needs using<br><br>int[] source = Enumerable.Range(0, 10000).ToArray();<br>[Benchmark]<br>public void For()<br>{<br>    for (int i = 0; i &lt; source.Length; i++)<br>    {<br>        source[i] = source[i] * 10;<br>    }<br>}<br>[Benchmark]<br>public void VectorizedUpdate()<br>{<br>    // arg1: Vector&lt;int&gt; =&gt; Vector&lt;int&gt;<br>    // arg2: int =&gt; int<br>    source.VectorizedUpdate(static x =&gt; x * 10, static x =&gt; x * 10);<br>}</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/514/1*Y-aqbg0_LqeaubfqbWvDLg.png" /></figure><p>While faster than for loops, performance varies by machine environment and size, so verification is recommended for each use case.</p><p>AsVectorizable() provides Aggregate, All, Any, Count, Select, and Zip:</p><pre>source.AsVectorizable().Aggregate((x, y) =&gt; Vector.Min(x, y), (x, y) =&gt; Math.Min(x, y))<br>source.AsVectorizable().All(x =&gt; Vector.GreaterThanAll(x, new(5000)), x =&gt; x &gt; 5000);<br>source.AsVectorizable().Any(x =&gt; Vector.LessThanAll(x, new(5000)), x =&gt; x &lt; 5000);<br>source.AsVectorizable().Count(x =&gt; Vector.GreaterThan(x, new(5000)), x =&gt; x &gt; 5000);</pre><p>Performance depends on data, but Count can show significant differences:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/464/1*1EH7j7GECfsKqpevUGIitg.png" /></figure><p>For Select and Zip, you follow with either ToArray or CopyTo:</p><pre>// Select<br>source.AsVectorizable().Select(x =&gt; x * 3, x =&gt; x * 3).ToArray();<br>source.AsVectorizable().Select(x =&gt; x * 3, x =&gt; x * 3).CopyTo(destination);<br><br>// Zip2<br>array1.AsVectorizable().Zip(array2, (x, y) =&gt; x + y, (x, y) =&gt; x + y).CopyTo(destination);<br>array1.AsVectorizable().Zip(array2, (x, y) =&gt; x + y, (x, y) =&gt; x + y).ToArray();<br>// Zip3<br>array1.AsVectorizable().Zip(array2, array3, (x, y, z) =&gt; x + y + z, (x, y, z) =&gt; x + y + z).CopyTo(destination);<br>array1.AsVectorizable().Zip(array2, array3, (x, y, z) =&gt; x + y + z, (x, y, z) =&gt; x + y + z).ToArray();</pre><p>Zip can be particularly interesting and fast for certain use cases (like merging two Vec3):</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/301/1*H4rCujrKgg_UnUqOoe_Ehg.png" /></figure><h3>LINQ to Tree</h3><p>Have you used LINQ to XML? In 2008 when LINQ appeared, XML was still dominant, and LINQ to XML’s usability was shocking. Now that JSON has taken over, LINQ to XML is rarely used.</p><p>However, LINQ to XML’s value lies in being a reference design for LINQ-style operations on tree structures — a guideline for making tree structures LINQ-compatible. Tree traversal abstractions work excellently with LINQ to Objects. A prime example is working with Roslyn’s SyntaxTree, where methods like Descendants are commonly used in Analyzers and Source Generators.</p><p>ZLinq extends this concept by defining an interface that generically enables Ancestors, Children, Descendants, BeforeSelf, and AfterSelf for tree structures:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/514/0*DGe5YzNSr82n3l3H.jpg" /></figure><p>This diagram shows traversal of Unity’s GameObject, but we’ve included standard implementations for FileSystem (DirectoryTree) and JSON (enabling LINQ to XML-style operations on System.Text.Json’s JsonNode). Of course, you can implement the interface for custom types:</p><pre>public interface ITraverser&lt;TTraverser, T&gt; : IDisposable<br>    where TTraverser : struct, ITraverser&lt;TTraverser, T&gt; // self<br>{<br>    T Origin { get; }<br>    TTraverser ConvertToTraverser(T next); // for Descendants<br>    bool TryGetHasChild(out bool hasChild); // optional: optimize use for Descendants<br>    bool TryGetChildCount(out int count);   // optional: optimize use for Children<br>    bool TryGetParent(out T parent); // for Ancestors<br>    bool TryGetNextChild(out T child); // for Children | Descendants<br>    bool TryGetNextSibling(out T next); // for AfterSelf<br>    bool TryGetPreviousSibling(out T previous); // BeforeSelf<br>}</pre><p>For JSON, you can write:</p><pre>var json = JsonNode.Parse(&quot;&quot;&quot;<br>// snip...<br>&quot;&quot;&quot;);<br><br>// JsonNode<br>var origin = json![&quot;nesting&quot;]![&quot;level1&quot;]![&quot;level2&quot;]!;<br>// JsonNode axis, Children, Descendants, Anestors, BeforeSelf, AfterSelf and ***Self.<br>foreach (var item in origin.Descendants().Select(x =&gt; x.Node).OfType&lt;JsonArray&gt;())<br>{<br>    // [true, false, true], [&quot;fast&quot;, &quot;accurate&quot;, &quot;balanced&quot;], [1, 1, 2, 3, 5, 8, 13]<br>    Console.WriteLine(item.ToJsonString(JsonSerializerOptions.Web));<br>}</pre><p>We’ve included standard LINQ to Tree implementations for Unity’s GameObject and Transform and Godot&#39;s Node. Since allocation and traversal performance are carefully optimized, they might even be faster than manual loops.</p><h3>OSS and Me</h3><p>There have been several incidents in .NET-related OSS in recent months, including the commercialization of well-known OSS projects. With over 40 OSS projects under <a href="https://github.com/Cysharp">github/Cysharp</a> and more under my personal and other organizations like MessagePack, totaling over 50,000 stars, I believe I’m one of the largest OSS providers in the .NET ecosystem.</p><p>Regarding commercialization, I have no plans for it, but maintenance has become challenging due to growing scale. A major factor in OSS projects attempting commercialization despite criticism is the mental burden on maintainers (compensation doesn’t match time investment). I experience this too!</p><p>Setting aside financial aspects, my request is for users to accept occasional maintenance delays! When developing large libraries like ZLinq, I need focused time, which means Issues and PRs for other libraries might go without response for months. I intentionally avoid looking at them, not even reading titles (avoiding dashboards and notification emails). This seemingly neglectful approach is necessary to create innovative libraries — a necessary sacrifice!</p><p>Even without that, the sheer number of libraries means rotation delays of months are inevitable. This is unavoidable due to absolute manpower shortage, so please accept these delays and don’t claim “this library is dead” just because responses are slow. That’s painful to hear! I try my best, but creating new libraries consumes tremendous time, causing cascading delays that drain my mental energy.</p><p>Also, irritations related to Microsoft can reduce motivation — a common experience for C# OSS maintainers. Despite this, I hope to continue long-term.</p><h3>Conclusion</h3><p>ZLinq’s structure changed significantly after feedback from the initial preview release. <a href="https://github.com/Akeit0">@Akeit0</a> provided many proposals for core performance-critical elements like the ValueEnumerable&lt;TEnumerator, T&gt; definition and adding Index to TryCopyTo. <a href="https://github.com/filzrev">@filzrev</a> contributed extensive test and benchmark infrastructure. Ensuring compatibility and performance improvements wouldn&#39;t have been possible without their contributions, for which I&#39;m deeply grateful.</p><p>While zero-allocation LINQ libraries aren’t novel, ZLinq’s thoroughness sets it apart. With experience and knowledge, driven by sheer determination, we implemented all methods, ran all test cases for complete compatibility, and implemented all optimizations including SIMD. This was truly challenging!</p><p>The timing was perfect as .NET 9/C# 13 provided all the language features needed for a full implementation. Simultaneously, maintaining support for Unity and .NET Standard 2.0 was also important.</p><p>Beyond being just a zero-allocation LINQ, LINQ to Tree is a favorite feature that I hope people will try!</p><p>One LINQ performance bottleneck is delegates, and some libraries adopt a ValueDelegate approach using structs to mimic Func. We deliberately avoided this because such definitions are impractical due to their complexity. It’s better to write inline code than use LINQ with ValueDelegate structures. Complicating internal structure and bloating assembly size for benchmark hacks is wasteful, so we accept only System.Linq-compatible.</p><p><a href="https://github.com/Cysharp/R3">R3</a> was an ambitious library intended to replace .NET’s standard System.Reactive, but replacing System.Linq would be a much larger or perhaps excessive undertaking, so I think there might be some resistance to adoption. However, I believe we’ve demonstrated sufficient benefits to justify the replacement, so I’d be very happy if you could try it out!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=1bb0a3e5c749" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[MasterMemory v3 — A Fast Read-Only In-Memory Database for C# with Source Generator Support]]></title>
            <link>https://neuecc.medium.com/mastermemory-v3-a-fast-read-only-in-memory-database-for-c-with-source-generator-support-f9689466d6cc?source=rss-61fea4eebf07------2</link>
            <guid isPermaLink="false">https://medium.com/p/f9689466d6cc</guid>
            <category><![CDATA[csharp]]></category>
            <dc:creator><![CDATA[Yoshifumi Kawai]]></dc:creator>
            <pubDate>Fri, 20 Dec 2024 09:57:57 GMT</pubDate>
            <atom:updated>2024-12-20T09:57:57.553Z</atom:updated>
            <content:encoded><![CDATA[<h3>MasterMemory v3 — A Fast Read-Only In-Memory Database for C# with Source Generator Support</h3><p>I’ve released <a href="https://github.com/Cysharp/MasterMemory">MasterMemory</a> v3! It finally supports Source Generators!</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/488/0*soVMUa2bRNE26zR2" /></figure><p>MasterMemory is a C# in-memory database that is fast, memory-efficient, and type-safe. It’s <em>4700</em> times faster than using SQLite directly!</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/947/0*AauGfv5ObLTlYoxu.png" /></figure><p>Originally, MasterMemory had an advanced design philosophy of generating C# code from C# code, doing Source Generator-like tasks in an era before Source Generators existed. When porting it now, I was impressed by how smoothly it could be ported and how the legacy code worked without any modifications. The times have finally caught up…</p><p>As such, database construction code and query portions are automatically generated by Source Generator from C# definitions like this:</p><pre>[MemoryTable(&quot;person&quot;), MessagePackObject(true)]<br>public record Person<br>{<br>    [PrimaryKey]<br>    public required int PersonId { get; init; }<br>    <br>    [SecondaryKey(0), NonUnique]<br>    [SecondaryKey(1, keyOrder: 1), NonUnique]<br>    public required int Age { get; init; }<br><br>    [SecondaryKey(2), NonUnique]<br>    [SecondaryKey(1, keyOrder: 0), NonUnique]<br>    public required Gender Gender { get; init; }<br><br>    public required string Name { get; init; }<br>}</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/394/0*NOL0oRfNwPNGiXTS.png" /></figure><p>Since it’s generated as C# code, not only do all queries have input completion and type-safe return values, but it also contributes to better performance.</p><p>Since it’s used as a read-only database, immutable class definitions are preferable, and with recent C# features like record, init, and required, it&#39;s become even more convenient to use as a Readonly Database. While required isn&#39;t available in Unity, record and init are, so there&#39;s no problem using it with Unity.</p><p>Note that the Unity version is now provided through NuGetForUnity. Also, it requires MessagePack for C# v3, which supports Source Generator.</p><p>MasterMemory is actually quite widely used. I’ve started to see it being adopted in games more frequently. So I’m really happy that we’ve finally resolved the hassle of code generation from external tools, which had been causing quite some concern!</p><p>Migration from v2 to v3 shouldn’t be too difficult. We deliberately avoided touching the quality of generated code, core functions, and method signatures, so it should work right out of the box just by removing the parts where you were running the command-line tool. Just make sure to set the namespace using assembly attributes.</p><p>Additionally, we’ve added support for records (which we hadn’t done before!) and #nullable enable (which we also hadn’t done before!), so the usability should be improved beyond just the generated parts.</p><p>In the future, we’re considering adding <a href="https://github.com/Cysharp/MemoryPack">MemoryPack</a> support, modernizing the API further (it’s currently netstandard2.0, so it’s old), and making overall improvements (like replacing generated code parts such as ImmutableBuilder). There’s a lot we can do, so I hope we can work on these improvements when the opportunity arises.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=f9689466d6cc" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[ConsoleAppFramework v5.3.0]]></title>
            <link>https://neuecc.medium.com/consoleappframework-v5-3-0-d627dea393e7?source=rss-61fea4eebf07------2</link>
            <guid isPermaLink="false">https://medium.com/p/d627dea393e7</guid>
            <category><![CDATA[csharp]]></category>
            <dc:creator><![CDATA[Yoshifumi Kawai]]></dc:creator>
            <pubDate>Tue, 17 Dec 2024 03:39:50 GMT</pubDate>
            <atom:updated>2024-12-17T08:35:06.887Z</atom:updated>
            <content:encoded><![CDATA[<h3><strong>ConsoleAppFramework v5.3.0 — Enhanced DI Integration through Auto-generated Methods from NuGet References, and More</strong></h3><p>I’ve made a relatively significant update to ConsoleAppFramework v5! For details about v5 itself, please refer to my previous article <a href="https://medium.com/@neuecc/consoleappframework-v5-zero-overhead-native-aot-compatible-cli-framework-for-c-8f496df8d9d1">ConsoleAppFramework v5 — Zero Overhead, Native AOT-compatible CLI Framework for C#</a>. While v5 introduced some interesting concepts that were well-received, it did sacrifice some usability aspects. This update addresses those issues, and I believe it has significantly improved the overall user experience!</p><h3>Disabling Automatic Name Conversion</h3><p>By default, command names and option names are automatically converted to kebab-case. While this follows standard command-line tool naming conventions, it might feel cumbersome when using the framework for internal applications or batch file creation. Therefore, we’ve added the ability to disable this conversion at the assembly level.</p><pre>using ConsoleAppFramework;<br><br>[assembly: ConsoleAppFrameworkGeneratorOptions(DisableNamingConversion = true)]<br><br>var app = ConsoleApp.Create();<br>app.Add&lt;MyProjectCommand&gt;();<br>app.Run(args);<br><br>public class MyProjectCommand<br>{<br>    public void Execute(string fooBarBaz)<br>    {<br>        Console.WriteLine(fooBarBaz);<br>    }<br>}</pre><p>The automatic conversion is disabled by [assembly: ConsoleAppFrameworkGeneratorOptions(DisableNamingConversion = true)]. In this example, the command becomes ExecuteCommand --fooBarBaz.</p><p>From an implementation perspective, while many Source Generators use AdditionalFiles with JSON or custom format files (like BannedSymbols.txt in BannedApiAnalyzers) to provide configuration, using files can be quite cumbersome. For setting just one or two boolean values, using assembly attributes is the most straightforward approach.</p><p>The implementation can pull this from CompilationProvider using Assembly.GetAttributes:</p><pre>var generatorOptions = context.CompilationProvider.Select((compilation, token) =&gt;<br>{<br>    foreach (var attr in compilation.Assembly.GetAttributes())<br>    {<br>        if (attr.AttributeClass?.Name == &quot;ConsoleAppFrameworkGeneratorOptionsAttribute&quot;)<br>        {<br>            var args = attr.NamedArguments;<br>            var disableNamingConversion = args.FirstOrDefault(x =&gt; x.Key == &quot;DisableNamingConversion&quot;).Value.Value as bool? ?? false;<br>            return new ConsoleAppFrameworkGeneratorOptions(disableNamingConversion);<br>        }<br>    }<br><br>    return new ConsoleAppFrameworkGeneratorOptions(DisableNamingConversion: false);<br>});</pre><p>By combining this with Source from other SyntaxProviders, we can reference the attribute values during generation.</p><h3>ConfigureServices/Logging/Configuration</h3><p>ConsoleAppFramework v5 had a constraint where it couldn’t generate code dependent on specific libraries due to its zero-dependency principle. This meant that integrating with DI required manually building the ServiceProvider, adding an extra step for users. To address this, we’ve added functionality that analyzes NuGet DLL references and makes the ConfigureServices method available on ConsoleAppBuilder when Microsoft.Extensions.DependencyInjectionis referenced.</p><pre>var app = ConsoleApp.Create()<br>    .ConfigureServices(service =&gt;<br>    {<br>        service.AddTransient&lt;MyService&gt;();<br>    });<br><br>app.Add(&quot;&quot;, ([FromServices] MyService service, int x, int y) =&gt; Console.WriteLine(x + y));<br><br>app.Run(args);</pre><p>This provides a new experience where the framework itself maintains zero dependencies while still being able to generate library-dependent code. This is achieved by pulling from MetadataReferencesProvider and feeding it into the generation process:</p><pre>var hasDependencyInjection = context.MetadataReferencesProvider<br>    .Collect()<br>    .Select((xs, _) =&gt;<br>    {<br>        var hasDependencyInjection = false;<br><br>        foreach (var x in xs)<br>        {<br>            var name = x.Display;<br>            if (name == null) continue;<br><br>            if (!hasDependencyInjection &amp;&amp; name.EndsWith(&quot;Microsoft.Extensions.DependencyInjection.dll&quot;))<br>            {<br>                hasDependencyInjection = true;<br>                continue;<br>            }<br><br>            // etc...<br>        }<br><br>        return new DllReference(hasDependencyInjection, hasLogging, hasConfiguration, hasJsonConfiguration, hasHost);<br>    });<br><br>context.RegisterSourceOutput(hasDependencyInjection, EmitConsoleAppConfigure);</pre><p>Reference analysis is performed for multiple dependencies. For example, if Microsoft.Extensions.Loggingis referenced, ConfigureLogging becomes available. This allows for clean integration with <a href="https://github.com/Cysharp/ZLogger">ZLogger</a>:</p><pre>// Package Import: ZLogger<br>var app = ConsoleApp.Create()<br>    .ConfigureLogging(x =&gt;<br>    {<br>        x.ClearProviders();<br>        x.SetMinimumLevel(LogLevel.Trace);<br>        x.AddZLoggerConsole();<br>        x.AddZLoggerFile(&quot;log.txt&quot;);<br>    });<br><br>app.Add&lt;MyCommand&gt;();<br>app.Run(args);<br><br>// inject logger to constructor<br>public class MyCommand(ILogger&lt;MyCommand&gt; logger)<br>{<br>    public void Echo(string msg)<br>    {<br>        logger.ZLogInformation($&quot;Message is {msg}&quot;);<br>    }<br>}</pre><p>Loading configuration from appsettings.json is now a common pattern, and when Microsoft.Extensions.Configuration.Json is referenced, ConfigureDefaultConfiguration becomes available. This automatically performs SetBasePath(System.IO.Directory.GetCurrentDirectory()) and AddJsonFile(&quot;appsettings.json&quot;, optional: true) (additional configuration via Action is possible, and ConfigureEmptyConfiguration is also available).</p><p>This makes it simple to read configuration, bind it to classes, and inject it into commands:</p><pre>// Package Import: Microsoft.Extensions.Configuration.Json<br>var app = ConsoleApp.Create()<br>    .ConfigureDefaultConfiguration()<br>    .ConfigureServices((configuration, services) =&gt;<br>    {<br>        // Package Import: Microsoft.Extensions.Options.ConfigurationExtensions<br>        services.Configure&lt;PositionOptions&gt;(configuration.GetSection(&quot;Position&quot;));<br>    });<br><br>app.Add&lt;MyCommand&gt;();<br>app.Run(args);<br><br>// inject options<br>public class MyCommand(IOptions&lt;PositionOptions&gt; options)<br>{<br>    public void Echo(string msg)<br>    {<br>        ConsoleApp.Log($&quot;Binded Option: {options.Value.Title} {options.Value.Name}&quot;);<br>    }<br>}</pre><p>For those wanting to build with Microsoft.Extensions.Hosting, ToConsoleAppBuilder becomes available when Microsoft.Extensions.Hosting is referenced:</p><pre>// Package Import: Microsoft.Extensions.Hosting<br>var app = Host.CreateApplicationBuilder()<br>    .ToConsoleAppBuilder();</pre><p>Additionally, the configured IServiceProvider is now automatically disposed after Run or RunAsync completes.</p><h3>RegisterCommands from Attribute</h3><p>While commands previously required Add or Add&lt;T&gt;, we&#39;ve added functionality to automatically add commands through class attributes:</p><pre>[RegisterCommands]<br>public class Foo<br>{<br>    public void Baz(int x)<br>    {<br>        Console.Write(x);<br>    }<br>}<br><br>[RegisterCommands(&quot;bar&quot;)]<br>public class Bar<br>{<br>    public void Baz(int x)<br>    {<br>        Console.Write(x);<br>    }<br>}</pre><p>These are automatically added:</p><pre>var app = ConsoleApp.Create();<br><br>// Commands:<br>//   baz<br>//   bar baz<br>app.Run(args);</pre><p>You can still use Add and Add&lt;T&gt; alongside these attribute-based registrations.</p><p>Initially, we planned to allow arbitrary attributes, but due to IncrementalGenerator API limitations, we&#39;re restricted to the fixed RegisterCommands attribute. Inheritance is also not supported.</p><p>Since the v5 release, we’ve continued making improvements, including allowing filters to be defined in external assemblies and optimizing the Incremental Generator implementation for better performance. The framework has evolved into an excellent solution!</p><p>By the way, regarding <a href="https://github.com/dotnet/command-line-api/">System.CommandLine</a>, they announced <a href="https://github.com/dotnet/command-line-api/issues/2338">Resetting System.CommandLine</a> in March due to ongoing issues. As expected, there hasn’t been much progress. This was predictable, and it’s better not to have high expectations. Using ConsoleAppFramework is a solid choice moving forward.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=d627dea393e7" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[MessagePack for C# v3 Release with Source Generator Support]]></title>
            <link>https://neuecc.medium.com/messagepack-for-c-v3-release-with-source-generator-support-893ed30d0e89?source=rss-61fea4eebf07------2</link>
            <guid isPermaLink="false">https://medium.com/p/893ed30d0e89</guid>
            <category><![CDATA[csharp]]></category>
            <dc:creator><![CDATA[Yoshifumi Kawai]]></dc:creator>
            <pubDate>Fri, 06 Dec 2024 08:10:47 GMT</pubDate>
            <atom:updated>2024-12-06T09:27:22.559Z</atom:updated>
            <content:encoded><![CDATA[<p>Last month, the <a href="https://github.com/MessagePack-CSharp/MessagePack-CSharp">MessagePack for C# project</a> joined the <a href="https://dotnetfoundation.org/">.NET Foundation</a>! I hope this will help users feel more confident about using the library with a stable perspective.</p><p>And now, after long development, the major version upgrade v3 has been released. While the core part remains mostly unchanged from v2, it fully incorporates Source Generator. Since IL dynamic generation still exists, it becomes a hybrid serializer with both IL dynamic generation and Source Generator. v3 comes with built-in Source Generator and Analyzer, and existing code will automatically be Source Generator-enabled just by compiling with v3. No additional code writing is required from users to support Source Generator when updating from v2 to v3!</p><p>Let’s look at the behavior in detail. For example, when you write code like:</p><pre>[MessagePackObject]<br>public class MyTestClass<br>{<br>    [Key(0)]<br>    public int MyProperty { get; set; }<br>}</pre><p>The following code is automatically generated internally by the Source Generator:</p><pre>partial class GeneratedMessagePackResolver<br>{<br>    internal sealed class MyTestClassFormatter : IMessagePackFormatter&lt;MyTestClass&gt;<br>    {<br>        public void Serialize(ref MessagePackWriter writer, MyTestClass value, MessagePackSerializerOptions options)<br>        {<br>            if (value == null)<br>            {<br>                writer.WriteNil();<br>                return;<br>            }<br><br>            writer.WriteArrayHeader(1);<br>            writer.Write(value.MyProperty);<br>        }<br><br>        public MyTestClass Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions options)<br>        {<br>            if (reader.TryReadNil())<br>            {<br>                return null;<br>            }<br><br>            options.Security.DepthStep(ref reader);<br>            var length = reader.ReadArrayHeader();<br>            var ____result = new MyTestClass();<br><br>            for (int i = 0; i &lt; length; i++)<br>            {<br>                switch (i)<br>                {<br>                    case 0:<br>                        ____result.MyProperty = reader.ReadInt32();<br>                        break;<br>                    default:<br>                        reader.Skip();<br>                        break;<br>                }<br>            }<br><br>            reader.Depth--;<br>            return ____result;<br>        }<br>    }<br>}</pre><p>Moreover, this GeneratedMessagePackResolver is already registered in the default options (like StandardResolver):</p><pre>public static readonly IFormatterResolver[] DefaultResolvers = [<br>    BuiltinResolver.Instance,<br>    AttributeFormatterResolver.Instance,<br>    SourceGeneratedFormatterResolver.Instance, // here<br>    ImmutableCollection.ImmutableCollectionResolver.Instance,<br>    CompositeResolver.Create(ExpandoObjectFormatter.Instance),<br>    DynamicGenericResolver.Instance, // only enable for RuntimeFeature.IsDynamicCodeSupported<br>    DynamicUnionResolver.Instance];</pre><p>Serialization target classes included in user code assemblies will prioritize using code generated by the Source Generator. GeneratedMessagePackResolver offers several customization points, such as changing the default namespace and names, or modifying generated formatters to be map-based. For more details, please check the new documentation. For those wanting to know the detailed changes from v2 to v3, please check the <a href="https://github.com/MessagePack-CSharp/MessagePack-CSharp/blob/develop/doc/migrating_v2-v3.md">Migration Guide v2 -&gt; v3</a>.</p><p>For Unity, the installation method has significantly changed. The core library is now common with the .NET version and requires installation from NuGet. Additionally, you need to download Unity-specific additional code via UPM. For details, please check the <a href="https://github.com/MessagePack-CSharp/MessagePack-CSharp/#unity-support">MessagePack-CSharp#unity-support</a> section.</p><p>The .unitypackage distribution has been discontinued. Also, mpc, which was required for IL2CPP support, is no longer needed. It has been completely migrated to Source Generator. Therefore, Unity support version starts from 2022.3.12f1. Regarding Source Generator, it is automatically enabled when installing the core library via NuGetForUnity, so no additional work is required.</p><h3>History and Next</h3><p>The original MessagePack for C# (v1) was released by me (Yoshifumi Kawai/@neuecc) in 2017. I created it as a performance-focused binary serializer because the existing (binary) serializers in 2016 couldn’t meet the performance requirements for solving issues in the game I was developing at the time. Along with it, I also released <a href="https://github.com/Cysharp/MagicOnion">MagicOnion</a>, a gRPC-based RPC framework created as a network system.</p><p>While v1 release only targeted byte[], .NET kept adding new I/O-related APIs like Span&lt;T&gt; and IBufferWriter&lt;T&gt;, so v2 introduced a new design focusing on these. This implementation was led and released by Microsoft Engineer <a href="https://github.com/AArnott">Andrew Arnott / @AArnott</a>.</p><p>Since then, it has continued under joint maintenance and moved from my personal repository (neuecc/MessagePack-CSharp) to an organization (MessagePack-CSharp/MessagePack-CSharp). It’s used in major Microsoft products like Visual Studio 2022, <a href="https://learn.microsoft.com/en-us/aspnet/core/signalr/messagepackhubprotocol">SignalR’s binary protocol</a>, and Blazor Server protocol, and has gathered the most stars on GitHub among .NET binary serializers. It’s also recommended as one of the migration targets for <a href="https://learn.microsoft.com/en-us/dotnet/standard/serialization/binaryformatter-migration-guide/">BinaryFormatter, which is being deprecated in .NET 9</a>.</p><p>With v3’s Source Generator support, we’ve taken the first step toward higher performance, flexibility, and AOT compatibility.</p><p>While I consider the MessagePack for C# project a great success, AArnott is currently starting development on his own new MessagePack project. During this time, I’ve also released <a href="https://github.com/Cysharp/MemoryPack">MemoryPack</a>, a serializer with a different format. Therefore, I think it’s necessary to explain somewhat about the future of MessagePack for C# and its characteristics.</p><p>I believe the maintenance system will continue with two people, but regarding active development, I might take the lead again. I operate with the understanding that MessagePack and MemoryPack have different characteristics as formats, and both are important. I like the original implementation of MessagePack for C#, and I think it’s still absolutely competitive even today.</p><p>AArnott’s different MessagePack serializer has slightly different fundamental philosophy. In that regard, I recognize it not as an improved serializer but as one with a different personality. Let me explain the differences.</p><h3>Binary spec, default settings and performance</h3><p>What’s important for serializer performance is both “specification and implementation.” For example, binary formats are generally faster than text formats like JSON. However, a well-implemented JSON serializer is faster than a mediocre binary serializer (I’ve demonstrated this by creating a serializer called <a href="https://github.com/neuecc/Utf8Json">Utf8Json</a>). So, both specification and implementation are important. If you can achieve both, that becomes the best-performing serializer.</p><p><a href="https://msgpack.org/">MessagePack’s binary specification</a> is expressed as a binary version of JSON, as its motto “It’s like JSON, but fast and small” suggests. However, MessagePack for C#’s default doesn’t necessarily aim to be JSON-like.</p><pre>[MessagePackObject]<br>public class MsgPackSchema<br>{<br>    [Key(0)]<br>    public bool Compact { get; set; }<br>    [Key(1)]<br>    public int Schema { get; set; }<br>}</pre><p>When this class is serialized, it would be expressed in JSON as [true, 0]. This is because the object is serialized array-based, whereas if serialized map-based, it would be expressed as {&quot;Compact&quot;:true,&quot;Schema&quot;:0}.</p><p>The advantage of array-based serialization is, as you can see, it becomes more compact in binary size. Compact size means less processing, which positively affects serialization speed. Also, for deserialization, since there’s no need to search for properties to deserialize by comparing strings, faster deserialization speed can be expected.</p><p>Note that array-based serialization is also adopted by msgpack-java, the reference implementation by MessagePack specification creator Sadayuki Furuhashi, so it’s not an unorthodox approach.</p><p>In MessagePack-CSharp, if you want to serialize in a JSON-like map-based format, you can write [MessagePackObject(true)]. Also, with Source Generator, you can override at the Resolver level to force map-based serialization.</p><pre>[MessagePackObject(keyAsPropertyName: true)]<br>public class MsgPackSchema<br>{<br>    public bool Compact { get; set; }<br>    public int Schema { get; set; }<br>}</pre><p>The advantages of maps are enabling flexible schema evolution, easier communication when interfacing with other languages, and higher self-descriptiveness of the binary itself. The disadvantages are the impact on size and performance, especially in arrays of objects where property names are included for each element, which becomes quite wasteful.</p><p>The default is set to array for pursuing compactness and performance. I considered MessagePack as a binary specification capable of achieving high performance before being JSON-like. Of course, maps are important too, so I made it possible to easily achieve map mode by just adding (true) to the attribute.</p><p>In array mode, you need to attach the Key attribute to all properties. This is necessary, just as Protocol Buffers requires numeric tags, when you’re not using the property name itself as the key. Of course, automatic numbering in sequence is possible, but I’ve determined that implicit handling of binary format keys is too risky (binary compatibility would break just by manipulating the order). In other words, explicit is the default. In large project development, both senior and junior members will touch the code; not everyone touching the code understands everything. So, implicit behavior should be avoided, and things should be explicit — this strong conviction led to this design choice.</p><p>However, attaching Keys to all properties is very painful (I had painful experiences with DataContract and protobuf-net before developing MessagePack-CSharp). So, we provided a feature to automatically attach them through Analyzer + Code Fix. This alleviates the pain of being explicit while getting the best of both worlds.</p><p>The other MessagePack serializer’s default appears to be map-based. This is partly because it’s based on <a href="https://github.com/eiriktsarpalis/PolyType">PolyType</a>, an abstraction library for creating Source Generator-based libraries, and partly because it seems to be an explicit preference for that approach.</p><p>A library can only choose one “default.” Even if it can process in either mode, there can only be one “default.” To reiterate, I prefer and prioritize “compactness and performance” as a binary format.</p><p>You might be hearing about PolyType for the first time. I’m not very favorable towards PolyType. While I think it’s very convenient for creating small things, I believe its limitations as an abstraction layer are too significant when aiming for best performance or expressing the best ideas. Therefore, I won’t adopt it in MessagePack for C# or in creating anything else.</p><h3>Unity(multiplatform) Support</h3><p>MessagePack for C# has provided first-class support for the Unity game engine since v1. This is partly because I serve as CEO of <a href="https://cysharp.com/">Cysharp</a>, an affiliated company of <a href="https://en.wikipedia.org/wiki/Cygames">Cygames</a>, a Japanese game company, and have deep connections with the video game industry. We’ve actually created and used things that run on Unity ourselves. Of course, we also use it for server-side and desktop applications.</p><p>Unity has its own AOT system called IL2CPP, which is essential especially for releasing on mobile platforms like iOS. Even before Source Generator existed, we created and provided mpc, a code generation tool using Roslyn. It’s no exaggeration to say that MessagePack being used in hundreds of mobile games is thanks to my passionate support. With v3 finally becoming Source Generator-based, the workflow will be greatly simplified!</p><p>Generally, Unity support has been quite undervalued in the .NET community. Also, from an outside perspective, Microsoft and Microsoft employees seem to share this attitude, with little interest in platforms other than their own. I don’t think this attitude is very favorable, and it’s also limiting the potential of .NET. I think Xamarin’s failure to achieve growth trajectory was also partly due to such cold regard from Microsoft itself.</p><p>I take care to ensure that the libraries I create can properly support Unity as much as possible (the latest being <a href="https://github.com/Cysharp/R3">Cysharp/R3</a>, a new Reactive Extensions library). As for the other MessagePack serializer, it doesn’t seem likely to have solid Unity support…</p><h3>Beyond v3</h3><p>v3’s Native AOT Support is not complete. It’s challenging that just making it Source Generator-based doesn’t result in complete Native AOT support. This is honestly perplexing given that it works perfectly with Unity’s AOT, IL2CPP, and I think it also shows Microsoft’s not-so-good habits. In other words, they’re providing something complex to achieve perfect support. That’s the current Native AOT. While I can understand some aspects of the complex and bizarre attributes and flows, I think they should have been simplified more. Well, it probably won’t be fixed anymore…</p><p>In terms of performance, there are also points that regressed from v1 to v2, so we need to make implementation improvements based on the latest insights. I’m particularly dissatisfied with how the wide use of ReadOnlySequence creates significant constraints.</p><p>Better asynchronous APIs due to the standardization of PipeReader/PipeWriter in .NET 9, and streaming support that achieves both performance might also become major topics.</p><p>Because MessagePack for C# is widely used, breaking changes are difficult to make, and maintaining compatibility is the most important topic. However, as the world changes, choosing not to evolve is choosing the path to extinction. I think there’s still a lot we can do, so I want to continue being the cutting-edge, best binary serializer in .NET (MemoryPack too…!)</p><p>First, please try v3’s Source Generator. I think one of the good things about OSS is that we can create better things with everyone’s power.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=893ed30d0e89" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Fast Dictionary Lookup of UTF-8 String in the C# 13 with .NET 9 AlternateLookup]]></title>
            <link>https://neuecc.medium.com/fast-dictionary-lookup-of-utf-8-string-in-the-c-13-with-net-9-alternatelookup-43798aef022d?source=rss-61fea4eebf07------2</link>
            <guid isPermaLink="false">https://medium.com/p/43798aef022d</guid>
            <category><![CDATA[csharp]]></category>
            <dc:creator><![CDATA[Yoshifumi Kawai]]></dc:creator>
            <pubDate>Thu, 29 Aug 2024 08:20:45 GMT</pubDate>
            <atom:updated>2024-09-12T03:14:30.235Z</atom:updated>
            <content:encoded><![CDATA[<h3>Fast Dictionary Lookup of UTF-8 String in the C# 13 with .NET 9 AlternateLookup</h3><p>In .NET 9, a new method GetAlternateLookup&lt;TKey, TValue, TAlternate&gt;() has been added to dictionary-like classes: Dictionary, ConcurrentDictionary, HashSet, FrozenDictionary, and FrozenSet. Until now, Dictionary operations could only be performed via TKey. This was natural, but it became problematic with string keys, as we want to operate with both string and ReadOnlySpan&lt;char&gt;. Previously, when only ReadOnlySpan&lt;char&gt; was available, conversion to string using ToString was mandatory, it allocates new memory even if we just wanted to reference a Dictionary value!</p><p>This issue has been resolved with the introduction of GetAlternateLookup in .NET 9, which allows dictionaries to have alternate search keys.</p><pre>var dict = new Dictionary&lt;string, int&gt;<br>{<br>    { &quot;foo&quot;, 10 },<br>    { &quot;bar&quot;, 20 },<br>    { &quot;baz&quot;, 30 }<br>};<br><br>var lookup = dict.GetAlternateLookup&lt;ReadOnlySpan&lt;char&gt;&gt;();<br><br>var keys = &quot;foo, bar, baz&quot;;<br><br>// .NET 9 SpanSplitEnumerator<br>foreach (Range range in keys.AsSpan().Split(&#39;,&#39;))<br>{<br>    ReadOnlySpan&lt;char&gt; key = keys.AsSpan(range).Trim();<br><br>    // Get/Add/Remove from string key dictionary using ReadOnlySpan&lt;char&gt;<br>    int value = lookup[key];<br>    Console.WriteLine(value);<br>}</pre><p>By the way, the usual string Split allocates an array and individual split strings. However, in .NET 8, <a href="https://learn.microsoft.com/en-us/dotnet/api/system.memoryextensions.split">MemoryExtensions.Split</a> was added, allowing a fixed number of splits on ReadOnlySpan&lt;char&gt;. In .NET 9, a new Split that returns SpanSplitEnumerator has been added. This allows cutting out ReadOnlySpan&lt;char&gt; from the original string without any additional allocations.</p><p>To reference keys with the extracted ReadOnlySpan&lt;char&gt;, GetAlternateLookup becomes necessary.</p><p>One use case is serializers, which frequently require key-value lookups. In <a href="https://github.com/MessagePack-CSharp/MessagePack-CSharp">MessagePack for C#</a> that I’m developing, we adopt multiple strategies for fast, allocation-free deserialization. One is <a href="https://github.com/MessagePack-CSharp/MessagePack-CSharp/blob/bcedbce3fd98cb294210d6b4a22bdc4c75ccd916/src/MessagePack/Internal/AutomataDictionary.cs">AutomataDictionary</a>, which treats UTF8 strings as 8-byte <a href="https://en.wikipedia.org/wiki/Automata_theory">automata</a>. This part is further inlined and embedded in IL Emit and Source Generator to eliminate dictionary lookups. Another is the <a href="https://github.com/MessagePack-CSharp/MessagePack-CSharp/blob/5793c81/src/MessagePack/Internal/AsymmetricKeyHashTable.cs">AsymmetricKeyHashTable</a> mechanism, which allows searching with two keys representing the same target, internally creating a dictionary searchable by both byte[] and ArraySegment&lt;byte&gt;.</p><pre>// From MessagePack for C#<br>internal interface IAsymmetricEqualityComparer&lt;TKey1, TKey2&gt;<br>{<br>    int GetHashCode(TKey1 key1);<br>    int GetHashCode(TKey2 key2);<br>    bool Equals(TKey1 x, TKey1 y);<br>    bool Equals(TKey1 x, TKey2 y); // Comparison between TKey1 and TKey2<br>}</pre><p>In other words, until now, scenarios requiring dictionaries with alternate search keys necessitated creating custom dictionaries, and for performance, even basic data structures had to be custom-made. However, from .NET 9, this is finally achievable with standard tools.</p><p>What’s needed for AlternateLookup is IAlternateEqualityComparer&lt;in TAlternate, T&gt;, defined as follows: (The definition is similar to IAsymmetricEqualityComparer, so I might have anticipated the future by 10 years)</p><pre>public interface IAlternateEqualityComparer&lt;in TAlternate, T&gt;<br>    where TAlternate : allows ref struct<br>    where T : allows ref struct<br>{<br>    bool Equals(TAlternate alternate, T other);<br>    int GetHashCode(TAlternate alternate);<br>    T Create(TAlternate alternate);<br>}</pre><p>The language feature <a href="https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/ref-struct">allows ref struct</a> added in C# 13 allows ref structs, such as Span&lt;T&gt;, to be used as generic type arguments.</p><p>Basically, this needs to be implemented along with IEqualityComparer&lt;T&gt;. In fact, Dictionary.GetAlternateLookup throws a runtime exception (not a compile-time check!) if the Dictionary&#39;s IEqualityComparer doesn&#39;t implement IAlternateEqualityComparer. Also, it&#39;s a bit odd that an EqualityComparer has a Create method, but this is necessary for Add operations.</p><p>Currently, the standard only provides IAlternateEqualityComparer for string. The EqualityComparer typically used for strings implements IAlternateEqualityComparer and can be operated with ReadOnlySpan&lt;char&gt;, but nothing else is provided.</p><p>However, what’s realistically needed in modern times is UTF8, ReadOnlySpan&lt;byte&gt;. I mentioned using it for serializer lookups, but the input of modern serializers is UTF8. There&#39;s no place for ReadOnlySpan&lt;char&gt;. So, let&#39;s prepare an IAlternateEqualityComparer like this!</p><pre>public sealed class Utf8StringEqualityComparer : IEqualityComparer&lt;byte[]&gt;, IAlternateEqualityComparer&lt;ReadOnlySpan&lt;byte&gt;, byte[]&gt;<br>{<br>    public static IEqualityComparer&lt;byte[]&gt; Default { get; } = new Utf8StringEqualityComparer();<br><br>    // IEqualityComparer<br><br>    public bool Equals(byte[]? x, byte[]? y)<br>    {<br>        if (x == null &amp;&amp; y == null) return true;<br>        if (x == null || y == null) return false;<br><br>        return x.AsSpan().SequenceEqual(y);<br>    }<br><br>    public int GetHashCode([DisallowNull] byte[] obj)<br>    {<br>        return GetHashCode(obj.AsSpan());<br>    }<br><br>    // IAlternateEqualityComparer<br><br>    public byte[] Create(ReadOnlySpan&lt;byte&gt; alternate)<br>    {<br>        return alternate.ToArray();<br>    }<br><br>    public bool Equals(ReadOnlySpan&lt;byte&gt; alternate, byte[] other)<br>    {<br>        return other.AsSpan().SequenceEqual(alternate);<br>    }<br><br>    public int GetHashCode(ReadOnlySpan&lt;byte&gt; alternate)<br>    {<br>        // System.IO.Hashing package, cast to int is safe for hashing<br>        return unchecked((int)XxHash3.HashToUInt64(alternate));<br>    }<br>}</pre><p>By default, byte[] is compared by reference, but we want to compare by data match, so we use ReadOnlySpan&lt;T&gt;.SequenceEqual. This achieves fast comparison utilizing SIMD, especially when T is one of several primitives. For hash code calculation, it&#39;s best to use <a href="https://learn.microsoft.com/en-us/dotnet/api/system.io.hashing.xxhash3">XxHash3</a>, the .NET implementation of XXH3, the latest version of the fast <a href="https://github.com/Cyan4973/xxHash">xxHash</a> algorithm series. This requires importing System.IO.Hashing from NuGet. The return value is ulong as it&#39;s calculated in 64 bits, but when a 32-bit value is needed, the xxHash author states that simply dropping bits is fine, so we can just cast to int.</p><p>Here’s an example of how to use it:</p><pre>// Create a dictionary with Utf8StringEqualityComparer<br><br>var dict = new Dictionary&lt;byte[], bool&gt;(Utf8StringEqualityComparer.Default)<br>{<br>    { &quot;foo&quot;u8.ToArray(), true },<br>    { &quot;bar&quot;u8.ToArray(), false },<br>    { &quot;baz&quot;u8.ToArray(), false }<br>};<br><br>var lookup = dict.GetAlternateLookup&lt;ReadOnlySpan&lt;byte&gt;&gt;();<br><br>// Assume we have this input<br><br>ReadOnlySpan&lt;byte&gt; json = &quot;&quot;&quot;    <br>{<br>    &quot;foo&quot;: 0,<br>    &quot;bar&quot;: 0,<br>    &quot;baz&quot;: 0<br>}<br>&quot;&quot;&quot;u8;<br><br>// System.Text.Json<br>var reader = new Utf8JsonReader(json);<br><br>while (reader.Read())<br>{<br>    if (reader.TokenType == JsonTokenType.PropertyName)<br>    {<br>        // Can search with the extracted Key<br>        ReadOnlySpan&lt;byte&gt; key = reader.ValueSpan;<br>        var flag = lookup[key];<br>        <br>        Console.WriteLine(flag);<br>    }<br>}</pre><p>One thing to note is that it’s better to avoid creating AlternateKey with string and ReadOnlySpan&lt;byte&gt;. This would always require encoding, resulting in the worst of both worlds (even if using <a href="https://learn.microsoft.com/en-us/dotnet/api/system.text.rune">Rune</a> for allocation-less processing, it&#39;s no match for byte[] keys that can be compared with just binary comparison). If you absolutely need both searches, it&#39;s better to prepare two dictionaries.</p><p>Anyway, this is a long-awaited feature for me! I’ve created dictionaries in various variations many times, unable to use generics for Span support and having to hard-code them. I’m very excited that it’s now available for general use. While allows ref struct has some complexities in generic definitions (maybe automatic assignment would have been fine?), it&#39;s an important advancement as a language.</p><p>Let&#39;s start using .NET 9 and C# 13. It&#39;s still in preview, but the official release should be in November.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=43798aef022d" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[ZLogger v2 Architecture: Leveraging .NET 8 to Maximize Performance]]></title>
            <link>https://neuecc.medium.com/zlogger-v2-architecture-leveraging-net-8-to-maximize-performance-2d9733b43789?source=rss-61fea4eebf07------2</link>
            <guid isPermaLink="false">https://medium.com/p/2d9733b43789</guid>
            <category><![CDATA[csharp]]></category>
            <dc:creator><![CDATA[Yoshifumi Kawai]]></dc:creator>
            <pubDate>Fri, 05 Jul 2024 11:42:05 GMT</pubDate>
            <atom:updated>2024-07-05T11:42:05.799Z</atom:updated>
            <content:encoded><![CDATA[<h3>ZLogger v2 Architecture: Leveraging .NET 8 to Maximize Performance</h3><p>We have released ZLogger v2, a new ultra-fast and low-allocation logging library for C# and .NET. It’s been completely redesigned from v1 to align with the latest C# features. While it works best with .NET 8, it supports .NET Standard 2.0 and above, as well as Unity 2022.2 and above. Both .NET and Unity versions support text messages and structured logging(JSON and MessagePack in default).</p><ul><li><a href="https://github.com/Cysharp/ZLogger">Cysharp/ZLogger</a></li></ul><p>The key point of the new design is the full adoption of String Interpolation, which achieves both clean syntax and performance.</p><pre>logger.ZLogInformation($&quot;Hello my name is {name}, {age} years old.&quot;);</pre><p>Code written like this is compiled into:</p><pre>if (logger.IsEnabled(LogLvel.Information))<br>{<br>    var handler = new ZLoggerInformationInterpolatedStringHandler(30, 2, logger);<br>    handler.AppendLiteral(&quot;Hello my name is &quot;);<br>    handler.AppendFormatted&lt;string&gt;(name, 0, null, &quot;name&quot;);<br>    handler.AppendLiteral(&quot;, &quot;);<br>    handler.AppendFormatted&lt;int&gt;(age, 0, null, &quot;age&quot;);<br>    handler.AppendLiteral(&quot; years old.&quot;);<br>}</pre><p>The efficiency is evident from the code: the format string is expanded at compile time rather than runtime, and parameters are received as generics in the form of AppendFormatted&lt;T&gt;, avoiding boxing. Incidentally, 30 in the constructor represents the string length, and 2 is the number of parameters, which contributes to efficiency by calculating the required initial buffer size.</p><p>String Interpolation itself has been a feature since C# 6.0, but <a href="https://devblogs.microsoft.com/dotnet/string-interpolation-in-c-10-and-net-6/">enhanced String Interpolation from C# 10.0</a> allows for custom String Interpolation.</p><p>The string fragments and parameters obtained this way are ultimately written directly to the Stream as UTF8 without being stringified, through <a href="https://github.com/Cysharp/Utf8StringInterpolation">Cysharp/Utf8StringInterpolation</a>, achieving high speed and low allocation.</p><p>For Structured Logging as well, by tightly coupling with System.Text.Json’s Utf8JsonWriter:</p><pre>// For example, write {&quot;name&quot;:&quot;foo&quot;,age:33} to Utf8JsonWriter<br>// Source Generator version, very easy to understand what&#39;s actually happening<br>public void WriteJsonParameterKeyValues(Utf8JsonWriter writer, JsonSerializerOptions jsonSerializerOptions)<br>{<br>    writer.WriteString(_jsonParameter_name, this.name);<br>    writer.WriteNumber(_jsonParameter_age, this.age);<br>}<br><br>// StringInterpolation version, seems a bit roundabout but does the same thing<br>public void WriteJsonParameterKeyValues(Utf8JsonWriter writer, JsonSerializerOptions jsonSerializerOptions)<br>{<br>    for (var i = 0; i &lt; ParameterCount; i++)<br>    {<br>        ref var p = ref parameters[i];<br>        writer.WritePropertyName(p.Name.AsSpan());<br>        // Explanation of MagicalBox will come later<br>        if (!magicalBox.TryReadTo(p.Type, p.BoxOffset, jsonWriter, jsonSerializerOptions))<br>        {<br>            // ....<br>        }<br>    }<br>}</pre><p>It’s written directly as UTF8 again. Structured Logging is a recent trend, so it’s implemented in loggers of various languages, but I don’t think there’s any other implementation that achieves such clean syntax while maintaining performance!</p><p>So, how about actual benchmark results? The allocation is at least overwhelmingly low.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*SYlXpy_OFJv5MRN0" /></figure><p>The reason for the hesitant statement about allocation is that NLog, which was carefully set up for high speed, was faster than expected, grrr…</p><p>Now, another feature of ZLogger is that it’s built directly on top of <a href="https://learn.microsoft.com/en-us/dotnet/core/extensions/logging">Microsoft.Extensions.Logging</a>. Usually, loggers have their own systems and use a bridge to connect with Microsoft.Extensions.Logging. In realistic applications, it’s almost impossible to avoid Microsoft.Extensions.Logging, such as when using ASP.NET. From .NET 8, with enhanced OpenTelemetry support and <a href="https://learn.microsoft.com/en-us/dotnet/aspire/get-started/aspire-overview">Aspire</a>, the importance of Microsoft.Extensions.Logging is increasing. Unlike ZLogger v1, v2 supports all features of Microsoft.Extensions.Logging, including Scope.</p><p>And for example, the quality of Serilog’s bridge library is quite low (I checked the source code as well), which is reflected in the actual performance numbers. ZLogger incurs no such overhead.</p><p>Also, default settings are very important. The standard settings of most loggers are quite slow, such as flushing each time when writing to a file stream. To speed this up, you need to properly adjust async and buffered settings, and ensure a reliable flush at the end to avoid loss, which is quite difficult. So, many people probably leave it at the default settings? ZLogger is adjusted to be the fastest by default, and the final flush is automatically applied with the lifecycle of Microsoft.Extensions’ DI, so there’s no loss when constructing applications with ApplicationBuilder, etc., without any conscious effort.</p><p>Note that the performance of flushing each time heavily depends on storage write performance, so you might find it’s not that slow when benchmarking locally on recent machines with M.2 SSDs, which are very fast. However, it’s better not to trust local results too much, as the storage performance of cloud servers where you actually deploy applications is unlikely to be that high.</p><h3>MagicalBox</h3><p>Here, I’ll introduce some tricks used to achieve performance. What’s carried over from v1 is the creation of an async asynchronous writing process utilizing <a href="https://devblogs.microsoft.com/dotnet/an-introduction-to-system-threading-channels/">System.Threading.Channels</a> and efficient use of buffered through <a href="https://learn.microsoft.com/en-us/dotnet/api/system.buffers.ibufferwriter-1">IBufferWriter&lt;byte&gt;</a> for optimizing writing to Stream, but I&#39;ll skip the explanation.</p><p>For JSON conversion, parameters are temporarily held as values in InterpolatedStringHandler. In this case, the question arises of how to hold the value of &lt;T&gt;. Normally, you&#39;d think to hold it as an object type, like List&lt;object&gt;.</p><pre>[InterpolatedStringHandler]<br>public ref struct ZLoggerInterpolatedStringHandler<br>{<br>    // Using object to store values of any &lt;T&gt; type, not good as it causes boxing<br>    List&lt;object&gt; parameters = new ();<br><br>    public void AppendFormatted&lt;T&gt;(T value, int alignment = 0, string? format = null, [CallerArgumentExpression(&quot;value&quot;)] string? argumentName = null)<br>    {<br>        parameters.Add((object)value);<br>    }<br>}<br></pre><p>To avoid this, ZLogger has prepared a mechanism called MagicalBox.</p><pre>[InterpolatedStringHandler]<br>public ref struct ZLoggerInterpolatedStringHandler<br>{<br>    // Pack infinitely into the magic box<br>    MagicalBox magicalBox;<br>    List&lt;int&gt; boxOffsets = new (); // Actually, this part is carefully cached<br><br>    public void AppendFormatted&lt;T&gt;(T value, int alignment = 0, string? format = null, [CallerArgumentExpression(&quot;value&quot;)] string? argumentName = null)<br>    {<br>        if (magicalBox.TryWrite(value, out var offset)) // No boxing occurs!<br>        {<br>            boxOffsets.Add(offset);<br>        }<br>    }<br>}</pre><p>MagicalBox is based on the concept that it can write any type (limited to unmanaged types) without boxing. Its actual implementation is just writing to byte[] using Unsafe.Write and reading using Unsafe.Read based on the offset.</p><pre>internal unsafe partial struct MagicalBox<br>{<br>    byte[] storage;<br>    int written;<br><br>    public MagicalBox(byte[] storage)<br>    {<br>        this.storage = storage;<br>    }<br><br>    public bool TryWrite&lt;T&gt;(T value, out int offset)<br>    {<br>        if (RuntimeHelpers.IsReferenceOrContainsReferences&lt;T&gt;())<br>        {<br>            offset = 0;<br>            return false;<br>        }<br>        Unsafe.WriteUnaligned(ref storage[written], value);<br>        offset = written;<br>        written += Unsafe.SizeOf&lt;T&gt;();<br>        return true;<br>    }<br><br>    public bool TryRead&lt;T&gt;(int offset, out T value)<br>    {<br>        if (!RuntimeHelpers.IsReferenceOrContainsReferences&lt;T&gt;())<br>        {<br>            value = default!;<br>            return false;<br>        }<br>        value = Unsafe.ReadUnaligned&lt;T&gt;(ref storage[offset]);<br>        return true;<br>    }<br>}</pre><p>This is based on implementation experience from <a href="https://github.com/Cysharp/MemoryPack">MemoryPack</a> serializer and works well.</p><p>Note that in the actual code, it becomes a <a href="https://github.com/Cysharp/ZLogger/blob/904ae90da3f1631d6ba9f66887b81f245cb2ef17/src/ZLogger/Internal/MagicalBox.cs">slightly more complex code</a> including efficient reuse of byte[] storage, non-generic Read support, special handling for Enum, etc. As expected.</p><h3>Custom Format Strings</h3><p>A good point of ZLogger’s String Interpolation is that if you include method calls in parameter values, they are called after the LogLevel check, preventing unnecessary execution.</p><pre>// This<br>logger.ZLogDebug($&quot;Id {obj.GetId()}: Data: {obj.GetData()}.&quot;);<br><br>// Is checked for LogLevel validity before methods are called, like this<br>if (logger.IsEnabled(LogLvel.Debug))<br>{<br>    // snip...<br>    writer.AppendFormatterd(obj.GetId());<br>    writer.AppendFormatterd(obj.GetData());<br>}</pre><p>However, when outputting method calls to Structured Logging, ZLogger uses <a href="https://learn.microsoft.com/en-us/dotnet/api/system.runtime.compilerservices.callerargumentexpressionattribute?view=net-8.0">CallerArgumentExpression </a>added from C# 10.0 onwards to get the parameter name, so in the case of method calls, it’s output with the rather awkward name “obj.GetId()”. Therefore, you can specify an alias with a special custom format string.</p><pre>// You can give an alias with @name<br>logger.ZLogDebug($&quot;Id {obj.GetId():@id}: Data: {obj.GetData():@data}.&quot;);</pre><p>In ZLogger, following the original expression of String Interpolation, you can specify alignment with “,” and format string with “:”. In addition, as a special designation, if the format string starts with @, it’s output as a parameter name.</p><p>The @ parameter name specification and format string can be used together.</p><pre>// Today is 2023-12-19.<br>// {&quot;date&quot;:&quot;2023-12-19T11:25:34.3642389+09:00&quot;}<br>logger.ZLogDebug($&quot;Today is {DateTime.Now:@date:yyyy-MM-dd}.&quot;);</pre><p>Another common special format string is “json”, which allows output in JsonSerialized form (this feature was inspired by Serilog’s capabilities)</p><pre>var position = new { Latitude = 25, Longitude = 134 };<br>var elapsed = 34;<br><br>// {&quot;position&quot;:{&quot;Latitude&quot;:25,&quot;Longitude&quot;:134},&quot;elapsed&quot;:34}<br>// Processed {&quot;Latitude&quot;:25,&quot;Longitude&quot;:134} in 034 ms.<br>logger.ZLogInformation($&quot;Processed {position:json} in {elapsed:000} ms.&quot;);</pre><p>Special format strings are also prepared for PrefixFormatter/SuffixFormatter to add log levels, categories, dates to the beginning/end.</p><pre>logging.AddZLoggerConsole(options =&gt;<br>{<br>    options.UsePlainTextFormatter(formatter =&gt;<br>    {<br>        // 2023-12-19 02:46:14.289 [DBG]......<br>        formatter.SetPrefixFormatter($&quot;{0:utc-longdate} [{1:short}]&quot;, (template, info) =&gt; template.Format(info.Timestamp, info.LogLevel));<br>    });<br>});</pre><p>For Timestamp, there are longdate, utc-longdate, dateonly, etc. For LogLevel, short converts to a 3-character log level notation (the length of the beginning matches, making it easier to read when opened in an editor). These built-in special format strings also have a performance optimization meaning. For example, the code for LogLevel looks like this, so it&#39;s absolutely more efficient to write with pre-built UTF8 strings than to create the format manually.</p><pre>static void AppendLogLevel(ref Utf8StringWriter&lt;IBufferWriter&lt;byte&gt;&gt; writer, ref LogLevel value, ref MessageTemplateChunk chunk)<br>{<br>    if (!chunk.NoAlignmentAndFormat)<br>    {<br>        if (chunk.Format == &quot;short&quot;)<br>        {<br>            switch (value)<br>            {<br>                case LogLevel.Trace:<br>                    writer.AppendUtf8(&quot;TRC&quot;u8);<br>                    return;<br>                case LogLevel.Debug:<br>                    writer.AppendUtf8(&quot;DBG&quot;u8);<br>                    return;<br>                case LogLevel.Information:<br>                    writer.AppendUtf8(&quot;INF&quot;u8);<br>                    return;<br>                case LogLevel.Warning:<br>                    writer.AppendUtf8(&quot;WRN&quot;u8);<br>                    return;<br>                case LogLevel.Error:<br>                    writer.AppendUtf8(&quot;ERR&quot;u8);<br>                    return;<br>                case LogLevel.Critical:<br>                    writer.AppendUtf8(&quot;CRI&quot;u8);<br>                    return;<br>                case LogLevel.None:<br>                    writer.AppendUtf8(&quot;NON&quot;u8);<br>                    return;<br>                default:<br>                    break;<br>            }<br>        }<br><br>        writer.AppendFormatted(value, chunk.Alignment, chunk.Format);<br>        return;<br>    }<br><br>    switch (value)<br>    {<br>        case LogLevel.Trace:<br>            writer.AppendUtf8(&quot;Trace&quot;u8);<br>            break;<br>        case LogLevel.Debug:<br>            writer.AppendUtf8(&quot;Debug&quot;u8);<br>            break;<br>        case LogLevel.Information:<br>            writer.AppendUtf8(&quot;Information&quot;u8);<br>            break;<br>        case LogLevel.Warning:<br>            writer.AppendUtf8(&quot;Warning&quot;u8);<br>            break;<br>        case LogLevel.Error:<br>            writer.AppendUtf8(&quot;Error&quot;u8);<br>            break;<br>        case LogLevel.Critical:<br>            writer.AppendUtf8(&quot;Critical&quot;u8);<br>            break;<br>        case LogLevel.None:<br>            writer.AppendUtf8(&quot;None&quot;u8);<br>            break;<br>        default:<br>            writer.AppendFormatted(value);<br>            break;<br>    }<br>}</pre><h3>.NET 8 XxHash3 + Non-GC Heap</h3><p><a href="https://learn.microsoft.com/en-us/dotnet/api/system.io.hashing.xxhash3">XxHash3</a> has been added from .NET 8. It’s the latest series of <a href="https://github.com/Cyan4973/xxHash/">XxHash</a>, the fastest hash algorithm, and its performance is such that it can be used for almost everything from small to large data without hesitation. Note that it requires System.IO.Hashing from NuGet, so it can be used even with .NET Standard 2.0, not just .NET 8.</p><p>ZLogger uses it in multiple places, but as one example, here’s the process of retrieving a cache from String Interpolation string literals:</p><pre>// LiteralList generated by $&quot;Hello my name is {name}, {age} years old.&quot;<br>// [&quot;Hello my name is &quot;, &quot;name&quot;, &quot;, &quot;, &quot;age&quot;, &quot; years old.&quot;]<br>// Process to retrieve UTF8 converted cache (MessageSequence) from this<br>static readonly ConcurrentDictionary&lt;LiteralList, MessageSequence&gt; cache = new();<br><br>// Non-.NET 8 version<br>#if !NET8_0_OR_GREATER<br>struct LiteralList(List&lt;string?&gt; literals) : IEquatable&lt;LiteralList&gt;<br>{<br>    [ThreadStatic]<br>    static XxHash3? xxhash;<br><br>    public override int GetHashCode()<br>    {<br>        var h = xxhash;<br>        if (h == null)<br>        {<br>            h = xxhash = new XxHash3();<br>        }<br>        else<br>        {<br>            h.Reset();<br>        }<br><br>        var span = CollectionsMarshal.AsSpan(literals);<br>        foreach (var item in span)<br>        {<br>            h.Append(MemoryMarshal.AsBytes(item.AsSpan()));<br>        }<br><br>        // https://github.com/Cyan4973/xxHash/issues/453<br>        // XXH3 64bit -&gt; 32bit, okay to simple cast answered by XXH3 author.<br>        return unchecked((int)h.GetCurrentHashAsUInt64());<br>    }<br><br>    public bool Equals(LiteralList other)<br>    {<br>        var xs = CollectionsMarshal.AsSpan(literals);<br>        var ys = CollectionsMarshal.AsSpan(other.literals);<br>        if (xs.Length == ys.Length)<br>        {<br>            for (int i = 0; i &lt; xs.Length; i++)<br>            {<br>                if (xs[i] != ys[i]) return false;<br>            }<br>            return true;<br>        }<br>        return false;<br>    }<br>}<br>#endif</pre><p>XxHash3 is a class (it would have been nice if it was a struct like <a href="https://learn.microsoft.com/en-us/dotnet/api/system.hashcode?view=net-8.0">System.HashCode</a>), so it’s being reused with ThreadStatic while generating GetHashCode. XxHash3 only outputs ulong, but according to the author, when dropping to 32 bits, it’s okay to drop directly without XOR or anything.</p><p>This is the normal usage, but for the .NET 8 version, we implemented an extreme optimization.</p><pre>#if NET8_0_OR_GREATER<br><br>struct LiteralList(List&lt;string?&gt; literals) : IEquatable&lt;LiteralList&gt;<br>{<br>    // literals are all const string, in .NET 8 it is allocated in Non-GC Heap so can compare by address.<br>    // https://devblogs.microsoft.com/dotnet/performance-improvements-in-net-8/#non-gc-heap<br>    static ReadOnlySpan&lt;byte&gt; AsBytes(ReadOnlySpan&lt;string?&gt; literals)<br>    {<br>        return MemoryMarshal.CreateSpan(<br>            ref Unsafe.As&lt;string?, byte&gt;(ref MemoryMarshal.GetReference(literals)),<br>            literals.Length * Unsafe.SizeOf&lt;string&gt;());<br>    }<br><br>    public override int GetHashCode()<br>    {<br>        return unchecked((int)XxHash3.HashToUInt64(AsBytes(CollectionsMarshal.AsSpan(literals))));<br>    }<br><br>    public bool Equals(LiteralList other)<br>    {<br>        var xs = CollectionsMarshal.AsSpan(literals);<br>        var ys = CollectionsMarshal.AsSpan(other.literals);<br>        return AsBytes(xs).SequenceEqual(AsBytes(ys));<br>    }<br>}<br>#endif</pre><p>It converts List&lt;string&gt;? to ReadOnlySpan&lt;byte&gt;, and then calls XxHash3.HashToUInt64 or SequenceEqual in one go. This is visibly more efficient, but is it legal to convert List&lt;string&gt;? to ReadOnlySpan&lt;byte&gt;? In this case, the conversion of string means converting to ReadOnlySpan&lt;IntPtr&gt;, that is, it&#39;s intended to convert to a list of addresses of strings in the heap.</p><p>That’s fine so far, but the problem is whether comparing addresses isn’t too dangerous. First, even if strings are identical as strings, they can often be at different addresses. Second, the addresses of strings in the heap are not fixed, they can move. If we’re asking for GetHashCode or Equals as a dictionary key, it must be completely fixed during application execution.</p><p>However, focusing on this usage example, AppendLiteral called by String Interpolation is always passed as a constant at compile time, like handler.AppendLiteral(&quot;Hello my name is &quot;);. Therefore, it&#39;s guaranteed to point to the same entity.</p><pre>[InterpolatedStringHandler]<br>public ref struct ZLoggerInterpolatedStringHandler<br>{<br>    public void AppendLiteral([ConstantExpected] string s)<br>}</pre><p>As a precaution, we explicitly state that only constants should be passed using <a href="https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.codeanalysis.constantexpectedattribute?view=net-8.0">ConstantExpected</a>, which has been enabled from .NET 8.</p><p>Another point is that such constant strings are already interned, but it wasn’t guaranteed that the place where they were interned wouldn’t move until .NET 8. However, with the introduction of Non-GC Heap from .NET 8, it can be said that it’s guaranteed not to move.</p><pre>// From .NET 8, the result of GC.GetGeneration for constants is int.MaxValue (in Non-GC Heap)<br>var str = &quot;foo&quot;;<br>Console.WriteLine(GC.GetGeneration(str)); // 2147483647</pre><p>This allowed us to maximize the speed of conversion from UTF16 String to UTF8 String, which is unavoidable in C#. Note that the Source Generator version can eliminate this lookup cost itself, so as the benchmark results showed, it’s even faster.</p><h3>.NET 8 IUtf8SpanFormattable</h3><p>ZLogger uses writing directly to UTF8 without going through strings as a pillar of performance. From .NET 8, <a href="https://learn.microsoft.com/en-us/dotnet/api/system.iutf8spanformattable?view=net-8.0">IUtf8SpanFormattable </a>has been added, which allows for generic direct conversion of values to UTF8. ZLogger supports .NET Standard 2.0 before .NET 8, so basic primitives like int and double are directly written to UTF8 through special handling, but in the case of .NET 8, the range of support is wider, so .NET 8 is recommended if possible.</p><p>Note that IUtf8SpanFormattable doesn’t care about the alignment of format strings, so <a href="https://github.com/Cysharp/Utf8StringInterpolation">Cysharp/Utf8StringInterpolation</a>, which is a separate library, is a library that adds alignment support while supporting .NET Standard 2.0.</p><h3>.NET 8 TimeProvider</h3><p><a href="https://learn.microsoft.com/en-us/dotnet/api/system.timeprovider?view=net-8.0">TimeProvider</a> is an abstraction of time-related APIs (including TimeZone, Timer, etc.) added from .NET 8, and it’s very useful for unit testing, etc., and will be an essential class in the future. TimeProvider is also available for .NET Standard 2.0 and Unity through <a href="https://www.nuget.org/packages/Microsoft.Bcl.TimeProvider/">Microsoft.Bcl.TimeProvider</a>, even for versions below .NET 8.</p><p>So in ZLogger, you can fix the time of log output by specifying TimerProvider in ZLoggerOptions.</p><pre>// It&#39;s better to use FakeTimeProvider from Microsoft.Extensions.TimeProvider.Testing<br>class FakeTime : TimeProvider<br>{<br>    public override DateTimeOffset GetUtcNow()<br>    {<br>        return new DateTimeOffset(1999, 12, 30, 11, 12, 33, TimeSpan.Zero);<br>    }<br>    <br>    public override TimeZoneInfo LocalTimeZone =&gt; TimeZoneInfo.Utc;<br>}<br><br>public class TimestampTest<br>{<br>    [Fact]<br>    public void LogInfoTimestamp()<br>    {<br>        var result = new List&lt;string&gt;();<br>        using var factory = LoggerFactory.Create(builder =&gt;<br>        {<br>            builder.AddZLoggerInMemory((options, _) =&gt;<br>            {<br>                options.TimeProvider = new FakeTime(); // Set TimeProvider to a custom one<br>                options.UsePlainTextFormatter(formatter =&gt;<br>                {<br>                    // Add Timestamp to the beginning<br>                    formatter.SetPrefixFormatter($&quot;{0} | &quot;, (template, info) =&gt; template.Format(info.Timestamp));<br>                });<br>            }, x =&gt;<br>            {<br>                x.MessageReceived += msg =&gt; result.Add(msg);<br>            });<br>        });<br><br>        var logger = factory.CreateLogger&lt;TimestampTest&gt;();<br>        logger.ZLogInformation($&quot;Foo&quot;);<br><br>        Assert.Equal(&quot;1999-12-30 11:12:33.000 | Foo&quot;, result[0]);<br>    }<br>}</pre><p>This can be effectively used when you need to test with exact matches of log output.</p><h3>Source Generator</h3><p>Microsoft.Extensions.Logging provides <a href="https://learn.microsoft.com/en-us/dotnet/core/extensions/logger-message-generator">LoggerMessageAttribute</a> and Source Generator as standard for high-performance log output.</p><p>While this is indeed excellent for generating UTF16 strings, there’s a question mark over the Structured Logging generation part.</p><pre>// This partial method<br>[LoggerMessage(LogLevel.Information, &quot;My name is {name}, age is {age}.&quot;)]<br>public static partial void MSLog(this ILogger logger, string name, int age, int other);<br><br>// Generates this class<br>private readonly struct __MSLogStruct : global::System.Collections.Generic.IReadOnlyList&lt;global::System.Collections.Generic.KeyValuePair&lt;string, object?&gt;&gt;<br>{<br>    private readonly global::System.String _name;<br>    private readonly global::System.Int32 _age;<br><br>    public __MSLogStruct(global::System.String name, global::System.Int32 age)<br>    {<br>        this._name = name;<br>        this._age = age;<br>    }<br><br>    public override string ToString()<br>    {<br>        var name = this._name;<br>        var age = this._age;<br>        return $&quot;My name is {name}, age is {age}.&quot;; // String generation seems fast (it&#39;s riding on C# 10.0&#39;s String Interpolation Improvements, so no complaints!)<br>    }<br><br>    public static readonly global::System.Func&lt;__MSLogStruct, global::System.Exception?, string&gt; Format = (state, ex) =&gt; state.ToString();<br><br>    public int Count =&gt; 4;<br><br>    // This is the code for Structured Logging, but hmm...?<br>    public global::System.Collections.Generic.KeyValuePair&lt;string, object?&gt; this[int index]<br>    {<br>        get =&gt; index switch<br>        {<br>            0 =&gt; new global::System.Collections.Generic.KeyValuePair&lt;string, object?&gt;(&quot;name&quot;, this._name),<br>            1 =&gt; new global::System.Collections.Generic.KeyValuePair&lt;string, object?&gt;(&quot;age&quot;, this._age),<br>            2 =&gt; new global::System.Collections.Generic.KeyValuePair&lt;string, object?&gt;(&quot;other&quot;, this._other),<br>            3 =&gt; new global::System.Collections.Generic.KeyValuePair&lt;string, object?&gt;(&quot;{OriginalFormat}&quot;, &quot;My name is {name}, age is {age}.&quot;),<br>            _ =&gt; throw new global::System.IndexOutOfRangeException(nameof(index)),  // return the same exception LoggerMessage.Define returns in this case<br>        };<br>    }<br><br>    public global::System.Collections.Generic.IEnumerator&lt;global::System.Collections.Generic.KeyValuePair&lt;string, object?&gt;&gt; GetEnumerator()<br>    {<br>        for (int i = 0; i &lt; 4; i++)<br>        {<br>            yield return this[i];<br>        }<br>    }<br><br>    global::System.Collections.IEnumerator global::System.Collections.IEnumerable.GetEnumerator() =&gt; GetEnumerator();<br>}<br><br>[global::System.CodeDom.Compiler.GeneratedCodeAttribute(&quot;Microsoft.Extensions.Logging.Generators&quot;, &quot;8.0.9.3103&quot;)]<br>public static partial void MSLog(this global::Microsoft.Extensions.Logging.ILogger logger, global::System.String name, global::System.Int32 age)<br>{<br>    if (logger.IsEnabled(global::Microsoft.Extensions.Logging.LogLevel.Information))<br>    {<br>        logger.Log(<br>            global::Microsoft.Extensions.Logging.LogLevel.Information,<br>            new global::Microsoft.Extensions.Logging.EventId(764917357, nameof(MSLog)),<br>            new __MSLogStruct(name, age),<br>            null,<br>            __MSLogStruct.Format);<br>    }<br>}</pre><p>With KeyValuePair&lt;string, object?&gt;, boxing can&#39;t be avoided when created normally, can&#39;t be helped.</p><p>So, ZLogger provides a similar Source Generator attribute called ZLoggerMessageAttribute. This enables UTF8 optimization and boxing-less JSON logging.</p><pre>// Just change LoggerMessage to ZLoggerMessage<br>// Note that in the format string part of ZLoggerMessage, you can use @ for aliases and json for JSON conversion, just like in the String Interpolation version<br>[ZLoggerMessage(LogLevel.Information, &quot;My name is {name}, age is {age}.&quot;)]<br>static partial void ZLoggerLog(this ILogger logger, string name, int age);<br><br>// This kind of code is generated<br>readonly struct ZLoggerLogState : IZLoggerFormattable<br>{<br>    // Pre-generate JsonEncodedText for JSON<br>    static readonly JsonEncodedText _jsonParameter_name = JsonEncodedText.Encode(&quot;name&quot;);<br>    static readonly JsonEncodedText _jsonParameter_age = JsonEncodedText.Encode(&quot;age&quot;);<br><br>    readonly string name;<br>    readonly int age;<br><br>    public ZLoggerLogState(string name, int age)<br>    {<br>        this.name = name;<br>        this.age = age;<br>    }<br><br>    public IZLoggerEntry CreateEntry(LogInfo info)<br>    {<br>        return ZLoggerEntry&lt;ZLoggerLogState&gt;.Create(info, this);<br>    }<br><br>    public int ParameterCount =&gt; 2;<br>    public bool IsSupportUtf8ParameterKey =&gt; true;<br>    public override string ToString() =&gt; $&quot;My name is {name}, age is {age}.&quot;;<br><br>    // Text messages are directly written to UTF8<br>    public void ToString(IBufferWriter&lt;byte&gt; writer)<br>    {<br>        var stringWriter = new Utf8StringWriter&lt;IBufferWriter&lt;byte&gt;&gt;(literalLength: 21, formattedCount: 2, bufferWriter: writer);<br>        stringWriter.AppendUtf8(&quot;My name is &quot;u8); // Write literals directly with u8<br>        stringWriter.AppendFormatted(name, 0, null);<br>        stringWriter.AppendUtf8(&quot;, age is &quot;u8);<br>        stringWriter.AppendFormatted(age, 0, null);<br>        stringWriter.AppendUtf8(&quot;.&quot;u8);            <br>        stringWriter.Flush();<br>    }<br><br>    // For JSON output, write directly to Utf8JsonWriter to completely avoid boxing<br>    public void WriteJsonParameterKeyValues(Utf8JsonWriter writer, JsonSerializerOptions jsonSerializerOptions, IKeyNameMutator? keyNameMutator = null)<br>    {<br>        // The method called differs depending on the type (WriteString, WriteNumber, etc...)<br>        writer.WriteString(_jsonParameter_name, this.name);<br>        writer.WriteNumber(_jsonParameter_age, this.age);<br>    }<br><br>    // Methods for extensions such as MessagePack support are actually generated below, but omitted<br>} <br><br>static partial void ZLoggerLog(this global::Microsoft.Extensions.Logging.ILogger logger, string name, int age)<br>{<br>    if (!logger.IsEnabled(LogLevel.Information)) return;<br>    logger.Log(<br>        LogLevel.Information,<br>        new EventId(-1, nameof(ZLoggerLog)),<br>        new ZLoggerLogState(name, age),<br>        null,<br>        (state, ex) =&gt; state.ToString()<br>    );<br>}</pre><p>By writing directly to Utf8JsonWriter and pre-generating key names as JsonEncodedText, we maximize the performance of JSON conversion.</p><p>Also, Structured Logging is not limited to JSON, other formats are possible. For example, using MessagePack could make it smaller and faster. ZLogger defines interfaces to avoid boxing even for output to protocols that are not built-in like JSON-specific ones.</p><pre>public interface IZLoggerFormattable : IZLoggerEntryCreatable<br>{<br>    int ParameterCount { get; }<br><br>    // Used for message output<br>    void ToString(IBufferWriter&lt;byte&gt; writer);<br>    <br>    // Used for JSON output<br>    void WriteJsonParameterKeyValues(Utf8JsonWriter jsonWriter, JsonSerializerOptions jsonSerializerOptions, IKeyNameMutator? keyNameMutator = null);<br><br>    // Used for other structured log outputs<br>    ReadOnlySpan&lt;byte&gt; GetParameterKey(int index);<br>    ReadOnlySpan&lt;char&gt; GetParameterKeyAsString(int index);<br>    object? GetParameterValue(int index);<br>    T? GetParameterValue&lt;T&gt;(int index);<br>    Type GetParameterType(int index);<br>}</pre><p>It’s a bit of an unusual interface, but by running a loop like this, we can eliminate the occurrence of boxing:</p><pre>for (var i in ParameterCount)<br>{<br>    var key = GetParameterKey(i);<br>    var value = GetParameterValue&lt;int&gt;();<br>}</pre><p>This design is the same as the usage of IDataRecord in ADO.NET. Also, in Unity, it’s common to retrieve via index to avoid allocation of arrays from native to managed.</p><h3>Unity</h3><p>Even with Unity 2023, the officially supported C# version is 9.0. ZLogger assumes C# 10.0 or higher String Interpolation as a prerequisite, so it won’t work normally. Normally. However, although it hasn’t been officially announced, we discovered that from Unity 2022.2, the version of the included compiler has been raised, and internally it&#39;s possible to compile with C# 10.0.</p><p>You can pass compiler options through the csc.rsp file, so if you explicitly specify the language version there, all C# 10.0 syntax becomes available.</p><pre>-langVersion:10</pre><p>As it is, the output csproj still specifies &lt;LangVersion&gt;9.0&lt;/LangVersion&gt;, so you can&#39;t write in C# 10.0 on the IDE. So let&#39;s overwrite the LangVersion using <a href="https://github.com/Cysharp/CsprojModifier">Cysharp/CsprojModifier</a>. If you create a file called LangVersion.props like this and have CsprojModifier mix it in, you&#39;ll be able to write as C# 10.0 on the IDE as well.</p><pre>&lt;Project xmlns=&quot;http://schemas.microsoft.com/developer/msbuild/2003&quot;&gt;<br>  &lt;PropertyGroup&gt;<br>    &lt;LangVersion&gt;10&lt;/LangVersion&gt;<br>    &lt;Nullable&gt;enable&lt;/Nullable&gt;<br>  &lt;/PropertyGroup&gt;<br>&lt;/Project&gt;</pre><p>For Unity, we’ve added an extension called AddZLoggerUnityDebug, so</p><pre>// Prepare such a global utility<br>public static class LogManager<br>{<br>    static ILoggerFactory loggerFactory;<br><br>    public static ILogger&lt;T&gt; CreateLogger&lt;T&gt;() =&gt; loggerFactory.CreateLogger&lt;T&gt;();<br>    public static readonly Microsoft.Extensions.Logging.ILogger Global;<br>    <br>    static LogManager()<br>    {<br>        loggerFactory = LoggerFactory.Create(logging =&gt;<br>        {<br>            logging.SetMinimumLevel(LogLevel.Trace);<br>            logging.AddZLoggerUnityDebug(); // log to UnityDebug<br>        });<br>        Global = loggerFactory.CreateLogger(&quot;Logger&quot;);<br>        Application.exitCancellationToken.Register(() =&gt;<br>        {<br>            loggerFactory.Dispose(); // flush when application exit.<br>        });<br>    }<br>}<br><br>// Try using it like this, for example<br>public class NewBehaviourScript : MonoBehaviour<br>{<br>    static readonly ILogger&lt;NewBehaviourScript&gt; logger = LogManager.CreateLogger&lt;NewBehaviourScript&gt;();<br><br>    void Start()<br>    {<br>        var name = &quot;foo&quot;;<br>        var hp = 100;<br>        logger.ZLogInformation($&quot;{name} HP is {hp}.&quot;);<br>    }<br>}</pre><blockquote><em>Note that the performance improvement of C# 10.0 String Interpolation is only applicable when using ZLog, and using String Interpolation for normal String generation will not improve performance. This is because DefaultInterpolatedStringHandler is needed in the runtime for string generation performance improvement, which is only included in .NET 6 and above. If DefaultInterpolatedStringHandler doesn’t exist, it falls back to the traditional string.Format, so boxing occurs as usual.</em></blockquote><p>It supports all JSON structured logging, output customization, file output, etc.</p><pre>var loggerFactory = LoggerFactory.Create(logging =&gt;<br>{<br>    logging.AddZLoggerFile(&quot;/path/to/logfile&quot;, options =&gt;<br>    {<br>        options.UseJsonFormatter();<br>    });<br>});</pre><p>And as one more bonus, with Unity 2022.3.12f1 and above, the C# compiler version is a bit higher, and if you specify -langVersion:preview, you can use C# 11.0. Also, ZLogger&#39;s Source Generator is automatically enabled, so you can use [ZLoggerMessage] to generate.</p><pre>public static partial class LogExtensions<br>{<br>    [ZLoggerMessage(LogLevel.Debug, &quot;Hello, {name}&quot;)]<br>    public static partial void Hello(this ILogger&lt;NewBehaviourScript&gt; logger, string name);<br>}</pre><p>Since the code generated by the Source Generator requires C# 11.0 (because it uses UTF8 String Literal extensively), [ZLoggerMessage] is limited to Unity 2022.3.12f1 and above.</p><p>By the way, Unity has released <a href="https://docs.unity3d.com/Packages/com.unity.logging@1.2/manual/index.html">com.unity.logging</a> as a standard logging library of the same kind. It allows structured logging and file output in the same way, and it had an interesting design of using Source Generator to automatically generate the class itself and generate method overloads according to arguments to avoid boxing of values. There’s a lot of talk about Burst, but I think this bold use of Source Generator is the key to performance. ZLogger is utilizing C# 10.0’s String Interpolation, but I hadn’t thought about such an approach as a workaround. It’s quite eye-opening. The performance is also quite refined.</p><p>ZLogger has better writing feel due to String Interpolation, and I’d like to think the performance is a good match… what do you think?</p><h3>Conclusion</h3><p>By the way, in creating ZLogger v2, <a href="https://twitter.com/hadashiA">@hadashiA</a>, famous for <a href="https://github.com/hadashiA/VContainer">VContainer</a> and <a href="https://github.com/hadashiA/VYaml">VYaml</a>, helped me from idea generation to detailed implementation, and put up with repeated specification overhauls. I think this v2 has become very complete, but I wouldn’t have reached this point alone, so I’m very grateful.</p><p>Anyway, I think ZLogger has become the strongest logger in terms of both ease of use and performance, so please give it a try.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=2d9733b43789" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[ConsoleAppFramework v5 — Zero Overhead, Native AOT-compatible CLI Framework for C#]]></title>
            <link>https://neuecc.medium.com/consoleappframework-v5-zero-overhead-native-aot-compatible-cli-framework-for-c-8f496df8d9d1?source=rss-61fea4eebf07------2</link>
            <guid isPermaLink="false">https://medium.com/p/8f496df8d9d1</guid>
            <category><![CDATA[csharp]]></category>
            <dc:creator><![CDATA[Yoshifumi Kawai]]></dc:creator>
            <pubDate>Fri, 14 Jun 2024 10:09:33 GMT</pubDate>
            <atom:updated>2024-06-14T10:09:33.897Z</atom:updated>
            <content:encoded><![CDATA[<h3>ConsoleAppFramework v5 — Zero Overhead, Native AOT-compatible CLI Framework for C#</h3><p>We have released a completely new version of <a href="https://github.com/Cysharp/ConsoleAppFramework">ConsoleAppFramework</a>. It is a brand new framework that has been completely redesigned and reimplemented from scratch. With the design principles of “<strong>Zero Dependency, Zero Overhead, Zero Reflection, Zero Allocation, AOT Safe</strong>”, it achieves overwhelming performance that outpaces others by a wide margin.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/788/0*Z4tlKFS3HUEBOtzE" /></figure><p>This benchmark is for cold startup without any warm-up, which we believe is the most relevant to actual usage in CLI applications. Compared to <a href="https://github.com/dotnet/command-line-api/">System.CommandLine</a>, it’s 280 times faster! The amount of memory allocation is also 100 to 1000 times less than other frameworks (the 400B shown is almost entirely system allocation, so the framework itself is 0).</p><p>This performance is achieved by generating everything with Source Generators. For example, consider the following code:</p><pre>using ConsoleAppFramework;<br><br>// args: ./cmd --foo 10 --bar 20<br>ConsoleApp.Run(args, (int foo, int bar) =&gt; Console.WriteLine($&quot;Sum: {foo + bar}&quot;));</pre><p>ConsoleAppFramework’s Source Generator analyzes the arguments of the lambda expression passed to Run and generates the Run method itself.</p><pre>internal static partial class ConsoleApp<br>{<br>    // Generate the Run method itself with arguments and body to match the lambda expression<br>    public static void Run(string[] args, Action&lt;int, int&gt; command)<br>    {<br>        // code body<br>    }<br>}</pre><p>Normally, C#’s Source Generators are triggered by attributes given to classes or methods, but ConsoleAppFramework monitors method invocations and uses them as the key for generation. This idea is inspired by Rust’s macros. In Rust, there are classifications like <a href="https://doc.rust-lang.org/book/ch19-06-macros.html">Attribute-like macros and Function-like macros</a>, and this approach can be considered a Function-like style.</p><p>The actual generated code in its entirety looks something like this:</p><pre>internal static partial class ConsoleApp<br>{<br>    public static void Run(string[] args, Action&lt;int, int&gt; command)<br>    {<br>        if (TryShowHelpOrVersion(args, 2, -1)) return;<br><br>        var arg0 = default(int);<br>        var arg0Parsed = false;<br>        var arg1 = default(int);<br>        var arg1Parsed = false;<br><br>        try<br>        {<br>            for (int i = 0; i &lt; args.Length; i++)<br>            {<br>                var name = args[i];<br><br>                switch (name)<br>                {<br>                    case &quot;--foo&quot;:<br>                    {<br>                        if (!TryIncrementIndex(ref i, args.Length) || !int.TryParse(args[i], out arg0)) { ThrowArgumentParseFailed(&quot;foo&quot;, args[i]); }<br>                        arg0Parsed = true;<br>                        break;<br>                    }<br>                    case &quot;--bar&quot;:<br>                    {<br>                        if (!TryIncrementIndex(ref i, args.Length) || !int.TryParse(args[i], out arg1)) { ThrowArgumentParseFailed(&quot;bar&quot;, args[i]); }<br>                        arg1Parsed = true;<br>                        break;<br>                    }<br>                    default:<br>                        // omit...(case-insensitive compare codes)<br>                        ThrowArgumentNameNotFound(name);<br>                        break;<br>                }<br>            }<br>            if (!arg0Parsed) ThrowRequiredArgumentNotParsed(&quot;foo&quot;);<br>            if (!arg1Parsed) ThrowRequiredArgumentNotParsed(&quot;bar&quot;);<br><br>            command(arg0!, arg1!);<br>        }<br>        catch (Exception ex)<br>        {<br>            Environment.ExitCode = 1;<br>            if (ex is ValidationException or ArgumentParseFailedException)<br>            {<br>                LogError(ex.Message);<br>            }<br>            else<br>            {<br>                LogError(ex.ToString());<br>            }<br>        }<br>    }<br><br>    static partial void ShowHelp(int helpId)<br>    {<br>        Log(&quot;&quot;&quot;<br>Usage: [options...] [-h|--help] [--version]<br><br>Options:<br>  --foo &lt;int&gt;     (Required)<br>  --bar &lt;int&gt;     (Required)<br>&quot;&quot;&quot;);<br>    }<br>}</pre><p>It looks like straightforward and simple code without any twists, doesn’t it? That’s important! The simpler the code, the faster it is! Simple despite being a framework, that’s why it’s fast. There is no extraneous code, and all the processing is aggregated in the method body itself, achieving zero overhead as a framework and the same speed as optimized handwritten code.</p><p>CLI applications typically involve single-shot execution from a cold start, making dynamic code generation (IL.Emit or Expression.Compile) and caching (speeding up subsequent matching through ArrayPool or Dictionary generation) less effective. Creating those would add more overhead. On the other hand, using reflection directly is slow in itself. ConsoleAppFramework dramatically speeds up single-shot execution by inline-generating all the necessary processing.</p><p>With no reflection, it also has overwhelming affinity with Native AOT, eliminating any disadvantages of C# in terms of cold startup speed.</p><p>Another feature is that since everything, including the ConsoleApp class, is generated by Source Generators, there are absolutely no dependencies, including ConsoleAppFramework itself.</p><p>There are various situations for creating console applications. Sometimes it’s a large batch application with many dependencies, and other times it’s a tiny single-function command. When creating a small command, you wouldn’t want to add any additional dependencies at all. Adding a reference to Microsoft.Extensions.Hosting alone brings in dozens of dependent DLLs! With ConsoleAppFramework, there are zero dependencies, including itself.</p><p>The advantage of zero dependencies is obviously a smaller binary size. Especially with Native AOT, binary size is a concern, but with ConsoleAppFramework, the additional cost is nearly zero.</p><p>And of course, a single function is not enough for a framework, so the following features are implemented. The rich set of features should be on par with other frameworks.</p><ul><li>SIGINT/SIGTERM(Ctrl+C) handling with gracefully shutdown via CancellationToken</li><li>Filter(middleware) pipeline to intercept before/after execution</li><li>Exit code management</li><li>Support for async commands</li><li>Registration of multiple commands</li><li>Registration of nested commands</li><li>Setting option aliases and descriptions from code document comment</li><li>System.ComponentModel.DataAnnotations attribute-based Validation</li><li>Dependency Injection for command registration by type and public methods</li><li>Microsoft.Extensions(Logging, Configuration, etc...) integration</li><li>High performance value parsing via ISpanParsable&lt;T&gt;</li><li>Parsing of params arrays</li><li>Parsing of JSON arguments</li><li>Help(-h|--help) option builder</li><li>Default show version(--version) option</li></ul><p>The generated code is modularized and varies depending on the features used by the code, always generating the minimum code required to implement that feature. This allows it to balance functionality and performance. Additionally, every feature has been carefully tuned to run at the fastest possible speed, so even with all features enabled, it remains overwhelmingly fast compared to others.</p><p>As an aside, delegates do have an allocation for delegate generation. In other words, it’s not truly zero allocation and zero overhead. However, ConsoleAppFramework does provide a mechanism to achieve true zero allocation. Pass a static function as a function pointer as follows:</p><pre>unsafe<br>{<br>    ConsoleApp.Run(args, &amp;Sum);<br>}<br><br>static void Sum(int x, int y) =&gt; Console.Write(x + y);</pre><p>Then it generates a method body with a delegate* managed&lt;&gt; argument (it may not be familiar, but C# has a language feature called managed function pointers).</p><pre>public static unsafe void Run(string[] args, delegate* managed&lt;int, int, void&gt; command)</pre><p>Now it’s completely and indisputably zero allocation and zero overhead!</p><h3>High-performance Value Conversion</h3><p>What is the fastest way to convert a string to a C# value? For int, it’s int.TryParse, right? What about others? Int is hardcoded, so it&#39;s easy, but how do you make string -&gt; T (or object) generic? It becomes a bit tricky, and in the past, <a href="https://learn.microsoft.com/en-us/dotnet/api/system.componentmodel.typeconverter?view=net-8.0">TypeConverter</a> was used. Of course, the performance is poor.</p><p>Alternatively, since JsonSerializer is now built-in, you could delegate it to that. Of course, the performance is not particularly good. Especially when considering cold startup, JsonSerializer requires caching, adding significant overhead for single-shot execution.</p><p>ConsoleAppFramework adopts <a href="https://learn.microsoft.com/en-us/dotnet/api/system.iparsable-1?view=net-8.0">IParsable</a> and <a href="https://learn.microsoft.com/en-us/dotnet/api/system.ispanparsable-1?view=net-8.0">ISpanParsable</a>. These were added in .NET 7 and use the static abstract interface added in C# 11.</p><pre>public interface IParsable&lt;TSelf&gt; where TSelf : IParsable&lt;TSelf&gt;?<br>{<br> static abstract TSelf Parse(string s, IFormatProvider? provider);<br> static abstract bool TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, [MaybeNullWhen(returnValue: false)] out TSelf result);<br>}</pre><p>Finally, with C# 11, a generic “string -&gt; value” conversion mechanism has been realized! ConsoleAppFramework adopts it without question as .NET 8/C# 12 is the minimum runtime requirement. New types introduced in .NET 8 such as Half and Int128, as well as user-defined types that implement IParsable&lt;T&gt;, can be used for high-performance processing!</p><p>However, for basic types like int, the Source Generator already knows it’s an int, so it directly executes int.TryParse.</p><p>As for value binding, it also supports params arrays and default values.</p><pre>ConsoleApp.Run(args, (<br>    [Argument]DateTime dateTime,  // Argument<br>    [Argument]Guid guidvalue,     //<br>    int intVar,                   // required<br>    bool boolFlag,                // flag<br>    MyEnum enumValue,             // enum<br>    int[] array,                  // array<br>    MyClass obj,                  // object<br>    string optional = &quot;abcde&quot;,    // optional<br>    double? nullableValue = null, // nullable<br>    params string[] paramsArray<br>    ) =&gt; { });</pre><p>C# 12 has just added <a href="https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/lambda-expressions#input-parameters-of-a-lambda-expression">the ability to use default values and params in lambda expressions</a>, which is reflected here.</p><h3>Defining with Document Comments</h3><p>In the past, or in other frameworks, adding Description and Alias was done using attributes. However, assigning attributes to each parameter of a method, especially with quite long strings, makes the method significantly less readable.</p><p>So ConsoleAppFramework decided to utilize document comments.</p><pre>class Commands<br>{<br>    /// &lt;summary&gt;<br>    /// Display Hello.<br>    /// &lt;/summary&gt;<br>    /// &lt;param name=&quot;message&quot;&gt;-m, Message to show.&lt;/param&gt;<br>    public static void Hello(string message) =&gt; Console.Write($&quot;Hello, {message}&quot;);<br>}</pre><p>This becomes a command like:</p><pre>Usage: [options...] [-h|--help] [--version]<br><br>Display Hello.<br><br>Options:<br>  -m|--message &lt;string&gt;    Message to show. (Required)</pre><p>With document comments, it’s possible to maintain a natural appearance even with many arguments. Being able to take this approach is a strength of the Source Generator approach, as the .xml file is not needed and comments can be read directly from the code. (However, some hacks were needed to make document comments readable in all environments with Source Generators)</p><h3>Adding Multiple Commands</h3><p>ConsoleApp.Run is a shortcut for a single command, but it&#39;s also possible to add multiple commands and nested subcommands. For example, let&#39;s look at the generation example when the following configuration is made.</p><pre>var app = ConsoleApp.Create();<br><br>app.Add(&quot;foo&quot;, () =&gt; { });<br>app.Add(&quot;foo bar&quot;, (int x, int y) =&gt; { });<br>app.Add(&quot;foo bar barbaz&quot;, (DateTime dateTime) =&gt; { });<br>app.Add(&quot;foo baz&quot;, async (string foo = &quot;test&quot;, CancellationToken cancellationToken = default) =&gt; { });<br><br>app.Run(args);</pre><p>The Add in this code is expanded as follows. The Source Generator knows the types of all the lambda expressions being added, so it assigns them to fields with unique types.</p><pre>partial struct ConsoleAppBuilder<br>{<br>    Action command0 = default!;<br>    Action&lt;int, int&gt; command1 = default!;<br>    Action&lt;global::System.DateTime&gt; command2 = default!;<br>    Func&lt;string, global::System.Threading.CancellationToken, Task&gt; command3 = default!;<br><br>    partial void AddCore(string commandName, Delegate command)<br>    {<br>        switch (commandName)<br>        {<br>            case &quot;foo&quot;:<br>                this.command0 = Unsafe.As&lt;Action&gt;(command);<br>                break;<br>            case &quot;foo bar&quot;:<br>                this.command1 = Unsafe.As&lt;Action&lt;int, int&gt;&gt;(command);<br>                break;<br>            case &quot;foo bar barbaz&quot;:<br>                this.command2 = Unsafe.As&lt;Action&lt;global::System.DateTime&gt;&gt;(command);<br>                break;<br>            case &quot;foo baz&quot;:<br>                this.command3 = Unsafe.As&lt;Func&lt;string, global::System.Threading.CancellationToken, Task&gt;&gt;(command);<br>                break;<br>            default:<br>                break;<br>        }<br>    }<br>}</pre><p>This prevents the need for arrays to hold Delegates and the reflection/boxing overhead of invoking them as Delegates.</p><p>In Run, a switch with constant strings is embedded to select the command from string[] args.</p><pre>partial void RunCore(string[] args)<br>{<br>    if (args.Length == 0)<br>    {<br>        ShowHelp(-1);<br>        return;<br>    }<br>    switch (args[0])<br>    {<br>        case &quot;foo&quot;:<br>            if (args.Length == 1)<br>            {<br>                RunCommand0(args, args.AsSpan(1), command0);<br>                return;<br>            }<br>            switch (args[1])<br>            {<br>                case &quot;bar&quot;:<br>                    if (args.Length == 2)<br>                    {<br>                        RunCommand1(args, args.AsSpan(2), command1);<br>                        return;<br>                    }<br>                    switch (args[2])<br>                    {<br>                        case &quot;barbaz&quot;:<br>                            RunCommand2(args, args.AsSpan(3), command2);<br>                            break;<br>                        default:<br>                            RunCommand1(args, args.AsSpan(2), command1);<br>                            break;<br>                    }<br>                    break;<br>                case &quot;baz&quot;:<br>                    RunCommand3(args, args.AsSpan(2), command3);<br>                    break;<br>                default:<br>                    RunCommand0(args, args.AsSpan(1), command0);<br>                    break;<br>            }<br>            break;<br>        default:<br>            ShowHelp(-1);<br>            break;<br>    }<br>}</pre><p>The fastest way in C# to jump from a string to specific code is to use a switch with string constants. The expanded algorithm has been revised several times, and in C# 12, as <a href="https://github.com/dotnet/roslyn/issues/56374">Performance: faster switch over string objects · Issue #56374 · dotnet/roslyn</a>, it first checks the length and then narrows down to a single character where the difference exists to match.</p><p>This is faster than matching from Dictionary&lt;string, T&gt; and has no initialization time or allocations, which is the strength of being able to leverage the C# compiler. Such processing can only be done with the Source Generator approach that outputs C# code itself. So it&#39;s absolutely the fastest.</p><h3>DI, CancellationToken, and Lifetime</h3><p>In addition to parameters that become valid as command parameters, arguments can also define types that you want to pass via DI (such as ILogger&lt;T&gt; or Option&lt;T&gt;) and special handling types such as ConsoleAppContext and CancellationToken.</p><p>Receiving via DI is effective in situations where the console application wants to share configuration files with ASP.NET projects. Forsuch cases, integration with Microsoft.Extensions.Hosting is possible.</p><p>Also, when CancellationToken is passed, lifetime management as a console application that hooks SIGINT/SIGTERM/SIGKILL (Ctrl+C) becomes active.</p><pre>await ConsoleApp.RunAsync(args, async (int foo, CancellationToken cancellationToken) =&gt;<br>{<br>    await Task.Delay(TimeSpan.FromSeconds(5), cancellationToken);<br>    Console.WriteLine($&quot;Foo: {foo}&quot;);<br>});</pre><p>The above code is expanded as follows:</p><pre>using var posixSignalHandler = PosixSignalHandler.Register(ConsoleApp.Timeout);<br>var arg0 = posixSignalHandler.Token;<br><br>await Task.Run(() =&gt; command(arg0!)).WaitAsync(posixSignalHandler.TimeoutToken);</pre><p>Using <a href="https://learn.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.posixsignalregistration?view=net-8.0">PosixSignalRegistration</a> added in .NET 6, it hooks SIGINT/SIGTERM/SIGKILL and cancels the CancellationToken. At the same time, it suppresses immediate termination (normally pressing Ctrl + C causes an immediate Abort, but it no longer Aborts).</p><p>This leaves room for the application to properly handle the CancellationToken.</p><p>However, if the CancellationToken is not handled, it simply ignores the termination command, which is troublesome in itself, so a forced termination timeout is set. By default, it is set to 5 seconds, but this can be freely changed with the ConsoleApp.Timeout property. If you want to turn off forced termination, specify ConsoleApp.Timeout = Timeout.InfiniteTimeSpan.</p><p><a href="https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.task.waitasync?view=net-8.0">Task.WaitAsync</a> is from .NET 6. In addition to passing a TimeSpan, it’s also possible to pass a CancellationToken, allowing conditions such as firing WaitAsync after PosixSignalRegistration fires, then after a timeout, rather than a simple few seconds later.</p><h3>Filter Pipeline</h3><p>ConsoleAppFramework adopts Filters as a mechanism to hook before and after execution. Also known as the middleware pattern, it’s a pattern often seen in languages that support async/await.</p><pre>internal class NopFilter(ConsoleAppFilter next) : ConsoleAppFilter(next) // ctor needs `ConsoleAppFilter next` and call base(next)<br>{<br>    // implement InvokeAsync as filter body<br>    public override async Task InvokeAsync(ConsoleAppContext context, CancellationToken cancellationToken)<br>    {<br>        try<br>        {<br>            /* on before */<br>            await Next.InvokeAsync(context, cancellationToken); // invoke next filter or command body<br>            /* on after */<br>        }<br>        catch<br>        {<br>            /* on error */<br>            throw;<br>        }<br>        finally<br>        {<br>            /* on finally */<br>        }<br>    }<br>}</pre><p>This design pattern is truly excellent, and if you need to provide a mechanism to hook execution, I highly recommend adopting this pattern. If async/await existed in the GoF era, it would have been included as an important design pattern.</p><p>The README introduces logging execution time, customizing ExitCode, prohibiting multiple executions, and authentication processing as things that can be done with filters. The wonderfulness of being able to realize various processes with a single Task InvokeAsync.</p><p>There are various approaches to designing filters, but ConsoleAppFramework chose the method that yields the highest performance. By receiving Next in the constructor and determining all the filters to be used statically at code generation time (dynamic addition is not allowed), everything is embedded and assembled.</p><pre>app.UseFilter&lt;NopFilter&gt;();<br>app.UseFilter&lt;NopFilter&gt;();<br>app.UseFilter&lt;NopFilter&gt;();<br>app.UseFilter&lt;NopFilter&gt;();<br>app.UseFilter&lt;NopFilter&gt;();<br><br>// The above code will generate the following code:<br><br>sealed class Command0Invoker(string[] args, Action command) : ConsoleAppFilter(null!)<br>{<br>    public ConsoleAppFilter BuildFilter()<br>    {<br>        var filter0 = new NopFilter(this);<br>        var filter1 = new NopFilter(filter0);<br>        var filter2 = new NopFilter(filter1);<br>        var filter3 = new NopFilter(filter2);<br>        var filter4 = new NopFilter(filter3);<br>        return filter4;<br>    }<br><br>    public override Task InvokeAsync(ConsoleAppContext context, CancellationToken cancellationToken)<br>    {<br>        return RunCommand0Async(context.Arguments, args, command, context, cancellationToken);<br>    }<br>}</pre><p>This avoids intermediate array allocations and lambda capture allocations, with only the number of filters + 1 (wrapping the method body) as the additional cost. Also, if the return value Task completes synchronously, something equivalent to Task.Completed is used, so there’s no need to make it a ValueTask.</p><p>Writing code that only receives Next in the constructor and passes it to base has become easy thanks to primary constructors in C# 12.</p><h3>Command-line Argument Syntax</h3><p>Apart from being passed to string[] args as space-separated, command-line arguments are completely free. It&#39;s somewhat assumed that -- or - are parameter identifiers, but in reality, anything goes, and in Windows, even / is often used.</p><p>That said, there are some standard rules to some extent. The most well-known are probably the <a href="https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html">POSIX standard</a> and its extension, the <a href="https://www.gnu.org/prep/standards/html_node/Command_002dLine-Interfaces.html">GNU Coding Standards</a>. ConsoleAppFramework also follows the POSIX standard to some extent and includes the --version and --help defined in the GNU Coding Standards as built-in options. The names are also --lower-kebab-case by default.</p><p>“To some extent” means that it doesn’t fully conform to the standard. Whether it’s standards or traditional conventions, not a few old rules are unacceptable from a modern perspective. For example, distinguishing between -x and -X to have different behaviors is an absolute no-no. Or even widely used practices like bundling, where -fdx is interpreted as -f, -d, -x, are not very good in my opinion. Bundling is also problematic in terms of performance as it complicates the parsing process.</p><p>Since ConsoleAppFramework prioritizes performance, it does not adopt rules that may cause performance issues. It is designed to not distinguish between uppercase and lowercase, but since case-insensitive matching is performed after lowercase matching first, there is no practical performance degradation.</p><p>Looking at <a href="https://learn.microsoft.com/en-us/dotnet/standard/commandline/syntax">Overview of System.CommandLine command-line syntax — .NET | Microsoft Learn</a>, it’s clear that System.CommandLine allows for quite flexible syntax interpretation. That’s a very good thing! It’s a good thing, but if it causes performance degradation, it’s a problem. And in fact, as evident from the benchmark results, the performance of System.CommandLine is very poor. This is unacceptable.</p><p>The wandering <a href="https://github.com/dotnet/command-line-api">System.CommandLine</a> seems to be decomposed again and changing its implementation. With <a href="https://github.com/dotnet/command-line-api/issues/2338">Resetting System.CommandLine</a>, it aims to have a small core as a POSIX standard parser adopted as standard in .NET 9 or .NET 10.</p><p>Even if they are adopted as standard, from a performance perspective, they will absolutely never surpass ConsoleAppFramework.</p><h3>Compatibility with v4</h3><p>Breaking changes! Not shying away from breaking changes is a good thing, it doesn’t hinder innovation, it’s necessary to remain cutting-edge. Running at the forefront of C# is also part of Cysharp’s identity. At the same time, of course, it’s a huge inconvenience. This change from v4 to v5 is like the change from .NET Framework to .NET Core, or from ASP.NET to ASP.NET Core, so it can’t be helped, it was an absolutely necessary change…</p><p>However, in reality, it hasn’t changed that much. The name conversion logic (lower-kebab-case) uses the same logic, so there’s no concern about names going out of sync. It’s just a matter of mapping the method names that cause compile errors. That happens quite often, right?</p><pre>var app = ConsoleApp.Create(args); app.Run(); -&gt; var app = ConsoleApp.Create(); app.Run(args);<br>app.AddCommand/AddSubCommand -&gt; app.Add(string commandName)<br>app.AddRootCommand -&gt; app.Add(&quot;&quot;)<br>app.AddCommands&lt;T&gt; -&gt; app.Add&lt;T&gt;<br>app.AddSubCommands&lt;T&gt; -&gt; app.Add&lt;T&gt;(string commandPath)<br>app.AddAllCommandType -&gt; NotSupported(use Add&lt;T&gt; manually)<br>[Option(int index)] -&gt; [Argument]<br>[Option(string shortName, string description)] -&gt; Xml Document Comment<br>ConsoleAppFilter.Order -&gt; NotSupported(global -&gt; class -&gt; method declrative order)<br>ConsoleAppOptions.GlobalFilters -&gt; app.UseFilter&lt;T&gt;</pre><p>Overall, I think the specification changes can be considered as simplifications, in other words, “improvements”.</p><p>Also, not relying on Microsoft.Extensions.Hosting by default is a big difference, but it can be resolved by adding one line. Riding on top of Hosting means using the ServiceProvider generated by Hosting, that&#39;s all. In reality, there&#39;s also Lifetime management, but ConsoleAppFramework handles that on its own, so in practical terms, there&#39;s no difference as long as you pass the ServiceProvider for DI.</p><pre>using var host = Host.CreateDefaultBuilder().Build(); // use using for host lifetime<br>ConsoleApp.ServiceProvider = host.ServiceProvider;</pre><p>In v4, ConsoleAppBase had to be inherited, but in v5, POCO is sufficient. Instead, please receive ConsoleAppContext and CancellationToken via constructor injection. This has also become less troublesome thanks to primary constructors in C# 12. This is another reason for abandoning the mechanism that requires a base class.</p><h3>True Incremental Generator</h3><p>Incremental Generators, if you just create them without any consideration, don’t actually become Incremental.</p><p>The first thing to do is to make it visible whether it is Incremental or not. Normally, the internal state is completely invisible when running, so it’s important to make it possible to check the state in unit tests. For example, a unit test like this is written.</p><pre>    [Fact]<br>    public void RunLambda()<br>    {<br>        var step1 = &quot;&quot;&quot;<br>using ConsoleAppFramework;<br><br>ConsoleApp.Run(args, int () =&gt; 0);<br>&quot;&quot;&quot;;<br><br>        var step2 = &quot;&quot;&quot;<br>using ConsoleAppFramework;<br><br>ConsoleApp.Run(args, int () =&gt; 100); // body change<br><br>Console.WriteLine(&quot;foo&quot;); // unrelated line<br>&quot;&quot;&quot;;<br><br>        var step3 = &quot;&quot;&quot;<br>using ConsoleAppFramework;<br><br>ConsoleApp.Run(args, int (int x, int y) =&gt; 100); // change signature<br><br>Console.WriteLine(&quot;foo&quot;);<br>&quot;&quot;&quot;;<br><br>        var reasons = CSharpGeneratorRunner.GetIncrementalGeneratorTrackedStepsReasons(&quot;ConsoleApp.Run.&quot;, step1, step2, step3);<br><br>        reasons[0][0].Reasons.Should().Be(&quot;New&quot;);<br>        reasons[1][0].Reasons.Should().Be(&quot;Unchanged&quot;);<br>        reasons[2][0].Reasons.Should().Be(&quot;Modified&quot;);<br><br>        VerifySourceOutputReasonIsCached(reasons[1]);<br>        VerifySourceOutputReasonIsNotCached(reasons[2]);<br>    }</pre><p>When you run the Driver with the trackIncrementalGeneratorSteps: true option for an Incremental Generator, the state of each step becomes visible. IncrementalStepRunReason has states like New, Unchanged, Modified, Cached, and Removed, and if the step before the final output is Unchanged or Cached, the output processing is skipped.</p><p>In the above unit test, step2 only has changes in parts that don’t affect the output code, so it’s Unchanged. So the final stage was Cached. step3 has changes that require regeneration, so it’s Modified and runs through the source code generation process.</p><p>IncrementalStepRunReason can be retrieved from TrackedSteps, but it&#39;s a bit too hard to read as is, so it&#39;s formatted to make it easier to check, which is the GetIncrementalGeneratorTrackedStepsReasons utility method.</p><pre>public static (string Key, string Reasons)[][] GetIncrementalGeneratorTrackedStepsReasons(string keyPrefixFilter, params string[] sources)<br>{<br>    var parseOptions = new CSharpParseOptions(LanguageVersion.CSharp12); // 12<br>    var driver = CSharpGeneratorDriver.Create(<br>        [new ConsoleAppGenerator().AsSourceGenerator()],<br>        driverOptions: new GeneratorDriverOptions(IncrementalGeneratorOutputKind.None, trackIncrementalGeneratorSteps: true))<br>        .WithUpdatedParseOptions(parseOptions);<br><br>    var generatorResults = sources<br>        .Select(source =&gt;<br>        {<br>            var compilation = baseCompilation.AddSyntaxTrees(CSharpSyntaxTree.ParseText(source, parseOptions));<br>            driver = driver.RunGenerators(compilation);<br>            return driver.GetRunResult().Results[0];<br>        })<br>        .ToArray();<br><br>    var reasons = generatorResults<br>        .Select(x =&gt; x.TrackedSteps<br>            .Where(x =&gt; x.Key.StartsWith(keyPrefixFilter) || x.Key == &quot;SourceOutput&quot;)<br>            .Select(x =&gt;<br>            {<br>                if (x.Key == &quot;SourceOutput&quot;)<br>                {<br>                    var values = x.Value.Where(x =&gt; x.Inputs[0].Source.Name?.StartsWith(keyPrefixFilter) ?? false);<br>                    return (<br>                        x.Key,<br>                        Reasons: string.Join(&quot;, &quot;, values.SelectMany(x =&gt; x.Outputs).Select(x =&gt; x.Reason).ToArray())<br>                    );<br>                }<br>                else<br>                {<br>                    return (<br>                        Key: x.Key.Substring(keyPrefixFilter.Length),<br>                        Reasons: string.Join(&quot;, &quot;, x.Value.SelectMany(x =&gt; x.Outputs).Select(x =&gt; x.Reason).ToArray())<br>                    );<br>                }<br>            })<br>            .OrderBy(x =&gt; x.Key)<br>            .ToArray())<br>        .ToArray();<br><br>    return reasons;<br>}</pre><p>It’s a mess and hard to understand, meaning that TrackedSteps itself is really hard to understand as is. Since TrackedSteps is an ImmutableDictionary, the enumeration order is random and hard to check, so I added numbering and sorting. Also, when multiple RegisterSourceOutputs are running (ConsoleAppFramework has two types: Run-based and Builder-based), it becomes confusing when they get mixed up, so I added filtering by keyPrefix.</p><h3>Summary</h3><p>Originally, ConsoleAppFramework was unique among Cysharp’s product lines in that it didn’t prioritize performance. It was built around the concept of integrating with Hosting, which was rare at the time, to create a CLI framework, and achieved some success. There were a few revisions that made Help richer and allowed writing in a Minimal API-like style, but the clunkiness became noticeable.</p><p>In particular, <a href="https://github.com/mayuki/Cocona">Cocona</a> is a truly excellent library that was influenced by ConsoleAppFramework while offering more flexibility and powerful features. At this rate, ConsoleAppFramework would be just an inferior version, which was a concern. It’s painful to not be able to recommend it with confidence as the best. After all, the creator of Cocona is a colleague at Cysharp…</p><p>So this time, while taking influence from some of Cocona’s APIs (like [Argument]), I strived to make it a framework with a completely different character. As explained in the parsing section, ConsoleAppFramework v5 sacrifices some flexibility for performance, so if you need rich functionality, I recommend using System.CommandLine or Cocona.</p><p>Also, from a performance perspective, the longer the actual execution time, the less the framework overhead matters. If the processing takes 10 minutes, 1 minute, or even 10 seconds, whether the framework portion takes 1ms or 50ms is like a margin of error. This is true even for JIT compilation, but in recent times with complaints about Native AOT and cold startup speed, it’s not something that can be dismissed outright, and it’s certainly better to be faster.</p><p>While the advantages of performance and zero dependencies are obvious, I believe it has also become a unique and interesting framework in terms of approach and design. Please give it a try! Of course, it’s also extremely practical, so you could consider it an essential library without hesitation!</p><p><a href="https://github.com/Cysharp/ConsoleAppFramework">https://github.com/Cysharp/ConsoleAppFramework</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=8f496df8d9d1" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How to create a modern C# web API client: An example implementation of the C# SDK for Anthropic…]]></title>
            <link>https://neuecc.medium.com/how-to-create-a-modern-c-web-api-client-an-example-implementation-of-the-c-sdk-for-anthropic-92a9f6cdd6d1?source=rss-61fea4eebf07------2</link>
            <guid isPermaLink="false">https://medium.com/p/92a9f6cdd6d1</guid>
            <category><![CDATA[anthropics]]></category>
            <category><![CDATA[claude]]></category>
            <category><![CDATA[csharp]]></category>
            <dc:creator><![CDATA[Yoshifumi Kawai]]></dc:creator>
            <pubDate>Sun, 24 Mar 2024 21:21:25 GMT</pubDate>
            <atom:updated>2024-03-24T21:21:25.033Z</atom:updated>
            <content:encoded><![CDATA[<h3><strong>How to create a modern C# web API client: An example implementation of the C# SDK for Anthropic Claude</strong></h3><p><a href="https://www.anthropic.com/">Anthropic Claude 3</a>, a recently emerged rising star among LLMs, has exceptionally high performance and surpasses GPT-4! I am greatly impressed by it. Therefore, I wanted to use it with C#, but since there was no SDK available, I created an unofficial one. The library is named Claudia, derived from Claude. It can be used across the .NET ecosystem, and I have confirmed its functionality in both Unity Runtime and Editor, so I believe it can be utilized in various ways depending on your ideas.</p><p><a href="https://github.com/Cysharp/Claudia">GitHub — Cysharp/Claudia</a></p><p>To give you an idea of what style of Web API SDK you can create in C#, please take a look at the Claudia usage example first.</p><p>The primary design principle in creating this SDK was to make it as similar as possible to the official <a href="https://github.com/anthropics/anthropic-sdk-python">Python SDK</a> and <a href="https://github.com/anthropics/anthropic-sdk-typescript">TypeScript SDK</a>. This is because the explanations in the documentation will be based on these official SDKs, and many articles in the world will also be based on the official SDKs. You may also want to use the official <a href="https://docs.anthropic.com/claude/prompt-library">prompt library</a> with API requests.</p><p>In such cases, if the API style is different, it will require cognitive load for conversion. Although it’s a trivial matter, it’s crucial and can be a stumbling block, so we thoroughly remove it. On top of that, balancing C#-ness without forcibly introducing dynamic elements is important in the design.</p><p>The appearance of the C# client looks like this:</p><pre>// C#<br>using Claudia;<br><br>var anthropic = new Anthropic();<br><br>var message = await anthropic.Messages.CreateAsync(new()<br>{<br>    Model = &quot;claude-3-opus-20240229&quot;,<br>    MaxTokens = 1024,<br>    Messages = [new() { Role = &quot;user&quot;, Content = &quot;Hello, Claude&quot; }]<br>});<br><br>Console.WriteLine(message);</pre><p>For comparison, the TypeScript version looks like this:</p><pre>// TypeScript<br>import Anthropic from &#39;@anthropic-ai/sdk&#39;;<br><br>const anthropic = new Anthropic();<br><br>const message = await anthropic.messages.create({<br>    model: &#39;claude-3-opus-20240229&#39;,<br>    max_tokens: 1024,<br>    messages: [{ role: &#39;user&#39;, content: &#39;Hello, Claude&#39; }],<br>});<br><br>console.log(message.content);</pre><p>They are quite similar, right? On top of that, the C# version doesn’t use dynamic or Dictionary&lt;string, object&gt;, and everything is specified with typed objects. The example above utilizes <a href="https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-9.0/target-typed-new">Target-typed new expressions</a> added in C# 9.0 and <a href="https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-12.0/collection-expressions">Collection expressions</a> added in C# 12, which are assumed to exist and are used to match the API nicely.</p><p>Often, APIs of dynamically typed languages appear (visually) simpler and easier to use, so being able to write with the same level of simplicity while being properly typed is a significant strength of modern C#. (The reason I decided to match the official TypeScript SDK in the first place was that I thought the API style of the official SDK was well-designed from my perspective; if it were terrible, I wouldn’t have attempted to match it.)</p><h3><strong>Streaming and Blazor</strong></h3><p>The Streaming API is also available, and when combined with Blazor, it’s easy to create a real-time updating Chat UI. The code is really just this, with the method body being just over 10 lines!</p><pre>[Inject]<br>public required Anthropic Anthropic { get; init; }<br><br>double temperature = 1.0;<br>string textInput = &quot;&quot;;<br>string systemInput = SystemPrompts.Claude3;<br>List&lt;Message&gt; chatMessages = new();<br><br>async Task SendClick()<br>{<br>    chatMessages.Add(new() { Role = Roles.User, Content = textInput });<br><br>    var stream = Anthropic.Messages.CreateStreamAsync(new()<br>    {<br>        Model = Models.Claude3Opus,<br>        MaxTokens = 1024,<br>        Temperature = temperature,<br>        System = string.IsNullOrWhiteSpace(systemInput) ? null : systemInput,<br>        Messages = chatMessages.ToArray()<br>    });<br><br>    var currentMessage = new Message { Role = Roles.Assistant, Content = &quot;&quot; };<br>    chatMessages.Add(currentMessage);<br><br>    textInput = &quot;&quot;;<br>    StateHasChanged();<br><br>    await foreach (var messageStreamEvent in stream)<br>    {<br>        if (messageStreamEvent is ContentBlockDelta content)<br>        {<br>            currentMessage.Content[0].Text += content.Delta.Text;<br>            StateHasChanged();<br>        }<br>    }<br>}</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/771/0*_Q0GiR7tvdTfN1Jv" /></figure><p>All request/response types are serializable with System.Text.Json.JsonSerializer, so serializing this List&lt;Message&gt; as-is will save it, and deserializing it will load it.</p><h3>Function Calling</h3><p>Claudia is not just an SDK that requests a REST API. It utilizes Source Generators to provide a mechanism for easily defining Function Calling.</p><p>What are the benefits of Function Calling? Currently, there are several things that LLMs can’t do on their own. For example, calculation is an area where they often return plausible-looking answers, and while you can improve the accuracy of plausibility by having them think step-by-step, they can’t perform accurate calculations (when given complex calculations, they tend to give answers that look correct but are wrong). In that case, if calculation is needed, you can simply use a calculator to calculate and create sentences based on that answer. They also can’t answer the current date and time. If you ask them to summarize or translate a specified web page, they will say they can’t see the contents. Function Calling solves these issues.</p><p>First, as an example, let’s define a function that returns a specified URL’s web page to Claude.</p><pre>public static partial class FunctionTools<br>{<br>    /// &lt;summary&gt;<br>    /// Retrieves the HTML from the specified URL.<br>    /// &lt;/summary&gt;<br>    /// &lt;param name=&quot;url&quot;&gt;The URL to retrieve the HTML from.&lt;/param&gt;<br>    [ClaudiaFunction]<br>    static async Task&lt;string&gt; GetHtmlFromWeb(string url)<br>    {<br>        using var client = new HttpClient();<br>        return await client.GetStringAsync(url);<br>    }<br>}</pre><p>The function defined with [ClaudiaFunction] generates various things through the Source Generator. To use this, it will be as follows:</p><pre>var input = new Message<br>{<br>    Role = Roles.User,<br>    Content = &quot;&quot;&quot;<br>        Could you summarize this page in three lines?<br>        https://docs.anthropic.com/claude/docs/intro-to-claude<br>&quot;&quot;&quot;<br>};<br><br>var message = await anthropic.Messages.CreateAsync(new()<br>{<br>    Model = Models.Claude3Haiku,<br>    MaxTokens = 1024,<br>    System = FunctionTools.SystemPrompt, // set generated prompt<br>    StopSequences = [StopSequnces.CloseFunctionCalls], // set &lt;/function_calls&gt; as stop sequence<br>    Messages = [input],<br>});<br><br>var partialAssistantMessage = await FunctionTools.InvokeAsync(message);<br><br>var callResult = await anthropic.Messages.CreateAsync(new()<br>{<br>    Model = Models.Claude3Haiku,<br>    MaxTokens = 1024,<br>    System = FunctionTools.SystemPrompt,<br>    Messages = [<br>        input,<br>        new() { Role = Roles.Assistant, Content = partialAssistantMessage! } // set as Assistant<br>    ],<br>});<br><br>// The page can be summarized in three lines:<br>// 1. Claude is a family of large language models developed by Anthropic designed to revolutionize the way you interact with AI.<br>// 2. This documentation is designed to help you get the most out of Claude, with clear explanations, examples, best practices, and links to additional resources.<br>// 3. Claude excels at a wide variety of tasks involving language, reasoning, analysis, coding, and more, and the documentation covers key capabilities, getting started with prompting, and using the API.<br>Console.WriteLine(callResult);</pre><p>Two requests are made to Claude. First, in the initial request to Claude, the question is sent along with a list and description of available functions. If it is determined that executing a function is optimal, the function name and parameters to be executed are returned. After that, executing the function locally and passing the result back to Claude yields the desired final result.</p><p>So what is the Source Generator doing? First, it generates FunctionTools.SystemPrompt that is passed to Claude&#39;s system text, and its contents are as follows (partially omitted).</p><pre>&lt;tools&gt;<br>    &lt;tool_description&gt;<br>        &lt;tool_name&gt;GetHtmlFromWeb&lt;/tool_name&gt;<br>        &lt;description&gt;Retrieves the HTML from the specified URL.&lt;/description&gt;<br>        &lt;parameters&gt;<br>            &lt;parameter&gt;<br>                &lt;name&gt;url&lt;/name&gt;<br>                &lt;type&gt;string&lt;/type&gt;<br>                &lt;description&gt;The URL to retrieve the HTML from.&lt;/description&gt;<br>            &lt;/parameter&gt;<br>        &lt;/parameters&gt;<br>    &lt;/tool_description&gt;<br>&lt;/tools&gt;</pre><p>It’s XML. Claude is designed to <a href="https://docs.anthropic.com/claude/docs/use-xml-tags">recognize XML tags</a>, and using XML tags is considered a best practice when you want to provide clear information systematically. Therefore, it automatically generates XML to pass from C# functions to Claude. You wouldn’t want to write this by hand, would you?</p><p>Claude then returns a result like the following in response to that request.</p><pre>&lt;function_calls&gt;<br>    &lt;invoke&gt;<br>        &lt;tool_name&gt;GetHtmlFromWeb&lt;/tool_name&gt;<br>        &lt;parameters&gt;<br>            &lt;url&gt;https://docs.anthropic.com/claude/docs/intro-to-claude&lt;/url&gt;<br>        &lt;/parameters&gt;<br>    &lt;/invoke&gt;</pre><p>Again, it’s XML (the closing tag is missing because it’s stopped by StopSequences. No further information is needed if you want to call a function, so it’s cut off). The Source Generator generates the FunctionTools.InvokeAsync method to parse this, execute the function (GetHtmlFromWeb), and pass it to Claude. The actually generated InvokeAsync method looks like this:</p><pre>public static async ValueTask&lt;string?&gt; InvokeAsync(MessageResponse message)<br>{<br>    var content = message.Content.FirstOrDefault(x =&gt; x.Text != null);<br>    if (content == null) return null;<br><br>    var text = content.Text;<br>    var tagStart = text .IndexOf(&quot;&lt;function_calls&gt;&quot;);<br>    if (tagStart == -1) return null;<br><br>    var functionCalls = text.Substring(tagStart) + &quot;&lt;/function_calls&gt;&quot;;<br>    var xmlResult = XElement.Parse(functionCalls);<br><br>    var sb = new StringBuilder();<br>    sb.AppendLine(functionCalls);<br>    sb.AppendLine(&quot;&lt;function_results&gt;&quot;);<br><br>    foreach (var item in xmlResult.Elements(&quot;invoke&quot;))<br>    {<br>        var name = (string)item.Element(&quot;tool_name&quot;)!;<br>        switch (name)<br>        {<br>            case &quot;GetHtmlFromWeb&quot;:<br>                {<br>                    var parameters = item.Element(&quot;parameters&quot;)!;<br><br>                    var _0 = (string)parameters.Element(&quot;url&quot;)!;<br><br>                    BuildResult(sb, &quot;GetHtmlFromWeb&quot;, await GetHtmlFromWeb(_0).ConfigureAwait(false));<br>                    break;<br>                }<br><br>            default:<br>                break;<br>        }<br>    }<br><br>    sb.Append(&quot;&lt;/function_results&gt;&quot;); // final assistant content cannot end with trailing whitespace<br><br>    return sb.ToString();<br><br>    static void BuildResult&lt;T&gt;(StringBuilder sb, string toolName, T result)<br>    {<br>        sb.AppendLine(@$&quot;    &lt;result&gt;<br>    &lt;tool_name&gt;{toolName}&lt;/tool_name&gt;<br>    &lt;stdout&gt;{result}&lt;/stdout&gt;<br>&lt;/result&gt;&quot;);<br>    }<br>}</pre><p>You wouldn’t want to write this by hand. Especially as the number of functions you want to call increases, it becomes more and more difficult.</p><p>By invoking &amp; generating XML and passing it back to Claude as the initial output result by the Assistant, you can obtain the desired answer. This technique is officially introduced as one of the best practices in <a href="https://docs.anthropic.com/claude/docs/prefill-claudes-response">Prefill Claude’s response</a> and is beneficial for guiding Claude’s responses in the desired direction. For example, if you return { as a prefill response, the probability of Claude outputting the result as JSON increases dramatically.</p><h3>vs Semantic Kernel</h3><p>It seems that C# users, in particular, tend to utilize <a href="https://github.com/microsoft/semantic-kernel">Semantic Kernel</a> for everything, but the functionality of Semantic Kernel is a bit excessive. If you are a C# engineer, it’s better to handle data storage and many other features on your own.</p><p><a href="https://docs.anthropic.com/claude/docs/intro-to-claude">The User Guides in Claude’s API documentation</a> are clear and excellent. Regardless of the framework you go through, ultimately what gets executed is the Raw API. Instead of a generic abstraction, I think it’s good to focus specifically on Claude and consider how to leverage its distinctive XML-based instructions.</p><h3>How to Create a Modern Web API Client</h3><p>From here, we’ll discuss how to design a modern API client based on Claudia’s design.</p><p>First, use <a href="https://learn.microsoft.com/en-us/dotnet/api/system.net.http.httpclient?view=net-8.0">HttpClient</a> as the communication foundation. It’s the only choice. There’s no room for objection. Even <a href="https://github.com/grpc/grpc-dotnet/tree/master/src/Grpc.Net.Client">Grpc.Net.Client</a> uses HttpClient for HTTP/2 gRPC communication. Like it or not, the foundation of all HTTP-based communication is HttpClient.</p><p>Here, it’s a good idea to allow accepting HttpMessageHandler from the outside.</p><pre>public class Anthropic : IMessages, IDisposable<br>{<br>    readonly HttpClient httpClient;<br><br>    // Make it public to allow changes to DefaultRequestHeaders and BaseAddress<br>    public HttpClient HttpClient =&gt; httpClient;<br><br>    public Anthropic()<br>        : this(new HttpClientHandler(), true)<br>    {<br>    }<br><br>    public Anthropic(HttpMessageHandler handler)<br>        : this(handler, true)<br>    {<br>    }<br><br>    public Anthropic(HttpMessageHandler handler, bool disposeHandler)<br>    {<br>        this.httpClient = new HttpClient(handler, disposeHandler);<br>    }<br><br>    public void Dispose()<br>    {<br>        httpClient.Dispose();<br>    }<br>}</pre><p>HttpClient is actually just a shell, and the entity is <a href="https://learn.microsoft.com/en-us/dotnet/api/system.net.http.httpmessagehandler">HttpMessageHandler</a>. HttpMessageHandler can do various things, such as implementing <a href="https://learn.microsoft.com/en-us/dotnet/api/system.net.http.delegatinghandler?view=net-8.0">DelegatingHandler</a> to hook the before and after of requests, and <a href="https://github.com/Cysharp/YetAnotherHttpHandler">Cysharp/YetAnotherHttpHandler</a> replaces the entire communication processing with a Rust implementation in the form of a HttpMessageHandler implementation. In cases where you want to use UnityWebRequest instead of the .NET runtime’s communication implementation in Unity, you can use <a href="https://gist.github.com/neuecc/854192b8d176170caf2c53fa7589dc90">UnityWebRequestHttpMessageHandler.cs</a> to replace the entire communication processing with Unity’s implementation.</p><p>Let’s also work on how to split the interfaces.</p><p>A two-level invocation style like client.Messages.CreateAsync, similar to .Controller.Method in MVC, is an intuitive and easy-to-use design. In particular, it&#39;s nice that it&#39;s friendly to input completion. To achieve this, first split the interface, but as a trick, make it an <a href="https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/interfaces/explicit-interface-implementation">explicit interface implementation</a> and return the interface itself with return this;.</p><pre>public interface IMessages<br>{<br>    Task&lt;MessageResponse&gt; CreateAsync(MessageRequest request, RequestOptions? overrideOptions = null, CancellationToken cancellationToken = default);<br>    IAsyncEnumerable&lt;IMessageStreamEvent&gt; CreateStreamAsync(MessageRequest request, RequestOptions? overrideOptions = null, CancellationToken cancellationToken = default);<br>}<br><br>public class Anthropic : IMessages, IDisposable<br>{<br>    public IMessages Messages =&gt; this;<br><br>    async Task&lt;MessageResponse&gt; IMessages.CreateAsync(MessageRequest request, RequestOptions? overrideOptions, CancellationToken cancellationToken)<br>    {<br>        // ...<br>    }<br><br>    async IAsyncEnumerable&lt;IMessageStreamEvent&gt; IMessages.CreateStreamAsync(MessageRequest request, RequestOptions? overrideOptions, [EnumeratorCancellation] CancellationToken cancellationToken)    <br>    {<br>        // ...<br>    }<br>}</pre><p>This way, there’s no allocation when going down one level (because it returns this), and since it’s an explicit implementation, it doesn’t appear in input completion at the top level, making it easy to use, performant, and easy to implement (because you can directly access all the client’s fields).</p><h3>User-Friendly Request Type Generation</h3><p>The <a href="https://docs.anthropic.com/claude/reference/messages_post">Anthropic request types</a> are quite organized and have a specification that is friendly to typed languages, but there are some parts that are either single string or an array of content blocks. It&#39;s a bit troublesome to have either/or, but it&#39;s not like Option&lt;Either&lt;List&lt;&gt;&gt;&gt; or anything like that. If you define it that way, the API client&#39;s feel will be terrible. If you think about it, in this case of the Anthropic API, a string is equivalent to a string content of length 1.</p><pre>// Instead of this<br>Content = [ new() { Type = &quot;text&quot;, Text = &quot;Hello, Claude&quot; }]<br><br>// I want to write like this<br>Content = &quot;Hello, Claude&quot;</pre><p>I think this is a good specification. It’s tedious to dogmatically write Type = “text”, Text = “…”. 95% of the usage will probably be single string content (Type can also be image, in which case the binary base64 string is set in Source; it’s an array to pass both images and text).</p><p>Let’s implement that specification in C#. In this case, it’s like normalizing, so I implemented it with implicit conversion.</p><pre>public record class Message<br>{<br>    /// &lt;summary&gt;<br>    /// user or assistant.<br>    /// &lt;/summary&gt;<br>    [JsonPropertyName(&quot;role&quot;)]<br>    public required string Role { get; set; }<br><br>    /// &lt;summary&gt;<br>    /// single string or an array of content blocks.<br>    /// &lt;/summary&gt;<br>    [JsonPropertyName(&quot;content&quot;)]<br>    public required Contents Content { get; set; }<br>}<br><br>public class Contents : Collection&lt;Content&gt;<br>{<br>    public static implicit operator Contents(string text)<br>    {<br>        var content = new Content<br>        {<br>            Type = ContentTypes.Text,<br>            Text = text<br>        };<br>        return new Contents { content };<br>    }<br>}</pre><p>Instead of Content[], I made it a custom collection and generated single string content from its implicit conversion from a string. It&#39;s not even the latest C# feature, but a technique that has been around for a long time. Reckless use is strictly prohibited, but utilizing it in such places is effective for improving the feel of the API client.</p><h3>Timeout</h3><p>Timeout is a common process, so it’s better to make it easily configurable by the user in the API client. However, since HttpClient has a Timeout property, it’s usually sufficient to set it. However, in Claudia, it’s intentionally disabled.</p><pre>public class Anthropic : IMessages, IDisposable<br>{<br>    public TimeSpan Timeout { get; init; } = TimeSpan.FromMinutes(10);<br><br>    public Anthropic(HttpMessageHandler handler, bool disposeHandler)<br>    {<br>        this.httpClient = new HttpClient(handler, disposeHandler);<br>        this.httpClient.Timeout = System.Threading.Timeout.InfiniteTimeSpan;<br>    }<br>}</pre><p>This is because the official Anthropic client has a specification that allows overriding the timeout setting for each method call, so I followed that specification. HttpClient or equivalent calls should be thread-safe (in fact, API clients may be registered as Singleton), so it’s not good to manipulate the properties of HttpClient in SendAsync. Therefore, the Timeout of HttpClient is disabled and processed manually.</p><p>The implementation method is to generate a <a href="https://learn.microsoft.com/en-us/dotnet/api/system.threading.cancellationtokensource.createlinkedtokensource?view=net-8.0">LinkedTokenSource</a>, create a CancellationToken that gets canceled after the timeout duration using CancelAfter, and pass it to HttpClient.SendAsync. This is the same as the internal implementation when HttpClient.Timeout has a timeout duration.</p><pre>// The actual code is mixed with retry processing, so it&#39;s slightly different<br>async Task&lt;TResult&gt; RequestWithAsync&lt;TResult&gt;(HttpRequestMessage message, CancellationToken cancellationToken, RequestOptions? overrideOptions)<br>{<br>    var timeout = overrideOptions?.Timeout ?? Timeout;<br>    using (var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))<br>    {<br>        cts.CancelAfter(timeout);<br><br>        try<br>        {<br>            var result = await httpClient.SendAsync(message, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(ConfigureAwait);<br>            return result;<br>        }<br>        catch (OperationCanceledException ex) when (ex.CancellationToken == cts.Token)<br>        {<br>            if (cancellationToken.IsCancellationRequested)<br>            {<br>                throw new OperationCanceledException(ex.Message, ex, cancellationToken);<br>            }<br>            else<br>            {<br>                throw new TimeoutException($&quot;The request was canceled due to the configured Timeout of {Timeout.TotalSeconds} seconds elapsing.&quot;, ex);<br>            }<br><br>            throw;<br>        }<br>    }<br>}</pre><p>Be careful with error handling when cancellation actually occurs (OperationCanceledException is thrown). First, you need to strip the LinkedToken. If passed through as-is, the Token of OperationCanceledException remains the LinkedToken, but this cannot be used to determine the cause of cancellation on the upstream side. If the cause of cancellation was the cancellation of the passed CancellationToken, create a new OperationCanceledException and change the cancellation reason Token.</p><p>If it was a timeout, it’s better to throw a TimeoutException instead of an OperationCanceledException. Note that if you use the timeout implementation of HttpClient, it throws a TaskCanceledException due to <a href="https://github.com/dotnet/runtime/issues/21965">historical reasons</a> (apparently, they wanted to change it but couldn&#39;t due to compatibility; it&#39;s not a very good design, so you don&#39;t need to follow it).</p><h3>Retry</h3><p>There may be some debate as to whether the API client itself should have retry functionality. However, it’s not as simple as just catching an exception when it occurs and retrying; you need to first distinguish between what can be retried and what cannot. For example, if authentication fails or the JSON thrown into the request is corrupted, retrying is pointless no matter how many times you do it, so it shouldn’t be retried. However, since such detailed conditions are only known to the API client itself, it’s good to incorporate retry processing.</p><p>In Claudia, following the official client, 408 Request Timeout, 409 Conflict, 429 Rate Limit, and &gt;=500 Internal errors are targeted for retry. Authentication failure (PermissionError(403)) or invalid request content (InvalidRequestError(400)) are not retried. The frequently occurring OverloadedError (error indicating that the result couldn’t be returned due to overload) is 529, which is resolved by hitting it a few times, so it’s retried.</p><p>The retry logic also follows the official client. If the response header has retry-after-ms or retry-after, it follows that, and if not (or if retry-after is larger than the specified value), the interval is controlled by Exponential Backoff with jitter.</p><h3>Cancellation</h3><p>The client side does not have a .Cancel() method or similar. This is because, in accordance with HttpClient, the client itself can be used almost like a singleton and shared across each call (it may be injected as a singleton by DI, depending on the case). Therefore, instead of .Cancel(), which affects everything, pass a CancellationToken to each call.</p><h3>Ultra-Fast Parsing of Server Sent Events</h3><p>The API for retrieving responses by streaming uses the <a href="https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events">server-sent events</a> specification and is sent via streaming. Specifically, text messages like the following are received.</p><pre>event: message_start<br>data: {&quot;type&quot;:&quot;message_start&quot;,&quot;message&quot;:...}<br><br>event: content_block_start<br>data: {&quot;type&quot;:&quot;content_block_start&quot;,&quot;index&quot;:...}</pre><p>It’s a repetition of event: event name, data: JSON, and so on. Now, when it comes to newline-delimited text messages, using <a href="https://learn.microsoft.com/en-us/dotnet/api/system.io.streamreader">StreamReader</a> and ReadLine is the correct answer, but it’s the wrong answer in modern C#.</p><p>ReadLine generates a string. To convert directly from UTF8 data for event name determination or eventually deserializing the JSON of data into an object, you can avoid using strings. In other words, zero allocation can be aimed for (except for generating objects to pass to the user). If you just don’t pass through strings. Therefore, StreamReader has no role to play.</p><p>Let’s look at the specific code. We’ll divide it into the first half (preparation) and the second half (parsing part).</p><pre>internal class StreamMessageReader<br>{<br>    readonly PipeReader reader;<br>    readonly bool configureAwait;<br>    MessageStreamEventKind currentEvent;<br><br>    public StreamMessageReader(Stream stream, bool configureAwait)<br>    {<br>        this.reader = PipeReader.Create(stream);<br>        this.configureAwait = configureAwait;<br>    }<br><br>    public async IAsyncEnumerable&lt;IMessageStreamEvent&gt; ReadMessagesAsync([EnumeratorCancellation] CancellationToken cancellationToken)<br>    {<br>    READ_AGAIN:<br>        var readResult = await reader.ReadAsync(cancellationToken).ConfigureAwait(configureAwait);<br><br>        if (!(readResult.IsCompleted | readResult.IsCanceled))<br>        {<br>            var buffer = readResult.Buffer;<br><br>            while (TryReadData(ref buffer, out var streamEvent))<br>            {<br>                yield return streamEvent;<br>                if (streamEvent.TypeKind == MessageStreamEventKind.MessageStop)<br>                {<br>                    yield break;<br>                }<br>            }<br><br>            reader.AdvanceTo(buffer.Start, buffer.End);<br>            goto READ_AGAIN;<br>        }<br>    }</pre><p>First, pass the Stream to <a href="https://learn.microsoft.com/en-us/dotnet/api/system.io.pipelines.pipereader">System.IO.Pipelines.PipeReader</a>. The Stream in this case is an unstable Stream streamed from the server over the network, so buffer management is difficult. PipeReader/PipeWriter has some quirks, but it takes care of that management nicely and is a very important library in modern C#.</p><p>The basic flow is to read the buffer (ReadAsync), parse it line by line (TryReadData) and yield return the object if it’s in a state where parsing is possible (the end of the line is not included, so it can’t be parsed), mark it up to the read part with AdvanceTo if the buffer is insufficient, and then ReadAsync again.</p><p>The user side was shown in the Blazor sample, but the basic approach is to enumerate with await foreach.</p><pre>await foreach (var messageStreamEvent in Anthropic.Messages.CreateStreamAsync())<br>{<br>}</pre><p>IAsyncEnumerable is very well-suited for streaming processing involving networks like this, and it has become much easier for the data source side to return an asynchronous sequence with yield return. It would be impossible to go back to the days when this didn’t exist.</p><p>Next is the second half, the processing to parse from the buffer decomposed by PipeReader.</p><pre>[SkipLocalsInit]<br>bool TryReadData(ref ReadOnlySequence&lt;byte&gt; buffer, [NotNullWhen(true)] out IMessageStreamEvent? streamEvent)<br>{<br>    var reader = new SequenceReader&lt;byte&gt;(buffer);<br>    Span&lt;byte&gt; tempBytes = stackalloc byte[64]; // alloc temp<br>    <br>    while (reader.TryReadTo(out ReadOnlySequence&lt;byte&gt; line, (byte)&#39;\n&#39;, advancePastDelimiter: true))<br>    {<br>        if (line.Length == 0)<br>        {<br>            continue; // next.<br>        }<br>        else if (line.FirstSpan[0] == &#39;e&#39;) // event<br>        {<br>            // Parse Event.<br>            if (!line.IsSingleSegment)<br>            {<br>                line.CopyTo(tempBytes);<br>            }<br>            var span = line.IsSingleSegment ? line.FirstSpan : tempBytes.Slice(0, (int)line.Length);<br><br>            var first = span[7]; // &quot;event: [c|m|p|e]&quot;<br><br>            if (first == &#39;c&#39;) // content_block_start/delta/stop<br>            {<br>                switch (span[23]) // event: content_block_..[]<br>                {<br>                    case (byte)&#39;a&#39;: // st[a]rt<br>                        currentEvent = MessageStreamEventKind.ContentBlockStart;<br>                        break;<br>                    case (byte)&#39;o&#39;: // st[o]p<br>                        currentEvent = MessageStreamEventKind.ContentBlockStop;<br>                        break;<br>                    case (byte)&#39;l&#39;: // de[l]ta<br>                        currentEvent = MessageStreamEventKind.ContentBlockDelta;<br>                        break;<br>                    default:<br>                        break;<br>                }<br>            }<br>            else if (first == &#39;m&#39;) // message_start/delta/stop<br>            {<br>                switch (span[17]) // event: message_..[]<br>                {<br>                    case (byte)&#39;a&#39;: // st[a]rt<br>                        currentEvent = MessageStreamEventKind.MessageStart;<br>                        break;<br>                    case (byte)&#39;o&#39;: // st[o]p<br>                        currentEvent = MessageStreamEventKind.MessageStop;<br>                        break;<br>                    case (byte)&#39;l&#39;: // de[l]ta<br>                        currentEvent = MessageStreamEventKind.MessageDelta;<br>                        break;<br>                    default:<br>                        break;<br>                }<br>            }<br>            else if (first == &#39;p&#39;)<br>            {<br>                currentEvent = MessageStreamEventKind.Ping;<br>            }<br>            else if (first == &#39;e&#39;)<br>            {<br>                currentEvent = (MessageStreamEventKind)(-1);<br>            }<br>            else<br>            {<br>                // Unknown Event, Skip.<br>                // throw new InvalidOperationException(&quot;Unknown Event. Line:&quot; + Encoding.UTF8.GetString(line.ToArray()));<br>                currentEvent = (MessageStreamEventKind)(-2);<br>            }<br><br>            continue;<br>        }<br>        else if (line.FirstSpan[0] == &#39;d&#39;) // data<br>        {<br>            // Parse Data.<br>            Utf8JsonReader jsonReader;<br>            if (line.IsSingleSegment)<br>            {<br>                jsonReader = new Utf8JsonReader(line.FirstSpan.Slice(6)); // skip data: <br>            }<br>            else<br>            {<br>                jsonReader = new Utf8JsonReader(line.Slice(6)); // ReadOnlySequence.Slice is slightly slow<br>            }<br><br>            switch (currentEvent)<br>            {<br>                case MessageStreamEventKind.Ping:<br>                    streamEvent = JsonSerializer.Deserialize&lt;Ping&gt;(ref jsonReader, AnthropicJsonSerialzierContext.Default.Options)!;<br>                    break;<br>                case MessageStreamEventKind.MessageStart:<br>                    streamEvent = JsonSerializer.Deserialize&lt;MessageStart&gt;(ref jsonReader, AnthropicJsonSerialzierContext.Default.Options)!;<br>                    break;<br>                // Omitted (Deserialize&lt;T&gt; for MessageDela, MessageStop, ContentBlockStart, ContentBlockDelta, ContentBlockStop, error similarly)<br>                default:<br>                    // unknown event, skip<br>                    goto END;<br>            }<br><br>            buffer = buffer.Slice(reader.Consumed);<br>            return true;<br>        }<br>    }<br>END:<br>    streamEvent = default;<br>    buffer = buffer.Slice(reader.Consumed);<br>    return false;<br>}</pre><p>The desired processing is to deserialize the JSON of data into an object from the two lines of event and data. The buffer doesn’t necessarily conveniently contain the two lines of event and data; it may contain only the event, only the data, or the data may be cut off (resulting in incomplete JSON). It needs to be structured so that it can be interrupted and resumed, taking these into consideration.</p><p>However, assuming that there is sufficient buffer for one line if a newline code exists, it loops with while (reader.TryReadTo(out ReadOnlySequence&lt;byte&gt; line, (byte)&#39;\n&#39;, advancePastDelimiter: true)) and uses this as a substitute for StreamReader.ReadLine. This reader is a <a href="https://learn.microsoft.com/en-us/dotnet/api/system.buffers.sequencereader-1?view=net-8.0">SequenceReader</a>, a utility that supports reading from ReadOnlySequence, and since it&#39;s a ref struct, there&#39;s no allocation for the reader itself. ReadOnlySequence is a class with many pitfalls to use correctly and efficiently, so it&#39;s more convenient and safer to implement based on such utilities.</p><p>First, in parsing the event, it reads from here what type the data is. The straightforward approach would be to determine with if (span.SequenceEqual(&quot;content_block_start&quot;)). Calling SequenceEqual on Span&lt;byte&gt; is implemented efficiently, so it&#39;s not bad, but is a series of if statements really good? So, in Claudia, the determination is actually simplified as follows.</p><pre>var first = span[7]; // &quot;event: [c|m|p|e]&quot;<br><br>if (first == &#39;c&#39;) // content_block_start/delta/stop<br>{<br>    switch (span[23]) // event: content_block_..[]<br>    {<br>        case (byte)&#39;a&#39;: // st[a]rt<br>            currentEvent = MessageStreamEventKind.ContentBlockStart;<br>            break;<br>        case (byte)&#39;o&#39;: // st[o]p<br>            currentEvent = MessageStreamEventKind.ContentBlockStop;<br>            break;<br>        case (byte)&#39;l&#39;: // de[l]ta<br>            currentEvent = MessageStreamEventKind.ContentBlockDelta;<br>            break;<br>        default:<br>            break;<br>    }<br>}<br>else if (first == &#39;m&#39;) // message_start/delta/stop<br>{<br>    switch (span[17]) // event: message_..[]<br>    {<br>        case (byte)&#39;a&#39;: // st[a]rt<br>            currentEvent = MessageStreamEventKind.MessageStart;<br>            break;<br>        case (byte)&#39;o&#39;: // st[o]p<br>            currentEvent = MessageStreamEventKind.MessageStop;<br>            break;<br>        case (byte)&#39;l&#39;: // de[l]ta<br>            currentEvent = MessageStreamEventKind.MessageDelta;<br>            break;<br>        default:<br>            break;<br>    }<br>}</pre><p>There are 8 types of messages: content_block_start/delta/stop, message_start/delta/stop, ping, and error. First, the first character can be used to determine whether it’s a content system, message system, or other. For start/delta/stop, the third character can be used to determine. So, by checking 1 byte twice, it can be classified. It’s clearly fast! However, it should be noted that there is a non-zero possibility of the check being broken by the addition of message types in the future (for example, if content_block_fforward comes, it may be misidentified as content_block_stop). Claudia is optimistically assuming it will be fine, but it’s something to keep in mind.</p><p>This can be said to be a variation of the code in Modern High-Performance C# 2023, which I presented before.</p><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fspeakerdeck.com%2Fplayer%2Fcaf19e3d44504a94979d2d1394478ba3&amp;display_name=Speaker+Deck&amp;url=https%3A%2F%2Fspeakerdeck.com%2Fneuecc%2Fmodern-high-performance-c-number-2023-edition&amp;image=https%3A%2F%2Ffiles.speakerdeck.com%2Fpresentations%2Fcaf19e3d44504a94979d2d1394478ba3%2Fslide_0.jpg%3F26849267&amp;key=a19fcc184b9711e1b4764040d3dc5c07&amp;type=text%2Fhtml&amp;schema=speakerdeck" width="710" height="399" frameborder="0" scrolling="no"><a href="https://medium.com/media/fd9f9af882971aab0226ef7257be0880/href">https://medium.com/media/fd9f9af882971aab0226ef7257be0880/href</a></iframe><p>When looking at text protocols, it’s hard to resist the urge to somehow cheat the determination. If you want to do strict determination while avoiding a series of if statements, first put in a length check. Make a rough branch with the length and then do an accurate check with SequenceEqual. It’s just about doing the same thing as the optimization of swtich to string in C# (the compiler is converting it to that kind of processing!). If there are many branches, it may be a good idea to take a hash code and branch, in other words, implement an inline Dictionary.</p><p>Lastly, the data line is JSON Deserialization. To deserialize from ReadOnlySpan&lt;byte&gt; or ReadOnlySequence&lt;byte&gt;, you need to pass it through <a href="https://learn.microsoft.com/en-us/dotnet/api/system.text.json.utf8jsonreader?view=net-8.0">Utf8JsonReader</a>. Note that Utf8JsonReader is also a ref struct, so it&#39;s not included in the allocation.</p><p>With this, we were able to process without going through String at all! There’s a feeling that it would be super simple if we used StreamReader, but we can’t help it because we’re suffering from a disease that makes us think we’ve lost if we go through a string.</p><h3>Source Generator vs Reflection</h3><p>For the implementation of Function Calling, Claudia adopted Source Generator. It was possible to create it based on reflection, but in this case, Source Generators yielded more desirable results. First, let’s compare what kind of function definition would be required if it were implemented with reflection, using the case of Semantic Kernel.</p><pre>public static partial class FunctionTools<br>{<br>    // Claudia Source Generator<br><br>    /// &lt;summary&gt;<br>    /// Retrieve the current time of day in Hour-Minute-Second format for a specified time zone. Time zones should be written in standard formats such as UTC, US/Pacific, Europe/London.<br>    /// &lt;/summary&gt;<br>    /// &lt;param name=&quot;timeZone&quot;&gt;The time zone to get the current time for, such as UTC, US/Pacific, Europe/London.&lt;/param&gt;<br>    [ClaudiaFunction]<br>    public static string TimeOfDay(string timeZone)<br>    {<br>        var time = TimeZoneInfo.ConvertTimeBySystemTimeZoneId(DateTime.UtcNow, timeZone);<br>        return time.ToString(&quot;HH:mm:ss&quot;);<br>    }<br><br>    // Semantic Kernel<br><br>    [KernelFunction]<br>    [Description(&quot;Retrieve the current time of day in Hour-Minute-Second format for a specified time zone. Time zones should be written in standard formats such as UTC, US/Pacific, Europe/London.&quot;)]<br>    public static string TimeOfDay([Description(&quot;The time zone to get the current time for, such as UTC, US/Pacific, Europe/London.&quot;)]string timeZone)<br>    {<br>        var time = TimeZoneInfo.ConvertTimeBySystemTimeZoneId(DateTime.UtcNow, timeZone);<br>        return time.ToString(&quot;HH:mm:ss&quot;);<br>    }<br>}</pre><p>In Function Calling, the information about the function must be given to Claude, so descriptions for both the method and parameters are required. In Claudia’s Source Generator implementation, I made it retrieve them from document comments. In Semantic Kernel, it retrieves them from the Description attribute. Document comments are more natural and easier to write. Attributes for parameters are not only harder to write but also become quite difficult to read when there are multiple parameters.</p><p>Also, with Source Generators, missing elements can be turned into compile errors as analyzers.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/753/0*ngdUlayY03u4trEM" /></figure><p>All checks, such as document comments not being written for all parameters or using unsupported types, can be known in real-time not only at compile-time but also at edit-time.</p><p>The drawback is that Source Generators have a higher implementation difficulty, and great care must be taken when using document comments.</p><p>To retrieve document comments on Roslyn, ISymbol.GetDocumentationCommtentXml() is the easiest, but whether it can be retrieved or not depends on &lt;GenerateDocumentaionFile&gt;. If it&#39;s false, it always returns null. That makes it too hard to use, so in Claudia, I tried to retrieve it from SyntaxNode, but that was also affected by &lt;GenerateDocumentaionFile&gt;.</p><p>So, I had no choice but to prepare an extension method like the following to successfully retrieve document comments in all situations (it’s a bit difficult to handle because it’s based on Trivia, but it’s much better than not being able to retrieve it).</p><pre>public static DocumentationCommentTriviaSyntax? GetDocumentationCommentTriviaSyntax(this SyntaxNode node)<br>{<br>    if (node.SyntaxTree.Options.DocumentationMode == DocumentationMode.None)<br>    {<br>        var withDocumentationComment = node.SyntaxTree.Options.WithDocumentationMode(DocumentationMode.Parse);<br>        var code = node.ToFullString();<br>        var newTree = CSharpSyntaxTree.ParseText(code, (CSharpParseOptions)withDocumentationComment);<br>        node = newTree.GetRoot();<br>    }<br><br>    foreach (var leadingTrivia in node.GetLeadingTrivia())<br>    {<br>        if (leadingTrivia.GetStructure() is DocumentationCommentTriviaSyntax structure)<br>        {<br>            return structure;<br>        }<br>    }<br><br>    return null;<br>}</pre><p>The state of DocumentationMode determines whether DocumentationCommentTriviaSyntax can be retrieved (it becomes None when GenerateDocumentaionFile=false), so if it&#39;s None, it&#39;s parsed again with DocumentationMode.Parse attached to retrieve it. Even if you generate a CSharpSyntaxTree by passing options to SyntaxNode as-is, it doesn&#39;t parse it again or changing DocumentationMode is useless, so it&#39;s done by converting it to a string and then calling ParseText.</p><h3>JSON Serializer</h3><p>Requests and responses are JSON in today’s world. And the library to use is <a href="https://learn.microsoft.com/en-us/dotnet/api/system.text.json.jsonserializer?view=net-8.0">System.Text.Json.JsonSerializer</a>, period. There is room for objection, but there isn’t. Like it or not, you have to use it now.</p><p>A feature of System.Text.Json is that it can process based on UTF8, so if you try to avoid going through strings as much as possible, you can expect high performance. To deserialize ReadOnlySpan&lt;byte&gt; or ReadOnlySequence&lt;byte&gt;, you need to pass it through <a href="https://learn.microsoft.com/en-us/dotnet/api/system.text.json.utf8jsonreader?view=net-8.0">Utf8JsonReader</a>. This is a ref struct, so there&#39;s no allocation, so just new it and use it. What about the Writer? <a href="https://learn.microsoft.com/en-us/dotnet/api/system.text.json.utf8jsonwriter?view=net-8.0">Utf8JsonWriter</a> is a class. Why? So, for the Writer, depending on how the application is built, if you can hold it in a field and reuse it, hold it in a field and reuse it (there&#39;s Reset), and if you can&#39;t hold it, pull it from [ThreadStatic].</p><p>When providing it in a library, since all the types to be used are determined, <a href="https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/source-generation?pivots=dotnet-8-0">source generating</a> it should improve performance and increase AOT safety. Claudia is also generating it.</p><pre>[JsonSourceGenerationOptions(<br>    GenerationMode = JsonSourceGenerationMode.Default,<br>    DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,<br>    WriteIndented = false)]<br>[JsonSerializable(typeof(MessageRequest))]<br>[JsonSerializable(typeof(Message))]<br>[JsonSerializable(typeof(Contents))]<br>[JsonSerializable(typeof(Content))]<br>[JsonSerializable(typeof(Metadata))]<br>[JsonSerializable(typeof(Source))]<br>[JsonSerializable(typeof(MessageResponse))]<br>[JsonSerializable(typeof(Usage))]<br>[JsonSerializable(typeof(ErrorResponseShape))]<br>[JsonSerializable(typeof(ErrorResponse))]<br>[JsonSerializable(typeof(Ping))]<br>[JsonSerializable(typeof(MessageStart))]<br>[JsonSerializable(typeof(MessageDelta))]<br>[JsonSerializable(typeof(MessageStop))]<br>[JsonSerializable(typeof(ContentBlockStart))]<br>[JsonSerializable(typeof(ContentBlockDelta))]<br>[JsonSerializable(typeof(ContentBlockStop))]<br>[JsonSerializable(typeof(MessageStartBody))]<br>[JsonSerializable(typeof(MessageDeltaBody))]<br>public partial class AnthropicJsonSerialzierContext : JsonSerializerContext<br>{<br>}</pre><pre>// When used internally, this JsonSerializerContext is always specified<br>JsonSerializer.SerializeToUtf8Bytes(request, AnthropicJsonSerialzierContext.Default.Options)</pre><p>One thing I stumbled upon was that JsonIgnoreCondition.WhenWritingNull, which normally (reflection-based) worked for Nullable&lt;T&gt; as well, stopped working with Source Generators and no longer ignored null. I had no choice but to work around it by directly attaching [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] to all Nullable&lt;T&gt; properties of the target types.</p><pre>public record class MessageRequest<br>{<br>    // ...<br><br>    [JsonPropertyName(&quot;temperature&quot;)]<br>    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]<br>    public double? Temperature { get; set; }<br>}</pre><p>Honestly, I feel like it’s an implementation leak in the Source Generator version, but since I was able to work around it, I’ll just leave it for now…</p><p>Like Azure OpenAI Service for the OpenAI API, people in AWS environments may find it easier to use <a href="https://aws.amazon.com/jp/bedrock/">Amazon Bedrock</a>. So, already added Bedrock support! It should be even easier to use now.</p><p>Creating a Web API client is not that difficult. However, many SDKs out there are never designed to be easy to use. We hope this article will help you build a better design.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=92a9f6cdd6d1" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[R3 — A New Modern Reimplementation of Reactive Extensions for C#]]></title>
            <link>https://neuecc.medium.com/r3-a-new-modern-reimplementation-of-reactive-extensions-for-c-cf29abcc5826?source=rss-61fea4eebf07------2</link>
            <guid isPermaLink="false">https://medium.com/p/cf29abcc5826</guid>
            <category><![CDATA[dotnet]]></category>
            <category><![CDATA[reactive-programming]]></category>
            <category><![CDATA[unity]]></category>
            <category><![CDATA[csharp]]></category>
            <dc:creator><![CDATA[Yoshifumi Kawai]]></dc:creator>
            <pubDate>Tue, 05 Mar 2024 10:31:44 GMT</pubDate>
            <atom:updated>2024-03-06T04:24:34.323Z</atom:updated>
            <content:encoded><![CDATA[<h3>R3 — A New Modern Reimplementation of Reactive Extensions for C#</h3><p>Recently, I officially released R3 as a new implementation of Reactive Extensions for C#! R3 is named as the third generation of Rx, considering <a href="https://github.com/dotnet/reactive">Rx for .NET</a> as the first generation and <a href="https://github.com/neuecc/UniRx">UniRx</a> as the second. The core part of Rx (almost identical to dotnet/reactive) is provided as a library common to .NET, while custom schedulers and operators specific to each platform are separated into different libraries. This approach allows us to offer a core library for all .NET platforms and extension libraries for various frameworks such as Unity, Godot, Avalonia, WPF, WinForms, WinUI3, Stride, LogicLooper, MAUI, MonoGame and Blazor.</p><ul><li><a href="https://github.com/Cysharp/R3">GitHub — Cysharp/R3</a></li></ul><p>While it includes some breaking changes and is not a drop-in replacement, transitioning from dotnet/reactive or UniRx is kept realistically manageable. This is part of the beauty of Rx, where vocabulary and operations are largely standardized in a LINQ-like manner, meaning the transition may not seem significantly different.</p><p>The UniRx I had previously developed was an Rx exclusively for Unity. Therefore, R3 provides extensive support for Unity, offering sufficient functionality to serve as a migration destination from UniRx. Additionally, by becoming a general-purpose library, it now supports many scenarios for use with .NET. It is not an Rx specialized for use with game engines, but rather, it has been created as a new implementation of Reactive Extensions.</p><h3>The History of Rx and vs. async/await</h3><p>Are you using Rx? The number of people answering “no” is increasing, not just in .NET or Unity, but in Java, Swift, and Kotlin as well. Its presence is clearly declining. Why? The answer is simple: the advent of async/await. Reactive Extensions for .NET first appeared in 2009, during the era of C# 3.0 and .NET Framework 3.5, a time when platforms like Silverlight and Windows Phone, now defunct, were still relevant. async/await (introduced in C# 5.0, 2012) didn’t exist yet, and even Tasks had not been introduced. As a side note, the “Extensions” in Reactive Extensions were named after the earlier <a href="https://en.wikipedia.org/wiki/Parallel_Extensions">Parallel Extensions</a> project, which included Parallel LINQ and the Task Parallel Library added in .NET Framework 4.0.</p><p>Initially, Rx spread across various languages as the definitive solution for asynchronous processing without language support, offering a powerful and user-friendly alternative to single-function Task or Promise. I, too, was mesmerized by Rx over TPL at the time. However, the landscape changed dramatically with the introduction of async/await, establishing it as the standard for asynchronous processing across numerous languages.</p><p>With the widespread adoption of async/await, the need for Rx just for asynchronous processing diminished, leading to a decline in its adoption rate. As the developer of <a href="https://github.com/neuecc/UniRx/">UniRx</a>, a standard for Rx in Unity, I quickly recognized the need for an async/await runtime tailored to game engines (Unity) and developed <a href="https://github.com/Cysharp/UniTask">UniTask</a> as soon as the necessary conditions (C# 7.0) were met in Unity.</p><h3>Rediscovering the Value of Rx</h3><p>Rx is not just for asynchronous processing, right? While it was hailed as “LINQ to Everything,” the notion of “Everything” might be noise, and it’s better to separate concerns and use the most optimal tools. Using Rx just for async processing is not the best approach; a single-value Observable should be represented by a Task for both clarity and performance benefits. This necessitates the integration of Rx with async/await through APIs that can coexist with asynchronous tasks, rather than focusing on minor details like being able to pass a Task to SelectMany because Observables are monads.</p><p>Simply being able to await is not sufficient for real-world application development. Various libraries have been devised for asynchronous/parallel processing, not just Rx, such as <a href="https://learn.microsoft.com/en-us/dotnet/standard/parallel-programming/dataflow-task-parallel-library">TPL Dataflow</a>. However, few people would choose to use these libraries from scratch today. It’s now 2024, and the winners have been decided: language-supported <a href="https://learn.microsoft.com/en-us/dotnet/csharp/asynchronous-programming/generate-consume-asynchronous-stream">IAsyncEnumerable</a> and <a href="https://devblogs.microsoft.com/dotnet/an-introduction-to-system-threading-channels/">System.Threading.Channels</a> are the best choices. These also incorporate backpressure characteristics, making operators related to backpressure in RxJava unnecessary for .NET. For more specific I/O operations, <a href="https://learn.microsoft.com/en-us/dotnet/standard/io/pipelines">System.IO.Pipelines</a> offers maximum performance.</p><p>Asynchronous LINQ might be a nice addition, but given its lower usage frequency compared to LINQ to Objects in actual asynchronous stream scenarios, it’s not something to be eagerly adopted (note that I have implemented <a href="https://github.com/Cysharp/UniTask/tree/809d23e/src/UniTask/Assets/Plugins/UniTask/Runtime/Linq">UniTaskAsyncEnumerable and LINQ</a> for UniTask myself). The dream of distributed queries (IQbservable) in Rx might have found its modern counterpart in <a href="https://graphql.org/">GraphQL</a>. In terms of distributed systems, <a href="https://kubernetes.io/">Kubernetes</a> has gained widespread adoption, with <a href="https://grpc.io/">gRPC</a> becoming the standard for RPC, and other options like <a href="https://learn.microsoft.com/en-us/dotnet/orleans/">Orleans</a>, <a href="https://getakka.net/">Akka.NET</a>, <a href="https://learn.microsoft.com/en-us/aspnet/core/signalr/introduction">SignalR</a>, and <a href="https://github.com/Cysharp/MagicOnion">MagicOnion</a> offering a variety of choices.</p><p>The landscape is no longer the same as in 2009, where various technologies competed for dominance. Just as no one would choose <a href="https://azure.microsoft.com/en-us/products/service-fabric">Service Fabric</a> today, venturing into distributed processing is not the future of Rx, in my opinion. Just because Rx was created by the Cloud Programmability Team doesn’t mean that making it useful for the cloud is the only correct approach. Of course, there could be multiple futures, and I hope one possible future for Rx is R3.</p><p>So, where does the value of Rx lie? I believe it returns to its roots: processing in-memory messaging with LINQ, or LINQ to Events. Especially on the client side and in UI processing, Rx continues to be valued, with Rx-like but more language-optimized options like <a href="https://kotlinlang.org/docs/flow.html">Kotlin Flow</a> and <a href="https://developer.apple.com/documentation/combine">Swift Combine</a> still actively used. Even in complex, event-heavy game applications, as a developer of UniRx used in the game engine (Unity), I find it extremely beneficial. The significance of the observer pattern and events is undeniable, and Rx’s role as a “better event” or the ultimate observer pattern remains unchanged.</p><h3>Reconstruction with R3</h3><p>Initially, I debated whether to maintain 100% compatibility with Rx interfaces while removing legacy APIs and adding new ones, or to fundamentally change them. However, to solve all the issues I perceive, radical changes were necessary. Inspired by the success of Kotlin Flow and Swift Combine, I decided to reconstruct Rx completely anew, tailored to the modern C# environment of .NET 8 and C# 12.</p><p>Even so, the differences in interfaces are not that significant in the end.</p><pre>public abstract class Observable&lt;T&gt;<br>{<br>    public IDisposable Subscribe(Observer&lt;T&gt; observer);<br>}<br><br>public abstract class Observer&lt;T&gt; : IDisposable<br>{<br>    public void OnNext(T value);<br>    public void OnErrorResume(Exception error);<br>    public void OnCompleted(Result result); // Result is (Success | Failure)<br>}</pre><p>At a glance, the main changes are the transformation of OnError into OnErrorResume and the shift from interface to abstract class. One of the changes I felt was necessary was OnError, where the behavior of unsubscribing due to exceptions in the pipeline was considered a <a href="https://www.infoq.com/presentations/Null-References-The-Billion-Dollar-Mistake-Tony-Hoare/">billion-dollar mistake</a> in Rx. In R3, exceptions flow to OnErrorResume without unsubscribing. Instead, the pipeline’s termination is indicated by passing a Result representing Success or Failure to OnCompleted.</p><p>The definitions of IObservable&lt;T&gt;/IObserver&lt;T&gt; are closely related to those of IEnumerable&lt;T&gt;/IEnumerator&lt;T&gt;, and they are claimed to be a <a href="https://en.wikipedia.org/wiki/Duality_(mathematics)">mathematical duality</a>, but there are practical inconveniences, with the most significant being that it stops on OnError. The inconvenience stems from the different lifetimes of exceptions in IEnumerable&lt;T&gt;&#39;s foreach and IObservable&lt;T&gt;. While an exception in foreach ends the iteration there, and if necessary, is handled with try-catch, often without retrying, subscribing to an Observable is different. The lifespan of an event subscription is long, and it&#39;s not unnatural to want it not to stop even if an exception occurs. Normal events do not stop when an exception occurs, but in Rx, due to the operator chain, there&#39;s always a possibility of exceptions occurring in the pipeline (e.g., Select or Where might throw exceptions). When considered as an alternative or superior to events, it becomes unnatural for it to stop due to exceptions.</p><p>And it’s not just about catching and retrying if needed! Re-subscribing to a stopped event in Rx is very difficult! Unlike events, Observables have a concept of completion. Subscribing to a completed IObservable immediately calls OnError | OnCompleted, thus automatic re-subscription risks re-subscribing to a completed sequence. Of course, this would lead to an infinite loop, without a way to detect and handle it properly. There are many questions on Stack Overflow about how to re-subscribe to UI subscriptions in Rx/Combine/Flow, and the answers often require writing very complex code. Reality is not solved with Repeat/Retry alone!</p><p>Therefore, it was changed to not stop on exceptions. To avoid confusion with the traditional stopping behavior, it was renamed to OnErrorResume. This solves all issues related to re-subscription. Moreover, this change has advantages; changing from stopping to not stopping is impossible (as the Dispose chain would run, making it impossible to restore state, leaving no means other than total re-subscription), but changing from not stopping to stopping is very easy to implement and performs well. Just prepare an operator that converts OnErrorResume to OnCompleted(Result.Failure) (a standard operator OnErrorResumeAsFailure has been added).</p><p>Rx itself, while having complex contracts (such as either OnError or OnCompleted is issued, but not both), lacks implementation guarantees in its interface, making correct implementation of custom operator is difficult. For instance, correctly handling the Disposable returned when Subscribe is delayed (using SingleAssignmentDisposable) is difficult to understand properly. Where do exceptions occurring in onNext during Subscribe go, to onError to be Disposed of, or do they continue? This behavior is not specifically regulated, so implementations can vary. R3 ensures most contracts by becoming an abstract class, unifying behavior and easing custom implementations.</p><p>The primary reason for making it an abstract class was to centralize management of all subscriptions. All Subscribes must go through the base class’s Subscribe implementation, enabling tracking of subscriptions. For example, it can be displayed as follows:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/833/0*wavHN7kcgx0_UbCt" /><figcaption>This is an extension window for Unity, but it exists for Godot and is offered as an API, allowing it to be logged or retrieved at any time, and making custom visualization possible.</figcaption></figure><p>While Task has Parallel Debugger (which also centralizes management in the base class when s_asyncDebuggingEnabled), visualizing Rx subscriptions is far more critical. Event subscription leaks are common, and developers often scramble to find them at the end of development, but with R3, this is no longer necessary! Significantly improved development efficiency!</p><p>R3 prioritizes subscription management and leak prevention, tracking all subscriptions with Observable Tracker and introducing the concept that “all Observables can complete.”</p><p>The basic principle of subscription management in Rx is disposing of IDisposable. However, unsubscribing is not limited to this; it can also be done by flowing OnError | OnCompleted (not guaranteed by the IObservable contract but implemented as such, and R3 ensures it will always be so through the base class). Thus, handling leaks from both upstream (issuance of OnError | OnCompleted) and downstream (Dispose) can more reliably prevent leaks.</p><p>While this may seem excessive, experience in developing actual applications suggests that excessive subscription management is just right. From this philosophy, R3 has made Observables like Observable.FromEvent, Observable.Timer, EveryUpdate, which previously had no means to issue OnCompleted, able to do so. The method of issuance is by passing a CancellationToken, leveraging the widely (or excessively) used CancellationToken in the modern API design post-async/await. Additionally, with the idea that all Observables can complete, disposing of a Subject now standardly issues OnCompleted.</p><h3>Reconsidering IScheduler</h3><p>IScheduler is the mechanism that enables the magic of moving through time and space in Rx. By passing it to Timer or ObserveOn, you can move values to any place (Thread, Dispatcher, PlayerLoop, etc.) and time.</p><pre>public interface IScheduler<br>{<br>    DateTimeOffset Now { get; }<br><br>    IDisposable Schedule&lt;TState&gt;(TState state, Func&lt;IScheduler, TState, IDisposable&gt; action);<br>    IDisposable Schedule&lt;TState&gt;(TState state, TimeSpan dueTime, Func&lt;IScheduler, TState, IDisposable&gt; action);<br>    IDisposable Schedule&lt;TState&gt;(TState state, DateTimeOffset dueTime, Func&lt;IScheduler, TState, IDisposable&gt; action);<br>}</pre><p>And, it turns out to be flawed. If you have ever looked at the Rx source code, you may have noticed that from the beginning, an additional, different definition has been prepared. For example, ThreadPoolScheduler implements interfaces like the following.</p><pre>public interface ISchedulerLongRunning<br>{<br>    IDisposable ScheduleLongRunning&lt;TState&gt;(TState state, Action&lt;TState, ICancelable&gt; action);<br>}<br><br>public interface ISchedulerPeriodic<br>{<br>    IDisposable SchedulePeriodic&lt;TState&gt;(TState state, TimeSpan period, Func&lt;TState, TState&gt; action);<br>}<br><br>public interface IStopwatchProvider<br>{<br>    IStopwatch StartStopwatch();<br>}<br><br>public abstract partial class LocalScheduler : IScheduler, IStopwatchProvider, IServiceProvider<br>{<br>}<br><br>public sealed class ThreadPoolScheduler : LocalScheduler, ISchedulerLongRunning, ISchedulerPeriodic<br>{<br>}</pre><p>And the following calls are made.</p><pre>public static IStopwatch StartStopwatch(this IScheduler scheduler)<br>{<br>    var swp = scheduler.AsStopwatchProvider();<br>    if (swp != null)<br>    {<br>        return swp.StartStopwatch();<br>    }<br><br>    return new EmulatedStopwatch(scheduler);<br>}<br><br>private static IDisposable SchedulePeriodic_&lt;TState&gt;(IScheduler scheduler, TState state, TimeSpan period, Func&lt;TState, TState&gt; action)<br>{<br>    var periodic = scheduler.AsPeriodic();<br>    if (periodic != null)<br>    {<br>        return periodic.SchedulePeriodic(state, period, action);<br>    }<br><br>    var swp = scheduler.AsStopwatchProvider();<br>    if (swp != null)<br>    {<br>        var spr = new SchedulePeriodicStopwatch&lt;TState&gt;(scheduler, state, period, action, swp);<br>        return spr.Start();<br>    }<br>    else<br>    {<br>        var spr = new SchedulePeriodicRecursive&lt;TState&gt;(scheduler, state, period, action);<br>        return spr.Start();<br>    }<br>}</pre><p>In essence, there are quite a few cases where raw IScheduler is not used. The reason for not using it is due to performance issues, as IScheduler.Schedule is defined only for single executions, and the idea is that multiple calls can be made recursively, but generating a new IDisposable each time poses a performance issue. To avoid this, ISchedulerPeriodic and others were prepared.</p><p>In that case, wouldn’t it be better to use something that properly reflects the reality rather than IScheduler? This led to the discovery that <a href="https://learn.microsoft.com/en-us/dotnet/api/system.timeprovider?view=net-8.0">TimeProvider</a>, added in .NET 8, can do what IScheduler did more efficiently.</p><pre>public abstract class TimeProvider<br>{<br>    // use these.<br>    public virtual ITimer CreateTimer(TimerCallback callback, object? state, TimeSpan dueTime, TimeSpan period);<br>    public virtual long GetTimestamp();<br>}</pre><p>The ITimer generated by CreateTimer has sufficient functionality to perform what ISchedulerPeriodic can do, and in scenarios where one-time executions are repeated (Schedule&lt;TState&gt;(TState state, TimeSpan dueTime, Func&lt;IScheduler, TState, IDisposable&gt; action)), using ITimer is more efficient than dotnet/reactive&#39;s ThreadPoolScheduler (which creates a new Timer each time).</p><p>Regarding the current time acquisition, TimeProvider also has DateTimeOffset TimeProvider.GetUtcNow() similar to DateTimeOffset IScheduler.Now, but it only uses long GetTimestamp. The reason is that only ticks are necessary for operator implementation, so it&#39;s better to avoid the overhead of wrapping it in DateTimeOffset and directly handle raw ticks for time calculations.</p><p>DateTimeOffset.UtcNow can be affected by changes to the OS system time, so it&#39;s better to use GetTimestamp (which uses a high-resolution timer from Stopwatch.GetTimestamp() as standard) without going through DateTimeOffset for that reason as well.</p><p>Another problem with IScheduler is the existence of synchronously operating schedulers like ImmediateScheduler and CurrentScheduler. Assigning time-related processes like Timer or Delay to these results in emulating asynchronous code that should not be used, i.e., sleeping the thread. Therefore, it might be better not to have synchronous Schedulers at all. In R3, they were completely removed, and specifying TimeProvider means always making asynchronous calls.</p><p>The problem with ImmediateScheduler and CurrentScheduler is not just that, but also that their performance is critically poor.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/686/0*EHRzSyiNESUvQ1k0" /><figcaption><em>Result of </em><em>Observable.Range(1, 10000).Subscribe()</em></figcaption></figure><p>The poor results of ImmediateScheduler, if not CurrentScheduler, might be counterintuitive. The ImmediateScheduler in dotnet/reactive new AsyncLockScheduler() for call Schedule, and the constructor of the base class LocalScheduler called by AsyncLockScheduler does SystemClock.Register, which locks, new WeakReference&lt;LocalScheduler&gt;(scheduler), and HashSet.Add. It&#39;s no wonder the performance is bad (although it&#39;s limited to just generating a SingleAssignmentDisposable each time for recursive calls, which is still a lot).</p><p>You might think it’s okay because Range is rarely used, but ImmediateScheduler is actually used quite often in unexpected places. A typical example is Merge, which uses ImmediateScheduler when IScheduler is unspecified, so if it&#39;s built to repeat frequent subscriptions, it may be called a considerable number of times. In fact, when I use dotnet/reactive in a server application, Merge and ImmediateScheduler once accounted for a significant portion of the server&#39;s memory usage. At that time, I managed to get by by creating a custom lightweight scheduler, specifying it directly, and thoroughly avoiding ImmediateScheduler. If there is a next dotnet/reactive, the performance improvement of ImmediateScheduler should be the first thing to do.</p><blockquote><em>The reason for doing </em><em>SystemClock.Register seems to be for monitoring changes to the system time with </em><em>DateTimeOffset.UtcNow. In other words, had we used long(timestamp) from the start, we wouldn&#39;t have invited such critical performance degradation. This is also one of the reasons for the failure in defining the IScheduler interface.</em></blockquote><p>By adopting TimeProvider, it’s also worth noting that unit testing has become easier with standard methods using <a href="https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.time.testing.faketimeprovider?view=dotnet-plat-ext-8.0">Microsoft.Extensions.Time.Testing.FakeTimeProvider</a>.</p><h3>FrameProvider</h3><p>One thing that is not present in other Rx libraries, but has been immensely effective in UniRx, is the frame-based operator. These include operators like DelayFrame that executes after a set number of frames, NextFrame for execution in the next frame, EveryUpdate as a factory that emits every frame, and EveryValueChanged for monitoring values every frame, all of which are convenient for use in game engines.</p><p>What I realized is that time and frames are conceptually similar, and not just in game engines, but also in UI processes where you have message loops and rendering loops, these concepts exist across various frameworks. Therefore, in R3, we abstracted frame-based processing in the form of FrameProvider, complementing TimerProvider. This allows the frame-based operators, previously only available to Unity, to work across any framework that supports C# (WinForms, WPF, WinUI3, MAUI, Godot, Avalonia, Stride, etc…).</p><pre>public abstract class FrameProvider<br>{<br>    public abstract long GetFrameCount();<br>    public abstract void Register(IFrameRunnerWorkItem callback);<br>}<br><br>public interface IFrameRunnerWorkItem<br>{<br>    // true, continue<br>    bool MoveNext(long frameCount);<br>}</pre><p>In R3, for every operator that requires a TimeProvider, we implemented a corresponding ***Frame operator.</p><ul><li>Return &lt;-&gt; ReturnFrame</li><li>Yield &lt;-&gt; YieldFrame</li><li>Interval &lt;-&gt; IntervalFrame</li><li>Timer &lt;-&gt; TimerFrame</li><li>Chunk &lt;-&gt; ChunkFrame</li><li>Debounce &lt;-&gt; DebounceFrame</li><li>Delay &lt;-&gt; DelayFrame</li><li>DelaySubscription &lt;-&gt; DelaySubscriptionFrame</li><li>ObserveOn(TimeProvider) &lt;-&gt; ObserveOn(FrameProvider)</li><li>Replay &lt;-&gt; ReplayFrame</li><li>Skip &lt;-&gt; SkipFrame</li><li>SkipLast &lt;-&gt; SkipLastFrame</li><li>SubscribeOn(TimeProvider) &lt;-&gt; SubscribeOn(FrameProvider)</li><li>Take &lt;-&gt; TakeFrame</li><li>TakeLast &lt;-&gt; TakeLastFrame</li><li>ThrottleFirst &lt;-&gt; ThrottleFirstFrame</li><li>ThrottleFirstLast &lt;-&gt; ThrottleFirstLastFrame</li><li>ThrottleLast &lt;-&gt; ThrottleLastFrame</li><li>Timeout &lt;-&gt; TimeoutFrame</li></ul><h3>async/await Integration</h3><p>First, we thoroughly eliminated Observables that return a single value, which are seen as a bad practice in existing Rx. These should be handled with async/await instead, as operators that return or expect a single value can introduce noise that leads to bad practices. First becomes FirstAsync, returning a Task&lt;T&gt;. AsyncSubject is removed; please use TaskCompletionSource instead.</p><p>Moreover, current C# code often involves asynchronous code, but fundamentally, Rx only accepts synchronous code. Carelessly, this could lead to a FireAndForget situation, and simply mixing it with SelectMany is not sufficient. Thus, we introduced special methods for Where/Select/Subscribe.</p><ul><li>SelectAwait(this Observable&lt;T&gt; source, Func&lt;T, CancellationToken, ValueTask&lt;TResult&gt;&gt; selector, AwaitOperation awaitOperation = Sequential, ...)</li><li>WhereAwait(this Observable&lt;T&gt; source, Func&lt;T, CancellationToken, ValueTask&lt;Boolean&gt;&gt; predicate, AwaitOperation awaitOperation = Sequential, ...)</li><li>SubscribeAwait(this Observable&lt;T&gt; source, Func&lt;T, CancellationToken, ValueTask&gt; onNextAsync, AwaitOperation awaitOperation = Sequential, ...)</li><li>SubscribeAwait(this Observable&lt;T&gt; source, Func&lt;T, CancellationToken, ValueTask&gt; onNextAsync, Action&lt;Result&gt; onCompleted, AwaitOperation awaitOperation = Sequential, ...)</li><li>SubscribeAwait(this Observable&lt;T&gt; source, Func&lt;T, CancellationToken, ValueTask&gt; onNextAsync, Action&lt;Exception&gt; onErrorResume, Action&lt;Result&gt; onCompleted, AwaitOperation awaitOperation = Sequential, ...)</li></ul><pre>public enum AwaitOperation<br>{<br>    /// &lt;summary&gt;All values are queued, and the next value waits for the completion of the asynchronous method.&lt;/summary&gt;<br>    Sequential,<br>    /// &lt;summary&gt;Drop new value when async operation is running.&lt;/summary&gt;<br>    Drop,<br>    /// &lt;summary&gt;If the previous asynchronous method is running, it is cancelled and the next asynchronous method is executed.&lt;/summary&gt;<br>    Switch,<br>    /// &lt;summary&gt;All values are sent immediately to the asynchronous method.&lt;/summary&gt;<br>    Parallel,<br>    /// &lt;summary&gt;All values are sent immediately to the asynchronous method, but the results are queued and passed to the next operator in order.&lt;/summary&gt;<br>    SequentialParallel,<br>    /// &lt;summary&gt;Send the first value and the last value while the asynchronous method is running.&lt;/summary&gt;<br>    ThrottleFirstLast<br>}</pre><p>SelectAwait, WhereAwait, SubscribeAwait accept asynchronous methods and offer six patterns for handling values that arrive while the asynchronous method is executing. Sequential queues the values until the asynchronous method completes. Drop discards all values that arrive while it’s executing,</p><p>useful for preventing multiple submissions in event handling. Switch cancels the ongoing asynchronous method and starts the next one, similar to Observable&lt;Observable&gt;.Switch. Parallel executes methods in parallel, like Observable&lt;Observable&gt;.Merge. SequentialParallel runs operations in parallel but ensures the values are passed to the next operator in the order they arrived. ThrottleFirstLast sends the first and last values received while the asynchronous method is running.</p><p>Furthermore, the following time-based filtering methods now accept asynchronous methods as well.</p><ul><li>Debounce(this Observable&lt;T&gt; source, Func&lt;T, CancellationToken, ValueTask&gt; throttleDurationSelector, ...)</li><li>ThrottleFirst(this Observable&lt;T&gt; source, Func&lt;T, CancellationToken, ValueTask&gt; sampler, ...)</li><li>ThrottleLast(this Observable&lt;T&gt; source, Func&lt;T, CancellationToken, ValueTask&gt; sampler, ...)</li><li>ThrottleFirstLast(this Observable&lt;T&gt; source, Func&lt;T, CancellationToken, ValueTask&gt; sampler, ...)</li></ul><p>We have also made Chunk accept asynchronous methods, and SkipUntil/TakeUntil has a variation that accepts CancellationToken and Task.</p><ul><li>SkipUntil(this Observable&lt;T&gt; source, CancellationToken cancellationToken)</li><li>SkipUntil(this Observable&lt;T&gt; source, Task task)</li><li>SkipUntil(this Observable&lt;T&gt; source, Func&lt;T, CancellationToken, ValueTask&gt; asyncFunc, ...)</li><li>TakeUntil(this Observable&lt;T&gt; source, CancellationToken cancellationToken)</li><li>TakeUntil(this Observable&lt;T&gt; source, Task task)</li><li>TakeUntil(this Observable&lt;T&gt; source, Func&lt;T, CancellationToken, ValueTask&gt; asyncFunc, ...)</li><li>Chunk(this Observable&lt;T&gt; source, Func&lt;T, CancellationToken, ValueTask&gt; asyncWindow, ...)</li></ul><p>For example, using an asynchronous version of Chunk allows you to create chunks at random intervals, not just fixed ones, enabling complex logic to be written more naturally and simply.</p><pre>Observable.Interval(TimeSpan.FromSeconds(1))<br>    .Index()<br>    .Chunk(async (_, ct) =&gt;<br>    {<br>        await Task.Delay(TimeSpan.FromSeconds(Random.Shared.Next(0, 5)), ct);<br>    })<br>    .Subscribe(xs =&gt;<br>    {<br>        Console.WriteLine(string.Join(&quot;, &quot;, xs));<br>    });</pre><p>async/await is indispensable in modern C# code, and we’ve made every effort to integrate it smoothly with Rx.</p><p>Retry operations can also benefit from async/await for better handling. Previously, Rx could only retry the entire pipeline, but with R3’s acceptance of async/await, retries can be performed on a per-asynchronous method execution basis.</p><pre>button.OnClickAsObservable()<br>    .SelectAwait(async (_, ct) =&gt;<br>    {<br>        var retry = 0;<br>    AGAIN:<br>        try<br>        {            <br>            return await DownloadTextAsync(&quot;https://google.com/&quot;, ct);<br>        }<br>        catch<br>        {<br>            if (retry++ &lt; 3) goto AGAIN;<br>            throw;<br>        }<br>    }, AwaitOperation.Drop)<br>    .Subscribe();</pre><p>Repeat can also be implemented with async/await. In this case, managing repeat conditions can be simpler than relying solely on Rx operators, potentially offering higher readability. Prioritizing readability (and performance) in coding is crucial. Let’s continue to effectively integrate Rx with async/await for better code.</p><p>You can also create Observables from asynchronous methods with Create and CreateFrom, which might allow for more straightforward descriptions compared to forcibly twisting operators.</p><ul><li>Create(Func&lt;Observer&lt;T&gt;, CancellationToken, ValueTask&gt; subscribe, ...)</li><li>CreateFrom(Func&lt;CancellationToken, IAsyncEnumerable&lt;T&gt;&gt; factory)</li></ul><h3>Naming Rules</h3><p>In R3, the names of several methods have been changed from those in dotnet/reactive or UniRx. For example:</p><ul><li>Buffer -&gt; Chunk</li><li>StartWith -&gt; Prepend</li><li>Distinct(selector) -&gt; DistinctBy</li><li>Throttle -&gt; Debounce</li><li>Sample -&gt; ThrottleLast</li></ul><p>Let’s explain the reasons for these changes.</p><p>First, when creating a LINQ-style library in .NET, the highest priority should be given to the method names implemented in LINQ to Objects (Enumerable). The reason why Buffer was changed to Chunk is because Enumerable.Chunk was added in .NET 6, and its function is the same as Buffer. Rx predates the introduction of Chunk, so there&#39;s nothing that can be done about the differing names, but if there are no constraints, names should align with LINQ to Objects. Therefore, Chunk is the only choice. The same goes for Prepend and DistinctBy.</p><p>You might resist changing Throttle to Debounce. This is because the world&#39;s standard is Debounce. In the Rx world, dotnet/reactive is the only one that refers to Debounce as Throttle. It could be argued that there&#39;s no need to change since RxNet is the progenitor of the Rx world, but now being in the minority, it&#39;s also correct to go along with the majority.</p><p>The reason for changing to Debounce is not just that, but also the existence of ThrottleFirst / ThrottleLast. These take the first or last value in a sampling period, respectively, forming a pair. But (dotnet/reactive&#39;s) Throttle behaves entirely differently, so the name Throttle is confusing. Originally, dotnet/reactive lacks ThrottleFirst and only has Sample, which corresponds to ThrottleLast, so it&#39;s fine. However, if adopting ThrottleFirst/ThrottleLast, inevitably, the name must be Debounce.</p><p>Regarding Sample, due to the symmetry in the names and functions of First/Last, it was renamed to ThrottleLast. In dotnet/reactive, since First doesn&#39;t exist, Sample would have been fine, but if adopting ThrottleFirst, the name inevitably becomes ThrottleLast.</p><p>There is a compromise to keep the name Sample and make it an alias for ThrottleLast (as is the case with RxJava), but having different names for the same function confuses users. There are quite a few questions like what&#39;s the difference between sample and throttleLast? Rx is complicated enough, and to avoid unnecessary confusion, aliases should definitely be avoided. Aliases like mapping Select to Map or Where to Filter are utterly foolish.</p><h3>Default Scheduler for Platforms</h3><p>In dotnet/reactive, the default scheduler is almost fixed. Technically, it’s possible to replace some behaviors by appropriately implementing IPlatformEnlightenmentProvider or IConcurrencyAbstractionLayer, but it&#39;s unnecessarily complicated and mostly hidden with [EditorBrowsable(EditorBrowsableState.Never)], so it&#39;s hardly expected to be used properly.</p><p>However, for Timer or Delay, if it’s WPF, they operate on DispatcherTimer, and in Unity, they work on Timer in the PlayerLoop, automatically dispatching to the main thread, which is convenient and advantageous for performance as ObserveOn becomes unnecessary in most cases.</p><p>In R3, we made it simple to replace the default TimeProvider/FrameProvider.</p><pre>public static class ObservableSystem<br>{<br>    public static TimeProvider DefaultTimeProvider { get; set; } = TimeProvider.System;<br>    public static FrameProvider DefaultFrameProvider { get; set; } = new NotSupportedFrameProvider();<br>}</pre><p>By replacing them at application startup, the best scheduler for that application will be used by default.</p><pre>// For example, in WPF, the Dispatcher series is set, so it automatically returns to the UI thread<br>public static class WpfProviderInitializer<br>{<br>    public static void SetDefaultObservableSystem(Action&lt;Exception&gt; unhandledExceptionHandler)<br>    {<br>        ObservableSystem.RegisterUnhandledExceptionHandler(unhandledExceptionHandler);<br>        ObservableSystem.DefaultTimeProvider = new WpfDispatcherTimerProvider();<br>        ObservableSystem.DefaultFrameProvider = new WpfRenderingFrameProvider();<br>    }<br>}<br><br>// In the case of Unity, PlayerLoop-based ones are used, avoiding ThreadPool<br>public static class UnityProviderInitializer<br>{<br>    [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterAssembliesLoaded)]<br>    public static void SetDefaultObservableSystem()<br>    {<br>        SetDefaultObservableSystem(static ex =&gt; UnityEngine.Debug.LogException(ex));<br>    }<br><br>    public static void SetDefaultObservableSystem(Action&lt;Exception&gt; unhandledExceptionHandler)<br>    {<br>        ObservableSystem.RegisterUnhandledExceptionHandler(unhandledExceptionHandler);<br>        ObservableSystem.DefaultTimeProvider = UnityTimeProvider.Update;<br>        ObservableSystem.DefaultFrameProvider = UnityFrameProvider.Update;<br>    }<br>}</pre><p>dotnet/reactive’s inability to change the default scheduler hardly supports a variety of platforms.</p><pre>internal static class SchedulerDefaults<br>{<br>    internal static IScheduler ConstantTimeOperations =&gt; ImmediateScheduler.Instance;<br>    internal static IScheduler TailRecursion =&gt; ImmediateScheduler.Instance;<br>    internal static IScheduler Iteration =&gt; CurrentThreadScheduler.Instance;<br>    internal static IScheduler TimeBasedOperations =&gt; DefaultScheduler.Instance;<br>    internal static IScheduler AsyncConversions =&gt; DefaultScheduler.Instance;<br>}</pre><p>Especially in AOT scenarios(NativeAOT, Unity IL2CPP) or web publishing (WebGL, WASM), there are situations where ThreadPool cannot be used and must be absolutely avoided. Thus, SchedulerDefaults.TimeBasedOperations being essentially fixed to ThreadPoolScheduler is regrettably restrictive.</p><h3>Pull IAsyncEnumerable vs Push Observable</h3><p>IAsyncEnumerable (or UniTask&#39;s IUniTaskAsyncEnumerable) is a pull-based asynchronous sequence. Reactive Extensions (Rx) is a push-based asynchronous sequence. They are similar. The fact that you can do LINQ-like operations with both is also similar. It&#39;s natural to say that which one to use depends on the case, but then, what are those cases? When should you use which? It would be nice to have some criteria for this decision.</p><p>Basically, if there’s a buffer (queue) behind the scenes, pull-based approaches seem suitable, so for network-related scenarios, it might be a good idea to use IAsyncEnumerable. Indeed, natural opportunities to use it come up with System.IO.Pipelines or System.Threading.Channels.</p><p>The place to use Rx is indeed related to events.</p><p>The deciding factor on which to use should be to choose the representation that is natural for the source. Raw events, such as OnMove or OnClick, are entirely push-based, with no buffer involved. It would also be suitable for high-frequency events like sensor data, or for events that come through the network where the buffer is hidden and delivered purely as events. This means Rx is the natural choice to handle them.</p><p>You could interpose a queue and deal with it via IAsyncEnumerable, but that would be unnatural. Alternatively, expressing the intentional dropping of values by not using a queue could also be done, but again, that’s unnatural. Being unnatural usually means worse performance and less clarity. In other words, it’s not good. Therefore, handle event-related things with Rx. With R3, integration with async/await allows you to explicitly specify buffering during asynchronous operations or dropping values with operators. This is clear and performs well. Let’s use R3.</p><h3>Conclusion</h3><p>I’ve pointed out many things, I have nothing but gratitude for the original creators of Rx.NET. Once again, I am in awe of the brilliance of the Rx concept and the organized functionality of its various operators. Although some parts of the implementation have become outdated, it has a track record, stability, and high quality. I have been using it from the very beginning and have been enthusiastic about it. I also want to thank the current maintainers. It’s very difficult to maintain a widely used library in an ever-changing environment.</p><p>However, I wanted to revive the value of Rx. And if it was to be rebuilt, I believed I was the only one who could do it. I know the history and implementation of Rx from the beginning, have implemented Rx itself (<a href="https://github.com/neuecc/UniRx">UniRx</a>), and through its widespread use, have become familiar with many use cases and issues. I’ve also been involved on the application side of Rx in large-scale implementations for game titles and implemented a custom runtime for async/await (<a href="https://github.com/Cysharp/UniTask">UniTask</a>) that has also been widely used, giving me insight into all aspects of this area. I have also accumulated experience in implementing high-performance serializers that have become standards in the industry, such as <a href="https://github.com/MessagePack-CSharp/MessagePack-CSharp">MessagePack for C#</a> and <a href="https://github.com/Cysharp/MemoryPack">MemoryPack</a>, developing network frameworks like <a href="https://github.com/Cysharp/MagicOnion/">MagicOnion</a> and applying new protocols (HTTP/2, gRPC), and in implementing modern high-performance C# in various areas with <a href="https://github.com/Cysharp/ZLogger/">ZLogger</a> and <a href="https://github.com/Cysharp/AlterNats/">AlterNats</a>. I have a sufficient technical foundation.</p><p>It’s fine for there to be multiple futures, so I hope you will see R3 as one possible future for Rx that I present. There may be another evolution and future for dotnet/reactive.</p><p>With that said, I believe R3 has shown enough potential and possibility to be considered a replacement. I have tried my best to consider migration scenarios as well, so please give it a try…!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=cf29abcc5826" width="1" height="1" alt="">]]></content:encoded>
        </item>
    </channel>
</rss>