с простым инжектором
Если вы используете Простой инжектор для работы в режиме DI, контейнер может помочь с этим. (Если вы не используете Simple Injector, см. «С другими структурами DI» ниже)
Функциональность описана в документации по Simple Injector, в разделе Расширенные сценарии: микшированиенаборы открытых универсальных и неуниверсальных компонентов .
Вам нужно будет немного изменить интерфейс и реализации вашего сервиса.
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();
}
Сервисы теперь являются универсальнымис ограничением типа, описывающим наименее специфичный тип сущности , который они могут обрабатывать.В качестве бонуса DoSomething
теперь придерживается принципа подстановки Лискова.Поскольку реализации служб предоставляют ограничения типов, интерфейсу IEntityService
он больше не нужен.
Зарегистрируйте все службы как одну коллекцию открытых обобщенных типов.Simple Injector понимает ограничения общего типа.При разрешении контейнер, по существу, фильтрует коллекцию до тех служб, для которых удовлетворено ограничение типа.
Вот рабочий пример, представленный в виде теста xUnit .
[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())
);
}
Как и в вашем примере, вы можете добавить ChildAEntityService<T> : IEntityService<T> where T : ChildAEntity
и UnusualEntityService<T> : IEntityService<T> where T : IUnusualEntity
, и все получится ...
[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())
);
}
Как я упоминал ранее, этот пример относится только к Simple Injector,Не все контейнеры могут так элегантно обрабатывать общие регистрации.Например, аналогичная регистрация завершается неудачно с контейнером DI Microsoft :
[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>))
);
}
с другими структурами DI
Я разработал альтернативное решение, которое будет работать с любым DIcontainer.
На этот раз мы удаляем определение универсального типа из интерфейса.Вместо этого метод CanHandle()
сообщит вызывающей стороне, может ли экземпляр обрабатывать данную сущность.
interface IEntityService
{
// Indicates whether or not the instance is able to handle the entity.
bool CanHandle(object entity);
void DoSomething(object entity);
}
Абстрактный базовый класс может обрабатывать большую часть шаблона проверки типов / приведения типов:
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);
}
Это означает, что фактические реализации службы могут быть очень простыми, например:
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();
}
Чтобы вытащить их из контейнера DI, вам понадобится дружественная фабрика:
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));
}
И наконец, чтобы доказать, что все это работает:
[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())
);
}
Из-за задействованного приведения типов я не думаю, что это так элегантно, как реализация Simple Injector.Это все еще довольно хорошо, и шаблон имеет некоторый прецедент.Это очень похоже на реализацию MVC Core Авторизация на основе политик ;в частности AuthorizationHandler
.