Reputation: 4559
I have a simple ASP.NET 5 Web API with a controller for uploading/downloading files, like:
[Authorize(Roles = "admin,browser,writer")]
[HttpPost("api/contents/{id}")]
public IActionResult UploadContent(IFormFile file,
[FromForm] string mimeType,
[FromForm] string id)
{
// store...
return CreatedAtAction(nameof(DownloadContent), new
{
id = id
});
}
[Authorize]
[HttpGet("api/contents/{id}")]
public FileResult DownloadContent([FromRoute] string id)
{
// fetch into item...
return File(item.Content, item.MimeType);
}
The upload action works fine, until the method returns: at this point, when the framework handles the serialization for the 201 return object (which should set its location to the corresponding download action) I get an InvalidOperationException
telling that No route matches the supplied values
.
I found this issue about a similar problem, but none of the mentioned solutions seem to work. I tried:
[ActionName("DownloadContent")]
, even though this should not make any difference; this appears to be relevant only when the action name ends with Async
(see this post, and the official issue).CreatedAtRoute
instead, by adding the corresponding Name="DownloadContent"
in the HttpGet attribute of DownloadContent
.CreatedAtActionResult
like:return new CreatedAtActionResult(nameof(DownloadContent),
nameof(ItemContentController), new { id = id }, null);
CreatedResult
object:string url = Url.Action(new UrlActionContext
{
Protocol = Request.Scheme,
Host = Request.Host.Value,
Action = nameof(DownloadContent)
});
return new CreatedResult(url, null);
Now, that's the point: the above code returns null
for the url
, and this then makes CreatedResult
throw as the url
for the 201 header's Location
is null. If I change this code by selecting a different action, e.g. Action = nameof(UploadContent)
, it works, and I get the URL for the action.
For completeness, my Startup.cs
Configure
method is like this:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseForwardedHeaders(new ForwardedHeadersOptions
{
ForwardedHeaders = ForwardedHeaders.XForwardedFor
| ForwardedHeaders.XForwardedProto
});
if (env.IsDevelopment()) app.UseDeveloperExceptionPage();
else
{
app.UseExceptionHandler("/Error");
if (Configuration.GetValue<bool>("Server:UseHSTS")) app.UseHsts();
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseCors("CorsPolicy");
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
// ... Swagger stuff ...
}
Upvotes: 0
Views: 1420
Reputation: 11339
When using overload CreatedAtAction(string, object)
, param object
is not route parameters as you assume, but body content. Hence, code is looking for a route to GET api/contents which doesn't exists.
You'll need to use overload CreatedAtAction(string, object, object)
where param #2 contains route params, and param #3 contains body content. So in your case, return CreatedAtAction(nameof(DownloadContent), new { id = id }, null);
will work.
I was able to reproduce your issue with this controller and Postman:
public class MyContentController : ControllerBase
{
[HttpGet("api/contents/{id}")]
public IActionResult DownloadContent(int id)
{
return Ok(1);
}
[HttpPost("api/contents/{id}")]
public IActionResult UploadContent(int id)
{
return CreatedAtAction(nameof(DownloadContent), new { id = id });
}
}
I got expected response when replacing
return CreatedAtAction(nameof(DownloadContent), new { id = id });
with
return CreatedAtAction(nameof(DownloadContent), new { id = id }, null);
Upvotes: 1