Decorating ASP.NET Web API IHttpController

感情迁移 提交于 2019-12-09 16:43:16

问题


I'm trying to wrap Web API controllers (IHttpController implementations) with decorators, but when I do this, Web API throws an exception, because somehow it is expecting the actual implementation.

Applying decorators to controllers is a trick I successfully apply to MVC controllers and I obviously like to do the same in Web API.

I created a custom IHttpControllerActivator that allows resolving decorated IHttpController implementations. Here's a stripped implementation:

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

My decorator looks like this:

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

However, when I run my application and request the ValuesController, Web API throws me the following InvalidCastException:

Unable to cast object of type 'WebApiTest.MyHttpControllerDecorator' to type 'WebApiTest.Controllers.ValuesController'.

Stacktrace:

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)

It's just as if Web API supplies us with the IHttpController abstraction but skips it and still depends on the implementation itself. This would of course be a severe violation of the Dependency Inversion principle and make the abstraction utterly useless. So I'm probably doing something wrong instead.

What I'm I doing wrong? How can I happily decorate my API Controllers?


回答1:


I would say, that the natural, designed way how to achieve this behaviour in ASP.NET Web API is with the Custom Message Handlers / Delegation Handlers

For example I do have this DelegationHandler in place

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

And this is how to inject that into the structure:

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



回答2:


You can work around this by implementing IHttpActionInvoker and "converting" the decorator into the decorated instance at the point that the IHttpController abstraction is no longer relevant.

This is easily done by inheriting from ApiControllerActionInvoker.

(I've hard coded the example and would expect any real world implementation to be more flexible.)

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

This was registered in Global.asax.cs

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

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

Whether you'd actually want to do this is another matter - who knows the ramifications of altering actionContext?




回答3:


You can provide a custom implementation of IHttpControllerSelector to alter the type instantiated for a particular controller. (Please note I have not tested this to exhaustion)

Update the decorator to be generic

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

Define the custom implementation of 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);
        }
    }
}

When registering the controllers, add in the registration of a decorated version of the controller type

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

Register the implementation of IHttpControllerSelector

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

This is the method for creating the Decorated instance

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


来源:https://stackoverflow.com/questions/20444059/decorating-asp-net-web-api-ihttpcontroller

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!