【搬运】ASP.NET Core Web API Attributes

原文地址:http://www.dotnetcurry.com/aspnet/1390/aspnet-core-web-api-attributes

本文介绍了可应用于aspnet core 控制器中的特性,Route特性定义路由,HTTP前缀的特性为Action的Http请求方法,From前缀的为Action参数的取值来源,在结尾最后又列出了在不同的Http请求方法中对取值来源的支持。

With ASP.NET Core there are various attributes that instruct the framework where to expect data as part of an HTTP request - whether the body, header, query-string, etc.

With C#, attributes make decorating API endpoints expressive, readable, declarative and simple. These attributes are very powerful! They allow aliasing, route-templating and strong-typing through data binding; however, knowing which are best suited for each HTTP verb, is vital.

In this article, we'll explore how to correctly define APIs and the routes they signify. Furthermore, we will learn about these framework attributes.

As a precursor to this article, one is expected to be familiar with modern C#, REST, Web API and HTTP.

ASP.NET Core HTTP attributes

ASP.NET Core has HTTP attributes for seven of the eight HTTP verbs listed in the Internet Engineering Task Force (IETF) RFC-7231 Document. The HTTP TRACE verb is the only exclusion in the framework. Below lists the HTTP verb attributes that are provided:

  • HttpGetAttribute
  • HttpPostAttribute
  • HttpPutAttribute
  • HttpDeleteAttribute
  • HttpHeadAttribute
  • HttpPatchAttribute
  • HttpOptionsAttribute

Likewise, the framework also provides a RouteAttribute. This will be detailed shortly.

In addition to the HTTP verb attributes, we will discuss the action signature level attributes. Where the HTTP verb attributes are applied to the action, these attributes are used within the parameter list. The list of attributes we will discuss are listed below:

  • FromServicesAttribute
  • FromRouteAttribute
  • FromQueryAttribute
  • FromBodyAttribute
  • FromFormAttribute

Ordering System

Imagine if you will, that we are building out an ordering system. We have an order model that represents an order. We need to create a RESTful Web API that allows consumers to create, read, update and delete orders – this is commonly referred to as CRUD.

Route Attribute

ASP.NET Core provides a powerful Route attribute. This can be used to define a top-level route at the controller class – doing so leaves a common route that actions can expand upon. For example consider the following:

[Route("api/[Controller]")]
public class OrdersController : Controller
{
    [HttpGet("{id}")]
    public Task<Order> Get([FromRoute] int id) 
        => _orderService.GetOrderAsync(id);
}

The Route attribute is given a template of "api/[Controller]". The "[Controller]" is a special naming convention that acts as a placeholder for the controller in context, i.e.; "Orders". Focusing our attention on the HttpGet we can see that we are providing a template argument of "{id}". This will make the HTTP Get route resemble "api/orders/1" – where the id is a variable.

HTTP GET Request

Let us consider an HTTP GET request.

In our collection of orders, each order has a unique identifier. We can walk up to the collection and ask for it by "id". Typical with RESTful best practices, this can be retrieved via its route, for example "api/orders/1". The action that handles this request could be written as such:

[HttpGet("api/orders/{id}")] //api/orders/7
public Task<Order> Get([FromRoute] int id,[FromServices] IOrderService orderService)
      => orderService.GetOrderAsync(id);

Note how easy it was to author an endpoint, we simply decorate the controller’s action with an HttpGet attribute.

This attribute will instruct the ASP.NET Core framework to treat this action as a handler of the HTTP GET verb and handle the routing. We supply an endpoint template as an argument to the attribute. The template serves as the route the framework will use to match on for incoming requests. Within this template, the {id} value corresponds to the portion of the route that is the "id" parameter.



This is a Task<Order> returning method, implying that the body of the method will represent an asynchronous operation that eventually yields an Order object once awaited. The method has two arguments, both of which leverage attributes.

First the FromRoute attribute tells the framework to look in the route (URL) for an "id" value and provide that as the id argument. Then the FromServices attribute – this resolves our IOrderService implementation. This attribute asks our dependency injection container for the corresponding implementation of the IOrderService. The implementation is provided as the orderService argument.

We then expressively define our intended method body as the order services’ GetOrderAsync function and pass to it the corresponding identifier.

We could have just as easily authored this to utilize the FromQuery attribute instead. This would then instruct the framework to anticipate a query-string with a name of "identifier" and corresponding integer value. The value is then passed into the action as the id parameters argument. Everything else is the same.

However, the most common approach is the aforementioned FromRoute usage – where the identifier is part of the URI.

[HttpGet("api/orders")] //api/orders?identifier=7
public Task<Order> Get([FromQuery(Name = "identifier")] int id,[FromServices] IOrderService orderService)
    => orderService.GetOrderAsync(id);

Notice how easy it is to alias the parameter?

We simply assign the Name property equal to the string "identifier" of the FromQuery attribute. This instructs the framework to look for a name that matches that in the query-string. If we were to omit this argument, then the name is assumed to be the name used as the actions parameter, "id". In other words, if we have a URL as "api/orders?id=17" the framework will not assign our “id” variable the number 17 as it is explicitly looking for a query-string with a name "identifier".

HTTP POST Request

Continuing with our ordering system, we will need to expose some functionality for consumers of our API to create orders.

Enter the HTTP POST request.

The syntax for writing this is seemingly identical to the aforementioned HTTP GET endpoints we just worked on. But rather than returning a resource, we will utilize an IActionResult. This interface has a large set of subclasses within the framework that are accessible via the Controller class. Since we inherit from Controller, we can leverage some of the conveniences exposed such as the StatusCode method.

With an HTTP GET, the request is for a resource; whereas an HTTP POST is a request to create a resource and the corresponding response is the status result of the POST request.

[HttpPost("api/orders")]
public async Task<IActionResult> Post([FromBody] Order order)
    => (await _orderService.CreateOrderAsync(order))
        ? (IActionResult)Created($"api/orders/{order.Id}", order) // HTTP 201
        : StatusCode(500); // HTTP 500

We use the HttpPost attribute, providing the template argument.

This time we do not need an "{id}" in our template as we are being given the entire order object via the body of the HTTP POST request. Additionally, we will need to use the async keyword to enable the use of the await keyword within the method body.

We have a Task<IActionResult> that represents our asynchronous operation. The order parameter is decorated with the [FromBody] attribute. This attribute instructs the framework to pick the order out from the body of the HTTP POST request, deserialize it into our strongly-typed C# Order class object and provide it as the argument to this action.

The method body is an expression. Instead of asking for our order service to be provided via the FromServices attribute like we have demonstrated in our HTTP GET actions, we have a class-scope instance we can use. It is typically favorable to use constructor injection and assign a class-scope instance variable to avoid redundancies.

We delegate the create operation to the order services' invocation of CreateOrderAsync, giving it the order. The service returns a bool indicating success or failure. If the call is successful, we'll return an HTTP status code of 201, Created. If the call fails, we will return an HTTP status code of 500, Internal Server Error.

Instead of using the FromBody one could just as easily use the FromForm attribute to decorate our order parameter. This would treat the HTTP POST request differently in that our order argument no longer comes from the body, but everything else would stay the same. The other attributes are not really applicable with an HTTP POST and you should avoid trying to use them.

[HttpPost("api/orders")]
public async Task<IActionResult> Post([FromForm] Order order)
    => (await _orderService.CreateOrderAsync(order))
        ? Ok() // HTTP 200
        : StatusCode(500);

Although this bends from HTTP conformity, it's not uncommon to see APIs that return an HTTP status code 200, Ok on success. I do not condone it.

By convention if a new resource is created, in this case an order, you should return a 201. If the server is unable to create the resource immediately, you could return a 202, accepted. The base controller class exposes the Ok(), Created() and Accepted() methods as a convenience to the developer.

HTTP PUT Request

Now that we're able to create and read orders, we will need to be able to update them.

The HTTP PUT verb is intended to be idempotent. This means that if an HTTP PUT request occurs, any subsequent HTTP PUT request with the same payload would result in the same response. In other words, multiple identical HTTP PUT requests are harmless and the resource is only impacted on the first request.

The HTTP PUT verb is very similar to the HTTP POST verb in that the ASP.NET Core attributes that pair together, are the same. Again, we will either leverage the FromBody or FromForm attributes. Consider the following:

[HttpPut("api/orders/{id}")]
public async Task<IActionResult> Put([FromRoute] int id, [FromBody] Order order)
    => (await _orderService.UpdateOrderAsync(id, order))
        ? Ok()
        : StatusCode(500);

We start with the HttpPut attribute supply a template that is actually identical to the HTTP GET. As you will notice we are taking on the {id} for the order that is being updated. The FromRoute attribute provides the id argument.

The FromBody attribute is what will deserialize the HTTP PUT request body as our C# Order instance into the order parameter. We express our operation as the invocation to the order services’ UpdateOrderAsync function, passing along the id and order. Finally, based on whether we are able to successfully update the order – we return either an HTTP status code of 200 or 500 for failures to update.

The return HTTP status code of 301, Moved Permanently should also be a consideration. If we were to add some additional logic to our underlying order service – we could check the given “id” against the order attempting to be updated. If the “id” doesn't correspond to the give order, it might be applicable to return a RedirectPermanent - 301 passing in the new URL for where the order can be found.

HTTP DELETE Request

The last operation on our agenda is the delete operation and this is exposed via an action that handles the HTTP DELETE request.

There is a lot of debate about whether an HTTP DELETE should be idempotent or not. I lean towards it not being idempotent as the initial request actually deletes the resource and subsequent requests would actually return an HTTP status code of 204, No Content.

From the perspective of the route template, we look to REST for inspiration and follow its suggested patterns.

The HTTP DELETE verb is similar to the HTTP GET in that we will use the {id} as part of the route and invoke the delete call on the collection of orders. This will delete the order for the given id.

[HttpDelete("api/orders/{id}")]
public async Task<IActionResult> Delete([FromRoute] int id) 
    => (await _orderService.DeleteOrderAsync(id)) 
        ? (IActionResult)Ok() 
        : NoContent();

While it is true that using the FromQuery with an HTTP DELETE request is possible, it is unconventional and ill-advised. It is best to stick with the FromRoute attribute.

Conclusion:

The ASP.NET Core framework makes authoring RESTful Web APIs simple and expressive. The power of the attributes allow your C# code to be decorated in a manner consistent with declarative programming paradigms. The controller actions' are self-documenting and constraints are easily legible. As a C# developer – reading an action is rather straight-forward and the code itself is elegant.

In conclusion and in accordance with RESTful best practices, the following table depicts which ASP.NET Core attributes complement each other the best.

  1. The FromQuery attribute can be used to take an identifier that is used as a HTTP DELETE request argument, but it is not as simple as leveraging the FromRoute attribute instead.
  2. The FromHeader attribute can be used as an additional parameter as part of an HTTP GET request, however it is not very common – instead use FromRoute or FromQuery.
    This article was technically reviewed by Daniel Jimenez Garcia.
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349

推荐阅读更多精彩内容