Aspnet-api-versioning: .Net Core 3.0 and using CreatedAtRoute result in No route matches the supplied values.

Created on 15 Oct 2019  路  38Comments  路  Source: microsoft/aspnet-api-versioning

When using .Net Core 3.0 within a WebApi project, if you return a CreateAtRoute result then it fails with a "No route matches the supplied values" error.

Version: 4.0.0-preview8.19405.7

Controller methods
For the Get
[HttpGet("{id}", Name = nameof(GetById))]
public IActionResult GetById(string id)

For the Create
[HttpPost]
public async Task Create(string id)
{
return CreatedAtRoute(nameof(GetById), new { id = 1 }, "test");
}

When call the Create method this then generates the exception:

System.InvalidOperationException: No route matches the supplied values.
at Microsoft.AspNetCore.Mvc.CreatedAtRouteResult.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.ObjectResult.ExecuteResultAsync(ActionContext context)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.g__Logged|21_0(ResourceInvoker invoker, IActionResult result)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.g__Awaited|29_0TFilter,TFilterAsync
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Rethrow(ResultExecutedContextSealed context)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.ResultNextTFilter,TFilterAsync
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.InvokeResultFilters()
--- End of stack trace from previous location where exception was thrown ---
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.g__Awaited|19_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.g__Logged|17_1(ResourceInvoker invoker)
at Microsoft.AspNetCore.Routing.EndpointMiddleware.g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger)
at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)

Within the Startup.cs

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllers();
        services.AddApiVersioning();
    }

    public virtual void Configure(IApplicationBuilder app, IWebHostEnvironment env, IHostApplicationLifetime lifetime)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        app.UseRouting();
        app.UseAuthorization();
        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers();
        });
    }
answered asp.net core question

Most helpful comment

@iv41879 this doesn't seem to be related to API versioning; however, as I suspected, you are not providing all of the route parameters. You are missing the userId parameter.

Make the following change:

c# return CreatedAtRoute(nameof(GetPhoto), new { userId, id = photo.Id }, photoToReturn);

And it will work.

Thanks

All 38 comments

You didn't put the full route template, but I'm willing to bet that you are versioning by URL segment. The UrlHelper doesn't understand how to find this value because it's _special_. It comes from different places and can even come from multiple places in a single request.

If you include the API version, things should work just fine. For example:

c# [HttpPost] public async Task Create(string id, ApiVersion apiVersion) => CreatedAtRoute(nameof(GetById), new { id = 1, version = apiVersion.ToString() }, "test");

Note: Other methods of API versioning do not have this problem because they don't appear in the URL path.

Thanks for the quick response. Has this behavior changed with .Net Core 3.0?

No. It's been that way for some time. You might be experiencing a difference in the routing systems. The _legacy_ routing system works quite different from _Endpoint Routing_. There's also the difference in behavior between CreatedAtAction vs CreatedAtRoute. The xxxRoute flavors rely on a named key which will not be API version aware. I now avoid this because you have come up with a naming scheme such as "GetByIdV1" and so on.

For completeness, I find myself always including all route parameters - required or not - as opposed to relying on _magic_ from the routing pipeline. For example, I wouldn't expect the routing system to build the URL for api/orders/{id}/items unless I give it the order identifier. Your mileage may vary, but at least this way you're not surprised if the routing system doesn't find an expected route parameter.

Thanks for the explanation on this.

I thought I had this sorted until I had some Controllers that used variable paths - and now cannot get it to work in this situation. For example:

[ApiController]
[ApiVersion("1.0")]
[Route("api/v{version:apiVersion}/[controller]/{who}/me")]
public class TestController : ControllerBase
{
    [HttpGet("{id:int}", Name = nameof(GetTestByIdv1))]
    public IActionResult GetTestByIdv1(string id, ApiVersion version)
    {
        return new OkObjectResult($"{id} : {version}");
    }

