【问题标题】:Decorating ASP.NET Web API IHttpController装饰 ASP.NET Web API IHttpController
【发布时间】:2023-03-28 12:25:01
【问题描述】:

我正在尝试用装饰器包装 Web API 控制器(IHttpController 实现),但是当我这样做时,Web API 会引发异常,因为它以某种方式期待实际的实现。

将装饰器应用于控制器是我成功应用于 MVC 控制器的一个技巧,我显然喜欢在 Web API 中做同样的事情。

我创建了一个自定义的IHttpControllerActivator,它允许解析修饰的IHttpController 实现。这是一个剥离的实现:

public class CrossCuttingConcernHttpControllerActivator : IHttpControllerActivator {
    private readonly Container container;
    public CrossCuttingConcernHttpControllerActivator(Container container) {
        this.container = container;
    }

    public IHttpController Create(HttpRequestMessage request, 
        HttpControllerDescriptor controllerDescriptor, Type controllerType)
    {
        var controller = (IHttpController)this.container.GetInstance(controllerType);

        // Wrap the instance in one or multiple decorators. Note that in reality, the 
        // decorator is applied by the container, but that doesn't really matter here.
        return new MyHttpControllerDecorator(controller);
    }
}

我的装饰器看起来像这样:

public class MyHttpControllerDecorator : IHttpController {
    private readonly IHttpController decoratee;
    public MyHttpControllerDecorator(IHttpController decoratee) {
        this.decoratee = decoratee;
    }

    public Task<HttpResponseMessage> ExecuteAsync(
        HttpControllerContext controllerContext,
        CancellationToken cancellationToken)
    {
        // this decorator does not add any logic. Just the minimal amount of code to
        // reproduce the issue.
        return this.decoratee.ExecuteAsync(controllerContext, cancellationToken);
    }
}

但是,当我运行我的应用程序并请求 ValuesController 时,Web API 会向我抛出以下 InvalidCastException

无法转换类型为“WebApiTest.MyHttpControllerDecorator”的对象 输入“WebApiTest.Controllers.ValuesController”。

堆栈跟踪:

at lambda_method(Closure , Object , Object[] )
at System.Web.Http.Controllers.ReflectedHttpActionDescriptor.ActionExecutor.<>c__DisplayClass13.<GetExecutor>b__c(Object instance, Object[] methodParameters)
at System.Web.Http.Controllers.ReflectedHttpActionDescriptor.ActionExecutor.Execute(Object instance, Object[] arguments)
at System.Web.Http.Controllers.ReflectedHttpActionDescriptor.<>c__DisplayClass5.<ExecuteAsync>b__4()
at System.Threading.Tasks.TaskHelpers.RunSynchronously[TResult](Func`1 func, CancellationToken cancellationToken)

就好像 Web API 为我们提供了 IHttpController 抽象但跳过它并仍然依赖于实现本身。这当然会严重违反依赖倒置原则,并使抽象完全无用。所以我可能做错了什么。

我做错了什么?如何愉快地装饰我的 API 控制器?

【问题讨论】:

    标签: c# dependency-injection asp.net-web-api decorator


    【解决方案1】:

    您可以提供IHttpControllerSelector 的自定义实现来更改为特定控制器实例化的类型。 (请注意,我没有对此进行过彻底的测试)

    将装饰器更新为通用

    public class MyHttpControllerDecorator<T> : MyHttpController
        where T : MyHttpController
    {
        public readonly T decoratee;
    
        public MyHttpControllerDecorator(T decoratee)
        {
            this.decoratee = decoratee;
        }
    
        public Task<HttpResponseMessage> ExecuteAsync(
            HttpControllerContext controllerContext,
            CancellationToken cancellationToken)
        {
            return this.decoratee.ExecuteAsync(controllerContext, cancellationToken);
        }
    
        [ActionName("Default")]
        public DtoModel Get(int id)
        {
            return this.decoratee.Get(id);
        }
    }
    

    定义IHttpControllerSelector的自定义实现

    public class CustomControllerSelector : DefaultHttpControllerSelector
    {
        private readonly HttpConfiguration configuration;
        public CustomControllerSelector(HttpConfiguration configuration)
            : base(configuration)
        {
            this.configuration = configuration;
        }
    
        public override HttpControllerDescriptor SelectController(
            HttpRequestMessage request)
        {
            var controllerTypes = this.configuration.Services
                .GetHttpControllerTypeResolver().GetControllerTypes(
                    this.configuration.Services.GetAssembliesResolver());
    
            var matchedTypes = controllerTypes.Where(i => 
                 typeof(IHttpController).IsAssignableFrom(i)).ToList();
    
            var controllerName = base.GetControllerName(request);
            var matchedController = matchedTypes.FirstOrDefault(i => 
                    i.Name.ToLower() == controllerName.ToLower() + "controller");
    
            if (matchedController.Namespace == "WebApiTest.Controllers")
            {
                Type decoratorType = typeof(MyHttpControllerDecorator<>);
                Type decoratedType = decoratorType.MakeGenericType(matchedController);
                return new HttpControllerDescriptor(this.configuration, controllerName, decoratedType);
            }
            else
            {
                return new HttpControllerDescriptor(this.configuration, controllerName, matchedController);
            }
        }
    }
    

    注册控制器时,添加控制器类型的修饰版本的注册

    var container = new SimpleInjector.Container();
    
    var services = GlobalConfiguration.Configuration.Services;
    
    var controllerTypes = services.GetHttpControllerTypeResolver()
        .GetControllerTypes(services.GetAssembliesResolver());
    
    Type decoratorType = typeof(MyHttpControllerDecorator<>);
    foreach (var controllerType in controllerTypes)
    {
        if (controllerType.Namespace == "WebApiTest.Controllers")
        {
            Type decoratedType = decoratorType.MakeGenericType(controllerType);
            container.Register(decoratedType, () => 
                DecoratorBuilder(container.GetInstance(controllerType) as dynamic));
        }
        else
        {
            container.Register(controllerType);
        }
    }
    

    注册IHttpControllerSelector的实现

    GlobalConfiguration.Configuration.Services.Replace(
        typeof(IHttpControllerSelector),
        new CustomControllerSelector(GlobalConfiguration.Configuration));
    

    这是创建装饰实例的方法

    private MyHttpControllerDecorator<T> DecoratorBuilder<T>(T instance)
        where T : IHttpController
    {
        return new MyHttpControllerDecorator<T>(instance);
    }
    

    【讨论】:

      【解决方案2】:

      您可以通过实现IHttpActionInvoker 并在IHttpController 抽象不再相关时将装饰器“转换”为装饰实例来解决此问题。

      这很容易通过从ApiControllerActionInvoker继承来完成。

      (我已经对示例进行了硬编码,并且希望任何现实世界的实现都更加灵活。)

      public class ContainerActionInvoker : ApiControllerActionInvoker
      {
          private readonly Container container;
      
          public ContainerActionInvoker(Container container)
          {
              this.container = container;
          }
      
          public override Task<HttpResponseMessage> InvokeActionAsync(
              HttpActionContext actionContext, 
              CancellationToken cancellationToken)
          {
              if (actionContext.ControllerContext.Controller is MyHttpControllerDecorator)
              {
                  MyHttpControllerDecorator decorator =
                      (MyHttpControllerDecorator)actionContext.ControllerContext.Controller;
                  // decoratee changed to public for the example
                  actionContext.ControllerContext.Controller = decorator.decoratee;
              }
      
              var result = base.InvokeActionAsync(actionContext, cancellationToken);
              return result;
          }
      }
      

      这是在Global.asax.cs注册的

      GlobalConfiguration.Configuration.Services.Replace(
          typeof(IHttpControllerActivator),
          new CrossCuttingConcernHttpControllerActivator(container));
      
      GlobalConfiguration.Configuration.Services.Replace(
          typeof(IHttpActionInvoker),
          new ContainerActionInvoker(container)); 
      

      您是否真的想要这样做是另一回事 - 谁知道更改 actionContext 的后果?

      【讨论】:

      • 在您的示例中,您从装饰器中提取了被装饰者。那不会删除装饰器添加的行为吗?这种行为还会被调用吗?
      • @Steven 是的,我确信它会绕过修饰调用 - 一个完整的解决方案需要处理调用自定义 IHttpActionInvoker 中的方法并绕过默认实现。
      【解决方案3】:

      我想说,在 ASP.NET Web API 中实现这种行为的自然、设计方法是使用 Custom Message Handlers / Delegation Handlers

      例如,我确实有这个DelegationHandler

      public class AuthenticationDelegationHandler : DelegatingHandler
      {
          protected override System.Threading.Tasks.Task<HttpResponseMessage> 
              SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
          {
              // I. do some stuff to create Custom Principal
              // e.g.
              var principal = CreatePrincipal();
              ...
      
              // II. return execution to the framework            
              return base.SendAsync(request, cancellationToken).ContinueWith(t =>
              {
                  HttpResponseMessage resp = t.Result;
                  // III. do some stuff once finished
                  // e.g.:
                  // SetHeaders(resp, principal);
      
                  return resp;
              });
          }
      

      这就是如何将它注入到结构中:

      public static class WebApiConfig
      {
          public static void Register(HttpConfiguration config)
          {
              config.MessageHandlers.Add(new AuthenticationDelegationHandler());
      

      【讨论】:

      • 感谢您的回复。我仍然有点恼火我不能装饰IHttpController,但你的解决方案可能会满足我的需求:-)。这种方法的唯一问题是委托处理程序是单例的,因此不能依赖任何生命周期较短的服务。有没有办法让 Web API 在每个请求上解析它们?
      • 好吧,因为我们正在使用外部服务(也可以是单例),但是将值设置到上下文中(请求,httpcontext 用户)......我们没有每个“请求”都需要实例。我会说 IHttpController 装饰器不能工作,因为最后,我们没有使用这个接口。该框架调查(反射)“真实”控制器,并根据其方法/参数/属性决定调用实例上的哪个方法。所以装饰器模式在这里不起作用。无论如何......委托处理程序应该解决问题
      • DelegationHandler 可以依赖(通过构造函数注入)可能(或需要)以每个请求(或更短的)生活方式定义的其他服务。发生这种情况时,DelegationHandler 的生命周期应该与其依赖项的最短生命周期一样短(或更短)。
      • 有趣的是,Web API 与 MVC 非常相似,但使用 MVC 可以毫无问题地修饰 IControllers,因为它是 Controller 基类,它完成了所有反射以获得到正确的行动。基础设施只调用IController 接口。 Web API 为我们提供了 IHttpController 抽象,但仍然取决于实际实现,这不是很奇怪吗?
      • 我明白你的意思。我只是想告诉你如何..根据我的经验。委托处理程序对于 CORS、Auth 等任何基本的东西都工作得很好......对于其余的我们使用 AOP......但正如我所说......只是想向您展示如何做到这一点的另一种方法;)
      猜你喜欢
      • 1970-01-01
      • 2023-03-10
      • 2010-10-15
      • 2017-06-25
      • 1970-01-01
      • 2019-02-06
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多