使用 .NET Core 3.x 構建RESTful Api(第三部分)


關於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的六種方式:

  1. [FromBody],請求的Body
  2. [FromFrom],請求的Body中的form數據
  3. [FromHeader],請求的Header
  4. [FromQuery],QueryString 參數
  5. [FromRoute],當前請求中的路由參數
  6. [FromService],當做Action參數而注入的服務

默認情況下,ASP .NET Core會使用 Complex Object Model Binder,它會把數據從 Value Provides 那里提取出來

而 Value Provides的順序是定義好的!

但是,我們在構建 Api 時,通常會使用 [ApiController] 這個 特性類,目的就是為了更好的構建 RESTful Api。

更改后:

  1. [FromBody],通常是用來推斷復雜類型參數,例如Post方式提交的數據
  2. [FromFrom],通常是用來推斷IFormFile和IFormFileCollection類型的Action參數,例如用來上傳單個或多個文件
  3. [FromRoute],用來推斷 Action的參數名和路由模板中的參數名一致的情況下
  4. [FromQuery],用來推斷其他的Action參數

關於過濾和搜索:

過濾:

實際上這兩者在實際的業務中通常應該是搭配使用的。

所謂過濾:就是過濾集合的一是,根據條件返回限定的集合數據

需求案例: 返回所有類型為國有企業的歐洲公司

分析:過濾條件自然是“國有企業”和“歐洲公司”

那么 uri 的設計就會是:GET  api/companies?Type=State-owned&regoin=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,測試成功!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM