{"id":1140,"date":"2025-05-27T14:47:27","date_gmt":"2025-05-27T06:47:27","guid":{"rendered":"https:\/\/www.hyy.net\/?p=1140"},"modified":"2025-05-27T14:47:27","modified_gmt":"2025-05-27T06:47:27","slug":"ultimate-asp-net-core-web-api-21-supporting-hateoas","status":"publish","type":"post","link":"https:\/\/diji.net\/?p=1140","title":{"rendered":"Ultimate ASP.NET Core Web API 21 SUPPORTING HATEOAS"},"content":{"rendered":"<p>21 SUPPORTING HATEOAS<br \/>\n21 \u652f\u6301 HATEOAS<\/p>\n<p>In this section, we are going to talk about one of the most important concepts in building RESTful APIs \u2014 HATEOAS and learn how to implement HATEOAS in ASP.NET Core Web API. This part relies heavily on the concepts we've implemented so far in paging, filtering, searching, sorting, and especially data shaping and builds upon the foundations we've put down in these parts.\u200c<br \/>\n\u5728\u672c\u8282\u4e2d\uff0c\u6211\u4eec\u5c06\u8ba8\u8bba\u6784\u5efa RESTful API \u4e2d\u6700\u91cd\u8981\u7684\u6982\u5ff5\u4e4b\u4e00 \u2014 HATEOAS\uff0c\u5e76\u5b66\u4e60\u5982\u4f55\u5728 ASP.NET Core Web API \u4e2d\u5b9e\u73b0 HATEOAS\u3002\u8fd9\u90e8\u5206\u5728\u5f88\u5927\u7a0b\u5ea6\u4e0a\u4f9d\u8d56\u4e8e\u6211\u4eec\u5230\u76ee\u524d\u4e3a\u6b62\u5728\u5206\u9875\u3001\u8fc7\u6ee4\u3001\u641c\u7d22\u3001\u6392\u5e8f\uff0c\u5c24\u5176\u662f\u6570\u636e\u8c03\u6574\u65b9\u9762\u5b9e\u73b0\u7684\u6982\u5ff5\uff0c\u5e76\u5efa\u7acb\u5728\u6211\u4eec\u5728\u8fd9\u4e9b\u90e8\u5206\u4e2d\u5960\u5b9a\u7684\u57fa\u7840\u4e4b\u4e0a\u3002<\/p>\n<h2>21.1 What is HATEOAS and Why is it so Important?<\/h2>\n<p>21.1 \u4ec0\u4e48\u662f HATEOAS\uff0c\u4e3a\u4ec0\u4e48\u5b83\u5982\u6b64\u91cd\u8981\uff1f<\/p>\n<p>HATEOAS (Hypermedia as the Engine of Application State) is a very important REST constraint. Without it, a REST API cannot be considered RESTful and many of the benefits we get by implementing a REST architecture are unavailable.\u200c<br \/>\nHATEOAS\uff08\u8d85\u5a92\u4f53\u4f5c\u4e3a\u5e94\u7528\u7a0b\u5e8f\u72b6\u6001\u7684\u5f15\u64ce\uff09\u662f\u4e00\u4e2a\u975e\u5e38\u91cd\u8981\u7684 REST \u7ea6\u675f\u3002\u6ca1\u6709\u5b83\uff0cREST API \u5c31\u4e0d\u80fd\u88ab\u89c6\u4e3a RESTful\uff0c\u5e76\u4e14\u6211\u4eec\u65e0\u6cd5\u901a\u8fc7\u5b9e\u65bd REST \u67b6\u6784\u83b7\u5f97\u8bb8\u591a\u597d\u5904\u3002<\/p>\n<p>Hypermedia refers to any kind of content that contains links to media types such as documents, images, videos, etc.<br \/>\n\u8d85\u5a92\u4f53\u662f\u6307\u5305\u542b\u6307\u5411\u5a92\u4f53\u7c7b\u578b\uff08\u5982\u6587\u6863\u3001\u56fe\u50cf\u3001\u89c6\u9891\u7b49\uff09\u7684\u94fe\u63a5\u7684\u4efb\u4f55\u7c7b\u578b\u7684\u5185\u5bb9\u3002<\/p>\n<p>REST architecture allows us to generate hypermedia links in our responses dynamically and thus make navigation much easier. To put this into perspective, think about a website that uses hyperlinks to help you navigate to different parts of it. You can achieve the same effect with HATEOAS in your REST API.<br \/>\nREST \u67b6\u6784\u5141\u8bb8\u6211\u4eec\u5728\u54cd\u5e94\u4e2d\u52a8\u6001\u751f\u6210\u8d85\u5a92\u4f53\u94fe\u63a5\uff0c\u4ece\u800c\u4f7f\u5bfc\u822a\u53d8\u5f97\u66f4\u52a0\u5bb9\u6613\u3002\u4e3a\u4e86\u6b63\u786e\u770b\u5f85\u8fd9\u4e00\u70b9\uff0c\u8bf7\u8003\u8651\u4e00\u4e2a\u4f7f\u7528\u8d85\u94fe\u63a5\u6765\u5e2e\u52a9\u60a8\u5bfc\u822a\u5230\u5176\u4e0d\u540c\u90e8\u5206\u7684\u7f51\u7ad9\u3002\u60a8\u53ef\u4ee5\u5728 REST API \u4e2d\u4f7f\u7528 HATEOAS \u5b9e\u73b0\u76f8\u540c\u7684\u6548\u679c\u3002<\/p>\n<p>Imagine a website that has a home page and you land on it, but there are no links anywhere. You need to scrape the website or find some other way to navigate it to get to the content you want. We're not saying that the website is the same as a REST API, but you get the point.<br \/>\n\u60f3\u8c61\u4e00\u4e0b\uff0c\u4e00\u4e2a\u7f51\u7ad9\u6709\u4e00\u4e2a\u4e3b\u9875\uff0c\u4f60\u767b\u9646\u5b83\uff0c\u4f46\u4efb\u4f55\u5730\u65b9\u90fd\u6ca1\u6709\u94fe\u63a5\u3002\u60a8\u9700\u8981\u6293\u53d6\u7f51\u7ad9\u6216\u627e\u5230\u5176\u4ed6\u65b9\u6cd5\u6765\u5bfc\u822a\u5b83\u4ee5\u8bbf\u95ee\u60a8\u60f3\u8981\u7684\u5185\u5bb9\u3002\u6211\u4eec\u5e76\u4e0d\u662f\u8bf4\u8be5\u7f51\u7ad9\u4e0e REST API \u76f8\u540c\uff0c\u4f46\u60a8\u660e\u767d\u4e86\u3002<\/p>\n<p>The power of being able to explore an API on your own can be very useful.<br \/>\n\u80fd\u591f\u81ea\u884c\u63a2\u7d22 API \u7684\u529f\u80fd\u53ef\u80fd\u975e\u5e38\u6709\u7528\u3002<\/p>\n<p>Let's see how that works.<br \/>\n\u8ba9\u6211\u4eec\u770b\u770b\u5b83\u662f\u5982\u4f55\u5de5\u4f5c\u7684\u3002<\/p>\n<h3>21.1.1 Typical Response with HATEOAS Implemented<\/h3>\n<p>21.1.1 \u5b9e\u65bdHATEOAS \u7684\u5178\u578b\u54cd\u5e94<\/p>\n<p>Once we implement HATEOAS in our API, we are going to have this type of response:\u200c<br \/>\n\u5728 API \u4e2d\u5b9e\u65bd HATEOAS \u540e\uff0c\u6211\u4eec\u5c06\u5f97\u5230\u8fd9\u79cd\u7c7b\u578b\u7684\u54cd\u5e94\uff1a<\/p>\n<p><img decoding=\"async\" src=\"\/images\/ultimateaspnetcorewebapi6\/2101.jpg\" alt=\"alt text\" \/><\/p>\n<p>As you can see, we got the list of our employees and for each employee all the actions we can perform on them. And so on...<br \/>\n\u5982\u60a8\u6240\u89c1\uff0c\u6211\u4eec\u5f97\u5230\u4e86\u6211\u4eec\u7684\u5458\u5de5\u540d\u5355\u4ee5\u53ca\u6211\u4eec\u53ef\u4ee5\u5bf9\u6bcf\u4f4d\u5458\u5de5\u6267\u884c\u7684\u6240\u6709\u4f5c\u3002\u7b49\u7b49......<\/p>\n<p>So, it's a nice way to make an API self-discoverable and evolvable.<br \/>\n\u56e0\u6b64\uff0c\u8fd9\u662f\u4f7f API \u53ef\u81ea\u6211\u53d1\u73b0\u548c\u53ef\u6f14\u5316\u7684\u597d\u65b9\u6cd5\u3002<\/p>\n<h3>21.1.2 What is a Link?\u200c<\/h3>\n<p>21.1.2 \u4ec0\u4e48\u662f\u94fe\u63a5\uff1f<\/p>\n<p>According to RFC5988, a link is &quot;a typed connection between two resources that are identified by Internationalised Resource Identifiers (IRIs)&quot;. Simply put, we use links to traverse the internet or rather the resources on the internet.<br \/>\n\u6839\u636e RFC5988 \u7684\u8bf4\u6cd5\uff0c\u94fe\u63a5\u662f\u201c\u7531\u56fd\u9645\u5316\u8d44\u6e90\u6807\u8bc6\u7b26 \uff08IRI\uff09 \u6807\u8bc6\u7684\u4e24\u4e2a\u8d44\u6e90\u4e4b\u95f4\u7684\u7c7b\u578b\u5316\u8fde\u63a5\u201d\u3002\u7b80\u800c\u8a00\u4e4b\uff0c\u6211\u4eec\u4f7f\u7528\u94fe\u63a5\u6765\u904d\u5386 Internet\uff0c\u6216\u8005\u66f4\u786e\u5207\u5730\u8bf4\u662f Internet \u4e0a\u7684\u8d44\u6e90\u3002<\/p>\n<p>Our responses contain an array of links, which consist of a few properties according to the RFC:<br \/>\n\u6211\u4eec\u7684\u54cd\u5e94\u5305\u542b\u4e00\u7cfb\u5217\u94fe\u63a5\uff0c\u6839\u636e RFC\uff0c\u8fd9\u4e9b\u94fe\u63a5\u7531\u4e00\u4e9b\u5c5e\u6027\u7ec4\u6210\uff1a<\/p>\n<p>\u2022 href - represents a target URI.<br \/>\nhref - \u8868\u793a\u76ee\u6807 URI\u3002<\/p>\n<p>\u2022 rel - represents a link relation type, which means it describes how the current context is related to the target resource.<br \/>\nrel -\u8868\u793a\u94fe\u63a5\u5173\u7cfb\u7c7b\u578b\uff0c\u8fd9\u610f\u5473\u7740\u5b83\u63cf\u8ff0\u5f53\u524d\u4e0a\u4e0b\u6587\u4e0e\u76ee\u6807\u8d44\u6e90\u7684\u5173\u7cfb\u3002<\/p>\n<p>\u2022 method - we need an HTTP method to know how to distinguish the same target URIs.<br \/>\nmethod - \u6211\u4eec\u9700\u8981\u4e00\u4e2a HTTP \u65b9\u6cd5\u6765\u4e86\u89e3\u5982\u4f55\u533a\u5206\u76f8\u540c\u7684\u76ee\u6807 URI\u3002<\/p>\n<h3>21.1.3 Pros\/Cons of Implementing HATEOAS<\/h3>\n<p>21.1.3 \u5b9e\u65bd HATEOAS \u7684\u5229\u5f0a<\/p>\n<p>So, what are all the benefits we can expect when implementing HATEOAS?<br \/>\n\u90a3\u4e48\uff0c\u5b9e\u65bd HATEOAS \u65f6\u6211\u4eec\u53ef\u4ee5\u671f\u5f85\u7684\u6240\u6709\u597d\u5904\u662f\u4ec0\u4e48\uff1f<\/p>\n<p>HATEOAS is not trivial to implement, but the rewards we reap are worth it. Here are the things we can expect to get when we implement HATEOAS:<br \/>\nHATEOAS \u7684\u5b9e\u65bd\u5e76\u975e\u6613\u4e8b\uff0c\u4f46\u6211\u4eec\u83b7\u5f97\u7684\u56de\u62a5\u662f\u503c\u5f97\u7684\u3002\u4ee5\u4e0b\u662f\u6211\u4eec\u5728\u5b9e\u65bd HATEOAS \u65f6\u53ef\u4ee5\u9884\u671f\u83b7\u5f97\u7684\u4e1c\u897f\uff1a<\/p>\n<p>\u2022 API becomes self-discoverable and explorable.<br \/>\nAPI \u53d8\u5f97\u53ef\u81ea\u6211\u53d1\u73b0\u548c\u53ef\u63a2\u7d22\u3002<\/p>\n<p>\u2022 A client can use the links to implement its logic, it becomes much easier, and any changes that happen in the API structure are directly reflected onto the client.<br \/>\n\u5ba2\u6237\u7aef\u53ef\u4ee5\u4f7f\u7528\u94fe\u63a5\u6765\u5b9e\u73b0\u5176\u903b\u8f91\uff0c\u8fd9\u53d8\u5f97\u66f4\u52a0\u5bb9\u6613\uff0c\u5e76\u4e14 API \u7ed3\u6784\u4e2d\u53d1\u751f\u7684\u4efb\u4f55\u66f4\u6539\u90fd\u4f1a\u76f4\u63a5\u53cd\u6620\u5230\u5ba2\u6237\u7aef\u4e0a\u3002<\/p>\n<p>\u2022 The server drives the application state and URL structure and not vice versa.<br \/>\n\u670d\u52a1\u5668\u9a71\u52a8\u5e94\u7528\u7a0b\u5e8f\u72b6\u6001\u548c URL \u7ed3\u6784\uff0c\u53cd\u4e4b\u5219\u4e0d\u7136\u3002<\/p>\n<p>\u2022 The link relations can be used to point to the developer\u2019s documentation.<br \/>\n\u94fe\u63a5\u5173\u7cfb\u53ef\u7528\u4e8e\u6307\u5411\u5f00\u53d1\u4eba\u5458\u7684\u6587\u6863\u3002<\/p>\n<p>\u2022 Versioning through hyperlinks becomes easier.<br \/>\n\u901a\u8fc7\u8d85\u94fe\u63a5\u8fdb\u884c\u7248\u672c\u63a7\u5236\u53d8\u5f97\u66f4\u52a0\u5bb9\u6613\u3002<\/p>\n<p>\u2022 Reduced invalid state transaction calls.<br \/>\n\u51cf\u5c11\u4e86\u65e0\u6548\u72b6\u6001\u4e8b\u52a1\u8c03\u7528\u3002<\/p>\n<p>\u2022 API is evolvable without breaking all the clients.<br \/>\nAPI \u662f\u53ef\u6f14\u5316\u7684\uff0c\u800c\u4e0d\u4f1a\u7834\u574f\u6240\u6709\u5ba2\u6237\u7aef\u3002<\/p>\n<p>We can do so much with HATEOAS. But since it's not easy to implement all these features, we should keep in mind the scope of our API and if we need all this. There is a great difference between a high-volume public API and some internal API that is needed to communicate between parts of the same system.<br \/>\n\u6211\u4eec\u53ef\u4ee5\u7528 HATEOAS \u505a\u5f88\u591a\u4e8b\u60c5\u3002\u4f46\u662f\u7531\u4e8e\u5b9e\u73b0\u6240\u6709\u8fd9\u4e9b\u529f\u80fd\u5e76\u4e0d\u5bb9\u6613\uff0c\u6211\u4eec\u5e94\u8be5\u8bb0\u4f4f API \u7684\u8303\u56f4\u4ee5\u53ca\u6211\u4eec\u662f\u5426\u9700\u8981\u6240\u6709\u8fd9\u4e9b\u3002\u5927\u5bb9\u91cf\u516c\u5171 API \u4e0e\u5728\u540c\u4e00\u7cfb\u7edf\u7684\u5404\u4e2a\u90e8\u5206\u4e4b\u95f4\u8fdb\u884c\u901a\u4fe1\u6240\u9700\u7684\u4e00\u4e9b\u5185\u90e8 API \u4e4b\u95f4\u5b58\u5728\u5f88\u5927\u5dee\u5f02\u3002<\/p>\n<p>That is more than enough theory for now. Let's get to work and see what the concrete implementation of HATEOAS looks like.<br \/>\n\u76ee\u524d\uff0c\u8fd9\u5df2\u7ecf\u8db3\u591f\u4e86\u3002\u8ba9\u6211\u4eec\u5f00\u59cb\u5de5\u4f5c\uff0c\u770b\u770b HATEOAS \u7684\u5177\u4f53\u5b9e\u73b0\u662f\u4ec0\u4e48\u6837\u5b50\u3002<\/p>\n<h2>21.2 Adding Links in the Project<\/h2>\n<p>21.2 \u5728\u9879\u76ee\u4e2d\u6dfb\u52a0\u94fe\u63a5<\/p>\n<p>Let\u2019s begin with the concept we know so far, and that\u2019s the link. In the Entities project, we are going to create the LinkModels folder and inside a new Link class:\u200c<br \/>\n\u8ba9\u6211\u4eec\u4ece\u6211\u4eec\u76ee\u524d\u77e5\u9053\u7684\u6982\u5ff5\u5f00\u59cb\uff0c\u8fd9\u5c31\u662f\u94fe\u63a5\u3002\u5728 Entities \u9879\u76ee\u4e2d\uff0c\u6211\u4eec\u5c06\u521b\u5efa LinkModels \u6587\u4ef6\u5939\uff0c\u5e76\u5728\u65b0\u7684 Link \u7c7b\u4e2d\uff1a<\/p>\n<pre><code>public class Link { public string? Href { get; set; } public string? Rel { get; set; } public string? Method { get; set; } public Link() { } public Link(string href, string rel, string method) { Href = href; Rel = rel; Method = method; } }<\/code><\/pre>\n<p>Note that we have an empty constructor, too. We'll need that for XML serialization purposes, so keep it that way.<br \/>\n\u8bf7\u6ce8\u610f\uff0c\u6211\u4eec\u8fd8\u6709\u4e00\u4e2a\u7a7a\u7684\u6784\u9020\u51fd\u6570\u3002\u6211\u4eec\u5c06\u9700\u8981\u5b83\u6765\u8fdb\u884c XML \u5e8f\u5217\u5316\uff0c\u56e0\u6b64\u8bf7\u4fdd\u6301\u8fd9\u79cd\u65b9\u5f0f\u3002<\/p>\n<p>Next, we need to create a class that will contain all of our links \u2014 LinkResourceBase:<br \/>\n\u63a5\u4e0b\u6765\uff0c\u6211\u4eec\u9700\u8981\u521b\u5efa\u4e00\u4e2a\u5305\u542b\u6240\u6709\u94fe\u63a5\u7684\u7c7b \u2014 LinkResourceBase\uff1a<\/p>\n<pre><code>public class LinkResourceBase { public LinkResourceBase() {} public List&lt;Link&gt; Links { get; set; } = new List&lt;Link&gt;(); }<\/code><\/pre>\n<p>And finally, since our response needs to describe the root of the controller, we need a wrapper for our links:<br \/>\n\u6700\u540e\uff0c\u7531\u4e8e\u6211\u4eec\u7684\u54cd\u5e94\u9700\u8981\u63cf\u8ff0\u63a7\u5236\u5668\u7684\u6839\uff0c\u56e0\u6b64\u6211\u4eec\u9700\u8981\u4e00\u4e2a\u94fe\u63a5\u7684\u5305\u88c5\u5668\uff1a<\/p>\n<pre><code>public class LinkCollectionWrapper&lt;T&gt; : LinkResourceBase { public List&lt;T&gt; Value { get; set; } = new List&lt;T&gt;(); public LinkCollectionWrapper() { } public LinkCollectionWrapper(List&lt;T&gt; value) =&gt; Value = value; }<\/code><\/pre>\n<p>This class might not make too much sense right now, but stay with us and it will become clear later down the road. For now, let's just assume we wrapped our links in another class for response representation purposes.<br \/>\n\u8fd9\u95e8\u8bfe\u73b0\u5728\u53ef\u80fd\u6ca1\u6709\u592a\u5927\u610f\u4e49\uff0c\u4f46\u8bf7\u7559\u5728\u6211\u4eec\u8eab\u8fb9\uff0c\u7a0d\u540e\u4f1a\u53d8\u5f97\u6e05\u6670\u3002\u73b0\u5728\uff0c\u6211\u4eec\u53ea\u5047\u8bbe\u6211\u4eec\u5c06\u94fe\u63a5\u5305\u88c5\u5728\u53e6\u4e00\u4e2a\u7c7b\u4e2d\u4ee5\u7528\u4e8e\u54cd\u5e94\u8868\u793a\u76ee\u7684\u3002<\/p>\n<p>Since our response will contain links too, we need to extend the XML serialization rules so that our XML response returns the properly formatted links. Without this, we would get something like:<br \/>\n\u7531\u4e8e\u6211\u4eec\u7684\u54cd\u5e94\u4e5f\u5c06\u5305\u542b\u94fe\u63a5\uff0c\u56e0\u6b64\u6211\u4eec\u9700\u8981\u6269\u5c55 XML \u5e8f\u5217\u5316\u89c4\u5219\uff0c\u4ee5\u4fbf\u6211\u4eec\u7684 XML \u54cd\u5e94\u8fd4\u56de\u683c\u5f0f\u6b63\u786e\u7684\u94fe\u63a5\u3002\u5982\u679c\u6ca1\u6709\u8fd9\u4e2a\uff0c\u6211\u4eec\u4f1a\u5f97\u5230\u8fd9\u6837\u7684\u7ed3\u679c\uff1a<\/p>\n<p><code>&lt;Links&gt;System.Collections.Generic.List<\/code>1[Entites.Models.Link]<Links><code>. So, in the Entities\/Models\/Entity class, we need to extend the WriteLinksToXml method to support links: <code>&lt;Links&gt;System.Collections.Generic.List<\/code>1[Entites.Models.Link]<Links><\/code>  .\u56e0\u6b64\uff0c\u5728 Entities\/Models\/Entity \u7c7b\u4e2d\uff0c\u6211\u4eec\u9700\u8981\u6269\u5c55 WriteLinksToXml \u65b9\u6cd5\u4ee5\u652f\u6301\u94fe\u63a5\uff1a<\/p>\n<pre><code>private void WriteLinksToXml(string key, object value, XmlWriter writer) { writer.WriteStartElement(key); if (value.GetType() == typeof(List&lt;Link&gt;)) { foreach (var val in value as List&lt;Link&gt;) { writer.WriteStartElement(nameof(Link)); WriteLinksToXml(nameof(val.Href), val.Href, writer); WriteLinksToXml(nameof(val.Method), val.Method, writer); WriteLinksToXml(nameof(val.Rel), val.Rel, writer); writer.WriteEndElement(); } } else { writer.WriteString(value.ToString()); } writer.WriteEndElement(); }<\/code><\/pre>\n<p>So, we check if the type is <code>List&lt;Link&gt;<\/code>. If it is, we iterate through all the links and call the method recursively for each of the properties: href, method, and rel.<br \/>\n\u56e0\u6b64\uff0c\u6211\u4eec\u68c0\u67e5\u7c7b\u578b\u662f\u5426\u4e3a<code>List&lt;Link&gt;<\/code>\u3002\u5982\u679c\u662f\uff0c\u6211\u4eec\u904d\u5386\u6240\u6709\u94fe\u63a5\uff0c\u5e76\u4e3a\u6bcf\u4e2a\u5c5e\u6027\u9012\u5f52\u8c03\u7528\u8be5\u65b9\u6cd5\uff1ahref\u3001method \u548c rel\u3002<\/p>\n<p>That's all we need for now. We have a solid foundation to implement HATEOAS in our project.<br \/>\n\u8fd9\u5c31\u662f\u6211\u4eec\u73b0\u5728\u9700\u8981\u7684\u3002\u6211\u4eec\u6709\u575a\u5b9e\u7684\u57fa\u7840\u6765\u5728\u6211\u4eec\u7684\u9879\u76ee\u4e2d\u5b9e\u65bd HATEOAS\u3002<\/p>\n<h2>21.3 Additional Project Changes<\/h2>\n<p>21.3 \u5176\u4ed6 Project \u66f4\u6539<\/p>\n<p>When we generate links, HATEOAS strongly relies on having the ids available to construct the links for the response. Data shaping, on the\u200c other hand, enables us to return only the fields we want. So, if we want only the name and age fields, the id field won\u2019t be added. To solve that, we have to apply some changes.<br \/>\n\u5f53\u6211\u4eec\u751f\u6210\u94fe\u63a5\u65f6\uff0cHATEOAS \u5f3a\u70c8\u4f9d\u8d56\u4e8e\u53ef\u7528\u7684 id \u6765\u6784\u5efa\u54cd\u5e94\u7684\u94fe\u63a5\u3002\u53e6\u4e00\u65b9\u9762\uff0c\u6570\u636e\u6574\u5f62\u4f7f\u6211\u4eec\u80fd\u591f\u4ec5\u8fd4\u56de\u6211\u4eec\u60f3\u8981\u7684\u5b57\u6bb5\u3002\u56e0\u6b64\uff0c\u5982\u679c\u6211\u4eec\u53ea\u9700\u8981 name \u548c age \u5b57\u6bb5\uff0c\u5219\u4e0d\u4f1a\u6dfb\u52a0 id \u5b57\u6bb5\u3002\u4e3a\u4e86\u89e3\u51b3\u8fd9\u4e2a\u95ee\u9898\uff0c\u6211\u4eec\u5fc5\u987b\u5e94\u7528\u4e00\u4e9b\u66f4\u6539\u3002<\/p>\n<p>The first thing we are going to do is to add a ShapedEntity class in the Entities\/Models folder:<br \/>\n\u6211\u4eec\u8981\u505a\u7684\u7b2c\u4e00\u4ef6\u4e8b\u662f\u5728 Entities\/Models \u6587\u4ef6\u5939\u4e2d\u6dfb\u52a0\u4e00\u4e2a ShapedEntity \u7c7b\uff1a<\/p>\n<pre><code>public class ShapedEntity { public ShapedEntity() { Entity = new Entity(); } public Guid Id { get; set; } public Entity Entity { get; set; } }<\/code><\/pre>\n<p>With this class, we expose the Entity and the Id property as well.<br \/>\n\u4f7f\u7528\u6b64\u7c7b\uff0c\u6211\u4eec\u8fd8\u516c\u5f00\u4e86 Entity \u548c Id \u5c5e\u6027\u3002<\/p>\n<p>Now, we have to modify the IDataShaper interface and the DataShaper class by replacing all Entity usage with ShapedEntity.<br \/>\n\u73b0\u5728\uff0c\u6211\u4eec\u5fc5\u987b\u4fee\u6539 IDataShaper \u63a5\u53e3\u548c DataShaper \u7c7b\uff0c\u5c06\u6240\u6709 Entity \u7528\u6cd5\u66ff\u6362\u4e3a ShapedEntity\u3002<\/p>\n<p>In addition to that, we need to extend the FetchDataForEntity method in the DataShaper class to get the id separately:<br \/>\n\u9664\u6b64\u4e4b\u5916\uff0c\u6211\u4eec\u8fd8\u9700\u8981\u5728 DataShaper \u7c7b\u4e2d\u6269\u5c55 FetchDataForEntity \u65b9\u6cd5\uff0c\u4ee5\u5355\u72ec\u83b7\u53d6 id\uff1a<\/p>\n<pre><code>private ShapedEntity FetchDataForEntity(T entity, IEnumerable&lt;PropertyInfo&gt; requiredProperties) { var shapedObject = new ShapedEntity(); foreach (var property in requiredProperties) { var objectPropertyValue = property.GetValue(entity); shapedObject.Entity.TryAdd(property.Name, objectPropertyValue); } var objectProperty = entity.GetType().GetProperty(&quot;Id&quot;); shapedObject.Id = (Guid)objectProperty.GetValue(entity); return shapedObject; }<\/code><\/pre>\n<p>Finally, let\u2019s add the LinkResponse class in the LinkModels folder; that will help us with the response once we start with the HATEOAS implementation:<br \/>\n\u6700\u540e\uff0c\u8ba9\u6211\u4eec\u5728 LinkModels \u6587\u4ef6\u5939\u4e2d\u6dfb\u52a0 LinkResponse \u7c7b;\u8fd9\u5c06\u6709\u52a9\u4e8e\u6211\u4eec\u5728\u5f00\u59cb HATEOAS \u5b9e\u73b0\u540e\u505a\u51fa\u54cd\u5e94\uff1a<\/p>\n<pre><code>public class LinkResponse\n{ public bool HasLinks { get; set; } public List&lt;Entity&gt; ShapedEntities { get; set; } public LinkCollectionWrapper&lt;Entity&gt; LinkedEntities { get; set; } public LinkResponse() { LinkedEntities = new LinkCollectionWrapper&lt;Entity&gt;(); ShapedEntities = new List&lt;Entity&gt;(); } }<\/code><\/pre>\n<p>With this class, we are going to know whether our response has links. If it does, we are going to use the LinkedEntities property. Otherwise, we are going to use the ShapedEntities property.<br \/>\n\u901a\u8fc7\u8fd9\u4e2a\u7c7b\uff0c\u6211\u4eec\u5c06\u77e5\u9053\u6211\u4eec\u7684\u54cd\u5e94\u662f\u5426\u6709\u94fe\u63a5\u3002\u5982\u679c\u662f\u8fd9\u6837\uff0c\u6211\u4eec\u5c06\u4f7f\u7528 LinkedEntities \u5c5e\u6027\u3002\u5426\u5219\uff0c\u6211\u4eec\u5c06\u4f7f\u7528 ShapedEntities \u5c5e\u6027\u3002<\/p>\n<h2>21.4 Adding Custom Media Types<\/h2>\n<p>21.4 \u6dfb\u52a0\u81ea\u5b9a\u4e49\u5a92\u4f53\u7c7b\u578b<\/p>\n<p>What we want to do is to enable links in our response only if it is explicitly asked for. To do that, we are going to introduce custom media types.\u200c<br \/>\n\u6211\u4eec\u60f3\u8981\u505a\u7684\u662f\uff0c\u53ea\u6709\u5728\u660e\u786e\u8981\u6c42\u7684\u60c5\u51b5\u4e0b\uff0c\u624d\u5728\u6211\u4eec\u7684\u54cd\u5e94\u4e2d\u542f\u7528\u94fe\u63a5\u3002\u4e3a\u6b64\uff0c\u6211\u4eec\u5c06\u5f15\u5165\u81ea\u5b9a\u4e49\u5a92\u4f53\u7c7b\u578b\u3002<\/p>\n<p>Before we start, let\u2019s see how we can create a custom media type. A custom media type should look something like this: application\/vnd.codemaze.hateoas+json. To compare it to the typical json media type which we use by default: application\/json.<br \/>\n\u5728\u5f00\u59cb\u4e4b\u524d\uff0c\u8ba9\u6211\u4eec\u770b\u770b\u5982\u4f55\u521b\u5efa\u81ea\u5b9a\u4e49\u5a92\u4f53\u7c7b\u578b\u3002\u81ea\u5b9a\u4e49\u5a92\u4f53\u7c7b\u578b\u5e94\u5982\u4e0b\u6240\u793a\uff1aapplication\/vnd.codemaze.hateoas+json\u3002\u5c06\u5176\u4e0e\u6211\u4eec\u9ed8\u8ba4\u4f7f\u7528\u7684\u5178\u578b json \u5a92\u4f53\u7c7b\u578b\u8fdb\u884c\u6bd4\u8f83\uff1aapplication\/json\u3002<\/p>\n<p>So let\u2019s break down the different parts of a custom media type:<br \/>\n\u56e0\u6b64\uff0c\u8ba9\u6211\u4eec\u5206\u89e3\u81ea\u5b9a\u4e49\u5a92\u4f53\u7c7b\u578b\u7684\u4e0d\u540c\u90e8\u5206\uff1a<\/p>\n<p>\u2022 vnd \u2013 vendor prefix; it\u2019s always there.<\/p>\n<p>\u2022 codemaze \u2013 vendor identifier; we\u2019ve chosen codemaze, because why not?<\/p>\n<p>\u2022 hateoas \u2013 media type name.<\/p>\n<p>\u2022 json \u2013 suffix; we can use it to describe if we want json or an XML response, for example.<\/p>\n<p>Now, let\u2019s implement that in our application.<br \/>\n\u73b0\u5728\uff0c\u8ba9\u6211\u4eec\u5728\u5e94\u7528\u7a0b\u5e8f\u4e2d\u5b9e\u73b0\u5b83\u3002<\/p>\n<h3>21.4.1 Registering Custom Media Types<\/h3>\n<p>21.4.1 \u6ce8\u518c\u81ea\u5b9a\u4e49\u5a92\u4f53\u7c7b\u578b<\/p>\n<p>First, we want to register our new custom media types in the middleware. Otherwise, we\u2019ll just get a 406 Not Acceptable message.<br \/>\n\u9996\u5148\uff0c\u6211\u4eec\u60f3\u5728\u4e2d\u95f4\u4ef6\u4e2d\u6ce8\u518c\u65b0\u7684\u81ea\u5b9a\u4e49\u5a92\u4f53\u7c7b\u578b\u3002\u5426\u5219\uff0c\u6211\u4eec\u53ea\u4f1a\u6536\u5230 406 Not Acceptable \u6d88\u606f\u3002<\/p>\n<p>Let\u2019s add a new extension method to our ServiceExtensions:<br \/>\n\u8ba9\u6211\u4eec\u5411 ServiceExtensions \u6dfb\u52a0\u65b0\u7684\u6269\u5c55\u65b9\u6cd5\uff1a<\/p>\n<pre><code>public static void AddCustomMediaTypes(this IServiceCollection services) { services.Configure&lt;MvcOptions&gt;(config =&gt; { var systemTextJsonOutputFormatter = config.OutputFormatters .OfType&lt;SystemTextJsonOutputFormatter&gt;()?.FirstOrDefault(); if (systemTextJsonOutputFormatter != null) { systemTextJsonOutputFormatter.SupportedMediaTypes .Add(&quot;application\/vnd.codemaze.hateoas+json&quot;); } var xmlOutputFormatter = config.OutputFormatters .OfType&lt;XmlDataContractSerializerOutputFormatter&gt;()? .FirstOrDefault(); if (xmlOutputFormatter != null) { xmlOutputFormatter.SupportedMediaTypes .Add(&quot;application\/vnd.codemaze.hateoas+xml&quot;); } }); }<\/code><\/pre>\n<p>We are registering two new custom media types for the JSON and XML output formatters. This ensures we don\u2019t get a 406 Not Acceptable response.<br \/>\n\u6211\u4eec\u6b63\u5728\u4e3a JSON \u548c XML \u8f93\u51fa\u683c\u5f0f\u5316\u7a0b\u5e8f\u6ce8\u518c\u4e24\u79cd\u65b0\u7684\u81ea\u5b9a\u4e49\u5a92\u4f53\u7c7b\u578b\u3002\u8fd9\u53ef\u786e\u4fdd\u6211\u4eec\u4e0d\u4f1a\u6536\u5230 406 Not Acceptable \u54cd\u5e94\u3002<\/p>\n<p>Now, we have to add that to the Program class, just after the AddControllers method:<br \/>\n\u73b0\u5728\uff0c\u6211\u4eec\u5fc5\u987b\u5c06\u5176\u6dfb\u52a0\u5230 Program \u7c7b\u4e2d\uff0c\u5c31\u5728 AddControllers \u65b9\u6cd5\u4e4b\u540e\uff1a<\/p>\n<pre><code>builder.Services.AddCustomMediaTypes();<\/code><\/pre>\n<p>Excellent. The registration process is done.<br \/>\n\u975e\u5e38\u597d\u3002\u6ce8\u518c\u8fc7\u7a0b\u5df2\u5b8c\u6210\u3002<\/p>\n<h3>21.4.2 Implementing a Media Type Validation Filter<\/h3>\n<p>21.4.2 \u5b9e\u73b0\u5a92\u4f53\u7c7b\u578b\u9a8c\u8bc1\u8fc7\u6ee4\u5668<\/p>\n<p>Now, since we\u2019ve implemented custom media types, we want our Accept header to be present in our requests so we can detect when the user requested the HATEOAS-enriched response.<br \/>\n\u73b0\u5728\uff0c\u7531\u4e8e\u6211\u4eec\u5df2\u7ecf\u5b9e\u73b0\u4e86\u81ea\u5b9a\u4e49\u5a92\u4f53\u7c7b\u578b\uff0c\u56e0\u6b64\u6211\u4eec\u5e0c\u671b Accept \u6807\u5934\u51fa\u73b0\u5728\u6211\u4eec\u7684\u8bf7\u6c42\u4e2d\uff0c\u4ee5\u4fbf\u6211\u4eec\u53ef\u4ee5\u68c0\u6d4b\u7528\u6237\u4f55\u65f6\u8bf7\u6c42\u4e86 HATEOAS \u6269\u5145\u7684\u54cd\u5e94\u3002<\/p>\n<p>To do that, we\u2019ll implement an ActionFilter in the Presentation project inside the ActionFilters folder, which will validate our Accept header and media types:<br \/>\n\u4e3a\u6b64\uff0c\u6211\u4eec\u5c06\u5728 ActionFilters \u6587\u4ef6\u5939\u5185\u7684 Presentation \u9879\u76ee\u4e2d\u5b9e\u73b0\u4e00\u4e2a ActionFilter\uff0c\u5b83\u5c06\u9a8c\u8bc1\u6211\u4eec\u7684 Accept \u6807\u5934\u548c\u5a92\u4f53\u7c7b\u578b\uff1a<\/p>\n<pre><code>public class ValidateMediaTypeAttribute : IActionFilter { public void OnActionExecuting(ActionExecutingContext context) { var acceptHeaderPresent = context.HttpContext .Request.Headers.ContainsKey(&quot;Accept&quot;); if (!acceptHeaderPresent) { context.Result = new BadRequestObjectResult($&quot;Accept header is missing.&quot;); return; } var mediaType = context.HttpContext .Request.Headers[&quot;Accept&quot;].FirstOrDefault(); if (!MediaTypeHeaderValue.TryParse(mediaType, out MediaTypeHeaderValue? outMediaType)) { context.Result = new BadRequestObjectResult($&quot;Media type not present. Please add Accept header with the required media type.&quot;); return; } context.HttpContext.Items.Add(&quot;AcceptHeaderMediaType&quot;, outMediaType); } public void OnActionExecuted(ActionExecutedContext context){} }<\/code><\/pre>\n<p>We check for the existence of the Accept header first. If it\u2019s not present, we return BadRequest. If it is, we parse the media type \u2014 and if there is no valid media type present, we return BadRequest.<br \/>\n\u6211\u4eec\u9996\u5148\u68c0\u67e5 Accept \u6807\u5934\u662f\u5426\u5b58\u5728\u3002\u5982\u679c\u4e0d\u5b58\u5728\uff0c\u5219\u8fd4\u56de BadRequest\u3002\u5982\u679c\u662f\uff0c\u6211\u4eec\u89e3\u6790\u5a92\u4f53\u7c7b\u578b\u2014\u2014\u5982\u679c\u4e0d\u5b58\u5728\u6709\u6548\u7684\u5a92\u4f53\u7c7b\u578b\uff0c\u6211\u4eec\u8fd4\u56de BadRequest\u3002<\/p>\n<p>Once we\u2019ve passed the validation checks, we pass the parsed media type to the HttpContext of the controller.<br \/>\n\u901a\u8fc7\u9a8c\u8bc1\u68c0\u67e5\u540e\uff0c\u6211\u4eec\u5c06\u89e3\u6790\u7684\u5a92\u4f53\u7c7b\u578b\u4f20\u9012\u7ed9\u63a7\u5236\u5668\u7684 HttpContext\u3002<\/p>\n<p>Now, we have to register the filter in the Program class:<br \/>\n\u73b0\u5728\uff0c\u6211\u4eec\u5fc5\u987b\u5728 Program \u7c7b\u4e2d\u6ce8\u518c\u8fc7\u6ee4\u5668\uff1a<\/p>\n<pre><code>builder.Services.AddScoped&lt;ValidateMediaTypeAttribute&gt;();<\/code><\/pre>\n<p>And to decorate the GetEmployeesForCompany action:<br \/>\n\u8981\u4fee\u9970 GetEmployeesForCompany\u4f5c\uff0c\u8bf7\u6267\u884c\u4ee5\u4e0b\u4f5c\uff1a<\/p>\n<pre><code>[HttpGet] [ServiceFilter(typeof(ValidateMediaTypeAttribute))] public async Task&lt;IActionResult&gt; GetEmployeesForCompany(Guid companyId, [FromQuery] EmployeeParameters employeeParameters)<\/code><\/pre>\n<p>Great job.<br \/>\n\u5e72\u5f97\u597d\u3002<\/p>\n<p>Finally, we can work on the HATEOAS implementation.<br \/>\n\u6700\u540e\uff0c\u6211\u4eec\u53ef\u4ee5\u8fdb\u884c HATEOAS \u5b9e\u73b0\u3002<\/p>\n<h2>21.5 Implementing HATEOAS<\/h2>\n<p>21.5 \u5b9e\u65bd HATEOAS<\/p>\n<p>We are going to start by creating a new interface in the Contracts\u200c project:<br \/>\n\u9996\u5148\uff0c\u5728 Contracts \u9879\u76ee\u4e2d\u521b\u5efa\u4e00\u4e2a\u65b0\u63a5\u53e3\uff1a<\/p>\n<pre><code>public interface IEmployeeLinks { LinkResponse TryGenerateLinks(IEnumerable&lt;EmployeeDto&gt; employeesDto, string fields, Guid companyId, HttpContext httpContext); }<\/code><\/pre>\n<p>Currently, you will get the error about HttpContext, but we will solve that a bit later.<br \/>\n\u76ee\u524d\uff0c\u60a8\u5c06\u6536\u5230\u6709\u5173 HttpContext \u7684\u9519\u8bef\uff0c\u4f46\u6211\u4eec\u7a0d\u540e\u4f1a\u89e3\u51b3\u8fd9\u4e2a\u95ee\u9898\u3002<\/p>\n<p>Let\u2019s continue by creating a new Utility folder in the main project and the EmployeeLinks class in it. Let\u2019s start by adding the required dependencies inside the class:<br \/>\n\u8ba9\u6211\u4eec\u7ee7\u7eed\u5728\u4e3b\u9879\u76ee\u4e2d\u521b\u5efa\u4e00\u4e2a\u65b0\u7684 Utility \u6587\u4ef6\u5939\uff0c\u5e76\u5728\u5176\u4e2d\u521b\u5efa\u4e00\u4e2a EmployeeLinks \u7c7b\u3002\u8ba9\u6211\u4eec\u4ece\u5728\u7c7b\u4e2d\u6dfb\u52a0\u6240\u9700\u7684\u4f9d\u8d56\u9879\u5f00\u59cb\uff1a<\/p>\n<pre><code>public class EmployeeLinks : IEmployeeLinks { private readonly LinkGenerator _linkGenerator; private readonly IDataShaper&lt;EmployeeDto&gt; _dataShaper; public EmployeeLinks(LinkGenerator linkGenerator, IDataShaper&lt;EmployeeDto&gt; dataShaper) { _linkGenerator = linkGenerator; _dataShaper = dataShaper; } }<\/code><\/pre>\n<p>We are going to use LinkGenerator to generate links for our responses and IDataShaper to shape our data. As you can see, the shaping logic is now extracted from the EmployeeService class, which we will modify a bit later.<br \/>\n\u6211\u4eec\u5c06\u4f7f\u7528 LinkGenerator \u4e3a\u6211\u4eec\u7684\u54cd\u5e94\u751f\u6210\u94fe\u63a5\uff0c\u5e76\u4f7f\u7528 IDataShaper \u6765\u5851\u9020\u6211\u4eec\u7684\u6570\u636e\u3002\u5982\u60a8\u6240\u89c1\uff0c\u8c03\u6574\u903b\u8f91\u73b0\u5728\u662f\u4ece EmployeeService \u7c7b\u4e2d\u63d0\u53d6\u7684\uff0c\u6211\u4eec\u7a0d\u540e\u5c06\u5bf9\u5176\u8fdb\u884c\u4fee\u6539\u3002<\/p>\n<p>After dependencies, we are going to add the first method:<br \/>\n\u5728\u4f9d\u8d56\u9879\u4e4b\u540e\uff0c\u6211\u4eec\u5c06\u6dfb\u52a0\u7b2c\u4e00\u4e2a\u65b9\u6cd5\uff1a<\/p>\n<pre><code>public LinkResponse TryGenerateLinks(IEnumerable&lt;EmployeeDto&gt; employeesDto, string fields, Guid companyId, HttpContext httpContext) { var shapedEmployees = ShapeData(employeesDto, fields); if (ShouldGenerateLinks(httpContext)) return ReturnLinkdedEmployees(employeesDto, fields, companyId, httpContext, shapedEmployees); return ReturnShapedEmployees(shapedEmployees);}<\/code><\/pre>\n<p>So, our method accepts four parameters. The employeeDto collection, the fields that are going to be used to shape the previous collection, companyId because routes to the employee resources contain the Id from the company, and httpContext which holds information about media types.<br \/>\n\u56e0\u6b64\uff0c\u6211\u4eec\u7684\u65b9\u6cd5\u63a5\u53d7\u56db\u4e2a\u53c2\u6570\u3002employeeDto \u96c6\u5408\u3001\u5c06\u7528\u4e8e\u5851\u9020\u524d\u4e00\u4e2a\u96c6\u5408\u7684\u5b57\u6bb5\u3001companyId\uff08\u56e0\u4e3a\u5230\u5458\u5de5\u8d44\u6e90\u7684\u8def\u7531\u5305\u542b\u6765\u81ea\u516c\u53f8\u7684 Id\uff09\u548c httpContext\uff08\u4fdd\u5b58\u6709\u5173\u5a92\u4f53\u7c7b\u578b\u7684\u4fe1\u606f\uff09\u3002<\/p>\n<p>The first thing we do is shape our collection. Then if the httpContext contains the required media type, we add links to the response. On the other hand, we just return our shaped data.<br \/>\n\u6211\u4eec\u505a\u7684\u7b2c\u4e00\u4ef6\u4e8b\u662f\u5851\u9020\u6211\u4eec\u7684\u6536\u85cf\u3002\u7136\u540e\uff0c\u5982\u679c httpContext \u5305\u542b\u6240\u9700\u7684\u5a92\u4f53\u7c7b\u578b\uff0c\u6211\u4eec\u5c06\u6dfb\u52a0\u6307\u5411\u54cd\u5e94\u7684\u94fe\u63a5\u3002\u53e6\u4e00\u65b9\u9762\uff0c\u6211\u4eec\u53ea\u8fd4\u56de\u6211\u4eec\u7684 shaped \u6570\u636e\u3002<\/p>\n<p>Of course, we have to add those not implemented methods:<br \/>\n\u5f53\u7136\uff0c\u6211\u4eec\u5fc5\u987b\u6dfb\u52a0\u90a3\u4e9b\u672a\u5b9e\u73b0\u7684\u65b9\u6cd5\uff1a<\/p>\n<pre><code>private List&lt;Entity&gt; ShapeData(IEnumerable&lt;EmployeeDto&gt; employeesDto, string fields) =&gt; _dataShaper.ShapeData(employeesDto, fields) .Select(e =&gt; e.Entity) .ToList();<\/code><\/pre>\n<p>The ShapeData method executes data shaping and extracts only the entity part without the Id property.<br \/>\nShapeData \u65b9\u6cd5\u6267\u884c\u6570\u636e\u8c03\u6574\uff0c\u5e76\u4ec5\u63d0\u53d6\u4e0d\u5e26 Id \u5c5e\u6027\u7684\u5b9e\u4f53\u90e8\u5206\u3002<\/p>\n<p>Let\u2019s add two additional methods:<br \/>\n\u8ba9\u6211\u4eec\u6dfb\u52a0\u4e24\u4e2a\u989d\u5916\u7684\u65b9\u6cd5\uff1a<\/p>\n<pre><code>private bool ShouldGenerateLinks(HttpContext httpContext) { var mediaType = (MediaTypeHeaderValue)httpContext.Items[&quot;AcceptHeaderMediaType&quot;]; return mediaType.SubTypeWithoutSuffix.EndsWith(&quot;hateoas&quot;, StringComparison.InvariantCultureIgnoreCase); } private LinkResponse ReturnShapedEmployees(List&lt;Entity&gt; shapedEmployees) =&gt; new LinkResponse { ShapedEntities = shapedEmployees };<\/code><\/pre>\n<p>In the ShouldGenerateLinks method, we extract the media type from the httpContext. If that media type ends with hateoas, the method returns true; otherwise, it returns false. The ReturnShapedEmployees method just returns a new LinkResponse with the ShapedEntities property populated. By default, the HasLinks property is false.<br \/>\n\u5728 ShouldGenerateLinks \u65b9\u6cd5\u4e2d\uff0c\u6211\u4eec\u4ece httpContext \u4e2d\u63d0\u53d6\u5a92\u4f53\u7c7b\u578b\u3002\u5982\u679c\u8be5\u5a92\u4f53\u7c7b\u578b\u4ee5 hateoas \u7ed3\u5c3e\uff0c\u5219\u8be5\u65b9\u6cd5\u8fd4\u56de true;\u5426\u5219\uff0c\u5b83\u5c06\u8fd4\u56de false\u3002ReturnShapedEmployees \u65b9\u6cd5\u53ea\u8fd4\u56de\u4e00\u4e2a\u586b\u5145\u4e86 ShapedEntities \u5c5e\u6027\u7684\u65b0 LinkResponse\u3002\u9ed8\u8ba4\u60c5\u51b5\u4e0b\uff0cHasLinks \u5c5e\u6027\u4e3a false\u3002<\/p>\n<p>After these methods, we have to add the ReturnLinkedEmployees method as well:<br \/>\n\u5728\u8fd9\u4e9b\u65b9\u6cd5\u4e4b\u540e\uff0c\u6211\u4eec\u8fd8\u5fc5\u987b\u6dfb\u52a0 ReturnLinkedEmployees \u65b9\u6cd5\uff1a<\/p>\n<pre><code>private LinkResponse ReturnLinkdedEmployees(IEnumerable&lt;EmployeeDto&gt; employeesDto, string fields, Guid companyId, HttpContext httpContext, List&lt;Entity&gt; shapedEmployees) { var employeeDtoList = employeesDto.ToList(); for (var index = 0; index &lt; employeeDtoList.Count(); index++) { var employeeLinks = CreateLinksForEmployee(httpContext, companyId, employeeDtoList[index].Id, fields); shapedEmployees[index].Add(&quot;Links&quot;, employeeLinks); } var employeeCollection = new LinkCollectionWrapper&lt;Entity&gt;(shapedEmployees); var linkedEmployees = CreateLinksForEmployees(httpContext, employeeCollection); return new LinkResponse { HasLinks = true, LinkedEntities = linkedEmployees }; }<\/code><\/pre>\n<p>In this method, we iterate through each employee and create links for it by calling the CreateLinksForEmployee method. Then, we just add it to the shapedEmployees collection. After that, we wrap the collection and create links that are important for the entire collection by calling the CreateLinksForEmployees method.<br \/>\n\u5728\u6b64\u65b9\u6cd5\u4e2d\uff0c\u6211\u4eec\u5faa\u73af\u8bbf\u95ee\u6bcf\u4e2a\u5458\u5de5\uff0c\u5e76\u901a\u8fc7\u8c03\u7528 CreateLinksForEmployee \u65b9\u6cd5\u4e3a\u5176\u521b\u5efa\u94fe\u63a5\u3002\u7136\u540e\uff0c\u6211\u4eec\u53ea\u9700\u5c06\u5176\u6dfb\u52a0\u5230 shapedEmployees \u96c6\u5408\u4e2d\u3002\u4e4b\u540e\uff0c\u6211\u4eec\u901a\u8fc7\u8c03\u7528 CreateLinksForEmployees \u65b9\u6cd5\u5305\u88c5\u96c6\u5408\u5e76\u521b\u5efa\u5bf9\u6574\u4e2a\u96c6\u5408\u5f88\u91cd\u8981\u7684\u94fe\u63a5\u3002<\/p>\n<p>Finally, we have to add those two new methods that create links:<br \/>\n\u6700\u540e\uff0c\u6211\u4eec\u5fc5\u987b\u6dfb\u52a0\u8fd9\u4e24\u4e2a\u521b\u5efa\u94fe\u63a5\u7684\u65b0\u65b9\u6cd5\uff1a<\/p>\n<pre><code>private List&lt;Link&gt; CreateLinksForEmployee(HttpContext httpContext, Guid companyId, Guid id, string fields = &quot;&quot;) { var links = new List&lt;Link&gt; { new Link(_linkGenerator.GetUriByAction(httpContext, &quot;GetEmployeeForCompany&quot;, values: new { companyId, id, fields }), &quot;self&quot;, &quot;GET&quot;), new Link(_linkGenerator.GetUriByAction(httpContext, &quot;DeleteEmployeeForCompany&quot;, values: new { companyId, id }), &quot;delete_employee&quot;, &quot;DELETE&quot;), new Link(_linkGenerator.GetUriByAction(httpContext, &quot;UpdateEmployeeForCompany&quot;, values: new { companyId, id }), &quot;update_employee&quot;, &quot;PUT&quot;), new Link(_linkGenerator.GetUriByAction(httpContext, &quot;PartiallyUpdateEmployeeForCompany&quot;, values: new { companyId, id }), &quot;partially_update_employee&quot;, &quot;PATCH&quot;) }; return links;\n} private LinkCollectionWrapper&lt;Entity&gt; CreateLinksForEmployees(HttpContext httpContext, LinkCollectionWrapper&lt;Entity&gt; employeesWrapper) { employeesWrapper.Links.Add(new Link(_linkGenerator.GetUriByAction(httpContext, &quot;GetEmployeesForCompany&quot;, values: new { }), &quot;self&quot;, &quot;GET&quot;)); return employeesWrapper; }<\/code><\/pre>\n<p>There are a few things to note here.<br \/>\n\u8fd9\u91cc\u6709\u51e0\u70b9\u9700\u8981\u6ce8\u610f\u3002<\/p>\n<p>We need to consider the fields while creating the links since we might be using them in our requests. We are creating the links by using the LinkGenerator\u2018s GetUriByAction method \u2014 which accepts HttpContext, the name of the action, and the values that need to be used to make the URL valid. In the case of the EmployeesController, we send the company id, employee id, and fields.<br \/>\n\u6211\u4eec\u5728\u521b\u5efa\u94fe\u63a5\u65f6\u9700\u8981\u8003\u8651\u8fd9\u4e9b\u5b57\u6bb5\uff0c\u56e0\u4e3a\u6211\u4eec\u53ef\u80fd\u4f1a\u5728\u8bf7\u6c42\u4e2d\u4f7f\u7528\u5b83\u4eec\u3002\u6211\u4eec\u4f7f\u7528 LinkGenerator \u7684 GetUriByAction \u65b9\u6cd5\u521b\u5efa\u94fe\u63a5\uff0c\u8be5\u65b9\u6cd5\u63a5\u53d7 HttpContext\u3001\u4f5c\u7684\u540d\u79f0\u4ee5\u53ca\u9700\u8981\u7528\u4e8e\u4f7f URL \u6709\u6548\u7684\u503c\u3002\u5bf9\u4e8e EmployeesController\uff0c\u6211\u4eec\u53d1\u9001\u516c\u53f8 ID\u3001\u5458\u5de5 ID \u548c\u5b57\u6bb5\u3002<\/p>\n<p>And that is it regarding this class.<br \/>\n\u8fd9\u5c31\u662f\u5173\u4e8e\u8fd9\u4e2a\u7c7b\u7684\u5185\u5bb9\u3002<\/p>\n<p>Now, we have to register this class in the Program class:<br \/>\n\u73b0\u5728\uff0c\u6211\u4eec\u5fc5\u987b\u5728 Program \u7c7b\u4e2d\u6ce8\u518c\u8fd9\u4e2a\u7c7b\uff1a<\/p>\n<pre><code>builder.Services.AddScoped&lt;IEmployeeLinks, EmployeeLinks&gt;();<\/code><\/pre>\n<p>After the service registration, we are going to create a new record inside the Entities\/LinkModels folder:<br \/>\n\u670d\u52a1\u6ce8\u518c\u540e\uff0c\u6211\u4eec\u5c06\u5728 Entities\/LinkModels \u6587\u4ef6\u5939\u4e2d\u521b\u5efa\u4e00\u4e2a\u65b0\u8bb0\u5f55\uff1a<\/p>\n<pre><code>public record LinkParameters(EmployeeParameters EmployeeParameters, HttpContext Context);<\/code><\/pre>\n<p>We are going to use this record to transfer required parameters from our controller to the service layer and avoid the installation of an additional NuGet package inside the Service and Service.Contracts projects.<br \/>\n\u6211\u4eec\u5c06\u4f7f\u7528\u6b64\u8bb0\u5f55\u5c06\u6240\u9700\u53c2\u6570\u4ece\u63a7\u5236\u5668\u4f20\u8f93\u5230\u670d\u52a1\u5c42\uff0c\u5e76\u907f\u514d\u5728 Service \u548c Service.Contracts \u9879\u76ee\u4e2d\u5b89\u88c5\u989d\u5916\u7684 NuGet \u5305\u3002<\/p>\n<p>Also for this to work, we have to add the reference to the Shared project, install the Microsoft.AspNetCore.Mvc.Abstractions package needed for HttpContext, and add required using directives:<br \/>\n\u6b64\u5916\uff0c\u8981\u4f7f\u5176\u6b63\u5e38\u5de5\u4f5c\uff0c\u6211\u4eec\u5fc5\u987b\u6dfb\u52a0\u5bf9 Shared \u9879\u76ee\u7684\u5f15\u7528\uff0c\u5b89\u88c5 HttpContext \u6240\u9700\u7684 Microsoft.AspNetCore.Mvc.Abstractions \u5305\uff0c\u5e76\u6dfb\u52a0\u6240\u9700\u7684 using \u6307\u4ee4\uff1a<\/p>\n<pre><code>using Microsoft.AspNetCore.Http; \nusing Shared.RequestFeatures;<\/code><\/pre>\n<p>Now, we can return to the IEmployeeLinks interface and fix that error by importing the required namespace. As you can see, we didn\u2019t have to install the Abstractions NuGet package since Contracts references Entities. If Visual Studio keeps asking for the package installation, just remove the Entities reference from the Contracts project and add it again.<br \/>\n\u73b0\u5728\uff0c\u6211\u4eec\u53ef\u4ee5\u8fd4\u56de\u5230 IEmployeeLinks \u63a5\u53e3\uff0c\u5e76\u901a\u8fc7\u5bfc\u5165\u6240\u9700\u7684\u547d\u540d\u7a7a\u95f4\u6765\u4fee\u590d\u8be5\u9519\u8bef\u3002\u5982\u4f60\u6240\u89c1\uff0c\u6211\u4eec\u4e0d\u5fc5\u5b89\u88c5\u62bd\u8c61 NuGet \u5305\uff0c\u56e0\u4e3a Contracts \u5f15\u7528\u5b9e\u4f53\u3002\u5982\u679c Visual Studio \u4e0d\u65ad\u8981\u6c42\u5b89\u88c5\u5305\uff0c\u53ea\u9700\u4ece Contracts \u9879\u76ee\u4e2d\u5220\u9664 Entities \u5f15\u7528\uff0c\u7136\u540e\u518d\u6b21\u6dfb\u52a0\u5b83\u3002<\/p>\n<p>Once that is done, we can modify the EmployeesController:<br \/>\n\u5b8c\u6210\u540e\uff0c\u6211\u4eec\u53ef\u4ee5\u4fee\u6539 EmployeesController\uff1a<\/p>\n<pre><code>[HttpGet] [ServiceFilter(typeof(ValidateMediaTypeAttribute))] public async Task&lt;IActionResult&gt; GetEmployeesForCompany(Guid companyId, [FromQuery] EmployeeParameters employeeParameters) { var linkParams = new LinkParameters(employeeParameters, HttpContext); var pagedResult = await _service.EmployeeService.GetEmployeesAsync(companyId, linkParams, trackChanges: false); Response.Headers.Add(&quot;X-Pagination&quot;, JsonSerializer.Serialize(pagedResult.metaData)); return Ok(pagedResult.employees); }<\/code><\/pre>\n<p>So, we create the linkParams variable and send it instead of employeeParameters to the service method.<br \/>\n\u56e0\u6b64\uff0c\u6211\u4eec\u521b\u5efa linkParams \u53d8\u91cf\u5e76\u5c06\u5176\uff08\u800c\u4e0d\u662f employeeParameters\uff09\u53d1\u9001\u5230 service \u65b9\u6cd5\u3002<\/p>\n<p>Of course, this means we have to modify the IEmployeeService interface:<br \/>\n\u5f53\u7136\uff0c\u8fd9\u610f\u5473\u7740\u6211\u4eec\u5fc5\u987b\u4fee\u6539 IEmployeeService \u63a5\u53e3\uff1a<\/p>\n<pre><code>Task&lt;(LinkResponse linkResponse, MetaData metaData)&gt; GetEmployeesAsync(Guid companyId, LinkParameters linkParameters, bool trackChanges);<\/code><\/pre>\n<p>Now the Tuple return type has the LinkResponse as the first field and also we have LinkParameters as the second parameter.<br \/>\n\u73b0\u5728\uff0cTuple \u8fd4\u56de\u7c7b\u578b\u5c06 LinkResponse \u4f5c\u4e3a\u7b2c\u4e00\u4e2a\u5b57\u6bb5\uff0c\u5e76\u5c06 LinkParameters \u4f5c\u4e3a\u7b2c\u4e8c\u4e2a\u53c2\u6570\u3002<\/p>\n<p>After we modified our interface, let\u2019s modify the EmployeeService class:<br \/>\n\u4fee\u6539\u63a5\u53e3\u540e\uff0c\u8ba9\u6211\u4eec\u4fee\u6539 EmployeeService \u7c7b\uff1a<\/p>\n<pre><code>private readonly IRepositoryManager _repository; private readonly ILoggerManager _logger; private readonly IMapper _mapper; private readonly IEmployeeLinks _employeeLinks; public EmployeeService(IRepositoryManager repository, ILoggerManager logger, IMapper mapper, IEmployeeLinks employeeLinks) {_repository = repository; _logger = logger; _mapper = mapper; _employeeLinks = employeeLinks; } public async Task&lt;(LinkResponse linkResponse, MetaData metaData)&gt; GetEmployeesAsync (Guid companyId, LinkParameters linkParameters, bool trackChanges) { if (!linkParameters.EmployeeParameters.ValidAgeRange) throw new MaxAgeRangeBadRequestException(); await CheckIfCompanyExists(companyId, trackChanges); var employeesWithMetaData = await _repository.Employee .GetEmployeesAsync(companyId, linkParameters.EmployeeParameters, trackChanges); var employeesDto = _mapper.Map&lt;IEnumerable&lt;EmployeeDto&gt;&gt;(employeesWithMetaData); var links = _employeeLinks.TryGenerateLinks(employeesDto, linkParameters.EmployeeParameters.Fields, companyId, linkParameters.Context); return (linkResponse: links, metaData: employeesWithMetaData.MetaData); }<\/code><\/pre>\n<p>First, we don\u2019t have the DataShaper injected anymore since this logic is now inside the EmployeeLinks class. Then, we change the method signature, fix a couple of errors since now we have linkParameters and not employeeParameters as a parameter, and we call the TryGenerateLinks method, which will return LinkResponse as a result.<br \/>\n\u9996\u5148\uff0c\u6211\u4eec\u4e0d\u518d\u6ce8\u5165 DataShaper\uff0c\u56e0\u4e3a\u6b64\u903b\u8f91\u73b0\u5728\u4f4d\u4e8e EmployeeLinks \u7c7b\u4e2d\u3002\u7136\u540e\uff0c\u6211\u4eec\u66f4\u6539\u65b9\u6cd5\u7b7e\u540d\uff0c\u4fee\u590d\u51e0\u4e2a\u9519\u8bef\uff0c\u56e0\u4e3a\u73b0\u5728\u6211\u4eec\u6709 linkParameters \u800c\u4e0d\u662f employeeParameters \u4f5c\u4e3a\u53c2\u6570\uff0c\u5e76\u4e14\u6211\u4eec\u8c03\u7528 TryGenerateLinks \u65b9\u6cd5\uff0c\u8be5\u65b9\u6cd5\u5c06\u8fd4\u56de LinkResponse \u4f5c\u4e3a\u7ed3\u679c\u3002<\/p>\n<p>Finally, we construct our Tuple and return it to the caller.<br \/>\n\u6700\u540e\uff0c\u6211\u4eec\u6784\u9020 Tuple \u5e76\u5c06\u5176\u8fd4\u56de\u7ed9\u8c03\u7528\u8005\u3002<\/p>\n<p>Now we can return to our controller and modify the GetEmployeesForCompany action:<br \/>\n\u73b0\u5728\u6211\u4eec\u53ef\u4ee5\u8fd4\u56de\u5230\u63a7\u5236\u5668\u5e76\u4fee\u6539 GetEmployeesForCompany\u4f5c\uff1a<\/p>\n<pre><code>[HttpGet] [ServiceFilter(typeof(ValidateMediaTypeAttribute))] public async Task&lt;IActionResult&gt; GetEmployeesForCompany(Guid companyId, [FromQuery] EmployeeParameters employeeParameters) { var linkParams = new LinkParameters(employeeParameters, HttpContext); var result = await _service.EmployeeService.GetEmployeesAsync(companyId, linkParams, trackChanges: false); Response.Headers.Add(&quot;X-Pagination&quot;, JsonSerializer.Serialize(result.metaData));return result.linkResponse.HasLinks ? Ok(result.linkResponse.LinkedEntities) : Ok(result.linkResponse.ShapedEntities); }<\/code><\/pre>\n<p>We change the pageResult variable name to result and use it to return the proper response to the client. If our result has links, we return linked entities, otherwise, we return shaped ones.<br \/>\n\u6211\u4eec\u5c06 pageResult \u53d8\u91cf\u540d\u79f0\u66f4\u6539\u4e3a result\uff0c\u5e76\u4f7f\u7528\u5b83\u5411\u5ba2\u6237\u7aef\u8fd4\u56de\u6b63\u786e\u7684\u54cd\u5e94\u3002\u5982\u679c\u6211\u4eec\u7684\u7ed3\u679c\u6709\u94fe\u63a5\uff0c\u6211\u4eec\u8fd4\u56de\u94fe\u63a5\u7684\u5b9e\u4f53\uff0c\u5426\u5219\uff0c\u6211\u4eec\u8fd4\u56de\u6709\u5f62\u72b6\u7684\u5b9e\u4f53\u3002<\/p>\n<p>Before we test this, we shouldn\u2019t forget to modify the ServiceManager\u2019s constructor:<br \/>\n\u5728\u6211\u4eec\u6d4b\u8bd5\u4e4b\u524d\uff0c\u6211\u4eec\u4e0d\u5e94\u8be5\u5fd8\u8bb0\u4fee\u6539 ServiceManager \u7684\u6784\u9020\u51fd\u6570\uff1a<\/p>\n<pre><code>public ServiceManager(IRepositoryManager repositoryManager, ILoggerManager logger, IMapper mapper, IEmployeeLinks employeeLinks) { _companyService = new Lazy&lt;ICompanyService&gt;(() =&gt; new CompanyService(repositoryManager, logger, mapper)); _employeeService = new Lazy&lt;IEmployeeService&gt;(() =&gt; new EmployeeService(repositoryManager, logger, mapper, employeeLinks)); }<\/code><\/pre>\n<p>Excellent. We can test this now:<br \/>\n\u975e\u5e38\u597d\u3002\u6211\u4eec\u73b0\u5728\u53ef\u4ee5\u6d4b\u8bd5\u4e00\u4e0b\uff1a<br \/>\n<a href=\"https:\/\/localhost:5001\/api\/companies\/C9D4C053-49B6-410C-BC78-2D54A9991870\/employees?pageNumber=1&amp;pageSize=4&amp;minAge=26&amp;maxAge=32&amp;searchTerm=A&amp;orderBy=namedesc&amp;fields=name,age\">https:\/\/localhost:5001\/api\/companies\/C9D4C053-49B6-410C-BC78-2D54A9991870\/employees?pageNumber=1&pageSize=4&minAge=26&maxAge=32&searchTerm=A&orderBy=namedesc&fields=name,age<\/a><\/p>\n<p><img decoding=\"async\" src=\"\/images\/ultimateaspnetcorewebapi6\/2102.jpg\" alt=\"alt text\" \/><\/p>\n<p>You can test this with the xml media type as well (we have prepared the request in Postman for you).<br \/>\n\u60a8\u4e5f\u53ef\u4ee5\u4f7f\u7528 xml \u5a92\u4f53\u7c7b\u578b\u5bf9\u6b64\u8fdb\u884c\u6d4b\u8bd5\uff08\u6211\u4eec\u5df2\u7ecf\u5728 Postman \u4e2d\u4e3a\u60a8\u51c6\u5907\u4e86\u8bf7\u6c42\uff09\u3002<\/p>\n","protected":false},"excerpt":{"rendered":"<p>21 SUPPORTING HATEOAS 21 \u652f\u6301 HATEOAS In this section, we are going to talk about one of the most important concepts in building RESTful APIs \u2014 HATEOAS and learn how to implement HATEOAS in ASP.NET Core Web API. This part relies heavily on the concepts we&#8217;ve implemented so far in paging, filtering, searching, sorting, and [&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-1140","post","type-post","status-publish","format-standard","hentry","category-uncategorized"],"_links":{"self":[{"href":"https:\/\/diji.net\/index.php?rest_route=\/wp\/v2\/posts\/1140","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=1140"}],"version-history":[{"count":0,"href":"https:\/\/diji.net\/index.php?rest_route=\/wp\/v2\/posts\/1140\/revisions"}],"wp:attachment":[{"href":"https:\/\/diji.net\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=1140"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/diji.net\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=1140"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/diji.net\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=1140"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}