{"id":1112,"date":"2025-05-27T14:46:26","date_gmt":"2025-05-27T06:46:26","guid":{"rendered":"https:\/\/www.hyy.net\/?p=1112"},"modified":"2025-05-27T14:46:26","modified_gmt":"2025-05-27T06:46:26","slug":"ultimate-asp-net-core-web-api-7-content-negotiation","status":"publish","type":"post","link":"https:\/\/diji.net\/?p=1112","title":{"rendered":"Ultimate ASP.NET Core Web API  7 CONTENT NEGOTIATION"},"content":{"rendered":"<h1>7  CONTENT NEGOTIATION<\/h1>\n<p>7 \u5185\u5bb9\u534f\u5546<\/p>\n<p>Content negotiation is one of the quality-of-life improvements we can add to our REST API to make it more user-friendly and flexible. And when we design an API, isn\u2019t that what we want to achieve in the first place?\u200c<br \/>\n\u5185\u5bb9\u534f\u5546\u662f\u6211\u4eec\u53ef\u4ee5\u6dfb\u52a0\u5230 REST API \u4e2d\u7684\u751f\u6d3b\u8d28\u91cf\u6539\u8fdb\u4e4b\u4e00\uff0c\u4ee5\u4f7f\u5176\u66f4\u52a0\u7528\u6237\u53cb\u597d\u548c\u7075\u6d3b\u3002\u5f53\u6211\u4eec\u8bbe\u8ba1 API \u65f6\uff0c\u8fd9\u4e0d\u6b63\u662f\u6211\u4eec\u6700\u521d\u60f3\u8981\u5b9e\u73b0\u7684\u76ee\u6807\u5417\uff1f<\/p>\n<p>Content negotiation is an HTTP feature that has been around for a while, but for one reason or another, it is often a bit underused.<br \/>\n\u5185\u5bb9\u534f\u5546\u662f\u4e00\u9879\u5df2\u7ecf\u5b58\u5728\u4e86\u4e00\u6bb5\u65f6\u95f4\u7684 HTTP \u529f\u80fd\uff0c\u4f46\u51fa\u4e8e\u67d0\u79cd\u539f\u56e0\uff0c\u5b83\u901a\u5e38\u6ca1\u6709\u5f97\u5230\u5145\u5206\u5229\u7528\u3002<\/p>\n<p>In short, content negotiation lets you choose or rather \u201cnegotiate\u201d the  content you want to get in a response to the REST API request.<br \/>\n\u7b80\u800c\u8a00\u4e4b\uff0c\u5185\u5bb9\u534f\u5546\u5141\u8bb8\u60a8\u9009\u62e9\u6216\u66f4\u786e\u5207\u5730\u8bf4\u662f\u201c\u534f\u5546\u201d\u60a8\u5e0c\u671b\u5728\u54cd\u5e94 REST API \u8bf7\u6c42\u65f6\u83b7\u5f97\u7684\u5185\u5bb9\u3002<\/p>\n<h2>7.1 What Do We Get Out of the Box?<\/h2>\n<p>7.1 \u6211\u4eec\u5f00\u7bb1\u5373\u7528\u4ec0\u4e48\uff1f<\/p>\n<p>By default, ASP.NET Core Web API returns a JSON formatted result.\u200c<br \/>\n\u9ed8\u8ba4\u60c5\u51b5\u4e0b\uff0cASP.NET Core Web API \u8fd4\u56de JSON \u683c\u5f0f\u7684\u7ed3\u679c\u3002<\/p>\n<p>We can confirm that by looking at the response from the GetCompanies action:<br \/>\n\u6211\u4eec\u53ef\u4ee5\u901a\u8fc7\u67e5\u770b GetCompanies action\u7684\u56de\u590d\u6765\u786e\u8ba4\u8fd9\u4e00\u70b9:<br \/>\n<a href=\"https:\/\/localhost:5001\/api\/companies\">https:\/\/localhost:5001\/api\/companies<\/a><\/p>\n<p><img decoding=\"async\" src=\"\/images\/ultimateaspnetcorewebapi6\/0701.jpg\" alt=\"alt text\" \/><\/p>\n<p>We can clearly see that the default result when calling GET on \/api\/companies returns the JSON result. We have also used the Accept header (as you can see in the picture above) to try forcing the server to return other media types like plain text and XML.<br \/>\n\u6211\u4eec\u53ef\u4ee5\u6e05\u695a\u5730\u770b\u5230\uff0c\u5728 \/api\/companies \u4e0a\u8c03\u7528 GET \u65f6\uff0c\u9ed8\u8ba4\u7ed3\u679c\u8fd4\u56de\u7684\u662f JSON \u7ed3\u679c\u3002\u6211\u4eec\u8fd8\u4f7f\u7528\u4e86 Accept \u6807\u5934\uff08\u5982\u4e0a\u56fe\u6240\u793a\uff09\u6765\u5c1d\u8bd5\u5f3a\u5236\u670d\u52a1\u5668\u8fd4\u56de\u5176\u4ed6\u5a92\u4f53\u7c7b\u578b\uff0c\u5982\u7eaf\u6587\u672c\u548c XML\u3002<\/p>\n<p>But that doesn\u2019t work. Why?<br \/>\n\u4f46\u8fd9\u884c\u4e0d\u901a\u3002\u4e3a\u4ec0\u4e48\uff1f<\/p>\n<p>Because we need to configure server formatters to format a response the way we want it.<br \/>\n\u56e0\u4e3a\u6211\u4eec\u9700\u8981\u914d\u7f6e\u670d\u52a1\u5668\u683c\u5f0f\u5316\u7a0b\u5e8f\u4ee5\u6309\u7167\u6211\u4eec\u60f3\u8981\u7684\u65b9\u5f0f\u683c\u5f0f\u5316\u54cd\u5e94\u3002<\/p>\n<p>Let\u2019s see how to do that.<br \/>\n\u8ba9\u6211\u4eec\u770b\u770b\u5982\u4f55\u505a\u5230\u8fd9\u4e00\u70b9\u3002<\/p>\n<h2>7.2 Changing the Default Configuration of Our Project<\/h2>\n<p>7.2 \u66f4\u6539\u6211\u4eec\u9879\u76ee\u7684\u9ed8\u8ba4\u914d\u7f6e<\/p>\n<p>A server does not explicitly specify where it formats a response to JSON.\u200c But you can override it by changing configuration options through the AddControllers method.<br \/>\n\u670d\u52a1\u5668\u6ca1\u6709\u660e\u786e\u6307\u5b9a\u5b83\u5bf9 JSON \u7684\u54cd\u5e94\u7684\u683c\u5f0f\u3002\u4f46\u662f\uff0c\u60a8\u53ef\u4ee5\u901a\u8fc7 AddControllers \u65b9\u6cd5\u66f4\u6539\u914d\u7f6e\u9009\u9879\u6765\u8986\u76d6\u5b83\u3002<\/p>\n<p>We can add the following options to enable the server to format the XML response when the client tries negotiating for it:<br \/>\n\u6211\u4eec\u53ef\u4ee5\u6dfb\u52a0\u4ee5\u4e0b\u9009\u9879\uff0c\u4f7f\u670d\u52a1\u5668\u80fd\u591f\u5728\u5ba2\u6237\u7aef\u5c1d\u8bd5\u534f\u5546 XML \u54cd\u5e94\u65f6\u683c\u5f0f\u5316 XML \u54cd\u5e94\uff1a<\/p>\n<pre><code>using CompanyEmployees.Extensions;\nusing Contracts;\nusing Microsoft.AspNetCore.HttpOverrides;\nusing NLog;\n\nvar builder = WebApplication.CreateBuilder(args);\n\n\/\/ Add services to the container.\nLogManager.LoadConfiguration(\n    string.Concat(Directory.GetCurrentDirectory(),\n    &quot;\/nlog.config&quot;));\nbuilder.Services.ConfigureLoggerService();\n\nbuilder.Services.ConfigureCors();\nbuilder.Services.ConfigureIISIntegration();\nbuilder.Services.ConfigureRepositoryManager();\nbuilder.Services.ConfigureServiceManager();\nbuilder.Services.ConfigureSqlContext(builder.Configuration);\nbuilder.Services.AddAutoMapper(typeof(Program));\n\/\/ builder.Services.AddControllers().AddApplicationPart(typeof(CompanyEmployees.Presentation.AssemblyReference).Assembly);\nbuilder.Services.AddControllers(config =&gt; { config.RespectBrowserAcceptHeader = true; }).AddXmlDataContractSerializerFormatters().AddApplicationPart(typeof(CompanyEmployees.Presentation.AssemblyReference).Assembly);<\/code><\/pre>\n<p>First things first, we must tell a server to respect the Accept header. After that, we just add the AddXmlDataContractSerializerFormatters method to support XML formatters.<br \/>\n\u9996\u5148\uff0c\u6211\u4eec\u5fc5\u987b\u544a\u8bc9\u670d\u52a1\u5668\u9075\u5b88 Accept \u6807\u5934\u3002\u4e4b\u540e\uff0c\u6211\u4eec\u53ea\u9700\u6dfb\u52a0 AddXmlDataContractSerializerFormatters \u65b9\u6cd5\u6765\u652f\u6301 XML \u683c\u5f0f\u5316\u7a0b\u5e8f\u3002<\/p>\n<p>Now that we have our server configured, let\u2019s test the content negotiation once more.<br \/>\n\u73b0\u5728\u6211\u4eec\u5df2\u7ecf\u914d\u7f6e\u4e86\u670d\u52a1\u5668\uff0c\u8ba9\u6211\u4eec\u518d\u6b21\u6d4b\u8bd5\u5185\u5bb9\u534f\u5546\u3002<\/p>\n<h2>7.3 Testing Content Negotiation<\/h2>\n<p>7.3 \u6d4b\u8bd5 Content Negotiation<\/p>\n<p>Let\u2019s see what happens now if we fire the same request through Postman:\u200c<br \/>\n\u8ba9\u6211\u4eec\u770b\u770b\u5982\u679c\u6211\u4eec\u901a\u8fc7 Postman \u89e6\u53d1\u76f8\u540c\u7684\u8bf7\u6c42\uff0c\u73b0\u5728\u4f1a\u53d1\u751f\u4ec0\u4e48\uff1a<br \/>\n<a href=\"https:\/\/localhost:5001\/api\/companies\">https:\/\/localhost:5001\/api\/companies<\/a><\/p>\n<p><img decoding=\"async\" src=\"\/images\/ultimateaspnetcorewebapi6\/0702.jpg\" alt=\"alt text\" \/><\/p>\n<p>We get an error because XmlSerializer cannot easily serialize our positional record type. There are two solutions to this. The first one is marking our CompanyDto record with the [Serializable] attribute:<br \/>\n\u6211\u4eec\u6536\u5230\u4e00\u4e2a\u9519\u8bef\uff0c\u56e0\u4e3a XmlSerializer \u65e0\u6cd5\u8f7b\u677e\u5730\u5e8f\u5217\u5316\u6211\u4eec\u7684\u4f4d\u7f6e\u8bb0\u5f55\u7c7b\u578b\u3002\u6709\u4e24\u79cd\u89e3\u51b3\u65b9\u6848\u3002\u7b2c\u4e00\u4e2a\u662f\u4f7f\u7528 [Serializable] \u5c5e\u6027\u6807\u8bb0\u6211\u4eec\u7684 CompanyDto \u8bb0\u5f55\uff1a<\/p>\n<pre><code>[Serializable] \npublic record CompanyDto(Guid Id, string Name, string FullAddress);<\/code><\/pre>\n<p>Now, we can send the same request again:<br \/>\n\u73b0\u5728\uff0c\u6211\u4eec\u53ef\u4ee5\u518d\u6b21\u53d1\u9001\u76f8\u540c\u7684\u8bf7\u6c42\uff1a<\/p>\n<p><img decoding=\"async\" src=\"\/images\/ultimateaspnetcorewebapi6\/0703.jpg\" alt=\"alt text\" \/><\/p>\n<p>This time, we are getting our XML response but, as you can see,properties have some strange names. That\u2019s because the compiler behind the scenes generates the record as a class with fields named like that (name_BackingField) and the XML serializer just serializes those fields with the same names.<br \/>\n\u8fd9\u4e00\u6b21\uff0c\u6211\u4eec\u6536\u5230\u4e86 XML \u54cd\u5e94\uff0c\u4f46\u6b63\u5982\u4f60\u6240\u770b\u5230\u7684\uff0c\u5c5e\u6027\u6709\u4e00\u4e9b\u5947\u602a\u7684\u540d\u79f0\u3002\u8fd9\u662f\u56e0\u4e3a\u540e\u53f0\u7f16\u8bd1\u5668\u5c06\u8bb0\u5f55\u751f\u6210\u4e3a\u4e00\u4e2a\u7c7b\uff0c\u5176\u4e2d\u5305\u542b\u7c7b\u4f3c \uff08name_BackingField\uff09 \u7684\u5b57\u6bb5\uff0c\u800c XML \u5e8f\u5217\u5316\u7a0b\u5e8f\u53ea\u662f\u5e8f\u5217\u5316\u5177\u6709\u76f8\u540c\u540d\u79f0\u7684\u8fd9\u4e9b\u5b57\u6bb5\u3002<\/p>\n<p>If we don\u2019t want these property names in our response, but the regular ones, we can implement a second solution. Let\u2019s modify our record with the init only property setters:<br \/>\n\u5982\u679c\u6211\u4eec\u4e0d\u5e0c\u671b\u54cd\u5e94\u4e2d\u5305\u542b\u8fd9\u4e9b\u5c5e\u6027\u540d\u79f0\uff0c\u800c\u662f\u5e38\u89c4\u5c5e\u6027\u540d\u79f0\uff0c\u5219\u53ef\u4ee5\u5b9e\u73b0\u7b2c\u4e8c\u4e2a\u89e3\u51b3\u65b9\u6848\u3002\u8ba9\u6211\u4eec\u4f7f\u7528 init only \u5c5e\u6027 setter \u4fee\u6539\u6211\u4eec\u7684\u8bb0\u5f55\uff1a<\/p>\n<pre><code>namespace Shared.DataTransferObjects\n{\n    [Serializable]\n    \/\/ public record CompanyDto(Guid Id, string Name, string FullAddress);\n    public record CompanyDto\n    {\n        public Guid Id { get; init; }\n        public string? Name { get; init; }\n        public string? FullAddress { get; init; }\n    }\n}<\/code><\/pre>\n<p>This object is still immutable and init-only properties protect the state of the object from mutation once initialization is finished.<br \/>\n\u6b64\u5bf9\u8c61\u4ecd\u7136\u662f\u4e0d\u53ef\u53d8\u7684\uff0c\u5e76\u4e14\u4ec5 init-only \u5c5e\u6027\u53ef\u5728\u521d\u59cb\u5316\u5b8c\u6210\u540e\u4fdd\u62a4\u5bf9\u8c61\u7684\u72b6\u6001\u514d\u53d7\u66f4\u6539\u3002<\/p>\n<p>Additionally, we have to make one more change in the MappingProfile class:<br \/>\n\u6b64\u5916\uff0c\u6211\u4eec\u8fd8\u5fc5\u987b\u5728 MappingProfile \u7c7b\u4e2d\u518d\u8fdb\u884c\u4e00\u9879\u66f4\u6539\uff1a<\/p>\n<pre><code>using AutoMapper;\nusing Entities.Models;\nusing Shared.DataTransferObjects;\n\nnamespace CompanyEmployees\n{\n    public class MappingProfile : Profile\n    {\n        public MappingProfile()\n        {\n            \/\/CreateMap&lt;Company, CompanyDto&gt;().ForCtorParam(&quot;FullAddress&quot;, opt =&gt; opt.MapFrom(x =&gt; string.Join(&#039; &#039;, x.Address, x.Country)));\n            CreateMap&lt;Company, CompanyDto&gt;().ForMember(c =&gt; c.FullAddress, opt =&gt; opt.MapFrom(x =&gt; string.Join(&#039; &#039;, x.Address, x.Country)));\n\n            CreateMap&lt;Employee, EmployeeDto&gt;();\n        }\n    }\n}<\/code><\/pre>\n<p>We are returning this mapping rule to a previous state since now, we do have properties in our object.<br \/>\n\u6211\u4eec\u5c06\u6b64\u6620\u5c04\u89c4\u5219\u8fd4\u56de\u5230\u4ee5\u524d\u7684\u72b6\u6001\uff0c\u56e0\u4e3a\u4ece\u73b0\u5728\u5f00\u59cb\uff0c\u6211\u4eec\u7684\u5bf9\u8c61\u4e2d\u786e\u5b9e\u6709\u5c5e\u6027\u3002<\/p>\n<p>Now, we can send the same request again:<br \/>\n\u73b0\u5728\uff0c\u6211\u4eec\u53ef\u4ee5\u518d\u6b21\u53d1\u9001\u76f8\u540c\u7684\u8bf7\u6c42\uff1a<\/p>\n<p><img decoding=\"async\" src=\"\/images\/ultimateaspnetcorewebapi6\/0704.jpg\" alt=\"alt text\" \/><\/p>\n<p>There is our XML response.<br \/>\n\u4e0b\u9762\u662f\u6211\u4eec\u7684 XML \u54cd\u5e94\u3002<\/p>\n<p>Now by changing the Accept header from text\/xml to text\/json, we can get differently formatted responses \u2014 and that is quite awesome, wouldn\u2019t you agree?<br \/>\n\u73b0\u5728\uff0c\u901a\u8fc7\u5c06 Accept \u6807\u5934\u4ece text\/xml \u66f4\u6539\u4e3a text\/json\uff0c\u6211\u4eec\u53ef\u4ee5\u83b7\u5f97\u4e0d\u540c\u683c\u5f0f\u7684\u54cd\u5e94 \u2014 \u8fd9\u771f\u662f\u592a\u68d2\u4e86\uff0c\u4f60\u4e0d\u540c\u610f\u5417\uff1f<\/p>\n<p>Okay, that was nice and easy.<br \/>\n\u597d\u5427\uff0c\u8fd9\u5f88\u597d\uff0c\u5f88\u5bb9\u6613\u3002<\/p>\n<p>But what if despite all this flexibility a client requests a media type that a server doesn\u2019t know how to format?<br \/>\n\u4f46\u662f\uff0c\u5982\u679c\u5c3d\u7ba1\u6709\u6240\u6709\u8fd9\u4e9b\u7075\u6d3b\u6027\uff0c\u4f46\u5ba2\u6237\u7aef\u8bf7\u6c42\u7684\u5a92\u4f53\u7c7b\u578b\u670d\u52a1\u5668\u4e0d\u77e5\u9053\u5982\u4f55\u683c\u5f0f\u5316\uff0c\u8be5\u600e\u4e48\u529e\uff1f<\/p>\n<h2>7.4 Restricting Media Types<\/h2>\n<p>7.4 \u9650\u5236\u5a92\u4f53\u7c7b\u578b<\/p>\n<p>Currently, it \u2013 the server - will default to a JSON type.\u200c<br \/>\n\u76ee\u524d\uff0c\u5b83\uff08\u670d\u52a1\u5668\uff09\u5c06\u9ed8\u8ba4\u4e3a JSON \u7c7b\u578b\u3002<\/p>\n<p>But we can restrict this behavior by adding one line to the configuration:<br \/>\n\u4f46\u662f\u6211\u4eec\u53ef\u4ee5\u901a\u8fc7\u5728\u914d\u7f6e\u4e2d\u6dfb\u52a0\u4e00\u884c\u6765\u9650\u5236\u8fd9\u79cd\u884c\u4e3a\uff1a<\/p>\n<pre><code>using CompanyEmployees.Extensions;\nusing Contracts;\nusing Microsoft.AspNetCore.HttpOverrides;\nusing NLog;\n\nvar builder = WebApplication.CreateBuilder(args);\n\n\/\/ Add services to the container.\nLogManager.LoadConfiguration(\n    string.Concat(Directory.GetCurrentDirectory(),\n    &quot;\/nlog.config&quot;));\nbuilder.Services.ConfigureLoggerService();\n\nbuilder.Services.ConfigureCors();\nbuilder.Services.ConfigureIISIntegration();\nbuilder.Services.ConfigureRepositoryManager();\nbuilder.Services.ConfigureServiceManager();\nbuilder.Services.ConfigureSqlContext(builder.Configuration);\nbuilder.Services.AddAutoMapper(typeof(Program));\n\/\/ builder.Services.AddControllers().AddApplicationPart(typeof(CompanyEmployees.Presentation.AssemblyReference).Assembly);\n\/\/ builder.Services.AddControllers(config =&gt; { config.RespectBrowserAcceptHeader = true; }).AddXmlDataContractSerializerFormatters().AddApplicationPart(typeof(CompanyEmployees.Presentation.AssemblyReference).Assembly);\nbuilder.Services.AddControllers(config =&gt; { config.RespectBrowserAcceptHeader = true; config.ReturnHttpNotAcceptable = true; }).AddXmlDataContractSerializerFormatters().AddApplicationPart(typeof(CompanyEmployees.Presentation.AssemblyReference).Assembly);\nvar app = builder.Build();<\/code><\/pre>\n<p>We added the ReturnHttpNotAcceptable = true option, which tells the server that if the client tries to negotiate for the media type the server doesn\u2019t support, it should return the 406 Not Acceptable status code.<br \/>\n\u6211\u4eec\u6dfb\u52a0\u4e86 ReturnHttpNotAcceptable = true \u9009\u9879\uff0c\u8be5\u9009\u9879\u544a\u8bc9\u670d\u52a1\u5668\uff0c\u5982\u679c\u5ba2\u6237\u7aef\u5c1d\u8bd5\u534f\u5546\u670d\u52a1\u5668\u4e0d\u652f\u6301\u7684\u5a92\u4f53\u7c7b\u578b\uff0c\u5b83\u5e94\u8fd4\u56de 406 Not Acceptable \u72b6\u6001\u4ee3\u7801\u3002<\/p>\n<p>This will make our application more restrictive and force the API consumer to request only the types the server supports. The 406 status code is created for this purpose.<br \/>\n\u8fd9\u5c06\u4f7f\u6211\u4eec\u7684\u5e94\u7528\u7a0b\u5e8f\u66f4\u5177\u9650\u5236\u6027\uff0c\u5e76\u5f3a\u5236 API \u4f7f\u7528\u8005\u4ec5\u8bf7\u6c42\u670d\u52a1\u5668\u652f\u6301\u7684\u7c7b\u578b\u3002406 \u72b6\u6001\u4ee3\u7801\u5c31\u662f\u4e3a\u6b64\u76ee\u7684\u800c\u521b\u5efa\u7684\u3002<\/p>\n<p>Now, let\u2019s try fetching the text\/css media type using Postman to see what happens:<br \/>\n\u73b0\u5728\uff0c\u8ba9\u6211\u4eec\u5c1d\u8bd5\u4f7f\u7528 Postman \u83b7\u53d6 text\/css \u5a92\u4f53\u7c7b\u578b\uff0c\u770b\u770b\u4f1a\u53d1\u751f\u4ec0\u4e48\uff1a<br \/>\n<a href=\"https:\/\/localhost:5001\/api\/companies\">https:\/\/localhost:5001\/api\/companies<\/a><\/p>\n<p><img decoding=\"async\" src=\"\/images\/ultimateaspnetcorewebapi6\/0705.jpg\" alt=\"alt text\" \/><\/p>\n<p>And as expected, there is no response body and all we get is a nice 406 Not Acceptable status code.<br \/>\n\u6b63\u5982\u9884\u671f\u7684\u90a3\u6837\uff0c\u6ca1\u6709\u54cd\u5e94\u6b63\u6587\uff0c\u6211\u4eec\u5f97\u5230\u7684\u53ea\u662f\u4e00\u4e2a\u5f88\u597d\u7684 406 Not Acceptable \u72b6\u6001\u4ee3\u7801\u3002<\/p>\n<p>So far so good.<br \/>\n\u76ee\u524d\u4e3a\u6b62\uff0c\u4e00\u5207\u90fd\u597d\u3002<\/p>\n<h2>7.5 More About Formatters<\/h2>\n<p>7.5 \u66f4\u591a\u5173\u4e8e Formatters<\/p>\n<p>If we want our API to support content negotiation for a type that is not \u201cin\u200c the box,\u201d we need to have a mechanism to do this.<br \/>\n\u5982\u679c\u6211\u4eec\u5e0c\u671b\u6211\u4eec\u7684 API \u652f\u6301\u975e\u201c\u5f00\u7bb1\u5373\u7528\u201d\u7684\u7c7b\u578b\u7684\u5185\u5bb9\u534f\u5546\uff0c\u6211\u4eec\u9700\u8981\u6709\u4e00\u79cd\u673a\u5236\u6765\u505a\u5230\u8fd9\u4e00\u70b9\u3002<\/p>\n<p>So, how can we do that?<br \/>\n\u90a3\u4e48\uff0c\u6211\u4eec\u8be5\u600e\u4e48\u505a\u5462\uff1f<\/p>\n<p>ASP.NET Core supports the creation of custom formatters. Their purpose is to give us the flexibility to create our formatter for any media types we need to support.<br \/>\nASP.NET Core \u652f\u6301\u521b\u5efa\u81ea\u5b9a\u4e49\u683c\u5f0f\u5316\u7a0b\u5e8f\u3002\u5b83\u4eec\u7684\u76ee\u7684\u662f\u8ba9\u6211\u4eec\u80fd\u591f\u7075\u6d3b\u5730\u4e3a\u9700\u8981\u652f\u6301\u7684\u4efb\u4f55\u5a92\u4f53\u7c7b\u578b\u521b\u5efa\u683c\u5f0f\u5316\u7a0b\u5e8f\u3002<\/p>\n<p>We can make the custom formatter by using the following method:<br \/>\n\u6211\u4eec\u53ef\u4ee5\u4f7f\u7528\u4ee5\u4e0b\u65b9\u6cd5\u5236\u4f5c\u81ea\u5b9a\u4e49\u683c\u5f0f\u5316\u7a0b\u5e8f\uff1a<\/p>\n<p>\u2022 Create an output formatter class that inherits the TextOutputFormatter class.<br \/>\n\u521b\u5efa\u7ee7\u627f TextOutputFormatter \u7c7b\u7684\u8f93\u51fa\u683c\u5f0f\u5316\u7a0b\u5e8f\u7c7b\u3002<\/p>\n<p>\u2022 Create an input formatter class that inherits the TextInputformatter class.<br \/>\n\u521b\u5efa\u7ee7\u627f TextInputformatter \u7c7b\u7684\u8f93\u5165\u683c\u5f0f\u5316\u7a0b\u5e8f\u7c7b\u3002<\/p>\n<p>\u2022 Add input and output classes to the InputFormatters and OutputFormatters collections the same way we did for the XML formatter.<br \/>\n\u5411 InputFormatters \u548c OutputFormatters \u96c6\u5408\u6dfb\u52a0\u8f93\u5165\u548c\u8f93\u51fa\u7c7b\uff0c\u65b9\u6cd5\u4e0e\u6211\u4eec\u5bf9 XML \u683c\u5f0f\u5316\u7a0b\u5e8f\u6267\u884c\u7684\u4f5c\u76f8\u540c\u3002<\/p>\n<p>Now let\u2019s have some fun and implement a custom CSV formatter for our example.<br \/>\n\u73b0\u5728\uff0c\u8ba9\u6211\u4eec\u6765\u627e\u70b9\u4e50\u5b50\uff0c\u4e3a\u6211\u4eec\u7684\u793a\u4f8b\u5b9e\u73b0\u4e00\u4e2a\u81ea\u5b9a\u4e49\u7684 CSV \u683c\u5f0f\u5316\u7a0b\u5e8f\u3002<\/p>\n<h2>7.6  Implementing a Custom Formatter<\/h2>\n<p>7.6 \u5b9e\u73b0\u81ea\u5b9a\u4e49\u683c\u5f0f\u5316\u7a0b\u5e8f<\/p>\n<p>Since we are only interested in formatting responses, we need to implement only an output formatter. We would need an input formatter only if a request body contained a corresponding type.\u200c<br \/>\n\u7531\u4e8e\u6211\u4eec\u53ea\u5bf9\u683c\u5f0f\u5316\u54cd\u5e94\u611f\u5174\u8da3\uff0c\u56e0\u6b64\u6211\u4eec\u53ea\u9700\u8981\u5b9e\u73b0\u4e00\u4e2a\u8f93\u51fa\u683c\u5f0f\u5316\u7a0b\u5e8f\u3002\u53ea\u6709\u5f53\u8bf7\u6c42\u6b63\u6587\u5305\u542b\u76f8\u5e94\u7684\u7c7b\u578b\u65f6\uff0c\u6211\u4eec\u624d\u9700\u8981 input \u683c\u5f0f\u5316\u7a0b\u5e8f\u3002<\/p>\n<p>The idea is to format a response to return the list of companies in a CSV format.<br \/>\n\u8fd9\u4e2a\u60f3\u6cd5\u662f\u8bbe\u7f6e\u54cd\u5e94\u7684\u683c\u5f0f\uff0c\u4ee5 CSV \u683c\u5f0f\u8fd4\u56de\u516c\u53f8\u5217\u8868\u3002<\/p>\n<p>Let\u2019s add a CsvOutputFormatter class to our main project:<br \/>\n\u8ba9\u6211\u4eec\u5c06 CsvOutputFormatter \u7c7b\u6dfb\u52a0\u5230\u6211\u4eec\u7684\u4e3b\u9879\u76ee\u4e2d\uff1a<\/p>\n<pre><code>using Microsoft.AspNetCore.Mvc.Formatters;\nusing Microsoft.Net.Http.Headers;\nusing Shared.DataTransferObjects;\nusing System.Text;\n\nnamespace CompanyEmployees\n{\n    public class CsvOutputFormatter : TextOutputFormatter\n    {\n        public CsvOutputFormatter()\n        {\n            SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse(&quot;text\/csv&quot;));\n            SupportedEncodings.Add(Encoding.UTF8); SupportedEncodings.Add(Encoding.Unicode);\n        }\n        protected override bool CanWriteType(Type? type)\n        {\n            if (typeof(CompanyDto).IsAssignableFrom(type) || typeof(IEnumerable&lt;CompanyDto&gt;).IsAssignableFrom(type))\n            {\n                return base.CanWriteType(type);\n            }\n            return false;\n        }\n        public override async Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)\n        {\n            var response = context.HttpContext.Response; var buffer = new StringBuilder();\n            if (context.Object is IEnumerable&lt;CompanyDto&gt;)\n            {\n                foreach (var company in (IEnumerable&lt;CompanyDto&gt;)context.Object)\n                {\n                    FormatCsv(buffer, company);\n                }\n            }\n            else\n            {\n                FormatCsv(buffer, (CompanyDto)context.Object);\n            }\n            await response.WriteAsync(buffer.ToString());\n        }\n        private static void FormatCsv(StringBuilder buffer, CompanyDto company)\n        {\n            buffer.AppendLine($&quot;{company.Id},\\&quot;{company.Name},\\&quot;{company.FullAddress}\\&quot;&quot;);\n        }\n    }\n}<\/code><\/pre>\n<p>There are a few things to note here:<br \/>\n\u8fd9\u91cc\u6709\u51e0\u70b9\u9700\u8981\u6ce8\u610f\uff1a<\/p>\n<p>\u2022 In the constructor, we define which media type this formatter should parse as well as encodings.<br \/>\n\u5728\u6784\u9020\u51fd\u6570\u4e2d\uff0c\u6211\u4eec\u5b9a\u4e49\u6b64\u683c\u5f0f\u5316\u7a0b\u5e8f\u5e94\u89e3\u6790\u7684\u5a92\u4f53\u7c7b\u578b\u4ee5\u53ca\u7f16\u7801\u3002<\/p>\n<p>\u2022 The CanWriteType method is overridden, and it indicates whether or not the CompanyDto type can be written by this serializer.<br \/>\nCanWriteType \u65b9\u6cd5\u88ab\u8986\u76d6\uff0c\u5b83\u6307\u793a\u6b64\u5e8f\u5217\u5316\u7a0b\u5e8f\u662f\u5426\u53ef\u4ee5\u5199\u5165 CompanyDto \u7c7b\u578b\u3002<\/p>\n<p>\u2022 The WriteResponseBodyAsync method constructs the response.<br \/>\nWriteResponseBodyAsync \u65b9\u6cd5\u6784\u9020\u54cd\u5e94\u3002<\/p>\n<p>\u2022 And finally, we have the FormatCsv method that formats a response the way we want it.<br \/>\n\u6700\u540e\uff0c\u6211\u4eec\u6709 FormatCsv \u65b9\u6cd5\uff0c\u5b83\u53ef\u4ee5\u6309\u7167\u6211\u4eec\u60f3\u8981\u7684\u65b9\u5f0f\u683c\u5f0f\u5316\u54cd\u5e94\u3002<\/p>\n<p>The class is pretty straightforward to implement, and the main thing that you should focus on is the FormatCsv method logic.<br \/>\n\u8be5\u7c7b\u7684\u5b9e\u73b0\u975e\u5e38\u7b80\u5355\uff0c\u60a8\u5e94\u8be5\u5173\u6ce8\u7684\u4e3b\u8981\u5185\u5bb9\u662f FormatCsv \u65b9\u6cd5\u903b\u8f91\u3002<\/p>\n<p>Now we just need to add the newly made formatter to the list of OutputFormatters in the ServicesExtensions class:<br \/>\n\u73b0\u5728\uff0c\u6211\u4eec\u53ea\u9700\u5c06\u65b0\u521b\u5efa\u7684\u683c\u5f0f\u5316\u7a0b\u5e8f\u6dfb\u52a0\u5230 ServicesExtensions \u7c7b\u4e2d\u7684 OutputFormatters \u5217\u8868\u4e2d\uff1a<\/p>\n<pre><code>public static IMvcBuilder AddCustomCSVFormatter(this IMvcBuilder builder) =&gt; builder.AddMvcOptions(config =&gt; config.OutputFormatters.Add(new CsvOutputFormatter()));<\/code><\/pre>\n<p>And to call it in the AddControllers:<br \/>\n\u8981\u5728 AddController \u4e2d\u8c03\u7528\u5b83\uff1a<\/p>\n<pre><code>builder.Services.AddControllers(config =&gt; { config.RespectBrowserAcceptHeader = true; config.ReturnHttpNotAcceptable = true; }).AddXmlDataContractSerializerFormatters() .AddCustomCSVFormatter() .AddApplicationPart(typeof(CompanyEmployees.Presentation.AssemblyReference).Assembly);<\/code><\/pre>\n<p>Let\u2019s run this and see if it works. This time we will put text\/csv as the value for the Accept header:<br \/>\n\u8ba9\u6211\u4eec\u8fd0\u884c\u5b83\uff0c\u770b\u770b\u5b83\u662f\u5426\u6709\u6548\u3002\u8fd9\u6b21\u6211\u4eec\u5c06 text\/csv \u4f5c\u4e3a Accept \u6807\u5934\u7684\u503c\uff1a<\/p>\n<p><a href=\"https:\/\/localhost:5001\/api\/companies\">https:\/\/localhost:5001\/api\/companies<\/a><\/p>\n<p><img decoding=\"async\" src=\"\/images\/ultimateaspnetcorewebapi6\/0706.jpg\" alt=\"alt text\" \/><\/p>\n<p>Well, what do you know, it works!<br \/>\n\u55ef\uff0c\u4f60\u77e5\u9053\u4ec0\u4e48\uff0c\u5b83\u6709\u6548\uff01<\/p>\n<p>In this chapter, we finished working with GET requests in our project and we are ready to move on to the POST PUT and DELETE requests. We have a lot more ground to cover, so let\u2019s get down to business.<br \/>\n\u5728\u672c\u7ae0\u4e2d\uff0c\u6211\u4eec\u5b8c\u6210\u4e86\u9879\u76ee\u4e2d GET \u8bf7\u6c42\u7684\u5904\u7406\uff0c\u5e76\u51c6\u5907\u7ee7\u7eed\u5904\u7406 POST PUT \u548c DELETE \u8bf7\u6c42\u3002\u6211\u4eec\u8fd8\u6709\u5f88\u591a\u9886\u57df\u8981\u6db5\u76d6\uff0c\u6240\u4ee5\u8ba9\u6211\u4eec\u5f00\u59cb\u8c08\u6b63\u4e8b\u3002<\/p>\n","protected":false},"excerpt":{"rendered":"<p>7 CONTENT NEGOTIATION 7 \u5185\u5bb9\u534f\u5546 Content negotiation is one of the quality-of-life improvements we can add to our REST API to make it more user-friendly and flexible. And when we design an API, isn\u2019t that what we want to achieve in the first place?\u200c \u5185\u5bb9\u534f\u5546\u662f\u6211\u4eec\u53ef\u4ee5\u6dfb\u52a0\u5230 REST API \u4e2d\u7684\u751f\u6d3b\u8d28\u91cf\u6539\u8fdb\u4e4b\u4e00\uff0c\u4ee5\u4f7f\u5176\u66f4\u52a0\u7528\u6237\u53cb\u597d\u548c\u7075\u6d3b\u3002\u5f53\u6211\u4eec\u8bbe\u8ba1 API \u65f6\uff0c\u8fd9\u4e0d\u6b63\u662f\u6211\u4eec\u6700\u521d\u60f3\u8981\u5b9e\u73b0\u7684\u76ee\u6807\u5417\uff1f Content negotiation is an HTTP [&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-1112","post","type-post","status-publish","format-standard","hentry","category-uncategorized"],"_links":{"self":[{"href":"https:\/\/diji.net\/index.php?rest_route=\/wp\/v2\/posts\/1112","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=1112"}],"version-history":[{"count":0,"href":"https:\/\/diji.net\/index.php?rest_route=\/wp\/v2\/posts\/1112\/revisions"}],"wp:attachment":[{"href":"https:\/\/diji.net\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=1112"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/diji.net\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=1112"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/diji.net\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=1112"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}