{"authors":[{"name":"Martin Stühmer","url":"https://daily-devops.net/authors/martin/"},{"name":"Jendrik Brack","url":"https://daily-devops.net/authors/jendrik/"}],"description":"Recent content in Roslyn on Daily DevOps \u0026 .NET","favicon":"https://daily-devops.net/images/logo_hu_6465d873dfa490cf.png","feed_url":"https://daily-devops.net/tags/roslyn/feed.json","home_page_url":"https://daily-devops.net/tags/roslyn/","icon":"https://daily-devops.net/images/logo_hu_5926de77762241ba.png","items":[{"authors":[{"name":"Martin Stühmer","url":"https://daily-devops.net/authors/martin/"}],"content_html":"\u003cp\u003e\u003ca href=\"/posts/dotnet-source-generators-hidden-costs/\"\u003eThe previous post on source generators\u003c/a\u003e was about the hidden costs — what generators take from you in build time, IDE responsiveness, and Hot Reload. The obvious follow-up question landed in my inbox more than once: \u003cem\u003efine, can we at least do it right?\u003c/em\u003e\u003c/p\u003e\n\u003cp\u003eYes. But \u0026ldquo;right\u0026rdquo; is more specific than the documentation lets on. \u003ccode\u003eIIncrementalGenerator\u003c/code\u003e exists for exactly one reason: to make sure your generator does real work only when its inputs change. Most generators in the wild do not achieve that. They implement the right interface, they compile cleanly, they pass their snapshot tests — and they still redo the full generation work on every edit, because somewhere in the pipeline a value is compared by reference and the cache silently misses.\u003c/p\u003e\n\u003cp\u003eThis post opens a four-part series on getting incrementalism right, and all four parts share one running example: \u003ccode\u003e[AutoToString]\u003c/code\u003e, a small generator that gives a class a \u003ccode\u003eToString\u003c/code\u003e override listing its public properties. This post uses its embryonic form to expose the equality contract; \u003ca href=\"/posts/incremental-source-generators-pipeline-patterns/\"\u003epart 2\u003c/a\u003e builds the generator properly.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-mental-model-a-pipeline-of-equality-checked-values\"\u003e\u003ca href=\"/posts/incremental-source-generators-done-right/#the-mental-model-a-pipeline-of-equality-checked-values\" title=\"The Mental Model: A Pipeline of Equality-Checked Values\"\u003eThe Mental Model: A Pipeline of Equality-Checked Values\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eForget \u0026ldquo;a generator is a method that emits code.\u0026rdquo; An \u003ccode\u003eIIncrementalGenerator\u003c/code\u003e is a dataflow graph. Every node in the pipeline produces a value. The framework caches that value. On the next run, if the node\u0026rsquo;s input is equal to the cached input — compared with \u003ccode\u003eEqualityComparer\u0026lt;T\u0026gt;.Default\u003c/code\u003e — the node does not execute and every node downstream of it stays cached too.\u003c/p\u003e\n\u003cp\u003eThat is the entire architecture. There is no second mechanism. The whole promise of incrementalism rests on your values answering \u003ccode\u003eEquals\u003c/code\u003e correctly. If your equality is wrong, your incrementalism is wrong, and nothing in the compiler will tell you.\u003c/p\u003e\n\u003cp\u003eHere is the smallest honest skeleton — the embryonic \u003ccode\u003eAutoToStringGenerator\u003c/code\u003e, before it knows anything about properties:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e[Generator]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003esealed\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eAutoToStringGenerator\u003c/span\u003e \u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003eIIncrementalGenerator\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003evoid\u003c/span\u003e \u003cspan class=\"n\"\u003eInitialize\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eIncrementalGeneratorInitializationContext\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"c1\"\u003e// Node 1: one string per attributed class. Cached per value.\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eIncrementalValuesProvider\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"kt\"\u003estring\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eclassNames\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSyntaxProvider\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eForAttributeWithMetadataName\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"s\"\u003e\u0026#34;AutoToString.AutoToStringAttribute\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"n\"\u003epredicate\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"kd\"\u003estatic\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003enode\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003e_\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003enode\u003c/span\u003e \u003cspan class=\"k\"\u003eis\u003c/span\u003e \u003cspan class=\"n\"\u003eClassDeclarationSyntax\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"n\"\u003etransform\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"kd\"\u003estatic\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ectx\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003e_\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003ectx\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eTargetSymbol\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eName\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"c1\"\u003e// Node 2: only re-runs for a class whose name actually changed.\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eRegisterSourceOutput\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eclassNames\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kd\"\u003estatic\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003espc\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eclassName\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003espc\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddSource\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e$\u0026#34;{className}.AutoToString.g.cs\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"err\"\u003e$\u003c/span\u003e\u003cspan class=\"s\"\u003e$\u0026#34;\u0026#34;\u0026#34;\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"s\"\u003e                partial class {{className}}\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"s\"\u003e                {\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"s\"\u003e                    public override string ToString() =\u0026gt; \u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e{{\u003c/span\u003e\u003cspan class=\"n\"\u003eclassName\u003c/span\u003e\u003cspan class=\"p\"\u003e}}\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;;\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"s\"\u003e\u0026#34;\u0026#34;\u0026#34;));\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis one is bulletproof — as a \u003cem\u003ecaching\u003c/em\u003e skeleton, and for a boring reason: the only thing flowing between the nodes is a \u003ccode\u003estring\u003c/code\u003e. Strings have value equality. Type a character in an unrelated file, the transform produces the same string, \u003ccode\u003eEqualityComparer\u0026lt;string\u0026gt;.Default\u003c/code\u003e says equal, \u003ccode\u003eRegisterSourceOutput\u003c/code\u003e never runs. (As an \u003ccode\u003eAutoToString\u003c/code\u003e implementation it is deliberately embryonic: it knows only the class name, so the \u003ccode\u003eToString\u003c/code\u003e it emits lists no properties, and because it ignores the containing namespace, the emitted partial only merges with classes in the global namespace. The marker attribute has to come from somewhere too — \u003ca href=\"/posts/incremental-source-generators-pipeline-patterns/\"\u003epart 2\u003c/a\u003e delivers it via \u003ccode\u003eRegisterPostInitializationOutput\u003c/code\u003e and fixes the rest.)\u003c/p\u003e\n\u003cp\u003eThe finished generator does not get away with a single string: a \u003ccode\u003eToString\u003c/code\u003e override worth generating needs the property names too. The moment you carry more than one piece of data per target, you reach for a \u003ccode\u003erecord\u003c/code\u003e with a collection property — and that is where the contract quietly breaks.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-default-you-will-get-wrong-first\"\u003e\u003ca href=\"/posts/incremental-source-generators-done-right/#the-default-you-will-get-wrong-first\" title=\"The Default You Will Get Wrong First\"\u003eThe Default You Will Get Wrong First\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThe obvious next step for \u003ccode\u003eAutoToString\u003c/code\u003e is a record holding the class name and its public property names — the very model the finished generator caches. This first draft looks correct. It compiles. It even feels idiomatic:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// This looks fine. It is not.\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003esealed\u003c/span\u003e \u003cspan class=\"k\"\u003erecord\u003c/span\u003e \u003cspan class=\"nc\"\u003eClassModel\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003eName\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eImmutableArray\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"kt\"\u003estring\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eProperties\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eA \u003ccode\u003erecord\u003c/code\u003e gives you value equality on its members, so \u003ccode\u003eName\u003c/code\u003e is fine. But \u003ccode\u003eImmutableArray\u0026lt;T\u0026gt;\u003c/code\u003e does \u003cstrong\u003enot\u003c/strong\u003e implement value equality. It is a struct wrapping a reference to a \u003ccode\u003eT[]\u003c/code\u003e, and its \u003ccode\u003eEquals\u003c/code\u003e compares that inner array \u003cem\u003ereference\u003c/em\u003e — not the contents. Two arrays holding identical strings are not equal:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003ea\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eImmutableArray\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCreate\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Id\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;Name\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;CreatedAt\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003eb\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eImmutableArray\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCreate\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Id\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;Name\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;CreatedAt\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eConsole\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eWriteLine\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ea\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSequenceEqual\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eb\u003c/span\u003e\u003cspan class=\"p\"\u003e));\u003c/span\u003e \u003cspan class=\"c1\"\u003e// True  - same contents\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eConsole\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eWriteLine\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ea\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eEquals\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eb\u003c/span\u003e\u003cspan class=\"p\"\u003e));\u003c/span\u003e        \u003cspan class=\"c1\"\u003e// False - different underlying arrays\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eConsole\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eWriteLine\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ea\u003c/span\u003e \u003cspan class=\"p\"\u003e==\u003c/span\u003e \u003cspan class=\"n\"\u003eb\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e             \u003cspan class=\"c1\"\u003e// False - same reference comparison\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eNow follow the consequence through the pipeline. Your transform runs on every edit (that part is by design — Roslyn has to re-check what changed). Each run builds a \u003cem\u003efresh\u003c/em\u003e \u003ccode\u003eImmutableArray\u0026lt;string\u0026gt;\u003c/code\u003e from the symbol\u0026rsquo;s members. Fresh array, fresh reference, \u003ccode\u003eEquals\u003c/code\u003e returns \u003ccode\u003efalse\u003c/code\u003e, the record\u0026rsquo;s synthesized equality returns \u003ccode\u003efalse\u003c/code\u003e, the cache misses, and everything downstream — including your string-building, formatting, \u003ccode\u003eAddSource\u003c/code\u003e — re-executes. Every single time. In every project that references your generator.\u003c/p\u003e\n\u003cp\u003eThe generator is still \u0026ldquo;incremental\u0026rdquo; in the sense that it implements the interface. It is non-incremental in every sense that matters. And you will not notice until someone opens a binlog, \u003ca href=\"/posts/dotnet-source-generators-hidden-costs/\"\u003elike I did in the predecessor post\u003c/a\u003e, and finds your generator at the top of the \u003ccode\u003eRunGenerators\u003c/code\u003e list.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-fix-equatablearrayt\"\u003e\u003ca href=\"/posts/incremental-source-generators-done-right/#the-fix-equatablearrayt\" title=\"The Fix: EquatableArray\u0026lt;T\u0026gt;\"\u003eThe Fix: EquatableArray\u0026lt;T\u0026gt;\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThe fix is a small wrapper that gives an array the equality semantics the pipeline needs. This is the centerpiece of part 1 — copy it into your generator project as-is:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eusing\u003c/span\u003e \u003cspan class=\"nn\"\u003eSystem\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eusing\u003c/span\u003e \u003cspan class=\"nn\"\u003eSystem.Collections\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eusing\u003c/span\u003e \u003cspan class=\"nn\"\u003eSystem.Collections.Generic\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eusing\u003c/span\u003e \u003cspan class=\"nn\"\u003eSystem.Collections.Immutable\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eusing\u003c/span\u003e \u003cspan class=\"nn\"\u003eSystem.Runtime.CompilerServices\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"cs\"\u003e/// \u0026lt;summary\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"cs\"\u003e/// An immutable array with value-based equality, safe to use\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"cs\"\u003e/// in incremental generator pipeline models.\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"cs\"\u003e/// \u0026lt;/summary\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003ereadonly\u003c/span\u003e \u003cspan class=\"k\"\u003estruct\u003c/span\u003e \u003cspan class=\"nc\"\u003eEquatableArray\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eT\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003eIEquatable\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eEquatableArray\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eT\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u0026gt;,\u003c/span\u003e \u003cspan class=\"n\"\u003eIEnumerable\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eT\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003ewhere\u003c/span\u003e \u003cspan class=\"n\"\u003eT\u003c/span\u003e \u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003eIEquatable\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eT\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003estatic\u003c/span\u003e \u003cspan class=\"k\"\u003ereadonly\u003c/span\u003e \u003cspan class=\"n\"\u003eEquatableArray\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eT\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eEmpty\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eArray\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eEmpty\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eT\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;());\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"k\"\u003ereadonly\u003c/span\u003e \u003cspan class=\"n\"\u003eT\u003c/span\u003e\u003cspan class=\"p\"\u003e[]?\u003c/span\u003e \u003cspan class=\"n\"\u003e_array\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"n\"\u003eEquatableArray\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eT\u003c/span\u003e\u003cspan class=\"p\"\u003e[]\u003c/span\u003e \u003cspan class=\"n\"\u003earray\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003e_array\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003earray\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"n\"\u003eEquatableArray\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eImmutableArray\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eT\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003earray\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"k\"\u003ethis\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eUnsafe\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAs\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eImmutableArray\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eT\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;,\u003c/span\u003e \u003cspan class=\"n\"\u003eT\u003c/span\u003e\u003cspan class=\"p\"\u003e[]?\u0026gt;(\u003c/span\u003e\u003cspan class=\"k\"\u003eref\u003c/span\u003e \u003cspan class=\"n\"\u003earray\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e??\u003c/span\u003e \u003cspan class=\"n\"\u003eArray\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eEmpty\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eT\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;())\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003eCount\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003e_array\u003c/span\u003e\u003cspan class=\"p\"\u003e?.\u003c/span\u003e\u003cspan class=\"n\"\u003eLength\u003c/span\u003e \u003cspan class=\"p\"\u003e??\u003c/span\u003e \u003cspan class=\"m\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"n\"\u003eReadOnlySpan\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eT\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eAsSpan\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003e_array\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAsSpan\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kt\"\u003ebool\u003c/span\u003e \u003cspan class=\"n\"\u003eEquals\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eEquatableArray\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eT\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eother\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eAsSpan\u003c/span\u003e\u003cspan class=\"p\"\u003e().\u003c/span\u003e\u003cspan class=\"n\"\u003eSequenceEqual\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eother\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAsSpan\u003c/span\u003e\u003cspan class=\"p\"\u003e());\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003eoverride\u003c/span\u003e \u003cspan class=\"kt\"\u003ebool\u003c/span\u003e \u003cspan class=\"n\"\u003eEquals\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003eobject?\u003c/span\u003e \u003cspan class=\"n\"\u003eobj\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eobj\u003c/span\u003e \u003cspan class=\"k\"\u003eis\u003c/span\u003e \u003cspan class=\"n\"\u003eEquatableArray\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eT\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eother\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026amp;\u0026amp;\u003c/span\u003e \u003cspan class=\"n\"\u003eEquals\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eother\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003eoverride\u003c/span\u003e \u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003eGetHashCode\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"c1\"\u003e// A default instance (null _array) is Equals-equal to Empty,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"c1\"\u003e// so it must hash like an empty array too.\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003e_array\u003c/span\u003e \u003cspan class=\"k\"\u003eis\u003c/span\u003e \u003cspan class=\"kc\"\u003enull\u003c/span\u003e \u003cspan class=\"p\"\u003e||\u003c/span\u003e \u003cspan class=\"n\"\u003e_array\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eLength\u003c/span\u003e \u003cspan class=\"p\"\u003e==\u003c/span\u003e \u003cspan class=\"m\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"m\"\u003e17\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eunchecked\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003ehash\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"m\"\u003e17\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"k\"\u003eforeach\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eT\u003c/span\u003e \u003cspan class=\"n\"\u003eitem\u003c/span\u003e \u003cspan class=\"k\"\u003ein\u003c/span\u003e \u003cspan class=\"n\"\u003e_array\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"n\"\u003ehash\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ehash\u003c/span\u003e \u003cspan class=\"p\"\u003e*\u003c/span\u003e \u003cspan class=\"m\"\u003e31\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e+\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eitem\u003c/span\u003e\u003cspan class=\"p\"\u003e?.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetHashCode\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e \u003cspan class=\"p\"\u003e??\u003c/span\u003e \u003cspan class=\"m\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003ehash\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"n\"\u003eIEnumerator\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eT\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eGetEnumerator\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"p\"\u003e((\u003c/span\u003e\u003cspan class=\"n\"\u003eIEnumerable\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eT\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;)(\u003c/span\u003e\u003cspan class=\"n\"\u003e_array\u003c/span\u003e \u003cspan class=\"p\"\u003e??\u003c/span\u003e \u003cspan class=\"n\"\u003eArray\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eEmpty\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eT\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;())).\u003c/span\u003e\u003cspan class=\"n\"\u003eGetEnumerator\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eIEnumerator\u003c/span\u003e \u003cspan class=\"n\"\u003eIEnumerable\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetEnumerator\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eGetEnumerator\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003estatic\u003c/span\u003e \u003cspan class=\"kt\"\u003ebool\u003c/span\u003e \u003cspan class=\"kd\"\u003eoperator\u003c/span\u003e \u003cspan class=\"p\"\u003e==(\u003c/span\u003e\u003cspan class=\"n\"\u003eEquatableArray\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eT\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eleft\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eEquatableArray\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eT\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eright\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eleft\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eEquals\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eright\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003estatic\u003c/span\u003e \u003cspan class=\"kt\"\u003ebool\u003c/span\u003e \u003cspan class=\"kd\"\u003eoperator\u003c/span\u003e \u003cspan class=\"p\"\u003e!=(\u003c/span\u003e\u003cspan class=\"n\"\u003eEquatableArray\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eT\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eleft\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eEquatableArray\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eT\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eright\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"p\"\u003e!\u003c/span\u003e\u003cspan class=\"n\"\u003eleft\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eEquals\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eright\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003estatic\u003c/span\u003e \u003cspan class=\"kd\"\u003eimplicit\u003c/span\u003e \u003cspan class=\"kd\"\u003eoperator\u003c/span\u003e \u003cspan class=\"n\"\u003eEquatableArray\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eT\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;(\u003c/span\u003e\u003cspan class=\"n\"\u003eT\u003c/span\u003e\u003cspan class=\"p\"\u003e[]\u003c/span\u003e \u003cspan class=\"n\"\u003earray\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003earray\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003estatic\u003c/span\u003e \u003cspan class=\"kd\"\u003eimplicit\u003c/span\u003e \u003cspan class=\"kd\"\u003eoperator\u003c/span\u003e \u003cspan class=\"n\"\u003eEquatableArray\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eT\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;(\u003c/span\u003e\u003cspan class=\"n\"\u003eImmutableArray\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eT\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003earray\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003earray\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThree details worth knowing before you paste:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ccode\u003eEquals\u003c/code\u003e uses \u003ccode\u003eSequenceEqual\u003c/code\u003e over spans — element-by-element, no allocation. \u003ccode\u003eGetHashCode\u003c/code\u003e combines all elements, because a hash based on the array reference would be just as broken as the equality we started with.\u003c/li\u003e\n\u003cli\u003eThe \u003ccode\u003eImmutableArray\u0026lt;T\u0026gt;\u003c/code\u003e constructor uses \u003ccode\u003eUnsafe.As\u003c/code\u003e to reuse the inner array without copying. Generators target \u003ccode\u003enetstandard2.0\u003c/code\u003e, so you reference the \u003ca href=\"https://www.nuget.org/packages/System.Memory\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eSystem.Memory\u003c/a\u003e package — it provides the \u003ccode\u003eReadOnlySpan\u0026lt;T\u0026gt;\u003c/code\u003e and \u003ccode\u003eAsSpan\u003c/code\u003e APIs the struct leans on, and it transitively brings in \u003ca href=\"https://www.nuget.org/packages/System.Runtime.CompilerServices.Unsafe\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eSystem.Runtime.CompilerServices.Unsafe\u003c/a\u003e for the cast. If the trick makes you nervous, \u003ccode\u003earray.ToArray()\u003c/code\u003e works too — you pay one copy per transform run, which is still infinitely cheaper than a permanent cache miss.\u003c/li\u003e\n\u003cli\u003eThis is not an exotic pattern I invented. It is what Microsoft ships inside its own generators — the \u003ccode\u003eSystem.Text.Json\u003c/code\u003e source generator in \u003ca href=\"https://github.com/dotnet/runtime/blob/main/src/libraries/Common/src/SourceGenerators/ImmutableEquatableArray.cs\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003edotnet/runtime\u003c/a\u003e carries an \u003ccode\u003eImmutableEquatableArray\u0026lt;T\u0026gt;\u003c/code\u003e for precisely this reason, and the \u003ca href=\"https://github.com/CommunityToolkit/dotnet\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eCommunityToolkit\u003c/a\u003e generators carry their own \u003ccode\u003eEquatableArray\u0026lt;T\u0026gt;\u003c/code\u003e. When the people who built the pipeline all wrap their arrays, take the hint.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eThe fixed model is a one-word change:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Now the record\u0026#39;s synthesized equality is correct all the way down.\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003esealed\u003c/span\u003e \u003cspan class=\"k\"\u003erecord\u003c/span\u003e \u003cspan class=\"nc\"\u003eClassModel\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003eName\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eEquatableArray\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"kt\"\u003estring\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eProperties\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\n\n\n\u003ch2 id=\"strip-to-the-smallest-equatable-shape-early\"\u003e\u003ca href=\"/posts/incremental-source-generators-done-right/#strip-to-the-smallest-equatable-shape-early\" title=\"Strip to the Smallest Equatable Shape Early\"\u003eStrip to the Smallest Equatable Shape Early\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThe array trap is the most common break, but it is a special case of a general rule: everything that flows through the pipeline must be value-equatable, and it must be \u003cem\u003esmall\u003c/em\u003e. My rules for a pipeline model:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eIt is a \u003ccode\u003erecord\u003c/code\u003e, so equality is synthesized member-wise.\u003c/li\u003e\n\u003cli\u003eEvery member is a primitive, a \u003ccode\u003estring\u003c/code\u003e, an \u003ccode\u003eenum\u003c/code\u003e, another equatable record, or an \u003ccode\u003eEquatableArray\u0026lt;T\u0026gt;\u003c/code\u003e of those.\u003c/li\u003e\n\u003cli\u003eIt contains \u003cstrong\u003eno\u003c/strong\u003e \u003ccode\u003eINamedTypeSymbol\u003c/code\u003e, no \u003ccode\u003eCompilation\u003c/code\u003e, no \u003ccode\u003eSyntaxNode\u003c/code\u003e, no \u003ccode\u003eSemanticModel\u003c/code\u003e. Ever.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eSide by side, the broken shape and the one that survives caching:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Broken: symbols are reference-equal across compilations.\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// This cache can never hit after an edit.\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003einternal\u003c/span\u003e \u003cspan class=\"kd\"\u003esealed\u003c/span\u003e \u003cspan class=\"k\"\u003erecord\u003c/span\u003e \u003cspan class=\"nc\"\u003eBrokenTarget\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eINamedTypeSymbol\u003c/span\u003e \u003cspan class=\"n\"\u003eType\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eImmutableArray\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eIPropertySymbol\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eProperties\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Fixed: nothing but value-equatable members.\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003einternal\u003c/span\u003e \u003cspan class=\"kd\"\u003esealed\u003c/span\u003e \u003cspan class=\"k\"\u003erecord\u003c/span\u003e \u003cspan class=\"nc\"\u003eClassModel\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003eNamespace\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003eName\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003eAccessModifier\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eEquatableArray\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"kt\"\u003estring\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eProperties\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eCompared with the two-member draft above, this shape has grown a namespace — emission needs it to write the \u003ccode\u003enamespace\u003c/code\u003e declaration — and an access modifier, which is here to make the general point: carry whatever the emitter needs, flattened to strings. Part 2\u0026rsquo;s final version slims the record back down to namespace, name, and properties, because that is all the \u003ccode\u003eToString\u003c/code\u003e emitter actually consumes. The contract does not care how many members the record has, only that every one of them compares by value.\u003c/p\u003e\n\u003cp\u003eThe conversion from symbol world to value world happens exactly once, inside the \u003ccode\u003etransform\u003c/code\u003e of \u003ccode\u003eForAttributeWithMetadataName\u003c/code\u003e. That is the last point in the pipeline where a symbol is allowed to exist:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"kd\"\u003estatic\u003c/span\u003e \u003cspan class=\"n\"\u003eClassModel\u003c/span\u003e \u003cspan class=\"n\"\u003eCreate\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eGeneratorAttributeSyntaxContext\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eCancellationToken\u003c/span\u003e \u003cspan class=\"n\"\u003ecancellationToken\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003esymbol\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eINamedTypeSymbol\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eTargetSymbol\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eImmutableArray\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"kt\"\u003estring\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003epropertyNames\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003esymbol\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetMembers\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eOfType\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eIPropertySymbol\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eWhere\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ep\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003ep\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eDeclaredAccessibility\u003c/span\u003e \u003cspan class=\"p\"\u003e==\u003c/span\u003e \u003cspan class=\"n\"\u003eAccessibility\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ePublic\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSelect\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ep\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003ep\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eName\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eToImmutableArray\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// Flatten ONCE. Nothing past this line ever sees a symbol again.\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eClassModel\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003esymbol\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eContainingNamespace\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eToDisplayString\u003c/span\u003e\u003cspan class=\"p\"\u003e(),\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003esymbol\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eName\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eSyntaxFacts\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetText\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003esymbol\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eDeclaredAccessibility\u003c/span\u003e\u003cspan class=\"p\"\u003e),\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eEquatableArray\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"kt\"\u003estring\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;(\u003c/span\u003e\u003cspan class=\"n\"\u003epropertyNames\u003c/span\u003e\u003cspan class=\"p\"\u003e));\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eYes, this transform re-runs whenever the attributed class is edited. That is fine — it is cheap, and it is \u003cem\u003esupposed\u003c/em\u003e to run then. What matters is what happens on every other edit: the transform produces a \u003ccode\u003eClassModel\u003c/code\u003e that is value-equal to the cached one, and the expensive downstream work — code building, formatting, emitting — never executes.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"why-symbols-poison-the-cache\"\u003e\u003ca href=\"/posts/incremental-source-generators-done-right/#why-symbols-poison-the-cache\" title=\"Why Symbols Poison the Cache\"\u003eWhy Symbols Poison the Cache\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eIt is worth understanding \u003cem\u003ewhy\u003c/em\u003e the symbol rule exists, because \u0026ldquo;just don\u0026rsquo;t\u0026rdquo; ages badly and the broken version looks so harmless:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Looks like a convenient cache key. It is a slow memory leak\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// wearing a cache miss as a disguise.\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003einternal\u003c/span\u003e \u003cspan class=\"kd\"\u003esealed\u003c/span\u003e \u003cspan class=\"k\"\u003erecord\u003c/span\u003e \u003cspan class=\"nc\"\u003eCachedModel\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eINamedTypeSymbol\u003c/span\u003e \u003cspan class=\"n\"\u003eType\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// roots: the symbol\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e//   -\u0026gt; its ContainingModule and ContainingAssembly\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e//   -\u0026gt; the Compilation that produced it\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e//   -\u0026gt; every syntax tree, metadata reference, and semantic\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e//      model bound to that compilation snapshot\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eTwo independent problems, either of which is disqualifying.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eFirst, correctness.\u003c/strong\u003e \u003ccode\u003eISymbol\u003c/code\u003e instances are reference-equal per compilation. Every keystroke produces a new \u003ccode\u003eCompilation\u003c/code\u003e, and the new compilation produces \u003cem\u003enew\u003c/em\u003e symbol instances for the same declarations. Your \u003ccode\u003eCachedModel\u003c/code\u003e from the previous run holds the old symbol; the transform produces a new one; \u003ccode\u003eEqualityComparer\u0026lt;CachedModel\u0026gt;.Default\u003c/code\u003e compares them, the record delegates to the symbol\u0026rsquo;s reference equality, and the answer is \u003ccode\u003efalse\u003c/code\u003e. Permanent cache miss, by construction. (Roslyn does have \u003ccode\u003eSymbolEqualityComparer.Default\u003c/code\u003e for comparing symbols deliberately — but the pipeline does not use it, and it would not fix the second problem anyway.)\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eSecond, memory.\u003c/strong\u003e The pipeline cache holds your model objects across runs — that is its job. If the model holds a symbol, the cache is now rooting the entire compilation snapshot the symbol came from, as the comment trail above spells out. Syntax trees, metadata references, semantic models: none of it can be collected while your \u0026ldquo;small cached record\u0026rdquo; is alive. Do this in a generator that ships in a popular package and you have built a memory leak into every Visual Studio and Rider instance that opens a consuming solution. Roslyn\u0026rsquo;s own \u003ca href=\"https://github.com/dotnet/roslyn/blob/main/docs/features/incremental-generators.cookbook.md\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eincrementality guidance\u003c/a\u003e calls this out for the same reason: symbols and compilations must never be captured in pipeline state.\u003c/p\u003e\n\u003cp\u003eStrings are boring. Boring is the point. A \u003ccode\u003eClassModel\u003c/code\u003e made of strings costs a few hundred bytes, compares in nanoseconds, and roots nothing.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-contract\"\u003e\u003ca href=\"/posts/incremental-source-generators-done-right/#the-contract\" title=\"The Contract\"\u003eThe Contract\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eHere it is in one paragraph. Every value flowing through an \u003ccode\u003eIIncrementalGenerator\u003c/code\u003e pipeline is cached and compared with \u003ccode\u003eEqualityComparer\u0026lt;T\u0026gt;.Default\u003c/code\u003e; therefore every value must be a small, immutable, value-equatable shape — records of primitives and \u003ccode\u003eEquatableArray\u0026lt;T\u0026gt;\u003c/code\u003e, flattened from symbols exactly once inside the transform, with no \u003ccode\u003eISymbol\u003c/code\u003e, \u003ccode\u003eCompilation\u003c/code\u003e, or \u003ccode\u003eSyntaxNode\u003c/code\u003e surviving past that point. Honor that and the framework does the caching for you. Break it anywhere, even in one property of one record, and your generator runs on every keystroke while looking perfectly incremental in the source.\u003c/p\u003e\n\u003cp\u003eThat is the foundation. Everything that follows in this series — the shape of the pipeline, the test that proves the cache actually hits, the package that survives contact with consuming projects — builds on this one contract. Get it wrong here and no operator, test, or packaging trick downstream will save you.\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003eEquality first. \u003cstrong\u003eEverything else is downstream.\u003c/strong\u003e\u003c/p\u003e\n\u003c/blockquote\u003e\n","date_modified":"2026-07-21T17:06:48+02:00","date_published":"2026-07-21T17:00:00+02:00","id":"https://daily-devops.net/posts/incremental-source-generators-done-right/","language":"en","summary":"Your incremental source generator rebuilds on every keystroke — because one record property silently broke the equality contract. Part 1 of 4: the mental model, why ImmutableArray betrays you, and the EquatableArray that fixes it.","tags":["dotnet","csharp","roslyn","sourcegenerators","performance","bestpractices"],"title":"Incremental Source Generators Done Right: The Equality Contract","url":"https://daily-devops.net/posts/incremental-source-generators-done-right/"}],"language":"en","title":"Roslyn on Daily DevOps \u0026 .NET","version":"https://jsonfeed.org/version/1.1"}