Question

I'm creating a new webapi using attribute routing to create a nested route as so:

    // PUT: api/Channels/5/Messages
    [ResponseType(typeof(void))]
    [Route("api/channels/{id}/messages")]
    public async Task<IHttpActionResult> PostChannelMessage(int id, Message message)
    {
        if (!ModelState.IsValid)
        {
            return BadRequest(ModelState);
        }

        if (id != message.ChannelId)
        {
            return BadRequest();
        }

        db.Messages.Add(message);
        await db.SaveChangesAsync();

        return CreatedAtRoute("DefaultApi", new { id = message.Id }, message);
    }

I however want to return a route that isn't nested i.e.:

/api/Messages/{id}

which is defined on the messages controller. However The CreatedAtRoute call above is not resolving this route and instead throwing. Have I done something wrong, or does it not support routing to different api controller? n.b. the route I am trying to hit is not an attribute route, just a default one.

The exception is:

Message: "An error has occurred." ExceptionMessage: "UrlHelper.Link must not return null." ExceptionType: "System.InvalidOperationException" StackTrace: " at System.Web.Http.Results.CreatedAtRouteNegotiatedContentResult1.Execute() at System.Web.Http.Results.CreatedAtRouteNegotiatedContentResult1.ExecuteAsync(CancellationToken cancellationToken) at System.Web.Http.Controllers.ApiControllerActionInvoker.d__0.MoveNext() --- End of stack trace from previous location where exception was thrown --- at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) at System.Runtime.CompilerServices.TaskAwaiter1.GetResult() at System.Web.Http.Controllers.ActionFilterResult.<ExecuteAsync>d__2.MoveNext() --- End of stack trace from previous location where exception was thrown --- at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) at System.Runtime.CompilerServices.TaskAwaiter1.GetResult() at System.Web.Http.Dispatcher.HttpControllerDispatcher.d__0.MoveNext()"

If it doesn't support this, what is the canonical way to return a 201 and can I do it in a refactor safe way?

Was it helpful?

Solution

Oh dear, this may be a new record for answering my own question.

return CreatedAtRoute("DefaultApi", new { controller = "messages", id = message.Id }, message);

does the trick. i.e. explicitly specifying the controller. I worked this our by seeing that the exception was related to the UrlHelper and reading its docs...

OTHER TIPS

Late to the party but an alternative answer. If the action you are routing to also uses attribute routing, you can give the route a name and pass that in to the CreatedAtRoute method. This is done by setting a Name property on the Route. Following your post example, consider the following action.

// GET: api/Messages/5
[Route("api/messages/{id}", Name="GetMessage")]
public async Task<IHttpActionResult> GetMessage(int id)
{
    // get the message
}

Note that the Name property on the route attribute, [Route("api/messages/{id}", Name="GetMessage")], is set to "GetMessage". By doing this we can call the CreatedAtRoute method from the PostChannelMessage action and pass in the route name like so:

return CreatedAtRoute("GetMessage", new { id = message.Id }, message);

This is a scenario I encountered and my searching led here so thought I would post this alternative answer in case it helps anyone else.

Just adding to the answers above: on Attribute Routing:

I was caught out by the parameter name, took me an hour to realise that the parameter needs to named correctly otherwise the Url Helper will return null.

i.e if you have an action method like:

[Route("api/messages/{id}", Name="GetAction")]
public IHttpActionResult GetEntity(int mySpecialUniqueId)
{
    // do some work.
}

Then the return should be:

return CreatedAtRoute("GetAction", new { mySpecialUniqueId = entity.Id }, entity);

On the more simple examples, the Id property kept throwing me off so i thought I would expand on it more in this answer to help save others time on this little issue.

See this more complicated example for more detail:

Attribute Routing and CreatedAtRoute

For dotnet 5 the solution is a slightly diffrent:

The controller with the GET method in it should look like this:

[Route("api/[controller]")]
[ApiController]
public class ItemController : ControllerBase
{
    [HttpGet("{id}", Name = nameof(Get))]
    public ActionResult Get([FromRoute] int id)
    {
        // code here.
    }
}

And the CreatedAtAction method like this:

return CreatedAtAction(nameof(ItemController.Get), "Item", new { item.Id }, item);

It is possible to specify the route to the controller directly as the second parameter of the CreatedAtAction method.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top