Search code examples
c#asp.net-web-apiasp.net-web-api2asp.net-web-api-routing

Web API Versioning With Inheritance


I am trying to get Web API versioning working with an inherited class. I am working with two very simple variations of the stock Values controller.

[ApiVersion("1.0")]
[RoutePrefix("api/v{version:apiVersion}/Values")]
[ControllerName("Values")]
public class ValuesController : ApiController
{
    // GET api/values
    [Route("")]
    public IEnumerable<string> Get()
    {
        return new string[] { "value1", "value2" };
    }

    // GET api/values/5
    [Route("{id:int}")]
    public virtual string Get(int id)
    {
        return "value from 1";
    }
}

[ApiVersion("2.0")]
[RoutePrefix("api/v{version:apiVersion}/Values")]
[ControllerName("Values")]
public class Values2Controller : ValuesController
{
    //Want to use the method in the base class
    //public IEnumerable<string> Get()
    //{
    //    return new string[] { "value2-1", "value2-2" };
    // }

    [Route("{id:int}")]
    // GET api/values/5
    public new string Get(int id)
    {
        return "value from 2";
    }
} 

My start up configuration is also pretty straightforward.

public static void Register(HttpConfiguration config)
{
    var constraintResolver = new DefaultInlineConstraintResolver()
    {
        ConstraintMap = {["apiVersion"] = typeof(ApiVersionRouteConstraint)}
    };
    config.MapHttpAttributeRoutes(constraintResolver);
    config.AddApiVersioning(o => { o.AssumeDefaultVersionWhenUnspecified = true; });
}

The non-overridden routes work exactly as I would expect http://localhost:32623/api/v1.0/Values/12 -> "value from 1" http://localhost:32623/api/v2.0/Values/12 -> "value from 2"

Calling v1 of the default Get Route http://localhost:32623/api/v1.0/Values -> Value1, Value2

However trying the same route on the child controller results in an error.

http://localhost:32623/api/v2.0/Values

<Message>
The HTTP resource that matches the request URI 'http://localhost:32623/api/v2.0/Values' does not support the API version '2.0'.
</Message>
<InnerError>
<Message>
No route providing a controller name with API version '2.0' was found to match request URI 'http://localhost:32623/api/v2.0/Values'.
</Message>
</InnerError>

The error message suggests that the overridden member expects a "1.0" route and I am able to work around this with a method like this in the child class.

[Route("")]
public override IEnumerable<string> Get()
{
    return base.Get();
}

But this seems less than ideal across a larger application. Is there a way to make this work the way I would like, without these "empty" overrides?


Solution

  • What you need to do is overwrite the DefaultDirectRoutePrivider to allow route inheritance:

    public class WebApiCustomDirectRouteProvider : DefaultDirectRouteProvider {
        protected override IReadOnlyList<IDirectRouteFactory>
            GetActionRouteFactories(HttpActionDescriptor actionDescriptor) {
            // inherit route attributes decorated on base class controller's actions
            return actionDescriptor.GetCustomAttributes<IDirectRouteFactory>(inherit: true);
        }
    }
    

    And with that done, you will then need to configure it in your web api configuration along with the custom route constraint

    public static void Register(HttpConfiguration config) {
        var constraintResolver = new DefaultInlineConstraintResolver() {
            ConstraintMap = {["apiVersion"] = typeof(ApiVersionRouteConstraint)}
        };
        var directRouteProvider = new WebApiCustomDirectRouteProvider();
        // Attribute routing. (with inheritance)
        config.MapHttpAttributeRoutes(constraintResolver, directRouteProvider);
        config.AddApiVersioning(_ => { _.AssumeDefaultVersionWhenUnspecified = true; });
    }
    

    So now the inheriting value controller will now have the base routes available in derived controllers

    For demonstrative purposes

    [ApiVersion("1.0")]
    [RoutePrefix("api/v{version:apiVersion}/Values")]
    [ControllerName("Values")]
    public class ValuesController : ApiController {
    
        [HttpGet]
        [Route("")] // GET api/v1.0/values
        public virtual IHttpActionResult Get() {
            return Ok(new string[] { "value1", "value2" });
        }
    
        [HttpGet]
        [Route("{id:int}")] // GET api/v1.0/values/5
        public virtual IHttpActionResult Get(int id) {
            return Ok("value from 1");
        }
    }
    
    [ApiVersion("2.0")]
    [RoutePrefix("api/v{version:apiVersion}/Values")]
    [ControllerName("Values")]
    public class Values2Controller : ValuesController {
    
        //Will have inherited GET "api/v2.0/Values" route
    
        // GET api/v2.0/values/5 (Route also inherited from base controller)
        public override IHttpActionResult Get(int id) {
            return Ok("value from 2");
        }
    } 
    

    You will notice that the route in the child is not used on the overridden action as it too will also be inherited from the base controller.