Skip to content

Open generics

Register a generic class once and let the container close it for every type argument.

2 min read

A generic class is registered as an open generic — one descriptor covering every closed construction. MSDI closes it on demand, so IRepository<Todo> and IRepository<User> both resolve from a single registration.

Registering against an open-generic interface

Use ServiceType with an unbound typeof:

Repository.cs
using Immediate.Injections.Shared;

public interface IRepository<T>
{
	Type ElementType { get; }
}

[RegisterTransient(ServiceType = typeof(IRepository<>))]
public sealed class Repository<T> : IRepository<T>
{
	public Type ElementType => typeof(T);
}
Generated output
ServiceDescriptor.Transient(typeof(IRepository<>), typeof(Repository<>));

Every closed construction now resolves without further registration:

var todos = provider.GetRequiredService<IRepository<Todo>>();   // Repository<Todo>
var users = provider.GetRequiredService<IRepository<User>>();   // Repository<User>

There is no generic-attribute equivalent of this: [RegisterTransient<IRepository<>>] is not legal C#, which is exactly why ServiceType still exists on the non-generic form.

The arities must match. typeof(IRepository<>) on a class with two type parameters, or a service type the class does not implement, is INJ0004.

Registering the class as itself

A bare attribute on a generic class registers the open generic against itself:

Cache.cs
using Immediate.Injections.Shared;

[RegisterSingleton]
public sealed class Cache<T>;
Generated output
ServiceDescriptor.Singleton(typeof(Cache<>), typeof(Cache<>));

Closed constructions

If you want just one closed construction rather than the open generic, name it explicitly with either the two-type-argument attribute form or a bound ServiceType:

Closed.cs
using Immediate.Injections.Shared;

// Registers IRepository<Todo> -> Repository<Todo>
[RegisterScoped<IRepository<Todo>, Repository<Todo>>]
public sealed class Repository<T> : IRepository<T>;
ClosedServiceType.cs
// Also registers IRepository<Todo> -> Repository<Todo>
[RegisterScoped(ServiceType = typeof(IRepository<Todo>))]
public sealed class Repository<T> : IRepository<T>;

Both forms may be repeated to register several closed constructions of the same class.

Strategies on a generic class

ImplementedInterfaces and SelfAndImplementedInterfaces work on generic classes, but they filter the interface list hard. An interface is included only if it is generic, has the same arity as the class, and is closed over the class’s own type parameters.

Filtering.cs
using Immediate.Injections.Shared;

public interface IMarker;
public interface IOne<T>;
public interface ITwo<T1, T2>;

[RegisterScoped(RegistrationStrategy = RegistrationStrategy.SelfAndImplementedInterfaces)]
public sealed class Service<T1, T2> : IMarker, IOne<T1>, ITwo<T1, T2>;

Only two registrations are emitted:

Generated output
ServiceDescriptor.Scoped(typeof(Service<,>), typeof(Service<,>));
ServiceDescriptor.Scoped(typeof(ITwo<,>), typeof(Service<,>));

IMarker is skipped because it is not generic. IOne<T1> is skipped because its arity does not match the class’s. The same rule drops an interface closed over a concrete type — a Service<T> : IOther<int> does not get an IOther<> registration, because int is not one of the class’s type parameters.

If you need those, add explicit attributes for them alongside the strategy.

What is not supported

MSDI cannot build an open-generic service from a factory delegate, so neither Factory nor UseProxyFactory works on a generic target class.

NotSupported.cs
// INJ0012 — Factory with an open-generic ServiceType
[RegisterScoped(ServiceType = typeof(IRepository<>), Factory = nameof(Create))]

// INJ0008 — UseProxyFactory with an open-generic ServiceType
[RegisterScoped(ServiceType = typeof(IRepository<>), UseProxyFactory = true)]

Closed constructions are unaffected: [RegisterScoped<IRepository<Todo>>] on Repository<T> may use a factory, because the resulting descriptor is not open.

Where to go next