Fluent API in C#

Powered by source code generation

03.03.2023

Introduction

The most important message first: You don't have to write a fluent API builder for your classes in C# by hand anymore. The M31.FluentAPI library can do that for you by leveraging source code generation at development time.

Everybody wants to use fluent APIs but writing them is tedious. But first things first, what is a fluent API, also known as a fluent interface? If you are a C# programmer, it is highly likely that you have already used one: LINQ. Consider a variable numbers of type IEnumerable<int>. The expression

numbers.Where(n => n > 0).OrderBy(n => n).Select(n => n * n)

filters for positive numbers, puts them in ascending order and finally squares each of them. It is easy to write (code completion) and very readable. Compare this to the static version, which would read like so:

Select(OrderBy(Where(numbers, n => n > 0), n => n), n => n * n)

I would like to argue that this expression is more difficult to read for the following two reasons: the order of the methods have now to be read from inside to outside and it is visually harder to match the arguments with the methods. Moreover, the first expression is also easier to write due to code completion.

The same idea can be applied to object initialization. Consider a class Student with the properties Name, Age and Semester. With a constructor the creation of a student is:

Student student = new Student("Alice", 21, 4);

With a fluent builder we can write instead:

Student student = new StudentBuilder()
    .WithName("Alice").OfAge(21).InSemester(4).Build();

The builder has an initialization method for each property which returns the builder itself. Only the last call, Build, returns the student.

If we make the WithName method static, return the student in the InSemester method and rename the builder class to CreateStudent we get the following:

Student student = CreateStudent
    .WithName("Alice").OfAge(21).InSemester(4);

Admittedly, for this small example a builder might not be necessary. However, this approach shines if the class has many properties and you want to guide the developer with a step-by-step initialization. We can control in which order the initialization methods have to be called and with the code completion of the IDE this line of code is very easy to write.

Implementation

Source

For the example above the implementation of the builder class can be realized as follows:

public class CreateStudent 
    : CreateStudent.IOfAge, CreateStudent.IInSemester
{
    private readonly Student student;

    private CreateStudent()
    {
        student = new Student();
    }

    public static IOfAge WithName(string name)
    {
        CreateStudent createStudent = new CreateStudent();
        createStudent.student.Name = name;
        return createStudent;
    }

    public IInSemester OfAge(int age)
    {
        student.Age = age;
        return this;
    }

    public Student InSemester(int semester)
    {
        student.Semester = semester;
        return student;
    }

    public interface IOfAge
    {
        IInSemester OfAge(int age);
    }

    public interface IInSemester
    {
        Student InSemester(int semester);
    }
}

By returning the builder as an interface from the methods WithName and OfAge, the initialization methods available for the subsequent step can be controlled.

Note that for this example I assumed that the properties of the Student class have public set accessors and can be set from outside of the class. For private set accessors reflection has to be used for setting the properties.

Code generation

Writing this builder class is tedious, in particular if the class has more than three properties. Luckily, the creation of the builder follows explicit rules and can hence be automated.

Roslyn, the .NET compiler platform, allows developers to write code that analyzes code and that can generate more code based on the results. This feature is called incremental source generators and is used for code generation at development time.

I have explored this technology and implemented an incremental source generator library that can create builders automatically. It can be used by decorating the target class and its members with attributes:

[FluentApi]
public class Student
{
    [FluentMember(0, "WithName")]
    public string Name { get; set; }

    [FluentMember(1, "OfAge")]
    public int Age { get; set; }

    [FluentMember(2, "InSemester")]
    public int Semester { get; set; } = 0;
}

The analyzer is triggered with every single keystroke and if the attributes are changed, the builder code will be regenerated. The generated code and the corresponding code completion by the IDE will be available immediately.

In addition to what I have shown above, the library allows you to create forks in the initialization flow and offers special attributes for boolean and collection properties. You may find a detailed usage guide in the repository: M31.FluentAPI.

If you would like to learn more about incremental source generators and how to implement your own, I can recommend these excellent resources by Andrew Lock and Pawel Gerr.

My take on code generation

Source generators were introduced in .NET 5 and I presume they will have a bright future with many applications. They can be used to improve and reimplement features that rely on runtime reflection such as serialization and dependency injection. A concrete example for this is the source generator Microsoft ships with .NET for serializing and deserializing JSON.

Source generation has two major advantages over reflection. Firstly, reflection is a powerful tool but it comes with a cost in performance. Source generators operate at development time and don't add any overhead at runtime. Secondly, errors can be found earlier. E.g. with a dependency injection container backed by code generation, a required but unregistered type leads to an analyzer error at development time instead of an application crash.

Besides these points there is another view on code generation: it can be seen as a novel way of sharing code. So far we rely heavily on packages and libraries that are installed and shipped within our application or library. By generating code at development time, runtime dependencies can be avoided. This is in particular interesting for library developers who strive for having zero or only few dependencies.

Whether this new way of sharing code will prevail over the standard way remains to be seen, as there are two sides of the coin. By including code instead of referencing a library, versioning and updating of dependencies will be much harder, in particular for transitive dependencies.

Finally, I would like to talk about another use case and the part of code generation that inspires me the most. Code generation allows us to augment the language and provide a developer experience that can not be provided by other means. Attributes and other markers are used to trigger code generation and the IDE offers code completion for the new code in a blink of an eye. For the consuming developer, using such a library can feel like a new language feature. Some source generators crafted by the community might even have feedback on future .NET versions and the most common use cases could be included in the standard library.

Conclusion

I had a deep dive into source generators and implemented a library for generating fluent APIs. This use case is a good fit for source generation as the implementation can be tedious on the one hand, but follows clear rules on the other hand. With a fluent builder object initialization becomes easier to read as well as easer to write.

From my point of view source generators are an exciting feature that has many more use cases and the potential to have a long lasting impact on .NET.

If you are interested in other source generators and use cases I suggest that you have a look at this list.

Thank you for reading. If you have any questions, thoughts, or feedback you'd like to share, please feel free to connect with me via mail. For more updates on upcoming blog posts and library releases, you may follow me on Twitter.

Happy Coding!
- Kevin