Building a canonical REST API with ASP. HATEOAS

Source: Internet
Author: User

Some of the preparatory knowledge required in this article can be seen here: http://www.cnblogs.com/cgzl/p/9010978.html and http://www.cnblogs.com/cgzl/p/9019314.html

To establish a Richardson maturity Level 2 post, GET, PUT, PATCH, delete restful API see here: Https://www.cnblogs.com/cgzl/p/9047626.html and https:/ /www.cnblogs.com/cgzl/p/9080960.html and Https://www.cnblogs.com/cgzl/p/9117448.html

This article will start the Web API project to a height of Richardson maturity Level 3, although it has not yet implemented all of the rest constraints, but has been more restful.

Code required in this article (right-click Save, suffix to zip): https://images2018.cnblogs.com/blog/986268/201806/986268-20180608085054518-398664058.jpg

HATEOAS (Hypermedia as the engine of application state) is the most complex constraint in the rest architecture style and is at the heart of building a mature rest service. It is important to break down the strict contract between the client and the server, which makes the client more intelligent and adaptive, and the REST service itself becomes easier to evolve and update.

The advantages of Hateoas are:

To be evolutionary and self-descriptive

hypermedia (hypermedia, for example, hyperlinks) drives how the API is consumed and used, and it tells the client how to use the API, how to interact with the API, such as how to delete resources, update resources, create resources, how to access the next page of resources, and so on.

For example, here is an example of a response that does not use Hateoas:

{    "id": 1,    "body": "My First blog post",    "postdate": "2015-05-30t21:41:12.650z"?}

If you do not use Hateoas, you may have these problems:

    • Clients need to know more about API intrinsic logic
    • If the API changes a little bit (adding additional rules, changing the rules) will break the API's consumer.
    • The API cannot evolve independently of the applications that consume it.

If you use Hateoas:

