Carsten Franke Carsten Franke - 2 months ago 11
ASP.NET (C#) Question

Forward values from custom HttpControllerSelector and HttpActionSelector to its descriptor

I have a MVC 5 Web API which returns a custom response in case of unexpected exceptions or if the controller or action were not found. Essentially, I've done exactly as shown there: http://weblogs.asp.net/imranbaloch/handling-http-404-error-in-asp-net-web-api Everything's working like a charm.

The problem is: I'd like to submit the error code from

SelectController()
and
SelectAction()
to my
ErrorController
. This way I would not have duplicate code and all the logic would be in the controller.

Unfortunately, I do not find any possible way to submit the error code to my controller. All the examples are redirecting to a specific error action (e.g.
ErrorController.NotFound404
) I'd like to redirect to
ErrorController.Main
and do all the magic there.

Another issue with the custom
ApiControllerActionSelector
is that the
Request
property is
null
in the
ErrorController
. This problem does not exist with the custom
DefaultHttpControllerSelector
.

Any ideas?

Best regards,
Carsten

Answer

Fortunately, I was able to find the solution myself. Let me show you how I got it up and running.

  1. The custom controller and action selector are forwarding the requested language and the current HTTP response code:
public class CustomDefaultHttpControllerSelector: DefaultHttpControllerSelector
{
    public CustomDefaultHttpControllerSelector(HttpConfiguration configuration) : base(configuration)
    {
    }

    public override HttpControllerDescriptor SelectController(HttpRequestMessage request)
    {
        HttpControllerDescriptor descriptor = null;
        try
        {
            descriptor = base.SelectController(request);
        }
        catch (HttpResponseException e)
        {
            var routeValues = request.GetRouteData().Values;
            routeValues.Clear();
            routeValues["controller"] = "Error";
            routeValues["action"] = "Main";
            routeValues["code"] = e.Response.StatusCode;
            routeValues["language"] = request.Headers?.AcceptLanguage?.FirstOrDefault()?.Value ?? "en";

            descriptor = base.SelectController(request);
        }
        return descriptor;
    }
}

public class CustomControllerActionSelector: ApiControllerActionSelector
{
    public CustomControllerActionSelector()
    {
    }

    public override HttpActionDescriptor SelectAction(HttpControllerContext controllerContext)
    {
        HttpActionDescriptor descriptor = null;
        try
        {
            descriptor = base.SelectAction(controllerContext);
        }
        catch (HttpResponseException e)
        {
            var routeData = controllerContext.RouteData;
            routeData.Values.Clear();
            routeData.Values["action"] = "Main";
            routeData.Values["code"] = e.Response.StatusCode;
            routeData.Values["language"] = controllerContext.Request?.Headers?.AcceptLanguage?.FirstOrDefault()?.Value ?? "en";
            IHttpController httpController = new ErrorController();
            controllerContext.Controller = httpController;
            controllerContext.ControllerDescriptor = new HttpControllerDescriptor(controllerContext.Configuration, "Error", httpController.GetType());
            descriptor = base.SelectAction(controllerContext);
        }
        return descriptor;
    }
}

Two important changes:

1.1. The list of route values needs to be cleared. Otherwise it tries to find an action in the ErrorController which maps to this list of values.

1.2. The code and language were added.

  1. The ErrorController itself:
[RoutePrefix("error")]
public class ErrorController: BaseController
{
    [HttpGet, HttpPost, HttpPut, HttpDelete, HttpHead, HttpOptions, AcceptVerbs("PATCH")]
    [Route("{code}/{language}")]
    public HttpResponseMessage Main(string code, string language)
    {
        HttpStatusCode parsedCode;
        var responseMessage = new HttpResponseMessage();
        if (!Enum.TryParse(code, true, out parsedCode))
        {
            parsedCode = HttpStatusCode.InternalServerError;
        }
        responseMessage.StatusCode = parsedCode;
        ...
    }
}
  1. I've removed the route mapping routes.MapHttpRoute(...). No matter what I've entered in the browser, it never called Handle404.

  2. HTTP status 400 (bad request) was not covered, yet. This could be easily achieved by using the ValidationModelAttribute as described on http://www.asp.net/web-api/overview/formats-and-model-binding/model-validation-in-aspnet-web-api (section "Handling Validation Errors").

Maybe this will help someone...