Complex Type Action Parameters and Controller Action Selection with ASP.NET Web API

How to use complex type action parameters in ASP.NET Web API and involve them inside the controller action selection logic
2012-09-30 22:04
Tugberk Ugurlu


If you are familiar with ASP.NET MVC and trying to find your way with ASP.NET Web API, you may have noticed that the default action selection logic with ASP.NET Web API is pretty different than the ASP.NET MVC's. First of all, the action parameters play a huge role on action selection in ASP.NET Web API. Consider the following controller and its two action methods:

public class CarsController : ApiController { 

    //GET /api/cars?categoryId=10
    public string[] GetCarsByCategoryId(int categoryId) { 
        
        return new[] { 
            "Car 1",
            "Car 2",
            "Car 3"
        };
    }
    
    //GET /api/cars?colorId=10
    public string[] GetCarsByColorId(int colorId) { 
        
        return new[] { 
            "Car 1",
            "Car 2"
        };
    }
}

This doesn’t going to cause the action ambiguity because the action parameter names are different. The default action selector (ApiControllerActionSelector) going to extract the action parameter names and try to match those with the URI parameters such as query string and route values. So if a GET request comes to /api/cars?categoryId=10, the GetCarsByCategoryId action method will be invoked. If a GET request comes to /api/cars?colorId=10 in this case, the GetCarsByColorId action method will be called.

It's possible to use complex types as action parameters for GET requests and bind the route and query string values by marking the complex type parameters with FromUriAttribute. However, the default action selection logic only considers simple types which are System.String, System.DateTime, System.Decimal, System.Guid, System.DateTimeOffset and System.TimeSpan. For example, if you have GetCars(Foo foo) and GetCars(Bar bar) methods inside your controller, you will get the ambiguous action error as the complex types are completely ignored by the ApiControllerActionSelector.

Let’s take the following as example here:

public class CarsByCategoryRequestCommand {

    public int CategoryId { get; set; }
    public int Page { get; set; }

    [Range(1, 50)]
    public int Take { get; set; }
}

public class CarsByColorRequestCommand {

    public int ColorId { get; set; }
    public int Page { get; set; }

    [Range(1, 50)]
    public int Take { get; set; }
}

public class CarsController : ApiController {

    public string[] GetCarsByCategoryId(
        [FromUri]CarsByCategoryRequestCommand cmd) {

        return new[] { 
            "Car 1",
            "Car 2",
            "Car 3"
        };
    }

    public string[] GetCarsByColorId(
        [FromUri]CarsByColorRequestCommand cmd) {

        return new[] { 
            "Car 1",
            "Car 2"
        };
    }
}

We are not performing any logic inside the action here but you can understand from the action parameter types that we are aiming to perform pagination here. So, we are receiving the inputs from the consumer. We can use simple types directy as action parameters but there is no built-in way to validate the simple types and I haven’t found an elegant way to hook something up for that. As a result, complex type action parameters comes in handy in such cases.

If we now send a GET request to /api/cars?colorId=23&page=2&take=12, we would get the ambiguity error message:

image

To workaround this issue, I created a new action selector which has the same implementation as the ApiControllerActionSelector and a few tweaks to make this feature work. It wasn’t easy at all. The ApiControllerActionSelector is not so extensible and I had to manually rewrite it (honestly, I didn’t directly copy-paste. I rewrote the every single line). I also thought that this could make it into the framework. So, I sent a pull request which got rejected:  3338. There is also an issue open to make the default action selector more extensible: #277. I encourage you to go and vote!

So, what can we do for now to make this work? Go and install the latest WebAPIDoodle package from the Official NuGet feed:

PM> Install-Package WebAPIDoodle

This package has a few useful components for ASP.NET Web API and one of them is the ComplexTypeAwareActionSelector. First of all, we need to replace the default action selector with our ComplexTypeAwareActionSelector as below. Note that ComplexTypeAwareActionSelector preserves all the features of the ApiControllerAction selector.

protected void Application_Start(object sender, EventArgs e) {

    var config = GlobalConfiguration.Configuration;
    config.Routes.MapHttpRoute(
        "DefaultApiRoute",
        "api/{controller}/{id}",
        new { id = RouteParameter.Optional }
    );

    // Replace the default action IHttpActionSelector with
    // WebAPIDoodle.Controllers.ComplexTypeAwareActionSelector
    config.Services.Replace(
        typeof(IHttpActionSelector),
        new ComplexTypeAwareActionSelector());
}

This package also contains an attribute named UriParametersAttribute which accepts a params string[] parameter. We can apply this attribute to action methods and pass the parameters that we want to be considered during the action selection. The below one shows the sample usage for our above case:

public class CarsController : ApiController {

    [UriParameters("CategoryId", "Page", "Take")]
    public string[] GetCarsByCategoryId(
        [FromUri]CarsByCategoryRequestCommand cmd) {

        return new[] { 
            "Car 1",
            "Car 2",
            "Car 3"
        };
    }

    [UriParameters("ColorId", "Page", "Take")]
    public string[] GetCarsByColorId(
        [FromUri]CarsByColorRequestCommand cmd) {

        return new[] { 
            "Car 1",
            "Car 2"
        };
    }
}

If we now send the proper GET requests as below, we should see it working:

image

image

You can also grab the sample to see this in action. I should also mention that I am not saying that this is the way to go. Clearly, this generates a lot of noise and we can do better here. The one solution would be to inspect the simple type properties of the complex type action parameter without needing the UriParametersAttribute.



New Comment