    [HttpPost]
    public IActionResult Create(string id, ApiVersion version)
    {
        return CreatedAtRoute(nameof(GetTestByIdv1), new { id = id, version = $"{version}" }, $"{id} : {version}");
    }
}

Request a Post to: http://localhost:5000/api/v1.0/test/1/me
Results in the same No route matches the supplied values.

If I remove the variable in the path name, i.e. change the route to
[Route("api/v{version:apiVersion}/[controller]/me")]
then it works as expected.

As mentioned previously the CreatedAtRoute is not Api aware so I tried using the CreatedAtAction but had the same issue.

I don't see how this could ever work in it's current state. There are several issues here.

  1. There is no parameter for {who} unless it's being _magically_ provided elsewhere
  2. You've specified an integer type constraint without actually using an integer parameter
    a. I guess this _could_ be intentional, but it seems odd

Your full templates are:

  • GET api/v{version:apiVersion}/[controller]/{who}/me/{id:int}
  • POST api/v{version:apiVersion}/[controller]/{who}/me

This would mean that I'd expect sample URLs to look like:

  • GET api/v1/test/bob/me/42
  • POST api/v1/test/bob/me

To match these signatures, your actions need to look like this:

```c#
[HttpGet("{id:int}", Name = nameof(GetTestByIdv1))]
public IActionResult GetTestByIdv1(string who, int id, ApiVersion version)
{
return this.Ok(new { id, who, version = $"{version}" });
}

[HttpPost]
public IActionResult Create(string who, ApiVersion version)
{
var test = new { id = 42, who };
var routeValues = new { who, id = test.id, version = $"{version}" }
return CreatedAtRoute(nameof(GetTestByIdv1), routeValues, test);
}
```

I hope that helps.

Thank you, Adding the "{who}" route to the route value response sorted the problem. The type of parameter for the id was just a typo as I was trying to make a simple example.

Just for completeness for anyone else having a similar problem this is a working example:

[ApiController]
[ApiVersion("1.0")]
[Route("api/v{version:apiVersion}/[controller]/{who}/me")]
public class TestController : ControllerBase
{
    [HttpGet("{id}", Name = nameof(GetTestByIdv1))]
    public IActionResult GetTestByIdv1(string id)
    {
        return new OkObjectResult($"{id}");
    }

    [HttpPost]
    public IActionResult Create([FromRoute] string who, string id, ApiVersion version)
    {
        return CreatedAtRoute(nameof(GetTestByIdv1), new { who, id = "1", version = $"{version}" }, "1");
    }
}

Again thanks for the quick response and your work on this.

Hi,

what if I am not using API versioning at all? The is the function with which I am having exact the same issue when calling CreatedAtRoute.

Controller source code:

using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using AutoMapper;
using CloudinaryDotNet;
using CloudinaryDotNet.Actions;
using DatingApp.API.Data;
using DatingApp.API.Dtos;
using DatingApp.API.Helpers;
using DatingApp.API.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;

namespace DatingApp.API.Controllers
{
    [Authorize]
    [Route("api/users/{userId}/photos")]
    [ApiController]
    public class PhotosController : ControllerBase
    {
        private readonly IRepository repo;
        private readonly IMapper mapper;
        private readonly IOptions<CloudinarySettings> cloudinaryConfig;
        private Cloudinary cloudinary;

        public PhotosController(IRepository repo, IMapper mapper,
                                IOptions<CloudinarySettings> cloudinaryConfig)
        {
            this.cloudinaryConfig = cloudinaryConfig;
            this.mapper = mapper;
            this.repo = repo;

            Account acc = new Account(
                this.cloudinaryConfig.Value.CloudName,
                this.cloudinaryConfig.Value.ApiKey,
                this.cloudinaryConfig.Value.ApiSecret
            );

            this.cloudinary = new Cloudinary(acc);
        }

