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
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: