关于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(); }); }
再来测试一下接口是否成功返回自定义异常信息:
测试成功!!!