{    "id": 1,    "body": "My First blog post",    "postdate": "2015-05-30t21:41:12.650z",    "links": [        {
   
     "rel": "Self",            "href": Http://blog.example.com/posts/{id},            "method": "GET"        },
    
{
       "rel": "Update-blog",
"href": Http://blog.example.com/posts/{id},
       "Method" "PUT"
}
.... ] }

The response contains several links, and the first link contains a link to get the current response, and the second link tells the client how to update the post.

Roy Fielding's famous quote: " if clients embed their controls in the design when they are deployed, they will not be able to gain the evolutionary nature of the controls that must be discovered in real time." That's what hypermedia can do."

For the example above, I can add another delete function without changing the response body result, and the client will find the deletion function through the links in the response, but it has no effect on the other parts.

The HTTP protocol is still very supportive of Hateoas:

If you think about it, this is how we usually surf the web. When browsing the website, we do not care about whether the hyperlink address in the webpage changes, as long as you know what the hyperlink is.

We can click the hyperlink to jump or submit the form, which is an example of the Hypermedia-driven application (browser) state.

If the server decides to change the address of the hyperlink, the client program (browser) does not fail because of this change, which tells the browser to use the hypermedia response to tell us what to do next.

So how do we show these link?

JSON and XML do not show the concept of link. But HTML does know that the anchor element:

<a href= "uri" rel= "type"  type= "media type" >

The href contains the URI

rel describes how link is related to resources

type is optional, which represents the type of media

In order to support Hateoas, these forms are useful:

{    ...    " Links ": [        {            " rel ":" Self ",            " href ": Http://blog.example.com/posts/{id},            " method ":" GET "        }< c14/> ....    ] }

Method: Defines the methods that need to be used

rel: Indicates the type of action

href: Contains the URI that is included in the execution of this action.

In order for the ASP. NET Core Web API to support Hateoas, you need to write your own code implementation manually. There are two ways to do this:

static type scheme : Requires base class (including link) and wrapper class, that is, the returned resource contains link, by inheriting from the same base class to implement.

Dynamic Type Scenarios : You might use ExpandoObject for a single resource, such as anonymous classes or ExpandoObject, and anonymous classes for collection class resources.

Wrapping classes with static base classes

First, create a linkresource that represents the link:

Then create an abstract parent class Linkresourcebase:

It has only one attribute links.

Then I let cityresource inherit from Linkresourcebase:

Finally, in the controller, we need to write code to create the links that the above concept mentions for the resource. There is also a need for urlhelper, which needs to be injected into the controller.

Since I want to create many routing-based link addresses for resource, I need to name the route for the relevant action:

Then create a method inside the controller that will add the required links for Cityresource and return the processed cityresource.

First add to the resource is its own link , here use Urlhelper and route name and Cityid as parameters can get href, don't need to pass Countryid? Because the controller's routing address already contains the Countryid parameter, Urlhelper will automatically handle the problem, and the value of rel can be self-filled, and here I use to express itself, API consumers need to know this part, through the value of rel, The API consumer will know what functionality the API provides, and the value of the last method is get.

Several other links are similar. You can add additional links as needed, but for this simple example of this article, these links will suffice.

The next thing to do is to make sure that whenever Cityresource is returned by action, the method is executed to create the associated link .

First consider the case of returning a single city, GET:

The same is true for post:

There is also a getcitiesforcountry this method, which returns a collection of resources, so I need to iterate through the collection and invoke the method on each resource:

You just need to use the Select method, which is itself a traversal.

The test, first of all, is get single city:

It looks OK, and then it's good to test the relevant operation with the link inside, I don't have the stickers.

Test post below:

The results are OK, the links are easy to use.

Finally look at the get of the collection:

Looks good, every resource in the collection has the right link. However , there is no link to the entire collection in the results . Nor can we directly change the result to look like this :

{     value: [City1, City2 ...]     Links: [Link1, Link2 ...]    }

Because this is an unreasonable JSON result, it is not the type of resource being requested.

For the time being, in order to support the collection of Hateoas, we need a wrapper class:

This class can be thought of as a special set for a particular type, which inherits from Linkresourcebase, has a linked property, and also guarantees that the type of T is also linkresourcebase, which guarantees that the elements in the returned collection also have the Links property This class has only one value property and the type is ienumerable<t>.

Go back to the controller and create a method called Createlinksforcities:

Note that both the parameter and the return type are linkcollectionresourcewrapper.

Finally, the method is called in the Get Action method:

Test:

The result is yes, and now for Cityresource it is almost a support for Hateoas.

Using dynamic types

The dynamic and anonymous types are used here.

Now the Get method inside the Countrycontroller returns the Ienumerable<expandoobject>, which is the countryresource after the shape:

I can't inherit this object from some kind of parent class to add the Links property. So in this case, the way to use anonymous classes is required.

This is also a single resource and a collection of resources in two cases.

A single resource

First, add a good name for the route:

Since expandoobject cannot inherit my defined parent class, I have to set up a method to return to links:

Due to the existence of the data shape, the parameters should be added fields. The first few links are well understood to be country resources related links, and then two resources are country resources of the sub-resources city, respectively, for country to create city and get country under the cities.

This method shows that we are already in the state of driving the application. This is also the highlight of Hateoas.

You can then add these links to the body of the response. The first is the Get method:

Return to links, add a links property for ExpandoObject, and return.

Test:

Ok. Then we add a few data shaping parameters:

Still OK, the href in Self's link also carries these parameters.

Then there is the method of post action:

Same as GET, except post does not require data shaping. Note The ID of the second parameter inside the returned Createdatroute, which I took out of the Linkedcountryresource, not the Countrymodel ID, which might be better, Because this ID should be inside the linkedcountryresource.

Test:

The result is OK.

Collection Resources

We did a page-turn on getcountries, and put the page's meta-data in the header of the response, with a link to the previous and next pages:

In fact, these two links in the Links collection is better, so the following method will add the previous page and the next page of the link:

This uses the previously created Createcountryuri method, which returns self and the previous page and the next page, respectively.

Finally, it is called in the Getcountries method:

The first two links in the metadata are removed.

The links are then created for the collection, then the data is shaped for the collection and the links are added to each object in the collection. Finally, it returns an anonymous class that contains the value and links.

Test:

The results are returned correctly.

Here's a test of the parameters:

The result should be OK, but the case seems to have some problems, this I directly in the source code to change it.

Here are two methods, in fact, in the project according to the situation or use a better.

Media Type

For the result of the response, its descriptive data or metadata should be placed in the header. For example, before the page, the total number of pages, the current number of pages and other data are placed in the header, and the next and previous page of the link is placed in the body of the response. Should these two links be part of the resource? Or are they describing the resources (metadata)? Other links also exist for this issue. If it is metadata, then it should be placed in the header, if it is part of the resource, it can be placed in the body of the response. Now the case is that the previous and previous formulations are different representations of the same resource . But to the present we request the Accept header is Application/json, namely wants the resource JSON expression, but returns is not the country resource expression, but is another kind of thing, It also has the links property based on the JSON representation of the country resource, so if we are asking for Application/json, then links should not be part of the resource.

What is actually returned now is another media type rather than a application/json, so that we undermine the self-descriptive nature of the resource constraint ( each message should contain enough information to let other things know how to handle the message ). So the type of content-type that we return is wrong, and it also causes the API consumer to not parse the response correctly from the Content-type type, which means I don't tell the API consumer how to handle the result. The solution, then, is to create a new media type.

Vendor-specific Media type Vendor-specific medium types

Its structure is broadly as follows:

application/Vnd.mycompany.hateoas+json

The first part of VND is the abbreviation for vendor, which is the principle of MIME type, which indicates that the media type is vendor-specific.

Next is the custom identity , which may also include additional values, where I use the company name, followed by Hateoas, which indicates that the returned response contains a link.

Finally, there is a "+json".

The entire media type indicates that the resource representation I need is in JSON format and with the associated link.

So when the requested media type is Application/json, only the JSON representation of the resource needs to be returned.

When requesting Application/vnd.mycompany.hateoas+json, you need to return a resource statement with a link.

To modify the action method:

Use Fromheader to read the value of accept in the header, and then determine if media type is custom, and that is the result of containing the link, otherwise, use the result that does not contain the link, and place the link related to the page in the custom header.

Test:

Request Application/json, return results without links.

To modify Media type:

The return is 406,not acceptable.

This is because the format of the ASP. NET core does not recognize our custom media type.

Add these two words in startup to support this media type:

Then test again:

It's right now.

Root Document

The RESTful API needs to provide a root document for the consumer of the API. Through this document, the API consumer can know how to interact with the rest of the API. You can interpret this as an index page.

This document is located at the root of the API and creates a Rootcontroller:

Its routing address is the root path/API.

It has only one get method to return the corresponding link by reading the value of accept in the header.

Here if the media type is the one I customized earlier, it will return three links: itself, get countries, create country. These three are sufficient, with these three links, the other operations and resources (city) routing address through a layer of links are obtained.

If the request type is other, it returns 204.

Because my program is so simple, it's enough to write this.

Now you may find more questions about the presentation of resources and the type of media.

Look at the links in the previous example, the format of these links is not a standard format, but I created the format, the consumer API does not know how to handle these link, the consumer API needs to learn from the API documentation how to parse link, I need to describe the value of REL in the API documentation.

We also know that media type medium is also the content of API's external interface contract. Here's another question: Hypermedia allows program controls, links, etc. to be provided when needed, and for links to an action, the API consumer does not know what to put in the request.

Before we had created a custom media type, recall country's get and post two action, which use different resourcemodel:

Although their properties are similar in my example, they are different model, and it is possible that the attributes differ greatly.

Then in two action, I use Application/json this media type, in fact, most of the current API in this project I use is Application/json. But in fact the two model is a different expression of the resource country, using Application/json is actually wrong. The vendor-specific media type should be used, for example:

Application/vnd.mycompany.country.display+json and Application/vnd.mycompany.country.create+json. It can be done more finely and flexibly depending on the situation. This way, the API consumer knows how much of the requested content should be sent for different actions.

Version

Our API has changed many times now, the API will definitely change, so we need version intervention.

The functionality of the API, the business logic, and even the resource model will change, but we need to ensure that the changes are not disruptive to the consumer of the API.

There are several ways to version control:

    • Insert version inside URI:/api/v1/countries
    • Querying strings by query string:/API/COUNTRIES?API-VERSION=V1
    • Custom header: For example: "Api-version" =v1

But in a restful world, these practices are not always possible.

In fact, Roy Fielding does not recommend versioning the RESTful API .

But in fact many people feel the need to version the API, because the requirements will always change, the API will always change. However, do not release management of everything, we should try to use the version carefully, as far as possible to make the API backwards compatible .

If the functionality or business logic of the API changes, HATEOAS will handle the matter well, and the API's consumers will not destroy it by observing the Hateoas.

But if the resource model changes , it's a real problem,Roy Fielding says , and it shouldn't be versioned.

These are actually the questions before, how do I let the API consumer know what the resource statement should be, and how can I ensure that as the API evolves, the consumer of the API will evolve?

According to Roy Fielding, the solution to these problems is to use on-demand coding constraints (code on Demand) to adapt the evolution of media types and resource representations, and the constraints mentioned in the API can extend the functionality of the client.

Perhaps in ASP. NET MVC or some Web sites can adapt to this change, if the js,html of these sites are generated from the server side, but most of the time, it is difficult to implement this adaptive change.

We may be able to add version numbers to the media type to properly handle changes in resource representations. For example:

Application/vnd.mycompany.country.display. v1+json and Application/vnd.mycompany.country.display. v2 +json

For example, I added a new attribute in the entity model to the Continental continent, which is, of course, empty:

Now the API consumers can create country when the continent assignment or can not be assigned value, then you need to create a continent property with the Resourcemodel for post this action:

Don't forget to do the automapper mapping configuration.

In the controller, the parameter types for post actions may be countryaddresource and countryaddwithcontinentresource, so there is a need to create a POST method:

Since there are two post methods with the same routing address, it is also necessary to decide which method to enter according to the value of the Content-type headerd. Here we can customize a custom constraint property tag that applies to the action method:

This is very simple, pass in the type of header that needs to be matched, and the value (allow multiple values), then find a match from the headers of request to return true.

Apply to two action respectively:

Finally, you need to register the two media types, note that the two are input:

The following test first uses the original Application/json:

404, that's right, because Content-type is out of the match.

Next use the media type of the original post method:

Will enter the original post method:

Using another media type, you'll get into another method, and it's good to not map it.

The second parameter of the custom constraint label Requestheadermatchingmediatypeattribute above meidatypes is the array, why?

Because, on the previous, this method receives the format JSON, but if I want to also support the receipt of XML, it is directly in the array to add another XML media type.

This constraint tag can not only filter a header type, but also multiple, for example, I also have to specify different methods according to the Accept header, then:

This prompts for repetition, but can be resolved by modifying the constraint tag class:

At this point, the error message is gone:

Microsoft's API Versioning library

Microsoft provides an API version-managed library:Microsoft.AspNetCore.Mvc.Versioning.

After installing with NuGet, register in Startup:

You will then need to mark the version on the controller:

In fact, I don't really like this version management, I feel very messy. If you are interested, take a look at the official documentation:

Https://github.com/Microsoft/aspnet-api-versioning/wiki/New-Services-Quick-Start

then I deleted the library.

In addition to the manual implementation of this Hateoas, there are many other options, such as OData. But OData is not just hateoas, it's trying to standardize the restful API, for example, it has a lot of rules for creating URIs, flipping pages, calling methods, and so on, but I still don't use OData very much.

This time write here, source code in: https://github.com/solenovex/ASP.NET-Core-2.0-RESTful-API-Tutorial

Continue next week.

Building a canonical REST API with ASP. HATEOAS

Related Article

Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.