{"id":229,"date":"2023-10-08T22:03:06","date_gmt":"2023-10-08T14:03:06","guid":{"rendered":"https:\/\/miie.net\/?p=229"},"modified":"2023-10-08T22:03:06","modified_gmt":"2023-10-08T14:03:06","slug":"ultimate-asp-net-core-web-api-chapter9-creating-resuources","status":"publish","type":"post","link":"https:\/\/diji.net\/?p=229","title":{"rendered":"Chapter9:CREATING RESUOURCES"},"content":{"rendered":"<h1>9 CREATING RESUOURCES<\/h1>\n<p>In this section, we are going to show you how to use the POST HTTP method to create resources in the database.<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<\/p>\n<\/p>\n<p>So, let\u2019s start.<br \/>\n\u90a3\u4e48\uff0c\u8ba9\u6211\u4eec\u5f00\u59cb\u5427\u3002<\/p>\n<h2>9.1 Handing POST Requests<\/h2>\n<p>Firstly, let\u2019s modify the decoration attribute for the <strong>GetCompany<\/strong> action in the <strong>Companies<\/strong> controller:<br \/>\n\u9996\u5148\uff0c\u8ba9\u6211\u4eec\u4fee\u6539<strong>Companies<\/strong>\u63a7\u5236\u5668\u4e2d <strong>GetCompany<\/strong>  \u64cd\u4f5c\u7684 decoration \u5c5e\u6027<\/p>\n<pre class=\"brush: csharp; title: ; notranslate\" title=\"\">\n&#x5B;HttpGet(&quot;{id}&quot;, Name = &quot;CompanyById&quot;)]\n<\/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\u64cd\u4f5c\u7684\u540d\u79f0\u3002\u6b64\u540d\u79f0\u5c06\u5728\u521b\u5efa\u65b0\u516c\u53f8\u7684\u64cd\u4f5c\u65b9\u6cd5\u4e2d\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 class in the <strong>Entities\/DataTransferObjects<\/strong> folder:<br \/>\n\u6211\u4eec\u6709\u4e00\u4e2a\u7528\u4e8e\u8f93\u51fa\u7684DTO\u7c7b\uff08GET\u65b9\u6cd5\uff09\uff0c\u4f46\u73b0\u5728\u6211\u4eec\u4e5f\u9700\u8981\u4e00\u4e2a\u7528\u4e8e\u8f93\u5165\u7684DTO\u7c7b\u3002\u56e0\u6b64\uff0c\u8ba9\u6211\u4eec\u5728<strong>Entities\/DataTransferObjects<\/strong>\u6587\u4ef6\u5939\u4e2d\u521b\u5efa\u4e00\u4e2a\u65b0\u7c7b\uff1a<\/p>\n<pre class=\"brush: csharp; title: ; notranslate\" title=\"\">\nnamespace Entities.DataTransferObjects\n{\n    public class CompanyForCreationDto { \n        public string Name { get; set; } \n        public string Address { get; set; } \n        public string Country { get; set; } \n    }\n}\n<\/pre>\n<p>We can see that this DTO class is almost the same as the <strong>Company<\/strong> class 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 \u7c7b\u51e0\u4e4e\u4e0e <strong>Company<\/strong>\u7c7b\u76f8\u540c\uff0c\u4f46\u6ca1\u6709 Id \u5c5e\u6027\u3002\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\u5e94\u8be5\u6ce8\u610f\u53e6\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\u66f4\u8f7b\u677e\u5730\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 - \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 <strong>ICompanyRepository<\/strong> interface\uff1a<br \/>\n\u8bf4\u5b8c\u4e86\u8fd9\u4e48\u591a\uff0c\u8ba9\u6211\u4eec\u7ee7\u7eed\u4fee\u6539 <strong>ICompanyRepository<\/strong> \u63a5\u53e3\uff1a<\/p>\n<pre class=\"brush: csharp; title: ; notranslate\" title=\"\">\nusing Entities.Models;\nusing System;\nusing System.Collections.Generic;\n\nnamespace Contracts\n{\n    public interface ICompanyRepository {\n        IEnumerable&lt;Company&gt; GetAllCompanies(bool trackChanges);\n        Company GetCompany(Guid companyId, bool trackChanges);\n        void CreateCompany(Company company);\n    }\n}\n<\/pre>\n<p>After the interface modification, we are going to implement that interface:<br \/>\n\u5728\u63a5\u53e3\u4fee\u6539\u4e4b\u540e\uff0c\u6211\u4eec\u5c06\u5b9e\u73b0\u8be5\u63a5\u53e3\uff1a<\/p>\n<pre class=\"brush: csharp; title: ; notranslate\" title=\"\">\nusing Contracts;\nusing Entities.Models;\nusing Entities;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System;\n\nnamespace Repository\n{\n    public class CompanyRepository : RepositoryBase&lt;Company&gt;, ICompanyRepository\n    { \n        public CompanyRepository(RepositoryContext repositoryContext) : base(repositoryContext) { }\n        public IEnumerable&lt;Company&gt; GetAllCompanies(bool trackChanges) =&gt; \n            FindAll(trackChanges).OrderBy(c =&gt; c.Name).ToList();\n\n        public Company GetCompany(Guid companyId, bool trackChanges) =&gt; \n            FindByCondition(c =&gt; c.Id.Equals(companyId), trackChanges).\n            SingleOrDefault();\n\n        public void CreateCompany(Company company) =&gt; Create(company);\n    }\n}\n<\/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\u660e\u786e\u5730\u4e3a\u6211\u4eec\u7684\u516c\u53f8\u751f\u6210\u65b0\u7684ID;\u8fd9\u5c06\u7531EF Core\u5b8c\u6210\u3002\u6211\u4eec\u6240\u8981\u505a\u7684\u5c31\u662f\u5c06\u516c\u53f8\u7684\u72b6\u6001\u8bbe\u7f6e\u4e3a\u201c\u5df2\u6dfb\u52a0\u201d\u3002<\/p>\n<p>Before we add a new action in our <strong>Companies<\/strong> controller, we have to create another mapping rule for the <strong>Company<\/strong> and <strong>CompanyForCreationDto<\/strong> objects. Let\u2019s do this in the <strong>MappingProfile<\/strong> class:<br \/>\n\u5728<strong>Companies<\/strong>\u63a7\u5236\u5668\u4e2d\u6dfb\u52a0\u65b0\u64cd\u4f5c\u4e4b\u524d\uff0c\u6211\u4eec\u5fc5\u987b\u4e3a<strong>Company<\/strong>\u548c <strong>CompanyForCreationDto<\/strong> \u5bf9\u8c61\u521b\u5efa\u53e6\u4e00\u4e2a\u6620\u5c04\u89c4\u5219\u3002\u8ba9\u6211\u4eec\u5728 <strong>MappingProfile<\/strong> \u7c7b\u4e2d\u6267\u884c\u6b64\u64cd\u4f5c\uff1a<\/p>\n<p><pre class=\"brush: csharp; title: ; notranslate\" title=\"\">\nusing AutoMapper;\nusing Entities.DataTransferObjects;\nusing Entities.Models;\n\nnamespace CompanyEmployees\n{\n    public class MappingProfile : Profile { \n        public MappingProfile() { \n            CreateMap&lt;Company, CompanyDto&gt;()\n                .ForMember(c =&gt; c.FullAddress, \n                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}\n<\/pre>\n<p>Our POST action will accept a parameter of the type <strong>CompanyForCreationDto<\/strong>, but we need the Company object for creation. Therefore, we have to create this mapping rule.<br \/>\n\u6211\u4eec\u7684 POST \u64cd\u4f5c\u5c06\u63a5\u53d7 <strong>CompanyForCreationDto<\/strong> \u7c7b\u578b\u7684\u53c2\u6570\uff0c\u4f46\u6211\u4eec\u9700\u8981 Company \u5bf9\u8c61\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\u8ba9\u6211\u4eec\u4fee\u6539\u63a7\u5236\u5668\uff1a<\/p>\n<pre class=\"brush: csharp; title: ; notranslate\" title=\"\">\n&#x5B;HttpPost]\n        public IActionResult CreateCompany(&#x5B;FromBody] CompanyForCreationDto company)\n        {\n            if (company == null)\n            {\n                _logger.LogError(&quot;CompanyForCreationDto object sent from client is null.&quot;);\n                return BadRequest(&quot;CompanyForCreationDto object is null&quot;);\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 CreatedAtRoute(&quot;CompanyById&quot;, new { id = companyToReturn.Id }, companyToReturn);\n        }\n<\/pre>\n<p>Let\u2019s use Postman to send the request and examine the result:<br \/>\n\u8ba9\u6211\u4eec\u4f7f\u7528Postman\u53d1\u9001\u8bf7\u6c42\u5e76\u68c0\u67e5\u7ed3\u679c\uff1a<br \/>\n(\u5728POSTMAN\u4e2d\u9009\u62e9POST\u65b9\u6cd5\uff0c\u5728BODY\u4e2d\u9009\u62e9RAW\uff0c\u7136\u540e\u9009\u62e9JSON\uff0c\u5728\u4e2d\u95f4\u7684\u6587\u672c\u533a\u57df\u8f93\u51fa\u4e00\u4e2a\u516c\u53f8\u7684JSON\u5185\u5bb9\u4fe1\u606f)<\/p>\n<p><a href=\"https:\/\/localhost:5001\/api\/companies\"><a href=\"https:\/\/localhost:5001\/api\/companies\"><a href=\"https:\/\/localhost:5001\/api\/companies\">https:\/\/localhost:5001\/api\/companies<\/a><\/a><\/a><\/p>\n<p><img decoding=\"async\" src=\"\/img\/20220824-ultimate-asp-net-core-web-api\/Image_9001.jpg\" alt=\"alt\" \/><\/p>\n<h2>9.2 Code Explanation<\/h2>\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. But the code in the controller contains several things worth mentioning.<br \/>\n\u8ba9\u6211\u4eec\u6765\u8c08\u8c08\u8fd9\u4e2a\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\u4e00\u70b9\u3002\u4f46\u662f\u63a7\u5236\u5668\u4e2d\u7684\u4ee3\u7801\u5305\u542b\u51e0\u4ef6\u503c\u5f97\u4e00\u63d0\u7684\u4e8b\u60c5\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: <strong>api\/companies<\/strong> \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 \u64cd\u4f5c\u76f8\u540c\u7684\u8bf7\u6c42 URI\uff1a<strong>api\/companies<\/strong> \u2014 \u4f46\u8fd9\u6b21\u6211\u4eec\u4f7f\u7528\u7684\u662f POST \u8bf7\u6c42\u3002<\/p>\n<p>The <strong>CreateCompany<\/strong> method has its own <strong>[HttpPost]<\/strong> 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 from the request body. Thus the usage of the <strong>[FromBody]<\/strong> attribute. Also, the company object is a complex type; therefore, we have to use <strong>[FromBody]<\/strong>.<br \/>\n<strong>CreateCompany<\/strong> \u65b9\u6cd5\u5177\u6709\u81ea\u5df1\u7684 <strong>[HttpPost]<\/strong> \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\u7684\u516c\u53f8\u53c2\u6570\u3002\u6211\u4eec\u4e0d\u662f\u4ece URI \u6536\u96c6\u5b83\uff0c\u800c\u662f\u4ece\u8bf7\u6c42\u6b63\u6587\u4e2d\u6536\u96c6\u5b83\u3002\u56e0\u6b64\uff0c\u7528\u6cd5 <strong>[FromBody]<\/strong> \u5c5e\u6027\u3002\u6b64\u5916\uff0c\u516c\u53f8\u5bf9\u8c61\u662f\u590d\u6742\u7c7b\u578b;\u56e0\u6b64\uff0c\u6211\u4eec\u5fc5\u987b\u4f7f\u7528 <strong>[FromBody]<\/strong> \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 <strong>[FromUri]<\/strong> attribute, though we wouldn\u2019t recommend that at all because of security reasons and the complexity of the request.<br \/>\n\u5982\u679c\u6211\u4eec\u613f\u610f\uff0c\u53ef\u4ee5\u901a\u8fc7\u7528 <strong>[FromUri]<\/strong> \u5c5e\u6027\u4fee\u9970\u5b83\u6765\u663e\u5f0f\u6807\u8bb0\u4ece URI \u83b7\u53d6\u6b64\u53c2\u6570\u7684\u64cd\u4f5c\uff0c\u5c3d\u7ba1\u7531\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 <strong>company<\/strong> parameter comes from the client, it could happen that it can\u2019t be deserialized. As a result, we would need to validate it against the reference type\u2019s default value, which is null.<br \/>\n\u7531\u4e8e <strong>company<\/strong> \u53c2\u6570\u6765\u81ea\u5ba2\u6237\u7aef\uff0c\u56e0\u6b64\u53ef\u80fd\u4f1a\u53d1\u751f\u65e0\u6cd5\u53cd\u5e8f\u5217\u5316\u7684\u60c5\u51b5\u3002\u56e0\u6b64\uff0c\u6211\u4eec\u9700\u8981\u6839\u636e\u5f15\u7528\u7c7b\u578b\u7684\u9ed8\u8ba4\u503c\uff08null\uff09\u5bf9\u5176\u8fdb\u884c\u9a8c\u8bc1\u3002<\/p>\n<p>After validation, we map the company for creation to the company entity, call the repository method for creation, and call the <strong>Save()<\/strong> method to save the entity to the database. After the save action, we map the company entity to the company DTO object to return it to the client.<br \/>\n\u9a8c\u8bc1\u540e\uff0c\u6211\u4eec\u5c06\u8981\u521b\u5efa\u7684\u516c\u53f8\u6620\u5c04\u5230\u516c\u53f8\u5b9e\u4f53\uff0c\u8c03\u7528\u5b58\u50a8\u5e93\u65b9\u6cd5\u8fdb\u884c\u521b\u5efa\uff0c\u5e76\u8c03\u7528 <strong>Save()<\/strong> \u65b9\u6cd5\u5c06\u5b9e\u4f53\u4fdd\u5b58\u5230\u6570\u636e\u5e93\u4e2d\u3002\u4fdd\u5b58\u64cd\u4f5c\u540e\uff0c\u6211\u4eec\u5c06\u516c\u53f8\u5b9e\u4f53\u6620\u5c04\u5230\u516c\u53f8 DTO \u5bf9\u8c61\uff0c\u4ee5\u5c06\u5176\u8fd4\u56de\u7ed9\u5ba2\u6237\u7aef\u3002<\/p>\n<p>The last thing to mention is this part of the code:<br \/>\n\u6700\u540e\u8981\u63d0\u5230\u7684\u662f\u4ee3\u7801\u7684\u8fd9\u4e00\u90e8\u5206\uff1a<\/p>\n<pre class=\"brush: csharp; title: ; notranslate\" title=\"\">\nCreatedAtRoute(&quot;CompanyById&quot;, new { id = companyToReturn.Id }, companyToReturn);\n<\/pre>\n<p><strong>CreatedAtRoute<\/strong> will return a status code 201, which stands for <strong>Created<\/strong>. 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 \/>\n<strong>CreatedAtRoute<\/strong> \u5c06\u8fd4\u56de\u72b6\u6001\u4ee3\u7801 201\uff0c\u5b83\u4ee3\u8868 <strong>Created<\/strong>\u3002\u6b64\u5916\uff0c\u5b83\u8fd8\u5c06\u4f7f\u7528\u65b0\u7684\u516c\u53f8\u5bf9\u8c61\u4ee5\u53ca\u5177\u6709\u7528\u4e8e\u68c0\u7d22\u8be5\u516c\u53f8\u7684\u5730\u5740\u7684\u54cd\u5e94\u6807\u5934\u3002\u6211\u4eec\u9700\u8981\u63d0\u4f9b\u64cd\u4f5c\u7684\u540d\u79f0\uff0c\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\u770b\u4e00\u4e0b\u54cd\u5e94\u7684\u6807\u5934\u90e8\u5206\uff0c\u6211\u4eec\u5c06\u770b\u5230\u4e00\u4e2a\u7528\u4e8e\u68c0\u7d22\u6240\u521b\u5efa\u516c\u53f8\u7684\u94fe\u63a5\uff1a<\/p>\n<p><img decoding=\"async\" src=\"\/img\/20220824-ultimate-asp-net-core-web-api\/Image_9002.jpg\" alt=\"alt\" \/><\/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\uff0c\u4ece\u800c\u66f4\u6539\u8d44\u6e90\u8868\u793a\u5f62\u5f0f\u3002\u6b64\u5916\uff0c\u5982\u679c\u6211\u4eec\u5c1d\u8bd5\u53d1\u9001\u6b64\u8bf7\u6c42\u51e0\u6b21\uff0c\u6211\u4eec\u5c06\u4e3a\u6bcf\u4e2a\u8bf7\u6c42\u83b7\u53d6\u4e00\u4e2a\u65b0\u5bf9\u8c61\uff08\u5b83\u80af\u5b9a\u4f1a\u6709\u4e0d\u540c\u7684Id\uff09\u3002<\/p>\n<p>Let\u2019s continue with child resources creation.<br \/>\n\u8ba9\u6211\u4eec\u7ee7\u7eed\u521b\u5efa\u5b50\u8d44\u6e90\u3002<\/p>\n<h2>9.3 Creating a Child Resource<\/h2>\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:<br \/>\n\u5728\u521b\u5efa\u516c\u53f8\u65f6\uff0c\u6211\u4eec\u521b\u5efa\u4e86\u201cCreateCompany\u201d\u64cd\u4f5c\u6240\u9700\u7684 DTO \u5bf9\u8c61\u3002\u56e0\u6b64\uff0c\u5bf9\u4e8e\u5458\u5de5\u521b\u5efa\uff0c\u6211\u4eec\u5c06\u505a\u540c\u6837\u7684\u4e8b\u60c5\uff1a<\/p>\n<pre class=\"brush: csharp; title: ; notranslate\" title=\"\">\nnamespace Entities.DataTransferObjects\n{\n    public class EmployeeForCreationDto { \n        public string Name { get; set; } \n        public int Age { get; set; } \n        public string Position { get; set; } }\n}\n<\/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 <strong>CompanyId<\/strong> because we accept that parameter through the route:<br \/>\n\u6211\u4eec\u6ca1\u6709 Id \u5c5e\u6027\uff0c\u56e0\u4e3a\u6211\u4eec\u5c06\u5728\u670d\u52a1\u5668\u7aef\u521b\u5efa\u8be5 Id\u3002\u4f46\u9664\u6b64\u4e4b\u5916\uff0c\u6211\u4eec\u6ca1\u6709\u516c\u53f8Id\uff0c\u56e0\u4e3a\u6211\u4eec\u901a\u8fc7\u8def\u7531\u63a5\u53d7\u8be5\u53c2\u6570\uff1a<\/p>\n<pre class=\"brush: csharp; title: ; notranslate\" title=\"\">\n&#x5B;Route(&quot;api\/companies\/{companyId}\/employees&quot;)]\n<\/pre>\n<p>The next step is to modify the <strong>IEmployeeRepository<\/strong> interface:<br \/>\n\u4e0b\u4e00\u6b65\u662f\u4fee\u6539 <strong>IEmployeeRepository<\/strong> \u63a5\u53e3\uff1a<\/p>\n<pre class=\"brush: csharp; title: ; notranslate\" title=\"\">\nusing Entities.Models;\nusing System.Collections.Generic;\nusing System;\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\n        void CreateEmployeeForCompany(Guid companyId, Employee employee);\n    }\n}\n<\/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 class=\"brush: csharp; title: ; notranslate\" title=\"\">\nusing Contracts;\nusing Entities.Models;\nusing Entities;\nusing System.Collections.Generic;\nusing System;\nusing System.Linq;\n\nnamespace Repository\n{\n    public class EmployeeRepository : RepositoryBase&lt;Employee&gt;, IEmployeeRepository { \n        public EmployeeRepository(RepositoryContext repositoryContext) : base(repositoryContext) { }\n        public IEnumerable&lt;Employee&gt; GetEmployees(Guid companyId, bool trackChanges) =&gt; \n            FindByCondition(e =&gt; e.CompanyId.Equals(companyId), trackChanges).OrderBy(e =&gt; e.Name);\n\n        public Employee GetEmployee(Guid companyId, Guid id, bool trackChanges) =&gt; \n            FindByCondition(e =&gt; e.CompanyId.Equals(companyId) &amp;&amp; e.Id.Equals(id), trackChanges).SingleOrDefault();\n\n        public void CreateEmployeeForCompany(Guid companyId, Employee employee) { \n            employee.CompanyId = companyId; \n            Create(employee); \n        }\n    }\n}\n<\/pre>\n<p>Because we are going to accept the employee DTO object in our action, but we also have to send an employee object to this repository method, we have to create an additional mapping rule in the <strong>MappingProfile<\/strong> class:<br \/>\n\u7531\u4e8e\u6211\u4eec\u5c06\u5728\u64cd\u4f5c\u4e2d\u63a5\u53d7\u5458\u5de5 DTO \u5bf9\u8c61\uff0c\u4f46\u8fd8\u5fc5\u987b\u5c06\u5458\u5de5\u5bf9\u8c61\u53d1\u9001\u5230\u6b64\u5b58\u50a8\u5e93\u65b9\u6cd5\uff0c\u56e0\u6b64\u6211\u4eec\u5fc5\u987b\u5728 <strong>MappingProfile<\/strong> \u7c7b\u4e2d\u521b\u5efa\u5176\u4ed6\u6620\u5c04\u89c4\u5219\uff1a<\/p>\n<pre class=\"brush: csharp; title: ; notranslate\" title=\"\">\nusing AutoMapper;\nusing Entities.DataTransferObjects;\nusing Entities.Models;\n\nnamespace CompanyEmployees\n{\n    public class MappingProfile : Profile { \n        public MappingProfile() { \n            CreateMap&lt;Company, CompanyDto&gt;()\n                .ForMember(c =&gt; c.FullAddress, \n                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            CreateMap&lt;EmployeeForCreationDto, Employee&gt;();\n        } \n    }\n}\n<\/pre>\n<p>Now, we can add a new action in the EmployeesController:<br \/>\n\u73b0\u5728\uff0c\u6211\u4eec\u53ef\u4ee5\u5728\u5458\u5de5\u63a7\u5236\u5668\u4e2d\u6dfb\u52a0\u4e00\u4e2a\u65b0\u64cd\u4f5c\uff1a<\/p>\n<pre class=\"brush: csharp; title: ; notranslate\" title=\"\">\n&#x5B;HttpPost]\n        public IActionResult CreateEmployeeForCompany(Guid companyId, &#x5B;FromBody] EmployeeForCreationDto employee)\n        {\n            if (employee == null)\n            {\n                _logger.LogError(&quot;EmployeeForCreationDto object sent from client is null.&quot;);\n                return BadRequest(&quot;EmployeeForCreationDto object is null&quot;);\n            }\n            var company = _repository.Company.GetCompany(companyId, trackChanges: false);\n            if (company == null)\n            {\n                _logger.LogInfo($&quot;Company with id: {companyId} doesn&#039;t exist in the database.&quot;);\n                return NotFound();\n            }\n            var employeeEntity = _mapper.Map&lt;Employee&gt;(employee);\n\n            _repository.Employee.CreateEmployeeForCompany(companyId, employeeEntity);\n            _repository.Save();\n            var employeeToReturn = _mapper.Map&lt;EmployeeDto&gt;(employeeEntity);\n\n            return CreatedAtRoute(&quot;GetEmployeeForCompany&quot;, new { companyId, id = employeeToReturn.Id }, employeeToReturn);\n        }\n<\/pre>\n<p>There are some differences in this code compared to the <strong>CreateCompany<\/strong> action. The first is that 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.<br \/>\n\u4e0e <strong>CreateCompany<\/strong> \u64cd\u4f5c\u76f8\u6bd4\uff0c\u6b64\u4ee3\u7801\u4e2d\u5b58\u5728\u4e00\u4e9b\u5dee\u5f02\u3002\u9996\u5148\uff0c\u6211\u4eec\u5fc5\u987b\u68c0\u67e5\u8be5\u516c\u53f8\u662f\u5426\u5b58\u5728\u4e8e\u6570\u636e\u5e93\u4e2d\uff0c\u56e0\u4e3a\u4e3a\u4e0d\u5b58\u5728\u7684\u516c\u53f8\u521b\u5efa\u5458\u5de5\u662f\u6ca1\u6709\u610f\u4e49\u7684\u3002<\/p>\n<p>The second difference is the return statement, which now has two parameters for the anonymous object.<br \/>\n\u7b2c\u4e8c\u4e2a\u533a\u522b\u662f return \u8bed\u53e5\uff0c\u5b83\u73b0\u5728\u6709\u4e24\u4e2a\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 \u64cd\u4f5c\u4e0a\u65b9\u7684 HTTP \u5c5e\u6027\uff1a<\/p>\n<pre class=\"brush: csharp; title: ; notranslate\" title=\"\">\n&#x5B;HttpGet(&quot;{id}&quot;, Name = &quot;GetEmployeeForCompany&quot;)]\n<\/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\/53a1237a-3ed3-4462-b9f0-5a7bb1056a33\/employees\"><a href=\"https:\/\/localhost:5001\/api\/companies\/53a1237a-3ed3-4462-b9f0-5a7bb1056a33\/employees\"><a href=\"https:\/\/localhost:5001\/api\/companies\/53a1237a-3ed3-4462-b9f0-5a7bb1056a33\/employees\">https:\/\/localhost:5001\/api\/companies\/53a1237a-3ed3-4462-b9f0-5a7bb1056a33\/employees<\/a><\/a><\/a><br \/>\n(\u539f\u6587\u7684GUID\u4e0d\u5728\u6570\u636e\u5e93\u4e2d\uff0c\u8bf7\u67e5\u770b\u6570\u636e\u5e93\u4e2d\u7684GUID\uff0c\u66ff\u6362\u4e0a\u9762URL\u4e2d\u7684\u5373\u53ef)<\/p>\n<p><img decoding=\"async\" src=\"\/img\/20220824-ultimate-asp-net-core-web-api\/Image_9003.jpg\" alt=\"alt\" \/><\/p>\n<p>Excellent. A new employee was created.<br \/>\n\u975e\u5e38\u597d\u3002\u521b\u5efa\u4e86\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\u770b\u4e00\u4e0b\u201c\u6807\u9898\u201d\u9009\u9879\u5361\uff0c\u6211\u4eec\u5c06\u770b\u5230\u4e00\u4e2a\u94fe\u63a5\uff0c\u7528\u4e8e\u83b7\u53d6\u65b0\u521b\u5efa\u7684\u5458\u5de5\u3002\u5982\u679c\u60a8\u590d\u5236\u8be5\u94fe\u63a5\u5e76\u968f\u4e4b\u53d1\u9001\u53e6\u4e00\u4e2a\u8bf7\u6c42\uff0c\u60a8\u80af\u5b9a\u4f1a\u5f97\u5230\u4ee5\u4e0b\u5458\u5de5\uff1a<\/p>\n<p><img decoding=\"async\" src=\"\/img\/20220824-ultimate-asp-net-core-web-api\/Image_9004.jpg\" alt=\"alt\" \/><\/p>\n<h2>9.4 Creating Children Resources Together with a Parent<\/h2>\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.<br \/>\n\u5728\u67d0\u4e9b\u60c5\u51b5\u4e0b\uff0c\u6211\u4eec\u5e0c\u671b\u521b\u5efa\u5177\u6709\u5176\u5b50\u8d44\u6e90\u7684\u7236\u8d44\u6e90\u3002\u6211\u4eec\u5e0c\u671b\u5728\u5bf9\u7236\u8d44\u6e90\u6267\u884c\u540c\u4e00\u8bf7\u6c42\u65f6\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\u505a\u5230\u8fd9\u4e00\u70b9\u3002<\/p>\n<p>The first thing we are going to do is extend the <strong>CompanyForCreationDto<\/strong> class:<br \/>\n\u6211\u4eec\u8981\u505a\u7684\u7b2c\u4e00\u4ef6\u4e8b\u662f\u5c06<strong>CompanyForCreationDto<\/strong> \u6269\u5c55\u5230\u7c7b\uff1a<\/p>\n<p><pre class=\"brush: csharp; title: ; notranslate\" title=\"\">\nusing System.Collections.Generic;\n\nnamespace Entities.DataTransferObjects\n{\n    public class CompanyForCreationDto { \n        public string Name { get; set; } \n        public string Address { get; set; } \n        public string Country { get; set; }\n\n        public IEnumerable&lt;EmployeeForCreationDto&gt; Employees { get; set; }\n    }\n}\n<\/pre>\n<p>We are not going to change the action logic inside the controller nor the repository 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\u64cd\u4f5c\u903b\u8f91\uff0c\u4e5f\u4e0d\u4f1a\u66f4\u6539\u5b58\u50a8\u5e93\u903b\u8f91;\u90a3\u91cc\u7684\u4e00\u5207\u90fd\u5f88\u68d2\u3002\u5c31\u8fd9\u6837\u3002\u8ba9\u6211\u4eec\u6765\u6d4b\u8bd5\u4e00\u4e0b\uff1a<\/p>\n<pre class=\"brush: csharp; title: ; notranslate\" title=\"\">\n{\n    &quot;name&quot;:&quot;Electronics Solutions Ltd&quot;,\n    &quot;address&quot;:&quot;312 Deliver Street,F 234&quot;,\n    &quot;Country&quot;:&quot;USA&quot;,\n    &quot;employees&quot;:&#x5B;\n        {\n            &quot;name&quot;:&quot;Joan Done&quot;,\n            &quot;age&quot;:29,\n            &quot;position&quot;:&quot;Manager&quot;\n        },\n        {\n            &quot;name&quot;:&quot;Martin Geil&quot;,\n            &quot;age&quot;:29,\n            &quot;position&quot;:&quot;Administrative&quot;\n        }\n    ]\n}\n<\/pre>\n<p><a href=\"https:\/\/localhost:5001\/api\/companies\"><a href=\"https:\/\/localhost:5001\/api\/companies\"><a href=\"https:\/\/localhost:5001\/api\/companies\">https:\/\/localhost:5001\/api\/companies<\/a><\/a><\/a><\/p>\n<p><img decoding=\"async\" src=\"\/img\/20220824-ultimate-asp-net-core-web-api\/Image_9005.jpg\" alt=\"alt\" \/><\/p>\n<p>You can see that this company was created successfully.<br \/>\n\u60a8\u53ef\u4ee5\u770b\u5230\u8fd9\u5bb6\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 <strong>\/employees<\/strong> part:<br \/>\n\u73b0\u5728\uff0c\u6211\u4eec\u53ef\u4ee5\u4ece\u201c\u6807\u9898\u201d\u9009\u9879\u5361\u590d\u5236\u4f4d\u7f6e\u94fe\u63a5\uff0c\u5c06\u5176\u7c98\u8d34\u5230\u53e6\u4e00\u4e2aPostman\u9009\u9879\u5361\u4e2d\uff0c\u7136\u540e\u53ea\u9700\u6dfb\u52a0 <strong>\/employees<\/strong> \u90e8\u5206\uff1a<\/p>\n<p><img decoding=\"async\" src=\"\/img\/20220824-ultimate-asp-net-core-web-api\/Image_9006.jpg\" alt=\"alt\" \/><\/p>\n<p>We have confirmed that the employees were created as well.<br \/>\n\u6211\u4eec\u5df2\u7ecf\u786e\u8ba4\u5458\u5de5\u4e5f\u662f\u521b\u5efa\u7684\u3002<\/p>\n<h2>9.5 Creating a Collection of Resources<\/h2>\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.<br \/>\n\u5230\u76ee\u524d\u4e3a\u6b62\uff0c\u6211\u4eec\u4e00\u76f4\u5728\u521b\u5efa\u5355\u4e2a\u8d44\u6e90\uff0c\u65e0\u8bba\u662f\u516c\u53f8\u8fd8\u662f\u5458\u5de5\u3002\u4f46\u662f\uff0c\u521b\u5efa\u8d44\u6e90\u96c6\u5408\u662f\u5f88\u6b63\u5e38\u7684\uff0c\u5728\u672c\u8282\u4e2d\uff0c\u6211\u4eec\u5c06\u8981\u4f7f\u7528\u3002\u200c<\/p>\n<p>If we take a look at the <strong>CreateCompany<\/strong> action, for example, we can see that the return part points to the <strong>CompanyById<\/strong> route (the <strong>GetCompany<\/strong> 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 <strong>CreateCompany<\/strong>  \u64cd\u4f5c\uff0c\u6211\u4eec\u53ef\u4ee5\u770b\u5230\u8fd4\u56de\u90e8\u5206\u6307\u5411 <strong>CompanyById<\/strong> \u8def\u7531\uff08<strong>GetCompany<\/strong> \u64cd\u4f5c\uff09\u3002\u4e5f\u5c31\u662f\u8bf4\uff0c\u6211\u4eec\u6ca1\u6709\u96c6\u5408\u521b\u5efa\u64cd\u4f5c\u6307\u5411\u7684 GET \u64cd\u4f5c\u3002\u56e0\u6b64\uff0c\u5728\u5f00\u59cb POST \u6536\u96c6\u64cd\u4f5c\u4e4b\u524d\uff0c\u6211\u4eec\u5c06\u5728\u516c\u53f8\u63a7\u5236\u5668\u4e2d\u521b\u5efa GetCompanyCollection \u64cd\u4f5c\u3002<\/p>\n<p>But first, let's modify the <strong>ICompanyRepository<\/strong> interface:<br \/>\n\u4f46\u9996\u5148\uff0c\u8ba9\u6211\u4eec\u4fee\u6539 <strong>ICompanyRepository<\/strong> \u63a5\u53e3\uff1a<\/p>\n<pre class=\"brush: csharp; title: ; notranslate\" title=\"\">\nusing Entities.Models;\nusing System;\nusing System.Collections.Generic;\n\nnamespace Contracts\n{\n    public interface ICompanyRepository {\n        IEnumerable&lt;Company&gt; GetAllCompanies(bool trackChanges);\n        Company GetCompany(Guid companyId, bool trackChanges);\n        void CreateCompany(Company company);\n\n        IEnumerable&lt;Company&gt; GetByIds(IEnumerable&lt;Guid&gt; ids, bool trackChanges);\n    }\n}\n\n<\/pre>\n<p>Then we have to change the <strong>CompanyRepository<\/strong> class:<br \/>\n\u7136\u540e\uff0c\u6211\u4eec\u5fc5\u987b\u66f4\u6539 <strong>CompanyRepository<\/strong> \u7c7b\uff1a<\/p>\n<p><pre class=\"brush: csharp; title: ; notranslate\" title=\"\">\nusing Contracts;\nusing Entities.Models;\nusing Entities;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System;\n\nnamespace Repository\n{\n    public class CompanyRepository : RepositoryBase&lt;Company&gt;, ICompanyRepository\n    { \n        public CompanyRepository(RepositoryContext repositoryContext) : base(repositoryContext) { }\n        public IEnumerable&lt;Company&gt; GetAllCompanies(bool trackChanges) =&gt; \n            FindAll(trackChanges).OrderBy(c =&gt; c.Name).ToList();\n\n        public Company GetCompany(Guid companyId, bool trackChanges) =&gt; \n            FindByCondition(c =&gt; c.Id.Equals(companyId), trackChanges).\n            SingleOrDefault();\n\n        public void CreateCompany(Company company) =&gt; Create(company);\n\n        public IEnumerable&lt;Company&gt; GetByIds(IEnumerable&lt;Guid&gt; ids, bool trackChanges) =&gt; \n            FindByCondition(x =&gt; ids.Contains(x.Id), trackChanges).ToList();\n    }\n}\n\n<\/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\u64cd\u4f5c\uff1a<\/p>\n<pre class=\"brush: csharp; title: ; notranslate\" title=\"\">\n&#x5B;HttpGet(&quot;collection\/({ids})&quot;, Name = &quot;CompanyCollection&quot;)]\n        public IActionResult GetCompanyCollection(IEnumerable&lt;Guid&gt; ids)\n        {\n            if (ids == null)\n            {\n                _logger.LogError(&quot;Parameter ids is null&quot;);\n                return BadRequest(&quot;Parameter ids is null&quot;);\n            }\n            var companyEntities = _repository.Company.GetByIds(ids, trackChanges: false);\n            if (ids.Count() != companyEntities.Count())\n            {\n                _logger.LogError(&quot;Some ids are not valid in a collection&quot;);\n                return NotFound();\n            }\n            var companiesToReturn = _mapper.Map&lt;IEnumerable&lt;CompanyDto&gt;&gt;(companyEntities);\n            return Ok(companiesToReturn);\n        }\n<\/pre>\n<p>And that's it. These actions are pretty straightforward, so let's continue towards POST implementation:<br \/>\n\u5c31\u662f\u8fd9\u6837\u3002\u8fd9\u4e9b\u64cd\u4f5c\u975e\u5e38\u7b80\u5355\uff0c\u56e0\u6b64\u8ba9\u6211\u4eec\u7ee7\u7eed\u5b9e\u73b0POST\uff1a<\/p>\n<pre class=\"brush: csharp; title: ; notranslate\" title=\"\">\n&#x5B;HttpPost(&quot;collection&quot;)]\n        public IActionResult CreateCompanyCollection(&#x5B;FromBody] IEnumerable&lt;CompanyForCreationDto&gt; companyCollection)\n        {\n            if (companyCollection == null)\n            {\n                _logger.LogError(&quot;Company collection sent from client is null.&quot;);\n                return BadRequest(&quot;Company collection is null&quot;);\n            }\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 CreatedAtRoute(&quot;CompanyCollection&quot;, new { ids }, companyCollectionToReturn);\n        }\n<\/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 take all the ids as a comma-separated string and navigate to the GET action for fetching our created companies.<br \/>\n\u56e0\u6b64\uff0c\u6211\u4eec\u68c0\u67e5\u96c6\u5408\u662f\u5426\u4e3a\u7a7a\uff0c\u5982\u679c\u4e3a\u7a7a\uff0c\u5219\u8fd4\u56de\u9519\u8bef\u8bf7\u6c42\u3002\u5982\u679c\u4e0d\u662f\uff0c\u5219\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\u6240\u6709 id \u4f5c\u4e3a\u9017\u53f7\u5206\u9694\u7684\u5b57\u7b26\u4e32\uff0c\u5e76\u5bfc\u822a\u5230 GET \u64cd\u4f5c\u4ee5\u83b7\u53d6\u6211\u4eec\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 <strong>GetCompanyCollection<\/strong> action?<br \/>\n\u73b0\u5728\u60a8\u53ef\u80fd\u4f1a\u95ee\uff0c\u5f53\u6211\u4eec\u671f\u671b\u5728 <strong>GetCompanyCollection<\/strong> \u64cd\u4f5c\u4e2d\u6536\u96c6 id \u65f6\uff0c\u4e3a\u4ec0\u4e48\u6211\u4eec\u8981\u53d1\u9001\u9017\u53f7\u5206\u9694\u7684\u5b57\u7b26\u4e32\uff1f<\/p>\n<p>Well, we can\u2019t just pass a list of ids in the <strong>CreatedAtRoute<\/strong> 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\u597d\u5427\uff0c\u6211\u4eec\u4e0d\u80fd\u53ea\u5728 <strong>CreatedAtRoute<\/strong> \u65b9\u6cd5\u4e2d\u4f20\u9012 id \u5217\u8868\uff0c\u56e0\u4e3a\u4e0d\u652f\u6301\u4f7f\u7528\u5217\u8868\u521b\u5efa\u6807\u5934\u4f4d\u7f6e\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=\"\/img\/20220824-ultimate-asp-net-core-web-api\/Image_9007.jpg\" alt=\"alt\" \/>  <\/p>\n<p>We can test our create action now:<br \/>\n\u6211\u4eec\u73b0\u5728\u53ef\u4ee5\u6d4b\u8bd5\u6211\u4eec\u7684\u521b\u5efa\u64cd\u4f5c\uff1a<br \/>\n<a href=\"https:\/\/localhost:5001\/api\/companies\/collection\"><a href=\"https:\/\/localhost:5001\/api\/companies\/collection\"><a href=\"https:\/\/localhost:5001\/api\/companies\/collection\">https:\/\/localhost:5001\/api\/companies\/collection<\/a><\/a><\/a><\/p>\n<pre class=\"brush: csharp; title: ; notranslate\" title=\"\">\n&#x5B;\n    {\n        &quot;name&quot;:&quot;Sales all over the world Ltd&quot;,\n        &quot;address&quot;:&quot;355 Open Street ,B 784&quot;,\n        &quot;country&quot;:&quot;USA&quot;\n    },\n    {\n        &quot;name&quot;:&quot;Branding Ltd&quot;,\n        &quot;address&quot;:&quot;255 Main street,K 334&quot;,\n        &quot;country&quot;:&quot;USA&quot;\n    }\n]\n<\/pre>\n<\/p>\n<p><img decoding=\"async\" src=\"\/img\/20220824-ultimate-asp-net-core-web-api\/Image_9008.jpg\" alt=\"alt\" \/><\/p>\n<p>Excellent. Let\u2019s check the header tab:<br \/>\n\u975e\u5e38\u597d\u3002\u8ba9\u6211\u4eec\u68c0\u67e5header\u9009\u9879\u5361\uff1a<\/p>\n<p><img decoding=\"async\" src=\"\/img\/20220824-ultimate-asp-net-core-web-api\/Image_9009.jpg\" alt=\"alt\" \/><\/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\u4e00\u4e2a\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=\"\/img\/20220824-ultimate-asp-net-core-web-api\/Image_9010.jpg\" alt=\"alt\" \/><\/p>\n<p>But we are getting the <strong>415 Unsupported Media Type<\/strong> message. This is because our API can\u2019t bind the <strong>string<\/strong> type parameter to the <strong>IEnumerable<Guid><\/strong> argument.<br \/>\n\u4f46\u662f\u6211\u4eec\u6536\u5230\u201c <strong>415 \u4e0d\u652f\u6301\u7684\u5a92\u4f53\u7c7b\u578b<\/strong> \u201d\u6d88\u606f\u3002\u8fd9\u662f\u56e0\u4e3a\u6211\u4eec\u7684 API \u65e0\u6cd5\u5c06\u5b57\u7b26\u4e32\u7c7b\u578b\u53c2\u6570\u7ed1\u5b9a\u5230 <strong>IEnumerable<Guid><\/strong> \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\u6b64\u95ee\u9898\u3002<\/p>\n<h2>9.6 Model Binding in API<\/h2>\n<p>Let\u2019s create the new folder <strong>ModelBinders<\/strong> in the main project and inside the new class <strong>ArrayModelBinder<\/strong>:<br \/>\n\u5728\u4e3b\u9879\u76ee\u4e2d\u521b\u5efa\u6587\u4ef6\u5939 <strong>ModelBinders<\/strong> \uff0c\u6dfb\u52a0\u4e00\u4e2a\u65b0\u7c7b <strong>ArrayModelBinder<\/strong> \uff1a<\/p>\n<pre class=\"brush: csharp; title: ; notranslate\" title=\"\">\nusing Microsoft.AspNetCore.Mvc.ModelBinding;\nusing System.ComponentModel;\nusing System.Reflection;\nusing System;\nusing System.Threading.Tasks;\nusing System.Linq;\n\nnamespace CompanyEmployees.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&#x5B;0];\n            var converter = TypeDescriptor.GetConverter(genericType);\n            var objectArray = providedValue.Split(new&#x5B;] { &quot;,&quot; },\n                StringSplitOptions.RemoveEmptyEntries).\n                Select(x =&gt; converter.ConvertFromString(x.Trim())).\n                ToArray();\n            var guidArray = Array.CreateInstance(genericType, objectArray.Length);\n            objectArray.CopyTo(guidArray, 0); bindingContext.Model = guidArray;\n            bindingContext.Result = ModelBindingResult.Success(bindingContext.Model);\n            return Task.CompletedTask;\n        }\n    }\n}\n<\/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\u4e00\u65e6\u6211\u4eec\u89e3\u91ca\u5b83\uff0c\u5b83\u5c06\u66f4\u5bb9\u6613\u7406\u89e3\u3002<\/p>\n<p>We are creating a model binder for the <strong>IEnumerable<\/strong> type. Therefore, we have to check if our parameter is the same type.<br \/>\n\u6211\u4eec\u6b63\u5728\u4e3a <strong>IEnumerable<\/strong> \u7c7b\u578b\u521b\u5efa\u4e00\u4e2a\u6a21\u578b\u7ed1\u5b9a\u5668\u3002\u56e0\u6b64\uff0c\u6211\u4eec\u5fc5\u987b\u68c0\u67e5\u6211\u4eec\u7684\u53c2\u6570\u662f\u5426\u4e3a\u540c\u4e00\u7c7b\u578b\u3002<\/p>\n<p>Next, we extract the value (a comma-separated string of GUIDs) with the <strong>ValueProvider.GetValue()<\/strong> expression. Because it is 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 <strong>ValueProvider.GetValue()<\/strong> \u8868\u8fbe\u5f0f\u63d0\u53d6\u503c\uff08\u4ee5\u9017\u53f7\u5206\u9694\u7684 GUID \u5b57\u7b26\u4e32\uff09\u3002\u56e0\u4e3a\u5b83\u662f\u5b57\u7b26\u4e32\u7c7b\u578b\uff0c\u6240\u4ee5\u6211\u4eec\u53ea\u68c0\u67e5\u5b83\u662f\u7a7a\u8fd8\u662f\u7a7a\u3002\u5982\u679c\u662f\uff0c\u5219\u8fd4\u56de null \u4f5c\u4e3a\u7ed3\u679c\uff0c\u56e0\u4e3a\u6211\u4eec\u5728\u63a7\u5236\u5668\u7684\u64cd\u4f5c\u4e2d\u6709\u4e00\u4e2a null \u68c0\u67e5\u3002\u5982\u679c\u4e0d\u662f\uff0c\u6211\u4eec\u7ee7\u7eed\u524d\u8fdb\u3002<\/p>\n<p>In the <strong>genericType<\/strong> variable, with the reflection help, we store the type the <strong>IEnumerable<\/strong> consists of. In our case, it is GUID. With the <strong>converter<\/strong> 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 <strong>IEnumerable<\/strong> parameter and then created a converter for that exact type, thus making this binder generic.<br \/>\n\u5728 <strong>genericType<\/strong> \u53d8\u91cf\u4e2d\uff0c\u901a\u8fc7\u53cd\u5c04\u5e2e\u52a9\uff0c\u6211\u4eec\u5b58\u50a8  <strong>IEnumerable<\/strong>  \u5305\u542b\u7684\u7c7b\u578b\u3002\u5728\u6211\u4eec\u7684\u4f8b\u5b50\u4e2d\uff0c\u5b83\u662fGUID\u3002\u968f\u7740\u8f6c\u6362\u5668\u53d8\u91cf\uff0c\u6211\u4eec\u521b\u5efa\u4e00\u4e2a GUID \u7c7b\u578b\u7684\u8f6c\u6362\u5668\u3002\u5982\u60a8\u6240\u89c1\uff0c\u6211\u4eec\u4e0d\u53ea\u662f\u5f3a\u5236\u6b64\u6a21\u578b\u7ed1\u5b9a\u5668\u4e2d\u7684GUID\u7c7b\u578b;\u76f8\u53cd\uff0c\u6211\u4eec\u68c0\u67e5\u4e86  <strong>IEnumerable<\/strong> \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\u7ed1\u5b9a\u5668\u6210\u4e3a\u901a\u7528\u7684\u3002<\/p>\n<p>After that, we create an array of type object (<strong>objectArray<\/strong>) that consist of all the GUID values we sent to the API and then create an array of GUID types ( <strong>guidArray<\/strong> ), copy all the values from the <strong>objectArray<\/strong> to the <strong>guidArray<\/strong>, and assign it to the <strong>bindingContext<\/strong>.<br \/>\n\u4e4b\u540e\uff0c\u6211\u4eec\u521b\u5efa\u4e00\u4e2a\u7c7b\u578b\u5bf9\u8c61\uff08 <strong>objectArray<\/strong> \uff09\u6570\u7ec4\uff0c\u5176\u4e2d\u5305\u542b\u6211\u4eec\u53d1\u9001\u5230API\u7684\u6240\u6709GUID\u503c\uff0c\u7136\u540e\u521b\u5efa\u4e00\u4e2aGUID\u7c7b\u578b\u6570\u7ec4\uff08 <strong>guidArray<\/strong> \uff09\uff0c\u5c06\u6240\u6709\u503c\u4ece<strong>objectArray<\/strong> \u590d\u5236\u5230 <strong>guidArray<\/strong> \uff0c\u5e76\u5c06\u5176\u5206\u914d\u7ed9\u7ed1\u5b9a <strong>bindingContext<\/strong>\u3002<\/p>\n<p>And that is it. Now, we have just to make a slight modification in the <strong>GetCompanyCollection<\/strong> action:<br \/>\n\u5c31\u662f\u8fd9\u6837\u3002\u73b0\u5728\uff0c\u6211\u4eec\u53ea\u9700\u8981\u5bf9 <strong>GetCompanyCollection<\/strong> \u64cd\u4f5c\uff1a<\/p>\n<pre class=\"brush: csharp; title: ; notranslate\" title=\"\">\npublic IActionResult GetCompanyCollection(&#x5B;ModelBinder(BinderType = typeof(ArrayModelBinder))]IEnumerable&lt;Guid&gt; ids)\n<\/pre>\n<p>Excellent.<br \/>\n\u975e\u5e38\u597d\u3002<\/p>\n<p>Our <strong>ArrayModelBinder<\/strong> 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 <strong>ArrayModelBinder<\/strong> \u5c06\u5728\u64cd\u4f5c\u6267\u884c\u4e4b\u524d\u89e6\u53d1\u3002\u5b83\u5c06\u53d1\u9001\u7684\u5b57\u7b26\u4e32\u53c2\u6570\u8f6c\u6362\u4e3a IEnumerable<Guid>\u7c7b\u578b\uff0c\u7136\u540e\u6267\u884c\u64cd\u4f5c\uff1a<\/p>\n<p><img decoding=\"async\" src=\"\/img\/20220824-ultimate-asp-net-core-web-api\/Image_9011.jpg\" alt=\"alt\" \/><\/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 \u64cd\u4f5c\u3002<\/p>\n","protected":false},"excerpt":{"rendered":"<p>9 CREATING RESUOURCES In this section, we are going to show you how to use the POST HTTP method to create resources in the database. \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 So, let\u2019s start. \u90a3\u4e48\uff0c\u8ba9\u6211\u4eec\u5f00\u59cb\u5427\u3002 9.1 Handing POST Requests Firstly, let\u2019s modify the decoration attribute for the GetCompany action in the Companies controller: \u9996\u5148\uff0c\u8ba9\u6211\u4eec\u4fee\u6539Companies\u63a7\u5236\u5668\u4e2d GetCompany \u64cd\u4f5c\u7684 decoration [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[3],"tags":[23],"class_list":["post-229","post","type-post","status-publish","format-standard","hentry","category-csharp","tag-ultimate-asp-net-core-web-api"],"_links":{"self":[{"href":"https:\/\/diji.net\/index.php?rest_route=\/wp\/v2\/posts\/229","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=229"}],"version-history":[{"count":0,"href":"https:\/\/diji.net\/index.php?rest_route=\/wp\/v2\/posts\/229\/revisions"}],"wp:attachment":[{"href":"https:\/\/diji.net\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=229"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/diji.net\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=229"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/diji.net\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=229"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}