使用 .NET Core 3.x 構建 RESTFUL Api (續)

關於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();
            });
        }

再來測試一下接口是否成功返回自定義異常信息:

 

測試成功!!!

相關文章
相關標籤/搜索