        [HttpGet("{id}", Name = nameof(GetPhoto))]
        public async Task<IActionResult> GetPhoto(int id)
        {
            var photoFromRepo = await this.repo.GetPhoto(id);
            var photo = this.mapper.Map<PhotoForReturnDto>(photoFromRepo);
            return Ok(photo);
        }

        [HttpPost]
        public async Task<IActionResult> AddPhotoForUser(int userId, 
            [FromForm]PhotoForCreationDto photoForCreationDto)
        {
            var userFromRepo = await this.repo.GetUser(userId);

            var file = photoForCreationDto.File;
            var uploadResult = new ImageUploadResult();

            if (file.Length > 0)
            {
                using (var stream = file.OpenReadStream())
                {
                    var uploadParams = new ImageUploadParams()
                    {
                        File = new FileDescription(file.Name, stream),
                    };

                    uploadResult = this.cloudinary.Upload(uploadParams);
                }
            }

            photoForCreationDto.Url = uploadResult.Uri.ToString();
            photoForCreationDto.PublicId = uploadResult.PublicId;

            var photo = this.mapper.Map<Photo>(photoForCreationDto);

            if (!userFromRepo.Photos.Any(p => p.IsMain))
                photo.IsMain = true;

            userFromRepo.Photos.Add(photo);

            if (await this.repo.SaveAll())
            {
                var photoToReturn = this.mapper.Map<PhotoForReturnDto>(photo);
                return CreatedAtRoute(nameof(PhotosController.GetPhoto), new { id = photo.Id }, photoToReturn);
            }

            return BadRequest("Could not add the photo");
        }
    }
}

@iv41879 Without all of the route template and configuration information, it's hard to say. I don't have a completely picture. From within the same controller, I would recommend using CreatedAtAction instead.

@commonsensesoftware Thanks for feedback. I have updated the source code for controller above. I have tried with CreatedAtActionbut the issue is the same.
In addition I am adding configuration section below:


        public void ConfigureServices(IServiceCollection services)
        {
            services.AddDbContext<DataContext>
                (options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
            services.AddControllers();
            services.AddCors();
            services.Configure<CloudinarySettings>(Configuration.GetSection("CloudinarySettings"));
            services.AddAutoMapper(typeof(Startup));
            services.AddTransient<Seed>();
            services.AddScoped<IRepository, Repository>();
            services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
                    .AddJwtBearer(options => {
                        options.TokenValidationParameters = new TokenValidationParameters
                        {
                            ValidateIssuerSigningKey = true,
                            IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII
                                .GetBytes(Configuration.GetSection("AppSettings:Token").Value)),
                            ValidateIssuer = false,
                            ValidateAudience = false
                        };
                    });
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env, Seed seeder)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler(builder => {
                    builder.Run(async context => {
                        context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;

                        var error = context.Features.Get<IExceptionHandlerFeature>();

                        if (error != null)
                        {
                            context.Response.AddApplicationError(error.Error.Message);
                            await context.Response.WriteAsync(error.Error.Message);
                        }
                    });
                });
            }
            app.UseCors(x => x.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod());
            app.UseAuthentication();
            app.UseHttpsRedirection();
            app.UseRouting();
            app.UseAuthorization();
            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });
        }

@iv41879 this doesn't seem to be related to API versioning; however, as I suspected, you are not providing all of the route parameters. You are missing the userId parameter.

Make the following change:

c# return CreatedAtRoute(nameof(GetPhoto), new { userId, id = photo.Id }, photoToReturn);

And it will work.

Thanks

@commonsensesoftware Thank you very much.
In the previous versions of ASP.NET Core this additional route parameter was not needed to be specified but moving to 3.0 all route parameters need to be specified - understood. Thank you!

No problem. I believe this is due to the differences in behavior of _Endpoint Routing_ versus the legacy routing system. Regardless, I always recommend including all of the route parameters for clarity.

It seems all the questions on this thread have been answered. Come back or reopen the issue if I've missed something.

Thanks.

Hi,

I hope re-opening this issue is not a problem. I have a similar problem running ASP.NET Core 3.0.

