Aspnet-api-versioning: Library changes model binding behavior for IEnumerable parameters

Created on 25 Oct 2018  路  4Comments  路  Source: microsoft/aspnet-api-versioning

I recently introduced the package Microsoft.AspNet.WebApi.Versioning in my project in order to version my API.

It works like a charm, but unfortunately, it has a weird side effect. It changes the default parameter value for a IEnumerable<T> uri parameter from null to an empty List<T>. I isolated it, using the basic example from the package repository.

Calling http://localhost:xxx/api/v1/helloworld returns

ids is null: True if versioning is not enabled (previous behavior)

ids is null: False if versioning is enabled

I'm afraid, that it changes other behaviors as well. How can I keep the version feature, but use the default model binding?

Controller

// breaks
[Microsoft.Web.Http.ApiVersion("1.0")]
[RoutePrefix( "api/v{version:apiVersion}/helloworld" )]

// working
//[RoutePrefix("api/v1/helloworld")]
public class HelloWorldController : ApiController
{
    [Route]
    public IHttpActionResult Get([FromUri] IEnumerable<Guid> ids)
    {
        return Ok($"ids is null: {ids == null}");
    }
}

Setup

public class Startup
{
    public void Configuration( IAppBuilder builder )
    {
        var configuration = new HttpConfiguration();
        var httpServer = new HttpServer( configuration );


        // breaks:
        configuration.AddApiVersioning();
        var constraintResolver = new DefaultInlineConstraintResolver() { ConstraintMap = { ["apiVersion"] = typeof(ApiVersionRouteConstraint) } };
        configuration.MapHttpAttributeRoutes(constraintResolver);

        // defaut (works)
        //configuration.MapHttpAttributeRoutes();
        builder.UseWebApi( httpServer );
    }
}

Reproduce

git clone https://github.com/smstuebe/webapi-versioning-parameter-binding.git
git checkout working
# if you want to break it
git checkout broken
answered asp.net web api question

Most helpful comment

This is curious indeed. It appears that this is a behavior in Web API. You _could_ call it a bug, but it's certainly an inconsistency. I presume it must have something to do with model binding and the processing of route parameters. I can repro the behavior you're seeing with vanilla Web API (no versioning).

Scenario 1

Define attribute-based route templates without any route parameters or constraints.

```c#
[RoutePrefix( "api/v1/helloworld" )]
public class HelloWorldController : ApiController
{
[Route]
public IHttpActionResult Get( [FromUri] IEnumerable ids ) => Ok( new { ids } );
}

`GET /api/v1/helloworld`

### Expected Result
```json
{"ids":null}

Actual Result

{"ids":null}

Scenario 2

Define the same attribute-based routes, but add a route parameter for version using the built-in
int constraint.

```c#
[RoutePrefix( "api/v{version:int}/helloworld" )]
public class HelloWorldController : ApiController
{
[Route]
public IHttpActionResult Get(
[FromUri] int version,
[FromUri] IEnumerable ids ) => Ok( new { version, ids } );
}

`GET /api/v1/helloworld`

### Expected Result
```json
{"version":1, "ids":null}

Actual Result

{"version":1, "ids":[]}

Conclusion

There appears to be some logic in the Web API model binding infrastructure that decides to use an empty sequence instead of null under some scenario. I don't know exactly what that is and it's out of my control. This behavior only seems to take effect when you have route parameters and/or with constraints. It appears this is not a new behavior, but something that has been there that you've never noticed or encountered.

I don't have the capacity to verify all the possible combinations, but I'm willing to bet you can put your money on the fact that this only happens for parameters of IEnumerable<T>. I found a related, curious result. I would expect GET /api/v1/helloworld?ids= to yield an empty sequence or possibly null, but that's not what happens at all. You get this instead:

{"ids":["00000000-0000-0000-0000-000000000000"]}

All 4 comments

Curious. Not sure how this would change. There should be no differences in model binding. Starting in 3.0+, there is a new model binder added for the API version itself, but it's completely additive. Thanks for providing a repro, I'll take a look and report back.

This is curious indeed. It appears that this is a behavior in Web API. You _could_ call it a bug, but it's certainly an inconsistency. I presume it must have something to do with model binding and the processing of route parameters. I can repro the behavior you're seeing with vanilla Web API (no versioning).

Scenario 1

Define attribute-based route templates without any route parameters or constraints.

```c#
[RoutePrefix( "api/v1/helloworld" )]
public class HelloWorldController : ApiController
{
[Route]
public IHttpActionResult Get( [FromUri] IEnumerable ids ) => Ok( new { ids } );
}

`GET /api/v1/helloworld`

### Expected Result
```json
{"ids":null}

Actual Result

{"ids":null}

Scenario 2

Define the same attribute-based routes, but add a route parameter for version using the built-in
int constraint.

```c#
[RoutePrefix( "api/v{version:int}/helloworld" )]
public class HelloWorldController : ApiController
{
[Route]
public IHttpActionResult Get(
[FromUri] int version,
[FromUri] IEnumerable ids ) => Ok( new { version, ids } );
}

`GET /api/v1/helloworld`

### Expected Result
```json
{"version":1, "ids":null}

Actual Result

{"version":1, "ids":[]}

Conclusion

There appears to be some logic in the Web API model binding infrastructure that decides to use an empty sequence instead of null under some scenario. I don't know exactly what that is and it's out of my control. This behavior only seems to take effect when you have route parameters and/or with constraints. It appears this is not a new behavior, but something that has been there that you've never noticed or encountered.

I don't have the capacity to verify all the possible combinations, but I'm willing to bet you can put your money on the fact that this only happens for parameters of IEnumerable<T>. I found a related, curious result. I would expect GET /api/v1/helloworld?ids= to yield an empty sequence or possibly null, but that's not what happens at all. You get this instead:

{"ids":["00000000-0000-0000-0000-000000000000"]}

Thanks for your detailed investigation. I think I'll implement a workaround and maybe look closer into the asp.net code, when I have some time << lol.

Looks like you have a solution to this issue. Feel free to come back if you have more questions. Thanks.

Was this page helpful?
0 / 5 - 0 ratings