Codexcite.Generators 1.1.3

dotnet add package Codexcite.Generators --version 1.1.3
NuGet\Install-Package Codexcite.Generators -Version 1.1.3
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Codexcite.Generators" Version="1.1.3" />
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add Codexcite.Generators --version 1.1.3
#r "nuget: Codexcite.Generators, 1.1.3"
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
// Install Codexcite.Generators as a Cake Addin
#addin nuget:?package=Codexcite.Generators&version=1.1.3

// Install Codexcite.Generators as a Cake Tool
#tool nuget:?package=Codexcite.Generators&version=1.1.3

Codexcite.Generators

Base Generators and helper functions for Source Generators.

NuGet Badge

Usage

Basic usage - just inherit from BaseTypeGenerator, define the marker attribute to look for and customize the code to be generated for each target type.

using Codexcite.Generators;
using Microsoft.CodeAnalysis;
using Sample.Generators.Shared;

namespace Sample.Generators;

[Generator]
public class PropertyChangesGenerator : BaseTypeGenerator<TypeInformation>
{
  protected override string MarkerAttribute => typeof(SampleMarkerAttribute).FullName;
  private static readonly string IgnorePropertyAttribute = typeof(SampleIgnoreAttribute).FullName;

  protected override string[] InterestingAttributes => new[] { MarkerAttribute, IgnorePropertyAttribute };

  protected override string GenerateCodeForType(TypeInformation typeInformation)
  { 
    // generate the necessary code for each one of the target types 
    return string.Empty;
  }
}

Focus only on specific types, for example only records:

  protected override bool IsSyntaxTargetForGeneration(SyntaxNode node)
    => node is RecordDeclarationSyntax { AttributeLists.Count: > 0 };

There will be a file generated for each target type. You can customize the name of the generated file.

  protected override string GetGeneratedFileName(TypeInformation typeToGenerate) 
    => $"{typeToGenerate.Name}Extensions.g.cs";

You can generate code that is global (not specific to each target type). Examples include common classes or attributes.

  protected override void RegisterGlobalGeneratedCode(IncrementalGeneratorInitializationContext context)
  {
    context.RegisterPostInitializationOutput(ctx => ctx.AddSource(
                                                              "GlobalGeneratedCode.g.cs",
                                                              SourceText.From("[code goes here]", Encoding.UTF8)));
  }

Use TypeInformation data to generate the necessary code:

  private static string GenerateExtensionClass(TypeInformation typeToGenerate)
  {
    var sb = new StringBuilder();
    sb.AppendLine("#nullable enable");
    sb.AppendLine("using System;");
    sb.AppendLine("using Sample.Generators.Shared;");
    sb.AppendLine("using Sample.Generators.Shared.Exceptions;");
    sb.AppendLine("using System.Collections.Generic;");
    sb.AppendLine("using System.Linq;");
    sb.AppendLine($"namespace {typeToGenerate.Namespace}"); // match the target type namespace
    sb.AppendLine("{");
    sb.AppendLine($"\tpublic static partial class {typeToGenerate.Name}Extensions"); // use the target type name
    sb.AppendLine("\t{");
    
    sb.AppendLine();
    // iterate through the target type's properties to generate code
    GenerateExtractValueChangeRequests(sb, typeToGenerate);
    sb.AppendLine();
    
    sb.AppendLine("\t}");
    sb.AppendLine("}");
    return sb.ToString();
  }

Generate one or more methods using the available TypeInformation:

  private static void GenerateExtractValueChangeRequests(StringBuilder sb, TypeInformation typeToGenerate)
  {
    sb.AppendLine($"\t\tpublic static IEnumerable<PropertyChangedRecord> ExtractPropertyChangeRecords(this {typeToGenerate.Name} target, " +
                  $"{typeToGenerate.Name} original)");
    sb.AppendLine("\t\t{");
    sb.AppendLine($"\t\t\tvar changes = new List<PropertyChangedRecord>({typeToGenerate.Properties.Length});");
    foreach (var member in typeToGenerate.Properties)
    {
      // skip properties marked with the ignore attribute
      if (member.Attributes.All(x => x.ClassName != IgnorePropertyAttribute)) 
      {
        sb.AppendLine($"\t\t\tif (!target.{member.Name}.{(member.Type.IsEnumerable ? "SequenceNullableEquals" : "NullableEquals")}(original.{member.Name}))");
        sb.AppendLine($"\t\t\t\tchanges.Add(new PropertyChangedRecord(nameof({typeToGenerate.Name}.{member.Name}), target.{member.Name}, original.{member.Name}));");
      }
    }

    sb.AppendLine("\t\t\treturn changes;");
    sb.AppendLine("\t\t}");
  }

Generator project configuration

Create a .Net Standard 2.0 project.

 
 <TargetFramework>netstandard2.0</TargetFramework>
 
 <IncludeBuildOutput>false</IncludeBuildOutput>

Reference the Codexcite.Generators nuget. Set PrivateAssets="all".

  
  <ItemGroup>
    <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.3" PrivateAssets="all" />
    <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.1.0" PrivateAssets="all" />
    <PackageReference Include="Codexcite.Generators" Version="1.1.3" PrivateAssets="all" />
  </ItemGroup>

If you use a shared project for the marker attributes, reference it too.

  <ItemGroup>
    <ProjectReference Include="..\Sample.Generators.Shared\Sample.Generators.Shared.csproj" PrivateAssets="All" />
  </ItemGroup>

Nuget package configuration

You'll probably want to pack your generator in a nuget package. There are several important steps to follow. Basic nuget package configuration. CopyLocalLockFileAssemblies ensures that all assemblies are copied to the output folder.

<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<PackageId>Sample.Generators</PackageId>
<Description>Example of source generators created using Codexcite.Generators package.</Description>
<Version>1.0.0</Version>
<PackageReleaseNotes>
Sample.Generators 1.0.0 - Initial version.
PackageReleaseNotes>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>

Include the generators assembly in the package as an analyzer.


<None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />

The analyzers do not have access to the referenced assemblies of the code they are analyzing, so you must include any referenced dlls in the package.

    
    <None Include="$(OutputPath)\Codexcite.Generators.dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
    <None Include="$(OutputPath)\Sample.Generators.Shared.dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />

That is enough for running the analyzer locally, but sometimes you also need to include all the framework dlls if you want to run the analyzer as part of a CI build.


<None Include="$(OutputPath)\Microsoft.CodeAnalysis.dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
<None Include="$(OutputPath)\Microsoft.CodeAnalysis.CSharp.dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />

<None Include="$(OutputPath)\System.Buffers.dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
<None Include="$(OutputPath)\System.Collections.Immutable.dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
<None Include="$(OutputPath)\System.Memory.dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
<None Include="$(OutputPath)\System.Numerics.Vectors.dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
<None Include="$(OutputPath)\System.Reflection.Metadata.dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
<None Include="$(OutputPath)\System.Runtime.CompilerServices.Unsafe.dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
<None Include="$(OutputPath)\System.Text.Encoding.CodePages.dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
<None Include="$(OutputPath)\System.Threading.Tasks.Extensions.dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />

Finally, if you use a shared dll, include it in the lib folder, so the target assembly can reference it.

    
    <None Include="$(OutputPath)\Sample.Generators.Shared.dll" Pack="true" PackagePath="lib\netstandard2.0" Visible="true" />

Target project configuration

Reference your generator nuget as an Analyzer. All the dependencies should be included.

<PackageReference Include="Sample.Analyzer" Version="1.0.0" OutputItemType="Analyzer" ReferenceOutputAssembly="false"/>

If you prefer to reference it as a project reference, then you need to also reference the dependencies. Notice the Shared project has ReferenceOutputAssembly="true" so that the assembly can be used in the target code.

<PackageReference Include="Codexcite.Generators" Version="1.1.3" OutputItemType="Analyzer" ReferenceOutputAssembly="false"/>
<ProjectReference Include="..\Sample.Generators.Shared\Sample.Generators.Shared.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="true" />
<ProjectReference Include="..\Sample.Generators\Sample.Generators.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />

Highly recommended: turn on EmitCompilerGeneratedFiles so the generated code is also written to files and you can commit it to source control. The files will be generated in folders named using the generator assembly and class name, inside a folder you can customize.

<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
<CompilerGeneratedFilesOutputPath>Generated</CompilerGeneratedFilesOutputPath>

Make sure to exclude the generated files from compilation, otherwise you'll get duplicated code errors.

  <ItemGroup>
    <Compile Remove="Generated/**/*.cs" />
  </ItemGroup>
  <ItemGroup>
    <None Include="Generated/**/*.cs" />
  </ItemGroup>

If the analyzer doesn't run on the CI pipeline on Azure DevOps, you can use the generated files to compile as normal by setting a condition for TF_BUILD.

  <ItemGroup>
    
    <Compile Remove="Generated/**/*.cs" Condition="'$(TF_BUILD)' != 'true'" />
  </ItemGroup>

To use your generator, mark the target types with your defined attribute. Exclude members from generation by using your ignore attribute.

  [SampleMarker]
  public record Example
  {
    public string? Name { get; set; }
    public int Age { get; set; }
    [SampleIgnore]
    public double? Height { get; set; }
  }

Advanced use

You can inherit directly from BaseGenerator<TDeclarationSyntax, TToGenerate> in order to fully customize your source generation.

  • TDeclarationSyntax: the type of MemberDeclarationSyntax that will be handled by this generator. Usually is TypeDeclarationSyntax.
  • TToGenerate: the intermediate type containing information about the target member used for generation. For example, Codexcite.Generators.Model.TypeInformation.

BaseGenerator<TDeclarationSyntax, TToGenerate> has 3 customizable steps used in source generation.

  1. Quick filtering of SyntaxNode elements to select the potential candidates for generation. Override IsSyntaxTargetForGeneration to customize.
protected override bool IsSyntaxTargetForGeneration(SyntaxNode node)
  => node is ClassDeclarationSyntax { AttributeLists.Count: > 0 }
       or StructDeclarationSyntax { AttributeLists.Count: > 0 }
       or RecordDeclarationSyntax { AttributeLists.Count: > 0 };
  1. Extracting information about the target and saving it in a TToGenerate object.
protected override TToGenerate ExtractTypeToGenerate(INamedTypeSymbol typeSymbol)
{
  // extract information here
}
  1. Generate code, based on the information contained in the TToGenerate
protected override string GenerateCodeForType(TToGenerate type)
{ 
  // generate the necessary code for the target type 
}

Acknowledgments

Inspired by Andrew Lock's excellent blog series about incremental source generators.

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 was computed. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • .NETStandard 2.0

    • No dependencies.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last updated
1.1.3 428 4/28/2022
1.1.2 358 4/27/2022
1.1.1 365 4/27/2022

Codexcite.Generators 1.1.3 - Refactor TypeInformation models
     Codexcite.Generators 1.1.2 - Update Project Urls
     Codexcite.Generators 1.1.1 - Handling System. namespace.
     Codexcite.Generators 1.1.0 - Better namespace handling. Identifies Enumerables and access modifiers.
     Codexcite.Generators 1.0.3 - Can use interesting attributes.
     Codexcite.Generators 1.0.2 - Full type names.
     Codexcite.Generators 1.0.1 - Handling Enum properties.
     Codexcite.Generators 1.0.0 - Initial version.