关于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; }
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 =
await context.Response.WriteAsync("The program Error!");
});
});
}
app.UseRouting(); app.UseAuthorization(); app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}

再来测试一下接口是否成功返回自定义异常信息:

测试成功!!!

使用 .NET Core 3.x 构建 RESTFUL Api (续)的更多相关文章

  1. 使用ASP.NET Core 3.x 构建 RESTful API - 2. 什么是RESTful API

    1. 使用ASP.NET Core 3.x 构建 RESTful API - 1.准备工作 什么是REST REST一词最早是在2000年,由Roy Fielding在他的博士论文<Archit ...

  2. 使用 .NET Core 3.x 构建 RESTFUL Api

    准备工作:在此之前你需要了解关于.NET .Core的基础,前面几篇文章已经介绍:https://www.cnblogs.com/hcyesdo/p/12834345.html 首先需要明确一点的就是 ...

  3. 使用ASP.NET Core 3.x 构建 RESTful API - 1.准备工作

    以前写过ASP.NET Core 2.x的REST API文章,今年再更新一下到3.0版本. 先决条件 我在B站有一个非常入门的ASP.NET Core 3.0的视频教程,如果您对ASP.NET Co ...

  4. 使用 .NET Core 3.x 构建RESTful Api(第三部分)

    关于HTTP HEAD 和 HTTP GET: 从执行性能来说,这两种其实并没有什么区别.最大的不同就是对于HTTP HEAD 来说,Api消费者请求接口数据时,如果是通过HTTP HEAD的方式去请 ...

  5. 使用ASP.NET Core 3.x 构建 RESTful API - 3.4 内容协商

    现在,当谈论起 RESTful Web API 的时候,人们总会想到 JSON.但是实际上,JSON 和 RESTful API 没有半毛钱关系,只不过 JSON 恰好是RESTful API 结果的 ...

  6. 使用ASP.NET Core 3.x 构建 RESTful API - 3.1 资源命名

    之前讲了RESTful API的统一资源接口这个约束,里面提到了资源是通过URI来进行识别的,每个资源都有自己的URI.URI里还涉及到资源的名称,而针对资源的名称却没有一个标准来进行规范,但是业界还 ...

  7. 使用ASP.NET Core 3.x 构建 RESTful API - 4.2 过滤和搜索

    向Web API传递参数 数据可以通过多种方式来传给API. Binding Source Attributes 会告诉 Model 的绑定引擎从哪里找到绑定源. 共有以下六种 Binding Sou ...

  8. 使用ASP.NET Core 3.x 构建 RESTful API - 3.2 路由和HTTP方法

    ASP.NET Core 3.x 的路由 路由机制会把一个请求的URI映射到一个Controller上面的Action,所以当你发送一个HTTP请求的时候,MVC框架会解析这个请求的URI,并尝试着把 ...

  9. 使用ASP.NET Core 3.x 构建 RESTful API - 3.3 状态码、错误/故障、ProblemDetails

    HTTP状态码 HTTP状态码会告诉API的消费者以下事情: 请求是否执行成功了 如果请求失败了,那么谁为它负责 HTTP的状态码有很多,但是Web API不一定需要支持所有的状态码.HTTP状态码一 ...

  10. 使用ASP.NET Core 3.x 构建 RESTful API - 5.1 输入验证

    说到验证,那就需要做三件事: 定义验证规则 按验证规则进行检查 报告验证的错误.在把错误报告给API消费者的时候,报告里并不包含到底是服务端还是API消费者引起的错误,这是状态码的工作.而通常响应的B ...

随机推荐

  1. Python Ethical Hacking - MODIFYING DATA IN HTTP LAYER(2)

    MODIFYING DATA IN HTTP LAYER Edit requests/responses. Replace download requests. Inject code(html/Ja ...

  2. 查看锁信息 v$lock 和 v$locked_object

    查看锁住的对象及会话id,serial# select a.*  from (SELECT o.object_name,               l.locked_mode,            ...

  3. 原来python还可以这样处理文件

    首先我为大家介绍一下python语言吧! Python 是一个高层次的结合了解释性.编译性.互动性和面向对象的脚本语言. Python 的设计具有很强的可读性,相比其他语言经常使用英文关键字,其他语言 ...

  4. spring tx——TransactionManger

    TransactionDefinition--事务定义 定义事务属性,包括传播级别.隔离级别.名称.超时.只读等 TransactionStatus--事务状态 事务状态,包含事务对象(jdbc为Da ...

  5. MySQL数据管理

    3.MySQL数据管理 3.1外键 方式一:  create table `grade`(  `gradeid` int(10) not null auto_increment comment '年纪 ...

  6. SpringBoot + Spring Cloud Eureka 服务注册与发现

    什么是Spring Cloud Eureka Eureka是Netflix公司开发的开源服务注册发现组件,服务发现可以说是微服务开发的核心功能了,微服务部署后一定要有服务注册和发现的能力,Eureka ...

  7. sed 指定行范围匹配

    sed -n '5,10{/pattern/p}' file sed是一个非交互性性文本编辑器,它编辑文件或标准输入 导出的文件拷贝.标准输入可能是来自键盘.文件重定向.字符串或变量,或者是一个管道文 ...

  8. 【laravel】基于jwt实现用户认证

    安装及基础配置 使用 composer 安装 # 建议使用1.0以上版本 composer require tymon/jwt-auth .*@rc 进行一些配置 有些文档会说要添加 Tymon\JW ...

  9. webpack 4.x版本手动配置

    运行 npm init -y 快速初始化项目 在项目根目录创建src源代码目录和dist产品目录 在src目录下创建 index.html mani.js文件如果后期使用entry打包,这里可以手动创 ...

  10. Numpy创建数组

    # 导入numpy 并赋予别名 np import numpy as np # 创建数组的常用的几种方式(列表,元组,range,arange,linspace(创建的是等差数组),zeros(全为 ...