I have the following controller:

[Route(Version.Path + "/[controller]")]
    [ApiController]
    public class WorkOrderController : ControllerBase
    {
        private readonly IWorkOrderManager _workOrderManager;

        public WorkOrderController(IWorkOrderManager workOrderManager)
        {
            _workOrderManager = workOrderManager;
        }

        [HttpGet("{id}")]
        [ApiConventionMethod(typeof(DefaultApiConventions), nameof(DefaultApiConventions.Get))]
        public async Task<ActionResult<WorkOrderResponse>> GetWorkOrderByIdAsync(Guid id)
        {
            var workOrder = await _workOrderManager.GetWorkOrderByIdAsync(id);
            if (workOrder == null)
            {
                return NotFound();
            }

            return workOrder;
        }

        [HttpPost]
        [ApiConventionMethod(typeof(DefaultApiConventions), nameof(DefaultApiConventions.Post))]
        public async Task<IActionResult> CreateWorkOrderAsync(WorkOrderRequest workOrderRequest)
        {
            var id = await _workOrderManager.CreateWorkOrderAsync(workOrderRequest);
            var action = CreatedAtAction(nameof(GetWorkOrderByIdAsync), new { id = id }, id);
            return action;
        }
    }

Version.Path is just string containing "api/v1". So the whole route would be "api/v1/[controller]".
The above action will give me an error saying

{ StatusCode = 500, Message = No route matches the supplied values. }

This is my startup definition:

public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        [SuppressMessage("CA1822", "CA1822", Justification = "Auto-generated code")]
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllers();
            services.AddDataAccessServices(Configuration[ConfigurationConstants.WorkplannerDbConnectionString]);
            services.AddScoped<IWorkOrderManager, WorkOrderManager>();

            services.AddAutoMapper(Assembly.GetExecutingAssembly());
            services.AddSwaggerGen(c =>
            {
                c.SwaggerDoc("v1", new OpenApiInfo { Title = "WebAPI", Version = "v1" });
            });
        }

        [SuppressMessage("CA1822", "CA1822", Justification = "Auto-generated code")]
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseHsts();
            }

            app.UseMiddleware<ExceptionMiddleware>();

            app.UseSwagger();
            app.UseSwaggerUI(c =>
            {
                c.RoutePrefix = string.Empty;
                c.SwaggerEndpoint("/swagger/v1/swagger.json", "WebAPI v1");
            });

            app.UseHttpsRedirection();

            app.UseRouting();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });
        }
    }

Would you happen to know what I am doing wrong?

Hmmm... I'm not entirely sure. Everything _looks_ correct. The only thing I can think of is that route parameter is a typed GUID, but the routing system wants it as a string. I don't _think_ that should matter, but it might. Try it with new {id = id.ToString()}. Beyond that, I'm really sure. Nothing is leaping out.

This doesn't appear to related to API versioning. General questions are probably more appropriately asked and answered on Stack Overflow (SO) or the ASP.NET repo.

I hope that helps, if not a little.

This looks a bit different in that you don't look to be using the ApiVersioning package, but are missing the version part of the route?

As you have defined the controller to be:
[Route(Version.Path + "/[controller]")]

Then would you not require something like:

var action = CreatedAtAction(nameof(GetWorkOrderByIdAsync), new { version = Version.Path, id = id }, id);

If you look at the example I gave above:

The route I defined:
[Route("api/v{version:apiVersion}/[controller]/{who}/me")]

The response I returned:
return CreatedAtRoute(nameof(GetTestByIdv1), new { who, id = "1", version = $"{version}" }, "1");

@cowlinb6 @commonsensesoftware Thanks for the replies.

I tried both of your suggestions to no avail. I even tried to simply remove my custom routing and stuck with the default routing pattern, and it still does not work.

Does it have something to do with what is passed in as value?

