Created, CreatedAtAction, CreatedAtRoute Methods In ASP.NET Core Explained With Examples

In this post we will discuss Created, CreatedAtAction and CreatedAtRoute methods available in ASP.NET Core controllers and, in particular, answer such questions as what it is and why it is needed, how do these methods work and how to use them, how to pass parameters and create link for an action in a different controller, examples how to use different overloaded versions, common errors we might encounter and how to resolve them.

Contents:

What and Why

So, first of all, let us understand what these methods are and what they do.

Created, CreatedAtAction, CreatedAtRoute and their overloads are methods of ControllerBase class, they provide convenient ways to return 201 Created response from Web API that signifies a successful request completion. Response includes a Location header with an URI that can be used to retrieve newly created resource.

So, the key points about these methods are:

Why Do I Need Location Header?

It is a convention to include Location header in 201 Created HTTP response. It is a good practice to follow this convention, however, it is not strictly required because this is only a “SHOULD” in RFC 2616.

Another question is do we need to return 201 Created instead of simple 200 OK response. Short answer is - yes, we should return 201 response when a resource is created in case we follow REST architectural style. But this is another topic which is not discussed in this post.

Difference Between Created, CreatedAtAction and CreatedAtRoute

As a result, these methods and their overloads produce similar HTTP responses. However, there are some differences to consider:

Which method to use depends on the use case and your preference. Next, we will discuss each of them in detail.

Created Explained

This method is very straightforward and has only two overloaded versions. Detailed signatures of both methods can be found here.

Since Created method is very simple, it needs only these two pieces of information:

Basically, the main difference between them is how you pass the value of the location header - as string or Uri.

Important points about passing location header using these methods:

Created Examples

The following code snippets demonstrate sample usage of Created method overloads.

Created (string uri, object value)

[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
    [HttpGet("{id}")]
    public IActionResult GetValue(int id, string version)
    {
        var value = $"Value {id} of version {version}";
        return Ok(value);
    }

    [HttpPost]
    public IActionResult Create()
    {
        var createdResource = new { Id = 1, Version = "1.0" };
        string uri = $"https://www.example.com/api/values/{createdResource.Id}?version={createdResource.Version}";
        return Created(uri, createdResource);
        // Location: https://www.example.com/api/values/1?version=1.0
    }
}

Created (Uri uri, object value)

[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
    [HttpGet("{id}")]
    public IActionResult GetValue(int id, string version)
    {
        var value = $"Value {id} of version {version}";
        return Ok(value);
    }

    [HttpPost]
    public IActionResult Create()
    {
        var createdResource = new { Id = 1, Version = "1.0" };
        Uri uri = new Uri($"https://www.example.com/api/values/{createdResource.Id}?version={createdResource.Version}");
        return Created(uri, createdResource);
        // Location: https://www.example.com/api/values/1?version=1.0
    }
}

CreatedAtAction Explained

This method provides more support in generating URI for the Location header.

As the name suggests, this method allows us to set Location URI of the newly created resource by specifying the name of an action where we can retrieve our resource.

To achieve that, ASP.NET Core framework might need the following information depending on how and where you define your action:

Overloaded version are only different in the way they handle controllerName and routeValues parameters because:

Next, let us look at examples of how to use each of the overloaded methods.

CreatedAtAction Examples

Below are examples how we can use each of the CreatedAtAction method overloads.

Note: response Location header will contain your base URI instead of ..., for example, https://localhost:5001/api/Values/1?version=1.0 in case of local development.

CreatedAtAction (string actionName, object routeValues, object value)

[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
    [HttpGet("{id}")]
    public IActionResult GetValue(int id, string version)
    {
        var value = $"Value {id} of version {version}";
        return Ok(value);
    }

    [HttpPost]
    public IActionResult Create()
    {
        var createdResource = new { Id = 1, Version = "1.0" };
        var actionName = nameof(GetValue);
        var routeValues = new { id = createdResource.Id, version = createdResource.Version };
        return CreatedAtAction(actionName, routeValues, createdResource);
        // Location: .../api/Values/1?version=1.0
    }
}

CreatedAtAction (string actionName, string controllerName, object routeValues, object value)

[Route("api/[controller]")]
[ApiController]
public class ValuesV2Controller : ControllerBase
{
    [HttpGet("{id}")]
    public IActionResult GetValue(int id, string version)
    {
        var value = $"Value {id} of version {version}";
        return Ok(value);
    }
}


[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
    [HttpGet]
    public IActionResult GetValue()
    {
        var value = "Value";
        return Ok(value);
    }

    [HttpPost]
    public IActionResult Create()
    {
        var createdResource = new { Id = 1, Version = "1.0" };
        var actionName = nameof(ValuesV2Controller.GetValue);
        var controllerName = "ValuesV2";
        var routeValues = new { id = createdResource.Id, version = createdResource.Version };
        return CreatedAtAction(actionName, controllerName, routeValues, createdResource);
        // Location: .../api/ValuesV2/1?version=1.0
    }
}

