如何根据当前请求操作方法属性将不同的具体实现注入控制器?

时间:2016-02-10 17:00:30

标签: asp.net-mvc dependency-injection autofac

我目前有一个自定义PrototypingControllerFactory,它会针对当前请求调用的操作方法查找自定义[Prototype]属性,并且根据属性是否存在将注入不同的实现接口ISomeService。 (在这种情况下,ISomeService或多或少地抽象了消息传递服务,因此模拟实现允许在实际实现尚未准备好处理特定消息时返回" canned"结果。)

例如,如果我有一个类似的控制器类:

public class MyController : Controller
{
    private readonly ISomeService _someService;

    public MyController(ISomeService someService /*, .... other dependencies */
    { _someService = someService; //... etc }


    public ActionResult Action1()
    {
        //...
        _someService.SomeMethod();
        //...
    }

    [Prototype]
    public ActionResult Action2()
    {
        //...
        _someService.SomeMethod();
        //...
    }
}

然后,当从http请求调用Action1时,_someService应该使用ISomeService的生产实现,但是当Action2被调用时,_someService应该有ISomeService的模拟版本。

从严格的设计角度来看,我意识到这可能表明在特定控制器中有太多动作(否则,例如,我可能只是将整个控制器标记为[Prototype]),但是由于项目惯性,我宁愿不尝试强制改变操作在控制器中的位置。

目前autofac注册具有以下内容:

builder.RegisterControllers(Assembly.GetExecutingAssembly());

if (ConfigurationManager.AppSettings["AllowPrototyping"] == "true")
{
    builder.RegisterType<PrototypingControllerFactory>().As<IControllerFactory>().InstancePerRequest();
}

然而,这意味着控制器工厂必须做一些奇特的工作来找出构造函数参数,并从DI容器中获取实例。最近,我发现了自定义控制器工厂和&#34; real&#34;之间的一些细微差别。控制器工厂是不可取的。

我想取消自定义控制器工厂,而是让autofac完全处理控制器的分辨率。

如何告诉autofac解析接口的不同实现,具体取决于当前正在执行的操作是否使用我的自定义[Prototype]属性进行修饰?

1 个答案:

答案 0 :(得分:1)

第一个选项取决于可靠地直接从路径数据确定控制器动作方法,这不一定是直截了当的。部分问题是,即使在授权过滤器运行之前,控制器的创建(以及因此注入依赖关系)也会在过程的早期发生。

如果一个神奇方法的某些可靠的实现说实际上存在ActionDescriptor GetActionDescriptor(RoutData routeData),那么我可以执行以下操作:

builder.Register<ISomeService>(c =>
{
    var httpRequest = c.Resolve<HttpRequestBase>();
    var actionDescriptor = GetActionDescriptor(httpRequest.RequestContext.RouteData);

    if (actionDescriptor.GetAttributes<PrototypeAttribute>().Any())
    {
        return new PrototypeSomeService();
    }
    return new RealSomeService();
}).InstancePerRequest();

然而,我最接近在控制器被实例化的位置获得ActionDescriptor是最重要的DefaultControllerFactory,这正是我想要摆脱的。

从默认控制器工厂,您可以使用受保护的DefaultControllerFactory.GetControllerType(),然后您可以从中创建ReflectedControllerDescriptor。但是你仍然需要进行一些调整以从controllerDescriptor.GetCanonicalActions()获取正确的动作描述符。 (事实上​​,我怀疑这种“混乱”导致原始自定义控制器工厂的微妙差异)。当从其他http处理程序使用路由时,这可能会变得更加复杂(例如,想想Elmah或MiniProfiler)。

最后,我选择通过RouteData继承ActionDescriptor来避免尝试将[PrototypeAttribute]映射到ActionFilterAttribute,以便我可以轻松加入OnActionExecuting从那时起,我为当前的HttpContext添加了一个标识符,例如

public class PrototypeAttribute : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        filterContext.RequestContext.HttpContext.Items.Add("Prototype", "Prototype");

        base.OnActionExecuting(filterContext);
    }
}

然后我修改了我的PrototypeSomeService,以便它包含ISomeService的实现,如果上下文不包含原型键,则委托给它,例如。

public class PrototypeSomeService : ISomeService
{
     private readonly ISomeService _wrappedService;
     private readonly HttpRequestBase _httpRequest;

     public PrototypeSomeService(ISomeService wrappedService, HttpRequestBase httpRequest)
     {
         _wrappedService = wrappedService;
         _httpRequest = httpRequest
     }

     public object SomeMethod()
     {
         if(_httpRequest.RequestContext.HttpContext.Items.Contains("Prototype"))
             return _wrappedService.SomeMethod();

         //other prototype logic...
         return prototypeResult;
     }
}

将所有这些组合在一起的最后一块是使用autofac的装饰器功能:

 var someServiceRegistration = builder.RegisterType<SomeService>().InstancePerRequest();

 if (ConfigurationManager.AppSettings["AllowPrototyping"] == "true")
 {
     someServiceRegistration.Named<ISomeService>("Prototype");

     builder.RegisterDecorator<ISomeService>(
          (c, inner) => new PrototypeSomeService(inner, c.Resolve<HttpRequestBase>()),
          fromKey: "Prototype"
     );
 }
 else
 {
     someServiceRegistration.As<ISomeService>();
 }

一个小缺点是你必须确保使用httpcontext项的唯一键,否则会发生奇怪的事情,但是很容易避免这种情况。使用guid。

这种方法允许我保留大部分现有代码不变,只修改服务的原型实现和autofac注册。

您可以在以下位置阅读有关autofac装饰器的更多信息: