<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Zero Heartbeat</title>
    <description>The latest articles on DEV Community by Zero Heartbeat (@zero_heartbeat_06a3625d7a).</description>
    <link>https://dev.to/zero_heartbeat_06a3625d7a</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3782380%2Fa9edbbf7-36b9-434e-8c5d-238ab3ad0caf.png</url>
      <title>DEV Community: Zero Heartbeat</title>
      <link>https://dev.to/zero_heartbeat_06a3625d7a</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/zero_heartbeat_06a3625d7a"/>
    <language>en</language>
    <item>
      <title>Decompiling C# expression trees: the lambda that became a tree of factory calls</title>
      <dc:creator>Zero Heartbeat</dc:creator>
      <pubDate>Thu, 22 Oct 2026 04:38:00 +0000</pubDate>
      <link>https://dev.to/zero_heartbeat_06a3625d7a/decompiling-c-expression-trees-the-lambda-that-became-a-tree-of-factory-calls-335b</link>
      <guid>https://dev.to/zero_heartbeat_06a3625d7a/decompiling-c-expression-trees-the-lambda-that-became-a-tree-of-factory-calls-335b</guid>
      <description>&lt;p&gt;A lambda is one of the few constructs in C# whose compilation depends on something &lt;em&gt;outside&lt;/em&gt; the lambda. Write &lt;code&gt;x =&amp;gt; x + 1&lt;/code&gt; and assign it to a &lt;code&gt;Func&amp;lt;int, int&amp;gt;&lt;/code&gt; and you get a hidden static method and a delegate; assign the identical text to an &lt;code&gt;Expression&amp;lt;Func&amp;lt;int, int&amp;gt;&amp;gt;&lt;/code&gt; and you get something else entirely — a nested sequence of calls to the factory methods on &lt;code&gt;System.Linq.Expressions.Expression&lt;/code&gt; that builds a runtime data structure describing the lambda, node by node. There is no expression-tree opcode and no dedicated metadata. The decompiled method is a page of &lt;code&gt;Expression.Parameter&lt;/code&gt;, &lt;code&gt;Expression.Constant&lt;/code&gt;, &lt;code&gt;Expression.Add&lt;/code&gt; and &lt;code&gt;Expression.Lambda&lt;/code&gt; calls, with every member the source named reached through &lt;code&gt;ldtoken&lt;/code&gt;. Reading that lowering explains what a decompiler such as &lt;a href="https://delta1labs.com/products/glass" rel="noopener noreferrer"&gt;Glass.NET&lt;/a&gt; can fold back into the one-liner, why captured variables show up as a &lt;code&gt;Constant&lt;/code&gt; of a closure object, and why the query provider on the other end of an &lt;code&gt;IQueryable&lt;/code&gt; has to do real work to find your variable's value.&lt;/p&gt;

&lt;h2&gt;
  
  
  Same lambda, two compilations
&lt;/h2&gt;

&lt;p&gt;Start with the fact that decides everything. The conversion of a lambda is chosen by its &lt;em&gt;target type&lt;/em&gt;, not by its text:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;Func&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;             &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// delegate: a method plus a delegate object&lt;/span&gt;
&lt;span class="n"&gt;Expression&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Func&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// expression tree: a data structure&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first line compiles the way &lt;a href="https://delta1labs.com/blog/decompile-lambdas-closures-display-classes" rel="noopener noreferrer"&gt;lambdas and closures&lt;/a&gt; always do — the body becomes a method &lt;code&gt;&amp;lt;M&amp;gt;b__0_0&lt;/code&gt; on the compiler's &lt;code&gt;&amp;lt;&amp;gt;c&lt;/code&gt; singleton class, and the assignment is an &lt;code&gt;ldftn&lt;/code&gt; plus a &lt;code&gt;newobj&lt;/code&gt; of &lt;code&gt;Func&amp;lt;int, int&amp;gt;&lt;/code&gt;, cached in a static field. The body's IL is three instructions: &lt;code&gt;ldarg.1&lt;/code&gt;, &lt;code&gt;ldc.i4.1&lt;/code&gt;, &lt;code&gt;add&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The second line produces no hidden method at all. The &lt;code&gt;+&lt;/code&gt; is never emitted as an &lt;code&gt;add&lt;/code&gt; instruction. Instead the compiler emits code that &lt;em&gt;constructs&lt;/em&gt; a description of the addition:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;ParameterExpression&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Expression&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Parameter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s"&gt;"x"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;Expression&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Func&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Expression&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Lambda&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Func&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;(&lt;/span&gt;
    &lt;span class="n"&gt;Expression&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Expression&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Constant&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;))),&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;ParameterExpression&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the whole lowering, and it is already what a decompiler without expression-tree recognition will show you. Three things to notice: the parameter name &lt;code&gt;"x"&lt;/code&gt; is kept as a string literal, the literal &lt;code&gt;1&lt;/code&gt; is boxed into an &lt;code&gt;object&lt;/code&gt; because &lt;code&gt;Expression.Constant&lt;/code&gt; takes &lt;code&gt;(object, Type)&lt;/code&gt;, and the parameter node is created once, stored in a local, and used twice — once in the body and once in the parameter array. In IL it is a chain of &lt;code&gt;call&lt;/code&gt; instructions against the static factory methods, with &lt;code&gt;typeof(int)&lt;/code&gt; spelled as &lt;code&gt;ldtoken&lt;/code&gt; plus &lt;code&gt;Type.GetTypeFromHandle&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;ldtoken&lt;/span&gt;    &lt;span class="n"&gt;System&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Int32&lt;/span&gt;
&lt;span class="n"&gt;call&lt;/span&gt;       &lt;span class="n"&gt;Type&lt;/span&gt; &lt;span class="n"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;GetTypeFromHandle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;RuntimeTypeHandle&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;ldstr&lt;/span&gt;      &lt;span class="s"&gt;"x"&lt;/span&gt;
&lt;span class="n"&gt;call&lt;/span&gt;       &lt;span class="n"&gt;ParameterExpression&lt;/span&gt; &lt;span class="n"&gt;Expression&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;Parameter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;stloc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;
&lt;span class="n"&gt;ldloc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;                                   &lt;span class="c1"&gt;// left operand: the parameter node&lt;/span&gt;
&lt;span class="n"&gt;ldc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;i4&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;
&lt;span class="n"&gt;box&lt;/span&gt;        &lt;span class="n"&gt;System&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Int32&lt;/span&gt;                   &lt;span class="c1"&gt;// Constant takes object&lt;/span&gt;
&lt;span class="n"&gt;ldtoken&lt;/span&gt;    &lt;span class="n"&gt;System&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Int32&lt;/span&gt;
&lt;span class="n"&gt;call&lt;/span&gt;       &lt;span class="n"&gt;Type&lt;/span&gt; &lt;span class="n"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;GetTypeFromHandle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;RuntimeTypeHandle&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;call&lt;/span&gt;       &lt;span class="n"&gt;ConstantExpression&lt;/span&gt; &lt;span class="n"&gt;Expression&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;Constant&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;object&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;call&lt;/span&gt;       &lt;span class="n"&gt;BinaryExpression&lt;/span&gt; &lt;span class="n"&gt;Expression&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Expression&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Expression&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;ldc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;i4&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;
&lt;span class="n"&gt;newarr&lt;/span&gt;     &lt;span class="n"&gt;ParameterExpression&lt;/span&gt;
&lt;span class="n"&gt;dup&lt;/span&gt;
&lt;span class="n"&gt;ldc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;i4&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;
&lt;span class="n"&gt;ldloc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;
&lt;span class="n"&gt;stelem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;ref&lt;/span&gt;
&lt;span class="n"&gt;call&lt;/span&gt;       &lt;span class="n"&gt;Expression&lt;/span&gt;&lt;span class="err"&gt;`&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;!!&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Expression&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Lambda&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Func&lt;/span&gt;&lt;span class="err"&gt;`&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;int32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;int32&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;Expression&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ParameterExpression&lt;/span&gt;&lt;span class="p"&gt;[])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(The assembly qualifiers are elided here for readability; the real IL spells out &lt;code&gt;[System.Linq.Expressions]System.Linq.Expressions.Expression&lt;/code&gt; every time, which is a large part of why the raw view of an expression-tree method is so long.)&lt;/p&gt;

&lt;p&gt;Every expression tree you will ever decompile has this skeleton: a &lt;code&gt;Parameter&lt;/code&gt; call per lambda parameter stored in a local, a body built bottom-up from factory calls that consume the stack, and a closing &lt;code&gt;Lambda&amp;lt;TDelegate&amp;gt;&lt;/code&gt; that takes the body and an explicit &lt;code&gt;ParameterExpression[]&lt;/code&gt;. The generic type argument of that final call is the delegate type — it is the one place the original &lt;code&gt;Expression&amp;lt;Func&amp;lt;int, int&amp;gt;&amp;gt;&lt;/code&gt; signature is visible in the body.&lt;/p&gt;

&lt;h2&gt;
  
  
  Members are reached by token, not by name
&lt;/h2&gt;

&lt;p&gt;As soon as the lambda touches a property, field or method, the lowering has to say &lt;em&gt;which&lt;/em&gt; member, and it does so the only way the runtime can guarantee is exact: a metadata token. Take a filter over a type with a property:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;Expression&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Func&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Customer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;adults&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Age&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="m"&gt;18&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The property access becomes &lt;code&gt;Expression.Property&lt;/code&gt;, and the &lt;code&gt;PropertyInfo&lt;/code&gt; it needs is obtained from the getter's method token:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;ParameterExpression&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Expression&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Parameter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Customer&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s"&gt;"c"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;Expression&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Func&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Customer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;adults&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Expression&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Lambda&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Func&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Customer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;(&lt;/span&gt;
    &lt;span class="n"&gt;Expression&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GreaterThanOrEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;Expression&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Property&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MethodInfo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;MethodBase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetMethodFromHandle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="cm"&gt;/* ldtoken Customer::get_Age */&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
        &lt;span class="n"&gt;Expression&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Constant&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;18&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;))),&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;ParameterExpression&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In IL the member reference is &lt;code&gt;ldtoken method instance int32 Customer::get_Age()&lt;/code&gt; followed by &lt;code&gt;call MethodBase::GetMethodFromHandle(RuntimeMethodHandle)&lt;/code&gt; and a &lt;code&gt;castclass MethodInfo&lt;/code&gt;. Fields go through &lt;code&gt;ldtoken field&lt;/code&gt; plus &lt;code&gt;FieldInfo.GetFieldFromHandle&lt;/code&gt; and &lt;code&gt;Expression.Field&lt;/code&gt;. Methods go through the same token-plus-handle pair into &lt;code&gt;Expression.Call&lt;/code&gt;. When the member lives on a generic type, the compiler uses the two-argument &lt;code&gt;GetMethodFromHandle(RuntimeMethodHandle, RuntimeTypeHandle)&lt;/code&gt; overload and also passes the declaring type's token, because a method handle alone does not identify which instantiation it belongs to.&lt;/p&gt;

&lt;p&gt;This is the detail that makes expression trees robust and decompilation easy at the same time. The tree does not contain the string &lt;code&gt;"Age"&lt;/code&gt;; it contains a token that resolves to exactly the &lt;code&gt;get_Age&lt;/code&gt; the source bound to, overload and all. A decompiler reads the token straight back out of the &lt;code&gt;ldtoken&lt;/code&gt; operand and prints &lt;code&gt;c.Age&lt;/code&gt;. The same tokens are what a renaming obfuscator rewrites consistently, which is why expression trees survive renaming while reflection-by-string does not.&lt;/p&gt;

&lt;p&gt;Operators that C# defines on a type rather than on a primitive carry their operator method along. A string comparison is the common case:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;Expression&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Func&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Customer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;named&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"Ada"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;does not lower to the two-argument &lt;code&gt;Expression.Equal&lt;/code&gt;. It lowers to the four-argument overload with the user-defined operator attached:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;Expression&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;Expression&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Property&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="cm"&gt;/* get_Name */&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;Expression&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Constant&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Ada"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
    &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                                                   &lt;span class="c1"&gt;// liftToNull&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MethodInfo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;MethodBase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetMethodFromHandle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="cm"&gt;/* ldtoken string::op_Equality */&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;MethodInfo&lt;/code&gt; is &lt;code&gt;String.op_Equality(string, string)&lt;/code&gt; — the compiler recorded which &lt;code&gt;==&lt;/code&gt; it resolved, so a query provider can tell string equality from reference equality. &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt; and &lt;code&gt;||&lt;/code&gt; become &lt;code&gt;AndAlso&lt;/code&gt; and &lt;code&gt;OrElse&lt;/code&gt;; the non-short-circuit &lt;code&gt;&amp;amp;&lt;/code&gt; and &lt;code&gt;|&lt;/code&gt; become &lt;code&gt;And&lt;/code&gt; and &lt;code&gt;Or&lt;/code&gt;; &lt;code&gt;?:&lt;/code&gt; becomes &lt;code&gt;Expression.Condition&lt;/code&gt; and &lt;code&gt;??&lt;/code&gt; becomes &lt;code&gt;Expression.Coalesce&lt;/code&gt;. Each maps back to exactly one piece of C# syntax, and a decompiler that knows the table rebuilds the body one node at a time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Captured variables: a Constant that is not constant
&lt;/h2&gt;

&lt;p&gt;The lowering gets genuinely interesting when the lambda uses a variable from the enclosing method:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;Expression&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Func&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Customer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;OlderThan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;min&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Age&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;min&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a delegate, &lt;code&gt;min&lt;/code&gt; would be hoisted into a display class and the lambda body would read it as a field — the familiar closure pattern. An expression tree needs the same thing, but it has to express "read field &lt;code&gt;min&lt;/code&gt; from this particular closure object" &lt;em&gt;as a tree node&lt;/em&gt;. The compiler does it with a &lt;code&gt;Constant&lt;/code&gt; whose value is the closure instance, and a &lt;code&gt;Field&lt;/code&gt; access on top of it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;c__DisplayClass0_0&lt;/span&gt; &lt;span class="n"&gt;closure&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;c__DisplayClass0_0&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;closure&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;min&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;min&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;ParameterExpression&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Expression&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Parameter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Customer&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s"&gt;"c"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Expression&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Lambda&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Func&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Customer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;(&lt;/span&gt;
    &lt;span class="n"&gt;Expression&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GreaterThan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;Expression&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Property&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="cm"&gt;/* get_Age */&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;Expression&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;Expression&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Constant&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;closure&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&amp;lt;&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;c__DisplayClass0_0&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
            &lt;span class="n"&gt;FieldInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetFieldFromHandle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="cm"&gt;/* ldtoken &amp;lt;&amp;gt;c__DisplayClass0_0::min */&lt;/span&gt;&lt;span class="p"&gt;))),&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;ParameterExpression&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;/p&gt;

&lt;p&gt;Two consequences follow from this shape, and both are visible only in the lowered form. First, the tree does not contain the &lt;em&gt;value&lt;/em&gt; of &lt;code&gt;min&lt;/code&gt;; it contains a reference to the closure object and a field to read from it. The value is fetched when the tree is compiled or walked, so a tree built from a mutable local sees later mutations exactly as a delegate would. Second, this is precisely why every LINQ query provider has a step that evaluates &lt;code&gt;Field&lt;/code&gt;-on-&lt;code&gt;Constant&lt;/code&gt; subtrees before translating a query: from the provider's point of view, &lt;code&gt;min&lt;/code&gt; is not a literal in the tree, it is a member access on an opaque object, and the provider has to execute that sub-expression to discover &lt;code&gt;18&lt;/code&gt; and turn it into a query parameter. If you have ever wondered why a captured variable becomes &lt;code&gt;@p0&lt;/code&gt; in generated SQL while an inline literal becomes a literal, this is the shape behind it.&lt;/p&gt;

&lt;p&gt;For a decompiler the pattern is a fingerprint. &lt;code&gt;Expression.Field(Expression.Constant(x, typeof(T)), f)&lt;/code&gt; where &lt;code&gt;T&lt;/code&gt; is a compiler-generated display class and &lt;code&gt;x&lt;/code&gt; is a local of that type collapses to the captured variable's name — the display-class field keeps the original name &lt;code&gt;min&lt;/code&gt;, so the recovered lambda reads &lt;code&gt;c =&amp;gt; c.Age &amp;gt; min&lt;/code&gt; even without a PDB. The surrounding &lt;code&gt;new &amp;lt;&amp;gt;c__DisplayClass0_0()&lt;/code&gt; and &lt;code&gt;closure.min = min&lt;/code&gt; are the same closure setup a delegate would need, and are folded away with it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conversions, checked arithmetic and boxing
&lt;/h2&gt;

&lt;p&gt;C# inserts implicit conversions silently; expression trees cannot, because every node has an exact type and the factory methods validate it. So a lambda that mixes types grows &lt;code&gt;Convert&lt;/code&gt; nodes the source never spelled out:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;Expression&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Func&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;double&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;half&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;/&lt;/span&gt; &lt;span class="m"&gt;2.0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;becomes&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;Expression&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Divide&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;Expression&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Convert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;double&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;      &lt;span class="c1"&gt;// the implicit int -&amp;gt; double promotion, made explicit&lt;/span&gt;
    &lt;span class="n"&gt;Expression&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Constant&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;2.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;double&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same happens for boxing — &lt;code&gt;Expression&amp;lt;Func&amp;lt;int, object&amp;gt;&amp;gt; box = x =&amp;gt; x;&lt;/code&gt; has a body of &lt;code&gt;Expression.Convert(x, typeof(object))&lt;/code&gt; — and for nullable lifting, where an &lt;code&gt;int?&lt;/code&gt; compared to a literal gets its literal wrapped in &lt;code&gt;Expression.Convert(Expression.Constant(3, typeof(int)), typeof(int?))&lt;/code&gt; so both operands of the comparison have the same nullable type. A &lt;code&gt;checked&lt;/code&gt; context changes the factory method rather than adding a node: &lt;code&gt;checked(x + 1)&lt;/code&gt; lowers to &lt;code&gt;Expression.AddChecked&lt;/code&gt;, and &lt;code&gt;checked((byte)x)&lt;/code&gt; to &lt;code&gt;Expression.ConvertChecked&lt;/code&gt;, so the overflow behaviour you asked for travels inside the tree as the choice of node type.&lt;/p&gt;

&lt;p&gt;This is the first place a decompiler has to make a judgement call. An &lt;code&gt;Expression.Convert(x, typeof(double))&lt;/code&gt; could be a cast the author wrote, &lt;code&gt;(double)x&lt;/code&gt;, or the promotion the compiler added. Both readings recompile to the same tree, so a decompiler can either print the cast — faithful but noisier — or drop conversions that C# would reinsert anyway and print &lt;code&gt;x / 2.0&lt;/code&gt;. Glass.NET prefers the shorter form where it is provably equivalent and keeps the explicit &lt;code&gt;Convert&lt;/code&gt; where it changes meaning, such as a narrowing cast or an &lt;code&gt;object&lt;/code&gt; box that an overload resolution depends on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Nested lambdas and Expression.Quote
&lt;/h2&gt;

&lt;p&gt;Real expression trees are usually query fragments, and query fragments call methods that themselves take lambdas. What the inner lambda lowers to depends, again, on its target type. Consider a filter written over &lt;code&gt;IQueryable&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;Expression&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Func&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IQueryable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Customer&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;,&lt;/span&gt; &lt;span class="n"&gt;IQueryable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Customer&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;active&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="n"&gt;q&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Active&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Queryable.Where&lt;/code&gt; takes an &lt;code&gt;Expression&amp;lt;Func&amp;lt;Customer, bool&amp;gt;&amp;gt;&lt;/code&gt;, so the inner lambda must itself become a tree — and the outer tree needs to carry that inner tree &lt;em&gt;as a value&lt;/em&gt;. The compiler lowers the inner lambda to its own &lt;code&gt;Expression.Lambda&lt;/code&gt; and wraps it in &lt;code&gt;Expression.Quote&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;ParameterExpression&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Expression&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Parameter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IQueryable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Customer&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;),&lt;/span&gt; &lt;span class="s"&gt;"q"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;ParameterExpression&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Expression&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Parameter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Customer&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s"&gt;"c"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;Expression&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Lambda&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Func&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IQueryable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Customer&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;,&lt;/span&gt; &lt;span class="n"&gt;IQueryable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Customer&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&amp;gt;(&lt;/span&gt;
    &lt;span class="n"&gt;Expression&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                                                             &lt;span class="c1"&gt;// static method&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MethodInfo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;MethodBase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetMethodFromHandle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="cm"&gt;/* ldtoken Queryable::Where&amp;lt;Customer&amp;gt;(IQueryable&amp;lt;Customer&amp;gt;, Expression&amp;lt;Func&amp;lt;Customer,bool&amp;gt;&amp;gt;) */&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;Expression&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;Expression&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Quote&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;Expression&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Lambda&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Func&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Customer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;(&lt;/span&gt;
                    &lt;span class="n"&gt;Expression&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Property&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="cm"&gt;/* get_Active */&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;ParameterExpression&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="p"&gt;}))&lt;/span&gt;
        &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;ParameterExpression&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice the &lt;code&gt;ldtoken&lt;/code&gt; for &lt;code&gt;Where&lt;/code&gt; names the &lt;em&gt;constructed&lt;/em&gt; generic method &lt;code&gt;Where&amp;lt;Customer&amp;gt;&lt;/code&gt;, so the type argument the compiler inferred is recorded in the tree and the decompiler can leave it inferred in the output. Notice also that the static call passes &lt;code&gt;null&lt;/code&gt; for the instance argument — Roslyn consistently uses the &lt;code&gt;Call(Expression, MethodInfo, Expression[])&lt;/code&gt; overload for both instance and static calls, which is a small but reliable fingerprint of compiler-generated trees versus hand-built ones.&lt;/p&gt;

&lt;p&gt;Now change the source so the sequence is a &lt;code&gt;List&amp;lt;Customer&amp;gt;&lt;/code&gt; instead. The call binds to &lt;code&gt;Enumerable.Where&lt;/code&gt;, whose parameter is a plain &lt;code&gt;Func&amp;lt;Customer, bool&amp;gt;&lt;/code&gt;, and the &lt;code&gt;Quote&lt;/code&gt; disappears: the inner &lt;code&gt;Expression.Lambda&amp;lt;Func&amp;lt;Customer, bool&amp;gt;&amp;gt;(...)&lt;/code&gt; node is passed directly as the argument. A &lt;code&gt;LambdaExpression&lt;/code&gt; node &lt;em&gt;is&lt;/em&gt; of delegate type, so no wrapping is needed. The presence or absence of &lt;code&gt;Quote&lt;/code&gt; therefore tells you, without looking at the method signature, whether the callee receives the inner lambda as data to translate or as code to run — and a decompiler folds both forms to the same &lt;code&gt;c =&amp;gt; c.Active&lt;/code&gt;, because that is what the source said in both cases.&lt;/p&gt;

&lt;h2&gt;
  
  
  Object creation, initializers and anonymous types
&lt;/h2&gt;

&lt;p&gt;Projections lower to a small family of node factories that a decompiler has to recognise as a group. A &lt;code&gt;new&lt;/code&gt; with an object initializer becomes &lt;code&gt;MemberInit&lt;/code&gt; over &lt;code&gt;New&lt;/code&gt;, with one &lt;code&gt;Bind&lt;/code&gt; per assigned member, each referencing the property's &lt;em&gt;setter&lt;/em&gt; token:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;Expression&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Func&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Customer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Dto&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;toDto&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;Dto&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Age&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Age&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;Expression&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MemberInit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;Expression&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;New&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;ConstructorInfo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;MethodBase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetMethodFromHandle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="cm"&gt;/* ldtoken Dto::.ctor() */&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;MemberBinding&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Expression&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Bind&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;MethodInfo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;MethodBase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetMethodFromHandle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="cm"&gt;/* set_Name */&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;Expression&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Property&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="cm"&gt;/* get_Name */&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
        &lt;span class="n"&gt;Expression&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Bind&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;MethodInfo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;MethodBase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetMethodFromHandle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="cm"&gt;/* set_Age  */&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;Expression&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Property&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="cm"&gt;/* get_Age  */&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Anonymous types take a different route, because they have no settable properties. &lt;code&gt;c =&amp;gt; new { c.Name, c.Age }&lt;/code&gt; lowers to the &lt;code&gt;Expression.New&lt;/code&gt; overload that takes the constructor, the argument expressions &lt;em&gt;and&lt;/em&gt; an array of the property getters as members. That member list is how a query provider pairs each constructor argument with the property it initialises, and it is also how a decompiler recovers the &lt;code&gt;new { Name = ..., Age = ... }&lt;/code&gt; syntax — the &lt;a href="https://delta1labs.com/blog/decompile-anonymous-types-generated-class" rel="noopener noreferrer"&gt;generated anonymous-type class&lt;/a&gt; itself is just the usual &lt;code&gt;&amp;lt;&amp;gt;f__AnonymousType0&lt;/code&gt; with its read-only properties.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the tree cannot tell you