CreatedAtAction (string actionName, object value)

[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
    [HttpGet]
    [ActionName("RetrieveValue")]
    public IActionResult GetValue()
    {
        var value = "Value";
        return Ok(value);
    }

    [HttpPost]
    public IActionResult Create()
    {
        var createdResource = new { Id = 1, Version = "1.0" };
        var actionName = "RetrieveValue";
        return CreatedAtAction(actionName, createdResource);
        // Location: .../api/Values
    }
}

CreatedAtRoute Explained

In my opinion, this method is more interesting and a bit harder to understand than others, but at the end it’s just an another way to specify location header in a 201 Created response.

In this case framework might need the following:

Usually, the main source of confusion here is routeName parameter since we have to set it by ourselves. In the examples we’ll see how to specify a name for a route.

Note: there is an interesting and often less discussed overload CreatedAtRoute (object routeValues, object value) which does not accept routeName, it is covered here.

CreatedAtRoute Examples

Note: response Location header will contain your base URI instead of ..., for example, https://localhost:5001/api/Values/1?version=1.0 in case of local development.

CreatedAtRoute (string routeName, object routeValues, object value)

[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
    [HttpGet("{id}", Name = "NameForGetValueEndpoint")]
    public IActionResult GetValue(int id, string version)
    {
        var value = $"Value {id} of version {version}";
        return Ok(value);
    }

    [HttpPost]
    public IActionResult Create()
    {
        var createdResource = new { Id = 1, Version = "1.0" };
        var routeValues = new { id = createdResource.Id, version = createdResource.Version };
        return CreatedAtRoute("NameForGetValueEndpoint", routeValues, createdResource);
        // Location: .../api/Values/1?version=1.0
    }
}

CreatedAtRoute (string routeName, object value)

[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
    [HttpGet(Name = "NameForGetValueEndpoint")]
    public IActionResult GetValue()
    {
        var value = "Value";
        return Ok(value);
    }

    [HttpPost]
    public IActionResult Create()
    {
        var createdResource = new { Id = 1, Version = "1.0" };
        return CreatedAtRoute("NameForGetValueEndpoint", createdResource);
        // Location: .../api/Values
    }
}

CreatedAtRoute (object routeValues, object value)

I think this overload is less commonly used (I can be wrong) and that’s why deserves some comments.

Example 1

By default, if no information about target route is passed, it will take the path of the current method and use it for creating Location header.

In the following example it takes the route of POST method and attaches all route values as query parameters. More about routeValues read in the next section.

[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
    [HttpGet]
    public IActionResult GetValue(int id, string version)
    {
        var value = $"Value {id} of version {version}";
        return Ok(value);
    }

    [HttpPost]
    public IActionResult Create()
    {
        var createdResource = new { Id = 1, Version = "1.0" };
        var routeValues = new { id = createdResource.Id, version = createdResource.Version };
        return CreatedAtRoute(routeValues, createdResource);
        // Location: .../api/Values?id=1&version=1.0
    }
}


Example 2

You can specify action and controller inside of routeValues and, as a result, get behavior similar to the one of CreatedAtAction method. As we discussed before, we can omit controller if both methods are inside the same controller.

[Route("api/[controller]")]
[ApiController]
public class ValuesV2Controller : ControllerBase
{
    [HttpGet("{id}")]
    public IActionResult GetValue(int id, string version)
    {
        var value = $"Value {id} of version {version}";
        return Ok(value);
    }
}

[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
    [HttpPost]
    public IActionResult Create()
    {
        var createdResource = new { Id = 1, Version = "1.0" };
        var routeValues = new
        {
            action = nameof(ValuesV2Controller.GetValue),
            controller = "ValuesV2",
            id = createdResource.Id,
            version = createdResource.Version
        };
        return CreatedAtRoute(routeValues, createdResource);
        // Location: .../api/ValuesV2/1?version=1.0
    }
}

About object routeValues parameter

In many overloaded versions of CreatedAtRoute and CreatedAtAction we can pass routeValues parameter. Let’s understand what it is and how to use it.

Route values are used by ASP.NET Core framework to generate the correct URI. Here are the values that can be passed:

Type of routeValues parameter

Most often, an anonymous type is used, it is clean and easy to use.

However, since the accepting type is object, we can pass any type. If the value is IEnumerable of KeyValuePair, for example, Dictionary, then its entries will be copied. Otherwise the object is interpreted as a set of key-value pairs where (property name, property value) is a key-value pair.

CreatedAtRoute vs CreatedAtAction

Now we know a lot about these two methods and, therefore, can discuss them together. Here are some key points to consider:

As always, which one to use depends on the use case. In general, CreatedAtRoute gives more options and includes functionality of CreatedAtAction. However, in some cases CreatedAtAction is more convenient, for example, when handling actions inside of the same controller.

Common Errors and Troubleshooting

If you encounter errors, for example, No route matches the supplied values or Cannot resolve action, you might want to check the following things: