Использование сервисов Automapper, построенных Autofac с помощью WebApi

я использую WebAPI + Autofac + Automapper, с репозиторием для доступа к данным. Мне нужно сопоставить модель с моими доменными сущностями, в частности, мне нужно преобразовать значение идентификатора в фактическую сущность. Ничего страшного, правда? Я сделал это в MVC без проблем. Я упрощу то, что делаю, чтобы раскрыть суть.

public class EntityConverter<T> : ITypeConverter<int, T>
        where T : Entity
{
   public EntityConverter(IRepository<T> repository)
   {
       _repository = repository;
   }

   private readonly IRepository<T> _repository;

   public T Convert(ResolutionContext context)
   {
       _repository.Get((int) context.SourceValue);
   }
}

репозитории зарегистрированы в Autofac и управляются как InstancePerApiRequest из-за сессии/управление транзакциями. Итак, мне нужно зарегистрировать мой конвертер в том же объеме:

 builder.RegisterGeneric(typeof(EntityConverter<>))
        .AsSelf()
        .InstancePerApiRequest();

конфигурация Automapper выглядит примерно так:

 var container = builder.Build(); // build the Autofac container and do what you will

 Mapper.Initialize(cfg => {
      cfg.ConstructServicesUsing(container.Resolve); // nope nope nope
      // configure mappings
      cfg.CreateMap<int, TestEntity>().ConvertUsing<EntityConverter<TestEntity>>()
});
Mapper.AssertConfigurationIsValid();

так вот, что хреново. Я понимаю Automapper требует ConstructServicesUsing парень, который будет установлен, прежде чем вы создадите свою конфигурацию. Если вы установите его позже, он не будет использоваться. Приведенный выше пример не будет работать, потому что container - это корневой. Если я попытаюсь решить EntityConverter<TestEntity>, Autofac будет жаловаться, что запрошенный тип зарегистрирован в другой области, и вы в да. Имеет смысл, я хочу область, созданную WebApi.

позвольте мне сделать паузу на секунду и осветить один факт об инъекции зависимостей WebApi (я действительно не думаю, что это Autofac-specific). WebApi создает IDependencyScope для запроса, и прячет его в HttpRequestMessage.Properties. Я не могу получить его обратно, если у меня нет доступа к тому же HttpRequestMessage экземпляра. Мой AsInstancePerApiRequest scoping on IRepository и мой конвертер, таким образом, полагается на это IDependencyScope.

Итак, это действительно мясо и картофель проблема, и я действительно разочарован этим отличием от MVC. Ты не можешь этого сделать!--18-->

 cfg.ConstructServicesUsing(GlobalConfiguration.Configuration.DependencyResolver.GetService);

это эквивалентно использованию container.Resolve. Я не могу использовать

 GlobalConfiguration.Configuration.DependencyResolver.BeginScope().GetService

потому что A), который создает новую область рядом с той, которую я действительно хочу B), на самом деле не позволяет мне очистить новую область, которую я создал. Использование Service Locator-это новый способ иметь ту же проблему; я не могу добраться до области, которую использует WebApi. Если мой конвертер и его зависимости были одним экземпляром или экземпляром на зависимость, это не было бы проблемой, но это не так, так и есть, и изменение этого создало бы намного больше проблем для меня.

теперь я могу создать конфигурацию AutoMapper с Autofac и зарегистрировать ее как один экземпляр. Я даже могу создать per-request IMappingEngine экземпляров. Но мне это не поможет, если конструктор службы всегда использует тот единственный делегат, который вы регистрируете в начале, который не имеет доступа к текущей области. Если бы я мог изменить этот делегат для каждого сопоставления двигатель экземпляр, я мог бы быть в бизнесе. Но я не могу.--18-->

так что can я делаю?

3 ответов


другой вариант, на этот раз встроенный, - использовать параметры для каждой карты:

Mapper.Map<Source, Destination>(dest, opt => opt.ConstructServicesUsing(type => Request.GetDependencyScope().GetService(typeof(YourServiceTypeToConstruct))));

Не беспокойтесь о настройке глобальной конфигурации IoC в конфигурации сопоставления.

другой вариант-использовать инструмент IoC для настройки создания экземпляра MappingEngine:

public MappingEngine(
    IConfigurationProvider configurationProvider,
    IDictionary<TypePair, IObjectMapper> objectMapperCache,
    Func<Type, object> serviceCtor)

первый-это просто картограф.Конфигурация, второй, вероятно, должен быть одноэлементным, а третий вы можете заполнить разрешением текущего вложенного контейнера. Этот это упростило бы необходимость каждый раз вызывать перегрузку карты.


обновление: Automapper был обновлен для поддержки этой функции. См. ответ @Jimmy Bogard

это решение может быть не очень приятным, но оно работает. Решение относится к WebAPI 2, я не уверен в предыдущих версиях.

в WebAPI 2 Вы можете получить текущий IDependencyScope из текущего HttpRequestMessage via GetDependencyScope() метод расширения. Ток HttpRequestMessage хранящийся в Items свойства текущего HttpContext. Зная, что ваша фабрика может выглядеть например:

Mapper.Initialize(cfg =>
{
    cfg.ConstructServicesUsing(serviceTypeToConstruct =>
    {
        var httpRequestMessage = HttpContext.Current.Items["MS_HttpRequestMessage"] as HttpRequestMessage;
        var currentDependencyScope = httpRequestMessage.GetDependencyScope();
        return currentDependencyScope.GetService(serviceTypeToConstruct);
    });
    // configure mappings
    // ...
});

это может быть или не подходит для вас.. но вот:

мы недавно сделали это.. для связующих моделей в MVC. Наши привязки моделей (по запросам GET) теперь используют службы, управляемые Ninject, для построения моделей.

в основном, мы введем завод (используя расширение заводов по Ninject.. возможно, есть аналогичный для Autofac) в класс" AutomapperBootstrapper", который, в свою очередь, создает отображение Automapper Profileи добавляет их в Automapper. Несколько как это:

Mapper.Initialize(cfg =>
{
    cfg.AddProfile(_factory.CreateServiceViewModelMappingProfile());
    // etc..
});

отображений использовать MapFrom(), которым оценивается каждый раз, когда происходит сопоставление. Что-то вроде этого:--8-->

Mapper.CreateMap<Service, ServiceViewModel>()
            .ForMember(x => x.Regions,
                opt =>
                    opt.MapFrom(x => getRegions()))

private IEnumerable<Region> getRegions() {
    return _factory.CreateTheService().GetRegions();
}

каждый раз, когда запускается связующее устройство модели, Ninject все еще подключает все зависимости для запроса, и все это фильтруется.

(для тех, кто заинтересован, Эта настройка в основном позволяет нам сделать это:/Area/Controller/Action/12, и наш метод действия контроллера это:

[HttpGet]
public ActionResult Action(ServiceViewModel model) {
    // ...
}

).