8

I'm using WebAPI + Autofac + Automapper, with a repository for data access. I need to map a model to my domain entities, specifically, I need to convert an identity value to the actual entity. No big deal, right? I've done this in MVC with no problem. I will simplify what I am doing to expose the essentials.

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);
   }
}

Repositories are registered with Autofac, and are managed as InstancePerApiRequest because of session/transaction management. So, I need to register my converter in that same scope:

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

The Automapper config looks something like:

 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();

So here's the part that sucks. I am to understand Automapper requires the ConstructServicesUsing guy to be set before you build your config. If you set it later, it won't be used. The example above won't work because container is the root scope. If I try and resolve EntityConverter<TestEntity>, Autofac will complain that the requested type is registered for a different scope, and the one you're in ain't it. Makes sense, I want the scope created by WebApi.

Let me pause a sec and cover one fact about WebApi dependency injection (I don't really think this is Autofac-specific). WebApi creates an IDependencyScope for the request, and stashes it in the HttpRequestMessage.Properties. I can't get it back again unless I have access to that same HttpRequestMessage instance. My AsInstancePerApiRequest scoping on IRepository and my converter thus rely on that IDependencyScope.

So, that's really the meat and potatoes of the problem, and I really frustrated with this difference from MVC. You can't do

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

That's equivalent to using container.Resolve. I can't use

 GlobalConfiguration.Configuration.DependencyResolver.BeginScope().GetService

because A) that creates a new scope next to the one I actually want B) doesn't really let me clean up the new scope I created. Using Service Locator is a new way to have the same problem; I can't get to the scope WebApi is using. If my converter and its dependencies were single instance or instance per dependency, it wouldn't be a problem, but they aren't, so it is, and changing that would create lots more problems for me.

Now, I can create AutoMapper config with Autofac and register it as a single instance. I can even create per-request IMappingEngine instances. But that doesn't do me any good if the service constructor always uses that single delegate you register at the beginning, which has no access to the current scope. If I could change that delegate per each mapping engine instance, I might be in business. But I can't.

So what can I do?

4

3 回答 3

7

另一个选项,这次是内置的,是使用每个地图选项:

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)

第一个只是 Mapper.Configuration,第二个应该是单例,第三个可以填入当前嵌套容器的分辨率。这将简化每次都必须调用 Map 重载。

于 2015-01-13T14:25:55.367 回答
6

更新: Automapper 已更新以支持该功能。见@Jimmy Bogard 的回答

这个解决方案可能不是很好,但它确实有效。该解决方案与 WebAPI 2 有关,我不确定以前的版本。

在 WebAPI 2 中,您可以通过扩展方法IDependencyScope从当前获取当前。Current存储在current 的属性中。知道您的工厂可能如下所示:HttpRequestMessageGetDependencyScope()HttpRequestMessageItemsHttpContext

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
    // ...
});
于 2014-07-01T11:43:19.200 回答
1

这可能适合您,也可能不适合您。但是这里有:

我们最近为 MVC 中的模型绑定器做了这个。我们的模型绑定器(在 GET 请求上)现在使用 Ninject 管理的服务来构建模型。

基本上,我们将一个工厂(使用 Ninject 的工厂扩展......也许 Autofac 有一个类似的)注入到“AutomapperBootstrapper”类中,该类反过来创建 Automapper 映射Profile并将它们添加到 Automapper。有点像这样:

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

映射Profile本身使用MapFrom(),每次映射发生时都会对其进行评估。像这样的东西:

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) {
    // ...
}

)。

于 2013-09-11T04:23:28.650 回答