Shows how to declare a binding directly on an implementation type with a custom attribute. A custom attribute can combine contract type, lifetime, and tag metadata by registering argument positions in the composition setup.
using Shouldly;
using Pure.DI;
DI.Setup(nameof(Composition))
.TypeAttribute<ServiceAttribute<TT>>()
.LifetimeAttribute<ServiceAttribute<TT>>()
.TagAttribute<ServiceAttribute<TT>>(1)
// Composition root
.Root<IMessageWriter>("Writer", "console");
var composition = new Composition();
var writer = composition.Writer;
writer.Write("Pure.DI");
writer.ShouldBeOfType<ConsoleMessageWriter>();
writer.ShouldBeSameAs(composition.Writer);
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
class ServiceAttribute<T> : Attribute
{
public ServiceAttribute(
Lifetime lifetime = Lifetime.Transient,
object? tag = null)
{
}
}
interface IMessageWriter
{
void Write(string message);
}
[Service<IMessageWriter>(Lifetime.Singleton, "console")]
class ConsoleMessageWriter : IMessageWriter
{
public void Write(string message) => Console.WriteLine(message);
}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 Sampledotnet 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 runNote
Implementation-level binding attributes are useful when the implementation assembly should describe its DI role while keeping the composition concise. Custom attributes participate in the same merge rules as the built-in Bind, Type, Tag, and Lifetime attributes: attributes in one square-bracket group form one binding, while separate Bind groups create separate bindings.
Because the attribute arguments are mapped by position in the setup, one custom attribute can carry contract, lifetime and tag together, and the class declaration stays readable. The registration tells the generator which argument holds what. Positional mapping is brittle when the attribute constructor changes: reordering parameters changes what every existing use means. Keep the constructor order stable or register explicit indices.
The following partial class will be generated
partial class Composition
{
#if NET9_0_OR_GREATER
private readonly Lock _lock = new Lock();
#else
private readonly Object _lock = new Object();
#endif
private ConsoleMessageWriter? _singletonConsoleMessageWriter1;
public IMessageWriter Writer
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get
{
if (_singletonConsoleMessageWriter1 is null)
lock (_lock)
if (_singletonConsoleMessageWriter1 is null)
{
_singletonConsoleMessageWriter1 = new ConsoleMessageWriter();
}
return _singletonConsoleMessageWriter1;
}
}
}Class diagram:
---
config:
class:
hideEmptyMembersBox: true
---
classDiagram
ConsoleMessageWriter --|> IMessageWriter : "console"
Composition ..> ConsoleMessageWriter : IMessageWriter Writer
namespace Pure.DI.UsageTests.Attributes.CustomBindAttributeScenario {
class Composition {
<<partial>>
+IMessageWriter Writer
}
class ConsoleMessageWriter {
<<class>>
+ConsoleMessageWriter()
}
class IMessageWriter {
<<interface>>
}
}
See also: