1. 程式人生 > 實用技巧 >使用 .NET Core 3.x 構建 RESTFUL Api (續)

使用 .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();
            });
        }

再來測試一下介面是否成功返回自定義異常資訊:

測試成功!!!