【搬運(yùn)】ASP.NET Core Web API Attributes

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

本文介紹了可應(yīng)用于aspnet core 控制器中的特性隔躲,Route特性定義路由案淋,HTTP前綴的特性為Action的Http請(qǐng)求方法饲梭,F(xiàn)rom前綴的為Action參數(shù)的取值來源,在結(jié)尾最后又列出了在不同的Http請(qǐng)求方法中對(duì)取值來源的支持娃善。

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.
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末多柑,一起剝皮案震驚了整個(gè)濱河市奶是,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌竣灌,老刑警劉巖聂沙,帶你破解...
    沈念sama閱讀 219,110評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異初嘹,居然都是意外死亡及汉,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,443評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門屯烦,熙熙樓的掌柜王于貴愁眉苦臉地迎上來坷随,“玉大人,你說我怎么就攤上這事驻龟∥旅迹” “怎么了?”我有些...
    開封第一講書人閱讀 165,474評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵迅脐,是天一觀的道長芍殖。 經(jīng)常有香客問我,道長谴蔑,這世上最難降的妖魔是什么豌骏? 我笑而不...
    開封第一講書人閱讀 58,881評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮隐锭,結(jié)果婚禮上窃躲,老公的妹妹穿的比我還像新娘。我一直安慰自己钦睡,他們只是感情好蒂窒,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,902評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著荞怒,像睡著了一般洒琢。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上褐桌,一...
    開封第一講書人閱讀 51,698評(píng)論 1 305
  • 那天衰抑,我揣著相機(jī)與錄音,去河邊找鬼荧嵌。 笑死呛踊,一個(gè)胖子當(dāng)著我的面吹牛砾淌,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播谭网,決...
    沈念sama閱讀 40,418評(píng)論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼汪厨,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了愉择?” 一聲冷哼從身側(cè)響起劫乱,我...
    開封第一講書人閱讀 39,332評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎锥涕,沒想到半個(gè)月后要拂,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,796評(píng)論 1 316
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡站楚,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,968評(píng)論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了搏嗡。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片窿春。...
    茶點(diǎn)故事閱讀 40,110評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖采盒,靈堂內(nèi)的尸體忽然破棺而出旧乞,到底是詐尸還是另有隱情,我是刑警寧澤磅氨,帶...
    沈念sama閱讀 35,792評(píng)論 5 346
  • 正文 年R本政府宣布尺栖,位于F島的核電站,受9級(jí)特大地震影響烦租,放射性物質(zhì)發(fā)生泄漏延赌。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,455評(píng)論 3 331
  • 文/蒙蒙 一叉橱、第九天 我趴在偏房一處隱蔽的房頂上張望挫以。 院中可真熱鬧,春花似錦窃祝、人聲如沸掐松。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,003評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽大磺。三九已至,卻和暖如春探膊,著一層夾襖步出監(jiān)牢的瞬間杠愧,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,130評(píng)論 1 272
  • 我被黑心中介騙來泰國打工突想, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留殴蹄,地道東北人究抓。 一個(gè)月前我還...
    沈念sama閱讀 48,348評(píng)論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像袭灯,于是被迫代替她去往敵國和親刺下。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,047評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容