關於Entity Model vs 面向外部的Modelsql
Entity Framework Core 使用 Entity Model 用來表示數據庫裏面的記錄。數據庫
面向外部的Model 則表示要傳輸的東西,有時候被稱爲 Dto,有時候被稱爲 ViewModel。api
關於Dto,API消費者經過Dto,僅提供給用戶須要的數據起到隔離的做用,防止API消費者直接接觸到核心的Entity Model。數組
可能你會以爲有點多餘,可是仔細想一想你會發現,Dto的存在是頗有必要的。安全
Entity Model 與數據庫實際上應該是有種依賴的關係,數據庫某一項功能發生改變,Entity Model也應該會作出相應的動做,那麼這個時候 API消費者在請求服務器接口數據時,若是直接接觸到了 Entity Model數據,那麼它也就沒法預測究竟是哪一項功能作出了改變。這個時候可能在作 API 請求的時候發生不可預估的錯誤。Dto的存在必定程度上解決了這一問題。服務器
那麼它的做用是?app
編寫Company的 Dto:async
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:ide
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; } public ICollection<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(); }); }
再來測試一下接口是否成功返回自定義異常信息:
測試成功!!!