{"id":1130,"date":"2025-05-27T14:47:04","date_gmt":"2025-05-27T06:47:04","guid":{"rendered":"https:\/\/www.hyy.net\/?p=1130"},"modified":"2025-05-27T14:47:04","modified_gmt":"2025-05-27T06:47:04","slug":"ultimate-asp-net-core-web-api-16-paging","status":"publish","type":"post","link":"https:\/\/diji.net\/?p=1130","title":{"rendered":"Ultimate ASP.NET Core Web API 16 PAGING"},"content":{"rendered":"<h1>16 PAGING<\/h1>\n<p>16 \u5206\u9875<\/p>\n<p>We have covered a lot of interesting features while creating our Web API project, but there are still things to do.\u200c<br \/>\n\u5728\u521b\u5efa Web API \u9879\u76ee\u65f6\uff0c\u6211\u4eec\u5df2\u7ecf\u4ecb\u7ecd\u4e86\u8bb8\u591a\u6709\u8da3\u7684\u529f\u80fd\uff0c\u4f46\u4ecd\u6709\u4e00\u4e9b\u4e8b\u60c5\u8981\u505a\u3002<\/p>\n<p>So, in this chapter, we\u2019re going to learn how to implement paging in ASP.NET Core Web API. It is one of the most important concepts in building RESTful APIs.<br \/>\n\u56e0\u6b64\uff0c\u5728\u672c\u7ae0\u4e2d\uff0c\u6211\u4eec\u5c06\u5b66\u4e60\u5982\u4f55\u5728 ASP.NET Core Web API \u4e2d\u5b9e\u73b0\u5206\u9875\u3002\u5b83\u662f\u6784\u5efa RESTful API \u4e2d\u6700\u91cd\u8981\u7684\u6982\u5ff5\u4e4b\u4e00\u3002<\/p>\n<p>If we inspect the GetEmployeesForCompany action in the EmployeesController, we can see that we return all the employees for the single company.<br \/>\n\u5982\u679c\u6211\u4eec\u68c0\u67e5 EmployeesController \u4e2d\u7684 GetEmployeesForCompany\u4f5c\uff0c\u6211\u4eec\u53ef\u4ee5\u770b\u5230\u6211\u4eec\u8fd4\u56de\u4e86\u5355\u4e2a\u516c\u53f8\u7684\u6240\u6709\u5458\u5de5\u3002<\/p>\n<p>But we don\u2019t want to return a collection of all resources when querying our API. That can cause performance issues and it\u2019s in no way optimized for public or private APIs. It can cause massive slowdowns and even application crashes in severe cases.<br \/>\n\u4f46\u662f\uff0c\u5728\u67e5\u8be2 API \u65f6\uff0c\u6211\u4eec\u4e0d\u60f3\u8fd4\u56de\u6240\u6709\u8d44\u6e90\u7684\u96c6\u5408\u3002\u8fd9\u53ef\u80fd\u4f1a\u5bfc\u81f4\u6027\u80fd\u95ee\u9898\uff0c\u5e76\u4e14\u5b83\u7edd\u4e0d\u4f1a\u9488\u5bf9\u516c\u6709\u6216\u79c1\u6709 API \u8fdb\u884c\u4f18\u5316\u3002\u5b83\u53ef\u80fd\u4f1a\u5bfc\u81f4\u5927\u89c4\u6a21\u51cf\u901f\uff0c\u4e25\u91cd\u65f6\u751a\u81f3\u4f1a\u5bfc\u81f4\u5e94\u7528\u7a0b\u5e8f\u5d29\u6e83\u3002<\/p>\n<p>Of course, we should learn a little more about Paging before we dive into code implementation.<br \/>\n\u5f53\u7136\uff0c\u5728\u6df1\u5165\u7814\u7a76\u4ee3\u7801\u5b9e\u73b0\u4e4b\u524d\uff0c\u6211\u4eec\u5e94\u8be5\u66f4\u591a\u5730\u4e86\u89e3 Paging\u3002<\/p>\n<h2>16.1 What is Paging?<\/h2>\n<p>16.1 \u4ec0\u4e48\u662f\u5206\u9875\uff1f<\/p>\n<p>Paging refers to getting partial results from an API. Imagine having millions of results in the database and having your application try to return all of them at once.\u200c<br \/>\n\u5206\u9875\u662f\u6307\u4ece API \u83b7\u53d6\u90e8\u5206\u7ed3\u679c\u3002\u60f3\u8c61\u4e00\u4e0b\uff0c\u6570\u636e\u5e93\u4e2d\u6709\u6570\u767e\u4e07\u4e2a\u7ed3\u679c\uff0c\u5e76\u8ba9\u60a8\u7684\u5e94\u7528\u7a0b\u5e8f\u5c1d\u8bd5\u4e00\u6b21\u8fd4\u56de\u6240\u6709\u7ed3\u679c\u3002<\/p>\n<p>Not only would that be an extremely ineffective way of returning the results, but it could also possibly have devastating effects on the application itself or the hardware it runs on. Moreover, every client has limited memory resources and it needs to restrict the number of shown results.<br \/>\n\u8fd9\u4e0d\u4ec5\u662f\u4e00\u79cd\u6781\u5176\u65e0\u6548\u7684\u8fd4\u56de\u7ed3\u679c\u7684\u65b9\u5f0f\uff0c\u800c\u4e14\u8fd8\u53ef\u80fd\u5bf9\u5e94\u7528\u7a0b\u5e8f\u672c\u8eab\u6216\u8fd0\u884c\u5b83\u7684\u786c\u4ef6\u4ea7\u751f\u6bc1\u706d\u6027\u7684\u5f71\u54cd\u3002\u6b64\u5916\uff0c\u6bcf\u4e2a\u5ba2\u6237\u7aef\u7684\u5185\u5b58\u8d44\u6e90\u90fd\u662f\u6709\u9650\u7684\uff0c\u5b83\u9700\u8981\u9650\u5236\u663e\u793a\u7684\u7ed3\u679c\u7684\u6570\u91cf\u3002<\/p>\n<p>Thus, we need a way to return a set number of results to the client in order to avoid these consequences. Let\u2019s see how we can do that.<br \/>\n\u56e0\u6b64\uff0c\u6211\u4eec\u9700\u8981\u4e00\u79cd\u65b9\u6cd5\u5c06\u4e00\u5b9a\u6570\u91cf\u7684\u7ed3\u679c\u8fd4\u56de\u7ed9\u5ba2\u6237\u7aef\uff0c\u4ee5\u907f\u514d\u8fd9\u4e9b\u540e\u679c\u3002\u8ba9\u6211\u4eec\u770b\u770b\u5982\u4f55\u505a\u5230\u8fd9\u4e00\u70b9\u3002<\/p>\n<h2>16.2 Paging Implementation<\/h2>\n<p>16.2 \u5206\u9875\u5b9e\u73b0<\/p>\n<p>Mind you, we don\u2019t want to change the base repository logic or implement\u200c any business logic in the controller.<br \/>\n\u8bf7\u6ce8\u610f\uff0c\u6211\u4eec\u4e0d\u60f3\u66f4\u6539\u57fa\u672c\u5b58\u50a8\u5e93\u903b\u8f91\u6216\u5728\u63a7\u5236\u5668\u4e2d\u5b9e\u73b0\u4efb\u4f55\u4e1a\u52a1\u903b\u8f91\u3002<\/p>\n<p>What we want to achieve is something like this: <a href=\"https:\/\/localhost:5001\/api\/companies\/companyId\/employees?pa\">https:\/\/localhost:5001\/api\/companies\/companyId\/employees?pa<\/a> geNumber=2&amp;pageSize=2. This should return the second set of two employees we have in our database.<br \/>\n\u6211\u4eec\u60f3\u8981\u5b9e\u73b0\u7684\u662f\u8fd9\u6837\u7684\uff1a<a href=\"https:\/\/localhost:5001\/api\/companies\/companyId\/employees?pageNumber=2&amp;pageSize=2\u3002\u8fd9\u5e94\u8be5\u8fd4\u56de\u6211\u4eec\u6570\u636e\u5e93\u4e2d\u7684\u7b2c\u4e8c\u7ec4\u4e24\u4e2a\u5458\u5de5\">https:\/\/localhost:5001\/api\/companies\/companyId\/employees?pageNumber=2&pageSize=2\u3002\u8fd9\u5e94\u8be5\u8fd4\u56de\u6211\u4eec\u6570\u636e\u5e93\u4e2d\u7684\u7b2c\u4e8c\u7ec4\u4e24\u4e2a\u5458\u5de5<\/a>\u3002<\/p>\n<p>We also want to constrain our API not to return all the employees even if someone calls <a href=\"https:\/\/localhost:5001\/api\/companies\/companyId\/employees\">https:\/\/localhost:5001\/api\/companies\/companyId\/employees<\/a>.<br \/>\n\u6211\u4eec\u8fd8\u5e0c\u671b\u7ea6\u675f\u6211\u4eec\u7684 API \u4e0d\u4f1a\u8fd4\u56de\u6240\u6709\u5458\u5de5\uff0c\u5373\u4f7f\u6709\u4eba\u8c03\u7528 <a href=\"https:\/\/localhost:5001\/api\/companies\/companyId\/employees\">https:\/\/localhost:5001\/api\/companies\/companyId\/employees<\/a>\u3002<\/p>\n<p>Let's start with the controller modification by modifying the GetEmployeesForCompany action:<br \/>\n\u8ba9\u6211\u4eec\u901a\u8fc7\u4fee\u6539 GetEmployeesForCompany\u4f5c\u6765\u4ece\u63a7\u5236\u5668\u4fee\u6539\u5f00\u59cb\uff1a<\/p>\n<pre><code>[HttpGet]\n\/\/ public async Task&lt;IActionResult&gt; GetEmployeesForCompany(Guid companyId)\npublic async Task&lt;IActionResult&gt; GetEmployeesForCompany(Guid companyId, [FromQuery] EmployeeParameters employeeParameters)\n{\n    var employees = await _service.EmployeeService.GetEmployeesAsync(companyId, trackChanges: false);\n    return Ok(employees);\n}<\/code><\/pre>\n<p>A few things to take note of here:<br \/>\n\u8fd9\u91cc\u9700\u8981\u6ce8\u610f\u4ee5\u4e0b\u51e0\u70b9\uff1a<\/p>\n<p>\u2022 We\u2019re using [FromQuery] to point out that we\u2019ll be using query parameters to define which page and how many employees we are requesting.<br \/>\n\u6211\u4eec\u4f7f\u7528 [FromQuery] \u6765\u6307\u51fa\uff0c\u6211\u4eec\u5c06\u4f7f\u7528\u67e5\u8be2\u53c2\u6570\u6765\u5b9a\u4e49\u6211\u4eec\u8bf7\u6c42\u7684\u9875\u9762\u548c\u5458\u5de5\u6570\u91cf\u3002<\/p>\n<p>\u2022 The EmployeeParameters class is the container for the actual parameters for the Employee entity.<br \/>\nEmployeeParameters \u7c7b\u662f Employee \u5b9e\u4f53\u7684\u5b9e\u9645\u53c2\u6570\u7684\u5bb9\u5668\u3002<\/p>\n<p>We also need to actually create the EmployeeParameters class. So, let\u2019s first create a RequestFeatures folder in the Shared project and then inside, create the required classes.<br \/>\n\u6211\u4eec\u8fd8\u9700\u8981\u5b9e\u9645\u521b\u5efa EmployeeParameters \u7c7b\u3002\u56e0\u6b64\uff0c\u8ba9\u6211\u4eec\u9996\u5148\u5728 Shared \u9879\u76ee\u4e2d\u521b\u5efa\u4e00\u4e2a RequestFeatures \u6587\u4ef6\u5939\uff0c\u7136\u540e\u5728\u5176\u4e2d\u521b\u5efa\u6240\u9700\u7684\u7c7b\u3002<\/p>\n<p>First the RequestParameters class:<br \/>\n\u9996\u5148\u662f RequestParameters \u7c7b\uff1a<\/p>\n<pre><code>namespace Shared.RequestFeatures\n{\n    public abstract class RequestParameters\n    {\n        const int maxPageSize = 50;\n        public int PageNumber { get; set; } = 1;\n        private int _pageSize = 10;\n        public int PageSize\n        {\n            get { return _pageSize; }\n            set { _pageSize = (value &gt; maxPageSize) ? maxPageSize : value; }\n        }\n    }\n}<\/code><\/pre>\n<p>And then the EmployeeParameters class:<br \/>\n\u7136\u540e\u662f EmployeeParameters \u7c7b\uff1a<\/p>\n<pre><code>namespace Shared.RequestFeatures\n{\n    public class EmployeeParameters : RequestParameters { }\n}<\/code><\/pre>\n<p>We create an abstract class to hold the common properties for all the entities in our project, and a single EmployeeParameters class that will hold the specific parameters. It is empty now, but soon it won\u2019t be.<br \/>\n\u6211\u4eec\u521b\u5efa\u4e00\u4e2a\u62bd\u8c61\u7c7b\u6765\u4fdd\u5b58\u9879\u76ee\u4e2d\u6240\u6709\u5b9e\u4f53\u7684\u516c\u5171\u5c5e\u6027\uff0c\u5e76\u521b\u5efa\u4e00\u4e2a EmployeeParameters \u7c7b\u6765\u4fdd\u5b58\u7279\u5b9a\u53c2\u6570\u3002\u5b83\u73b0\u5728\u662f\u7a7a\u7684\uff0c\u4f46\u5f88\u5feb\u5c31\u4f1a\u7a7a\u4e86\u3002<\/p>\n<p>In the abstract class, we are using the maxPageSize constant to restrict our API to a maximum of 50 rows per page. We have two public properties \u2013 PageNumber and PageSize. If not set by the caller, PageNumber will be set to 1, and PageSize to 10.<br \/>\n\u5728\u62bd\u8c61\u7c7b\u4e2d\uff0c\u6211\u4eec\u4f7f\u7528 maxPageSize \u5e38\u91cf\u5c06 API \u9650\u5236\u4e3a\u6bcf\u9875\u6700\u591a 50 \u884c\u3002\u6211\u4eec\u6709\u4e24\u4e2a\u516c\u5171\u5c5e\u6027 \u2013 PageNumber \u548c PageSize\u3002\u5982\u679c\u8c03\u7528\u65b9\u672a\u8bbe\u7f6e\uff0c\u5219 PageNumber \u5c06\u8bbe\u7f6e\u4e3a 1\uff0cPageSize \u5c06\u8bbe\u7f6e\u4e3a 10\u3002<\/p>\n<p>Now we can return to the controller and import a using directive for the EmployeeParameters class:<br \/>\n\u73b0\u5728\u6211\u4eec\u53ef\u4ee5\u8fd4\u56de\u5230\u63a7\u5236\u5668\u5e76\u5bfc\u5165 EmployeeParameters \u7c7b\u7684 using \u6307\u4ee4\uff1a<\/p>\n<pre><code>using Shared.RequestFeatures;<\/code><\/pre>\n<p>After that change, let\u2019s implement the most important part \u2014 the repository logic. We need to modify the GetEmployeesAsync method in the IEmployeeRepository interface and the EmployeeRepository class.<br \/>\n\u66f4\u6539\u4e4b\u540e\uff0c\u8ba9\u6211\u4eec\u5b9e\u73b0\u6700\u91cd\u8981\u7684\u90e8\u5206 \u2014 \u5b58\u50a8\u5e93\u903b\u8f91\u3002\u6211\u4eec\u9700\u8981\u4fee\u6539 IEmployeeRepository \u63a5\u53e3\u4e2d\u7684 GetEmployeesAsync \u65b9\u6cd5\u548c EmployeeRepository \u7c7b\u3002<\/p>\n<p>So, first the interface modification:<br \/>\n\u6240\u4ee5\uff0c\u9996\u5148\u8fdb\u884c\u63a5\u53e3\u4fee\u6539\uff1a<\/p>\n<pre><code>using Entities.Models;\nusing Shared.RequestFeatures;\n\nnamespace Contracts;\n\npublic interface IEmployeeRepository\n{\n    \/\/ Task&lt;IEnumerable&lt;Employee&gt;&gt; GetEmployeesAsync(Guid companyId, bool trackChanges);\n    Task&lt;IEnumerable&lt;Employee&gt;&gt; GetEmployeesAsync(Guid companyId, EmployeeParameters employeeParameters, bool trackChanges);\n\n    Task&lt;Employee&gt; GetEmployeeAsync(Guid companyId, Guid id, bool trackChanges);\n    void CreateEmployeeForCompany(Guid companyId, Employee employee);\n    void DeleteEmployee(Employee employee);\n}<\/code><\/pre>\n<p>As Visual Studio suggests, we have to add the reference to the Shared project.<br \/>\n\u6b63\u5982 Visual Studio \u6240\u5efa\u8bae\u7684\uff0c\u6211\u4eec\u5fc5\u987b\u6dfb\u52a0\u5bf9 Shared \u9879\u76ee\u7684\u5f15\u7528\u3002<\/p>\n<p>After that, let\u2019s modify the repository logic:<br \/>\n\u4e4b\u540e\uff0c\u8ba9\u6211\u4eec\u4fee\u6539\u4ed3\u5e93\u903b\u8f91\uff1a<\/p>\n<pre><code>\/\/public async Task&lt;IEnumerable&lt;Employee&gt;&gt; GetEmployeesAsync(Guid companyId, bool trackChanges) =&gt;\n\/\/  await FindByCondition(e =&gt; e.CompanyId.Equals(companyId), trackChanges)\n\/\/  .OrderBy(e =&gt; e.Name)\n\/\/  .ToListAsync();\n\npublic async Task&lt;IEnumerable&lt;Employee&gt;&gt; GetEmployeesAsync(Guid companyId, EmployeeParameters employeeParameters, bool trackChanges) =&gt; \n    await FindByCondition(e =&gt; e.CompanyId.Equals(companyId), trackChanges)\n    .OrderBy(e =&gt; e.Name)\n    .Skip((employeeParameters.PageNumber - 1) * employeeParameters.PageSize)\n    .Take(employeeParameters.PageSize)\n    .ToListAsync();<\/code><\/pre>\n<p>Okay, the easiest way to explain this is by example.<br \/>\n\u597d\u7684\uff0c\u89e3\u91ca\u8fd9\u4e00\u70b9\u7684\u6700\u7b80\u5355\u65b9\u6cd5\u662f\u4e3e\u4f8b\u8bf4\u660e\u3002<\/p>\n<p>Say we need to get the results for the third page of our website, counting 20 as the number of results we want. That would mean we want to skip the first ((3 \u2013 1) <em> 20) = 40 results, then take the next 20 and return them to the caller.<br \/>\n\u5047\u8bbe\u6211\u4eec\u9700\u8981\u83b7\u53d6\u7f51\u7ad9\u7b2c\u4e09\u9875\u7684\u7ed3\u679c\uff0c\u5c06 20 \u7b97\u4f5c\u6211\u4eec\u60f3\u8981\u7684\u7ed3\u679c\u6570\u3002\u8fd9\u610f\u5473\u7740\u6211\u4eec\u8981\u8df3\u8fc7\u7b2c\u4e00\u4e2a \uff08\uff083 \u2013 1\uff09 <\/em> 20\uff09 = 40 \u4e2a\u7ed3\u679c\uff0c\u7136\u540e\u83b7\u53d6\u63a5\u4e0b\u6765\u7684 20 \u4e2a\u7ed3\u679c\u5e76\u5c06\u5b83\u4eec\u8fd4\u56de\u7ed9\u8c03\u7528\u8005\u3002<\/p>\n<p>Does that make sense?<br \/>\n\u8fd9\u6709\u610f\u4e49\u5417\uff1f<\/p>\n<p>Since we call this repository method in our service layer, we have to modify it as well.<br \/>\n\u7531\u4e8e\u6211\u4eec\u5728\u670d\u52a1\u5c42\u4e2d\u8c03\u7528\u6b64\u5b58\u50a8\u5e93\u65b9\u6cd5\uff0c\u56e0\u6b64\u6211\u4eec\u4e5f\u5fc5\u987b\u5bf9\u5176\u8fdb\u884c\u4fee\u6539\u3002<\/p>\n<p>So, let\u2019s start with the IEmployeeService modification:<br \/>\n\u90a3\u4e48\uff0c\u8ba9\u6211\u4eec\u4ece IEmployeeService \u4fee\u6539\u5f00\u59cb\uff1a<\/p>\n<pre><code>using Entities.Models;\nusing Shared.DataTransferObjects;\nusing Shared.RequestFeatures;\n\nnamespace Service.Contracts;\n\npublic interface IEmployeeService\n{\n    \/\/ Task&lt;IEnumerable&lt;EmployeeDto&gt;&gt; GetEmployeesAsync(Guid companyId, bool trackChanges);\n\n    Task&lt;IEnumerable&lt;EmployeeDto&gt;&gt; GetEmployeesAsync(Guid companyId, EmployeeParameters employeeParameters, bool trackChanges);\n    \/\/ ... \n}<\/code><\/pre>\n<p>In this interface, we only have to modify the GetEmployeesAsync method by adding a new parameter.<br \/>\n\u5728\u6b64\u63a5\u53e3\u4e2d\uff0c\u6211\u4eec\u53ea\u9700\u901a\u8fc7\u6dfb\u52a0\u65b0\u53c2\u6570\u6765\u4fee\u6539 GetEmployeesAsync \u65b9\u6cd5\u3002<\/p>\n<p>After that, let\u2019s modify the EmployeeService class:<br \/>\n\u4e4b\u540e\uff0c\u6211\u4eec\u6765\u4fee\u6539 EmployeeService \u7c7b\uff1a<\/p>\n<pre><code>\/\/public async Task&lt;IEnumerable&lt;EmployeeDto&gt;&gt; GetEmployeesAsync(Guid companyId, bool trackChanges)\n\/\/{\n\/\/       await CheckIfCompanyExists(companyId, trackChanges);\n\/\/  var employeesFromDb = await _repository.Employee.GetEmployeesAsync(companyId, trackChanges);\n\/\/  var employeesDto = _mapper.Map&lt;IEnumerable&lt;EmployeeDto&gt;&gt;(employeesFromDb);\n\/\/  return employeesDto;\n\/\/}\n\npublic async Task&lt;IEnumerable&lt;EmployeeDto&gt;&gt; GetEmployeesAsync(Guid companyId, EmployeeParameters employeeParameters, bool trackChanges)\n{\n    await CheckIfCompanyExists(companyId, trackChanges); \n    var employeesFromDb = await _repository.Employee.GetEmployeesAsync(companyId, employeeParameters, trackChanges); \n    var employeesDto = _mapper.Map&lt;IEnumerable&lt;EmployeeDto&gt;&gt;(employeesFromDb); \n    return employeesDto;\n}<\/code><\/pre>\n<p>Nothing too complicated here. We just accept an additional parameter and pass it to the repository method.<br \/>\n\u8fd9\u91cc\u6ca1\u4ec0\u4e48\u592a\u590d\u6742\u7684\u3002\u6211\u4eec\u53ea\u63a5\u53d7\u4e00\u4e2a\u989d\u5916\u7684\u53c2\u6570\u5e76\u5c06\u5176\u4f20\u9012\u7ed9 repository \u65b9\u6cd5\u3002<\/p>\n<p>Finally, we have to modify the GetEmployeesForCompany action and fix that error by adding another argument to the GetEmployeesAsync method call:<br \/>\n\u6700\u540e\uff0c\u6211\u4eec\u5fc5\u987b\u4fee\u6539 GetEmployeesForCompany\u4f5c\uff0c\u5e76\u901a\u8fc7\u5411 GetEmployeesAsync \u65b9\u6cd5\u8c03\u7528\u6dfb\u52a0\u53e6\u4e00\u4e2a\u53c2\u6570\u6765\u4fee\u590d\u8be5\u9519\u8bef\uff1a<\/p>\n<pre><code>[HttpGet]\npublic async Task&lt;IActionResult&gt; GetEmployeesForCompany(Guid companyId, [FromQuery] EmployeeParameters employeeParameters)\n{\n    var employees = await _service.EmployeeService.GetEmployeesAsync(companyId, employeeParameters, trackChanges: false);\n    return Ok(employees);\n}<\/code><\/pre>\n<h2>16.3 Concrete Query<\/h2>\n<p>16.3 \u5177\u4f53\u67e5\u8be2<\/p>\n<p>Before we continue, we should create additional employees for the company with the id: C9D4C053-49B6-410C-BC78-2D54A9991870. We are doing this because we have only a small number of employees per company and we need more of them for our example. You can use a predefined request in Part16 in Postman, and just change the request body with the following objects:\u200c<br \/>\n\u5728\u6211\u4eec\u7ee7\u7eed\u4e4b\u524d\uff0c\u6211\u4eec\u5e94\u8be5\u4e3a ID \u4e3a C9D4C053-49B6-410C-BC78-2D54A9991870 \u7684\u516c\u53f8\u521b\u5efa\u989d\u5916\u7684\u5458\u5de5\u3002\u6211\u4eec\u8fd9\u6837\u505a\u662f\u56e0\u4e3a\u6bcf\u5bb6\u516c\u53f8\u53ea\u6709\u5c11\u91cf\u5458\u5de5\uff0c\u6211\u4eec\u9700\u8981\u66f4\u591a\u7684\u5458\u5de5\u6765\u8bc1\u660e\u6211\u4eec\u7684\u793a\u4f8b\u3002\u60a8\u53ef\u4ee5\u5728 Postman \u7684 Part16 \u4e2d\u4f7f\u7528\u9884\u5b9a\u4e49\u7684\u8bf7\u6c42\uff0c\u53ea\u9700\u4f7f\u7528\u4ee5\u4e0b\u5bf9\u8c61\u66f4\u6539\u8bf7\u6c42\u6b63\u6587\uff1a<\/p>\n<table>\n<thead>\n<tr>\n<th><\/th>\n<th><\/th>\n<th><\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>{&quot;name&quot;: &quot;Mihael Worth&quot;,&quot;age&quot;: 30,&quot;position&quot;: &quot;Marketing expert&quot;}<\/td>\n<td>{&quot;name&quot;: &quot;John Spike&quot;,&quot;age&quot;: 32,&quot;position&quot;: &quot;Marketing expert II&quot;}<\/td>\n<td>{&quot;name&quot;: &quot;Nina Hawk&quot;,&quot;age&quot;: 26,&quot;position&quot;: &quot;Marketing expert II&quot;}<\/td>\n<\/tr>\n<tr>\n<td>{&quot;name&quot;: &quot;Mihael Fins&quot;,&quot;age&quot;: 30,&quot;position&quot;: &quot;Marketing expert&quot; }<\/td>\n<td>{&quot;name&quot;: &quot;Martha Grown&quot;,&quot;age&quot;: 35, &quot;position&quot;: &quot;Marketing expert II&quot;}<\/td>\n<td>{&quot;name&quot;: &quot;Kirk Metha&quot;,&quot;age&quot;: 30,&quot;position&quot;: &quot;Marketing expert&quot; }<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Now we should have eight employees for this company, and we can try a request like this:<br \/>\n\u73b0\u5728\uff0c\u8fd9\u5bb6\u516c\u53f8\u5e94\u8be5\u6709 8 \u540d\u5458\u5de5\uff0c\u6211\u4eec\u53ef\u4ee5\u5c1d\u8bd5\u5982\u4e0b\u8bf7\u6c42\uff1a<\/p>\n<p><a href=\"https:\/\/localhost:5001\/api\/companies\/C9D4C053-49B6-410C-BC78-2D54A9991870\/employees?pageNumber=2&amp;pageSize=2\">https:\/\/localhost:5001\/api\/companies\/C9D4C053-49B6-410C-BC78-2D54A9991870\/employees?pageNumber=2&pageSize=2<\/a><\/p>\n<p>So, we request page two with two employees:<br \/>\n\u56e0\u6b64\uff0c\u6211\u4eec\u8bf7\u6c42\u7b2c 2 \u9875\u6709\u4e24\u540d\u5458\u5de5\uff1a<\/p>\n<p><a href=\"https:\/\/localhost:5001\/api\/companies\/C9D4C053-49B6-410C-BC78\">https:\/\/localhost:5001\/api\/companies\/C9D4C053-49B6-410C-BC78<\/a>- 2D54A9991870\/employees?pageNumber=2&amp;pageSize=2<\/p>\n<p><img decoding=\"async\" src=\"\/images\/ultimateaspnetcorewebapi6\/1601.jpg\" alt=\"alt text\" \/><\/p>\n<p>If that\u2019s what you got, you\u2019re on the right track. We can check our result in the database:<br \/>\n\u5982\u679c\u8fd9\u5c31\u662f\u4f60\u5f97\u5230\u7684\uff0c\u90a3\u4f60\u5c31\u8d70\u5728\u6b63\u786e\u7684\u8f68\u9053\u4e0a\u3002\u6211\u4eec\u53ef\u4ee5\u5728\u6570\u636e\u5e93\u4e2d\u68c0\u67e5\u6211\u4eec\u7684\u7ed3\u679c\uff1a<\/p>\n<p><img decoding=\"async\" src=\"\/images\/ultimateaspnetcorewebapi6\/1602.jpg\" alt=\"alt text\" \/><\/p>\n<p>And we can see that we have the correct data returned.<br \/>\n\u6211\u4eec\u53ef\u4ee5\u770b\u5230\u6211\u4eec\u8fd4\u56de\u4e86\u6b63\u786e\u7684\u6570\u636e\u3002<\/p>\n<p>Now, what can we do to improve this solution?<br \/>\n\u73b0\u5728\uff0c\u6211\u4eec\u80fd\u505a\u4e9b\u4ec0\u4e48\u6765\u6539\u8fdb\u8fd9\u4e2a\u89e3\u51b3\u65b9\u6848\u5462\uff1f<\/p>\n<h2>16.4 Improving the Solution<\/h2>\n<p>16.4 \u6539\u8fdb\u89e3\u51b3\u65b9\u6848<\/p>\n<p>Since we\u2019re returning just a subset of results to the caller, we might as\u200c well have a PagedList instead of List.<br \/>\n\u7531\u4e8e\u6211\u4eec\u53ea\u5411\u8c03\u7528\u8005\u8fd4\u56de\u7ed3\u679c\u7684\u5b50\u96c6\uff0c\u56e0\u6b64\u6211\u4eec\u4e5f\u53ef\u4ee5\u4f7f\u7528 PagedList \u800c\u4e0d\u662f List\u3002<\/p>\n<p>PagedList will inherit from the List class and will add some more to it. We can also move the skip\/take logic to the PagedList since it makes more sense.<br \/>\nPagedList \u5c06\u4ece List \u7c7b\u7ee7\u627f\uff0c\u5e76\u5411\u5176\u6dfb\u52a0\u66f4\u591a\u5185\u5bb9\u3002\u6211\u4eec\u8fd8\u53ef\u4ee5\u5c06 skip\/take \u903b\u8f91\u79fb\u52a8\u5230 PagedList\uff0c\u56e0\u4e3a\u5b83\u66f4\u6709\u610f\u4e49\u3002<\/p>\n<p>So, let\u2019s first create a new MetaData class in the Shared\/RequestFeatures folder:<br \/>\n\u56e0\u6b64\uff0c\u8ba9\u6211\u4eec\u9996\u5148\u5728 Shared\/RequestFeatures \u6587\u4ef6\u5939\u4e2d\u521b\u5efa\u4e00\u4e2a\u65b0\u7684 MetaData \u7c7b\uff1a<\/p>\n<pre><code>namespace Shared.RequestFeatures\n{\n    public class MetaData { \n        public int CurrentPage { get; set; } \n        public int TotalPages { get; set; } \n        public int PageSize { get; set; } \n        public int TotalCount { get; set; } \n        public bool HasPrevious =&gt; CurrentPage &gt; 1; \n        public bool HasNext =&gt; CurrentPage &lt; TotalPages; }\n}<\/code><\/pre>\n<p>Then, we are going to implement the PagedList class in the same folder:<br \/>\n\u7136\u540e\uff0c\u6211\u4eec\u5c06\u5728\u540c\u4e00\u6587\u4ef6\u5939\u4e2d\u5b9e\u73b0 PagedList \u7c7b\uff1a<\/p>\n<pre><code>namespace Shared.RequestFeatures\n{\n    public class PagedList&lt;T&gt; : List&lt;T&gt;\n    {\n        public MetaData MetaData { get; set; }\n        public PagedList(List&lt;T&gt; items, int count, int pageNumber, int pageSize)\n        {\n            MetaData = new MetaData\n            {\n                TotalCount = count,\n                PageSize = pageSize,\n                CurrentPage = pageNumber,\n                TotalPages = (int)Math.Ceiling(count \/ (double)pageSize)\n            };\n            AddRange(items);\n        }\n\n        public static PagedList&lt;T&gt; ToPagedList(IEnumerable&lt;T&gt; source, int pageNumber, int pageSize)\n        {\n            var count = source.Count();\n            var items = source\n                .Skip((pageNumber - 1) * pageSize)\n                .Take(pageSize)\n                .ToList();\n\n            return new PagedList&lt;T&gt;(items, count, pageNumber, pageSize);\n        }\n    }\n}<\/code><\/pre>\n<p>As you can see, we\u2019ve transferred the skip\/take logic to the static method inside of the PagedList class. And in the MetaData class, we\u2019ve added a few more properties that will come in handy as metadata for our response.<br \/>\n\u5982\u4f60\u6240\u89c1\uff0c\u6211\u4eec\u5df2\u5c06 skip\/take \u903b\u8f91\u8f6c\u79fb\u5230 PagedList \u7c7b\u5185\u7684\u9759\u6001\u65b9\u6cd5\u3002\u5728 MetaData \u7c7b\u4e2d\uff0c\u6211\u4eec\u6dfb\u52a0\u4e86\u66f4\u591a\u5c5e\u6027\uff0c\u8fd9\u4e9b\u5c5e\u6027\u5c06\u4f5c\u4e3a\u54cd\u5e94\u7684\u5143\u6570\u636e\u6d3e\u4e0a\u7528\u573a\u3002<\/p>\n<p>HasPrevious is true if the CurrentPage is larger than 1, and HasNext is calculated if the CurrentPage is smaller than the number of total pages. TotalPages is calculated by dividing the number of items by the page size and then rounding it to the larger number since a page needs to exist even if there is only one item on it.<br \/>\n\u5982\u679c CurrentPage \u5927\u4e8e 1\uff0c\u5219 HasPrevious \u4e3a true\uff0c\u5982\u679c CurrentPage \u5c0f\u4e8e\u603b\u9875\u6570\uff0c\u5219\u8ba1\u7b97 HasNext\u3002TotalPages \u7684\u8ba1\u7b97\u65b9\u6cd5\u662f\u5c06\u9879\u76ee\u6570\u9664\u4ee5\u9875\u9762\u5927\u5c0f\uff0c\u7136\u540e\u5c06\u5176\u56db\u820d\u4e94\u5165\u4e3a\u66f4\u5927\u7684\u6570\u5b57\uff0c\u56e0\u4e3a\u5373\u4f7f\u9875\u9762\u4e0a\u53ea\u6709\u4e00\u4e2a\u9879\u76ee\uff0c\u9875\u9762\u4e5f\u9700\u8981\u5b58\u5728\u3002<\/p>\n<p>Now that we\u2019ve cleared that up, let\u2019s change our EmployeeRepository and EmployeesController accordingly.<br \/>\n\u73b0\u5728\u6211\u4eec\u5df2\u7ecf\u6e05\u9664\u4e86\u8fd9\u4e2a\u95ee\u9898\uff0c\u8ba9\u6211\u4eec\u76f8\u5e94\u5730\u66f4\u6539\u6211\u4eec\u7684 EmployeeRepository \u548c EmployeesController\u3002<\/p>\n<p>Let\u2019s start with the interface modification:<br \/>\n\u8ba9\u6211\u4eec\u4ece\u63a5\u53e3\u4fee\u6539\u5f00\u59cb\uff1a<\/p>\n<pre><code>\/\/ Task&lt;IEnumerable&lt;Employee&gt;&gt; GetEmployeesAsync(Guid companyId, EmployeeParameters employeeParameters, bool trackChanges);\nTask&lt;PagedList&lt;Employee&gt;&gt; GetEmployeesAsync(Guid companyId, EmployeeParameters employeeParameters, bool trackChanges);<\/code><\/pre>\n<p>Then, let\u2019s change the repository class:<br \/>\n\u7136\u540e\uff0c\u8ba9\u6211\u4eec\u66f4\u6539 repository \u7c7b\uff1a<\/p>\n<pre><code>\/\/  public async Task&lt;IEnumerable&lt;Employee&gt;&gt; GetEmployeesAsync(Guid companyId, EmployeeParameters employeeParameters, bool trackChanges) =&gt; \n\/\/await FindByCondition(e =&gt; e.CompanyId.Equals(companyId), trackChanges)\n\/\/.OrderBy(e =&gt; e.Name)\n\/\/.Skip((employeeParameters.PageNumber - 1) * employeeParameters.PageSize)\n\/\/.Take(employeeParameters.PageSize)\n\/\/.ToListAsync();\n\npublic async Task&lt;PagedList&lt;Employee&gt;&gt; GetEmployeesAsync(Guid companyId, EmployeeParameters employeeParameters, bool trackChanges) { \n    var employees = await FindByCondition(e =&gt; e.CompanyId.Equals(companyId), trackChanges).OrderBy(e =&gt; e.Name).ToListAsync(); \n    return PagedList&lt;Employee&gt;.ToPagedList(employees, employeeParameters.PageNumber, employeeParameters.PageSize); \n}<\/code><\/pre>\n<p>After that, we are going to modify the IEmplyeeService interface:<br \/>\n\u4e4b\u540e\uff0c\u6211\u4eec\u5c06\u4fee\u6539 IEmplyeeService \u63a5\u53e3\uff1a<\/p>\n<pre><code>\/\/ Task&lt;IEnumerable&lt;EmployeeDto&gt;&gt; GetEmployeesAsync(Guid companyId, EmployeeParameters employeeParameters, bool trackChanges);\nTask&lt;(IEnumerable&lt;EmployeeDto&gt; employees, MetaData metaData)&gt; GetEmployeesAsync(Guid companyId, EmployeeParameters employeeParameters, bool trackChanges);<\/code><\/pre>\n<p>Now our method returns a Tuple containing two fields \u2013 employees and metadata.<br \/>\n\u73b0\u5728\uff0c\u6211\u4eec\u7684\u65b9\u6cd5\u8fd4\u56de\u4e00\u4e2a\u5305\u542b\u4e24\u4e2a\u5b57\u6bb5\u7684 Tuple \u2013 employees \u548c metadata\u3002<\/p>\n<p>So, let\u2019s implement that in the EmployeeService class:<br \/>\n\u56e0\u6b64\uff0c\u8ba9\u6211\u4eec\u5728 EmployeeService \u7c7b\u4e2d\u5b9e\u73b0\u5b83\uff1a<\/p>\n<pre><code>public async Task&lt;(IEnumerable&lt;EmployeeDto&gt; employees, MetaData metaData)&gt; GetEmployeesAsync(Guid companyId, EmployeeParameters employeeParameters, bool trackChanges)\n{\n    await CheckIfCompanyExists(companyId, trackChanges);\n    var employeesWithMetaData = await _repository.Employee.GetEmployeesAsync(companyId, employeeParameters, trackChanges);\n    var employeesDto = _mapper.Map&lt;IEnumerable&lt;EmployeeDto&gt;&gt;(employeesWithMetaData);\n    return (employees: employeesDto, metaData: employeesWithMetaData.MetaData);\n}<\/code><\/pre>\n<p>We change the method signature and the name of the employeesFromDb variable to employeesWithMetaData since this name is now more suitable. After the mapping action, we construct a Tuple and return it to the caller.<br \/>\n\u6211\u4eec\u5c06\u65b9\u6cd5\u7b7e\u540d\u548c employeesFromDb \u53d8\u91cf\u7684\u540d\u79f0\u66f4\u6539\u4e3a employeesWithMetaData\uff0c\u56e0\u4e3a\u6b64\u540d\u79f0\u73b0\u5728\u66f4\u5408\u9002\u3002\u5728 mapping\u4f5c\u4e4b\u540e\uff0c\u6211\u4eec\u6784\u9020\u4e00\u4e2a Tuple \u5e76\u5c06\u5176\u8fd4\u56de\u7ed9\u8c03\u7528\u8005\u3002<\/p>\n<p>Finally, let\u2019s modify the controller:<br \/>\n\u6700\u540e\uff0c\u6211\u4eec\u6765\u4fee\u6539\u63a7\u5236\u5668\uff1a<\/p>\n<pre><code>[HttpGet]\npublic async Task&lt;IActionResult&gt; GetEmployeesForCompany(Guid companyId, [FromQuery] EmployeeParameters employeeParameters)\n{\n    var pagedResult = await _service.EmployeeService.GetEmployeesAsync(companyId, employeeParameters, trackChanges: false);\n    Response.Headers.Add(&quot;X-Pagination&quot;, JsonSerializer.Serialize(pagedResult.metaData));\n    return Ok(pagedResult.employees);\n}  <\/code><\/pre>\n<p>The new thing in this action is that we modify the response header and add our metadata as the X-Pagination header. For this, we need the System.Text.Json namespace.<br \/>\n\u8fd9\u4e2a\u52a8\u4f5c\u7684\u65b0\u5185\u5bb9\u662f\u6211\u4eec\u4fee\u6539\u54cd\u5e94\u6807\u5934\u5e76\u5c06\u6211\u4eec\u7684\u5143\u6570\u636e\u6dfb\u52a0\u4e3a X-Pagination \u6807\u5934\u3002\u4e3a\u6b64\uff0c\u6211\u4eec\u9700\u8981 System.Text.Json \u547d\u540d\u7a7a\u95f4\u3002<\/p>\n<p>Now, if we send the same request we did earlier, we are going to get the same result:<br \/>\n\u73b0\u5728\uff0c\u5982\u679c\u6211\u4eec\u53d1\u9001\u4e4b\u524d\u6240\u505a\u7684\u76f8\u540c\u8bf7\u6c42\uff0c\u6211\u4eec\u5c06\u5f97\u5230\u76f8\u540c\u7684\u7ed3\u679c\uff1a<\/p>\n<p><a href=\"https:\/\/localhost:5001\/api\/companies\/C9D4C053-49B6-410C-BC78-2D54A9991870\/employees?pageNumber=2&amp;pageSize=2\">https:\/\/localhost:5001\/api\/companies\/C9D4C053-49B6-410C-BC78-2D54A9991870\/employees?pageNumber=2&pageSize=2<\/a><\/p>\n<p><img decoding=\"async\" src=\"\/images\/ultimateaspnetcorewebapi6\/1603.jpg\" alt=\"alt text\" \/><\/p>\n<p>But now we have some additional useful information in the X-Pagination response header:<br \/>\n\u4f46\u662f\u73b0\u5728\u6211\u4eec\u5728 X-Pagination \u54cd\u5e94\u6807\u5934\u4e2d\u6709\u4e00\u4e9b\u989d\u5916\u7684\u6709\u7528\u4fe1\u606f\uff1a<\/p>\n<p><img decoding=\"async\" src=\"\/images\/ultimateaspnetcorewebapi6\/1604.jpg\" alt=\"alt text\" \/><\/p>\n<p>As you can see, all of our metadata is here. We can use this information when building any kind of frontend pagination to our benefit. You can play around with different requests to see how it works in other scenarios.<br \/>\n\u5982\u60a8\u6240\u89c1\uff0c\u6211\u4eec\u6240\u6709\u7684\u5143\u6570\u636e\u90fd\u5728\u8fd9\u91cc\u3002\u6211\u4eec\u53ef\u4ee5\u5728\u6784\u5efa\u4efb\u4f55\u7c7b\u578b\u7684\u524d\u7aef\u5206\u9875\u65f6\u4f7f\u7528\u8fd9\u4e9b\u4fe1\u606f\u3002\u60a8\u53ef\u4ee5\u5c1d\u8bd5\u4e0d\u540c\u7684\u8bf7\u6c42\uff0c\u4ee5\u67e5\u770b\u5b83\u5728\u5176\u4ed6\u573a\u666f\u4e2d\u7684\u5de5\u4f5c\u539f\u7406\u3002<\/p>\n<p>We could also use this data to generate links to the previous and next pagination page on the backend, but that is part of the HATEOAS and is out of the scope of this chapter.<br \/>\n\u6211\u4eec\u4e5f\u53ef\u4ee5\u4f7f\u7528\u8fd9\u4e9b\u6570\u636e\u5728\u540e\u7aef\u751f\u6210\u6307\u5411\u4e0a\u4e00\u4e2a\u548c\u4e0b\u4e00\u4e2a\u5206\u9875\u9875\u9762\u7684\u94fe\u63a5\uff0c\u4f46\u8fd9\u662f HATEOAS \u7684\u4e00\u90e8\u5206\uff0c\u8d85\u51fa\u4e86\u672c\u7ae0\u7684\u8303\u56f4\u3002<\/p>\n<h3>16.4.1 Additional Advice\u200c<\/h3>\n<p>16.4.1 \u5176\u4ed6\u5efa\u8bae<\/p>\n<p>This solution works great with a small amount of data, but with bigger tables with millions of rows, we can improve it by modifying the GetEmployeesAsync repository method:<br \/>\n\u6b64\u89e3\u51b3\u65b9\u6848\u9002\u7528\u4e8e\u5c11\u91cf\u6570\u636e\uff0c\u4f46\u5bf9\u4e8e\u5177\u6709\u6570\u767e\u4e07\u884c\u7684\u8f83\u5927\u8868\uff0c\u6211\u4eec\u53ef\u4ee5\u901a\u8fc7\u4fee\u6539 GetEmployeesAsync \u5b58\u50a8\u5e93\u65b9\u6cd5\u6765\u6539\u8fdb\u5b83\uff1a<\/p>\n<pre><code>public async Task&lt;PagedList&lt;Employee&gt;&gt; GetEmployeesAsync(Guid companyId, EmployeeParameters employeeParameters, bool trackChanges)\n{\n    var employees = await FindByCondition(e =&gt; e.CompanyId.Equals(companyId), trackChanges)\n        .OrderBy(e =&gt; e.Name)\n        .Skip((employeeParameters.PageNumber - 1) * employeeParameters.PageSize)\n        .Take(employeeParameters.PageSize)\n        .ToListAsync();\n    var count = await FindByCondition(e =&gt; e.CompanyId.Equals(companyId), trackChanges).CountAsync();\n    return new PagedList&lt;Employee&gt;(employees, count, employeeParameters.PageNumber, employeeParameters.PageSize);\n}<\/code><\/pre>\n<p>Even though we have an additional call to the database with the CountAsync method, this solution was tested upon millions of rows and was much faster than the previous one. Because our table has few rows, we will continue using the previous solution, but feel free to switch to this one if you want.<br \/>\n\u5c3d\u7ba1\u6211\u4eec\u4f7f\u7528 CountAsync \u65b9\u6cd5\u5bf9\u6570\u636e\u5e93\u8fdb\u884c\u4e86\u989d\u5916\u7684\u8c03\u7528\uff0c\u4f46\u6b64\u89e3\u51b3\u65b9\u6848\u5df2\u5728\u6570\u767e\u4e07\u884c\u4e0a\u8fdb\u884c\u4e86\u6d4b\u8bd5\uff0c\u5e76\u4e14\u6bd4\u4ee5\u524d\u7684\u89e3\u51b3\u65b9\u6848\u5feb\u5f97\u591a\u3002\u7531\u4e8e\u6211\u4eec\u7684\u8868\u7684\u884c\u6570\u5f88\u5c11\uff0c\u56e0\u6b64\u6211\u4eec\u5c06\u7ee7\u7eed\u4f7f\u7528\u4ee5\u524d\u7684\u89e3\u51b3\u65b9\u6848\uff0c\u4f46\u5982\u679c\u9700\u8981\uff0c\u8bf7\u968f\u65f6\u5207\u6362\u5230\u6b64\u89e3\u51b3\u65b9\u6848\u3002<\/p>\n<p>Also, to enable the client application to read the new X-Pagination header that we\u2019ve added in our action, we have to modify the CORS configuration:<br \/>\n\u6b64\u5916\uff0c\u8981\u4f7f\u5ba2\u6237\u7aef\u5e94\u7528\u7a0b\u5e8f\u80fd\u591f\u8bfb\u53d6\u6211\u4eec\u5728\u4f5c\u4e2d\u6dfb\u52a0\u7684\u65b0 X-Pagination \u6807\u5934\uff0c\u6211\u4eec\u5fc5\u987b\u4fee\u6539 CORS \u914d\u7f6e\uff1a<\/p>\n<pre><code>public static class ServiceExtensions\n{\n    public static void ConfigureCors(this IServiceCollection services) =&gt;\n        services.AddCors(options =&gt;\n        {\n            options.AddPolicy(&quot;CorsPolicy&quot;, builder =&gt;\n            builder.AllowAnyOrigin()\n            .AllowAnyMethod()\n            .AllowAnyHeader()\n            .WithExposedHeaders(&quot;X-Pagination&quot;));\n        });\n\n        \/\/ ...\n}<\/code><\/pre>\n","protected":false},"excerpt":{"rendered":"<p>16 PAGING 16 \u5206\u9875 We have covered a lot of interesting features while creating our Web API project, but there are still things to do.\u200c \u5728\u521b\u5efa Web API \u9879\u76ee\u65f6\uff0c\u6211\u4eec\u5df2\u7ecf\u4ecb\u7ecd\u4e86\u8bb8\u591a\u6709\u8da3\u7684\u529f\u80fd\uff0c\u4f46\u4ecd\u6709\u4e00\u4e9b\u4e8b\u60c5\u8981\u505a\u3002 So, in this chapter, we\u2019re going to learn how to implement paging in ASP.NET Core Web API. It is one of the most important concepts in [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[],"class_list":["post-1130","post","type-post","status-publish","format-standard","hentry","category-uncategorized"],"_links":{"self":[{"href":"https:\/\/diji.net\/index.php?rest_route=\/wp\/v2\/posts\/1130","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/diji.net\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/diji.net\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/diji.net\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/diji.net\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=1130"}],"version-history":[{"count":0,"href":"https:\/\/diji.net\/index.php?rest_route=\/wp\/v2\/posts\/1130\/revisions"}],"wp:attachment":[{"href":"https:\/\/diji.net\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=1130"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/diji.net\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=1130"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/diji.net\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=1130"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}