With Simple Injector
If you happen to be using Simple Injector for DI duties, the container can help with this. (If you're not using Simple Injector, see "With Other DI Frameworks," below)
The functionality is described in the Simple Injector docs, under Advanced Scenarios: Mixing collections of open-generic and non-generic components.
You'll need to make a slight adjustment to your service interface and implementations.
interface IEntityService<T>
{
void DoSomething(T entity);
}
class BaseEntityService<T> : IEntityService<T> where T : BaseEntity
{
public void DoSomething(T entity) => throw new NotImplementedException();
}
class ChildBEntityService<T> : IEntityService<T> where T : ChildBEntity
{
public void DoSomething(T entity) => throw new NotImplementedException();
}
The services are now generic, with a type constraint describing the least specific entity type they're able to handle. As a bonus, DoSomething
now adheres to the Liskov Substitution Principle. Since the service implementations provide type constraints, the IEntityService
interface no longer needs one.
Register all of the services as a single collection of open generics. Simple Injector understands the generic type constraints. When resolving, the container will, essentially, filter the collection down to only those services for which the type constraint is satisfied.
Here's a working example, presented as an xUnit test.
[Theory]
[InlineData(typeof(GrandChildAEntity), new[] { typeof(GrandChildAEntityService<GrandChildAEntity>), typeof(BaseEntityService<GrandChildAEntity>) })]
[InlineData(typeof(BaseEntity), new[] { typeof(BaseEntityService<BaseEntity>) })]
[InlineData(typeof(ChildBEntity), new[] { typeof(ChildBEntityService<ChildBEntity>), typeof(BaseEntityService<ChildBEntity>) })]
[InlineData(typeof(ChildAEntity), new[] { typeof(BaseEntityService<ChildAEntity>) })]
public void Test1(Type entityType, Type[] expectedServiceTypes)
{
var container = new Container();
// Services will be resolved in the order they were registered
container.Collection.Register(typeof(IEntityService<>), new[] {
typeof(ChildBEntityService<>),
typeof(GrandChildAEntityService<>),
typeof(BaseEntityService<>),
});
container.Verify();
var serviceType = typeof(IEntityService<>).MakeGenericType(entityType);
Assert.Equal(
expectedServiceTypes,
container.GetAllInstances(serviceType).Select(s => s.GetType())
);
}
Similar to your example, you can add ChildAEntityService<T> : IEntityService<T> where T : ChildAEntity
and UnusualEntityService<T> : IEntityService<T> where T : IUnusualEntity
and everything works out...
[Theory]
[InlineData(typeof(GrandChildAEntity), new[] { typeof(UnusualEntityService<GrandChildAEntity>), typeof(ChildAEntityService<GrandChildAEntity>), typeof(GrandChildAEntityService<GrandChildAEntity>), typeof(BaseEntityService<GrandChildAEntity>) })]
[InlineData(typeof(BaseEntity), new[] { typeof(BaseEntityService<BaseEntity>) })]
[InlineData(typeof(ChildBEntity), new[] { typeof(UnusualEntityService<ChildBEntity>), typeof(ChildBEntityService<ChildBEntity>), typeof(BaseEntityService<ChildBEntity>) })]
[InlineData(typeof(ChildAEntity), new[] { typeof(ChildAEntityService<ChildAEntity>), typeof(BaseEntityService<ChildAEntity>) })]
public void Test2(Type entityType, Type[] expectedServiceTypes)
{
var container = new Container();
// Services will be resolved in the order they were registered
container.Collection.Register(typeof(IEntityService<>), new[] {
typeof(UnusualEntityService<>),
typeof(ChildAEntityService<>),
typeof(ChildBEntityService<>),
typeof(GrandChildAEntityService<>),
typeof(BaseEntityService<>),
});
container.Verify();
var serviceType = typeof(IEntityService<>).MakeGenericType(entityType);
Assert.Equal(
expectedServiceTypes,
container.GetAllInstances(serviceType).Select(s => s.GetType())
);
}
As I mentioned before, this example is specific to Simple Injector. Not all containers are able to handle generic registrations so elegantly. For example, a similar registration fails with Microsoft's DI container:
[Fact]
public void Test3()
{
var services = new ServiceCollection()
.AddTransient(typeof(IEntityService<>), typeof(BaseEntityService<>))
.AddTransient(typeof(IEntityService<>), typeof(GrandChildAEntityService<>))
.AddTransient(typeof(IEntityService<>), typeof(ChildBEntityService<>))
.BuildServiceProvider();
// Exception message: System.ArgumentException : GenericArguments[0], 'GrandChildBEntity', on 'GrandChildAEntityService`1[T]' violates the constraint of type 'T'.
Assert.Throws<ArgumentException>(
() => services.GetServices(typeof(IEntityService<ChildBEntity>))
);
}
With Other DI Frameworks
I've devised an alternate solution that should work with any DI container.
This time, we remove the generic type definition from the interface. Instead, the CanHandle()
method will let the caller know whether an instance can handle a given entity.
interface IEntityService
{
// Indicates whether or not the instance is able to handle the entity.
bool CanHandle(object entity);
void DoSomething(object entity);
}
An abstract base class can handle most of the type-checking/casting boilerplate:
abstract class GenericEntityService<T> : IEntityService
{
// Indicates that the service can handle an entity of typeof(T),
// or of a type that inherits from typeof(T).
public bool CanHandle(object entity)
=> entity != null && typeof(T).IsAssignableFrom(entity.GetType());
public void DoSomething(object entity)
{
// This could also throw an ArgumentException, although that
// would violate the Liskov Substitution Principle
if (!CanHandle(entity)) return;
DoSomethingImpl((T)entity);
}
// This is the method that will do the actual processing
protected abstract void DoSomethingImpl(T entity);
}
Which means the actual service implementations can be very simple, like:
class BaseEntityService : GenericEntityService<BaseEntity>
{
protected override void DoSomethingImpl(BaseEntity entity) => throw new NotImplementedException();
}
class ChildBEntityService : GenericEntityService<ChildBEntity>
{
protected override void DoSomethingImpl(ChildBEntity entity) => throw new NotImplementedException();
}
To get them out of the DI container, you'll want a friendly factory:
class EntityServiceFactory
{
readonly IServiceProvider serviceProvider;
public EntityServiceFactory(IServiceProvider serviceProvider)
=> this.serviceProvider = serviceProvider;
public IEnumerable<IEntityService> GetServices(BaseEntity entity)
=> serviceProvider
.GetServices<IEntityService>()
.Where(s => s.CanHandle(entity));
}
And finally, to prove it all works:
[Theory]
[InlineData(typeof(GrandChildAEntity), new[] { typeof(UnusualEntityService), typeof(ChildAEntityService), typeof(GrandChildAEntityService), typeof(BaseEntityService) })]
[InlineData(typeof(BaseEntity), new[] { typeof(BaseEntityService) })]
[InlineData(typeof(ChildBEntity), new[] { typeof(UnusualEntityService), typeof(ChildBEntityService), typeof(BaseEntityService) })]
[InlineData(typeof(ChildAEntity), new[] { typeof(ChildAEntityService), typeof(BaseEntityService) })]
public void Test4(Type entityType, Type[] expectedServiceTypes)
{
// Services appear to be resolved in reverse order of registration, but
// I'm not sure if this behavior is guaranteed.
var serviceProvider = new ServiceCollection()
.AddTransient<IEntityService, UnusualEntityService>()
.AddTransient<IEntityService, ChildAEntityService>()
.AddTransient<IEntityService, ChildBEntityService>()
.AddTransient<IEntityService, GrandChildAEntityService>()
.AddTransient<IEntityService, BaseEntityService>()
.AddTransient<EntityServiceFactory>() // this should have an interface, but I omitted it to keep the example concise
.BuildServiceProvider();
// Don't get hung up on this line--it's part of the test, not the solution.
BaseEntity entity = (dynamic)Activator.CreateInstance(entityType);
var entityServices = serviceProvider
.GetService<EntityServiceFactory>()
.GetServices(entity);
Assert.Equal(
expectedServiceTypes,
entityServices.Select(s => s.GetType())
);
}
Because of the casting involved, I don't think this is as elegant as the Simple Injector implementation. It's still pretty good, though, and the pattern has some precedent. It's very similar to the implementation of MVC Core's Policy-Based Authorization; specifically AuthorizationHandler
.