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(); }}
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; ...
Then we create a new builder and derive from TenantBuilder
:
public class ConfidentialTenantBuilder : TenantBuilder{ public ConfidentialTenantBuilder WithSecret(string secret) { Tenant.Secret = secret; return this; }}
Now, we can use it:
public static void Main(){ var tenant = new ConfidentialTenantBuilder() .WithSecret("secret") .WithId("Test") .WithBaseUrl("https://tenant1.local") .Build();}
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();}
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(); }}
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>
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; }}
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; }}
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(); }}
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"); }}
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.