使用 .NET Core 3.x 構建 RESTFUL Api (續)
關於Entity Model vs 面向外部的Model
Entity Framework Core 使用 Entity Model 用來表示資料庫裡面的記錄。
面向外部的Model 則表示要傳輸的東西,有時候被稱為 Dto,有時候被稱為 ViewModel。
關於Dto,API消費者通過Dto,僅提供給使用者需要的資料起到隔離的作用,防止API消費者直接接觸到核心的Entity Model。
可能你會覺得有點多餘,但是仔細想想你會發現,Dto的存在是很有必要的。
Entity Model 與資料庫實際上應該是有種依賴的關係,資料庫某一項功能發生改變,Entity Model也應該會做出相應的動作,那麼這個時候 API消費者在請求伺服器介面資料時,如果直接接觸到了 Entity Model資料,那麼它也就無法預測到底是哪一項功能做出了改變。這個時候可能在做 API 請求的時候發生不可預估的錯誤。Dto的存在一定程度上解決了這一問題。
那麼它的作用是?
- 系統更加健壯
- 系統更加可靠
- 系統易於進化
編寫Company的 Dto:
using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; namespace Routine.Api.Models { public class CompanyDto { public Guid Id { get; set; } public string Name { get; set; } } }
對比Company的 Entity Model:
using System; using System.Collections.Generic; namespace Routine.Api.Entities { /// <summary> /// 公司 /// </summary> public class Company { public Guid Id { get; set; } public string Name { get; set; } public string Introduction { get; set; } publicICollection<Employee> Employees { get; set; } } }
Id和Name屬性是一致的,對於 Employees集合 以及 Introduction 字串為了區分,這裡不提供給 Dto
如何使用?
這裡就涉及到了如何從 Entity Model 的資料轉化到 Dto
分析:我們給API消費者提供的資料肯定是一個集合,那麼可以先將Company的Dto定義為一個List集合,再通過迴圈 Entity Model 的資料,將資料新增到集合並且賦值給 Dto 對應的屬性。
控制器程式碼:
[HttpGet] //IActionResult定義了一些合約,它可以代表ActionResult返回的結果 public async Task<ActionResult<IEnumerable<CompanyDto>>> GetCompanies() { var companies =await _companyRepository.GetCompaniesAsync();//讀取出來的是List var companyDtos = new List<CompanyDto>(); foreach (var company in companies) { companyDtos.Add(new CompanyDto { Id = company.Id, Name = company.Name }); }; return Ok(companyDtos); }
}
這裡你可能注意到了 返回的是 ActionResult<T>
關於 ActionResult<T>,好處就是讓 API 消費者意識到此介面的返回型別,就是將介面的返回型別進一步的明確,可以方便呼叫,讓程式碼的可讀性也更高。
你可以返回IEnumerable型別,也可以直接返回List,當然這兩者並沒有什麼區別,因為List也實現了 IEnumerable 這個介面!
那麼這樣做會面臨又一個問題。如果 Dto 需要的資料又20甚至50條往上,那麼這樣寫會顯得非常的笨拙而且也很容易出錯。
如何處理呢? dotnet生態給我們提供了一個很好的物件屬性對映器 AutoMapper!!!
關於 AutoMapper,官方解釋:基於約定的物件屬性對映器。
它還存在一個作用,在處理對映關係時出現如果出現空引用異常,就是對映的目標型別出現了與源型別不匹配的屬性欄位,那麼就會自動忽略這一異常。
如何下載?
開啟 nuget 工具包,搜尋AutoMapper ,下載第二個!!! 原因是這個更好的實現依賴注入,可以看到它也依賴於 AutoMapper,相當於把第一個也一併下載了。
如何使用 AutoMapper?
第一步進入 Startup類 註冊AutoMapper服務!
public void ConfigureServices(IServiceCollection services) { //services.AddMvc(); core 3.0以前是這樣寫的,這個服務包括了TageHelper等 WebApi不需要的東西,所有3.0以後可以不這樣寫 services.AddControllers(); //註冊AutoMapper服務 services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies()); //配置介面服務:涉及到這個服務註冊的生命週期這裡採用AddScoped,表示每次的Http請求 services.AddScoped<ICompanyRepository, CompanyRepository>(); //獲取配置檔案中的資料庫字串連線 var sqlConnection = Configuration.GetConnectionString("SqlServerConnection"); //配置上下文類DbContext,因為它本身也是一套服務 services.AddDbContext<RoutineDbContext>(options => { options.UseSqlServer(sqlConnection); }); }
關於 AddAutoMapper() 方法,實際上它需要返回一個 程式集陣列,就是AutoMapper的執行配置檔案,那麼通過 GetAssemblies 去掃描AutoMapper下的所有配置檔案即可。
第二步:建立處理 AutoMapper 對映類
using AutoMapper; using Routine.Api.Entities; using Routine.Api.Models; namespace Routine.Api.Profiles { public class CompanyProfiles:Profile { public CompanyProfiles() { //新增對映關係,處理源型別與對映目標型別屬性名稱不一致的問題 //引數一:源型別,引數二:目標對映型別 CreateMap<Company, CompanyDto>() .ForMember(target=>target.CompanyName, opt=> opt.MapFrom(src=>src.Name)); } } }
分析:通過CreateMap,對於引數一:源型別,引數二:目標對映型別。
關於 ForMember方法的作用,有時候你得考慮一個情況,前面已經說過,AutoMapper 是基於約定的物件到物件(Object-Object)的屬性對映器,如果所對映的屬性欄位不一致一定是無法對映成功的!
約定即屬性欄位與源型別屬性名稱須一致!!!但是你也可以處理這一情況的發生,通過lambda表示式,將目標對映型別和源型別關係重對映即可。
第三步:開始資料對映
先來看對映前的程式碼:通過集合迴圈賦值:
[HttpGet] //IActionResult定義了一些合約,它可以代表ActionResult返回的結果 public async Task<ActionResult<IEnumerable<CompanyDto>>> GetCompanies() { var companies =await _companyRepository.GetCompaniesAsync();//讀取出來的是List var companyDtos = new List<CompanyDto>(); foreach (var company in companies) { companyDtos.Add(new CompanyDto { Id = company.Id, Name = company.Name }); } return Ok(companyDtos); }
通過 AutoMapper對映:
[HttpGet] //IActionResult定義了一些合約,它可以代表ActionResult返回的結果 public async Task<ActionResult<IEnumerable<CompanyDto>>> GetCompanies() { var companies =await _companyRepository.GetCompaniesAsync();//讀取出來的是List var companyDtos = _mapper.Map<IEnumerable<CompanyDto>>(companies); return Ok(companyDtos); }
分析:Map()方法處理需要返回的目標對映型別,然後帶入源型別。
關於獲取父子關係的資源:
所謂 父:Conmpany(公司)、子:Employees(員工)
可能你注意到了基本上就是主從表的引用關係
那麼我們在設計AP uri 的時候也需要考慮到這一點
需求案例 1:查詢某一公司下的所有員工資訊
分析:設計到員工資訊,也需要需要實現 Entity Model 對 EmployeeDtos 的轉換,所以需要建立 EmployeeDto
對比 Employee 的 Entity Model和EmployeeDto
Entity Model 程式碼:
using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; namespace Routine.Api.Entities { /// <summary> /// 員工 /// </summary> public class Employee { public Guid Id { get; set; } //公司外來鍵 public Guid CompanyId { get; set; } //公司表導航屬性 public Company Company { get; set; } public string EmployeeNo { get; set; } public string FirstName { get; set; } public string LastName { get; set; } //性別列舉 public Gender Gender { get; set; } public DateTime DateOfBirth { get; set; } } }
EmployeeDto 程式碼:
分析:對性別 Gender 列舉型別做了處理,改成了string型別,方便呼叫。另外對於姓名 Name 也是將 FirstName 和 LastName合併,年齡 Age 改成了 int型別
那麼,這些改動我們都需要在 EmployeeProfile類中在對映時進行標註,不然由於物件屬性對映器的約定,無法進行對映!!!
using System; namespace Routine.Api.Models { public class EmployeeDto { public Guid Id { get; set; } public Guid CompanyId { get; set; } public string EmployeeNo { get; set; } public string Name { get; set; } public string GenderDispaly { get; set; } public int Age { get; set; } } }
EmployeeProfile類程式碼:
邏輯和 CompanyProfile類的對映是一樣的
using AutoMapper; using Routine.Api.Entities; using Routine.Api.Models; using System; namespace Routine.Api.Profiles { public class EmployeeProfile:Profile { public EmployeeProfile() { CreateMap<Employee, EmployeeDto>() .ForMember(target => target.Name, opt => opt.MapFrom(src => $"{src.FirstName} {src.LastName}")) .ForMember(target=>target.GenderDispaly, opt=>opt.MapFrom(src=>src.Gender.ToString())) .ForMember(target=>target.Age, opt=>opt.MapFrom(src=>DateTime.Now.Year-src.DateOfBirth.Year)); } } }
接下來開始建立 EmployeeController 控制器,來通過對映器實現對映關係
EmployeeController :
需要注意 uir 的設計,我們查詢的是某一個公司下的所有員工資訊,所以也需要是 Entity Model 對 EmployeeDtos的轉換,同樣是藉助 物件屬性對映器。
using AutoMapper; using Microsoft.AspNetCore.Mvc; using Routine.Api.Models; using Routine.Api.Service; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; namespace Routine.Api.Controllers { [ApiController] [Route("api/companies/{companyId}/employees")] public class EmployeesController:ControllerBase { private readonly IMapper _mapper; private readonly ICompanyRepository _companyRepository; public EmployeesController(IMapper mapper, ICompanyRepository companyRepository) { _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); _companyRepository = companyRepository ?? throw new ArgumentNullException(nameof(companyRepository)); } [HttpGet] public async Task<ActionResult<IEnumerable<EmployeeDto>>> GetEmployeesForCompany(Guid companyId) { if (! await _companyRepository.CompanyExistsAsync(companyId)) { return NotFound(); } var employees =await _companyRepository.GetEmployeesAsync(companyId); var employeeDtos = _mapper.Map<IEnumerable<EmployeeDto>>(employees); return Ok(employeeDtos); } } }
介面測試(某一公司下的所有員工資訊):
需求案例 2:查詢某一公司下的某一員工資訊
來想想相比需求案例1哪些地方需要進行改動的?
既然是某一個員工,說明 uir 需要加個員工的引數 Id進去。
還有除了判斷該公司是否存在,還需要判斷該員工是否存在。
另外,既然是某一個員工,所以返回的應該是個物件而非IEnumable集合。
程式碼:
[HttpGet("{employeeId}")] public async Task<ActionResult<EmployeeDto>> GetEmployeeForCompany(Guid companyId,Guid employeeId) { //判斷公司存不存在 if (!await _companyRepository.CompanyExistsAsync(companyId)) { return NotFound(); } //判斷員工存不存在 var employee = await _companyRepository.GetEmployeeAsync(companyId, employeeId); if (employee==null) { return NotFound(); } //對映到 Dto var employeeDto = _mapper.Map<EmployeeDto>(employee); return Ok(employeeDto); }
介面測試(某一公司下的某一員工資訊):
可以看到測試成功!
關於故障處理:
這裡的“故障”主要是指伺服器故障或者是丟擲異常的故障,ASP.NET Core 對於 伺服器故障一般會引發 500 狀態碼錯誤,對於這種錯誤,會導致一種後果就是在出現故障後
故障資訊會將程式異常細節顯示出來,這就對API消費者不夠友好,而且也造成一定的安全隱患。但此後果是在開發環境下產生也就是Development。
當然ASP.NET Core開發團隊也意識到了這種問題!
偽造程式異常:
引發異常後接口測試:
可以看到此異常已經暴露了程式細節給 API 消費者 ,這種做法欠妥。
怎麼辦呢? 試試改一下開發的環境狀態!
重新測試介面:
問題解決!
但是你可能想根據這些異常丟擲一些自定義的資訊給 API 消費者 實際上也可以。
回到 Stratup 類:新增一箇中間件 app.UseExceptionHandler即可
分析:意思是如果有未處理的異常發生的時候就會走 else 裡面的程式碼,實際專案中這一塊需要記錄一下日誌
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } else { app.UseExceptionHandler(appBulider => { appBulider.Run(async context => { context.Response.StatusCode = 500 await context.Response.WriteAsync("The program Error!"); }); }); } app.UseRouting(); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); }
再來測試一下介面是否成功返回自定義異常資訊:
測試成功!!!