Skip to content

Latest commit

 

History

History
203 lines (170 loc) · 6.85 KB

generics.md

File metadata and controls

203 lines (170 loc) · 6.85 KB

Generics

Generic types are supported out of the box: a single binding like Bind<IRepository<TT>>().To<Repository<TT>>() covers IRepository<User>, IRepository<Order> and any other instantiation used in the object graph. Since Pure.DI is a source generator, each of them is turned into concrete, reflection-free code at compile time. This is the normal way to bind repositories, serializers and per-entity handlers in a real project: one line covers every closed instantiation the graph meets.

Important

Instead of open generic types, as in classical DI container libraries, regular generic types with marker types as type parameters are used here. Such "marker" types allow to define dependency graph more precisely.

For the case of IRepository<TT>, TT is a marker type, which allows the usual IRepository<TT> to be used instead of an open generic type like IRepository<>. This makes it easy to bind generic types by specifying marker types such as TT, TT1, etc. as parameters of generic types:

using Shouldly;
using Pure.DI;

DI.Setup(nameof(Composition))
    // Disable Resolve methods to keep the public API minimal
    .Hint(Hint.Resolve, "Off")
    // Binding a generic interface to a generic implementation
    // using the marker type TT. This allows Pure.DI to match
    // IRepository<User> to Repository<User>, IRepository<Order> to Repository<Order>, etc.
    .Bind<IRepository<TT>>().To<Repository<TT>>()
    .Bind<IDataService>().To<DataService>()

    // Composition root
    .Root<IDataService>("DataService");

var composition = new Composition();
var dataService = composition.DataService;

// Verifying that the correct generic types were injected
dataService.Users.ShouldBeOfType<Repository<User>>();
dataService.Orders.ShouldBeOfType<Repository<Order>>();

interface IRepository<T>;

class Repository<T> : IRepository<T>;

// Domain entities
record User;

record Order;

interface IDataService
{
    IRepository<User> Users { get; }

    IRepository<Order> Orders { get; }
}

class DataService(
    IRepository<User> users,
    IRepository<Order> orders)
    : IDataService
{
    public IRepository<User> Users { get; } = users;

    public IRepository<Order> Orders { get; } = orders;
}
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

Actually, the property DataService looks like:

public IDataService DataService
{
  get
  {
    return new DataService(new Repository<User>(), new Repository<Order>());
  }
}

Even in this simple scenario, it is not possible to precisely define the binding of an abstraction to its implementation using open generic types:

.Bind(typeof(IMap<,>)).To(typeof(Map<,>))

You can try to match them by order or by name derived from the .NET type reflection. But this is not reliable, since order and name matching is not guaranteed. For example, there is some interface with two arguments of type _key and value. But in its implementation the sequence of type arguments is mixed up: first comes the value and then the key and the names do not match:

class Map<TV, TK>: IMap<TKey, TValue> { }

At the same time, the marker types TT1 and TT2 handle this easily. They determine the exact correspondence between the type arguments in the interface and its implementation:

.Bind<IMap<TT1, TT2>>().To<Map<TT2, TT1>>()

The first argument of the type in the interface, corresponds to the second argument of the type in the implementation and is a key. The second argument of the type in the interface, corresponds to the first argument of the type in the implementation and is a value. This is a simple example. Obviously, there are plenty of more complex scenarios where tokenized types will be useful. Marker types are regular .NET types marked with a special attribute, such as:

[GenericTypeArgument]
internal abstract class TT1 { }

[GenericTypeArgument]
internal abstract class TT2 { }

This way you can easily create your own, including making them fit the constraints on the type parameter, for example:

[GenericTypeArgument]
internal struct TTS { }

[GenericTypeArgument]
internal interface TTDisposable: IDisposable { }

[GenericTypeArgument]
internal interface TTEnumerator<out T>: IEnumerator<T> { }

Only the instantiations the graph actually uses are generated, so the cost stays proportional to the closed types you inject: adding IRepository<Invoice> needs no binding, and an unused IRepository<Report> produces no code at all.

Warning

Marker types are not real dependencies. Never inject IRepository<TT> outside a generic binding - the generator sees a closed type there and reports that it cannot resolve it.

Prefer markers over a hand-written binding per closed type whenever the implementation follows the same type parameter. When one closed type needs another implementation, add an explicit binding for it; see Complex generics for reordered and nested type parameters.

The following partial class will be generated
partial class Composition
{
  public IDataService DataService
  {
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    get
    {
      return new DataService(new Repository<User>(), new Repository<Order>());
    }
  }
}

Class diagram:

---
 config:
  class:
   hideEmptyMembersBox: true
---
classDiagram
	DataService --|> IDataService
	RepositoryᐸUserᐳ --|> IRepositoryᐸUserᐳ
	RepositoryᐸOrderᐳ --|> IRepositoryᐸOrderᐳ
	Composition ..> DataService : IDataService DataService
	DataService *-- RepositoryᐸUserᐳ : IRepositoryᐸUserᐳ
	DataService *-- RepositoryᐸOrderᐳ : IRepositoryᐸOrderᐳ
	namespace Pure.DI.UsageTests.Generics.GenericsScenario {
		class Composition {
		<<partial>>
		+IDataService DataService
		}
		class DataService {
				<<class>>
			+DataService(IRepositoryᐸUserᐳ users, IRepositoryᐸOrderᐳ orders)
		}
		class IDataService {
			<<interface>>
		}
		class IRepositoryᐸOrderᐳ {
			<<interface>>
		}
		class IRepositoryᐸUserᐳ {
			<<interface>>
		}
		class RepositoryᐸOrderᐳ {
				<<class>>
			+Repository()
		}
		class RepositoryᐸUserᐳ {
				<<class>>
			+Repository()
		}
	}
Loading

See also: