zoukankan      html  css  js  c++  java
  • 使用 .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; }
            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();
                });
            }

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

    测试成功!!!

  • 相关阅读:
    使用Cloud application Studio在C4C UI里创建下拉列表(dropdown list)
    如何使用Kubernetes里的NetworkPolicy
    SpringBoot应用和PostgreSQL数据库部署到Kubernetes上的一个例子
    Kubernetes API server工作原理
    Kubernetes Helm入门指南
    两张图弄懂函数的递归(以golang为例)
    (十四)golang--函数和包
    【自然语言处理(三)】主题模型
    【自然语言处理】使用朴素贝叶斯进行语种检测
    【自然语言处理】利用朴素贝叶斯进行新闻分类(自己处理数据)
  • 原文地址:https://www.cnblogs.com/hcyesdo/p/13470022.html
Copyright © 2011-2022 走看看