Code Generation with .NET 5 – Builder pattern

As you start working on larger codebases, you will find a growing need to start introducing patterns to make code intuitive to use, easy to read, and structured in such a way that makes it difficult for developers using your code to make errors. You’ll often be trying to tackle the following goals when modeling the public API surface for your objects:

  • Ensuring that objects are valid before they are used
  • Make object immutable as to prevent changes to it after it is constructed

The most common way to accomplish this is by initializing the object form constructor and setting all properties to read-only. For example

public class Person
{
  public Person(string firstName, string lastName, DateTime? birthDate = default)
  {
    if(firstName == null) throw new ArgumentNullException(nameof(firstName));
    if(lastName == null) throw new ArgumentNullException(nameof(lastName));
    FirstName = firstName;
    LastName = lastName;
    BirthDate = birthDate;
  }
  public string FirstName { get; private set; }
  public string LastName { get; private set; }
  public DateTime? BirthDate { get; private set; }
}

static void Main()
{
  var person = new Person("John", "Smith", new DateTime(1980,3,1));
}

On the surface this is good. The class is immutable as after it’s constructed, it is read only. We’re also protecting the two mandatory parameters with validation clauses. But there are a few downsides:

  • Reading the object creation line doesn’t tell us what each parameter is. We have to either go to class structure or trigger IntelliSense to figure out what those values actually correspond to.
  • We will only know about the first validation error
  • There’s some boilerplate involved in writing this
  • Order of parameters need to be maintained to ensure that existing code doesn’t map arguments incorrectly
  • If we introduce a new optional parameter, anyone using reflection may fail if they were looking for a constructor with a specific signature.

As the number of fields on the object grows, constructor initialization becomes even more difficult to read. You could improve readability by using parameter names explicitly when constructing object. ex new Person(firstName: "John", lastName: "Smith", dateOfBirth: new DateTime(1980,3,1)). But at some point, you’ll find this is still awkward. Enter the builder pattern.

We’re going to construct an object who’s sole purpose is to construct our Person. It’s modeled to make it easy to use, enforce all rules, and make refactoring safe. A typical class with builder may look like this:

public class Person
{
  public static PersonBuilder Builder => new PersonBuilder();
  public Person(string firstName, string lastName, DateTime? birthDate = default)
  {
    if (firstName == null) throw new ArgumentNullException(nameof(firstName));
    if (lastName == null) throw new ArgumentNullException(nameof(lastName));
    FirstName = firstName;
    LastName = lastName;
    BirthDate = birthDate;
  }
  public string FirstName { get; private set; }
  public string LastName { get; private set; }
  public DateTime? BirthDate { get; private set; }

  public class PersonBuilder
  {
    private string _firstName;
    private string _lastName;
    private DateTime? _birthDate;
    public PersonBuilder FirstName(string firstName)
    {
      _firstName = firstName;
      return this;
    }
    public PersonBuilder LastName(string lastName)
    {
      _lastName = lastName;
      return this;
    }
    public PersonBuilder BirthDate(DateTime? birthDate)
    {
      _birthDate = birthDate;
      return this;
    }
    public PersonBuilder BirthDate(int year, int month, int day)
    {
      _birthDate = new DateTime(year, month, day);
      return this;
    }
    public Person Build()
    {
      Validate();
      return new Person(_firstName, _lastName, _birthDate);
    }
    public void Validate()
    {
      void AddError(Dictionary<string, string> items, string property, string message)
      {
        if (items.TryGetValue(property, out var errors))
          items[property] = $"{ errors}\n{ message}";
        else
          items[property] = message;
      }
      Dictionary<string, string> errors = new Dictionary<string, string>();
      if (_firstName == null) AddError("FirstName", _firstName);
      if (_lastName == null) AddError("LastName", _firstName);
      if (errors.Count > 0)
        throw new BuilderException(typeof(Person), errors);
    }
  }
}

static void Main()
{
  var person = Person.Builder
    .FirstName("John")
    .LastName("Smith")
    .BirthDate(1980,3,1)
    .Build();
}

Wow, look at that awesome API for constructing our object. So neat, all rules are enforced, we can pass it between methods and “build-it-up”. Whoever is going to be using our code is going to really appreciate this… Wait a minute… look at all that extra code I had to write to make it happen! The builder code is bigger than the class itself!

And this is the fundamental issue with this pattern – it’s a ton of boilerplate code that is just super boring to write and maintain. If only this was a language feature… or auto-generated. Sometimes I venture to the dark side and write Java Spring. While I’m always happy to come back to the light of C#, there’s one killer library it has called Lombok that can autogenerate builders at compile time, get/set methods (because Java lacks properties), and a few other things. Alas, we can’t do that in .NET…

“.NET 5 has entered chat”

.NET 5 has introduced an awesome new feature to generate code based on Roslyn analyzers, which is currently in preview. The idea is that you can auto-generate additional code to include in your project at compile time. This code doesn’t actually get added to your project as user code – it’s dynamically added to your project when it is sent to the compiler. You can read all about C# code generators here, but here’s a short summary of how it works.

Image Picture1

You get to provide the stuff in the orange box by writing a generator class. To get started with this, you’ll need .NET Core 5.0 preview and Visual Studio 2019 preview. In order to create a generator, we first create a project like this:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
    <LangVersion>preview</LangVersion>
  </PropertyGroup>
  <PropertyGroup>
    <RestoreAdditionalProjectSources>https://dotnet.myget.org/F/roslyn/api/v3/index.json ;$(RestoreAdditionalProjectSources)</RestoreAdditionalProjectSources>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="3.6.0-3.final" PrivateAssets="all" />
  </ItemGroup>
</Project>

We then create a generator class similar to the following:

[Generator]
public class MyGenerator : ISourceGenerator
{
  public void Execute(SourceGeneratorContext context)
  {
    // 1. get reference to roslyn analyzer and figure out what code we want to generate
    var syntaxTrees = context.Compilation.SyntaxTrees;
    // 2. generate some code
    var myAutoGeneratedCode = "public class Blah {}";
    // 3. include it as part of build
    context.AddSource("blah", SourceText.From(myAutoGeneratedCode, Encoding.UTF8));
  }
  public void Initialize(InitializationContext context)
  {
    // often not needed
  }
}

As soon as I read this blog post I knew what had to be done. After 3 glasses of wine and an evening spent coding, I present you a working auto-generated builder pattern for .NET 5. The project is a work in progress and has not yet being packaged into a proper NuGet package, but it’s enough for you to try it out and get a feel for what’s possible and coming down.

Lets take a look at what it takes to introduce builder pattern in this new world

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>netcoreapp3.1</TargetFramework>
    <LangVersion>preview</LangVersion>
  </PropertyGroup>
  <ItemGroup>
    <ProjectReference Include="..\BuilderCommon\BuilderCommon.csproj" />
    <ProjectReference Include="..\BuilderGenerator\BuilderGenerator.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
  </ItemGroup>
</Project>

Notice the reference to Analyzer responsible for generating our code. That is a project with our code generator. We also reference a common library that contains the attributes used to mark classes that need a builder to be generated, as well as BuilderException class. In the future, both of these would be part of a NuGet package.

In the end this what we end up with

using System;
using System.ComponentModel.DataAnnotations;
using BuilderCommon;

namespace Sample
{
    [GenerateBuilder]
    public partial class Person
    {
        [Required]
        public string FirstName { get; private set; }
        [Required]
        public string LastName { get; private set; }
        public DateTime? BirthDate { get; private set; }
    }
}
using System;
using System.Collections.Generic;
namespace Sample
{
  using System;
  using System.ComponentModel.DataAnnotations;
  using BuilderCommon;
  using Newtonsoft.Json;
  partial class Person
  {
    private Person(){}
    public static PersonBuilder Builder => new PersonBuilder();
    public class PersonBuilder
    {

      private string _firstName;
      public PersonBuilder FirstName(string FirstName)
      {
        _firstName = FirstName;
        return this;
      }


      private string _lastName;
      public PersonBuilder LastName(string LastName)
      {
        _lastName = LastName;
        return this;
      }


      private DateTime? _birthDate;
      public PersonBuilder BirthDate(DateTime? BirthDate)
      {
        _birthDate = BirthDate;
        return this;
      }


      public Person Build()
      {
        Validate();
        return new Person
        {
          FirstName = _firstName,
LastName = _lastName,
BirthDate = _birthDate,

        };
      }
      public void Validate()
      {
        void AddError(Dictionary<string, string> items, string property, string message)
        {
          if (items.TryGetValue(property, out var errors))
            items[property] = $"{errors}\n{message}";
          else
            items[property] = message;
        }
        Dictionary<string,string> errors = new Dictionary<string, string>();
        if(_firstName == default)  AddError(errors, "FirstName", "Value is required");
if(_lastName == default)  AddError(errors, "LastName", "Value is required");

        if(errors.Count > 0)
          throw new BuilderCommon.BuilderException(errors);
      }
    }
  }
}

using System;
using System.ComponentModel.DataAnnotations;
using BuilderCommon;
using Newtonsoft.Json;

namespace Sample
{
    class Program
    {
        static void Main(string[] args)
        {
            var myDataClass = Person.Builder 
                .FirstName("John")
                .LastName("Smith")
                .BirthDate(new DateTime(1980,3,1))
                .Build();
            
            Console.WriteLine(JsonConvert.SerializeObject(myDataClass, Formatting.Indented));

            Console.WriteLine("==== Builder Validation ====");
            try
            {
                Person.Builder 
                    .FirstName("John")
                    .Build();
            }
            catch (BuilderException ex)
            {
                Console.WriteLine(ex);
            }
        }
    }
}
"C:\Program Files\dotnet\dotnet.exe" C:/projects/BuilderPatternGenerator/Sample/bin/Debug/netcoreapp3.1/Sample.dll
{
  "FirstName": "John",
  "LastName": "Smith",
  "BirthDate": "1980-03-01T00:00:00"
}
==== Builder Validation ====
BuilderCommon.BuilderException: Error building object. The following properties have errors:
   LastName: Value is required

   at Sample.Person.PersonBuilder.Validate() in C:\projects\BuilderPatternGenerator\Sample\Person.Builder.cs:line 67
   at Sample.Person.PersonBuilder.Build() in C:\projects\BuilderPatternGenerator\Sample\Person.Builder.cs:line 43
   at Sample.Program.Main(String[] args) in C:\projects\BuilderPatternGenerator\Sample\Program.cs:line 33

Process finished with exit code 0.

The IntelliSense for this will work only in Visual Studio 2019 preview. I mainly use Rider, and while the code does compile, your IDE will complain that you have syntax errors. I expect as the project evolves, we will get support for syntax validation for generated code in other IDEs.

What’s extra cool is that a very similar code can be reused to provide design-time and compile-time validation of many of the rules via standard Roslyn analyzer to enforce the builder’s validation rules. This means that if you forgot to include a mandatory field in the builder, you can actually get a compiler error and a syntax error highlight in your IDE.

You can find the generator and the sample in the Git repo below. This is not production quality by any means and is meant to demonstrate what will be possible when .NET 5 hits prime time. Along with all the recent additions to C# syntax, the future of .NET has never looked so exciting!


8 Comments

  1. Adam Paquette May 7, 2020 at 10:41 pm

    This is so beautiful. I assume that libraries like AutoMapper could compile conversion functions AOT and get performance boost over reflexion.

  2. joo May 9, 2020 at 6:42 pm

    Hi,
    Nice article!

    I think in second code snippet there should be call to local function:
    “`
    if (_firstName == null) AddError(“FirstName”, _firstName);
    “`
    instead of
    “`
    if (_firstName == null) errors.Add(“FirstName”, _firstName);
    “`

    1. Andrew Stakhov May 10, 2020 at 1:01 pm

      Good catch. I’ve updated the article.

  3. Jannes May 11, 2020 at 4:37 pm

    You don’t need { private set; } for stuff that you initialize in the constructor. “private set;” actually makes it technically not immutable.

    Might also be a nice way to get rid of the [Required] tag in this case: if it’s { get; } only (without default value), then it’s definitely required.

    I’m always kind of put off by the lack of compile time errors for not setting the required fields in the builder pattern. Which was something the (admittedly ugly) original constructor arguments version did provide. I assume the generator could inspect the syntax tree to make sure that both FirstName and LastName are in fact called and produce a build error if not? I guess it would have to iterate through all the call sites, not sure if that’s feasible or even desirable.

    1. Andrew Stakhov May 11, 2020 at 9:10 pm

      Immutability is not a requirement for builder pattern, though it can benefit from it (and I do mention it in the article). I’m working on an improved version of builder pattern that changes a few things around based on feedback I got, amongst which are the points you raise. Mainly it now generates a constructor for all arguments (if user-defined one with same signature is not found), so you don’t need private set. The [GenerateBuidler] attribute will be expanded with options to control the style of the builder it generates. One of the options is “guiding” interfaces for Required parameters, and it kind of looks like this:

      public class PersonBuilder : PersonBuilder.IRequiredFirstName, PersonBuilder.IRequiredLastName
      {
      public interface IRequiredFirstName
      {
      public IRequiredLastName FirstName(string firstName);
      }
      public interface IRequiredLastName
      {
      public PersonBuilder LastName(string lastName);
      }
      private string _firstName;
      public IRequiredLastName FirstName(string FirstName)
      {
      _firstName = FirstName;
      return this;
      }
      private string _lastName;
      public PersonBuilder LastName(string LastName)
      {
      _lastName = LastName;
      return this;
      }
      private DateTime? _birthDate;
      public PersonBuilder BirthDate(DateTime? BirthDate)
      {
      _birthDate = BirthDate;
      return this;
      }

      This forces the consumer to supply mandatory parameters before they can get to Build method. The downside is that they have to do it in specific order, which for the most part is the same tradeoff you would have with constructor anyways.

  4. MJM May 13, 2020 at 7:56 pm

    How it work wit IOC such as Unity.Container

    1. Andrew Stakhov May 13, 2020 at 8:36 pm

      For the most part dependency injection is done for objects that represent services, where lifecycle management and object creation is delegated to the container. Same way as you normally don’t have direct constructor calls when using IOC, there is no reason to use builder. You may use parts of builder when setting up dependencies for services explicitly (usually done via extension methods in Startup.cs if you’re using .NET Core native DI container. Having said that, even in IOC code you will generally create data structure classes without DI injection, and this is where you would benefit from this. Basically look at your code, everywhere you have a direct constructor call can potentially benefit from builder pattern.

      PS: Don’t use unity, it’s one of the worst container frameworks out there. Use either Microsoft.Extensions.DependencyInjection as is the default choice for .NET Core projects (though it can be used with .NET framework apps), or take a look at Autofac.

  5. eliav April 10, 2021 at 6:40 am

    hey,
    this looks great but i cant figure out where does the file is generated

Leave a comment

Your email address will not be published. Required fields are marked *