Skip to content

Support API Version in URL Generation #663

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
commonsensesoftware opened this issue Sep 21, 2020 · 8 comments
Closed

Support API Version in URL Generation #663

commonsensesoftware opened this issue Sep 21, 2020 · 8 comments
Assignees
Milestone

Comments

@commonsensesoftware
Copy link
Collaborator

Add support to resolve the current API version as an ambient value during URL generation via the IUrlHelper. Today, URL generation works exactly has it is supposed, but often confuses those that version by URL segment and do not include the API version route parameter as an input value. The default IUrlHelper implementations provided by ASP.NET only consider well-known route parameters (ex: [controller], [action], etc) as a possible ambient value.

@voroninp
Copy link

Hello, Chris.

I have an issue even when I do not use version in url.
Here's the repo to reproduce the problem.
When I run POST, I get:

System.InvalidOperationException: No route matches the supplied values.
at Microsoft.AspNetCore.Mvc.CreatedAtActionResult.OnFormatting(ActionContext context)
at Microsoft.AspNetCore.Mvc.Infrastructure.ObjectResultExecutor.ExecuteAsyncCore(ActionContext context, ObjectResult result, Type objectType, Object value)
at Microsoft.AspNetCore.Mvc.Infrastructure.ObjectResultExecutor.ExecuteAsync(ActionContext context, ObjectResult result)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.ResultNext[TFilter,TFilterAsync](State& next, Scope& scope, Object& state, Boolean& isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.InvokeNextResultFilterAsyncTFilter,TFilterAsync
--- End of stack trace from previous location ---
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Rethrow(ResultExecutedContextSealed context)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.ResultNext[TFilter,TFilterAsync](State& next, Scope& scope, Object& state, Boolean& isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.InvokeResultFilters()
--- End of stack trace from previous location ---
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.g__Awaited|20_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.g__Awaited|17_0(ResourceInvoker invoker, Task task, IDisposable scope)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.g__Awaited|17_0(ResourceInvoker invoker, Task task, IDisposable scope)
at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
at Swashbuckle.AspNetCore.SwaggerUI.SwaggerUIMiddleware.Invoke(HttpContext httpContext)
at Swashbuckle.AspNetCore.Swagger.SwaggerMiddleware.Invoke(HttpContext httpContext, ISwaggerProvider swaggerProvider)
at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddlewareImpl.Invoke(HttpContext context)

HEADERS
=======
Accept: /
Connection: keep-alive
Host: localhost:5084
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36
Accept-Encoding: gzip, deflate, br, zstd
Accept-Language: en-US,en;q=0.9,ru;q=0.8
Cookie: .AspNetCore.Antiforgery._qyHzYA0nYs=CfDJ8L6H2Ao5KRNOqEdd0Szk4ky1IHlDMHIOBjVn9eU2S15AZ2zcAW2hegOp8DUfyiWTkparHfXkVkVGEy7v9W2FzlNOleMu0j8qqiUvaU0ILZfpcQBBIaRh8et9783kcc7PbB7jDdsEHOKYHFkPM6vJHMM; .AspNetCore.Antiforgery.N1usb0hhv5Y=CfDJ8L6H2Ao5KRNOqEdd0Szk4kzXNo9YEuLaHQyvbXKtkcMXTJqt132QyOaHKRKk_XyULGuqWp1gxHwf9ci6OR3lxqbcYb8On0hSgoCb0iFP_I-ZmtofuMp3YrW1NWfX-2mRMU30PAJK2ZQd0l942kKsLTs
Origin: http://localhost:5084
Referer: http://localhost:5084/swagger/index.html
Content-Length: 0
sec-ch-ua: "Google Chrome";v="125", "Chromium";v="125", "Not.A/Brand";v="24"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty

Am I missing something here?

@commonsensesoftware
Copy link
Collaborator Author

From the looks of it, this is simply because you are missing the route parameter in the template. The correct signature should be:

[HttpGet("{id}")]
public async Task<string> Get(int id) => $"Hello, {id}!";

It's also worth nothing that the issue with URL generation only exists if you are versioning by URL segment, which you are not (currently) doing and I don't recommend. 😉

@voroninp
Copy link

Unfortunately, even with "{id}" I have the same error.

@commonsensesoftware
Copy link
Collaborator Author

Using the BasicExample as the baseline project, I added your example controller as follows:

[ApiController]
[Route( "[controller]" )]
public sealed class HomeController : ControllerBase
{
    [HttpGet( "{id}" )]
    public IActionResult Get( int id ) => Ok( $"Hello, {id}!" );

    [HttpPost]
    public IActionResult Post() => CreatedAtAction( nameof( Get ), new { id = 42 }, null );
}

Request

POST /home?api-version=1.0 HTTP/2

Response

HTTP/2 201
Location: http://localhost/home/42
api-supported-versions: 1.0

Hopefully that helps you figure out the difference from your setup.

@voroninp
Copy link

Ofc there are differences =)

public static void ConfigureApiVersioning(this IServiceCollection services)
{
    var headerApiVersionReader = new HeaderApiVersionReader("Accept-Version");

    services.AddSingleton<IConfigureOptions<SwaggerGenOptions>, ApiVersioning.SwaggerGenOptionsConfigurer>();
    services.AddSingleton<IConfigureOptions<SwaggerUIOptions>, ApiVersioning.SwaggerUIOptionsConfigurer>();

    services.AddApiVersioning(cfg =>
    {
        cfg.ApiVersionSelector = new CurrentImplementationApiVersionSelector(cfg);
        cfg.AssumeDefaultVersionWhenUnspecified = true;
        cfg.ReportApiVersions = true;

        cfg.ApiVersionReader = ApiVersionReader.Combine(
            new MediaTypeApiVersionReader(),
            headerApiVersionReader,
            new QueryStringApiVersionReader());
    })
    .AddMvc() // This is not the same AddMvc method you might have thought about.
    .AddApiExplorer(opt =>
    {
        opt.GroupNameFormat = "VV"; // Shows version as Major.Minor
        opt.ApiVersionParameterSource = headerApiVersionReader;
    });
}

Get method works fine.
If I explicitly specify not supported version 2.0 I get 400 for both Get and Post.

But if I omit it or specify 1.0 for Post, it fails with 500.

@voroninp
Copy link

SwaggerUIOptionsConfigurer causes the issue. Without it, Postman requests work

@commonsensesoftware
Copy link
Collaborator Author

You can inject IApiVersionDescriptionProvider directly. There's almost no reason to ever use the factory. I don't see anything else obvious that would cause that problem. 🤔

@voroninp
Copy link

Yes, injecting IEnumerable<EndpointDataSource> dataSources somehow leaves them empty.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants