I want to use CreatedAtAction
to return an HTTP 201 to the client with the correctLocation
header, but I'm doing it from a class and namespace different than the actual controller. This is because I'm usingNSwag to generate a controller from an OpenAPI 2 spec, which generates the actual controller and an interface to implement the logic for the controller in, which I inherit from. It is in this child class that
I'm trying to use CreatedAtAction
with nameof
to ensure that if I change the name of the action in the OpenAPI spec that it results in errors that the compiler can help track down.
The issue is that I can't seem to find the right combination of nameof
to use withCreatedAtAction
to get a 201 out of it; instead it errors with HTTP 500: "InvalidOperationException: No route matches the supplied values.
" I've looked around for this error and I haven't found anyone trying to useCreatedAtAction
with an action pointing to a different namespace and class; only within the same namespace. Usually this issue surfaces when the get action referenced doesn't exist, but mine does exist, just on a different class.
More concretely, I use an OpenAPI spec file with NSwag to generate something like the following code (omitting unrelated methods):
//----------------------
// <auto-generated>
// Generated using the NSwag toolchain v12.0.15.0 (NJsonSchema v9.13.22.0 (Newtonsoft.Json v11.0.0.0)) (http://NSwag.org)
// </auto-generated>
//----------------------
using Microsoft.AspNetCore.Mvc;
using System.Threading.Tasks;
namespace Project.Controllers.Generated
{
#pragma warning disable
[System.CodeDom.Compiler.GeneratedCode("NSwag", "12.0.15.0 (NJsonSchema v9.13.22.0 (Newtonsoft.Json v11.0.0.0))")]
public interface IThingController
{
Task<ActionResult> CreateParametersAsync(ThingParameters parameters);
Task<ActionResult<ThingParameters>> GetParametersAsync(int id);
}
[System.CodeDom.Compiler.GeneratedCode("NSwag", "12.0.15.0 (NJsonSchema v9.13.22.0 (Newtonsoft.Json v11.0.0.0))")]
[Microsoft.AspNetCore.Mvc.Route("api/thing/selection")]
public partial class ThingController : ControllerBase
{
private IThingController _implementation;
public ThingController(IThingController implementation)
{
_implementation = implementation;
}
[Microsoft.AspNetCore.Mvc.HttpPost, Microsoft.AspNetCore.Mvc.Route("parameters")]
public async Task<ActionResult> CreateParameters([Microsoft.AspNetCore.Mvc.FromBody] ThingParameters parameters)
{
return await _implementation.CreateParametersAsync(parameters);
}
[Microsoft.AspNetCore.Mvc.HttpGet, Microsoft.AspNetCore.Mvc.Route("parameters/{id}")]
public async Task<ActionResult<ThingParameters>> GetParameters(int id)
{
return await _implementation.GetParametersAsync(id);
}
}
#pragma warning restore
}
I then implement the interface (omitting all unrelated methods):
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Project.Controllers.Generated;
namespace Project.Controllers
{
public class ThingDataController : ControllerBase, IThingController
{
public async Task<ActionResult> CreateParametersAsync(Project.Controllers.Generated.ThingParameters parameters)
{
Model.ThingParameters newParameters = Convert(parameters);
if (!ValidlyOverlaps(repository.List(), newParameters))
{
return BadRequest();
}
int id = repository.Create(newParameters);
return CreatedAtAction(nameof(ThingController.GetParameters), nameof(ThingController), new { id }, parameters);
}
public async Task<ActionResult<Project.Controllers.Generated.ThingParameters>> GetParametersAsync(int id)
{
Model.ThingParameters settings = repository.Retrieve(id);
if (settings == null)
{
return NotFound();
}
return Ok(Convert(settings));
}
}
}
And I connect the implementation to the generated controller in Startup.ConfigureServices
viaservices.AddTransient<IThingController, ThingDataController>();
Stepping through the code under the debugger confirms that it gets to the CreatedAtAction line, so since there's no application logic there I'm guessing ASP.NET Core is barfing on the arguments I'm providing.
Visual Studio 2017 warns me that it can't resolve the controller or action in thenameof
expressions. I've tried fully qualifying the names but that gives the same error. I've also tried replacing thenameof
expressions with raw strings of both the controller name and the fully qualified controller name to see if that would work but it gives the same error. The full exception in the HTTP 500 response is:
System.InvalidOperationException: No route matches the supplied values.
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.Builder.RouterMiddleware.Invoke(HttpContext httpContext)
at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)
What is the correct thing to put in the nameof
arguments to the
CreatedAtAction
in this case?
I'm using ASP.NET Core 2.1.2 with NSwag.MSBuild 12.0.15, targeting Linux running .NET Core under Docker using the official ASP.NET Core base images.