Fluent builder inheritance - how to make it work in C#?

Fluent builder is a well-known design pattern that helps to build objects of a given type. It makes code more readable and maintainable thanks to method chaining. The problem occurs when you try to extend the functionality of the existing builder using inheritance.

Fluent builder inheritance - how to make it work in C#?

Extending existing fluent builder

Below you can see a simple example of the fluent builder. Of course, in real life logic of building methods is usually far more complex than that.

public class Tenant{    public string Id { get; set; }    public Uri BaseUrl { get; set; }    public string Secret { get; set; }    public string QaCloudId { get; set; }}
public class TenantBuilder{    private readonly Tenant _tenant;
    public TenantBuilder()    {        _tenant = new Tenant();    }
    public TenantBuilder WithId(string id)    {        _tenant.Id = id;        return this;    }
    public TenantBuilder WithBaseUrl(string baseUri)    {        _tenant.BaseUrl = new Uri(baseUri);        return this;    }
    public Tenant Build()    {        return _tenant;    }}
public class Program{    public static void Main()    {        var tenant = new TenantBuilder()            .WithId("Tenant1")            .WithBaseUrl("https://tenant1.local")            .Build();    }}
C#

Now let’s imagine that we want to extend this builder with a method that will also fill Secret property on the Tenant object. Let’s assume that we want to do it using inheritance following the open-closed principle. First of all, we change the access modifier of tenant property on the first builder level (and fix usages).

public class TenantBuilder{    protected readonly Tenant Tenant;    ...
C#

Then we create a new builder and derive from TenantBuilder:

public class ConfidentialTenantBuilder : TenantBuilder{    public ConfidentialTenantBuilder WithSecret(string secret)    {        Tenant.Secret = secret;        return this;    }}
C#

Now, we can use it:

public static void Main(){    var tenant = new ConfidentialTenantBuilder()        .WithSecret("secret")        .WithId("Test")        .WithBaseUrl("https://tenant1.local")        .Build();}
C#

And everything seems to work fine. But what if we change the order of these methods?

public static void Main(){    var tenant = new ConfidentialTenantBuilder()        .WithId("Test")        .WithBaseUrl("https://tenant1.local")        .WithSecret("secret")        .Build();}
C#

As a result, this code will not compile. Why? The problem is that when we create ConfidentialTenantBuilder and then invoke WithId method, a builder of type TenantBuilder is returned which does not have WithSecret method. In other words, we lose fluency.

Prepare fluent builder for inheritance

How to prepare an existing fluent builder for an inheritance? The solution for that is named recursive generics and is based on passing the child builder type up to the parent. Then parent builder can cast this to child type.

public class Tenant{    public string Id { get; set; }    public Uri BaseUrl { get; set; }    public string Secret { get; set; }    public string QaCloudId { get; set; }}
public class TenantBuilder<TBuilder> where TBuilder : TenantBuilder<TBuilder>{    protected readonly Tenant Tenant;
    public TenantBuilder()    {        Tenant = new Tenant();    }
    public TBuilder WithId(string id)    {        Tenant.Id = id;        return (TBuilder)this;    }
    public TBuilder WithBaseUrl(string baseUri)    {        Tenant.BaseUrl = new Uri(baseUri);        return (TBuilder)this;    }
    public Tenant Build()    {        return Tenant;    }}
public class ConfidentialTenantBuilder : TenantBuilder<ConfidentialTenantBuilder>{    public ConfidentialTenantBuilder WithSecret(string secret)    {        Tenant.Secret = secret;        return this;    }}
public class Program{    public static void Main()    {        var tenant = new ConfidentialTenantBuilder()            .WithSecret("secret")            .WithId("Test")            .WithBaseUrl("https://tenant1.local")            .Build();    }}
C#

Now everything should work fine. As you can see, in each parent method we cast this to TBuilder which is a generic type of child builder. This casting is possible (and safe), because of strange-looking constraints:

public class TenantBuilder<TBuilder> where TBuilder : TenantBuilder<TBuilder>
C#

which is a perfectly valid one because we just expect TBuilder type to inherit from the parent builder with TBuilder as a generic type argument.

Remember to adjust child builder too

You might have noticed that although we enabled parent builder inheritance we cannot derive from child builder because of the same issue. To fix that we need to make child builder generic in the same way like for parent builder.

public class ConfidentialTenantBuilder<TBuilder>    : TenantBuilder<TBuilder> where TBuilder : ConfidentialTenantBuilder<TBuilder>{    public TBuilder WithSecret(string secret)    {        Tenant.Secret = secret;        return (TBuilder)this;    }}
C#

In the same vein, let’s derive from ConfidentialTenantBuilder:

public class QaTenantBuilder<TBuilder>    : ConfidentialTenantBuilder<TBuilder>    where TBuilder : QaTenantBuilder<TBuilder>{    public TBuilder WithQaCloudId(string qaCloudId)    {        Tenant.QaCloudId = qaCloudId;        return (TBuilder)this;    }}
C#

And use it in the client code:

public class Program{    public class TestTenantBuilder : QaTenantBuilder<TestTenantBuilder> {}
    public static void Main()    {        var tenant = new TestTenantBuilder()            .WithSecret("secret")            .WithQaCloudId("cloud01")            .WithId("Test")            .WithBaseUrl("https://tenant1.local")            .Build();    }}
C#

Vualá 🎉! We could also implement an implicit operator to cast your builder to target type easily:

public class Program{    public class TestTenantBuilder : QaTenantBuilder<TestTenantBuilder>    {        public static implicit operator Tenant(TestTenantBuilder builder)        {            return builder.Build();        }    }
    public static void Main()    {        Tenant tenant = new TestTenantBuilder()            .WithSecret("secret")            .WithQaCloudId("cloud01")            .WithId("Test")            .WithBaseUrl("https://tenant1.local");    }}
C#

You can find the source code of described builder here.

Conclusion

To sum up, fluent builder inheritance is possible but requires parent builders to be prepared for that. It has to follow the recursive generics approach to cast itself to the appropriate child builder type. It’s worth remembering, especially if you create a library that other developers will use in their projects.

Comments

Add comment
The comments system is based on GitHub Issues API. Once you add your comment to the linked issue on my GitHub repository, it will show up below.
© brokul.dev 2024