var id = await _workOrderManager.CreateWorkOrderAsync(workOrderRequest);
return CreatedAtAction(nameof(GetWorkOrderByIdAsync), new { id = id }, id);

In the examples i've seen, the last argument is always an entity or so. This time, I only want to return the ID, is there something that can go wrong?

While @cowlinb6 is correct, I don't see any of the API versioning services being registered or used. It appears are you are using your own versioning semantics and customizations. The value shouldn't matter for generating the route. It's only used to produce the response. I'm guessing that ApiConventionMethodAttribute is some kind of custom routing, but you've stated you removed it and it still doesn't work. The arguably rules it out.

For these types of issues, I would suggest starting with a purely vanilla controller setup. I would start with everything as simple as possible. This means removing constants, statics, custom routing, etc. Make it as bare bones and vanilla as possible. You should then see things work. Once things are working, start progressively adding back in the things you want. That should eventually lead you the culprit of the problem.

I also noticed that in your setup you have not included services.AddMvcCore() or services.AddMvc(). I'm not sure that services.AddControllers() implicitly calls one of those. It could be that you haven't registered some required services. This would be evident by even the most basic setup still not working.

I hope that helps give you a few additional ideas to try.

Should have spotted this before, it looks like you're missing the Name in the Get definition attribute.

   [HttpGet("{id}", Name = nameof(GetWorkOrderByIdAsync))]

Good eye @cowlinb6, but @victormarante is using CreatedAtAction which uses the action method name as opposed to CreatedAtRoute which uses the registered route name.

Ok its a breaking change in .Net Core 3.0! Don't let your action names end in async.

https://github.com/aspnet/AspNetCore/issues/15316

@cowlinb6 Oh ok, good catch! I appreciate the help, I will try to remove Async and see if that helps!

@cowlinb6 In fact, nothing prevent us to keep "Async" prefix at the end of action method (for exemple in order to comply with a project naming scheme) but we must know that:

  • if route path is infer from method name (ie when no HttpXxxx attribute is set), a trailing "Async" will be automatically removed from method name,
  • When CreatedAtAction is called, a trailing "Async" must be manually remove from method name before using it as actionName parameter (no more nameof(Methodname)).

A better approach (more symetric and with less breaking changes) could have been to also automatically try to removed a trailing "Async" in actionName parameter inside CreatedAtAction method.

If we try to have a more global view, CreatedAtAction is poorly designed.

When attribute HttpXxxx(Name="MyNewName") is used, "MyNewName" is used for generating the URL but "MyNewName" cannot be used for actionName parameter of CreatedAtAction method.

Contrary to what the documentation says, actionName parameter of CreatedAtAction method is not :

The name of the action to use for generating the URL.

A better solution should have been to have a method CreatedAtMethod with a methodName parameter (obviously without trailing "Async" removal complexity).

Man this stuff is so broken. So far none of the suggestions on any issue work.

@phillip-haydon that's a seemingly unsubstantiated claim without any context or description of your specific issue. If you want to share something, then I'm sure someone can lend a hand.

As follow up to those that encounter this issue. Here are some basic rules and guidelines:

  1. CreatedAtRoute or any other URL generator based on route name is not API version aware. This means that the route names need to be unique; consider a naming scheme such as "GetV1", "GetV2", and so on.
  2. Prefer CreatedAtAction over CreatedAtRoute as it does not have the naming restrictions. This obviously isn't an option for link generation outside of the current controller.
  3. Do not assume that the routing system will automatically fill in or otherwise provide the API version if you are versioning by URL segment.
    a. You can get current API version by adding it as an action parameter (the recommended way)
    b. You can imperatively access the requested API version via HttpContext.GetRequestedApiVersion()

It's also worth noting that versioning by URL segment is the least RESTful of all methods as it violates the _Uniform Interface_ constraint. All other methods do not suffer from these issues because they produce stable, consistent URL paths. Even the query string, while technically part of the URL, is not part of the path, does not identify a resource, and is not used in route matching. It's always the client's job to specify the API version they want, which is really referring to the _representation_ they want to work with.

