This was a project I was tasked with a few months back. The codebase at my job uses OData models for many of their API endpoints. These OData models are almost exact copies of the regular entities we use for everything else.

The issue we had was a developer would make a change to an entity model but they would forget to update the corresponding OData model which would cause endpoints to fail. The solution we came up with was to use a source code generator to build the OData models based on the entity models at compile-time.

The first thing I had to figure out was how do I build a source-code generator?…. So I began my research. Luckily there are a few good examples of how to build a basic generator online, so I started there.

  • You need a few NuGet packages. These are: Microsoft.CodeAnalysis.CSharp and Microsoft.CodeAnalysis.Analyzers
  • With these installed we can create a new class. This will inherit from the ISourceGenerator interface and expects two methods, public void Execute(GeneratorExecutionContext context) and public void Initialize(GeneratorInitializationContext context)
  • We also need to apply the [Generator] attribute to this class. This just lets the compiler know that it needs to build this code and then use it as if it were a regular .cs file.
  • Something that will become important as we get further into this project is being able to debug the code. The issue is that this code is all run before the default debugger is initialized since it happens during compile time. To fix this we can, inside the Initialize method add the following code block.

public void Initialize(GeneratorInitializationContext context)
{
#if DEBUG
    if(!Debugger.IsAttached)
    {
        Debugger.Launch();
    }
#endif
}
  • The next steps for me were to figure out how to access the models in the NuGet package that has the name “app.upp.Entities” which is in the namespace “App.Vendor.Entities”. I also needed to think about how to identify each class I needed to copy, but we can come back to this later. So at the top of the Execute method I make an IAssemblySymbol object like this:
IAssemblySymbol assemblySymbol = 
    context.Compilation.SourceModule.ReferencedAssemblySymbols
   .First(q => q.Name.Contains("app.upp.Entities"));
  • This will give me access to the NuGet packages’ namespaces which I can now use to access the individual classes. I will do this with the following code block.
var members = assemblySymbol.GlobalNamespace
   .GetNamespaceMembers().First(q => q.Name == "App")
   .GetNamespaceMembers().First(q => q.Name == "Vendor")
   .GetNamespaceMembers().First(q => q.Name == "Entities")
   .GetMemberTypes().ToList();
  • Now I have a list of all the classes in this NuGet package but I don’t necessarily want to copy every model because there are some that are unrelated to what I need. To get just the classes I want I can create a custom attribute called [CopyClass] which I will apply to every class I want to make a copy of. To do this, I created a new file named CopyClassAttribute.cs and inside the class, I wrote the following:
[System.AttributeUsage(System.AttributeTargets.Class, AllowMultiple=false)]
public class CopyClassAttribute : System.Attribute {}
  • Now I can add the copy attribute to all the required classes.
  • Back inside the DynamicClassGenerator Execute method I need to loop through all the available classes and make a dictionary of each NamedType (class name) and the list of all parameters in that class. To do this I wrote the following:
var targets = new HashSet<INamedTypeSymbol>();
var toMap = new Dictionary<INamedTypeSymbol, List<ISymbol>>();

foreach (var member in members) 
{
   if (member.GetAttributes().Any(a => a.AttributeClass?.Name.Contains("CopyClass") == true))
   {
      targets.Add(member);
      var memberProperties = member.GetMembers().Where(m => m.Kind == SymbolKind.Property).ToList();
      toMap.Add(member, memberProperties);
   }
}
  • Great! Now I have a dictionary of every class name and the corresponding properties. I can now start building the actual classes. To do this I will use a StringBuilder and just append each new class to the bottom. I also need to make sure and include any using statements that would be used by any of the copied classes.
var sb = new StringBuilder();
sb.AppendLine("using System;")
sb.AppendLine("using System.ComponentModel.DataAnnotations;")
sb.AppendLine("using System.ComponentModel.DataAnnotations.Schema;")
sb.AppendLine("using System.Collections.Generic;")
sb.AppendLine("using System.Linq;")
sb.AppendLine("using System.Threading.Tasks;")
sb.AppendLine($"namespace {_nameSpace};")
sb.AppendLine("{")

foreach (var target in toMap)
{
   sb.Append(CreateNewClass(target.Key, target.Value, partialClasses));
   sb.AppendLine();
}
sb.AppendLine(@"}");

context.AddSource("ControllerGenerator", SourceText.From(sb.ToString(), Encoding.UTF8);
  • Cool, so now I hope it’s clear how this is going to work. We just loop over the classes copying them exactly to a new generated file. You probably noticed I’m using a function in the above code called CreateNewClass. This will be declared like this:
private string CreateNewClass(INamedTypeSymbol targetKey, List<ISymbol> targevValues, string[] partialClasses)
{
   var className = targetKey.Name;
   var sourceBuilder = new StringBuilder();
   if (partialClasses.Contains(className))
   {
      sourceBuilder.AppendLine($"public partial class {className}");
   }
   else
   {
      sourceBuilder.AppendLine($"public class {className}");
   }
   
   sourceBuilder.AppendLine("{");
   var firstElement = targetValues.First();
   var firstType = (IPropertySymbol)firstElement;
   var firstTypeName = firstType.Type.ToString();
   sourceBuilder.AppendLine("[Key]");
   sourceBuilder.AppendLine($"public {firstTypeName} {firstElement.Name} {{get; set;}}");

   foreach (var member in targetValues.Skip(1))
   {
      if (member.IsVirtual) continue;
      var memberName = member.Name;
      var memberType = (IPropertySymbol)member;
      var memberTypeName = memberType.Type.ToString();
      
      if (string.Equals(memberName, className))
      {
         memberName = "_" + memberName;
      }

      var memberAttribute = memberType.GetAttributes().ToList().FirstOrDefault();
      if (memberAttribute != null)
      {
         sourceBuilder.AppendLine($"[{memberAttribute}] public {memberTypeName} {memberName} {{get; set;}}");
      }
      else 
      {
         sourceBuilder.AppendLine($"public {memberTypeName} {memberName} {{get; set;}}");
      }
   }
   sourceBuilder.AppendLine(@"}");
   return sourceBuilder.ToString();
}
  • That’s pretty much it! When the app is built, this code will run generating a new file called “ControllerGenerator” which will have a list of all the classes that were copied. You can then reference these models throughout the code.

Thanks so much for reading. I haven’t posted much this past year because I’ve been so busy with real-life stuff but hopefully, I have some more time now to put out articles about what I’m working on.

Thanks!

-k