關於HTTP HEAD 和 HTTP GET:
從執行效能來說,這兩種其實並沒有什麼區別。最大的不同就是對於HTTP HEAD 來說,Api消費者請求介面資料時,如果是通過HTTP HEAD的方式去請求,
應該是不會把 Body返回回去的。那麼它會返回什麼呢? 比如說,Headers的一些響應頭資料,例如Content-Type的一些資源資訊。而HTTP GET是會將
Body裡面的資料返回的。因此,可以通過HTTP HEAD去檢測該 Api 是否存在資源,換一種說法就是該 Api 是否可用。
關於如何給 Api 傳遞資料:
資料可以通過多種方式傳遞給 Api!
事實上,Bind Source Attribute 會告訴 Model 的繫結引擎從哪裡去找到繫結。
Bind Source Attribute的六種方式:
- [FromBody],請求的Body
- [FromFrom],請求的Body中的form資料
- [FromHeader],請求的Header
- [FromQuery],QueryString 引數
- [FromRoute],當前請求中的路由引數
- [FromService],當做Action引數而注入的服務
預設情況下,ASP .NET Core會使用 Complex Object Model Binder,它會把資料從 Value Provides 那裡提取出來
而 Value Provides的順序是定義好的!
但是,我們在構建 Api 時,通常會使用 [ApiController] 這個 特性類,目的就是為了更好的構建 RESTful Api。
更改後:
- [FromBody],通常是用來推斷複雜型別引數,例如Post方式提交的資料
- [FromFrom],通常是用來推斷IFormFile和IFormFileCollection型別的Action引數,例如用來上傳單個或多個檔案
- [FromRoute],用來推斷 Action的引數名和路由模板中的引數名一致的情況下
- [FromQuery],用來推斷其他的Action引數
關於過濾和搜尋:
過濾:
實際上這兩者在實際的業務中通常應該是搭配使用的。
所謂過濾:就是過濾集合的一是,根據條件返回限定的集合資料
需求案例: 返回所有型別為國有企業的歐洲公司
分析:過濾條件自然是“國有企業”和“歐洲公司”
那麼 uri 的設計就會是:GET api/companies?Type=State-owned®oin=Europe
所以過濾就是:我們把某個欄位的名字和與之匹配的值一起傳遞給 Api ,並將這些以集合的方式返回
搜尋:
搜尋實際上超出了過濾的範圍,針對搜尋我們通常不會把要搜尋的欄位傳遞過去,而是隻把要搜尋的值傳遞給 Api,
然後 Api 自行決定應該對哪些欄位來查詢該值,一般是全文搜尋
例如:api/companies?q=xxx
如果還不理解?
過濾:根據條件,將某一集合的資料按條件進行移除或選擇
搜尋:可以是空集合,根據要搜尋的值,將資料新增到集合中,再返回
注意:過濾和搜尋這些引數並不是資源的一部分。
案例程式碼:過濾員工性別(引數: genderDisplay)、搜尋匹配資料(引數:q)
實現類處理業務邏輯:
分析:
首先第二個if判斷,如果都為空,那麼就是返回全部資料,什麼也沒發生。
第二個if判斷性別引數是否為空,如果不是,那麼就編寫過濾性別的程式碼,在這之前將定義的items就是查詢到該公司下的所有員工然後在處理其他事件。
第三個if判斷搜尋的值是否為空,如果不是,就編寫模糊查詢的程式碼,這裡是多欄位模糊查詢
以上if執行完畢後,實際上並沒有生成一個完整的 SQL 語句,實際上這樣做就是為了效能,最後才通過 ToList返回集合,至於你是過濾還是搜尋都無所謂!
public async Task<IEnumerable<Employee>> GetEmployeesAsync(Guid companyId, string genderDisplay, string q) { if (companyId==Guid.Empty) { throw new ArgumentNullException(nameof(companyId)); } if (string.IsNullOrWhiteSpace(genderDisplay) || string.IsNullOrWhiteSpace(q)) { return await _context.Employees .Where(x => x.CompanyId == companyId) .OrderBy(x => x.EmployeeNo) .ToListAsync(); } var items = _context.Employees.Where(x => x.CompanyId == companyId); if (!string.IsNullOrWhiteSpace(genderDisplay)) { genderDisplay = genderDisplay.Trim(); var gender = Enum.Parse<Gender>(genderDisplay); items = items.Where(x => x.Gender == gender); } if (!string.IsNullOrWhiteSpace(q)) { q = q.Trim(); items = items.Where(x => x.EmployeeNo.Contains(q) || x.FirstName.Contains(q) || x.LastName.Contains(q)); } return await items .OrderBy(x => x.EmployeeNo) .ToListAsync(); }
控制器呼叫:
通過[FromQuery]的Name來指定引數匹配的名稱是什麼,比如:gender或者是genderDisplay
public async Task<ActionResult<IEnumerable<EmployeeDto>>> GetEmployeesForCompany(Guid companyId,[FromQuery(Name = "gender")] string genderDisplay,string q) { if (! await _companyRepository.CompanyExistsAsync(companyId)) { return NotFound(); } var employees =await _companyRepository.GetEmployeesAsync(companyId, genderDisplay,q); var employeeDtos = _mapper.Map<IEnumerable<EmployeeDto>>(employees); return Ok(employeeDtos); }
介面測試:
還需要考慮一種情況:
在實際業務當中呢,這種搜尋過濾的條件肯定不止一兩個,一般是多個屬性進行搜尋或者過濾,這個時候,如果也按照查詢字串的方式傳遞給 Api ,那麼就會顯得非常的複雜也很容易寫錯。
那怎麼辦呢?
很簡單,其實只需要寫一個對應的類就好了,把需要查詢的欄位屬性全部放到類裡面。
這樣就算後期想再增加條件屬性只需要編寫類裡面的程式碼,無需在,Api 介面中在去增加引數。
新增一個CompanyParameters類:
分析:分別定義公司名稱屬性欄位和全文搜尋屬性欄位
namespace Routine.Api.ResoureParameters { public class CompanyDtoParameters { public string CompanyName { get; set; } public string SearchTerm { get; set; } } }
業務邏輯類:
public async Task<IEnumerable<Company>> GetCompaniesAsync(CompanyDtoParameters companyParameters) { if (companyParameters==null) { throw new ArgumentNullException(nameof(companyParameters)); } if (string.IsNullOrWhiteSpace(companyParameters.CompanyName) && string.IsNullOrWhiteSpace(companyParameters.SearchTerm)) { return await _context.Companies.ToListAsync(); } var queryableCompany = _context.Companies as IQueryable<Company>; if (!string.IsNullOrWhiteSpace(companyParameters.CompanyName)) { companyParameters.CompanyName = companyParameters.CompanyName.Trim(); queryableCompany = queryableCompany.Where(x => x.Name == companyParameters.CompanyName); } if (!string.IsNullOrWhiteSpace(companyParameters.SearchTerm)) { companyParameters.SearchTerm = companyParameters.SearchTerm.Trim(); queryableCompany = queryableCompany.Where(x => x.Name.Contains(companyParameters.SearchTerm) || x.Introduction.Contains(companyParameters.SearchTerm)); } return await queryableCompany.ToListAsync(); }
控制器呼叫:
注意:需要加上[FromQuery]標記,不然會出現狀態碼為 415 ,也就是不支援的媒體型別(MediaType)
分析:此時方法的引數是一個類,就相當於它是一個複雜的資料型別,這個時候請求 Api 的時候它可能會認為繫結源是來自於QueryString查詢字串。
所以我們需要手動指定一些繫結源。
[HttpGet] public async Task<ActionResult<IEnumerable<CompanyDto>>> GetCompanies([FromQuery]CompanyDtoParameters companyDtoParameters) { var companies =await _companyRepository.GetCompaniesAsync(companyDtoParameters); var companyDtos = _mapper.Map<IEnumerable<CompanyDto>>(companies); return Ok(companyDtos); }
重新測試介面:
介面測試成功!
關於HTTP 方法的安全性與冪等性:
安全性是指方法執行後並不會改變資源的表述,例如 GET 它只是查詢獲取資源,它並不會改變資源的表述,所以它是安全的。
冪等性是指方法無論執行多少次都會返回得到同樣的結果,例如對資源進行修改,而修改的內容是一樣的,所以無論修改多少次得到的結果也是一樣的,例如 HTTP PUT 就是冪等的。
案例1:如何建立 POST 父資源(Company):
在建立資源請求之前,首先要明確一個理念,那就是建立資源的 DTO 是否要和 GET 請求查詢的 Dto 的屬性欄位
內容一致呢?
答案是不應該將POST,GET請求使用同一個 Dto 。
其實原因很簡單,仔細想想,其實POST Action方法請求的資源大部分業務情況與 GET Action 請求的資源情況是不一樣的
儘管有時候可能作為查詢的Dto屬性和作為建立資源的POST Dto屬性一樣。這個時候也應該將它們分開使用。
因為在未來業務處理中 DTO 中的屬性可能隨時都在發生改變。
所以,這樣分開寫 DTO 的好處就是方便後期的重構。
簡單點來說就是針對 查詢、建立、更新三大類小塊我們都應該使用不同的 DTO。
對 Company 這個 Entity Model做一下 POST 建立資源的請求:
建立 CompanyAddDto類:
using System; namespace Routine.Api.Models { public class CompanyAddDto { public string Name { get; set; } public string Introduction { get; set; } } }
對比一下GET Action 請求的 Dto ,即 CompanyDto:
實際上兩者一般來說屬性很可能根據業務情況不一樣!!!
using System; namespace Routine.Api.Models { public class CompanyDto { public Guid Id { get; set; } public string CompanyName { get; set; } } }
注意,別忘記新增 mapper 對映關係了,這裡就是從 CompanyAddDto 對映到 Employee(Entity Model),因為是新增到資料庫裡面。
因為CompanyDto和Company屬性欄位並沒有什麼改變,所以不需要對專門的欄位進行配置
using AutoMapper; using Routine.Api.Entities; using Routine.Api.Models; namespace Routine.Api.Profiles { public class CompanyProfiles:Profile { public CompanyProfiles() { CreateMap<CompanyAddDto, Company>(); } } }
在控制器新增 POST 請求的方法:
需要注意在資源新增後還需要重新對映回 GET 請求查詢資源的 Dto!
[HttpPost] public async Task<ActionResult<CompanyDto>> CreateCompany(CompanyAddDto company) //如果 companyAddDto為空,ASP.NET Core會自動返回 400錯誤,這是因為 [ApiController] Attribute的作用 { //需要將資源對映到 EntityModel var entity = _mapper.Map<Company>(company); _companyRepository.AddCompany(entity); await _companyRepository.SaveAsync(); //此時新增完成後,返回出去的還是Dto,所有還需要進行一次對映 var returnDto = _mapper.Map<CompanyDto>(entity); //CreatedAtRoute,會返回一些響應頭的資源執行我們返回帶著一個地址的head,而這個head含有一個uri,例如 201 表示新增成功,還有就是 uri,通過這個uri可以找到這個新建立的資源 //引數1:生成uri名稱,與返回的GET方法名一樣,引數2:路由值,引數3:物件值 return CreatedAtRoute(nameof(GetCompany), new { companyId= returnDto.Id}, returnDto); }
關於返回的 CreatedAtRoute方法,註釋標註了作用和對應的引數,第一個引數 GetCompany 對應的就是GET Action標註的路由名稱,如下:
接下來進行 POST 請求的介面測試,開啟 Postman 工具。
返回狀態碼 201 表示 Post 成功!
再看看Headers裡面給我們帶回了什麼資訊:
這實際上就是返回 CreatedAtRoute 方法的作用,會帶著剛剛新增的資源的 uri 地址
案例2:如何建立 POST 子資源(Employee):
建立子資源其實和建立父資源差不多。
同樣新增 EmployeeAddDto 類:
using System; using Routine.Api.Entities; namespace Routine.Api.Models { public class EmployeeAddDto { 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:
可以看出這兩個類的區別還是很大的
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; } } }
在控制器新增 POST 請求的方法:
需要注意的是,因為Employee作為子資源所以需要帶著 CompanyId 回去
[HttpPost] public async Task<ActionResult<EmployeeDto>> CreateEmployeeForCompany(Guid companyId, EmployeeAddDto employee) { if (!await _companyRepository.CompanyExistsAsync(companyId)) { return NotFound(); } var entity = _mapper.Map<Employee>(employee); _companyRepository.AddEmployee(companyId,entity); await _companyRepository.SaveAsync(); var dtoToReturn = _mapper.Map<EmployeeDto>(entity); return CreatedAtRoute(nameof(GetEmployeeForCompany), new { companyId, employeeId = dtoToReturn.Id }, dtoToReturn); }
接下來進行 POST 請求的介面測試,開啟 Postman 工具。
返回201,介面測試成功!
同樣來看看 Headers裡面返回的一些資源:
將剛剛新增的資源以 uri 形式返回。
案例3:同時建立父子資源:
業務需求:同時建立多個子資源 employee
需要在建立POST Action的Company方法上擴充套件一下就行了,在 CompanyAddDto中新增Employees屬性集合:
最好和Entity Model的 Employee中 Employee一樣,這樣就無效對這個屬性在對映的時候做配置了。
介面測試:
POST 成功 !
案例4:剛剛新增了多個子資源,那麼如何新增多個父資源呢?
這就需要重新寫一個 uri ,因為當前 api/companies uri 是針對於單個 Company的 Post 建立資源。
既然重新寫一個uri,那麼直接建立一個新的控制器,標註 Attribute [ApiController]為 api/companycollections
ConpanyCollectionController控制器程式碼:
編寫一個建構函式的依賴注入,分別注入 AutoMapper 以及 CompanyRepository 業務邏輯類
分析:既然是建立多個Company,那麼返回的也是一個 IEnumerable的Dto集合,引數也是一個為 IEnumerable的 CompanyAddDto集合,這沒有什麼問題。
然後迴圈新增資料就好了。在此之前還是一樣的,需要將 CompanyAddDto 對映到 Entity Model對應的Company 裡面!
using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using AutoMapper; using Microsoft.AspNetCore.Mvc; using Routine.Api.Entities; using Routine.Api.Models; using Routine.Api.Service; namespace Routine.Api.Controllers { [ApiController] [Route("api/companycollections")] public class CompanyCollectionsController:ControllerBase { private readonly IMapper _mapper; private readonly ICompanyRepository _companyRepository; public CompanyCollectionsController(IMapper mapper,ICompanyRepository companyRepository) { _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); _companyRepository = companyRepository ?? throw new ArgumentNullException(nameof(companyRepository)); } [HttpPost] public async Task<ActionResult<IEnumerable<CompanyDto>>> CreateCompanyCollection( IEnumerable<CompanyAddDto> companyCollection) { var companyEntities = _mapper.Map<IEnumerable<Company>>(companyCollection); foreach (var company in companyEntities) { _companyRepository.AddCompany(company); } await _companyRepository.SaveAsync(); return Ok(); } } }
這裡先測試是否會返回一個 Ok 200 的狀態碼
介面測試:
返回 200,測試成功!