{"id":1110,"date":"2025-05-27T14:46:21","date_gmt":"2025-05-27T06:46:21","guid":{"rendered":"https:\/\/www.hyy.net\/?p=1110"},"modified":"2025-05-27T14:46:21","modified_gmt":"2025-05-27T06:46:21","slug":"ultimate-asp-net-core-web-api-6-getting-additional-resources","status":"publish","type":"post","link":"https:\/\/diji.net\/?p=1110","title":{"rendered":"Ultimate ASP.NET Core Web API  6 GETTING ADDITIONAL RESOURCES"},"content":{"rendered":"<p>6 GETTING ADDITIONAL RESOURCES<br \/>\n\u83b7\u53d6\u989d\u5916\u8d44\u6e90<\/p>\n<p>As of now, we can continue with GET requests by adding additional actions to our controller. Moreover, we are going to create one more controller for the Employee resource and implement an additional action in it.\u200c<br \/>\n\u622a\u81f3\u76ee\u524d\uff0c\u6211\u4eec\u53ef\u4ee5\u901a\u8fc7\u5411\u63a7\u5236\u5668\u6dfb\u52a0\u5176\u4ed6\u4f5c\u6765\u7ee7\u7eed\u5904\u7406 GET \u8bf7\u6c42\u3002\u6b64\u5916\uff0c\u6211\u4eec\u5c06\u4e3a Employee \u8d44\u6e90\u518d\u521b\u5efa\u4e00\u4e2a\u63a7\u5236\u5668\uff0c\u5e76\u5728\u5176\u4e2d\u5b9e\u65bd\u4e00\u4e2a\u989d\u5916\u7684\u4f5c\u3002<\/p>\n<h2>6.1 Getting a Single Resource From the Database<\/h2>\n<p>6.1 \u4ece\u6570\u636e\u5e93\u4e2d\u83b7\u53d6\u5355\u4e2a\u8d44\u6e90<\/p>\n<p>Let\u2019s start by modifying the ICompanyRepository interface:\u200c<br \/>\n\u8ba9\u6211\u4eec\u4ece\u4fee\u6539 ICompanyRepository \u63a5\u53e3\u5f00\u59cb\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    }\n}<\/code><\/pre>\n<p>Then, we are going to implement this interface in the CompanyRepository.cs file:<br \/>\n\u7136\u540e\uff0c\u6211\u4eec\u5c06\u5728 CompanyRepository.cs \u6587\u4ef6\u4e2d\u5b9e\u73b0\u8fd9\u4e2a\u63a5\u53e3\uff1a<\/p>\n<pre><code>using Contracts;\nusing Entities.Models;\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; FindAll(trackChanges).OrderBy(c =&gt; c.Name).ToList();\n\n        public Company GetCompany(Guid companyId, bool trackChanges) =&gt; FindByCondition(c =&gt; c.Id.Equals(companyId), trackChanges).SingleOrDefault();\n    }\n}<\/code><\/pre>\n<p>Then, we have to modify the ICompanyService interface:<br \/>\n\u7136\u540e\uff0c\u6211\u4eec\u5fc5\u987b\u4fee\u6539 ICompanyService \u63a5\u53e3\uff1a<\/p>\n<pre><code>using Shared;\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    }\n}<\/code><\/pre>\n<p>And of course, we have to implement this interface in the CompanyService class:<br \/>\n\u5f53\u7136\uff0c\u6211\u4eec\u5fc5\u987b\u5728 CompanyService \u7c7b\u4e2d\u5b9e\u73b0\u6b64\u63a5\u53e3\uff1a<\/p>\n<pre><code>using AutoMapper;\nusing Contracts;\nusing Service.Contracts;\nusing Shared;\n\nnamespace Service\n{\n    internal sealed class CompanyService : ICompanyService\n    {\n        private readonly IRepositoryManager _repository;\n        private readonly ILoggerManager _logger;\n        private readonly IMapper _mapper;\n        public CompanyService(IRepositoryManager repository, ILoggerManager logger, IMapper mapper)\n        {\n            _repository = repository;\n            _logger = logger;\n            _mapper = mapper;\n        }\n\n        public IEnumerable&lt;CompanyDto&gt; GetAllCompanies(bool trackChanges)\n        {\n            var companies = _repository.Company.GetAllCompanies(trackChanges);\n            var companiesDto = _mapper.Map&lt;IEnumerable&lt;CompanyDto&gt;&gt;(companies);\n            return companiesDto;\n        }\n\n        public CompanyDto GetCompany(Guid id, bool trackChanges)\n        {\n            var company = _repository.Company.GetCompany(id, trackChanges);\n            \/\/Check if the company is null\n            var companyDto = _mapper.Map&lt;CompanyDto&gt;(company);\n            return companyDto;\n        }\n    }\n}<\/code><\/pre>\n<p>So, we are calling the repository method that fetches a single company from the database, maps the result to companyDto, and returns it. You can also see the comment about the null checks, which we are going to solve just in a minute.<br \/>\n\u56e0\u6b64\uff0c\u6211\u4eec\u8c03\u7528\u4e86 repository \u65b9\u6cd5\uff0c\u8be5\u65b9\u6cd5\u4ece\u6570\u636e\u5e93\u4e2d\u83b7\u53d6\u5355\u4e2a\u516c\u53f8\uff0c\u5c06\u7ed3\u679c\u6620\u5c04\u5230 companyDto\uff0c\u7136\u540e\u8fd4\u56de\u5b83\u3002\u60a8\u8fd8\u53ef\u4ee5\u67e5\u770b\u6709\u5173 null \u68c0\u67e5\u7684\u6ce8\u91ca\uff0c\u6211\u4eec\u7a0d\u540e\u5c06\u89e3\u51b3\u8be5\u95ee\u9898\u3002<\/p>\n<p>Finally, let\u2019s change the CompanyController class:<br \/>\n\u6700\u540e\uff0c\u8ba9\u6211\u4eec\u66f4\u6539 CompanyController \u7c7b\uff1a<\/p>\n<pre><code>using Microsoft.AspNetCore.Mvc;\nusing Service.Contracts;\n\nnamespace CompanyEmployees.Presentation.Controllers\n{\n    [Route(&quot;api\/[controller]&quot;)]\n    [ApiController]\n    public class CompaniesController : ControllerBase\n    {\n        private readonly IServiceManager _service;\n        public CompaniesController(IServiceManager service) =&gt; _service = service;\n\n        [HttpGet]\n        public IActionResult GetCompanies()\n        {\n            \/\/ throw new Exception(&quot;Exception&quot;);\n            var companies = _service.CompanyService.GetAllCompanies(trackChanges: false);\n            return Ok(companies);\n        }\n\n        [HttpGet(&quot;{id:guid}&quot;)]\n        public IActionResult GetCompany(Guid id)\n        {\n            var company = _service.CompanyService.GetCompany(id, trackChanges: false);\n            return Ok(company);\n        }\n\n    }\n}<\/code><\/pre>\n<p>The route for this action is \/api\/companies\/id and that\u2019s because the \/api\/companies part applies from the root route (on top of the controller) and the id part is applied from the action attribute [HttpGet(\u201c{id:guid}\u201c)]. You can also see that we are using a route constraint (:guid part) where we explicitly state that our id parameter is of the GUID type. We can use many different constraints like int, double, long, float, datetime, bool, length, minlength, maxlength, and many others.<br \/>\n\u6b64\u4f5c\u7684\u8def\u7531\u662f \/api\/companies\/id\uff0c\u8fd9\u662f\u56e0\u4e3a \/api\/companies \u90e8\u5206\u4ece\u6839\u8def\u7531\uff08\u5728\u63a7\u5236\u5668\u7684\u9876\u90e8\uff09\u5e94\u7528\uff0c\u800c id \u90e8\u5206\u4ece\u4f5c\u5c5e\u6027 [HttpGet\uff08\u201c{id\uff1aguid}\u201d\uff09] \u5e94\u7528\u3002\u60a8\u8fd8\u53ef\u4ee5\u770b\u5230\uff0c\u6211\u4eec\u6b63\u5728\u4f7f\u7528\u8def\u7531\u7ea6\u675f\uff08\uff1aguid \u90e8\u5206\uff09\uff0c\u5176\u4e2d\u6211\u4eec\u663e\u5f0f\u58f0\u660e\u6211\u4eec\u7684 id \u53c2\u6570\u662f GUID \u7c7b\u578b\u3002\u6211\u4eec\u53ef\u4ee5\u4f7f\u7528\u8bb8\u591a\u4e0d\u540c\u7684\u7ea6\u675f\uff0c\u5982 int\u3001double\u3001long\u3001float\u3001datetime\u3001bool\u3001length\u3001minlength\u3001maxlength \u7b49\u3002<\/p>\n<p>Let\u2019s use Postman to send a valid request towards our API:<br \/>\n\u8ba9\u6211\u4eec\u4f7f\u7528 Postman \u5411\u6211\u4eec\u7684 API \u53d1\u9001\u4e00\u4e2a\u6709\u6548\u7684\u8bf7\u6c42\uff1a<br \/>\n<a href=\"https:\/\/localhost:5001\/api\/companies\/3d490a70-94ce-4d15-9494-5248280c2ce3\">https:\/\/localhost:5001\/api\/companies\/3d490a70-94ce-4d15-9494-5248280c2ce3<\/a><\/p>\n<p><img decoding=\"async\" src=\"\/images\/ultimateaspnetcorewebapi6\/0601.jpg\" alt=\"alt text\" \/><\/p>\n<p>Great. This works as expected. But, what if someone uses an invalid id parameter?<br \/>\n\u4f1f\u5927\u3002\u8fd9\u6309\u9884\u671f\u5de5\u4f5c\u3002\u4f46\u662f\uff0c\u5982\u679c\u6709\u4eba\u4f7f\u7528\u4e86\u65e0\u6548\u7684 id \u53c2\u6570\u600e\u4e48\u529e\uff1f<\/p>\n<h3>6.1.1 Handling Invalid Requests in a Service Layer\u200c<\/h3>\n<p>6.1.1 \u5904\u7406\u670d\u52a1\u5c42\u4e2d\u7684\u65e0\u6548\u8bf7\u6c42<\/p>\n<p>As you can see, in our service method, we have a comment stating that the result returned from the repository could be null, and this is something we have to handle. We want to return the NotFound response to the client but without involving our controller\u2019s actions. We are going to keep them nice and clean as they already are.<br \/>\n\u5982\u4f60\u6240\u89c1\uff0c\u5728\u6211\u4eec\u7684 service \u65b9\u6cd5\u4e2d\uff0c\u6211\u4eec\u6709\u4e00\u6761\u6ce8\u91ca\uff0c\u6307\u51fa\u4ece\u5b58\u50a8\u5e93\u8fd4\u56de\u7684\u7ed3\u679c\u53ef\u80fd\u662f null\uff0c\u8fd9\u662f\u6211\u4eec\u5fc5\u987b\u5904\u7406\u7684\u4e8b\u60c5\u3002\u6211\u4eec\u5e0c\u671b\u5c06 NotFound \u54cd\u5e94\u8fd4\u56de\u7ed9\u5ba2\u6237\u7aef\uff0c\u4f46\u4e0d\u6d89\u53ca\u63a7\u5236\u5668\u7684\u4f5c\u3002\u6211\u4eec\u5c06\u4fdd\u6301\u5b83\u4eec\u5df2\u7ecf\u7684\u826f\u597d\u548c\u5e72\u51c0\u3002<\/p>\n<p>So, what we are going to do is to create custom exceptions that we can call from the service methods and interrupt the flow. Then our error handling middleware can catch the exception, process the response, and return it to the client. This is a great way of handling invalid requests inside a service layer without having additional checks in our controllers.<br \/>\n\u56e0\u6b64\uff0c\u6211\u4eec\u8981\u505a\u7684\u662f\u521b\u5efa\u81ea\u5b9a\u4e49\u5f02\u5e38\uff0c\u6211\u4eec\u53ef\u4ee5\u4ece\u670d\u52a1\u65b9\u6cd5\u4e2d\u8c03\u7528\u8fd9\u4e9b\u5f02\u5e38\u5e76\u4e2d\u65ad\u6d41\u3002\u7136\u540e\u6211\u4eec\u7684\u9519\u8bef\u5904\u7406\u4e2d\u95f4\u4ef6\u53ef\u4ee5\u6355\u83b7\u5f02\u5e38\uff0c\u5904\u7406\u54cd\u5e94\uff0c\u5e76\u5c06\u5176\u8fd4\u56de\u7ed9\u5ba2\u6237\u7aef\u3002\u8fd9\u662f\u5728\u670d\u52a1\u5c42\u5185\u5904\u7406\u65e0\u6548\u8bf7\u6c42\u7684\u597d\u65b9\u6cd5\uff0c\u800c\u65e0\u9700\u5728\u6211\u4eec\u7684\u63a7\u5236\u5668\u4e2d\u8fdb\u884c\u989d\u5916\u7684\u68c0\u67e5\u3002<\/p>\n<p>That said, let\u2019s start with a new Exceptions folder creation inside the Entities project. Since, in this case, we are going to create a not found response, let\u2019s create a new NotFoundException class inside that folder:<br \/>\n\u4e5f\u5c31\u662f\u8bf4\uff0c\u8ba9\u6211\u4eec\u4ece Entities \u9879\u76ee\u4e2d\u521b\u5efa\u65b0\u7684 Exceptions \u6587\u4ef6\u5939\u5f00\u59cb\u3002\u7531\u4e8e\u5728\u672c\u4f8b\u4e2d\uff0c\u6211\u4eec\u5c06\u521b\u5efa\u4e00\u4e2a\u672a\u627e\u5230\u7684\u54cd\u5e94\uff0c\u56e0\u6b64\u8ba9\u6211\u4eec\u5728\u8be5\u6587\u4ef6\u5939\u4e2d\u521b\u5efa\u4e00\u4e2a\u65b0\u7684 NotFoundException \u7c7b\uff1a<\/p>\n<pre><code>namespace Entities.Exceptions\n{\n    public abstract class NotFoundException : Exception\n    {\n        protected NotFoundException(string message) : base(message) { }\n    }\n}<\/code><\/pre>\n<p>This is an abstract class, which will be a base class for all the individual not found exception classes. It inherits from the Exception class to represent the errors that happen during application execution. Since in our current case, we are handling the situation where we can\u2019t find the company in the database, we are going to create a new CompanyNotFoundException class in the same Exceptions folder:<br \/>\n\u8fd9\u662f\u4e00\u4e2a\u62bd\u8c61\u7c7b\uff0c\u5b83\u5c06\u6210\u4e3a\u6240\u6709\u5355\u4e2a not found \u5f02\u5e38\u7c7b\u7684\u57fa\u7c7b\u3002\u5b83\u7ee7\u627f\u81ea Exception \u7c7b\uff0c\u4ee5\u8868\u793a\u5e94\u7528\u7a0b\u5e8f\u6267\u884c\u671f\u95f4\u53d1\u751f\u7684\u9519\u8bef\u3002\u7531\u4e8e\u5728\u5f53\u524d\u60c5\u51b5\u4e0b\uff0c\u6211\u4eec\u6b63\u5728\u5904\u7406\u5728\u6570\u636e\u5e93\u4e2d\u627e\u4e0d\u5230\u516c\u53f8\u7684\u60c5\u51b5\uff0c\u56e0\u6b64\u6211\u4eec\u5c06\u5728\u540c\u4e00\u4e2a Exceptions \u6587\u4ef6\u5939\u4e2d\u521b\u5efa\u4e00\u4e2a\u65b0\u7684 CompanyNotFoundException \u7c7b\uff1a<\/p>\n<pre><code>namespace Entities.Exceptions\n{\n    public sealed class CompanyNotFoundException : NotFoundException\n    {\n        public CompanyNotFoundException(Guid companyId) : base($&quot;The company with id: {companyId} doesn&#039;t exist in the database.&quot;) { }\n    }\n}<\/code><\/pre>\n<p>Right after that, we can remove the comment in the GetCompany method and throw this exception:<br \/>\n\u7d27\u63a5\u7740\uff0c\u6211\u4eec\u53ef\u4ee5\u5220\u9664 GetCompany \u65b9\u6cd5\u4e2d\u7684\u6ce8\u91ca\u5e76\u5f15\u53d1\u4ee5\u4e0b\u5f02\u5e38\uff1a<\/p>\n<pre><code>using AutoMapper;\nusing Contracts;\nusing Entities.Exceptions;\nusing Service.Contracts;\nusing Shared;\n\nnamespace Service\n{\n    internal sealed class CompanyService : ICompanyService\n    {\n        private readonly IRepositoryManager _repository;\n        private readonly ILoggerManager _logger;\n        private readonly IMapper _mapper;\n        public CompanyService(IRepositoryManager repository, ILoggerManager logger, IMapper mapper)\n        {\n            _repository = repository;\n            _logger = logger;\n            _mapper = mapper;\n        }\n\n        public IEnumerable&lt;CompanyDto&gt; GetAllCompanies(bool trackChanges)\n        {\n            var companies = _repository.Company.GetAllCompanies(trackChanges);\n            var companiesDto = _mapper.Map&lt;IEnumerable&lt;CompanyDto&gt;&gt;(companies);\n            return companiesDto;\n        }\n\n        \/\/public CompanyDto GetCompany(Guid id, bool trackChanges)\n        \/\/{\n        \/\/    var company = _repository.Company.GetCompany(id, trackChanges);\n        \/\/    \/\/Check if the company is null\n        \/\/    var companyDto = _mapper.Map&lt;CompanyDto&gt;(company);\n        \/\/    return companyDto;\n        \/\/}\n\n        public CompanyDto GetCompany(Guid id, bool trackChanges)\n        {\n            var company = _repository.Company.GetCompany(id, trackChanges);\n            if (company is null) throw new CompanyNotFoundException(id);\n            var companyDto = _mapper.Map&lt;CompanyDto&gt;(company);\n            return companyDto;\n        }\n    }\n}<\/code><\/pre>\n<p>Finally, we have to modify our error middleware because we don\u2019t want to return the 500 error message to our clients for every custom error we throw from the service layer.<br \/>\n\u6700\u540e\uff0c\u6211\u4eec\u5fc5\u987b\u4fee\u6539\u6211\u4eec\u7684\u9519\u8bef\u4e2d\u95f4\u4ef6\uff0c\u56e0\u4e3a\u6211\u4eec\u4e0d\u60f3\u4e3a\u6211\u4eec\u4ece\u670d\u52a1\u5c42\u629b\u51fa\u7684\u6bcf\u4e2a\u81ea\u5b9a\u4e49\u9519\u8bef\u8fd4\u56de 500 \u9519\u8bef\u6d88\u606f\u7ed9\u5ba2\u6237\u7aef\u3002<\/p>\n<p>So, let\u2019s modify the ExceptionMiddlewareExtensions class in the main project:<br \/>\n\u56e0\u6b64\uff0c\u8ba9\u6211\u4eec\u4fee\u6539\u4e3b\u9879\u76ee\u4e2d\u7684 ExceptionMiddlewareExtensions \u7c7b\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                        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>We remove the hardcoded StatusCode setup and add the part where we populate it based on the type of exception we throw in our service layer. We are also dynamically populating the Message property of the ErrorDetails object that we return as the response.<br \/>\n\u6211\u4eec\u5220\u9664\u786c\u7f16\u7801\u7684 StatusCode \u8bbe\u7f6e\uff0c\u5e76\u6dfb\u52a0\u90e8\u5206\uff0c\u6839\u636e\u6211\u4eec\u5728\u670d\u52a1\u5c42\u4e2d\u629b\u51fa\u7684\u5f02\u5e38\u7c7b\u578b\u6765\u586b\u5145\u5b83\u3002\u6211\u4eec\u8fd8\u52a8\u6001\u586b\u5145\u4f5c\u4e3a\u54cd\u5e94\u8fd4\u56de\u7684 ErrorDetails \u5bf9\u8c61\u7684 Message \u5c5e\u6027\u3002<\/p>\n<p>Additionally, you can see the advantage of using the base abstract exception class here (NotFoundException in this case). We are not checking for the specific class implementation but the base type. This allows us to have multiple not found classes that inherit from the NotFoundException class and this middleware will know that we want to return the NotFound response to the client.<br \/>\n\u6b64\u5916\uff0c\u60a8\u53ef\u4ee5\u5728\u6b64\u5904\u770b\u5230\u4f7f\u7528\u57fa\u62bd\u8c61\u5f02\u5e38\u7c7b\u7684\u4f18\u52bf\uff08\u5728\u672c\u4f8b\u4e2d\u4e3a NotFoundException\uff09\u3002\u6211\u4eec\u68c0\u67e5\u7684\u4e0d\u662f\u7279\u5b9a\u7684\u7c7b\u5b9e\u73b0\uff0c\u800c\u662f\u57fa\u7c7b\u578b\u3002\u8fd9\u5141\u8bb8\u6211\u4eec\u62e5\u6709\u591a\u4e2a\u4ece NotFoundException \u7c7b\u7ee7\u627f\u7684\u672a\u627e\u5230\u7684\u7c7b\uff0c\u5e76\u4e14\u6b64\u4e2d\u95f4\u4ef6\u5c06\u77e5\u9053\u6211\u4eec\u60f3\u8981\u5c06 NotFound \u54cd\u5e94\u8fd4\u56de\u7ed9\u5ba2\u6237\u7aef\u3002<\/p>\n<p>Excellent. Now, we can start the app and send the invalid request:<br \/>\n\u975e\u5e38\u597d\u3002\u73b0\u5728\uff0c\u6211\u4eec\u53ef\u4ee5\u542f\u52a8\u5e94\u7528\u7a0b\u5e8f\u5e76\u53d1\u9001\u65e0\u6548\u8bf7\u6c42\uff1a<\/p>\n<p><a href=\"https:\/\/localhost:5001\/api\/companies\/3d490a70-94ce-4d15-9494-5248280c2ce2\">https:\/\/localhost:5001\/api\/companies\/3d490a70-94ce-4d15-9494-5248280c2ce2<\/a><\/p>\n<p><img decoding=\"async\" src=\"\/images\/ultimateaspnetcorewebapi6\/0602.jpg\" alt=\"alt text\" \/><\/p>\n<p>We can see the status code we require and also the response object with proper StatusCode and Message properties. Also, if you inspect the log message, you will see that we are logging a correct message.<br \/>\n\u6211\u4eec\u53ef\u4ee5\u770b\u5230\u6240\u9700\u7684\u72b6\u6001\u4ee3\u7801\uff0c\u8fd8\u53ef\u4ee5\u770b\u5230\u5177\u6709\u9002\u5f53 StatusCode \u548c Message \u5c5e\u6027\u7684\u54cd\u5e94\u5bf9\u8c61\u3002\u6b64\u5916\uff0c\u5982\u679c\u60a8\u68c0\u67e5\u65e5\u5fd7\u6d88\u606f\uff0c\u60a8\u5c06\u770b\u5230\u6211\u4eec\u8bb0\u5f55\u7684\u6d88\u606f\u6b63\u786e\u65e0\u8bef\u3002<\/p>\n<p>With this approach, we have perfect control of all the exceptional cases in our app. We have that control due to global error handler implementation. For now, we only handle the invalid id sent from the client, but we will handle more exceptional cases in the rest of the project.<br \/>\n\u901a\u8fc7\u8fd9\u79cd\u65b9\u6cd5\uff0c\u6211\u4eec\u53ef\u4ee5\u5b8c\u7f8e\u5730\u63a7\u5236\u5e94\u7528\u7a0b\u5e8f\u4e2d\u7684\u6240\u6709\u7279\u6b8a\u60c5\u51b5\u3002\u7531\u4e8e\u5168\u5c40\u9519\u8bef\u5904\u7406\u7a0b\u5e8f\u7684\u5b9e\u73b0\uff0c\u6211\u4eec\u62e5\u6709\u4e86\u8fd9\u79cd\u63a7\u5236\u6743\u3002\u76ee\u524d\uff0c\u6211\u4eec\u53ea\u5904\u7406\u5ba2\u6237\u7aef\u53d1\u9001\u7684\u65e0\u6548 ID\uff0c\u4f46\u6211\u4eec\u5c06\u5728\u9879\u76ee\u7684\u5176\u4f59\u90e8\u5206\u5904\u7406\u66f4\u591a\u5f02\u5e38\u60c5\u51b5\u3002<\/p>\n<p>In our tests for a published app, the regular request sent from Postman took 7ms and the exceptional one took 14ms. So you can see how fast the response is.<br \/>\n\u5728\u6211\u4eec\u5bf9\u5df2\u53d1\u5e03\u5e94\u7528\u7a0b\u5e8f\u7684\u6d4b\u8bd5\u4e2d\uff0c\u4ece Postman \u53d1\u9001\u7684\u5e38\u89c4\u8bf7\u6c42\u9700\u8981 7 \u6beb\u79d2\uff0c\u7279\u6b8a\u8bf7\u6c42\u9700\u8981 14 \u6beb\u79d2\u3002\u6240\u4ee5\u4f60\u53ef\u4ee5\u770b\u5230\u54cd\u5e94\u6709\u591a\u5feb\u3002<\/p>\n<p>Of course, we are using exceptions only for these exceptional cases (Company not found, Employee not found...) and not throwing them all over the application. So, if you follow the same strategy, you will not face any performance issues.<br \/>\n\u5f53\u7136\uff0c\u6211\u4eec\u53ea\u5bf9\u8fd9\u4e9b\u7279\u6b8a\u60c5\u51b5\uff08\u672a\u627e\u5230\u516c\u53f8\u3001\u672a\u627e\u5230\u5458\u5de5\u7b49\uff09\u4f7f\u7528\u5f02\u5e38\uff0c\u800c\u4e0d\u662f\u5728\u6574\u4e2a\u5e94\u7528\u7a0b\u5e8f\u4e2d\u629b\u51fa\u5b83\u4eec\u3002\u56e0\u6b64\uff0c\u5982\u679c\u60a8\u9075\u5faa\u76f8\u540c\u7684\u7b56\u7565\uff0c\u60a8\u5c06\u4e0d\u4f1a\u9047\u5230\u4efb\u4f55\u6027\u80fd\u95ee\u9898\u3002<\/p>\n<p>Lastly, if you have an application where you have to throw custom exceptions more often and maybe impact your performance, we are going to provide an alternative to exceptions in the first bonus chapter of this book (Chapter 32).<br \/>\n\u6700\u540e\uff0c\u5982\u679c\u60a8\u7684\u5e94\u7528\u7a0b\u5e8f\u5fc5\u987b\u66f4\u9891\u7e41\u5730\u5f15\u53d1\u81ea\u5b9a\u4e49\u5f02\u5e38\uff0c\u5e76\u4e14\u53ef\u80fd\u4f1a\u5f71\u54cd\u60a8\u7684\u6027\u80fd\uff0c\u6211\u4eec\u5c06\u5728\u672c\u4e66\u7684\u96441\u7ae0\uff08\u7b2c 32 \u7ae0\uff09\u4e2d\u63d0\u4f9b\u5f02\u5e38\u7684\u66ff\u4ee3\u65b9\u6848\u3002<\/p>\n<h2>6.2 Parent\/Child Relationships in Web API<\/h2>\n<p>6.2 Web API \u4e2d\u7684\u7236\/\u5b50\u5173\u7cfb<\/p>\n<p>Up until now, we have been working only with the company, which is a parent (principal) entity in our API. But for each company, we have a related employee (dependent entity). Every employee must be related to a certain company and we are going to create our URIs in that manner.\u200c<br \/>\n\u5230\u76ee\u524d\u4e3a\u6b62\uff0c\u6211\u4eec\u53ea\u4e0e\u516c\u53f8\u5408\u4f5c\uff0c\u8be5\u516c\u53f8\u662f\u6211\u4eec API \u4e2d\u7684\u7236\uff08\u4e3b\u4f53\uff09\u5b9e\u4f53\u3002\u4f46\u5bf9\u4e8e\u6bcf\u5bb6\u516c\u53f8\uff0c\u6211\u4eec\u90fd\u6709\u4e00\u4e2a\u76f8\u5173\u7684\u5458\u5de5\uff08\u4f9d\u8d56\u5b9e\u4f53\uff09\u3002\u6bcf\u4e2a\u5458\u5de5\u90fd\u5fc5\u987b\u4e0e\u67d0\u5bb6\u516c\u53f8\u76f8\u5173\uff0c\u6211\u4eec\u5c06\u4ee5\u8fd9\u79cd\u65b9\u5f0f\u521b\u5efa\u6211\u4eec\u7684 URI\u3002<\/p>\n<p>That said, let\u2019s create a new controller in the Presentation project and name it EmployeesController:<br \/>\n\u4e5f\u5c31\u662f\u8bf4\uff0c\u8ba9\u6211\u4eec\u5728 Presentation \u9879\u76ee\u4e2d\u521b\u5efa\u4e00\u4e2a\u65b0\u63a7\u5236\u5668\uff0c\u5e76\u5c06\u5176\u547d\u540d\u4e3a EmployeesController\uff1a<\/p>\n<pre><code>using Microsoft.AspNetCore.Mvc;\nusing Service.Contracts;\n\nnamespace CompanyEmployees.Presentation.Controllers\n{\n    [Route(&quot;api\/companies\/{companyId}\/employees&quot;)]\n    [ApiController]\n    public class EmployeesController : ControllerBase\n    {\n        private readonly IServiceManager _service;\n        public EmployeesController(IServiceManager service) =&gt; _service = service;\n    }\n}<\/code><\/pre>\n<p>We are familiar with this code, but our main route is a bit different. As we said, a single employee can\u2019t exist without a company entity and this is exactly what we are exposing through this URI. To get an employee or employees from the database, we have to specify the companyId parameter, and that is something all actions will have in common. For that reason, we have specified this route as our root route.<br \/>\n\u6211\u4eec\u719f\u6089\u8fd9\u6bb5\u4ee3\u7801\uff0c\u4f46\u662f\u6211\u4eec\u7684\u4e3b\u8981\u8def\u7ebf\u6709\u70b9\u4e0d\u540c\u3002\u6b63\u5982\u6211\u4eec\u6240\u8bf4\uff0c\u6ca1\u6709\u516c\u53f8\u5b9e\u4f53\u5c31\u4e0d\u80fd\u5b58\u5728\u5355\u4e2a\u5458\u5de5\uff0c\u8fd9\u6b63\u662f\u6211\u4eec\u901a\u8fc7\u6b64 URI \u516c\u5f00\u7684\u5185\u5bb9\u3002\u8981\u4ece\u6570\u636e\u5e93\u4e2d\u83b7\u53d6\u4e00\u4e2a\u6216\u591a\u4e2a\u5458\u5de5\uff0c\u6211\u4eec\u5fc5\u987b\u6307\u5b9a companyId \u53c2\u6570\uff0c\u8fd9\u662f\u6240\u6709\u4f5c\u7684\u5171\u540c\u70b9\u3002\u56e0\u6b64\uff0c\u6211\u4eec\u5df2\u5c06\u6b64\u8def\u7531\u6307\u5b9a\u4e3a\u6211\u4eec\u7684\u6839\u8def\u7531\u3002<\/p>\n<p>Before we create an action to fetch all the employees per company, we have to modify the IEmployeeRepository interface:<br \/>\n\u5728\u6211\u4eec\u521b\u5efa\u4e00\u4e2a\u52a8\u4f5c\u6765\u83b7\u53d6\u6bcf\u4e2a\u516c\u53f8\u7684\u6240\u6709\u5458\u5de5\u4e4b\u524d\uff0c\u6211\u4eec\u5fc5\u987b\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    }\n}<\/code><\/pre>\n<p>After interface modification, we are going to modify the EmployeeRepository class:<br \/>\n\u4fee\u6539\u63a5\u53e3\u540e\uff0c\u6211\u4eec\u5c06\u4fee\u6539 EmployeeRepository \u7c7b\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    }\n}<\/code><\/pre>\n<p>Then, before we start adding code to the service layer, we are going to create a new DTO. Let\u2019s name it EmployeeDto and add it to the Shared\/DataTransferObjects folder:<br \/>\n\u7136\u540e\uff0c\u5728\u6211\u4eec\u5f00\u59cb\u5411\u670d\u52a1\u5c42\u6dfb\u52a0\u4ee3\u7801\u4e4b\u524d\uff0c\u6211\u4eec\u5c06\u521b\u5efa\u4e00\u4e2a\u65b0\u7684 DTO\u3002\u8ba9\u6211\u4eec\u5c06\u5176\u547d\u540d\u4e3a EmployeeDto \u5e76\u5c06\u5176\u6dfb\u52a0\u5230 Shared\/DataTransferObjects \u6587\u4ef6\u5939\u4e2d\uff1a<\/p>\n<pre><code>namespace Shared.DataTransferObjects\n{\n    public record CompanyDto(Guid Id, string Name, string FullAddress);\n}<\/code><\/pre>\n<p>Since we want to return this DTO to the client, we have to create a mapping rule inside the MappingProfile class:<br \/>\n\u7531\u4e8e\u6211\u4eec\u60f3\u5c06\u6b64 DTO \u8fd4\u56de\u7ed9\u5ba2\u6237\u7aef\uff0c\u56e0\u6b64\u6211\u4eec\u5fc5\u987b\u5728 MappingProfile \u7c7b\u4e2d\u521b\u5efa\u4e00\u4e2a\u6620\u5c04\u89c4\u5219\uff1a<\/p>\n<pre><code>using AutoMapper;\nusing Entities.Models;\nusing Shared.DataTransferObjects;\n\nnamespace CompanyEmployees\n{\n    public class MappingProfile : Profile\n    {\n        public MappingProfile()\n        {\n            CreateMap&lt;Company, CompanyDto&gt;().ForCtorParam(&quot;FullAddress&quot;, opt =&gt; opt.MapFrom(x =&gt; string.Join(&#039; &#039;, x.Address, x.Country)));\n            CreateMap&lt;Employee, EmployeeDto&gt;();\n        }\n    }\n}<\/code><\/pre>\n<p>Now, we can modify the IEmployeeService interface:<br \/>\n\u73b0\u5728\uff0c\u6211\u4eec\u53ef\u4ee5\u4fee\u6539 IEmployeeService \u63a5\u53e3\uff1a<\/p>\n<pre><code>using Shared.DataTransferObjects;\n\nnamespace Service.Contracts\n{\n    public interface IEmployeeService { \n        IEnumerable&lt;EmployeeDto&gt; GetEmployees(Guid companyId, bool trackChanges); \n    }\n}<\/code><\/pre>\n<p>And of course, we have to implement this interface in the EmployeeService class:<br \/>\n\u5f53\u7136\uff0c\u6211\u4eec\u5fc5\u987b\u5728 EmployeeService \u7c7b\u4e2d\u5b9e\u73b0\u8fd9\u4e2a\u63a5\u53e3\uff1a<\/p>\n<pre><code>using AutoMapper;\nusing Contracts;\nusing Entities.Exceptions;\nusing Service.Contracts;\nusing Shared.DataTransferObjects;\n\nnamespace Service\n{\n    internal sealed class EmployeeService : IEmployeeService\n    {\n        private readonly IRepositoryManager _repository;\n        private readonly ILoggerManager _logger;\n        private readonly IMapper _mapper;\n        public EmployeeService(IRepositoryManager repository, ILoggerManager logger, IMapper mapper)\n        {\n            _repository = repository;\n            _logger = logger;\n            _mapper = mapper;\n        }\n\n        public IEnumerable&lt;EmployeeDto&gt; GetEmployees(Guid companyId, bool trackChanges)\n        {\n            var company = _repository.Company.GetCompany(companyId, trackChanges);\n            if (company is null) throw new CompanyNotFoundException(companyId);\n            var employeesFromDb = _repository.Employee.GetEmployees(companyId, trackChanges);\n            var employeesDto = _mapper.Map&lt;IEnumerable&lt;EmployeeDto&gt;&gt;(employeesFromDb);\n            return employeesDto;\n        }\n    }\n}<\/code><\/pre>\n<p>Here, we first fetch the company entity from the database. If it doesn\u2019t exist, we return the NotFound response to the client. If it does, we fetch all the employees for that company, map them to the collection of EmployeeDto and return it to the caller.<br \/>\n\u5728\u8fd9\u91cc\uff0c\u6211\u4eec\u9996\u5148\u4ece\u6570\u636e\u5e93\u4e2d\u83b7\u53d6 company \u5b9e\u4f53\u3002\u5982\u679c\u4e0d\u5b58\u5728\uff0c\u6211\u4eec\u5c06 NotFound \u54cd\u5e94\u8fd4\u56de\u7ed9\u5ba2\u6237\u7aef\u3002\u5982\u679c\u662f\u8fd9\u6837\uff0c\u6211\u4eec\u5c06\u83b7\u53d6\u8be5\u516c\u53f8\u7684\u6240\u6709\u5458\u5de5\uff0c\u5c06\u5b83\u4eec\u6620\u5c04\u5230 EmployeeDto \u7684\u96c6\u5408\uff0c\u5e76\u5c06\u5176\u8fd4\u56de\u7ed9\u8c03\u7528\u65b9\u3002<\/p>\n<p>Finally, let\u2019s modify the Employees controller:<br \/>\n\u6700\u540e\uff0c\u8ba9\u6211\u4eec\u4fee\u6539 Employees \u63a7\u5236\u5668\uff1a<\/p>\n<pre><code>[HttpGet]\npublic IActionResult GetEmployeesForCompany(Guid companyId)\n{\n    var employees = _service.EmployeeService.GetEmployees(companyId, trackChanges: false);\n    return Ok(employees);\n}<\/code><\/pre>\n<p>This code is pretty straightforward \u2014 nothing we haven\u2019t seen so far \u2014 but we need to explain just one thing. As you can see, we have the companyId parameter in our action and this parameter will be mapped from the main route. For that reason, we didn\u2019t place it in the [HttpGet] attribute as we did with the GetCompany action.<br \/>\n\u8fd9\u6bb5\u4ee3\u7801\u975e\u5e38\u7b80\u5355 \u2014 \u5230\u76ee\u524d\u4e3a\u6b62\u6211\u4eec\u8fd8\u6ca1\u6709\u89c1\u8fc7 \u2014 \u4f46\u6211\u4eec\u53ea\u9700\u8981\u89e3\u91ca\u4e00\u4ef6\u4e8b\u3002\u5982\u60a8\u6240\u89c1\uff0c\u6211\u4eec\u7684\u4f5c\u4e2d\u6709 companyId \u53c2\u6570\uff0c\u6b64\u53c2\u6570\u5c06\u4ece\u4e3b\u8def\u7531\u6620\u5c04\u3002\u56e0\u6b64\uff0c\u6211\u4eec\u6ca1\u6709\u50cf\u5bf9 GetCompany\u4f5c\u90a3\u6837\u5c06\u5176\u653e\u5728 [HttpGet] \u5c5e\u6027\u4e2d\u3002<\/p>\n<p>That done, we can send a request with a valid companyId:<br \/>\n\u5b8c\u6210\u540e\uff0c\u6211\u4eec\u53ef\u4ee5\u53d1\u9001\u5177\u6709\u6709\u6548 companyId\uff1a<br \/>\n<a href=\"https:\/\/localhost:5001\/api\/companies\/c9d4c053-49b6-410c-bc78-2d54a9991870\/employees\">https:\/\/localhost:5001\/api\/companies\/c9d4c053-49b6-410c-bc78-2d54a9991870\/employees<\/a><\/p>\n<p><img decoding=\"async\" src=\"\/images\/ultimateaspnetcorewebapi6\/0603.jpg\" alt=\"alt text\" \/><\/p>\n<p>And with an invalid companyId:<br \/>\n\u5e76\u4e14\u4f7f\u7528\u65e0\u6548\u7684 companyId\uff1a<br \/>\n<a href=\"https:\/\/localhost:5001\/api\/companies\/c9d4c053-49b6-410c-bc78-2d54a9991873\/employees\">https:\/\/localhost:5001\/api\/companies\/c9d4c053-49b6-410c-bc78-2d54a9991873\/employees<\/a><\/p>\n<p><img decoding=\"async\" src=\"\/images\/ultimateaspnetcorewebapi6\/0604.jpg\" alt=\"alt text\" \/><\/p>\n<p>Excellent. Let\u2019s continue by fetching a single employee.<br \/>\n\u975e\u5e38\u597d\u3002\u8ba9\u6211\u4eec\u7ee7\u7eed\u83b7\u53d6\u5355\u4e2a\u5458\u5de5\u3002<\/p>\n<h2>6.3 Getting a Single Employee for Company<\/h2>\n<p>6.3 \u4e3a\u516c\u53f8\u62db\u8058\u4e00\u540d\u5458\u5de5<\/p>\n<p>So, as we did in previous sections, let\u2019s start with the\u200c IEmployeeRepository interface modification:<br \/>\n\u56e0\u6b64\uff0c\u6b63\u5982\u6211\u4eec\u5728\u524d\u9762\u7684\u90e8\u5206\u4e2d\u6240\u505a\u7684\u90a3\u6837\uff0c\u8ba9\u6211\u4eec\u4ece IEmployeeRepository \u63a5\u53e3\u4fee\u6539\u5f00\u59cb\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    }\n}<\/code><\/pre>\n<p>Now, let\u2019s implement this method in the EmployeeRepository class:<br \/>\n\u73b0\u5728\uff0c\u8ba9\u6211\u4eec\u5728 EmployeeRepository \u7c7b\u4e2d\u5b9e\u73b0\u6b64\u65b9\u6cd5\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\n    }\n}<\/code><\/pre>\n<p>Next, let\u2019s add another exception class in the Entities\/Exceptions folder:<br \/>\n\u63a5\u4e0b\u6765\uff0c\u8ba9\u6211\u4eec\u5728 Entities\/Exceptions \u6587\u4ef6\u5939\u4e2d\u6dfb\u52a0\u53e6\u4e00\u4e2a\u5f02\u5e38\u7c7b\uff1a<\/p>\n<pre><code>namespace Entities.Exceptions\n{\n    public class EmployeeNotFoundException : NotFoundException\n    {\n        public EmployeeNotFoundException(Guid employeeId) : base($&quot;Employee with id: {employeeId} doesn&#039;t exist in the database.&quot;) { }\n    }\n}<\/code><\/pre>\n<p>We will soon see why do we need this class.<br \/>\n\u6211\u4eec\u5f88\u5feb\u5c31\u4f1a\u660e\u767d\u4e3a\u4ec0\u4e48\u6211\u4eec\u9700\u8981\u8fd9\u4e2a\u7c7b\u3002<\/p>\n<p>To continue, we have to modify the IEmployeeService interface:<br \/>\n\u8981\u7ee7\u7eed\uff0c\u6211\u4eec\u5fc5\u987b\u4fee\u6539 IEmployeeService \u63a5\u53e3\uff1a<\/p>\n<pre><code>using Shared.DataTransferObjects;\n\nnamespace Service.Contracts\n{\n    public interface IEmployeeService { \n        IEnumerable&lt;EmployeeDto&gt; GetEmployees(Guid companyId, bool trackChanges);\n        EmployeeDto GetEmployee(Guid companyId, Guid id, bool trackChanges);\n    }\n}<\/code><\/pre>\n<p>And implement this new method in the EmployeeService class:<br \/>\n\u5e76\u5728 EmployeeService \u7c7b\u4e2d\u5b9e\u73b0\u8fd9\u4e2a\u65b0\u65b9\u6cd5\uff1a<\/p>\n<pre><code>using AutoMapper;\nusing Contracts;\nusing Entities.Exceptions;\nusing Service.Contracts;\nusing Shared.DataTransferObjects;\n\nnamespace Service\n{\n    internal sealed class EmployeeService : IEmployeeService\n    {\n        private readonly IRepositoryManager _repository;\n        private readonly ILoggerManager _logger;\n        private readonly IMapper _mapper;\n        public EmployeeService(IRepositoryManager repository, ILoggerManager logger, IMapper mapper)\n        {\n            _repository = repository;\n            _logger = logger;\n            _mapper = mapper;\n        }\n\n        public IEnumerable&lt;EmployeeDto&gt; GetEmployees(Guid companyId, bool trackChanges)\n        {\n            var company = _repository.Company.GetCompany(companyId, trackChanges);\n            if (company is null) \n                throw new CompanyNotFoundException(companyId);\n            var employeesFromDb = _repository.Employee.GetEmployees(companyId, trackChanges);\n            var employeesDto = _mapper.Map&lt;IEnumerable&lt;EmployeeDto&gt;&gt;(employeesFromDb);\n            return employeesDto;\n        }\n\n        public EmployeeDto GetEmployee(Guid companyId, Guid id, bool trackChanges)\n        {\n            var company = _repository.Company.GetCompany(companyId, trackChanges);\n            if (company is null) \n                throw new CompanyNotFoundException(companyId);\n            var employeeDb = _repository.Employee.GetEmployee(companyId, id, trackChanges);\n            if (employeeDb is null) \n                throw new EmployeeNotFoundException(id);\n            var employee = _mapper.Map&lt;EmployeeDto&gt;(employeeDb);\n            return employee;\n        }\n    }\n}\n<\/code><\/pre>\n<p>This is also a pretty clear code and we can see the reason for creating a new exception class.<br \/>\n\u8fd9\u4e5f\u662f\u4e00\u4e2a\u975e\u5e38\u6e05\u6670\u7684\u4ee3\u7801\uff0c\u6211\u4eec\u53ef\u4ee5\u770b\u5230\u521b\u5efa\u65b0\u7684\u5f02\u5e38\u7c7b\u7684\u539f\u56e0\u3002<\/p>\n<p>Finally, let\u2019s modify the EmployeeController class:<br \/>\n\u6700\u540e\uff0c\u8ba9\u6211\u4eec\u4fee\u6539 EmployeeController \u7c7b\uff1a<\/p>\n<pre><code>using Microsoft.AspNetCore.Mvc;\nusing Service.Contracts;\n\nnamespace CompanyEmployees.Presentation.Controllers\n{\n    [Route(&quot;api\/companies\/{companyId}\/employees&quot;)]\n    [ApiController]\n    public class EmployeesController : ControllerBase\n    {\n        private readonly IServiceManager _service;\n        public EmployeesController(IServiceManager service) =&gt; _service = service;\n\n        [HttpGet]\n        public IActionResult GetEmployeesForCompany(Guid companyId)\n        {\n            var employees = _service.EmployeeService.GetEmployees(companyId, trackChanges: false);\n            return Ok(employees);\n        }\n\n        [HttpGet(&quot;{id:guid}&quot;)]\n        public IActionResult GetEmployeeForCompany(Guid companyId, Guid id)\n        {\n            var employee = _service.EmployeeService.GetEmployee(companyId, id, trackChanges: false);\n            return Ok(employee);\n        }\n    }\n}<\/code><\/pre>\n<p>Excellent. You can see how clear our action is.<br \/>\n\u975e\u5e38\u597d\u3002\u4f60\u53ef\u4ee5\u770b\u5230\u6211\u4eec\u7684\u884c\u52a8\u662f\u591a\u4e48\u660e\u786e\u3002<\/p>\n<p>We can test this action by using already created requests from the Bonus 2-CompanyEmployeesRequests.postman_collection.json file placed in the folder with the exercise files:<br \/>\n\u6211\u4eec\u53ef\u4ee5\u4f7f\u7528\u4f4d\u4e8e\u5305\u542b\u7ec3\u4e60\u6587\u4ef6\u7684\u6587\u4ef6\u5939\u4e2d\u7684 Bonus 2-CompanyEmployeesRequests.postman_collection.json \u6587\u4ef6\u4e2d\u5df2\u521b\u5efa\u7684\u8bf7\u6c42\u6765\u6d4b\u8bd5\u6b64\u4f5c\uff1a<\/p>\n<p><a href=\"https:\/\/localhost:5001\/api\/companies\/c9d4c053-49b6-410c-bc78-2d54a9991870\/employees\/86dba8c0-d178-41e7-938c-ed49778fb52a\">https:\/\/localhost:5001\/api\/companies\/c9d4c053-49b6-410c-bc78-2d54a9991870\/employees\/86dba8c0-d178-41e7-938c-ed49778fb52a<\/a><\/p>\n<p><img decoding=\"async\" src=\"\/images\/ultimateaspnetcorewebapi6\/0605.jpg\" alt=\"alt text\" \/><\/p>\n<p>When we send the request with an invalid company or employee id:<br \/>\n\u5f53\u6211\u4eec\u4f7f\u7528\u65e0\u6548\u7684\u516c\u53f8\u6216\u5458\u5de5 ID \u53d1\u9001\u8bf7\u6c42\u65f6\uff1a<br \/>\n<a href=\"https:\/\/localhost:5001\/api\/companies\/c9d4c053-49b6-410c-bc78-2d54a9991870\/employees\/86dba8c0-d178-41e7-938c-ed49778fb52c\">https:\/\/localhost:5001\/api\/companies\/c9d4c053-49b6-410c-bc78-2d54a9991870\/employees\/86dba8c0-d178-41e7-938c-ed49778fb52c<\/a><\/p>\n<p><img decoding=\"async\" src=\"\/images\/ultimateaspnetcorewebapi6\/0606.jpg\" alt=\"alt text\" \/><\/p>\n<p><img decoding=\"async\" src=\"\/images\/ultimateaspnetcorewebapi6\/0607.jpg\" alt=\"alt text\" \/><\/p>\n<p>Our responses are pretty self-explanatory, which makes for a good user experience.<br \/>\n\u6211\u4eec\u7684\u56de\u7b54\u4e0d\u8a00\u81ea\u660e\uff0c\u8fd9\u5e26\u6765\u4e86\u826f\u597d\u7684\u7528\u6237\u4f53\u9a8c\u3002<\/p>\n<p>Until now, we have received only JSON formatted responses from our API. But what if we want to support some other format, like XML for example?<br \/>\n\u5230\u76ee\u524d\u4e3a\u6b62\uff0c\u6211\u4eec\u53ea\u6536\u5230\u4e86\u6765\u81ea API \u7684 JSON \u683c\u5f0f\u7684\u54cd\u5e94\u3002\u4f46\u662f\uff0c\u5982\u679c\u6211\u4eec\u60f3\u8981\u652f\u6301\u4e00\u4e9b\u5176\u4ed6\u683c\u5f0f\uff0c\u4f8b\u5982 XML\uff0c\u8be5\u600e\u4e48\u529e\uff1f<\/p>\n<p>Well, in the next chapter we are going to learn more about Content Negotiation and enabling different formats for our responses.<br \/>\n\u90a3\u4e48\uff0c\u5728\u4e0b\u4e00\u7ae0\u4e2d\uff0c\u6211\u4eec\u5c06\u4e86\u89e3\u6709\u5173 Content Negotiation \u548c\u4e3a\u6211\u4eec\u7684\u54cd\u5e94\u542f\u7528\u4e0d\u540c\u683c\u5f0f\u7684\u66f4\u591a\u4fe1\u606f\u3002<\/p>\n","protected":false},"excerpt":{"rendered":"<p>6 GETTING ADDITIONAL RESOURCES \u83b7\u53d6\u989d\u5916\u8d44\u6e90 As of now, we can continue with GET requests by adding additional actions to our controller. Moreover, we are going to create one more controller for the Employee resource and implement an additional action in it.\u200c \u622a\u81f3\u76ee\u524d\uff0c\u6211\u4eec\u53ef\u4ee5\u901a\u8fc7\u5411\u63a7\u5236\u5668\u6dfb\u52a0\u5176\u4ed6\u4f5c\u6765\u7ee7\u7eed\u5904\u7406 GET \u8bf7\u6c42\u3002\u6b64\u5916\uff0c\u6211\u4eec\u5c06\u4e3a Employee \u8d44\u6e90\u518d\u521b\u5efa\u4e00\u4e2a\u63a7\u5236\u5668\uff0c\u5e76\u5728\u5176\u4e2d\u5b9e\u65bd\u4e00\u4e2a\u989d\u5916\u7684\u4f5c\u3002 6.1 Getting a Single Resource From the Database 6.1 [&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-1110","post","type-post","status-publish","format-standard","hentry","category-uncategorized"],"_links":{"self":[{"href":"https:\/\/diji.net\/index.php?rest_route=\/wp\/v2\/posts\/1110","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=1110"}],"version-history":[{"count":0,"href":"https:\/\/diji.net\/index.php?rest_route=\/wp\/v2\/posts\/1110\/revisions"}],"wp:attachment":[{"href":"https:\/\/diji.net\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=1110"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/diji.net\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=1110"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/diji.net\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=1110"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}