2

Our team maintains a self-hosted ASP.NET Web API. The project uses attribute routing and we have dozens of existing controllers. Lets say, the API is exposed via the main path /api/purpose1/... with all the existing controllers being placed as resources underneath.

Now I want to introduce a new parallel main path, e. g. /api/purpose2/. It should be possible to activate both main paths independently of each other via a boolean variable in a config file.

Since all the controllers are within one assembly, the attribute routing approach always finds and adds them to both purpose1 and purpose2. This contradicts the independency of purpose1 and purpose2. So I used attribute routing for purpose1 and convention-based routing for purpose2. That at least worked, but I'm not happy with the mixture of two different routing approaches.

So my question is: can I disable certain controller classes with attribute routing?

mu88
  • 4,156
  • 1
  • 23
  • 47
  • Please check [IControllerActivator](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllers.icontrolleractivator?view=aspnetcore-3.1) and [IControllerFactory](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllers.icontrollerfactory?view=aspnetcore-3.1) interfaces. Implementing these can give you full control over controller creation. – Peter Csala May 08 '20 at 06:42
  • Alternatively you could override the [OnActionExecuting](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.controller.onactionexecuting?view=aspnetcore-3.1) method of the Controller to examine the feature flag. – Peter Csala May 08 '20 at 06:50
  • @PeterCsala Thank you, I will take a look at this. Do I have to implement both interfaces? – mu88 May 08 '20 at 07:01
  • IControllerFactory should be enough. I'll provide an example. – Peter Csala May 08 '20 at 07:55
  • Which .net core or .net framework version are you using? – Peter Csala May 08 '20 at 08:10
  • Great! We're using .NET Framework 4.6.2 – mu88 May 08 '20 at 08:43
  • [Here](https://www.codeproject.com/Tips/732449/Understanding-and-Extending-Controller-Factory-i-3) you can find a quite neat implementation. Please bear in mind that `DefaultContollerFactory` is **internal** in .NET Core – Peter Csala May 08 '20 at 09:24

2 Answers2

2

OnActionExecuting example:

V1 controller

[Route("api/[controller]")]
[ApiController]
public class SampleV1Controller : VersioningAwareControllerBase
{
    [HttpGet]
    public IActionResult Get()
    {
        return new OkObjectResult("V1");
    }
}

V2 controller

[Route("api/[controller]")]
[ApiController]
public class SampleV2Controller : VersioningAwareControllerBase
{
    [HttpGet]
    public IActionResult Get()
    {
        return new OkObjectResult("V2");
    }
}

Versioning aware base controller

public abstract class VersioningAwareControllerBase: ControllerBase, IActionFilter
{
    public void OnActionExecuted(ActionExecutedContext context)
    {
        if (!FeatureFlags.ShouldDeprecateV1 ||
            !string.Equals(context.RouteData.Values["controller"].ToString(), "samplev1",
                StringComparison.OrdinalIgnoreCase))
            return;

        context.Result = NotFound();
        context.Canceled = true;
    }

    public void OnActionExecuting(ActionExecutingContext context) { }
}
Peter Csala
  • 17,736
  • 16
  • 35
  • 75
  • That looks good, I will try this. However, I'm curious how an example of `IControllerActivator` and `IControllerFactory` would look like – mu88 May 08 '20 at 09:29
  • 1
    Please check these two articles to get better insight: [IControllerActivator](https://andrewlock.net/controller-activation-and-dependency-injection-in-asp-net-core-mvc/), [IControllerFactory](http://www.jomendez.com/2016/01/20/how-to-create-custom-controller-factory-asp-net/) – Peter Csala May 08 '20 at 09:32
2

Peter Csala's answer is fine, however, it has a dependency to System.Web.Mvc. In our case, this dependency wasn't there before and I found a solution that does not require adding it.

I've extended ApiControllerActionInvoker the following way:

internal class CustomHttpActionInvoker : ApiControllerActionInvoker
{
    public CustomHttpActionInvoker(IConfigProvider configProvider)
    {
        ConfigProvider = configProvider;
        InvokeActionFunc = base.InvokeActionAsync;
    }

    /// <summary>FOR AUTOMATED TESTS ONLY</summary>
    internal CustomHttpActionInvoker(IConfigProvider configProvider,
                                     Func<HttpActionContext, CancellationToken, Task<HttpResponseMessage>> invokeActionFunc)
    {
        ConfigProvider = configProvider;
        InvokeActionFunc = invokeActionFunc;
    }

    private IConfigProvider ConfigProvider { get; }

    private Func<HttpActionContext, CancellationToken, Task<HttpResponseMessage>> InvokeActionFunc { get; }

    /// <inheritdoc />
    public override Task<HttpResponseMessage> InvokeActionAsync(HttpActionContext actionContext, CancellationToken cancellationToken)
    {
        var isRelevantRequest = actionContext.ControllerContext.Controller is MyRelevantController;
        if (isRelevantRequest && ConfigProvider.IsPurpose1)
        {
            return InvokeActionFunc(actionContext, cancellationToken);
        }

        if (!isRelevantRequest && ConfigProvider.IsPurpose2)
        {
            return InvokeActionFunc(actionContext, cancellationToken);
        }

        return Task.FromResult(new HttpResponseMessage(HttpStatusCode.NotFound));
    }
}

The internal constructor was introduced to support easier unit testing.

The following code registers the custom class:

var config = new HttpConfiguration();
config.MapHttpAttributeRoutes();
config.Services.Replace(typeof(IHttpActionInvoker), new CustomHttpActionInvoker(MyConfigProvider));
mu88
  • 4,156
  • 1
  • 23
  • 47