
如何快速實現REST API集成以優化業務流程
{
//services.AddMvc(); core 3.0以前是這樣寫的,這個服務包括了 TageHelper等 WebApi不需要的東西,所有3.0以后可以不這樣寫
services.AddControllers();
}
注意配置中間件的區域管道順序不能隨意改動。
管道就是客戶端通過一些指令指向服務器端,在這個過程中呢,會經過一些手動配置的中間件,比如說路由中間件、靜態資源中間件等,從客戶端出發到服務器端,將數據處理后,再由服務器端原路返回到客戶端這樣的一個過程。但是在請求的過程中也不排除中間件出現短路的情況,這樣也就不會進入到第二個中間件了,而是直接返回到客戶端。
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
API對外合約:API消費者需要使用到三個概念
API對外提供統一資源接口,業界對RESTful資源命名也有規則
使用名詞而不是動詞
要體現資源的結構/關系
通過id獲取單個用戶應該是:api/user/{userId},而不是 api/user/users。這樣寫就是讓API具有很好的可讀性和可預測性
需求案例1
系統存在兩個資源:Company(公司)、Employee(員工),現在需要獲取某個公司下的所有員工
分析:應該使用HTTP GET。API在設計的時候需要體現公司與員工的一個包含關系
常見錯誤做法:api/employees,api/employee/{companyId} 。這兩個URI都沒有體現公司和員工的一個包含關系
建議做法:api/companies/{companyId}/employees
需求案例2
需要獲取某個公司下的某個員工
常見錯誤做法:api/employees/{employeeId}
建議做法:api/companies/{companyId}/employees/{employeeId}
自定義查詢怎么命名?
需求:獲取所有用戶信息,并且按年齡從大到小排序
常見錯誤做法:api/user/orderby/age
建議做法:api/user?orderby=age (通過QueryString查詢字符串,多條件使用 & 符號)
HTTP狀態碼
請求是否成功?如果請求失敗了,誰來為此負責
2xx 開頭狀態碼
3xx 開頭狀態碼
用于跳轉。例如告訴瀏覽器搜索引擎,某個頁面的網址已經永久改變,絕大多數的WebApi都不需要這類的狀態碼
4xx 開頭:客戶端錯誤
5xx 開頭狀態碼
500 – Internal serever error,表示服務器出現了錯誤,客戶端無能為力,只能以后再試試
還有就是RESTful API 返回的結果不一定Json格式的
先看控制器代碼:
using Microsoft.AspNetCore.Mvc;
using Routine.Api.Service;
using System;
using System.Threading.Tasks;
namespace Routine.Api.Controllers
{
[ApiController] //好處:ApiController不是強制的
//1.會啟用使用屬性路由(Attribute Routing)
//2.自動HTTP 400響應
//3.推斷參數的綁定源
//4.Multipart/form-data 請求推斷
//5.錯誤狀態代碼的問題詳細信息
[Route("api/companies")] //寫法一
//[Route("api/[controller]")] //寫法二:意思是相當于刨除了Controller后綴,獲取前面的 Companies C可以是小寫,如果你改名了那么你路由的uri也跟著變了(不建議這樣寫)
public class CompaniesController:ControllerBase
{
private readonly ICompanyRepository _companyRepository;
public CompaniesController(ICompanyRepository companyRepository)
{
_companyRepository = companyRepository ??
throw new ArgumentNullException(nameof(companyRepository));
}
[HttpGet]
//IActionResult定義了一些合約,它可以代表ActionResult返回的結果
public async Task<IActionResult> GetCompanies()
{
var companies =await _companyRepository.GetCompaniesAsync();//讀取出來的是List
return Ok(companies);
}
[HttpGet("{companyId}")] // Controller標注了ApiController => uri=> api/companies/{companyId}
public async Task<IActionResult> GetCompany(Guid companyId)
{
//判斷該公司是否存在方法一:這種方法在處理并發請求時可能會出現錯誤,原因是查到之后,進行刪除,進入company后也可能是404找不到了
//var exists =await _companyRepository.CompanyExistsAsync(compamyId);
//if (!exists)
//{
// //不存在應該返回404
// return NotFound();
//}
var company = await _companyRepository.GetCompanyAsync(companyId);//讀取出來的是List
//方法二
if (company==null)
{
return NotFound();
}
return Ok(company);
}
}
}
為了更好的構建RESTful API 對于 uri 的設計規則也有很嚴格的要求。
在控制器標注 ApiController,它會自動啟用路由屬性
通過 [Route] 設計路由規則
比如:接口一:GetCompanies,請求的方式:GET,通過Route 去設置路由規則 [Router(“api/companies”)],即查詢所有公司信息
[Router(“api/companies”)] => api/companies
接口二:GetCompany,請求方式:GET,只不過在 添加了 [HTTPGET(”{companyId}”)] =>api/companies/{companyId},即查詢某一公司的信息
關于第二種路由寫法請看注釋
通過Postman工具測試一下
測試一:接口一
測試二:接口二
以上兩個接口測試完畢?。。?/p>
對于ASP.NET Core 3.x以前對于 404 NotFound請求狀態碼輸出的格式不太友好,而ASP.NET Core 3.x對于404請求狀態碼也做了友好的提示。
現在將接口偽造錯誤信息,提示 404 如圖:
關于構建 RESTful API 存在的內容協商
所謂內容協商就是這樣一個過程,針對一個響應,當有多種表述格式可用時,選取最佳的一種表述格式,這些表述可以是XML,JSON,甚至是自定義的格式規則
Accept Header:負責指定輸出類型
Media Type(媒體類型)
404 Not Acceptable
輸出格式:ASP.NET Core 里面對應的就是 Output Formatters 我們稱為:輸出類型的格式化器
也就是說如果一個API消費者,設置了Accept Header的媒體類型為Json,那么這個RESTful API也應該返回的是JSON,
但是呢如果服務器只接收XML的格式,這個時候請求的媒體類型不被服務器所接受,那么就會返回 406 這個狀態碼
總而言之,盡量避免不寫Accept Header,避免客戶端和服務器端接收和返回的類型不一致導致錯誤。
有輸出那么就會有輸入了?。。?/p>
Content-Type-Header:負責指定輸入
Media Type(媒體類型)
輸出格式:ASP.NET Core里面對應的就是 Input Formatters
比如說:對于一個客戶端的POST請求,即添加資源信息,那么就需要輸入參數,這些參數可能是放在Body里面,那么在Body里面的這些參數可能是對象的那種格式。
那么我們就需要通過 Content-Type-Header來確定Body里面的參數是什么樣的類型,可能是Json也可能是Xml或者是自定義的格式,指明之后,RESTful API才能更好的對這些參數進行處理。
看Startup類代碼:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Routine.Api.Data;
using Routine.Api.Service;
namespace Routine.Api
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
//services.AddMvc(); core 3.0以前是這樣寫的,這個服務包括了TageHelper等 WebApi不需要的東西,所有3.0以后可以不這樣寫
services.AddControllers(setup =>
{
//setup.ReturnHttpNotAcceptable=false;//如果客戶端默認為xml格式,服務器端為json,false就不會返回406
setup.ReturnHttpNotAcceptable = true;//如果請求的類型和服務器請求的類型不一致就返回406
//setup.OutputFormatters.Add(new XmlDataContractSerializerOutputFormatter());
//setup.OutputFormatters.Insert(0, new XmlDataContractSerializerOutputFormatter());
}).AddXmlDataContractSerializerFormatters();
//配置接口服務:涉及到這個服務注冊的生命周期這里采用AddScoped,表示每次的Http請求
services.AddScoped<ICompanyRepository, CompanyRepository>();
//獲取配置文件中的數據庫字符串連接
var sqlConnection = Configuration.GetConnectionString("SqlServerConnection");
//配置上下文類DbContext,因為它本身也是一套服務
services.AddDbContext<RoutineDbContext>(options =>
{
options.UseSqlServer(sqlConnection);
});
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
}
setup.ReturnHttpNotAcceptable就是處理是在客戶端與服務器端數據產生沖突時,是否要即將產生 406 的狀態碼。
setup.OutputFormatters.Add(new XmlDataContractSerializerOutputFormatter())
分析:實際上OutputFormatters 是一個集合 ,通過Add方法添加服務器允許接受XML格式的數據功能。因為集合中默認只有Json
setup.OutputFormatters.Insert(0,new XmlDataContractSerializerOutputFormatter())
分析:實際上剛剛寫的是一種方法。Insert就是指明格式順序,默認是JSON,通過Insert設置 0 ,就是指明XML為默認接受的數據格式
實際上以上兩種寫法都是 ASP.NET Core 3.x以前的寫法。
ASP.NET Core 3.x的實際寫法:就是在AddControllers后面添加XmlDataContractSerializerOutputFormatter方法。這樣不管是輸入輸出都已經設置好了XML的格式數據
postman接口測試:取消setup.OutputFormatters.Insert(0,new XmlDataContractSerializerOutputFormatter())的注釋
默認xml:
最后,關于構建RESTFUL Api的URI規則及原理? ?
關于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,相當于把第一個也一并下載了。
第一步進入 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類中在映射時進行標注,不然由于對象屬性映射器的約定,無法進行映射?。。?/p>
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();
});
}
再來測試一下接口是否成功返回自定義異常信息:
測試成功?。。?/p>
文章轉自微信公眾號@DotNet