aspnet-api-versioning: Api Versioning break CreatedAt on MVC Core 2.2
If I make GET
request on http://localhost:5000/api/v1/values/42, then I’ve got the following result
{
"value": "model 42"
}
and it’s ok. Bui if I make a POST
on http://localhost:5000/api/v1/values
with following body
{
"value": 42
}
I’ve got the following stacktrace
fail: Microsoft.AspNetCore.Server.Kestrel[13]
Connection id "0HLIS5H63UVVC", Request id "0HLIS5H63UVVC:00000003": An unhandled exception was thrown by the application.
System.NullReferenceException: Object reference not set to an instance of an object.
at Microsoft.AspNetCore.Mvc.Routing.ApiVersionRouteConstraint.Match(HttpContext httpContext, IRouter route, String routeKey, RouteValueDictionary values, RouteDirection routeDirection)
at Microsoft.AspNetCore.Routing.Template.TemplateBinder.TryProcessConstraints(HttpContext httpContext, RouteValueDictionary combinedValues, String& parameterName, IRouteConstraint& constraint)
at Microsoft.AspNetCore.Routing.DefaultLinkGenerator.TryProcessTemplate(HttpContext httpContext, RouteEndpoint endpoint, RouteValueDictionary values, RouteValueDictionary ambientValues, LinkOptions options, ValueTuple`2& result)
at Microsoft.AspNetCore.Routing.DefaultLinkGenerator.GetPathByEndpoints(List`1 endpoints, RouteValueDictionary values, RouteValueDictionary ambientValues, PathString pathBase, FragmentString fragment, LinkOptions options)
at Microsoft.AspNetCore.Routing.DefaultLinkGenerator.GetPathByAddress[TAddress](HttpContext httpContext, TAddress address, RouteValueDictionary values, RouteValueDictionary ambientValues, Nullable`1 pathBase, FragmentString fragment, LinkOptions options)
at Microsoft.AspNetCore.Routing.LinkGeneratorRouteValuesAddressExtensions.GetPathByRouteValues(LinkGenerator generator, HttpContext httpContext, String routeName, Object values, Nullable`1 pathBase, FragmentString fragment, LinkOptions options)
at Microsoft.AspNetCore.Mvc.Routing.EndpointRoutingUrlHelper.Action(UrlActionContext urlActionContext)
at Microsoft.AspNetCore.Mvc.UrlHelperExtensions.Action(IUrlHelper helper, String action, String controller, Object values, String protocol, String host, String fragment)
at Microsoft.AspNetCore.Mvc.UrlHelperExtensions.Action(IUrlHelper helper, String action, String controller, Object values, String protocol, String host)
at Microsoft.AspNetCore.Mvc.CreatedAtActionResult.OnFormatting(ActionContext context)
at Microsoft.AspNetCore.Mvc.Infrastructure.ObjectResultExecutor.ExecuteAsync(ActionContext context, ObjectResult result)
at Microsoft.AspNetCore.Mvc.ObjectResult.ExecuteResultAsync(ActionContext context)
at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeResultAsync(IActionResult result)
at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeNextResultFilterAsync[TFilter,TFilterAsync]()
at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.Rethrow(ResultExecutedContext context)
at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.ResultNext[TFilter,TFilterAsync](State& next, Scope& scope, Object& state, Boolean& isCompleted)
at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeResultFilters()
at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeNextResourceFilter()
at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.Rethrow(ResourceExecutedContext context)
at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeFilterPipelineAsync()
at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeAsync()
at Microsoft.AspNetCore.Routing.EndpointMiddleware.Invoke(HttpContext httpContext)
at Microsoft.AspNetCore.Routing.EndpointRoutingMiddleware.Invoke(HttpContext httpContext)
at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.ProcessRequests[TContext](IHttpApplication`1 application)
info: Microsoft.AspNetCore.Hosting.Internal.WebHost[2]
Request finished in 191.4428ms 500
I can put breakpoint inside Post
action and see that it calls correctly. The problem is somewhere inside CreatedAtAction.
Here is minimal repro .csproj
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp2.2</TargetFramework>
<AspNetCoreHostingModel>OutOfProcess</AspNetCoreHostingModel>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="Microsoft.AspNetCore.Razor.Design" Version="2.2.0" PrivateAssets="All" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Versioning" Version="3.0.0-beta2" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer" Version="3.0.0-beta2" />
</ItemGroup>
</Project>
and Program.cs
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
namespace ApiVersionTrouble
{
public static class Program
{
public static void Main(string[] args)
{
CreateWebHostBuilder(args).Build().Run();
}
public static IWebHostBuilder CreateWebHostBuilder(string[] args)
{
return WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>();
}
}
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
var builder = services.AddMvc();
builder.SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
services.AddApiVersioning();
services.AddVersionedApiExplorer();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseMvc();
}
}
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{version:apiVersion}/values")]
[Produces("application/json")]
public class ValuesController : ControllerBase
{
[HttpGet("{settingsId:long}")]
public ValueResponse Get([FromRoute] long settingsId)
{
return new ValueResponse(settingsId);
}
[HttpPost]
public ActionResult Post([FromBody] ValueInput model)
{
var response = new ValueResponse(model.Value);
return CreatedAtAction(
nameof(Get),
new {version = "1", settingsId = model.Value},
response);
}
}
public class ValueInput
{
public long Value { get; set; }
}
public class ValueResponse
{
public ValueResponse(long id)
{
Value = "model " + id.ToString("D");
}
public string Value { get; }
}
}
About this issue
- Original URL
- State: closed
- Created 6 years ago
- Reactions: 3
- Comments: 22 (1 by maintainers)
I can confirm that there was indeed a bug in the ApiVersionRouteConstraint with Endpoint Routing. The new URL generation mechanism can call IRouteConstraint instances with a
null
HttpContext. This was fixed and is included in 3.1.The example here is similar to the scenario you are describing. The only difference is that it uses
CreatedAtRoute
.I tweaked it ever so slightly to repro your exact scenario:
Here’s the results of calling POST:
This demonstrates that
CreatedAtAction
is working.You might want to make sure you’ve done a restore and clean build with the latest update. You might also check and/or report the stack trace to confirm it’s happening within API versioning. Given the information you’ve provided, I’m not repro’ing your scenario (anymore).
I’m happy to investigate further with some more info.
@vanbukin I had same thing and am raising issue on aspnetcore. you can get behaviour back if you set
EnableEndpointRouting
flag false inMvcOptions
withinAddMvc(options => ...)
3.1 has been published and includes support for Endpoint Routing. All of the sample applications have been updated to use Endpoint Routing by default as well. Thanks.
@elonmallin, you didn’t provide information about your controller or action, but I’m willing to guess that you’re versioning by URL segment as in the examples above. Based on what you’ve provided, I surmise that you are simply not including the API version as one of the route parameters for URL generation. I can’t say why the legacy routing system automatically has the route parameter populated, but Endpoint Routing doesn’t.
You should be able to resolve things using one of the following:
Option 1
Explicitly retrieve the requested API version and specify it in the route generation process.
Option 2
Implicitly retrieve the requested API version via model binding and specify it in the route generation process.
@vanbukin, @AdamWillden is correct. 2.2 introduces a new type of routing system called Endpoint Routing, which is the new default. You can use the legacy routing mechanism by disabling this in the options. I’ve been aware of this change for some time and, in fact, some of those design driving the routing changes come from my feedback. Unfortunately, the ASP.NET has an entire platform team and I’m just one guy. I’m not even part of the ASP.NET team.
That said, I’m working toward supporting this as quickly as possible. The largest barrier isn’t the changes in code, but the amount of testing and coordinating that the API Versioning 3.0 release. The 3.0 release brings a lot of fixes and other great features that do not depend on 2.2. If I jump the gun and force that immediately, all library consumers have to move up to 2.2, which is almost certainly not viable for everyone.
I had a recent chat with the ASP.NET team and their LTS story will be on 2.1 and that isn’t expected to change again until 3.1. My current plan is to release 3.0, which only depends on 2.1. That will have a good story for existing adopters for the foreseeable future. I don’t have the capacity to maintain a ton of version variations, so I definitely won’t backport features and bug fixes will have to absolutely critical for me to consider them on older versions. The reported issues have dropped significantly since the Beta 2 release, which indicates stability and has given me some bandwidth. I anticipate that the official release will be later this month. The changes to support Endpoint Routing should be mostly internal shuffling and minor refactoring. I don’t have a firm date, but I anticipate that it will be sometime in January.
I know the community is champing at the bit for this. I’m going as fast as my fingers and physics allow, but - again - I’m just one guy. I still have to do my day job on top of it. Thanks for you patience.
Agreed; they don’t have to be same controller. Be aware that the route tables are not API version aware. This may require you to create a route naming convention which bakes in the API version; for example, “GetUserV1” or “GetUserV2”.
The reason other versioning methods do not suffer from this problem is because the API version is not in the route template (e.g. the path). Query parameters, headers, etc are specified by the client - as they should be. The server’s job is not to tell the client which version they want. By definition of the Uniform Interface constraint, the path
api/v1/orders/123
andapi/v2/orders/123
implies two completely different resources, but that’s almost certainly not true; they are different representations - at best. The path is the resource identifier. Other methods use a stable path and identifier. The client should indicate which representation they want by specifying the appropriate media type, query parameter, header, etc.No problem. It appears the primary reason it’s not working for you is that you have not applied the ApiVersionRouteConstraint. This is required when versioning by URL segment.
Your route template is:
But it should be:
apiVersion is the default name associated with the route constraint. It’s possible to change this name in the options if you really want to. The name version is your user-defined route parameter name. This can be named whatever you want, but I usually use version in the examples.
I hope that helps.
BTW: support for the ApiVersion as a model-bound action parameter is a new feature 3.0. This way you don’t have to hardcode the values nor use
Request.HttpContext.GetRequestedApiVersion()
(which is still available).@commonsensesoftware You are awesome! Thank you!