generic-builders.md
July 3, 2026 · View on GitHub
Generic builders
Sometimes many related types need the same build-up treatment. A single Builders<IMessage<TT, TT2>>("BuildUp") call creates a builder for every type implementing IMessage<TT, TT2> that is visible at compile time — here both QueryMessage<,> and CommandMessage<,> get their own BuildUp overload.
Each builder injects members marked with the [Dependency] attribute into an existing instance, so instances created elsewhere (e.g. by an API controller) still receive their dependencies.
using Shouldly;
using Pure.DI;
DI.Setup(nameof(Composition))
.Bind(Tag.Id).To(() => (TT)(object)Guid.NewGuid())
.Bind().To<MessageTracker<TT>>()
// Generic builder to inject dependencies into existing messages
.Builders<IMessage<TT, TT2>>("BuildUp");
var composition = new Composition();
// A Query is created (e.g. by API controller), ID is missing
var query = new QueryMessage<Guid, string>();
// Composition injects dependencies and generates an ID
var queryWithDeps = composition.BuildUp(query);
queryWithDeps.Id.ShouldNotBe(Guid.Empty);
queryWithDeps.Tracker.ShouldBeOfType<MessageTracker<string>>();
// A Command is created, usually with a specific ID
var command = new CommandMessage<Guid, int>();
// Composition injects dependencies only
var commandWithDeps = composition.BuildUp(command);
commandWithDeps.Id.ShouldBe(Guid.Empty);
commandWithDeps.Tracker.ShouldBeOfType<MessageTracker<int>>();
// Works with abstract types/interfaces too
var queryMessage = new QueryMessage<Guid, double>();
queryMessage = composition.BuildUp(queryMessage);
queryMessage.ShouldBeOfType<QueryMessage<Guid, double>>();
queryMessage.Id.ShouldNotBe(Guid.Empty);
queryMessage.Tracker.ShouldBeOfType<MessageTracker<double>>();
interface IMessageTracker<T>;
class MessageTracker<T> : IMessageTracker<T>;
interface IMessage<out TId, TContent>
{
TId Id { get; }
IMessageTracker<TContent>? Tracker { get; }
}
record QueryMessage<TId, TContent> : IMessage<TId, TContent>
where TId : struct
{
public TId Id { get; private set; }
[Dependency]
public IMessageTracker<TContent>? Tracker { get; set; }
// Injects a new ID
[Dependency]
public void SetId([Tag(Tag.Id)] TId id) => Id = id;
}
record CommandMessage<TId, TContent> : IMessage<TId, TContent>
where TId : struct
{
public TId Id { get; }
[Dependency]
public IMessageTracker<TContent>? Tracker { get; set; }
}
Running this code sample locally
- Make sure you have the .NET SDK 10.0 or later installed
dotnet --list-sdk
- Create a net10.0 (or later) console application
dotnet new console -n Sample
dotnet add package Pure.DI
dotnet add package Shouldly
- Copy the example code into the Program.cs file
You are ready to run the example 🚀
dotnet run
Note
Generic builders provide compile-time type safety while allowing flexible object graph construction.
The following partial class will be generated
partial class Composition
{
#pragma warning disable CS0162
[MethodImpl(MethodImplOptions.NoInlining)]
public IMessage<T1, T4> BuildUp<T1, T4>(IMessage<T1, T4> buildingInstance)
where T1: struct
{
if (buildingInstance is null) throw new ArgumentNullException(nameof(buildingInstance));
if (TryBuildUp<T1, T4>(buildingInstance))
{
return buildingInstance;
}
throw new ArgumentException($"Unable to build an instance of typeof type {buildingInstance.GetType()}.", "buildingInstance");
}
#pragma warning restore CS0162
#pragma warning disable CS0162
[MethodImpl(MethodImplOptions.NoInlining)]
public bool TryBuildUp<T1, T4>(IMessage<T1, T4> buildingInstance)
where T1: struct
{
if (buildingInstance is null) throw new ArgumentNullException(nameof(buildingInstance));
switch (buildingInstance)
{
case QueryMessage<T1, T4> QueryMessage_TT_TT2:
BuildUp(QueryMessage_TT_TT2);
return true;
case CommandMessage<T1, T4> CommandMessage_TT_TT2:
BuildUp(CommandMessage_TT_TT2);
return true;
default:
return false;
}
return false;
}
#pragma warning restore CS0162
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public CommandMessage<T1, T4> BuildUp<T1, T4>(CommandMessage<T1, T4> buildingInstance)
where T1: struct
{
if (buildingInstance is null) throw new ArgumentNullException(nameof(buildingInstance));
CommandMessage<T1, T4> transientCommandMessageTTTT2;
CommandMessage<T1, T4> localBuildingInstance = buildingInstance;
localBuildingInstance.Tracker = new MessageTracker<T4>();
transientCommandMessageTTTT2 = localBuildingInstance;
return transientCommandMessageTTTT2;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public QueryMessage<T1, T4> BuildUp<T1, T4>(QueryMessage<T1, T4> buildingInstance)
where T1: struct
{
if (buildingInstance is null) throw new ArgumentNullException(nameof(buildingInstance));
QueryMessage<T1, T4> transientQueryMessageTTTT2;
QueryMessage<T1, T4> localBuildingInstance = buildingInstance;
T1 transientTT = (T1)(object)Guid.NewGuid();
localBuildingInstance.Tracker = new MessageTracker<T4>();
localBuildingInstance.SetId(transientTT);
transientQueryMessageTTTT2 = localBuildingInstance;
return transientQueryMessageTTTT2;
}
}
Class diagram:
---
config:
class:
hideEmptyMembersBox: true
---
classDiagram
MessageTrackerᐸT4ᐳ --|> IMessageTrackerᐸT4ᐳ
Composition ..> IMessageᐸT1ˏT4ᐳ : IMessageᐸT1ˏT4ᐳ BuildUpᐸT1ˏT4ᐳ(Pure.DI.UsageTests.Generics.GenericBuildersScenario.IMessage<T1, T4> buildingInstance)
Composition ..> CommandMessageᐸT1ˏT4ᐳ : CommandMessageᐸT1ˏT4ᐳ BuildUpᐸT1ˏT4ᐳ(Pure.DI.UsageTests.Generics.GenericBuildersScenario.CommandMessage<T1, T4> buildingInstance)
Composition ..> QueryMessageᐸT1ˏT4ᐳ : QueryMessageᐸT1ˏT4ᐳ BuildUpᐸT1ˏT4ᐳ(Pure.DI.UsageTests.Generics.GenericBuildersScenario.QueryMessage<T1, T4> buildingInstance)
CommandMessageᐸT1ˏT4ᐳ *-- MessageTrackerᐸT4ᐳ : IMessageTrackerᐸT4ᐳ
QueryMessageᐸT1ˏT4ᐳ *-- MessageTrackerᐸT4ᐳ : IMessageTrackerᐸT4ᐳ
QueryMessageᐸT1ˏT4ᐳ *-- T1 : "Id" T1
namespace Pure.DI.UsageTests.Generics.GenericBuildersScenario {
class CommandMessageᐸT1ˏT4ᐳ {
<<record>>
+IMessageTrackerᐸT4ᐳɁ Tracker
}
class Composition {
<<partial>>
+IMessageᐸT1ˏT4ᐳ BuildUpᐸT1ˏT4ᐳ(Pure.DI.UsageTests.Generics.GenericBuildersScenario.IMessage<T1, T4> buildingInstance)
+CommandMessageᐸT1ˏT4ᐳ BuildUpᐸT1ˏT4ᐳ(Pure.DI.UsageTests.Generics.GenericBuildersScenario.CommandMessage<T1, T4> buildingInstance)
+QueryMessageᐸT1ˏT4ᐳ BuildUpᐸT1ˏT4ᐳ(Pure.DI.UsageTests.Generics.GenericBuildersScenario.QueryMessage<T1, T4> buildingInstance)
}
class IMessageTrackerᐸT4ᐳ {
<<interface>>
}
class IMessageᐸT1ˏT4ᐳ {
<<interface>>
}
class MessageTrackerᐸT4ᐳ {
<<class>>
+MessageTracker()
}
class QueryMessageᐸT1ˏT4ᐳ {
<<record>>
+IMessageTrackerᐸT4ᐳɁ Tracker
+SetId(T1 id) : Void
}
}
See also: