Skip to content

Latest commit

 

History

History
153 lines (128 loc) · 4.69 KB

generics.md

File metadata and controls

153 lines (128 loc) · 4.69 KB

Generics

CSharp

Generic types are also supported.

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 IDependency<TT>, TT is a marker type, which allows the usual IDependency<TT> to be used instead of an open generic type like IDependency<>. This makes it easy to bind generic types by specifying marker types such as TT, TT1, etc. as parameters of generic types:

interface IDependency<T>;

class Dependency<T> : IDependency<T>;

interface IService
{
    IDependency<int> IntDependency { get; }

    IDependency<string> StringDependency { get; }
}

class Service(
    IDependency<int> intDependency,
    IDependency<string> stringDependency)
    : IService
{
    public IDependency<int> IntDependency { get; } = intDependency;

    public IDependency<string> StringDependency { get; } = stringDependency;
}

DI.Setup(nameof(Composition))
    // This hint indicates to not generate methods such as Resolve
    .Hint(Hint.Resolve, "Off")
    .Bind<IDependency<TT>>().To<Dependency<TT>>()
    .Bind<IService>().To<Service>()

    // Composition root
    .Root<IService>("Root");

var composition = new Composition();
var service = composition.Root;
service.IntDependency.ShouldBeOfType<Dependency<int>>();
service.StringDependency.ShouldBeOfType<Dependency<string>>();

Actually, the property Root looks like:

public IService Root
{
  get
  {
    return new Service(new Dependency<int>(), new Dependency<string>());
  }
}

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> { }

The following partial class will be generated:

partial class Composition
{
  private readonly Composition _root;

  public Composition()
  {
    _root = this;
  }

  internal Composition(Composition parentScope)
  {
    _root = (parentScope ?? throw new ArgumentNullException(nameof(parentScope)))._root;
  }

  public IService Root
  {
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    get
    {
      return new Service(new Dependency<int>(), new Dependency<string>());
    }
  }
}

Class diagram:

classDiagram
	class Composition {
		<<partial>>
		+IService Root
	}
	Service --|> IService
	class Service {
		+Service(IDependencyᐸInt32ᐳ intDependency, IDependencyᐸStringᐳ stringDependency)
	}
	DependencyᐸInt32ᐳ --|> IDependencyᐸInt32ᐳ
	class DependencyᐸInt32ᐳ {
		+Dependency()
	}
	DependencyᐸStringᐳ --|> IDependencyᐸStringᐳ
	class DependencyᐸStringᐳ {
		+Dependency()
	}
	class IService {
		<<interface>>
	}
	class IDependencyᐸInt32ᐳ {
		<<interface>>
	}
	class IDependencyᐸStringᐳ {
		<<interface>>
	}
	Composition ..> Service : IService Root
	Service *--  DependencyᐸInt32ᐳ : IDependencyᐸInt32ᐳ
	Service *--  DependencyᐸStringᐳ : IDependencyᐸStringᐳ