{"id":26296422,"url":"https://github.com/charliedigital/dn7-source-generators","last_synced_at":"2025-05-09T00:51:54.424Z","repository":{"id":188486627,"uuid":"678835989","full_name":"CharlieDigital/dn7-source-generators","owner":"CharlieDigital","description":"A sample project that shows how to use .NET source generators to reduce boilerplate code.","archived":false,"fork":false,"pushed_at":"2023-11-05T21:30:47.000Z","size":2134,"stargazers_count":6,"open_issues_count":0,"forks_count":1,"subscribers_count":3,"default_branch":"main","last_synced_at":"2025-03-31T19:51:15.068Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/CharlieDigital.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null}},"created_at":"2023-08-15T13:49:53.000Z","updated_at":"2025-02-25T08:11:16.000Z","dependencies_parsed_at":"2023-08-15T15:26:17.136Z","dependency_job_id":"8c9f430f-6565-43af-83ef-94106e0159b9","html_url":"https://github.com/CharlieDigital/dn7-source-generators","commit_stats":null,"previous_names":["charliedigital/dn7-source-generators"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/CharlieDigital%2Fdn7-source-generators","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/CharlieDigital%2Fdn7-source-generators/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/CharlieDigital%2Fdn7-source-generators/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/CharlieDigital%2Fdn7-source-generators/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/CharlieDigital","download_url":"https://codeload.github.com/CharlieDigital/dn7-source-generators/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":253171232,"owners_count":21865289,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-03-15T04:18:21.285Z","updated_at":"2025-05-09T00:51:54.400Z","avatar_url":"https://github.com/CharlieDigital.png","language":"C#","funding_links":[],"categories":[],"sub_categories":[],"readme":"# .NET Source Generators with .NET 7\n\n.NET source generators provide a framework level utility to dynamically generate source code both at _dev time and build time_. Whether that’s whole classes, individual functions (using C# partial classes), or strongly-typed runtime bindings.\n\nTo better understand what this means, check out this capture below; when I add the `Product` entity class, the `ProductRepository` type _is created automatically by the generator_:\n\n![Adding entity adds repository type](media/generate-code-2.gif)\n\nThere is no `ProductRepository.cs`; this purely generated class is detected by the language server (for autocomplete) and gets added to the source tree at build time for static type checking, _but generates no actual file and requires no additional work_ except to declare the `Product` type!\n\nAside from the release of LINQ and `System.Linq.Expressions` to support it, source generators are one of the key .NET platform meta-features.\n\nIn this exercise, we'll explore source generators in the context of building a data access layer that allows us to dramatically reduce some of the more tedious, boiler-plate involved.  We’ll see how we can create a generator so that declaring an entity type automatically generates the scaffolding required to interact with that type.\n\n## Problem Outline\n\nThe Repository pattern is particularly suited to working with document-oriented databases like Firestore and CosmosDB since each document represents a self-encapsulated entity.  In general, because of this alignment, it is often enough to write a simple generic repository class that acts as a gateway to the underlying database.\n\nHowever, because of the limitations of document-oriented databases, complex operations may often require writing custom concrete implementations per-type like `OrderRepository: RepositoryBase\u003cOrder\u003e`.\n\nThis approach allows us to extend `Order` specific operations in a well encapsulated manner and in fact, it would be nice if we had an entity specific repository for each entity type in our domain space.\n\nAs the number of entity types increases, this can easily become tedious! With Roslyn-powered .NET source generators, we can automate away this tedium this while still retaining flexibility.\n\n## The Domain Space\n\nConsider the following simple domain model and data access contract:\n\n```cs\nnamespace runtime;\n\npublic interface IRepository\u003cT\u003e where T: Entity {\n  Task Add(T Entity);\n\n  Task Delete(T Entity);\n\n  Task Update(T Entity);\n}\n\npublic abstract class RepositoryBase\u003cT\u003e : IRepository\u003cT\u003e where T: Entity {\n  public virtual async Task Add(T Entity) {\n    await Task.CompletedTask;\n    Console.WriteLine($\"Added → {typeof(T).Name}\");\n  }\n\n  public virtual async Task Delete(T Entity) {\n    await Task.CompletedTask;\n    Console.WriteLine($\"Deleted → {typeof(T).Name}\");\n  }\n\n  public virtual async Task Update(T Entity) {\n    await Task.CompletedTask;\n    Console.WriteLine($\"Updated → {typeof(T).Name}\");\n  }\n}\n\npublic abstract class Entity { }\n\npublic class User : Entity { }\n\npublic class Order : Entity { }\n```\n\nGiven this `IRepository\u003cT\u003e` contract, the `RepositoryBase\u003cT\u003e` class can cover most of our common CRUD cases, but we'd then have to create a `UserRepository : RepositoryBase\u003cUser\u003e` and `OrderRepository\u003cOrder\u003e`.  This can become tedious as our entity domain space increases.\n\nTo work around this, we can leverage .NET source generators to create the entity repositories for us!\n\nTo get started, we'll create two projects:\n\n1. `runtime` - this is our application runtime where our own code goes and where we'll have the base definition of our domain model and data access layer.\n2. `generator` - this is where we'll place our code generator.\n\nIf you're following along with VS Code:\n\n```shell\nmkdir dn7-src-gen\ncd dn7-src-gen\ndotnet add sln            # Create the solution file\nmkdir generator\nmkdir runtime\ncd generator\ndotnet new classlib      # Class library project type for the generator\ncd ../runtime\ndotnet new console       # Console project type for our runtime app\ncd ../\ndotnet sln add runtime\ndotnet sln add generator\n```\n\n## Creating the Generator\n\nWhen creating a generator, you'll want to create a separate project to be referenced by your main project.\n\nHere is the `.csproj` file for the generator project:\n\n```xml\n\u003cProject Sdk=\"Microsoft.NET.Sdk\"\u003e\n  \u003cPropertyGroup\u003e\n    \u003cTargetFramework\u003enet7.0\u003c/TargetFramework\u003e\n    \u003cEnforceExtendedAnalyzerRules\u003etrue\u003c/EnforceExtendedAnalyzerRules\u003e\n  \u003c/PropertyGroup\u003e\n  \u003cItemGroup\u003e\n    \u003cPackageReference Include=\"Microsoft.CodeAnalysis.Analyzers\" Version=\"3.3.4\"\u003e\n      \u003cIncludeAssets\u003eruntime; build; native; contentfiles; analyzers; buildtransitive\u003c/IncludeAssets\u003e\n      \u003cPrivateAssets\u003eall\u003c/PrivateAssets\u003e\n    \u003c/PackageReference\u003e\n    \u003cPackageReference Include=\"Microsoft.CodeAnalysis.Analyzers\" Version=\"4.6.0\" /\u003e\n  \u003c/ItemGroup\u003e\n\u003c/Project\u003e\n```\n\n\u003e 💡 Note that [while the documentation mentions](https://learn.microsoft.com/en-us/dotnet/csharp/roslyn-sdk/source-generators-overview#get-started-with-source-generators) that the project needs to target `netstandard2.0`, it means that any .NET 5 and above target will also work.\n\nWe need to add two references:\n\n```shell\ncd generator\ndotnet add package Microsoft.CodeAnalysis.CSharp\ndotnet add package Microsoft.CodeAnalysis.Analyzers\n```\n\nThere are two parts to our generator:\n\n1. A receiver that handles accumulating our target entity classes.\n2. A generator which uses the accumulated entity classes to generate repositories.\n\nThe first is a very simple `ISyntaxContextReceiver`.  You can think of it like a listener that handles each node as the syntax tree is traversed.  In our case, what we want to do is to find all of the classes which implement the `Entity` base class:\n\n```cs\n/// \u003csummary\u003e\n/// This will receive each \"syntax context\" to determine if we want to act on it.\n/// Here, we'll capture a list of models that implement Entity.\n/// \u003c/summary\u003e\npublic class RepoSyntaxReceiver : ISyntaxContextReceiver {\n  public List\u003cstring\u003e Models = new();\n\n  public void OnVisitSyntaxNode(GeneratorSyntaxContext context) {\n    if (context.Node is not ClassDeclarationSyntax classDec\n      || classDec.BaseList == null) {\n      return;\n    }\n\n    // If the base class list has Entity, we want to generate a repo for it!\n    if (classDec.BaseList.Types.Any(t =\u003e t.ToString() == \"Entity\")) {\n      Models.Add(classDec.Identifier.ToString());\n    }\n  }\n}\n```\n\nOnce we've collected the entity class names, we want to generate a repository file for each one.  This is where our generator comes into play:\n\n```cs\n/// \u003csummary\u003e\n/// This is our generator that actually outputs the source code.\n/// \u003c/summary\u003e\n[Generator]\npublic class RepoGenerator : ISourceGenerator {\n  /// \u003csummary\u003e\n  /// We hook up our receiver here so that we can access it later.\n  /// \u003c/summary\u003e\n  public void Initialize(GeneratorInitializationContext context) {\n    context.RegisterForSyntaxNotifications(() =\u003e new RepoSyntaxReceiver());\n  }\n\n  /// \u003csummary\u003e\n  /// And consume the receiver here.\n  /// \u003c/summary\u003e\n  public void Execute(GeneratorExecutionContext context) {\n    var models = (context.SyntaxContextReceiver as RepoSyntaxReceiver).Models;\n\n    foreach (var modelClass in models) {\n      var src = $@\"\nusing System;\n\nnamespace runtime;\n\npublic partial class {modelClass}Repository : RepositoryBase\u003c{modelClass}\u003e {{ }}\";\n\n      context.AddSource($\"{modelClass}Repository.g.cs\", src);\n    }\n  }\n```\n\nFor our domain space, this will generate classes like:\n\n```cs\npublic partial class OrderRepository : RepositoryBase\u003cOrder\u003e { };\npublic partial class UserRepository : RepositoryBase\u003cUser\u003e { };\n```\n\nTake note that we've declared these as `partial` classes; we'll explore this in a bit.\n\n## Using the Generator\n\nFrom our runtime project -- the one that contains our actual code -- we can now reference these generated classes by updating our `runtime.csproj` file:\n\n```xml\n\u003cProject Sdk=\"Microsoft.NET.Sdk\"\u003e\n  \u003cPropertyGroup\u003e\n    \u003cOutputType\u003eExe\u003c/OutputType\u003e\n    \u003cTargetFramework\u003enet7.0\u003c/TargetFramework\u003e\n    \u003cImplicitUsings\u003eenable\u003c/ImplicitUsings\u003e\n    \u003cNullable\u003eenable\u003c/Nullable\u003e\n  \u003c/PropertyGroup\u003e\n  \u003cItemGroup\u003e\n    \u003cProjectReference\n      Include=\"../generator/generator.csproj\"\n      OutputItemType=\"Analyzer\"\n      ReferenceOutputAssembly=\"false\" /\u003e\n  \u003c/ItemGroup\u003e\n\u003c/Project\u003e\n\n```\n\nAnd consuming them is without fanfare:\n\n```cs\nusing runtime;\n\nvar users = new UserRepository();\n\nawait users.Add(new ());\nawait users.Delete(new ());\nawait users.Update(new());\n\nvar orders = new OrderRepository();\n\nawait orders.Add(new ());\nawait orders.Delete(new ());\nawait orders.Update(new());\n```\n\n## Extending the Generated Repository\n\nThis is incredibly handy and has many use cases!  But if we only needed the basic, common CRUD operations, a generic `Repository\u003cT\u003e` would have been sufficient.  The purpose of creating a strongly typed instance is to provide a well-encapsulated place to organize model-specific operations.\n\nFor example, what if we want to implement an operation on `Order` like `UpdateIfNotShipped`?  Because we're using [partial classes](https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/partial-classes-and-methods), we can easily extend our generated repositories:\n\n```cs\n/// \u003csummary\u003e\n/// A partial class that extends the generated class.\n/// \u003c/summary\u003e\npublic partial class OrderRepository {\n\n  /// \u003csummary\u003e\n  /// This method doesn't exist in the contract, but we can extend the generated\n  /// code using partial classes to add model-scoped behavior.\n  /// \u003c/summary\u003e\n  public async Task UpdateIfNotShipped(\n    Order entity\n  ) {\n    await Task.CompletedTask;\n    Console.WriteLine(\"Order → UpdateIfNotShipped\");\n  }\n}\n```\n\nNow our runtime code can look like this:\n\n```cs\nusing runtime;\n\n// Fully generated\nvar users = new UserRepository();\n\nawait users.Add(new ());\nawait users.Delete(new ());\nawait users.Update(new());\n\n// Generated + partial class in this namespace.\nvar orders = new OrderRepository();\n\nawait orders.Add(new ());\nawait orders.Delete(new ());\nawait orders.Update(new());\nawait orders.UpdateIfNotShipped(new()); // Added via partial\n```\n\n## Dev Experience\n\nA key question is how is the dev experience?  You can see that even in VS Code, we have full autocomplete support for our generated class:\n\n![Autocomplete for generated methods](./media/generate-code.gif)\n\nAnd note the last line:\n\n```cs\nawait orders.UpdateIfNotShipped(new()); // Added via partial\n```\n\nOur local, partial class allows us to extend the generated model as well.\n\n## A Note on Caching\n\nYou may run into issues with caching while developing your own generators.  To work around this, you can use the `build-server shutdown` command to effectively restart the build server:\n\n```shell\ncd runtime\n\n# Reset the build server to clear cache.\ndotnet build-server shutdown\ndotnet run\n\n# Or combine:\ndotnet build-server shutdown \u0026\u0026 dotnet run\n```\n\nThis should clear the cache if you are observing that your generated code isn't reflecting updates to your string template.\n\n## Closing Thoughts\n\n.NET source generators are a powerful platform capability that can be harnessed to reduce a lot of boilerplate code while still providing strongly-typed access.\n\nFor performance sensitive use cases where [.NET native ahead-of-time compilation (AOT)](https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot/?tabs=net7%2Cwindows) can decrease startup times, using source generators instead of reflection is a necessary technique.\n\nThere are numerous use cases where you can take advantage of this mechanism to simplify your every day workflow.\n\nIt's one of the many reasons that the .NET ecosystem is one of the best for building and delivering complex, high performance applications.\n\n## Resources\n\n- https://learn.microsoft.com/en-us/dotnet/csharp/roslyn-sdk/source-generators-overview\n- https://wengier.com/SourceGeneratorPlayground/\n- https://blog.devops.dev/net-source-generator-way-to-improve-performance-3a03bca7c6d\n- https://khalidabuhakmeh.com/dotnet-source-generators-finding-class-declarations\n- https://github.com/amis92/csharp-source-generators\n- https://learn.microsoft.com/en-us/answers/questions/1184090/looking-for-assistance-clearing-the-cache-for-upda\n- https://turnerj.com/blog/the-pain-points-of-csharp-source-generators","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcharliedigital%2Fdn7-source-generators","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcharliedigital%2Fdn7-source-generators","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcharliedigital%2Fdn7-source-generators/lists"}