{"id":1116,"date":"2025-05-27T14:46:34","date_gmt":"2025-05-27T06:46:34","guid":{"rendered":"https:\/\/www.hyy.net\/?p=1116"},"modified":"2025-05-27T14:46:34","modified_gmt":"2025-05-27T06:46:34","slug":"ultimate-asp-net-core-web-api-9-creating-resources","status":"publish","type":"post","link":"https:\/\/diji.net\/?p=1116","title":{"rendered":"Ultimate ASP.NET Core Web API  9 CREATING RESOURCES"},"content":{"rendered":"<p>9 CREATING RESOURCES<br \/>\n9 \u521b\u5efa\u8d44\u6e90<\/p>\n<p>In this section, we are going to show you how to use the POST HTTP method to create resources in the database.\u200c<br \/>\n\u5728\u672c\u8282\u4e2d\uff0c\u6211\u4eec\u5c06\u5411\u60a8\u5c55\u793a\u5982\u4f55\u4f7f\u7528 POST HTTP \u65b9\u6cd5\u5728\u6570\u636e\u5e93\u4e2d\u521b\u5efa\u8d44\u6e90\u3002<\/p>\n<p>So, let\u2019s start.<br \/>\n\u90a3\u4e48\uff0c\u8ba9\u6211\u4eec\u5f00\u59cb\u5427\u3002<\/p>\n<h2>9.1 Handling POST Requests<\/h2>\n<p>9.1 \u5904\u7406 POST \u8bf7\u6c42<\/p>\n<p>Firstly, let\u2019s modify the decoration attribute for the GetCompany action in the Companies controller:\u200c<br \/>\n\u9996\u5148\uff0c\u8ba9\u6211\u4eec\u5728 Companies \u63a7\u5236\u5668\u4e2d\u4fee\u6539 GetCompany\u4f5c\u7684 decoration \u5c5e\u6027\uff1a<\/p>\n<pre><code>[HttpGet(&quot;{id:guid}&quot;, Name = &quot;CompanyById&quot;)]<\/code><\/pre>\n<p>With this modification, we are setting the name for the action. This name will come in handy in the action method for creating a new company.<br \/>\n\u901a\u8fc7\u6b64\u4fee\u6539\uff0c\u6211\u4eec\u5c06\u8bbe\u7f6e\u4f5c\u7684\u540d\u79f0\u3002\u8fd9\u4e2a\u540d\u5b57\u5728\u521b\u5efa\u65b0\u516c\u53f8\u7684\u4f5c\u65b9\u6cd5\u4e2d\u4f1a\u6d3e\u4e0a\u7528\u573a\u3002<\/p>\n<p>We have a DTO class for the output (the GET methods), but right now we need the one for the input as well. So, let\u2019s create a new record in the Shared\/DataTransferObjects folder:<br \/>\n\u6211\u4eec\u6709\u4e00\u4e2a\u7528\u4e8e\u8f93\u51fa\u7684 DTO \u7c7b\uff08GET \u65b9\u6cd5\uff09\uff0c\u4f46\u73b0\u5728\u6211\u4eec\u4e5f\u9700\u8981\u4e00\u4e2a\u7528\u4e8e\u8f93\u5165\u7684 DTO \u7c7b\u3002\u56e0\u6b64\uff0c\u8ba9\u6211\u4eec\u5728 Shared\/DataTransferObjects \u6587\u4ef6\u5939\u4e2d\u521b\u5efa\u4e00\u4e2a\u65b0\u8bb0\u5f55\uff1a<\/p>\n<pre><code>public record CompanyForCreationDto(string Name, string Address, string Country);<\/code><\/pre>\n<p>We can see that this DTO record is almost the same as the Company record but without the Id property. We don\u2019t need that property when we create an entity.<br \/>\n\u6211\u4eec\u53ef\u4ee5\u770b\u5230\uff0c\u6b64 DTO \u8bb0\u5f55\u4e0e Company \u8bb0\u5f55\u51e0\u4e4e\u76f8\u540c\uff0c\u4f46\u6ca1\u6709 Id \u5c5e\u6027\u3002\u5728\u521b\u5efa\u5b9e\u4f53\u65f6\uff0c\u6211\u4eec\u4e0d\u9700\u8981\u8be5\u5c5e\u6027\u3002<\/p>\n<p>We should pay attention to one more thing. In some projects, the input and output DTO classes are the same, but we still recommend separating them for easier maintenance and refactoring of our code. Furthermore, when we start talking about validation, we don\u2019t want to validate the output objects \u2014 but we definitely want to validate the input ones.<br \/>\n\u6211\u4eec\u8fd8\u5e94\u8be5\u6ce8\u610f\u4e00\u4ef6\u4e8b\u3002\u5728\u67d0\u4e9b\u9879\u76ee\u4e2d\uff0c\u8f93\u5165\u548c\u8f93\u51fa DTO \u7c7b\u662f\u76f8\u540c\u7684\uff0c\u4f46\u6211\u4eec\u4ecd\u7136\u5efa\u8bae\u5c06\u5b83\u4eec\u5206\u5f00\uff0c\u4ee5\u4fbf\u4e8e\u7ef4\u62a4\u548c\u91cd\u6784\u4ee3\u7801\u3002\u6b64\u5916\uff0c\u5f53\u6211\u4eec\u5f00\u59cb\u8ba8\u8bba\u9a8c\u8bc1\u65f6\uff0c\u6211\u4eec\u4e0d\u60f3\u9a8c\u8bc1\u8f93\u51fa\u5bf9\u8c61 \u2014 \u4f46\u6211\u4eec\u80af\u5b9a\u60f3\u8981\u9a8c\u8bc1\u8f93\u5165\u5bf9\u8c61\u3002<\/p>\n<p>With all of that said and done, let\u2019s continue by modifying the ICompanyRepository interface:<br \/>\n\u5b8c\u6210\u6240\u6709\u8fd9\u4e9b\u4f5c\u540e\uff0c\u8ba9\u6211\u4eec\u7ee7\u7eed\u4fee\u6539 ICompanyRepository \u63a5\u53e3\uff1a<\/p>\n<pre><code>using Entities.Models;\n\nnamespace Contracts\n{\n    public interface ICompanyRepository\n    {\n        IEnumerable&lt;Company&gt; GetAllCompanies(bool trackChanges); \n        Company GetCompany(Guid companyId, bool trackChanges);\n        void CreateCompany(Company company);\n    }\n}<\/code><\/pre>\n<p>After the interface modification, we are going to implement that interface:<br \/>\n\u4fee\u6539\u63a5\u53e3\u540e\uff0c\u6211\u4eec\u5c06\u5b9e\u73b0\u8be5\u63a5\u53e3\uff1a<\/p>\n<pre><code>public void CreateCompany(Company company) =&gt; Create(company);<\/code><\/pre>\n<p>We don\u2019t explicitly generate a new Id for our company; this would be done by EF Core. All we do is to set the state of the company to Added.<br \/>\n\u6211\u4eec\u4e0d\u4f1a\u4e3a\u516c\u53f8\u663e\u5f0f\u751f\u6210\u65b0 ID;\u8fd9\u5c06\u7531 EF Core \u5b8c\u6210\u3002\u6211\u4eec\u6240\u505a\u7684\u53ea\u662f\u5c06\u516c\u53f8\u7684\u72b6\u6001\u8bbe\u7f6e\u4e3a Added\u3002<\/p>\n<p>Next, we want to modify the ICompanyService interface:<br \/>\n\u63a5\u4e0b\u6765\uff0c\u6211\u4eec\u8981\u4fee\u6539 ICompanyService \u63a5\u53e3\uff1a<\/p>\n<pre><code>using Shared.DataTransferObjects;\n\nnamespace Service.Contracts\n{\n    public interface ICompanyService\n    {\n        IEnumerable&lt;CompanyDto&gt; GetAllCompanies(bool trackChanges);\n        CompanyDto GetCompany(Guid companyId, bool trackChanges);\n        CompanyDto CreateCompany(CompanyForCreationDto company);\n    }\n}<\/code><\/pre>\n<p>And of course, we have to implement this method in the CompanyService class:<br \/>\n\u5f53\u7136\uff0c\u6211\u4eec\u5fc5\u987b\u5728 CompanyService \u7c7b\u4e2d\u5b9e\u73b0\u6b64\u65b9\u6cd5\uff1a<\/p>\n<pre><code>public CompanyDto CreateCompany(CompanyForCreationDto company)\n{\n    var companyEntity = _mapper.Map&lt;Company&gt;(company);\n    _repository.Company.CreateCompany(companyEntity);\n    _repository.Save();\n    var companyToReturn = _mapper.Map&lt;CompanyDto&gt;(companyEntity);\n    return companyToReturn;\n}<\/code><\/pre>\n<p>Here, we map the company for creation to the company entity, call the repository method for creation, and call the Save() method to save the entity to the database. After that, we map the company entity to the company DTO object to return it to the controller.<br \/>\n\u5728\u8fd9\u91cc\uff0c\u6211\u4eec\u5c06\u8981\u521b\u5efa\u7684\u516c\u53f8\u6620\u5c04\u5230\u516c\u53f8\u5b9e\u4f53\uff0c\u8c03\u7528\u7528\u4e8e\u521b\u5efa\u7684\u5b58\u50a8\u5e93\u65b9\u6cd5\uff0c\u5e76\u8c03\u7528 Save\uff08\uff09 \u65b9\u6cd5\u5c06\u5b9e\u4f53\u4fdd\u5b58\u5230\u6570\u636e\u5e93\u3002\u4e4b\u540e\uff0c\u6211\u4eec\u5c06\u516c\u53f8\u5b9e\u4f53\u6620\u5c04\u5230\u516c\u53f8 DTO \u5bf9\u8c61\uff0c\u4ee5\u5c06\u5176\u8fd4\u56de\u7ed9\u63a7\u5236\u5668\u3002<\/p>\n<p>But we don\u2019t have the mapping rule for this so we have to create another mapping rule for the Company and CompanyForCreationDto objects.Let\u2019s do this in the MappingProfile class:<br \/>\n\u4f46\u662f\u6211\u4eec\u6ca1\u6709\u7528\u4e8e\u6b64\u76ee\u7684\u7684\u6620\u5c04\u89c4\u5219\uff0c\u56e0\u6b64\u6211\u4eec\u5fc5\u987b\u4e3a Company \u548c CompanyForCreationDto \u5bf9\u8c61\u521b\u5efa\u53e6\u4e00\u4e2a\u6620\u5c04\u89c4\u5219\u3002\u8ba9\u6211\u4eec\u5728 MappingProfile \u7c7b\u4e2d\u6267\u884c\u6b64\u4f5c\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;().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            CreateMap&lt;CompanyForCreationDto, Company&gt;();\n        }\n    }\n}<\/code><\/pre>\n<p>Our POST action will accept a parameter of the type CompanyForCreationDto, and as you can see our service method accepts the parameter of the same type as well, but we need the Company object to send it to the repository layer for creation. Therefore, we have to create this mapping rule.<br \/>\n\u6211\u4eec\u7684 POST\u4f5c\u5c06\u63a5\u53d7 CompanyForCreationDto \u7c7b\u578b\u7684\u53c2\u6570\uff0c\u5982\u60a8\u6240\u89c1\uff0c\u6211\u4eec\u7684\u670d\u52a1\u65b9\u6cd5\u4e5f\u63a5\u53d7\u76f8\u540c\u7c7b\u578b\u7684\u53c2\u6570\uff0c\u4f46\u6211\u4eec\u9700\u8981 Company \u5bf9\u8c61\u5c06\u5176\u53d1\u9001\u5230\u5b58\u50a8\u5e93\u5c42\u8fdb\u884c\u521b\u5efa\u3002\u56e0\u6b64\uff0c\u6211\u4eec\u5fc5\u987b\u521b\u5efa\u6b64\u6620\u5c04\u89c4\u5219\u3002<\/p>\n<p>Last, let\u2019s modify the controller:<br \/>\n\u6700\u540e\uff0c\u6211\u4eec\u4fee\u6539\u63a7\u5236\u5668\uff1a<\/p>\n<pre><code>[HttpPost]\npublic IActionResult CreateCompany([FromBody] CompanyForCreationDto company)\n{\n    if (company is null)\n        return BadRequest(&quot;CompanyForCreationDto object is null&quot;);\n    var createdCompany = _service.CompanyService.CreateCompany(company);\n\n    return CreatedAtRoute(&quot;CompanyById&quot;, new { id = createdCompany.Id }, createdCompany);\n}<\/code><\/pre>\n<p>Let\u2019s use Postman to send the request and examine the result:<br \/>\n\u8ba9\u6211\u4eec\u4f7f\u7528 Postman \u53d1\u9001\u8bf7\u6c42\u5e76\u68c0\u67e5\u7ed3\u679c\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\/0901.jpg\" alt=\"alt text\" \/><\/p>\n<h2>9.2 Code Explanation<\/h2>\n<p>9.2 \u4ee3\u7801\u8bf4\u660e<\/p>\n<p>Let\u2019s talk a little bit about this code. The interface and the repository parts are pretty clear, so we won\u2019t talk about that. We have already explained the code in the service method. But the code in the controller contains several things worth mentioning.\u200c<br \/>\n\u6211\u4eec\u6765\u8c08\u8c08\u8fd9\u6bb5\u4ee3\u7801\u3002\u754c\u9762\u548c\u5b58\u50a8\u5e93\u90e8\u5206\u975e\u5e38\u6e05\u6670\uff0c\u56e0\u6b64\u6211\u4eec\u4e0d\u4f1a\u8ba8\u8bba\u8fd9\u4e9b\u3002\u6211\u4eec\u5df2\u7ecf\u5728 service \u65b9\u6cd5\u4e2d\u89e3\u91ca\u4e86\u4ee3\u7801\u3002\u4f46\u662f\u63a7\u5236\u5668\u4e2d\u7684\u4ee3\u7801\u5305\u542b\u51e0\u4e2a\u503c\u5f97\u4e00\u63d0\u7684\u5185\u5bb9\u3002<\/p>\n<p>If you take a look at the request URI, you\u2019ll see that we use the same one as for the GetCompanies action: api\/companies \u2014 but this time we are using the POST request.<br \/>\n\u5982\u679c\u60a8\u67e5\u770b\u8bf7\u6c42 URI\uff0c\u60a8\u4f1a\u53d1\u73b0\u6211\u4eec\u4f7f\u7528\u4e0e GetCompanies\u4f5c\u76f8\u540c\u7684 URI\uff1aapi\/companies\uff0c\u4f46\u8fd9\u6b21\u6211\u4eec\u4f7f\u7528\u7684\u662f POST \u8bf7\u6c42\u3002<\/p>\n<p>The CreateCompany method has its own [HttpPost] decoration attribute, which restricts it to POST requests. Furthermore, notice the company parameter which comes from the client. We are not collecting it from the URI but the request body. Thus the usage of the [FromBody] attribute. Also, the company object is a complex type; therefore, we have to use [FromBody].<br \/>\nCreateCompany \u65b9\u6cd5\u5177\u6709\u81ea\u5df1\u7684 [HttpPost] \u4fee\u9970\u5c5e\u6027\uff0c\u8be5\u5c5e\u6027\u5c06\u5176\u9650\u5236\u4e3a POST \u8bf7\u6c42\u3002\u6b64\u5916\uff0c\u8bf7\u6ce8\u610f\u6765\u81ea\u5ba2\u6237\u7aef\u7684 company \u53c2\u6570\u3002\u6211\u4eec\u4e0d\u662f\u4ece URI \u4e2d\u6536\u96c6\u6570\u636e\uff0c\u800c\u662f\u4ece\u8bf7\u6c42\u6b63\u6587\u4e2d\u6536\u96c6\u6570\u636e\u3002\u56e0\u6b64\uff0c\u4f7f\u7528[FromBody] \u5c5e\u6027\u3002\u6b64\u5916\uff0ccompany \u5bf9\u8c61\u662f\u4e00\u4e2a\u590d\u6742\u7c7b\u578b;\u56e0\u6b64\uff0c\u6211\u4eec\u5fc5\u987b\u4f7f\u7528 [FromBody]\u3002<\/p>\n<p>If we wanted to, we could explicitly mark the action to take this parameter from the URI by decorating it with the [FromUri] attribute, though we wouldn\u2019t recommend that at all because of security reasons and the complexity of the request.<br \/>\n\u5982\u679c\u9700\u8981\uff0c\u6211\u4eec\u53ef\u4ee5\u901a\u8fc7\u4f7f\u7528 [FromUri] \u5c5e\u6027\u4fee\u9970\u4f5c\u6765\u663e\u5f0f\u6807\u8bb0\u4ece URI \u4e2d\u83b7\u53d6\u6b64\u53c2\u6570\u7684\u4f5c\uff0c\u5c3d\u7ba1\u51fa\u4e8e\u5b89\u5168\u539f\u56e0\u548c\u8bf7\u6c42\u7684\u590d\u6742\u6027\uff0c\u6211\u4eec\u6839\u672c\u4e0d\u5efa\u8bae\u8fd9\u6837\u505a\u3002<\/p>\n<p>Because the company parameter comes from the client, it could happen that it can\u2019t be deserialized. As a result, we have to validate it against the reference type\u2019s default value, which is null.<br \/>\n\u7531\u4e8e company \u53c2\u6570\u6765\u81ea\u5ba2\u6237\u7aef\uff0c\u56e0\u6b64\u53ef\u80fd\u65e0\u6cd5\u5bf9\u5176\u8fdb\u884c\u53cd\u5e8f\u5217\u5316\u3002\u56e0\u6b64\uff0c\u6211\u4eec\u5fc5\u987b\u6839\u636e\u5f15\u7528\u7c7b\u578b\u7684\u9ed8\u8ba4\u503c \uff08null\uff09 \u5bf9\u5176\u8fdb\u884c\u9a8c\u8bc1\u3002<\/p>\n<p>The last thing to mention is this part of the code:<br \/>\n\u6700\u540e\u8981\u63d0\u5230\u7684\u662f\u8fd9\u90e8\u5206\u4ee3\u7801\uff1a<\/p>\n<pre><code>CreatedAtRoute(&quot;CompanyById&quot;, new { id = companyToReturn.Id }, companyToReturn);<\/code><\/pre>\n<p>CreatedAtRoute will return a status code 201, which stands for Created. Also, it will populate the body of the response with the new company object as well as the Location attribute within the response header with the address to retrieve that company. We need to provide the name of the action, where we can retrieve the created entity.<br \/>\nCreatedAtRoute \u5c06\u8fd4\u56de\u72b6\u6001\u4ee3\u7801 201\uff0c\u4ee3\u8868 Created\u3002\u6b64\u5916\uff0c\u5b83\u8fd8\u5c06\u4f7f\u7528\u65b0\u7684 company \u5bf9\u8c61\u4ee5\u53ca\u54cd\u5e94\u6807\u5934\u4e2d\u7684 Location \u5c5e\u6027\u586b\u5145\u54cd\u5e94\u6b63\u6587\uff0c\u5176\u4e2d\u5305\u542b\u7528\u4e8e\u68c0\u7d22\u8be5\u516c\u53f8\u7684\u5730\u5740\u3002\u6211\u4eec\u9700\u8981\u63d0\u4f9b\u4f5c\u7684\u540d\u79f0\uff0c\u4ee5\u4fbf\u6211\u4eec\u53ef\u4ee5\u5728\u5176\u4e2d\u68c0\u7d22\u521b\u5efa\u7684\u5b9e\u4f53\u3002<\/p>\n<p>If we take a look at the headers part of our response, we are going to see a link to retrieve the created company:<br \/>\n\u5982\u679c\u6211\u4eec\u67e5\u770b\u54cd\u5e94\u7684 headers \u90e8\u5206\uff0c\u6211\u4eec\u5c06\u770b\u5230\u4e00\u4e2a\u94fe\u63a5\u6765\u68c0\u7d22\u521b\u5efa\u7684\u516c\u53f8\uff1a<\/p>\n<p><img decoding=\"async\" src=\"\/images\/ultimateaspnetcorewebapi6\/0902.jpg\" alt=\"alt text\" \/><\/p>\n<p>Finally, from the previous example, we can confirm that the POST method is neither safe nor idempotent. We saw that when we send the POST request, it is going to create a new resource in the database \u2014 thus changing the resource representation. Furthermore, if we try to send this request a couple of times, we will get a new object for every request (it will have a different Id for sure).<br \/>\n\u6700\u540e\uff0c\u4ece\u524d\u9762\u7684\u793a\u4f8b\u4e2d\uff0c\u6211\u4eec\u53ef\u4ee5\u786e\u8ba4 POST \u65b9\u6cd5\u65e2\u4e0d\u5b89\u5168\u4e5f\u4e0d\u5e42\u7b49\u3002\u6211\u4eec\u770b\u5230\uff0c\u5f53\u6211\u4eec\u53d1\u9001 POST \u8bf7\u6c42\u65f6\uff0c\u5b83\u5c06\u5728\u6570\u636e\u5e93\u4e2d\u521b\u5efa\u4e00\u4e2a\u65b0\u8d44\u6e90\u2014\u2014\u4ece\u800c\u6539\u53d8\u8d44\u6e90\u8868\u793a\u5f62\u5f0f\u3002\u6b64\u5916\uff0c\u5982\u679c\u6211\u4eec\u5c1d\u8bd5\u591a\u6b21\u53d1\u9001\u6b64\u8bf7\u6c42\uff0c\u6211\u4eec\u5c06\u4e3a\u6bcf\u4e2a\u8bf7\u6c42\u83b7\u53d6\u4e00\u4e2a\u65b0\u5bf9\u8c61\uff08\u5b83\u80af\u5b9a\u4f1a\u5177\u6709\u4e0d\u540c\u7684 Id\uff09\u3002<\/p>\n<p>Excellent.<br \/>\n\u975e\u5e38\u597d\u3002<\/p>\n<p>There is still one more thing we need to explain.<br \/>\n\u8fd8\u6709\u4e00\u4ef6\u4e8b\u6211\u4eec\u9700\u8981\u89e3\u91ca\u3002<\/p>\n<h3>9.2.1 Validation from the ApiController Attribute\u200c<\/h3>\n<p>9.2.1 \u4ece ApiController \u5c5e\u6027\u8fdb\u884c\u9a8c\u8bc1<\/p>\n<p>In this section, we are going to talk about the [ApiController] attribute that we can find right below the [Route] attribute in our controller:<br \/>\n\u5728\u672c\u8282\u4e2d\uff0c\u6211\u4eec\u5c06\u8ba8\u8bba [ApiController] \u5c5e\u6027\uff0c\u6211\u4eec\u53ef\u4ee5\u5728\u63a7\u5236\u5668\u4e2d\u7684 [Route] \u5c5e\u6027\u6b63\u4e0b\u65b9\u627e\u5230\u8be5\u5c5e\u6027\uff1a<\/p>\n<pre><code>[Route(&quot;api\/companies&quot;)] [ApiController] public class CompaniesController : ControllerBase {<\/code><\/pre>\n<p>But, before we start with the explanation, let\u2019s place a breakpoint in the CreateCompany action, right on the if (company is null) check.Then, let\u2019s use Postman to send an invalid POST request:<br \/>\n\u4f46\u662f\uff0c\u5728\u5f00\u59cb\u89e3\u91ca\u4e4b\u524d\uff0c\u8ba9\u6211\u4eec\u5728 CreateCompany\u4f5c\u4e2d\u653e\u7f6e\u4e00\u4e2a\u65ad\u70b9\uff0c\u5c31\u5728 if \uff08company is null\uff09 \u68c0\u67e5\u4e0a\u3002\u7136\u540e\uff0c\u8ba9\u6211\u4eec\u4f7f\u7528 Postman \u53d1\u9001\u4e00\u4e2a\u65e0\u6548\u7684 POST \u8bf7\u6c42\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\/0903.jpg\" alt=\"alt text\" \/><\/p>\n<p>We are going to talk about Validation in chapter 13, but for now, we have to explain a couple of things.<br \/>\n\u6211\u4eec\u5c06\u5728\u7b2c 13 \u7ae0\u8ba8\u8bba\u9a8c\u8bc1\uff0c\u4f46\u73b0\u5728\uff0c\u6211\u4eec\u5fc5\u987b\u89e3\u91ca\u51e0\u4ef6\u4e8b\u3002<\/p>\n<p>First of all, we have our response - a Bad Request in Postman, and we have error messages that state what\u2019s wrong with our request. But, we never hit that breakpoint that we\u2019ve placed inside the CreateCompany action.<br \/>\n\u9996\u5148\uff0c\u6211\u4eec\u6709\u6211\u4eec\u7684\u54cd\u5e94 - Postman \u4e2d\u7684 Bad Request\uff0c\u5e76\u4e14\u6211\u4eec\u6709\u9519\u8bef\u6d88\u606f\u8bf4\u660e\u6211\u4eec\u7684\u8bf7\u6c42\u51fa\u4e86\u4ec0\u4e48\u95ee\u9898\u3002\u4f46\u662f\uff0c\u6211\u4eec\u4ece\u672a\u9047\u5230\u653e\u7f6e\u5728 CreateCompany\u4f5c\u4e2d\u7684\u65ad\u70b9\u3002<\/p>\n<p>Why is that?<br \/>\n\u4e3a\u4ec0\u4e48\uff1f<\/p>\n<p>Well, the [ApiController] attribute is applied to a controller class to enable the following opinionated, API-specific behaviors:<br \/>\n\u55ef\uff0c[ApiController] \u5c5e\u6027\u5e94\u7528\u4e8e\u63a7\u5236\u5668\u7c7b\uff0c\u4ee5\u542f\u7528\u4ee5\u4e0b\u7279\u5b9a\u4e8e API \u7684\u56fa\u6267\u5df1\u89c1\u7684\u884c\u4e3a\uff1a<\/p>\n<p>\u2022 Attribute routing requirement<br \/>\n\u5c5e\u6027\u8def\u7531\u8981\u6c42<\/p>\n<p>\u2022 Automatic HTTP 400 responses<br \/>\n\u81ea\u52a8 HTTP 400 \u54cd\u5e94<\/p>\n<p>\u2022 Binding source parameter inference<br \/>\n\u7ed1\u5b9a\u6e90\u53c2\u6570\u63a8\u7406<\/p>\n<p>\u2022 Multipart\/form-data request inference<br \/>\n\u90e8\u5206\/\u8868\u5355\u6570\u636e\u8bf7\u6c42\u63a8\u7406<\/p>\n<p>\u2022 Problem details for error status codes<br \/>\n\u9519\u8bef\u72b6\u6001\u4ee3\u7801\u7684\u95ee\u9898\u8be6\u7ec6\u4fe1\u606f<\/p>\n<p>As you can see, it handles the HTTP 400 responses, and in our case, since the request\u2019s body is null, the [ApiController] attribute handles that and returns the 400 (BadReqeust) response before the request even hits the CreateCompany action.<br \/>\n\u5982\u60a8\u6240\u89c1\uff0c\u5b83\u5904\u7406 HTTP 400 \u54cd\u5e94\uff0c\u5728\u6211\u4eec\u7684\u793a\u4f8b\u4e2d\uff0c\u7531\u4e8e\u8bf7\u6c42\u6b63\u6587\u4e3a null\uff0c\u56e0\u6b64 [ApiController] \u5c5e\u6027\u4f1a\u5904\u7406\u8be5\u54cd\u5e94\uff0c\u5e76\u5728\u8bf7\u6c42\u5230\u8fbe CreateCompany\u4f5c\u4e4b\u524d\u8fd4\u56de 400 \uff08BadReqeust\uff09 \u54cd\u5e94\u3002<\/p>\n<p>This is useful behavior, but it prevents us from sending our custom responses with different messages and status codes to the client. This will be very important once we get to the Validation.<br \/>\n\u8fd9\u662f\u6709\u7528\u7684\u884c\u4e3a\uff0c\u4f46\u5b83\u4f1a\u963b\u6b62\u6211\u4eec\u5c06\u5177\u6709\u4e0d\u540c\u6d88\u606f\u548c\u72b6\u6001\u4ee3\u7801\u7684\u81ea\u5b9a\u4e49\u54cd\u5e94\u53d1\u9001\u5230\u5ba2\u6237\u7aef\u3002\u4e00\u65e6\u6211\u4eec\u8fdb\u5165\u9a8c\u8bc1\uff0c\u8fd9\u5c06\u975e\u5e38\u91cd\u8981\u3002<\/p>\n<p>So to enable our custom responses from the actions, we are going to add this code into the Program class right above the AddControllers method:<br \/>\n\u56e0\u6b64\uff0c\u4e3a\u4e86\u542f\u7528\u6765\u81ea\u4f5c\u7684\u81ea\u5b9a\u4e49\u54cd\u5e94\uff0c\u6211\u4eec\u5c06\u4ee5\u4e0b\u4ee3\u7801\u6dfb\u52a0\u5230 AddControllers \u65b9\u6cd5\u6b63\u4e0a\u65b9\u7684 Program \u7c7b\u4e2d\uff1a<\/p>\n<pre><code>builder.Services.Configure&lt;ApiBehaviorOptions&gt;(options =&gt; { options.SuppressModelStateInvalidFilter = true; });<\/code><\/pre>\n<p>With this, we are suppressing a default model state validation that is implemented due to the existence of the [ApiController] attribute in all API controllers. So this means that we can solve the same problem differently, by commenting out or removing the [ApiController] attribute only, without additional code for suppressing validation. It's all up to you. But we like keeping it in our controllers because, as you could\u2019ve seen, it provides additional functionalities other than just 400 \u2013 Bad Request responses.<br \/>\n\u8fd9\u6837\uff0c\u6211\u4eec\u5c06\u7981\u6b62\u9ed8\u8ba4\u6a21\u578b\u72b6\u6001\u9a8c\u8bc1\uff0c\u8be5\u9a8c\u8bc1\u662f\u7531\u4e8e\u6240\u6709 API \u63a7\u5236\u5668\u4e2d\u5b58\u5728 [ApiController] \u5c5e\u6027\u800c\u5b9e\u73b0\u7684\u3002\u56e0\u6b64\uff0c\u8fd9\u610f\u5473\u7740\u6211\u4eec\u53ef\u4ee5\u4ee5\u4e0d\u540c\u7684\u65b9\u5f0f\u89e3\u51b3\u76f8\u540c\u7684\u95ee\u9898\uff0c\u53ea\u9700\u6ce8\u91ca\u6389\u6216\u5220\u9664 [ApiController] \u5c5e\u6027\uff0c\u800c\u65e0\u9700\u989d\u5916\u7684\u4ee3\u7801\u6765\u6291\u5236\u9a8c\u8bc1\u3002\u8fd9\u4e00\u5207\u90fd\u53d6\u51b3\u4e8e\u60a8\u3002\u4f46\u662f\u6211\u4eec\u559c\u6b22\u5c06\u5b83\u4fdd\u5b58\u5728\u6211\u4eec\u7684\u63a7\u5236\u5668\u4e2d\uff0c\u56e0\u4e3a\u6b63\u5982\u4f60\u6240\u770b\u5230\u7684\uff0c\u5b83\u63d0\u4f9b\u4e86\u989d\u5916\u7684\u529f\u80fd\uff0c\u800c\u4e0d\u4ec5\u4ec5\u662f 400 \u2013 Bad Request \u54cd\u5e94\u3002<\/p>\n<p>Now, once we start the app and send the same request, we will hit that breakpoint and see our response in Postman.<br \/>\n\u73b0\u5728\uff0c\u4e00\u65e6\u6211\u4eec\u542f\u52a8\u5e94\u7528\u7a0b\u5e8f\u5e76\u53d1\u9001\u76f8\u540c\u7684\u8bf7\u6c42\uff0c\u6211\u4eec\u5c06\u547d\u4e2d\u8be5\u65ad\u70b9\u5e76\u5728 Postman \u4e2d\u770b\u5230\u6211\u4eec\u7684\u54cd\u5e94\u3002<\/p>\n<p>Nicely done.<br \/>\n\u5e72\u5f97\u6f02\u4eae\u3002<\/p>\n<p>Now, we can remove that breakpoint and continue with learning about the creation of child resources.<br \/>\n\u73b0\u5728\uff0c\u6211\u4eec\u53ef\u4ee5\u5220\u9664\u8be5\u65ad\u70b9\u5e76\u7ee7\u7eed\u4e86\u89e3\u5b50\u8d44\u6e90\u7684\u521b\u5efa\u3002<\/p>\n<h2>9.3 Creating a Child Resource<\/h2>\n<p>9.3 \u521b\u5efa\u5b50\u8d44\u6e90<\/p>\n<p>While creating our company, we created the DTO object required for the CreateCompany action. So, for employee creation, we are going to do the same thing:\u200c<br \/>\n\u5728\u521b\u5efa\u516c\u53f8\u65f6\uff0c\u6211\u4eec\u521b\u5efa\u4e86 CreateCompany\u4f5c\u6240\u9700\u7684 DTO \u5bf9\u8c61\u3002\u6240\u4ee5\uff0c\u5bf9\u4e8e\u5458\u5de5\u521b\u5efa\uff0c\u6211\u4eec\u5c06\u505a\u540c\u6837\u7684\u4e8b\u60c5\uff1a<\/p>\n<pre><code>public record EmployeeForCreationDto(string Name, int Age, string Position);<\/code><\/pre>\n<p>We don\u2019t have the Id property because we are going to create that Id on the server-side. But additionally, we don\u2019t have the CompanyId because we accept that parameter through the route:[Route(&quot;api\/companies\/{companyId}\/employees&quot;)]<br \/>\n\u6211\u4eec\u6ca1\u6709 Id \u5c5e\u6027\uff0c\u56e0\u4e3a\u6211\u4eec\u8981\u5728\u670d\u52a1\u5668\u7aef\u521b\u5efa\u8be5 Id\u3002\u4f46\u6b64\u5916\uff0c\u6211\u4eec\u6ca1\u6709 CompanyId\uff0c\u56e0\u4e3a\u6211\u4eec\u901a\u8fc7\u8def\u7531\u63a5\u53d7\u8be5\u53c2\u6570\uff1a[Route\uff08\u201capi\/companies\/{companyId}\/employees\u201d\uff09]<\/p>\n<p>The next step is to modify the IEmployeeRepository interface:<br \/>\n\u4e0b\u4e00\u6b65\u662f\u4fee\u6539 IEmployeeRepository \u63a5\u53e3\uff1a<\/p>\n<pre><code>using Entities.Models;\n\nnamespace Contracts\n{\n    public interface IEmployeeRepository\n    {\n        IEnumerable&lt;Employee&gt; GetEmployees(Guid companyId, bool trackChanges);\n        Employee GetEmployee(Guid companyId, Guid id, bool trackChanges);\n        void CreateEmployeeForCompany(Guid companyId, Employee employee);\n    }\n}<\/code><\/pre>\n<p>Of course, we have to implement this interface:<br \/>\n\u5f53\u7136\uff0c\u6211\u4eec\u5fc5\u987b\u5b9e\u73b0\u8fd9\u4e2a\u63a5\u53e3\uff1a<\/p>\n<pre><code>using Contracts;\nusing Entities.Models;\n\nnamespace Repository\n{\n    public class EmployeeRepository : RepositoryBase&lt;Employee&gt;, IEmployeeRepository\n    {\n        public EmployeeRepository(RepositoryContext repositoryContext) : base(repositoryContext) { }\n        public IEnumerable&lt;Employee&gt; GetEmployees(Guid companyId, bool trackChanges) =&gt; FindByCondition(e =&gt; e.CompanyId.Equals(companyId), trackChanges).OrderBy(e =&gt; e.Name).ToList();\n        public Employee GetEmployee(Guid companyId, Guid id, bool trackChanges) =&gt; FindByCondition(e =&gt; e.CompanyId.Equals(companyId) &amp;&amp; e.Id.Equals(id), trackChanges).SingleOrDefault();\n        public void CreateEmployeeForCompany(Guid companyId, Employee employee) { employee.CompanyId = companyId; Create(employee); }\n\n    }\n}<\/code><\/pre>\n<p>Because we are going to accept the employee DTO object in our action and send it to a service method, but we also have to send an employee object to this repository method, we have to create an additional mapping rule in the MappingProfile class:<br \/>\n\u56e0\u4e3a\u6211\u4eec\u8981\u5728\u4f5c\u4e2d\u63a5\u53d7 employee DTO \u5bf9\u8c61\u5e76\u5c06\u5176\u53d1\u9001\u5230 service \u65b9\u6cd5\uff0c\u4f46\u6211\u4eec\u8fd8\u5fc5\u987b\u5c06 employee \u5bf9\u8c61\u53d1\u9001\u5230\u6b64\u5b58\u50a8\u5e93\u65b9\u6cd5\uff0c\u56e0\u6b64\u6211\u4eec\u5fc5\u987b\u5728 MappingProfile \u7c7b\u4e2d\u521b\u5efa\u989d\u5916\u7684\u6620\u5c04\u89c4\u5219\uff1a<\/p>\n<pre><code>CreateMap&lt;EmployeeForCreationDto, Employee&gt;();<\/code><\/pre>\n<p>The next thing we have to do is IEmployeeService modification:<br \/>\n\u63a5\u4e0b\u6765\u6211\u4eec\u8981\u505a\u7684\u662f\u4fee\u6539 IEmployeeService\uff1a<\/p>\n<pre><code>using Shared.DataTransferObjects;\n\nnamespace Service.Contracts\n{\n    public interface IEmployeeService\n    {\n        IEnumerable&lt;EmployeeDto&gt; GetEmployees(Guid companyId, bool trackChanges);\n        EmployeeDto GetEmployee(Guid companyId, Guid id, bool trackChanges);\n        EmployeeDto CreateEmployeeForCompany(Guid companyId, EmployeeForCreationDto employeeForCreation, bool trackChanges);\n    }\n}<\/code><\/pre>\n<p>And implement this new method in EmployeeService:<br \/>\n\u5e76\u5728 EmployeeService \u4e2d\u5b9e\u73b0\u8fd9\u4e2a\u65b0\u65b9\u6cd5\uff1a<\/p>\n<pre><code>public EmployeeDto CreateEmployeeForCompany(Guid companyId, EmployeeForCreationDto employeeForCreation, bool trackChanges)\n{\n    var company = _repository.Company.GetCompany(companyId, trackChanges);\n    if (company is null) throw new CompanyNotFoundException(companyId);\n    var employeeEntity = _mapper.Map&lt;Employee&gt;(employeeForCreation);\n    _repository.Employee.CreateEmployeeForCompany(companyId, employeeEntity);\n    _repository.Save();\n    var employeeToReturn = _mapper.Map&lt;EmployeeDto&gt;(employeeEntity);\n    return employeeToReturn;\n}<\/code><\/pre>\n<p>We have to check whether that company exists in the database because there is no point in creating an employee for a company that does not exist. After that, we map the DTO to an entity, call the repository methods to create a new employee, map back the entity to the DTO, and return it to the caller.<br \/>\n\u6211\u4eec\u5fc5\u987b\u68c0\u67e5\u6570\u636e\u5e93\u4e2d\u662f\u5426\u5b58\u5728\u8be5\u516c\u53f8\uff0c\u56e0\u4e3a\u4e3a\u4e0d\u5b58\u5728\u7684\u516c\u53f8\u521b\u5efa\u5458\u5de5\u662f\u6ca1\u6709\u610f\u4e49\u7684\u3002\u4e4b\u540e\uff0c\u6211\u4eec\u5c06 DTO \u6620\u5c04\u5230\u5b9e\u4f53\uff0c\u8c03\u7528\u5b58\u50a8\u5e93\u65b9\u6cd5\u4ee5\u521b\u5efa\u65b0\u5458\u5de5\uff0c\u5c06\u5b9e\u4f53\u6620\u5c04\u56de DTO\uff0c\u5e76\u5c06\u5176\u8fd4\u56de\u7ed9\u8c03\u7528\u65b9\u3002<\/p>\n<p>Now, we can add a new action in the EmployeesController:<br \/>\n\u73b0\u5728\uff0c\u6211\u4eec\u53ef\u4ee5\u5728 EmployeesController \u4e2d\u6dfb\u52a0\u4e00\u4e2a\u65b0\u52a8\u4f5c\uff1a<\/p>\n<pre><code>[HttpPost]\npublic IActionResult CreateEmployeeForCompany(Guid companyId, [FromBody] EmployeeForCreationDto employee)\n{\n    if (employee is null) \n        return BadRequest(&quot;EmployeeForCreationDto object is null&quot;);\n    var employeeToReturn = _service.EmployeeService.CreateEmployeeForCompany(companyId, employee, trackChanges: false);\n    return CreatedAtRoute(&quot;GetEmployeeForCompany&quot;, new { companyId, id = employeeToReturn.Id }, employeeToReturn);\n}<\/code><\/pre>\n<p>As we can see, the main difference between this action and the CreateCompany action (if we exclude the fact that we are working with different DTOs) is the return statement, which now has two parameters for the anonymous object.<br \/>\n\u6b63\u5982\u6211\u4eec\u6240\u770b\u5230\u7684\uff0c\u6b64\u4f5c\u4e0e CreateCompany\u4f5c\uff08\u5982\u679c\u6392\u9664\u6211\u4eec\u4f7f\u7528\u7684\u662f\u4e0d\u540c DTO \u7684\u4e8b\u5b9e\uff09\u4e4b\u95f4\u7684\u4e3b\u8981\u533a\u522b\u5728\u4e8e return \u8bed\u53e5\uff0c\u8be5\u8bed\u53e5\u73b0\u5728\u6709\u4e24\u4e2a\u7528\u4e8e\u533f\u540d\u5bf9\u8c61\u7684\u53c2\u6570\u3002<\/p>\n<p>For this to work, we have to modify the HTTP attribute above the GetEmployeeForCompany action:<br \/>\n\u4e3a\u6b64\uff0c\u6211\u4eec\u5fc5\u987b\u4fee\u6539 GetEmployeeForCompany\u4f5c\u4e0a\u65b9\u7684 HTTP \u5c5e\u6027\uff1a<\/p>\n<pre><code>[HttpGet(&quot;{id:guid}&quot;, Name = &quot;GetEmployeeForCompany&quot;)]\npublic IActionResult GetEmployeeForCompany(Guid companyId, Guid id)\n{\n    var employee = _service.EmployeeService.GetEmployee(companyId, id, trackChanges: false);\n    return Ok(employee);\n}<\/code><\/pre>\n<p>Let\u2019s give this a try:<br \/>\n\u8ba9\u6211\u4eec\u8bd5\u4e00\u8bd5\uff1a<br \/>\n<a href=\"https:\/\/localhost:5001\/api\/companies\/14759d51-e9c1-4afc-f9bf-08d98898c9c3\/employees\">https:\/\/localhost:5001\/api\/companies\/14759d51-e9c1-4afc-f9bf-08d98898c9c3\/employees<\/a><\/p>\n<p><img decoding=\"async\" src=\"\/images\/ultimateaspnetcorewebapi6\/0904.jpg\" alt=\"alt text\" \/><\/p>\n<p>Excellent. A new employee was created.<br \/>\n\u975e\u5e38\u597d\u3002\u521b\u5efa\u4e86\u4e00\u4e2a\u65b0\u5458\u5de5\u3002<\/p>\n<p>If we take a look at the Headers tab, we'll see a link to fetch our newly created employee. If you copy that link and send another request with it, you will get this employee for sure:<br \/>\n\u5982\u679c\u6211\u4eec\u67e5\u770b Headers \u9009\u9879\u5361\uff0c\u6211\u4eec\u5c06\u770b\u5230\u4e00\u4e2a\u94fe\u63a5\uff0c\u7528\u4e8e\u83b7\u53d6\u6211\u4eec\u65b0\u521b\u5efa\u7684\u5458\u5de5\u3002\u5982\u679c\u60a8\u590d\u5236\u8be5\u94fe\u63a5\u5e76\u53d1\u9001\u53e6\u4e00\u4e2a\u8bf7\u6c42\uff0c\u60a8\u80af\u5b9a\u4f1a\u5f97\u5230\u8fd9\u4e2a\u5458\u5de5\uff1a<\/p>\n<p><img decoding=\"async\" src=\"\/images\/ultimateaspnetcorewebapi6\/0905.jpg\" alt=\"alt text\" \/><\/p>\n<h2>9.4 Creating Children Resources Together with a Parent<\/h2>\n<p>9.4 \u4e0e\u7236\u8d44\u6e90\u4e00\u8d77\u521b\u5efa\u5b50\u8d44\u6e90<\/p>\n<p>There are situations where we want to create a parent resource with its children. Rather than using multiple requests for every single child, we want to do this in the same request with the parent resource.\u200c<br \/>\n\u5728\u67d0\u4e9b\u60c5\u51b5\u4e0b\uff0c\u6211\u4eec\u5e0c\u671b\u521b\u5efa\u4e00\u4e2a\u5305\u542b\u5176\u5b50\u8d44\u6e90\u7684\u7236\u8d44\u6e90\u3002\u6211\u4eec\u5e0c\u671b\u5728\u5177\u6709\u7236\u8d44\u6e90\u7684\u540c\u4e00\u8bf7\u6c42\u4e2d\u6267\u884c\u6b64\u4f5c\uff0c\u800c\u4e0d\u662f\u5bf9\u6bcf\u4e2a\u5b50\u9879\u4f7f\u7528\u591a\u4e2a\u8bf7\u6c42\u3002<\/p>\n<p>We are going to show you how to do this.<br \/>\n\u6211\u4eec\u5c06\u5411\u60a8\u5c55\u793a\u5982\u4f55\u6267\u884c\u6b64\u4f5c\u3002<\/p>\n<p>The first thing we are going to do is extend the CompanyForCreationDto class:<br \/>\n\u6211\u4eec\u8981\u505a\u7684\u7b2c\u4e00\u4ef6\u4e8b\u662f\u6269\u5c55 CompanyForCreationDto \u7c7b\uff1a<\/p>\n<pre><code>public record CompanyForCreationDto(string Name, string Address, string Country, IEnumerable&lt;EmployeeForCreationDto&gt; Employees);<\/code><\/pre>\n<p>We are not going to change the action logic inside the controller nor the repository\/service logic; everything is great there. That\u2019s all. Let\u2019s test it:<br \/>\n\u6211\u4eec\u4e0d\u4f1a\u66f4\u6539\u63a7\u5236\u5668\u5185\u90e8\u7684\u52a8\u4f5c\u903b\u8f91\uff0c\u4e5f\u4e0d\u4f1a\u66f4\u6539\u5b58\u50a8\u5e93\/\u670d\u52a1\u903b\u8f91;\u90a3\u91cc\u7684\u4e00\u5207\u90fd\u5f88\u68d2\u3002\u5c31\u8fd9\u6837\u3002\u8ba9\u6211\u4eec\u6d4b\u8bd5\u4e00\u4e0b\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\/0906.jpg\" alt=\"alt text\" \/><\/p>\n<p>You can see that this company was created successfully.<br \/>\n\u60a8\u53ef\u4ee5\u770b\u5230\u6b64\u516c\u53f8\u5df2\u6210\u529f\u521b\u5efa\u3002<\/p>\n<p>Now we can copy the location link from the Headers tab, paste it in another Postman tab, and just add the \/employees part:<br \/>\n\u73b0\u5728\u6211\u4eec\u53ef\u4ee5\u4ece Headers \u9009\u9879\u5361\u590d\u5236\u4f4d\u7f6e\u94fe\u63a5\uff0c\u5c06\u5176\u7c98\u8d34\u5230\u53e6\u4e00\u4e2a Postman \u9009\u9879\u5361\u4e2d\uff0c\u7136\u540e\u6dfb\u52a0 \/employees \u90e8\u5206\uff1a<\/p>\n<p><img decoding=\"async\" src=\"\/images\/ultimateaspnetcorewebapi6\/0907.jpg\" alt=\"alt text\" \/><\/p>\n<p>We have confirmed that the employees were created as well.<br \/>\n\u6211\u4eec\u5df2\u7ecf\u786e\u8ba4\u5458\u5de5\u4e5f\u88ab\u521b\u5efa\u51fa\u6765\u4e86\u3002<\/p>\n<p>9.5 Creating a Collection of Resources<br \/>\n9.5 \u521b\u5efa\u8d44\u6e90\u96c6\u5408<\/p>\n<p>Until now, we have been creating a single resource whether it was Company or Employee. But it is quite normal to create a collection of resources, and in this section that is something we are going to work with.\u200c<br \/>\n\u5230\u76ee\u524d\u4e3a\u6b62\uff0c\u6211\u4eec\u4e00\u76f4\u5728\u521b\u5efa\u5355\u4e2a\u8d44\u6e90\uff0c\u65e0\u8bba\u662f Company \u8fd8\u662f Employee\u3002\u4f46\u662f\u521b\u5efa\u8d44\u6e90\u96c6\u5408\u662f\u5f88\u6b63\u5e38\u7684\uff0c\u5728\u672c\u8282\u4e2d\uff0c\u6211\u4eec\u5c06\u8981\u5904\u7406\u8fd9\u4e00\u70b9\u3002<\/p>\n<p>If we take a look at the CreateCompany action, for example, we can see that the return part points to the CompanyById route (the GetCompany action). That said, we don\u2019t have the GET action for the collection creating action to point to. So, before we start with the POST collection action, we are going to create the GetCompanyCollection action in the Companies controller.<br \/>\n\u4f8b\u5982\uff0c\u5982\u679c\u6211\u4eec\u770b\u4e00\u4e0b CreateCompany\u4f5c\uff0c\u6211\u4eec\u53ef\u4ee5\u770b\u5230\u8fd4\u56de\u90e8\u5206\u6307\u5411 CompanyById \u8def\u7531\uff08GetCompany\u4f5c\uff09\u3002\u4e5f\u5c31\u662f\u8bf4\uff0c\u6211\u4eec\u6ca1\u6709\u8981\u6307\u5411\u7684\u96c6\u5408\u521b\u5efa\u4f5c\u7684 GET\u4f5c\u3002\u56e0\u6b64\uff0c\u5728\u5f00\u59cb\u6267\u884c POST \u96c6\u5408\u4f5c\u4e4b\u524d\uff0c\u6211\u4eec\u5c06\u5728 Companies \u63a7\u5236\u5668\u4e2d\u521b\u5efa GetCompanyCollection\u4f5c\u3002<\/p>\n<p>But first, let's modify the ICompanyRepository interface:<br \/>\n\u4f46\u9996\u5148\uff0c\u8ba9\u6211\u4eec\u4fee\u6539 ICompanyRepository \u63a5\u53e3\uff1a<\/p>\n<pre><code>IEnumerable&lt;Company&gt; GetByIds(IEnumerable&lt;Guid&gt; ids, bool trackChanges);<\/code><\/pre>\n<p>Then we have to change the CompanyRepository class:<br \/>\n\u7136\u540e\u6211\u4eec\u5fc5\u987b\u66f4\u6539 CompanyRepository \u7c7b\uff1a<\/p>\n<pre><code>public IEnumerable&lt;Company&gt; GetByIds(IEnumerable&lt;Guid&gt; ids, bool trackChanges) =&gt; FindByCondition(x =&gt; ids.Contains(x.Id), trackChanges) .ToList();<\/code><\/pre>\n<p>After that, we are going to modify ICompanyService:<br \/>\n\u4e4b\u540e\uff0c\u6211\u4eec\u5c06\u4fee\u6539 ICompanyService\uff1a<\/p>\n<pre><code>using Shared.DataTransferObjects;\n\nnamespace Service.Contracts\n{\n    public interface ICompanyService\n    {\n        IEnumerable&lt;CompanyDto&gt; GetAllCompanies(bool trackChanges);\n        CompanyDto GetCompany(Guid companyId, bool trackChanges);\n        CompanyDto CreateCompany(CompanyForCreationDto company);\n        IEnumerable&lt;CompanyDto&gt; GetByIds(IEnumerable&lt;Guid&gt; ids, bool trackChanges);\n    }\n}<\/code><\/pre>\n<p>And implement this in CompanyService:<br \/>\n\u5e76\u5728 CompanyService \u4e2d\u5b9e\u73b0\u8fd9\u4e00\u70b9\uff1a<\/p>\n<pre><code>public IEnumerable&lt;CompanyDto&gt; GetByIds(IEnumerable&lt;Guid&gt; ids, bool trackChanges)\n{\n    if (ids is null)\n        throw new IdParametersBadRequestException();\n    var companyEntities = _repository.Company.GetByIds(ids, trackChanges);\n    if (ids.Count() != companyEntities.Count())\n        throw new CollectionByIdsBadRequestException();\n    var companiesToReturn = _mapper.Map&lt;IEnumerable&lt;CompanyDto&gt;&gt;(companyEntities);\n    return companiesToReturn;\n}<\/code><\/pre>\n<p>Here, we check if ids parameter is null and if it is we stop the execution flow and return a bad request response to the client. If it\u2019s not null, we fetch all the companies for each id in the ids collection. If the count of ids and companies mismatch, we return another bad request response to the client. Finally, we are executing the mapping action and returning the result to the caller.<br \/>\n\u5728\u8fd9\u91cc\uff0c\u6211\u4eec\u68c0\u67e5 ids \u53c2\u6570\u662f\u5426\u4e3a null\uff0c\u5982\u679c\u4e3a null\uff0c\u5219\u505c\u6b62\u6267\u884c\u6d41\u5e76\u5411\u5ba2\u6237\u7aef\u8fd4\u56de\u9519\u8bef\u7684\u8bf7\u6c42\u54cd\u5e94\u3002\u5982\u679c\u5b83\u4e0d\u4e3a null\uff0c\u6211\u4eec\u5c06\u83b7\u53d6 ids \u96c6\u5408\u4e2d\u6bcf\u4e2a id \u7684\u6240\u6709\u516c\u53f8\u3002\u5982\u679c ids \u548c companies \u7684\u8ba1\u6570\u4e0d\u5339\u914d\uff0c\u6211\u4eec\u5c06\u5411\u5ba2\u6237\u7aef\u8fd4\u56de\u53e6\u4e00\u4e2a\u9519\u8bef\u7684\u8bf7\u6c42\u54cd\u5e94\u3002\u6700\u540e\uff0c\u6211\u4eec\u6267\u884c mapping\u4f5c\u5e76\u5c06\u7ed3\u679c\u8fd4\u56de\u7ed9\u8c03\u7528\u8005\u3002<\/p>\n<p>Of course, we don\u2019t have these two exception classes yet, so let\u2019s create them.<br \/>\n\u5f53\u7136\uff0c\u6211\u4eec\u8fd8\u6ca1\u6709\u8fd9\u4e24\u4e2a\u5f02\u5e38\u7c7b\uff0c\u6240\u4ee5\u8ba9\u6211\u4eec\u521b\u5efa\u5b83\u4eec\u3002<\/p>\n<p>Since we are returning a bad request result, we are going to create a new abstract class in the Entities\/Exceptions folder:<br \/>\n\u7531\u4e8e\u6211\u4eec\u8fd4\u56de\u4e86\u4e00\u4e2a\u9519\u8bef\u7684\u8bf7\u6c42\u7ed3\u679c\uff0c\u56e0\u6b64\u6211\u4eec\u5c06\u5728 Entities\/Exceptions \u6587\u4ef6\u5939\u4e2d\u521b\u5efa\u4e00\u4e2a\u65b0\u7684\u62bd\u8c61\u7c7b\uff1a<\/p>\n<pre><code>namespace Entities.Exceptions\n{\n    public abstract class BadRequestException : Exception\n    {\n        protected BadRequestException(string message) : base(message) { }\n    }\n}<\/code><\/pre>\n<p>Then, in the same folder, let\u2019s create two new specific exception classes:<br \/>\n\u7136\u540e\uff0c\u5728\u540c\u4e00\u4e2a\u6587\u4ef6\u5939\u4e2d\uff0c\u8ba9\u6211\u4eec\u521b\u5efa\u4e24\u4e2a\u65b0\u7684\u7279\u5b9a\u5f02\u5e38\u7c7b\uff1a<\/p>\n<pre><code>namespace Entities.Exceptions\n{\n    public sealed class IdParametersBadRequestException : BadRequestException\n    {\n        public IdParametersBadRequestException() : base(&quot;Parameter ids is null&quot;) { }\n    }\n\n}<\/code><\/pre>\n<pre><code>namespace Entities.Exceptions\n{\n    public sealed class CollectionByIdsBadRequestException : BadRequestException\n    {\n        public CollectionByIdsBadRequestException() : base(&quot;Collection count mismatch comparing to ids.&quot;) { }\n    }\n}<\/code><\/pre>\n<p>At this point, we\u2019ve removed two errors from the GetByIds method. But, to show the correct response to the client, we have to modify the ConfigureExceptionHandler class \u2013 the part where we populate the StatusCode property:<br \/>\n\u6b64\u65f6\uff0c\u6211\u4eec\u5df2\u7ecf\u4ece GetByIds \u65b9\u6cd5\u4e2d\u5220\u9664\u4e86\u4e24\u4e2a\u9519\u8bef\u3002\u4f46\u662f\uff0c\u4e3a\u4e86\u5411\u5ba2\u6237\u7aef\u663e\u793a\u6b63\u786e\u7684\u54cd\u5e94\uff0c\u6211\u4eec\u5fc5\u987b\u4fee\u6539 ConfigureExceptionHandler \u7c7b \u2013 \u6211\u4eec\u586b\u5145 StatusCode \u5c5e\u6027\u7684\u90e8\u5206\uff1a<\/p>\n<pre><code>using Contracts;\nusing Entities.ErrorModel;\nusing Entities.Exceptions;\nusing Microsoft.AspNetCore.Diagnostics;\n\nnamespace CompanyEmployees.Extensions\n{\n    public static class ExceptionMiddlewareExtensions\n    {\n        public static void ConfigureExceptionHandler(this WebApplication app, ILoggerManager logger)\n        {\n            app.UseExceptionHandler(appError =&gt;\n            {\n                appError.Run(async context =&gt;\n                {\n                    context.Response.ContentType = &quot;application\/json&quot;;\n                    var contextFeature = context.Features.Get&lt;IExceptionHandlerFeature&gt;();\n                    if (contextFeature != null)\n                    {\n                        \/\/context.Response.StatusCode = contextFeature.Error\n                        \/\/switch\n                        \/\/{\n                        \/\/    NotFoundException =&gt; StatusCodes.Status404NotFound,\n                        \/\/    _ =&gt; StatusCodes.Status500InternalServerError\n                        \/\/};\n\n                        context.Response.StatusCode = contextFeature.Error switch\n                        {\n                            NotFoundException =&gt; StatusCodes.Status404NotFound,\n                            BadRequestException =&gt; StatusCodes.Status400BadRequest,\n                            _ =&gt; StatusCodes.Status500InternalServerError\n                        };\n\n                        logger.LogError($&quot;Something went wrong: {contextFeature.Error}&quot;);\n                        await context.Response.WriteAsync(new ErrorDetails()\n                        {\n                            StatusCode = context.Response.StatusCode,\n                            Message = contextFeature.Error.Message,\n                        }.ToString());\n                    }\n                });\n            });\n        }\n    }\n}<\/code><\/pre>\n<p>After that, we can add a new action in the controller:<br \/>\n\u4e4b\u540e\uff0c\u6211\u4eec\u53ef\u4ee5\u5728\u63a7\u5236\u5668\u4e2d\u6dfb\u52a0\u4e00\u4e2a\u65b0\u7684\u52a8\u4f5c\uff1a<\/p>\n<pre><code>[HttpGet(&quot;collection\/({ids})&quot;, Name = &quot;CompanyCollection&quot;)]\npublic IActionResult GetCompanyCollection(IEnumerable&lt;Guid&gt; ids)\n{\n    var companies = _service.CompanyService.GetByIds(ids, trackChanges: false);\n    return Ok(companies);\n}<\/code><\/pre>\n<p>And that's it. This action is pretty straightforward, so let's continue towards POST implementation.<br \/>\n\u5c31\u662f\u8fd9\u6837\u3002\u6b64\u4f5c\u975e\u5e38\u7b80\u5355\uff0c\u56e0\u6b64\u8ba9\u6211\u4eec\u7ee7\u7eed\u8fdb\u884c POST \u5b9e\u73b0\u3002<\/p>\n<p>Let\u2019s modify the ICompanyService interface first:<br \/>\n\u8ba9\u6211\u4eec\u5148\u4fee\u6539 ICompanyService \u63a5\u53e3\uff1a<\/p>\n<pre><code>using Shared.DataTransferObjects;\n\nnamespace Service.Contracts\n{\n    public interface ICompanyService\n    {\n        IEnumerable&lt;CompanyDto&gt; GetAllCompanies(bool trackChanges);\n        CompanyDto GetCompany(Guid companyId, bool trackChanges);\n        CompanyDto CreateCompany(CompanyForCreationDto company);\n        IEnumerable&lt;CompanyDto&gt; GetByIds(IEnumerable&lt;Guid&gt; ids, bool trackChanges);\n        (IEnumerable&lt;CompanyDto&gt; companies, string ids) CreateCompanyCollection(IEnumerable&lt;CompanyForCreationDto&gt; companyCollection);\n    }\n}<\/code><\/pre>\n<p>So, this new method will accept a collection of the CompanyForCreationDto type as a parameter, and return a Tuple with two fields (companies and ids) as a result.<br \/>\n\u56e0\u6b64\uff0c\u8fd9\u4e2a\u65b0\u65b9\u6cd5\u5c06\u63a5\u53d7 CompanyForCreationDto \u7c7b\u578b\u7684\u96c6\u5408\u4f5c\u4e3a\u53c2\u6570\uff0c\u5e76\u8fd4\u56de\u4e00\u4e2a\u5305\u542b\u4e24\u4e2a\u5b57\u6bb5\uff08companies \u548c ids\uff09\u7684 Tuples\u3002<\/p>\n<p>That said, let\u2019s implement it in the CompanyService class:<br \/>\n\u4e5f\u5c31\u662f\u8bf4\uff0c\u8ba9\u6211\u4eec\u5728 CompanyService \u7c7b\u4e2d\u5b9e\u73b0\u5b83\uff1a<\/p>\n<pre><code>public (IEnumerable&lt;CompanyDto&gt; companies, string ids) CreateCompanyCollection(IEnumerable&lt;CompanyForCreationDto&gt; companyCollection)\n{\n    if (companyCollection is null)\n        throw new CompanyCollectionBadRequest();\n    var companyEntities = _mapper.Map&lt;IEnumerable&lt;Company&gt;&gt;(companyCollection);\n    foreach (var company in companyEntities)\n    {\n        _repository.Company.CreateCompany(company);\n    }\n    _repository.Save(); \n    var companyCollectionToReturn = _mapper.Map&lt;IEnumerable&lt;CompanyDto&gt;&gt;(companyEntities);\n    var ids = string.Join(&quot;,&quot;, companyCollectionToReturn.Select(c =&gt; c.Id));\n    return (companies: companyCollectionToReturn, ids: ids);\n}<\/code><\/pre>\n<p>So, we check if our collection is null and if it is, we return a bad request. If it isn\u2019t, then we map that collection and save all the collection elements to the database. Finally, we map the company collection back, take all the ids as a comma-separated string, and return the Tuple with these two fields as a result to the caller.<br \/>\n\u56e0\u6b64\uff0c\u6211\u4eec\u68c0\u67e5\u6211\u4eec\u7684\u96c6\u5408\u662f\u5426\u4e3a null\uff0c\u5982\u679c\u4e3a null\uff0c\u5219\u8fd4\u56de\u4e00\u4e2a\u9519\u8bef\u7684\u8bf7\u6c42\u3002\u5982\u679c\u4e0d\u662f\uff0c\u90a3\u4e48\u6211\u4eec\u6620\u5c04\u8be5\u96c6\u5408\u5e76\u5c06\u6240\u6709\u96c6\u5408\u5143\u7d20\u4fdd\u5b58\u5230\u6570\u636e\u5e93\u4e2d\u3002\u6700\u540e\uff0c\u6211\u4eec\u5c06 company \u96c6\u5408\u6620\u5c04\u56de\u6765\uff0c\u5c06\u6240\u6709 id \u4f5c\u4e3a\u9017\u53f7\u5206\u9694\u7684\u5b57\u7b26\u4e32\uff0c\u5e76\u5c06\u5305\u542b\u8fd9\u4e24\u4e2a\u5b57\u6bb5\u7684 Tuple \u4f5c\u4e3a\u7ed3\u679c\u8fd4\u56de\u7ed9\u8c03\u7528\u8005\u3002<\/p>\n<p>Again, we can see that we don\u2019t have the exception class, so let\u2019s just create it:<br \/>\n\u540c\u6837\uff0c\u6211\u4eec\u53ef\u4ee5\u770b\u5230\u6211\u4eec\u6ca1\u6709 exception \u7c7b\uff0c\u6240\u4ee5\u8ba9\u6211\u4eec\u521b\u5efa\u5b83\uff1a<\/p>\n<pre><code>namespace Entities.Exceptions\n{\n    public sealed class CompanyCollectionBadRequest : BadRequestException\n    {\n        public CompanyCollectionBadRequest() : base(&quot;Company collection sent from a client is null.&quot;)\n        { }\n    }\n}<\/code><\/pre>\n<p>Finally, we can add a new action in the CompaniesController:<br \/>\n\u6700\u540e\uff0c\u6211\u4eec\u53ef\u4ee5\u5728 CompaniesController \u4e2d\u6dfb\u52a0\u4e00\u4e2a\u65b0\u4f5c\uff1a<\/p>\n<pre><code>[HttpPost(&quot;collection&quot;)]\npublic IActionResult CreateCompanyCollection([FromBody] IEnumerable&lt;CompanyForCreationDto&gt; companyCollection)\n{\n    var result = _service.CompanyService.CreateCompanyCollection(companyCollection);\n    return CreatedAtRoute(&quot;CompanyCollection&quot;, new { result.ids }, result.companies);\n}<\/code><\/pre>\n<p>We receive the companyCollection parameter from the client, send it to the service method, and return a result with a comma-separated string and our newly created companies.<br \/>\n\u6211\u4eec\u4ece\u5ba2\u6237\u7aef\u63a5\u6536 companyCollection \u53c2\u6570\uff0c\u5c06\u5176\u53d1\u9001\u5230 service \u65b9\u6cd5\uff0c\u5e76\u8fd4\u56de\u4e00\u4e2a\u5e26\u6709\u9017\u53f7\u5206\u9694\u5b57\u7b26\u4e32\u7684\u7ed3\u679c\u548c\u6211\u4eec\u65b0\u521b\u5efa\u7684\u516c\u53f8\u3002<\/p>\n<p>Now you may ask, why are we sending a comma-separated string when we expect a collection of ids in the GetCompanyCollection action?<br \/>\n\u73b0\u5728\u60a8\u53ef\u80fd\u4f1a\u95ee\uff0c\u5f53\u6211\u4eec\u671f\u671b\u5728 GetCompanyCollection\u4f5c\u4e2d\u6709\u4e00\u4e2a id \u96c6\u5408\u65f6\uff0c\u4e3a\u4ec0\u4e48\u8fd8\u8981\u53d1\u9001\u4e00\u4e2a\u9017\u53f7\u5206\u9694\u7684\u5b57\u7b26\u4e32\uff1f<\/p>\n<p>Well, we can\u2019t just pass a list of ids in the CreatedAtRoute method because there is no support for the Header Location creation with the list. You may try it, but we're pretty sure you would get the location like this:<br \/>\n\u6211\u4eec\u4e0d\u80fd\u53ea\u5728 CreatedAtRoute \u65b9\u6cd5\u4e2d\u4f20\u9012 id \u5217\u8868\uff0c\u56e0\u4e3a\u4e0d\u652f\u6301\u4f7f\u7528\u8be5\u5217\u8868\u521b\u5efa Header Location\u3002\u60a8\u53ef\u4ee5\u5c1d\u8bd5\u4e00\u4e0b\uff0c\u4f46\u6211\u4eec\u975e\u5e38\u786e\u5b9a\u60a8\u4f1a\u5f97\u5230\u8fd9\u6837\u7684\u4f4d\u7f6e\uff1a<\/p>\n<p><img decoding=\"async\" src=\"\/images\/ultimateaspnetcorewebapi6\/0908.jpg\" alt=\"alt text\" \/><\/p>\n<p>We can test our create action now with a bad request:<br \/>\n\u6211\u4eec\u73b0\u5728\u53ef\u4ee5\u4f7f\u7528\u9519\u8bef\u7684\u8bf7\u6c42\u6765\u6d4b\u8bd5\u6211\u4eec\u7684 create\u64cd\u4f5c\uff1a<br \/>\n<a href=\"https:\/\/localhost:5001\/api\/companies\/collection\">https:\/\/localhost:5001\/api\/companies\/collection<\/a><\/p>\n<p><img decoding=\"async\" src=\"\/images\/ultimateaspnetcorewebapi6\/0910.jpg\" alt=\"alt text\" \/><\/p>\n<p>We can see that the request is handled properly and we have a correct response.<br \/>\n\u6211\u4eec\u53ef\u4ee5\u770b\u5230\u8bf7\u6c42\u5f97\u5230\u4e86\u6b63\u786e\u7684\u5904\u7406\uff0c\u5e76\u4e14\u6211\u4eec\u5f97\u5230\u4e86\u6b63\u786e\u7684\u54cd\u5e94\u3002<\/p>\n<p>Now, let\u2019s send a valid request:<br \/>\n\u73b0\u5728\uff0c\u8ba9\u6211\u4eec\u53d1\u9001\u4e00\u4e2a\u6709\u6548\u7684\u8bf7\u6c42\uff1a<br \/>\n<a href=\"https:\/\/localhost:5001\/api\/companies\/collection\">https:\/\/localhost:5001\/api\/companies\/collection<\/a><\/p>\n<p><img decoding=\"async\" src=\"\/images\/ultimateaspnetcorewebapi6\/0911.jpg\" alt=\"alt text\" \/><\/p>\n<p>Excellent. Let\u2019s check the header tab:<br \/>\n\u975e\u5e38\u597d\u3002\u8ba9\u6211\u4eec\u68c0\u67e5\u4e00\u4e0b header \u9009\u9879\u5361\uff1a<\/p>\n<p><img decoding=\"async\" src=\"\/images\/ultimateaspnetcorewebapi6\/0912.jpg\" alt=\"alt text\" \/><\/p>\n<p>We can see a valid location link. So, we can copy it and try to fetch our newly created companies:<br \/>\n\u6211\u4eec\u53ef\u4ee5\u770b\u5230\u6709\u6548\u7684\u4f4d\u7f6e\u94fe\u63a5\u3002\u56e0\u6b64\uff0c\u6211\u4eec\u53ef\u4ee5\u590d\u5236\u5b83\u5e76\u5c1d\u8bd5\u83b7\u53d6\u6211\u4eec\u65b0\u521b\u5efa\u7684\u516c\u53f8\uff1a<\/p>\n<p><img decoding=\"async\" src=\"\/images\/ultimateaspnetcorewebapi6\/0913.jpg\" alt=\"alt text\" \/><\/p>\n<p>But we are getting the 415 Unsupported Media Type message. This is because our API can\u2019t bind the string type parameter to the IEnumerable<Guid> argument in the GetCompanyCollection action.<br \/>\n\u4f46\u662f\u6211\u4eec\u6536\u5230\u4e86 415 Unsupported Media Type \u6d88\u606f\u3002\u8fd9\u662f\u56e0\u4e3a\u6211\u4eec\u7684 API \u65e0\u6cd5\u5c06\u5b57\u7b26\u4e32\u7c7b\u578b\u53c2\u6570\u7ed1\u5b9a\u5230 GetCompanyCollection\u4f5c\u4e2d\u7684 IEnumerable \u53c2\u6570\u3002<\/p>\n<p>Well, we can solve this with a custom model binding.<br \/>\n\u597d\u5427\uff0c\u6211\u4eec\u53ef\u4ee5\u901a\u8fc7\u81ea\u5b9a\u4e49\u6a21\u578b\u7ed1\u5b9a\u6765\u89e3\u51b3\u8fd9\u4e2a\u95ee\u9898\u3002<\/p>\n<h2>9.6 Model Binding in API<\/h2>\n<p>9.6 API \u4e2d\u7684\u6a21\u578b\u7ed1\u5b9a<\/p>\n<p>Let\u2019s create the new folder ModelBinders in the Presentation project and inside the new class ArrayModelBinder:\u200c<br \/>\n\u8ba9\u6211\u4eec\u5728 Presentation \u9879\u76ee\u548c\u65b0\u7c7b ArrayModelBinder \u4e2d\u521b\u5efa\u65b0\u6587\u4ef6\u5939 ModelBinders\uff1a<\/p>\n<pre><code>using Microsoft.AspNetCore.Mvc.ModelBinding;\nusing System.ComponentModel;\nusing System.Reflection;\n\nnamespace CompanyEmployees.Presentation.ModelBinders\n{\n    public class ArrayModelBinder : IModelBinder\n    {\n        public Task BindModelAsync(ModelBindingContext bindingContext)\n        {\n            if (!bindingContext.ModelMetadata.IsEnumerableType)\n            {\n                bindingContext.Result = ModelBindingResult.Failed();\n                return Task.CompletedTask;\n            }\n            var providedValue = bindingContext.ValueProvider.GetValue(bindingContext.ModelName).ToString();\n            if (string.IsNullOrEmpty(providedValue))\n            {\n                bindingContext.Result = ModelBindingResult.Success(null);\n                return Task.CompletedTask;\n            }\n            var genericType = bindingContext.ModelType.GetTypeInfo().GenericTypeArguments[0];\n            var converter = TypeDescriptor.GetConverter(genericType);\n            var objectArray = providedValue.Split(new[] { &quot;,&quot; }, StringSplitOptions.RemoveEmptyEntries).Select(x =&gt; converter.ConvertFromString(x.Trim())).ToArray();\n            var guidArray = Array.CreateInstance(genericType, objectArray.Length);\n            objectArray.CopyTo(guidArray, 0);\n            bindingContext.Model = guidArray;\n            bindingContext.Result = ModelBindingResult.Success(bindingContext.Model);\n            return Task.CompletedTask;\n        }\n    }\n}\n<\/code><\/pre>\n<p>At first glance, this code might be hard to comprehend, but once we explain it, it will be easier to understand.<br \/>\n\u4e4d\u4e00\u770b\uff0c\u8fd9\u6bb5\u4ee3\u7801\u53ef\u80fd\u5f88\u96be\u7406\u89e3\uff0c\u4f46\u662f\u4e00\u65e6\u6211\u4eec\u89e3\u91ca\u4e86\u5b83\uff0c\u5c31\u4f1a\u66f4\u5bb9\u6613\u7406\u89e3\u3002<\/p>\n<p>We are creating a model binder for the IEnumerable type. Therefore, we have to check if our parameter is the same type.<br \/>\n\u6211\u4eec\u6b63\u5728\u4e3a IEnumerable \u7c7b\u578b\u521b\u5efa\u6a21\u578b\u7ed1\u5b9a\u5668\u3002\u56e0\u6b64\uff0c\u6211\u4eec\u5fc5\u987b\u68c0\u67e5\u6211\u4eec\u7684\u53c2\u6570\u662f\u5426\u4e3a\u76f8\u540c\u7c7b\u578b\u3002<\/p>\n<p>Next, we extract the value (a comma-separated string of GUIDs) with the ValueProvider.GetValue() expression. Because it is a type string, we just check whether it is null or empty. If it is, we return null as a result because we have a null check in our action in the controller. If it is not, we move on.<br \/>\n\u63a5\u4e0b\u6765\uff0c\u6211\u4eec\u4f7f\u7528 ValueProvider.GetValue\uff08\uff09 \u8868\u8fbe\u5f0f\u63d0\u53d6\u503c\uff08\u4ee5\u9017\u53f7\u5206\u9694\u7684 GUID \u5b57\u7b26\u4e32\uff09\u3002\u56e0\u4e3a\u5b83\u662f\u4e00\u4e2a\u7c7b\u578b\u5b57\u7b26\u4e32\uff0c\u6240\u4ee5\u6211\u4eec\u53ea\u68c0\u67e5\u5b83\u662f null \u8fd8\u662f\u7a7a\u3002\u5982\u679c\u662f\uff0c\u6211\u4eec\u5c06\u8fd4\u56de null\uff0c\u56e0\u4e3a\u6211\u4eec\u5728\u63a7\u5236\u5668\u4e2d\u7684\u4f5c\u4e2d\u6709 null \u68c0\u67e5\u3002\u5982\u679c\u4e0d\u662f\uff0c\u6211\u4eec\u7ee7\u7eed\u524d\u8fdb\u3002<\/p>\n<p>In the genericType variable, with the reflection help, we store the type the IEnumerable consists of. In our case, it is GUID. With the converter variable, we create a converter to a GUID type. As you can see, we didn\u2019t just force the GUID type in this model binder; instead, we inspected what is the nested type of the IEnumerable parameter and then created a converter for that exact type, thus making this binder generic.<br \/>\n\u5728 genericType \u53d8\u91cf\u4e2d\uff0c\u5728\u53cd\u5c04\u5e2e\u52a9\u4e0b\uff0c\u6211\u4eec\u5b58\u50a8 IEnumerable \u5305\u542b\u7684\u7c7b\u578b\u3002\u5728\u6211\u4eec\u7684\u4f8b\u5b50\u4e2d\uff0c\u5b83\u662f GUID\u3002\u4f7f\u7528 converter \u53d8\u91cf\uff0c\u6211\u4eec\u521b\u5efa\u5230 GUID \u7c7b\u578b\u7684\u8f6c\u6362\u5668\u3002\u5982\u4f60\u6240\u89c1\uff0c\u6211\u4eec\u4e0d\u4ec5\u5728\u6b64\u6a21\u578b\u7ed1\u5b9a\u5668\u4e2d\u5f3a\u5236\u4f7f\u7528 GUID \u7c7b\u578b;\u76f8\u53cd\uff0c\u6211\u4eec\u68c0\u67e5\u4e86 IEnumerable \u53c2\u6570\u7684\u5d4c\u5957\u7c7b\u578b\u662f\u4ec0\u4e48\uff0c\u7136\u540e\u4e3a\u8be5\u786e\u5207\u7c7b\u578b\u521b\u5efa\u4e86\u4e00\u4e2a\u8f6c\u6362\u5668\uff0c\u4ece\u800c\u4f7f\u6b64 Binder \u6210\u4e3a\u901a\u7528\u7684\u3002<\/p>\n<p>After that, we create an array of type object (objectArray) that consist of all the GUID values we sent to the API and then create an array of GUID types (guidArray), copy all the values from the objectArray to the guidArray, and assign it to the bindingContext.<br \/>\n\u4e4b\u540e\uff0c\u6211\u4eec\u521b\u5efa\u4e00\u4e2a\u5bf9\u8c61\u7c7b\u578b\u7684\u6570\u7ec4 \uff08objectArray\uff09\uff0c\u5176\u4e2d\u5305\u542b\u6211\u4eec\u53d1\u9001\u5230 API \u7684\u6240\u6709 GUID \u503c\uff0c\u7136\u540e\u521b\u5efa\u4e00\u4e2a GUID \u7c7b\u578b\u6570\u7ec4 \uff08guidArray\uff09\uff0c\u5c06\u6240\u6709\u503c\u4ece objectArray \u590d\u5236\u5230 guidArray\uff0c\u5e76\u5c06\u5176\u5206\u914d\u7ed9 bindingContext\u3002<\/p>\n<p>These are the required using directives:<br \/>\n\u8fd9\u4e9b\u662f\u5fc5\u9700\u7684 using \u6307\u4ee4\uff1a<\/p>\n<pre><code>using Microsoft.AspNetCore.Mvc.ModelBinding; \nusing System.ComponentModel; \nusing System.Reflection;<\/code><\/pre>\n<p>And that is it. Now, we have just to make a slight modification in the GetCompanyCollection action:<br \/>\n\u5c31\u662f\u8fd9\u6837\u3002\u73b0\u5728\uff0c\u6211\u4eec\u53ea\u9700\u8981\u5728 GetCompanyCollection\u4f5c\u4e2d\u8fdb\u884c\u8f7b\u5fae\u4fee\u6539\uff1a<\/p>\n<pre><code>[HttpGet(&quot;collection\/({ids})&quot;, Name = &quot;CompanyCollection&quot;)]\n\/\/ public IActionResult GetCompanyCollection(IEnumerable&lt;Guid&gt; ids)\npublic IActionResult GetCompanyCollection([ModelBinder(BinderType = typeof(ArrayModelBinder))] IEnumerable&lt;Guid&gt; ids)\n{\n    var companies = _service.CompanyService.GetByIds(ids, trackChanges: false);\n    return Ok(companies);\n}<\/code><\/pre>\n<p>This is the required namespace:<br \/>\n\u8fd9\u662f\u5fc5\u9700\u7684\u547d\u540d\u7a7a\u95f4\uff1a<\/p>\n<pre><code>using CompanyEmployees.Presentation.ModelBinders;<\/code><\/pre>\n<p>Visual Studio will provide two different namespaces to resolve the error, so be sure to pick the right one.<br \/>\nVisual Studio \u5c06\u63d0\u4f9b\u4e24\u4e2a\u4e0d\u540c\u7684\u547d\u540d\u7a7a\u95f4\u6765\u89e3\u51b3\u9519\u8bef\uff0c\u56e0\u6b64\u8bf7\u52a1\u5fc5\u9009\u62e9\u6b63\u786e\u7684\u547d\u540d\u7a7a\u95f4\u3002<\/p>\n<p>Excellent.<br \/>\n\u975e\u5e38\u597d\u3002<\/p>\n<p>Our ArrayModelBinder will be triggered before an action executes. It will convert the sent string parameter to the IEnumerable<Guid> type, and then the action will be executed:<br \/>\n\u6211\u4eec\u7684 ArrayModelBinder \u5c06\u5728 action \u6267\u884c\u4e4b\u524d\u89e6\u53d1\u3002\u5b83\u4f1a\u5c06\u53d1\u9001\u7684\u5b57\u7b26\u4e32\u53c2\u6570\u8f6c\u6362\u4e3a IEnumerable \u7c7b\u578b\uff0c\u7136\u540e\u6267\u884c\u4f5c\uff1a<\/p>\n<p><a href=\"https:\/\/localhost:5001\/api\/companies\/collection\/(582ea192-6fb7-44ff-a2a1-08d988ca3ca9,a216fbbe-ebbd-4e09-a2a2-08d988ca3ca9\">https:\/\/localhost:5001\/api\/companies\/collection\/(582ea192-6fb7-44ff-a2a1-08d988ca3ca9,a216fbbe-ebbd-4e09-a2a2-08d988ca3ca9<\/a>)<\/p>\n<p><img decoding=\"async\" src=\"\/images\/ultimateaspnetcorewebapi6\/0913.jpg\" alt=\"alt text\" \/><\/p>\n<p>Well done.<br \/>\n\u5e72\u7684\u597d\u3002<\/p>\n<p>We are ready to continue towards DELETE actions.<br \/>\n\u6211\u4eec\u5df2\u51c6\u5907\u597d\u7ee7\u7eed\u6267\u884c DELETE\u4f5c\u3002<\/p>\n","protected":false},"excerpt":{"rendered":"<p>9 CREATING RESOURCES 9 \u521b\u5efa\u8d44\u6e90 In this section, we are going to show you how to use the POST HTTP method to create resources in the database.\u200c \u5728\u672c\u8282\u4e2d\uff0c\u6211\u4eec\u5c06\u5411\u60a8\u5c55\u793a\u5982\u4f55\u4f7f\u7528 POST HTTP \u65b9\u6cd5\u5728\u6570\u636e\u5e93\u4e2d\u521b\u5efa\u8d44\u6e90\u3002 So, let\u2019s start. \u90a3\u4e48\uff0c\u8ba9\u6211\u4eec\u5f00\u59cb\u5427\u3002 9.1 Handling POST Requests 9.1 \u5904\u7406 POST \u8bf7\u6c42 Firstly, let\u2019s modify the decoration attribute for the GetCompany action in the [&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-1116","post","type-post","status-publish","format-standard","hentry","category-uncategorized"],"_links":{"self":[{"href":"https:\/\/diji.net\/index.php?rest_route=\/wp\/v2\/posts\/1116","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=1116"}],"version-history":[{"count":0,"href":"https:\/\/diji.net\/index.php?rest_route=\/wp\/v2\/posts\/1116\/revisions"}],"wp:attachment":[{"href":"https:\/\/diji.net\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=1116"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/diji.net\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=1116"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/diji.net\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=1116"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}