Search code examples
.netresthttp-status-codeswebapi

.Net Web API return statuscode with specific type without actionresult?


I'm creating a web API and I'm using specific return types, as I have done it many times. Usually, the return status code does not matter that much as long as it conforms to REST conventions, which the framework indeed does, so I leave it at default. For example, this gives me statuscode 200 OK for most successful operations and the use of specific types helps generating front-end code with tools like swagger. This way, however, I cannot modify the status code of the HTTP response and I'm stuck with the generic statuscodes, so I cannot, say, return a 201 Created statuscode as a result of a successful registration or new item created.

The documentation points to IActionResult and ActionResult<T> for specifying the actual status code, specifically, the use of convenience methods like Ok() Created() etc. My problem with this is that it not only enforces the return type on my controllers, it also adds parts I do not want to use (for example, in case of Created(), a CreatedResult dto) and I want my API to return as little info as required. Not to mention that a return type of Task<ActionResult<MyShinyDto>> looks painfully ugly. So I would rather avoid the use of these predefined types. I'm also aware that in this case, I would need to make use of the ProducesAttribute to tell code generator tools like swagger about the returned types and codes.

I already created a general exception handling middleware using the MS docs on it where I spefically set the return status code based on an exception to status code mapping I created, but I would rather not reroute every request through a middleware just to set the statuscode.

So is there really no other way to do what I want? I imagined .Net would have some kind of attribute that I can decorate the contoller methods with indicative of the return type, but I could not find any. The closest I found was ProducesAttribute, but it does not modify the Status Code, it is only indicating it to code gen tools.


Solution

  • I post an answer here in case anyone needs something similar.

    So I ended up creating a custom ActionFilter that does the job although I hoped for something a bit more elegant.

    public class StatusCodeAttribute : ProducesResponseTypeAttribute, IActionFilter 
    {
        private readonly int statusCode;
        public StatusCodeAttribute(HttpStatusCode statusCode) 
            :base((int)statusCode)
        {
            this.statusCode = (int)statusCode;
        }
        public void OnActionExecuted(ActionExecutedContext context)
        {
            context.HttpContext.Response.StatusCode = this.statusCode;
        }
    
        public void OnActionExecuting(ActionExecutingContext context)
        {
            // not going to use this
            return;
        }
    }
    

    Implementing the IActionFilter interface gives you access to the context of an action call, and through it, the HttpContext itself, so you can set the status code manually (yeah, it was a wrong assumption that any means of redirection is needed). I used the HttpStatusCode enum to safeguard against using arbitrary codes.

    The implementation also uses the ProducesResponseTypeAttribute to indicate the response status code to code editors. Since we do not use generic return types, the exact types are picked up by swagger. The only downside of this implementation is that the Content-Type is not. Below image testifies that swagger indeed picked up the data on my EventController, but it did not know about the Contet-Type.

    Even though there is room for improvement, this is basically what I needed. If anybody knows any downsides to my implementation, please do not hesitate to comment, although I do not think that setting the status code this way would be harmful in any way nor would it be a performance hindrance.

    Status code is picked up by swagger

    EDIT

    I forgot to mention that I changed the base class of the Controller from ControllerBase to Controller, because this implementation has the filter features I used in the code.