It breaks on an action defined as:

[HttpGet, Route("{id:int}", Name = nameof(...)]

Moving it to

[HttpGet("{id:int}", Name = nameof(...)]

Resolved the issue, but spending ages sifting through documentation to find anything about this breaking change... old routing > new routing.

Atleast the EF issues I ran into were documented as a proper list.

https://docs.microsoft.com/en-us/ef/core/what-is-new/ef-core-3.0/breaking-changes

Rather than "here's a link to github try and figure it out for yourself"

@phillip-haydon well, I can certainly appreciate that. Sounds like this is frustration that is specific to the ASP.NET team. Yes - I'm at Microsoft, but neither I nor this repo are directly tied to the ASP.NET team. Occasionally, we interact a few times a year. I can't speak to how they manage their breaking changes, but I've put a callout on the repo landing page and I outline all the breaking changes, no matter how small, during each release. I also include release notes for every published package. I've done my best to keep the wiki up-to-date and relevant. The community has been keen to step in or speak up if I fall behind, so feel the documentation here is reasonably up-to-snuff. In terms of scale, I'm sure there's a lot more to handle and doc at the platform level. You might consider letting the ASP.NET team know they need to do a better job and outline what your expectations are.

I just see /microsoft/... and this exact issue spread across multiple repos, so I don't know who is ultimately responsible for this stuff, nor if this was a breaking change introduced by asp.net team or this repos team. It's just really frustrating.

:(

This is so (excuse me) incredibly stupid breaking change. I just literally spent whole day and half hunting this deep in decompiled ASP.NET Core MVC... and it is just a side note in upgrade guide, which I missed. It is even worse, that both Rider AND VS2019 suggest nameof(ActionMethod) in place of CreatedAtAction parameter. This, I am sorry, but I have to repeat it, stupid change, means, you either have to use strings in place of action name or you have to get rid of Async suffix. Former causes VS (and Rider) to highlight that action name in red as an error (unknown action) and the latter... well, breaks the coding style, as I keep all my Async code suffixed.

CC: @msftgits

@mx0r I can certainly appreciate the frustration. I haven't hit that particular issue myself, but if I did, it would have driven me bonkers. I've certainly had such experiences in the past. While I'm a steward of Microsoft, I'm not on the ASP.NET team nor do I have a tremendous amount of interaction with them. The best way to have your voice heard on this issue is to kvetch on the ASP.NET repo. I certainly pass along these customer frustrations when I can, but it's infrequent since I don't interact much with the team.

On a more personal note, I've really moved toward strictly using method/action names which map directly to HTTP. This has nothing to do with conventions. The API is HTTP. The methods of the API are the HTTP methods; that's technically the language. The C#/.NET part is simply a fa莽ade married on top of it. There's nothing wrong with that, but I've found that thinking in terms of HTTP leads me to truly RESTful designs and is a better reflection of what happens over the wire. Again - that's my style and preference. I realize it's a highly subjective and even hotly contested topic.

One of the things that I've done to deal with this type of issue is to use a custom .editorconfig policy. I frequently have this come up with test name conventions. I thought I'd be able to come up with a policy for you, but sadly it seems that the required functionality isn't supported - yet. (see: https://github.com/dotnet/roslyn/issues/18354). You can enforce that a method ends with the Async suffix, but not the inverse. That _may_ have been a reasonable compromise for you. If they ever support it, that would be my suggestion for dealing with this scenario. It doesn't address the initial breaking change, but at least it would provide a way to fix all of the naming issues and prevent them from happening again in the future.

@commonsensesoftware Hey, thank you for your reply! Yes, I understand your approach, I wanted to keep with CRUD naming (so CreateAsync, ReadAsync,...) but in this case, even GetAsync would not help, because of that Async suffix. In the end, I resorted to dropping the Async keyword to be able to use nameof(...), which has it's benefits with regards to code stability.

What I actually do not understand is, what prompted the change in the first place. Why not to keep all method names as they are, thus allowing ReadAsync AND Read to be present, because, if I for some reason had ReadAsync(..) and Read(..) actions both, it must result in ambiguity and thus error. Motivation behind the change is lost to me.

Also, it is a sneaky change and it is, in my opinion, so poorly documented, that even now, when I actually know, what I am searching for in migration documentation, I am unable to find it (I wanted to check the migration docs, whether there is some reason for the change given). This is a kind of thing, that should be directly in Routing documentation as a big warning, as it is magic, happening in the background and the only result for the developer is, that the route is not found.

So it is not as much as naming issue per se, it is more documentation issue.

Anyway, thank you once again for dropping in an providing some insights! Greatly appreciated!

This is has been closed for a while, but this issue cropped back up in a new issue and I just noticed something in the release notes that should be useful for anyone on this thread or in the future. According the release notes here, you can configure MVC like this:

c# services.AddMvc(options => { options.SuppressAsyncSuffixInActionNames = false; });

Which will result in things behaving the way they used to. The reason for the change was not arbitrary; it addresses a different bug. If you're not affected by said bug and want to continue using the Async suffix as you had been doing, this is how you re-enable it. If you are affected by the bug, then it would seem you have no choice, but to refactor; though I suspect you've already done so.

May you find this useful.

@commonsensesoftware
EDIT: After writing this, I went to try the AddControllers() options again and... the option is there. I am pretty sure it was not, last time I checked. Yet still, I will try to work with it now.

So please, disregard the rest for now.


services.AddMvc(options =>
{
   options.SuppressAsyncSuffixInActionNames = false;
});

Thank you, but I can not use AddMvc as I am trying to do UseEndpoints, I can only use AddControllers, if I am not mistaken. And the whole change from MVC to Endpoints was the main reason I started investigating it. I am surprised by the lack of similar option in Endpoints infrastructure.

Btw, is there somewhere the description of the bug, that was fixed by this Async removal? It is interesting to know, there is some bigger reason behind it and I would actually like to learn more.

Thanks!

Possibly a different issue but the same 500.
System.InvalidOperationException: No route matches the supplied values.
Following the documentation here:
https://docs.microsoft.com/en-us/aspnet/core/tutorials/first-mongo-app?view=aspnetcore-3.1&tabs=visual-studio

The GET is fine but the POST throws the error.
.net core 3.1
Mongo (4.2.1)

Can they reopen this issue?

@johnathan-s can you elaborate and/or provide additional details about your scenario. This repo is for API Versioning, and only API Versioning. This repo is not owned or run about the ASP.NET. If your question is not specifically about versioning the ASP.NET repo or StackOverflow is a better choice.

I presume you've tried all of the guidance on this thread to no avail?

Thank you. I have a thread going on the document page. (https://docs.microsoft.com/en-us/aspnet/core/tutorials/first-mongo-app?view=aspnetcore-3.1&tabs=visual-studio#feedbackm)
That thread may be more relevant to the issue at hand.
I will follow up on that thread.

I had a similar problem but it was fixed after I reduced the number of variables in the URI of Get<Item>ById controller.
Didn't work:

Departments/{departmentID}/Courses/{courseID}/Students/{studentID}

Worked:

Department1/Course1/Students/{studentID}

You didn't put the full route template, but I'm willing to bet that you are versioning by URL segment. The UrlHelper doesn't understand how to find this value because it's _special_. It comes from different places and can even come from multiple places in a single request.

If you include the API version, things should work just fine. For example:

[HttpPost]
public async Task Create(string id, ApiVersion apiVersion) =>
  CreatedAtRoute(nameof(GetById), new { id = 1, version = apiVersion.ToString() }, "test");

Note: Other methods of API versioning do not have this problem because they don't appear in the URL path.

perfect!

Was this page helpful?
0 / 5 - 0 ratings