&lt;/h2&gt;

&lt;p&gt;Constant folding runs before expression-tree lowering, so arithmetic on literals is gone by the time the tree is built. &lt;code&gt;x =&amp;gt; x * (60 * 60)&lt;/code&gt; lowers to &lt;code&gt;Expression.Multiply(x, Expression.Constant(3600, typeof(int)))&lt;/code&gt;; no decompiler can recover &lt;code&gt;60 * 60&lt;/code&gt;, because the compiled assembly never contained it. The same applies to &lt;code&gt;const&lt;/code&gt; fields referenced from the lambda — they are inlined as &lt;code&gt;Constant&lt;/code&gt; nodes with the value, and the name of the constant is lost.&lt;/p&gt;

&lt;p&gt;The second limit is one the language imposes rather than the compiler. An expression-tree lambda can only have an expression body: no statements, no assignment, no &lt;code&gt;?.&lt;/code&gt;, no pattern matching, no &lt;code&gt;dynamic&lt;/code&gt;, no &lt;code&gt;async&lt;/code&gt;, no tuples, no local functions, and the compiler refuses the conversion (CS0834 and friends) rather than lowering them. The practical consequence for decompilation is that a compiler-generated tree is always a pure nest of factory calls with no control flow between them — so if you see branches, loops or a &lt;code&gt;Block&lt;/code&gt; node being built, you are looking at a hand-constructed tree, not a lowered lambda, and a decompiler correctly leaves it as the factory calls.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a decompiler folds, and what it shows
&lt;/h2&gt;

&lt;p&gt;Everything above reduces to a small set of rules. The outer &lt;code&gt;Expression.Lambda&amp;lt;TDelegate&amp;gt;(body, new ParameterExpression[] { ... })&lt;/code&gt; with parameters created by &lt;code&gt;Expression.Parameter(typeof(T), "name")&lt;/code&gt; gives the lambda header, including the original parameter names, which are literal strings in the IL. The body folds bottom-up: each binary and unary factory maps to one operator, &lt;code&gt;Property&lt;/code&gt;/&lt;code&gt;Field&lt;/code&gt;/&lt;code&gt;Call&lt;/code&gt; print the member their &lt;code&gt;ldtoken&lt;/code&gt; resolves to, &lt;code&gt;Constant&lt;/code&gt; prints a literal, &lt;code&gt;Field&lt;/code&gt; on a &lt;code&gt;Constant&lt;/code&gt; of a display class prints the captured variable, &lt;code&gt;Quote&lt;/code&gt; is transparent, &lt;code&gt;MemberInit&lt;/code&gt;/&lt;code&gt;Bind&lt;/code&gt; and the three-argument &lt;code&gt;New&lt;/code&gt; rebuild initializers and anonymous types, and &lt;code&gt;Convert&lt;/code&gt; is dropped where C# would reinsert it. What remains ambiguous — a &lt;code&gt;Convert&lt;/code&gt; that might be an explicit cast, a &lt;code&gt;Constant&lt;/code&gt; that might have been an arithmetic expression, a four-argument &lt;code&gt;Equal&lt;/code&gt; whose operator method is just &lt;code&gt;==&lt;/code&gt; in source — is rendered the shortest way that recompiles to the same tree.&lt;/p&gt;

&lt;p&gt;That is exactly what &lt;a href="https://delta1labs.com/products/glass" rel="noopener noreferrer"&gt;Glass.NET&lt;/a&gt; does when it meets a method that builds a tree: it shows the lambda when the shape is a compiler lowering, and the factory calls when it is not. Both views are worth having. The folded lambda tells you what the author wrote; the lowered view tells you what the query provider on the other end actually receives — the closure &lt;code&gt;Constant&lt;/code&gt; it must evaluate, the &lt;code&gt;Convert&lt;/code&gt; nodes it must translate, the &lt;code&gt;op_Equality&lt;/code&gt; it must map to a string comparison, and the &lt;code&gt;Quote&lt;/code&gt; that marks a sub-query it has to translate rather than run. Open an assembly that builds queries, pick a method that returns an &lt;code&gt;Expression&amp;lt;...&amp;gt;&lt;/code&gt;, and switch between the C# and IL views to watch a one-line lambda unfold into the tree the runtime really gets. For the full workflow, see &lt;a href="https://delta1labs.com/blog/how-to-decompile-dotnet-dll-to-csharp" rel="noopener noreferrer"&gt;how to decompile a .NET DLL to C#&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>glassnet</category>
      <category>decompilation</category>
      <category>csharp</category>
    </item>
    <item>
      <title>Obfuscating a type you serialize with Newtonsoft.Json: [JsonProperty] is the contract, and TypeNameHandling''s $type is a landmine</title>
      <dc:creator>Zero Heartbeat</dc:creator>
      <pubDate>Wed, 21 Oct 2026 23:44:27 +0000</pubDate>
      <link>https://dev.to/zero_heartbeat_06a3625d7a/obfuscating-a-type-you-serialize-with-newtonsoftjson-jsonproperty-is-the-contract-and-mki</link>
      <guid>https://dev.to/zero_heartbeat_06a3625d7a/obfuscating-a-type-you-serialize-with-newtonsoftjson-jsonproperty-is-the-contract-and-mki</guid>
      <description>&lt;p&gt;Here is a type that works perfectly until the day you obfuscate it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Order&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Customer&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;decimal&lt;/span&gt; &lt;span class="n"&gt;Total&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;DateTime&lt;/span&gt; &lt;span class="n"&gt;PlacedOn&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Serialize it with &lt;code&gt;JsonConvert.SerializeObject(order)&lt;/code&gt; and you get &lt;code&gt;{"Customer":"…","Total":…,"PlacedOn":"…"}&lt;/code&gt;. Run the assembly through a renamer and serialize again, and you get &lt;code&gt;{"a":"…","b":…,"c":"…"}&lt;/code&gt;. Nothing threw. The build was green. Every consumer of that JSON — a frontend, a sister service, a document you stored last week and read back today — is now broken, and it broke at runtime, in a place no compiler and no build step could ever see.&lt;/p&gt;

&lt;p&gt;This is the single most common way obfuscation "breaks serialization," and with &lt;strong&gt;Newtonsoft.Json&lt;/strong&gt; it bites harder than with &lt;code&gt;System.Text.Json&lt;/code&gt;, for one structural reason: Json.NET has &lt;strong&gt;no compile-time source generator&lt;/strong&gt;. &lt;code&gt;System.Text.Json&lt;/code&gt; lets you escape the reflection walk entirely with a &lt;code&gt;[JsonSerializable]&lt;/code&gt; context that freezes the names into generated code &lt;em&gt;before&lt;/em&gt; the obfuscator runs. Json.NET has no first-party equivalent — serialization always goes through a contract resolver that reflects over your live type at runtime. There is no escape hatch. So pinning the wire names with attributes is not an optimization you can skip for a hot path; it is the entire contract.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the rename reaches the wire
&lt;/h2&gt;

&lt;p&gt;Internal calls survive renaming because they are linked by &lt;em&gt;metadata token&lt;/em&gt;: the compiler emits &lt;code&gt;call&lt;/code&gt; against a token, and a renamer that rewrites the callee rewrites every token that points at it, so the graph stays connected. Json.NET's &lt;code&gt;DefaultContractResolver&lt;/code&gt; does not use a token. At serialize time it walks your type with reflection, reads each member's actual CLR &lt;code&gt;Name&lt;/code&gt;, applies the resolver's naming strategy, and writes &lt;em&gt;that string&lt;/em&gt; as the JSON key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Inside the contract resolver, conceptually:&lt;/span&gt;
&lt;span class="c1"&gt;//   property.PropertyName = namingStrategy.GetPropertyName(member.Name, hasSpecifiedName: false);&lt;/span&gt;
&lt;span class="c1"&gt;//   member.Name == "Customer"  -&amp;gt;  "Customer"   (default strategy: verbatim)&lt;/span&gt;
&lt;span class="c1"&gt;//                               -&amp;gt;  "customer"   (CamelCaseNamingStrategy)&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;settings&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;JsonSerializerSettings&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;ContractResolver&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;DefaultContractResolver&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;NamingStrategy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;CamelCaseNamingStrategy&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The JSON key &lt;code&gt;"Customer"&lt;/code&gt; exists only because the property is &lt;em&gt;named&lt;/em&gt; &lt;code&gt;Customer&lt;/code&gt;. There is no token joining the string in the file to the member in the assembly — the join &lt;strong&gt;is&lt;/strong&gt; the name. Deserialization runs the same machinery in reverse: it reads the incoming key and matches it, by string, to a member. So when the renamer turns &lt;code&gt;Customer&lt;/code&gt; into &lt;code&gt;a&lt;/code&gt;, the resolver does exactly what it is told — it reads the member now called &lt;code&gt;a&lt;/code&gt; and writes &lt;code&gt;"a"&lt;/code&gt; — and the round-trip to anyone expecting &lt;code&gt;"Customer"&lt;/code&gt; is dead. A naming strategy changes the &lt;em&gt;casing&lt;/em&gt; of that string; it does nothing to save you, because it is still deriving the key from a name the obfuscator just moved.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix is [JsonProperty], not a keep rule
&lt;/h2&gt;

&lt;p&gt;The instinct is to exclude the DTO from renaming. Don't — that surrenders exactly the type an attacker most wants to read, and it is unnecessary. The wire name should be a &lt;em&gt;constant&lt;/em&gt;, not a side effect of the member name, and &lt;code&gt;[JsonProperty]&lt;/code&gt; makes it one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Order&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;JsonProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"customer"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Customer&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;JsonProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"total"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;decimal&lt;/span&gt; &lt;span class="n"&gt;Total&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;JsonProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"placedOn"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;DateTime&lt;/span&gt; &lt;span class="n"&gt;PlacedOn&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the resolver reads the attribute's string literal — &lt;code&gt;"customer"&lt;/code&gt; — not the member name. That literal lives in the custom-attribute blob in metadata, which renaming never touches; the renamer is free to turn &lt;code&gt;Customer&lt;/code&gt; into &lt;code&gt;b&lt;/code&gt;, and the JSON key is still &lt;code&gt;"customer"&lt;/code&gt;. This is the key distinction: the member &lt;em&gt;does&lt;/em&gt; get renamed (which is the point of obfuscating), and only the string Json.NET reads is pinned. A keep rule would preserve the member name on disk — the opposite of what you want.&lt;/p&gt;

&lt;p&gt;The rule, then, is simple and total: &lt;strong&gt;every serialized member gets an explicit &lt;code&gt;[JsonProperty]&lt;/code&gt; name.&lt;/strong&gt; A single un-attributed property falls back to its CLR name and becomes the one field that breaks. If you serialize fields or opt a type into &lt;code&gt;MemberSerialization.Fields&lt;/code&gt; with &lt;code&gt;[JsonObject]&lt;/code&gt;, the same applies to every field. This is easy to enforce in review and easy to see in a diff — and unlike a naming strategy, it does not quietly depend on the member name surviving.&lt;/p&gt;

&lt;h2&gt;
  
  
  The constructor is a second name binding
&lt;/h2&gt;

&lt;p&gt;Immutable DTOs hide a second trap. When a type has no default constructor, Json.NET deserializes it by matching each JSON value to a &lt;em&gt;constructor parameter&lt;/em&gt; — &lt;strong&gt;by parameter name&lt;/strong&gt;, case-insensitively:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Order&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;JsonConstructor&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;decimal&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DateTime&lt;/span&gt; &lt;span class="n"&gt;placedOn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// param names matter&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Customer&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;Total&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;PlacedOn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;placedOn&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;JsonProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"customer"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Customer&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;JsonProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"total"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;decimal&lt;/span&gt; &lt;span class="n"&gt;Total&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;JsonProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"placedOn"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;DateTime&lt;/span&gt; &lt;span class="n"&gt;PlacedOn&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Json.NET pairs the &lt;code&gt;"customer"&lt;/code&gt; value with the constructor parameter named &lt;code&gt;customer&lt;/code&gt;. That pairing lives in parameter-name metadata — and if your obfuscator rewrites parameter names to &lt;code&gt;p0, p1, p2&lt;/code&gt;, the match fails and each property is silently left at its default. Two things keep this safe. Mark the chosen constructor with &lt;code&gt;[JsonConstructor]&lt;/code&gt; so Json.NET is unambiguous about which one to use, and ensure parameter-name renaming is off for it. Most obfuscators, Nebula included, leave parameter names alone by default — they are not a meaningful attack surface — but when a DTO deserializes through a constructor, that default is load-bearing, so confirm it rather than assume it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enums serialize by their member name too
&lt;/h2&gt;

&lt;p&gt;By default Json.NET writes enums as their &lt;em&gt;numeric&lt;/em&gt; value, which renaming never touches. But the moment you add a &lt;code&gt;StringEnumConverter&lt;/code&gt; — common, because a string is more readable and more stable on the wire — the enum is written by its &lt;em&gt;member&lt;/em&gt; name, and that is a renameable identifier again:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;JsonConverter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;StringEnumConverter&lt;/span&gt;&lt;span class="p"&gt;))]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;enum&lt;/span&gt; &lt;span class="n"&gt;Priority&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;EnumMember&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"low"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;  &lt;span class="n"&gt;Low&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;EnumMember&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"high"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="n"&gt;High&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Json.NET honours &lt;code&gt;[EnumMember(Value = …)]&lt;/code&gt; on enum members, so pin each one. Now &lt;code&gt;Priority.High&lt;/code&gt; serializes as &lt;code&gt;"high"&lt;/code&gt; whether or not the field &lt;code&gt;High&lt;/code&gt; has been renamed, because the converter reads the attribute's literal rather than the member name.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bigger trap: TypeNameHandling writes your type's name into the document
&lt;/h2&gt;

&lt;p&gt;Everything above is a property or a value. &lt;code&gt;TypeNameHandling&lt;/code&gt; is different: it writes your &lt;em&gt;type's identity&lt;/em&gt; into the document. When you enable it to round-trip polymorphic graphs — a base &lt;code&gt;Payment&lt;/code&gt; with &lt;code&gt;CreditCardPayment&lt;/code&gt; and &lt;code&gt;BankTransferPayment&lt;/code&gt; subclasses, an &lt;code&gt;object&lt;/code&gt;-typed field, a &lt;code&gt;List&amp;lt;IMessage&amp;gt;&lt;/code&gt; — Json.NET embeds a &lt;code&gt;$type&lt;/code&gt; discriminator so it can reconstruct the exact runtime type on the way back:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;settings&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;JsonSerializerSettings&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;TypeNameHandling&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TypeNameHandling&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Auto&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;JsonConvert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SerializeObject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// {&lt;/span&gt;
&lt;span class="c1"&gt;//   "$type": "MyApp.Payments.CreditCardPayment, MyApp",&lt;/span&gt;
&lt;span class="c1"&gt;//   "last4": "4242", "amount": 42.50&lt;/span&gt;
&lt;span class="c1"&gt;// }&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look at the value of &lt;code&gt;$type&lt;/code&gt;: it is the type's &lt;strong&gt;assembly-qualified name&lt;/strong&gt; — namespace, type name, assembly — assembled from the exact identifiers an obfuscator renames. Rename &lt;code&gt;CreditCardPayment&lt;/code&gt; to &lt;code&gt;a&lt;/code&gt; and flatten the namespace, and the same object now serializes as &lt;code&gt;"$type": "a, MyApp"&lt;/code&gt;. Your &lt;code&gt;[JsonProperty]&lt;/code&gt; attributes do nothing here, because &lt;code&gt;$type&lt;/code&gt; is not a property of your type; it is the type's name, and no property attribute pins a type name.&lt;/p&gt;

&lt;p&gt;That makes &lt;code&gt;$type&lt;/code&gt; far more dangerous than a property key, because it breaks &lt;em&gt;across builds and across versions&lt;/em&gt;, not just within one. A document written by an un-obfuscated build, or by last month's release, carries &lt;code&gt;"…CreditCardPayment, MyApp"&lt;/code&gt;. Feed it to your obfuscated build — whose type is now called &lt;code&gt;a&lt;/code&gt; — and Json.NET cannot resolve the name and throws on deserialize. Anything that persists polymorphic JSON (a stored document, a queued message, a cached payload, a saved project file) is exposed the instant the type names on the two sides disagree.&lt;/p&gt;

&lt;p&gt;The fix is neither an attribute nor a keep rule. It is a custom &lt;code&gt;ISerializationBinder&lt;/code&gt; that maps a &lt;strong&gt;stable logical name you choose&lt;/strong&gt; to the &lt;code&gt;Type&lt;/code&gt;, so the wire carries a constant you own while the type underneath renames freely:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;StableTypeBinder&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ISerializationBinder&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// The wire names are string literals; the Type values rename consistently with your code.&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;Dictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ByName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"creditCardPayment"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CreditCardPayment&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"bankTransferPayment"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BankTransferPayment&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;Dictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ByType&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
        &lt;span class="n"&gt;ByName&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToDictionary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kv&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;kv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;kv&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;kv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;BindToName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Type&lt;/span&gt; &lt;span class="n"&gt;serializedType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;assemblyName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;typeName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;assemblyName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                 &lt;span class="c1"&gt;// keep the assembly out of the wire entirely&lt;/span&gt;
        &lt;span class="n"&gt;typeName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ByType&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;serializedType&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;   &lt;span class="c1"&gt;// -&amp;gt; "creditCardPayment", a constant&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Type&lt;/span&gt; &lt;span class="nf"&gt;BindToType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;assemblyName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;typeName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ByName&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;typeName&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;settings&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;JsonSerializerSettings&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;TypeNameHandling&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TypeNameHandling&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Auto&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;SerializationBinder&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;StableTypeBinder&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="c1"&gt;// {"$type":"creditCardPayment","last4":"4242","amount":42.50}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The dictionary keys — &lt;code&gt;"creditCardPayment"&lt;/code&gt; — are string literals the obfuscator cannot touch. The dictionary &lt;em&gt;values&lt;/em&gt; are &lt;code&gt;typeof(CreditCardPayment)&lt;/code&gt;, which Json.NET and the renamer both resolve through the metadata token, so they rename consistently with the rest of your code. The result is a discriminator that is stable across every build and every version, no matter how aggressively the types are renamed, and as a bonus it keeps your namespace layout out of the document. (It is also the recommended posture for &lt;code&gt;TypeNameHandling&lt;/code&gt; on security grounds: a binder that only resolves a fixed allow-list of types refuses to instantiate anything else, which is the mitigation for the deserialization-gadget risk &lt;code&gt;TypeNameHandling&lt;/code&gt; otherwise carries.)&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Prove it with a round-trip on the protected build
&lt;/h2&gt;

&lt;p&gt;Whichever path you pick, the test that matters runs against the &lt;em&gt;obfuscated&lt;/em&gt; output, not the debug build — the whole failure mode is that the names only move after Nebula runs. A round-trip against the protected assembly is the one assertion that catches it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Fact&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Protected_build_round_trips_the_wire_contract&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;settings&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;JsonSerializerSettings&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;TypeNameHandling&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TypeNameHandling&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Auto&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;SerializationBinder&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;StableTypeBinder&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="n"&gt;Payment&lt;/span&gt; &lt;span class="n"&gt;payment&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;CreditCardPayment&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;Last4&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"4242"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Amount&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;42.50m&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;JsonConvert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SerializeObject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// The contract, asserted literally — these are what consumers and stored docs depend on.&lt;/span&gt;
    &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"\"$type\":\"creditCardPayment\""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"\"last4\":\"4242\""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;DoesNotContain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"\"a\""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;                 &lt;span class="c1"&gt;// no leaked renamed member or type&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;back&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CreditCardPayment&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;JsonConvert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DeserializeObject&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Payment&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;)!;&lt;/span&gt;
    &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"4242"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;back&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Last4&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;                     &lt;span class="c1"&gt;// constructor/members matched by pinned names&lt;/span&gt;
    &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;42.50m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;back&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Amount&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wire this against the post-obfuscation artifact in CI and the "serialization broke" incident becomes a failed test instead of a customer email. The principle underneath it is the one worth carrying to every reflection-driven framework you obfuscate: &lt;strong&gt;a name a framework reads at runtime is a contract, and a contract belongs in a string literal the obfuscator cannot move — not in a member name, a type name, or a namespace it is paid to.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://delta1labs.com/products/nebula/" rel="noopener noreferrer"&gt;Nebula.NET&lt;/a&gt; renames aggressively by design; &lt;code&gt;[JsonProperty]&lt;/code&gt;, &lt;code&gt;[EnumMember]&lt;/code&gt;, and a stable &lt;code&gt;ISerializationBinder&lt;/code&gt; are how you keep the handful of names that have to stay while losing the ones that should go. For the &lt;code&gt;System.Text.Json&lt;/code&gt; side of the same story, see &lt;a href="https://delta1labs.com/blog/obfuscate-system-text-json-jsonpropertyname-contract/" rel="noopener noreferrer"&gt;obfuscating a type you serialize with System.Text.Json&lt;/a&gt;; for the broader pattern, &lt;a href="https://delta1labs.com/blog/obfuscation-reflection-serialization-pitfalls/" rel="noopener noreferrer"&gt;what obfuscation breaks&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>nebulanet</category>
      <category>obfuscation</category>
      <category>dotnet</category>
      <category>deepdive</category>
    </item>
    <item>
      <title>Decompiling C# string interpolation and interpolated string handlers</title>
      <dc:creator>Zero Heartbeat</dc:creator>
      <pubDate>Wed, 21 Oct 2026 13:09:20 +0000</pubDate>
      <link>https://dev.to/zero_heartbeat_06a3625d7a/decompiling-c-string-interpolation-and-interpolated-string-handlers-3fmi</link>
      <guid>https://dev.to/zero_heartbeat_06a3625d7a/decompiling-c-string-interpolation-and-interpolated-string-handlers-3fmi</guid>
      <description>&lt;p&gt;An interpolated string is one of the most innocent-looking pieces of C# you can write — &lt;code&gt;$"Hello {name}, you have {count} messages"&lt;/code&gt; — and one of the least predictable once it reaches IL. There is no interpolation instruction. The compiler rewrites every &lt;code&gt;$"..."&lt;/code&gt; into ordinary method calls, and it has &lt;em&gt;three&lt;/em&gt; completely different ways of doing so: a &lt;code&gt;string.Concat&lt;/code&gt;, a &lt;code&gt;string.Format&lt;/code&gt; with a positional template, or — on modern targets — a construct-and-append sequence against &lt;code&gt;DefaultInterpolatedStringHandler&lt;/code&gt;. Which one it picks depends on your target framework, the types in the holes, and whether the receiving method opted into a handler of its own. Walking each lowering explains what a decompiler such as &lt;a href="https://delta1labs.com/products/glass" rel="noopener noreferrer"&gt;Glass.NET&lt;/a&gt; can fold back into &lt;code&gt;$"..."&lt;/code&gt;, and where it can only show you the machinery the compiler actually emitted.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three lowerings, one syntax
&lt;/h2&gt;

&lt;p&gt;The single most useful fact about &lt;code&gt;$"..."&lt;/code&gt; is that the syntax tells you almost nothing about the IL. The compiler looks at &lt;em&gt;where the string is going&lt;/em&gt; and &lt;em&gt;what is in the holes&lt;/em&gt; and chooses:&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;The rest of this post is those four boxes, in order, with the IL each one produces and the fingerprint a decompiler uses to recognize it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The concat shortcut
&lt;/h2&gt;

&lt;p&gt;If every hole in the string is &lt;em&gt;already&lt;/em&gt; a &lt;code&gt;string&lt;/code&gt;, and no hole has an alignment or format specifier, the compiler does the cheapest possible thing: it calls &lt;code&gt;String.Concat&lt;/code&gt;. Given&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nf"&gt;Greet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;first&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;last&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s"&gt;$"&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;first&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;last&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;the body is simply a three-argument concat — the literal space is just another operand:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ldarg.1              // first
ldstr      " "
ldarg.2              // last
call       string System.String::Concat(string, string, string)
ret
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is byte-for-byte identical to what &lt;code&gt;first + " " + last&lt;/code&gt; compiles to, and identical to a hand-written &lt;code&gt;string.Concat(first, " ", last)&lt;/code&gt;. There is no marker that says "this was interpolation." A decompiler is free to render it as &lt;code&gt;$"{first} {last}"&lt;/code&gt;, as &lt;code&gt;first + " " + last&lt;/code&gt;, or as the explicit concat — all three are faithful readings of the same IL. The interpolation, as a distinct idea, did not survive compilation.&lt;/p&gt;

&lt;h2&gt;
  
  
  The format template
&lt;/h2&gt;

&lt;p&gt;The moment a hole is &lt;em&gt;not&lt;/em&gt; a string — a number, a &lt;code&gt;DateTime&lt;/code&gt;, anything that needs formatting — the classic lowering is &lt;code&gt;String.Format&lt;/code&gt; with a positional template. This is what you get on &lt;code&gt;netstandard2.0&lt;/code&gt;, on .NET Framework, and on any target that predates the handler type. For&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nf"&gt;Status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;rate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s"&gt;$"&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt; items at &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;rate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;P1&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;the compiler builds the template string, boxes the value-type arguments into an &lt;code&gt;object[]&lt;/code&gt;, and calls &lt;code&gt;Format&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;ldstr&lt;/span&gt;      &lt;span class="s"&gt;"{0} items at {1:P1}"&lt;/span&gt;
&lt;span class="n"&gt;ldc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;i4&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;
&lt;span class="n"&gt;newarr&lt;/span&gt;     &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;System&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Runtime&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="n"&gt;System&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Object&lt;/span&gt;
&lt;span class="n"&gt;dup&lt;/span&gt;
&lt;span class="n"&gt;ldc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;i4&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;
&lt;span class="n"&gt;ldarg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;              &lt;span class="c1"&gt;// count&lt;/span&gt;
&lt;span class="n"&gt;box&lt;/span&gt;        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;System&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Runtime&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="n"&gt;System&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Int32&lt;/span&gt;      &lt;span class="c1"&gt;// int -&amp;gt; object&lt;/span&gt;
&lt;span class="n"&gt;stelem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;ref&lt;/span&gt;
&lt;span class="n"&gt;dup&lt;/span&gt;
&lt;span class="n"&gt;ldc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;i4&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;
&lt;span class="n"&gt;ldarg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;              &lt;span class="c1"&gt;// rate&lt;/span&gt;
&lt;span class="n"&gt;box&lt;/span&gt;        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;System&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Runtime&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="n"&gt;System&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Double&lt;/span&gt;     &lt;span class="c1"&gt;// double -&amp;gt; object&lt;/span&gt;
&lt;span class="n"&gt;stelem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;ref&lt;/span&gt;
&lt;span class="n"&gt;call&lt;/span&gt;       &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;System&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;Format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;object&lt;/span&gt;&lt;span class="p"&gt;[])&lt;/span&gt;
&lt;span class="n"&gt;ret&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things are worth noticing here, because they are exactly what you see in the decompiled output. First, your format specifier &lt;code&gt;:P1&lt;/code&gt; is preserved verbatim inside the template as &lt;code&gt;{1:P1}&lt;/code&gt; — the decompiler can read it straight out of the &lt;code&gt;ldstr&lt;/code&gt;. Second, the value types are &lt;em&gt;boxed&lt;/em&gt;; the &lt;code&gt;object[]&lt;/code&gt; is the allocation cost this lowering carries, and it is the reason the newer handler form exists.&lt;/p&gt;

&lt;p&gt;Because this is a literal &lt;code&gt;String.Format&lt;/code&gt; call, it is indistinguishable from one you wrote by hand. A decompiler will show one of the two. Many print the &lt;code&gt;string.Format("{0} items at {1:P1}", count, rate)&lt;/code&gt; call as-is, because it is the lower-level, more portable reading; a C#-aware decompiler may re-sugar it to &lt;code&gt;$"{count} items at {rate:P1}"&lt;/code&gt; by parsing the template and mapping &lt;code&gt;{0}&lt;/code&gt;,&lt;code&gt;{1}&lt;/code&gt; back onto the argument order. Both are correct.&lt;/p&gt;

&lt;h2&gt;
  
  
  The handler dance
&lt;/h2&gt;

&lt;p&gt;On .NET 6 and later, a plain assignment of an interpolated string to a &lt;code&gt;string&lt;/code&gt; no longer goes through &lt;code&gt;String.Format&lt;/code&gt;. The compiler lowers it against &lt;a href="https://delta1labs.com/products/glass" rel="noopener noreferrer"&gt;&lt;code&gt;DefaultInterpolatedStringHandler&lt;/code&gt;&lt;/a&gt;, a &lt;code&gt;ref struct&lt;/code&gt; in &lt;code&gt;System.Runtime.CompilerServices&lt;/code&gt; that accumulates the result into an internal buffer — no template parsing, no &lt;code&gt;object[]&lt;/code&gt;, no boxing of the arguments into the array. The same &lt;code&gt;Status&lt;/code&gt; method on a modern target decompiles to this shape:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nf"&gt;Status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;rate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;DefaultInterpolatedStringHandler&lt;/span&gt; &lt;span class="n"&gt;handler&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
        &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;DefaultInterpolatedStringHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// literalLength=9, formattedCount=2&lt;/span&gt;
    &lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AppendFormatted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AppendLiteral&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;" items at "&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AppendFormatted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"P1"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToStringAndClear&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read the constructor arguments, because they are the first half of the fingerprint. The handler is created with &lt;code&gt;(int literalLength, int formattedCount)&lt;/code&gt; — the total number of literal characters (&lt;code&gt;" items at "&lt;/code&gt; is nine, plus any other literal runs) and the number of holes. The handler uses those to size its buffer up front. Then the body is a strict interleaving: one &lt;code&gt;AppendLiteral&lt;/code&gt; per run of literal text, one &lt;code&gt;AppendFormatted&lt;/code&gt; per hole, in source order, finished by exactly one &lt;code&gt;ToStringAndClear()&lt;/code&gt; that produces the &lt;code&gt;string&lt;/code&gt; and returns the buffer. In IL it is a sequence of &lt;code&gt;call&lt;/code&gt;/&lt;code&gt;callvirt&lt;/code&gt; against a local of the handler type:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ldloca.s   handler
ldc.i4.s   9
ldc.i4.2
call       instance void DefaultInterpolatedStringHandler::.ctor(int32, int32)
ldloca.s   handler
ldarg.1
call       instance void DefaultInterpolatedStringHandler::AppendFormatted&amp;lt;int32&amp;gt;(!!0)
ldloca.s   handler
ldstr      " items at "
call       instance void DefaultInterpolatedStringHandler::AppendLiteral(string)
...
ldloca.s   handler
call       instance string DefaultInterpolatedStringHandler::ToStringAndClear()
ret
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That literal-length-and-count constructor, the alternating &lt;code&gt;AppendLiteral&lt;/code&gt;/&lt;code&gt;AppendFormatted&amp;lt;T&amp;gt;&lt;/code&gt; calls on a single handler local, and the terminating &lt;code&gt;ToStringAndClear&lt;/code&gt; are a shape nothing else produces. A modern decompiler recognizes it and folds the whole block back to &lt;code&gt;$"{count} items at {rate:P1}"&lt;/code&gt;. A decompiler that does not — or that is reading output where the optimizer hoisted or reordered some of the appends — shows the handler calls literally. That is not a failure: it is the honest reading, and it is often &lt;em&gt;more&lt;/em&gt; informative than the sugar, because it shows you exactly where the buffer allocation and each &lt;code&gt;AppendFormatted&amp;lt;T&amp;gt;&lt;/code&gt; generic instantiation happen.&lt;/p&gt;

&lt;h2&gt;
  
  
  Alignment, format, and the culture trap
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;AppendFormatted&lt;/code&gt; family has several overloads, and which one the compiler selects tells you what was in the braces. A bare hole &lt;code&gt;{x}&lt;/code&gt; becomes &lt;code&gt;AppendFormatted&amp;lt;T&amp;gt;(T value)&lt;/code&gt;. A format specifier &lt;code&gt;{x:N2}&lt;/code&gt; becomes &lt;code&gt;AppendFormatted&amp;lt;T&amp;gt;(T value, string? format)&lt;/code&gt; with the format as a literal string argument. An alignment &lt;code&gt;{x,10}&lt;/code&gt; becomes &lt;code&gt;AppendFormatted&amp;lt;T&amp;gt;(T value, int alignment)&lt;/code&gt;, and both together &lt;code&gt;{x,10:N2}&lt;/code&gt; becomes &lt;code&gt;AppendFormatted&amp;lt;T&amp;gt;(T value, int alignment, string? format)&lt;/code&gt;. So you can reconstruct the exact brace contents from the overload and its constant arguments — the format text is right there in the call.&lt;/p&gt;

&lt;p&gt;There is one behavioral subtlety that the decompiled view makes visible and that bites people who refactor between the forms. &lt;code&gt;DefaultInterpolatedStringHandler&lt;/code&gt; formats using the &lt;em&gt;current culture&lt;/em&gt; by default, exactly like &lt;code&gt;string.Format&lt;/code&gt;. If you need invariant or a specific culture, the interpolated string must be passed to an API that threads an &lt;code&gt;IFormatProvider&lt;/code&gt; into the handler's constructor — the compiler then emits a constructor overload taking the provider. If you see a &lt;code&gt;DefaultInterpolatedStringHandler&lt;/code&gt; constructed &lt;em&gt;without&lt;/em&gt; a provider argument, the result is culture-sensitive; that is a real semantic detail hiding in the lowered IL, not noise.&lt;/p&gt;

&lt;h2&gt;
  
  
  Custom handlers: when the string is never built
&lt;/h2&gt;

&lt;p&gt;The handler pattern is not limited to building a &lt;code&gt;string&lt;/code&gt;. Any method can accept an interpolated string directly by taking a parameter whose type is marked &lt;code&gt;[InterpolatedStringHandler]&lt;/code&gt;. The compiler then constructs &lt;em&gt;that&lt;/em&gt; handler and appends into it, so the interpolated string is formatted straight into the destination with no intermediate &lt;code&gt;string&lt;/code&gt; at all. The clearest example is &lt;code&gt;StringBuilder.Append&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;sb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"id=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt; name=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;does not build a string and append it. It lowers to the construction of a &lt;code&gt;StringBuilder.AppendInterpolatedStringHandler&lt;/code&gt; that writes directly into &lt;code&gt;sb&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;handler&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;StringBuilder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AppendInterpolatedStringHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sb&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// note: sb passed in&lt;/span&gt;
&lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AppendLiteral&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"id="&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AppendFormatted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AppendLiteral&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;" name="&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AppendFormatted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;sb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ref&lt;/span&gt; &lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The giveaway in the decompiled code is that the handler's constructor takes a &lt;em&gt;third&lt;/em&gt; argument — the &lt;code&gt;StringBuilder&lt;/code&gt; itself — and the final call is &lt;code&gt;Append(ref handler)&lt;/code&gt; rather than &lt;code&gt;Append(string)&lt;/code&gt;. That extra constructor argument comes from an &lt;code&gt;[InterpolatedStringHandlerArgument("sb")]&lt;/code&gt; attribute on the parameter, which tells the compiler to forward the receiver into the handler.&lt;/p&gt;

&lt;p&gt;The most striking case is a handler that can decide &lt;em&gt;not to build the string at all&lt;/em&gt;. &lt;code&gt;Debug.Assert(condition, $"expensive {Describe()} message")&lt;/code&gt; must not pay for the message when the assertion passes. Its handler constructor has a trailing &lt;code&gt;out bool&lt;/code&gt; parameter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;handler&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;AssertInterpolatedStringHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;17&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;condition&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;shouldAppend&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;shouldAppend&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AppendLiteral&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"expensive "&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AppendFormatted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;Describe&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AppendLiteral&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;" message"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;Debug&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;condition&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;ref&lt;/span&gt; &lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When &lt;code&gt;condition&lt;/code&gt; is true the constructor sets &lt;code&gt;shouldAppend = false&lt;/code&gt;, the whole &lt;code&gt;if&lt;/code&gt; block is skipped, and &lt;code&gt;Describe()&lt;/code&gt; is never called. This is the conditional-evaluation behavior people rely on without realizing it, and it is &lt;em&gt;only&lt;/em&gt; visible in the lowered form — there is no way to express "skip the interpolation" in the &lt;code&gt;$"..."&lt;/code&gt; syntax itself. A decompiler that folds this back to &lt;code&gt;Debug.Assert(condition, $"...")&lt;/code&gt; is correct about the result but is hiding the &lt;code&gt;shouldAppend&lt;/code&gt; guard; switching to the IL or lowered-C# view in &lt;a href="https://delta1labs.com/products/glass" rel="noopener noreferrer"&gt;Glass.NET&lt;/a&gt; is how you confirm the message really is skipped on the hot path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Constant interpolation disappears entirely
&lt;/h2&gt;

&lt;p&gt;The last case is the one with nothing to recover. If every part of an interpolated string is a compile-time constant, the whole thing is folded at compile time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"2"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Banner&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;$"Acme v&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;Version&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// becomes the constant "Acme v2"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Banner&lt;/code&gt; is emitted as the literal string &lt;code&gt;"Acme v2"&lt;/code&gt;. There is no handler, no &lt;code&gt;Format&lt;/code&gt;, no &lt;code&gt;Concat&lt;/code&gt; — the metadata holds a constant, and any reference to &lt;code&gt;Banner&lt;/code&gt; is just &lt;code&gt;ldstr "Acme v2"&lt;/code&gt;. A decompiler shows a plain string constant, because that is all that exists. The same is true of a no-hole interpolated string like &lt;code&gt;$"ready"&lt;/code&gt;, which is just the literal &lt;code&gt;"ready"&lt;/code&gt;. Interpolation that the compiler could resolve statically leaves no trace of having been interpolation.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to take from the decompiled view
&lt;/h2&gt;

&lt;p&gt;String interpolation is the cleanest illustration of a theme that runs through this whole series: the surface syntax is a &lt;em&gt;request&lt;/em&gt;, and the compiler answers it differently depending on context. The same &lt;code&gt;$"..."&lt;/code&gt; can be a &lt;code&gt;String.Concat&lt;/code&gt;, a &lt;code&gt;String.Format&lt;/code&gt; with a boxed &lt;code&gt;object[]&lt;/code&gt;, a &lt;code&gt;DefaultInterpolatedStringHandler&lt;/code&gt; sequence, a direct append into a &lt;code&gt;StringBuilder&lt;/code&gt;, or — when the inputs are constant — nothing but a baked-in literal. Only some of those carry a fingerprint strong enough to rebuild the &lt;code&gt;$"..."&lt;/code&gt;: the handler dance and the custom-handler forms are recognizable; the concat and format forms are indistinguishable from hand-written calls; the constant form is already gone. A good decompiler folds back what it can prove and shows the lowered calls where it cannot — and the lowered calls are often the point, because that is where you see the boxing, the buffer allocation, the culture dependence, and the &lt;code&gt;shouldAppend&lt;/code&gt; guard that the sugar quietly hides. Open the assembly in &lt;a href="https://delta1labs.com/products/glass" rel="noopener noreferrer"&gt;Glass.NET&lt;/a&gt;, switch a method to IL, and watch a one-line &lt;code&gt;$"..."&lt;/code&gt; unfold into the calls the runtime actually makes. For the full workflow, see &lt;a href="https://delta1labs.com/blog/how-to-decompile-dotnet-dll-to-csharp" rel="noopener noreferrer"&gt;how to decompile a .NET DLL to C#&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>glassnet</category>
      <category>decompilation</category>
      <category>csharp</category>
    </item>
    <item>
      <title>Obfuscating code that uses dynamic: the runtime binder looks members up by name, so a renamed property is a RuntimeBinderException</title>
      <dc:creator>Zero Heartbeat</dc:creator>
      <pubDate>Fri, 16 Oct 2026 20:11:12 +0000</pubDate>
      <link>https://dev.to/zero_heartbeat_06a3625d7a/obfuscating-code-that-uses-dynamic-the-runtime-binder-looks-members-up-by-name-so-a-renamed-3h92</link>
      <guid>https://dev.to/zero_heartbeat_06a3625d7a/obfuscating-code-that-uses-dynamic-the-runtime-binder-looks-members-up-by-name-so-a-renamed-3h92</guid>
      <description>&lt;p&gt;Here is a method that works on every build except the one you ship:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Settings&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;    &lt;span class="n"&gt;Timeout&lt;/span&gt;  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Endpoint&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"https://api.example"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Configure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;dynamic&lt;/span&gt; &lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Timeout&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;          &lt;span class="c1"&gt;// compiles; passes tests; dies when obfuscated&lt;/span&gt;
    &lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Endpoint&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"https://api.example/v2"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the debug build, &lt;code&gt;Configure(new Settings())&lt;/code&gt; does what it says. On the obfuscated build it throws:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Microsoft.CSharp.RuntimeBinder.RuntimeBinderException:
  'a' does not contain a definition for 'Timeout'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing is wrong with the obfuscator. The type was renamed to &lt;code&gt;a&lt;/code&gt;, its property was renamed, and the &lt;code&gt;dynamic&lt;/code&gt; site — which never knew about either — kept asking for &lt;code&gt;Timeout&lt;/code&gt; by name. To see why that is unavoidable, and what to do about it, read the IL the compiler emitted for that one line.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the name lives
&lt;/h2&gt;

&lt;p&gt;A normal property read, &lt;code&gt;int timeout = settings.Timeout;&lt;/code&gt; with &lt;code&gt;settings&lt;/code&gt; typed as &lt;code&gt;Settings&lt;/code&gt;, compiles to a &lt;code&gt;callvirt&lt;/code&gt; against a metadata token:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight llvm"&gt;&lt;code&gt;&lt;span class="err"&gt;callvirt&lt;/span&gt; &lt;span class="err"&gt;instance&lt;/span&gt; &lt;span class="err"&gt;int32&lt;/span&gt; &lt;span class="nl"&gt;Acme.Settings:&lt;/span&gt;&lt;span class="err"&gt;:get_Timeout&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The token points at a row in the method table. When Nebula renames &lt;code&gt;get_Timeout&lt;/code&gt; to &lt;code&gt;a&lt;/code&gt;, it rewrites the row and every token that references it — the call site and the member move together, and the string &lt;code&gt;Timeout&lt;/code&gt; ceases to exist anywhere in the assembly. That is why renaming is invisible to ordinary code.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;dynamic&lt;/code&gt; version compiles to something else entirely. The compiler generates a hidden nested class to hold a cached call site, and the site is initialised with a &lt;strong&gt;binder&lt;/strong&gt; that is handed the member name as a string:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// hidden container the compiler generates for Configure
.class nested private '&amp;lt;&amp;gt;o__0'
{
  .field public static class [System.Runtime]System.Runtime.CompilerServices.CallSite`1&amp;lt;
      class [System.Runtime]System.Func`3&amp;lt;CallSite, object, object&amp;gt;&amp;gt; '&amp;lt;&amp;gt;p__0'
}

// first use of the site: build the binder, with the member NAME as data
IL_0007: ldc.i4.0                                       // CSharpBinderFlags.None
IL_0008: ldstr      "Timeout"                           // &amp;lt;-- the binding, as a string
IL_000d: ldtoken    Acme.Program                        // calling context
IL_0012: call       class Type Type::GetTypeFromHandle(RuntimeTypeHandle)
IL_0017: ldc.i4.1
IL_0018: newarr     CSharpArgumentInfo
...
IL_002a: call       CallSiteBinder Binder::GetMember(CSharpBinderFlags, string, Type, IEnumerable`1&amp;lt;CSharpArgumentInfo&amp;gt;)
IL_002f: call       CallSite`1&amp;lt;...&amp;gt; CallSite`1&amp;lt;...&amp;gt;::Create(CallSiteBinder)
IL_0034: stsfld     ... '&amp;lt;&amp;gt;o__0'::'&amp;lt;&amp;gt;p__0'

// every use: invoke the site's delegate with the receiver
IL_0039: ldsfld     '&amp;lt;&amp;gt;o__0'::'&amp;lt;&amp;gt;p__0'
IL_003e: ldfld      !0 CallSite`1&amp;lt;...&amp;gt;::Target
IL_0043: ldsfld     '&amp;lt;&amp;gt;o__0'::'&amp;lt;&amp;gt;p__0'
IL_0048: ldarg.0                                        // settings (object)
IL_0049: callvirt   instance !2 Func`3&amp;lt;CallSite, object, object&amp;gt;::Invoke(!0, !1)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is no token for &lt;code&gt;Timeout&lt;/code&gt; anywhere in that sequence. The only mention of the member is &lt;code&gt;ldstr "Timeout"&lt;/code&gt;, a string literal passed to &lt;code&gt;Binder.GetMember&lt;/code&gt;. At runtime, the C# runtime binder takes the &lt;em&gt;actual&lt;/em&gt; type of the object that arrives in &lt;code&gt;settings&lt;/code&gt;, searches it for a member called &lt;code&gt;Timeout&lt;/code&gt; — the same search reflection does — builds a rule for that type, caches it in the site, and executes it. The assignment compiles the same way through &lt;code&gt;Binder.SetMember(..., "Endpoint", ...)&lt;/code&gt;; a method call through &lt;code&gt;Binder.InvokeMember(..., "Save", ...)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;After renaming, the type has a member &lt;code&gt;a&lt;/code&gt; and the site still says &lt;code&gt;"Timeout"&lt;/code&gt;. The binder searches, finds nothing, and throws. The link was a name, and the renamer, which only moves tokens, had no reason to know.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why string encryption is beside the point
&lt;/h2&gt;

&lt;p&gt;The reflex, on seeing &lt;code&gt;ldstr "Timeout"&lt;/code&gt;, is to think Nebula's string encryption will take care of it. It will encrypt it — and the decrypted value handed to &lt;code&gt;Binder.GetMember&lt;/code&gt; at runtime is the same &lt;code&gt;"Timeout"&lt;/code&gt;, which still does not exist on the renamed type. Encryption changes nothing about the binding.&lt;/p&gt;

&lt;p&gt;What it changes is the &lt;em&gt;leak&lt;/em&gt;. In an unencrypted build, every dynamic site is a plain-text signpost in the user-string heap: a reader opening the assembly sees &lt;code&gt;"Timeout"&lt;/code&gt; passed to a binder right beside a type whose members are now &lt;code&gt;a&lt;/code&gt;, &lt;code&gt;b&lt;/code&gt;, &lt;code&gt;c&lt;/code&gt;, and has recovered one original name for free. This is the quieter cost of &lt;code&gt;dynamic&lt;/code&gt; in a protected assembly — where it does not break, it still narrates. With &lt;code&gt;encryptStrings&lt;/code&gt; on, the signpost is gone from the static view; the lookup is exactly as broken as before.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which operations bind by name
&lt;/h2&gt;

&lt;p&gt;Not every &lt;code&gt;dynamic&lt;/code&gt; operation is a name lookup, and knowing which are tells you where to look:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Member access, assignment, invocation&lt;/strong&gt; — &lt;code&gt;s.Timeout&lt;/code&gt;, &lt;code&gt;s.Endpoint = …&lt;/code&gt;, &lt;code&gt;s.Save()&lt;/code&gt; — go through &lt;code&gt;GetMember&lt;/code&gt;, &lt;code&gt;SetMember&lt;/code&gt;, &lt;code&gt;InvokeMember&lt;/code&gt; with the member name as a string. These break when the member is renamed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Indexing&lt;/strong&gt; — &lt;code&gt;s[0]&lt;/code&gt; — goes through &lt;code&gt;GetIndex&lt;/code&gt;/&lt;code&gt;SetIndex&lt;/code&gt;, which resolves the type's &lt;em&gt;default member&lt;/em&gt; (&lt;code&gt;[DefaultMember("Item")]&lt;/code&gt;, the indexer's &lt;code&gt;Item&lt;/code&gt; property). Rename the indexer and the dynamic index breaks the same way.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Operators and conversions&lt;/strong&gt; — &lt;code&gt;a + b&lt;/code&gt;, &lt;code&gt;(int)d&lt;/code&gt; — go through &lt;code&gt;BinaryOperation&lt;/code&gt;/&lt;code&gt;Convert&lt;/code&gt;, which resolve user-defined operators by their special names, &lt;code&gt;op_Addition&lt;/code&gt;, &lt;code&gt;op_Implicit&lt;/code&gt;. Those are &lt;code&gt;specialname&lt;/code&gt; members the runtime itself relies on, and Nebula does not rename them, so dynamic arithmetic over your types keeps working.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Invocation of a delegate value&lt;/strong&gt; — &lt;code&gt;d(x)&lt;/code&gt; where &lt;code&gt;d&lt;/code&gt; holds a delegate — goes through &lt;code&gt;Invoke&lt;/code&gt;, no name, no problem.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ExpandoObject&lt;/code&gt; and any &lt;code&gt;IDynamicMetaObjectProvider&lt;/code&gt;&lt;/strong&gt; — the "members" are dictionary keys supplied at runtime, not CLR members. Renaming never touches data, so &lt;code&gt;expando.Timeout&lt;/code&gt; keeps working regardless. The same is true of a &lt;code&gt;dynamic&lt;/code&gt; over a COM &lt;code&gt;IDispatch&lt;/code&gt; proxy: the names are resolved by the COM server, not by your metadata.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The dangerous case is the first bullet applied to &lt;strong&gt;types you own&lt;/strong&gt;, and there is one pattern that hides it particularly well.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern that breaks a whole visitor at once
&lt;/h2&gt;

&lt;p&gt;Double dispatch by &lt;code&gt;dynamic&lt;/code&gt; is a popular shortcut for a visitor without the &lt;code&gt;Accept&lt;/code&gt; boilerplate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;abstract&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Shape&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Circle&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Shape&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;R&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;init&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Rect&lt;/span&gt;   &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Shape&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;W&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;init&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;H&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;init&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Area&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="nf"&gt;Of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Shape&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;Compute&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="kt"&gt;dynamic&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;    &lt;span class="c1"&gt;// picks the overload at runtime&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="nf"&gt;Compute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Circle&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PI&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;R&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;R&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="nf"&gt;Compute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Rect&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;W&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;H&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This compiles to &lt;code&gt;Binder.InvokeMember(..., "Compute", typeof(Area), ...)&lt;/code&gt; with the runtime binder selecting the overload by the argument's actual type. Rename &lt;code&gt;Compute&lt;/code&gt; to &lt;code&gt;b&lt;/code&gt; and &lt;em&gt;every&lt;/em&gt; shape fails on the first call — not one property, the entire dispatch — with &lt;code&gt;'Area' does not contain a definition for 'Compute'&lt;/code&gt;. Because the overloads are &lt;code&gt;private&lt;/code&gt;, nothing else in the assembly references them by anything but this one string, so there is no compile-time trace that they are reached at all. The unprotected test suite is green; the protected build cannot compute a single area.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fixes, in order of preference
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Replace &lt;code&gt;dynamic&lt;/code&gt; with an interface or a virtual.&lt;/strong&gt; This is the right answer far more often than it feels. Duck typing between types you own is what interfaces are for, and the interface call is a token-bound &lt;code&gt;callvirt&lt;/code&gt; that survives renaming, runs without a binder, and is checked by the compiler:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;IHasArea&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="nf"&gt;Area&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Circle&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Shape&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;IHasArea&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="err"&gt;…&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="nf"&gt;Area&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PI&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;R&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;R&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="nf"&gt;Of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Shape&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="n"&gt;IHasArea&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Area&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// no binder, no name&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The visitor becomes an ordinary virtual &lt;code&gt;Accept&lt;/code&gt; or an &lt;code&gt;IHasArea&lt;/code&gt;; the settings reader takes &lt;code&gt;Settings&lt;/code&gt; or an &lt;code&gt;ISettings&lt;/code&gt;. If the &lt;code&gt;dynamic&lt;/code&gt; existed only to avoid writing a type, writing the type is the fix.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Pin only the members a site reaches.&lt;/strong&gt; Where &lt;code&gt;dynamic&lt;/code&gt; is genuinely required — a scripting host that calls into your objects, a dynamic-language runtime, a plugin boundary you do not control — the set of members it touches is small and known. Mark exactly those with the standard &lt;code&gt;[Obfuscation]&lt;/code&gt; attribute, which Nebula honours through &lt;code&gt;autoDetect&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Settings&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Obfuscation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Exclude&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;    &lt;span class="n"&gt;Timeout&lt;/span&gt;  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Obfuscation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Exclude&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Endpoint&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt;  &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;InternalCacheKey&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;      &lt;span class="c1"&gt;// still renames&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;    &lt;span class="n"&gt;_retries&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                                  &lt;span class="c1"&gt;// still renames&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Exclude = true&lt;/code&gt; on a member keeps that member's name and nothing else; the type still renames, the other members still rename. Or keep it out of the source entirely with the &lt;code&gt;exclude&lt;/code&gt; list in &lt;code&gt;nebula.config.json&lt;/code&gt;, which takes &lt;code&gt;Namespace.Type.Member&lt;/code&gt; for a single member:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"inputs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"bin/Release/net8.0/Acme.Studio.dll"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"encryptStrings"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"controlFlowObfuscation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"exclude"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"Acme.Settings.Timeout"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"Acme.Settings.Endpoint"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"Acme.Area.Compute"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What you should &lt;em&gt;not&lt;/em&gt; do is exclude &lt;code&gt;Acme.Settings&lt;/code&gt; wholesale. That preserves every member — the ones the binder reaches and the dozen it never will — to protect two properties, and it is the type an attacker most wants to read.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Find every site before you ship.&lt;/strong&gt; Dynamic sites announce themselves in the IL: a reference to &lt;code&gt;Microsoft.CSharp.RuntimeBinder.Binder&lt;/code&gt;, hidden &lt;code&gt;&amp;lt;&amp;gt;o__&lt;/code&gt; containers, and &lt;code&gt;ldstr&lt;/code&gt; literals feeding them. &lt;code&gt;nebula inspect --strings&lt;/code&gt; lists every literal in the assembly, and the ones that are also member names on your types are the ones to look at — &lt;code&gt;Timeout&lt;/code&gt;, &lt;code&gt;Endpoint&lt;/code&gt;, &lt;code&gt;Compute&lt;/code&gt; stand out immediately beside genuine user-facing strings. Cross-check against a search for &lt;code&gt;dynamic&lt;/code&gt; in the source and you have the complete list of members to pin.&lt;/p&gt;

&lt;h2&gt;
  
  
  The test that catches it
&lt;/h2&gt;

&lt;p&gt;Whatever you choose, assert it on the &lt;strong&gt;protected&lt;/strong&gt; build, because the failure only exists there. A dynamic site throws at the first use, so a test that exercises each site is a test that fails loudly when a pin is missing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Fact&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Dynamic_sites_still_bind_on_the_protected_build&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Settings&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nf"&gt;Configure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;                                           &lt;span class="c1"&gt;// GetMember "Timeout", SetMember "Endpoint"&lt;/span&gt;
    &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"https://api.example/v2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Endpoint&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PI&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Area&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;Circle&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;R&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt; &lt;span class="n"&gt;precision&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// InvokeMember "Compute"&lt;/span&gt;
    &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;6.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="n"&gt;Area&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;Rect&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;W&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;H&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run that against the output of the obfuscation step in CI — the same place a &lt;a href="https://delta1labs.com/blog/obfuscate-system-text-json-jsonpropertyname-contract/" rel="noopener noreferrer"&gt;round-trip serialization test&lt;/a&gt; belongs — and a &lt;code&gt;RuntimeBinderException&lt;/code&gt; becomes a red build instead of a customer's crash report.&lt;/p&gt;

&lt;p&gt;The principle is the same one that governs reflection and serialization: &lt;strong&gt;a name that is resolved at runtime is a contract, and a renamer cannot see a contract that is written as a string.&lt;/strong&gt; &lt;code&gt;dynamic&lt;/code&gt; is the C# feature that turns an ordinary member access into exactly that kind of string. &lt;a href="https://delta1labs.com/products/nebula/" rel="noopener noreferrer"&gt;Nebula.NET&lt;/a&gt; will rename everything you let it; your job is to make sure the one name a binder will ask for by text is either no longer asked for, or deliberately kept. For the wider map of what runtime name lookups break, see &lt;a href="https://delta1labs.com/blog/obfuscation-reflection-serialization-pitfalls/" rel="noopener noreferrer"&gt;what obfuscation breaks&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>nebulanet</category>
      <category>obfuscation</category>
      <category>dotnet</category>
      <category>deepdive</category>
    </item>
    <item>
      <title>Site licenses: binding one Keyright key to the customer''s domain instead of a machine, and what that binding can and cannot prove</title>
      <dc:creator>Zero Heartbeat</dc:creator>
      <pubDate>Fri, 16 Oct 2026 20:10:06 +0000</pubDate>
      <link>https://dev.to/zero_heartbeat_06a3625d7a/site-licenses-binding-one-keyright-key-to-the-customers-domain-instead-of-a-machine-and-what-okb</link>
      <guid>https://dev.to/zero_heartbeat_06a3625d7a/site-licenses-binding-one-keyright-key-to-the-customers-domain-instead-of-a-machine-and-what-okb</guid>
      <description>&lt;p&gt;A customer with four hundred desktops does not want to activate four hundred machines, and their IT team will tell you so before the purchase order is signed. The answer every vendor arrives at is a &lt;em&gt;site license&lt;/em&gt;: one key for the whole organisation, no per-machine ceremony. The question that rarely gets written down is what such a key is &lt;strong&gt;bound to&lt;/strong&gt;. A node-locked key is bound to a machine fingerprint. A floating key is bound to a seat count on a server. A site key with no binding at all is a file that unlocks your product for anyone who has it — and files travel.&lt;/p&gt;

&lt;p&gt;This post builds a site license on &lt;a href="https://delta1labs.com/products/keyright/" rel="noopener noreferrer"&gt;Keyright&lt;/a&gt; that is bound to something the whole organisation shares and a stranger does not: the directory domain. It is a signed entitlement, not a new license type; the gate is twenty lines; and the threat model is stated honestly, because a domain name is a fence, not a lock.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the key says
&lt;/h2&gt;

&lt;p&gt;A node-locked Keyright license carries a &lt;code&gt;machineId&lt;/code&gt; and the validator refuses to run on any machine whose fingerprint does not match. A site license sets &lt;code&gt;machineId&lt;/code&gt; to nothing and instead names the site in the signed entitlement set:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"product"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="s2"&gt;"acme-studio"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"licensee"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Contoso Ltd"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"expiryUtc"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2027-10-17T00:00:00Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tier"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="s2"&gt;"pro"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"entitlements"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"site:domain"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="s2"&gt;"corp.contoso.example"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"site:domain-sid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"S-1-5-21-3623811015-3361044348-30300820"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"export"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="s2"&gt;"true"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"max-projects"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="s2"&gt;"unlimited"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"signature"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"…"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things matter here. The &lt;code&gt;site:&lt;/code&gt; entries are ordinary entitlements — string values inside the canonical payload that the signature covers — so a customer can no more change &lt;code&gt;corp.contoso.example&lt;/code&gt; to &lt;code&gt;home.example&lt;/code&gt; than they can push the expiry out a year: one changed byte and &lt;code&gt;LicenseStatus.SignatureInvalid&lt;/code&gt; is what the app sees. And the prefix is a convention, not a Keyright feature: it keeps &lt;em&gt;where this key may run&lt;/em&gt; visibly separate from &lt;em&gt;what this key unlocks&lt;/em&gt;, so nobody later gates a feature on a site claim or vice versa.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;site:domain-sid&lt;/code&gt; is optional. The DNS domain is the portable, cross-platform binding; the SID is the Windows-only hardening you add when the customer or the contract value justifies it. We will come back to what each one actually proves.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the machine's identity comes from
&lt;/h2&gt;

&lt;p&gt;The app has to answer "what domain am I on?" from the machine itself, offline, at startup. There are two tempting APIs and one of them is wrong.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Environment.UserDomainName&lt;/code&gt; returns the NetBIOS domain of the &lt;em&gt;user&lt;/em&gt; running the process — &lt;code&gt;CONTOSO&lt;/code&gt; — and on a machine that is not joined to any domain it returns the machine name instead. That makes it useless for a fail-closed check (you cannot tell "not joined" from "joined to a domain that happens to share the machine's name") and it is flat, so it cannot express that &lt;code&gt;emea.corp.contoso.example&lt;/code&gt; is part of &lt;code&gt;corp.contoso.example&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;IPGlobalProperties.GetIPGlobalProperties().DomainName&lt;/code&gt; returns the host's &lt;strong&gt;primary DNS suffix&lt;/strong&gt;, which domain membership sets: &lt;code&gt;emea.corp.contoso.example&lt;/code&gt; on a joined workstation, an empty string on a laptop that was never joined. It is hierarchical, it is cross-platform (on Linux it reads the host's configured domain name), and the empty-string case is exactly the signal you want.&lt;/p&gt;

&lt;p&gt;For the SID, &lt;code&gt;WindowsIdentity.GetCurrent().User.AccountDomainSid&lt;/code&gt; returns the SID of the domain the current account belongs to — &lt;code&gt;S-1-5-21-…&lt;/code&gt; with the final relative identifier stripped. For a domain user on a joined machine that is the forest's domain SID. For a local account it is the local machine's SID, which will not match and should not.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.Net.NetworkInformation&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.Security.Principal&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;ISiteIdentity&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;DnsDomain&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;   &lt;span class="c1"&gt;// "emea.corp.contoso.example"; null when not joined&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;DomainSid&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;   &lt;span class="c1"&gt;// "S-1-5-21-…"; null off Windows or for a local account&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RealSiteIdentity&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ISiteIdentity&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;DnsDomain&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;get&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;try&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;IPGlobalProperties&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetIPGlobalProperties&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;DomainName&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsNullOrWhiteSpace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TrimEnd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sc"&gt;'.'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;ToLowerInvariant&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;                       &lt;span class="c1"&gt;// no network stack info → treat as unjoined&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;DomainSid&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;get&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;OperatingSystem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsWindows&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;try&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;WindowsIdentity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetCurrent&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;AccountDomainSid&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// null for well-known/non-domain SIDs&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every failure path returns &lt;code&gt;null&lt;/code&gt;, and &lt;code&gt;null&lt;/code&gt; means "not joined". That is deliberate: the gate below treats an unknown identity on a site-licensed key as a refusal, not a pass.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The gate
&lt;/h2&gt;

&lt;p&gt;The gate runs after &lt;code&gt;Validate()&lt;/code&gt; and only does anything when the key carries a site claim. Matching the DNS domain is a &lt;strong&gt;suffix match on label boundaries&lt;/strong&gt;: a key issued for &lt;code&gt;corp.contoso.example&lt;/code&gt; must accept a workstation in the child domain &lt;code&gt;emea.corp.contoso.example&lt;/code&gt;, but must not accept &lt;code&gt;notcorp.contoso.example&lt;/code&gt; — hence the leading dot in the comparison.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;enum&lt;/span&gt; &lt;span class="n"&gt;SiteStatus&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;NotSiteLicensed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;NotJoined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;WrongDomain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;WrongDomainSid&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SiteGate&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;SiteStatus&lt;/span&gt; &lt;span class="nf"&gt;Check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LicenseInfo&lt;/span&gt; &lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ISiteIdentity&lt;/span&gt; &lt;span class="n"&gt;site&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;claim&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Entitlements&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetRaw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"site:domain"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsNullOrWhiteSpace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;claim&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;SiteStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NotSiteLicensed&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;            &lt;span class="c1"&gt;// ordinary key: nothing to check here&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;here&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;site&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DnsDomain&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;here&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;SiteStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NotJoined&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                  &lt;span class="c1"&gt;// fail closed: unknown ≠ allowed&lt;/span&gt;

        &lt;span class="n"&gt;claim&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;claim&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Trim&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;TrimEnd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sc"&gt;'.'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;ToLowerInvariant&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;inDomain&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;here&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;claim&lt;/span&gt;
                     &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="n"&gt;here&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;EndsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"."&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="n"&gt;claim&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;StringComparison&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Ordinal&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;inDomain&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;SiteStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WrongDomain&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;sidClaim&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Entitlements&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetRaw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"site:domain-sid"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsNullOrWhiteSpace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sidClaim&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Equals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;site&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DomainSid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sidClaim&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Trim&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;StringComparison&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OrdinalIgnoreCase&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;SiteStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WrongDomainSid&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;SiteStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wire it at startup, after the signature and expiry checks the SDK already performs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;KeyrightClient&lt;/span&gt; &lt;span class="n"&gt;License&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;KeyrightClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;KeyrightOptions&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Product&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"acme-studio"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;PublicKeyBase64&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AcmeKeys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LicensePublicKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="c1"&gt;// public key only; safe to ship&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="n"&gt;LicenseInfo&lt;/span&gt; &lt;span class="n"&gt;info&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;License&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Validate&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;                  &lt;span class="c1"&gt;// signature, product, expiry, (no) node-lock&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsValid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;RunEvaluation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;site&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SiteGate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;RealSiteIdentity&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;site&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SiteStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Ok&lt;/span&gt; &lt;span class="k"&gt;or&lt;/span&gt; &lt;span class="n"&gt;SiteStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NotSiteLicensed&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;RunEvaluation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;site&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;                  &lt;span class="c1"&gt;// "this key is for corp.contoso.example"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;RunLicensed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note what the SDK did &lt;em&gt;not&lt;/em&gt; do: with &lt;code&gt;machineId&lt;/code&gt; absent, &lt;code&gt;Validate()&lt;/code&gt; never computed a machine fingerprint, so there is no per-machine state, nothing to "activate", and nothing that drifts when IT re-images a desktop. Four hundred machines, one file, one check.&lt;/p&gt;

&lt;p&gt;One deliberate ordering: the site check runs &lt;em&gt;after&lt;/em&gt; &lt;code&gt;IsValid&lt;/code&gt;. A key with a bad signature never reaches the gate, so the entitlements it carries — including a &lt;code&gt;site:domain&lt;/code&gt; an attacker could have typed in — are never read. Entitlements are only trustworthy once the signature over them has verified, and Keyright enforces that by returning an empty entitlement set for anything that did not validate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting the values from the customer
&lt;/h2&gt;

&lt;p&gt;The weak point of any identity-bound license is the hand-off: the customer has to tell you what their domain is, and if they tell you something subtly different from what the machine reports (&lt;code&gt;contoso.example&lt;/code&gt; versus &lt;code&gt;corp.contoso.example&lt;/code&gt;, a NetBIOS name instead of a DNS one), the first launch after purchase fails with "wrong domain" while everyone involved insists they are on the right domain.&lt;/p&gt;

&lt;p&gt;Close that gap by making the product report its own answer, from exactly the code path the gate uses:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// yourapp --site-info&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;RealSiteIdentity&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"DNS domain : &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DnsDomain&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="s"&gt;"(not domain-joined)"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"Domain SID : &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DomainSid&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="s"&gt;"(n/a)"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ask for that output on the order form. Neither value is confidential — the DNS suffix is in every email the company sends, and the domain SID is readable by any authenticated user in the domain — so this is no more sensitive than asking for a company name. The issuing side writes the two strings into the entitlements and signs; what the customer pasted is, by construction, what the check will see. (Issue for the &lt;em&gt;parent&lt;/em&gt; domain the customer names, not the child the diagnostic happened to run in: a key for &lt;code&gt;corp.contoso.example&lt;/code&gt; covers EMEA and APAC; a key for &lt;code&gt;emea.corp.contoso.example&lt;/code&gt; does not.)&lt;/p&gt;

&lt;h2&gt;
  
  
  What the binding proves — honestly
&lt;/h2&gt;

&lt;p&gt;Here is the part that belongs in your own threat model, not just the marketing page.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A DNS domain name is a policy fence.&lt;/strong&gt; It is not a secret and it is not a cryptographic identity. Anyone holding a leaked site key can stand up a throwaway forest named &lt;code&gt;corp.contoso.example&lt;/code&gt;, join a VM to it, and the name check passes. What the name &lt;em&gt;does&lt;/em&gt; stop is the overwhelmingly common failure: the employee who copies &lt;code&gt;license.json&lt;/code&gt; to a home PC, the contractor who keeps it after the engagement, the file that ends up in a shared drive. Those machines are not joined to the customer's domain, the gate fails closed, and the product tells them precisely why. For most site licenses — where the customer is a known organisation with a contract — that is the entire job.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A domain SID raises the bar considerably.&lt;/strong&gt; The SID of a forest is generated at random when the forest is created; there is no supported tooling to choose it, so reproducing &lt;code&gt;S-1-5-21-3623811015-3361044348-30300820&lt;/code&gt; means cloning the customer's actual domain controller, not typing a name into a wizard. An attacker outside the organisation cannot do that; an insider with domain admin rights &lt;em&gt;could&lt;/em&gt;, but an insider with domain admin rights is also the person the customer's contract already covers. Pin the SID when the deal is large enough that a renamed forest is a realistic attack, and accept that it is Windows-only — on a Linux workstation &lt;code&gt;DomainSid&lt;/code&gt; is &lt;code&gt;null&lt;/code&gt;, and the gate above skips the SID comparison only when the key does not carry one, so issue Linux-only sites without it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Neither is a seat count.&lt;/strong&gt; A site key says nothing about &lt;em&gt;how many&lt;/em&gt; machines run it, because it was designed not to. If a contract needs a hard ceiling — "up to 400 concurrent installs" — the instrument is online activation with per-machine seats and a server that counts them, which is a different Keyright feature with a different operational cost (&lt;a href="https://delta1labs.com/blog/node-locking-seat-management/" rel="noopener noreferrer"&gt;node-locking and seat management&lt;/a&gt; covers it). Trying to get a ceiling out of a domain claim is how vendors end up with a site license that is neither convenient nor enforced.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tests: a fake site, three machines
&lt;/h2&gt;

&lt;p&gt;The gate is pure given an &lt;code&gt;ISiteIdentity&lt;/code&gt;, so the three machines in the figure are three tests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;FakeSite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;DnsDomain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;DomainSid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ISiteIdentity&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;LicenseInfo&lt;/span&gt; &lt;span class="nf"&gt;SiteKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;sid&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="n"&gt;TestLicenses&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Valid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entitlements&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"site:domain"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"site:domain-sid"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sid&lt;/span&gt;&lt;span class="p"&gt;!,&lt;/span&gt;          &lt;span class="c1"&gt;// null entry is dropped by the test helper&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Fact&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Child_domain_workstation_passes_by_suffix&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SiteStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SiteGate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nf"&gt;SiteKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"corp.contoso.example"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"S-1-5-21-1-2-3"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;FakeSite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"emea.corp.contoso.example"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"S-1-5-21-1-2-3"&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;

&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Fact&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Unjoined_laptop_fails_closed&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SiteStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NotJoined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SiteGate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nf"&gt;SiteKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"corp.contoso.example"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;FakeSite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DnsDomain&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DomainSid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;

&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Fact&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Lookalike_domain_does_not_suffix_match&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SiteStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WrongDomain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SiteGate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nf"&gt;SiteKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"corp.contoso.example"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;FakeSite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"notcorp.contoso.example"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;

&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Fact&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Renamed_forest_fails_the_sid_check&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SiteStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WrongDomainSid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SiteGate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nf"&gt;SiteKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"corp.contoso.example"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"S-1-5-21-1-2-3"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;FakeSite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"corp.contoso.example"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"S-1-5-21-9-9-9"&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The third test is the one people forget. Without the leading dot in the suffix comparison, &lt;code&gt;notcorp.contoso.example&lt;/code&gt; ends with &lt;code&gt;corp.contoso.example&lt;/code&gt; and the gate opens for a domain the customer never owned.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rolling it out
&lt;/h2&gt;

&lt;p&gt;Treat the site claim like any new enforcement: issue the first site keys with the claim present but the gate in &lt;strong&gt;audit mode&lt;/strong&gt; — log &lt;code&gt;SiteStatus&lt;/code&gt; without acting on it — for a release, read the logs, then flip to enforcing. The &lt;a href="https://delta1labs.com/blog/license-enforcement-audit-mode-shadow-rollout/" rel="noopener noreferrer"&gt;audit-mode rollout&lt;/a&gt; pattern applies unchanged, and the thing it will catch is the one this post cannot predict for you: the customer whose "one domain" turns out to be three forests from two acquisitions.&lt;/p&gt;

&lt;p&gt;A site license is a promise about &lt;em&gt;where&lt;/em&gt; software may run, made to an organisation rather than a machine. Keyright lets you sign that promise into the key; the domain gives the app something real to check it against; and the SID, when you need it, makes the check expensive to lie to. What it does not do is count — and knowing that is what keeps the convenience from quietly becoming a leak.&lt;/p&gt;

</description>
      <category>keyright</category>
      <category>licensing</category>
      <category>dotnet</category>
      <category>deepdive</category>
    </item>
    <item>
      <title>Decompiling nullable value types: int? is a struct, a lifted operator is a HasValue check, and the box that erases Nullable&lt;T&gt;</title>
      <dc:creator>Zero Heartbeat</dc:creator>
      <pubDate>Fri, 16 Oct 2026 20:08:05 +0000</pubDate>
      <link>https://dev.to/zero_heartbeat_06a3625d7a/decompiling-nullable-value-types-int-is-a-struct-a-lifted-operator-is-a-hasvalue-check-and-the-4k44</link>
      <guid>https://dev.to/zero_heartbeat_06a3625d7a/decompiling-nullable-value-types-int-is-a-struct-a-lifted-operator-is-a-hasvalue-check-and-the-4k44</guid>
      <description>&lt;p&gt;Everything the C# language lets you do with &lt;code&gt;int?&lt;/code&gt; is a lie the compiler tells on behalf of a struct. There is no null in an &lt;code&gt;int?&lt;/code&gt;. There is no special "nullable integer" type the CLR knows how to add. There is &lt;code&gt;System.Nullable&amp;lt;int&amp;gt;&lt;/code&gt; — two fields, sixteen bytes, a value type like any other — and a compiler that rewrites &lt;code&gt;a + b&lt;/code&gt;, &lt;code&gt;a == null&lt;/code&gt;, &lt;code&gt;a ?? 5&lt;/code&gt; and &lt;code&gt;(object)a&lt;/code&gt; into calls on that struct so thoroughly that the operators you typed never reach the IL. Reading an obfuscated or symbol-less assembly that uses nullable arithmetic means reading those rewrites, and a decompiler that renders them literally produces code that is correct, compilable, and unreadable.&lt;/p&gt;

&lt;p&gt;This post follows each lowering through the IL and back out, and shows what &lt;a href="https://delta1labs.com/products/glass/" rel="noopener noreferrer"&gt;Glass.NET&lt;/a&gt; has to recognise to give you the line you wrote.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;int?&lt;/code&gt; is in the signature blob, not in an attribute
&lt;/h2&gt;

&lt;p&gt;First the easy part, because it is the opposite of a trap readers of &lt;a href="https://delta1labs.com/blog/decompile-nullable-reference-types/" rel="noopener noreferrer"&gt;nullable reference types&lt;/a&gt; have learned to expect. &lt;code&gt;string?&lt;/code&gt; leaves the type alone and records nullability in &lt;code&gt;[Nullable]&lt;/code&gt; attributes — compile-time annotations the runtime ignores. &lt;code&gt;int?&lt;/code&gt; is a &lt;em&gt;different type&lt;/em&gt;: the generic struct &lt;code&gt;Nullable&amp;lt;T&amp;gt;&lt;/code&gt; instantiated over &lt;code&gt;int&lt;/code&gt;, and it appears as such in every signature:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.method public hidebysig static valuetype [System.Runtime]System.Nullable`1&amp;lt;int32&amp;gt;
        Add(valuetype [System.Runtime]System.Nullable`1&amp;lt;int32&amp;gt; a,
            valuetype [System.Runtime]System.Nullable`1&amp;lt;int32&amp;gt; b) cil managed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A decompiler prints &lt;code&gt;int?&lt;/code&gt; for &lt;code&gt;valuetype System.Nullable`1&amp;lt;int32&amp;gt;&lt;/code&gt; by the same rule it prints &lt;code&gt;int&lt;/code&gt; for &lt;code&gt;System.Int32&lt;/code&gt; — the language's shorthand for a known type. No attribute is consulted; nothing can be stripped that would lose it. Metadata hardening that removes &lt;code&gt;[Nullable]&lt;/code&gt; annotations leaves &lt;code&gt;int?&lt;/code&gt; completely intact, because the information was never an annotation.&lt;/p&gt;

&lt;p&gt;The struct itself is two fields and a handful of members, and those members are the entire vocabulary of every lowering that follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;Nullable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt;
&lt;span class="err"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;hasValue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;internal&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;Nullable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;              &lt;span class="c1"&gt;// the "has a value" constructor&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;HasValue&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt; &lt;span class="n"&gt;Value&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;                &lt;span class="c1"&gt;// throws InvalidOperationException when !hasValue&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt; &lt;span class="nf"&gt;GetValueOrDefault&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;          &lt;span class="c1"&gt;// returns value unconditionally — no check, no throw&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;default(Nullable&amp;lt;T&amp;gt;)&lt;/code&gt; — both fields zero — is what the language calls &lt;code&gt;null&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lifted arithmetic: a conjunction, two &lt;code&gt;GetValueOrDefault&lt;/code&gt;s, and a &lt;code&gt;newobj&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Take the method whose signature appears above:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;+&lt;/code&gt; on two &lt;code&gt;int?&lt;/code&gt; values is a &lt;em&gt;lifted&lt;/em&gt; operator: the result is null if either operand is null, otherwise the sum. The compiler emits that rule directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;locals&lt;/span&gt; &lt;span class="k"&gt;init&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;valuetype&lt;/span&gt; &lt;span class="n"&gt;Nullable&lt;/span&gt;&lt;span class="err"&gt;`&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;int32&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;V_0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;valuetype&lt;/span&gt; &lt;span class="n"&gt;Nullable&lt;/span&gt;&lt;span class="err"&gt;`&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;int32&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;V_1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;valuetype&lt;/span&gt; &lt;span class="n"&gt;Nullable&lt;/span&gt;&lt;span class="err"&gt;`&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;int32&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;V_2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;IL_0000&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ldarg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;
  &lt;span class="n"&gt;IL_0001&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;stloc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;                                   &lt;span class="c1"&gt;// copy a to a temp (operands evaluated once)&lt;/span&gt;
  &lt;span class="n"&gt;IL_0002&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ldarg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;
  &lt;span class="n"&gt;IL_0003&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;stloc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;                                   &lt;span class="c1"&gt;// copy b&lt;/span&gt;
  &lt;span class="n"&gt;IL_0004&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ldloca&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;   &lt;span class="n"&gt;V_0&lt;/span&gt;
  &lt;span class="n"&gt;IL_0006&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;call&lt;/span&gt;       &lt;span class="n"&gt;instance&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;Nullable&lt;/span&gt;&lt;span class="err"&gt;`&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;int32&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;::&lt;/span&gt;&lt;span class="nf"&gt;get_HasValue&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="n"&gt;IL_000b&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ldloca&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;   &lt;span class="n"&gt;V_1&lt;/span&gt;
  &lt;span class="n"&gt;IL_000d&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;call&lt;/span&gt;       &lt;span class="n"&gt;instance&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;Nullable&lt;/span&gt;&lt;span class="err"&gt;`&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;int32&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;::&lt;/span&gt;&lt;span class="nf"&gt;get_HasValue&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="n"&gt;IL_0012&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;and&lt;/span&gt;                                       &lt;span class="c1"&gt;// both have a value?&lt;/span&gt;
  &lt;span class="n"&gt;IL_0013&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;brtrue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;   &lt;span class="n"&gt;IL_0020&lt;/span&gt;
  &lt;span class="n"&gt;IL_0015&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ldloca&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;   &lt;span class="n"&gt;V_2&lt;/span&gt;
  &lt;span class="n"&gt;IL_0017&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;initobj&lt;/span&gt;    &lt;span class="n"&gt;Nullable&lt;/span&gt;&lt;span class="err"&gt;`&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;int32&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;              &lt;span class="c1"&gt;// the null result: default(Nullable&amp;lt;int&amp;gt;)&lt;/span&gt;
  &lt;span class="n"&gt;IL_001d&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ldloc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;
  &lt;span class="n"&gt;IL_001e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ret&lt;/span&gt;
  &lt;span class="n"&gt;IL_0020&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ldloca&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;   &lt;span class="n"&gt;V_0&lt;/span&gt;
  &lt;span class="n"&gt;IL_0022&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;call&lt;/span&gt;       &lt;span class="n"&gt;instance&lt;/span&gt; &lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="n"&gt;Nullable&lt;/span&gt;&lt;span class="err"&gt;`&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;int32&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;::&lt;/span&gt;&lt;span class="nf"&gt;GetValueOrDefault&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="n"&gt;IL_0027&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ldloca&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;   &lt;span class="n"&gt;V_1&lt;/span&gt;
  &lt;span class="n"&gt;IL_0029&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;call&lt;/span&gt;       &lt;span class="n"&gt;instance&lt;/span&gt; &lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="n"&gt;Nullable&lt;/span&gt;&lt;span class="err"&gt;`&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;int32&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;::&lt;/span&gt;&lt;span class="nf"&gt;GetValueOrDefault&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="n"&gt;IL_002e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;add&lt;/span&gt;
  &lt;span class="n"&gt;IL_002f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;newobj&lt;/span&gt;     &lt;span class="n"&gt;instance&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="n"&gt;Nullable&lt;/span&gt;&lt;span class="err"&gt;`&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;int32&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;::.&lt;/span&gt;&lt;span class="nf"&gt;ctor&lt;/span&gt;&lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;IL_0034&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ret&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three details are worth noticing, because they are what a decompiler keys on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The operands are copied to &lt;strong&gt;temporaries&lt;/strong&gt; first, so each is evaluated exactly once even though it is read twice (once for &lt;code&gt;HasValue&lt;/code&gt;, once for the value). A hand-written version would usually not bother.&lt;/li&gt;
&lt;li&gt;The value is read with &lt;strong&gt;&lt;code&gt;GetValueOrDefault&lt;/code&gt;&lt;/strong&gt;, not &lt;code&gt;Value&lt;/code&gt;. &lt;code&gt;HasValue&lt;/code&gt; was just checked, so the re-check inside &lt;code&gt;Value&lt;/code&gt; would be redundant and its throw path unreachable; &lt;code&gt;GetValueOrDefault&lt;/code&gt; is a plain field read. Seeing &lt;code&gt;GetValueOrDefault&lt;/code&gt; immediately after a &lt;code&gt;HasValue&lt;/code&gt; guard is the single strongest signal that you are looking at compiler output rather than source.&lt;/li&gt;
&lt;li&gt;The null result is &lt;strong&gt;&lt;code&gt;initobj&lt;/code&gt;&lt;/strong&gt;, not a call — there is no "null constructor"; the null state is the struct zeroed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Render that literally and you get:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// faithful to the IL, and not what anyone wrote:&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;num&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;num2&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;num&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HasValue&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;num2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HasValue&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;?(&lt;/span&gt;&lt;span class="n"&gt;num&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetValueOrDefault&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="n"&gt;num2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetValueOrDefault&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That compiles and is semantically exact. It is also three lines, two temporaries and four calls for a single &lt;code&gt;+&lt;/code&gt;. Glass recognises the whole shape — temporaries, conjunction of &lt;code&gt;HasValue&lt;/code&gt;, a pair of &lt;code&gt;GetValueOrDefault&lt;/code&gt; feeding one binary operator, &lt;code&gt;newobj&lt;/code&gt; on the true arm and &lt;code&gt;initobj&lt;/code&gt; on the false — and emits &lt;code&gt;a + b&lt;/code&gt;. The same template covers every lifted arithmetic, bitwise and shift operator; only the instruction between the two &lt;code&gt;GetValueOrDefault&lt;/code&gt; calls changes.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Lifted equality is a different shape
&lt;/h2&gt;

&lt;p&gt;Comparison is not lowered the same way, because the rule is different: two nulls are &lt;em&gt;equal&lt;/em&gt;, and a null is unequal to any value. The compiler produces a branch-free expression for it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="nf"&gt;Same&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  IL_0004: ldloca.s   V_0
  IL_0006: call       instance !0 Nullable`1&amp;lt;int32&amp;gt;::GetValueOrDefault()
  IL_000b: ldloca.s   V_1
  IL_000d: call       instance !0 Nullable`1&amp;lt;int32&amp;gt;::GetValueOrDefault()
  IL_0012: ceq                                       // values equal (both 0 when both null)
  IL_0014: ldloca.s   V_0
  IL_0016: call       instance bool Nullable`1&amp;lt;int32&amp;gt;::get_HasValue()
  IL_001b: ldloca.s   V_1
  IL_001d: call       instance bool Nullable`1&amp;lt;int32&amp;gt;::get_HasValue()
  IL_0022: ceq                                       // same has-value state
  IL_0024: and
  IL_0025: ret
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is &lt;code&gt;(a.GetValueOrDefault() == b.GetValueOrDefault()) &amp;amp; (a.HasValue == b.HasValue)&lt;/code&gt;. Walk the cases: both null → &lt;code&gt;0 == 0&lt;/code&gt; and &lt;code&gt;false == false&lt;/code&gt; → true. One null, one &lt;code&gt;5&lt;/code&gt; → &lt;code&gt;0 == 5&lt;/code&gt; is false → false. Both &lt;code&gt;5&lt;/code&gt; → true. Note that &lt;code&gt;GetValueOrDefault&lt;/code&gt; is called here &lt;em&gt;without&lt;/em&gt; a prior &lt;code&gt;HasValue&lt;/code&gt; guard — it is being used precisely because it is safe on a null — so the "&lt;code&gt;GetValueOrDefault&lt;/code&gt; after guard" signal from the arithmetic case does not apply, and the decompiler needs a second template for this shape. &lt;code&gt;&amp;lt;&lt;/code&gt;, &lt;code&gt;&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;=&lt;/code&gt;, &lt;code&gt;&amp;gt;=&lt;/code&gt; lower to the value comparison ANDed with &lt;code&gt;a.HasValue &amp;amp; b.HasValue&lt;/code&gt; (two nulls are &lt;em&gt;not&lt;/em&gt; less-than each other), which is a third variant of the same idea.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;== null&lt;/code&gt;, &lt;code&gt;??&lt;/code&gt;, and the pattern &lt;code&gt;is int v&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The simpler forms have simpler lowerings, each of which is a single-template match:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="nf"&gt;IsNull&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;        &lt;span class="c1"&gt;// ldarga a; call get_HasValue; ldc.i4.0; ceq&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt;  &lt;span class="nf"&gt;OrFive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;           &lt;span class="c1"&gt;// a.HasValue ? a.GetValueOrDefault() : 5&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt;  &lt;span class="nf"&gt;Force&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;           &lt;span class="c1"&gt;// ldarga a; call get_Value   — the only place Value appears&lt;/span&gt;
&lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="nf"&gt;Test&lt;/span&gt;  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// a.HasValue &amp;amp;&amp;amp; a.GetValueOrDefault() &amp;gt; 0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;== null&lt;/code&gt; never compares a reference: there is no reference. It is &lt;code&gt;!a.HasValue&lt;/code&gt;, and &lt;code&gt;!= null&lt;/code&gt; is &lt;code&gt;a.HasValue&lt;/code&gt;. &lt;code&gt;??&lt;/code&gt; becomes a conditional on &lt;code&gt;HasValue&lt;/code&gt; with &lt;code&gt;GetValueOrDefault&lt;/code&gt; on the value arm — again not &lt;code&gt;Value&lt;/code&gt;, because the guard already ran. The explicit cast &lt;code&gt;(int)a&lt;/code&gt; is the &lt;strong&gt;only&lt;/strong&gt; lowering that calls &lt;code&gt;get_Value&lt;/code&gt;, because it is the only one whose contract is to throw when there is nothing there; a decompiler can use that as a tell. And the type pattern &lt;code&gt;a is int v&lt;/code&gt; over a nullable is lowered to &lt;code&gt;HasValue&lt;/code&gt; plus &lt;code&gt;GetValueOrDefault&lt;/code&gt; into &lt;code&gt;v&lt;/code&gt;, with no &lt;code&gt;isinst&lt;/code&gt; anywhere — the runtime type test the pattern looks like never happens, because the compiler already knows what the struct contains.&lt;/p&gt;

&lt;h2&gt;
  
  
  The box that erases &lt;code&gt;Nullable&amp;lt;T&amp;gt;&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Everything so far is the compiler rewriting language into calls. Boxing is different: here the &lt;em&gt;runtime&lt;/em&gt; does something special, and the IL says less than it seems to.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;object&lt;/span&gt; &lt;span class="nf"&gt;Box&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;              &lt;span class="c1"&gt;// box valuetype Nullable`1&amp;lt;int32&amp;gt;&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;   &lt;span class="nf"&gt;Unbox&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;object&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;?)&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;      &lt;span class="c1"&gt;// unbox.any valuetype Nullable`1&amp;lt;int32&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Boxing an ordinary struct copies it to the heap behind an object header. The &lt;code&gt;box&lt;/code&gt; instruction applied to &lt;code&gt;Nullable&amp;lt;T&amp;gt;&lt;/code&gt; does &lt;strong&gt;not&lt;/strong&gt; do that. The runtime checks &lt;code&gt;hasValue&lt;/code&gt;: if it is false, the result is a &lt;strong&gt;null reference&lt;/strong&gt;; if it is true, the result is a &lt;strong&gt;boxed &lt;code&gt;T&lt;/code&gt;&lt;/strong&gt; — a heap object of type &lt;code&gt;System.Int32&lt;/code&gt;, byte-for-byte identical to boxing a plain &lt;code&gt;int&lt;/code&gt;. &lt;code&gt;Nullable&amp;lt;int&amp;gt;&lt;/code&gt; itself never reaches the heap. The consequences are visible from C#:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;object&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;?)&lt;/span&gt;&lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetType&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;            &lt;span class="c1"&gt;// typeof(int) — not Nullable&amp;lt;int&amp;gt;&lt;/span&gt;
&lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;               &lt;span class="c1"&gt;// true&lt;/span&gt;
&lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;              &lt;span class="c1"&gt;// true — same object satisfies both&lt;/span&gt;
&lt;span class="kt"&gt;object&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;?)&lt;/span&gt;&lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// n == null; there is no "boxed empty nullable"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;unbox.any Nullable&amp;lt;int32&amp;gt;&lt;/code&gt; is the mirror image: given a boxed &lt;code&gt;int&lt;/code&gt; it constructs a &lt;code&gt;Nullable&amp;lt;int&amp;gt;&lt;/code&gt; with a value; given a &lt;strong&gt;null reference&lt;/strong&gt; it produces an &lt;em&gt;empty&lt;/em&gt; &lt;code&gt;Nullable&amp;lt;int&amp;gt;&lt;/code&gt; instead of throwing the &lt;code&gt;NullReferenceException&lt;/code&gt; that &lt;code&gt;unbox.any&lt;/code&gt; on any other value type would throw. So &lt;code&gt;(int?)o&lt;/code&gt; is the one cast in C# that quietly turns &lt;code&gt;null&lt;/code&gt; into a value-type default.&lt;/p&gt;

&lt;p&gt;For a decompiler this is a case of &lt;em&gt;not&lt;/em&gt; inventing code. The &lt;code&gt;box&lt;/code&gt; and &lt;code&gt;unbox.any&lt;/code&gt; instructions name &lt;code&gt;Nullable&amp;lt;int32&amp;gt;&lt;/code&gt; in their type operand, and the temptation is to render them as a conversion call or a &lt;code&gt;new int?(…)&lt;/code&gt;. The author wrote a plain implicit conversion and a plain cast; the special behaviour lives inside the runtime's implementation of two ordinary instructions, and the right output is &lt;code&gt;object o = a;&lt;/code&gt; and &lt;code&gt;(int?)o&lt;/code&gt;, nothing more.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a decompiler gets wrong, and why it matters
&lt;/h2&gt;

&lt;p&gt;Three failure modes show up in practice, and they are all versions of rendering the lowering instead of the language:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The &lt;code&gt;HasValue&lt;/code&gt;/&lt;code&gt;GetValueOrDefault&lt;/code&gt; dance printed in full&lt;/strong&gt; — correct, verbose, and a reliable sign the tool has no lifted-operator templates. A method with six nullable additions becomes forty lines of temporaries.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;new int?(x)&lt;/code&gt; where the source had &lt;code&gt;x&lt;/code&gt;&lt;/strong&gt; — the implicit conversion from &lt;code&gt;int&lt;/code&gt; to &lt;code&gt;int?&lt;/code&gt; is a &lt;code&gt;newobj&lt;/code&gt; in the IL, and printing the constructor call is faithful but noisy. Glass emits the conversion implicitly wherever the target type makes it unambiguous.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A conversion call around &lt;code&gt;box&lt;/code&gt;/&lt;code&gt;unbox.any&lt;/code&gt;&lt;/strong&gt; — inventing a method that is not in the IL to explain behaviour that is in the runtime.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The reason these matter beyond aesthetics is that nullable arithmetic clusters in exactly the code a reader is usually trying to understand — optional configuration, financial calculations with "not set" states, pattern-heavy domain logic — and the lowering multiplies its length by three or four. A decompiler that folds the templates back gives you the author's five lines; one that does not gives you a page of struct calls that all look the same.&lt;/p&gt;

&lt;p&gt;The underlying lesson is the same one that governs &lt;a href="https://delta1labs.com/blog/decompile-null-conditional-coalescing/" rel="noopener noreferrer"&gt;&lt;code&gt;?.&lt;/code&gt; and &lt;code&gt;??&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://delta1labs.com/blog/decompile-foreach-enumerator-try-finally-for-loop/" rel="noopener noreferrer"&gt;&lt;code&gt;foreach&lt;/code&gt;&lt;/a&gt;: the C# compiler lowers language features into ordinary IL over ordinary types, and the decompiler's job is to know every one of those shapes well enough to run them backwards. &lt;code&gt;Nullable&amp;lt;T&amp;gt;&lt;/code&gt; is unusual only in how completely it disappears — into a struct on the way in, and into a plain boxed &lt;code&gt;int&lt;/code&gt; or a null on the way out.&lt;/p&gt;

</description>
      <category>glassnet</category>
      <category>decompilation</category>
      <category>dotnet</category>
      <category>deepdive</category>
    </item>
    <item>
      <title>Obfuscating a type you serialize with System.Text.Json: [JsonPropertyName] is the contract, the renamed property is not</title>
      <dc:creator>Zero Heartbeat</dc:creator>
      <pubDate>Fri, 16 Oct 2026 13:57:40 +0000</pubDate>
      <link>https://dev.to/zero_heartbeat_06a3625d7a/obfuscating-a-type-you-serialize-with-systemtextjson-jsonpropertyname-is-the-contract-the-3a5d</link>
      <guid>https://dev.to/zero_heartbeat_06a3625d7a/obfuscating-a-type-you-serialize-with-systemtextjson-jsonpropertyname-is-the-contract-the-3a5d</guid>
      <description>&lt;p&gt;Here is a type that works perfectly until the day you obfuscate it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Order&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Customer&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;decimal&lt;/span&gt; &lt;span class="n"&gt;Total&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;DateTime&lt;/span&gt; &lt;span class="n"&gt;PlacedOn&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Serialize it with the default reflection-based &lt;code&gt;System.Text.Json&lt;/code&gt; and you get &lt;code&gt;{"customer":"…","total":…,"placedOn":"…"}&lt;/code&gt;. Run the assembly through a renamer and serialize again, and you get &lt;code&gt;{"a":"…","b":…,"c":"…"}&lt;/code&gt;. Nothing threw. The build was green. Every consumer of that JSON — a frontend, a sister service, a stored document you read back next week — is now broken, and it broke at runtime, in a place no compiler and no &lt;code&gt;validateOnBuild&lt;/code&gt; could ever see.&lt;/p&gt;

&lt;p&gt;This is the single most common way obfuscation "breaks serialization", and it is worth understanding precisely, because the fix is not a blunt keep rule — it is one attribute, and it lets the property keep renaming.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the rename reaches the wire
&lt;/h2&gt;

&lt;p&gt;Internal calls survive renaming because they are linked by &lt;em&gt;metadata token&lt;/em&gt;: the compiler emits &lt;code&gt;call&lt;/code&gt; against a token, and a renamer that rewrites the callee rewrites every token that points at it, so the graph stays connected. Reflection-based &lt;code&gt;System.Text.Json&lt;/code&gt; does not use a token. At serialize time it walks your type with reflection, reads each property's actual CLR &lt;code&gt;Name&lt;/code&gt;, applies the configured naming policy, and writes &lt;em&gt;that string&lt;/em&gt; as the JSON key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;opts&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;JsonSerializerOptions&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;PropertyNamingPolicy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;JsonNamingPolicy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CamelCase&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="c1"&gt;// For each property p:  key = opts.PropertyNamingPolicy.ConvertName(p.Name);&lt;/span&gt;
&lt;span class="c1"&gt;//   p.Name == "Customer"  -&amp;gt;  "customer"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The JSON key &lt;code&gt;"customer"&lt;/code&gt; exists only because the property is &lt;em&gt;named&lt;/em&gt; &lt;code&gt;Customer&lt;/code&gt;. There is no token joining the string in the file to the member in the assembly — the join &lt;strong&gt;is&lt;/strong&gt; the name. Deserialization runs the same machinery in reverse: it reads the incoming key and matches it, by string, to a property. So when the renamer turns &lt;code&gt;Customer&lt;/code&gt; into &lt;code&gt;a&lt;/code&gt;, the serializer does exactly what it is told — it reads the property now called &lt;code&gt;a&lt;/code&gt; and writes &lt;code&gt;"a"&lt;/code&gt; — and the round-trip to anyone expecting &lt;code&gt;"customer"&lt;/code&gt; is dead.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix is an attribute, not a keep rule
&lt;/h2&gt;

&lt;p&gt;The instinct is to exclude the DTO from renaming. Don't — that surrenders exactly the type an attacker most wants to read, and it is unnecessary. The wire name should be a &lt;em&gt;constant&lt;/em&gt;, not a side effect of the member name, and &lt;code&gt;[JsonPropertyName]&lt;/code&gt; makes it one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Order&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;JsonPropertyName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"customer"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Customer&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;JsonPropertyName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"total"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;decimal&lt;/span&gt; &lt;span class="n"&gt;Total&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;JsonPropertyName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"placedOn"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;DateTime&lt;/span&gt; &lt;span class="n"&gt;PlacedOn&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the serializer reads the attribute's string literal — &lt;code&gt;"customer"&lt;/code&gt; — not the property name. The renamer is free to turn &lt;code&gt;Customer&lt;/code&gt; into &lt;code&gt;b&lt;/code&gt;; the attribute still says &lt;code&gt;"customer"&lt;/code&gt;, the JSON key is still &lt;code&gt;"customer"&lt;/code&gt;, and the contract holds. This is the key distinction: the member &lt;em&gt;does&lt;/em&gt; get renamed (which is the point of obfuscating), and only the string the serializer reads is pinned. A keep rule would preserve the member name on disk — the opposite of what you want. The attribute preserves the &lt;em&gt;behaviour&lt;/em&gt; while the name disappears.&lt;/p&gt;

&lt;p&gt;The rule, then, is simple and total: &lt;strong&gt;every serialized member gets an explicit name.&lt;/strong&gt; A single un-attributed property falls back to its CLR name and becomes the one field that breaks. That is easy to enforce in review, and easy to see in a diff.&lt;/p&gt;

&lt;h2&gt;
  
  
  The constructor is a second name binding
&lt;/h2&gt;

&lt;p&gt;Immutable DTOs hide a second trap. When a type has no parameterless constructor, &lt;code&gt;System.Text.Json&lt;/code&gt; deserializes it by matching each JSON property to a &lt;em&gt;constructor parameter&lt;/em&gt; — &lt;strong&gt;by parameter name&lt;/strong&gt;, case-insensitively:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Order&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;decimal&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DateTime&lt;/span&gt; &lt;span class="n"&gt;placedOn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// param names matter&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Customer&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;Total&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;PlacedOn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;placedOn&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;JsonPropertyName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"customer"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Customer&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;JsonPropertyName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"total"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;decimal&lt;/span&gt; &lt;span class="n"&gt;Total&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;JsonPropertyName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"placedOn"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;DateTime&lt;/span&gt; &lt;span class="n"&gt;PlacedOn&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The serializer pairs the &lt;code&gt;"customer"&lt;/code&gt; JSON value with the constructor parameter named &lt;code&gt;customer&lt;/code&gt;. That pairing lives in parameter-name metadata — and if your obfuscator rewrites parameter names to &lt;code&gt;p0, p1, p2&lt;/code&gt;, the match fails and each property is silently left at its default. Two things keep this safe. Mark the chosen constructor with &lt;code&gt;[JsonConstructor]&lt;/code&gt; so the serializer is unambiguous about which one to use, and ensure parameter-name renaming is off for it. Most obfuscators, Nebula included, leave parameter names alone by default — they are not a meaningful attack surface — but when a DTO deserializes through a constructor, that default is load-bearing, so confirm it rather than assume it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enums, extension data, and the members that don't bind by name
&lt;/h2&gt;

&lt;p&gt;A few more members have their own rules. String-serialized enums are written by their &lt;em&gt;member&lt;/em&gt; name unless you override it — the same reflection-reads-the-name problem — so pin them explicitly ([.NET 9] gives you &lt;code&gt;[JsonStringEnumMemberName]&lt;/code&gt; for exactly this):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;JsonConverter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;JsonStringEnumConverter&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Priority&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;))]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;enum&lt;/span&gt; &lt;span class="n"&gt;Priority&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;JsonStringEnumMemberName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"low"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;  &lt;span class="n"&gt;Low&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;JsonStringEnumMemberName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"high"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="n"&gt;High&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;[JsonExtensionData]&lt;/code&gt; on a &lt;code&gt;Dictionary&amp;lt;string, object&amp;gt;&lt;/code&gt; is the one member whose &lt;em&gt;property&lt;/em&gt; name never reaches the wire — its contents are spliced into the parent object — so it needs no &lt;code&gt;[JsonPropertyName]&lt;/code&gt;; the dictionary &lt;em&gt;keys&lt;/em&gt; are runtime data, not member names, and renaming never touches them. And &lt;code&gt;[JsonIgnore]&lt;/code&gt; members, by definition, are not serialized and carry no contract.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real escape from reflection: the source generator
&lt;/h2&gt;

&lt;p&gt;Everything above is about the &lt;em&gt;reflection&lt;/em&gt; serializer reading a live property name. The source generator sidesteps the walk entirely. A &lt;code&gt;[JsonSerializable]&lt;/code&gt; context generates serialization code at compile time, baking each JSON name into the generated metadata as a string constant and reading the property directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;JsonSourceGenerationOptions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PropertyNamingPolicy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;JsonKnownNamingPolicy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CamelCase&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;JsonSerializable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;))]&lt;/span&gt;
&lt;span class="k"&gt;internal&lt;/span&gt; &lt;span class="k"&gt;partial&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderJsonContext&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;JsonSerializerContext&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;JsonSerializer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Serialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;OrderJsonContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Default&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The names are resolved &lt;em&gt;before&lt;/em&gt; Nebula runs and frozen into the generated context as literals, so renaming the DTO afterwards cannot change what the generated code emits — it already knows which property &lt;code&gt;"customer"&lt;/code&gt; means. (The generated &lt;code&gt;OrderJsonContext&lt;/code&gt; type and its &lt;code&gt;Default.Order&lt;/code&gt; entry point are referenced by your call sites and rename consistently with them, like any other internal link; only a reflective use of the context by name would be a separate dependency.) For a hot DTO you are obfuscating, the source generator is both the faster path and the one with the fewest name hazards — but you still attribute the names you care about, because the generator reads &lt;code&gt;[JsonPropertyName]&lt;/code&gt; the same way reflection does.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prove it with a round-trip on the protected build
&lt;/h2&gt;

&lt;p&gt;Whichever path you pick, the test that matters runs against the &lt;em&gt;obfuscated&lt;/em&gt; output, not the debug build — the whole failure mode is that the names only move after Nebula runs. A round-trip against the protected assembly is the one assertion that catches it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Fact&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Protected_build_round_trips_the_wire_contract&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Acme"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;42.50m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;2026&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;16&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;JsonSerializer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Serialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// The contract, asserted literally — these keys are what consumers depend on.&lt;/span&gt;
    &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"\"customer\":\"Acme\""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"\"total\":42.50"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;DoesNotContain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"\"a\":"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;          &lt;span class="c1"&gt;// no leaked renamed member&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;back&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;JsonSerializer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Deserialize&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;)!;&lt;/span&gt;
    &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Acme"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;back&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Customer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;            &lt;span class="c1"&gt;// constructor matched by name&lt;/span&gt;
    &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;42.50m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;back&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Total&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wire this against the post-obfuscation artifact in CI and the "serialization broke" incident becomes a failed test instead of a customer email. The principle underneath it is the one worth carrying to every reflection-driven framework you obfuscate: &lt;strong&gt;a name a framework reads at runtime is a contract, and a contract belongs in a string literal the obfuscator cannot move — not in a member name it is paid to.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://delta1labs.com/products/nebula/" rel="noopener noreferrer"&gt;Nebula.NET&lt;/a&gt; renames aggressively by design; the attributes above are how you keep the one name that has to stay while losing the ones that should go. For the broader pattern across reflection and serialization, see &lt;a href="https://delta1labs.com/blog/obfuscation-reflection-serialization-pitfalls/" rel="noopener noreferrer"&gt;what obfuscation breaks&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>nebulanet</category>
      <category>obfuscation</category>
      <category>dotnet</category>
      <category>deepdive</category>
    </item>
    <item>
      <title>What HTTP status does a failed license check deserve? 402, 403, Problem Details, and a header the client can read</title>
      <dc:creator>Zero Heartbeat</dc:creator>
      <pubDate>Fri, 16 Oct 2026 13:55:38 +0000</pubDate>
      <link>https://dev.to/zero_heartbeat_06a3625d7a/what-http-status-does-a-failed-license-check-deserve-402-403-problem-details-and-a-header-the-1e6e</link>
      <guid>https://dev.to/zero_heartbeat_06a3625d7a/what-http-status-does-a-failed-license-check-deserve-402-403-problem-details-and-a-header-the-1e6e</guid>
      <description>&lt;p&gt;A licensed service makes one decision on the way in — is this caller allowed to run right now? — and the HTTP response it sends when the answer is &lt;em&gt;no&lt;/em&gt; is almost always an afterthought. The two common failures are a bare &lt;code&gt;500&lt;/code&gt; with a stack trace (which tells the caller nothing and leaks your internals) and a &lt;code&gt;200 OK&lt;/code&gt; carrying &lt;code&gt;{ "ok": false }&lt;/code&gt; in the body (which no HTTP client, proxy or retry policy will ever notice, because as far as the stack is concerned the request succeeded). Between them sits a contract the gate actually owes its callers: the right status code, a machine-readable reason, and a header a client can branch on — without ever echoing the license key back.&lt;/p&gt;

&lt;p&gt;This post builds that contract around a &lt;a href="https://delta1labs.com/products/keyright/" rel="noopener noreferrer"&gt;Keyright&lt;/a&gt; licence. The verification itself — checking the signature, the expiry, the machine binding — is covered elsewhere; here the licence has already been verified and reduced to a &lt;code&gt;LicenseStatus&lt;/code&gt;, and the only question is how to turn that status into an HTTP response a CLI, a browser and a sister service can all handle correctly.&lt;/p&gt;

&lt;h2&gt;
  
  
  A license failure is not an authentication failure
&lt;/h2&gt;

&lt;p&gt;The reflex is to reach for &lt;code&gt;401 Unauthorized&lt;/code&gt;, because the request was "rejected". That is the wrong axis. &lt;code&gt;401&lt;/code&gt; means &lt;em&gt;I do not know who you are&lt;/em&gt;; it is an authentication signal, and the spec requires it to carry a &lt;code&gt;WWW-Authenticate&lt;/code&gt; header telling the client how to present credentials. A client that gets a &lt;code&gt;401&lt;/code&gt; will re-authenticate — and if its credentials are valid, it will succeed, retry, and hit exactly the same wall. You have built a loop.&lt;/p&gt;

&lt;p&gt;A licence failure is orthogonal to identity. The caller can be perfectly authenticated — a valid token, a known API key — and the thing that lapsed is the &lt;em&gt;licence&lt;/em&gt;. The status code has to say "I know exactly who you are, and this is still not allowed," which is &lt;code&gt;403 Forbidden&lt;/code&gt;. That covers a revoked key, a machine mismatch, or a request for a feature the licence does not include.&lt;/p&gt;

&lt;p&gt;There is one case where &lt;code&gt;403&lt;/code&gt; undersells the situation. When the licence has simply &lt;em&gt;expired&lt;/em&gt; — the subscription ran out — the remedy is not "ask an administrator for permission", it is "renew". For that, &lt;code&gt;402 Payment Required&lt;/code&gt; says more. It was marked reserved in the original HTTP spec, but it is passed through cleanly by every proxy and is widely used by subscription APIs today; the only rule is not to expect a browser to do anything magical with it. Emitting &lt;code&gt;402&lt;/code&gt; lets a client distinguish a &lt;em&gt;commercial&lt;/em&gt; wall from a &lt;em&gt;permissions&lt;/em&gt; wall, which is precisely the distinction a licence gate exists to make.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The reason belongs in a problem+json body
&lt;/h2&gt;

&lt;p&gt;A status code is a single number; a client that wants to show a useful message or branch on the &lt;em&gt;specific&lt;/em&gt; reason needs structured detail. The standard shape for that is &lt;a href="https://www.rfc-editor.org/rfc/rfc9457" rel="noopener noreferrer"&gt;RFC 9457&lt;/a&gt; — Problem Details for HTTP APIs, the successor to RFC 7807 — served as &lt;code&gt;application/problem+json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://delta1labs.com/problems/license-expired"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"License expired"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;402&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"detail"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"This license expired on 2026-09-30. Renew to continue."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"licenseStatus"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Expired"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"expiresOn"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-09-30T00:00:00Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"renewUrl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://delta1labs.com/account/renew"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"traceId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"00-3f1a…-01"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;type&lt;/code&gt; URI is the contract. A client branches on it — a stable string you promise not to change — not on the human &lt;code&gt;title&lt;/code&gt;, which you are free to reword or localise. The members below &lt;code&gt;detail&lt;/code&gt; are &lt;em&gt;extension members&lt;/em&gt;: RFC 9457 lets you add any JSON you like, and this is where the licence-specific facts go. &lt;code&gt;licenseStatus&lt;/code&gt; mirrors Keyright's enum, &lt;code&gt;expiresOn&lt;/code&gt; gives a CLI something to print, &lt;code&gt;renewUrl&lt;/code&gt; gives a UI a button. What is &lt;em&gt;not&lt;/em&gt; there is the key, the signature, or the licensee's email — the error body is the single most-copied artifact in a support flow, pasted into tickets and screenshots, so it is held to the same rule as a log line.&lt;/p&gt;

&lt;p&gt;ASP.NET Core has first-class support for this. Register the services once:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddProblemDetails&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;AddProblemDetails()&lt;/code&gt; registers an &lt;code&gt;IProblemDetailsService&lt;/code&gt; and wires the framework to emit &lt;code&gt;application/problem+json&lt;/code&gt; for error responses automatically, including content negotiation.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gate: an endpoint filter that maps status to response
&lt;/h2&gt;

&lt;p&gt;The cleanest place to enforce the licence on a minimal API is an &lt;code&gt;IEndpointFilter&lt;/code&gt;: it runs after model binding and authentication but before the handler, it has the &lt;code&gt;HttpContext&lt;/code&gt;, and it can short-circuit by returning an &lt;code&gt;IResult&lt;/code&gt; instead of calling &lt;code&gt;next&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;LicenseGateFilter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ILicenseState&lt;/span&gt; &lt;span class="n"&gt;license&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IEndpointFilter&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;ValueTask&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;object&lt;/span&gt;&lt;span class="p"&gt;?&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;InvokeAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;EndpointFilterInvocationContext&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;EndpointFilterDelegate&lt;/span&gt; &lt;span class="n"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;license&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Current&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// already-verified snapshot, not a fresh crypto check&lt;/span&gt;

        &lt;span class="c1"&gt;// Happy paths run the handler.&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="n"&gt;LicenseStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Valid&lt;/span&gt; &lt;span class="k"&gt;or&lt;/span&gt; &lt;span class="n"&gt;LicenseStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ExpiringSoon&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="n"&gt;LicenseStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ExpiringSoon&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HttpContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"Keyright-License"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"warning; reason=expiring-soon"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="c1"&gt;// Everything else short-circuits with a problem+json response.&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;license&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="n"&gt;LicenseStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ServerUnavailable&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HttpContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RetryAfter&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"30"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HttpContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"Keyright-License"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;$"blocked; status=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Problem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;statusCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;extensions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;Dictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;object&lt;/span&gt;&lt;span class="p"&gt;?&amp;gt;&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"licenseStatus"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
                &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"expiresOn"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;license&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ExpiresOn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="c1"&gt;// null for non-expiry failures&lt;/span&gt;
                &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"renewUrl"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="m"&gt;402&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s"&gt;"https://delta1labs.com/account/renew"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LicenseStatus&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ILicenseState&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="k"&gt;switch&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;LicenseStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Expired&lt;/span&gt;           &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;402&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"https://delta1labs.com/problems/license-expired"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                            &lt;span class="s"&gt;"License expired"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;$"This license expired on &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ExpiresOn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;yyyy&lt;/span&gt;&lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="n"&gt;MM&lt;/span&gt;&lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="n"&gt;dd&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;. Renew to continue."&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;LicenseStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Revoked&lt;/span&gt;           &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;403&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"https://delta1labs.com/problems/license-revoked"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                            &lt;span class="s"&gt;"License revoked"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"This license has been revoked. Contact your administrator."&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;LicenseStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MachineMismatch&lt;/span&gt;   &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;403&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"https://delta1labs.com/problems/machine-mismatch"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                            &lt;span class="s"&gt;"License not valid on this machine"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"This license is bound to a different machine."&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;LicenseStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EntitlementMissing&lt;/span&gt;&lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;403&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"https://delta1labs.com/problems/entitlement-missing"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                            &lt;span class="s"&gt;"Feature not licensed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Your license does not include this feature."&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;LicenseStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SignatureInvalid&lt;/span&gt;  &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;403&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"https://delta1labs.com/problems/license-invalid"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                            &lt;span class="s"&gt;"License invalid"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"The license could not be verified."&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;LicenseStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ServerUnavailable&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;503&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"https://delta1labs.com/problems/license-unavailable"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                            &lt;span class="s"&gt;"License service unavailable"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Could not reach the license service. Please retry shortly."&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;_&lt;/span&gt;                               &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;403&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"https://delta1labs.com/problems/license-invalid"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                            &lt;span class="s"&gt;"License invalid"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"The license could not be verified."&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Attach it to the endpoints — or a whole group — that the licence protects:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;pro&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MapGroup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/api/pro"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;AddEndpointFilter&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;LicenseGateFilter&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
&lt;span class="n"&gt;pro&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MapPost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/export"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ExportRequest&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;Export&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two deliberate choices. First, &lt;code&gt;license.Current&lt;/code&gt; reads an &lt;em&gt;already-verified snapshot&lt;/em&gt; — the filter never re-runs a signature check per request, so the gate adds a dictionary lookup, not cryptography, to the hot path. Second, the &lt;code&gt;Keyright-License&lt;/code&gt; response header carries the machine-readable state even on the happy path (as a &lt;code&gt;warning&lt;/code&gt; for an expiring licence), so a sister service or a CLI can react — renew, warn, back off — by reading one header instead of parsing the body. It carries the &lt;em&gt;status&lt;/em&gt;, never the key.&lt;/p&gt;

&lt;h3&gt;
  
  
  4xx is you, 5xx is us
&lt;/h3&gt;

&lt;p&gt;The split between &lt;code&gt;ServerUnavailable → 503 + Retry-After&lt;/code&gt; and the &lt;code&gt;4xx&lt;/code&gt; family is what makes a generic HTTP client behave. A &lt;code&gt;402&lt;/code&gt; or &lt;code&gt;403&lt;/code&gt; is a definite, client-caused answer: it will not change on retry, there is no &lt;code&gt;Retry-After&lt;/code&gt;, and a well-behaved client surfaces it to a human. A &lt;code&gt;503&lt;/code&gt; with &lt;code&gt;Retry-After: 30&lt;/code&gt; is the opposite — &lt;em&gt;not now, ask again&lt;/em&gt; — and a standard retry policy (Polly, &lt;code&gt;HttpClient&lt;/code&gt; resilience) will back off and retry it automatically while leaving the &lt;code&gt;4xx&lt;/code&gt; alone. The grace logic that decides &lt;em&gt;how long&lt;/em&gt; the app keeps serving while the licence service is unreachable lives behind the gate; the gate only reports the state it is handed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The test: assert the status, the type, and the absent key
&lt;/h2&gt;

&lt;p&gt;A gate like this earns a &lt;code&gt;WebApplicationFactory&lt;/code&gt; test that pins all three parts of the contract — the status code, the problem &lt;code&gt;type&lt;/code&gt;, and the one thing that must never be in the body.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Fact&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;Expired_license_returns_402_problem_and_never_the_key&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;LicensedAppFactory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LicenseStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Expired&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"KR-SECRET-KEY-DO-NOT-LEAK"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateClient&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;res&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;PostAsJsonAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/api/pro/export"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;format&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"csv"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HttpStatusCode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PaymentRequired&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusCode&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;          &lt;span class="c1"&gt;// 402&lt;/span&gt;
    &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"application/problem+json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ContentType&lt;/span&gt;&lt;span class="p"&gt;!.&lt;/span&gt;&lt;span class="n"&gt;MediaType&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ReadAsStringAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;JsonDocument&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"https://delta1labs.com/problems/license-expired"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RootElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;GetString&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Expired"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RootElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"licenseStatus"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;GetString&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

    &lt;span class="c1"&gt;// The bearer credential must never appear in an error the client can copy.&lt;/span&gt;
    &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;DoesNotContain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"KR-SECRET-KEY-DO-NOT-LEAK"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"blocked; status=Expired"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetValues&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Keyright-License"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;Single&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That last assertion is the one worth keeping forever. Status codes and problem types are a contract you can evolve; a leaked key in an error body is a credential disclosure that will sit in a support ticket for years. A licence gate's job is to say &lt;em&gt;no&lt;/em&gt; clearly — to the HTTP stack, to the client's retry policy, and to the human reading the message — while giving away nothing a reader did not already have.&lt;/p&gt;

&lt;p&gt;If you are wiring licence verification into a .NET service, &lt;a href="https://delta1labs.com/products/keyright/" rel="noopener noreferrer"&gt;Keyright&lt;/a&gt; hands you the verified &lt;code&gt;LicenseStatus&lt;/code&gt;; the HTTP contract above is how you make it legible to everything downstream.&lt;/p&gt;

</description>
      <category>keyright</category>
      <category>licensing</category>
      <category>dotnet</category>
      <category>deepdive</category>
    </item>
    <item>
      <title>Decompiling a decimal constant: why 3.14m is not an IL constant, and the [DecimalConstant] attribute that rebuilds it</title>
      <dc:creator>Zero Heartbeat</dc:creator>
      <pubDate>Fri, 16 Oct 2026 13:53:24 +0000</pubDate>
      <link>https://dev.to/zero_heartbeat_06a3625d7a/decompiling-a-decimal-constant-why-314m-is-not-an-il-constant-and-the-decimalconstant-3fna</link>
      <guid>https://dev.to/zero_heartbeat_06a3625d7a/decompiling-a-decimal-constant-why-314m-is-not-an-il-constant-and-the-decimalconstant-3fna</guid>
      <description>&lt;p&gt;Take the most ordinary line of C# imaginable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;decimal&lt;/span&gt; &lt;span class="n"&gt;TaxRate&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0.075m&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compile it, open the assembly, and look for where &lt;code&gt;0.075&lt;/code&gt; is stored. It is not in the IL — there is no &lt;code&gt;ldc&lt;/code&gt; instruction for it. It is not in the metadata &lt;strong&gt;Constant&lt;/strong&gt; table, where &lt;code&gt;const int&lt;/code&gt; and default parameter values live. The number has, apparently, vanished. It hasn't: it has been dismantled into five integers and tucked into an attribute, and a decompiler that doesn't know to look there will render your tax rate as a pile of constructor arguments. This is the one numeric literal in C# that a decompiler has to &lt;em&gt;reconstruct&lt;/em&gt; rather than read, and the mechanism is worth seeing in full.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why &lt;code&gt;decimal&lt;/code&gt; has nowhere to live
&lt;/h2&gt;

&lt;p&gt;Every other numeric literal has a native home. A &lt;code&gt;const int&lt;/code&gt; or a default &lt;code&gt;int&lt;/code&gt; parameter is written into the &lt;strong&gt;Constant&lt;/strong&gt; metadata table, whose blob format defines an encoding for each primitive element type — &lt;code&gt;ELEMENT_TYPE_I4&lt;/code&gt;, &lt;code&gt;ELEMENT_TYPE_R8&lt;/code&gt;, &lt;code&gt;ELEMENT_TYPE_STRING&lt;/code&gt;, and so on. A literal in a method body becomes an instruction: &lt;code&gt;ldc.i4 42&lt;/code&gt;, &lt;code&gt;ldc.r8 3.14&lt;/code&gt;. Both channels share one assumption — that the value is a type the CLR represents natively.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;decimal&lt;/code&gt; is not. &lt;code&gt;System.Decimal&lt;/code&gt; is a 128-bit &lt;strong&gt;struct&lt;/strong&gt; defined in the class library, built from a 96-bit integer plus a scale and a sign. The CLR has no load opcode for it and the Constant blob format has no element type for it. So when the language says "this &lt;code&gt;decimal&lt;/code&gt; is a constant", there is no metadata slot that can hold the answer. The compiler has to encode the value some other way — and the way it chose is &lt;code&gt;System.Runtime.CompilerServices.DecimalConstantAttribute&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The attribute &lt;em&gt;is&lt;/em&gt; the number
&lt;/h2&gt;

&lt;p&gt;Here is the field above as the compiler emits it, in IL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;field&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;initonly&lt;/span&gt; &lt;span class="n"&gt;valuetype&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;System&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Runtime&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="n"&gt;System&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Decimal&lt;/span&gt; &lt;span class="n"&gt;TaxRate&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;custom&lt;/span&gt; &lt;span class="n"&gt;instance&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;System&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Runtime&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="n"&gt;System&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CompilerServices&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DecimalConstantAttribute&lt;/span&gt;&lt;span class="p"&gt;::.&lt;/span&gt;&lt;span class="nf"&gt;ctor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="n"&gt;uint8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;uint8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;uint32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;uint32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;uint32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="m"&gt;01&lt;/span&gt; &lt;span class="m"&gt;00&lt;/span&gt; &lt;span class="m"&gt;03&lt;/span&gt; &lt;span class="m"&gt;00&lt;/span&gt; &lt;span class="m"&gt;00&lt;/span&gt; &lt;span class="m"&gt;00&lt;/span&gt; &lt;span class="m"&gt;00&lt;/span&gt; &lt;span class="m"&gt;00&lt;/span&gt; &lt;span class="m"&gt;00&lt;/span&gt; &lt;span class="m"&gt;00&lt;/span&gt; &lt;span class="m"&gt;00&lt;/span&gt; &lt;span class="m"&gt;00&lt;/span&gt; &lt;span class="m"&gt;4&lt;/span&gt;&lt;span class="n"&gt;B&lt;/span&gt; &lt;span class="m"&gt;00&lt;/span&gt; &lt;span class="m"&gt;00&lt;/span&gt; &lt;span class="m"&gt;00&lt;/span&gt; &lt;span class="m"&gt;00&lt;/span&gt; &lt;span class="m"&gt;00&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those five constructor arguments are not metadata about the number — they &lt;em&gt;are&lt;/em&gt; the number, in exactly the pieces &lt;code&gt;System.Decimal&lt;/code&gt; is assembled from. The constructor signature is &lt;code&gt;(byte scale, byte sign, uint hi, uint mid, uint low)&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;scale&lt;/strong&gt; = &lt;code&gt;3&lt;/code&gt; — the number of digits after the decimal point (divide the integer by 10³).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;sign&lt;/strong&gt; = &lt;code&gt;0&lt;/code&gt; — positive (1 would mean negative).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;hi / mid / low&lt;/strong&gt; = &lt;code&gt;0 / 0 / 75&lt;/code&gt; — the three 32-bit words of the 96-bit unscaled integer; here just &lt;code&gt;75&lt;/code&gt; in the low word.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Put together: the unscaled integer &lt;code&gt;75&lt;/code&gt;, scaled by &lt;code&gt;10⁻³&lt;/code&gt;, positive → &lt;code&gt;0.075&lt;/code&gt;. The decompiler runs the same arithmetic the runtime's &lt;code&gt;decimal(int lo, int mid, int hi, bool isNegative, byte scale)&lt;/code&gt; constructor would, and recovers the literal &lt;code&gt;0.075m&lt;/code&gt;. Nothing is rounded and nothing is lost — the attribute is a complete, exact description of the value.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  A &lt;code&gt;const decimal&lt;/code&gt; has no initializer — on purpose
&lt;/h2&gt;

&lt;p&gt;There is a second surprise waiting for a decompiler that only reads method bodies. You might expect &lt;code&gt;const decimal TaxRate = 0.075m&lt;/code&gt; to compile to a store in the static constructor — &lt;code&gt;.cctor&lt;/code&gt; assigns the field — but it does not. &lt;code&gt;const&lt;/code&gt; in C# means &lt;em&gt;compile-time constant&lt;/em&gt;: the value has to be usable in other constant contexts, so the compiler cannot implement it by running code at type-init time (that would make it &lt;code&gt;readonly&lt;/code&gt;, not &lt;code&gt;const&lt;/code&gt;). Instead:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the field is emitted as a &lt;code&gt;static initonly&lt;/code&gt; field carrying the &lt;code&gt;[DecimalConstant]&lt;/code&gt; attribute, and&lt;/li&gt;
&lt;li&gt;every &lt;em&gt;use&lt;/em&gt; of &lt;code&gt;TaxRate&lt;/code&gt; in source is replaced, at compile time, by the reconstructed value inlined at the call site.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the &lt;code&gt;.cctor&lt;/code&gt; is empty of any store to &lt;code&gt;TaxRate&lt;/code&gt;, and there is no instance initializer. A decompiler that walks constructors looking for the field's value finds nothing and would wrongly report the field as uninitialized. The value is in the attribute, and recovering the original declaration means reading it from there — then presenting the field as &lt;code&gt;const&lt;/code&gt;, not &lt;code&gt;static readonly&lt;/code&gt;, because the &lt;code&gt;initonly&lt;/code&gt; flag plus the &lt;code&gt;[DecimalConstant]&lt;/code&gt; attribute together are the signature of a &lt;code&gt;const decimal&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  A default &lt;code&gt;decimal&lt;/code&gt; parameter takes a different road than a default &lt;code&gt;int&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The same gap reshapes optional parameters. Compare:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;A&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;    &lt;span class="n"&gt;rate&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;B&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;decimal&lt;/span&gt; &lt;span class="n"&gt;rate&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0.075m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For &lt;code&gt;A&lt;/code&gt;, the default &lt;code&gt;5&lt;/code&gt; goes into the &lt;strong&gt;Constant&lt;/strong&gt; table and the parameter gets the &lt;code&gt;HasDefault&lt;/code&gt; flag; a decompiler reads the Constant blob and writes &lt;code&gt;= 5&lt;/code&gt;. For &lt;code&gt;B&lt;/code&gt;, the Constant table can't hold a &lt;code&gt;decimal&lt;/code&gt;, so the compiler marks the parameter &lt;code&gt;[opt]&lt;/code&gt; (the &lt;code&gt;Optional&lt;/code&gt; flag) &lt;strong&gt;without&lt;/strong&gt; a &lt;code&gt;HasDefault&lt;/code&gt; entry, and attaches &lt;code&gt;[DecimalConstant(3, 0, 0, 0, 75)]&lt;/code&gt; to the &lt;em&gt;parameter&lt;/em&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.method ... void B(valuetype System.Decimal rate)
{
  .param [1]
    .custom instance void ...DecimalConstantAttribute::.ctor(uint8,uint8,uint32,uint32,uint32)
      = ( 01 00 03 00 00 00 00 00 00 00 00 00 4B 00 00 00 00 00 )
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A decompiler recovering default values therefore needs &lt;em&gt;two&lt;/em&gt; code paths: read the Constant table for the primitives, and check for &lt;code&gt;[DecimalConstant]&lt;/code&gt; on the parameter for &lt;code&gt;decimal&lt;/code&gt;. Honour only the Constant table and the &lt;code&gt;decimal&lt;/code&gt; default disappears — the parameter renders as having none. Worse, a decompiler that treats &lt;code&gt;[DecimalConstant]&lt;/code&gt; as an ordinary custom attribute will print it &lt;em&gt;literally&lt;/em&gt; into the source:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// WRONG — the attribute is an encoding, not something the author wrote:&lt;/span&gt;
&lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;B&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nf"&gt;DecimalConstant&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;75&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="kt"&gt;decimal&lt;/span&gt; &lt;span class="n"&gt;rate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That line never existed in the original code. The attribute is the compiler's private encoding of &lt;code&gt;= 0.075m&lt;/code&gt;, and a faithful decompiler consumes it and renders the thing the programmer actually typed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Glass treats it as a value, not an annotation
&lt;/h2&gt;

&lt;p&gt;The rule a decompiler has to encode is: &lt;strong&gt;&lt;code&gt;[DecimalConstant]&lt;/code&gt; is never shown — it is always consumed.&lt;/strong&gt; Wherever it appears — on a &lt;code&gt;const&lt;/code&gt; field, a &lt;code&gt;static readonly&lt;/code&gt; field initialised from a constant, or an optional parameter — &lt;a href="https://delta1labs.com/products/glass/" rel="noopener noreferrer"&gt;Glass.NET&lt;/a&gt; reads the five arguments, reassembles the &lt;code&gt;System.Decimal&lt;/code&gt;, and emits the literal the author wrote: &lt;code&gt;const decimal TaxRate = 0.075m;&lt;/code&gt;, &lt;code&gt;decimal rate = 0.075m&lt;/code&gt;. The attribute itself is suppressed, exactly as the &lt;code&gt;[Extension]&lt;/code&gt; attribute on an extension method or the &lt;code&gt;[IsReadOnly]&lt;/code&gt; modreq on a &lt;code&gt;readonly struct&lt;/code&gt; is suppressed — these are the compiler's bookkeeping, not the source.&lt;/p&gt;

&lt;p&gt;It is a small feature with an outsized failure mode. Get it wrong and every monetary constant, tax rate and threshold in a decompiled financial app comes back as &lt;code&gt;new decimal(75, 0, 0, false, 3)&lt;/code&gt; or, worse, as an attribute the programmer never wrote — technically correct, humanly unreadable. Get it right and &lt;code&gt;0.075m&lt;/code&gt; reads back as &lt;code&gt;0.075m&lt;/code&gt;, which, for the one numeric type that has no native home in the metadata, is the whole job.&lt;/p&gt;

&lt;p&gt;See how Glass handles the other values the compiler hides in metadata rather than IL in &lt;a href="https://delta1labs.com/blog/decompile-optional-named-arguments-default-values-constant-table/" rel="noopener noreferrer"&gt;decompiling optional and named arguments&lt;/a&gt; and &lt;a href="https://delta1labs.com/blog/decompile-array-initializers-runtimehelpers-initializearray/" rel="noopener noreferrer"&gt;decompiling an array initializer&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>glassnet</category>
      <category>decompilation</category>
      <category>dotnet</category>
      <category>deepdive</category>
    </item>
    <item>
      <title>Obfuscating a COM-visible .NET assembly: the GUID is the contract, the DispId is the contract, and the class name is the ProgID unless you pin it</title>
      <dc:creator>Zero Heartbeat</dc:creator>
      <pubDate>Fri, 16 Oct 2026 07:59:57 +0000</pubDate>
      <link>https://dev.to/zero_heartbeat_06a3625d7a/obfuscating-a-com-visible-net-assembly-the-guid-is-the-contract-the-dispid-is-the-contract-and-4fhd</link>
      <guid>https://dev.to/zero_heartbeat_06a3625d7a/obfuscating-a-com-visible-net-assembly-the-guid-is-the-contract-the-dispid-is-the-contract-and-4fhd</guid>
      <description>&lt;p&gt;A COM-visible .NET assembly is a strange thing to obfuscate because its most important consumer does not know it is .NET. A VBA macro, a VBScript in a scheduled task, a twenty-year-old C++ host — they reach your assembly through COM, and the contract they bind to is not the public C# API a developer would protect. It is three pieces of metadata living in three different places: a &lt;strong&gt;GUID&lt;/strong&gt; on the class and the interface, a &lt;strong&gt;DispId&lt;/strong&gt; on each member, and a &lt;strong&gt;ProgID&lt;/strong&gt; that, unless you say otherwise, is just your type's namespace-qualified name. Obfuscate it like an ordinary library and the GUIDs come through untouched — but the ProgID moves the instant you rename the class, and a late-bound client breaks on both the moved ProgID and the renamed member names.&lt;/p&gt;

&lt;p&gt;This post reads what a COM client resolves at each hop, draws the line for &lt;a href="https://delta1labs.com/products/nebula" rel="noopener noreferrer"&gt;Nebula&lt;/a&gt;, and shows the layout that keeps the pinned surface tiny.&lt;/p&gt;

&lt;h2&gt;
  
  
  The assembly as COM sees it
&lt;/h2&gt;

&lt;p&gt;Here is the shape most COM-visible components ship — an explicit &lt;code&gt;IDispatch&lt;/code&gt; interface and a coclass that implements it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.Runtime.InteropServices&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Acme.Automation.Engine&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// the real logic — renames wholesale&lt;/span&gt;

&lt;span class="k"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;Acme.Automation&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;ComVisible&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Guid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"8d9b3a2e-6c41-4f7a-9b21-2f0c7e5a1d44"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;                 &lt;span class="c1"&gt;// IID — pinned, survives anyway&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;InterfaceType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ComInterfaceType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InterfaceIsIDispatch&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;IWidget&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;DispId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Caption&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;                  &lt;span class="c1"&gt;// member name + DispId = contract&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;DispId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;    &lt;span class="nf"&gt;Resize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;ComVisible&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Guid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"1f6e0c74-3a8b-4d2e-8c15-9a7b4e3f0c21"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;                 &lt;span class="c1"&gt;// CLSID — pinned, survives anyway&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;ClassInterface&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ClassInterfaceType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;                     &lt;span class="c1"&gt;// no autogenerated interface&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;ProgId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Acme.Automation.Widget"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;                            &lt;span class="c1"&gt;// pin the ProgID — see below&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Widget&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IWidget&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;WidgetEngine&lt;/span&gt; &lt;span class="n"&gt;_engine&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;WidgetEngine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Open&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;   &lt;span class="c1"&gt;// private: renames freely&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Caption&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Caption&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Caption&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;Resize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Resize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Registration turns that into registry entries with &lt;code&gt;regasm&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;regasm Acme.Automation.dll /tlb:Acme.Automation.tlb /codebase
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;regasm&lt;/code&gt; writes three things that matter: a &lt;code&gt;ProgID → CLSID&lt;/code&gt; mapping, a &lt;code&gt;CLSID → assembly&lt;/code&gt; mapping, and (with &lt;code&gt;/tlb&lt;/code&gt;) a type library describing the interface. Every one of those is keyed by the &lt;strong&gt;GUIDs&lt;/strong&gt;, which is why they survive renaming — an attribute GUID is a string constant, and &lt;a href="https://delta1labs.com/blog/obfuscate-custom-attributes-metadata" rel="noopener noreferrer"&gt;renaming never touches a constant&lt;/a&gt;. The parts that are &lt;em&gt;not&lt;/em&gt; keyed by GUID are the ProgID and the member names, and those are the whole job.&lt;/p&gt;

&lt;h2&gt;
  
  
  How a late-bound client binds — and where it breaks
&lt;/h2&gt;

&lt;p&gt;Walk a script through the hops. &lt;code&gt;CreateObject("Acme.Automation.Widget")&lt;/code&gt; does this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Look up the &lt;strong&gt;ProgID&lt;/strong&gt; &lt;code&gt;Acme.Automation.Widget&lt;/code&gt; in the registry → get a &lt;strong&gt;CLSID&lt;/strong&gt; (the coclass GUID).&lt;/li&gt;
&lt;li&gt;Instantiate the CLSID → get an &lt;code&gt;IDispatch&lt;/code&gt; pointer.&lt;/li&gt;
&lt;li&gt;For each call, &lt;code&gt;GetIDsOfNames("Resize")&lt;/code&gt; → a &lt;strong&gt;DispId&lt;/strong&gt;; then &lt;code&gt;Invoke(dispId, args)&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Three name-based lookups, and obfuscation can poison two of them. The ProgID in step 1 &lt;strong&gt;defaults to the type's namespace-qualified name&lt;/strong&gt; — &lt;code&gt;Acme.Automation.Widget&lt;/code&gt;. Rename the class to &lt;code&gt;a&lt;/code&gt; and the namespace to &lt;code&gt;b&lt;/code&gt;, and &lt;code&gt;regasm&lt;/code&gt; would now register &lt;code&gt;b.a&lt;/code&gt;; the script still asks for &lt;code&gt;Acme.Automation.Widget&lt;/code&gt;, finds nothing, and dies with &lt;em&gt;ActiveX component can't create object&lt;/em&gt;. The member name in step 3 &lt;strong&gt;is your CLR method name&lt;/strong&gt;; rename &lt;code&gt;Resize&lt;/code&gt; to &lt;code&gt;a&lt;/code&gt; and &lt;code&gt;GetIDsOfNames("Resize")&lt;/code&gt; returns &lt;code&gt;DISP_E_UNKNOWNNAME&lt;/code&gt; — a run-time failure with no build error, exactly like a &lt;a href="https://delta1labs.com/blog/obfuscate-powershell-binary-module-cmdlet-parameters" rel="noopener noreferrer"&gt;renamed PowerShell parameter&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The layout that shrinks the pinned surface
&lt;/h2&gt;

&lt;p&gt;Two moves make most of the assembly rename freely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pin the ProgID with &lt;code&gt;[ProgId]&lt;/code&gt;.&lt;/strong&gt; The default ProgID is the type's namespace-qualified name, which chains the client's &lt;code&gt;CreateObject&lt;/code&gt; string to a name you want to obfuscate. Put &lt;code&gt;[ProgId("Acme.Automation.Widget")]&lt;/code&gt; on the coclass — as in the sample above — and the ProgID becomes a pinned attribute constant that no longer depends on the class name. Now Nebula can rename &lt;code&gt;Widget&lt;/code&gt; to anything and the namespace with it; &lt;code&gt;regasm&lt;/code&gt; reads the ProgID off the attribute, and the script's &lt;code&gt;CreateObject&lt;/code&gt; still resolves.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Put the contract on an explicit interface, the logic in an engine.&lt;/strong&gt; With &lt;code&gt;[ClassInterface(ClassInterfaceType.None)]&lt;/code&gt;, COM sees only the members of &lt;code&gt;IWidget&lt;/code&gt; — so the &lt;em&gt;only&lt;/em&gt; member names that are contract are the interface's. The coclass can hold private fields and helpers that rename freely, and the real work lives in a separate &lt;code&gt;Acme.Automation.Engine&lt;/code&gt; namespace with no COM attributes at all, which renames, string-encrypts and control-flow-flattens like any other library:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Acme.Automation.Engine — no COM, no public contract, renames wholesale.&lt;/span&gt;
&lt;span class="k"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;Acme.Automation.Engine&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;internal&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;WidgetEngine&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;internal&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;WidgetEngine&lt;/span&gt; &lt;span class="nf"&gt;Open&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* licensing gate, state load */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;internal&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Caption&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;internal&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;Resize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* the real algorithm */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The kept surface is now exactly: the interface member names, their &lt;code&gt;DispId&lt;/code&gt;s, the two GUIDs, and the ProgID string. Everything worth protecting is on the other side of the interface.&lt;/p&gt;

&lt;h2&gt;
  
  
  The config
&lt;/h2&gt;

&lt;p&gt;Keep the interface by name (so its member names survive), keep the coclass type if you rely on the &lt;em&gt;default&lt;/em&gt; ProgID — but with &lt;code&gt;[ProgId]&lt;/code&gt; pinned you can rename it — preserve the interop attributes, and rename everything else:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"inputs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"bin/Release/net48/Acme.Automation.dll"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"outputDirectory"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"protected"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="nl"&gt;"preservePublicApi"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"encryptStrings"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"controlFlowObfuscation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="nl"&gt;"renaming"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"rules"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"pattern"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Acme.Automation.IWidget"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"keep"&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"pattern"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Acme.Automation.Widget"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nl"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"rename"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"pattern"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Acme.Automation.Engine.*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"rename"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"preserveAttributes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"System.Runtime.InteropServices.*"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="nl"&gt;"controlFlow"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"exclude"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Acme.Automation.IWidget"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three notes. The &lt;code&gt;keep&lt;/code&gt; rule is on the &lt;strong&gt;interface&lt;/strong&gt;, not the coclass — the interface member names are what late binding resolves, and keeping the interface keeps &lt;code&gt;Caption&lt;/code&gt; and &lt;code&gt;Resize&lt;/code&gt;. The coclass is set to &lt;code&gt;rename&lt;/code&gt; deliberately, which is only safe &lt;em&gt;because&lt;/em&gt; &lt;code&gt;[ProgId]&lt;/code&gt; is pinned; drop that attribute and you must change this rule to &lt;code&gt;keep&lt;/code&gt; or &lt;code&gt;CreateObject&lt;/code&gt; breaks. &lt;code&gt;preserveAttributes&lt;/code&gt; guards &lt;code&gt;[Guid]&lt;/code&gt;, &lt;code&gt;[DispId]&lt;/code&gt;, &lt;code&gt;[ComVisible]&lt;/code&gt;, &lt;code&gt;[InterfaceType]&lt;/code&gt;, &lt;code&gt;[ClassInterface]&lt;/code&gt; and &lt;code&gt;[ProgId]&lt;/code&gt; against any attribute-stripping pass — losing &lt;code&gt;[ComVisible]&lt;/code&gt; makes the type invisible to &lt;code&gt;regasm&lt;/code&gt; with no error, and losing &lt;code&gt;[DispId]&lt;/code&gt; makes the members renumber, which silently reorders an early-bound client's vtable. String encryption is safe throughout: the ProgID and GUID are attribute arguments and are not touched, and &lt;code&gt;GetIDsOfNames&lt;/code&gt; reflects on the real member names in metadata, not on any string literal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verify by registering and calling — in both binding styles
&lt;/h2&gt;

&lt;p&gt;Every failure here is a run-time bind miss, so register the &lt;em&gt;protected&lt;/em&gt; assembly and exercise it both ways. Late-bound, through the ProgID and &lt;code&gt;IDispatch&lt;/code&gt; names:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;' late-bound.vbs — exercises the ProgID (step 1) and member names (step 3)
Set w = CreateObject("Acme.Automation.Widget")   ' ProgID must still resolve
w.Caption = "Report"                             ' GetIDsOfNames("Caption")
WScript.Echo w.Resize(800, 600)                  ' GetIDsOfNames("Resize")
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Early-bound, through the type library — this exercises the GUIDs and the &lt;code&gt;DispId&lt;/code&gt;s, binding by vtable with no names at run time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Reference the generated interop (tlbimp / COM reference), then:&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;Acme&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Automation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Widget&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;            &lt;span class="c1"&gt;// CLSID GUID&lt;/span&gt;
&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Caption&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Report"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                            &lt;span class="c1"&gt;// DispId 1 via vtable&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Resize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;800&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;600&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;                      &lt;span class="c1"&gt;// DispId 2 via vtable&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Register the protected DLL with &lt;code&gt;regasm protected\Acme.Automation.dll /tlb /codebase&lt;/code&gt;, run both clients in the &lt;strong&gt;bitness you ship&lt;/strong&gt; (a 32-bit host needs the 32-bit &lt;code&gt;regasm&lt;/code&gt; and the 32-bit registry view), and unregister with &lt;code&gt;regasm /u&lt;/code&gt; afterwards so you are never testing against stale entries. If the assembly is strong-named, &lt;a href="https://delta1labs.com/blog/re-sign-obfuscated-assembly-strong-name-authenticode" rel="noopener noreferrer"&gt;re-sign after obfuscation&lt;/a&gt;. An assembly that merely registers is not proven; one that &lt;code&gt;CreateObject&lt;/code&gt;s &lt;em&gt;and&lt;/em&gt; dispatches in both styles is.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;A COM-visible assembly's contract is not its C# surface. The GUIDs on the class and interface survive obfuscation because they are attribute constants, and the registry is keyed by them — that much is free. What is not free is the ProgID, which defaults to the namespace-qualified class name and therefore moves the moment you rename the class, and the member names a late-bound &lt;code&gt;IDispatch&lt;/code&gt; client passes to &lt;code&gt;GetIDsOfNames&lt;/code&gt;, which are your CLR method and property names. Pin the ProgID with &lt;code&gt;[ProgId]&lt;/code&gt; so the class can rename, keep the interface so its member names survive, pin the &lt;code&gt;DispId&lt;/code&gt;s so early-bound vtables never renumber, preserve the &lt;code&gt;InteropServices&lt;/code&gt; attributes, and push the real work into an engine namespace that renames wholesale. Then prove it by registering the protected assembly and calling it both late-bound through the ProgID and early-bound through the type library — because the build will not tell you which name you broke.&lt;/p&gt;

</description>
      <category>nebulanet</category>
      <category>obfuscation</category>
      <category>dotnet</category>
      <category>deepdive</category>
    </item>
    <item>
      <title>Obfuscating an app that registers itself by convention: assembly-scanning DI binds on type names, so renaming unregisters your services</title>
      <dc:creator>Zero Heartbeat</dc:creator>
      <pubDate>Fri, 16 Oct 2026 07:59:49 +0000</pubDate>
      <link>https://dev.to/zero_heartbeat_06a3625d7a/obfuscating-an-app-that-registers-itself-by-convention-assembly-scanning-di-binds-on-type-names-2aml</link>
      <guid>https://dev.to/zero_heartbeat_06a3625d7a/obfuscating-an-app-that-registers-itself-by-convention-assembly-scanning-di-binds-on-type-names-2aml</guid>
      <description>&lt;p&gt;A great many .NET applications never actually write down their service registrations. They scan. At startup an app sweeps an assembly, finds every type that looks like a service — by a naming convention, usually — and registers it. &lt;a href="https://delta1labs.com/blog/obfuscation-reflection-serialization-pitfalls" rel="noopener noreferrer"&gt;Scrutor&lt;/a&gt; made this a one-liner, and hand-rolled reflection loops did it long before that. It is tidy, it is convention-over-configuration, and it has a specific, quiet failure mode under obfuscation: the convention is almost always a &lt;em&gt;string match against the type name&lt;/em&gt;, and renaming changes every name.&lt;/p&gt;

&lt;p&gt;The result is the worst shape a bug can take. The build is green — the scan is valid code. The app starts — the scan runs without error. And then the first request asks the container for &lt;code&gt;IOrderService&lt;/code&gt;, the container has never heard of it, and you get &lt;code&gt;Unable to resolve service for type 'IOrderService'&lt;/code&gt; pointing at the controller, nowhere near the scan that silently registered nothing. This post is about why that happens for &lt;a href="https://delta1labs.com/products/nebula" rel="noopener noreferrer"&gt;Nebula&lt;/a&gt;, and the one-line change that makes it stop.&lt;/p&gt;

&lt;h2&gt;
  
  
  The registration that breaks
&lt;/h2&gt;

&lt;p&gt;Here is the shape that gets written most often — scan the assembly, take everything whose name ends in a convention word, register it against its interfaces:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Scan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scan&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;scan&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FromAssemblyOf&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;OrderService&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddClasses&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;EndsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Service"&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;   &lt;span class="c1"&gt;// &amp;lt;-- string match on the name&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AsImplementedInterfaces&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithScopedLifetime&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read what that predicate actually does: at startup it enumerates the assembly's types and keeps the ones whose &lt;code&gt;Name&lt;/code&gt; ends in the seven characters &lt;code&gt;S-e-r-v-i-c-e&lt;/code&gt;. Before obfuscation that is &lt;code&gt;OrderService&lt;/code&gt;, &lt;code&gt;PricingService&lt;/code&gt;, &lt;code&gt;InventoryService&lt;/code&gt;. After Nebula renames them to &lt;code&gt;a&lt;/code&gt;, &lt;code&gt;b&lt;/code&gt;, &lt;code&gt;c&lt;/code&gt;, the predicate asks each of them &lt;em&gt;"does your name end in 'Service'?"&lt;/em&gt; and each answers &lt;em&gt;no&lt;/em&gt;. The scan keeps zero types, &lt;code&gt;AsImplementedInterfaces&lt;/code&gt; has nothing to bind, and &lt;code&gt;IOrderService&lt;/code&gt; is never registered.&lt;/p&gt;

&lt;p&gt;Nothing in that is a bug the compiler or the obfuscator could catch. The predicate is valid. The rename did exactly what it was told. The two facts are simply incompatible, and the incompatibility only becomes visible when something tries to resolve a service that the empty scan never added.&lt;/p&gt;

&lt;h2&gt;
  
  
  The distinction that matters: name vs shape
&lt;/h2&gt;

&lt;p&gt;Not every scan breaks. The difference is whether the scan finds a type by its &lt;strong&gt;name&lt;/strong&gt; or by its &lt;strong&gt;shape&lt;/strong&gt;, and that line is the whole of it. A name is a string that renaming rewrites — and the string literal in your predicate does &lt;em&gt;not&lt;/em&gt; get rewritten with it, so the comparison goes stale. A shape — &lt;em&gt;implements this interface, carries this attribute, derives from this base&lt;/em&gt; — is a relationship in the metadata graph, and renaming is a &lt;em&gt;consistent&lt;/em&gt; rewrite of that graph: it renames the interface and the implementing type together, so "implements" still holds.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// SURVIVES — matches on the implements-relationship, a metadata token identity&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddClasses&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AssignableTo&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ICommandHandler&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;())&lt;/span&gt;

&lt;span class="c1"&gt;// SURVIVES — matches on an attribute type token; renaming keeps attribute and target in sync&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddClasses&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithAttribute&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ServiceAttribute&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;())&lt;/span&gt;

&lt;span class="c1"&gt;// BREAKS — matches on a frozen string the rename cannot reach&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddClasses&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;EndsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Handler"&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;

&lt;span class="c1"&gt;// BREAKS WORSE — resolves a type from a name string that no longer names anything&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddSingleton&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Acme.Orders.OrderService"&lt;/span&gt;&lt;span class="p"&gt;)!)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix is one line, and it buys back the names
&lt;/h2&gt;

&lt;p&gt;Give the convention a &lt;em&gt;marker&lt;/em&gt; and scan for that instead of for the spelling. A marker interface is the cleanest, because it costs nothing at run time and says exactly what the convention meant:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;IAppService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;                    &lt;span class="c1"&gt;// marker; renaming keeps it resolvable by token&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderService&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IAppService&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;IOrderService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Scan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scan&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;scan&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FromAssemblyOf&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;OrderService&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddClasses&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AssignableTo&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IAppService&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;())&lt;/span&gt;  &lt;span class="c1"&gt;// &amp;lt;-- shape, not name&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AsImplementedInterfaces&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithScopedLifetime&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the scan asks each type &lt;em&gt;"do you implement &lt;code&gt;IAppService&lt;/code&gt;?"&lt;/em&gt;, which the metadata answers correctly before and after obfuscation. Nebula is free to rename &lt;code&gt;OrderService&lt;/code&gt;, &lt;code&gt;IOrderService&lt;/code&gt; and every other type in the layer, because the scan no longer reads a single name — the only thing it depends on is the &lt;em&gt;implements&lt;/em&gt; relationship, and renaming preserves that by construction. You get the obfuscation you wanted &lt;em&gt;and&lt;/em&gt; the convention registration you wanted, with no overlap between them.&lt;/p&gt;

&lt;p&gt;An attribute works identically if you prefer not to add an interface to the type's hierarchy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Service&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ServiceLifetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Scoped&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PricingService&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IPricingService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddClasses&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithAttribute&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ServiceAttribute&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The attribute is matched by its type token, which the rename keeps consistent on the attribute class and on every type decorated with it.&lt;/p&gt;

&lt;h2&gt;
  
  
  When you cannot change the scan
&lt;/h2&gt;

&lt;p&gt;Sometimes the scan is in a library you do not own, and it filters on a name you cannot influence. The escape hatch is to keep the names the scan relies on — but keep them &lt;em&gt;narrowly&lt;/em&gt;, and understand the cost. A Nebula config excludes just the convention-named types from renaming:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"inputs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"bin/Release/net8.0/Acme.Orders.dll"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"outputDirectory"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"protected"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="nl"&gt;"encryptStrings"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"controlFlowObfuscation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="nl"&gt;"renaming"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"rules"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"pattern"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*Service"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nl"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"keep"&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"pattern"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*Handler"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nl"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"keep"&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"pattern"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*Repository"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"keep"&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"pattern"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Acme.Orders.*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"rename"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Be clear-eyed about the trade: every &lt;code&gt;keep&lt;/code&gt; rule here leaves a class name in the metadata, and the set of names you are exposing is precisely your service layer — the map of what the app does. The earlier the rule sits, the more it wins over the broad &lt;code&gt;rename&lt;/code&gt;, so a &lt;code&gt;*Service&lt;/code&gt; keep pins every service's name. That is why the refactor is the better answer whenever the scan is yours: switching one predicate from &lt;code&gt;Name.EndsWith&lt;/code&gt; to &lt;code&gt;AssignableTo&lt;/code&gt; turns the whole &lt;code&gt;keep&lt;/code&gt; block back into &lt;code&gt;rename&lt;/code&gt; and hands the names back to the obfuscator. Use the exclude only for the third-party scan you genuinely cannot reach.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fail fast instead of at the first request
&lt;/h2&gt;

&lt;p&gt;The reason this bug is expensive is distance — the empty scan and the failed resolution are far apart. Close the distance with a startup smoke test that asserts the services you expect are actually registered, and run it against the &lt;em&gt;obfuscated&lt;/em&gt; build, because that is the only build where the scan can come up empty:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Fact&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Expected_services_are_registered_after_scanning&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;provider&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;BuildProductionServiceProvider&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;   &lt;span class="c1"&gt;// same Scan(...) as the app&lt;/span&gt;

    &lt;span class="c1"&gt;// Resolve the way the app will — if the scan registered nothing, this throws here,&lt;/span&gt;
    &lt;span class="c1"&gt;// in a test, instead of on the first production request.&lt;/span&gt;
    &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NotNull&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetService&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IOrderService&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;());&lt;/span&gt;
    &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NotNull&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetService&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IPricingService&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;());&lt;/span&gt;

    &lt;span class="c1"&gt;// Or assert the count the convention should have produced:&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;handlers&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetServices&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ICommandHandler&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;().&lt;/span&gt;&lt;span class="nf"&gt;ToList&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;handlers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Count&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;BuildServiceProvider(validateOnBuild: true)&lt;/code&gt; will &lt;em&gt;not&lt;/em&gt; catch this on its own — it validates that the registrations present can be constructed, and a missing registration is simply absent, not invalid. You have to assert the &lt;em&gt;presence&lt;/em&gt; of what the scan should have found. Run this test over the protected assembly in CI and a scan that renaming emptied turns red in the build, which is exactly where you want it, rather than in a production stack trace that blames the controller.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;Convention-based DI registration is a string match against type names far more often than it looks, and renaming obfuscation changes every one of those names while leaving the string literal in your predicate frozen — so the scan quietly registers nothing and the app fails at the first resolution, a green build away from the cause. The fix is to make the scan match on &lt;em&gt;shape&lt;/em&gt; rather than &lt;em&gt;spelling&lt;/em&gt;: &lt;code&gt;AssignableTo&lt;/code&gt; a marker interface or &lt;code&gt;WithAttribute&lt;/code&gt; a marker attribute, both of which renaming rewrites consistently across the metadata graph, so the scan keeps finding your types no matter what they are called. That one change lets Nebula rename your entire service layer — the names you obfuscated to hide — while the registration keeps working. When the scan is not yours to change, pin just the convention-named types with narrow &lt;code&gt;keep&lt;/code&gt; rules and accept that you are leaving those names visible. Either way, prove it with a startup test that resolves the expected services against the obfuscated build, so an emptied scan fails in CI and never in production.&lt;/p&gt;

</description>
      <category>nebulanet</category>
      <category>obfuscation</category>
      <category>dotnet</category>
      <category>deepdive</category>
    </item>
    <item>
      <title>Logging a license check: structured events an operator can read, and never the key itself</title>
      <dc:creator>Zero Heartbeat</dc:creator>
      <pubDate>Fri, 16 Oct 2026 07:59:02 +0000</pubDate>
      <link>https://dev.to/zero_heartbeat_06a3625d7a/logging-a-license-check-structured-events-an-operator-can-read-and-never-the-key-itself-c04</link>
      <guid>https://dev.to/zero_heartbeat_06a3625d7a/logging-a-license-check-structured-events-an-operator-can-read-and-never-the-key-itself-c04</guid>
      <description>&lt;p&gt;A license check is the single event that decides whether your software is allowed to run tonight, and it is almost always logged in one of two wrong ways. Either it is logged as &lt;em&gt;nothing&lt;/em&gt; — the verification happens, succeeds or fails silently, and the first anyone hears of a lapsed key is a user complaint — or it is logged as a &lt;em&gt;disaster&lt;/em&gt;: &lt;code&gt;log.LogInformation("license: {@License}", info)&lt;/code&gt;, which serialises the whole object and writes the signed key, the raw signature and the licensee's email into a file that will be shipped to an aggregator and pasted into a support ticket by lunchtime.&lt;/p&gt;

&lt;p&gt;This post builds the version in between: one structured, queryable event per verification, emitted so that an operator can read it, alert on it, and correlate it — and so that the key itself never leaves the process. It is about logging a &lt;a href="https://delta1labs.com/products/keyright" rel="noopener noreferrer"&gt;Keyright&lt;/a&gt; verification the way you would log an auth event, because that is what it is.&lt;/p&gt;

&lt;h2&gt;
  
  
  The thing you are verifying
&lt;/h2&gt;

&lt;p&gt;Keyright's &lt;code&gt;Validate()&lt;/code&gt; returns a &lt;code&gt;LicenseInfo&lt;/code&gt; with a &lt;code&gt;Status&lt;/code&gt;, a &lt;code&gt;Licensee&lt;/code&gt;, an expiry and the derived &lt;code&gt;DaysRemaining&lt;/code&gt;/&lt;code&gt;IsExpiringSoon&lt;/code&gt; that the renewal and health-check work already lean on:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;LicenseInfo&lt;/span&gt; &lt;span class="n"&gt;info&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;licenseKey&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// info.Status        -&amp;gt; LicenseStatus (Valid, Expired, Revoked, SignatureInvalid, ...)&lt;/span&gt;
&lt;span class="c1"&gt;// info.Licensee      -&amp;gt; "Acme GmbH"&lt;/span&gt;
&lt;span class="c1"&gt;// info.ExpiryUtc     -&amp;gt; DateTime?&lt;/span&gt;
&lt;span class="c1"&gt;// info.DaysRemaining -&amp;gt; int&lt;/span&gt;
&lt;span class="c1"&gt;// info.IsExpiringSoon-&amp;gt; bool&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;licenseKey&lt;/code&gt; in that call is the thing that must never reach the log. Everything else on &lt;code&gt;info&lt;/code&gt; is fair to record; the key and the signature are not. So the first design decision is to compute a &lt;strong&gt;fingerprint&lt;/strong&gt; of the key once, and pass &lt;em&gt;that&lt;/em&gt; to the logger — never the key.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.Security.Cryptography&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.Text&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nf"&gt;Fingerprint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;licenseKey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// A stable, non-reversible handle: first 8 hex of SHA-256 over the key.&lt;/span&gt;
    &lt;span class="c1"&gt;// Enough to say "same key as the one in the ticket?" — not enough to be the key.&lt;/span&gt;
    &lt;span class="n"&gt;Span&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;hash&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;stackalloc&lt;/span&gt; &lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;32&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="n"&gt;SHA256&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;HashData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Encoding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UTF8&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;licenseKey&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Convert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToHexString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;)[..&lt;/span&gt;&lt;span class="m"&gt;8&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;ToLowerInvariant&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;   &lt;span class="c1"&gt;// e.g. "9f3a1c20"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your payload carries a key-id claim, prefer that — it is already a non-secret handle. The fingerprint is the fallback for keys that do not.&lt;/p&gt;

&lt;h2&gt;
  
  
  One event, declared once
&lt;/h2&gt;

&lt;p&gt;The redaction rule is only as good as the number of places it has to hold. Spread the logging across a dozen &lt;code&gt;log.LogInformation("...")&lt;/code&gt; call sites and the one that passes &lt;code&gt;licenseKey&lt;/code&gt; instead of &lt;code&gt;fingerprint&lt;/code&gt; is the leak. So declare the event &lt;em&gt;once&lt;/em&gt;, as a source-generated &lt;code&gt;LoggerMessage&lt;/code&gt;, whose signature makes the raw key unpassable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Microsoft.Extensions.Logging&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;partial&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;LicenseLog&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Note the parameters: fingerprint, status, licensee, days — never the key.&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;LoggerMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;EventId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;4000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Level&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;LogLevel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Information&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Message&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"License verified: {Status} for {Licensee} (key {Fingerprint}), {DaysRemaining} day(s) remaining"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;partial&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Verified&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt; &lt;span class="n"&gt;ILogger&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;licensee&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;fingerprint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;daysRemaining&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;LoggerMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;EventId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;4001&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Level&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;LogLevel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Warning&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Message&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"License expiring: {Status} for {Licensee} (key {Fingerprint}) expires in {DaysRemaining} day(s)"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;partial&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Expiring&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt; &lt;span class="n"&gt;ILogger&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;licensee&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;fingerprint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;daysRemaining&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;LoggerMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;EventId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;4002&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Level&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;LogLevel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Message&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"License invalid: {Status} for {Licensee} (key {Fingerprint})"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;partial&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Invalid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt; &lt;span class="n"&gt;ILogger&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;licensee&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;fingerprint&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The source generator turns each of these into a zero-allocation method that checks the level, writes the named fields and formats the message only if a sink wants it. There is no &lt;code&gt;licenseKey&lt;/code&gt; parameter on any of them, so a caller &lt;em&gt;cannot&lt;/em&gt; log the key through this type even by accident — the compiler stops them. That is the whole reason to prefer the generator over a free-form template: the rule is now a method signature, not a code-review convention.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mapping status to level
&lt;/h2&gt;

&lt;p&gt;The dispatch from &lt;code&gt;LicenseStatus&lt;/code&gt; to one of those three events is the translation table, and it lines up with how alerting already works — Information is quiet, Warning is noticed, Error is counted and paged:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;LogLicense&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt; &lt;span class="n"&gt;ILogger&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;LicenseInfo&lt;/span&gt; &lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;fingerprint&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;licensee&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Licensee&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="s"&gt;"(none)"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Status&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;LicenseStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Valid&lt;/span&gt; &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsExpiringSoon&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Expiring&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;licensee&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fingerprint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DaysRemaining&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;LicenseStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Valid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Verified&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;licensee&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fingerprint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DaysRemaining&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="c1"&gt;// Expired, Revoked, SignatureInvalid, MachineMismatch, ClockTampered, NoLicense&lt;/span&gt;
            &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Invalid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;licensee&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fingerprint&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;/p&gt;

&lt;p&gt;A JSON console sink then renders the healthy case like this — note that the key is nowhere in it, only its fingerprint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Timestamp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-10-16T09:00:01.42Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"LogLevel"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Information"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"EventId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"License verified: Valid for Acme GmbH (key 9f3a1c20), 48 day(s) remaining"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"State"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"Status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Valid"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"Licensee"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Acme GmbH"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"Fingerprint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"9f3a1c20"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"DaysRemaining"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;48&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because &lt;code&gt;Status&lt;/code&gt;, &lt;code&gt;Fingerprint&lt;/code&gt; and &lt;code&gt;DaysRemaining&lt;/code&gt; are named structured fields and not just text baked into the message, a log backend can graph "distinct fingerprints seen this week" or alert on "EventId 4002 count &amp;gt; 0" without parsing the message string. That is the dividend of the structured form: the event is &lt;em&gt;queryable&lt;/em&gt;, not just readable.&lt;/p&gt;

&lt;h2&gt;
  
  
  A scope so the rest of the logs correlate
&lt;/h2&gt;

&lt;p&gt;The verification is one line, but the work that follows it — a background lease renewal, an entitlement check, a feature gate — produces its own logs, and an incident reviewer wants to tie them to the license that was in force. Open a logging scope keyed by the fingerprint around that work, and every line inside it carries the field automatically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;BeginScope&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;Dictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;object&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"LicenseKey"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fingerprint&lt;/span&gt; &lt;span class="p"&gt;}))&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogLicense&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fingerprint&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// ... anything logged in here also carries LicenseKey=9f3a1c20 ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now a &lt;code&gt;SignatureInvalid&lt;/code&gt; at 03:00 and a failed entitlement check at 03:01 share a &lt;code&gt;LicenseKey&lt;/code&gt; field, and the reviewer can pull the whole story with one filter. The scope carries the &lt;em&gt;fingerprint&lt;/em&gt;, never the key — the same rule, applied to the correlation handle.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prove the key never escapes
&lt;/h2&gt;

&lt;p&gt;The one assertion worth writing is the negative one: that the raw key does not appear in anything the logger emits. &lt;code&gt;Microsoft.Extensions.Diagnostics.Testing&lt;/code&gt; ships a &lt;code&gt;FakeLogger&lt;/code&gt; that captures records in memory, so the test is direct:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Fact&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Verification_never_logs_the_raw_key&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;fake&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;FakeLogger&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;LicenseService&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
    &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"KR1-REALKEY-abcdef ..."&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;          &lt;span class="c1"&gt;// the actual signed key&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;info&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;LicenseInfo&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;Status&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;LicenseStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Valid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Licensee&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Acme"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DaysRemaining&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;48&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="n"&gt;fake&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogLicense&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;Fingerprint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

    &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;FakeLogRecord&lt;/span&gt; &lt;span class="n"&gt;rec&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;fake&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Collector&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetSnapshot&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;DoesNotContain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;kv&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StructuredState&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt;
            &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;DoesNotContain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;kv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;    &lt;span class="c1"&gt;// check the structured fields too&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Assert on both the rendered message &lt;em&gt;and&lt;/em&gt; the structured state, because a leak can hide in either — a stray &lt;code&gt;{Key}&lt;/code&gt; in a template shows up in the message, a &lt;code&gt;BeginScope&lt;/code&gt; that captured the key shows up in the state. This test is the thing that keeps the rule true as the code grows: the day someone adds a fourth event that passes the key, it goes red.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;A license check deserves to be logged like the auth event it is: one structured record, not silence and not a data dump. Declare the event once as a source-generated &lt;code&gt;LoggerMessage&lt;/code&gt; whose parameters are a fingerprint and the non-secret fields, so the raw key is unpassable by construction. Map &lt;code&gt;LicenseStatus&lt;/code&gt; onto levels that your existing alerting already understands — Information for valid, Warning for expiring, Error for invalid — and give each a stable &lt;code&gt;EventId&lt;/code&gt; so a dashboard counts failures without string-matching. Emit it on verification and on transitions, not on every request. Wrap the work in a scope keyed by the fingerprint so the rest of the logs correlate. Then write the one test that proves the key never reaches the sink, and let it guard the rule forever. The result is a log an operator can read, a backend can query, and a support ticket can safely carry — because the one thing that must not travel never made it past the first line.&lt;/p&gt;

</description>
      <category>keyright</category>
      <category>licensing</category>
      <category>dotnet</category>
      <category>deepdive</category>
    </item>
  </channel>
</rss>
