A fluent builder library for creating objects with configurable blueprints. Supports pre-configured states and property overrides via fluent syntax. Ideal for unit tests, database seeding, configuration management, domain objects (DDD), and more.
$ dotnet add package ViniBas.FluentBlueprintBuilderCopyright (c) Vinícius Bastos da Silva 2026.
Licensed under the GNU Lesser General Public License v3 (LGPL v3).
See the LICENSE.txt file for details.
ViniBas.FluentBlueprintBuilder is a library that provides an abstract class to assist in easily building objects using a fluent syntax. It supports creating pre-configured states (blueprints), as well as overriding individual values at usage time.
Builders created with this library can be used in various scenarios:
First, you need to install the library in your project as a NuGet package:
dotnet add package ViniBas.FluentBlueprintBuilder
You must create your builder classes by inheriting from BlueprintBuilder<>.
There are two variants of BlueprintBuilder:
BlueprintBuilder<TBuilder, TBlueprint, TTarget>BlueprintBuilder<TBuilder, TTarget> (here, TBuilder itself is used as the blueprint)Expected generic type arguments:
BlueprintBuilder<TBuilder, TTarget>, the builder itself acts as the blueprint.Your builder class that inherits from BlueprintBuilder<> must provide a public parameterless constructor so the instance can be created internally by the Create method.
There are three methods you may override: ConfigureBlueprints, ConfigureDefaultValues, and GetInstance.
ConfigureBlueprintsConfigureBlueprints receives a dictionary as a parameter. You must add at least one blueprint.
A blueprint consists of:
string), andFunc<TBlueprint>) that creates the blueprint instance.Blueprints are selected when calling Build, based on the blueprintKey or index passed to it (see Using the Builder).
Overriding this method is:
BlueprintBuilder<TBuilder, TBlueprint, TTarget>BlueprintBuilder<TBuilder, TTarget> (its default implementation adds a "default" blueprint returning the current TBuilder instance; you can reference this key via the DefaultBlueprintName constant)ConfigureDefaultValuesConfigureDefaultValues allows you to define the baseline state for your blueprint properties. This method is called automatically during builder initialization.
Inside this method, you can use the Set method to assign default values. This is the ideal place to integrate data generation libraries like Bogus to ensure every built object has valid, unique data.
Values defined here:
Set calls in the fluent chain.GetInstanceGetInstance creates the target object (TTarget) based on the blueprint instance received as a parameter.
Overriding this method is optional in both variants. The default implementation uses reflection:
TTarget instance using a compatible constructor.To use the default GetInstance, TTarget must have at least one constructor whose parameters can be satisfied by blueprint properties (or a parameterless constructor).
If you override GetInstance, you can build the target object manually—useful if you want to avoid reflection or if TTarget has no compatible constructors.
Consider that we want to create an instance of the User class as the target:
public class User
{
public string Name { get; set; } = string.Empty;
public int Age { get; set; }
public string Email { get; set; } = string.Empty;
}
BlueprintBuilder<TBuilder, TBlueprint, TTarget>using ViniBas.FluentBlueprintBuilder;
using Bogus; // Example library for data generation
public class UserBuilder : BlueprintBuilder<UserBuilder, UserBlueprint, User>
{
protected override void ConfigureDefaultValues()
{
var faker = new Faker();
// Dynamic default values (generated per build)
Set(x => x.Name, _ => faker.Name.FullName());
Set(x => x.Age, _ => faker.Random.Int(18, 80));
// Dependent value: Email depends on the generated Name
Set(x => x.Email, bp => faker.Internet.Email(bp.Name));
}
protected override void ConfigureBlueprints(IDictionary<string, Func<UserBlueprint>> blueprints)
{
blueprints["default"] = () => new UserBlueprint("John Doe", 30, "john@example.com");
blueprints["admin"] = () => new UserBlueprint("Admin User", 40, "admin@example.com");
blueprints["minor"] = () => new UserBlueprint("Kid User", 15, "kid@example.com");
}
// Optional:
protected override User GetInstance(UserBlueprint blueprint)
=> new ()
{
Name = blueprint.Name,
Age = blueprint.Age,
Email = blueprint.Email
};
}
public record UserBlueprint(string Name, int Age, string Email);
BlueprintBuilder<TBuilder, TTarget>using ViniBas.FluentBlueprintBuilder;
public class UserBuilder : BlueprintBuilder<UserBuilder, User>
{
public string Name { get; set; } = "John Doe";
public int Age { get; set; } = 30;
public string Email { get; set; } = "john@example.com";
// Optional (the default implementation already registers DefaultBlueprintName => this).
// Override only if you need to add more blueprints:
protected override void ConfigureBlueprints(IDictionary<string, Func<UserBuilder>> blueprints)
{
base.ConfigureBlueprints(blueprints); // registers "default" => this
blueprints["admin"] = () => new UserBuilder { Name = "Admin User", Age = 40, Email = "admin@example.com" };
}
// Optional:
protected override User GetInstance(UserBuilder blueprint)
=> new ()
{
Name = blueprint.Name,
Age = blueprint.Age,
Email = blueprint.Email
};
}
The fluent API uses the following methods:
Create(defaultBlueprintKey?) to create a builder instance, optionally specifying a per-instance default blueprint key. This key is used as a fallback by Build when neither a blueprintKey nor an index is explicitly passed to it. If none of these are provided, the first registered blueprint is used.Set (optional) to override blueprint values. It supports:
.Set(p => p.Age, 25).Set(p => p.Id, _ => Guid.NewGuid()) (useful for unique values per build).Set(p => p.Email, bp => $"{bp.Name}@example.com") (value depends on the current blueprint state)Build(blueprintKey?, index?) to construct the final target object. You can specify either a blueprintKey (case-insensitive string; throws KeyNotFoundException if not found) or an index (zero-based position based on blueprint registration order) to select the desired blueprint. If both are provided and refer to different blueprints, an ArgumentOutOfRangeException is thrown. If neither is provided, falls back to the default key set via Create, or to the first registered blueprint.BuildMany to construct a sequence of target objects. It has two overloads:
BuildMany(params string[] blueprintKeys) — builds one object per provided key, in the given order.BuildMany(uint? size, params string[] blueprintKeys) — builds size objects, cycling through the provided keys in order. If no keys are given, it cycles through all registered blueprints in registration order. If size is omitted, it defaults to the number of provided keys, or to the total number of registered blueprints if no keys are given.Clone() to create an independent copy of the current builder, inheriting its default blueprint key and all Set overrides configured so far. This is useful when the same builder instance (possibly with some Set calls already applied) is used in multiple places, but additional Set overrides are only relevant in some of them — cloning avoids having those overrides affect other usages.var user = UserBuilder.Create()
.Set(p => p.Name, "Changed Name")
// Dynamic value depending on the current state of the blueprint
.Set(p => p.Email, bp => $"changed.{bp.Name.Replace(" ", ".")}@example.com")
.Build("my-blueprint-key");
// A base builder shared across multiple usages
var baseBuilder = UserBuilder.Create("admin")
.Set(p => p.Name, "Shared Name");
// Extra Set only needed in one specific usage — clone to avoid contaminating baseBuilder
var specificBuilder = baseBuilder.Clone()
.Set(p => p.Email, "specific@example.com");
var sharedUser = baseBuilder.Build(); // Name = "Shared Name", Email = from blueprint
var specificUser = specificBuilder.Build(); // Name = "Shared Name", Email = "specific@example.com"
BlueprintBuilder<> implements the IBlueprintBuilder<TTarget> interface, which exposes Build and BuildMany. This allows you to declare dependencies on IBlueprintBuilder<TTarget> in your classes and substitute them with mocks (e.g., using Moq) in unit tests:
// Production code
public class OrderService(IBlueprintBuilder<Order> builder)
{
public Order CreateDefaultOrder() => builder.Build();
}
// Unit test
var mockBuilder = new Mock<IBlueprintBuilder<Order>>();
mockBuilder.Setup(b => b.Build(null, null)).Returns(new Order { ... });
var service = new OrderService(mockBuilder.Object);
Three protected members are available inside your builder subclass to support unit tests:
RegisteredBlueprintKeys — returns an IReadOnlyList<string> with the keys of all registered blueprints in registration order. Useful for asserting that blueprints were configured correctly.CreateBlueprint(string blueprintKey) — returns the raw blueprint instance produced by the factory registered under the given key, without applying any Set overrides or calling GetInstance. Useful for testing the blueprint configuration in isolation from target construction.GetInstance(TBlueprint blueprint) — the virtual method that converts a blueprint into a TTarget. When overridden, it can be tested in isolation by passing a blueprint directly — without going through key resolution or Set overrides.To access these members from a test without polluting the production builder's public API, create a test-only subclass in your test project that exposes them. Note that Create() returns TBuilder (the production type), so the helper must be instantiated directly with new — the constructor already triggers ConfigureBlueprints:
// In the test project only
public class UserBuilderTestHelper : UserBuilder
{
public new IReadOnlyList<string> RegisteredBlueprintKeys => base.RegisteredBlueprintKeys;
public UserBlueprint GetBlueprint(string key) => CreateBlueprint(key);
public User BuildFromBlueprint(UserBlueprint blueprint) => GetInstance(blueprint);
}
// Unit tests
[Fact]
public void ConfigureBlueprints_ShouldRegisterExpectedBlueprints()
{
var keys = new UserBuilderTestHelper().RegisteredBlueprintKeys;
Assert.Contains("default", keys);
Assert.Contains("admin", keys);
Assert.Contains("minor", keys);
}
[Fact]
public void Blueprint_Admin_ShouldHaveExpectedValues()
{
var blueprint = new UserBuilderTestHelper().GetBlueprint("admin");
Assert.Equal("Admin User", blueprint.Name);
Assert.Equal(40, blueprint.Age);
}
[Fact]
public void GetInstance_Admin_ShouldMapBlueprintToTargetCorrectly()
{
var helper = new UserBuilderTestHelper();
// Blueprint can come from ConfigureBlueprints or be constructed manually for edge cases
var blueprint = helper.GetBlueprint("admin");
var target = helper.BuildFromBlueprint(blueprint);
Assert.Equal("Admin User", target.Name);
Assert.Equal(40, target.Age);
}