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
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).
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
}
`GET /api/v1/helloworld`
### Expected Result
```json
{"ids":null}
{"ids":null}
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
}
`GET /api/v1/helloworld`
### Expected Result
```json
{"version":1, "ids":null}
{"version":1, "ids":[]}
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.
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# ids ) => Ok( new { ids } );
[RoutePrefix( "api/v1/helloworld" )]
public class HelloWorldController : ApiController
{
[Route]
public IHttpActionResult Get( [FromUri] IEnumerable
}
Actual Result
Scenario 2
Define the same attribute-based routes, but add a route parameter for version using the built-in
int constraint.
```c# ids ) => Ok( new { version, ids } );
[RoutePrefix( "api/v{version:int}/helloworld" )]
public class HelloWorldController : ApiController
{
[Route]
public IHttpActionResult Get(
[FromUri] int version,
[FromUri] IEnumerable
}
Actual Result
Conclusion
There appears to be some logic in the Web API model binding infrastructure that decides to use an empty sequence instead of
nullunder 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 expectGET /api/v1/helloworld?ids=to yield an empty sequence or possiblynull, but that's not what happens at all. You get this instead: