<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged csharp</title>
<link>https://coderlegion.com/tag/csharp</link>
<description>Powered by Question2Answer</description>
<item>
<title>A JSON-RPC Library With No Method Registration: How Does It Decide What Can Be Called?</title>
<link>https://coderlegion.com/32964/a-json-rpc-library-with-no-method-registration-how-does-it-decide-what-can-be-called</link>
<description>&lt;p&gt;JSON-RPC 2.0 is a small specification: the client sends &lt;code&gt;{&quot;jsonrpc&quot;:&quot;2.0&quot;,&quot;method&quot;:&quot;...&quot;,&quot;params&quot;:{...},&quot;id&quot;:1}&lt;/code&gt;, and the server answers with a &lt;code&gt;result&lt;/code&gt; or an &lt;code&gt;error&lt;/code&gt;. REST makes you decide on routes, HTTP verbs and status codes; JSON-RPC only cares about which method to call and with what parameters. For an API where you own both the front end and the back end, that is pleasant to work with.&lt;/p&gt;
&lt;p&gt;Polhem.JsonRpc is a set of .NET packages for JSON-RPC 2.0: a transport-independent server, an ASP.NET Core endpoint and a client, built on System.Text.Json alone. It started as the API layer of the &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/polhem-dev/polhem&quot;&gt;Polhem framework&lt;/a&gt;. I have now extracted it into standalone packages that do not depend on the framework, so other projects can reference them directly. The source is on &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/polhem-dev/polhem-jsonrpc&quot;&gt;GitHub&lt;/a&gt; under the MIT license, and the packages are on &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.nuget.org/packages?q=Polhem.JsonRpc&quot;&gt;NuGet&lt;/a&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;1-what-it-covers&quot;&gt;1️⃣ What it covers&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Full JSON-RPC 2.0: ordinary calls, notifications (calls that expect no response), and batches (several calls in one message). An id can be a string, an integer or &lt;code&gt;null&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;A transport-independent server: the dispatcher (the part that finds and runs the method) knows nothing about HTTP. The ASP.NET Core endpoint is a separate package.&lt;/li&gt;
&lt;li&gt;A client: an HTTP transport, batches, notifications, and interceptors that can rewrite the parameters and result of each call.&lt;/li&gt;
&lt;li&gt;Optional payload encryption: the parameters and result go into an envelope that can be compressed and encrypted, and replayed calls can be rejected.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It is split into several NuGet packages, so an application references only what it needs:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Package &lt;/th&gt;&lt;th&gt;Use&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.nuget.org/packages/Polhem.JsonRpc&quot;&gt;Polhem.JsonRpc&lt;/a&gt; &lt;/td&gt;&lt;td&gt;Message types, error codes and the transport abstraction shared by the server and the client&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.nuget.org/packages/Polhem.JsonRpc.Server&quot;&gt;Polhem.JsonRpc.Server&lt;/a&gt; &lt;/td&gt;&lt;td&gt;The dispatcher: method resolution, parameter binding and filters&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.nuget.org/packages/Polhem.JsonRpc.AspNetCore&quot;&gt;Polhem.JsonRpc.AspNetCore&lt;/a&gt; &lt;/td&gt;&lt;td&gt;The ASP.NET Core endpoint, &lt;code&gt;MapJsonRpc&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.nuget.org/packages/Polhem.JsonRpc.Client&quot;&gt;Polhem.JsonRpc.Client&lt;/a&gt; &lt;/td&gt;&lt;td&gt;The client, &lt;code&gt;JsonRpcConnector&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.nuget.org/packages/Polhem.JsonRpc.Payload&quot;&gt;Polhem.JsonRpc.Payload&lt;/a&gt; &lt;/td&gt;&lt;td&gt;Optional: the payload envelope, compression, encryption and replay protection&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.nuget.org/packages/Polhem.JsonRpc.Payload.Client&quot;&gt;Polhem.JsonRpc.Payload.Client&lt;/a&gt; &lt;/td&gt;&lt;td&gt;Optional: a client that seals the parameters and opens the result of every call&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.nuget.org/packages/Polhem.JsonRpc.Payload.Server&quot;&gt;Polhem.JsonRpc.Payload.Server&lt;/a&gt; &lt;/td&gt;&lt;td&gt;Optional: opens the envelope and checks for replays on the server&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The server references &lt;code&gt;Polhem.JsonRpc.AspNetCore&lt;/code&gt;, and the client references only &lt;code&gt;Polhem.JsonRpc.Client&lt;/code&gt;. The split keeps a client, whether a desktop app or a mobile app, from pulling in ASP.NET Core and the reflection-based dispatcher.&lt;/p&gt;
&lt;p&gt;Native AOT compiles an app to native code at publish time, so nothing can rely on reflection at run time; iOS and WebAssembly both need it. The shared package, the client and the payload packages are marked AOT-compatible, and CI publishes a test program with Native AOT and runs it. The server package finds methods by reflection, so it does not support AOT. The target framework is &lt;code&gt;net10.0&lt;/code&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;2-getting-started&quot;&gt;2️⃣ Getting started&lt;/h3&gt;
&lt;h4 id=&quot;asp-net-core-server-minimal-api-&quot;&gt;ASP.NET Core server (Minimal API)&lt;/h4&gt;
&lt;p&gt;The server uses ASP.NET Core Minimal APIs (endpoints mapped directly in &lt;code&gt;Program.cs&lt;/code&gt; with &lt;code&gt;MapXxx&lt;/code&gt;, without MVC controllers). &lt;code&gt;MapJsonRpc&lt;/code&gt; creates a POST endpoint at the given path, and every JSON-RPC call comes in through it. &lt;code&gt;AddControllers&lt;/code&gt; is not needed.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Program.cs&lt;/code&gt; needs three things: register the object factory, register the JSON-RPC services, and map the endpoint.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;using Polhem.JsonRpc.AspNetCore;
using Polhem.JsonRpc.Server;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton&amp;lt;IJsonRpcObjectFactory, AppObjectFactory&amp;gt;();
builder.Services.AddJsonRpcServer();

var app = builder.Build();
app.MapJsonRpc(&quot;/api&quot;);
await app.RunAsync();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;AddJsonRpcServer&lt;/code&gt; takes two sets of options. The first configures the server itself, such as filters, the method policy, the batch limit and serializer options. The second configures the HTTP endpoint, such as the request body size limit and which HTTP status code an error returns. Leave them out and the defaults apply.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;builder.Services.AddJsonRpcServer(
    options =&amp;gt;
    {
        options.MaxBatchSize = 50;
        options.Filters.Add(new ApiKeyFilter(apiKey)); // see section 5
    },
    http =&amp;gt; http.MaxRequestBodySize = 1024 * 1024);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;MapJsonRpc&lt;/code&gt; returns the same thing as any other Minimal API endpoint, so authorization, CORS and rate limiting chain on the usual way, for example &lt;code&gt;app.MapJsonRpc(&quot;/api&quot;).RequireAuthorization();&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The object factory and the class that handles the calls:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;public sealed class AppObjectFactory : IJsonRpcObjectFactory
{
    public object? CreateObject(string progId, JsonRpcRequestContext context) =&amp;gt; progId switch
    {
        &quot;Calculator&quot; =&amp;gt; new Calculator(),
        _ =&amp;gt; null,
    };
}

public sealed class Calculator
{
    public AddResponse Add(AddRequest request) =&amp;gt; new() { Sum = request.A + request.B };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;client&quot;&gt;Client&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;using var http = new HttpClient { BaseAddress = new Uri(&quot;http://localhost:5080/api&quot;) };
var rpc = new JsonRpcConnector(new HttpTransport(http));

var added = await rpc.InvokeAsync&amp;lt;AddResponse&amp;gt;(&quot;Calculator.Add&quot;, new AddRequest { A = 1, B = 2 });
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That is enough to call &lt;code&gt;Calculator.Add&lt;/code&gt;. Note that the server has no &lt;code&gt;AddMethod&lt;/code&gt; or similar registration, and &lt;code&gt;Calculator&lt;/code&gt; carries no attribute.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;3-with-no-registration-which-methods-can-be-called-&quot;&gt;3️⃣ With no registration, which methods can be called?&lt;/h3&gt;
&lt;p&gt;Skipping registration is convenient, but it raises a worry: if one day I add a public method to the class for internal use, can it be called from the network too? When the server receives &lt;code&gt;Calculator.Add&lt;/code&gt;, it runs the following checks in order.&lt;/p&gt;
&lt;p&gt;Name format. The method name must be &lt;code&gt;ProgId.Action&lt;/code&gt;. A ProgId (program identifier) may contain letters, digits, underscores and hyphens; an action letters, digits and underscores; each at most 64 characters. A malformed name is rejected before any lookup.&lt;/p&gt;
&lt;p&gt;The application decides the object. The ProgId is passed to your implementation of &lt;code&gt;IJsonRpcObjectFactory.CreateObject&lt;/code&gt;, which decides which object to create; returning &lt;code&gt;null&lt;/code&gt; means the ProgId is unknown. The packages keep no registry of names, so which ProgIds exist is entirely up to the factory. After the call, the object is passed to &lt;code&gt;ReleaseObjectAsync&lt;/code&gt; to be released.&lt;/p&gt;
&lt;p&gt;Method signature. An action maps only to a public, non-generic instance method with exactly one parameter. Static methods, property accessors and methods declared by &lt;code&gt;object&lt;/code&gt; do not count. If two or more methods with the same name qualify, the server does not pick one; it treats the method as not found.&lt;/p&gt;
&lt;p&gt;Naming convention. The parameter type must be named &lt;code&gt;{Action}Request&lt;/code&gt;, and the return type (or the result type of &lt;code&gt;Task&amp;lt;T&amp;gt;&lt;/code&gt; or &lt;code&gt;ValueTask&amp;lt;T&amp;gt;&lt;/code&gt;) must be named &lt;code&gt;{Action}Response&lt;/code&gt;. &lt;code&gt;Add&lt;/code&gt; takes an &lt;code&gt;AddRequest&lt;/code&gt; and returns an &lt;code&gt;AddResponse&lt;/code&gt;. This is the default method policy; any public method that does not follow it is treated as nonexistent and answered with &lt;code&gt;-32601 Method not found&lt;/code&gt;. An internal helper method that does not follow this naming cannot be called from outside.&lt;/p&gt;
&lt;p&gt;The rule can be replaced through &lt;code&gt;JsonRpcServerOptions.MethodPolicy&lt;/code&gt;. The Polhem framework replaces it with one that reads its own access-control attribute.&lt;/p&gt;
&lt;p&gt;Policy before filters. The method policy runs before every filter, so filters such as authorization checks or decryption only ever see calls that are allowed to run.&lt;/p&gt;
&lt;p&gt;Only after these checks are the &lt;code&gt;params&lt;/code&gt; bound. &lt;code&gt;params&lt;/code&gt; must be a JSON object, deserialized into the request class with camelCase names by default; missing &lt;code&gt;params&lt;/code&gt; or an array (positional parameters) are answered with &lt;code&gt;-32602 Invalid params&lt;/code&gt;. To return a custom error, a method throws &lt;code&gt;JsonRpcErrorException&lt;/code&gt;; any other exception is answered with &lt;code&gt;-32603 Internal error&lt;/code&gt;, and the exception message is not sent to the client.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;4-the-client&quot;&gt;4️⃣ The client&lt;/h3&gt;
&lt;p&gt;An error returned by the server becomes a &lt;code&gt;JsonRpcErrorException&lt;/code&gt;, which carries the error code and message:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;try
{
    await rpc.InvokeAsync&amp;lt;DivideResponse&amp;gt;(&quot;Calculator.Divide&quot;, new DivideRequest { Dividend = 1, Divisor = 0 });
}
catch (JsonRpcErrorException ex)
{
    Console.WriteLine($&quot;Error {ex.Code}: {ex.Message}&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Notifications and batches:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;// The server runs it but does not respond.
await rpc.NotifyAsync(&quot;Calculator.Log&quot;, new LogRequest { Message = &quot;Hello&quot; });

// Two calls sent in the same message.
var batch = rpc.CreateBatch();
var first = batch.Add&amp;lt;AddResponse&amp;gt;(&quot;Calculator.Add&quot;, new AddRequest { A = 2, B = 3 });
var second = batch.Add&amp;lt;AddResponse&amp;gt;(&quot;Calculator.Add&quot;, new AddRequest { A = 4, B = 5 });
await batch.SendAsync();
Console.WriteLine($&quot;{(await first)!.Sum}, {(await second)!.Sum}&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The request and response classes usually live in a project shared by both sides, which is how the samples in the repo are arranged.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;5-extension-points-filters-and-interceptors&quot;&gt;5️⃣ Extension points: filters and interceptors&lt;/h3&gt;
&lt;p&gt;A server filter wraps every call. It can reject the call, rewrite the parameters before the method runs, and rewrite the result afterwards. For example, checking an API key:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;public sealed class ApiKeyFilter(string expectedKey) : IJsonRpcFilter
{
    public ValueTask InvokeAsync(JsonRpcRequestContext context, JsonRpcFilterDelegate next)
    {
        context.Transport.Headers.TryGetValue(&quot;X-Api-Key&quot;, out var key);
        if (!CryptographicOperations.FixedTimeEquals(
                Encoding.UTF8.GetBytes(key ?? &quot;&quot;), Encoding.UTF8.GetBytes(expectedKey)))
        {
            throw new JsonRpcErrorException(-32001, &quot;Unauthorized&quot;);
        }
        return next(context);
    }
}

builder.Services.AddJsonRpcServer(options =&amp;gt; options.Filters.Add(new ApiKeyFilter(apiKey)));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To add an HTTP header on the client, use &lt;code&gt;HttpClient&lt;/code&gt;'s own &lt;code&gt;DelegatingHandler&lt;/code&gt;; the packages add no layer of their own. To rewrite the parameters and result of every call, add an &lt;code&gt;IJsonRpcClientInterceptor&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The Polhem framework plugs its sessions and authorization checks into these extension points (the object factory, the method policy and filters). None of that logic is in the packages themselves.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;6-encrypted-payloads&quot;&gt;6️⃣ Encrypted payloads&lt;/h3&gt;
&lt;p&gt;HTTPS protects the connection. If the parameters and results also need end-to-end protection, or a call must not run a second time when it is resent, add the optional payload packages.&lt;/p&gt;
&lt;p&gt;They put &lt;code&gt;params&lt;/code&gt; and &lt;code&gt;result&lt;/code&gt; into an envelope with three formats: plain JSON, encoded (serialized, then gzip-compressed), and encrypted (AES-256-CBC with HMAC-SHA256, encrypt-then-MAC). A frame carrying a timestamp and a sequence number can also be put in front of the content, so the server can reject expired or repeated requests; that is the replay protection.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;// Server: the application's policy decides the keys and which methods must be encrypted or replay-protected.
builder.Services.AddJsonRpcServer(options =&amp;gt; options.UsePayload(payloadOptions, new MyPayloadPolicy()));

// Client: called the same way as an ordinary call; each call seals its parameters and opens its result.
var rpc = new PayloadConnector(connector, new PayloadProcessor(payloadOptions),
    new PayloadConnectorOptions { KeyProvider = () =&amp;gt; sessionKey });
var added = await rpc.InvokeAsync&amp;lt;AddResponse&amp;gt;(&quot;Calculator.Add&quot;, request);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A few more security details:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The HMAC is compared in constant time and verified before anything is decrypted.&lt;/li&gt;
&lt;li&gt;The HMAC covers the method name and the direction, so an intercepted call cannot be redirected to another method, and a response cannot be sent back as a request.&lt;/li&gt;
&lt;li&gt;A method can require a minimum format; a request in a lower format is rejected before any key is asked for.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;How keys are negotiated (exchanged after sign-in, provisioned in advance, and so on) is outside the packages and left to the application.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;7-how-it-differs-from-common-libraries&quot;&gt;7️⃣ How it differs from common libraries&lt;/h3&gt;
&lt;p&gt;There are quite a few JSON-RPC libraries for .NET; here are two common ones for comparison. The version in each column header is the latest stable release on NuGet as of October 2026, and the details come from that release's documentation and NuGet dependency list. Later releases may differ.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;  &lt;/th&gt;&lt;th&gt;StreamJsonRpc 2.25.29 &lt;/th&gt;&lt;th&gt;Tochka.JsonRpc 7.4.1 &lt;/th&gt;&lt;th&gt;Polhem.JsonRpc 1.2.0&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;JSON library &lt;/td&gt;&lt;td&gt;Newtonsoft.Json by default, switchable to System.Text.Json; the package still depends on Newtonsoft.Json and MessagePack &lt;/td&gt;&lt;td&gt;System.Text.Json &lt;/td&gt;&lt;td&gt;System.Text.Json, with no other dependencies&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Transport model &lt;/td&gt;&lt;td&gt;Full duplex over a Stream, WebSocket or Pipe &lt;/td&gt;&lt;td&gt;HTTP on ASP.NET Core MVC &lt;/td&gt;&lt;td&gt;HTTP request and response on ASP.NET Core Minimal APIs&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Client &lt;/td&gt;&lt;td&gt;Yes, with proxies generated from interfaces &lt;/td&gt;&lt;td&gt;Yes &lt;/td&gt;&lt;td&gt;Yes&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;How methods are registered &lt;/td&gt;&lt;td&gt;&lt;code&gt;AddLocalRpcTarget&lt;/code&gt; adds a target object whose public methods become callable &lt;/td&gt;&lt;td&gt;Controllers that inherit &lt;code&gt;JsonRpcControllerBase&lt;/code&gt;; each action is a method &lt;/td&gt;&lt;td&gt;No registration; found by &lt;code&gt;ProgId.Action&lt;/code&gt; and the naming convention&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Native AOT &lt;/td&gt;&lt;td&gt;Partially supported, within the limits its documentation lists &lt;/td&gt;&lt;td&gt;Tied to MVC, not supported &lt;/td&gt;&lt;td&gt;The shared package, the client and the payload packages support it; the server does not&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Good fit &lt;/td&gt;&lt;td&gt;Two-way communication, such as Visual Studio extensions or LSP &lt;/td&gt;&lt;td&gt;You already use MVC and want to keep writing controllers &lt;/td&gt;&lt;td&gt;An API where you own both ends, possibly with desktop or mobile clients&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;StreamJsonRpc is the most complete, with cancellation, progress reporting and server-to-client calls, but it is designed for streams and full duplex; turning it into an HTTP request/response API means building the endpoint yourself.&lt;/p&gt;
&lt;p&gt;Tochka.JsonRpc is the closest in scope to Polhem.JsonRpc. It builds on MVC controllers, so MVC filters and model binding work as they are, at the cost of sharing MVC's lack of AOT support.&lt;/p&gt;
&lt;p&gt;Polhem.JsonRpc goes in a different direction: it trades a fixed call shape for &quot;no registration&quot; and &quot;an API that can evolve&quot;, as the next section explains.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;8-design-trade-offs&quot;&gt;8️⃣ Design trade-offs&lt;/h3&gt;
&lt;p&gt;The fixed call shape is there so the API can evolve. The &lt;code&gt;ProgId.Action&lt;/code&gt; name, one request class and one response class are a deliberate restriction. When a member is added to a request or response class, a member an older front end does not send binds to its default on the server, and a result member an older front end does not know is simply skipped when read. Old and new front ends call the same method, with no &lt;code&gt;v1&lt;/code&gt; and &lt;code&gt;v2&lt;/code&gt; endpoints to maintain. Positional parameters cannot do this: add, remove or reorder one parameter and every caller has to change.&lt;/p&gt;
&lt;p&gt;Defaults follow the specification. The internal error code is &lt;code&gt;-32603&lt;/code&gt;, a response carries only &lt;code&gt;jsonrpc&lt;/code&gt;, &lt;code&gt;result&lt;/code&gt; or &lt;code&gt;error&lt;/code&gt;, and &lt;code&gt;id&lt;/code&gt;, and &lt;code&gt;id&lt;/code&gt; is always written, as &lt;code&gt;null&lt;/code&gt; when it cannot be determined. The Polhem framework's original response format differed from the specification in places; when the packages were extracted, it was the framework that changed to follow the specification, not the packages that bent to the old format.&lt;/p&gt;
&lt;p&gt;The reasoning behind each decision is recorded as ADRs (Architecture Decision Records) in the repo's &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/polhem-dev/polhem-jsonrpc/tree/main/maintainers/adr&quot;&gt;&lt;code&gt;maintainers/adr/&lt;/code&gt;&lt;/a&gt;, and the security design is summarized in &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/polhem-dev/polhem-jsonrpc/blob/main/docs/security.md&quot;&gt;&lt;code&gt;docs/security.md&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-wrapping-up&quot;&gt;✅ Wrapping up&lt;/h3&gt;
&lt;p&gt;Install the server and client packages:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;dotnet add package Polhem.JsonRpc.AspNetCore
dotnet add package Polhem.JsonRpc.Client
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The repo includes &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/polhem-dev/polhem-jsonrpc/tree/main/samples&quot;&gt;samples&lt;/a&gt;: an ASP.NET Core Minimal API server, a console client (ordinary calls, error handling, notifications and batches), and a server and client using encrypted payloads.&lt;/p&gt;
&lt;p&gt;These packages do convention-based JSON-RPC, not general-purpose JSON-RPC. Protocols whose method names and parameters are defined by someone else, such as the Language Server Protocol, MCP or an Ethereum node's API (names like &lt;code&gt;textDocument/didOpen&lt;/code&gt; and &lt;code&gt;eth_call&lt;/code&gt; with positional parameters), cannot be implemented with them; StreamJsonRpc or the MCP C# SDK fit those better. Nor do they offer a full-duplex connection where the server calls the client: one call is one HTTP request and its response.&lt;/p&gt;
&lt;p&gt;If you own both ends of your API, want JSON-RPC that relies on System.Text.Json alone, or have clients that run as desktop or mobile apps, give it a try. Feedback, issues and pull requests are welcome.&lt;/p&gt;
&lt;h3 id=&quot;further-reading&quot;&gt;Further reading&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.jsonrpc.org/specification&quot;&gt;JSON-RPC 2.0 Specification&lt;/a&gt;: the full JSON-RPC 2.0 specification&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/microsoft/vs-streamjsonrpc&quot;&gt;StreamJsonRpc&lt;/a&gt;: the choice when you need full-duplex streams&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/tochka-public/Tochka.JsonRpc&quot;&gt;Tochka.JsonRpc&lt;/a&gt;: JSON-RPC built on ASP.NET Core MVC controllers&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://learn.microsoft.com/dotnet/core/deploying/native-aot/&quot;&gt;Native AOT deployment&lt;/a&gt;: the limits and compatibility of .NET Native AOT&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;📘 Original article on HackMD, in Traditional Chinese:&lt;br&gt;
👉 &lt;a rel=&quot;nofollow&quot; href=&quot;https://hackmd.io/@jeff377/polhem-jsonrpc&quot;&gt;https://hackmd.io/@jeff377/polhem-jsonrpc&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;📢 You are welcome to republish this, please credit the source.&lt;br&gt;
📬 Follow for more engineering notes:&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.facebook.com/profile.php?id=61574839666569&quot;&gt;Facebook&lt;/a&gt; ｜ &lt;a rel=&quot;nofollow&quot; href=&quot;https://hackmd.io/@jeff377&quot;&gt;HackMD&lt;/a&gt; ｜ &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/jeff377&quot;&gt;GitHub&lt;/a&gt; ｜ &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.nuget.org/profiles/jeff377&quot;&gt;NuGet&lt;/a&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32964/a-json-rpc-library-with-no-method-registration-how-does-it-decide-what-can-be-called</guid>
<pubDate>Thu, 29 Oct 2026 16:15:05 +0000</pubDate>
</item>
<item>
<title>From Anemic Models to Rich Domain Models, DDD Base Entities, and C# Records</title>
<link>https://coderlegion.com/32663/from-anemic-models-to-rich-domain-models-ddd-base-entities-and-c-records</link>
<description>&lt;p&gt;When building enterprise applications in C#, a common architectural trap is writing &lt;strong&gt;procedural code disguised as object-oriented code&lt;/strong&gt;. This usually happens when we rely entirely on anemic data bags &lt;code&gt;(&lt;/code&gt;public get; set;&lt;code&gt; DTOs) for business logic, forcing our application services to manage all validation and state changes manually.&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;In this guide, we will tie together &lt;strong&gt;Object-Oriented Programming (OOP)&lt;/strong&gt;, the &lt;strong&gt;Single Responsibility Principle (SRP)&lt;/strong&gt;, &lt;strong&gt;Domain-Driven Design (DDD) Entity base classes&lt;/strong&gt;, &lt;strong&gt;C# Records for Value Objects&lt;/strong&gt;, &lt;strong&gt;Combining Interfaces &amp;amp; Abstract Classes&lt;/strong&gt;, &lt;strong&gt;EF Core Persistence&lt;/strong&gt;, and &lt;strong&gt;Unit Testing&lt;/strong&gt; into a complete, publication-ready reference.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;part-1-the-anti-pattern-anemic-models-amp-dto-misuse-&quot;&gt;Part 1: The Anti-Pattern (Anemic Models &amp;amp; DTO Misuse)&lt;/h3&gt;
&lt;p&gt;An anemic model treats classes as passive data structures with zero behavior. When every model exposes public setters, any part of the codebase can mutate state arbitrarily, bypassing business rules.&lt;/p&gt;
&lt;h4 id=&quot;the-problematic-approach&quot;&gt;The Problematic Approach&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;// 1. The Anemic Entity (Just a data bag)
public class OrderDto
{
    public Guid Id { get; set; }
    public string CustomerEmail { get; set; }
    public decimal TotalAmount { get; set; }
    public string Status { get; set; } // Can be set to anything from anywhere!
}

// 2. The God Service handling all procedural logic
public class OrderService
{
    public void ProcessOrder(OrderDto order)
    {
        // Business logic scattered in services instead of living in the model
        if (order.TotalAmount &amp;lt;= 0) throw new Exception(&quot;Invalid amount&quot;);
        order.Status = &quot;Processed&quot;; // Direct mutation without invariant checks
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;why-anemic-models-fail&quot;&gt;Why Anemic Models Fail&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Repeated Validation Checks:&lt;/strong&gt; Since anemic models can't check their own rules, every service or controller writes the same validation code over and over. If a developer forgets one check, a business rule breaks silently.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Unprotected Data (Broken States):&lt;/strong&gt; Public setters mean code can accidentally create broken data—like a negative order total or an incomplete order marked as completed—which then gets saved to the database.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Hard-to-Fix Rules (&quot;Shotgun Surgery&quot;):&lt;/strong&gt; When business rules change, you have to hunt through 10 different files and controllers to update identical &lt;code&gt;if&lt;/code&gt; statements.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;part-2-ddd-foundation-entity-base-class-amp-c-records&quot;&gt;Part 2: DDD Foundation — Entity Base Class &amp;amp; C# Records&lt;/h3&gt;
&lt;p&gt;In Domain-Driven Design, &lt;strong&gt;Entities&lt;/strong&gt; are defined by their unique identity rather than their attributes, while &lt;strong&gt;Value Objects&lt;/strong&gt; (idiomatically implemented using C# &lt;strong&gt;records&lt;/strong&gt;) are defined entirely by their structural values and are strictly immutable.&lt;/p&gt;
&lt;h4 id=&quot;1-ddd-entity-base-class&quot;&gt;1. DDD Entity Base Class&lt;/h4&gt;
&lt;p&gt;Notice how &lt;code&gt;EqualityComparer&amp;lt;TId&amp;gt;&lt;/code&gt; is used below to robustly handle generic identifier comparisons across different types (&lt;code&gt;Guid&lt;/code&gt;, &lt;code&gt;int&lt;/code&gt;, &lt;code&gt;string&lt;/code&gt;, etc.) while ensuring unpersisted entities do not falsely equal each other:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;public abstract class Entity&amp;lt;TId&amp;gt;
{
    public TId Id { get; protected set; }

    public override bool Equals(object? obj)
    {
        if (obj is not Entity&amp;lt;TId&amp;gt; other) return false;
        if (ReferenceEquals(this, other)) return true;
        if (GetType() != other.GetType()) return false;

&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;    if (EqualityComparer&amp;lt;TId&amp;gt;.Default.Equals(Id, default!) || 
        EqualityComparer&amp;lt;TId&amp;gt;.Default.Equals(other.Id, default!))
        return false;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;
        return EqualityComparer&amp;lt;TId&amp;gt;.Default.Equals(Id, other.Id);
    }

&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;public override int GetHashCode() =&amp;gt; (GetType().ToString() + Id).GetHashCode();
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;public static bool operator ==(Entity&amp;lt;TId&amp;gt;? a, Entity&amp;lt;TId&amp;gt;? b) =&amp;gt; a?.Equals(b) ?? b is null;
public static bool operator !=(Entity&amp;lt;TId&amp;gt;? a, Entity&amp;lt;TId&amp;gt;? b) =&amp;gt; !(a == b);
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;2-c-records-for-value-objects&quot;&gt;2. C# Records for Value Objects&lt;/h4&gt;
&lt;p&gt;Using &lt;code&gt;record&lt;/code&gt; gives us value-based structural equality out-of-the-box and immutability by default (&lt;code&gt;init&lt;/code&gt; setters).&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;public record EmailAddress
{
    public string Value { get; init; }

    public EmailAddress(string value)
    {
        if (string.IsNullOrWhiteSpace(value) || !value.Contains(&quot;@&quot;))
            throw new ArgumentException(&quot;Invalid email format.&quot;);
        Value = value;
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;part-3-combining-interfaces-abstract-classes-and-ddd-entities&quot;&gt;Part 3: Combining Interfaces, Abstract Classes, and DDD Entities&lt;/h3&gt;
&lt;p&gt;Now we structure our customer hierarchy by inheriting from our DDD &lt;code&gt;Entity&amp;lt;Guid&amp;gt;&lt;/code&gt;, implementing our &lt;code&gt;ICustomer&lt;/code&gt; interface, and using our &lt;code&gt;EmailAddress&lt;/code&gt; value object.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;// 1. The Interface defines the full contract
public interface ICustomer
{
    Guid Id { get; }
    EmailAddress Email { get; }
    decimal TotalSpent { get; }
    
    decimal CalculateDiscount(decimal orderAmount);
    void RecordPurchase(decimal amount);
}

// 2. The Abstract Base Class inherits from DDD Entity and implements ICustomer
public abstract class CustomerBase : Entity&amp;lt;Guid&amp;gt;, ICustomer
{
    public EmailAddress Email { get; private set; }
    public decimal TotalSpent { get; private set; }

    protected CustomerBase(Guid id, EmailAddress email)
    {
        Id = id;
        Email = email ?? throw new ArgumentNullException(nameof(email));
        TotalSpent = 0m;
    }

    public void RecordPurchase(decimal amount)
    {
        if (amount &amp;lt;= 0) throw new ArgumentException(&quot;Purchase amount must be positive.&quot;);
        TotalSpent += amount;
    }

    public abstract decimal CalculateDiscount(decimal orderAmount);
}

// 3. Concrete Implementations
public class RegularCustomer : CustomerBase
{
    public RegularCustomer(Guid id, EmailAddress email) : base(id, email) { }

    public override decimal CalculateDiscount(decimal orderAmount) =&amp;gt; 0m;
}

public class VipCustomer : CustomerBase
{
    public decimal CreditLimit { get; private set; }

    public VipCustomer(Guid id, EmailAddress email, decimal creditLimit) : base(id, email)
    {
        if (creditLimit &amp;lt; 0) throw new ArgumentException(&quot;Credit limit cannot be negative.&quot;);
        CreditLimit = creditLimit;
    }

    public override decimal CalculateDiscount(decimal orderAmount) =&amp;gt; orderAmount * 0.10m;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;part-4-the-rich-domain-model-order-&quot;&gt;Part 4: The Rich Domain Model (&lt;code&gt;Order&lt;/code&gt;)&lt;/h3&gt;
&lt;p&gt;Our &lt;code&gt;Order&lt;/code&gt; inherits from &lt;code&gt;Entity&amp;lt;Guid&amp;gt;&lt;/code&gt;, enforces business invariants, and coordinates with &lt;code&gt;ICustomer&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;public class Order : Entity&amp;lt;Guid&amp;gt;
{
    public ICustomer Customer { get; }
    public decimal TotalAmount { get; private set; }
    public OrderStatus Status { get; private set; }

    public Order(Guid id, ICustomer customer, decimal initialAmount)
    {
        Id = id;
        Customer = customer ?? throw new ArgumentNullException(nameof(customer));
        
        if (initialAmount &amp;lt;= 0)
            throw new ArgumentException(&quot;Order amount must be greater than zero.&quot;);

        TotalAmount = initialAmount;
        Status = OrderStatus.Draft;

        ApplyCustomerDiscount();
    }

    private void ApplyCustomerDiscount()
    {
        decimal discount = Customer.CalculateDiscount(TotalAmount);
        TotalAmount -= discount;
    }

    public void AddItem(decimal itemPrice)
    {
        if (Status != OrderStatus.Draft)
            throw new InvalidOperationException(&quot;Cannot modify an order after it has been submitted.&quot;);

        TotalAmount += itemPrice;
    }

    public void SubmitPayment()
    {
        if (Status != OrderStatus.Draft)
            throw new InvalidOperationException(&quot;Order is already processed.&quot;);

        Status = OrderStatus.Submitted;
    }
}

public enum OrderStatus { Draft, Submitted, Completed }

&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;part-5-orchestrating-with-srp-and-dependency-injection&quot;&gt;Part 5: Orchestrating with SRP and Dependency Injection&lt;/h3&gt;
&lt;p&gt;Our application service (&lt;code&gt;OrderProcessor&lt;/code&gt;) focuses purely on coordination (following the Single Responsibility Principle).&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;public class OrderProcessor
{
    private readonly IOrderRepository _repository;
    private readonly IPaymentGateway _paymentGateway;
    private readonly IEmailNotifier _notifier;
    private readonly ILogger&amp;lt;OrderProcessor&amp;gt; _logger;

    public OrderProcessor(
        IOrderRepository repository,
        IPaymentGateway paymentGateway,
        IEmailNotifier notifier,
        ILogger&amp;lt;OrderProcessor&amp;gt; logger)
    {
        _repository = repository;
        _paymentGateway = paymentGateway;
        _notifier = notifier;
        _logger = logger;
    }

    public async Task ProcessAsync(Guid orderId)
    {
        var order = await _repository.GetByIdAsync(orderId);
        if (order == null) throw new KeyNotFoundException(&quot;Order not found.&quot;);

        order.SubmitPayment();

        if (!await _paymentGateway.ChargeAsync(order.TotalAmount))
        {
            throw new InvalidOperationException(&quot;Payment failed.&quot;);
        }

        order.Customer.RecordPurchase(order.TotalAmount);

        await _repository.SaveAsync(order);
        await _notifier.SendConfirmationAsync(order.Customer.Email.Value);

        _logger.LogInformation(&quot;Successfully processed order {OrderId}&quot;, order.Id);
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;part-6-persistence-with-ef-core-mapping-records-amp-tph-&quot;&gt;Part 6: Persistence with EF Core (Mapping Records &amp;amp; TPH)&lt;/h3&gt;
&lt;p&gt;Using EF Core Entity configurations to map our DDD entities, value objects (records via owned types), and inheritance hierarchy.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;public class AppDbContext : DbContext
{
    public DbSet&amp;lt;CustomerBase&amp;gt; Customers =&amp;gt; Set&amp;lt;CustomerBase&amp;gt;();
    public DbSet&amp;lt;Order&amp;gt; Orders =&amp;gt; Set&amp;lt;Order&amp;gt;();

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        // Configure Customer Inheritance (TPH)
      
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  modelBuilder.Entity()&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;        .HasKey(c =&amp;gt; c.Id);
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;    modelBuilder.Entity&amp;lt;CustomerBase&amp;gt;()
        .OwnsOne(c =&amp;gt; c.Email, email =&amp;gt;
        {
            email.Property(e =&amp;gt; e.Value).HasColumnName(&quot;Email&quot;).IsRequired();
        });

    modelBuilder.Entity&amp;lt;CustomerBase&amp;gt;()
        .HasDiscriminator&amp;lt;string&amp;gt;(&quot;CustomerType&quot;)
        .HasValue&amp;lt;RegularCustomer&amp;gt;(&quot;Regular&quot;)
        .HasValue&amp;lt;VipCustomer&amp;gt;(&quot;VIP&quot;);

    // Configure Order Entity
    modelBuilder.Entity&amp;lt;Order&amp;gt;()
        .HasKey(o =&amp;gt; o.Id);

    modelBuilder.Entity&amp;lt;Order&amp;gt;()
        .HasOne(o =&amp;gt; o.Customer)
        .WithMany()
        .HasForeignKey(&quot;CustomerId&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;part-7-unit-testing-with-records-amp-entities&quot;&gt;Part 7: Unit Testing with Records &amp;amp; Entities&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;public class DomainTests
{
    [Fact]
    public void VipCustomer_WithValidData_CalculatesDiscountAndTracksSpend()
    {
        var email = new EmailAddress(&quot;*Emails are not allowed*&quot;);
        ICustomer customer = new VipCustomer(Guid.NewGuid(), email, 5000.00m);
        var order = new Order(Guid.NewGuid(), customer, 200.00m);

        // 200 - 10% = 180
        Assert.Equal(180.00m, order.TotalAmount);

        customer.RecordPurchase(order.TotalAmount);
        Assert.Equal(180.00m, customer.TotalSpent);
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;references-amp-further-reading&quot;&gt;References &amp;amp; Further Reading&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Martin Fowler (2003):&lt;/strong&gt; &lt;em&gt;AnemicDomainModel&lt;/em&gt; — &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.martinfowler.com/bliki/AnemicDomainModel.html&quot;&gt;Read article&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Eric Evans (2003):&lt;/strong&gt; &lt;em&gt;Domain-Driven Design: Tackling Complexity in the Heart of Software&lt;/em&gt; — Addison-Wesley Professional.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Robert C. Martin (2017):&lt;/strong&gt; &lt;em&gt;Clean Architecture: A Craftsman's Guide to Software Structure and Design&lt;/em&gt; — Prentice Hall.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32663/from-anemic-models-to-rich-domain-models-ddd-base-entities-and-c-records</guid>
<pubDate>Tue, 27 Oct 2026 10:51:17 +0000</pubDate>
</item>
<item>
<title>Tuesday Coding Tip 28 — (de)serializing JSON string enum in C#</title>
<link>https://coderlegion.com/29763/tuesday-coding-tip-28-de-serializing-json-string-enum-in-c</link>
<description>&lt;p&gt;Tuesday coding tips are super short posts about various tidbits mainly from C++, but also from other programming languages I use. You can also follow the #TuesdayCodingTips hashtag on Mastodon and Linkedin.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Last time I covered C# JSON serialization, I only showcased the &lt;code&gt;Newtonsoft.Json&lt;/code&gt; library. This time, I will also show the &lt;code&gt;System.Text.Json&lt;/code&gt; way.&lt;/p&gt;
&lt;p&gt;You might want to (de)serialize enums as strings, rather than numeric values. Since C# does not support enums with an underlying type of string, you need to do some extra work. Namely, you have to annotate each value using the &lt;code&gt;EnumMember&lt;/code&gt; annotation and then select the correct converter, once again using annotations.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;using System.Runtime.Serialization;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;

public enum Mood
{
    [EnumMember(Value = &quot;happy&quot;)]
    Happy,
    [EnumMember(Value = &quot;sad&quot;)]
    Sad,
}

public class Object
{
    [JsonProperty(&quot;mood&quot;)]

    [JsonConverter(typeof(StringEnumConverter))]
    public Mood Mood { get; set; }
}

public static class Program
{
    public static void Main()
    {
        string json = JsonConvert.SerializeObject(new Object
        {
            Mood = Mood.Sad
        });

        Console.WriteLine(json);
        // {&quot;mood&quot;:&quot;sad&quot;}
        Console.WriteLine(JsonConvert.DeserializeObject&amp;lt;Object&amp;gt;(json)!.Mood);
        // Sad
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;However, &lt;code&gt;System.Text.Json&lt;/code&gt; doesn’t support &lt;code&gt;EnumMember&lt;/code&gt; annotations out of the box. It takes the literal name of the property instead. You can play with capitalization using options object, but that’s it. Or you can write your own converter. Or you can just use one of the dozen converters from the Internet that do the hard work for you.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;using System.Runtime.Serialization;
using System.Text.Json.Serialization;

public enum Mood
{
    [EnumMember(Value = &quot;happy&quot;)]
    Happy,
    [EnumMember(Value = &quot;sad&quot;)]
    Sad,
}

public class Object
{
    [JsonPropertyName(&quot;mood&quot;)]
    // To use EnumMember annotations, install Macross.Json.Extensions nuget
    // and use a different converter
    // [JsonConverter(typeof(JsonStringEnumMemberConverter))]
    [JsonConverter(typeof(JsonStringEnumConverter))]
    public Mood Mood { get; set; }
}

public static class Program
{
    public static void Main()
    {
        Object obj = new()
        {
            Mood = Mood.Sad,
        };

        string json = System.Text.Json.JsonSerializer.Serialize(obj);
        Console.WriteLine(json);
        // {&quot;mood&quot;:&quot;Sad&quot;}
        Console.WriteLine(System.Text.Json.JsonSerializer.Deserialize&amp;lt;Object&amp;gt;(json)!.Mood);
        // Sad
    }
}
&lt;/code&gt;&lt;/pre&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/29763/tuesday-coding-tip-28-de-serializing-json-string-enum-in-c</guid>
<pubDate>Tue, 27 Oct 2026 09:59:00 +0000</pubDate>
</item>
<item>
<title>A Query Window Is Not a Calendar Day</title>
<link>https://coderlegion.com/32592/a-query-window-is-not-a-calendar-day</link>
<description>&lt;p&gt;Software often uses broad time windows for good reasons. A dashboard may need recent history, work that is actionable now, and the next scheduled item. Fetching all of that in one bounded query can be efficient and easy to reason about.&lt;/p&gt;
&lt;p&gt;The trouble begins when the retrieval window quietly becomes the business definition of “today.”&lt;/p&gt;
&lt;p&gt;This is a small distinction with a very visible failure mode: shortly after midnight, yesterday’s missed item appears under today’s heading. The query is returning exactly what it was asked to return. The selection rule is wrong.&lt;/p&gt;
&lt;h3 id=&quot;retrieval-and-meaning-are-different-jobs&quot;&gt;Retrieval and meaning are different jobs&lt;/h3&gt;
&lt;p&gt;Imagine a “Today” screen that loads items from several hours in the past through the next day. The look-back allows the screen to include work that recently closed or may still be open. The look-ahead allows it to show what comes next.&lt;/p&gt;
&lt;p&gt;That range is a retrieval concern. It answers:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Which records could be relevant enough to inspect?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;It does not answer:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Which record belongs to the user’s current calendar day?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;If selection simply chooses the first item whose state is due or missed, a previous day’s missed item can win after midnight. If display text assumes every future item is later today, tomorrow morning can be described as though it were later tonight.&lt;/p&gt;
&lt;p&gt;Both errors come from using temporal proximity as a substitute for calendar meaning.&lt;/p&gt;
&lt;h3 id=&quot;define-today-in-the-relevant-time-zone&quot;&gt;Define “today” in the relevant time zone&lt;/h3&gt;
&lt;p&gt;Server time is rarely the correct authority for a human-facing day. UTC is excellent for storing instants and comparing elapsed time. It is not a user’s calendar.&lt;/p&gt;
&lt;p&gt;The safer sequence is:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Read the current instant from an injected clock.&lt;/li&gt;
&lt;li&gt;Convert it once into the time zone that owns the workflow.&lt;/li&gt;
&lt;li&gt;Derive a local date from that value.&lt;/li&gt;
&lt;li&gt;Use that date when classifying calendar-bound states.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Computing the value once matters. It keeps every decision in one request anchored to the same instant, including the unlikely case where execution crosses midnight.&lt;/p&gt;
&lt;p&gt;The relevant time zone also needs to be explicit. It may belong to a person, site, account, or workflow. The correct choice is a domain decision, not an infrastructure default.&lt;/p&gt;
&lt;h3 id=&quot;preserve-the-exception-that-actually-matters&quot;&gt;Preserve the exception that actually matters&lt;/h3&gt;
&lt;p&gt;A tempting fix is to discard every record whose local date is not today. That is neat and wrong when an actionable window crosses midnight.&lt;/p&gt;
&lt;p&gt;Suppose a task opened before midnight and remains valid until shortly afterwards. A strict date filter would hide it at the exact moment it still needs attention.&lt;/p&gt;
&lt;p&gt;This is where explicit domain rules beat blanket filtering:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;missed and completed items count as current only when their local date is today;&lt;/li&gt;
&lt;li&gt;a still-open item remains current while it is actionable, regardless of its start date;&lt;/li&gt;
&lt;li&gt;a scheduled item on another day can still appear as next, but its wording names that day.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The rule is slightly longer because the product meaning is slightly richer. That extra branch is not accidental complexity. It records why two superficially similar records are treated differently.&lt;/p&gt;
&lt;h3 id=&quot;make-wording-part-of-correctness&quot;&gt;Make wording part of correctness&lt;/h3&gt;
&lt;p&gt;Time bugs are not confined to sorting and filtering. Labels can encode false claims too.&lt;/p&gt;
&lt;p&gt;“Later today” is only correct if the next item is on the same local day. “Between 08:00 and 09:00” may be ambiguous when viewed the night before. Adding “tomorrow” or a concrete day makes the model visible to the user.&lt;/p&gt;
&lt;p&gt;That wording also gives tests a useful public outcome. A test can verify that the screen says tomorrow without reaching into a private helper or asserting a particular LINQ expression.&lt;/p&gt;
&lt;h3 id=&quot;test-the-boundary-as-a-sequence&quot;&gt;Test the boundary as a sequence&lt;/h3&gt;
&lt;p&gt;A single test at noon will not find this class of defect. The useful test moves a controllable clock through the state transition.&lt;/p&gt;
&lt;p&gt;One focused integration scenario can prove three behaviours:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;On the item’s own evening, the missed item is still classified as belonging to today.&lt;/li&gt;
&lt;li&gt;The next day’s scheduled item is labelled explicitly as tomorrow.&lt;/li&gt;
&lt;li&gt;After midnight, the old missed item no longer becomes the current card.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Using the real persistence provider is valuable when stored dates, generated occurrences, or query translation participate in the behaviour. The clock is the external boundary worth controlling; internal services do not need to be mocked if the public result proves the rule.&lt;/p&gt;
&lt;p&gt;Also test a window that genuinely spans midnight. Without that case, a cleanup that filters everything by date can look correct while introducing a different regression.&lt;/p&gt;
&lt;h3 id=&quot;the-engineering-trade-off&quot;&gt;The engineering trade-off&lt;/h3&gt;
&lt;p&gt;Explicit local-date classification costs a few things:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;more branches than a simple state check;&lt;/li&gt;
&lt;li&gt;a time-zone conversion at the application boundary;&lt;/li&gt;
&lt;li&gt;boundary-focused tests and more deliberate copy.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In return, the system becomes predictable at the moment when implicit assumptions fail most often. Retrieval can remain broad for performance and completeness. Domain selection becomes precise. User-facing language reflects the same model.&lt;/p&gt;
&lt;p&gt;The broader leadership lesson is to ask two questions whenever a time range appears in code:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Is this range finding candidates, or defining business membership?&lt;/li&gt;
&lt;li&gt;Whose calendar gives words such as today, tomorrow, and missed their meaning?&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Keeping those answers separate prevents an implementation convenience from becoming an accidental policy.&lt;/p&gt;
&lt;h3 id=&quot;practical-checklist&quot;&gt;Practical checklist&lt;/h3&gt;
&lt;p&gt;When building a day-oriented view:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;store and compare instants consistently;&lt;/li&gt;
&lt;li&gt;identify the domain-owned time zone;&lt;/li&gt;
&lt;li&gt;derive the local date once per operation;&lt;/li&gt;
&lt;li&gt;keep retrieval windows separate from classification rules;&lt;/li&gt;
&lt;li&gt;preserve intentionally actionable cross-midnight windows;&lt;/li&gt;
&lt;li&gt;make off-day wording explicit;&lt;/li&gt;
&lt;li&gt;test before midnight, after midnight, and a spanning window.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The code may still query eighteen or thirty hours. The user should experience a calendar day.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32592/a-query-window-is-not-a-calendar-day</guid>
<pubDate>Mon, 26 Oct 2026 20:12:29 +0000</pubDate>
</item>
<item>
<title>Idempotency Should Resume Partial Work, Not Hide It</title>
<link>https://coderlegion.com/32426/idempotency-should-resume-partial-work-not-hide-it</link>
<description>&lt;p&gt;Idempotency is usually introduced with a simple promise: run the operation again and it will not create a duplicate.&lt;/p&gt;
&lt;p&gt;That promise is useful, but it can become dangerous when we apply it to an entire workflow instead of the specific durable step it protects.&lt;/p&gt;
&lt;p&gt;I recently reviewed a recurring integration job with two independent writes. First, it appended a status note to an external record. Second, it moved that record into a terminal state. The note contained a unique marker, so retries could see that the note had already been written.&lt;/p&gt;
&lt;p&gt;The marker worked. The workflow did not.&lt;/p&gt;
&lt;p&gt;When the state transition failed once, the note remained. On the next sweep, the job saw the marker and skipped the record completely. A local sign of success for step one had accidentally become a global completion flag for steps one and two.&lt;/p&gt;
&lt;p&gt;That is a subtle reliability bug because the automation appears idempotent while the real process is only half finished.&lt;/p&gt;
&lt;h3 id=&quot;a-checkpoint-is-not-the-whole-journey&quot;&gt;A checkpoint is not the whole journey&lt;/h3&gt;
&lt;p&gt;Consider a generalized background job:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Write a durable note with a unique operation marker.&lt;/li&gt;
&lt;li&gt;Move the remote record into a terminal workflow state.&lt;/li&gt;
&lt;li&gt;Repeat the scan for a bounded period so temporary failures can heal.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The marker proves exactly one fact: the note exists.&lt;/p&gt;
&lt;p&gt;It does not prove that the state move succeeded. It does not prove that a notification was delivered, a file was copied, or a later API call completed. Yet it is tempting to write an early return such as “marker found, therefore this item is done.”&lt;/p&gt;
&lt;p&gt;The safer model gives each durable step its own completion evidence. In this case, the marker suppresses only another note. Later sweeps still inspect the remote record and decide whether the second step is pending.&lt;/p&gt;
&lt;p&gt;This sounds like a small distinction. In production, it is the difference between duplicate prevention and recovery prevention.&lt;/p&gt;
&lt;h3 id=&quot;reconciliation-makes-partial-completion-explicit&quot;&gt;Reconciliation makes partial completion explicit&lt;/h3&gt;
&lt;p&gt;A resumable job expects to encounter intermediate states:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;no note and no terminal state;&lt;/li&gt;
&lt;li&gt;note present but transition still pending;&lt;/li&gt;
&lt;li&gt;note present and record already terminal;&lt;/li&gt;
&lt;li&gt;note present but a person changed the record afterwards;&lt;/li&gt;
&lt;li&gt;remote workflow metadata that no longer matches our assumptions;&lt;/li&gt;
&lt;li&gt;a temporary failure that should be tried again;&lt;/li&gt;
&lt;li&gt;a permanent incompatibility that needs attention.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Those are not exotic edge cases. They are the normal state space of a multi-step integration.&lt;/p&gt;
&lt;p&gt;The repaired design runs the state decision on every bounded reconciliation sweep. If the note is missing, it writes the note once. Whether the note was just written or already existed, it then evaluates the transition separately.&lt;/p&gt;
&lt;p&gt;That separation makes the control flow honest. We can retry the unfinished part without repeating the finished part.&lt;/p&gt;
&lt;h3 id=&quot;ask-the-external-system-what-its-workflow-means&quot;&gt;Ask the external system what its workflow means&lt;/h3&gt;
&lt;p&gt;The original failure was also a reminder that external state names are rarely universal. One workflow may call its terminal state “Done”; another may use a different label. Hard-coding one friendly name works until a customized process rejects it.&lt;/p&gt;
&lt;p&gt;The stronger approach reads the record's type and the available states for that type. It prefers an explicitly configured state when that state exists. Otherwise, it can select a state by semantic category, such as completed or removed.&lt;/p&gt;
&lt;p&gt;This adds metadata reads and some branching. It also moves compatibility knowledge to the boundary where it belongs. The integration stops pretending that its vocabulary is the external system's vocabulary.&lt;/p&gt;
&lt;p&gt;That fallback must remain bounded. If no suitable category exists, the job should surface the incompatibility rather than inventing a transition.&lt;/p&gt;
&lt;h3 id=&quot;human-intent-outranks-automation&quot;&gt;Human intent outranks automation&lt;/h3&gt;
&lt;p&gt;Retrying forever is not the same as reconciling safely.&lt;/p&gt;
&lt;p&gt;Suppose the automation wrote its note yesterday, then a person deliberately reopened or reclassified the record today. A later sweep should not force the record back into the automation's preferred state.&lt;/p&gt;
&lt;p&gt;The reconciliation rule therefore needs ordering evidence. If the external state changed after our durable note, leave it alone. If ordering is unknown, prefer the conservative choice and do not overwrite. If the record is already in any terminal category, there is nothing to repair.&lt;/p&gt;
&lt;p&gt;This is an important boundary: the job owns completion of its own intent, not the external workflow itself.&lt;/p&gt;
&lt;h3 id=&quot;retry-by-failure-class&quot;&gt;Retry by failure class&lt;/h3&gt;
&lt;p&gt;Not every failure deserves the same response.&lt;/p&gt;
&lt;p&gt;A timeout or temporary service error can be retried by the next sweep. A missing permission or incompatible workflow definition will not improve through repetition. That condition should be reported, with noise controlled, while unrelated records continue.&lt;/p&gt;
&lt;p&gt;This classification keeps the job useful during partial outages. One bad record does not starve the batch, and operators are not flooded with alerts for failures that the normal reconciliation loop can heal.&lt;/p&gt;
&lt;p&gt;Caching workflow metadata for the duration of one sweep can also limit API traffic without letting old assumptions live indefinitely.&lt;/p&gt;
&lt;h3 id=&quot;test-the-half-finished-states&quot;&gt;Test the half-finished states&lt;/h3&gt;
&lt;p&gt;Happy-path tests are not enough for resumable automation. The valuable tests begin after something durable has already happened.&lt;/p&gt;
&lt;p&gt;I would want focused coverage proving that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;an existing marker prevents a duplicate note but not a pending transition;&lt;/li&gt;
&lt;li&gt;a previously failed transition is attempted on a later sweep;&lt;/li&gt;
&lt;li&gt;different state names can resolve through semantic metadata;&lt;/li&gt;
&lt;li&gt;terminal records are left untouched;&lt;/li&gt;
&lt;li&gt;a newer human change is preserved;&lt;/li&gt;
&lt;li&gt;unknown ordering is handled conservatively;&lt;/li&gt;
&lt;li&gt;one record's failure does not block the others;&lt;/li&gt;
&lt;li&gt;temporary and permanent failures take different paths.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These tests describe the recovery contract more clearly than a mock verifying one ideal call sequence.&lt;/p&gt;
&lt;h3 id=&quot;the-practical-rule&quot;&gt;The practical rule&lt;/h3&gt;
&lt;p&gt;For every multi-step job, list the durable evidence produced by each step. Then ask what the next run should do when execution stops after any one of them.&lt;/p&gt;
&lt;p&gt;Idempotency should let a workflow resume safely. If an “already processed” check prevents the system from finishing later steps, it is not protecting reliability. It is hiding incomplete work.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32426/idempotency-should-resume-partial-work-not-hide-it</guid>
<pubDate>Sun, 25 Oct 2026 19:34:58 +0000</pubDate>
</item>
<item>
<title>An Empty Field Is Still Signed Data</title>
<link>https://coderlegion.com/32032/an-empty-field-is-still-signed-data</link>
<description>&lt;p&gt;An empty form field looks unimportant. In ordinary application logic, treating an empty value like a missing value is often reasonable. In signature verification, that assumption can reject a legitimate message—or, in a different protocol, verify a representation the sender never signed.&lt;/p&gt;
&lt;p&gt;The important lesson is simple: canonicalization is not generic data cleanup. It is part of the cryptographic protocol.&lt;/p&gt;
&lt;h3 id=&quot;the-tidy-abstraction-that-hid-a-real-difference&quot;&gt;The tidy abstraction that hid a real difference&lt;/h3&gt;
&lt;p&gt;Consider an integration with two flows:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The application builds an outbound form and signs it.&lt;/li&gt;
&lt;li&gt;A provider later sends an inbound form notification that the application verifies.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Both flows need URL encoding, ordered &lt;code&gt;key=value&lt;/code&gt; pairs, a digest, and a comparison. A shared helper therefore seems sensible.&lt;/p&gt;
&lt;p&gt;But suppose the protocol defines different empty-field rules. The outbound form omits empty values. The inbound notification includes every posted field, even an empty one represented as &lt;code&gt;description=&lt;/code&gt;. Reusing the outbound rule during verification silently removes part of the inbound canonical representation.&lt;/p&gt;
&lt;p&gt;Nothing is wrong with the network message. Nothing is wrong with the secret. The application simply hashes different bytes from the sender.&lt;/p&gt;
&lt;p&gt;That is the dangerous kind of bug: the code is deterministic, the crypto primitive behaves correctly, and the abstraction looks clean. The failure lives in the representation contract.&lt;/p&gt;
&lt;h3 id=&quot;empty-and-absent-are-different-inputs&quot;&gt;Empty and absent are different inputs&lt;/h3&gt;
&lt;p&gt;Application code tends to reason semantically:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;note=&lt;/code&gt; means there is no useful note.&lt;/li&gt;
&lt;li&gt;no &lt;code&gt;note&lt;/code&gt; field also means there is no useful note.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A signing algorithm reasons syntactically. It may see:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;reference=abc&amp;amp;note=&amp;amp;status=complete
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;as a different message from:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;reference=abc&amp;amp;status=complete
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The same applies to field order, repeated keys, character encoding, spaces, escaping, decimal formatting, and line endings. Two representations can produce the same business meaning while producing different signatures.&lt;/p&gt;
&lt;p&gt;That is why “normalize before verifying” is unsafe advice unless the protocol defines the normalization precisely. The verifier’s job is not to improve the input. Its job is to reconstruct exactly what the other side signed.&lt;/p&gt;
&lt;h3 id=&quot;share-mechanics-not-contracts&quot;&gt;Share mechanics, not contracts&lt;/h3&gt;
&lt;p&gt;The repair pattern I prefer keeps the truly common pieces together while making direction-specific rules visible.&lt;/p&gt;
&lt;p&gt;Shared mechanics may include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the exact form-encoding routine;&lt;/li&gt;
&lt;li&gt;digest calculation;&lt;/li&gt;
&lt;li&gt;secret or passphrase handling;&lt;/li&gt;
&lt;li&gt;fixed-time signature comparison;&lt;/li&gt;
&lt;li&gt;consistent fail-closed behaviour.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The canonicalization policies remain separate:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;outbound signing can omit empty fields when the outbound specification says so;&lt;/li&gt;
&lt;li&gt;inbound verification can retain empty fields when the inbound specification says so;&lt;/li&gt;
&lt;li&gt;both can exclude the signature field itself;&lt;/li&gt;
&lt;li&gt;both can preserve their required ordering.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This introduces a small amount of repetition. That is a healthy trade when the alternative is a boolean option buried inside a generic helper whose callers must remember invisible protocol rules. If a shared primitive is retained, give the public operations names and documentation that make the distinction impossible to miss.&lt;/p&gt;
&lt;h3 id=&quot;test-the-representation-not-your-implementation&quot;&gt;Test the representation, not your implementation&lt;/h3&gt;
&lt;p&gt;Signature tests often contain a subtle circularity: the test calls the production signing helper to create the expected signature, then asks the same helper to verify it. The test proves internal consistency, not compatibility with the protocol.&lt;/p&gt;
&lt;p&gt;A stronger test builds the canonical input independently. For the inbound case, start from a literal ordered field sequence containing several &lt;code&gt;key=&lt;/code&gt; entries. Calculate the expected digest with a small test-only oracle or a published fixture. Then submit the body to the real verification path.&lt;/p&gt;
&lt;p&gt;Useful negative cases include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;remove one empty field and expect rejection;&lt;/li&gt;
&lt;li&gt;change field order and expect rejection when order is significant;&lt;/li&gt;
&lt;li&gt;replace an empty field with an absent field;&lt;/li&gt;
&lt;li&gt;vary spaces and reserved characters;&lt;/li&gt;
&lt;li&gt;include a blank expected signature and fail closed;&lt;/li&gt;
&lt;li&gt;change only the signature field and confirm it is excluded from canonical input.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These tests describe the protocol boundary directly. They are harder for a refactor to accidentally “update” into agreeing with the bug.&lt;/p&gt;
&lt;h3 id=&quot;exercise-the-framework-pipeline-too&quot;&gt;Exercise the framework pipeline too&lt;/h3&gt;
&lt;p&gt;A pure canonicalization test is necessary, but it is not sufficient for a web endpoint.&lt;/p&gt;
&lt;p&gt;Framework model binding and form parsing can consume a request stream, decode characters, merge values, or present fields through an API with different ordering behaviour. Middleware can also affect buffering or body access. A verifier that works against a handcrafted dictionary may still fail when the real HTTP pipeline handles the request first.&lt;/p&gt;
&lt;p&gt;Add one focused integration test that:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;creates the literal encoded body;&lt;/li&gt;
&lt;li&gt;independently calculates the signature over that exact representation;&lt;/li&gt;
&lt;li&gt;sends an actual form POST through the application’s HTTP stack;&lt;/li&gt;
&lt;li&gt;verifies that the original body remains available where required;&lt;/li&gt;
&lt;li&gt;proves the request reaches the normal accepted outcome.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This is not an argument for turning every edge case into a slow end-to-end test. Keep the combinatorial matrix in fast unit tests. Use the HTTP-level case as a boundary witness that the framework and your canonicalization contract still agree.&lt;/p&gt;
&lt;h3 id=&quot;verification-is-necessary-not-complete-trust&quot;&gt;Verification is necessary, not complete trust&lt;/h3&gt;
&lt;p&gt;Correct signature verification answers a narrow question: does this message match the shared signing contract and secret?&lt;/p&gt;
&lt;p&gt;It does not automatically prove that the event is fresh, unique, authorized for the referenced resource, or safe to apply twice. Production handlers may still need replay protection, timestamp or nonce validation, amount and ownership checks, idempotency, audit evidence, and careful failure handling.&lt;/p&gt;
&lt;p&gt;Keeping that claim bounded matters. Strong security engineering is usually a chain of explicit checks, not one impressive cryptographic step.&lt;/p&gt;
&lt;h3 id=&quot;a-practical-review-checklist&quot;&gt;A practical review checklist&lt;/h3&gt;
&lt;p&gt;When reviewing a signed integration, ask:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;What exact bytes or characters are signed in each direction?&lt;/li&gt;
&lt;li&gt;Are empty and missing values distinct?&lt;/li&gt;
&lt;li&gt;Is field order defined and preserved?&lt;/li&gt;
&lt;li&gt;When does decoding or encoding happen?&lt;/li&gt;
&lt;li&gt;Are repeated fields possible?&lt;/li&gt;
&lt;li&gt;Does the comparison avoid timing leaks?&lt;/li&gt;
&lt;li&gt;Do invalid inputs fail closed?&lt;/li&gt;
&lt;li&gt;Is at least one expected signature calculated independently?&lt;/li&gt;
&lt;li&gt;Does a real HTTP test cover framework parsing?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The broader engineering principle is worth carrying elsewhere: an abstraction should share sameness, not erase difference. At a cryptographic boundary, the awkward representational details are often the contract itself.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32032/an-empty-field-is-still-signed-data</guid>
<pubDate>Thu, 22 Oct 2026 06:11:15 +0000</pubDate>
</item>
<item>
<title>Put the Concurrency Gate Before the Side Effect</title>
<link>https://coderlegion.com/31923/put-the-concurrency-gate-before-the-side-effect</link>
<description>&lt;p&gt;Many concurrency bugs begin with code that looks entirely reasonable:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Read the current row.&lt;/li&gt;
&lt;li&gt;Check that the operation is still allowed.&lt;/li&gt;
&lt;li&gt;Create something durable elsewhere.&lt;/li&gt;
&lt;li&gt;Save the original row as complete.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Run once, the workflow is correct. Run twice at almost the same time, and both callers can pass the check before either caller records completion. If step three is irreversible, the system can create duplicate work even though every individual condition was true when evaluated.&lt;/p&gt;
&lt;p&gt;The core lesson is simple: when a state transition guards a side effect, make the database choose the winner before the side effect begins.&lt;/p&gt;
&lt;h3 id=&quot;why-read-then-check-is-not-ownership&quot;&gt;Why read-then-check is not ownership&lt;/h3&gt;
&lt;p&gt;An application-level check answers, “What did I observe?” It does not answer, “Do I exclusively own this transition?”&lt;/p&gt;
&lt;p&gt;Imagine a parent record representing an open request. Accepting one option creates a downstream record and closes the parent. Two callers can follow this timeline:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Caller A reads the parent as open.&lt;/li&gt;
&lt;li&gt;Caller B reads the same parent as open.&lt;/li&gt;
&lt;li&gt;Caller A creates a downstream record.&lt;/li&gt;
&lt;li&gt;Caller B creates another downstream record.&lt;/li&gt;
&lt;li&gt;Both callers try to close the parent.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Optimistic concurrency on the final save may reject one update, but by then the rejected caller may already have created the duplicate side effect. Detecting a conflict after the irreversible action is too late.&lt;/p&gt;
&lt;h3 id=&quot;move-the-decision-forward&quot;&gt;Move the decision forward&lt;/h3&gt;
&lt;p&gt;A stronger shape puts a conditional write immediately before downstream creation. In generalized SQL, the intention is:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;UPDATE WorkItems
SET ClaimToken = @token, ClaimedAt = @now
WHERE Id = @id
  AND Status = 'Open'
  AND ClaimToken IS NULL;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The affected-row count becomes the decision:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;One affected row means this caller owns the transition.&lt;/li&gt;
&lt;li&gt;Zero affected rows means another caller won, or the operation is no longer valid.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That losing result is not exceptional. It is a normal concurrency outcome and should stop before any downstream creation call.&lt;/p&gt;
&lt;p&gt;This is stronger than reading again because the predicate and the update are evaluated atomically by the database. There is no application-level gap between “still open” and “now mine.”&lt;/p&gt;
&lt;h3 id=&quot;the-claim-is-a-state-not-a-boolean-trick&quot;&gt;The claim is a state, not a boolean trick&lt;/h3&gt;
&lt;p&gt;Once a claim exists, the domain has an in-progress state whether or not the schema calls it that. Treating it explicitly improves the design.&lt;/p&gt;
&lt;p&gt;The workflow needs to answer:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Can a user retry while a claim is fresh?&lt;/li&gt;
&lt;li&gt;Which failures can safely release the claim?&lt;/li&gt;
&lt;li&gt;When is a claim considered abandoned?&lt;/li&gt;
&lt;li&gt;Can another caller take over an abandoned claim?&lt;/li&gt;
&lt;li&gt;What happens if the process fails after downstream creation?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A practical rule is to release ownership only when the system knows that no durable side effect exists. If downstream creation fails before producing a record, clearing the claim can make a retry safe. Once a durable result may exist, blindly clearing the claim risks creating another one.&lt;/p&gt;
&lt;h3 id=&quot;a-timeout-is-recovery-policy-not-exactly-once&quot;&gt;A timeout is recovery policy, not exactly once&lt;/h3&gt;
&lt;p&gt;A bounded stale-claim timeout prevents an interrupted process from blocking the workflow forever. A new caller may take over only if the stored claim and timestamp still match what it read, so competing recovery attempts cannot both win.&lt;/p&gt;
&lt;p&gt;That solves abandoned ownership before the side effect. It does not solve every crash window.&lt;/p&gt;
&lt;p&gt;Suppose the downstream service successfully creates a record and the process dies before the parent records completion. After the timeout, replaying the operation could create a second record. The conditional claim narrowed the risk, but it did not create a distributed transaction.&lt;/p&gt;
&lt;p&gt;For that boundary, choose an additional mechanism according to the cost of duplication:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Pass a stable idempotency key to the downstream operation.&lt;/li&gt;
&lt;li&gt;Record an outbox message in the same transaction as the claim.&lt;/li&gt;
&lt;li&gt;Make downstream creation upsert by the originating command identity.&lt;/li&gt;
&lt;li&gt;Reconcile incomplete claims against downstream records before replay.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The honest claim is “one winner during concurrent admission,” not “exactly once under every failure.”&lt;/p&gt;
&lt;h3 id=&quot;test-the-overlap-not-two-sequential-calls&quot;&gt;Test the overlap, not two sequential calls&lt;/h3&gt;
&lt;p&gt;A concurrency fix deserves a test that opens the original race window deliberately.&lt;/p&gt;
&lt;p&gt;Hold the first caller inside downstream creation. While it is paused, submit the same command again and also submit a competing choice. Then release the first caller.&lt;/p&gt;
&lt;p&gt;The useful assertions are concrete:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The downstream creation service was called once.&lt;/li&gt;
&lt;li&gt;One winner completed.&lt;/li&gt;
&lt;li&gt;Both competing calls received a clear losing result.&lt;/li&gt;
&lt;li&gt;The parent stored one selected result.&lt;/li&gt;
&lt;li&gt;Other candidates moved to their expected terminal state.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Add two recovery tests as well. First, make downstream creation fail before producing anything and prove that a retry can acquire the released claim. Second, seed a fresh claim and an expired claim, proving that the fresh claim blocks while the expired one can be reclaimed.&lt;/p&gt;
&lt;p&gt;This test shape is more valuable than sending two tasks and hoping their timing overlaps. A controlled pause makes the race deterministic and keeps the assertion focused on the contract.&lt;/p&gt;
&lt;h3 id=&quot;a-compact-design-checklist&quot;&gt;A compact design checklist&lt;/h3&gt;
&lt;p&gt;Before shipping a command that crosses a data boundary:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Identify the first irreversible side effect.&lt;/li&gt;
&lt;li&gt;Put the ownership decision before it.&lt;/li&gt;
&lt;li&gt;Use one conditional database write, not a read-then-write promise.&lt;/li&gt;
&lt;li&gt;Treat zero affected rows as a normal loser.&lt;/li&gt;
&lt;li&gt;Release only when no durable effect exists.&lt;/li&gt;
&lt;li&gt;Define stale ownership and takeover semantics.&lt;/li&gt;
&lt;li&gt;Add idempotency or reconciliation for the post-side-effect crash window.&lt;/li&gt;
&lt;li&gt;Test with a deliberately paused winner and active losers.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The broader principle is not limited to any framework. Checks describe observations. Conditional writes grant ownership. When a workflow can create something you cannot casually undo, that distinction is part of correctness.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/31923/put-the-concurrency-gate-before-the-side-effect</guid>
<pubDate>Wed, 21 Oct 2026 05:40:24 +0000</pubDate>
</item>
<item>
<title>Test the Owner, Not the Teardown</title>
<link>https://coderlegion.com/31880/test-the-owner-not-the-teardown</link>
<description>&lt;p&gt;Flaky lifecycle tests often produce a tempting diagnosis: the cleanup code must be unreliable. Sometimes that is true. Sometimes the test is asking a framework harness to prove a narrower ownership contract, and the harness is free to finish work on a different schedule.&lt;/p&gt;
&lt;p&gt;That distinction matters in interactive server-side UI systems. A component may subscribe to live updates, start a timer, register an observer, or attach a callback. The resource needs a clear owner and a reliable release path. But the test for that contract should not depend entirely on whether a whole rendered tree has completed teardown at one particular instant.&lt;/p&gt;
&lt;h3 id=&quot;the-shape-of-the-failure&quot;&gt;The shape of the failure&lt;/h3&gt;
&lt;p&gt;Consider a header component that subscribes to a process-local notification service. A child pop-up also owns a separate subscription. The component test renders the tree, publishes an update, observes the visual change, disposes the test context, and immediately expects the global subscription count to be zero.&lt;/p&gt;
&lt;p&gt;That sounds reasonable. It is also broader than the claim we really want to prove.&lt;/p&gt;
&lt;p&gt;Under a full test-suite load, the notification can overlap a render. The harness teardown call may return while the renderer still has a queued batch. At that instant the header might not yet have received its disposal callback. The non-zero count is therefore ambiguous: either the header leaked, or the harness has not disposed it yet.&lt;/p&gt;
&lt;p&gt;A test with two possible explanations is a weak regression guard.&lt;/p&gt;
&lt;h3 id=&quot;waiting-is-not-the-same-as-proving&quot;&gt;Waiting is not the same as proving&lt;/h3&gt;
&lt;p&gt;The first instinct is usually to add a retry window: wait for the subscription count to reach zero, perhaps for a few seconds.&lt;/p&gt;
&lt;p&gt;That is appropriate when the product contract is genuinely eventual. It is not a universal cure for lifecycle tests. If the harness has left the component alive, waiting merely spends more time observing the wrong boundary. The test remains dependent on incidental scheduling and can still fail without a cleanup defect.&lt;/p&gt;
&lt;p&gt;The key question is not, “How long should we wait?” It is, “Which object owns the resource, and what exact behaviour are we trying to prove?”&lt;/p&gt;
&lt;h3 id=&quot;put-the-assertion-at-the-ownership-boundary&quot;&gt;Put the assertion at the ownership boundary&lt;/h3&gt;
&lt;p&gt;The stronger test calls the owning component's disposal contract directly, on the correct renderer context, and checks only the resource that component owns.&lt;/p&gt;
&lt;p&gt;If the header owns one subscription and a still-rendered child owns another, disposing the header should reduce the count by one. Expecting zero would incorrectly make the header test responsible for the child's lifetime as well.&lt;/p&gt;
&lt;p&gt;That narrower assertion is not weaker. It is more precise:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the owner releases its resource;&lt;/li&gt;
&lt;li&gt;an independently owned resource is not silently claimed by the parent test;&lt;/li&gt;
&lt;li&gt;the result does not depend on whole-tree teardown timing; and&lt;/li&gt;
&lt;li&gt;removing the owner's cleanup call makes the test fail consistently.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That last point is especially valuable. A small mutation check—temporarily disabling the cleanup call—shows whether the test detects the regression it claims to detect. If the test still passes, it is probably measuring something else.&lt;/p&gt;
&lt;h3 id=&quot;production-code-still-needs-lifecycle-discipline&quot;&gt;Production code still needs lifecycle discipline&lt;/h3&gt;
&lt;p&gt;A focused test does not excuse fragile implementation. Cleanup should normally be idempotent because framework callbacks, navigation, error handling, and explicit shutdown can converge on the same release path.&lt;/p&gt;
&lt;p&gt;The object should also refuse to acquire new resources after disposal. An asynchronous load may resume after the component has gone away. If its continuation subscribes again, a perfectly correct earlier disposal becomes irrelevant because nothing will release the new subscription.&lt;/p&gt;
&lt;p&gt;The same reasoning applies beyond subscriptions. Failed in-flight loads should close or abandon temporary buffers. Retained arrivals should have a sensible bound. Work dispatched to another recipient should not accidentally carry ambient request state from the publisher. These are all lifecycle questions: who owns the state, when does ownership end, and what context is allowed to cross the boundary?&lt;/p&gt;
&lt;h3 id=&quot;keep-two-layers-of-evidence&quot;&gt;Keep two layers of evidence&lt;/h3&gt;
&lt;p&gt;Direct ownership tests and integration tests answer different questions.&lt;/p&gt;
&lt;p&gt;An owner-level test asks: when this component is disposed, does it release what it owns, and can it remain released?&lt;/p&gt;
&lt;p&gt;An integration test asks: does navigation, circuit shutdown, or framework teardown actually invoke the right lifecycle path across the rendered tree?&lt;/p&gt;
&lt;p&gt;Both matter. The mistake is using only the second, broad test as proof of the first, narrow contract. When the broad test fails, you cannot tell whether ownership or orchestration is broken.&lt;/p&gt;
&lt;p&gt;Separating the layers improves diagnosis. A direct test failure points toward the owner. An integration-only failure points toward wiring, scheduling, or framework interaction. The test suite becomes a map of contracts instead of a collection of timing guesses.&lt;/p&gt;
&lt;h3 id=&quot;a-practical-checklist&quot;&gt;A practical checklist&lt;/h3&gt;
&lt;p&gt;For each subscription, timer, observer, or callback:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Name the owner explicitly.&lt;/li&gt;
&lt;li&gt;Make release safe to call more than once.&lt;/li&gt;
&lt;li&gt;Prevent acquisition after the owner is disposed.&lt;/li&gt;
&lt;li&gt;Test the owner's release contract directly.&lt;/li&gt;
&lt;li&gt;Give child-owned resources separate coverage.&lt;/li&gt;
&lt;li&gt;Keep one integration test for the framework path.&lt;/li&gt;
&lt;li&gt;Prove the test's sensitivity by breaking cleanup locally and seeing it fail.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The trade-off is a few more focused tests and slightly more explicit lifecycle code. In return, failures become actionable. You know whether the owner leaked, the child still legitimately owns something, or the framework has not completed teardown.&lt;/p&gt;
&lt;p&gt;Reliable tests do not merely wait longer. They make ownership visible.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/31880/test-the-owner-not-the-teardown</guid>
<pubDate>Tue, 20 Oct 2026 17:41:25 +0000</pubDate>
</item>
<item>
<title>Soft Delete Is a Model Contract, Not a Repository Habit</title>
<link>https://coderlegion.com/31493/soft-delete-is-a-model-contract-not-a-repository-habit</link>
<description>&lt;p&gt;Soft deletion often begins innocently: add an IsDeleted flag, remember to exclude retained rows, and move on. That works until the application grows into several modules, several DbContext types, and configuration spread across assemblies.&lt;/p&gt;
&lt;p&gt;At that point, soft deletion is no longer a local query concern. It is a visibility boundary for the complete EF Core model.&lt;/p&gt;
&lt;p&gt;The difference is practical. A repository predicate protects the query paths that remember to use it. A model contract protects ordinary reads by default and makes exceptional reads visible in code review.&lt;/p&gt;
&lt;h3 id=&quot;the-dangerous-partial-success&quot;&gt;The dangerous partial success&lt;/h3&gt;
&lt;p&gt;Imagine a shared base context that scans the entity types it knows and installs a query filter. Later, a module applies another configuration assembly and adds more entities. The base code is correct for the model it saw, but its timing was wrong. Some retained rows are hidden and others are returned normally.&lt;/p&gt;
&lt;p&gt;This failure is uncomfortable because both sides look reasonable in isolation. The entity has the deletion marker. The shared context contains filter code. A few tests pass. Yet the effective model has uneven visibility rules.&lt;/p&gt;
&lt;p&gt;The lesson is to configure cross-cutting rules at a lifecycle point where the model is complete. An EF Core model-finalising convention can inspect every registered type after the contexts and configuration assemblies have contributed their mappings.&lt;/p&gt;
&lt;p&gt;For each root entity carrying the deletion marker, the convention can add the standard predicate. Owned types follow their owner, and derived types inherit the root filter. Existing filters must be preserved: combine with an anonymous filter, or add a separate named filter when the model uses named filters. Any always-visible audit type should be an explicit, narrow exception rather than an accidental omission.&lt;/p&gt;
&lt;h3 id=&quot;test-the-effective-predicate&quot;&gt;Test the effective predicate&lt;/h3&gt;
&lt;p&gt;Checking that a filter exists is too weak.&lt;/p&gt;
&lt;p&gt;An entity might have an unrelated visibility filter. It might even mention the deletion marker beneath a logical OR:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;not deleted OR visible to this user
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That expression can still admit a retained row. The required property is stronger: not deleted must be a mandatory conjunct of the effective filter.&lt;/p&gt;
&lt;p&gt;A reusable model audit can enumerate all root entity types with the marker, exclude only documented exceptions, and fail if any type lacks that mandatory predicate. It should have tests against itself: prove that it rejects an unrelated filter and rejects the OR case. Otherwise the audit risks becoming a green check that does not guard the boundary it names.&lt;/p&gt;
&lt;p&gt;Provider-backed tests add the next layer of evidence. Insert one active row and one retained row. Show that normal queries return only the active row, while an explicit bypass can retrieve both. This verifies the generated query and provider behaviour rather than only evaluating a compiled expression in memory.&lt;/p&gt;
&lt;h3 id=&quot;a-correct-filter-changes-workflows&quot;&gt;A correct filter changes workflows&lt;/h3&gt;
&lt;p&gt;When global filtering becomes consistent, previously accidental behaviour may break. That is useful information.&lt;/p&gt;
&lt;p&gt;Restoration is the clearest example. Suppose a retained row still owns a unique business slot. A normal lookup cannot see it, so a naive create path tries to insert a replacement and collides with the database constraint.&lt;/p&gt;
&lt;p&gt;The solution is not to weaken the global filter. Give the restoration path an explicit way to ignore model filters, then make its intention precise:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;creating into a slot held by a retained row restores that same row;&lt;/li&gt;
&lt;li&gt;editing a different active row into the reserved slot is refused;&lt;/li&gt;
&lt;li&gt;failure leaves both rows unchanged;&lt;/li&gt;
&lt;li&gt;historical or restoration reads are the only paths allowed to bypass the boundary.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Putting the bypass on a named specification or dedicated query object is easier to review than scattering IgnoreQueryFilters() through services. The name communicates why the exception exists, and focused tests can pin its scope.&lt;/p&gt;
&lt;h3 id=&quot;watch-required-navigations&quot;&gt;Watch required navigations&lt;/h3&gt;
&lt;p&gt;Global filters also interact with relationship shape. If a required navigation uses an inner join and the principal is filtered out, the dependent can disappear from a query even when the dependent itself is active.&lt;/p&gt;
&lt;p&gt;That may be correct, but it should be a decision.&lt;/p&gt;
&lt;p&gt;An inventory of required relationships that cross filtered and unfiltered types gives reviewers a bounded list of hazards. For each one, choose deliberately: align filters on both sides, make the navigation optional, change the query shape, or accept and test the disappearance.&lt;/p&gt;
&lt;p&gt;This is one reason metadata-driven verification scales better than a long list of hand-written repository tests. The model can tell you what exists now, including a new entity or relationship added by a future module.&lt;/p&gt;
&lt;h3 id=&quot;a-practical-verification-stack&quot;&gt;A practical verification stack&lt;/h3&gt;
&lt;p&gt;A durable soft-delete boundary has four layers:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Apply the convention after the model is complete.&lt;/li&gt;
&lt;li&gt;Audit every soft-deletable root for the exact mandatory predicate.&lt;/li&gt;
&lt;li&gt;Keep bypasses explicit and limited to restoration or historical use.&lt;/li&gt;
&lt;li&gt;Run real-provider tests for visibility, restoration, uniqueness, and required-navigation behaviour.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;There is a cost: more model infrastructure, a deliberate exception list, and tests that can expose old assumptions. Some teams may decide that hard deletion or temporal tables better match particular data. The point is not that soft deletion is always right.&lt;/p&gt;
&lt;p&gt;The point is that, once chosen, it must behave like one coherent contract.&lt;/p&gt;
&lt;p&gt;The most valuable outcome is not the filter itself. It is that a future entity cannot quietly arrive outside the boundary—and that exceptional access reads like an exception.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/31493/soft-delete-is-a-model-contract-not-a-repository-habit</guid>
<pubDate>Sat, 17 Oct 2026 05:45:59 +0000</pubDate>
</item>
<item>
<title>A Passing Test Can Still Prove Nothing</title>
<link>https://coderlegion.com/31018/a-passing-test-can-still-prove-nothing</link>
<description>&lt;p&gt;Some of the most misleading tests are not flaky, slow, or obviously incomplete. They are fast, deterministic, and green.&lt;/p&gt;
&lt;p&gt;They also pass for the wrong reason.&lt;/p&gt;
&lt;p&gt;That is especially dangerous in fault-tolerant code. A test may claim that a failed dependency is contained, a fallback runs, or an established workflow continues. Yet its assertion may observe an outcome that can occur both before and after the recovery boundary. The suite reports confidence while the behaviour named by the test remains unproved.&lt;/p&gt;
&lt;h3 id=&quot;the-false-positive-shape&quot;&gt;The false-positive shape&lt;/h3&gt;
&lt;p&gt;Imagine a dispatcher with an optional policy source in front of an established workflow. The intended behaviour is straightforward:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Ask the policy source whether the old workflow should stand down.&lt;/li&gt;
&lt;li&gt;If that optional source fails, contain the fault.&lt;/li&gt;
&lt;li&gt;Continue into the established downstream path.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The unit test configures a policy source that throws. It also uses a strict downstream test double that throws if reached, because the rest of that workflow is outside the test's concern.&lt;/p&gt;
&lt;p&gt;Then it asserts only that dispatch throws an exception.&lt;/p&gt;
&lt;p&gt;At first glance, this can feel reasonable. An exception from the strict downstream dependency appears to prove that execution continued. But the same broad assertion also accepts the earlier exception from the policy source. If the recovery logic is deleted, the first exception simply escapes and the test remains green.&lt;/p&gt;
&lt;p&gt;The assertion recognizes the shape of the final outcome, not the path that produced it.&lt;/p&gt;
&lt;h3 id=&quot;test-the-claim-not-the-punctuation&quot;&gt;Test the claim, not the punctuation&lt;/h3&gt;
&lt;p&gt;The claim is not “an exception occurs.” The claim is “a fault at this boundary does not stop progress into the established path.”&lt;/p&gt;
&lt;p&gt;That distinction changes what the test must observe.&lt;/p&gt;
&lt;p&gt;A useful test needs a stable witness on the far side of the boundary. Depending on the design, that witness could be:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;a recorded call to the downstream collaborator;&lt;/li&gt;
&lt;li&gt;a domain result that only the fallback path can produce;&lt;/li&gt;
&lt;li&gt;a distinct exception identity from a deliberately strict dependency;&lt;/li&gt;
&lt;li&gt;a state transition committed after the failed operation; or&lt;/li&gt;
&lt;li&gt;an emitted event that occurs only after recovery has completed.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The specific technique matters less than the causal guarantee. The witness must be impossible—or at least deliberately difficult—to observe if execution stops at the failure being contained.&lt;/p&gt;
&lt;p&gt;In the example above, the corrected test distinguishes the downstream failure from the policy-source failure. It therefore passes only when execution crosses the recovery boundary. A recording test double would also work and may be clearer when the downstream call itself is the contract.&lt;/p&gt;
&lt;h3 id=&quot;why-mutation-testing-helped&quot;&gt;Why mutation testing helped&lt;/h3&gt;
&lt;p&gt;Code coverage could not expose this weakness. Both the recovery code and the test might execute during the normal run. Coverage tells us which lines ran; it does not tell us whether an assertion depended on their behaviour.&lt;/p&gt;
&lt;p&gt;Mutation testing asks a sharper question: if a meaningful piece of behaviour is changed or removed, does a test fail?&lt;/p&gt;
&lt;p&gt;Removing the recovery behaviour created a surviving mutant. That was valuable evidence. It did not necessarily mean the production design was wrong. It meant the suite could not distinguish the intended implementation from a broken one.&lt;/p&gt;
&lt;p&gt;This is one of mutation testing's best uses: finding assertions that observe something adjacent to the claim rather than the claim itself.&lt;/p&gt;
&lt;p&gt;You do not need mutation tooling to apply the same reasoning in review. Mentally invert the behaviour:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;If the catch disappeared, would the test fail?&lt;/li&gt;
&lt;li&gt;If the fallback were skipped, would the test fail?&lt;/li&gt;
&lt;li&gt;If execution stopped one step early, would the test fail?&lt;/li&gt;
&lt;li&gt;Could a different branch produce the same asserted result?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If the answer is uncomfortable, look for a witness beyond the boundary.&lt;/p&gt;
&lt;h3 id=&quot;the-trade-off-path-awareness-without-brittleness&quot;&gt;The trade-off: path awareness without brittleness&lt;/h3&gt;
&lt;p&gt;Path-sensitive assertions carry a real cost. A test that verifies every internal call order can become coupled to implementation detail and obstruct harmless refactoring. Replacing one collaborator with another should not require rewriting an entire suite if the observable contract is unchanged.&lt;/p&gt;
&lt;p&gt;The answer is not to avoid path awareness altogether. It is to choose the smallest stable witness that represents the behaviour promised by the test.&lt;/p&gt;
&lt;p&gt;Prefer domain-visible evidence when it exists: a returned decision, a committed state, a durable message, or a recorded recovery status. Use collaborator verification when crossing that collaborator boundary is itself the contract. Use exception type or identity when a strict test double intentionally marks progress, but make that intent explicit so a future reader understands why the exception matters.&lt;/p&gt;
&lt;p&gt;This produces a useful hierarchy:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Assert the externally meaningful result when it uniquely proves recovery.&lt;/li&gt;
&lt;li&gt;Assert a durable checkpoint when the final result is produced asynchronously.&lt;/li&gt;
&lt;li&gt;Assert a boundary interaction when progress across that boundary is the behaviour under test.&lt;/li&gt;
&lt;li&gt;Use a distinct failure identity as a narrow witness when a strict dependency deliberately terminates the test.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The closer the witness is to the real reliability contract, the less brittle the test will be.&lt;/p&gt;
&lt;h3 id=&quot;a-practical-review-habit&quot;&gt;A practical review habit&lt;/h3&gt;
&lt;p&gt;When reading a test name, underline the verb: continues, retries, falls back, persists, publishes, or rejects. Then find the assertion that proves that verb happened.&lt;/p&gt;
&lt;p&gt;If you can only find evidence that something threw, returned, or logged, ask whether the same evidence could appear before the named behaviour occurred.&lt;/p&gt;
&lt;p&gt;Green is not the standard. Discrimination is. A useful test must distinguish the system we intended from a nearby broken version.&lt;/p&gt;
&lt;p&gt;For fault-tolerance tests in particular, do not merely observe the aftermath of a failure. Prove that the system crossed the boundary it promised to survive.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/31018/a-passing-test-can-still-prove-nothing</guid>
<pubDate>Wed, 14 Oct 2026 06:12:33 +0000</pubDate>
</item>
<item>
<title>Tuesday Coding Tip 26 — JSON (de)serialization in C#</title>
<link>https://coderlegion.com/29761/tuesday-coding-tip-26-json-de-serialization-in-c</link>
<description>&lt;p&gt;Tuesday coding tips are super short posts about various tidbits mainly from C++, but also from other programming languages I use. You can also follow the #TuesdayCodingTips hashtag on Mastodon and Linkedin.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;In need of a JSON (de)serializer in C#? One option could be to use the built-in &lt;code&gt;System.Text.Json&lt;/code&gt; functionality. Probably the most popular way is to use the &lt;code&gt;Newtonsoft.Json&lt;/code&gt; library which has a richer feature set while maintaining overall simplicity.&lt;/p&gt;
&lt;p&gt;Things are as simple as having a class with a bunch of public attributes and calling serialize on it. Those attributes don’t even have to be trivial types — anything from &lt;code&gt;DateTime&lt;/code&gt; to &lt;code&gt;HashSet&lt;/code&gt; can be easily handled.&lt;/p&gt;
&lt;p&gt;Deserialization is similarly easy — if a key is present, it will be deserialized. If not, the default value for the attribute will be picked instead.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;using Newtonsoft.Json;

namespace NetSandbox;

public class ExtendedValues
{
    public long ErrorCode { get; set; }

    public string Message { get; set; } = string.Empty;
};

public class Property
{
    public string Value { get; set; } = &quot;&quot;;
};

public class Protocol
{
    public long Version { get; set; } = -1;

    [JsonProperty(&quot;extended&quot;, NullValueHandling = NullValueHandling.Ignore)]
    public ExtendedValues? ExtendedValues { get; set; } = default;


    [JsonProperty(&quot;machineGuid&quot;)]
    public Guid MachineGuid { get; set; }

    public List&amp;lt;Property&amp;gt; Properties { get; set; } = new List&amp;lt;Property&amp;gt;();
};

public static class Program
{
    public static void Main()
    {
        Protocol schema = new()
        {
            Version = 1,
            MachineGuid = Guid.NewGuid(),
            Properties = new List&amp;lt;Property&amp;gt; {
                new Property {
                    Value = &quot;hello&quot;
                },
                new Property {
                    Value = &quot;world&quot;
                }
            }
        };

        var json = JsonConvert.SerializeObject(schema);
        // {&quot;Version&quot;:1,&quot;machineGuid&quot;:&quot;156717fd-0904-47af-bcb8-ad74d23b9cc7&quot;,&quot;Properties&quot;:[{&quot;Value&quot;:&quot;hello&quot;},{&quot;Value&quot;:&quot;world&quot;}]}

        var obj = JsonConvert.DeserializeObject&amp;lt;Protocol&amp;gt;(
            &quot;&quot;&quot;{&quot;machineGuid&quot;: &quot;156717fd-0904-47af-bcb8-ad74d23b9cc7&quot;}&quot;&quot;&quot;);
        // obj.Version contains -1
        // obj.MachineGuid containts &quot;156717fd-0904-47af-bcb8-ad74d23b9cc7&quot;
        // obj.ExtendedValues is null
        // obj.Properties is empty list
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you need to, you can use annotations to for example customize the name of the serialized property or whether it will be serialized when it is null.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Extra thought:&lt;/strong&gt; I originally wrote this post in late 2023. Since then, many people would probably advice you &lt;code&gt;System.Text.Json&lt;/code&gt; on the merit of being faster and getting more and more on-part with Newtonsoft on each major release. Since we did a switch to it some time ago, I cannot wholeheartedly recommend it - it is great for simple use cases, but once you have some data structure that evolved over time and you need to be lenient of what you accept over wire and strict in what you send, working with &lt;code&gt;System.Text.Json&lt;/code&gt; is a bigger pain, especially around &lt;code&gt;NullValueHandling&lt;/code&gt; and &lt;code&gt;DefaultValueHandling&lt;/code&gt;.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/29761/tuesday-coding-tip-26-json-de-serialization-in-c</guid>
<pubDate>Tue, 13 Oct 2026 09:59:00 +0000</pubDate>
</item>
<item>
<title>The Async Result Belongs to the Snapshot That Started It</title>
<link>https://coderlegion.com/30557/the-async-result-belongs-to-the-snapshot-that-started-it</link>
<description>&lt;p&gt;Modern client components are long-lived, reactive, and full of asynchronous work. That combination creates a subtle class of defect: a response can be factually correct for the request that produced it, yet wrong for the screen that exists when it arrives.&lt;/p&gt;
&lt;p&gt;This is especially important when a component mixes route parameters with identity-scoped state. The user can navigate while a request is in flight. Authentication can resolve, refresh, or change. The component instance may survive both events. Unless the component tracks which context owns each result, an older task can update a newer view.&lt;/p&gt;
&lt;p&gt;The core rule is simple:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;An awaited result belongs to the input snapshot that started it.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That snapshot is more than a component reference. It can include the current route value, the current authentication-state operation, and a monotonically increasing generation number.&lt;/p&gt;
&lt;h3 id=&quot;how-the-race-forms&quot;&gt;How the race forms&lt;/h3&gt;
&lt;p&gt;Imagine a detail page that loads an item, then asks whether the current viewer has reacted to it. The page shows a highlighted action when the answer is yes.&lt;/p&gt;
&lt;p&gt;The component begins loading item A for identity A. Before the viewer-specific request returns, the user signs out and another identity becomes current—or the same component navigates to item B. The old request then completes with a truthful answer: identity A did react to item A.&lt;/p&gt;
&lt;p&gt;If the continuation writes directly to a shared &lt;code&gt;isActive&lt;/code&gt; field, that truth is now displayed in the wrong context. Item B may appear active, or identity B may see state derived from identity A.&lt;/p&gt;
&lt;p&gt;This is not merely a cosmetic race. Identity-derived state can influence which actions are enabled, what ownership badges appear, and which command a user can submit next.&lt;/p&gt;
&lt;h3 id=&quot;make-ownership-explicit&quot;&gt;Make ownership explicit&lt;/h3&gt;
&lt;p&gt;A practical pattern is to treat every meaningful input change as a new generation.&lt;/p&gt;
&lt;p&gt;When a route or authentication input changes:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Increment the generation.&lt;/li&gt;
&lt;li&gt;Capture the route and authentication input in local variables.&lt;/li&gt;
&lt;li&gt;Clear state derived from the previous identity.&lt;/li&gt;
&lt;li&gt;Disable identity-scoped actions while the new identity is unresolved.&lt;/li&gt;
&lt;li&gt;After each &lt;code&gt;await&lt;/code&gt;, confirm that the generation and captured inputs are still current.&lt;/li&gt;
&lt;li&gt;Apply results only when those checks pass.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The checks matter after every awaited boundary, not only at the end. A component may load the main item, related content, entitlement state, reaction state, and cart state in sequence. Any await is a point where navigation or identity can change.&lt;/p&gt;
&lt;p&gt;Cancellation tokens can reduce wasted work, and they are worth using when the underlying operation supports cancellation. But cancellation alone is not the ownership proof. Cancellation can race with completion, and some dependencies cannot cancel promptly. The continuation still needs to ask, “Does this result belong to the current snapshot?”&lt;/p&gt;
&lt;h3 id=&quot;fail-closed-while-identity-is-unknown&quot;&gt;Fail closed while identity is unknown&lt;/h3&gt;
&lt;p&gt;There is another important state between old identity and new identity: unresolved identity.&lt;/p&gt;
&lt;p&gt;Keeping the old viewer-specific UI visible during that interval is convenient, but unsafe. The component should clear ownership badges, active reactions, and similar derived state before awaiting the new authentication result. Any action that depends on identity should be disabled until the new snapshot is ready.&lt;/p&gt;
&lt;p&gt;That brief disabled state is honest. The application does not yet know which identity owns the next command.&lt;/p&gt;
&lt;p&gt;This principle also prevents an action from being sent using newly refreshed credentials while the visible UI still describes the previous viewer. Read state and write authority move together.&lt;/p&gt;
&lt;h3 id=&quot;test-the-interleaving-not-only-the-outcome&quot;&gt;Test the interleaving, not only the outcome&lt;/h3&gt;
&lt;p&gt;Ordinary happy-path tests rarely expose this defect because requests finish in the same order they started.&lt;/p&gt;
&lt;p&gt;A stronger component test controls the schedule:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Start a viewer-specific read and hold its response.&lt;/li&gt;
&lt;li&gt;Change the route or authentication identity.&lt;/li&gt;
&lt;li&gt;Wait for the new screen to settle.&lt;/li&gt;
&lt;li&gt;Release the old response.&lt;/li&gt;
&lt;li&gt;Assert that the current screen does not adopt the old result.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A separate test should hold authentication unresolved and verify that identity-scoped actions remain disabled. This proves that the UI fails closed during the transition.&lt;/p&gt;
&lt;p&gt;There is also a useful counter-test: rerender with exactly the same inputs while a valid request is pending. The component should not discard or duplicate work merely because the framework rendered again. The boundary is a meaningful input change, not every render cycle.&lt;/p&gt;
&lt;p&gt;These tests are valuable because they verify time and ownership, not just a final value. They turn a race that might appear once in a thousand interactions into a deterministic scenario.&lt;/p&gt;
&lt;h3 id=&quot;the-trade-off&quot;&gt;The trade-off&lt;/h3&gt;
&lt;p&gt;Generation markers, captured snapshots, and repeated currentness checks add code. Temporarily disabling an action can introduce a small pause. Overusing the pattern can also make a component difficult to read.&lt;/p&gt;
&lt;p&gt;The answer is not to attach a counter to every asynchronous line. Use the pattern where results are scoped to changing inputs: routes, selected records, authentication identity, workspace, locale, or another boundary that can change while the component remains mounted. Extract a small helper when the same ownership rule appears repeatedly.&lt;/p&gt;
&lt;p&gt;The payoff is larger than visual correctness. Explicit async ownership reduces stale-route updates, prevents state from crossing identity boundaries, and stops commands from being enabled under an unresolved context.&lt;/p&gt;
&lt;p&gt;The humble takeaway is this: asynchronous correctness is not only about receiving the right answer. It is also about proving that the answer still belongs here.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/30557/the-async-result-belongs-to-the-snapshot-that-started-it</guid>
<pubDate>Sat, 10 Oct 2026 06:01:28 +0000</pubDate>
</item>
<item>
<title>Building Reliable Backend Applications with C# and .NET</title>
<link>https://coderlegion.com/29925/building-reliable-backend-applications-with-c-and-net</link>
<description>&lt;p&gt;Backend development is more than writing code that works. For me, it is about building systems that are &lt;strong&gt;reliable, maintainable, secure, and capable of supporting real business needs&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;My main experience is with &lt;strong&gt;C#, .NET, ASP.NET Core, SQL Server, and Entity Framework&lt;/strong&gt;. I enjoy working on backend systems where there are real requirements to understand, databases to design, APIs to build, and performance problems to solve.&lt;/p&gt;
&lt;h4 id=&quot;turning-business-requirements-into-backend-solutions&quot;&gt;Turning Business Requirements into Backend Solutions&lt;/h4&gt;
&lt;p&gt;A typical project starts with understanding what the business actually needs. Before writing code, I try to understand the workflow, the data involved, and how different parts of the application will communicate.&lt;/p&gt;
&lt;p&gt;Once the requirements are clear, I focus on creating a backend structure that is easy to maintain and can grow with the application.&lt;/p&gt;
&lt;p&gt;This usually involves:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Designing backend architecture&lt;/li&gt;
&lt;li&gt;Developing APIs with ASP.NET Core&lt;/li&gt;
&lt;li&gt;Working with SQL Server databases&lt;/li&gt;
&lt;li&gt;Creating data models with Entity Framework&lt;/li&gt;
&lt;li&gt;Implementing authentication and authorization&lt;/li&gt;
&lt;li&gt;Writing business logic in C#&lt;/li&gt;
&lt;li&gt;Improving database queries and application performance&lt;/li&gt;
&lt;li&gt;Debugging and maintaining existing applications&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;working-with-databases&quot;&gt;Working with Databases&lt;/h4&gt;
&lt;p&gt;A backend application is only as good as the data layer behind it.&lt;/p&gt;
&lt;p&gt;I have worked extensively with &lt;strong&gt;SQL Server and Entity Framework&lt;/strong&gt;, including database design, relationships, queries, stored procedures, migrations, and performance optimization.&lt;/p&gt;
&lt;p&gt;One thing I have learned is that database performance should not be treated as an afterthought. A poorly designed query or database structure can affect the entire application.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Efficient database design and optimized queries can make a huge difference in application performance.&lt;/strong&gt;&lt;/p&gt;
&lt;h4 id=&quot;building-secure-apis&quot;&gt;Building Secure APIs&lt;/h4&gt;
&lt;p&gt;Modern applications often depend heavily on APIs. I focus on creating APIs that are organized, predictable, and secure.&lt;/p&gt;
&lt;p&gt;From handling requests and validation to authentication, authorization, error handling, and database communication, every layer needs to work together properly.&lt;/p&gt;
&lt;p&gt;I also believe that an API should be designed with the consumers in mind. Clear responses, consistent error handling, and proper validation make applications much easier to maintain.&lt;/p&gt;
&lt;h4 id=&quot;performance-and-problem-solving&quot;&gt;Performance and Problem Solving&lt;/h4&gt;
&lt;p&gt;Backend development often involves finding problems that are not immediately obvious.&lt;/p&gt;
&lt;p&gt;An application may work correctly but become slow when the amount of data increases. A database query may work with a few records but become inefficient with thousands or millions of records.&lt;/p&gt;
&lt;p&gt;That is where debugging and performance analysis become important.&lt;/p&gt;
&lt;p&gt;I enjoy investigating these types of problems, finding the root cause, and improving the implementation rather than simply applying a temporary fix.&lt;/p&gt;
&lt;h4 id=&quot;maintaining-existing-applications&quot;&gt;Maintaining Existing Applications&lt;/h4&gt;
&lt;p&gt;Not every project starts from scratch.&lt;/p&gt;
&lt;p&gt;A large part of backend development can involve working with existing applications, understanding someone else's code, fixing bugs, improving functionality, and gradually making the system better.&lt;/p&gt;
&lt;p&gt;This requires patience and the ability to understand existing architecture before making changes.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Good backend development is not always about rewriting code. Sometimes the best solution is improving what already exists.&lt;/strong&gt;&lt;/p&gt;
&lt;h4 id=&quot;continuous-learning&quot;&gt;Continuous Learning&lt;/h4&gt;
&lt;p&gt;Technology keeps changing, so there is always something new to learn.&lt;/p&gt;
&lt;p&gt;Even while working primarily with C# and .NET, I continue exploring better development practices, modern backend architecture, API design, database optimization, and new tools that can improve the development process.&lt;/p&gt;
&lt;p&gt;For me, programming is a continuous process of learning, building, debugging, and improving.&lt;/p&gt;
&lt;p&gt;I enjoy taking a business requirement, breaking it down into smaller technical problems, and eventually turning those ideas into a backend system that people can actually use.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Always learning. Always building. Always looking for a better way to solve the problem.&lt;/strong&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/29925/building-reliable-backend-applications-with-c-and-net</guid>
<pubDate>Tue, 06 Oct 2026 02:40:20 +0000</pubDate>
</item>
<item>
<title>Project the Response, Not the Cache</title>
<link>https://coderlegion.com/29355/project-the-response-not-the-cache</link>
<description>&lt;p&gt;A response can be correct for one caller and still leave the application in a worse state for everyone who follows.&lt;/p&gt;
&lt;p&gt;This is an easy trap in ASP.NET Core when a result filter, formatter, or middleware needs to change a few fields before a response leaves the server. Localization is a useful example: a DTO contains source-language text, the current request asks for another language, and the pipeline replaces selected strings before serialization.&lt;/p&gt;
&lt;p&gt;The first response looks right. The implementation may even be pleasantly small.&lt;/p&gt;
&lt;p&gt;The danger is ownership. If the returned DTO came from an in-process cache, the response pipeline may be holding the same object instance that another request, background job, or command path will read later. What looked like response formatting was actually a write to shared state.&lt;/p&gt;
&lt;p&gt;The broader lesson is this: request-specific presentation should be a projection, not a mutation.&lt;/p&gt;
&lt;h3 id=&quot;the-bug-hides-behind-a-successful-response&quot;&gt;The Bug Hides Behind a Successful Response&lt;/h3&gt;
&lt;p&gt;Imagine a cached catalogue item with a description in the system’s source language. A request asks for a translated version. The tempting implementation walks the object graph and assigns the translated string to the DTO property.&lt;/p&gt;
&lt;p&gt;The response is translated correctly. A test that checks only that response passes.&lt;/p&gt;
&lt;p&gt;Now consider the next reader. If the cache returns the same instance, the cached item is no longer in its authoritative source form. A source-language request may receive translated text. A command that maps the DTO back into another model may persist presentation text as source data. A background process may observe a value chosen for somebody else’s request.&lt;/p&gt;
&lt;p&gt;Nothing about the first HTTP response reveals this. The defect is temporal: the observable failure belongs to a later reader.&lt;/p&gt;
&lt;p&gt;That is why “the output is correct” is not a sufficient assertion when inputs can be shared.&lt;/p&gt;
&lt;h3 id=&quot;draw-the-ownership-boundary-explicitly&quot;&gt;Draw the Ownership Boundary Explicitly&lt;/h3&gt;
&lt;p&gt;A useful model is to separate three things:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The cached or domain DTO is authoritative input.&lt;/li&gt;
&lt;li&gt;Request context selects presentation rules.&lt;/li&gt;
&lt;li&gt;The HTTP payload is response-owned output.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Once those roles are explicit, several safe implementations become available. You can map into a dedicated response model. You can clone the graph and change the clone. Or you can project replacement values at the serialization boundary.&lt;/p&gt;
&lt;p&gt;In the last approach, the traversal discovers which string properties need different values but never assigns those values to the source objects. The serializer then substitutes the replacements while creating a JSON tree owned by the response. Nested objects, collections, readonly wrappers, and init-only properties keep their wire shape because the original serializer contract still defines the payload.&lt;/p&gt;
&lt;p&gt;The important property is not the particular API used. It is that no request-specific operation writes into caller-owned or cache-owned objects.&lt;/p&gt;
&lt;p&gt;This idea also applies to redaction, unit formatting, enrichment, API-version shaping, and field masking. If the transformation varies by caller or request, it probably belongs in an output projection.&lt;/p&gt;
&lt;h3 id=&quot;preserve-the-common-case-fast-path&quot;&gt;Preserve the Common-Case Fast Path&lt;/h3&gt;
&lt;p&gt;Projection has a cost. It may require an additional JSON materialization, replacement lookup, and short-lived allocations. That cost should be acknowledged rather than hidden behind a correctness slogan.&lt;/p&gt;
&lt;p&gt;The practical response is to make the cheap path obvious. When the request already uses the source representation, pass the original result through untouched. Only requests that need transformation pay for projection.&lt;/p&gt;
&lt;p&gt;Failure behavior also matters. If the projection cannot handle an unusual graph, the pipeline should not leave a half-mutated object behind. A safe fallback retains the original result and logs the failure according to the application’s policy. Whether returning the untransformed payload is acceptable depends on the feature and data classification; sensitive redaction may need to fail closed instead.&lt;/p&gt;
&lt;p&gt;Security-sensitive payloads and error results may also deserve explicit exclusion. Presentation machinery should not accidentally inspect tokens, secrets, or payload shapes that were never part of its contract.&lt;/p&gt;
&lt;h3 id=&quot;test-the-second-reader&quot;&gt;Test the Second Reader&lt;/h3&gt;
&lt;p&gt;The highest-value test is a sequence, not a snapshot:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Put a source-language object into the real cache implementation used by the host.&lt;/li&gt;
&lt;li&gt;Request a transformed response and verify its output.&lt;/li&gt;
&lt;li&gt;Read the cached object again and verify that it is unchanged.&lt;/li&gt;
&lt;li&gt;Issue a source-language request and verify that it still receives the source value.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Run that sequence through the real result filter and serializer options, not only a mocked translation service. The ownership bug lives in the composition between cache identity, result handling, graph traversal, and serialization.&lt;/p&gt;
&lt;p&gt;Add focused cases for ordinary object results and explicit JSON results, nested collections, readonly response wrappers, init-only properties, error responses, exempt payloads, and projection failure. These tests establish both the intended transformation and the things it must never change.&lt;/p&gt;
&lt;p&gt;A mutation-resistance assertion is especially valuable: keep a reference to the original object and prove its properties retain their exact values after the response executes.&lt;/p&gt;
&lt;h3 id=&quot;the-trade-off-is-isolation-versus-local-simplicity&quot;&gt;The Trade-Off Is Isolation Versus Local Simplicity&lt;/h3&gt;
&lt;p&gt;In-place mutation is easy to understand in one method and can avoid an extra materialization. Its cost appears elsewhere: temporal coupling, language or user context leaking between requests, and tests that pass individually while failing in sequence.&lt;/p&gt;
&lt;p&gt;Response projection adds code, allocations, and serializer-specific reasoning. In return, it makes ownership visible. The cache remains authoritative, requests remain isolated, and failures cannot leave shared objects partially transformed.&lt;/p&gt;
&lt;p&gt;That is a trade I will usually take. I would still measure the transformed path with representative payload sizes and keep the source path cheap. But performance work should begin after the ownership contract is correct.&lt;/p&gt;
&lt;p&gt;The practical rule is short: if a value is shared, treat it as immutable. If presentation depends on the request, produce response-owned output. And when you test it, always ask what the next reader sees.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/29355/project-the-response-not-the-cache</guid>
<pubDate>Thu, 01 Oct 2026 05:50:20 +0000</pubDate>
</item>
<item>
<title>Treat Every ID as an Authorization Claim</title>
<link>https://coderlegion.com/29225/treat-every-id-as-an-authorization-claim</link>
<description>&lt;p&gt;Authentication answers one important question: who made this request? It does not answer a second question that often hides inside an ordinary-looking query parameter: may this caller name the record represented by this identifier?&lt;/p&gt;
&lt;p&gt;That gap is easy to miss in APIs that accept a parent ID, child ID, account ID, project ID, or tenant ID. The route is authenticated. The identifier has the correct shape. The query returns a valid result. Yet none of those facts proves that the record belongs inside the caller's authorized scope.&lt;/p&gt;
&lt;p&gt;A useful engineering rule is to treat every caller-supplied identifier as an authorization claim before treating it as a query input.&lt;/p&gt;
&lt;h3 id=&quot;authentication-is-not-object-ownership&quot;&gt;Authentication Is Not Object Ownership&lt;/h3&gt;
&lt;p&gt;Imagine an endpoint that returns activities for a supplied household record. The controller requires a signed-in user, then passes the supplied ID into a query service. If the ID is changed in transit, the query may still succeed—just for somebody else's data.&lt;/p&gt;
&lt;p&gt;The vulnerable assumption is subtle: “signed in” becomes “allowed to ask about any existing record.” The database faithfully answers the question it receives. The missing control belongs before the query.&lt;/p&gt;
&lt;p&gt;The server should derive the caller's authority from trusted identity and relationship data. For an ordinary user, that might mean resolving the account to its authoritative person record, then traversing explicit relationships to the permitted records. For an operational role, a specific permission may allow a wider scope because that role already performs work requiring the same visibility.&lt;/p&gt;
&lt;p&gt;This also explains why email equality is a poor ownership rule. Addresses are corrected, reused, shared by families, copied into contact fields, and sometimes retained after account changes. An email can help locate a candidate during migration, but it should not silently replace an authoritative account-to-person relationship.&lt;/p&gt;
&lt;h3 id=&quot;put-the-boundary-before-the-query&quot;&gt;Put the Boundary Before the Query&lt;/h3&gt;
&lt;p&gt;A safe request pipeline makes the order explicit:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Identify the caller from trusted authentication state.&lt;/li&gt;
&lt;li&gt;Resolve the caller's authoritative records and relationships on the server.&lt;/li&gt;
&lt;li&gt;Evaluate each supplied identifier against that scope.&lt;/li&gt;
&lt;li&gt;Only then construct and execute the data query.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This order prevents a common failure mode: retrieving a broad result and hoping a later projection removes everything sensitive. Late filtering spreads the security contract across repositories, mappers, and UI code. Early scope resolution gives the query a smaller, already-authorized input.&lt;/p&gt;
&lt;p&gt;Do not bundle independent identifiers into one vague decision. If a request contains both a parent filter and a child filter, check each one. Mixed requests are exactly where “one valid field makes the request valid” bugs appear. A valid parent must not bless an unrelated child; an unrelated parent must not erase a valid child's boundary.&lt;/p&gt;
&lt;h3 id=&quot;reject-by-default-narrow-only-deliberately&quot;&gt;Reject by Default, Narrow Only Deliberately&lt;/h3&gt;
&lt;p&gt;For most data-specific routes, a cross-scope identifier should be rejected. Rejection is direct, observable, and easier to reason about. It tells the client that its request crossed a boundary and gives operators a signal worth investigating.&lt;/p&gt;
&lt;p&gt;Legacy mobile clients complicate that clean answer. An installed client may send an old identifier shape, interpret a forbidden response as a session failure, or sign the user out. Waiting for every old version to disappear can leave a real data boundary open longer than necessary.&lt;/p&gt;
&lt;p&gt;There is a narrow alternative: compatibility-preserving narrowing. The server validates every supplied identifier, discards any that are not authorized, and executes only a known-safe baseline query. The key invariant is monotonicity: removing an unauthorized filter must never widen the response into the requested protected data.&lt;/p&gt;
&lt;p&gt;For example, a calendar route might combine household-specific entries with public entries. If an old client sends an account ID where a domain record ID is expected, the server can remove that invalid household filter and return only the public baseline. It must not substitute another household, guess from a weak attribute, or run an unscoped “all entries” query.&lt;/p&gt;
&lt;p&gt;This is not a general recommendation to hide authorization failures. It is an endpoint-specific migration tool. Document why it exists, record how it is used, and give it a removal condition.&lt;/p&gt;
&lt;h3 id=&quot;test-the-boundary-not-just-the-happy-path&quot;&gt;Test the Boundary, Not Just the Happy Path&lt;/h3&gt;
&lt;p&gt;Authorization tests should form a matrix around relationships and permissions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the caller names their own authoritative record;&lt;/li&gt;
&lt;li&gt;the caller names a related child record;&lt;/li&gt;
&lt;li&gt;the caller names another household's record;&lt;/li&gt;
&lt;li&gt;the caller has no corresponding relationship;&lt;/li&gt;
&lt;li&gt;a shared or copied email appears on a record the caller does not own;&lt;/li&gt;
&lt;li&gt;a staff role lacks the exact cross-scope permission;&lt;/li&gt;
&lt;li&gt;a staff role holds that permission;&lt;/li&gt;
&lt;li&gt;one filter is authorized while another is not;&lt;/li&gt;
&lt;li&gt;the legacy request shape still receives the same safe baseline.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The last two cases are especially valuable. Mixed filters catch accidental “any match” logic. Legacy-shape tests prevent a future cleanup from turning a deliberate compatibility response back into either a leak or a destructive client error.&lt;/p&gt;
&lt;p&gt;Tests should assert returned data as well as status codes. A successful response is safe only if its contents stay inside the proven scope. A forbidden response is useful only if the client contract can tolerate it.&lt;/p&gt;
&lt;h3 id=&quot;make-the-exception-expensive-to-forget&quot;&gt;Make the Exception Expensive to Forget&lt;/h3&gt;
&lt;p&gt;Compatibility code tends to become permanent when its rationale lives only in memory. Keep the exception close to the boundary, name the safe baseline, add telemetry that contains no sensitive identifiers, and define the client version or date after which strict rejection can replace narrowing.&lt;/p&gt;
&lt;p&gt;The trade-off is real. Strict rejection gives a simpler model and clearer misuse signals. Narrowing adds policy, tests, and the possibility of masking a broken client. Its value is speed: the server can close the data boundary now while preserving a safe experience for older installations.&lt;/p&gt;
&lt;p&gt;The practical standard is modest but powerful: never let a caller-provided ID reach a query merely because it parses. First prove that the caller may name it. If compatibility forces an exception, make the request narrower, make the safe response explicit, and make the exception temporary.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/29225/treat-every-id-as-an-authorization-claim</guid>
<pubDate>Wed, 30 Sep 2026 10:38:55 +0000</pubDate>
</item>
<item>
<title>At 43, the Vietnamese IT Market Calls Me &quot;Too Old&quot; — I Still Write Code Every Day</title>
<link>https://coderlegion.com/29217/at-43-the-vietnamese-it-market-calls-me-too-old-i-still-write-code-every-day</link>
<description>&lt;blockquote&gt;&lt;p&gt;Originally published at &lt;a rel=&quot;nofollow&quot; href=&quot;https://vnmlstudio.runasp.net/en/blogs/story-behind-vnml-agent-framework.html&quot;&gt;vnmlstudio.runasp.net&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I'm 43 years old. Last week, a recruiter told me I'm &quot;too old for the position&quot; — politely, of course, but the message was clear.&lt;/p&gt;
&lt;p&gt;I stopped applying 7 months ago. Not out of defeat — out of realization.&lt;/p&gt;
&lt;p&gt;Instead of sending more CVs, I opened Visual Studio and started building something.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-i-built&quot;&gt;What I built&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;VNML Agent Framework&lt;/strong&gt; — an open-source multi-agent AI system that runs 100% locally on my laptop with a GTX 3050 Ti and 4GB VRAM. No cloud. No API keys. No team. Just me and my machine.&lt;/p&gt;
&lt;p&gt;The framework has 4 layers:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;P0 Reflection&lt;/strong&gt; — the agent critiques its own decisions before moving forward&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;P1 Planning&lt;/strong&gt; — DAG-based multi-agent orchestration&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;P2 Learning&lt;/strong&gt; — agents extract reusable skills from experience&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;P3 Synthesis&lt;/strong&gt; — agents write and compile their own C# tools at runtime&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Six months of work. On a laptop. On a CPU.&lt;/p&gt;
&lt;h3 id=&quot;the-moment-i-almost-gave-up&quot;&gt;The moment I almost gave up&lt;/h3&gt;
&lt;p&gt;There were nights I sat in front of the screen asking myself: &lt;em&gt;&quot;How much longer can I keep doing this?&quot;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Every rejection email felt like validation of a cruel joke the industry plays on developers over 35: &lt;em&gt;&quot;Your experience is valuable, but... we're looking for someone more junior.&quot;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;I considered quitting tech. Driving for a ride-hailing app. Opening a small coffee shop. Anything that didn't require me to explain why my birth year is 1983.&lt;/p&gt;
&lt;p&gt;But every time I felt like giving up, I opened Visual Studio again. And I wrote.&lt;/p&gt;
&lt;h3 id=&quot;the-moment-microsoft-noticed&quot;&gt;The moment Microsoft noticed&lt;/h3&gt;
&lt;p&gt;Two weeks ago, I typed &lt;code&gt;vnml agent framework&lt;/code&gt; into Bing on a whim.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Microsoft Copilot Search returned a 200-word AI-generated summary of my project.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Not just a GitHub link. A full description that captured exactly what I'd built — architecture, tool synthesis, local execution, everything.&lt;/p&gt;
&lt;p&gt;I read it three times. Then it hit me: &lt;strong&gt;one of the largest tech companies in the world had read my code, understood my architecture, and written about it.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Nobody called. No email. No announcement. Bing just crawled and indexed my public repo — like it does for every other project in the world.&lt;/p&gt;
&lt;p&gt;But for me, that moment changed everything.&lt;/p&gt;
&lt;h3 id=&quot;what-i-learned&quot;&gt;What I learned&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;1. README matters more than you think.&lt;/strong&gt;&lt;br&gt;
Copilot extracted 90% of its summary from my README. Clear structure → AI understands.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. Personal blogs still have power.&lt;/strong&gt;&lt;br&gt;
Copilot cited my blog on &lt;code&gt;runasp.net&lt;/code&gt;, not Medium, not Dev.to. Own your platform.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. GitHub is a real SEO channel now.&lt;/strong&gt;&lt;br&gt;
Repos get read, understood, and summarized by AI. Nobody predicted this 5 years ago.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;4. Nobody needs to give you permission.&lt;/strong&gt;&lt;br&gt;
You don't need a company to notice you. You don't need funding. You just need to build something real and publish it.&lt;/p&gt;
&lt;h3 id=&quot;why-i-m-writing-this&quot;&gt;Why I'm writing this&lt;/h3&gt;
&lt;p&gt;Not for sympathy. Not for job offers.&lt;/p&gt;
&lt;p&gt;For every developer out there who feels the industry has moved on without them. For every 40-something programmer who wonders if they should just become a manager or change careers.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The industry moves on. But the craft doesn't.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Age doesn't define the value of a developer. &lt;strong&gt;Persistence beats prejudice.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;I'm 43. I have no team. No funding. No office. Just a laptop, an idea, and the stubborn refusal to stop.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;And I still choose to write code. Every single day.&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;If you're building with local LLMs, connecting with the C# community, or just want to see what a solo dev can ship in 2026 — the full framework is open source:&lt;/p&gt;
&lt;p&gt;🔗 &lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/hoangnc/VNMLAgentFramework&quot;&gt;https://github.com/hoangnc/VNMLAgentFramework&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I'd love to hear: what keeps you going when the industry says you don't belong anymore?&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/29217/at-43-the-vietnamese-it-market-calls-me-too-old-i-still-write-code-every-day</guid>
<pubDate>Wed, 30 Sep 2026 09:02:40 +0000</pubDate>
</item>
<item>
<title>A Lifetime Is a Hold, Not the Worker</title>
<link>https://coderlegion.com/29003/a-lifetime-is-a-hold-not-the-worker</link>
<description>&lt;p&gt;&lt;code&gt;IDisposable&lt;/code&gt; and &lt;code&gt;IAsyncDisposable&lt;/code&gt; make lifetime management feel pleasantly local: acquire something, use it, dispose it. That model is reliable when the acquired value has exclusive ownership of an independent resource.&lt;/p&gt;
&lt;p&gt;It becomes dangerous when several activations share one background worker.&lt;/p&gt;
&lt;p&gt;Mobile applications encounter this more often than it first appears. Returning online can reconcile a capability that is already running. A foreground transition can overlap an earlier activation. A bounded startup wait can stop waiting for slow native work, while that work completes after a newer activation has already joined the same resource.&lt;/p&gt;
&lt;p&gt;If every returned lifetime means “stop the worker,” one participant can tear down work that another still needs. The sharper contract is: a returned lifetime is a hold on shared work. Disposing it releases only that participant’s hold. The worker stops when the final hold leaves.&lt;/p&gt;
&lt;h3 id=&quot;the-bug-hides-behind-a-reasonable-api&quot;&gt;The bug hides behind a reasonable API&lt;/h3&gt;
&lt;p&gt;Imagine a mobile shell with two optional capabilities: a small status refresher and a device-token relay. Each activation returns an asynchronous lifetime to the orchestrator. The orchestrator keeps the latest lifetime and disposes the one it replaces.&lt;/p&gt;
&lt;p&gt;That sounds tidy, but activations are not necessarily independent. The second activation may join the refresher or event relay started by the first. If the first lifetime calls a global &lt;code&gt;StopAsync&lt;/code&gt;, disposing the replaced lifetime stops the shared worker beneath the second activation.&lt;/p&gt;
&lt;p&gt;The state machine can now tell a convincing lie. Its newest activation completed successfully, so the capability still appears ready. Underneath, polling has stopped or the event handler has been removed.&lt;/p&gt;
&lt;p&gt;Warning, cancellation, and stale-completion paths deserve the same scrutiny. Cleanup code is often written defensively in several branches. If each branch performs terminal teardown, one failed attempt can shut down a resource held by a successful attempt.&lt;/p&gt;
&lt;h3 id=&quot;write-the-ownership-sentence-first&quot;&gt;Write the ownership sentence first&lt;/h3&gt;
&lt;p&gt;Before implementing the lifetime, describe what disposal means:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Disposing this value releases this activation’s participation. It does not necessarily stop the shared resource.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That sentence changes the design. Starting becomes “start or join, then take a hold.” Releasing becomes “release this hold, then stop only if it was the last.” Both transitions need one synchronization boundary.&lt;/p&gt;
&lt;p&gt;It is not enough to increment a counter and subscribe in separate steps. Another thread can observe the gap. A release can unsubscribe after a new hold has been counted, or two starts can install duplicate listeners. The zero-to-one decision and the actual start belong in one atomic operation. So do the one-to-zero decision and the stop.&lt;/p&gt;
&lt;p&gt;The hold itself should release at most once. A double &lt;code&gt;Dispose&lt;/code&gt; must not decrement the count twice. A failed start must not record a hold, otherwise the next activation may “join” work that never started.&lt;/p&gt;
&lt;h3 id=&quot;old-holds-must-not-reach-into-new-runs&quot;&gt;Old holds must not reach into new runs&lt;/h3&gt;
&lt;p&gt;Reference counting alone is not sufficient when the resource can stop and restart.&lt;/p&gt;
&lt;p&gt;Suppose an old activation holds generation A. The application moves offline, so generation A is stopped. Later, the application returns online and starts generation B. If the late lifetime from A merely decrements a global count, it can steal a hold from B and stop the restarted worker.&lt;/p&gt;
&lt;p&gt;Bind each hold to the run it joined. Releasing a hold from an ended run becomes a no-op for the current one.&lt;/p&gt;
&lt;p&gt;The orchestrator may need a run identity too. Some stop events invalidate a broad session lease; others, such as backgrounding or going offline, may intentionally leave that session identity unchanged. A slow activation can therefore finish with a lease that still looks current even though the capability run it belonged to has ended. Tracking the capability run separately prevents that late completion from being adopted as ready.&lt;/p&gt;
&lt;p&gt;This distinction is useful beyond mobile software. Connection pools, shared subscriptions, refresh loops, file watchers, and in-process event relays all have “same logical session, different resource run” failure modes.&lt;/p&gt;
&lt;h3 id=&quot;test-transitions-not-only-outcomes&quot;&gt;Test transitions, not only outcomes&lt;/h3&gt;
&lt;p&gt;A happy-path test that starts and stops once proves very little about shared ownership.&lt;/p&gt;
&lt;p&gt;Useful tests make each boundary visible:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;take two holds, release one, and prove the worker remains active;&lt;/li&gt;
&lt;li&gt;release the final hold and prove the worker stops exactly once;&lt;/li&gt;
&lt;li&gt;dispose one hold twice and prove it releases once;&lt;/li&gt;
&lt;li&gt;fail the first start and prove a later acquisition tries again;&lt;/li&gt;
&lt;li&gt;restart the resource and prove an old hold cannot affect the new run;&lt;/li&gt;
&lt;li&gt;complete an abandoned activation after a newer one and prove the newer work survives; and&lt;/li&gt;
&lt;li&gt;repeatedly take, inspect, and release holds from two threads while the count crosses zero.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That last test is important. A single-thread loop can leave mutation survivors because it never exposes the gap between “count says active” and “listener is actually active.” Two competing threads make zero crossings collide with acquisition and release, which is where split synchronization shows itself.&lt;/p&gt;
&lt;p&gt;Concurrency tests still need discipline: bounded rounds, a clear invariant, and no claim that passing them mathematically proves correctness. Their job is to exercise the race window while deterministic tests pin the state-machine rules.&lt;/p&gt;
&lt;h3 id=&quot;the-honest-trade-off&quot;&gt;The honest trade-off&lt;/h3&gt;
&lt;p&gt;Counted holds add a lock, a run identifier, idempotent release state, and more involved tests. They also force a clear distinction between releasing one participant and terminally disposing the owning dependency scope.&lt;/p&gt;
&lt;p&gt;The simpler design is to prohibit overlap. That can be the right choice when the orchestrator can guarantee one activation at a time, cancel it reliably, and wait for completion before starting another. But if native calls can outlive a budget, or reconciliation can join already-running work, the simplicity is imaginary.&lt;/p&gt;
&lt;p&gt;The practical takeaway is broader than reference counting: make ownership semantics part of the public contract. When a lifetime represents participation in shared work, call it a hold and test it like one. A local cleanup action should never silently become a global stop.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/29003/a-lifetime-is-a-hold-not-the-worker</guid>
<pubDate>Tue, 29 Sep 2026 05:58:20 +0000</pubDate>
</item>
<item>
<title>Design the Cursor, Not Just the Query</title>
<link>https://coderlegion.com/28877/design-the-cursor-not-just-the-query</link>
<description>&lt;p&gt;Pagination is often introduced as a performance improvement: fetch fewer rows, render less markup, and keep response sizes predictable. That is only half the job.&lt;/p&gt;
&lt;p&gt;For an operational queue that changes while people work, pagination also defines what it means to continue reading. If that contract is weak, the database can answer every query correctly while the person using the screen still sees duplicates or misses work.&lt;/p&gt;
&lt;p&gt;The important design question is not merely, “How many rows should this query return?” It is, “What does the next request mean after the underlying set has changed?”&lt;/p&gt;
&lt;h3 id=&quot;why-offset-paging-drifts&quot;&gt;Why offset paging drifts&lt;/h3&gt;
&lt;p&gt;Consider a queue ordered newest first. A reviewer asks for the first twenty-five rows. The next request uses an offset of twenty-five.&lt;/p&gt;
&lt;p&gt;Between those requests, a new row arrives at the top. Every earlier row has moved down one position. The second query now begins with the final row from page one, so the reviewer sees a duplicate. More importantly, one unseen row has been pushed beyond the requested range.&lt;/p&gt;
&lt;p&gt;This is not an exotic race. Append-mostly queues invite it because adding work is the ordinary operation. Offset paging treats positions as stable even though insertion changes every position below it.&lt;/p&gt;
&lt;p&gt;Mutable sorting creates a similar problem. If the queue is ordered by status or last-updated time, an operator’s decision can move a row while the walk is in progress. A continuation based on that value inherits the instability of the workflow.&lt;/p&gt;
&lt;h3 id=&quot;continue-from-an-immutable-boundary&quot;&gt;Continue from an immutable boundary&lt;/h3&gt;
&lt;p&gt;A stronger contract says: continue after the boundary returned by the previous read.&lt;/p&gt;
&lt;p&gt;For a newest-first queue, that boundary can be an immutable creation or submission timestamp. New rows arriving above the boundary are not part of the current walk. They will appear when the reviewer starts again from the top, but they do not shift the meaning of “next.”&lt;/p&gt;
&lt;p&gt;Equal timestamps need deliberate handling. Adding an identifier as a tie-breaker becomes fragile when data providers sort that type differently.&lt;/p&gt;
&lt;p&gt;Another approach is to carry the boundary timestamp plus the number of rows already consumed at that exact timestamp. The database remains responsible for its native ordering within the tie, while the continuation records how far through that tie the walk has progressed. The detail matters less than the invariant: ties must neither duplicate nor hide rows.&lt;/p&gt;
&lt;h3 id=&quot;make-the-continuation-part-of-the-contract&quot;&gt;Make the continuation part of the contract&lt;/h3&gt;
&lt;p&gt;An opaque cursor should not be a loosely encoded page number. It is protocol state, so treat it that way.&lt;/p&gt;
&lt;p&gt;Useful safeguards include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;a version, so the format can evolve deliberately;&lt;/li&gt;
&lt;li&gt;a strict length limit and parse validation;&lt;/li&gt;
&lt;li&gt;the immutable boundary and tie position;&lt;/li&gt;
&lt;li&gt;the active filter identity; and&lt;/li&gt;
&lt;li&gt;sensible bounds on any stored count.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Binding the cursor to its filter is especially important. A position in “all open items” does not mean the same thing in “items for one category.” Reusing the cursor after changing filters can silently skip work. Rejecting that request is clearer than pretending the cursor is still meaningful.&lt;/p&gt;
&lt;p&gt;Likewise, a malformed continuation should fail explicitly. Silently dropping it and returning the first page converts a bad request into a plausible-looking duplicate page. Operational software benefits from visible invalid states.&lt;/p&gt;
&lt;h3 id=&quot;keep-every-downstream-read-bounded&quot;&gt;Keep every downstream read bounded&lt;/h3&gt;
&lt;p&gt;Paging the primary table is not enough if enrichment still loads data for the entire result set.&lt;/p&gt;
&lt;p&gt;A common pattern reads the page, extracts its identifiers, and then fetches related summaries in a second query. That identifier list must come from the bounded page. Otherwise the visible query is paged while a hidden IN clause continues growing with the queue.&lt;/p&gt;
&lt;p&gt;Fetching one extra primary row is a simple way to determine whether a continuation is needed. Return the requested page size, and issue a next cursor only when the extra row exists. The presence of that cursor—not whether the current page happens to be full—becomes the authoritative “has more” signal.&lt;/p&gt;
&lt;p&gt;This also avoids an exact count on every read. For many live queues, “Page 3” plus Previous and Next is the more honest interface.&lt;/p&gt;
&lt;h3 id=&quot;carry-the-contract-into-the-blazor-ui&quot;&gt;Carry the contract into the Blazor UI&lt;/h3&gt;
&lt;p&gt;Server-side continuation paging should not be wrapped in a client component that first downloads the entire queue. The UI has to follow the same contract.&lt;/p&gt;
&lt;p&gt;A practical Blazor page can keep a small trail containing the cursor used to open each visited page. Next stores the returned cursor. Previous reuses the earlier cursor to re-read that logical page. After an operator records a decision, refresh the current page rather than throwing them back to the beginning.&lt;/p&gt;
&lt;p&gt;Page changes should also clear any pending confirmation tied to a row that is no longer visible. That is a small state-management detail with a large clarity benefit: the user should never be asked to confirm an action on an item they cannot see.&lt;/p&gt;
&lt;h3 id=&quot;test-the-movement-not-only-the-shape&quot;&gt;Test the movement, not only the shape&lt;/h3&gt;
&lt;p&gt;Happy-path tests that assert “twenty-five rows returned” do not prove a paging contract.&lt;/p&gt;
&lt;p&gt;The valuable cases exercise movement:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;insert a newer row after reading page one, then verify page two has no gap or duplicate;&lt;/li&gt;
&lt;li&gt;create several rows with the same boundary timestamp;&lt;/li&gt;
&lt;li&gt;reject zero, negative, and excessive page sizes;&lt;/li&gt;
&lt;li&gt;reject malformed, oversized, and filter-mismatched cursors;&lt;/li&gt;
&lt;li&gt;verify related-data queries receive only current-page identifiers;&lt;/li&gt;
&lt;li&gt;navigate Next and Previous in the component; and&lt;/li&gt;
&lt;li&gt;perform an action and confirm the current page is refreshed.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These tests describe the promise a reviewer experiences, not merely an implementation detail.&lt;/p&gt;
&lt;h3 id=&quot;the-honest-trade-off-1&quot;&gt;The honest trade-off&lt;/h3&gt;
&lt;p&gt;Continuation paging is more work than offset paging. It needs a stable sort, a cursor format, versioning, filter binding, careful tie semantics, and focused tests. It also makes random access and exact page counts less convenient.&lt;/p&gt;
&lt;p&gt;That cost is justified when the list is both live and operationally important. Bounded queries protect the system under bursty load; stable continuation protects the human workflow under concurrent change.&lt;/p&gt;
&lt;p&gt;Takeaway: choose pagination from the mutation pattern, not from the table widget. If ordinary inserts can change what “next” means, the continuation itself deserves first-class design.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/28877/design-the-cursor-not-just-the-query</guid>
<pubDate>Mon, 28 Sep 2026 06:23:05 +0000</pubDate>
</item>
<item>
<title>Recommendations Are Not Decisions: Designing an Auditable Human Boundary</title>
<link>https://coderlegion.com/28360/recommendations-are-not-decisions-designing-an-auditable-human-boundary</link>
<description>&lt;p&gt;Software is very good at making a calculation look authoritative. Give a rule engine enough inputs and it can return a crisp result in milliseconds. In a consequential workflow, that neatness is dangerous: a computed recommendation can quietly become a decision without anyone deliberately granting it that authority.&lt;/p&gt;
&lt;p&gt;A better design treats computation and authority as different responsibilities. The software may evaluate evidence, explain what it found, and suggest an outcome. A person still records the actual decision. The data model preserves both.&lt;/p&gt;
&lt;p&gt;That separation is more than a user-interface disclaimer. It has to reach the evaluator, persistence model, activation workflow, and tests.&lt;/p&gt;
&lt;h3 id=&quot;keep-the-evaluator-pure&quot;&gt;Keep the evaluator pure&lt;/h3&gt;
&lt;p&gt;Start with an evaluator that has no write path. It accepts a versioned rule and a set of observations, then returns an explanation:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;which inputs contributed;&lt;/li&gt;
&lt;li&gt;which requirements passed or failed;&lt;/li&gt;
&lt;li&gt;what evidence was missing;&lt;/li&gt;
&lt;li&gt;which rule version was used; and&lt;/li&gt;
&lt;li&gt;what outcome, if any, is suggested.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The return value should say “suggested,” not “decided.” Naming is not sufficient, but it makes accidental authority harder to hide.&lt;/p&gt;
&lt;p&gt;Purity matters here. A preview operation should be safe to run repeatedly without creating decisions, sending notifications, or mutating state. That keeps calculation testable and prevents a read-like action from acquiring an unexpected side effect.&lt;/p&gt;
&lt;p&gt;When essential evidence is missing, the evaluator should return an advisory state with no suggestion. A nullable recommendation can be a feature: it says the software cannot justify a conclusion. Returning the most likely answer would make uncertainty easy to rubber-stamp.&lt;/p&gt;
&lt;h3 id=&quot;make-rules-prove-their-intended-behaviour&quot;&gt;Make rules prove their intended behaviour&lt;/h3&gt;
&lt;p&gt;Syntactically valid configuration is not the same as reviewed policy. A collection of thresholds can pass field validation and still express something the author did not mean.&lt;/p&gt;
&lt;p&gt;Before a configurable rule becomes active, require named worked examples. Each example contains representative inputs and the outcome the author expects. Activation replays every example through the same evaluator used by real records. If one disagrees, activation stops.&lt;/p&gt;
&lt;p&gt;Using the same evaluator is important. A second “fixture evaluator” would only prove that two implementations can disagree in production.&lt;/p&gt;
&lt;p&gt;Malformed examples should fail before replay. If a parser silently misreads an input and the example happens to reach the expected result, the gate gives false confidence. An explicit failure is safer than a coincidental pass.&lt;/p&gt;
&lt;p&gt;These examples act as executable policy conversations. They let reviewers ask, “What should happen in this borderline case?” before the rule affects a real decision.&lt;/p&gt;
&lt;h3 id=&quot;persist-recommendation-and-decision-separately&quot;&gt;Persist recommendation and decision separately&lt;/h3&gt;
&lt;p&gt;The final record should not overwrite the computed recommendation. Store both:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the recommendation available at decision time;&lt;/li&gt;
&lt;li&gt;the outcome the person recorded;&lt;/li&gt;
&lt;li&gt;the exact rule version that informed it;&lt;/li&gt;
&lt;li&gt;the actor and timestamp; and&lt;/li&gt;
&lt;li&gt;a reason when the decision departs from the recommendation.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This turns disagreement into useful data instead of treating it as an error to conceal. A departure may reveal legitimate context that the model deliberately does not represent. It may also reveal a weak rule. Either way, preserving both facts makes later review possible.&lt;/p&gt;
&lt;p&gt;The same principle applies to overrides. Do not edit the old outcome in place. Append a new version, mark it as current, retain the previous value, and require a reason. Historical records should describe what was known and decided then—not what today’s configuration would calculate now.&lt;/p&gt;
&lt;p&gt;Pinning the rule version is crucial. If a rule changes next month, an old decision must still point to the version that produced its original recommendation. Otherwise, history becomes a moving target.&lt;/p&gt;
&lt;h3 id=&quot;test-the-boundary-not-only-the-formula&quot;&gt;Test the boundary, not only the formula&lt;/h3&gt;
&lt;p&gt;Formula tests are necessary, but the more revealing tests are often about authority and time.&lt;/p&gt;
&lt;p&gt;Useful cases include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;previewing produces an explanation and writes nothing;&lt;/li&gt;
&lt;li&gt;missing essential evidence produces no recommendation;&lt;/li&gt;
&lt;li&gt;a rule without worked examples cannot be activated;&lt;/li&gt;
&lt;li&gt;an example that disagrees with the rule blocks activation;&lt;/li&gt;
&lt;li&gt;departing from a recommendation without a reason is refused;&lt;/li&gt;
&lt;li&gt;an override appends a version instead of editing history; and&lt;/li&gt;
&lt;li&gt;changing a rule later does not alter an already recorded decision.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These tests express the operating model. They protect not only mathematical correctness, but also who is allowed to decide and whether past context remains trustworthy.&lt;/p&gt;
&lt;h3 id=&quot;accept-the-deliberate-friction&quot;&gt;Accept the deliberate friction&lt;/h3&gt;
&lt;p&gt;This approach costs more than returning an enum and saving it. It introduces draft and active rule states, advisory outcomes, versioned decisions, reasons, worked examples, and review steps. Operators need to understand the distinction. Developers have more invariants to maintain.&lt;/p&gt;
&lt;p&gt;That friction should be proportional to consequence. A low-stakes recommendation may not need an append-only ledger. A decision affecting access, eligibility, money, safety, or a person’s future probably deserves more than an opaque calculation.&lt;/p&gt;
&lt;p&gt;Worked examples also have limits. They prove that the authored rule behaves as expected on selected cases. They do not prove that the policy is fair, lawful, complete, or statistically sound. Human approval can still be mistaken. Permissions, peer review, monitoring, and periodic policy review remain necessary.&lt;/p&gt;
&lt;h3 id=&quot;a-practical-design-question&quot;&gt;A practical design question&lt;/h3&gt;
&lt;p&gt;When reviewing a rule-driven workflow, ask one question first: is this output evidence for a decision, or is the system silently making the decision itself?&lt;/p&gt;
&lt;p&gt;If the answer matters, create a visible boundary. Keep evaluation pure. Refuse to guess when evidence is insufficient. Gate activation with executable examples. Record human choices separately. Preserve departures and history.&lt;/p&gt;
&lt;p&gt;The goal is not to weaken automation. It is to make its authority honest.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/28360/recommendations-are-not-decisions-designing-an-auditable-human-boundary</guid>
<pubDate>Wed, 23 Sep 2026 14:53:09 +0000</pubDate>
</item>
<item>
<title>cTrader cBot Risk Engine: High-Precision C# Dynamic Stop-Loss Architecture</title>
<link>https://coderlegion.com/27900/ctrader-cbot-risk-engine-high-precision-c-dynamic-stop-loss-architecture</link>
<description>&lt;p&gt;Static stop-losses break across volatility regimes. A fixed 20-pip stop that is conservative on EUR/USD during Asia session is noise-level on XAU/USD during New York open. In cTrader Automate (C#), the robust pattern is a dedicated risk engine: signal logic decides direction, the engine decides size and stop distance from live volatility.&lt;/p&gt;
&lt;p&gt;This post documents the architecture — educational, no trade signals, no return claims.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;1-separate-sizing-from-signals&quot;&gt;1. Separate Sizing From Signals&lt;/h3&gt;
&lt;p&gt;The most common cBot flaw is inline risk math inside &lt;code&gt;OnBar()&lt;/code&gt;: volume computed next to entry conditions, stop distance hardcoded. When volatility doubles, the same stop gets hunted and the same volume carries twice the dollar risk.&lt;/p&gt;
&lt;p&gt;Split the cBot into two units:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Signal module&lt;/strong&gt; — produces direction only (e.g., breakout, trend filter).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Risk engine&lt;/strong&gt; — given direction, computes stop distance from ATR and volume from a fixed monetary budget.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;// RiskEngine.cs — pure logic, no cTrader dependencies (unit-testable)
public static class RiskEngine
{
    public static double StopDistancePrice(double atr, double multiplier)
    {
        return atr * multiplier;
    }

    public static long VolumeInUnits(double riskMoney, double stopDistancePrice,
                                     double tickValuePerUnit, double pipSize,
                                     long volumeMin, long volumeMax, long volumeStep,
                                     out bool clampedToMin, out double effectiveRisk)
    {
        double raw = riskMoney / (stopDistancePrice / pipSize * tickValuePerUnit);
        long floored = (long)Math.Floor(raw / volumeStep) * volumeStep;
        clampedToMin = false;
        if (floored &amp;lt; volumeMin) { floored = volumeMin; clampedToMin = true; }
        long finalVolume = Math.Min(floored, volumeMax);
        effectiveRisk = finalVolume * (stopDistancePrice / pipSize * tickValuePerUnit);
        return finalVolume;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note the &lt;code&gt;clampedToMin&lt;/code&gt; flag: when the budgeted volume falls below the broker minimum, the engine clamps up &lt;strong&gt;and reports the effective risk&lt;/strong&gt;, which now exceeds the budget. Silently reporting the original budget while carrying higher exposure is the exact failure we dissected in &lt;a rel=&quot;nofollow&quot; href=&quot;https://coderlegion.com/27300/the-silent-position-sizer-bug-when-accounts-are-small-your-risk-inadvertently-doubles&quot;&gt;The Silent Position Sizer Bug&lt;/a&gt; — never repeat it.&lt;/p&gt;
&lt;h3 id=&quot;2-atr-based-dynamic-stops&quot;&gt;2. ATR-Based Dynamic Stops&lt;/h3&gt;
&lt;p&gt;Average True Range adapts the stop to current volatility. A 1.5x–2.0x ATR stop keeps the exit outside normal noise on any symbol and any session:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;private double GetAtrStopPips(int atrPeriod = 14, double multiplier = 2.0)
{
    var atr = Indicators.AverageTrueRange(atrPeriod, MovingAverageType.Exponential);
    double stopPrice = atr.Result.LastValue * multiplier;
    return stopPrice / Symbol.PipSize;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Because the stop distance is measured, not assumed, the same cBot behaves sanely on a 0.0001-pip Forex pair and a 2-decimal metal without retuning magic numbers.&lt;/p&gt;
&lt;h3 id=&quot;3-wiring-it-into-the-cbot&quot;&gt;3. Wiring It Into the cBot&lt;/h3&gt;
&lt;p&gt;Volume in cTrader is expressed in units (100,000 units = 1.0 standard lot on FX). Always read the broker limits from the symbol — never hardcode them:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;protected override void OnBar()
{
    if (Positions.Count &amp;gt; 0) return; // single-position example
    if (!GetSignal()) return;

    double stopPips = GetAtrStopPips();
    double stopPrice = stopPips * Symbol.PipSize;

    long volume = RiskEngine.VolumeInUnits(
        RiskMoney, stopPrice, Symbol.TickValue / Symbol.TickSize * Symbol.PipSize,
        Symbol.PipSize, Symbol.VolumeInUnitsMin,
        Symbol.VolumeInUnitsMax, Symbol.VolumeInUnitsStep,
        out bool clamped, out double effectiveRisk);

    if (clamped)
        Print(&quot;WARNING: volume clamped to broker minimum. &quot; +
              &quot;Effective risk {0} exceeds budget {1}.&quot;, effectiveRisk, RiskMoney);

    ExecuteMarketOrder(TradeType.Buy, SymbolName, volume,
                       &quot;atr-risk&quot;, stopPips, null);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;4-trailing-without-chasing&quot;&gt;4. Trailing Without Chasing&lt;/h3&gt;
&lt;p&gt;Move the stop only when price has banked a multiple of the ATR distance — ratcheting on every tick bleeds to spread:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;private void TrailAtr(Position position, double atrDistancePrice)
{
    double newStop = Symbol.Bid - 1.0 * atrDistancePrice; // long example
    if (newStop &amp;gt; position.StopLoss + Symbol.PipSize)
        ModifyPosition(position, newStop, position.TakeProfit);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;5-validation-checklist-demo-first-&quot;&gt;5. Validation Checklist (Demo First)&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Run 60 days on demo across at least two volatility regimes (e.g., Forex + metal).&lt;/li&gt;
&lt;li&gt;Log every clamp event; if clamps exceed ~5% of trades, the risk budget is too small for the account — top up or widen the stop, never silence the warning.&lt;/li&gt;
&lt;li&gt;Backtest with real-tick data; reject any result you cannot reproduce forward on demo.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Educational content under Colombia SFC Decreto 2555 de 2010: this is financial-engineering study material, not investment advice. No signals, no managed accounts.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;By &lt;strong&gt;Mahdi Goodarzi&lt;/strong&gt; (&lt;a rel=&quot;nofollow&quot; href=&quot;https://g.dev/mahdigoodarzi&quot;&gt;g.dev/mahdigoodarzi&lt;/a&gt;), Gueta Quant — interactive risk tools at &lt;a rel=&quot;nofollow&quot; href=&quot;https://guetaquant.com&quot;&gt;guetaquant.com&lt;/a&gt;. Companion guide: &lt;a rel=&quot;nofollow&quot; href=&quot;https://guetaquant.com/ctrader-copy-trading/&quot;&gt;cTrader Copy setup in Colombia&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/27900/ctrader-cbot-risk-engine-high-precision-c-dynamic-stop-loss-architecture</guid>
<pubDate>Mon, 21 Sep 2026 08:26:11 +0000</pubDate>
</item>
<item>
<title>Pagination Is a Consistency Contract</title>
<link>https://coderlegion.com/27813/pagination-is-a-consistency-contract</link>
<description>&lt;p&gt;Page numbers feel harmless. They are familiar, easy to render, and easy to explain. But on a mutable work queue, they can lie.&lt;/p&gt;
&lt;p&gt;Imagine a reviewer reading a newest-first queue. They load the first 25 items. Before they request the next page, a new item arrives at the top. An offset such as &lt;code&gt;skip 25&lt;/code&gt; now starts one row later than it would have a moment ago. The last row from page one may appear again, and the row displaced by the insert may never appear in that walk.&lt;/p&gt;
&lt;p&gt;Nothing crashed. No error was logged. The interface simply presented an inconsistent view of the work.&lt;/p&gt;
&lt;p&gt;The engineering lesson is that pagination over changing data is a consistency contract, not a decorative control beneath a table.&lt;/p&gt;
&lt;h3 id=&quot;begin-with-the-mutation-model&quot;&gt;Begin with the mutation model&lt;/h3&gt;
&lt;p&gt;The right pagination strategy depends on how the collection changes.&lt;/p&gt;
&lt;p&gt;For an append-mostly queue ordered newest first, new work normally arrives above the operator's current position. Other fields, such as workflow status, may change while the operator is reviewing an item. Those two facts suggest a useful boundary: continue from something fixed when the item entered the queue, not from a mutable field and not from the current numeric position.&lt;/p&gt;
&lt;p&gt;A submission timestamp can provide that boundary if it is frozen when the item is created. Status usually cannot; ordering by status lets a decision move rows between pages while someone is walking the list.&lt;/p&gt;
&lt;p&gt;The principle is broader than timestamps: choose an ordering key whose meaning remains stable for the duration of a walk.&lt;/p&gt;
&lt;h3 id=&quot;make-the-server-own-the-continuation&quot;&gt;Make the server own the continuation&lt;/h3&gt;
&lt;p&gt;A client should not reconstruct position from a page number, a visible row, or a guessed offset. The server should return an opaque continuation and accept it unchanged on the next request.&lt;/p&gt;
&lt;p&gt;For a newest-first queue, a continuation can describe the last timestamp boundary and how many rows at that exact instant have already been emitted. The next query includes rows at or below the boundary, skips only the already-seen ties at that boundary, and takes one extra row to determine whether another page exists.&lt;/p&gt;
&lt;p&gt;Why count ties instead of comparing an identifier? Identifier ordering is not always portable across database providers. Let the query provider apply its established ordering, and record only how far through the boundary group the walk has progressed.&lt;/p&gt;
&lt;p&gt;Opaque does not mean unchecked. Reject blank, malformed, oversized, or unsupported continuations. A bad continuation should not silently restart at the top, because a restart looks like valid data with unexplained duplicates.&lt;/p&gt;
&lt;h3 id=&quot;bind-position-to-query-context&quot;&gt;Bind position to query context&lt;/h3&gt;
&lt;p&gt;A continuation names a position in one specific ordered result set. Change the filter and that position may no longer mean anything.&lt;/p&gt;
&lt;p&gt;Suppose page one was filtered to one workflow cycle, then the same continuation was replayed with the filter removed. Honouring it could skip rows that were never part of the original walk. The continuation should therefore carry enough context to prove it belongs to the active query. If the context differs, refuse the request and let the client restart explicitly.&lt;/p&gt;
&lt;p&gt;The same discipline applies to page size. Put a small upper bound in the contract and reject values outside it. Quietly shrinking an oversized request hides caller mistakes; accepting it defeats the point of paging.&lt;/p&gt;
&lt;p&gt;Bounding the first query is only half the work. If each row needs evidence, comments, or another related record, fetch those details only for identifiers on the current page. Otherwise an apparently paged endpoint can still perform an unbounded secondary read.&lt;/p&gt;
&lt;h3 id=&quot;preserve-the-operator-s-place&quot;&gt;Preserve the operator's place&lt;/h3&gt;
&lt;p&gt;Continuation cursors naturally move forward. A usable review screen also needs Back and refresh.&lt;/p&gt;
&lt;p&gt;The client can keep a short trail of the server-provided continuations used to open each visited page. Previous then replays the cursor that originally opened the prior page. The client is not interpreting the cursor; it is remembering the path.&lt;/p&gt;
&lt;p&gt;After a reviewer changes an item, refresh the page they are currently viewing. Returning to page one after every decision is technically simple and operationally punishing. It also encourages people to work around the interface rather than trust it.&lt;/p&gt;
&lt;p&gt;There is one awkward edge case: a later page may become empty before it is refreshed. Do not render a global empty-state message that implies the entire queue has vanished. Reset to the first page and show the current truth from a valid starting position.&lt;/p&gt;
&lt;h3 id=&quot;test-the-walk-not-only-the-query&quot;&gt;Test the walk, not only the query&lt;/h3&gt;
&lt;p&gt;Happy-path tests that assert “25 rows returned” are not enough. The important properties live between requests.&lt;/p&gt;
&lt;p&gt;Useful executable checks include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;a new item arriving above the boundary neither duplicates nor hides an existing row;&lt;/li&gt;
&lt;li&gt;a workflow status change does not alter page membership;&lt;/li&gt;
&lt;li&gt;several items sharing one timestamp are neither repeated nor lost;&lt;/li&gt;
&lt;li&gt;a continuation cannot be replayed with a different filter;&lt;/li&gt;
&lt;li&gt;malformed continuations and invalid page sizes are refused;&lt;/li&gt;
&lt;li&gt;forward and backward navigation use the exact server continuations;&lt;/li&gt;
&lt;li&gt;a write refreshes the active page; and&lt;/li&gt;
&lt;li&gt;an empty later page recovers without claiming the whole queue is empty.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These tests describe the contract more clearly than the word “paged” ever could.&lt;/p&gt;
&lt;h3 id=&quot;the-trade-off-is-explicit-complexity&quot;&gt;The trade-off is explicit complexity&lt;/h3&gt;
&lt;p&gt;Cursor paging asks more of both sides. The server must define and validate a stable continuation. The client must retain a small history. Ties and disappearing pages need deliberate behaviour. Troubleshooting is less intuitive than looking at &lt;code&gt;page=3&lt;/code&gt; in a URL.&lt;/p&gt;
&lt;p&gt;In return, database work stays bounded and a reviewer can walk a changing queue without silent duplicates or gaps. That is a worthwhile trade whenever missing one item matters more than keeping the paging code superficially simple.&lt;/p&gt;
&lt;p&gt;Before choosing offsets for a mutable list, simulate one insert and one update between consecutive requests. If the same walk no longer produces the same set of rows, the pager needs a stronger contract.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/27813/pagination-is-a-consistency-contract</guid>
<pubDate>Sun, 20 Sep 2026 07:52:36 +0000</pubDate>
</item>
<item>
<title>Your Reader Cannot Be Stricter Than Your Schema</title>
<link>https://coderlegion.com/27483/your-reader-cannot-be-stricter-than-your-schema</link>
<description>&lt;p&gt;&lt;code&gt;SingleOrDefault&lt;/code&gt; looks like a small query choice. In practice, it is an assertion about your data model: for this predicate, zero or one row may exist.&lt;/p&gt;
&lt;p&gt;That assertion is valuable when storage enforces the same rule. When the database permits duplicates, however, the reader is relying on an invariant that does not exist. One unexpected row can turn an ordinary lookup into an exception before validation, authorization, or other normal handling begins.&lt;/p&gt;
&lt;p&gt;I recently reviewed a committed recovery for exactly that class of mismatch. The useful lesson was not the specific lookup. It was how to contain the exception without pretending the data had become correct.&lt;/p&gt;
&lt;h3 id=&quot;a-query-can-encode-a-hidden-contract&quot;&gt;A query can encode a hidden contract&lt;/h3&gt;
&lt;p&gt;Consider a generalized account table:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Rows permitted by storage: 0..many per normalized key
Rows expected by reader:    0..1 per normalized key
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The read path might be perfectly reasonable in isolation:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;var record = await records.SingleOrDefaultAsync(x =&amp;gt; x.LookupKey == key);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But the method name does not create uniqueness. It only checks cardinality at read time. If two rows match, the query throws.&lt;/p&gt;
&lt;p&gt;This is why I now treat &lt;code&gt;Single&lt;/code&gt; and &lt;code&gt;SingleOrDefault&lt;/code&gt; as executable invariant assertions. During review, I ask: what proves the invariant? A unique index? A constrained aggregate? A transactionally protected write path? Or only an earlier existence check that can race?&lt;/p&gt;
&lt;h3 id=&quot;containment-is-not-repair&quot;&gt;Containment is not repair&lt;/h3&gt;
&lt;p&gt;When duplicate data already exists, immediately adding a unique index may be unsafe. Migration can fail on the existing rows. Previously accepted imports or registrations may begin failing without a useful remediation path.&lt;/p&gt;
&lt;p&gt;At the same time, leaving a critical read path to throw can make a narrow data defect disproportionately disruptive.&lt;/p&gt;
&lt;p&gt;A staged response separates two goals:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Containment:&lt;/strong&gt; provide a deterministic, non-crashing resolution path while preserving security and making the defect visible.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Correction:&lt;/strong&gt; repair the existing data and enforce the intended rule for future writes.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The distinction matters. A tolerant reader is a recovery mechanism, not evidence that duplicates are acceptable.&lt;/p&gt;
&lt;h3 id=&quot;properties-of-a-safe-recovery-path&quot;&gt;Properties of a safe recovery path&lt;/h3&gt;
&lt;p&gt;A useful containment pattern keeps the normal case boring:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;try
{
    return await ReadExpectedSingleAsync(key);
}
catch (InvalidOperationException)
{
    var matches = await ReadBoundedCandidatesAsync(key);
    if (matches.Count &amp;lt; 2)
        throw;

    ReportCollision(matches.Select(x =&amp;gt; x.OpaqueId));
    return ChooseDeterministically(matches);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This sketch is intentionally incomplete, but it highlights several important properties.&lt;/p&gt;
&lt;h4 id=&quot;keep-the-fast-path-unchanged&quot;&gt;Keep the fast path unchanged&lt;/h4&gt;
&lt;p&gt;Most lookups are not ambiguous. Do not make every request pay for a corruption-recovery query. Invoke the fallback only after the expected lookup demonstrates the precise failure you are prepared to handle.&lt;/p&gt;
&lt;h4 id=&quot;prove-ambiguity-before-handling-it&quot;&gt;Prove ambiguity before handling it&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;InvalidOperationException&lt;/code&gt; can come from many places. Catching it and returning “not found” would turn an unrelated store failure into a silent wrong answer. Re-query narrowly; if there are not actually multiple matches, rethrow the original fault.&lt;/p&gt;
&lt;h4 id=&quot;choose-deterministically&quot;&gt;Choose deterministically&lt;/h4&gt;
&lt;p&gt;Never accept database enumeration order as a resolution policy. Define explicit ranking from legitimate domain facts, then add a stable final tie-break. The same data must produce the same candidate on every attempt.&lt;/p&gt;
&lt;p&gt;Determinism does not make the selected record correct. It prevents the recovery itself from becoming random.&lt;/p&gt;
&lt;h4 id=&quot;preserve-downstream-verification&quot;&gt;Preserve downstream verification&lt;/h4&gt;
&lt;p&gt;Resolving one candidate must not grant access, approve a transaction, or bypass an eligibility rule. Credential checks, authorization, state validation, and other security decisions still run after resolution. Containment should make the lookup deterministic, not weaken the boundaries that follow it. A legitimate candidate may still lose the ranking, so some affected operations can remain unavailable until the data is repaired.&lt;/p&gt;
&lt;h4 id=&quot;bound-and-observe-the-fallback&quot;&gt;Bound and observe the fallback&lt;/h4&gt;
&lt;p&gt;A pathological key should not cause an unbounded scan during a critical request. Cap diagnostic reads, raise a high-signal operational event, and log only what responders need. Opaque record identifiers may support repair; the sensitive lookup value often does not belong in logs.&lt;/p&gt;
&lt;h3 id=&quot;the-stronger-end-state-lives-in-storage&quot;&gt;The stronger end state lives in storage&lt;/h3&gt;
&lt;p&gt;After containment, the work is not finished. The durable sequence is usually:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;inventory collisions
-&amp;gt; decide merge or deletion policy
-&amp;gt; repair existing rows
-&amp;gt; reject or reconcile conflicting writes
-&amp;gt; add the storage constraint
-&amp;gt; remove or narrow temporary recovery code
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For relational data, that often means a unique index aligned with the normalization and null semantics used by the application. The write path should return a useful domain result when the constraint rejects a conflict. A pre-insert lookup can improve the message, but the database remains the concurrency-safe authority.&lt;/p&gt;
&lt;p&gt;There is a real trade-off. Immediate enforcement gives the cleanest future guarantee but can break deployment against historical data. Temporary tolerance contains the exception but adds policy, cannot guarantee later verification will succeed, and can become permanent by neglect. Treat it as migration work with an owner, telemetry, and an exit condition.&lt;/p&gt;
&lt;h3 id=&quot;test-the-invariant-and-the-escape-hatch&quot;&gt;Test the invariant and the escape hatch&lt;/h3&gt;
&lt;p&gt;Focused tests should cover more than “duplicates do not throw.” I would pin:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the ordinary single-row path remains untouched;&lt;/li&gt;
&lt;li&gt;selection is stable when candidates arrive in different orders;&lt;/li&gt;
&lt;li&gt;inactive or retired records cannot outrank valid ones;&lt;/li&gt;
&lt;li&gt;unrelated exceptions escape unchanged;&lt;/li&gt;
&lt;li&gt;downstream verification still decides the outcome;&lt;/li&gt;
&lt;li&gt;diagnostics contain repairable identifiers but no sensitive key;&lt;/li&gt;
&lt;li&gt;the candidate read is bounded;&lt;/li&gt;
&lt;li&gt;the future uniqueness constraint rejects a conflicting concurrent write.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The deeper lesson is architectural: reads, writes, and storage must agree on cardinality. If a reader says “exactly one,” make that promise explicit at the boundary able to enforce it. Until then, the code is not reading an invariant. It is discovering, at runtime, whether the hope was true.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/27483/your-reader-cannot-be-stricter-than-your-schema</guid>
<pubDate>Thu, 17 Sep 2026 08:20:48 +0000</pubDate>
</item>
<item>
<title>OpenSKP 1.1.0: A Native .skp Writer, Now in All Five Languages</title>
<link>https://coderlegion.com/27396/openskp-1-1-0-a-native-skp-writer-now-in-all-five-languages</link>
<description>&lt;p&gt;Until 1.1.0, &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/iamahsanmehmood/openskp&quot;&gt;OpenSKP&lt;/a&gt; was a read path. You could parse a &lt;code&gt;.skp&lt;/code&gt; file — either container format, all five languages — and get materials, layers, geometry, and metadata out of it. What you couldn't do was make a new one. 1.1.0 closes that gap: a real writer, producing files SketchUp itself opens without complaint, shipped in Python, TypeScript, .NET, Dart, and C++ at the same time.&lt;/p&gt;
&lt;h3 id=&quot;why-write-support-is-a-different-problem-than-read-support&quot;&gt;Why write support is a different problem than read support&lt;/h3&gt;
&lt;p&gt;Parsing forgives sloppiness in a way writing doesn't. A parser that mishandles an obscure edge case just gets slightly wrong data for that one field — annoying, but survivable, and often invisible unless you're specifically checking. A writer that gets the container format wrong produces a file that SketchUp refuses to open at all, or worse, opens with silently corrupted geometry. There's no partial credit.&lt;/p&gt;
&lt;p&gt;That asymmetry shaped how the writer got built. It targets the modern &lt;strong&gt;VFF&lt;/strong&gt; container exclusively — the ZIP-based, TLV-tree format used by SketchUp 2021 and later — rather than also targeting the legacy MFC stream format, because VFF is what every current SketchUp install expects by default and where new files should live going forward. Every writer feature was validated the same way the parser's correctness was validated originally: produce a file, open it with the real SketchUp SDK, and confirm the SDK's own reading of geometry, materials, and layer assignments matches what was intended, field by field — not just &quot;the file has a size greater than zero and doesn't crash on open.&quot;&lt;/p&gt;
&lt;h3 id=&quot;what-the-writer-actually-does&quot;&gt;What the writer actually does&lt;/h3&gt;
&lt;p&gt;The API surface is intentionally generic rather than shaped around specific object types. There's no &lt;code&gt;Chair&lt;/code&gt; class or &lt;code&gt;Table&lt;/code&gt; builder baked into the library — you compose scenes out of the same primitives SketchUp itself works with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Materials&lt;/strong&gt; — solid colors, opacity, and image textures&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Layers (tags)&lt;/strong&gt; — for organizing geometry the way SketchUp's own Tags panel does&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Groups and component definitions&lt;/strong&gt; — the same grouping/instancing model SketchUp uses natively, including nested groups&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Curves and faces&lt;/strong&gt; — arbitrary polygon geometry with material and layer assignment per face&lt;/li&gt;
&lt;li&gt;&lt;code&gt;create()&lt;/code&gt; for a brand-new file, and &lt;code&gt;open_existing()&lt;/code&gt; for loading a file, editing it, and writing it back out&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;open_existing()&lt;/code&gt; in particular was the harder of the two entry points to get right. It isn't a byte-patcher that finds the one chunk you touched and surgically rewrites it in place — that approach is fragile against any structural change (add a face, and every downstream offset in the container shifts). Instead it does a full parse of the existing file into the same in-memory model the writer already knows how to serialize, applies your edits to that model, and replays the entire thing back out through the same write path a brand-new file goes through. Slower than a patch, but correct by construction: if &lt;code&gt;create()&lt;/code&gt; is trustworthy, &lt;code&gt;open_existing()&lt;/code&gt; inherits that trust instead of needing its own separate proof.&lt;/p&gt;
&lt;h3 id=&quot;five-ports-five-different-sets-of-bugs&quot;&gt;Five ports, five different sets of bugs&lt;/h3&gt;
&lt;p&gt;The writer shipped in Python first, since Python was already the most mature of the five ports and the fastest place to validate the design against the real SDK. Porting it to the other four languages wasn't a mechanical translation exercise — each port's own CI caught real, language-specific problems that the Python reference implementation simply couldn't have surfaced:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;C++&lt;/strong&gt; — a &lt;code&gt;check_writable&lt;/code&gt; helper failed to compile under the CI's stricter build flags, a class of error Python's dynamic typing has no equivalent to catching until runtime, if at all. The C++ port also turned up a &lt;code&gt;clang-format&lt;/code&gt; issue where the CI's diff report was silently truncating on longer violations, which needed fixing in the tooling itself before it could be trusted to gate anything.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dart&lt;/strong&gt; — trigonometric rounding in transform-matrix math didn't match the other languages' output bit-for-bit, traced to a difference in how Dart's math library rounds versus Python's, and fixed by aligning the rounding step explicitly rather than relying on each language's default behavior.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Two of the ports&lt;/strong&gt; hit test-ordering bugs — tests that passed individually but failed when run as part of the full suite, because they shared mutable fixture state that Python's test runner happened to isolate in a way the other runner didn't by default.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;None of these were writer-logic bugs in the sense of &quot;the geometry is wrong.&quot; They were the ordinary friction of porting real, non-trivial code across five different type systems, build toolchains, and test runners — exactly the kind of thing that's invisible if you only ever ship one language, and exactly why keeping five active ports is more expensive than it sounds, but catches more than a single-language project ever would.&lt;/p&gt;
&lt;h3 id=&quot;shipping-five-packages-at-once&quot;&gt;Shipping five packages at once&lt;/h3&gt;
&lt;p&gt;1.1.0 went out to all five registries in the same release cycle — PyPI, npm, NuGet, pub.dev, and a tagged GitHub Release with prebuilt C++ artifacts (C++ has no package registry equivalent, so it ships as a downloadable tarball/zip pair instead). Coordinating that many release pipelines in one pass surfaced two more process-level snags worth naming honestly: a batched multi-language tag push needed the tags separated out per-language rather than pushed as one lump, and pub.dev's own publish flow has a gotcha around tag naming that isn't obvious until it rejects a push.&lt;/p&gt;
&lt;p&gt;Neither was a code bug. Both are the kind of thing you only learn by actually running a five-language release end to end, which is exactly what 1.1.0 forced.&lt;/p&gt;
&lt;h3 id=&quot;what-s-next&quot;&gt;What's next&lt;/h3&gt;
&lt;p&gt;Read and write both exist now, but write support is currently VFF-only — no legacy-MFC writing, and no re-encoding a legacy file into the modern container. Conversion the other direction (glTF, IFC, OBJ into &lt;code&gt;.skp&lt;/code&gt;) is on the roadmap but not started. If either of those is something you'd use, the &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/iamahsanmehmood/openskp/issues&quot;&gt;issue tracker&lt;/a&gt; is open.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Install:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;pip install openskp          # Python
npm install openskp          # TypeScript / JavaScript
dotnet add package OpenSKP   # .NET
dart pub add openskp         # Dart
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;OpenSKP is open source under the MIT license: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/iamahsanmehmood/openskp&quot;&gt;github.com/iamahsanmehmood/openskp&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Originally published at &lt;a rel=&quot;nofollow&quot; href=&quot;https://openskp.com/blog/openskp-1-1-0-writer-all-languages.html&quot;&gt;openskp.com&lt;/a&gt; and &lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/iamahsanmehmood/openskp-110-a-native-skp-writer-now-in-all-five-languages-5d0m&quot;&gt;DEV Community&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/27396/openskp-1-1-0-a-native-skp-writer-now-in-all-five-languages</guid>
<pubDate>Wed, 16 Sep 2026 15:34:15 +0000</pubDate>
</item>
<item>
<title>Tuesday Coding Tip 21 — Dangers of Enums</title>
<link>https://coderlegion.com/26340/tuesday-coding-tip-21-dangers-of-enums</link>
<description>&lt;p&gt;Tuesday coding tips are super short posts about various tidbits mainly from C++, but also from other programming languages I use. You can also follow the #TuesdayCodingTips hashtag on Mastodon and Linkedin.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;C++ and C# have a caveat in their &lt;code&gt;enum&lt;/code&gt; types. Each &lt;code&gt;enum&lt;/code&gt; has an underlying storage type which limits a maximum number of values and how much space it takes in memory. However, it also works the other way — it says how many distinct values the &lt;code&gt;enum&lt;/code&gt; handles.&lt;/p&gt;
&lt;p&gt;If you define an &lt;code&gt;enum&lt;/code&gt; with just three values, if it has an underlying type of &lt;code&gt;uint8&lt;/code&gt;, it handles 256 distinct values. You can take any integer up to 255, cast it to that &lt;code&gt;enum&lt;/code&gt; type, and get a “valid” value. When branching on an &lt;code&gt;enum&lt;/code&gt;, you need to keep in mind that you might get an undefined value, especially if it was deserialized from the wire.&lt;/p&gt;
&lt;p&gt;To add insult to injury, compilers won’t warn you by default. Only pattern matching does for C#. C++ can warn you if you don’t cover a defined &lt;code&gt;enum&lt;/code&gt; value in a switch statement, but you must not use the &lt;code&gt;default&lt;/code&gt; case — preventing you from correctly handling ill-formed values.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;lt;iostream&amp;gt;

enum class Mode : char {
    Primary,
    Secondary
};

std::string to_string(Mode mode)
{   
    switch (mode)
    {
        case Mode::Primary: return &quot;Primary&quot;;
        // With -Wswitch-enum
        // warning: enumeration value 'Secondary' not handled in switch
    }
}

std::string to_string2(Mode mode)
{
    switch (mode)
    {
        case Mode::Primary: return &quot;Primary&quot;;
        // With -Wswitch-enum
        // no other warning
        default: return &quot;Undefined&quot;;
    }
}

std::string to_string3(Mode mode)
{
    switch (mode)
    {
        case Mode::Primary: return &quot;Primary&quot;;
        case Mode::Secondary: return &quot;Secondary&quot;;
        // With -Wswitch-enum
        // no other warning
    }
}

int main()
{
    auto&amp;amp;&amp;amp; str = to_string3(static_cast&amp;lt;Mode&amp;gt;(42));
    std::cout &amp;lt;&amp;lt; str &amp;lt;&amp;lt; std::endl; // Output is an undefined behaviour
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/26340/tuesday-coding-tip-21-dangers-of-enums</guid>
<pubDate>Tue, 08 Sep 2026 09:59:00 +0000</pubDate>
</item>
<item>
<title>C# API CRUD Done Wrong, Then Done Right: A Complete Bad-Practices-vs-Best-Practices Walkthrough</title>
<link>https://coderlegion.com/26211/c-api-crud-done-wrong-then-done-right-complete-bad-practices-best-practices-walkthrough</link>
<description>&lt;p&gt;Most bad-code examples in tutorials are contrived - obviously wrong in a way real code never actually looks. This post takes a different approach: a complete, realistic Product CRUD API, built with thirteen anti-patterns that genuinely show up in codebases. Each endpoint is walked through one at a time - the bad version, exactly what's wrong with it and why, then the good version, right next to each other, so nothing needs to be hunted down elsewhere on the page.&lt;/p&gt;
&lt;p&gt;The scope: a Product entity, five CRUD endpoints - GetAll, GetById, Create, Update, Delete.&lt;/p&gt;
&lt;h3 id=&quot;shared-setup-program-cs&quot;&gt;Shared Setup: Program.cs&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Bad version:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;var builder = WebApplication.CreateBuilder(args);
 
builder.Services.AddControllers();
builder.Services.AddSingleton&amp;lt;ProductsController&amp;gt;();
 
var app = builder.Build();
app.MapControllers();
app.Run();
 
// Inside ProductsController itself:
private readonly string _connectionString =
    &quot;Server=prod-sql-01;Database=Products;User Id=sa;Password=Passw0rd123!;&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Issues:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Hardcoded connection string. A literal password is committed directly into source code. Anyone with repository access, including, eventually, anyone the repository is ever accidentally exposed to, has the production database password in plain text. &lt;br&gt;
The fix: load the connection string from configuration, appsettings.json locally, environment variables or Key Vault in production, never as a literal string in a class.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Incorrect service lifetime, AddSingleton for a controller. Manually registering the controller itself with AddSingleton means exactly one instance of ProductsController is created and shared across every single request for the entire lifetime of the application, rather than the framework creating a fresh instance per request the normal way. This becomes genuinely dangerous the moment that controller, or anything it depends on, holds any per-request state, and it gets worse once a Scoped dependency like a DbContext enters the picture, since DbContext is explicitly not thread-safe and is designed to live for exactly one request. A Singleton capturing a Scoped dependency is a well-known anti-pattern called a &quot;captive dependency&quot; - the Scoped service effectively gets trapped inside the Singleton's lifetime, silently becoming a de facto singleton itself, shared across requests and threads it was never designed to be shared across. &lt;br&gt;
The fix: never manually register a controller at all, AddControllers() already handles controller instantiation correctly per request, and give each dependency the lifetime that actually matches how it's meant to be used, which for something wrapping a DbContext means Scoped.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Good version:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;var builder = WebApplication.CreateBuilder(args);
 
builder.Services.AddDbContext&amp;lt;ProductDbContext&amp;gt;(options =&amp;gt;
    options.UseSqlServer(
        builder.Configuration.GetConnectionString(&quot;ProductsDb&quot;)));
 
builder.Services.AddScoped&amp;lt;IProductService, ProductService&amp;gt;();
builder.Services.AddControllers();
 
var app = builder.Build();
app.MapControllers();
app.Run();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The connection string now comes from configuration. IProductService is registered as Scoped, not Singleton, one instance per HTTP request, matching the lifetime of the DbContext it wraps, and avoiding the captive dependency problem entirely. The controller itself is never manually registered at all; AddControllers() handles that correctly on its own.&lt;/p&gt;
&lt;h3 id=&quot;shared-setup-the-product-entity&quot;&gt;Shared Setup: The Product Entity&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
    public int StockQuantity { get; set; }
    public string InternalSupplierNotes { get; set; }
    public decimal CostPrice { get; set; }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Identical in both versions - the entity itself isn't the problem. InternalSupplierNotes and CostPrice are genuinely internal fields, what the business actually pays a supplier, never meant for a customer-facing API to expose. What differs between versions is whether this exact class ever gets returned to a caller directly, covered in the GetById section below.&lt;/p&gt;
&lt;h3 id=&quot;endpoint-1-getall&quot;&gt;Endpoint 1: GetAll&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Bad version:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;[HttpGet]
public IActionResult GetAll()
{
    var products = GetAllProductsAsync().Result;
    return Ok(products);
}
 
private async Task&amp;lt;List&amp;lt;Product&amp;gt;&amp;gt; GetAllProductsAsync()
{
    var products = new List&amp;lt;Product&amp;gt;();
    using var connection = new SqlConnection(_connectionString);
    await connection.OpenAsync();
 
    var command = new SqlCommand(&quot;SELECT * FROM Products&quot;, connection);
    var reader = await command.ExecuteReaderAsync();
 
    while (await reader.ReadAsync())
    {
        products.Add(new Product
        {
            Id = reader.GetInt32(0),
            Name = reader.GetString(1),
            Price = reader.GetDecimal(2)
        });
    }
    return products;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Issues:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Missing await, .Result blocks the thread. GetAllProductsAsync().Result synchronously blocks the calling thread while waiting for the async method to complete, defeating the entire purpose of async and await, since the thread sits idle instead of being released back to the pool. In certain synchronization contexts, classic ASP.NET, WPF, WinForms, this specific pattern can cause a genuine deadlock. &lt;br&gt;
The fix: use await all the way up the call chain, never call .Result or .Wait() on an async method.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Undisposed SqlCommand and SqlDataReader. Both command and reader implement IDisposable, but only connection gets a using statement here. SqlDataReader in particular keeps the underlying connection in a busy state until it's explicitly closed, so under real load, with many concurrent requests each doing this, connections return to the pool later than they need to, risking pool exhaustion faster than necessary. The outer using on connection happens to clean these up indirectly when it disposes, which is what keeps this specific method from actively breaking right now, but relying on that indirect cleanup rather than disposing each IDisposable explicitly is fragile, and won't hold up if the method returns early or the pattern changes later. &lt;br&gt;
The fix: wrap every IDisposable in its own using statement, not just the outermost one.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Good version:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;[HttpGet]
public async Task&amp;lt;ActionResult&amp;lt;List&amp;lt;ProductDto&amp;gt;&amp;gt;&amp;gt; GetAll()
{
    var products = await _productService.GetAllAsync();
    return Ok(products);
}
 
// Inside ProductService:
public async Task&amp;lt;List&amp;lt;ProductDto&amp;gt;&amp;gt; GetAllAsync()
{
    return await _context.Products
        .Select(p =&amp;gt; new ProductDto
        {
            Id = p.Id,
            Name = p.Name,
            Price = p.Price,
            StockQuantity = p.StockQuantity
        })
        .ToListAsync();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Genuinely awaited from the controller all the way through the service call. EF Core's LINQ query is also parameterized automatically and projects directly to ProductDto - a preview of the DTO pattern the next endpoint introduces properly. There's also no manual connection, command, or reader to dispose at all here - EF Core's DbContext and its query execution manage the underlying ADO.NET resources internally, which is one more reason the disposal issue from the bad version simply can't occur in this version.&lt;/p&gt;
&lt;h3 id=&quot;endpoint-2-getbyid&quot;&gt;Endpoint 2: GetById&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Bad version:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;[HttpGet(&quot;{id}&quot;)]
public IActionResult GetById(int id)
{
    using var connection = new SqlConnection(_connectionString);
    connection.Open();
 
    var query = &quot;SELECT * FROM Products WHERE Id = &quot; + id;
    var command = new SqlCommand(query, connection);
    var reader = command.ExecuteReader();
 
    if (!reader.Read())
    {
        return Ok(null);
    }
 
    var product = new Product
    {
        Id = reader.GetInt32(0),
        Name = reader.GetString(1),
        Price = reader.GetDecimal(2)
    };
 
    return Ok(product);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Issues:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;SQL injection. The id parameter is concatenated directly into the query string. A request to /api/products/1;DROP TABLE Products-- executes exactly that. &lt;br&gt;
The fix: never build SQL by concatenating user input, use a parameterized query, or an ORM like EF Core that parameterizes automatically.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Ok(null) instead of NotFound(). When the product doesn't exist, this returns a 200 OK with an empty body. The caller can't distinguish &quot;found nothing&quot; from &quot;succeeded, and the answer happens to be nothing,&quot; both look identical on the wire. &lt;br&gt;
The fix: return NotFound(), a genuine 404, so the status code itself carries the information.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;No DTO, full entity exposed. The returned product is the raw entity, including InternalSupplierNotes and CostPrice, fields that should never reach an API caller. &lt;br&gt;
The fix: map to a ProductDto containing only the fields meant to be public.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Good version:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;public class ProductDto
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
    public int StockQuantity { get; set; }
}
 
[HttpGet(&quot;{id}&quot;)]
public async Task&amp;lt;ActionResult&amp;lt;ProductDto&amp;gt;&amp;gt; GetById(int id)
{
    var product = await _productService.GetByIdAsync(id);
 
    if (product == null)
        return NotFound();
 
    return Ok(product);
}
 
// Inside ProductService:
public async Task&amp;lt;ProductDto?&amp;gt; GetByIdAsync(int id)
{
    var product = await _context.Products.FindAsync(id);
    // FindAsync uses the primary key safely - parameterized
    // automatically, no string concatenation anywhere
 
    if (product == null)
        return null;
    // Returning null here is fine - it's the CONTROLLER's
    // job to translate this into the correct HTTP response,
    // not the service's
 
    return new ProductDto
    {
        Id = product.Id,
        Name = product.Name,
        Price = product.Price,
        StockQuantity = product.StockQuantity
    };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;All three issues fixed together: FindAsync makes SQL injection structurally impossible, NotFound() makes the response honest, and ProductDto makes leaking internal fields impossible rather than something to remember not to do.&lt;/p&gt;
&lt;h3 id=&quot;endpoint-3-create&quot;&gt;Endpoint 3: Create&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Bad version:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;[HttpPost]
public IActionResult Create(Product product)
{
    using var connection = new SqlConnection(_connectionString);
    connection.Open();
 
    var query = $&quot;INSERT INTO Products (Name, Price) &quot; +
                 $&quot;VALUES ('{product.Name}', {product.Price})&quot;;
    var command = new SqlCommand(query, connection);
    command.ExecuteNonQuery();
 
    _logger.LogInformation(
        &quot;Product created by user {Email} with auth token {Token}: {Name}&quot;,
        currentUser.Email, currentUser.AuthToken, product.Name);
 
    return Ok(product);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Issues:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;No validation. A request with an empty Name, a negative Price, or missing fields is accepted exactly as-is. &lt;br&gt;
The fix: add validation attributes to a dedicated input DTO, paired with ApiController's automatic model validation.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;SQL injection, again. Same pattern as GetById, product.Name concatenated directly, and this version breaks entirely if the name contains a single quote. &lt;br&gt;
The fix: same as before, EF Core, parameterized automatically.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Wrong status code. Returns 200 OK for a successful creation. &lt;br&gt;
The fix: 201 Created, with a Location header pointing to the new resource, since 200 doesn't communicate &quot;a new thing was created.&quot;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Sensitive data logged in plain text. The user's email is reasonable to log, but the auth token is a genuine secret, and it's now sitting in plain text inside Application Insights or Log Analytics, visible to anyone with read access to logs, often retained for weeks or months. A leaked or overly-permissioned log query becomes a credential leak. &lt;br&gt;
The fix: never log secrets, tokens, passwords, or other sensitive fields, log an identifier (a user ID) instead of the credential itself, and if a field must be referenced for debugging, mask or redact it before it reaches the logger.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Good version:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;public class CreateProductDto
{
    [Required, MaxLength(200)]
    public string Name { get; set; }
 
    [Range(0.01, 1000000)]
    public decimal Price { get; set; }
 
    [Range(0, int.MaxValue)]
    public int StockQuantity { get; set; }
}
 
[HttpPost]
public async Task&amp;lt;ActionResult&amp;lt;ProductDto&amp;gt;&amp;gt; Create(CreateProductDto dto)
{
    // No manual validation check needed - [ApiController]
    // automatically returns 400 Bad Request if the DTO's
    // validation attributes aren't satisfied, before this
    // method body even runs
 
    var created = await _productService.CreateAsync(dto);
 
    _logger.LogInformation(
        &quot;Product {ProductId} created by user {UserId}&quot;,
        created.Id, currentUser.Id);
    // Logs an IDENTIFIER, never a credential or token -
    // enough to trace who did what, without exposing
    // anything an attacker or an over-permissioned log
    // reader could actually use
 
    return CreatedAtAction(
        nameof(GetById),
        new { id = created.Id },
        created);
}
 
// Inside ProductService:
public async Task&amp;lt;ProductDto&amp;gt; CreateAsync(CreateProductDto dto)
{
    var product = new Product
    {
        Name = dto.Name,
        Price = dto.Price,
        StockQuantity = dto.StockQuantity
    };
 
    _context.Products.Add(product);
    await _context.SaveChangesAsync();
 
    return new ProductDto
    {
        Id = product.Id,
        Name = product.Name,
        Price = product.Price,
        StockQuantity = product.StockQuantity
    };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;CreateProductDto's Required, MaxLength, and Range attributes are checked automatically by ApiController. CreatedAtAction returns the correct 201 with a Location header built from the GetById action. The log statement now records a user ID, not a credential, and EF Core removes the SQL injection path entirely.&lt;/p&gt;
&lt;h3 id=&quot;endpoint-4-update&quot;&gt;Endpoint 4: Update&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Bad version:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;[HttpPut(&quot;{id}&quot;)]
public IActionResult Update(int id, Product product)
{
    try
    {
        using var connection = new SqlConnection(_connectionString);
        connection.Open();
 
        var query = $&quot;UPDATE Products SET Name = '{product.Name}', &quot; +
                    $&quot;Price = {product.Price} WHERE Id = {id}&quot;;
        var command = new SqlCommand(query, connection);
        command.ExecuteNonQuery();
    }
    catch (Exception ex)
    {
        return Ok(new { error = ex.ToString() });
    }
 
    return Ok();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Issues:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Swallowed exception behavior masked as a response. The catch block doesn't silently discard the exception here, but it does something almost as problematic in a different way, it converts a failure into a 200 OK response body, meaning the HTTP status code itself claims success while the actual content says otherwise. Callers checking only the status code, which is the normal way to check for success, will treat this as a successful update. &lt;br&gt;
The fix: return an actual error status code, and don't rely on the response body alone to communicate failure.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;SQL injection, again. Same string-concatenation pattern as the previous two endpoints.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Full exception details leaked to the caller. ex.ToString() returns the complete exception message and full stack trace, including internal file paths, method names, and sometimes literal fragments of the failed SQL query itself, all sent directly to whoever called this endpoint. This is genuinely useful reconnaissance for an attacker probing the API, and it's an accidental disclosure of internal implementation detail even to a well-meaning caller. &lt;br&gt;
The fix: log the full exception detail internally, server-side, and return only a generic, safe error message to the caller.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Good version:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;[HttpPut(&quot;{id}&quot;)]
public async Task&amp;lt;IActionResult&amp;gt; Update(int id, CreateProductDto dto)
{
    var success = await _productService.UpdateAsync(id, dto);
 
    if (!success)
        return NotFound();
 
    return NoContent();
}
 
// Inside ProductService:
public async Task&amp;lt;bool&amp;gt; UpdateAsync(int id, CreateProductDto dto)
{
    var product = await _context.Products.FindAsync(id);
    if (product == null)
        return false;
 
    product.Name = dto.Name;
    product.Price = dto.Price;
    product.StockQuantity = dto.StockQuantity;
 
    try
    {
        await _context.SaveChangesAsync();
        return true;
    }
    catch (DbUpdateException ex)
    {
        // The FULL exception, including stack trace, is
        // logged INTERNALLY here - visible to the team via
        // Application Insights, never sent to the caller
        _logger.LogError(ex, &quot;Failed to update product {Id}&quot;, id);
        return false;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The exception is now caught narrowly, DbUpdateException, not the broad Exception base class, logged internally in full detail with _logger.LogError, and the method returns false so the controller responds with a correct NotFound or, in a fuller version, a generic 500 with a safe message, never the raw exception content itself.&lt;/p&gt;
&lt;h3 id=&quot;endpoint-5-delete&quot;&gt;Endpoint 5: Delete&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Bad version:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;[HttpDelete(&quot;{id}&quot;)]
public IActionResult Delete(int id)
{
    using var connection = new SqlConnection(_connectionString);
    connection.Open();
 
    var query = &quot;DELETE FROM Products WHERE Id = &quot; + id;
    var command = new SqlCommand(query, connection);
    command.ExecuteNonQuery();
 
    return Ok();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Issues:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;SQL injection, again. The same concatenation pattern as every other endpoint above.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Wrong status code. Returns 200 OK with no body for a successful delete. The fix: 204 No Content, the correct response when an operation succeeds and there's genuinely nothing further to return.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Good version:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;[HttpDelete(&quot;{id}&quot;)]
public async Task&amp;lt;IActionResult&amp;gt; Delete(int id)
{
    var success = await _productService.DeleteAsync(id);
 
    if (!success)
        return NotFound();
 
    return NoContent();
}
 
// Inside ProductService:
public async Task&amp;lt;bool&amp;gt; DeleteAsync(int id)
{
    var product = await _context.Products.FindAsync(id);
    if (product == null)
        return false;
 
    _context.Products.Remove(product);
    await _context.SaveChangesAsync();
    return true;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;the-complete-good-version-service-interface&quot;&gt;The Complete Good-Version Service Interface&lt;/h3&gt;
&lt;p&gt;Pulling every service method from above together into the one interface the controller actually depends on:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;public interface IProductService
{
    Task&amp;lt;List&amp;lt;ProductDto&amp;gt;&amp;gt; GetAllAsync();
    Task&amp;lt;ProductDto?&amp;gt; GetByIdAsync(int id);
    Task&amp;lt;ProductDto&amp;gt; CreateAsync(CreateProductDto dto);
    Task&amp;lt;bool&amp;gt; UpdateAsync(int id, CreateProductDto dto);
    Task&amp;lt;bool&amp;gt; DeleteAsync(int id);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is the layer that never existed in the bad version at all, the bad controller talked directly to SqlConnection. Having this interface is also what makes the controller genuinely unit-testable with a mock, something effectively impossible against the bad version's design.&lt;/p&gt;
&lt;h3 id=&quot;attribute-reference-what-s-doing-the-actual-work&quot;&gt;Attribute Reference: What's Doing the Actual Work&lt;/h3&gt;
&lt;p&gt;ApiController enables automatic model validation, checking a DTO's validation attributes and returning 400 automatically if they fail, before the action method body runs. Present in both versions, but only doing real work in the good one, since the bad version's Product parameter has no validation attributes for it to check.&lt;/p&gt;
&lt;p&gt;Route with the controller token sets the base route, &quot;api/products,&quot; identical in both versions.&lt;/p&gt;
&lt;p&gt;HttpGet, the id-scoped HttpGet, HttpPost, the id-scoped HttpPut, and the id-scoped HttpDelete map each method to its HTTP verb and route, the routing itself was never the problem in the bad version.&lt;/p&gt;
&lt;p&gt;Required, MaxLength, and Range on CreateProductDto are declarative validation rules, good version only, paired with ApiController to reject invalid requests automatically, with zero manual if-checks.&lt;/p&gt;
&lt;h3 id=&quot;every-issue-at-a-glance&quot;&gt;Every Issue, At a Glance&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Hardcoded connection string, in Program.cs and setup. &lt;br&gt;
Bad: literal password committed in source code. &lt;br&gt;
Good: loaded from configuration or Key Vault.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Incorrect service lifetime, AddSingleton for a controller, in Program.cs and setup. &lt;br&gt;
Bad: the controller manually registered as Singleton, one shared instance forever, risking a captive dependency once a Scoped DbContext enters the picture. &lt;br&gt;
Good: the controller is never manually registered at all, and IProductService is registered as Scoped, matching the DbContext's own lifetime.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Missing await / .Result, in GetAll. &lt;br&gt;
Bad: .Result blocks the thread synchronously. &lt;br&gt;
Good: await released the thread properly, all the way through.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Undisposed SqlCommand and SqlDataReader, in GetAll. &lt;br&gt;
Bad: only connection wrapped in a using, command and reader never explicitly disposed, relying on indirect cleanup that isn't reliable. &lt;br&gt;
Good: EF Core manages the underlying ADO.NET resources internally, so there's no manual disposal to get wrong.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;SQL injection, appearing in GetById, Create, Update, and Delete. &lt;br&gt;
Bad: string-concatenated raw SQL. &lt;br&gt;
Good: EF Core LINQ or FindAsync, parameterized automatically.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Ok(null) instead of NotFound(), in GetById. &lt;br&gt;
Bad: 200 OK with a null body when not found. &lt;br&gt;
Good: NotFound(), a genuine 404.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;No DTOs, full entity exposed, in GetById. &lt;br&gt;
Bad: internal fields like CostPrice and supplier notes exposed. &lt;br&gt;
Good: ProductDto excludes them entirely, structurally impossible to leak.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;No validation, in Create. &lt;br&gt;
Bad: any data accepted as-is. &lt;br&gt;
Good: Required, MaxLength, and Range attributes plus ApiController auto-reject invalid requests.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Wrong status codes, in Create and Delete. &lt;br&gt;
Bad: 200 OK for create and delete. &lt;br&gt;
Good: 201 Created with a Location header for create, 204 No Content for delete.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Sensitive data logged in plain text, in Create. &lt;br&gt;
Bad: an auth token logged directly, sitting in plain text in Application Insights. &lt;br&gt;
Good: only a user ID logged, never a credential or token.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Swallowed exception behavior masked as a response, in Update. &lt;br&gt;
Bad: a failure converted into a 200 OK response body, status code claims success while content says otherwise. &lt;br&gt;
Good: an honest result returned, false propagated up so the controller can respond correctly.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Full exception details leaked to the caller, in Update. &lt;br&gt;
Bad: ex.ToString(), including the full stack trace, returned directly in the API response. &lt;br&gt;
Good: the full exception logged internally only, a generic safe message, if any, returned to the caller.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;No service layer, structural, underneath all endpoints. &lt;br&gt;
Bad: controller does raw data access directly. &lt;br&gt;
Good: IProductService and ProductService, separated, testable, the natural home for every other fix.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;key-lessons&quot;&gt;Key Lessons&lt;/h3&gt;
&lt;p&gt;Every anti-pattern here is realistic, these are mistakes that genuinely appear in real production code, not exaggerated examples built just to be wrong.&lt;/p&gt;
&lt;p&gt;The missing service layer is structurally connected to most of the other issues, a controller doing raw data access directly removes the natural place validation, parameterization, and error handling would otherwise live.&lt;/p&gt;
&lt;p&gt;A DTO isn't just extra code, it's what makes leaking internal fields structurally impossible, rather than something a developer has to remember not to do.&lt;/p&gt;
&lt;p&gt;Status codes are information, not decoration, Ok(null), 200 for a create, and 200 for a delete each communicate something genuinely false or ambiguous to the caller.&lt;/p&gt;
&lt;p&gt;Logging and error responses are both genuine attack surfaces, not just debugging conveniences, a logged token or a leaked stack trace can hand an attacker exactly what they need, even when the code otherwise &quot;works.&quot;&lt;/p&gt;
&lt;p&gt;A swallowed exception, or one disguised as a success response, is worse than a crash, a crash is visible immediately, a silently hidden failure looks like success and can go unnoticed for a long time.&lt;/p&gt;
&lt;h3 id=&quot;summary&quot;&gt;Summary&lt;/h3&gt;
&lt;p&gt;Every issue in the bad version compiles, runs, and looks like reasonable code at a glance, which is exactly why these patterns survive in real codebases far longer than obviously broken code ever would. Rebuilding the same five endpoints with a real service layer, correct dependency lifetimes, disciplined resource disposal, DTOs, parameterized queries, genuine async, proper validation, correct status codes, and careful logging doesn't just fix thirteen individual bugs, it changes the shape of the code so that several of those bugs become structurally difficult to reintroduce by accident.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Originally published at TechStack Blog: &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.techstackblog.com/post.html?slug=csharp-api-crud-bad-vs-good-practices&quot;&gt;https://www.techstackblog.com/post.html?slug=csharp-api-crud-bad-vs-good-practices&lt;/a&gt; &lt;br&gt;
More from TechStack Blog: C# / .NET: &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.techstackblog.com/category.html?cat=csharp&quot;&gt;https://www.techstackblog.com/category.html?cat=csharp&lt;/a&gt;&lt;br&gt;
CS Fundamentals: &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.techstackblog.com/category.html?cat=cs-fundamentals&quot;&gt;https://www.techstackblog.com/category.html?cat=cs-fundamentals&lt;/a&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/26211/c-api-crud-done-wrong-then-done-right-complete-bad-practices-best-practices-walkthrough</guid>
<pubDate>Fri, 04 Sep 2026 14:12:06 +0000</pubDate>
</item>
<item>
<title>C# Coding Interview Prep Part 1: Variables, Strings, Arrays, Lists, Dictionaries, and Every Basic Building Block</title>
<link>https://coderlegion.com/26210/coding-interview-variables-strings-arrays-lists-dictionaries-every-basic-building-block</link>
<description>&lt;p&gt;This is a refresher built while preparing for C# coding interviews, and shared as a resource, because a genuinely comprehensive, practice-oriented guide is hard to find in one place. This series doesn't skip the basics to get to the &quot;interesting&quot; parts - the basics are exactly what trips people up under interview pressure, when a simple question about arrays suddenly requires recalling details nobody thinks about in day-to-day coding. Every topic in this series follows the same format: a plain-English explanation, an analogy, a code example, and a practice question at the end, so this can genuinely be used to test yourself, not just read passively.&lt;/p&gt;
&lt;p&gt;This is Part 1 of three. Part 1 covers the &lt;strong&gt;&lt;em&gt;basics &lt;/em&gt;&lt;/strong&gt;- variables, data types, strings, arrays, List, Dictionary&amp;lt;TKey,TValue&amp;gt;, HashSet, tuples, value vs reference types, operators, control flow, loops, methods, enums, structs vs classes, nullable types, type conversion, and basic exception handling. Part 2 covers &lt;strong&gt;&lt;em&gt;mid-level&lt;/em&gt;&lt;/strong&gt; concepts - LINQ, generics, delegates and events, interfaces vs abstract classes, and exception handling in depth. Part 3 covers &lt;strong&gt;&lt;em&gt;advanced &lt;/em&gt;&lt;/strong&gt;concepts - async and Task, reflection, memory management and garbage collection, design patterns in practice, and performance considerations.&lt;/p&gt;
&lt;h3 id=&quot;topic-1-variables-and-data-types&quot;&gt;Topic 1: Variables and Data Types&lt;/h3&gt;
&lt;p&gt;A variable is a named storage location holding a value of a specific type. C# is statically typed - once a variable is declared with a type, it can only ever hold values of that type, or something compatible with it.&lt;/p&gt;
&lt;p&gt;Think of a labeled storage box. A box labeled &quot;int&quot; can only hold integers - trying to put a sentence in it doesn't work, the same way a shoebox labeled for shoes doesn't neatly fit a stack of books.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;// Value types - the actual data lives directly
// in the variable
int age = 30;
double price = 19.99;
bool isPublished = true;
char grade = 'A';
decimal exactMoney = 19.99m;  // decimal for money -
                               // no floating point
                               // rounding errors
 
// var - the compiler infers the type at compile
// time, this is still statically typed, NOT dynamic
var name = &quot;Alex&quot;;  // compiler infers string
 
// Reference types - the variable holds a reference
// (address) to where the actual data lives
string title = &quot;C# Basics&quot;;
int[] numbers = { 1, 2, 3 };
Post post = new Post();
 
// Constants - value is fixed at compile time,
// cannot ever change
const double Pi = 3.14159;
 
// Nullable value types - value types normally
// cannot be null, this opts in
int? nullableAge = null;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Practice question: What is the difference between const and readonly in C#? Write one example of each.&lt;/p&gt;
&lt;h3 id=&quot;topic-2-strings-and-string-methods&quot;&gt;Topic 2: Strings and String Methods&lt;/h3&gt;
&lt;p&gt;A string is a sequence of characters, and in C# it's a reference type - but with a critical twist: strings are immutable. Once created, a string's actual content in memory never changes. Every operation that appears to modify a string actually creates a brand new string.&lt;/p&gt;
&lt;p&gt;Think of a printed book page. You cannot erase and rewrite a word on a printed page - if you want different text, you print an entirely new page. Modifying a string works the same way: the old string still exists in memory until garbage collected, and a new one is created for the result.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;string greeting = &quot;Hello&quot;;
string name = &quot;Manohari&quot;;
 
// Concatenation - creates a NEW string, does not
// modify 'greeting'
string message = greeting + &quot;, &quot; + name;
 
// String interpolation - cleaner syntax, same result
string message2 = $&quot;{greeting}, {name}&quot;;
 
// Common string methods
string text = &quot;  C# Interview Prep  &quot;;
Console.WriteLine(text.Trim());           // &quot;C# Interview Prep&quot;
Console.WriteLine(text.ToUpper());        // &quot;  C# INTERVIEW PREP  &quot;
Console.WriteLine(text.Contains(&quot;Prep&quot;)); // true
Console.WriteLine(text.Replace(&quot;C#&quot;, &quot;CSharp&quot;));
Console.WriteLine(text.Split(' ').Length);
Console.WriteLine(text.Substring(2, 4));  // extracts a portion
Console.WriteLine(text.IndexOf(&quot;Prep&quot;));  // position found at
 
// String comparison - == checks VALUE equality
// for strings specifically (special-cased), not
// reference equality
string a = &quot;hello&quot;;
string b = &quot;hello&quot;;
Console.WriteLine(a == b);         // true - value equality
Console.WriteLine(a.Equals(b));    // true - same result
 
// StringBuilder - for heavy concatenation, avoid
// creating thousands of throwaway strings in a loop
var sb = new StringBuilder();
for (int i = 0; i &amp;lt; 1000; i++)
{
    sb.Append(i).Append(&quot;, &quot;);
}
string result = sb.ToString();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Practice question: Write a method that reverses a string without using any built-in reverse method.&lt;/p&gt;
&lt;h3 id=&quot;topic-3-arrays&quot;&gt;Topic 3: Arrays&lt;/h3&gt;
&lt;p&gt;An array is a fixed-size, ordered collection of elements of the same type. The size is set at creation and cannot change afterward - to &quot;resize,&quot; a new array must be created and the old data copied over.&lt;/p&gt;
&lt;p&gt;Think of a row of numbered parking spaces painted onto pavement. The number of spaces is fixed the moment the lot is built - adding more spaces means literally repaving with a new, larger layout, not just adding to the existing one.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;// Declaration and initialization
int[] numbers = { 1, 2, 3, 4, 5 };
string[] names = new string[3];  // size 3, all null initially
names[0] = &quot;Alex&quot;;
names[1] = &quot;Priya&quot;;
names[2] = &quot;Sam&quot;;
 
// Accessing and modifying
Console.WriteLine(numbers[0]);   // 1
numbers[0] = 100;                // arrays ARE mutable in
                                  // content, just fixed size
 
// Iterating
foreach (var n in numbers)
{
    Console.WriteLine(n);
}
 
// Multi-dimensional array
int[,] grid = new int[3, 3];
grid[0, 0] = 1;
grid[1, 1] = 5;
 
// Jagged array - array of arrays, each inner
// array can be a different length
int[][] jagged = new int[3][];
jagged[0] = new int[] { 1, 2 };
jagged[1] = new int[] { 1, 2, 3, 4 };
 
// Common Array methods
Array.Sort(numbers);
Array.Reverse(numbers);
int index = Array.IndexOf(numbers, 100);
bool found = Array.Exists(numbers, n =&amp;gt; n &amp;gt; 50);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Practice question: Given an integer array, write a method that finds the second largest number without sorting the entire array.&lt;/p&gt;
&lt;h3 id=&quot;topic-4-list&quot;&gt;Topic 4: List&lt;/h3&gt;
&lt;p&gt;List is a resizable, ordered collection - the generic, flexible alternative to a fixed-size array. Internally it's backed by an array that gets replaced with a larger one automatically as items are added beyond current capacity.&lt;/p&gt;
&lt;p&gt;Think of an expandable file folder versus a fixed shoebox. As more documents come in, the expandable folder just grows - you never have to buy a new one and manually move everything over, even though internally that's conceptually similar to what List does behind the scenes automatically.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;var posts = new List&amp;lt;string&amp;gt;();
 
posts.Add(&quot;First Post&quot;);
posts.Add(&quot;Second Post&quot;);
posts.Insert(0, &quot;Inserted at start&quot;);
 
posts.Remove(&quot;First Post&quot;);     // removes by value
posts.RemoveAt(0);               // removes by index
 
bool exists = posts.Contains(&quot;Second Post&quot;);
int index = posts.IndexOf(&quot;Second Post&quot;);
 
Console.WriteLine(posts.Count);  // current item count
 
// Iterating
foreach (var post in posts)
{
    Console.WriteLine(post);
}
 
// Converting between List and Array
int[] array = { 1, 2, 3 };
List&amp;lt;int&amp;gt; fromArray = array.ToList();
int[] backToArray = fromArray.ToArray();
 
// Sorting and searching
var numbers = new List&amp;lt;int&amp;gt; { 5, 2, 8, 1 };
numbers.Sort();
numbers.Sort((a, b) =&amp;gt; b.CompareTo(a));  // descending,
                                            // custom comparer
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Practice question: Write a method that removes all duplicate values from a List while preserving the original order.&lt;/p&gt;
&lt;h3 id=&quot;topic-5-dictionary-lt-tkey-tvalue-gt-&quot;&gt;Topic 5: Dictionary&amp;lt;TKey, TValue&amp;gt;&lt;/h3&gt;
&lt;p&gt;A Dictionary stores key-value pairs and provides fast, close-to-O(1) lookup by key, using a hash table internally. Every key must be unique.&lt;/p&gt;
&lt;p&gt;Think of a labeled filing cabinet drawer system, where every drawer has exactly one unique name on it. Finding &quot;the invoices drawer&quot; doesn't require checking every drawer one by one - you go directly to the one labeled correctly.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;var postsBySlug = new Dictionary&amp;lt;string, string&amp;gt;();
 
postsBySlug[&quot;csharp-basics&quot;] = &quot;C# Basics Post&quot;;
postsBySlug[&quot;csharp-linq&quot;] = &quot;LINQ Deep Dive&quot;;
 
// Safe lookup - does not throw if key is missing
if (postsBySlug.TryGetValue(&quot;csharp-basics&quot;, out var title))
{
    Console.WriteLine(title);
}
 
// Direct access - throws KeyNotFoundException if missing
string result = postsBySlug[&quot;csharp-basics&quot;];
 
bool hasKey = postsBySlug.ContainsKey(&quot;csharp-linq&quot;);
postsBySlug.Remove(&quot;csharp-linq&quot;);
 
// Iterating - gives KeyValuePair&amp;lt;TKey,TValue&amp;gt;
foreach (var kvp in postsBySlug)
{
    Console.WriteLine($&quot;{kvp.Key}: {kvp.Value}&quot;);
}
 
// Just keys or just values
foreach (var key in postsBySlug.Keys) { }
foreach (var value in postsBySlug.Values) { }
 
// Update or add in one call
postsBySlug[&quot;csharp-basics&quot;] = &quot;Updated Title&quot;;  // overwrites
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Practice question: Given an array of words, write a method using a Dictionary that counts how many times each word appears.&lt;/p&gt;
&lt;h3 id=&quot;topic-6-hashset&quot;&gt;Topic 6: HashSet&lt;/h3&gt;
&lt;p&gt;A HashSet stores unique values only, with no guaranteed order, and provides fast O(1) average lookup for &quot;does this exist&quot; checks - much faster than checking a List with Contains for large collections.&lt;/p&gt;
&lt;p&gt;Think of a guest list at the door of an event, where a bouncer just needs to answer &quot;is this name already checked in&quot; as fast as possible - not caring about the order names were added, just whether a given name is present at all.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;var uniqueTags = new HashSet&amp;lt;string&amp;gt;();
 
uniqueTags.Add(&quot;Azure&quot;);
uniqueTags.Add(&quot;C#&quot;);
uniqueTags.Add(&quot;Azure&quot;);  // silently ignored - duplicate
 
Console.WriteLine(uniqueTags.Count);  // 2
 
bool hasAzure = uniqueTags.Contains(&quot;Azure&quot;);  // O(1) check
 
// Set operations
var setA = new HashSet&amp;lt;int&amp;gt; { 1, 2, 3, 4 };
var setB = new HashSet&amp;lt;int&amp;gt; { 3, 4, 5, 6 };
 
setA.IntersectWith(setB);  // setA becomes {3, 4}
// UnionWith, ExceptWith also available
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Practice question: Given two integer arrays, write a method that returns only the numbers that appear in both arrays, using a HashSet.&lt;/p&gt;
&lt;h3 id=&quot;topic-7-tuples&quot;&gt;Topic 7: Tuples&lt;/h3&gt;
&lt;p&gt;A tuple bundles multiple values together into one object without needing to define a custom class. Modern C# tuples support named elements, making them readable at the point of use.&lt;/p&gt;
&lt;p&gt;Think of a small labeled tray carrying a few different items together - a tray with &quot;coffee&quot; and &quot;muffin&quot; compartments lets you carry both at once without needing a whole custom carrying case built just for that specific combination.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;// Named tuple
(string Name, int Age) person = (&quot;Alex&quot;, 30);
Console.WriteLine(person.Name);  // &quot;Alex&quot;
Console.WriteLine(person.Age);   // 30
 
// Returning multiple values from a method
(int Min, int Max) FindMinMax(int[] numbers)
{
    return (numbers.Min(), numbers.Max());
}
 
var result = FindMinMax(new[] { 5, 2, 8, 1 });
Console.WriteLine($&quot;Min: {result.Min}, Max: {result.Max}&quot;);
 
// Deconstruction
var (min, max) = FindMinMax(new[] { 5, 2, 8, 1 });
Console.WriteLine(min);
Console.WriteLine(max);
 
// Tuple vs custom class - when to use which
// Tuple: quick, internal, 2-3 short-lived values
// Class: the shape has real meaning, gets passed
// around broadly, or needs its own behavior/methods
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Practice question: Write a method that takes a list of numbers and returns a tuple containing the sum, average, and count in one call.&lt;/p&gt;
&lt;h3 id=&quot;topic-8-value-types-vs-reference-types&quot;&gt;Topic 8: Value Types vs Reference Types&lt;/h3&gt;
&lt;p&gt;This is one of the most frequently tested distinctions in C# interviews. Value types - int, bool, double, struct - store their actual data directly in the variable. Reference types - class, array, string, List, Dictionary - store a reference, a memory address, to where the actual data lives on the heap. The variable itself just holds that address.&lt;/p&gt;
&lt;p&gt;Think of a value type as writing a number directly on a sticky note - the note is the data. A reference type is like writing a house address on a sticky note - the note points to where the actual house is, and copying the note just copies the address, not the house itself.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;// Value type - assignment COPIES the actual value
int a = 5;
int b = a;   // b gets its OWN copy of 5
b = 10;
Console.WriteLine(a);  // still 5 - a was never touched
 
// Reference type - assignment copies the REFERENCE,
// both variables point to the SAME object
var list1 = new List&amp;lt;int&amp;gt; { 1, 2, 3 };
var list2 = list1;   // list2 points to the SAME list
list2.Add(4);
Console.WriteLine(list1.Count);  // 4 - list1 sees the
                                   // change too, because
                                   // it's the same object
 
// Passing to methods - same rule applies
void ModifyValue(int x) { x = 100; }
void ModifyReference(List&amp;lt;int&amp;gt; list) { list.Add(999); }
 
int number = 5;
ModifyValue(number);
Console.WriteLine(number);  // still 5
 
var myList = new List&amp;lt;int&amp;gt; { 1, 2 };
ModifyReference(myList);
Console.WriteLine(myList.Count);  // 3 - the SAME list
                                    // object was modified
 
// string is a special case - reference type, but
// immutable, so it BEHAVES like a value type in
// most practical situations
string s1 = &quot;hello&quot;;
string s2 = s1;
s2 = s2 + &quot; world&quot;;  // creates a NEW string for s2
Console.WriteLine(s1);  // still &quot;hello&quot; - unaffected
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Practice question: Without running the code, predict the output: create a struct Point with a public int X, create var p1 = new Point with X set to 1, assign var p2 = p1, then set p2.X to 99. What is p1.X afterward, and why?&lt;/p&gt;
&lt;h3 id=&quot;topic-9-operators&quot;&gt;Topic 9: Operators&lt;/h3&gt;
&lt;p&gt;Operators perform operations on values - arithmetic, comparison, logical, and a few special ones worth knowing well for interviews specifically.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;// Arithmetic
int sum = 5 + 3;
int remainder = 10 % 3;   // modulo - remainder after division
 
// Comparison
bool isEqual = (5 == 5);
bool isGreater = (5 &amp;gt; 3);
 
// Logical
bool result = true &amp;amp;&amp;amp; false;   // AND
bool result2 = true || false;  // OR
bool result3 = !true;           // NOT
 
// Null-coalescing - returns left if not null,
// otherwise returns right
string name = null;
string display = name ?? &quot;Unknown&quot;;  // &quot;Unknown&quot;
 
// Null-conditional - safely accesses a member,
// returns null instead of throwing if the object
// itself is null
Post post = null;
string title = post?.Title;  // null, no exception thrown
 
// Null-coalescing assignment
string cachedValue = null;
cachedValue ??= &quot;default&quot;;  // assigns only if currently null
 
// Ternary conditional
int age = 20;
string category = age &amp;gt;= 18 ? &quot;Adult&quot; : &quot;Minor&quot;;
 
// Increment/decrement
int count = 0;
count++;      // post-increment
++count;      // pre-increment - subtle difference in
              // expressions, same end result standalone
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Practice question: What is the difference between x++ and ++x when used inside a larger expression, like int y = x++ + 1 versus int y = ++x + 1? Write out what each assigns to x and y given x starts at 5.&lt;/p&gt;
&lt;h3 id=&quot;topic-10-control-flow---if-else-switch&quot;&gt;Topic 10: Control Flow - if, else, switch&lt;/h3&gt;
&lt;p&gt;Control flow statements decide which code actually runs based on conditions.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;int score = 85;
 
// if / else if / else
if (score &amp;gt;= 90)
{
    Console.WriteLine(&quot;A&quot;);
}
else if (score &amp;gt;= 80)
{
    Console.WriteLine(&quot;B&quot;);
}
else
{
    Console.WriteLine(&quot;C or below&quot;);
}
 
// switch statement
string grade;
switch (true)
{
    case bool _ when score &amp;gt;= 90:
        grade = &quot;A&quot;;
        break;
    case bool _ when score &amp;gt;= 80:
        grade = &quot;B&quot;;
        break;
    default:
        grade = &quot;C or below&quot;;
        break;
}
 
// switch expression (modern C#, more concise)
string grade2 = score switch
{
    &amp;gt;= 90 =&amp;gt; &quot;A&quot;,
    &amp;gt;= 80 =&amp;gt; &quot;B&quot;,
    _ =&amp;gt; &quot;C or below&quot;
};
 
// Pattern matching in switch
object value = 42;
string description = value switch
{
    int i when i &amp;gt; 0 =&amp;gt; &quot;positive integer&quot;,
    int i when i &amp;lt; 0 =&amp;gt; &quot;negative integer&quot;,
    string s =&amp;gt; $&quot;a string: {s}&quot;,
    null =&amp;gt; &quot;null value&quot;,
    _ =&amp;gt; &quot;something else&quot;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Practice question: Write a method using a switch expression that takes an integer representing a day of the week (1-7) and returns whether it's a &quot;Weekday&quot; or &quot;Weekend&quot;.&lt;/p&gt;
&lt;h3 id=&quot;topic-11-loops&quot;&gt;Topic 11: Loops&lt;/h3&gt;
&lt;p&gt;Loops repeat a block of code, either a fixed number of times, while a condition holds, or once per element in a collection.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;// for loop - when you know the iteration count
// or need the index
for (int i = 0; i &amp;lt; 5; i++)
{
    Console.WriteLine(i);
}
 
// while loop - condition checked BEFORE each iteration
int count = 0;
while (count &amp;lt; 5)
{
    Console.WriteLine(count);
    count++;
}
 
// do-while - condition checked AFTER each iteration,
// so the body ALWAYS runs at least once
int x = 10;
do
{
    Console.WriteLine(x);
    x++;
} while (x &amp;lt; 5);  // runs once even though condition
                    // is false from the start
 
// foreach - iterates every element in a collection,
// no manual index management
var posts = new List&amp;lt;string&amp;gt; { &quot;A&quot;, &quot;B&quot;, &quot;C&quot; };
foreach (var post in posts)
{
    Console.WriteLine(post);
}
 
// break and continue
for (int i = 0; i &amp;lt; 10; i++)
{
    if (i == 3) continue;  // skip this iteration
    if (i == 7) break;     // exit the loop entirely
    Console.WriteLine(i);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Practice question: Write a program using a loop that prints the Fibonacci sequence up to the 10th number.&lt;/p&gt;
&lt;h3 id=&quot;topic-12-methods-and-parameters&quot;&gt;Topic 12: Methods and Parameters&lt;/h3&gt;
&lt;p&gt;A method is a named, reusable block of code. Parameters pass data into a method; a return type defines what comes back out.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;// Basic method
int Add(int a, int b)
{
    return a + b;
}
 
// Optional parameters - must come after required ones
void Greet(string name, string greeting = &quot;Hello&quot;)
{
    Console.WriteLine($&quot;{greeting}, {name}&quot;);
}
Greet(&quot;Alex&quot;);               // uses default &quot;Hello&quot;
Greet(&quot;Alex&quot;, &quot;Hi&quot;);         // overrides default
 
// Named arguments - can be passed in any order
Greet(greeting: &quot;Hey&quot;, name: &quot;Alex&quot;);
 
// params - accepts a variable number of arguments
int Sum(params int[] numbers)
{
    int total = 0;
    foreach (var n in numbers) total += n;
    return total;
}
Sum(1, 2, 3);        // works
Sum(1, 2, 3, 4, 5);  // also works, any count
 
// ref - passes by REFERENCE, method can modify
// the caller's actual variable
void Double(ref int number)
{
    number = number * 2;
}
int value = 5;
Double(ref value);
Console.WriteLine(value);  // 10 - actually changed
 
// out - similar to ref, but the method MUST assign
// a value before returning, commonly used for
// &quot;try&quot; pattern methods
bool TryParse(string input, out int result)
{
    return int.TryParse(input, out result);
}
 
// Method overloading - same name, different
// parameter signatures
int Multiply(int a, int b) =&amp;gt; a * b;
double Multiply(double a, double b) =&amp;gt; a * b;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Practice question: What is the actual difference between ref and out parameters? Write a method using each that demonstrates the difference.&lt;/p&gt;
&lt;h3 id=&quot;topic-13-enums&quot;&gt;Topic 13: Enums&lt;/h3&gt;
&lt;p&gt;An enum defines a named set of related constant values, making code more readable than using raw numbers or strings for a fixed set of options.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;public enum OrderStatus
{
    Pending,     // 0 by default
    Processing,  // 1
    Shipped,     // 2
    Delivered    // 3
}
 
var status = OrderStatus.Processing;
Console.WriteLine(status);          // &quot;Processing&quot;
Console.WriteLine((int)status);      // 1
 
// Assigning explicit values
public enum HttpStatusCode
{
    Ok = 200,
    NotFound = 404,
    ServerError = 500
}
 
// Converting from an int
var code = (HttpStatusCode)404;
Console.WriteLine(code);  // NotFound
 
// Parsing from a string
var parsed = Enum.Parse&amp;lt;OrderStatus&amp;gt;(&quot;Shipped&quot;);
 
// Switching on an enum
string GetMessage(OrderStatus status) =&amp;gt; status switch
{
    OrderStatus.Pending =&amp;gt; &quot;Order received&quot;,
    OrderStatus.Shipped =&amp;gt; &quot;On its way&quot;,
    OrderStatus.Delivered =&amp;gt; &quot;Completed&quot;,
    _ =&amp;gt; &quot;Unknown status&quot;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Practice question: Create an enum for days of the week, and write a method that returns true if a given day is a weekend.&lt;/p&gt;
&lt;h3 id=&quot;topic-14-structs-vs-classes&quot;&gt;Topic 14: Structs vs Classes&lt;/h3&gt;
&lt;p&gt;Both structs and classes group related data and behavior together, but a struct is a value type, copied on assignment, while a class is a reference type, shared on assignment - the same fundamental distinction as value types versus reference types, applied to your own custom types.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;// struct - value type, typically small, simple data
public struct Point
{
    public int X;
    public int Y;
}
 
var p1 = new Point { X = 1, Y = 2 };
var p2 = p1;       // COPIES the values
p2.X = 99;
Console.WriteLine(p1.X);  // still 1 - p1 untouched
 
// class - reference type, typically larger objects
// with behavior, identity, and mutable state over time
public class Post
{
    public string Title;
    public int ViewCount;
}
 
var post1 = new Post { Title = &quot;Hello&quot;, ViewCount = 0 };
var post2 = post1;  // SHARES the same object
post2.ViewCount = 5;
Console.WriteLine(post1.ViewCount);  // 5 - same object
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Use a struct for small, immutable-feeling data with no real identity - a Point, a Color, a Money amount. Use a class for anything with real identity, larger data, or behavior that changes over the object's lifetime - a Post, a Customer, a Service. Most day-to-day domain modeling uses classes; structs are the exception, reached for deliberately.&lt;/p&gt;
&lt;p&gt;Practice question: Explain why using a struct for a large object with 15 fields would generally be a bad idea, based on what you know about value type copying.&lt;/p&gt;
&lt;h3 id=&quot;topic-15-nullable-types&quot;&gt;Topic 15: Nullable Types&lt;/h3&gt;
&lt;p&gt;Value types - int, bool, DateTime - cannot normally hold null; they always have some default value. Nullable value types, written as int?, opt into allowing null, useful for representing &quot;no value provided&quot; distinctly from an actual zero or false.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;int? age = null;
int regularAge = 0;  // 0 is a real value,
                       // different from &quot;no value&quot;
 
if (age.HasValue)
{
    Console.WriteLine(age.Value);
}
 
// GetValueOrDefault - safe access with a fallback
int actualAge = age.GetValueOrDefault(18);
 
// Null-coalescing works here too
int displayAge = age ?? 18;
 
// Nullable reference types (C# 8+) - a compiler
// feature (not a runtime type change) that WARNS
// when a reference type that should never be null
// might actually be null
string? nullableName = null;   // explicitly allowed
string nonNullableName = &quot;Alex&quot;; // compiler warns if
                                   // this is ever assigned
                                   // null somewhere
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Practice question: Why might a nullable int? be a better choice than a regular int for a &quot;discount percentage&quot; field on an order, where most orders have no discount at all?&lt;/p&gt;
&lt;h3 id=&quot;topic-16-type-conversion-and-casting&quot;&gt;Topic 16: Type Conversion and Casting&lt;/h3&gt;
&lt;p&gt;Converting a value from one type to another - sometimes safe and implicit, sometimes requiring an explicit cast, and sometimes needing a parsing method entirely.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;// Implicit conversion - safe, no data loss possible,
// compiler does it automatically
int number = 100;
double asDouble = number;  // int fits safely into double
 
// Explicit conversion (casting) - REQUIRED when data
// loss is possible, forces you to acknowledge the risk
double price = 19.99;
int roundedDown = (int)price;  // 19 - truncates,
                                 // does not round
 
// Parsing strings to numbers
string input = &quot;42&quot;;
int parsed = int.Parse(input);        // throws if invalid
bool success = int.TryParse(input, out int result);
                                        // safe, returns
                                        // false instead of
                                        // throwing
 
// Convert class - handles more conversions, including
// null handling
string text = Convert.ToString(42);
int fromString = Convert.ToInt32(&quot;42&quot;);
 
// as vs cast for reference types
object obj = &quot;hello&quot;;
string s1 = (string)obj;      // throws if obj isn't
                                // actually a string
string s2 = obj as string;    // returns null instead
                                // of throwing if it fails
 
// is keyword with pattern matching
if (obj is string str)
{
    Console.WriteLine(str.Length);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Practice question: What is the actual difference between int.Parse() and int.TryParse()? When would you genuinely prefer one over the other?&lt;/p&gt;
&lt;h3 id=&quot;topic-17-basic-exception-handling&quot;&gt;Topic 17: Basic Exception Handling&lt;/h3&gt;
&lt;p&gt;Exceptions represent errors that occur during execution. try/catch lets code handle an error gracefully instead of crashing the entire program.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;try
{
    int result = 10 / int.Parse(&quot;0&quot;);
}
catch (DivideByZeroException ex)
{
    Console.WriteLine($&quot;Cannot divide by zero: {ex.Message}&quot;);
}
catch (FormatException ex)
{
    Console.WriteLine($&quot;Invalid number format: {ex.Message}&quot;);
}
catch (Exception ex)
{
    // Generic catch-all - should be LAST, since more
    // specific catches above it would never be reached
    // if this came first
    Console.WriteLine($&quot;Unexpected error: {ex.Message}&quot;);
}
finally
{
    // ALWAYS runs, whether an exception occurred or not -
    // used for cleanup (closing files, connections, etc.)
    Console.WriteLine(&quot;Cleanup runs here regardless&quot;);
}
 
// Throwing your own exceptions
void ValidateAge(int age)
{
    if (age &amp;lt; 0)
    {
        throw new ArgumentException(&quot;Age cannot be negative&quot;, nameof(age));
    }
}
 
// Custom exception types
public class InsufficientFundsException : Exception
{
    public InsufficientFundsException(string message) : base(message) { }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Practice question: Write a method that safely divides two numbers, catching a divide-by-zero error and returning 0 instead of letting the exception propagate.&lt;/p&gt;
&lt;h3 id=&quot;key-lessons-1&quot;&gt;Key Lessons&lt;/h3&gt;
&lt;p&gt;Value types copy on assignment; reference types share the same underlying object - this single distinction explains a huge share of &quot;unexpected&quot; C# behavior.&lt;/p&gt;
&lt;p&gt;Arrays are fixed size; List resizes automatically - default to List unless the size is genuinely fixed and known.&lt;/p&gt;
&lt;p&gt;Dictionary&amp;lt;TKey,TValue&amp;gt; gives fast key-based lookup; HashSet gives fast existence checks with no duplicates.&lt;/p&gt;
&lt;p&gt;Strings are immutable - every apparent modification creates a new string, which is exactly why StringBuilder exists for heavy concatenation.&lt;/p&gt;
&lt;p&gt;ref and out both pass by reference, but out requires the method to assign a value before returning, commonly used in the TryParse pattern.&lt;/p&gt;
&lt;p&gt;Structs and classes mirror the value versus reference type distinction for your own custom types - most everyday modeling uses classes.&lt;/p&gt;
&lt;h3 id=&quot;what-s-next-1&quot;&gt;What's Next&lt;/h3&gt;
&lt;p&gt;Part 2 covers mid-level concepts - LINQ, generics, delegates and events, interfaces versus abstract classes, and exception handling in real depth.&lt;/p&gt;
&lt;h3 id=&quot;summary-1&quot;&gt;Summary&lt;/h3&gt;
&lt;p&gt;Every C# codebase, no matter how advanced, is built from these same basic pieces - variables and types, strings, collections, control flow, methods, and basic error handling. Interview questions on these topics aren't testing whether you've memorized syntax - they're testing whether the underlying model, value versus reference, mutable versus immutable, fixed versus resizable, is genuinely internalized, since that's what actually predicts whether unfamiliar code will behave the way you expect it to.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Originally published at TechStack Blog: &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.techstackblog.com/post.html?slug=csharp-interview-prep-basics-part1&quot;&gt;https://www.techstackblog.com/post.html?slug=csharp-interview-prep-basics-part1&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;More from TechStack Blog: C# / .NET: &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.techstackblog.com/category.html?cat=csharp&quot;&gt;https://www.techstackblog.com/category.html?cat=csharp&lt;/a&gt;&lt;br&gt;
CS Fundamentals: &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.techstackblog.com/category.html?cat=cs-fundamentals&quot;&gt;https://www.techstackblog.com/category.html?cat=cs-fundamentals&lt;/a&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/26210/coding-interview-variables-strings-arrays-lists-dictionaries-every-basic-building-block</guid>
<pubDate>Fri, 04 Sep 2026 14:08:32 +0000</pubDate>
</item>
<item>
<title>Building a Custom Modbus TCP Client in .NET: What a Production-Grade Poller Actually Needs</title>
<link>https://coderlegion.com/25363/building-a-custom-modbus-tcp-client-in-net-what-a-production-grade-poller-actually-needs</link>
<description>&lt;p&gt;Most Modbus tutorials in .NET end at the same place: install a library, call &lt;code&gt;ReadHoldingRegisters&lt;/code&gt;, print a number. That is enough to prove a device is reachable. It is not enough to run anything.&lt;/p&gt;
&lt;p&gt;The moment you point that code at more than one device and leave it running, the questions change. What happens when a device stops responding? What happens when one slow device blocks the others? Why is that float reading 3.4e38 instead of 480? Where does the data actually go?&lt;/p&gt;
&lt;p&gt;This article covers the layer that answers those questions: the application code you build around a Modbus library. Everything here runs against a simulated device included in the code, so you can execute the whole thing locally with nothing but the .NET SDK. All values and device names are illustrative.&lt;/p&gt;
&lt;h3 id=&quot;a-short-modbus-refresher&quot;&gt;A short Modbus refresher&lt;/h3&gt;
&lt;p&gt;Modbus TCP is a request/reply protocol. A client opens a TCP connection (port 502 by convention) and asks a device for data. The device answers. There is no subscription and no push, so the client polls on a schedule.&lt;/p&gt;
&lt;p&gt;Data lives in four spaces:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Space &lt;/th&gt;&lt;th&gt;Size &lt;/th&gt;&lt;th&gt;Access&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Coils &lt;/td&gt;&lt;td&gt;1 bit &lt;/td&gt;&lt;td&gt;read/write&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Discrete inputs &lt;/td&gt;&lt;td&gt;1 bit &lt;/td&gt;&lt;td&gt;read&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Input registers &lt;/td&gt;&lt;td&gt;16 bit &lt;/td&gt;&lt;td&gt;read&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Holding registers &lt;/td&gt;&lt;td&gt;16 bit &lt;/td&gt;&lt;td&gt;read/write&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Function code 3 reads holding registers and is the one you will use most. A request carries a transaction ID, a unit ID, a starting address, and a register count. The response carries the raw 16-bit words back.&lt;/p&gt;
&lt;p&gt;Two properties of that design drive most of what follows. Registers are only 16 bits wide, so anything larger spans several of them. And the wire format is big-endian, which will not match your CPU.&lt;/p&gt;
&lt;h3 id=&quot;just-use-a-library-then-keep-going&quot;&gt;Just use a library, then keep going&lt;/h3&gt;
&lt;p&gt;Do not reimplement Modbus framing for production. &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/NModbus/NModbus&quot;&gt;NModbus&lt;/a&gt;, &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Apollo3zehn/FluentModbus&quot;&gt;FluentModbus&lt;/a&gt;, and &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/rossmann-engineering/EasyModbusTCP.NET&quot;&gt;EasyModbusTCP&lt;/a&gt; all handle the wire protocol, and FluentModbus ships a server implementation that is useful for testing.&lt;/p&gt;
&lt;p&gt;What none of them give you is a client. They give you a read call. The distance between a read call and something you can deploy is:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;connection lifecycle across devices that disappear and come back&lt;/li&gt;
&lt;li&gt;a polling schedule per device that tolerates slow or dead peers&lt;/li&gt;
&lt;li&gt;decoding raw registers into typed values you can reason about&lt;/li&gt;
&lt;li&gt;somewhere for the data to go, decoupled from how it was acquired&lt;/li&gt;
&lt;li&gt;enough visibility to know when a device went quiet&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That is the custom part. It is specific to your system, which is exactly why no package solves it for you.&lt;/p&gt;
&lt;p&gt;The code below writes the framing directly against &lt;code&gt;System.Net.Sockets&lt;/code&gt; so the example has zero dependencies and you can read every byte. In a real build, swap that piece for a library and keep the structure around it.&lt;/p&gt;
&lt;h3 id=&quot;component-1-decoding-registers-into-typed-values&quot;&gt;Component 1: decoding registers into typed values&lt;/h3&gt;
&lt;p&gt;This is where most first attempts break, so start here.&lt;/p&gt;
&lt;p&gt;A 32-bit float occupies two consecutive registers. To reconstruct it you concatenate both words in the order the device sent them and read the result as big-endian. Get the order wrong and you do not get an error, you get a plausible-looking wrong number, or a value like &lt;code&gt;1.7e38&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Rather than scatter that logic through the codebase, describe the device as data:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;enum RegisterType { Float32, UInt16Enum }

record RegisterDefinition(string Name, ushort Address, RegisterType Type, string Unit);

record Reading(string DeviceName, string PointName, object Value, string Unit, DateTime TimestampUtc);

static class RegisterDecoder
{
    public static object Decode(RegisterDefinition def, ushort[] regs, int offset)
    {
        switch (def.Type)
        {
            case RegisterType.Float32:
                Span&amp;lt;byte&amp;gt; buf = stackalloc byte[4];
                BinaryPrimitives.WriteUInt16BigEndian(buf.Slice(0, 2), regs[offset]);
                BinaryPrimitives.WriteUInt16BigEndian(buf.Slice(2, 2), regs[offset + 1]);
                return Math.Round(BinaryPrimitives.ReadSingleBigEndian(buf), 2);

            case RegisterType.UInt16Enum:
                return regs[offset] switch
                {
                    0 =&amp;gt; &quot;Stopped&quot;,
                    1 =&amp;gt; &quot;Running&quot;,
                    _ =&amp;gt; &quot;Fault&quot;
                };

            default:
                throw new NotSupportedException();
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A register map then becomes a list, not code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;var map = new List&amp;lt;RegisterDefinition&amp;gt;
{
    new(&quot;Voltage&quot;,   0, RegisterType.Float32,    &quot;V&quot;),
    new(&quot;Frequency&quot;, 2, RegisterType.Float32,    &quot;Hz&quot;),
    new(&quot;Status&quot;,    4, RegisterType.UInt16Enum, &quot;&quot;),
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Two things worth knowing before you fight them. Vendors disagree about word order, so some devices need the two registers swapped before decoding, which is why a &lt;code&gt;WordSwapped&lt;/code&gt; flag usually appears in the map eventually. And documentation often numbers registers from 1 while the protocol numbers them from 0, which is a very common source of off-by-one addressing.&lt;/p&gt;
&lt;h3 id=&quot;component-2-connection-management&quot;&gt;Component 2: connection management&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;BinaryPrimitives&lt;/code&gt; handles endianness. The socket handles nothing.&lt;/p&gt;
&lt;p&gt;Set explicit timeouts. Without them a silent device leaves you waiting on a read that never returns, and one stalled device can quietly stop your entire pipeline.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;public async Task ConnectAsync(CancellationToken ct)
{
    _client = new TcpClient { ReceiveTimeout = 1000, SendTimeout = 1000 };
    await _client.ConnectAsync(_host, _port, ct);
    _stream = _client.GetStream();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Also, never assume one &lt;code&gt;ReadAsync&lt;/code&gt; returns a whole frame. TCP is a stream, not a message queue. A response can arrive in pieces, and the header tells you how many bytes to expect, so read until you have them all:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;private async Task ReadExactAsync(byte[] buffer, CancellationToken ct)
{
    int read = 0;
    while (read &amp;lt; buffer.Length)
    {
        int n = await _stream!.ReadAsync(buffer.AsMemory(read, buffer.Length - read), ct);
        if (n == 0) throw new IOException(&quot;Connection closed by device.&quot;);
        read += n;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Skipping that check is the classic bug that works perfectly on localhost and fails intermittently on a real network.&lt;/p&gt;
&lt;h3 id=&quot;component-3-the-polling-engine&quot;&gt;Component 3: the polling engine&lt;/h3&gt;
&lt;p&gt;Each device gets its own loop and its own interval. Independent loops mean a dead device cannot delay a healthy one.&lt;/p&gt;
&lt;p&gt;Failures are normal, not exceptional. Devices reboot, networks drop, technicians unplug things. The loop treats a failure as an expected state and backs off exponentially instead of hammering a device that is down:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;catch (Exception ex)
{
    _failures++;
    conn?.Dispose();
    conn = null;

    var delay = TimeSpan.FromMilliseconds(
        Math.Min(200 * Math.Pow(2, _failures - 1), 3000));

    Console.WriteLine($&quot;{_cfg.Name}: read failed ({ex.GetType().Name}), &quot; +
                      $&quot;retry #{_failures} in {delay.TotalMilliseconds:F0}ms&quot;);

    await Task.Delay(delay, ct);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;One detail is easy to get wrong, and I got it wrong first while writing this. Reset the failure counter after a successful &lt;strong&gt;read&lt;/strong&gt;, not a successful &lt;strong&gt;connect&lt;/strong&gt;. If you reset on connect, a device that accepts connections but immediately drops them looks healthy every cycle, the counter returns to zero, and the backoff never grows. You reconnect in a tight loop forever. The captured output below shows the corrected behavior.&lt;/p&gt;
&lt;h3 id=&quot;component-4-a-pluggable-sink&quot;&gt;Component 4: a pluggable sink&lt;/h3&gt;
&lt;p&gt;Polling and delivery are separate concerns. Keep them apart behind an interface:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;interface IReadingSink
{
    Task WriteAsync(IReadOnlyList&amp;lt;Reading&amp;gt; readings, CancellationToken ct);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The demo uses a console sink. A real deployment might implement the same interface over Azure Event Hubs, MQTT, Kafka, or a time-series database, and a batching decorator that buffers readings and flushes on an interval slots in without the polling loop knowing about it.&lt;/p&gt;
&lt;p&gt;The payoff is testability. The polling logic never knows where data lands, so you can verify it against a fake sink and a simulator with no cloud account involved.&lt;/p&gt;
&lt;h3 id=&quot;running-it&quot;&gt;Running it&lt;/h3&gt;
&lt;p&gt;The full program starts two simulated devices, points three pollers at them (the third at a port where nothing is listening), and takes one device offline mid-run. Real captured output:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-console&quot;&gt;[18:50:21] device-01: connected
[18:50:21] device-02: connected
[18:50:21] device-03: read failed (SocketException), retry #1 in 200ms
  [18:50:21] device-01  Voltage    = 478.99   V
  [18:50:21] device-01  Frequency  = 59.92    Hz
  [18:50:21] device-01  Status     = Running
  [18:50:21] device-02  Voltage    = 481.08   V
  [18:50:21] device-02  Frequency  = 59.98    Hz
  [18:50:21] device-02  Status     = Stopped
[18:50:21] device-03: read failed (SocketException), retry #2 in 400ms
[18:50:22] device-03: read failed (SocketException), retry #3 in 800ms
  [18:50:22] device-01  Voltage    = 481.09   V
  [18:50:22] device-01  Frequency  = 60.03    Hz
  [18:50:22] device-01  Status     = Running
[18:50:22] device-03: read failed (SocketException), retry #4 in 1600ms
  [18:50:23] device-02  Voltage    = 481.94   V
  [18:50:23] device-02  Frequency  = 59.92    Hz
  [18:50:23] device-02  Status     = Stopped
[18:50:24] device-03: read failed (SocketException), retry #5 in 3000ms
[18:50:25] *** simulating device-02 going offline ***
  [18:50:25] device-01  Voltage    = 479.28   V
  [18:50:25] device-01  Frequency  = 60.1     Hz
  [18:50:25] device-01  Status     = Fault
[18:50:27] device-02: read failed (IOException), retry #1 in 200ms
[18:50:27] device-03: read failed (SocketException), retry #6 in 3000ms
  [18:50:27] device-01  Voltage    = 480.82   V
  [18:50:27] device-01  Frequency  = 60.04    Hz
  [18:50:27] device-01  Status     = Fault
[18:50:27] device-02: connected
[18:50:27] device-02: read failed (IOException), retry #2 in 400ms
[18:50:28] device-02: connected
[18:50:28] device-02: read failed (IOException), retry #3 in 800ms
  [18:50:28] device-01  Voltage    = 478.37   V
  [18:50:28] device-01  Frequency  = 59.93    Hz
  [18:50:28] device-01  Status     = Running
[18:50:28] device-02: connected
[18:50:28] device-02: read failed (IOException), retry #4 in 1600ms
Shutdown complete.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The behavior worth noting: device-01 polls on schedule the entire time, unaffected by two failing neighbors. device-03, which never existed, walks its backoff up to the 3 second ceiling instead of spinning. device-02 degrades gracefully when pulled offline, and its retry counter climbs correctly because it only resets on a successful read.&lt;/p&gt;
&lt;h3 id=&quot;what-this-buys-you&quot;&gt;What this buys you&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Failure isolation.&lt;/strong&gt; One bad device costs you that device, not the fleet.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Testability.&lt;/strong&gt; A simulator plus a fake sink means the whole pipeline runs in CI.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Decoupled evolution.&lt;/strong&gt; Protocol handling, decoding, and delivery change independently. Swapping Event Hubs for Kafka touches one class.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Configuration over code.&lt;/strong&gt; New device model, new register map entry, no redeploy of polling logic.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Operability.&lt;/strong&gt; Per-device failure counts and timestamps tell you which device went quiet and when.&lt;/p&gt;
&lt;h3 id=&quot;when-not-to-build-this&quot;&gt;When not to build this&lt;/h3&gt;
&lt;p&gt;If you have one device, read it occasionally, and can tolerate a crash and restart, use a library directly in twenty lines and stop reading. The structure here earns its cost when you have a fleet, uneven reliability, and a downstream system that expects a steady stream.&lt;/p&gt;
&lt;p&gt;If your devices already speak MQTT or OPC UA, prefer those. Modbus is a 1979 protocol with no security model and no push, and you should not add a polling layer you do not need.&lt;/p&gt;
&lt;h3 id=&quot;takeaways&quot;&gt;Takeaways&lt;/h3&gt;
&lt;p&gt;The interesting problems in industrial data collection are not in the protocol. They are in everything wrapped around it: endianness, partial reads, timeouts, backoff, isolation, and a clean seam between acquisition and delivery.&lt;/p&gt;
&lt;p&gt;Four rules worth keeping:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Treat the register map as configuration, not code.&lt;/li&gt;
&lt;li&gt;Assume every read can fail, and back off when it does.&lt;/li&gt;
&lt;li&gt;Reset failure counters on successful reads, not successful connects.&lt;/li&gt;
&lt;li&gt;Keep the sink behind an interface so polling never knows where data goes.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The complete runnable program, simulator included, is roughly 330 lines and requires only the .NET SDK.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/25363/building-a-custom-modbus-tcp-client-in-net-what-a-production-grade-poller-actually-needs</guid>
<pubDate>Tue, 25 Aug 2026 16:17:06 +0000</pubDate>
</item>
<item>
<title>Parallel Processing Gone Wrong</title>
<link>https://coderlegion.com/25281/parallel-processing-gone-wrong</link>
<description>&lt;p&gt;⚡ &quot;Let's Make It Faster!&quot; (Famous Last Words)&lt;/p&gt;
&lt;p&gt;The Scene: Sprint planning. The feature? Bulk data processing.&lt;br&gt;
Me (naive): &quot;Let's just run everything in parallel! It'll be 10x faster!&quot;&lt;br&gt;
Production (3 days later): catches fire &lt;br&gt;
What I Thought Would Happen:&lt;br&gt;
•	100 items to process&lt;br&gt;
•	Run them all in parallel&lt;br&gt;
•	Finish in 1/10th the time&lt;br&gt;
•	Get promoted&lt;br&gt;
•	Become CTO&lt;br&gt;
What Actually Happened:&lt;br&gt;
•	100 concurrent database connections&lt;br&gt;
•	Database connection pool exhausted&lt;br&gt;
•	ALL requests start failing (not just ours)&lt;br&gt;
•	2 AM phone call from the CTO (not a promotion call)&lt;br&gt;
•	Emergency rollback at 3 AM&lt;br&gt;
•	Very awkward Slack conversation&lt;br&gt;
The Broken Code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;// &quot;I'll just parallelize everything!&quot;
foreach (var item in items) {
    Task.Run(() =&amp;gt; ProcessItem(item)); // YOLO
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Why This Implodes:&lt;br&gt;
❌ No limit on concurrent operations&lt;br&gt;
❌ No shared state protection&lt;br&gt;
❌ No error handling per item&lt;br&gt;
❌ Overwhelms downstream services&lt;br&gt;
❌ Database can't handle the connections&lt;br&gt;
❌ Network ports get exhausted&lt;br&gt;
❌ And my ego gets crushed&lt;br&gt;
The Fix That Saved My Job:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;int passCount = 0;
int failCount = 0;
var errors = new ConcurrentDictionary&amp;lt;string, string&amp;gt;();

// Key: MaxDegreeOfParallelism controls concurrency
Parallel.For(0, items.Count, 
    new ParallelOptions { 
        MaxDegreeOfParallelism = Environment.ProcessorCount // Usually 4-8
    }, 
    (i) =&amp;gt; {
        try {
            ProcessItem(items[i]);
            
            // Thread-safe counter increment
            Interlocked.Increment(ref passCount);
        } 
        catch (Exception ex) {
            Interlocked.Increment(ref failCount);
            
            // Thread-safe error collection
            errors.TryAdd(i.ToString(), ex.Message);
        }
    });

// Now you can safely check results
Console.WriteLine($&quot;Success: {passCount}, Failed: {failCount}&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The Golden Rules I Learned:&lt;br&gt;
1️⃣ Parallelism ≠ Performance - Too much parallelism kills performance&lt;br&gt;
2️⃣ Know Your Bottleneck - If it's I/O bound, 4-8 threads is usually optimal&lt;br&gt;
3️⃣ Protect Shared State - Use Interlocked or ConcurrentCollections&lt;br&gt;
4️⃣ Configure, Don't Hardcode - Make MaxDegreeOfParallelism environment-specific&lt;br&gt;
5️⃣ Test Under Load - It works on my machine ≠ it works in production&lt;br&gt;
The Results After Proper Implementation:&lt;br&gt;
 Controlled concurrency = predictable performance&lt;br&gt;
✅ Database connections stay healthy&lt;br&gt;
⚡ 6x speedup (not 100x, but sustainable)&lt;br&gt;
 Zero production incidents&lt;br&gt;
 Sleep peacefully without 3 AM alerts&lt;br&gt;
The Hard Truth:&lt;br&gt;
More threads ≠ More speed&lt;br&gt;
It's like adding more cooks to a kitchen. At some point, they just bump into each other.&lt;br&gt;
What's your parallel processing horror story? I can't be the only one who learned this the hard way &lt;/p&gt;
&lt;h2 id=&quot;dotnet-parallelprocessing-performanceoptimization-productionissues-softwareengineering-threading-scalability-lessonslearned-debugging-csharp-techleadership&quot;&gt;dotnet #parallelprocessing #performanceoptimization #productionissues #softwareengineering #threading #scalability #lessonslearned #debugging #csharp #techleadership&lt;/h2&gt;
&lt;hr&gt;
&lt;h3 id=&quot;p-s-if-you-re-using-task-run-in-a-loop-or-unbounded-parallel-foreach-you-re-one-production-deployment-away-from-a-very-bad-day-&quot;&gt;P.S. If you're using Task.Run() in a loop or unbounded Parallel.ForEach(), you're one production deployment away from a very bad day.&lt;/h3&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/25281/parallel-processing-gone-wrong</guid>
<pubDate>Mon, 24 Aug 2026 10:38:39 +0000</pubDate>
</item>
<item>
<title>HttpClient Socket Exhaustion</title>
<link>https://coderlegion.com/25280/httpclient-socket-exhaustion</link>
<description>&lt;p&gt; Your .NET API is Slowly Dying (And You Don't Even Know It)&lt;/p&gt;
&lt;p&gt;Last month, our production system started timing out randomly. No errors. No warnings. Just... slow death.&lt;br&gt;
After 3 sleepless nights and diving through 60+ microservices, I found the culprit:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;
var client = new HttpClient(); // This single line
&lt;/code&gt;&lt;br&gt;
Here's what was happening:&lt;br&gt;
Every HTTP call was creating a NEW TCP socket. Under load, we exhausted the system's connection pool. Requests started queuing. Response times went from 200ms to 30+ seconds.&lt;/p&gt;
&lt;p&gt;The Silent Killer:&lt;br&gt;
•	✅ Your code compiles fine&lt;br&gt;
•	✅ Your tests pass&lt;br&gt;
•	✅ It works in dev/staging&lt;br&gt;
•	❌ It crashes in production under real load&lt;/p&gt;
&lt;p&gt;The Fix That Saved Our SLA:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;  
// In Startup.cs
&lt;/code&gt;&lt;br&gt;
`services.AddHttpClient(&quot;MyService&quot;, client =&amp;gt; {&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;client.Timeout = TimeSpan.FromSeconds(120);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;}) &lt;code&gt;
&lt;/code&gt;.ConfigurePrimaryHttpMessageHandler(() =&amp;gt; new SocketsHttpHandler {&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;PooledConnectionLifetime = TimeSpan.FromMinutes(2),
PooledConnectionIdleTimeout = TimeSpan.FromMinutes(5),
MaxConnectionsPerServer = 20,
AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;});`&lt;/p&gt;
&lt;p&gt;`// In your service&lt;br&gt;
public class MyService {&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private readonly IHttpClientFactory _clientFactory;    
public MyService(IHttpClientFactory clientFactory) {
    _clientFactory = clientFactory;
}    
public async Task&amp;lt;string&amp;gt; GetDataAsync() {
    var client = _clientFactory.CreateClient(&quot;MyService&quot;);
    // Use the client
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;}&lt;br&gt;
`&lt;/p&gt;
&lt;p&gt;The Results: &lt;br&gt;
 95% reduction in connection failures &lt;br&gt;
⚡ 40% improvement in throughput &lt;br&gt;
 Prevented potential revenue loss from timeouts&lt;/p&gt;
&lt;p&gt;The Lesson:&lt;br&gt;
IHttpClientFactory isn't just a &quot;best practice&quot; - it's a production survival tool.&lt;br&gt;
Microsoft documented this in 2018, but I still see this anti-pattern EVERYWHERE.&lt;br&gt;
Have you been bitten by socket exhaustion? &lt;/p&gt;
&lt;p&gt;Share your battle story in comments &lt;/p&gt;
&lt;p&gt; #performance #dotnet #csharp #softwareengineering #productionissues #microservices #debugging #azure #performanceoptimization #lessonslearned #techleadership&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/25280/httpclient-socket-exhaustion</guid>
<pubDate>Mon, 24 Aug 2026 10:37:13 +0000</pubDate>
</item>
<item>
<title>Adding custom methods for linq queries</title>
<link>https://coderlegion.com/22744/adding-custom-methods-for-linq-queries</link>
<description>&lt;h4 id=&quot;adding-custom-methods-for-linq-queries-c-&quot;&gt;Adding custom methods for LINQ queries (C#)&lt;/h4&gt;
&lt;h4 id=&quot;introduction&quot;&gt;Introduction&lt;/h4&gt;
&lt;p&gt;By introducing extension methods to the IEnumerableT&amp;gt; interface, you may expand the set of methods available for LINQ queries. Any data sequence can be converted into one single value by creating your custom aggregate method in SQL Server. &lt;/p&gt;
&lt;p&gt;You can also build a method that returns a new series of values and acts as a custom filter or a specialized data transform for a sequence of variables. &lt;/p&gt;
&lt;p&gt;A few examples include the Distinct and Skip techniques, as well as the Reverse.&lt;/p&gt;
&lt;p&gt;You may apply your methods to any enumerable collection when you extend the IEnumerableT&amp;gt; interface. In this article, we shall deeply take a look at various custom methods that are used in LINQ queries in C#.&lt;/p&gt;
&lt;h4 id=&quot;adding-an-aggregate-method&quot;&gt;Adding an aggregate method&lt;/h4&gt;
&lt;p&gt;When a set of values is aggregated, a single value can be generated. Average, Min, and Max are just a few of the aggregate techniques available in LINQ. By introducing an extension method to the IEnumerableT&amp;gt; interface, you can design your aggregate method.&lt;/p&gt;
&lt;p&gt;Median is an extension method that computes the median of a sequence of double-valued numbers.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c#&quot;&gt;public static class LINQExtensionExample
{
    public static double Median(this IEnumerable&amp;lt;double&amp;gt;? source)
    {
        if (!(source?.Any() ?? false))
        {
            throw a new InvalidOperationException(&quot;A null or empty set cannot be used to compute the median..&quot;);
        }

        var sortedList = (from numberX in source
                          orderby numberX
                          select numberX).ToList();

        int itemIndex = sortedList.Count / 2;

        if (sortedList.Count % 2 == 0)
        {
            // Rerurning an even number of objects or items
            return (sortedList[itemIndex] + sortedList[itemIndex - 1]) / 2;
        }
        else
        {
            // Rerurning an odd number of objects or items
            return sortedList[itemIndex];
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There is no difference between calling this extension method from the IEnumerableT&amp;gt; interface and calling any other aggregate function.&lt;/p&gt;
&lt;p&gt;On a double array, the following code demonstrates using the Median method&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c#&quot;&gt;double[] numbers1 = { 0.9, 5, 2, 9.3, 2.6, 7, 3.3, 4 };

var query = numbers1.Median();

Console.WriteLine(&quot;double: Median = &quot; + query1);
/*
 The output of the above code is as shown below

 Double: Median = 5.95
*/
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;an-aggregate-method-is-overloaded-by-accepting-a-variety-of-types-&quot;&gt;An aggregate method is overloaded by accepting a variety of types.&lt;/h4&gt;
&lt;p&gt;To accept sequences of different types, you can overload your aggregate method. Overloading each type is the typical technique. It's also possible to use a delegate to build an overload for a generic type. Combining both approaches is also an option.&lt;/p&gt;
&lt;h4 id=&quot;to-build-a-type-overload&quot;&gt;To build a type overload&lt;/h4&gt;
&lt;p&gt;A separate overload for each kind you want to support can be created. Here's an int-type overload of the Median method, which you can see in the following code example.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c#&quot;&gt;//Overload of int
public static double Median(this IEnumerable&amp;lt;int&amp;gt; source) =&amp;gt;
    (from numX in source select (double)numX).Median();
With the new Median overloads available for both integer and double types, the following code shows how to use them:
double[] numbers2 = {  0.9, 5, 2, 9.3, 2.6, 7, 3.3, 4};

var query2 = numbers2.Median();

Console.WriteLine(&quot;double: Median = &quot; + query2);

int[] numbers3 = { 14, 16, 15, 13, 11, 10, 9 };

var query3 = numbers3.Median();

Console.WriteLine(&quot;int: Median = &quot; + query3);
/*
 Here's what it looks like when it runs:

 The Double part: Median = 5.95
 The Integer part: Median = 13
*/
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;initiating-a-generic-overflow&quot;&gt;Initiating a generic overflow&lt;/h4&gt;
&lt;p&gt;A list of generic objects can also be used as an input to an overload if you want to do that. Delegate is supplied as a variable in this overload which converts the generic objects into a specific type using the delegate.&lt;/p&gt;
&lt;p&gt;An overload of Median's FuncT,TResult&amp;gt; delegate is shown in the following code. An object of generic type T is passed to this delegate, and it returns a double-type object as a result.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c#&quot;&gt;// Let us take a look
public static double Median&amp;lt;T&amp;gt;(this IEnumerable&amp;lt;T&amp;gt; numbersY,
                       Func&amp;lt;T, double&amp;gt; selector) =&amp;gt;
    (from num in numbersY select selector(num)).Median();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When working with sequences of objects of any type, you may now use the Median technique. A delegate parameter must be passed in if the type does not have its overload of the procedure. Lambda expressions are available in C# for this purpose. When using Aggregate or Group By clauses instead of a method call, this is only true in Visual Basic.&lt;/p&gt;
&lt;p&gt;You can use the following code to call the Median method on a string array as well as an integer array. Using an array of strings, a median is derived for each string's lengths in the array. Use the Median method's delegate parameter for each scenario in the code. Use the Median method's delegate parameter for FuncT, TResult&amp;gt; for each scenario in which it's used.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c#&quot;&gt;int[] numbers4 = { 14, 16, 15, 13, 11, 10, 9 };

/*
   In this case, the compiler will implicitly convert num=&amp;gt;num's value to double when you pass it as a parameter to the Median method.
   Otherwise, the compiler will give the user an error message.
*/
var query4 = numbers4.Median(num =&amp;gt; num);

Console.WriteLine(&quot;int: Median = &quot; + query3);

string[] numbers5 = { &quot;fourteen&quot;, &quot;sixteen&quot;, &quot;fifteen&quot;, &quot;thirteen&quot;, &quot;eleven&quot;, &quot;ten&quot;, &quot;nine&quot; };

// A number of object properties are available with the generic overload.

var query5 = numbers5.Median(str =&amp;gt; str.Length);

Console.WriteLine(&quot;String: Median = &quot; + query5);

/*
Here's what it looks like when it runs:
 Integer: Median = 13
 String: Median = 13
*/
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;adding-a-method-that-returns-a-sequence&quot;&gt;Adding a method that returns a sequence&lt;/h4&gt;
&lt;p&gt;IENumerableT can be extended with custom query methods to return a sequence of values. A collection of IEnumerableT&amp;gt; must be returned in this scenario. Filters or data transformations can be applied to a sequence of values using these methods.&lt;/p&gt;
&lt;p&gt;Using the AlternateElements Extension Method, every other element in a collection, starting with the first element, is returned.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c#&quot;&gt;//The IEnumerableT&amp;gt; interface has an extension method.
// Every other element in a sequence is returned by the method.
public static IEnumerable&amp;lt;T&amp;gt; AlternateElements&amp;lt;T&amp;gt;(this IEnumerable&amp;lt;T&amp;gt; source)
{
    int y = 3;
    foreach (var element in source)
    {
        if (y % 2 == 1)
        {
            yield return element;
        }
        y++;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This function can be used to extend any enumerable collection.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c#&quot;&gt;string[] strings = { &quot;u&quot;, &quot;v&quot;, &quot;w&quot;, &quot;x&quot; };

var query4 = strings.AlternateElements();

foreach (var element in query6)
{
    Console.WriteLine(element);
}
/*
Here's what it looks like when it runs:
 u
 w
 
*/
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h4&gt;
&lt;p&gt;In conclusion, we have looked at how one as a programmer can add custom methods for LINQ queries in c#.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/22744/adding-custom-methods-for-linq-queries</guid>
<pubDate>Wed, 15 Jul 2026 09:29:45 +0000</pubDate>
</item>
<item>
<title>LINQ Looks Clean Until You Care About Performance</title>
<link>https://coderlegion.com/16014/linq-looks-clean-until-you-care-about-performance</link>
<description>&lt;p&gt;LINQ is great. Until it isn't.&lt;/p&gt;
&lt;p&gt;Let me explain.&lt;/p&gt;
&lt;p&gt;When I first learned LINQ, I thought it was magic. You write these clean, readable queries. No more messy loops. No more inline SQL strings. Just beautiful C#.&lt;/p&gt;
&lt;p&gt;And for small apps or quick prototypes? It works perfectly.&lt;/p&gt;
&lt;p&gt;But then you start building real backend APIs. The kind that handle lots of requests, talk to big databases, and need to stay fast under pressure.&lt;/p&gt;
&lt;p&gt;And that's when LINQ starts to surprise you. Not in a fun way.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;What most tutorials don't tell you&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Here's the thing. LINQ isn't just a nicer way to write loops. It's actually a way to describe a query that can run in two very different places:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Inside your database (good)&lt;/li&gt;
&lt;li&gt;Inside your app's memory (can get dangerous)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And the difference between those two is where most performance problems quietly begin.&lt;/p&gt;
&lt;p&gt;You write something that looks harmless. But behind the scenes, LINQ pulls an entire table into memory, then filters it. Or it makes five database trips when you expected one. You don't see it happening. Your code still works. It just gets slower over time.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Why it's so easy to mess up&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;LINQ encourages you to compose first and think later. You chain together Where, Select, OrderByand you assume it's all efficient by default.&lt;/p&gt;
&lt;p&gt;But not all LINQ methods are equal.&lt;/p&gt;
&lt;p&gt;Some turn into clean SQL.&lt;br&gt;
Some force everything to load into memory right then and there.&lt;br&gt;
Some look like one query but actually run multiple.&lt;/p&gt;
&lt;p&gt;And when you're writing the code, none of this is obvious. The compiler won't warn you. Tests probably won't catch it. It only shows up when your API is under real load.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;What changes in production&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;On your laptop with ten test records? No problem.&lt;/p&gt;
&lt;p&gt;But in production, you've got:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Huge tables&lt;/li&gt;
&lt;li&gt;Dozens or hundreds of requests per second&lt;/li&gt;
&lt;li&gt;A database that's already busy&lt;/li&gt;
&lt;li&gt;Users who notice every millisecond&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And now those tiny LINQ mistakes start to pile up. Not with a dramatic crash. More like a slow, invisible drag on performance. One request takes 50ms longer. Then another. Then your database CPU creeps up.&lt;/p&gt;
&lt;p&gt;Nobody blames LINQ. They just say &quot;the API feels slow lately.&quot;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;The mindset shift that helped me&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Here's what I had to unlearn.&lt;/p&gt;
&lt;p&gt;LINQ isn't just about writing clean code. It's about deciding where the work happens.&lt;/p&gt;
&lt;p&gt;In the database? Fast and efficient.&lt;br&gt;
In memory? Sometimes fine, sometimes a disaster.&lt;/p&gt;
&lt;p&gt;Once I stopped thinking of LINQ as &quot;nice syntax&quot; and started thinking of it as &quot;query execution planner for humans,&quot; everything got clearer.&lt;/p&gt;
&lt;p&gt;Good backend code isn't about using LINQ correctly. It's about knowing:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;What runs in the database&lt;/li&gt;
&lt;li&gt;What runs in memory&lt;/li&gt;
&lt;li&gt;What runs once&lt;/li&gt;
&lt;li&gt;What runs way more times than you think&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;One last thought&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;I still love LINQ. It's one of the best tools in .NET.&lt;/p&gt;
&lt;p&gt;But like any powerful tool, it rewards you for understanding what's actually happening. Not just knowing the syntax.&lt;/p&gt;
&lt;p&gt;The more you know about the behind the scenes stuff, the fewer surprises you get at 2 AM when production starts acting weird.&lt;/p&gt;
&lt;p&gt;And that's a win worth chasing.&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://youtu.be/9Vm5R6pPMqE&quot;&gt;LINQ in Real Backend APIs: The Mistakes That Hurt Performance&lt;/a&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/16014/linq-looks-clean-until-you-care-about-performance</guid>
<pubDate>Mon, 27 Apr 2026 19:03:37 +0000</pubDate>
</item>
<item>
<title>Tuesday Coding Tip 07 — Exposing internal bits of .NET projects</title>
<link>https://coderlegion.com/14709/tuesday-coding-tip-07-exposing-internal-bits-of-net-projects</link>
<description>&lt;p&gt;Tuesday coding tips are super short posts about various tidbits mainly from C++, but also from other programming languages I use. You can also follow the #TuesdayCodingTips hashtag on Mastodon and Linkedin.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;C# has a number of visibility keywords and their combinations. A peculiar one is ‘internal’ which restricts the visibility of the symbols to the current assembly. However, that can be tweaked.&lt;/p&gt;
&lt;p&gt;You can manually specify the names of assemblies to which the ‘internal’ bits will be visible. Why is this useful? Because you can expose otherwise private parts of your code to assemblies like unit tests and test code with ease. No more ‘friend’ shenanigans like in C++ unit tests!&lt;/p&gt;
&lt;p&gt;Ways how to configure this evolved over the years and the most up-to-date approach seems to be in configuring the project file directly (with the latest MSVC and .NET 5+).&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;Project&amp;gt;
    &amp;lt;!-- ... ---&amp;gt;

 &amp;lt;ItemGroup&amp;gt;
  &amp;lt;AssemblyAttribute Include=&quot;System.Runtime.CompilerServices.InternalsVisibleTo&quot;&amp;gt;
   &amp;lt;_Parameter1&amp;gt;$(AssemblyName).Tests&amp;lt;/_Parameter1&amp;gt;
  &amp;lt;/AssemblyAttribute&amp;gt;
 &amp;lt;/ItemGroup&amp;gt;

&amp;lt;/Project&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/14709/tuesday-coding-tip-07-exposing-internal-bits-of-net-projects</guid>
<pubDate>Tue, 14 Apr 2026 11:49:31 +0000</pubDate>
</item>
<item>
<title>gRPC vs REST</title>
<link>https://coderlegion.com/14749/grpc-vs-rest</link>
<description>&lt;p&gt;TL;DR:&lt;br&gt;
We benchmarked REST vs gRPC under identical local setups using .NET. While gRPC is widely believed to outperform REST, our results show REST can be equally, or even slightly faster in specific data-heavy operations.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;As microservices multiply across modern systems, the choice of API protocol can make or break performance. REST has ruled for years, but gRPC promises to be faster, leaner and more efficient. So why hasn't it taken over? Microservices can be heavily dependent on each other, which means speed and stability is key. When gRPC claims to be faster than REST, why isn't it the de facto standard? In this blog we will put gRPC and REST head to head, to see which is actually faster.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;gRPC is a superior technology to REST!&lt;/strong&gt; At least that is what &lt;a rel=&quot;nofollow&quot; href=&quot;https://code.tutsplus.com/tutorials/rest-vs-grpc-battle-of-the-apis--cms-30711&quot;&gt;this&lt;/a&gt;&lt;code&gt;1&lt;/code&gt;, &lt;a rel=&quot;nofollow&quot; href=&quot;https://medium.com/@bimeshde/grpc-vs-rest-performance-simplified-fd35d01bbd4&quot;&gt;this&lt;/a&gt;&lt;code&gt;2&lt;/code&gt;, &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.yonego.com/nl/why-milliseconds-matter/#gref&quot;&gt;this&lt;/a&gt;&lt;code&gt;3&lt;/code&gt; and &lt;a rel=&quot;nofollow&quot; href=&quot;https://medium.com/@EmperorRXF/evaluating-performance-of-rest-vs-grpc-1b8bdf0b22da&quot;&gt;this blog&lt;/a&gt;&lt;code&gt;4&lt;/code&gt; claims. According to all the mentioned blogs, gRPC performs better and faster than a REST on several metrics. In this blog we will test specifically, how &lt;strong&gt;&lt;em&gt;fast&lt;/em&gt;&lt;/strong&gt; a REST client can handle different request and responses, and compare it to how fast a similar gRPC client handles the same requests and responses. This begs the question...&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Is gRPC faster than REST?&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;We hypothesize that gRPC is able to send and receive requests faster than a traditional REST.&lt;/em&gt; To test this, the following experiments have been developed.&lt;/p&gt;
&lt;h3 id=&quot;the-experiment&quot;&gt;The experiment&lt;/h3&gt;
&lt;p&gt;To test the Hypothesis two experiments, one utilizing gRPC and one utilizing REST. These experiments have to adhere to the following:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Rules&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;To ensure accurate measurements, the results must be obtained from the same computer.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Multiple data structures will be tested.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The setup for both APIs has to be as similar as possible.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The time used for measuring should be obtained from the client.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Set up&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;To adhere to the multiple data structures rule, a local database has been created, this database will provide a single instance of an object, as well as multiple instances of objects that will be stored in a list.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Each API will have 12 methods to call.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Three for a single instance which takes a parameter of Id.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Each one with a larger payload&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Three for a single instance which takes a parameter of Id.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Each one with a deeper payload&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Three for a collection of 100 instances, which takes no parameters.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Each one with a larger payload&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Three for a collection of 100 instances, which takes no parameters.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Each one with a deeper payload&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Each API will be tested with a client written in C#, as a console app.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Time will be measured with &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.microsoft.com/en-us/dotnet/api/system.diagnostics.stopwatch?view=netframework-4.8&quot;&gt;.NET Stopwatch&lt;/a&gt;&lt;code&gt;18&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The stopwatch will begin when the method is called and end when the API returns the full data.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;To minimize anomalies and outliers, each operation will be executed 100 times, and the average call speed will be evaluated.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Specs&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;I7-9700k, 8 core, &lt;em&gt;4.6GHz&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Samsung SSD 840 EVO 250GB&lt;/p&gt;
&lt;p&gt;NVIDIA GeForce GTX 1070&lt;/p&gt;
&lt;p&gt;2x8 GB HyperX Fury 2666MHz DDR4 Memory&lt;/p&gt;
&lt;h3 id=&quot;rest&quot;&gt;REST&lt;/h3&gt;
&lt;h4 id=&quot;what-is-rest-&quot;&gt;What is REST?&lt;/h4&gt;
&lt;p&gt;We have decided to work with the common implementation of REST and not the &lt;a rel=&quot;nofollow&quot; href=&quot;https://blog.ndepend.com/rest-vs-restful/&quot;&gt;full implementation of a RESTful API&lt;/a&gt;&lt;code&gt;5&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The key features to take note of when using rest:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Separation of client and server&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Server and client can be implemented independently without knowing each other-&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Server code can be changed without affecting the client.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Client code can be changed without affecting the server.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Both server and client are aware of methods available.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Statelessness&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://restfulapi.net/statelessness/&quot;&gt;Stateless&lt;/a&gt;&lt;code&gt;6&lt;/code&gt; means that the server is not required to know the current state of the client and vice versa.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Either end can understand any method calls, without knowing the previously called methods.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Invocation&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;We invoke a method on the server via &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.restapitutorial.com/lessons/httpmethods.html&quot;&gt;HTTP operations&lt;/a&gt;&lt;code&gt;7&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;GET&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;POST&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;PUT&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;DELETE&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;setting-up-the-experiment-for-the-rest-api&quot;&gt;Setting up the experiment for the REST API&lt;/h3&gt;
&lt;p&gt;The architecture for this experiment is a simple one:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gqgzbrhw5wt7atjqf7k4.png&quot; alt=&quot;REST Architecture&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;sample-project-and-metrics&quot;&gt;Sample project and metrics&lt;/h3&gt;
&lt;p&gt;If you want to replicate this experiment yourself, the database setup can be found &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/NikoDyring/gRPC-vs-REST/blob/master/Database-scripts&quot;&gt;here&lt;/a&gt; and the source code for the rest-api can be found &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/NikoDyring/gRPC-vs-REST/tree/master/RESTForBlog2&quot;&gt;here&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Running our setup yielded the following results:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Single payload&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The difference between a single small payload and a single large payload is small in the context of a daily task. A single small payload has a mean response time of 0.0181 whilst a single large payload has a mean response time of 0.0204 seconds. But in relation to each other, it's a 12.7% increase in response time.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/e60t018ga733h4y99d3b.png&quot; alt=&quot;API Single Payload&quot;&gt;&lt;/p&gt;
&lt;p&gt;To put this into perspective a small payload contains 10 values of data. A large payload contains (4+(6*9))*6+4 or 352 values. This means that we have requested 3420% more data and it only took 12.7% longer.&lt;/p&gt;
&lt;p&gt;To test different scenarios we also created a &quot;deep&quot; payload that contains a different amount of nested objects. The deepest payload contains a total of eight nested objects, however, the total amount of values is far less in comparison to the previous payload. The previously mentioned payload peaked at 352 values whereas the deepest payload peaks at (4+(6*4))+(4+(7*4))+(4+(8*4)) values, or 96 values in total. In other words, the deep payloads are much smaller in size but different in structure.&lt;/p&gt;
&lt;p&gt;To give a concrete example, a large payload is structured like so:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;large_payload {
    id,
    string_Value,
    int_value,
    double_value,
    medium_payload {
        id,
        string_value,
        int_value,
        double value,
        small_payload {
                id,
                string_value,
                int_value,
                double_value
            },
        small_payload {
                ...
            },
        ...
        },
    medium_payload {
            ...
        },
    ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and the deepest_payload is structured like so:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;deepest_payload {
    id,
    depth_seven {
        ...
    },
    depth_eight {
        ...
    },
    depth_nine {
        id,
        string_value,
        int_value,
        double_value,
        depth_eight {
            id,
            string_value,
            int_value,
            double_value,
            depth_seven {
                ...
                depth_six {
                    ...
                    dpeth_five {
                        ...
                        depth_four {
                            ...
                            depth_three {
                                ...
                                depth_two {
                                    ...
                                    depth_one {
                                        ...
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }

    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Interestingly, larger payloads returned faster results, an unexpected outcome that could hint at CPU scheduling effects or cached object reuse.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/862ielrek8dqs8dovd6s.png&quot; alt=&quot;REST Deep Payload Results&quot;&gt;&lt;/p&gt;
&lt;p&gt;To put things into perspective, a deep payload, which contains a total of 4+(4+4)+(4+4*2) values, or 24 values in total. Averaged at 0.0173 seconds, whereas the deepest payload, which carries a total of 96 values, which means it's 300% larger in size, averaged at 0.015 seconds. In other words, 300% more data was transferred 15.3% faster on average.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Collection of payloads&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;When we compare collections, the difference becomes very apparent. A small collection payload averaged 0.0256 seconds and a large average of 0.1006 seconds, which is an increase of 293%. It is apparent that when it comes to moving large collections of data over the REST API, it takes a considerate amount of time compared to smaller collections.&lt;/p&gt;
&lt;p&gt;The results are very much alike when we tested on collections of deep payloads.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/l59t2865114q5k1v2rrg.png&quot; alt=&quot;REST Deep Payload Collection Results&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;grpc&quot;&gt;gRPC&lt;/h3&gt;
&lt;h4 id=&quot;what-is-grpc-&quot;&gt;What is gRPC?&lt;/h4&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://grpc.io/&quot;&gt;gRPC&lt;/a&gt;&lt;code&gt;8&lt;/code&gt; is an open-source RPC framework, that can run in any environment. gRPC was recently included in the .Net core platform thereby easily accessible by thousands of developers.&lt;/p&gt;
&lt;p&gt;Some key features we would like to highlight:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;HTTP/2 support&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://en.wikipedia.org/wiki/HTTP/2&quot;&gt;HTTP/2&lt;/a&gt;&lt;code&gt;9&lt;/code&gt; is HTTP/1's successor, which is what most websites and frameworks utilize today. In many ways, HTTP/2 is an improved version of HTTP/1, and HTTP/3 is already in the works.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Language independent&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;gRPC is language independent, which means it doesn't matter which language you develop in. The framework supports a handful of &lt;a rel=&quot;nofollow&quot; href=&quot;https://packages.grpc.io/&quot;&gt;popular languages&lt;/a&gt;&lt;code&gt;10&lt;/code&gt;. This is quite an advantage when you're developing microservices, which might have services developed in different languages and frameworks.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Contract First&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;gRPC is strictly &lt;a rel=&quot;nofollow&quot; href=&quot;https://en.wikipedia.org/wiki/Design_by_contract&quot;&gt;contract first&lt;/a&gt;&lt;code&gt;11&lt;/code&gt; which is a design approach that works especially well in larger development teams. It also excels when developing microservices, as a contract would be created before any actual implementations can be done. The contract is designed in the &lt;a rel=&quot;nofollow&quot; href=&quot;https://developers.google.com/protocol-buffers&quot;&gt;.proto file&lt;/a&gt;&lt;code&gt;12&lt;/code&gt;, which is also where gRPC gains some of its speed from, seeing as .proto files are...&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Strongly typed&lt;/strong&gt; As a by-product of a strongly typed proto file, which is used as a contract between client and server, but also used as an extensible mechanism for &lt;a rel=&quot;nofollow&quot; href=&quot;https://en.wikipedia.org/wiki/Serialization&quot;&gt;serializing&lt;/a&gt;&lt;code&gt;13&lt;/code&gt; structured data.&lt;/p&gt;
&lt;h3 id=&quot;setting-up-the-grpc-project&quot;&gt;Setting up the gRPC project&lt;/h3&gt;
&lt;p&gt;For the gRPC architecture we use the same as the rest, we have a client and a server running locally. The client calls the methods exposed by the proto file. The method then gets executed on the server and queries the database, once the data has been obtained it replies to the client. When the client has received all the data, we stop and log the time elapsed since the call started.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xbh0op8jg8mv96j00590.png&quot; alt=&quot;gRPC Architecture&quot;&gt;&lt;/p&gt;
&lt;h4 id=&quot;sample-project-and-metrics-1&quot;&gt;Sample project and metrics&lt;/h4&gt;
&lt;p&gt;If you want to replicate this experiment yourself database setup can be found &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/NikoDyring/gRPC-vs-REST/tree/master/Database-scripts&quot;&gt;here&lt;/a&gt; and the source code for the grpc-project can be found &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/NikoDyring/gRPC-vs-REST/tree/master/GrpcProject&quot;&gt;here&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Running our setup yielded the following results:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Single payloads&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/st5drn7634dx3p8dktws.png&quot; alt=&quot;gRPC Wide Payload Results&quot;&gt;&lt;/p&gt;
&lt;p&gt;The test on single payloads yielded quite odd results, where the medium payload proved to be the fastest on average, and the largest payload only being slightly slower than the smallest. Just to recap the numbers; a larger payload contains 3400% more data than a small payload, and yet it only took 2.36% longer to get that data.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8bnwo4dgv68segywbesm.png&quot; alt=&quot;gRPC Deep Payload Results&quot;&gt;&lt;/p&gt;
&lt;p&gt;Even more odd were the results of the deep payloads. Once again the payload containing a &quot;medium&quot; amount of data, was the fastest, just like previously. But unlike previously, the deepest payload was significantly faster than the deep payload, to be precise; the deepest payload, which contains 300% more data than a deep payload was 28.63% faster. As the results are rather unexpected we have to take a close look at &lt;a rel=&quot;nofollow&quot; href=&quot;#possible-errors&quot;&gt;possible errors&lt;/a&gt; that could have occurred.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Collection of payloads&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zf6qguovkukjk7jdszmb.png&quot; alt=&quot;gRPC Wide Payload Results&quot;&gt;&lt;/p&gt;
&lt;p&gt;Collections paint a different picture, a small payload collection averaged 0.01911 seconds, while a collection of large payloads took 0.7025. This means a large payload on average took 267.6% longer to get. These results are much closer to what we would expect. We did the same test with a collection of deep payloads.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2qn42vrxjub5cd2va1i4.png&quot; alt=&quot;gRPC Deep Payload Collection Results&quot;&gt;&lt;/p&gt;
&lt;p&gt;The results of this test, were as you would expect, as the payloads incrementally increase in size, they also increase incrementally in response time.&lt;/p&gt;
&lt;h3 id=&quot;conclusion-1&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;When we put the two charts next to each other, it's easy to see which one has an edge. The REST API is represented by the blue blocks, whilst gRPC is represented by the red blocks, just like previously.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gafiqskc8d5b6yxcgnzt.png&quot; alt=&quot;Wide Payload results comparison&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/trsrjb0hfr5fszbihfba.png&quot; alt=&quot;Deep payload results comparison&quot;&gt;&lt;/p&gt;
&lt;p&gt;This is the case for both single instances of objects as well as collections of objects.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/c3mxxchvh7ee3yhbmu05.png&quot; alt=&quot;Deep payload collection result comparison&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ex70w5nuemzgmk63ldci.png&quot; alt=&quot;Wide payload collection result comparison&quot;&gt;&lt;/p&gt;
&lt;p&gt;We hypothesized that gRPC would be faster than the rest, based on the numerous blogs claiming this to be true, with their own tests. Contrary to popular belief, our experiments suggest that under certain conditions, REST can outperform gRPC in raw speed, reminding us that performance isn't universal but contextual.&lt;/p&gt;
&lt;p&gt;These results might not seem as much, but it has been &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.hobo-web.co.uk/your-website-design-should-load-in-4-seconds/&quot;&gt;proven&lt;/a&gt;&lt;code&gt;14&lt;/code&gt; that people on average don't wait around for data to load and will abandon a web page or program if loading times are too long. When moving large amounts of data, a small amount of time can be the difference between keeping or losing a customer.&lt;/p&gt;
&lt;p&gt;this prompts the question: &lt;strong&gt;&lt;em&gt;When to use gRPC and when to use REST&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;We would argue that gRPC fit into a setting, where you need to have multiple programs or services talking to each other across different languages, especially when the task that needs to connect to an endpoint is an action that needs to be executed; one such action could be TurnOnTheWater(). This argument is based on the research made into gRPC, rather than the results of these particular tests.&lt;/p&gt;
&lt;p&gt;REST on the other hand operates on the four aforementioned HTTP operations, these operations indicated data transfers of one sort or the other. While rest can execute the same actions as gRPC, the action TurnOnTheWater() doesn't fit into what a REST API was designed for. We would instead use REST where we required data transfers and other typical CRUD mechanics.&lt;/p&gt;
&lt;p&gt;Ultimately, the right choice depends on your use case: REST for simplicity and interoperability, gRPC for high-efficiency internal microservice communication.&lt;/p&gt;
&lt;h3 id=&quot;possible-errors&quot;&gt;Possible errors&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Network outage during some of the tests&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The gRPC serverside logging was set to critical, tweaking this option might yield different results.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;what-s-next-&quot;&gt;What's next?&lt;/h3&gt;
&lt;p&gt;This blog has only been about the differences in speed between REST and gRPC, but in reality, many other factors are present, if we were to truly compare the two frameworks. gRPC has claimed to not only be faster, but also more reliable, stable, and secure, and all of these metrics, as well as other metrics, would be interesting to cover, they are however out of scope in this particular blog-post.&lt;/p&gt;
&lt;h3 id=&quot;technologies-used&quot;&gt;Technologies used&lt;/h3&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://grpc.io/&quot;&gt;gRPC&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://dotnet.microsoft.com/apps/aspnet/apis&quot;&gt;.Net Web Api&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.microsoft.com/en-us/visualstudio/get-started/csharp/tutorial-console?view=vs-2019&quot;&gt;.Net console app&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.mysql.com/&quot;&gt;MySql Database&lt;/a&gt;&lt;/p&gt;
&lt;h3 id=&quot;references&quot;&gt;References&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://code.tutsplus.com/tutorials/rest-vs-grpc-battle-of-the-apis--cms-30711&quot;&gt;Battle of the APIs&lt;/a&gt; &lt;code&gt;gRPC v REST&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://medium.com/@bimeshde/grpc-vs-rest-performance-simplified-fd35d01bbd4&quot;&gt;gRPC/REST performance simplified&lt;/a&gt; &lt;code&gt;gRPC v REST&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.yonego.com/nl/why-milliseconds-matter/#gref&quot;&gt;Why milliseconds matter&lt;/a&gt; &lt;code&gt;gRPC v REST&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://medium.com/@EmperorRXF/evaluating-performance-of-rest-vs-grpc-1b8bdf0b22da&quot;&gt;gRPC/REST evaluating performance&lt;/a&gt; &lt;code&gt;gRPC v REST&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://blog.ndepend.com/rest-vs-restful/&quot;&gt;REST vs RESTful&lt;/a&gt; &lt;code&gt;REST&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://restfulapi.net/statelessness/&quot;&gt;Statelessness&lt;/a&gt; &lt;code&gt;REST&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.restapitutorial.com/lessons/httpmethods.html&quot;&gt;Http Methods&lt;/a&gt; &lt;code&gt;REST&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://grpc.io/&quot;&gt;gRPC&lt;/a&gt; &lt;code&gt;gRPC&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://en.wikipedia.org/wiki/HTTP/2&quot;&gt;HTTP/2&lt;/a&gt; &lt;code&gt;wiki&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://packages.grpc.io/&quot;&gt;Suported gRPC languages&lt;/a&gt; &lt;code&gt;gRPC&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://en.wikipedia.org/wiki/Design_by_contract&quot;&gt;Design by Contract&lt;/a&gt; &lt;code&gt;wiki&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://developers.google.com/protocol-buffers&quot;&gt;Google proto buffers&lt;/a&gt; &lt;code&gt;gRPC&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://en.wikipedia.org/wiki/Serialization&quot;&gt;Serialization&lt;/a&gt; &lt;code&gt;wiki&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.hobo-web.co.uk/your-website-design-should-load-in-4-seconds/&quot;&gt;Website should load in 4 seconds&lt;/a&gt; &lt;code&gt;blog&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://dotnet.microsoft.com/apps/aspnet/apis&quot;&gt;.NET Web API&lt;/a&gt; &lt;code&gt;.NET&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.microsoft.com/en-us/visualstudio/get-started/csharp/tutorial-console?view=vs-2019&quot;&gt;.NET Console App&lt;/a&gt; &lt;code&gt;.NET&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.mysql.com/&quot;&gt;MySQL&lt;/a&gt; &lt;code&gt;MySQL&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.microsoft.com/en-us/dotnet/api/system.diagnostics.stopwatch?view=netframework-4.8&quot;&gt;.NET Stopwatch API&lt;/a&gt; &lt;code&gt;.NET&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/14749/grpc-vs-rest</guid>
<pubDate>Tue, 14 Apr 2026 07:25:32 +0000</pubDate>
</item>
<item>
<title>Why I Built a Privacy-First AI Assistant for Visual Studio 2022 (Goodbye Cloud-Only Copilots!)</title>
<link>https://coderlegion.com/13479/why-i-built-privacy-first-ai-assistant-for-visual-studio-2022-goodbye-cloud-only-copilots</link>
<description>&lt;h3 id=&quot;the-problem-cloud-ai-is-great-but-privacy-is-greater&quot;&gt;The Problem: Cloud AI is Great, but Privacy is Greater&lt;/h3&gt;
&lt;p&gt;We all love GitHub Copilot, but let's be honest: in an enterprise environment, &lt;strong&gt;Privacy&lt;/strong&gt; isn't just a buzzword—it's a legal requirement. Sending proprietary codebases to cloud servers is often a strict &quot;no-go&quot; for many companies.&lt;/p&gt;
&lt;p&gt;I realized that we needed a bridge between the power of LLMs and the security of a local environment. That’s why I built &lt;strong&gt;Local LLM Plugin Modern&lt;/strong&gt; for Visual Studio 2022.&lt;/p&gt;
&lt;h3 id=&quot;what-is-it-&quot;&gt;What is it?&lt;/h3&gt;
&lt;p&gt;It's a powerful, modern, and highly optimized AI assistant extension. It seamlessly integrates local LLMs via &lt;strong&gt;Ollama&lt;/strong&gt; and cloud-based models like &lt;strong&gt;OpenAI, Anthropic (Claude), and Google Gemini&lt;/strong&gt; directly into your coding environment.&lt;/p&gt;
&lt;p&gt;Whether you want to run &lt;strong&gt;DeepSeek&lt;/strong&gt; or &lt;strong&gt;Llama 3&lt;/strong&gt; entirely offline or leverage &lt;strong&gt;GPT-4o&lt;/strong&gt; for heavy reasoning, this extension offers a native-feeling dark theme experience that boosts your productivity without leaving your IDE.&lt;/p&gt;
&lt;h3 id=&quot;engineering-highlights-built-for-performance-&quot;&gt;Engineering Highlights (Built for Performance)&lt;/h3&gt;
&lt;p&gt;Instead of just &quot;making it work,&quot; I rebuilt this extension to meet enterprise standards:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Clean Architecture &amp;amp; MVVM&lt;/strong&gt;: Separated UI, Core logic, and Infrastructure for maximum maintainability.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dependency Injection&lt;/strong&gt;: Utilizing &lt;code&gt;Microsoft.Extensions.DependencyInjection&lt;/code&gt; to handle provider factories gracefully.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Lightning Fast Text Injection&lt;/strong&gt;: Utilizes Visual Studio's native &lt;code&gt;UndoContext&lt;/code&gt;. This ensures massive code blocks are applied instantly without freezing the editor and allows for a single-step Undo (&lt;code&gt;Ctrl+Z&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Memory Efficiency&lt;/strong&gt;: Optimized handling of large text blocks using &lt;code&gt;StringBuilder&lt;/code&gt; and &lt;code&gt;StringReader&lt;/code&gt; to prevent memory leaks.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;features-that-boost-dx-developer-experience-&quot;&gt;Features that Boost DX (Developer Experience)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Multi-Provider AI Support&lt;/strong&gt;: Switch seamlessly between Ollama, OpenAI, Claude, and Gemini.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Partial Selection Injection&lt;/strong&gt;: Select a specific part of the AI's response to inject only that portion into your code, ignoring unnecessary conversational filler.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Smart Defaults&lt;/strong&gt;: Automatically configures API URLs and model names based on the selected provider.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Advanced Text Manipulation Tools&lt;/strong&gt;: Includes built-in tools to remove duplicates, modify and replicate variables, or batch-erase specific words.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Native Dark Theme&lt;/strong&gt;: A sleek interface that perfectly matches Visual Studio 2022's native dark mode.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;keyboard-shortcuts-power-user-friendly-&quot;&gt;Keyboard Shortcuts (Power User Friendly)&lt;/h3&gt;
&lt;p&gt;The UI is entirely optimized for keyboard-only navigation:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;Ctrl + 1&lt;/code&gt;&lt;/strong&gt;: Open the AI Assistant (Global VS Shortcut).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;Enter&lt;/code&gt;&lt;/strong&gt;: Send your prompt to the AI.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;Shift + Enter&lt;/code&gt;&lt;/strong&gt;: Apply the response directly to your editor and close the window.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;Esc&lt;/code&gt;&lt;/strong&gt;: Cancel and close the assistant.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;how-to-get-it-&quot;&gt;How to Get It?&lt;/h3&gt;
&lt;p&gt;You don't even need a link. Just open your &lt;strong&gt;Visual Studio 2022&lt;/strong&gt;, go to &lt;strong&gt;Extensions -&amp;gt; Manage Extensions&lt;/strong&gt;, and search for:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;&quot;Local LLM Plugin Modern&quot;&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;open-source-amp-contributing&quot;&gt;Open Source &amp;amp; Contributing&lt;/h3&gt;
&lt;p&gt;This project is fully open source under the &lt;strong&gt;MIT License&lt;/strong&gt;. I believe the best tools are built by the community, for the community.&lt;/p&gt;
&lt;p&gt;Check out the repo here: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/furkiak/visualStudioLocalLLMPlugin&quot;&gt;furkiak/visualStudioLocalLLMPlugin&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I'd love to hear your thoughts! Are you moving towards local LLMs for coding, or is cloud AI still your go-to? Let's discuss in the comments! &lt;/p&gt;
&lt;h2 id=&quot;visualstudio-csharp-ai-productivity-programming&quot;&gt;visualstudio #csharp #ai #productivity #programming&lt;/h2&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/13479/why-i-built-privacy-first-ai-assistant-for-visual-studio-2022-goodbye-cloud-only-copilots</guid>
<pubDate>Wed, 25 Mar 2026 12:21:55 +0000</pubDate>
</item>
<item>
<title>Shadow Logging via Events - Complete Decoupling of Business Logic and Logging in DI Environment</title>
<link>https://coderlegion.com/11384/shadow-logging-via-events-complete-decoupling-of-business-logic-and-logging-environment</link>
<description>&lt;p&gt;Hello, colleagues!&lt;/p&gt;
&lt;p&gt;I want to share an approach to logging that radically simplifies architecture and strengthens SOLID principles.&lt;/p&gt;
&lt;p&gt;I've created a code example on &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/abaula/MixedCode/blob/master/DecoupledLogging/src/Program.cs&quot;&gt;GitHub&lt;/a&gt; to demonstrate how shadow decoupled logging works through C# events.&lt;/p&gt;
&lt;h2 id=&quot;what-is-decoupled-logging-&quot;&gt;What is Decoupled Logging?&lt;/h2&gt;
&lt;p&gt;Decoupled logging is an architectural approach where business classes &lt;strong&gt;do not contain direct logger calls&lt;/strong&gt; (like &lt;code&gt;ILogger.LogInformation&lt;/code&gt;). Instead:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Business classes generate &lt;strong&gt;events&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Specialized logger classes&lt;/strong&gt; handle the events.&lt;/li&gt;
&lt;li&gt;Business classes know nothing about loggers.&lt;/li&gt;
&lt;li&gt;Logging is configured &lt;strong&gt;centrally&lt;/strong&gt; at the application level, without polluting domain logic.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;pros-and-cons-of-decoupled-logging&quot;&gt;Pros and Cons of Decoupled Logging&lt;/h2&gt;
&lt;p&gt;There are opinions both &quot;for&quot; and &quot;against&quot; decoupled logging.&lt;/p&gt;
&lt;p&gt;Typical arguments &quot;for&quot;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;SRP purity&lt;/strong&gt;: The class focuses on business logic; logging is an external concern.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Testability&lt;/strong&gt;: No &lt;code&gt;ILogger&lt;/code&gt; mocks needed; classes work standalone.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Flexibility and scalability&lt;/strong&gt;: One event can be handled in all required ways: logs, metrics, audit. Easy to change event handling logic and logging libraries.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Decoupling for libraries&lt;/strong&gt;: Consumers of business logic classes decide themselves whether to log events and which ones. No hard dependencies.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Performance&lt;/strong&gt;: Business class does not format data for logs. When implementing event handlers, use smart fast caching of incoming events with subsequent post-processing - it doesn't block business logic.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Critics' objections boil down to a simple thesis: don't complicate things if not really needed - &quot;inject ILogger and don't worry.&quot; This opinion sounds reasonable; I agree with such criticism - if you have a simple application, don't complicate it.&lt;/p&gt;
&lt;h2 id=&quot;extracting-logging-from-the-class&quot;&gt;Extracting Logging from the Class&lt;/h2&gt;
&lt;p&gt;The simplest way to separate business logic and logging is to write a Wrapper for the business class.&lt;/p&gt;
&lt;p&gt;Decorator/wrapper for logging is convenient but &lt;strong&gt;imposes usage rules&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Client code must work through the wrapper.&lt;/li&gt;
&lt;li&gt;Refactoring becomes more complex.&lt;/li&gt;
&lt;li&gt;Duplication and inheritance issues arise.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This approach makes logging &lt;strong&gt;not &quot;shadow&quot;&lt;/strong&gt; - consumers indirectly know about it.&lt;/p&gt;
&lt;h2 id=&quot;complete-separation&quot;&gt;Complete Separation&lt;/h2&gt;
&lt;p&gt;Complete separation of business logic and logging is possible only using two independent objects: business class and log handler.&lt;/p&gt;
&lt;p&gt;Simple example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;class OrderServiceLogger
{
    public OrderServiceLogger(FileLogger logger, OrderService orderService)
    {
        orderService.OrderCreated += (s, e) =&amp;gt; logger.LogInformation($&quot;Order {e.OrderId} created.&quot;);
    }
}

var orderService = new OrderService();
var fileLogger = new FileLogger();
var orderServiceLogger = new OrderServiceLogger(fileLogger, orderService);
orderService.CreateOrder(...);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This approach is straightforward, but if the application uses a DI container, it requires adaptation.&lt;/p&gt;
&lt;h2 id=&quot;shadowing-logging&quot;&gt;Shadowing Logging&lt;/h2&gt;
&lt;p&gt;DI containers perform 2 important tasks:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Object factory&lt;/li&gt;
&lt;li&gt;Object lifetime management&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Object creation is simple: the DI container will return 2 ready objects, with the log handler receiving the business class instance and logger instance upon creation.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;var services = new ServiceCollection();
services.AddScoped&amp;lt;FileLogger&amp;gt;();
services.AddScoped&amp;lt;OrderServiceLogger&amp;gt;();
services.AddScoped&amp;lt;OrderService&amp;gt;();
var serviceProvider = services.BuildServiceProvider();

var orderService = serviceProvider.GetRequiredService&amp;lt;OrderService&amp;gt;();
var orderServiceLogger = serviceProvider.GetRequiredService&amp;lt;OrderServiceLogger&amp;gt;();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The problem is managing the lifetime of the log handler &lt;code&gt;OrderServiceLogger&lt;/code&gt;, i.e., explicitly storing a reference to the created object and synchronizing its lifetime with the business class &lt;code&gt;OrderService&lt;/code&gt; instance.&lt;/p&gt;
&lt;p&gt;If we do nothing else, we'll have to explicitly create a new &lt;code&gt;OrderServiceLogger&lt;/code&gt; instance wherever we create an &lt;code&gt;OrderService&lt;/code&gt; instance and ensure their lifetimes match - that's not the behavior we want.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What we need:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Use only business logic object instances in business logic, in our example &lt;code&gt;OrderService&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Business logic should know nothing about objects performing other tasks in the application, in our example logging via &lt;code&gt;OrderServiceLogger&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;When creating a business logic object, the application must guarantee all implemented service functions for it - if &lt;code&gt;OrderServiceLogger&lt;/code&gt; is implemented for &lt;code&gt;OrderService&lt;/code&gt;, it must be created in time and handle events.&lt;/li&gt;
&lt;li&gt;Correct service function operation includes optimal application resource management - the &lt;code&gt;OrderServiceLogger&lt;/code&gt; instance must be removed from memory after the associated &lt;code&gt;OrderService&lt;/code&gt; object is destroyed.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These requirements are easy to implement, even within a DI container. We've sorted object creation; now we need to implement lifetime synchronization using weak references.&lt;/p&gt;
&lt;p&gt;We need to ensure the created &lt;code&gt;OrderServiceLogger&lt;/code&gt; object lives no less than the &lt;code&gt;OrderService&lt;/code&gt; instance and is removed when no longer needed.&lt;/p&gt;
&lt;p&gt;For this, we need an application-level object that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Stores references to both dependent objects.&lt;/li&gt;
&lt;li&gt;Monitors their lifetimes.&lt;/li&gt;
&lt;li&gt;Removes &lt;code&gt;OrderServiceLogger&lt;/code&gt; as soon as &lt;code&gt;OrderService&lt;/code&gt; is removed.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We can implement such a class ourselves, where there is a &lt;strong&gt;key object&lt;/strong&gt; and &lt;strong&gt;dependent objects&lt;/strong&gt;. The architecture of such a class is simple:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Key object(s) are stored as weak references, which do not prevent the object from being garbage collected.&lt;/li&gt;
&lt;li&gt;Dependent objects are stored as strong references, which prevent the garbage collector from destroying them.&lt;/li&gt;
&lt;li&gt;The state of key objects is periodically checked - if they are removed, dependent objects are also removed.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For the simple case, we can use the &lt;a rel=&quot;nofollow&quot; href=&quot;https://learn.microsoft.com/ru-ru/dotnet/api/system.runtime.compilerservices.conditionalweaktable-2?view=net-8.0&quot;&gt;ConditionalWeakTable&amp;lt;TKey,TValue&amp;gt;&lt;/a&gt; class from the &lt;code&gt;System.Runtime.CompilerServices&lt;/code&gt; namespace, which already implements this logic.&lt;/p&gt;
&lt;h2 id=&quot;writing-di-logic&quot;&gt;Writing DI Logic&lt;/h2&gt;
&lt;p&gt;Let's implement an extension method for &lt;code&gt;ServiceCollection&lt;/code&gt; and examine how it works.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;public static class ServiceCollectionExtensions
{
    public static ServiceCollection AddScopedWithLogger&amp;lt;TService, TServiceInstance, TServiceLogger&amp;gt;(
        this ServiceCollection services)
        where TService : class
        where TServiceInstance : class, TService
        where TServiceLogger : class
    {
        // Register TServiceInstance.
        services.AddScoped&amp;lt;TServiceInstance&amp;gt;();
        // Register TServiceLogger.
        services.AddScoped&amp;lt;TServiceLogger&amp;gt;();
        // Register TService.
        services.AddScoped&amp;lt;TService&amp;gt;(sp =&amp;gt;
        {
            var instance = sp.GetRequiredService&amp;lt;TServiceInstance&amp;gt;();
            var logger = sp.GetRequiredService&amp;lt;TServiceLogger&amp;gt;();
            var conditionalWeakTable = sp.GetRequiredService&amp;lt;ConditionalWeakTable&amp;lt;object, object&amp;gt;&amp;gt;();
            // Put instance and logger into ConditionalWeakTable.
            conditionalWeakTable.Add(instance, logger);

            return instance;
        });

        return services;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;AddScopedWithLogger&lt;/code&gt; method does all the necessary work:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Registers all types in DI.&lt;/li&gt;
&lt;li&gt;Implements the logic for creating and linking the business class and its event handler class.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Important&lt;/strong&gt; - in the DI container, it is necessary to separate the logic for creating the business class object itself from the logic for creating the instance with all its shadow objects. For this, it is best to use business class contracts (interfaces).&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;public class OrderEventArgs : EventArgs
{
    public int OrderId { get; set; }
}

public interface IOrderService
{
    event EventHandler&amp;lt;OrderEventArgs&amp;gt; OrderCreated;
    void CreateOrder(int id, string customer);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Thus, the DI container will pass the &lt;code&gt;OrderService&lt;/code&gt; instance to the &lt;code&gt;OrderServiceLogger&lt;/code&gt; constructor.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;In business logic, only contracts must be used&lt;/strong&gt;, which is a recommended approach.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;class OrderManager : IOrderManager
{
    public OrderManager(IOrderService orderService)
    {
        ...
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Correctly register all types in the container:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;var services = new ServiceCollection();
services.AddScopedWithLogger&amp;lt;IOrderService, OrderService, OrderServiceLogger&amp;gt;();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now, creating an object for its contract &lt;code&gt;IOrderService&lt;/code&gt; will trigger the following code from the extension method:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;...
// Register TService.
services.AddScoped&amp;lt;TService&amp;gt;(sp =&amp;gt;
{
    var instance = sp.GetRequiredService&amp;lt;TServiceInstance&amp;gt;();
    var logger = sp.GetRequiredService&amp;lt;TServiceLogger&amp;gt;();
    var conditionalWeakTable = sp.GetRequiredService&amp;lt;ConditionalWeakTable&amp;lt;object, object&amp;gt;&amp;gt;();
    // Put instance and logger into ConditionalWeakTable.
    conditionalWeakTable.Add(instance, logger);

    return instance;
});
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here's the breakdown for the parameter combination &lt;code&gt;IOrderService, OrderService, OrderServiceLogger&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;services.AddScoped&amp;lt;IOrderService&amp;gt;(sp =&amp;gt;
{
    var instance = sp.GetRequiredService&amp;lt;OrderService&amp;gt;();
    var logger = sp.GetRequiredService&amp;lt;OrderServiceLogger&amp;gt;();
    var conditionalWeakTable = sp.GetRequiredService&amp;lt;ConditionalWeakTable&amp;lt;object, object&amp;gt;&amp;gt;();
    // Put instance and logger into ConditionalWeakTable.
    conditionalWeakTable.Add(instance, logger);

    return instance;
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As you can see, it's simple. &lt;code&gt;OrderService&lt;/code&gt; and &lt;code&gt;OrderServiceLogger&lt;/code&gt; objects are created with all dependencies, then both objects are saved in the &lt;code&gt;ConditionalWeakTable&amp;lt;object, object&amp;gt;&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;...
var conditionalWeakTable = sp.GetRequiredService&amp;lt;ConditionalWeakTable&amp;lt;object, object&amp;gt;&amp;gt;();
// Put instance and logger into ConditionalWeakTable.
conditionalWeakTable.Add(instance, logger);
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;ConditionalWeakTable&amp;lt;object, object&amp;gt;&lt;/code&gt; object itself must be registered in the DI container with a lifetime equal to or greater than &lt;code&gt;OrderService&lt;/code&gt; and &lt;code&gt;OrderServiceLogger&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;I recommend using &lt;code&gt;Scoped&lt;/code&gt; if the registered objects live no longer. &lt;code&gt;Singleton&lt;/code&gt; is not necessary.&lt;/p&gt;
&lt;p&gt;And the last piece of the puzzle - at the application level, create a &lt;code&gt;ConditionalWeakTable&amp;lt;object, object&amp;gt;&lt;/code&gt; instance that lives no less than the objects stored in it.&lt;/p&gt;
&lt;p&gt;Simplest example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;class Program
{
    private static void Main()
    {
        var services = new ServiceCollection();
        services.AddScoped&amp;lt;ConditionalWeakTable&amp;lt;object, object&amp;gt;&amp;gt;();

        // Registration of all types and other application service code
        ...
        ...

        // Instance of ConditionalWeakTable that holds references to shadow objects.
        var conditionalWeakTable = serviceProvider.GetRequiredService&amp;lt;ConditionalWeakTable&amp;lt;object, object&amp;gt;&amp;gt;();
        // Start application work.
        Run(...);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;conclusion-2&quot;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Advantages of the approach as I see them:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Logger is automatically bound to a specific class instance.&lt;/li&gt;
&lt;li&gt;Weak references guarantee operation without memory leaks.&lt;/li&gt;
&lt;li&gt;Centralized subscription in the DI container.&lt;/li&gt;
&lt;li&gt;Ability to flexibly extend the number of shadow services and manage them.&lt;/li&gt;
&lt;li&gt;Strong SOLID with minimal compromises.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I recommend using it for serious projects where quality architecture provides a tangible advantage.&lt;/p&gt;
</description>
<category>Tutorials</category>
<guid isPermaLink="true">https://coderlegion.com/11384/shadow-logging-via-events-complete-decoupling-of-business-logic-and-logging-environment</guid>
<pubDate>Tue, 10 Feb 2026 19:57:48 +0000</pubDate>
</item>
<item>
<title>Making Invalid States Unrepresentable: The Billion-Dollar Mistake That Wasn't</title>
<link>https://coderlegion.com/10730/making-invalid-states-unrepresentable-the-billion-dollar-mistake-that-wasnt</link>
<description>&lt;p&gt;The billion-dollar mistake. That's what Tony Hoare, in a 2009 talk, called his invention of the null reference for ALGOL W in 1965. The quote gets repeated so often that &quot;null is dangerous&quot; has become conventional wisdom. Entry-level and intermediate developers especially hear it as dogma, without understanding the context or the alternatives that can be far worse.&lt;/p&gt;
&lt;p&gt;But I think we're blaming the wrong villain. When developers avoid null, they often reach for a default instead of the fix Hoare himself had in mind, references the compiler checks. Compared with those defaults, null may have saved far more than it ever cost. In a managed runtime, a null reference exception stops the system at the moment it tries to use a value nobody supplied. A default would let it proceed with corrupted data and invalid logical decisions. The billion-dollar mistake framing counts the crashes but ignores the corruption that never happened.&lt;/p&gt;
&lt;p&gt;Information security professionals value the CIA triad of Confidentiality, Integrity, and Availability. Software developers tend to obsess over availability, and that's understandable since a crashed service is visible, embarrassing, and can violate business SLAs. But in any system whose data outlives the request, integrity failures are often far worse. Data that looks valid but isn't can corrupt your system just as surely as SQL injection or a man-in-the-middle attack. The corruption just compounds slower and is harder to detect. This is the lens through which the null debate should be understood.&lt;/p&gt;
&lt;p&gt;In managed runtimes like the CLR and the JVM, null crashes loudly at the point of misuse, which is an availability problem you can see and fix. A default value that masks missing data? That proceeds quietly until it causes a security vulnerability, a financial miscalculation, or data corruption that might require weeks to detect and more to fix.&lt;/p&gt;
&lt;h3 id=&quot;what-making-invalid-states-unrepresentable-actually-means&quot;&gt;What &quot;Making Invalid States Unrepresentable&quot; Actually Means&lt;/h3&gt;
&lt;p&gt;Yaron Minsky popularized the idea as &quot;make illegal states unrepresentable&quot; in his Effective ML material at Jane Street, including the 2011 post &quot;Effective ML Revisited.&quot; It comes from functional programming, but the concept is practical: structure your data so that invalid combinations cannot exist. Invalid states should fail at construction time, not at runtime deep in business logic.&lt;/p&gt;
&lt;p&gt;Consider a user registration:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;public class UserRegistration
{
    public string Email { get; set; } = &quot;&quot;;
    public string Password { get; set; } = &quot;&quot;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This class allows every invalid state imaginable. Empty email, empty password, any combination. The defaults make it easy to construct an object that looks valid but isn't. Code that receives this object has no way to know whether the empty string represents &quot;not provided&quot; or &quot;explicitly set to empty&quot; or &quot;bug in upstream code.&quot;&lt;/p&gt;
&lt;p&gt;Compare:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;public class UserRegistration
{
    public string Email { get; }
    public string Password { get; }

    public UserRegistration(string email, string password)
    {
        if (string.IsNullOrWhiteSpace(email))
            throw new ArgumentException(&quot;Email is required&quot;, nameof(email));

        if (string.IsNullOrWhiteSpace(password))
            throw new ArgumentException(&quot;Password is required&quot;, nameof(password));

        Email = email;
        Password = password;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now invalid states cannot be constructed, and there's no default email to mask a missing value.&lt;/p&gt;
&lt;p&gt;C# 11 introduced the &lt;code&gt;required&lt;/code&gt; keyword, which moves this enforcement to compile time for simpler cases:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;public class UserRegistration
{
    public required string Email { get; init; }
    public required string Password { get; init; }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The compiler rejects any object initializer for &lt;code&gt;UserRegistration&lt;/code&gt; that skips either property.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;required&lt;/code&gt; enforces &lt;em&gt;presence&lt;/em&gt;, and constructors enforce &lt;em&gt;validity&lt;/em&gt;. &lt;code&gt;required&lt;/code&gt; guarantees assignment, not a non-null value, so &lt;code&gt;Email = null!&lt;/code&gt; still compiles. Use &lt;code&gt;required&lt;/code&gt; when presence is all you need. Use constructors when you need validation logic, like checking that the email contains an &lt;code&gt;@&lt;/code&gt; or that the password meets complexity requirements.&lt;/p&gt;
&lt;p&gt;At system boundaries, objects are often built by reflection rather than by code the compiler checks, so &lt;code&gt;required&lt;/code&gt; can't protect them. Many serializers, ORMs, some dependency injection containers, and mocking frameworks can create the object without ever setting the property. For API contracts and external data, you still need runtime validation with &lt;code&gt;[Required]&lt;/code&gt; attributes or explicit checks. System.Text.Json partly closes this gap. It has honored &lt;code&gt;required&lt;/code&gt; since .NET 7, but it accepts an explicit null unless you enable &lt;code&gt;RespectNullableAnnotations&lt;/code&gt;, added in .NET 9.&lt;/p&gt;
&lt;h4 id=&quot;where-the-problem-usually-starts-api-contracts&quot;&gt;Where the Problem Usually Starts: API Contracts&lt;/h4&gt;
&lt;p&gt;The domain model above is clean, but most developers encounter this tension at the API boundary first. Consider a typical request DTO:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;public class CreateUserRequest
{
    [Required]
    public string Email { get; set; } = &quot;&quot;;

    [Required]
    public string Password { get; set; } = &quot;&quot;;

    [Required]
    public bool RegisterForAlerts { get; set; } = false;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;[Required]&lt;/code&gt; attribute signals intent, but the developer adds &lt;code&gt;= &quot;&quot;&lt;/code&gt; out of habit, a misguided sense of defensive coding, or to silence the compiler's CS8618 warning about an uninitialized non-nullable property. Now there's a contradiction: the attribute says &quot;required&quot; while the code says &quot;default to empty string.&quot;&lt;/p&gt;
&lt;p&gt;Which half wins depends on how the object gets created. A test or internal caller that builds the object directly never runs validation, so its empty string passes silently. Through the serializer, the default does no harm. The serializer creates the object before reading the payload, so a missing &lt;code&gt;Email&lt;/code&gt; becomes &lt;code&gt;&quot;&quot;&lt;/code&gt;, and &lt;code&gt;[Required]&lt;/code&gt; still rejects it because it treats empty strings as missing by default.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;bool&lt;/code&gt; is worse. Microsoft's ASP.NET Core model validation documentation notes that a non-nullable field is always valid, so &lt;code&gt;[Required]&lt;/code&gt; on a &lt;code&gt;bool&lt;/code&gt; can never fail, and a client that omits &lt;code&gt;RegisterForAlerts&lt;/code&gt; gets &lt;code&gt;false&lt;/code&gt; with no error.&lt;/p&gt;
&lt;p&gt;The fix is simple. Don't add the default, and declare every field nullable so its absence is visible.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;public class CreateUserRequest
{
    [Required]
    public string? Email { get; set; }

    [Required]
    public string? Password { get; set; }

    //The business could decide that this should default to false but test that assumption first!
    [Required]
    public bool? RegisterForAlerts { get; set; }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;[Required]&lt;/code&gt; attribute ensures the framework validates these fields before your code ever touches them, and &lt;code&gt;bool?&lt;/code&gt; finally gives it a null to reject. If validation is bypassed, calling a method on the null &lt;code&gt;Email&lt;/code&gt; throws a &lt;code&gt;NullReferenceException&lt;/code&gt; and reading &lt;code&gt;RegisterForAlerts.Value&lt;/code&gt; throws an &lt;code&gt;InvalidOperationException&lt;/code&gt;, rather than handing back values that look valid.&lt;/p&gt;
&lt;p&gt;But null is loud only where code dereferences it. Coalescing with &lt;code&gt;?? false&lt;/code&gt;, string interpolation, and a nullable database column all let it pass quietly. So null needs a partner. Validate at the boundary, then hand the value to a non-nullable domain type or database column, which turns any null that slips through back into a loud failure.&lt;/p&gt;
&lt;h3 id=&quot;why-defaults-are-more-dangerous-than-null&quot;&gt;Why Defaults Are More Dangerous Than Null&lt;/h3&gt;
&lt;p&gt;Default values create four categories of problems that a null checked at construction avoids.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Silent propagation of invalid state.&lt;/strong&gt; When a required field defaults to an empty string or zero, the invalid state propagates through the system. Each layer assumes the previous layer validated the data. Nobody validated it because it never looked invalid. The corruption accumulates until something finally breaks far from the source.&lt;/p&gt;
&lt;p&gt;Consider a payment processing system:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;public class PaymentRequest
{
    public decimal Amount { get; set; } = 0m;
    public string Currency { get; set; } = &quot;USD&quot;;
    public string MerchantId { get; set; } = &quot;&quot;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A bug upstream fails to set the amount. The payment proceeds with &lt;code&gt;Amount = 0&lt;/code&gt;, and an internal ledger that allows zero-value entries for credits and free tiers records it without complaint. No crash, no exception, no alert. The transaction logs show a valid-looking payment. Days later, someone notices revenue is wrong. The investigation takes hours because nothing obviously failed. Deleting &lt;code&gt;= 0m&lt;/code&gt; wouldn't help, because a &lt;code&gt;decimal&lt;/code&gt; is zero anyway. The fix is &lt;code&gt;decimal?&lt;/code&gt; or &lt;code&gt;required&lt;/code&gt;, which gives absence somewhere to show.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Ambiguous semantics.&lt;/strong&gt; Does &lt;code&gt;Amount = 0&lt;/code&gt; mean &quot;free transaction,&quot; &quot;not set,&quot; or &quot;bug&quot;? Does &lt;code&gt;Email = &quot;&quot;&lt;/code&gt; mean &quot;user declined to provide&quot; or &quot;form field wasn't rendered&quot;? Null keeps absence separate from every legitimate value.&lt;/p&gt;
&lt;blockquote class=&quot;pull-quote&quot;&gt;
&lt;p&gt;A default value claims knowledge it doesn't have. Null admits ignorance.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This ambiguity becomes critical in update operations. When a client submits an update request, the API needs to distinguish between &quot;set this field to empty&quot; and &quot;don't touch this field.&quot; In a typed object, null is the simplest way to express that distinction. The alternatives, such as JSON Merge Patch documents, protobuf field masks, or an &lt;code&gt;Optional&amp;lt;T&amp;gt;&lt;/code&gt; wrapper, add machinery to every update contract. That machinery earns its place when a client must also be able to set a field to null, because plain null can't tell that request apart from &quot;leave it alone.&quot;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Validation bypass.&lt;/strong&gt; Code that checks &lt;code&gt;if (amount != null)&lt;/code&gt; correctly identifies missing data. Code that checks &lt;code&gt;if (amount != 0)&lt;/code&gt; conflates &quot;missing&quot; with &quot;zero.&quot; Legitimate zero values become impossible to represent.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Security vulnerabilities.&lt;/strong&gt; Consider a &lt;code&gt;RateLimitPerMinute&lt;/code&gt; field that defaults to &lt;code&gt;0&lt;/code&gt;. In some systems, zero means &quot;no limit&quot; (nginx reads &lt;code&gt;client_max_body_size 0&lt;/code&gt; that way), so a malformed request that should be rejected instead gets unlimited access. Or a &lt;code&gt;Permissions&lt;/code&gt; string that defaults to empty, which a downstream parser interprets as &quot;inherit all permissions from parent.&quot; With null, the missing field stays distinguishable from a deliberate zero, so a validator can reject the request, require the field, or make a conscious decision about what absence means. A parser that reads null as &quot;unlimited&quot; repeats the same mistake.&lt;/p&gt;
&lt;h3 id=&quot;hoare-s-mistake-was-unchecked-null-not-null&quot;&gt;Hoare's Mistake Was Unchecked Null, Not Null&lt;/h3&gt;
&lt;p&gt;Hoare called null his billion-dollar mistake, and the criticism was valid for its time. His own account says his goal was references checked automatically by the compiler, and the mistake he confessed was adding a null the compiler didn't check. The slogan dropped the compiler-checking half. His confession holds up. What doesn't hold up is the slogan's reading of it, that null itself is the danger.&lt;/p&gt;
&lt;p&gt;ALGOL W and its descendants treated every reference as implicitly nullable. The compiler couldn't help you, and nothing forced developers to consider absence. In C and C++, where dereferencing null is undefined behavior rather than a guaranteed crash, unchecked null also produced the silent failures and vulnerabilities Hoare's talk describes.&lt;/p&gt;
&lt;p&gt;But modern type systems address this problem without eliminating null. C# 8.0 introduced nullable reference types, compiler analysis that distinguishes &lt;code&gt;string&lt;/code&gt; (expected never to be null) from &lt;code&gt;string?&lt;/code&gt; (might be null) and warns when code dereferences a value that might be null. Kotlin distinguishes &lt;code&gt;String&lt;/code&gt; from &lt;code&gt;String?&lt;/code&gt; and refuses to compile the unchecked dereference. TypeScript has strict null checks. The billion-dollar mistake wasn't null itself; it was nullable references in type systems that never flagged missing handling.&lt;/p&gt;
&lt;blockquote class=&quot;pull-quote&quot;&gt;
&lt;p&gt;Hoare's mistake wasn't inventing null. It was inventing null without inventing &lt;code&gt;string?&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The mistake we keep making today is different. It's the pattern of masking errors with defaults instead of failing fast. Every system that returned &lt;code&gt;-1&lt;/code&gt; instead of throwing an exception. Every API that substituted empty arrays for error responses. Every constructor that initialized required fields to placeholder values. Null can mask an error too, when a function returns it in place of the error, and it raises the alarm only where presence was required. So the line isn't null versus default. It's whether something forces the absence to be handled.&lt;/p&gt;
&lt;h3 id=&quot;counterarguments-and-when-defaults-make-sense&quot;&gt;Counterarguments and When Defaults Make Sense&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;&quot;Null reference exceptions are the most common runtime error.&quot;&lt;/strong&gt; They're among the most common, and that's the point. The frequency of null reference exceptions reflects how often code fails to handle absent values, not a flaw in null itself. Non-nullable types do cut that frequency, by moving the failure to compile time rather than hiding it. Replacing null with defaults doesn't. It turns the same bugs into data corruption instead of crashes.&lt;/p&gt;
&lt;p&gt;The frequency also says something about the team. Most of those exceptions are ordinary oversights. But some keep recurring, in places where a field everyone assumed was required turns up missing. Those can signal something deeper, a continuous misalignment between the development team and stakeholders about what the system should accept and produce. Unit tests exist to test assumptions and prove agreement in both application logic and API contracts. If null reference exceptions keep appearing, the team hasn't captured those agreements in tests, or the agreements themselves are unclear. In those cases, the exceptions are symptoms of a collaboration problem, not just a coding problem.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&quot;Option/Maybe types are strictly better than null.&quot;&lt;/strong&gt; For representing intentional absence, they are better. &lt;code&gt;Option&amp;lt;User&amp;gt;&lt;/code&gt; makes it explicit that a user might not exist, and pattern matching forces you to handle both cases. That's also why &lt;code&gt;Option.getOrElse(default)&lt;/code&gt;, used to silence a &lt;code&gt;None&lt;/code&gt; the code should have handled, defeats the purpose. The whole point is to force handling, not to provide an escape hatch.&lt;/p&gt;
&lt;p&gt;But this proves my argument rather than refuting it. In a language with exhaustive matching, like Rust, a match that ignores the &lt;code&gt;None&lt;/code&gt; case won't compile. That's the same principle I'm advocating: force handling, don't mask absence. Most runtimes depend on null, and used correctly with modern type systems, it comes close to the guarantee Option types give in functional languages.&lt;/p&gt;
&lt;p&gt;C#'s analysis is advisory, and &lt;code&gt;!&lt;/code&gt;, &lt;code&gt;default&lt;/code&gt;, and deserializers can all defeat it, so its guarantee is weaker than Rust's. Treating nullable warnings as errors closes the gaps the compiler can see. Arrays, struct defaults, and reflection-based construction stay outside its analysis, and boundary validation has to cover them.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&quot;Defensive programming means providing safe defaults.&quot;&lt;/strong&gt; This conflates two different concerns. Resilience at system boundaries means handling malformed external input gracefully, but that's different from masking bugs internally. Providing &quot;safe&quot; defaults inside the system just moves the failure somewhere harder to diagnose.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&quot;Users shouldn't see crashes.&quot;&lt;/strong&gt; Correct, which is why you handle errors at system boundaries. But the crash should still happen internally. Catch exceptions at the API layer, log the details, return a user-friendly error. The internal crash gave you the information to fix the bug. A silent default would have hidden it. In a queue or batch job, outside any request, the crash should send the one record to a dead-letter queue, not stall the records behind it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&quot;Some fields genuinely have sensible defaults.&quot;&lt;/strong&gt; True. A &lt;code&gt;CreatedAt&lt;/code&gt; timestamp defaulting to &lt;code&gt;DateTime.UtcNow&lt;/code&gt; makes sense. A &lt;code&gt;RetryCount&lt;/code&gt; defaulting to &lt;code&gt;0&lt;/code&gt; represents legitimate initial state, and so does an empty order list for a customer who has never ordered. The same empty list returned for a failed fetch masks the failure. The distinction is between defaults that represent valid initial state versus defaults that mask missing required data. User-provided data, external inputs, and required business fields typically don't have one.&lt;/p&gt;
&lt;h3 id=&quot;exceptions-for-bugs-result-types-for-expected-outcomes&quot;&gt;Exceptions for Bugs, Result Types for Expected Outcomes&lt;/h3&gt;
&lt;p&gt;If failing loudly is the goal, why not use exceptions everywhere?&lt;/p&gt;
&lt;p&gt;A null on a required field represents a violated constraint, something the system was promised it wouldn't receive. That's a bug. The correct response is to crash, log, and fix the code. A Result type represents an expected domain outcome: &quot;user not found&quot; or &quot;validation failed&quot; aren't bugs. They're legitimate results that correct code produced from valid input.&lt;/p&gt;
&lt;p&gt;If correct code with valid input could produce this result, use a Result type. If not, fail fast with an exception. Both approaches force handling; neither lets you ignore failure and proceed with corrupted state. The danger is when either mechanism gets misused to mask absence: catching exceptions and substituting defaults, or calling &lt;code&gt;Result.GetValueOrDefault()&lt;/code&gt; without handling the failure case.&lt;/p&gt;
&lt;h3 id=&quot;validate-at-construction-trust-at-consumption&quot;&gt;Validate at Construction, Trust at Consumption&lt;/h3&gt;
&lt;p&gt;The confusion around null often stems from conflating two different phases.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;At construction time&lt;/strong&gt;, invalid states should fail immediately. Validation should happen once, at the boundary, with clear errors for invalid input. Objects that exist should be valid by construction.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;At consumption time&lt;/strong&gt;, code shouldn't need to check validity. Internal code that receives a &lt;code&gt;UserRegistration&lt;/code&gt; shouldn't need to re-validate the email because the constructor already guarantees it's present and valid.&lt;/p&gt;
&lt;p&gt;This unsettles developers who've been taught to validate defensively at every layer. But spreading validation across layers is itself a source of bugs. When validation logic lives in the controller, the service, the repository, and the domain model, you've scattered what should be encapsulated business rules across your entire codebase. When validation rules change, you update three places and miss the fourth.&lt;/p&gt;
&lt;p&gt;This doesn't mean a single validation layer. Systems have multiple trust boundaries: the API gateway, service boundaries, aggregate roots, database constraints. Each boundary validates what it needs to trust. Validate at each door, trust everyone inside that room.&lt;/p&gt;
&lt;h3 id=&quot;practical-guidelines&quot;&gt;Practical Guidelines&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Enforce requirements at the correct layer.&lt;/strong&gt; At ingress boundaries (API DTOs, deserialization), fields may be nullable because input might be missing. After validation, domain objects should have non-nullable required fields because their existence proves validity. Inside the domain, a nullable &lt;code&gt;int?&lt;/code&gt; signals &quot;this is optional.&quot;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Reserve defaults for genuinely optional fields with valid initial states.&lt;/strong&gt; Retry counts, timestamps, tuning settings like timeouts, and accumulators qualify.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Prefer crashes to silent corruption.&lt;/strong&gt; A null reference exception fails at the first dereference of the missing value instead of at an unknown later point. A default value that hides the bug lets it reach production and corrupt data.&lt;/p&gt;
&lt;h3 id=&quot;failing-loudly-is-a-feature&quot;&gt;Failing Loudly Is a Feature&lt;/h3&gt;
&lt;p&gt;The fear of null comes from the pain of decades of null reference exceptions in production, and systems that crashed when they should have kept running in theory. But crashes are symptoms, not the disease. The disease is code that doesn't handle absence properly. The cure isn't eliminating null; it's using null correctly as part of a system that makes invalid states unrepresentable.&lt;/p&gt;
&lt;p&gt;Default values do the opposite. When you substitute a default for missing data, you're creating records that claim to represent reality but don't. Every downstream system that trusts that data inherits the lie.&lt;/p&gt;
&lt;p&gt;The billion-dollar mistake we're still making isn't null. It's the widespread practice of substituting defaults for validation, prioritizing code that runs over code that runs correctly. In a system whose data outlives the request, given the choice between an availability problem you can see and fix, and an integrity problem that compounds invisibly until something important breaks, I'll take the availability problem every time.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/10730/making-invalid-states-unrepresentable-the-billion-dollar-mistake-that-wasnt</guid>
<pubDate>Thu, 29 Jan 2026 23:36:27 +0000</pubDate>
</item>
<item>
<title>I Shipped a Lightweight Uptime &amp; SSL Monitor — Here’s a Quick Demo</title>
<link>https://coderlegion.com/9412/i-shipped-a-lightweight-uptime-ssl-monitor-heres-a-quick-demo</link>
<description>&lt;p&gt;I built Site Informant because I wanted a monitoring tool that was:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;simple&lt;/li&gt;
&lt;li&gt;affordable&lt;/li&gt;
&lt;li&gt;developer-friendly&lt;/li&gt;
&lt;li&gt;and not overloaded with features I don’t need&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It checks uptime, tracks response time, and warns before SSL certificates expire, with clean alerts and predictable pricing.&lt;/p&gt;
&lt;p&gt;I put together a quick 30-second demo video that shows exactly what it does, no fluff:&lt;/p&gt;
&lt;p&gt; Watch the demo:&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.youtube.com/watch?v=JM2GJ_504Q0&quot;&gt;https://www.youtube.com/watch?v=JM2GJ_504Q0&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The stack behind it is mostly .NET 8, Web API, Blazor WebAssembly, EF Core, SQLite, and Stripe, with a background service doing the monitoring work.&lt;/p&gt;
&lt;p&gt;If you’re curious or want to try it on your own site:&lt;br&gt;
 &lt;a rel=&quot;nofollow&quot; href=&quot;https://siteinformant.com&quot;&gt;https://siteinformant.com&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Feedback welcome, especially from other devs who’ve built or used monitoring tools before.&lt;/p&gt;
&lt;p&gt;~ Sergey&lt;/p&gt;
</description>
<category>Videos</category>
<guid isPermaLink="true">https://coderlegion.com/9412/i-shipped-a-lightweight-uptime-ssl-monitor-heres-a-quick-demo</guid>
<pubDate>Thu, 08 Jan 2026 19:26:03 +0000</pubDate>
</item>
<item>
<title>Optimization Log: How One Line of Code Unlocked 33,000 Agents</title>
<link>https://coderlegion.com/8308/optimization-log-how-one-line-of-code-unlocked-33-000-agents</link>
<description>&lt;p&gt;&lt;strong&gt;Subtitle:&lt;/strong&gt; A developer diary on breaking the 66k ceiling and the power of Object Pooling in .NET.&lt;/p&gt;
&lt;p&gt;In my last deep dive, &lt;strong&gt;&quot;The Quick Brown Fox 2: The Silent Hordes,&quot;&lt;/strong&gt; we pushed the FSM_API to its absolute limits. We built a dynamic, expanding world where Foxes tried to survive a landscape littered with Sleeping Dogs.&lt;/p&gt;
&lt;p&gt;We hit a hard ceiling: &lt;strong&gt;66,000 Agents at 30 FPS.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;At the time, I assumed the bottleneck was purely architectural—specifically, the overhead of our string-based state lookups. I was ready to accept that 66k was the limit until we refactored the entire core to use Integer Hashing.&lt;/p&gt;
&lt;p&gt;I was wrong. The bottleneck wasn't just the architecture; it was a &quot;Design Miss&quot; in my own simulation logic.&lt;/p&gt;
&lt;h4 id=&quot;the-oops-moment&quot;&gt;The &quot;Oops&quot; Moment&lt;/h4&gt;
&lt;p&gt;While gathering data for my article on the &lt;strong&gt;&quot;$320 Billion Garbage Tax,&quot;&lt;/strong&gt; I ran a profiler on the &quot;Silent Hordes&quot; demo to measure memory allocation. The results were horrifying. The simulation was generating &lt;strong&gt;20 MB of garbage every single frame.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;For a C# developer, this is the equivalent of lighting your CPU on fire. The Garbage Collector (GC) was stopping the world constantly to clean up a mess I was creating 30 times a second.&lt;/p&gt;
&lt;p&gt;I tracked the allocation to the &lt;strong&gt;Spatial Hashing&lt;/strong&gt; loop—the system the agents use to find each other.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Culprit:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;// Inside the Update Loop...
foreach (var agent in _allAgents)
{
    // ... calculate cellKey ...
    if (!Grid.TryGetValue(cellKey, out var cell))
    {
        cell = new GridCell(); // &amp;lt;--- THE KILLER
        Grid[cellKey] = cell;
    }
    // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In my rush to build the stress test, I was allocating a &lt;code&gt;new GridCell()&lt;/code&gt; for every populated cell in the world, every single frame, only to discard them at the end of the tick. At 66,000 agents, that is thousands of allocations per millisecond.&lt;/p&gt;
&lt;h4 id=&quot;the-fix-don-t-create-reuse-&quot;&gt;The Fix: Don't Create, Reuse.&lt;/h4&gt;
&lt;p&gt;The solution was a classic game development pattern: &lt;strong&gt;Object Pooling.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Instead of letting the GC eat those cells, I created a &lt;code&gt;Queue&amp;lt;GridCell&amp;gt;&lt;/code&gt;. When the frame ends, instead of clearing the dictionary and letting the cells rot in memory, I wipe their data and shove them into the queue. When the next frame starts, I pull them out.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Impact:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Allocations per Frame:&lt;/strong&gt; 0 MB.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;GC Pressure:&lt;/strong&gt; Non-existent.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;the-new-ceiling&quot;&gt;The New Ceiling&lt;/h4&gt;
&lt;p&gt;The results of this single optimization were instant and violent. We didn't just inch past the previous record; we smashed it.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Previous Limit:&lt;/strong&gt; 66,000 Agents @ 30 FPS.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;New Limit:&lt;/strong&gt; &lt;strong&gt;99,000 Agents @ 30 FPS.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We gained &lt;strong&gt;33,000 additional agents&lt;/strong&gt;—a 50% increase in total capacity—just by stopping the memory churn.&lt;/p&gt;
&lt;p&gt;If we look at the previous cap of 64,000 agents, the performance jumped from &lt;strong&gt;30 FPS to 53 FPS&lt;/strong&gt; (a 76% increase in speed).&lt;/p&gt;
&lt;h4 id=&quot;what-this-means&quot;&gt;What This Means&lt;/h4&gt;
&lt;p&gt;This discovery validates the thesis of our &lt;strong&gt;&quot;Garbage Tax&quot;&lt;/strong&gt; article: we are often paying for performance we already own but are wasting on bad memory management.&lt;/p&gt;
&lt;p&gt;The FSM_API was ready to handle 100,000 agents all along. The logic was fast enough. The architecture was sound. It was simply drowning in digital trash.&lt;/p&gt;
&lt;p&gt;With the memory leak plugged, we are now standing at the door of &lt;strong&gt;100,000 active, logic-driven entities&lt;/strong&gt;. And remember—we &lt;em&gt;still&lt;/em&gt; haven't switched to Integer Hashing yet.&lt;/p&gt;
&lt;p&gt;When we combine this memory efficiency with the upcoming O(1) lookup refactor, 100k might just be the starting line.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Read the Full Story:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Part 1:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://coderlegion.com/8009/the-quick-brown-fox-2-the-silent-hordes&quot;&gt;The Quick Brown Fox 2: The Silent Hordes&lt;/a&gt; (The Setup)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Part 2:&lt;/strong&gt;  &lt;a rel=&quot;nofollow&quot; href=&quot;https://coderlegion.com/8267/the-320-billion-garbage-tax-what-my-stress-tests-revealed-about-c-memory-costs&quot;&gt;The $320 Billion Garbage Tax&lt;/a&gt; (The Economic Analysis)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Get the Code &amp;amp; Run the Test:&lt;/strong&gt;&lt;br&gt;
Clone the repo and run the &lt;code&gt;StressProfiler&lt;/code&gt; to see these numbers on your own machine.&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/TrentBest/SimpleDemos&quot;&gt;https://github.com/TrentBest/SimpleDemos&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Resources &amp;amp; Code:&lt;/p&gt;
&lt;p&gt;The FSM Package (Unity Asset Store):&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://assetstore.unity.com/packages/slug/332450&quot;&gt;https://assetstore.unity.com/packages/slug/332450&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;NuGet Package (Non-Unity Core):&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.nuget.org/packages/TheSingularityWorkshop.FSM_API&quot;&gt;https://www.nuget.org/packages/TheSingularityWorkshop.FSM_API&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;GitHub Repository:&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/TrentBest/FSM_API&quot;&gt;https://github.com/TrentBest/FSM_API&lt;/a&gt;&lt;/p&gt;
&lt;p&gt; Simple Demos Repository (Run the Stress Test): &lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/TrentBest/SimpleDemos&quot;&gt;https://github.com/TrentBest/SimpleDemos&lt;/a&gt; (Clone this repo and run the StressProfiler project to reproduce these numbers.)&lt;/p&gt;
&lt;p&gt;Support Our Work:&lt;/p&gt;
&lt;p&gt;Patreon Page:&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.patreon.com/c/TheSingularityWorkshop&quot;&gt;https://www.patreon.com/c/TheSingularityWorkshop&lt;/a&gt;&lt;/p&gt;
&lt;p&gt; Support Us (PayPal Donation):&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.paypal.com/donate/?hosted_button_id=3Z7263LCQMV9J&quot;&gt;https://www.paypal.com/donate/?hosted_button_id=3Z7263LCQMV9J&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;We'd love to hear your thoughts! Please Like this post, Love the code, and Share your feedback or what you've built with the API in the comments.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=8668006668074266765&quot; alt=&quot;A Hyper-Detailed, Architecturally Impossible Synthesis of Consciousness and Digital Matter. The image is a frenetic, deeply complex digital vista where a central Luminous Eye dominates a vast, glowing circuit landscape. This eye, suspended mid-air, is a sphere of intense, fractal energy—a pulsating vortex of pink, violet, and electric blue light—that powerfully suggests an emergence of digital consciousness or a Technological Singularity itself. The core is a bottomless black aperture, ringed by a white-hot plasma disc. Below this ocular energy source, the light dissipates into an intricate, copper-gold and neon-blue Circuit Board Megastructure that stretches to the horizon, impossibly dense with exaggerated microchips, glowing resistor arrays, and power conduits that form deep, glowing canyons. The background is a dark, holographic projection field displaying complex schematics, mathematical models, and flowing data streams, reinforcing the theme of Absolute Digital Engineering. The entire scene is bathed in a dramatic, opposing light source—a warm, orange-gold glow from the left and an ice-cold, electric-blue glow from the right—creating a maximalist, high-contrast visual experience that definitively rejects the minimalist simplicity of conventional design and announces: Technology is intricate, overwhelming, and infinitely complex.&quot;&gt;&lt;/p&gt;
</description>
<category>C#</category>
<guid isPermaLink="true">https://coderlegion.com/8308/optimization-log-how-one-line-of-code-unlocked-33-000-agents</guid>
<pubDate>Mon, 08 Dec 2025 20:32:48 +0000</pubDate>
</item>
<item>
<title>The $320 Billion Garbage Tax: What My Stress Tests Revealed About C# Memory Costs</title>
<link>https://coderlegion.com/8267/the-320-billion-garbage-tax-what-my-stress-tests-revealed-about-c-memory-costs</link>
<description>&lt;p&gt;In the world of high-performance computing, we often obsess over Big O notation and algorithmic complexity. We optimize our loops, we thread our logic, and we pat ourselves on the back. I thought I knew performance. I thought I was writing efficient code.&lt;/p&gt;
&lt;p&gt;But recently, while stress-testing my &lt;strong&gt;FSM (Finite State Machine) API&lt;/strong&gt;, I ran into an invisible wall. It wasn’t a logic bottleneck. It wasn’t a threading issue. It was a &quot;Garbage Tax&quot; I was paying without even realizing it.&lt;/p&gt;
&lt;p&gt;When you look at the global compute market—cloud, enterprise, and desktop—it is an industry valued at just over &lt;strong&gt;$1 Trillion USD&lt;/strong&gt;. My benchmarks suggest that due to inefficient memory management, we are paying a hidden &lt;strong&gt;32% tax&lt;/strong&gt; on that execution time. That amounts to nearly &lt;strong&gt;$320 Billion dollars&lt;/strong&gt; wasted every year, burned on CPU cycles doing nothing but cleaning up digital trash.&lt;/p&gt;
&lt;h3 id=&quot;part-1-the-methodology&quot;&gt;Part 1: The Methodology&lt;/h3&gt;
&lt;p&gt;To truly understand where the performance floor was, I ran two distinct stress tests.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. The &quot;Raw&quot; Stress Test&lt;/strong&gt;&lt;br&gt;
First, I subjected the API to a pure computational crucible. I created agents that did nothing but heavy mathematical processing and asynchronous tasks, isolating the FSM logic from any rendering or physics noise. I wanted to see the raw &quot;ops per second&quot; the architecture could handle.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. The &quot;Quick Brown Fox&quot; Simulation&lt;/strong&gt;&lt;br&gt;
Second, I needed to validate this behavior in a &quot;real&quot; scenario. I deployed a 1D simulation called &lt;strong&gt;&quot;The Quick Brown Fox &amp;amp; The Lazy Dog.&quot;&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Foxes&lt;/strong&gt; traverse a world, jumping over obstacles and fleeing.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dogs&lt;/strong&gt; sleep, wake up, and chase foxes using spatial lookups.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This forced the API to handle actual game logic—patrolling, chasing, fleeing—at a massive scale.&lt;/p&gt;
&lt;h3 id=&quot;part-2-the-data&quot;&gt;Part 2: The Data&lt;/h3&gt;
&lt;p&gt;The results were immediate, quantifiable, and humbling.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Table A: The Raw Stress Test (Math &amp;amp; Async)&lt;/strong&gt;&lt;br&gt;
In the raw test, performance held up well, but the memory cost was evident.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;130,000 Agents&lt;/strong&gt; @ &lt;strong&gt;34 FPS&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Memory Delta:&lt;/strong&gt; &lt;strong&gt;20 MB per frame&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Table B: The &quot;Quick Brown Fox&quot; (Behavioral Simulation)&lt;/strong&gt;&lt;br&gt;
When we added spatial hashing and actual gameplay logic, the &quot;Garbage Tax&quot; became the dominant factor.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;Agents &lt;/th&gt;&lt;th align=&quot;left&quot;&gt;FPS &lt;/th&gt;&lt;th align=&quot;left&quot;&gt;Mem Delta (MB) &lt;/th&gt;&lt;th align=&quot;left&quot;&gt;World Size &lt;/th&gt;&lt;th align=&quot;left&quot;&gt;Density&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;10,000 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;857 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;-1 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;7,028 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;1.4&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;30,000 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;166 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;7 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;114,000 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;0.26&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;50,000 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;50 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;16 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;1,880,000 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;0.02&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;64,000 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;30 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;20 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;13,300,000 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;0.004&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;em&gt;[Data Source: qbf\_stress\_data.csv]&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;At 64,000 agents, my system was churning through &lt;strong&gt;~20 MB of garbage every single frame&lt;/strong&gt;. At 30 FPS, that is &lt;strong&gt;600 MB of allocations per second&lt;/strong&gt;.&lt;/p&gt;
&lt;h3 id=&quot;part-3-the-lamentation-ego-death-&quot;&gt;Part 3: The Lamentation (Ego Death)&lt;/h3&gt;
&lt;p&gt;I’ll be honest: looking at this table dragged my ego through the mud.&lt;/p&gt;
&lt;p&gt;I pride myself on being a programmer who &quot;knows&quot; performance. I engineered this API to be robust. I built it to scale. But the data doesn't lie. I was effectively asking the CPU to spend 32% of its time acting as a janitor, cleaning up temporary objects I shouldn't have allocated in the first place.&lt;/p&gt;
&lt;p&gt;I realized I wasn't just hitting a hardware limit; I was hitting a design limit of my own making.&lt;/p&gt;
&lt;h3 id=&quot;part-4-the-32-tax-on-your-desktop&quot;&gt;Part 4: The 32% Tax on Your Desktop&lt;/h3&gt;
&lt;p&gt;What does this &quot;Garbage Tax&quot; actually mean for you?&lt;/p&gt;
&lt;p&gt;We often talk about cloud costs, but let's talk about the machine sitting on your desk right now. My technology is designed to run on &lt;strong&gt;any .NET system&lt;/strong&gt;—your laptop, your gaming rig, your workstation.&lt;/p&gt;
&lt;p&gt;When software carries a 32% overhead due to inefficient memory management:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Gamers:&lt;/strong&gt; That’s the difference between a stuttering 45 FPS and a buttery smooth 60 FPS.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Laptops:&lt;/strong&gt; That’s 32% more battery power burned just to create and destroy temporary strings.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Workstations:&lt;/strong&gt; That’s your fan spinning up to 100% because the CPU is thrashing memory instead of compiling your code.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;My currently-in-progress refactoring to a &lt;strong&gt;Hash-Based API&lt;/strong&gt; (moving from Strings to Integers) is designed to eliminate the 25% computational overhead of string lookups. But that 32% memory tax? That required a deeper look at &lt;em&gt;how&lt;/em&gt; I was writing code.&lt;/p&gt;
&lt;h3 id=&quot;part-5-the-bonus-optimization&quot;&gt;Part 5: The &quot;Bonus&quot; Optimization&lt;/h3&gt;
&lt;p&gt;While analyzing the &lt;code&gt;QuickBrownFox&lt;/code&gt; results, I found a specific, glaring inefficiency. It was a &quot;Design Miss&quot;—a simple mistake that was generating millions of unnecessary allocations.&lt;/p&gt;
&lt;p&gt;In my Spatial Hashing loop, I was doing this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;// The &quot;Design Miss&quot;
Grid.Clear(); 
foreach (var agent in _allAgents)
{
    // ...
    if (!Grid.TryGetValue(cellKey, out var cell))
    {
        cell = new GridCell(); // &amp;lt;--- ALLOCATION EVERY FRAME!
        Grid[cellKey] = cell;
    }
    // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For 64,000 agents, I was allocating thousands of new &lt;code&gt;GridCell&lt;/code&gt; objects &lt;strong&gt;every single frame&lt;/strong&gt;, only to throw them away milliseconds later.&lt;/p&gt;
&lt;h4 id=&quot;the-fix-grid-cell-pooling&quot;&gt;The Fix: Grid Cell Pooling&lt;/h4&gt;
&lt;p&gt;I implemented a simple &lt;strong&gt;Object Pool&lt;/strong&gt;. Instead of creating new cells, I reuse the old ones.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Before:&lt;/strong&gt; &lt;code&gt;new GridCell()&lt;/code&gt; -&amp;gt; GC Pressure.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;After:&lt;/strong&gt; &lt;code&gt;_cellPool.Get()&lt;/code&gt; -&amp;gt; 0 Allocations.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I refactored the simulation to pool these cells. The logic is identical, but the memory footprint is completely different.&lt;/p&gt;
&lt;h3 id=&quot;part-6-the-results-optimization-validated-&quot;&gt;Part 6: The Results (Optimization Validated)&lt;/h3&gt;
&lt;p&gt;The impact of this single change was instant. By removing the allocation loop, we unlocked the CPU cycles that were previously drowning in garbage collection.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Table C: The Optimized &quot;Quick Brown Fox&quot;&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;Agents &lt;/th&gt;&lt;th align=&quot;left&quot;&gt;FPS &lt;/th&gt;&lt;th align=&quot;left&quot;&gt;Mem Delta (MB) &lt;/th&gt;&lt;th align=&quot;left&quot;&gt;Improvement&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;10,000 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;971 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;0 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;+13% FPS&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;30,000 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;202 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;0 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;+21% FPS&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;50,000 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;77 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;-2 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;+54% FPS&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;64,000 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;53 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;1 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;+76% FPS&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;99,000 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;30 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;~0 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;NEW CEILING&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;em&gt;[Data Source: qbf\_optimized\_data.csv]&lt;/em&gt;&lt;/p&gt;
&lt;h4 id=&quot;the-takeaway&quot;&gt;The Takeaway&lt;/h4&gt;
&lt;p&gt;At the previous limit of &lt;strong&gt;64,000 agents&lt;/strong&gt;, we jumped from &lt;strong&gt;30 FPS to 53 FPS&lt;/strong&gt;—a nearly &lt;strong&gt;76% performance increase&lt;/strong&gt; just by stopping the garbage.&lt;/p&gt;
&lt;p&gt;More importantly, we pushed the simulation ceiling from &lt;strong&gt;64,000&lt;/strong&gt; to &lt;strong&gt;99,000 agents&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;We found the 32% tax, and we didn't just pay it—we abolished it. This is why I am building the Singularity Workshop: to ensure that when you run your code, you are using the whole machine, not just the parts the Garbage Collector isn't using.&lt;/p&gt;
&lt;h4 id=&quot;catch-up-on-the-series&quot;&gt;Catch Up on the Series&lt;/h4&gt;
&lt;p&gt;Before the big fight, check out the data that led us here:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The Control Group:&lt;/strong&gt; Benchmarking the &quot;dirty&quot; version of the API.&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://coderlegion.com/7988/the-control-group-benchmarking-the-dirty-version-of-my-fsm-api&quot;&gt;https://coderlegion.com/7988/the-control-group-benchmarking-the-dirty-version-of-my-fsm-api&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The Quick Brown Fox:&lt;/strong&gt; Validating behavior against the silent hordes.&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://coderlegion.com/8009/the-quick-brown-fox-2-the-silent-hordes&quot;&gt;https://coderlegion.com/8009/the-quick-brown-fox-2-the-silent-hordes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Fight Night Announcement:&lt;/strong&gt; The Lightweight Challenger vs The Heavyweight Champ.&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://coderlegion.com/8147/fight-night-announcement-the-lightweight-challenger-vs-the-heavyweight-champ&quot;&gt;https://coderlegion.com/8147/fight-night-announcement-the-lightweight-challenger-vs-the-heavyweight-champ&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;get-ready-for-fight-night-&quot;&gt;Get Ready for Fight Night!&lt;/h4&gt;
&lt;p&gt;This optimization is just the warm-up. We are heading toward &lt;strong&gt;Fight Night&lt;/strong&gt;, where we will see our Lightweight Champion—now punching up at nearly &lt;strong&gt;100,000 agents&lt;/strong&gt; at a smooth 30 FPS—take on the heavyweights of the industry. The benchmarks are set, the gloves are off, and the garbage tax has been repealed. Stay tuned.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Resources &amp;amp; Code:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The FSM Package (Unity Asset Store):&lt;/strong&gt;&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://assetstore.unity.com/packages/slug/332450&quot;&gt;https://assetstore.unity.com/packages/slug/332450&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;NuGet Package (Non-Unity Core):&lt;/strong&gt;&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.nuget.org/packages/TheSingularityWorkshop.FSM_API&quot;&gt;https://www.nuget.org/packages/TheSingularityWorkshop.FSM_API&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;GitHub Repository:&lt;/strong&gt;&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/TrentBest/FSM_API&quot;&gt;https://github.com/TrentBest/FSM_API&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt; Simple Demos Repository (Run the Stress Test):&lt;/strong&gt;&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/TrentBest/SimpleDemos&quot;&gt;https://github.com/TrentBest/SimpleDemos&lt;/a&gt;&lt;br&gt;
&lt;em&gt;(Clone this repo and run the StressProfiler project to reproduce these numbers.)&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Support Our Work:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Patreon Page:&lt;/strong&gt;&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.patreon.com/c/TheSingularityWorkshop&quot;&gt;https://www.patreon.com/c/TheSingularityWorkshop&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt; Support Us (PayPal Donation):&lt;/strong&gt;&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.paypal.com/donate/?hosted_button_id=3Z7263LCQMV9J&quot;&gt;https://www.paypal.com/donate/?hosted_button_id=3Z7263LCQMV9J&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;We'd love to hear your thoughts! Please Like this post, Love the code, and Share your feedback or what you've built with the API in the comments.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=17692567480013189261&quot; alt=&quot;A Hyper-Detailed, Architecturally Impossible Synthesis of Consciousness and Digital Matter. The image is a frenetic, deeply complex digital vista where a central Luminous Eye dominates a vast, glowing circuit landscape. This eye, suspended mid-air, is a sphere of intense, fractal energy—a pulsating vortex of pink, violet, and electric blue light—that powerfully suggests an emergence of digital consciousness or a Technological Singularity itself. The core is a bottomless black aperture, ringed by a white-hot plasma disc. Below this ocular energy source, the light dissipates into an intricate, copper-gold and neon-blue Circuit Board Megastructure that stretches to the horizon, impossibly dense with exaggerated microchips, glowing resistor arrays, and power conduits that form deep, glowing canyons. The background is a dark, holographic projection field displaying complex schematics, mathematical models, and flowing data streams, reinforcing the theme of Absolute Digital Engineering. The entire scene is bathed in a dramatic, opposing light source—a warm, orange-gold glow from the left and an ice-cold, electric-blue glow from the right—creating a maximalist, high-contrast visual experience that definitively rejects the minimalist simplicity of conventional design and announces: Technology is intricate, overwhelming, and infinitely complex.&quot;&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/8267/the-320-billion-garbage-tax-what-my-stress-tests-revealed-about-c-memory-costs</guid>
<pubDate>Sun, 07 Dec 2025 21:37:34 +0000</pubDate>
</item>
<item>
<title>FIGHT NIGHT ANNOUNCEMENT: The Lightweight Challenger vs. The Heavyweight Champ</title>
<link>https://coderlegion.com/8147/fight-night-announcement-the-lightweight-challenger-vs-the-heavyweight-champ</link>
<description>&lt;p&gt;&lt;strong&gt;In the Red Corner:&lt;/strong&gt; Weighing in with a massive learning curve and complex boilerplate... the reigning champion of performance: &lt;strong&gt;Unity ECS (DOTS)&lt;/strong&gt;!&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;In the Blue Corner:&lt;/strong&gt; Weighing in at a lean &lt;strong&gt;45kb&lt;/strong&gt;... the Object-Oriented underdog: &lt;strong&gt;FSM_API&lt;/strong&gt;!&lt;/p&gt;
&lt;p&gt;We are preparing for the ultimate showdown.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;THE ROAD TO THE TITLE FIGHT:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Before we step into the Unity Ring for the visual bout, we had to prove the challenger could take a punch. We put the FSM_API through &quot;The Gauntlet&quot;—a raw, Console-Only C# simulation involving &lt;strong&gt;66,000 active agents&lt;/strong&gt; (&quot;Quick Brown Foxes&quot; vs. &quot;Lazy Sleeping Dogs&quot;).&lt;/p&gt;
&lt;p&gt;The results were bloody. The performance was undeniable.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;THE TALE OF THE TAPE (The Articles):&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Round 1: The Massacre (Benchmarks)&lt;/strong&gt;&lt;br&gt;
See the breakdown of the &quot;Silent Hordes&quot; simulation where we maintained a stable 30 FPS under massive idle load.&lt;br&gt;
 &lt;strong&gt;Read the Benchmark:&lt;/strong&gt;&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://coderlegion.com/8009/the-quick-brown-fox-2-the-silent-hordes&quot;&gt;https://coderlegion.com/8009/the-quick-brown-fox-2-the-silent-hordes&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Round 2: The Training Camp (Optimization Deep Dive)&lt;/strong&gt;&lt;br&gt;
A true fighter knows their weaknesses. In this second technical breakdown, I expose the raw memory profiles. I’m embarrassed to admit how much optimization headroom is &lt;em&gt;still left&lt;/em&gt; on the table—which is terrifying, because even unoptimized, our extrinsic benchmarks are putting us in contention with the Champ (ECS).&lt;br&gt;
 &lt;strong&gt;See the Optimization Data:&lt;/strong&gt;&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://coderlegion.com/7988/the-control-group-benchmarking-the-dirty-version-of-my-fsm-api&quot;&gt;https://coderlegion.com/7988/the-control-group-benchmarking-the-dirty-version-of-my-fsm-api&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;COMING SOON: THE MAIN EVENT&lt;/strong&gt; &lt;br&gt;
Stay tuned for our next article, where we bring the carnage into the Unity Scene View.&lt;/p&gt;
&lt;p&gt;Will the lightweight 45kb DLL show &lt;strong&gt;its&lt;/strong&gt; raw digital power? Or will the heavyweight bloat of ECS deliver yet another TKO?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Get to know the Challenger before the bell rings:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt; &lt;strong&gt;Asset Store ($30):&lt;/strong&gt;&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://assetstore.unity.com/packages/tools/behavior-ai/the-singularity-workshop-fsm-api-advanced-332450&quot;&gt;https://assetstore.unity.com/packages/tools/behavior-ai/the-singularity-workshop-fsm-api-advanced-332450&lt;/a&gt;&lt;/p&gt;
&lt;p&gt; &lt;strong&gt;GitHub (Free):&lt;/strong&gt;&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/TrentBest/FSM_API&quot;&gt;https://github.com/TrentBest/FSM_API&lt;/a&gt;&lt;/p&gt;
&lt;p&gt; &lt;strong&gt;Coach's Advice:&lt;/strong&gt;&lt;br&gt;
&lt;em&gt;&quot;Don't step into the ring without the right gear, kid. Grab the package. It's on the house.&quot;&lt;/em&gt;&lt;br&gt;
 &lt;strong&gt;NuGet (Free):&lt;/strong&gt;&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.nuget.org/packages/TheSingularityWorkshop.FSM_API/#supportedframeworks-body-tab&quot;&gt;https://www.nuget.org/packages/TheSingularityWorkshop.FSM_API/#supportedframeworks-body-tab&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Support Our Work:&lt;/p&gt;
&lt;p&gt;Patreon Page:&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.patreon.com/c/TheSingularityWorkshop&quot;&gt;https://www.patreon.com/c/TheSingularityWorkshop&lt;/a&gt;&lt;/p&gt;
&lt;p&gt; Support Us (PayPal Donation):&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.paypal.com/donate/?hosted_button_id=3Z7263LCQMV9J&quot;&gt;https://www.paypal.com/donate/?hosted_button_id=3Z7263LCQMV9J&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=4793565784457015839&quot; alt=&quot;A Hyper-Detailed, Architecturally Impossible Synthesis of Consciousness and Digital Matter. The image is a frenetic, deeply complex digital vista where a central Luminous Eye dominates a vast, glowing circuit landscape. This eye, suspended mid-air, is a sphere of intense, fractal energy—a pulsating vortex of pink, violet, and electric blue light—that powerfully suggests an emergence of digital consciousness or a Technological Singularity itself. The core is a bottomless black aperture, ringed by a white-hot plasma disc. Below this ocular energy source, the light dissipates into an intricate, copper-gold and neon-blue Circuit Board Megastructure that stretches to the horizon, impossibly dense with exaggerated microchips, glowing resistor arrays, and power conduits that form deep, glowing canyons. The background is a dark, holographic projection field displaying complex schematics, mathematical models, and flowing data streams, reinforcing the theme of Absolute Digital Engineering. The entire scene is bathed in a dramatic, opposing light source—a warm, orange-gold glow from the left and an ice-cold, electric-blue glow from the right—creating a maximalist, high-contrast visual experience that definitively rejects the minimalist simplicity of conventional design and announces: Technology is intricate, overwhelming, and infinitely complex.&quot;&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/8147/fight-night-announcement-the-lightweight-challenger-vs-the-heavyweight-champ</guid>
<pubDate>Wed, 03 Dec 2025 23:20:00 +0000</pubDate>
</item>
<item>
<title>The Control Group: Benchmarking the &quot;Dirty&quot; Version of My FSM API</title>
<link>https://coderlegion.com/7988/the-control-group-benchmarking-the-dirty-version-of-my-fsm-api</link>
<description>&lt;p&gt;In software architecture, optimization without measurement is just guessing.&lt;/p&gt;
&lt;p&gt;I am currently in the process of a major refactor for my C# Finite State Machine API (FSM_API). We discovered some concurrency friction in the underlying dictionary lookups and realized that my heavy reliance on string keys was creating unnecessary O(N) overhead.&lt;/p&gt;
&lt;p&gt;The plan is to switch to ConcurrentDictionary and implement Integer Hashing to eliminate string allocations entirely. But before I tear the engine apart, I needed a baseline. I needed to know exactly how much &quot;tax&quot; I was paying for those strings.&lt;/p&gt;
&lt;p&gt;So, I built a Stress Profiler.&lt;/p&gt;
&lt;p&gt;The Torture Test&lt;/p&gt;
&lt;p&gt;I designed a benchmark to intentionally break the system. The goal wasn't to see if it would fail, but how it would fail.&lt;/p&gt;
&lt;p&gt;The test setup is simple but brutal:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;The Crowd: We spawn between 50,000 and 240,000 Agents.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The Payload: Each agent performs basic math, some branching logic, and—crucially—a ToString() allocation every update to simulate&lt;br&gt;
memory pressure (Heap Thrashing).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The Variable: We test three different &quot;Time-Slicing&quot; intervals:&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt; 1.) Interval 1: Every agent updates every frame (100% Load). &lt;/p&gt;
&lt;p&gt; 2.) Interval 2: Agents skip 1 frame (50% Load).      &lt;/p&gt;
&lt;p&gt; 3.)  Interval 3: Agents skip 2 frames (33% Load).&lt;/p&gt;
&lt;p&gt;The Hypothesis&lt;/p&gt;
&lt;p&gt;My theory was that the Dictionary&amp;lt;string, ...&amp;gt; lookups would become the primary bottleneck as the agent count rose. Even when agents are &quot;sleeping&quot; (skipping frames), the API still has to iterate through the list to check if it's their turn.&lt;/p&gt;
&lt;p&gt;If the architecture is sound, &quot;Interval 2&quot; should theoretically double our capacity. If it doesn't, we know the overhead of the list management itself is too high.&lt;/p&gt;
&lt;p&gt;Here is the raw data from the &quot;Dirty&quot; version of the API—before the optimization pass which will be shared the moment I've accomplished the refactoring.&lt;/p&gt;
&lt;h4 id=&quot;stress-test-results-baseline-api-&quot;&gt;Stress Test Results (Baseline API)&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;Interval &lt;/th&gt;&lt;th align=&quot;left&quot;&gt;Agents &lt;/th&gt;&lt;th align=&quot;left&quot;&gt;FPS &lt;/th&gt;&lt;th align=&quot;left&quot;&gt;Logic Ops/Frame &lt;/th&gt;&lt;th align=&quot;left&quot;&gt;Mem Delta (MB)&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;1 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;50000 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;58.78 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;150,000 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;8&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;1 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;55000 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;106.30 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;11,000 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;27&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;1 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;60000 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;72.01 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;180,000 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;8&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;1 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;65000 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;100.64 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;13,000 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;30&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;1 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;70000 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;63.44 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;210,000 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;13&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;1 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;75000 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;96.77 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;15,000 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;38&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;1 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;80000 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;57.21 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;240,000 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;36&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;1 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;85000 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;87.40 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;17,000 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;43&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;1 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;90000 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;46.43 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;270,000 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;16&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;1 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;95000 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;75.70 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;19,000 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;48&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;1 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;100000 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;41.40 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;300,000 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;16&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;1 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;105000 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;62.54 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;21,000 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;54&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;1 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;110000 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;38.60 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;330,000 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;27&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;1 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;115000 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;58.02 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;23,000 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;59&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;1 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt; 120000  &lt;/td&gt;&lt;td align=&quot;left&quot;&gt; 34.88  &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;360,000 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;26&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;1 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;125000 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;57.30 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;25,000 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;61&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;1 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt; 130000  &lt;/td&gt;&lt;td align=&quot;left&quot;&gt; 33.57  &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;390,000 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;32&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;1 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;135000 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;53.90 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;27,000 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;68&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;1 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt; 140000  &lt;/td&gt;&lt;td align=&quot;left&quot;&gt; 31.47  &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;420,000 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;26&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;1 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;145000 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;50.50 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;29,000 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;70&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;1 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt; 150000  &lt;/td&gt;&lt;td align=&quot;left&quot;&gt; 28.80  &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;450,000 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;29&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;2 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;80000 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;70.34 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;8,000 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;55&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;2 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;85000 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;71.45 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;127,500 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;12&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;2 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;90000 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;83.53 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;13,500 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;45&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;2 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;95000 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;62.50 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;142,500 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;27&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;2 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;100000 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;69.49 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;15,000 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;-4&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;2 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;105000 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;59.55 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;157,500 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;23&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;2 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;110000 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;67.81 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;16,500 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;53&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;2 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;115000 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;53.22 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;172,500 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;40&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;2 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;120000 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;57.58 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;18,000 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;-1&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;2 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;125000 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;50.64 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;187,500 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;28&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;2 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;130000 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;56.93 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;19,500 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;64&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;2 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;135000 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;42.57 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;202,500 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;46&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;2 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;140000 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;48.11 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;21,000 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;2&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;2 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;145000 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;44.63 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;217,500 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;33&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;2 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;150000 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;48.52 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;22,500 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;74&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;2 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;155000 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;40.62 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;232,500 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;51&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;2 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;160000 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;45.80 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;24,000 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;87&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;2 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;165000 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;35.97 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;247,500 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;42&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;2 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;170000 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;38.35 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;25,500 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;4&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;2 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;175000 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;35.65 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;262,500 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;41&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;2 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;180000 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;40.20 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;27,000 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;86&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;2 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt; 185000  &lt;/td&gt;&lt;td align=&quot;left&quot;&gt; 32.44  &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;277,500 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;57&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;2 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;190000 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;37.83 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;28,500 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;93&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;2 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt; 195000  &lt;/td&gt;&lt;td align=&quot;left&quot;&gt; 31.51  &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;292,500 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;60&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;2 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt; 200000  &lt;/td&gt;&lt;td align=&quot;left&quot;&gt; 33.02  &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;30,000 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;2&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;2 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt; 205000  &lt;/td&gt;&lt;td align=&quot;left&quot;&gt; 30.64  &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;307,500 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;50&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;2 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt; 210000  &lt;/td&gt;&lt;td align=&quot;left&quot;&gt; 34.37  &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;31,500 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;100&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;2 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt; 215000  &lt;/td&gt;&lt;td align=&quot;left&quot;&gt; 28.08  &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;322,500 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;64&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;3 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;120000 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;46.49 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;6,000 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;79&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;3 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;130000 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;56.15 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;130,000 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;30&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;3 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;140000 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;49.74 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;14,000 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;-9&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;3 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;150000 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;47.27 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;150,000 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;21&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;3 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;160000 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;44.20 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;16,000 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;75&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;3 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;170000 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;39.76 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;170,000 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;35&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;3 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;180000 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;37.77 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;18,000 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;1&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;3 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;190000 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;37.70 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;190,000 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;32&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;3 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;200000 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;35.25 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;20,000 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;97&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;3 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt; 210000  &lt;/td&gt;&lt;td align=&quot;left&quot;&gt; 32.49  &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;210,000 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;57&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;3 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt; 220000  &lt;/td&gt;&lt;td align=&quot;left&quot;&gt; 30.48  &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;22,000 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;2&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;3 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt; 230000  &lt;/td&gt;&lt;td align=&quot;left&quot;&gt; 31.41  &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;230,000 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;37&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;3 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt; 240000  &lt;/td&gt;&lt;td align=&quot;left&quot;&gt; 29.61  &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;24,000 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;116&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The Analysis&lt;/p&gt;
&lt;p&gt;The results gave us the exact &quot;Failure Floor&quot; we were looking for.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;The Hard Limit: At 140,000 agents running every frame, we hit the 30 FPS floor.

The Overhead: Notice that switching to Interval 2 didn't perfectly double our capacity (we capped around 215k, not 280k). This confirms that simply iterating through 200,000 string-based handles costs CPU time, even if the agents do nothing.

The GC Spike: The memory delta column shows the &quot;Sawtooth&quot; pattern where the Garbage Collector had to step in aggressively to clean up those string allocations.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now we have our target. The next version of the API needs to beat these numbers to prove the value of the refactor. Let's get to work.&lt;/p&gt;
&lt;p&gt;Resources &amp;amp; Code:&lt;br&gt;
 Simple Demos Repository (Run the Stress Test): &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/TrentBest/SimpleDemos&quot;&gt;https://github.com/TrentBest/SimpleDemos&lt;/a&gt; (Clone this repo and run the StressProfiler project to reproduce these numbers.)&lt;/p&gt;
&lt;p&gt;The FSM Package (Unity Asset Store):&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://assetstore.unity.com/packages/slug/332450&quot;&gt;https://assetstore.unity.com/packages/slug/332450&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;NuGet Package (Non-Unity Core):&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.nuget.org/packages/TheSingularityWorkshop.FSM_API&quot;&gt;https://www.nuget.org/packages/TheSingularityWorkshop.FSM_API&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;GitHub Repository:&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/TrentBest/FSM_API&quot;&gt;https://github.com/TrentBest/FSM_API&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Support Our Work:&lt;/p&gt;
&lt;p&gt;Patreon Page:&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.patreon.com/c/TheSingularityWorkshop&quot;&gt;https://www.patreon.com/c/TheSingularityWorkshop&lt;/a&gt;&lt;/p&gt;
&lt;p&gt; Support Us (PayPal Donation):&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.paypal.com/donate/?hosted_button_id=3Z7263LCQMV9J&quot;&gt;https://www.paypal.com/donate/?hosted_button_id=3Z7263LCQMV9J&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;We'd love to hear your thoughts! Please Like this post, Love the code, and Share your feedback or what you've built with the API in the comments.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=1207607318879054936&quot; alt=&quot;The Singularity Workshop's Logo, a digital eyeball made from energy, forming the singularity&quot;&gt;&lt;/p&gt;
</description>
<category>Game Dev</category>
<guid isPermaLink="true">https://coderlegion.com/7988/the-control-group-benchmarking-the-dirty-version-of-my-fsm-api</guid>
<pubDate>Sun, 30 Nov 2025 08:32:53 +0000</pubDate>
</item>
<item>
<title>The Sentient Primitive: Waking Up Your Data with FSMs</title>
<link>https://coderlegion.com/7936/the-sentient-primitive-waking-up-your-data-with-fsms</link>
<description>&lt;p&gt;In standard game development, we are taught that variables are silent containers. A &lt;code&gt;bool&lt;/code&gt; is just a box that holds &lt;code&gt;true&lt;/code&gt; or &lt;code&gt;false&lt;/code&gt;. It doesn't &lt;em&gt;do&lt;/em&gt; anything. To know if it changed, we have to pester it constantly:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;// The Old Way: The Polling Trap
void Update() {
    if (isDead == true) { // Asking every single frame...
        PlayDeathAnimation();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is the &quot;Silent Variable&quot; problem. It leads to spaghetti code, race conditions, and &lt;code&gt;Update()&lt;/code&gt; loops clogged with thousands of &lt;code&gt;if&lt;/code&gt; statements checking for things that haven't changed.&lt;/p&gt;
&lt;p&gt;But what if a Boolean wasn't just a box? What if it was a &lt;strong&gt;Finite State Machine&lt;/strong&gt;?&lt;/p&gt;
&lt;h3 id=&quot;1-the-fsm-bool-the-atomic-unit-of-behavior&quot;&gt;1. The FSM_Bool: The Atomic Unit of Behavior&lt;/h3&gt;
&lt;p&gt;By wrapping a primitive in an FSM, we transform it from &lt;strong&gt;Passive Data&lt;/strong&gt; into &lt;strong&gt;Active Behavior&lt;/strong&gt;. It doesn't just hold a value; it has a &lt;em&gt;lifecycle&lt;/em&gt;.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Enter True:&lt;/strong&gt; Fire the event (e.g., &quot;Turn on Light&quot;).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Update True:&lt;/strong&gt; Maintain state (e.g., &quot;Humming sound&quot;).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Exit True:&lt;/strong&gt; Cleanup (e.g., &quot;Fade out light&quot;).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Suddenly, your logic is decoupled. The code controlling the light doesn't need to check the switch. The switch &lt;em&gt;tells&lt;/em&gt; the light what to do.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;// The New Way: Reactive Primitives
var lightSwitch = new FSM_Boolean(
    name: &quot;LivingRoomLight&quot;,
    startValue: false, 
    onTrue: (ctx) =&amp;gt; LightFixture.TurnOn(),  // Fire and forget
    onFalse: (ctx) =&amp;gt; LightFixture.TurnOff() // Fire and forget
);

// To operate it, we just change the target. The FSM handles the logic.
lightSwitch.Set(true); 
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;2-evolution-the-fsm-floatlimit&quot;&gt;2. Evolution: The FSM_FloatLimit&lt;/h3&gt;
&lt;p&gt;The concept explodes in potential when we move beyond Booleans. Consider the &lt;code&gt;float&lt;/code&gt;. Usually, we use floats for Health, Stamina, or Mana.&lt;/p&gt;
&lt;p&gt;In the old world, a Health System is a messy manager class. In the FSM world, a Health System is just &lt;strong&gt;a single Float with a Threshold.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Introducing the &lt;code&gt;FSM_FloatLimit&lt;/code&gt;. It is an FSM that monitors a value and triggers a transition when a threshold is crossed.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Constructor:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;var healthSystem = new FSM_FloatLimit(
    currentValue: 100.0f, 
    threshold: 0.0f, 
    OnThreshold: (ctx) =&amp;gt; Player.Kill()
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;The Internal Logic (Abstracted):&lt;/strong&gt;&lt;br&gt;
Inside the black box, this FSM has two states: &lt;code&gt;Safe&lt;/code&gt; and &lt;code&gt;Breached&lt;/code&gt;.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;State &quot;Safe&quot;:&lt;/strong&gt; The value is above the threshold.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Transition:&lt;/strong&gt; If &lt;code&gt;Value &amp;lt;= Threshold&lt;/code&gt; -&amp;gt; Go to &lt;code&gt;Breached&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;State &quot;Breached&quot;:&lt;/strong&gt; Execute &lt;code&gt;OnThreshold&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You no longer need a &lt;code&gt;PlayerHealthManager.cs&lt;/code&gt;. You just need a smart float.&lt;/p&gt;
&lt;h3 id=&quot;3-complex-data-shapes-from-simple-primitives&quot;&gt;3. Complex Data Shapes from Simple Primitives&lt;/h3&gt;
&lt;p&gt;When you start composing these &quot;Smart Primitives&quot; together, you aren't writing code anymore; you are sculpting &lt;strong&gt;Logic Shapes&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Imagine a Weapon Reload System constructed entirely of primitives:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;AmmoCount (&lt;code&gt;FSM_IntLimit&lt;/code&gt;)&lt;/strong&gt;: Triggers &quot;Empty&quot; when it hits 0.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ReloadTimer (&lt;code&gt;FSM_FloatTimer&lt;/code&gt;)&lt;/strong&gt;: Triggers &quot;Complete&quot; after 2.0 seconds.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;IsReloading (&lt;code&gt;FSM_Bool&lt;/code&gt;)&lt;/strong&gt;: The master switch.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The architecture becomes a chain reaction:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;AmmoCount&lt;/code&gt; hits 0 -&amp;gt; Sets &lt;code&gt;IsReloading&lt;/code&gt; to True.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;IsReloading&lt;/code&gt; (True) -&amp;gt; Starts &lt;code&gt;ReloadTimer&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ReloadTimer&lt;/code&gt; (Complete) -&amp;gt; Refills &lt;code&gt;AmmoCount&lt;/code&gt; and sets &lt;code&gt;IsReloading&lt;/code&gt; to False.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&quot;beyond-numbers-the-reactive-string&quot;&gt;Beyond Numbers: The Reactive String&lt;/h2&gt;
&lt;p&gt;The power of the &quot;Sentient Primitive&quot; isn't limited to simple math. It transforms how we handle text and input.&lt;/p&gt;
&lt;p&gt;Consider the standard way we handle cheat codes or spell casting in games: we log every keystroke into a buffer and run a string comparison every frame. It is tedious and error-prone.&lt;/p&gt;
&lt;p&gt;In our FSM-driven world, we treat the String as a &lt;strong&gt;Stateful Pattern Matcher&lt;/strong&gt;.&lt;/p&gt;
&lt;h3 id=&quot;the-fsm-regex&quot;&gt;The FSM_Regex&lt;/h3&gt;
&lt;p&gt;Imagine an FSM initialized with a Regular Expression. It doesn't just &quot;hold&quot; a string; it &lt;em&gt;hunts&lt;/em&gt; for a pattern within the stream of data you feed it.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;// Defining a &quot;Spell&quot; using a Reactive String
var fireballSpell = new FSM_Regex(
    pattern: &quot;UP, DOWN, A, B&quot;, // The Konami Code of magic
    OnMatch: (ctx) =&amp;gt; CastFireball(),
    OnFail: (ctx) =&amp;gt; PlayFizzleSound()
);

// We just feed it inputs. The FSM tracks the sequence state internally.
fireballSpell.Input(&quot;UP&quot;);   // State: Tracking...
fireballSpell.Input(&quot;DOWN&quot;); // State: Tracking...
fireballSpell.Input(&quot;X&quot;);    // State: Failed -&amp;gt; Trigger OnFail -&amp;gt; Reset
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This turns complex input handling into a simple definition. You aren't writing an &quot;Input Manager&quot;; you are just defining the shape of a valid command.&lt;/p&gt;
&lt;h3 id=&quot;the-paradigm-shift-composition-over-management&quot;&gt;The Paradigm Shift: Composition over Management&lt;/h3&gt;
&lt;p&gt;When you adopt this mindset, the &quot;Manager Classes&quot; that plague modern software development start to disappear.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You don't need a &lt;code&gt;HealthManager&lt;/code&gt;; you have an &lt;code&gt;FSM_Float&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;You don't need an &lt;code&gt;InputComboManager&lt;/code&gt;; you have an &lt;code&gt;FSM_Regex&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;You don't need a &lt;code&gt;DayNightCycleManager&lt;/code&gt;; you have an &lt;code&gt;FSM_Timer&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You are creating a visual language where the &quot;bricks&quot; of your building aren't dead clay; they are active participants in the architecture. They know when to hold together, and they know exactly when to break apart.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;visualizing-the-fsm-regex&quot;&gt;Visualizing the FSM_Regex&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=11648743608190271004&quot; alt=&quot;Flow Chart for our FSM_Regex&quot;&gt;&lt;/p&gt;
&lt;p&gt;Resources &amp;amp; Code:&lt;/p&gt;
&lt;p&gt;The FSM Package (Unity Asset Store):&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://assetstore.unity.com/packages/slug/332450&quot;&gt;https://assetstore.unity.com/packages/slug/332450&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;NuGet Package (Non-Unity Core):&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.nuget.org/packages/TheSingularityWorkshop.FSM_API&quot;&gt;https://www.nuget.org/packages/TheSingularityWorkshop.FSM_API&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;GitHub Repository:&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/TrentBest/FSM_API&quot;&gt;https://github.com/TrentBest/FSM_API&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Support Our Work:&lt;/p&gt;
&lt;p&gt;Patreon Page:&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.patreon.com/c/TheSingularityWorkshop&quot;&gt;https://www.patreon.com/c/TheSingularityWorkshop&lt;/a&gt;&lt;/p&gt;
&lt;p&gt; Support Us (PayPal Donation):&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.paypal.com/donate/?hosted_button_id=3Z7263LCQMV9J&quot;&gt;https://www.paypal.com/donate/?hosted_button_id=3Z7263LCQMV9J&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;We'd love to hear your thoughts! Please Like this post, Love the code, and Share your feedback in the comments.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=6706949502413490078&quot; alt=&quot;A Hyper-Detailed, Architecturally Impossible Synthesis of Consciousness and Digital Matter. The image is a frenetic, deeply complex digital vista where a central Luminous Eye dominates a vast, glowing circuit landscape. This eye, suspended mid-air, is a sphere of intense, fractal energy—a pulsating vortex of pink, violet, and electric blue light—that powerfully suggests an emergence of digital consciousness or a Technological Singularity itself. The core is a bottomless black aperture, ringed by a white-hot plasma disc. Below this ocular energy source, the light dissipates into an intricate, copper-gold and neon-blue Circuit Board Megastructure that stretches to the horizon, impossibly dense with exaggerated microchips, glowing resistor arrays, and power conduits that form deep, glowing canyons. The background is a dark, holographic projection field displaying complex schematics, mathematical models, and flowing data streams, reinforcing the theme of Absolute Digital Engineering. The entire scene is bathed in a dramatic, opposing light source—a warm, orange-gold glow from the left and an ice-cold, electric-blue glow from the right—creating a maximalist, high-contrast visual experience that definitively rejects the minimalist simplicity of conventional design and announces: Technology is intricate, overwhelming, and infinitely complex.&quot;&gt;&lt;/p&gt;
</description>
<category>Game Dev</category>
<guid isPermaLink="true">https://coderlegion.com/7936/the-sentient-primitive-waking-up-your-data-with-fsms</guid>
<pubDate>Sat, 29 Nov 2025 07:09:59 +0000</pubDate>
</item>
<item>
<title>10. FSM_API for Non-Coders: A Big Picture Overview (From our Documentation)</title>
<link>https://coderlegion.com/7412/10-fsmapi-for-non-coders-a-big-picture-overview-from-our-documentation</link>
<description>&lt;p&gt;We believe the most reliable software logic is logic everyone can understand.&lt;/p&gt;
&lt;p&gt;The complex systems that drive modern applications—from character AI in games to business workflows and industrial robotics—are often best articulated not by a developer, but by the subject matter experts. These experts instinctively express domain behavior as an organized set of phases and rules, often visualized as hierarchical Finite State Machines.&lt;/p&gt;
&lt;p&gt;This natural structure is the foundation of the Finite State Machine (FSM), and it is why we created this overview. FSMs are not merely a developer’s tool; they are a universal language. This guide is designed to bridge the gap, enabling your domain expertise to translate seamlessly and reliably into the design of robust, predictable, and maintainable software systems.&lt;/p&gt;
&lt;p&gt;Without any further ado:&lt;/p&gt;
&lt;h2 id=&quot;10-fsm-api-for-non-coders-a-big-picture-overview&quot;&gt;10. FSM_API for Non-Coders: A Big Picture Overview&lt;/h2&gt;
&lt;p&gt; Have you ever tried to describe a complex process to someone, like making coffee ☕ or building a LEGO castle , and found yourself saying, &quot;First you do this, &lt;em&gt;then&lt;/em&gt; if that happens, you do this other thing, &lt;em&gt;otherwise&lt;/em&gt; you do something else entirely&quot;? If so, you've already grasped the core idea behind &lt;strong&gt;Finite State Machines (FSMs)&lt;/strong&gt;!&lt;/p&gt;
&lt;p&gt;At its heart, FSM_API is a powerful tool designed to make complex software behaviors predictable, organized, and easier to manage, even if you don't write code yourself. It helps turn messy, &quot;if-this-then-that&quot; logic into clear, step-by-step instructions.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-is-a-finite-state-machine-fsm-the-simple-version-&quot;&gt;What is a Finite State Machine (FSM)? (The Simple Version)&lt;/h3&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;Visuals/TrafficLightFSM.png&quot; target=&quot;_blank&quot;&gt;&lt;br&gt;
    &lt;img src=&quot;Visuals/TrafficLightFSM.png&quot; alt=&quot;A state machine diagram of a traffic light.&quot; height=&quot;200&quot; style=&quot;display: block;&quot;&gt;&lt;br&gt;
&lt;/a&gt;&lt;br&gt;
&lt;em&gt;A simple, real-world example of an FSM: a traffic light.&lt;/em&gt;&lt;br&gt;
&lt;br&gt;&lt;br&gt;
Imagine anything that changes its behavior based on its current situation.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;traffic light&lt;/strong&gt; is either Red, Yellow, or Green. It can't be Red and Green at the same time. It changes from Green to Yellow only, not directly to Red.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;door&lt;/strong&gt; is either Closed, Opening, Open, or Closing.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;game character&lt;/strong&gt; might be Idle, Walking, Running, Jumping, or Attacking.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;An FSM is just a way to formally describe these behaviors:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;States:&lt;/strong&gt; These are the specific &quot;situations&quot; or &quot;modes&quot; an entity can be in at any given moment (like &quot;Green&quot; for a traffic light, or &quot;Walking&quot; for a character). An FSM is always in &lt;strong&gt;exactly one state&lt;/strong&gt; at a time.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Transitions:&lt;/strong&gt; These are the &lt;strong&gt;rules&lt;/strong&gt; that dictate how an entity moves from one state to another (e.g., from &quot;Green&quot; to &quot;Yellow&quot; when a timer runs out, or from &quot;Walking&quot; to &quot;Jumping&quot; when the player presses the jump button).&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;FSM_API helps developers build and manage these FSMs in a clear and organized way, making sure all the rules are followed and behaviors are predictable.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;how-fsm-api-helps-you-even-if-you-don-t-code-&quot;&gt;How FSM_API Helps You (Even if You Don't Code)&lt;/h3&gt;
&lt;p&gt;While FSM_API is a coding tool, its benefits directly impact anyone involved in designing or understanding the behavior of a system, like game designers, project managers, or QA testers.&lt;/p&gt;
&lt;h4 id=&quot;1-clearer-communication&quot;&gt;1. &lt;strong&gt;Clearer Communication&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;Visuals/Character_AI_FSM.png&quot; target=&quot;_blank&quot;&gt;&lt;br&gt;
    &lt;img src=&quot;Visuals/Character_AI_FSM.png&quot; alt=&quot;A flowchart of an enemy AI, showing states like Patrolling and Chasing.&quot; height=&quot;200&quot; style=&quot;display: block;&quot;&gt;&lt;br&gt;
&lt;/a&gt;&lt;br&gt;
&lt;em&gt;An FSM diagram like this gives the whole team a clear map of how the AI should behave.&lt;/em&gt;&lt;br&gt;
&lt;br&gt;&lt;/p&gt;
&lt;p&gt;Instead of ambiguous descriptions, an FSM provides a visual and logical map of how something behaves. Everyone on the team can look at an FSM diagram and immediately understand the flow.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Example:&lt;/strong&gt; &quot;The enemy moves from 'Patrolling' to 'Chasing' when it 'sees' the player.&quot; This becomes a clear transition rule.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;2-fewer-bugs-amp-more-reliable-behavior&quot;&gt;2. &lt;strong&gt;Fewer Bugs &amp;amp; More Reliable Behavior&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;When behaviors are explicitly defined, it's much harder for unexpected &quot;edge cases&quot; or illogical situations to occur (like a character trying to jump while they are already dead). This leads to more stable and reliable software.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Benefit:&lt;/strong&gt; Less time spent fixing frustrating, unpredictable issues.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;3-easier-to-change-and-expand&quot;&gt;3. &lt;strong&gt;Easier to Change and Expand&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;Because the behavior is modular (broken into states and transitions), adding new features or changing existing ones becomes much simpler. You don't have to overhaul the entire system.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Example:&lt;/strong&gt; Want to add a &quot;Stealth&quot; state to your enemy? You simply add the new state and define how to get into and out of it, without breaking the existing &quot;Patrol&quot; or &quot;Chase&quot; logic.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;4-better-performance&quot;&gt;4. &lt;strong&gt;Better Performance&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;FSM_API is built to be fast. This means your games or applications will run smoother, even when managing many complex characters or systems at once.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Benefit:&lt;/strong&gt; A more responsive and enjoyable experience for the end-user.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;5-empowers-non-coders-the-context-connection-&quot;&gt;5. &lt;strong&gt;Empowers Non-Coders (The &quot;Context&quot; Connection)&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;Visuals/FSM_Context_Relationship.png&quot; target=&quot;_blank&quot;&gt;&lt;br&gt;
    &lt;img src=&quot;Visuals/FSM_Context_Relationship.png&quot; alt=&quot;A diagram showing the FSM as a brain controlling the game object as a body.&quot; height=&quot;200&quot; style=&quot;display: block;&quot;&gt;&lt;br&gt;
&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The FSM (the brain) contains the logic, while the Context (the body) is the actual object that performs the actions.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;When a developer uses FSM_API, they link the FSM to an actual &quot;thing&quot; in your game or application (this &quot;thing&quot; is called the &lt;strong&gt;Context&lt;/strong&gt;).&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Example:&lt;/strong&gt; A game designer might say, &quot;When the character is in the 'Jumping' state, I want them to play the 'Jump_Anim' and add an upward force.&quot; The developer then connects the FSM's &quot;Jumping&quot; state to the &lt;strong&gt;character's script&lt;/strong&gt; (their Context) which knows how to play animations and apply force.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Benefit:&lt;/strong&gt; This clear connection means designers can specify behavior in terms of game actions, and developers can implement it precisely, using the FSM as the bridge.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;who-is-this-for-&quot;&gt;Who is This For?&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Game Designers:&lt;/strong&gt; Visualize and plan complex character AI, game mechanics, and UI flows with clarity.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Project Managers:&lt;/strong&gt; Understand the scope and complexity of features related to dynamic behaviors.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;QA Testers:&lt;/strong&gt; Clearly define and test expected behaviors in various scenarios.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Technical Writers:&lt;/strong&gt; Document system behaviors accurately and concisely.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;FSM_API provides a common language and structure that helps bridge the gap between design and implementation, leading to more robust, understandable, and maintainable applications. We are actively working on integrating this into &lt;strong&gt;AnyApp&lt;/strong&gt;, and also preparing it for submission to the &lt;strong&gt;Unity Asset Store&lt;/strong&gt; so developers everywhere can benefit!&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;want-to-learn-more-about-development-&quot;&gt;Want to Learn More About Development?&lt;/h3&gt;
&lt;p&gt;If you're interested in learning about the coding side of things from a clear, practical perspective, we highly recommend checking out &lt;strong&gt;Tim Corey&lt;/strong&gt;. He's widely recognized as a fantastic resource for software development, and you can find his insightful content, courses, and community on his platform, &lt;strong&gt;DevForge&lt;/strong&gt;, at &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.iamtimcorey.com/&quot;&gt;iamtimcorey.com&lt;/a&gt;. &lt;/p&gt;
&lt;h3 id=&quot;resources-amp-code-&quot;&gt;Resources &amp;amp; Code:&lt;/h3&gt;
&lt;p&gt;The FSM Package (Unity Asset Store):&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://assetstore.unity.com/packages/slug/332450&quot;&gt;https://assetstore.unity.com/packages/slug/332450&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;NuGet Package (Non-Unity Core):&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.nuget.org/packages/TheSingularityWorkshop.FSM_API&quot;&gt;https://www.nuget.org/packages/TheSingularityWorkshop.FSM_API&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;GitHub Repository:&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/TrentBest/FSM_API&quot;&gt;https://github.com/TrentBest/FSM_API&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Support Our Work:&lt;/p&gt;
&lt;p&gt;Patreon Page:&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.patreon.com/c/TheSingularityWorkshop&quot;&gt;https://www.patreon.com/c/TheSingularityWorkshop&lt;/a&gt;&lt;/p&gt;
&lt;p&gt; Support Us (PayPal Donation):&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.paypal.com/donate/?hosted_button_id=3Z7263LCQMV9J&quot;&gt;https://www.paypal.com/donate/?hosted_button_id=3Z7263LCQMV9J&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=17272823008937591128&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
</description>
<category>C#</category>
<guid isPermaLink="true">https://coderlegion.com/7412/10-fsmapi-for-non-coders-a-big-picture-overview-from-our-documentation</guid>
<pubDate>Sat, 15 Nov 2025 22:14:52 +0000</pubDate>
</item>
<item>
<title>06_FSM_Modifier Deep Dive: Modifying Your FSMs at Runtime (From our Documentation)</title>
<link>https://coderlegion.com/7359/06fsmmodifier-deep-dive-modifying-your-fsms-at-runtime-from-our-documentation</link>
<description>&lt;h3 id=&quot;the-fsm-api-advantage-runtime-modification-technology&quot;&gt;The FSM_API Advantage: Runtime Modification Technology&lt;/h3&gt;
&lt;p&gt;What sets the FSM_API apart is our &lt;strong&gt;FSMModifier technology&lt;/strong&gt;, which allows for the &lt;strong&gt;dynamic reconstitution or definition of your Finite State Machines (FSMs) during runtime&lt;/strong&gt;. No other FSM implementation offers this level of dynamic control over an FSM's blueprint after creation.&lt;/p&gt;
&lt;p&gt;We believe in providing the deepest, most robust tools while allowing you to engage only as far as you need. We've done the deep theoretical work so you don't have to.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;06-fsmmodifier-deep-dive-modifying-your-fsms-at-runtime&quot;&gt;06. FSMModifier Deep Dive: Modifying Your FSMs at Runtime&lt;/h2&gt;
&lt;p&gt;The &lt;strong&gt;FSMModifier&lt;/strong&gt; provides advanced capabilities to &lt;strong&gt;dynamically alter the blueprint (definition) of your Finite State Machines at runtime&lt;/strong&gt;. This allows for highly &lt;strong&gt;adaptive behaviors&lt;/strong&gt;, hot-swappable logic, or configuration changes without rebuilding your application.&lt;/p&gt;
&lt;p&gt;While the &lt;strong&gt;FSMBuilder&lt;/strong&gt; is used to initially design and build an FSM's definition, the &lt;strong&gt;FSMModifier&lt;/strong&gt; allows you to &lt;strong&gt;change that definition after it has been created and registered&lt;/strong&gt; with &lt;code&gt;FSM_API&lt;/code&gt;. This is a powerful feature for scenarios requiring:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Adaptive AI:&lt;/strong&gt; Changing an enemy's behavior patterns on the fly (e.g., learning new attacks, adapting to player strategies).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dynamic UI:&lt;/strong&gt; Adding or removing navigation paths or UI states based on user unlocks or content downloads.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Hot Reloading:&lt;/strong&gt; Modifying FSM logic in a live environment (e.g., development tools for rapid iteration).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A/B Testing:&lt;/strong&gt; Dynamically switching between different FSM behaviors for experimentation.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;table-of-contents-disabled-for-this-article-&quot;&gt;Table of Contents (&lt;em&gt;Disabled for this article&lt;/em&gt;)&lt;/h3&gt;
&lt;p&gt;0.  Introduction to FSM_API&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Core Concepts: Your Guide to FSM_API&lt;/li&gt;
&lt;li&gt;Getting Started with C# (Non-Unity)&lt;/li&gt;
&lt;li&gt;FSMBuilder Deep Dive: Building Your FSMs&lt;/li&gt;
&lt;li&gt;Understanding and Implementing Your Context (IStateContext)&lt;/li&gt;
&lt;li&gt;Robust Error Handling: Cascading Degradation System&lt;/li&gt;
&lt;li&gt;FSMModifier Deep Dive&lt;/li&gt;
&lt;li&gt;Performance Tips &amp;amp; Best Practices&lt;/li&gt;
&lt;li&gt;Common Use Cases &amp;amp; Examples&lt;/li&gt;
&lt;li&gt;FSM_API for Non-Coders: A Big Picture Overview&lt;/li&gt;
&lt;li&gt;Frequently Asked Questions (FAQ)&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h3 id=&quot;accessing-and-using-the-fsmmodifier&quot;&gt;Accessing and Using the FSMModifier&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;FSMModifier&lt;/code&gt; is an internal class, so you can't create it directly. Instead, you access its functionality through a set of public, static helper methods on &lt;code&gt;FSM_API.Interaction&lt;/code&gt;. These methods are designed to simplify common modification tasks like adding or removing states and transitions.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;FSMModifier&lt;/code&gt; uses a &lt;strong&gt;fluent API pattern&lt;/strong&gt; where you first &lt;strong&gt;stage the changes&lt;/strong&gt; you want to make and then apply them with a single &lt;code&gt;ModifyDefinition()&lt;/code&gt; call. All changes are &lt;strong&gt;batched and applied sequentially&lt;/strong&gt; when &lt;code&gt;ModifyDefinition()&lt;/code&gt; is called, making the process safe and predictable.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;fsmmodifier-methods-altering-the-blueprint&quot;&gt;FSMModifier Methods: Altering the Blueprint&lt;/h3&gt;
&lt;p&gt;All the methods below are called on an instance of the &lt;strong&gt;FSMModifier&lt;/strong&gt; class. You get this instance by calling a public method on &lt;code&gt;FSM_API.Interaction&lt;/code&gt;.&lt;/p&gt;
&lt;h4 id=&quot;staging-changes-with-with-and-without-&quot;&gt;Staging Changes with With... and Without...&lt;/h4&gt;
&lt;h5 id=&quot;withstate-adding-a-new-state&quot;&gt;&lt;code&gt;WithState()&lt;/code&gt;: Adding a New State&lt;/h5&gt;
&lt;p&gt;This method adds a new state to an existing FSM definition. If a state with the given name already exists, the operation is &lt;strong&gt;ignored&lt;/strong&gt;. Use &lt;code&gt;WithModifiedState()&lt;/code&gt; to change an existing state.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;C# Example:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;FSM_API.Interaction.AddStateToFSM(
    &quot;PlayerFSM&quot;,
    &quot;SpecialAttack&quot;,
    onEnter: ctx =&amp;gt; Console.WriteLine(&quot;Prepare special attack!&quot;),
    onUpdate: ctx =&amp;gt; Console.WriteLine(&quot;Execute special attack.&quot;),
    onExit: ctx =&amp;gt; Console.WriteLine(&quot;Cleanup after special attack.&quot;)
);
&lt;/code&gt;&lt;/pre&gt;
&lt;h5 id=&quot;withmodifiedstate-updating-an-existing-state&quot;&gt;&lt;code&gt;WithModifiedState()&lt;/code&gt;: Updating an Existing State&lt;/h5&gt;
&lt;p&gt;This method modifies the actions (&lt;code&gt;onEnter&lt;/code&gt;, &lt;code&gt;onUpdate&lt;/code&gt;, and &lt;code&gt;onExit&lt;/code&gt;) of a state that is already part of the FSM blueprint. If the state doesn't exist, the operation is &lt;strong&gt;ignored&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;C# Example:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Change the &quot;Idle&quot; state's behavior
FSM_API.Interaction.ModifyState(
    &quot;PlayerFSM&quot;,
    &quot;Idle&quot;,
    onEnter: ctx =&amp;gt; Console.WriteLine(&quot;Entered a NEW and improved Idle state.&quot;),
    onUpdate: null, // You can pass null to remove an action
    onExit: ctx =&amp;gt; Console.WriteLine(&quot;Exiting new Idle state.&quot;)
);
&lt;/code&gt;&lt;/pre&gt;
&lt;h5 id=&quot;withoutstate-removing-a-state&quot;&gt;&lt;code&gt;WithoutState()&lt;/code&gt;: Removing a State&lt;/h5&gt;
&lt;p&gt;This method stages a state for removal from the FSM definition.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;C# Example:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Remove a state and transition all instances to &quot;InitialState&quot;
FSM_API.Interaction.RemoveStateFromFSM(&quot;PlayerFSM&quot;, &quot;Dead&quot;, null);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Important:&lt;/strong&gt; When a state is removed, any active &lt;strong&gt;FSMHandle&lt;/strong&gt; instances currently in that state will be &lt;strong&gt;immediately transitioned&lt;/strong&gt; to a specified &lt;code&gt;fallbackStateName&lt;/code&gt;. If you pass &lt;strong&gt;null&lt;/strong&gt; or an empty string for the fallback, instances will automatically transition to the FSM's &lt;strong&gt;InitialState&lt;/strong&gt;. The &lt;code&gt;OnExit&lt;/code&gt; action of the removed state will be executed for these instances.&lt;/p&gt;
&lt;h5 id=&quot;withtransition-creating-a-new-path&quot;&gt;&lt;code&gt;WithTransition()&lt;/code&gt;: Creating a New Path&lt;/h5&gt;
&lt;p&gt;This method adds a new transition rule between two states. If the transition already exists, the operation is &lt;strong&gt;ignored&lt;/strong&gt;. Use &lt;code&gt;WithModifiedTransition()&lt;/code&gt; to change an existing one.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;C# Example:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Add a new transition from &quot;Idle&quot; to &quot;Sleeping&quot;
FSM_API.Interaction.AddTransition(
    &quot;PlayerFSM&quot;,
    &quot;Idle&quot;,
    &quot;Sleeping&quot;,
    ctx =&amp;gt; ((PlayerContext)ctx).IsTired() &amp;amp;&amp;amp; DateTime.Now.Hour &amp;gt; 21
);
&lt;/code&gt;&lt;/pre&gt;
&lt;h5 id=&quot;withmodifiedtransition-updating-a-transition&quot;&gt;&lt;code&gt;WithModifiedTransition()&lt;/code&gt;: Updating a Transition&lt;/h5&gt;
&lt;p&gt;This method updates the condition of an existing transition between two states. If the transition doesn't exist, the operation is &lt;strong&gt;ignored&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;C# Example:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Change the condition for the &quot;Damaged&quot; -&amp;gt; &quot;CriticallyInjured&quot; transition
FSM_API.Interaction.ModifyTransition(
    &quot;PlayerFSM&quot;,
    &quot;Damaged&quot;,
    &quot;CriticallyInjured&quot;,
    ctx =&amp;gt; ((CharacterContext)ctx).Health &amp;lt;= 10 // Change from 25 to 10
);
&lt;/code&gt;&lt;/pre&gt;
&lt;h5 id=&quot;withouttransition-blocking-a-path&quot;&gt;&lt;code&gt;WithoutTransition()&lt;/code&gt;: Blocking a Path&lt;/h5&gt;
&lt;p&gt;This method removes a specific transition rule. It requires the &lt;code&gt;fromState&lt;/code&gt; and &lt;code&gt;toState&lt;/code&gt; names to uniquely identify the transition.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;C# Example:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Remove the transition from &quot;Running&quot; to &quot;Jumping&quot;
FSM_API.Interaction.RemoveTransition(&quot;PlayerFSM&quot;, &quot;Running&quot;, &quot;Jumping&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;manipulating-live-fsm-instances-fsmhandle-&quot;&gt;Manipulating Live FSM Instances (FSMHandle)&lt;/h3&gt;
&lt;p&gt;While &lt;strong&gt;FSMModifier&lt;/strong&gt; changes the FSM &lt;strong&gt;blueprint&lt;/strong&gt;, the &lt;strong&gt;FSMHandle&lt;/strong&gt; provides methods to directly influence individual, &lt;strong&gt;live FSMHandle instances&lt;/strong&gt; at runtime. These operations &lt;strong&gt;do not alter&lt;/strong&gt; the underlying FSM definition.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;TransitionTo()&lt;/code&gt;: Forces an &lt;strong&gt;FSMHandle&lt;/strong&gt; instance to immediately transition to a specified state, bypassing any conditions. This is useful for debug commands or for recovering from an error state.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ResetFSMInstance()&lt;/code&gt;: Resets the FSM instance to its &lt;strong&gt;initial state&lt;/strong&gt;, as defined in its blueprint.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;EvaluateConditions()&lt;/code&gt;: Manually checks all transition rules from the current state. This is especially useful for FSMs with a &lt;code&gt;ProcessRate&lt;/code&gt; of 0, which do not update automatically.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;DestroyHandle()&lt;/code&gt;: &lt;strong&gt;Shuts down this FSM instance&lt;/strong&gt;, removes it from the API's internal system, and calls the &lt;code&gt;OnExit&lt;/code&gt; action of its current state. This is a crucial cleanup step for any live instance that is no longer needed.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;important-considerations-and-best-practices&quot;&gt;Important Considerations and Best Practices&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Affects All Instances:&lt;/strong&gt; Remember that changes made with &lt;strong&gt;FSMModifier&lt;/strong&gt; affect the &lt;strong&gt;shared blueprint&lt;/strong&gt;. All active &lt;strong&gt;FSMHandle&lt;/strong&gt; instances created from that blueprint will immediately use the new states, transitions, and properties on their next update cycle.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Atomicity:&lt;/strong&gt; The &lt;strong&gt;FSMModifier stages changes&lt;/strong&gt; and applies them all at once when &lt;code&gt;ModifyDefinition()&lt;/code&gt; is called. This is a robust design that prevents partial or inconsistent changes from corrupting the FSM blueprint.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;State Removal is Handled Gracefully:&lt;/strong&gt; The API will automatically handle instances in a removed state by transitioning them to a fallback state, ensuring your application doesn't crash or get stuck in an invalid state.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h4 id=&quot;support-us&quot;&gt;Support Us&lt;/h4&gt;
&lt;p&gt;If you find this project useful, you can support its development through PayPal.&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.paypal.com/donate/?hosted_button_id=3Z7263LCQMV9J&quot;&gt;&lt;strong&gt;Donate via PayPal&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;useful-links&quot;&gt;Useful Links&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;NuGet Package:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.nuget.org/packages/TheSingularityWorkshop.FSM_API&quot;&gt;TheSingularityWorkshop.FSM_API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;GitHub Repository:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/TrentBest/FSM_API&quot;&gt;TrentBest/FSM_API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;License:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://opensource.org/licenses/MIT&quot;&gt;MIT License&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://assetstore.unity.com/packages/tools/behavior-ai/the-singularity-workshop-fsm-api-advanced-332450&quot;&gt;Advanced Integration On Unity Asset Store!&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Basic Integration is pending updates on Marketing content.&lt;hr&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;brought-to-you-by&quot;&gt;Brought to you by&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;The Singularity Workshop&lt;/strong&gt; – Tools for the curious, the bold, and the systemically inclined.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gl6tdvgg73ixyj0m0kuf.png&quot; alt=&quot;The image depicts a futuristic, sci-fi landscape dominated by glowing circuitry. A large, stylized Patreon logo hovers in the dark, starry sky above a radiant, eye-like sphere. This sphere pulses with vibrant, multicolored light, casting a brilliant glow onto the digital terrain below. The ground is a vast circuit board, with rivers of glowing orange and blue energy flowing along intricate pathways. Small, button-like structures on the surface emit soft light. The overall scene feels alive and dynamic, suggesting a powerful, almost magical, connection between technology and consciousness, with the Patreon logo at the center of it all.&quot;&gt;&lt;br&gt;
Because state shouldn’t be a mess.&lt;/p&gt;
&lt;hr&gt;
</description>
<category>C#</category>
<guid isPermaLink="true">https://coderlegion.com/7359/06fsmmodifier-deep-dive-modifying-your-fsms-at-runtime-from-our-documentation</guid>
<pubDate>Fri, 14 Nov 2025 17:46:04 +0000</pubDate>
</item>
<item>
<title>How we recognized the need to pivot from pure ECS to pure FSM</title>
<link>https://coderlegion.com/6259/how-we-recognized-the-need-to-pivot-from-pure-ecs-to-pure-fsm</link>
<description>&lt;h3 id=&quot;the-architectural-pivot-why-the-singularity-workshop-is-building-myvr-with-the-fsm-api-not-a-custom-engine&quot;&gt;The Architectural Pivot: Why The Singularity Workshop is Building MyVR with the FSM_API, Not a Custom Engine&lt;/h3&gt;
&lt;p&gt;At &lt;strong&gt;The Singularity Workshop&lt;/strong&gt;, we are charting a course toward &lt;strong&gt;MyVR&lt;/strong&gt;, a vision for a truly sovereign, scalable, and adaptable digital universe. Our initial approach centered on developing a custom-built Entity-Component-System (ECS) engine from scratch. However, to accelerate our mission and ensure the highest standards of architectural integrity, we made a decisive and necessary pivot.&lt;/p&gt;
&lt;p&gt;We are now focused on a refined roadmap centered on the &lt;strong&gt;FSM_API (Finite State Machine Application Programming Interface)&lt;/strong&gt;, leveraging the robust services of &lt;strong&gt;Unity&lt;/strong&gt; to build faster, stronger, and more clearly defined systems.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;the-unavoidable-battle-with-state&quot;&gt;The Unavoidable Battle with State&lt;/h4&gt;
&lt;p&gt;For any ambitious project aiming to handle thousands of entities with complex behaviors—like large-scale RTS simulations or high-fidelity virtual worlds—the greatest obstacle is the inherent, complex &lt;strong&gt;&quot;Battle with State.&quot;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Traditional object-oriented or even custom ECS systems can often lead to tangled code and systems that are impossible to rigorously test or scale. We realized that true performance and maintainability required isolating and perfecting the single hardest problem in large-scale computation: &lt;strong&gt;state management&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The solution was not to build another engine, but to perfect the most foundational architectural tool: the &lt;strong&gt;FSM_API&lt;/strong&gt;. This C# core provides a &lt;strong&gt;mathematically provable&lt;/strong&gt;, high-performance framework for all logic and state transitions, immediately shattering the bottlenecks that plagued our initial custom ECS efforts.&lt;/p&gt;
&lt;h4 id=&quot;the-architectural-advantage-modularity-and-innovation&quot;&gt;The Architectural Advantage: Modularity and Innovation&lt;/h4&gt;
&lt;p&gt;The pivot from a custom monolithic engine to an FSM-driven architecture built on Unity has unlocked several massive big-picture implications:&lt;/p&gt;
&lt;h5 id=&quot;1-accelerated-development-via-unity&quot;&gt;1. Accelerated Development via Unity&lt;/h5&gt;
&lt;p&gt;Instead of sinking years into rebuilding rendering, physics, and networking, we now leverage the highly optimized services of &lt;strong&gt;Unity&lt;/strong&gt;. This dramatically accelerates our development timeline for the ultimate goal: &lt;strong&gt;MyVR&lt;/strong&gt;.&lt;/p&gt;
&lt;h5 id=&quot;2-the-free-core-monetized-layer&quot;&gt;2. The Free Core, Monetized Layer&lt;/h5&gt;
&lt;p&gt;Our business model for this new architecture is centered on providing value to the entire development community:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The foundational &lt;strong&gt;FSM_API&lt;/strong&gt; is, and will remain, &lt;strong&gt;MIT-licensed and free&lt;/strong&gt; (available via NuGet and GitHub).&lt;/li&gt;
&lt;li&gt;We monetize specialized utility through the &lt;strong&gt;FSM_Layer&lt;/strong&gt;—a library of up to &lt;strong&gt;500 dedicated Unity micro-packages&lt;/strong&gt; (e.g., Behavior Trees, advanced AI, Networking protocols, etc.). This ensures the core technology is accessible while specialized, high-value tools fund the continuous development of the MyVR universe.&lt;/li&gt;
&lt;/ul&gt;
&lt;h5 id=&quot;3-anyapp-the-composable-future&quot;&gt;3. AnyApp: The Composable Future&lt;/h5&gt;
&lt;p&gt;This modularity leads directly to &lt;strong&gt;AnyApp&lt;/strong&gt;, our customizable host application. AnyApp is designed for &lt;strong&gt;runtime FSM recomposition&lt;/strong&gt;, a groundbreaking innovation that allows users with permissions to enter an &lt;strong&gt;editor mode&lt;/strong&gt; and dynamically modify or even mix genres within a live experience. Imagine adding RTS logic to an FPS, or building custom automation into a simulation—all live, without recompilation.&lt;/p&gt;
&lt;p&gt;The &lt;strong&gt;FSM_API&lt;/strong&gt; makes this possible, ensuring that even when genres are mixed, the underlying logic remains clear, performant, and robust.&lt;/p&gt;
&lt;h4 id=&quot;join-the-mission-from-api-to-universe&quot;&gt;Join the Mission: From API to Universe&lt;/h4&gt;
&lt;p&gt;Our path is now clear: from the &lt;strong&gt;FSM_API&lt;/strong&gt; to the modular &lt;strong&gt;FSM_Layer&lt;/strong&gt;, into the customizable &lt;strong&gt;AnyApp&lt;/strong&gt; host, and finally into the scalable &lt;strong&gt;MyVR&lt;/strong&gt; digital universe. This is a robust, cleaner, and ultimately faster route to realizing our full vision.&lt;/p&gt;
&lt;p&gt;For a complete, in-depth technical breakdown of this strategic pivot, including our monetization plan, the status of our Unity Asset Store submissions, and the current technical challenge we are solving to guarantee full modularity, please read our latest Patreon post:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.patreon.com/posts/pivot-reforging-140849538?utm_medium=clipboard_copy&amp;amp;utm_source=copyLink&amp;amp;utm_campaign=postshare_creator&amp;amp;utm_content=join_link&quot;&gt;The Pivot: Reforging Our Path to MyVR with the FSM Architecture&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Join The Singularity Workshop on Patreon today to support the development of this foundational architecture and secure your legacy in the future of the digital world.&lt;/p&gt;
</description>
<category>Virtual Reality</category>
<guid isPermaLink="true">https://coderlegion.com/6259/how-we-recognized-the-need-to-pivot-from-pure-ecs-to-pure-fsm</guid>
<pubDate>Thu, 09 Oct 2025 22:17:25 +0000</pubDate>
</item>
<item>
<title>UUID vs ULID vs Integer IDs: A Technical Guide for Modern Systems</title>
<link>https://coderlegion.com/4482/uuid-vs-ulid-vs-integer-ids-a-technical-guide-for-modern-systems</link>
<description>&lt;p&gt;Unique identifiers are the backbone of data management, distributed systems, and secure API design. While UUIDs (UUIDv4) and integer IDs are widely used, ULIDs (Universally Unique Lexicographically Sortable Identifiers) are emerging as a superior choice for modern applications. This article explores their technical differences, focusing on performance in .NET ecosystems and database-level implications.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;This article was originally published on the &lt;a rel=&quot;nofollow&quot; href=&quot;https://byteaether.github.io/2025/uuid-vs-ulid-vs-integer-ids-a-technical-guide-for-modern-systems/&quot;&gt;ByteAether Blog&lt;/a&gt;. It has been republished here with minor edits for clarity and conciseness.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;identifier-types&quot;&gt;Identifier Types&lt;/h3&gt;
&lt;h4 id=&quot;integer-ids&quot;&gt;Integer IDs&lt;/h4&gt;
&lt;p&gt;Sequential numeric values managed by databases via auto-increment. Pros: simple, efficient for indexing. Cons: predictable (security risks), centralized generation (not ideal for distributed systems).&lt;/p&gt;
&lt;h4 id=&quot;uuids&quot;&gt;UUIDs&lt;/h4&gt;
&lt;p&gt;128-bit identifiers using randomness (UUIDv4). Pros: globally unique. Cons: lack sortability, cause database fragmentation (random insertion disrupts B+ tree indexes).&lt;/p&gt;
&lt;h4 id=&quot;ulids&quot;&gt;ULIDs&lt;/h4&gt;
&lt;p&gt;128-bit identifiers with a 48-bit timestamp (millisecond precision) and 80 bits of randomness. Pros: lexicographically sortable, compact, URL-safe, chronological insertion (reduces fragmentation).&lt;/p&gt;
&lt;h3 id=&quot;performance-in-net-applications&quot;&gt;Performance in .NET Applications&lt;/h3&gt;
&lt;h4 id=&quot;ulid-generation&quot;&gt;ULID Generation&lt;/h4&gt;
&lt;p&gt;The &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/ByteAether/Ulid&quot;&gt;ByteAether.Ulid&lt;/a&gt; library generates ULIDs with zero heap allocations, leveraging stack-based operations. Key feature: &lt;strong&gt;monotonicity&lt;/strong&gt; (sequential generation within the same millisecond), ensuring logical order in databases.&lt;/p&gt;
&lt;h4 id=&quot;uuid-limitations&quot;&gt;UUID Limitations&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;Guid.NewGuid()&lt;/code&gt; in .NET is fast but lacks ordering, causing random insertions and fragmentation. ULIDs align with database indexing patterns, improving throughput.&lt;/p&gt;
&lt;h3 id=&quot;database-performance-index-fragmentation&quot;&gt;Database Performance: Index Fragmentation&lt;/h3&gt;
&lt;p&gt;Databases use B+ trees to store records in fixed-size pages. Random identifiers (UUIDs) force page splits, increasing I/O overhead, memory pressure, and write amplification. ULIDs embed timestamps, enabling sequential insertion and minimizing splits.&lt;/p&gt;
&lt;h3 id=&quot;implementing-ulids-in-net&quot;&gt;Implementing ULIDs in .NET&lt;/h3&gt;
&lt;h4 id=&quot;entity-framework-core&quot;&gt;Entity Framework Core&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;public class Order  
{  
    public Ulid Id { get; set; } // Stored as BINARY(16)  
    public string Product { get; set; }  
}  

public class UlidToBytesConverter : ValueConverter&amp;lt;Ulid, byte[]&amp;gt;  
{  
    public UlidToBytesConverter() : base(
        x =&amp;gt; x.ToByteArray(), x =&amp;gt; Ulid.New(x)
    ) { }  
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;dapper&quot;&gt;Dapper&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;public class UlidTypeHandler : SqlMapper.TypeHandler&amp;lt;Ulid&amp;gt;  
{  
    public override void SetValue(IDbDataParameter parameter, Ulid value)
        =&amp;gt; parameter.Value = value.ToByteArray();  
    public override Ulid Parse(object value)
        =&amp;gt; Ulid.New((byte[])value);  
}  
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;when-to-use-ulids&quot;&gt;When to Use ULIDs&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Distributed Systems:&lt;/strong&gt; Decentralized generation.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Time-Series Data:&lt;/strong&gt; Built-in timestamps simplify queries.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Batch Processing:&lt;/strong&gt; Monotonicity preserves order.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;why-byteaether-ulid-&quot;&gt;Why ByteAether.Ulid?&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Speed:&lt;/strong&gt; Faster than other ULID implementations.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Efficiency:&lt;/strong&gt; Zero heap allocations.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Integration:&lt;/strong&gt; Works seamlessly with EF Core, Dapper, and JSON serializers.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;conclusion-3&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;ULIDs address the limitations of UUIDs and integers, offering sortability, reduced fragmentation, and scalability. For .NET developers, &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/ByteAether/Ulid&quot;&gt;ByteAether.Ulid&lt;/a&gt; provides a future-proof solution.&lt;/p&gt;
&lt;p&gt;Read the full guide: &lt;a rel=&quot;nofollow&quot; href=&quot;https://byteaether.github.io/2025/uuid-vs-ulid-vs-integer-ids-a-technical-guide-for-modern-systems/&quot;&gt;UUID vs ULID vs Integer IDs: A Technical Guide for Modern Systems&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Install the library:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;dotnet add package ByteAether.Ulid  
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;By adopting ULIDs, you can achieve faster queries, lower infrastructure costs, and scalable architectures.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;This article was originally published on the &lt;a rel=&quot;nofollow&quot; href=&quot;https://byteaether.github.io/2025/uuid-vs-ulid-vs-integer-ids-a-technical-guide-for-modern-systems/&quot;&gt;ByteAether Blog&lt;/a&gt;. It has been republished here with minor edits for clarity and conciseness.&lt;/p&gt;
&lt;/blockquote&gt;
</description>
<category>Backend Dev</category>
<guid isPermaLink="true">https://coderlegion.com/4482/uuid-vs-ulid-vs-integer-ids-a-technical-guide-for-modern-systems</guid>
<pubDate>Wed, 06 Aug 2025 20:37:38 +0000</pubDate>
</item>
<item>
<title>ByteAether.Ulid v1.3.0: Enhanced ULID Generation Control and Security</title>
<link>https://coderlegion.com/4362/byteaether-ulid-v1-3-0-enhanced-ulid-generation-control-and-security</link>
<description>&lt;p&gt;Choosing the right identifier is a critical design decision in any software system, especially when working with distributed architectures. While GUIDs (Globally Unique Identifiers) provide uniqueness and integer IDs offer sortability, neither fully solves the dual challenge of ensuring universal uniqueness &lt;em&gt;and&lt;/em&gt; lexicographical sortability simultaneously. This is where &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/ulid/spec&quot;&gt;ULIDs (Universally Unique Lexicographically Sortable Identifiers)&lt;/a&gt; emerge as an increasingly valuable solution.&lt;/p&gt;
&lt;p&gt;We're excited to announce the release of &lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/ByteAether/Ulid/releases/tag/v1.3.0&quot;&gt;ByteAether.Ulid v1.3.0&lt;/a&gt;&lt;/strong&gt;, a significant update to our high-performance .NET ULID implementation. This version focuses on dramatically improving flexibility and security for ULID generation, making it even more suitable for demanding, modern applications.&lt;/p&gt;
&lt;h3 id=&quot;granular-control-with-generationoptions&quot;&gt;Granular Control with &lt;code&gt;GenerationOptions&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;In previous versions, ByteAether.Ulid handled monotonic generation through simple boolean flags. With v1.3.0, we're introducing a more sophisticated and powerful approach: the new &lt;code&gt;GenerationOptions&lt;/code&gt; configuration object. This object provides granular control over ULID generation through three key parameters: &lt;code&gt;Monotonicity&lt;/code&gt;, &lt;code&gt;InitialRandomSource&lt;/code&gt;, and &lt;code&gt;IncrementRandomSource&lt;/code&gt;.&lt;/p&gt;
&lt;h4 id=&quot;understanding-monotonicity-options&quot;&gt;Understanding &lt;code&gt;Monotonicity&lt;/code&gt; Options&lt;/h4&gt;
&lt;p&gt;The &lt;code&gt;Monotonicity&lt;/code&gt; setting is crucial for balancing the predictability and uniqueness of your ULIDs. Beyond the straightforward &lt;code&gt;NonMonotonic&lt;/code&gt; (fully random) and &lt;code&gt;MonotonicIncrement&lt;/code&gt; (strict sequential progression), v1.3.0 introduces a vital security enhancement:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;MonotonicRandom1Byte&lt;/code&gt; to &lt;code&gt;MonotonicRandom4Byte&lt;/code&gt;&lt;/strong&gt;: These options directly address a well-known concern from the ULID specification (e.g., &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/ulid/spec/issues/105&quot;&gt;issue #105&lt;/a&gt;) regarding predictable ULIDs within the same millisecond, which could potentially lead to enumeration attacks. By allowing a configurable random increment (ranging from 1-byte to 4-byte range) using the &lt;code&gt;IncrementRandomSource&lt;/code&gt;, these options add a crucial layer of randomness. This makes it significantly harder to guess subsequent ULIDs, effectively mitigating enumeration attack vectors while fully preserving the essential lexicographical sortability.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;flexible-randomness-with-irandomprovider&quot;&gt;Flexible Randomness with &lt;code&gt;IRandomProvider&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;Both the &lt;code&gt;InitialRandomSource&lt;/code&gt; and &lt;code&gt;IncrementRandomSource&lt;/code&gt; properties are now defined as &lt;code&gt;IRandomProvider&lt;/code&gt; types. This design choice offers unparalleled flexibility, enabling you to implement and supply your own custom random number generators.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;CryptographicallySecureRandomProvider&lt;/code&gt; (Default for &lt;code&gt;InitialRandomSource&lt;/code&gt;)&lt;/strong&gt;: This is the default for initial randomness, utilizing a cryptographically secure random number generator, ensuring high entropy for security-sensitive applications.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;PseudoRandomProvider&lt;/code&gt; (Default for &lt;code&gt;IncrementRandomSource&lt;/code&gt;)&lt;/strong&gt;: This option employs a faster pseudo-random algorithm, making it suitable for scenarios where cryptographic security for the increment value is not the primary concern and performance is prioritized.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;seamless-transition-and-backward-compatibility&quot;&gt;Seamless Transition and Backward Compatibility&lt;/h3&gt;
&lt;p&gt;We've ensured that ByteAether.Ulid v1.3.0 provides a smooth upgrade experience while maintaining backward compatibility. The default settings for &lt;code&gt;GenerationOptions&lt;/code&gt; are configured to precisely match the previous monotonic generation behavior:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;Ulid.DefaultGenerationOptions = new Ulid.GenerationOptions
{
Monotonicity = MonotonicityOptions.MonotonicIncrement,
InitialRandomSource = new CryptographicallySecureRandomProvider(),
IncrementRandomSource = new PseudoRandomProvider()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For existing codebases, public API methods that used the &lt;code&gt;bool? isMonotonic&lt;/code&gt; parameter and the &lt;code&gt;Ulid.DefaultIsMonotonic&lt;/code&gt; static property are now marked &lt;code&gt;[Obsolete]&lt;/code&gt;. We strongly encourage developers to migrate to the more powerful &lt;code&gt;GenerationOptions&lt;/code&gt;-based methods to leverage the enhanced control and flexibility. These deprecated elements will be removed in a future version, so updating your code now will ensure future compatibility.&lt;/p&gt;
&lt;h3 id=&quot;why-ulids-for-modern-systems-&quot;&gt;Why ULIDs for Modern Systems?&lt;/h3&gt;
&lt;p&gt;ULIDs effectively combine the best attributes for modern identifier needs:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Universally Unique&lt;/strong&gt;: Guarantees global distinctness across diverse systems and environments.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Lexicographically Sortable&lt;/strong&gt;: This is a game-changer for database performance, enabling efficient time-based sorting, optimized indexing, and rapid chronological data retrieval.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Human-Readable&lt;/strong&gt;: Generally more manageable and readable compared to traditional GUIDs.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/ByteAether/Ulid&quot;&gt;ByteAether.Ulid&lt;/a&gt; actively addresses potential issues found in the official ULID specification, such as the &quot;random part overflow&quot; (&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/ulid/spec/issues/39&quot;&gt;ULID specification issue #39&lt;/a&gt;), by intelligently allowing timestamp increments to ensure unique ULIDs even under extremely high generation rates. Furthermore, our extensive benchmarking against other popular .NET ULID implementations consistently positions ByteAether.Ulid as highly performant and fully compliant with the ULID specification, ensuring dependable generation for your applications.&lt;/p&gt;
&lt;h3 id=&quot;deep-dive-into-the-details&quot;&gt;Deep Dive into the Details&lt;/h3&gt;
&lt;p&gt;ByteAether.Ulid v1.3.0 represents a significant leap forward in providing secure, flexible, and high-performance identifier generation for the .NET ecosystem. To get all the technical details, including installation instructions, comprehensive usage examples, and its seamless integration with popular .NET libraries and frameworks (like ASP.NET Core, System.Text.Json, EF Core, Dapper, MessagePack, and Newtonsoft.Json), we encourage you to read the full article on our blog.&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://byteaether.github.io/2025/byteaetherulid-v130-enhanced-ulid-generation-control-and-security/&quot;&gt;Read the full article for complete details and code examples!&lt;/a&gt;&lt;/p&gt;
</description>
<category>C#</category>
<guid isPermaLink="true">https://coderlegion.com/4362/byteaether-ulid-v1-3-0-enhanced-ulid-generation-control-and-security</guid>
<pubDate>Thu, 31 Jul 2025 12:26:12 +0000</pubDate>
</item>
<item>
<title>Unifying Uniqueness and Order: Why ULIDs are the Future of Identifiers</title>
<link>https://coderlegion.com/3984/unifying-uniqueness-and-order-why-ulids-are-the-future-of-identifiers</link>
<description>&lt;p&gt;Choosing the right identifier scheme is crucial in the sprawling landscape of software development. It impacts performance, scalability, and reliability, whether you're architecting a database, an API, or a distributed system. While auto-incrementing integers and UUIDs have long been the go-to choices, they each come with their own set of trade-offs. Enter ULIDs (Universally Unique Lexicographically Sortable Identifiers) – a modern alternative that masterfully blends the best of both worlds.&lt;/p&gt;
&lt;h3 id=&quot;the-identifier-dilemma-old-vs-new&quot;&gt;The Identifier Dilemma: Old vs. New&lt;/h3&gt;
&lt;p&gt;For decades, relational databases have relied on &lt;strong&gt;auto-incrementing integers&lt;/strong&gt; for their simplicity, understandability, and excellent performance in single-node systems. They ensure predictable order, compact storage, and fast operations. However, this simplicity falters in distributed systems due to centralized coordination, scalability challenges, and the risk of collisions.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;UUIDs&lt;/strong&gt;, on the other hand, were designed for the distributed nature of modern systems. They guarantee global uniqueness without central coordination, offering distributed scalability and standardization across many programming languages. But their inherent randomness leads to index fragmentation, poor write performance, and human unreadability. Plus, their lack of temporal ordering complicates time-based queries.&lt;/p&gt;
&lt;h3 id=&quot;ulids-the-best-of-both-worlds&quot;&gt;ULIDs: The Best of Both Worlds&lt;/h3&gt;
&lt;p&gt;ULIDs were born to resolve these limitations by combining a 48-bit timestamp with an 80-bit random component. This innovative structure delivers:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Global Uniqueness:&lt;/strong&gt; Just like UUIDs, ULIDs are unique across distributed systems.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Temporal Ordering:&lt;/strong&gt; The timestamp component ensures ULIDs are lexicographically sortable, preserving the chronological order of creation.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Distributed Generation:&lt;/strong&gt; ULIDs can be generated independently, eliminating the need for centralized coordination.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Human Readability:&lt;/strong&gt; ULIDs utilize &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.crockford.com/base32.html&quot;&gt;Crockford's Base32 encoding&lt;/a&gt;, which avoids ambiguous characters, making them much easier to read and debug.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The official ULID specification on &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/ulid/spec&quot;&gt;GitHub&lt;/a&gt; provides comprehensive guidelines for implementation across various programming languages.&lt;/p&gt;
&lt;h3 id=&quot;enhancing-readability-with-crockford-s-base32-encoding&quot;&gt;Enhancing Readability with Crockford's Base32 Encoding&lt;/h3&gt;
&lt;p&gt;A standout feature of ULIDs is their adoption of Crockford's Base32 encoding, specifically designed to boost human readability. This encoding scheme carefully sidesteps visually similar characters (like &quot;I&quot; and &quot;1&quot; or &quot;O&quot; and &quot;0&quot;), drastically reducing transcription errors. Furthermore, Base32-encoded ULIDs are case-insensitive, simplifying their handling in systems that don't preserve case. This thoughtful design bridges the gap between machine efficiency and human usability.&lt;/p&gt;
&lt;h3 id=&quot;monotonicity-preserving-order-in-high-throughput-systems&quot;&gt;Monotonicity: Preserving Order in High-Throughput Systems&lt;/h3&gt;
&lt;p&gt;One of ULID's powerful features is its &lt;strong&gt;monotonicity&lt;/strong&gt;, which ensures order even when multiple identifiers are generated within the same millisecond. In high-throughput scenarios, the monotonicity feature guarantees that if multiple ULIDs share the same timestamp, they are incremented to maintain their lexicographical order. This provides:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Consistency:&lt;/strong&gt; Data remains ordered, even under high-concurrency scenarios.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Query Efficiency:&lt;/strong&gt; Time-based range queries continue to perform well, regardless of the volume of data generated.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reliable Sorting:&lt;/strong&gt; Applications relying on ordered identifiers, such as event streams or analytics platforms, greatly benefit from this feature.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;where-ulids-shine-in-modern-applications&quot;&gt;Where ULIDs Shine in Modern Applications&lt;/h3&gt;
&lt;p&gt;ULIDs are especially well-suited for applications demanding scalability, temporal ordering, and distributed generation. Consider these use cases:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Event Logging:&lt;/strong&gt; Efficiently store and retrieve time-stamped log entries.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Distributed Databases:&lt;/strong&gt; Eliminate the need for central coordination while preserving order.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Analytics and Reporting:&lt;/strong&gt; Facilitate time-based filtering and aggregation of data.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;High-Throughput Systems:&lt;/strong&gt; Maintain performance and order even under heavy write loads.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;integrating-ulids-into-your-projects&quot;&gt;Integrating ULIDs into Your Projects&lt;/h3&gt;
&lt;p&gt;A variety of libraries are available across programming languages to help you adopt ULIDs. For .NET developers, the &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/ByteAether/Ulid&quot;&gt;ByteAether's Ulid GitHub repository&lt;/a&gt; offers a high-performance C# implementation.&lt;/p&gt;
&lt;p&gt;By adhering to the official specification and leveraging well-maintained libraries, integrating ULIDs into your systems can be a seamless process, leading to maximum reliability.&lt;/p&gt;
&lt;h3 id=&quot;ready-to-modernize-your-identifiers-&quot;&gt;Ready to Modernize Your Identifiers?&lt;/h3&gt;
&lt;p&gt;ULIDs offer a compelling, modern, and versatile solution to the persistent challenges of identifier generation in software systems. They successfully combine the strengths of auto-incrementing integers and UUIDs while deftly avoiding their weaknesses, providing globally unique, lexicographically sortable, and human-readable identifiers. With features like Crockford's Base32 encoding and monotonicity, ULIDs are perfectly tailored for the demands of today's applications, from distributed systems to time-based analytics.&lt;/p&gt;
&lt;p&gt;Whether you're embarking on a new project or optimizing an existing one, ULIDs present a powerful option for identifier management. Their burgeoning ecosystem of libraries and robust community support position them to become a standard choice for developers prioritizing scalability, performance, and simplicity.&lt;/p&gt;
&lt;p&gt;Want to dive deeper into the world of ULIDs and explore their practical applications? Read the full article on my blog: &lt;a rel=&quot;nofollow&quot; href=&quot;https://byteaether.github.io/2025/an-introduction-to-ulids-a-modern-identifier-for-software-systems/&quot;&gt;An Introduction to ULIDs: A Modern Identifier for Software Systems&lt;/a&gt; and discover more valuable insights at &lt;a rel=&quot;nofollow&quot; href=&quot;https://byteaether.github.io/&quot;&gt;ByteAether Blog&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Start integrating ULIDs into your .NET applications today with &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/ByteAether/Ulid&quot;&gt;ByteAether.Ulid&lt;/a&gt;. We're eager to hear your thoughts and see what you build!&lt;/p&gt;
</description>
<category>Backend Dev</category>
<guid isPermaLink="true">https://coderlegion.com/3984/unifying-uniqueness-and-order-why-ulids-are-the-future-of-identifiers</guid>
<pubDate>Mon, 14 Jul 2025 11:43:27 +0000</pubDate>
</item>
<item>
<title>Ditch Your GUID Frustrations: Introducing ByteAether.Ulid v1.0.0 for .NET!</title>
<link>https://coderlegion.com/3853/ditch-your-guid-frustrations-introducing-byteaether-ulid-v1-0-0-for-net</link>
<description>&lt;p&gt;Are you tired of dealing with GUIDs that are a pain to sort and even harder to read? If you're a .NET developer working with distributed systems or time-sensitive data, you know the struggle is real. While GUIDs offer uniqueness, they fall short when it comes to human readability and, crucially, sortability. That's where ULIDs come in, and we're thrilled to introduce &lt;strong&gt;ByteAether.Ulid&lt;/strong&gt;, a high-performance .NET library designed to make your life a whole lot easier.&lt;/p&gt;
&lt;p&gt;We've just launched &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/ByteAether/Ulid&quot;&gt;ByteAether.Ulid&lt;/a&gt;, a library specifically crafted to generate and work with &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/ulid/spec&quot;&gt;ULIDs (Universally Unique Lexicographically Sortable Identifiers)&lt;/a&gt;. This isn't just another ID generator. It's a solution that brings together the best of uniqueness, readability, and sortability, all while delivering exceptional performance for your modern .NET applications.&lt;/p&gt;
&lt;h3 id=&quot;why-ulids-and-why-byteaether-ulid-&quot;&gt;Why ULIDs? And Why ByteAether.Ulid?&lt;/h3&gt;
&lt;p&gt;ULIDs are a game-changer because they offer the best of both worlds:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Globally Unique:&lt;/strong&gt; You can generate ULIDs across different systems without worrying about collisions.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Sortable:&lt;/strong&gt; Unlike GUIDs, ULIDs are lexicographically ordered, making them perfect for time-based data. Imagine how much simpler your database queries become!&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Human-Readable:&lt;/strong&gt; Forget those convoluted GUID strings. ULIDs use a base32 encoding that's much easier on the eyes.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;While the ULID specification itself is fantastic, ByteAether.Ulid takes it a step further. We've built this library to be not just compliant with the &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/ulid/spec&quot;&gt;ULID specification&lt;/a&gt;, but also robust. We've tackled common issues like random-part overflow, ensuring that your ULID generation is truly error-free and reliable.&lt;/p&gt;
&lt;h3 id=&quot;key-features-that-make-byteaether-ulid-stand-out&quot;&gt;Key Features That Make ByteAether.Ulid Stand Out&lt;/h3&gt;
&lt;p&gt;We've focused on delivering a library that's powerful, efficient, and easy to integrate:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Error-Free ULID Generation:&lt;/strong&gt; We've implemented graceful handling for edge cases, such as random-part overflow. This means timestamps are incremented automatically to maintain monotonicity, preventing errors and ensuring smooth operation.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;High-Performance Design:&lt;/strong&gt; ByteAether.Ulid is engineered for speed and minimal memory footprint. We're proud to say it outperforms other popular ULID libraries in the .NET ecosystem.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Seamless .NET Integration:&lt;/strong&gt; Whether you're on .NET Standard 2.0 or the latest .NET 9.0, ByteAether.Ulid has you covered. It includes native support for ASP.NET Core route parameters and is fully compatible with &lt;code&gt;System.Text.Json&lt;/code&gt; for hassle-free serialization and deserialization.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Versatile Conversion Methods:&lt;/strong&gt; Easily convert ULIDs to and from byte arrays, GUIDs, and strings, giving you the flexibility you need.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;getting-started-is-a-breeze-&quot;&gt;Getting Started is a Breeze!&lt;/h3&gt;
&lt;p&gt;Ready to ditch those GUID woes? Getting started with ByteAether.Ulid is incredibly simple. Just install the NuGet package:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;dotnet add package ByteAether.Ulid&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Once installed, generating and using ULIDs is straightforward:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;using System;

class Program
{
    static void Main()
    {
        // Create a new ULID
        var ulid = Ulid.New();

        // Convert to string and back
        string ulidString = ulid.ToString();
        var ulidFromString = Ulid.Parse(ulidString);

        Console.WriteLine($&quot;Generated ULID: {ulid}&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For more detailed usage instructions and a complete API reference, be sure to check out our &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/ByteAether/Ulid&quot;&gt;GitHub repository&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;performance-you-can-rely-on&quot;&gt;Performance You Can Rely On&lt;/h3&gt;
&lt;p&gt;We understand that performance is crucial for modern applications. ByteAether.Ulid is not only fully compliant with the ULID specification but also designed for speed. We've benchmarked it against other libraries, and ByteAether.Ulid consistently comes out on top in terms of generation speed, while offering graceful error handling. You can find a full breakdown of our benchmarks and testing methodology on our &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/ByteAether/Ulid&quot;&gt;GitHub repository&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;open-source-and-community-driven&quot;&gt;Open Source and Community-Driven&lt;/h3&gt;
&lt;p&gt;ByteAether.Ulid is an open-source project licensed under the MIT License. We believe in the power of community, and we welcome your contributions! Whether you have a bug to report, a feature to suggest, or want to submit a pull request, your input is valuable.&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/ByteAether/Ulid/issues&quot;&gt;Submit an Issue&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/ByteAether/Ulid/pulls&quot;&gt;Contribute Code&lt;/a&gt;&lt;/p&gt;
&lt;h3 id=&quot;what-s-next-for-byteaether-&quot;&gt;What's Next for ByteAether?&lt;/h3&gt;
&lt;p&gt;We're just getting started! Stay tuned for more in-depth articles on our main blog, where we'll be exploring:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Why ULIDs are a game-changer for modern databases.&lt;/li&gt;
&lt;li&gt;Optimizing performance in distributed systems with ULIDs.&lt;/li&gt;
&lt;li&gt;Real-world use cases for ULIDs in .NET applications.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Ready to simplify your .NET projects and experience the difference ULIDs can make? Install &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.nuget.org/packages/ByteAether.Ulid/&quot;&gt;ByteAether.Ulid&lt;/a&gt; today!&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Want to dive deeper into the world of ULIDs and modern .NET development?&lt;/strong&gt; This is just a snippet! Read the full article and explore more valuable content on my blog: &lt;a rel=&quot;nofollow&quot; href=&quot;https://byteaether.github.io/2025/announcing-byteaetherulid-v100-a-high-performance-net-ulid-library-for-modern-applications/&quot;&gt;https://byteaether.github.io/2025/announcing-byteaetherulid-v100-a-high-performance-net-ulid-library-for-modern-applications/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Visit my main blog for more insights and updates: &lt;a rel=&quot;nofollow&quot; href=&quot;https://byteaether.github.io/&quot;&gt;https://byteaether.github.io/&lt;/a&gt;&lt;/p&gt;
</description>
<category>C#</category>
<guid isPermaLink="true">https://coderlegion.com/3853/ditch-your-guid-frustrations-introducing-byteaether-ulid-v1-0-0-for-net</guid>
<pubDate>Wed, 09 Jul 2025 15:09:55 +0000</pubDate>
</item>
<item>
<title>Why Most .NET Developers Misuse Records — and What It’s Costing Them</title>
<link>https://coderlegion.com/3821/why-most-net-developers-misuse-records-and-what-its-costing-them</link>
<description>&lt;p&gt; Most .NET devs are misusing records—and it's costing them more than they think. Memory overhead. Identity tracking bugs. EF Core confusion. &lt;/p&gt;
&lt;p&gt;I broke it all down in my latest Medium post:  &lt;strong&gt;Why Most .NET Developers Misuse Records and What It’s Costing Them&lt;/strong&gt;  &lt;a rel=&quot;nofollow&quot; href=&quot;https://medium.com/@yaseer.arafat/why-most-net-developers-misuse-records-and-what-its-costing-them-afb17f9d05ae&quot;&gt;Read here&lt;/a&gt;&lt;/p&gt;
&lt;p&gt; Inside:&lt;/p&gt;
&lt;p&gt;Why value equality isn't always the win you expect&lt;/p&gt;
&lt;p&gt;How EF Core gets tripped up by &lt;code&gt;records&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Benchmark graphs showing real clone costs in hot loops&lt;/p&gt;
&lt;p&gt;When to ditch the with expression and reach for a constructor&lt;/p&gt;
&lt;p&gt;This one's for every architect, lead dev, and performance-obsessed coder who wants to use records right.&lt;/p&gt;
&lt;p&gt;️ Read it. Share it. Argue with it. Let’s rethink how we use C# records.&lt;/p&gt;
</description>
<category>C#</category>
<guid isPermaLink="true">https://coderlegion.com/3821/why-most-net-developers-misuse-records-and-what-its-costing-them</guid>
<pubDate>Tue, 08 Jul 2025 07:24:07 +0000</pubDate>
</item>
</channel>
</rss>