如何在ASP.NET Core API中使用Created(或CreatedAtAction / CreatedAtRoute)

58

我想告知我的API用户新创建对象的位置。 我知道有Created() CreatedAtRoute()CreatedAtAction(),但我不确定如何使用它。

这是我尝试过的:

我有一个Get资源,我想指向它。 它以ID作为输入:

    [HttpGet("/${id}", Name = "GetProduct")]
    [ProducesResponseType(typeof(Produkt), 200)]
    public IActionResult Get([FromRoute] int id)
    {
       // some code...
        return Ok(...);
    }

当通过我的POST路由创建产品时,我希望通过Location标头指向此资源。

尝试1

    [HttpPost]
    [ProducesResponseType(typeof(Produkt), 200)]
    public IActionResult CreateNewProduct([FromBody] ProduktDtoForCreate productFromBody)
    {
        //...
        return CreatedAtRoute("GetProduct", new { id = productToCreate.Id }, productToCreate);
    }

这将返回一个位置标头http://localhost:5000/$15003

尝试2

    [HttpPost]
    [ProducesResponseType(typeof(Produkt), 200)]
    public IActionResult CreateNewProduct([FromBody] ProduktDtoForCreate productFromBody)
    {
        //...
        return Created(new Uri($"{Request.Path}/{productToCreate.Id}", UriKind.Relative), productToCreate);
    }

这个可以工作并返回/api/v1.0/produkte/16004,但是似乎不需要使用当前请求来指向新的位置。另外我不确定这是否是一个好的做法?


尝试2是一个很好的解决方案。明智之举。 - ᴍᴀᴛᴛ ʙᴀᴋᴇʀ
3个回答

42

CreatedAtAction 在我看来提供了最佳输出。以下控制器代码将完成您所需的操作:


[Route("api/products")]
[ApiController]
public class ProductsController : ControllerBase
{
    private readonly IProductRepository productRepository;

    public ProductsController(IProductRepository productRepository)
    {
        this.productRepository = productRepository;
    }

    [HttpPost]
    [Route("")]
    [ProducesResponseType(StatusCodes.Status201Created)]
    public ActionResult<Product> CreateProduct(ProductCreateDto product)
    {
        if (product is null)
            return BadRequest(new ArgumentNullException());

        var entity = productRepository.CreateProduct(product);

        return CreatedAtAction(nameof(GetProduct), new { id = entity.ID }, entity);
    }

    [HttpGet]
    [Route("{id}")]
    public ActionResult<Product> GetProduct(int id)
    {
        return productRepository.GetProduct(id);
    }
}

发出以下请求:

POST http://localhost:5000/api/products HTTP/1.1 
Host: localhost:5000
Connection: keep-alive 
Content-Length: 25 
Content-Type: application/json

{ "name": "ACME Widget" }

将产生以下响应:

HTTP/1.1 201 Created
Date: Mon, 12 Oct 2020 09:50:00 GMT
Content-Type: application/json; charset=utf-8
Server: Kestrel
Content-Length: 29
Location: http://localhost:5000/api/products/1

{"id":1,"name":"ACME Widget"}

这不是对同一实体的不必要的第二次调用吗?有没有办法避免这种情况,并直接从POST控制器返回Created?在我的情况下,我不需要/ {id} 来检索个人。 - jens
1
POST请求在请求体中包含要创建的实体,返回状态码201,并在响应体中包含新创建的实体。这不是第二次调用。有人认为响应体是多余的,因为你刚刚提交了它 - 然而,如上面的示例所示,创建操作为实体添加了额外的细节(在本例中为ID,但也可能包括日期、创建者等),因此这很有用。这也符合这里描述的行为:https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/201 - phil_rawlings
如果API不支持按ID获取资源的GET方法,那么CreatedAtAction应该链接到哪里? - user3625699

7
在Get方法的路由中,去掉前导的"/"和"$"符号(即应该只剩下"{id}")。有前导的"/"意味着路由相对于应用程序的基础部分;将其移除后,该方法的路由就变成了相对于控制器的基础路径。"$"被视为路由中的一个字面字符,因此它出现在Attempt 1中的Location头部中。你需要做出这些更改,并且你会发现你的CreatedAtRoute调用会按照你的期望工作。

5
我还要补充一点,你的CreateNewProduct方法的ProducesResponseType注释应该指定201返回代码,而不是200。 - Graham Watts
谢谢Graham!我太专注于CreatedAt方法,以至于在我的Get方法中犯了一个愚蠢的错误 :) 祝你2018年过得愉快! - Riscie

3

引用RFC 7231

201(已创建)状态码表示请求已被满足,并导致创建了一个或多个新资源。 请求创建的主要资源由响应中的“位置”头字段标识,如果未收到“位置”字段,则由有效的请求URI标识。

标识资源的方式取决于上下文。 在我看来,如果创建的资源驻留在<request_uri>/<id>,则标识符可以只是<id>


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接