Building a specification REST API with ASP. NET Core 2.1--page flipping/sorting/filtering, etc.

Source: Internet
Author: User
Tags net serialization

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

The code required in this article (right-click Save, change the suffix to zip): https://images2018.cnblogs.com/blog/986268/201806/986268-20180604151009219-514390264.jpg

This code has been updated to ASP. NET Core 2.1. (Migrating from ASP. NET Core 2.0 to ASP. 2.1:https://docs.microsoft.com/en-us/aspnet/core/migration/20_21?view=aspnetcore-2.1)

This article mainly introduces some common situations, including: Collection update, page flipping, sorting, filtering and so on. However, the Web API, which is Richardson maturity Level 2, does not meet the standards and constraints of restful APIs.

Update operations for Collections

Look at this update collection of the case, the original database in China has 4 cities (peiping, Shanghai, Sheng Beijing, Vladivostok); and a few centuries after the change of Peiping called Beijing, Shengjing renamed Shenyang, Vladivostok does not belong to China has been deleted , Weihai from the county into the city even if it is New , while Shanghai remains unchanged. Now is the whole of China's cities to update the operation, which will contain: Add, delete, update operation. Look at the code:

Collection update, I took a total of three steps:

1. Delete cities that do not exist in the database but are not in the data

2. The data in the data that is not in the database is added, in fact, only the ID is 0.

3. Update the data entries that exist in the original and incoming parameters in the database.

Then save it.

First look at the original data:

Then we perform the update of the collection:

After execution, query again:

The collection is updated as expected.

I am sure you will write this code or have a simpler implementation (please post it). But this is not the point, I saw someone write this, the above three-step code written in the AutoMapper configuration file:

First, you need to ignore the mapping operation of the cities property of country, and then write that part of the code in Aftermap , so that the action method is simple, you can use the AutoMapper:

This is an optional notation, not necessarily in the AutoMapper configuration file.

Page

Page flipping avoids some performance problems and does not have to load all the data at once. Therefore, it is best to use paging by default, and the number of entries per page must be limited, not too large.

The paging information should pass parameters using the query string (stringg). The format should look like this:

Http://localhost:5000/api/country? pageindex=12&pagesize=10

Here I like to use the word pageindex, which also means that the number of pages is starting from 0, of course, many people like to use pagenumber and other words, that is, more like the page from 1 onwards, this is actually casual.

in ASP. NET core, I'm going to use LINQ to dynamically assemble an expression of a query (IQUERYABLE<T> You can create an expression tree), which is deferred until all the conditions have been judged and the final query expression is formed before executing (querying the database). This query expression queries the database only when the iteration is in progress.

The following method can be used to trigger an iterative action :

    • foreach loop
    • ToList (), ToArray (), ToDictionary () and the corresponding asynchronous version (Toxxxxasync ())
    • A single query , such as Average (), Count (), first (), FirstOrDefault (), Singleordefault (), and so on, and the corresponding asynchronous version.

You need to be sure that you want to use Skip () and take () and where () before the iteration occurs.

I'll write the code at 1.1:

First we need to pass in pageindex and pagesize from the parameter (query string argument), and also assign the default value to prevent the API consumers from setting pageindex and pagesize; Because the value of pagesize is determined by the consumer of the API. , you should set a maximum value on the backend so that the API's consumers set a large value.

Since almost all resources use page flipping, it is best to use a common class to encapsulate these paging related information:

(I've put this class in the core project for the time being).

This public class is very simple, can set the default value for PageIndex and pagesize, also set a maximum number of entries per page is 100; there is also an order by property, the default value is "Id", because the page must be sorted first, but the current attribute is not available.

For specific resources, we can build a class that inherits from Paginationbase, which is the country parameter class:

Because there are no special parameters for the time being, the inside is empty.

Let me revise countryrepository:

You can see that I have formed the expression of this query, and directly set out the iteration action, return the query results.

Back in the action method:

I used this parameter class instead of the previous pageindex and pagesize parameters, because the ASP. NET core is smart enough to parse the two parameters into this class.

Test it below:

I will not carry out many tests, this is useful.

If you are using a relational database, you should be able to see the printed SQL statement on the output medium of log (but I am using a memory database, so I do not see it), if you use a relational database or do not see the SQL statement, please configure:

Returns metadata for page flipping

It is clear that only the data that is returned to the current page is not satisfied, at least the total number of pages, totals, and so on, may need to be returned to the previous or next page link. But how do you return this information to the API consumer along with the data from the page?

The following is the way to return this data:

{     "data": [{Country1}, {Country2} ...],     "metadata": {"prev": "/api/...", ...}    

But doing so causes the body of the response to no longer conform to the Accept header (not the JSON representation of the resource), and it is not a application/json, but a new media type.

So if returning such data violates the rest rules (although the Richardson maturity level of this article is at most 2), it violates self-describing constraints (refer to this series of pre-knowledge articles), and API consumers do not know how to application/ JSON this set of Contety-type to explain the response data.

So the meta-data on page flipping is not part of the resource presentation . We should use a custom header, such as "X-pagination" , to express the page turn metadata, which is also more commonly used.

First, I create a class that can hold the data for paging:

You can do this with this class: The class inherits from List<t>, and it also contains paginationbase as an attribute, as well as the ability to determine whether there are previous and next pages. Create an instance of the class using a static method.

This static method may have a little bit of a problem, it is OK to use asynchronous methods, but if you use asynchronous methods, such as source. Countasync () and source. Tolistasync (), there will be some problems, because I need to modify the Countryrepository Getcountriesasync method return type, change to the above type, so its interface icountryrepository also need to change , and its interface is the core of the entire project and placed in the core project, and the whole project of the Heart (contract) I personally think it should be related to the specific ORM, but this is dependent on Entityframeworkcore (Tolistasync ()). So I finally decided to get rid of this static method, which might lead to more code, and also add the Hasprevious and Hasnext properties to determine if there are previous and next pages:

(temporarily in the core project).

Then modify the Countryrepository:

Then in the action method, we also need to generate the previous page and the next page of the URI, so you can use urlhelper here, you need to register in the startup Configureservices method:

Then go back to the controller and create a method to generate the URI:

Here I also set up an enumeration, Paginationresourceuritype. I also added a clone () method to Paginationbase to create a property value and another instance of it, because there is an operation to modify the PageIndex property, and perhaps clone is not the best approach, and the direct new may be more appropriate.

Here's how to modify the action method:

Create two links using the previous method, then make the page-related data an anonymous class, serialize it using json.net, and place it in the response's custom header: "X-pagination".

The body part is also the collection data of the resource.

Test it:

The body of the response returned with a normal return, and then look at the header of the response:

You can see the custom x-pagination header, and then I'll copy the Nextpagelink link inside and send the request:

There's no problem.

This action currently has a Richardson maturity level of nearly 3 (HATEOAS), but not yet. The page is now here, filter and turn the page.

Filtering and searching

filtering means attaching some conditions to a collection resource and then filtering out the result, and its URI is in the following form:

http://Localhost:5000/api/countries?englishname=china

So you need to write the name of the property and the value of the property in the query string to indicate that you want to filter by the value of this property, and of course you can write multiple filter conditions.

The conditions for filtering are applied to Resourcemodel(or Dto,viewmodel), such as Countryresource, and not to other levels of model, Because API consumers only know Resourcemodel, it does not know the details of the internal implementation, that is, do not know the appearance of Entitymodel.

Search , is a search keyword to blur the filter collection resources, there may be more than one property for this keyword fuzzy filtering.

The URI of the search is roughly the following form:

http://Localhost/api/countries?searchterm=hin

The above URI can be understood as a property of a string type for a countries resource, and its value contains a hin that matches the criteria and returns the result that matches that condition.

First look at the implementation of the filter. In the Get action method of countries, I use the Countryresourceparameters class as a parameter, so to increase the filter for a property, just extend the class, The added attribute name is the same as the property name inside the Resourcemodel:

Then there is the way to modify the countryrepository inside:

The first step is to append the filter before performing the paging action, which must be iqueryable<country> to dynamically assemble the query expression, so the AsQueryable () method is used And then judge two conditions and attach the conditions (note the case and the two spaces), and then perform the paging query.

Because of the addition of parameters, the Createuri method also needs to be changed:

This method parameter becomes countryresourceparameters, and the Clone method clones the Countryresourceparameters class:

Test below:

No problem, but also look at the header:

The result is OK for this.

Below I do some data to have the same englishname, and then test:

OK, then look at the header:

Using Nextlink to send the request again, the result is OK, I do not map.

However, you should note that X-pagination's property name does not match the CamelCase naming specification, so you need to add some configuration when converting to JSON:

Then test it again:

The name of the property conforms to the CamelCase specification, but the case of the query string inside Previouslink and Nextlink is still incorrect, so I simply removed the Clone () method, And then, in the Createcountryuri method, the new link parameter is created directly:

Test:

Now the naming finally conforms to the norm.

Sort

The previous pages need to be sorted, and they are sorted by ID for the time being. In fact, the API consumer may have the resource make a forward or reverse sort by a property or multiple attributes of the resource .

Let's start with the simplest example and only consider sorting by just one property (for the property of the resource, for example, Countryresource englishname), for this example, I'll use a rather stupid method.

First of all, I assume that the parameter class inside the attribute if the end of "desc", for example: "Englishname desc", then is in accordance with the englishname in reverse order, and "Englishname" is a positive sequence arrangement.

Just modify the code inside the Countryrepository:

Well, very cumbersome code.

Test it first:

At least the function is OK, then check the reverse:

OK, so although the code is cumbersome, it can be dealt with in this simple situation.

We'll optimize it for the first time. It is too laborious to judge a property as above, so let us analyze that the value of the order is a string , and the type of the lambda expression inside the order () method is expression , The specific type is expression<func<country, object>>. Here's a quick look, in case you don't know the lambda expression. A lambda expression is an anonymous function whose type is func(a variable that can be assigned to a Func type):

At the same time, we can assign this lambda expression to expressions:

The type of parameter received by this LINQ method is Expression<func<country, object>>.

Using expression, we can build expression tree, and with expression tree, you can represent some logic. At run time, the provider of LINQ will parse the expression Tree and translate the logic into SQL statements:

Looking at the sorting criteria above, we can map the string of the order and the expression, just like the Key-value key value pair, and this might be a little bit nicer. So you'll think of Dictionary<k, v>.

So the revised code is as follows:

I believe you can understand, I will not explain, the following test:

In short it is easy to use, I will not post other test results of the picture.

The above code should be extracted to encapsulate a method function and generics, but I will not do so for the time being.

After the first optimization, using dictionary, the code is a lot simpler, but there is a manual conversion of the property name string into expression. The reason for this is that it is only supported by the argument type of expression, which is perfect if the string is supported.

Fortunately, there is a Microsoft library that supports this kind of operation, which is called System.Linq.Dynamic.Core(the author of the Red Dress):

I installed it in the infrastructure project for repository use.

Modify the code of the sort section again:

Notice here that the name of the namespace is:System.Linq.Dynamic.Core.

After the second optimization, the code has been very concise, but there are many areas to be perfected, such as:

    • a property of the Resource model may be mapped to multiple properties on the entity model : The Name property is typically mapped to Entitymodel FirstName and LastName Properties
    • the positive order on the Resource model may be reversed in the entity model : Age ascending, and the birthdate of the entity model is descending
    • need to support sorting of multiple attributes : Englishname desc, Id, Chinesename.
    • Multiplexing

Third optimization, to solve the problem caused by the Model attribute mapping.

That is, to map a property from Resourcemodel to one or more attributes of the entity model, and the order of the permutations between them may be different, to cite an extreme example:

Suppose Resourcemodel has a property called rank, which maps the two attributes of the entity model to result (score) and weight (weight); Assuming this is the model for weightlifting, ranking result (rank) is based on score (result) From high to low, but if more than one player has the same results, then the weight of the top ranking.

Also known as rank ASC, Result DESC, Weight ASC.

In the program, a string "Rank ASC" is mapped to a collection, and the type of the collection element has two attributes: the attribute name of the Entity model and the direction of the sort.

So first set up the class of this element in the collection:

Here I am using the word revert, indicating whether the direction is the opposite of the resource model's property direction.

And then we're doing a whole set of mappings for Countryresource, but first I'm thinking about creating an abstract parent that might be something common:

Since the id attribute may be common to each relevant model, in this parent class, I have added a mapping of the id attribute, the ID is a one-to map, and the sort direction is the same.

Then I write a subclass that derives from Propertymapping for Countryresource:

Note that the red box is important, and when you compare key, ignore the case.

Here, the portion of the mapping between the resource and the entity model is almost done, and the next step is to consider the whole sort of problem, such as an extension method:

It is applied to IQueryable and passes in the string and attribute mapping table.

After some preliminary examination, the order is broken into an array of field properties by pressing ",". Then remove the possible space on both sides, determine whether it is reversed, extract the name of the property. If the name is not found in the mapping table or the value corresponding to the name is empty, an exception is thrown.

The field array is then looped first, and then the inner layer loops the property collection for that field mapping.

Finally, you can build the desired sort expression by dynamiclinq.

When using the order of DYNAMICLINQ, be aware that the sorting criteria must be reversed, not the letter can try.

We then modified the repository:

There is one sentence left, very concise. But this requires a new countrypropertymapping class, which is unfriendly to unit testing, and maybe it's more appropriate to put it in a container.

Then create a container:

The registers and resolve of the container are used to register and extract the mapping tables, respectively.

There is also a method for checking the existence of mappings, fields is a string of one or more field properties, in the form of "Englishname,chinesename", and it checks to see if the corresponding key can be found in the Mapping Configuration table (Mappingdictionary) , the validation fails if it is not found.

This container is also a container throughout the application, so it needs to be registered in startup, because its code may be much more (because it is also a container, and there is a lot of code for registering the content), so I wrote a separate extension method:

This method can be called in startup to register with the ASP. NET Core Service container:

Then modify the Countryrepository again:

The container service is injected first, and the required mapping table is taken out of the container from the model type on both sides of the map:

Test:

Looks OK, so we're going to optimize this for the sort.

Sort of exception

It is also necessary to consider the case where the field in the order is not present in the mapping table, so I use this method to judge:

I put this method in the Propertymappingcontainer, because Propertymappingcontainer itself is actually a service, put in or more appropriate.

It is important to note that the field inside the fileds may be this form of "Englishname desc", so you need to remove the space and the DESC part.

It can then be called in the action method:

Test:

Should be no problem, I will not test more, in the future to implement unit testing.

Resource Shaping

If the properties of a resource are more, then the client's API consumers may need only a subset of the attributes, and the data should be shaped, and doing so may improve performance.

Data shaping takes into account two scenarios, a collection of resources and a single resource .

Aggregate resource Shaping

First of all, consider the collection of resources, first I do an extension method, the ienumerable<t> can be converted to ienumerable<dynamic>, here to use the dynamic (ExpandoObject):

Because the reflection consumes resources, I put the required attributes into a PropertyInfo at once. If fields are empty, all attributes are required, and all public and instance properties are placed in the collection, otherwise the required attributes are put in.

Then loop the data source, use reflection to get the value of the property through PropertyInfo, and finally make a ExpandoObject, and then put this ExpandoObject in the result set.

Next modify the parameter class, because this is a generic thing, and that is to add a Fields property for Paginationbase:

Finally, modify the action method:

Test:

It's handy. However, the returned data is not CamelCase, because json.net serialization of Contractresolver does not apply to dictionary. Here's how to deal with this problem.

Open startup, in services. ADDMVC () Add:

This is the contractresolver that is configured with JSON conversion.

In the test:

It's OK now.

Handling Exceptions

However, if the API consumer provides a non-existent attribute in fields, then the bad Request should be returned.

In principle I may be able to use the validation method inside the Properymappingcontainer, but the data is shaped without using a mapping table. And the purpose is different, one is the sort one is the data shaping, so because the focus on the separation bar (SoC).

All we have to do is to give a field and a type, and we need to make a judgment that the fields contained within the domain exist in this type, so it's better to do a service, and you can inject it.

Look at the code:

This class is relatively simple to talk about, do not forget to register in the startup.

Then inject and use it inside the controller, and don't forget to modify the Createcountryuri method:

Test:

Ok.

Shape a single resource

This is similar to the principle of the set, first set up an extension method:

You can modify the action again:

Test:

Is easy to use, I will not test more.

For data shaping it is important to take the ID with you as much as possible, or you may not be able to get related links.

Today I write here, there are a lot more in-depth features not done, I will not do.

So far, these web APIs are still not known as RESTful APIs, maturity is not high enough, and some constraints are not met. The next article will upgrade these APIs to support Hateoas.

Code in this: https://github.com/solenovex/ASP.NET-Core-2.0-RESTful-API-Tutorial

Project some files of the visit directory may be incorrect, temporarily do not process.

Building a specification REST API with ASP. NET Core 2.1--page flipping/sorting/filtering, etc.

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.