原文地址: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.
- 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.
- 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.