{"id":1144,"date":"2025-05-27T14:47:36","date_gmt":"2025-05-27T06:47:36","guid":{"rendered":"https:\/\/www.hyy.net\/?p=1144"},"modified":"2025-05-27T14:47:36","modified_gmt":"2025-05-27T06:47:36","slug":"ultimate-asp-net-core-web-api-23-root-document","status":"publish","type":"post","link":"https:\/\/diji.net\/?p=1144","title":{"rendered":"Ultimate ASP.NET Core Web API 23 ROOT DOCUMENT"},"content":{"rendered":"<p>23 ROOT DOCUMENT<br \/>\n23 \u6839\u6587\u6863<\/p>\n<p>In this section, we are going to create a starting point for the consumers of our API. This starting point is also known as the Root Document. The Root Document is the place where consumers can learn how to interact with the rest of the API.\u200c<br \/>\n\u5728\u672c\u8282\u4e2d\uff0c\u6211\u4eec\u5c06\u4e3a API \u7684\u4f7f\u7528\u8005\u521b\u5efa\u4e00\u4e2a\u8d77\u70b9\u3002\u6b64\u8d77\u70b9\u4e5f\u79f0\u4e3a\u6839\u6587\u6863\u3002\u6839\u6587\u6863\u662f\u4f7f\u7528\u8005\u53ef\u4ee5\u5b66\u4e60\u5982\u4f55\u4e0e API \u7684\u5176\u4f59\u90e8\u5206\u8fdb\u884c\u4ea4\u4e92\u7684\u5730\u65b9\u3002<\/p>\n<h2>23.1 Root Document Implementation<\/h2>\n<p>23.1 \u6839\u6587\u6863\u5b9e\u73b0<\/p>\n<p>This document should be created at the api root, so let\u2019s start by creating\u200c a new controller:<br \/>\n\u8fd9\u4e2a\u6587\u6863\u5e94\u8be5\u5728 api \u6839\u76ee\u5f55\u4e0b\u521b\u5efa\uff0c\u6240\u4ee5\u8ba9\u6211\u4eec\u4ece\u521b\u5efa\u4e00\u4e2a\u65b0\u7684\u63a7\u5236\u5668\u5f00\u59cb\uff1a<\/p>\n<pre><code>[Route(&quot;api&quot;)] [ApiController] public class RootController : ControllerBase { }<\/code><\/pre>\n<p>We are going to generate links towards the API actions. Therefore, we have to inject LinkGenerator:<br \/>\n\u6211\u4eec\u5c06\u751f\u6210\u6307\u5411 API\u4f5c\u7684\u94fe\u63a5\u3002\u56e0\u6b64\uff0c\u6211\u4eec\u5fc5\u987b\u6ce8\u5165 LinkGenerator\uff1a<\/p>\n<pre><code>[Route(&quot;api&quot;)] [ApiController] public class RootController : ControllerBase { private readonly LinkGenerator _linkGenerator; public RootController(LinkGenerator linkGenerator) =&gt; _linkGenerator = linkGenerator; }<\/code><\/pre>\n<p>In this controller, we only need a single action, GetRoot, which will be executed with the GET request on the \/api URI.<br \/>\n\u5728\u6b64\u63a7\u5236\u5668\u4e2d\uff0c\u6211\u4eec\u53ea\u9700\u8981\u4e00\u4e2a\u4f5c GetRoot\uff0c\u8be5\u4f5c\u5c06\u4f7f\u7528 \/api URI \u4e0a\u7684 GET \u8bf7\u6c42\u6267\u884c\u3002<\/p>\n<p>There are several links that we are going to create in this action. The link to the document itself and links to actions available on the URIs at the root level (actions from the Companies controller). We are not creating links to employees, because they are children of the company \u2014 and in our API if we want to fetch employees, we have to fetch the company first.<br \/>\n\u6211\u4eec\u5c06\u5728\u6b64\u4f5c\u4e2d\u521b\u5efa\u591a\u4e2a\u94fe\u63a5\u3002\u6307\u5411\u6587\u6863\u672c\u8eab\u7684\u94fe\u63a5\uff0c\u4ee5\u53ca\u6307\u5411\u6839\u7ea7\u522b URI \u4e0a\u53ef\u7528\u4f5c\u7684\u94fe\u63a5\uff08\u6765\u81ea Companies \u63a7\u5236\u5668\u7684\u4f5c\uff09\u3002\u6211\u4eec\u4e0d\u4f1a\u521b\u5efa\u6307\u5411\u5458\u5de5\u7684\u94fe\u63a5\uff0c\u56e0\u4e3a\u4ed6\u4eec\u662f\u516c\u53f8\u7684\u5b50\u516c\u53f8 \u2014 \u5728\u6211\u4eec\u7684 API \u4e2d\uff0c\u5982\u679c\u6211\u4eec\u60f3\u83b7\u53d6\u5458\u5de5\uff0c\u6211\u4eec\u5fc5\u987b\u5148\u83b7\u53d6\u516c\u53f8\u3002<\/p>\n<p>If we inspect our CompaniesController, we can see that GetCompanies and CreateCompany are the only actions on the root URI level (api\/companies). Therefore, we are going to create links only to them.<br \/>\n\u5982\u679c\u6211\u4eec\u68c0\u67e5 CompaniesController\uff0c\u6211\u4eec\u53ef\u4ee5\u770b\u5230 GetCompanies \u548c CreateCompany \u662f\u6839 URI \u7ea7\u522b \uff08api\/companies\uff09 \u4e0a\u7684\u552f\u4e00\u4f5c\u3002\u56e0\u6b64\uff0c\u6211\u4eec\u5c06\u4ec5\u521b\u5efa\u6307\u5411\u5b83\u4eec\u7684\u94fe\u63a5\u3002<\/p>\n<p>Before we start with the GetRoot action, let\u2019s add a name for the CreateCompany and GetCompanies actions in the CompaniesController:<br \/>\n\u5728\u5f00\u59cb GetRoot\u4f5c\u4e4b\u524d\uff0c\u8ba9\u6211\u4eec\u5728 CompaniesController \u4e2d\u4e3a CreateCompany \u548c GetCompanies\u4f5c\u6dfb\u52a0\u4e00\u4e2a\u540d\u79f0\uff1a<\/p>\n<pre><code>[HttpGet(Name = &quot;GetCompanies&quot;)] public async Task&lt;IActionResult&gt; GetCompanies()\n[HttpPost(Name = &quot;CreateCompany&quot;)] [ServiceFilter(typeof(ValidationFilterAttribute))] public async Task&lt;IActionResult&gt; CreateCompany([FromBody]CompanyForCreationDto company)\n<\/code><\/pre>\n<p>We are going to use the Link class to generate links:<br \/>\n\u6211\u4eec\u5c06\u4f7f\u7528 Link \u7c7b\u6765\u751f\u6210\u94fe\u63a5\uff1a<\/p>\n<pre><code>public class Link { public string Href { get; set; } public string Rel { get; set; } public string Method { get; set; } \u2026 }<\/code><\/pre>\n<p>This class contains all the required properties to describe our actions while creating links in the GetRoot action. The Href property defines the URI to the action, the Rel property defines the identification of the action type, and the Method property defines which HTTP method should be used for that action.<br \/>\n\u6b64\u7c7b\u5305\u542b\u63cf\u8ff0 GetRoot\u4f5c\u4e2d\u521b\u5efa\u94fe\u63a5\u65f6\u7684\u4f5c\u6240\u9700\u7684\u6240\u6709\u5c5e\u6027\u3002Href \u5c5e\u6027\u5b9a\u4e49\u4f5c\u7684 URI\uff0cRel \u5c5e\u6027\u5b9a\u4e49\u4f5c\u7c7b\u578b\u7684\u6807\u8bc6\uff0cMethod \u5c5e\u6027\u5b9a\u4e49\u5e94\u4f7f\u7528\u54ea\u4e2a HTTP \u65b9\u6cd5\u6267\u884c\u8be5\u4f5c\u3002<\/p>\n<p>Now, we can create the GetRoot action:<br \/>\n\u73b0\u5728\uff0c\u6211\u4eec\u53ef\u4ee5\u521b\u5efa GetRoot\u4f5c\uff1a<\/p>\n<pre><code>[HttpGet(Name = &quot;GetRoot&quot;)] public IActionResult GetRoot([FromHeader(Name = &quot;Accept&quot;)] string mediaType) { if(mediaType.Contains(&quot;application\/vnd.codemaze.apiroot&quot;)) { var list = new List&lt;Link&gt; { new Link { Href = _linkGenerator.GetUriByName(HttpContext, nameof(GetRoot), new {}), Rel = &quot;self&quot;, Method = &quot;GET&quot; }, new Link { Href = _linkGenerator.GetUriByName(HttpContext, &quot;GetCompanies&quot;, new {}), Rel = &quot;companies&quot;, Method = &quot;GET&quot; }, new Link{ Href = _linkGenerator.GetUriByName(HttpContext, &quot;CreateCompany&quot;, new {}), Rel = &quot;create_company&quot;, Method = &quot;POST&quot; } }; return Ok(list); } return NoContent(); }<\/code><\/pre>\n<p>In this action, we generate links only if a custom media type is provided from the Accept header. Otherwise, we return NoContent(). To generate links, we use the GetUriByName method from the LinkGenerator class.<br \/>\n\u5728\u6b64\u4f5c\u4e2d\uff0c\u4ec5\u5f53 Accept \u6807\u5934\u63d0\u4f9b\u4e86\u81ea\u5b9a\u4e49\u5a92\u4f53\u7c7b\u578b\u65f6\uff0c\u6211\u4eec\u624d\u4f1a\u751f\u6210\u94fe\u63a5\u3002\u5426\u5219\uff0c\u6211\u4eec\u5c06\u8fd4\u56de NoContent\uff08\uff09\u3002\u8981\u751f\u6210\u94fe\u63a5\uff0c\u6211\u4eec\u4f7f\u7528 LinkGenerator \u7c7b\u4e2d\u7684 GetUriByName \u65b9\u6cd5\u3002<\/p>\n<p>That said, we have to register our custom media types for the json and xml formats. To do that, we are going to extend the AddCustomMediaTypes extension method:<br \/>\n\u4e5f\u5c31\u662f\u8bf4\uff0c\u6211\u4eec\u5fc5\u987b\u4e3a json \u548c xml \u683c\u5f0f\u6ce8\u518c\u81ea\u5b9a\u4e49\u5a92\u4f53\u7c7b\u578b\u3002\u4e3a\u6b64\uff0c\u6211\u4eec\u5c06\u6269\u5c55 AddCustomMediaTypes \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;); systemTextJsonOutputFormatter.SupportedMediaTypes .Add(&quot;application\/vnd.codemaze.apiroot+json&quot;); } var xmlOutputFormatter = config.OutputFormatters .OfType&lt;XmlDataContractSerializerOutputFormatter&gt;()? .FirstOrDefault(); if (xmlOutputFormatter != null) { xmlOutputFormatter.SupportedMediaTypes .Add(&quot;application\/vnd.codemaze.hateoas+xml&quot;); xmlOutputFormatter.SupportedMediaTypes .Add(&quot;application\/vnd.codemaze.apiroot+xml&quot;); } }); }<\/code><\/pre>\n<p>We can now inspect our result:<br \/>\n\u6211\u4eec\u73b0\u5728\u53ef\u4ee5\u68c0\u67e5\u6211\u4eec\u7684\u7ed3\u679c\uff1a<br \/>\n<a href=\"https:\/\/localhost:5001\/api\">https:\/\/localhost:5001\/api<\/a><\/p>\n<p><img decoding=\"async\" src=\"\/images\/ultimateaspnetcorewebapi6\/2301.jpg\" alt=\"alt text\" \/><\/p>\n<p>This works great.<br \/>\n\u8fd9\u6548\u679c\u5f88\u597d\u3002<\/p>\n<p>Let\u2019s test what is going to happen if we don\u2019t provide the custom media type:<br \/>\n\u8ba9\u6211\u4eec\u6d4b\u8bd5\u4e00\u4e0b\u5982\u679c\u4e0d\u63d0\u4f9b\u81ea\u5b9a\u4e49\u5a92\u4f53\u7c7b\u578b\u4f1a\u53d1\u751f\u4ec0\u4e48\u60c5\u51b5\uff1a<\/p>\n<p><a href=\"https:\/\/localhost:5001\/api\">https:\/\/localhost:5001\/api<\/a><\/p>\n<p><img decoding=\"async\" src=\"\/images\/ultimateaspnetcorewebapi6\/2302.jpg\" alt=\"alt text\" \/><\/p>\n<p>Well, we get the 204 No Content message as expected. Of course, you can test the xml request as well:<br \/>\n\u597d\u5427\uff0c\u6211\u4eec\u5982\u9884\u671f\u7684\u90a3\u6837\u6536\u5230\u4e86 204 No Content \u6d88\u606f\u3002\u5f53\u7136\uff0c\u60a8\u4e5f\u53ef\u4ee5\u6d4b\u8bd5 xml \u8bf7\u6c42\uff1a<\/p>\n<p><a href=\"https:\/\/localhost:5001\/api\">https:\/\/localhost:5001\/api<\/a><\/p>\n<p><img decoding=\"async\" src=\"\/images\/ultimateaspnetcorewebapi6\/2303.jpg\" alt=\"alt text\" \/><\/p>\n<p>Great.<br \/>\n\u4f1f\u5927\u3002<\/p>\n<p>Now we can move on to the versioning chapter.<br \/>\n\u73b0\u5728\u6211\u4eec\u53ef\u4ee5\u7ee7\u7eed\u8fdb\u884c\u7248\u672c\u63a7\u5236\u7ae0\u8282\u3002<\/p>\n","protected":false},"excerpt":{"rendered":"<p>23 ROOT DOCUMENT 23 \u6839\u6587\u6863 In this section, we are going to create a starting point for the consumers of our API. This starting point is also known as the Root Document. The Root Document is the place where consumers can learn how to interact with the rest of the API.\u200c \u5728\u672c\u8282\u4e2d\uff0c\u6211\u4eec\u5c06\u4e3a API \u7684\u4f7f\u7528\u8005\u521b\u5efa\u4e00\u4e2a\u8d77\u70b9\u3002\u6b64\u8d77\u70b9\u4e5f\u79f0\u4e3a\u6839\u6587\u6863\u3002\u6839\u6587\u6863\u662f\u4f7f\u7528\u8005\u53ef\u4ee5\u5b66\u4e60\u5982\u4f55\u4e0e API [&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-1144","post","type-post","status-publish","format-standard","hentry","category-uncategorized"],"_links":{"self":[{"href":"https:\/\/diji.net\/index.php?rest_route=\/wp\/v2\/posts\/1144","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=1144"}],"version-history":[{"count":0,"href":"https:\/\/diji.net\/index.php?rest_route=\/wp\/v2\/posts\/1144\/revisions"}],"wp:attachment":[{"href":"https:\/\/diji.net\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=1144"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/diji.net\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=1144"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/diji.net\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=1144"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}