從SpringBoot到DotNet_4.完善篇

Purearc發表於2024-08-16

image-20240413204305811

第一章 分頁

​ 在開發 RESTful API 時,分頁功能是非常常見的需求,尤其是在處理大量資料時。分頁不僅能提高查詢效率,還能改善使用者體驗,讓前端能夠逐步載入資料而不是一次性全部載入。

​ 在第一章中,我們將探討如何實現分頁功能,並且確保分頁引數透過查詢字串傳遞、設定合理的預設值和閾限值,並在資料庫中實現高效分頁。

image-20240413204157220

1.簡單分頁

​ 對引數進行改造,新增pageNumberpageSize兩個欄位,並設定預設值,同時為了效能保證,當pageSize過大的時候將會設定一個預設值。

using System.Text.RegularExpressions;
namespace FakeXiecheng.ResourceParameters;
public class TouristRouteResourceParamaters
{
    // 對於 Title 欄位的關鍵詞查詢
    public string? Keyword { get; set; }

    // 對於 Rating 欄位的篩選
    public string? RatingOperator { get; set; }
    public int? RatingValue { get; set; }

    private string _rating;
    public string? Rating
    {
        get { return _rating; }
        set
        {
            if (!string.IsNullOrWhiteSpace(value))
            {
                Regex regex = new Regex(@"([A-Za-z\-]+)(\d+)");
                Match match = regex.Match(value);
                if (match.Success)
                {
                    RatingOperator = match.Groups[1].Value;
                    RatingValue = Int32.Parse(match.Groups[2].Value);
                }
            }
            _rating = value;
        }
    }

    private int _pageNumber = 1;
    public int PageNumber 
    {
        get
        { return _pageNumber; }
        set
        {
            if (value >=1)
            {
                _pageNumber = value;
            } 
        }
    }
    private int _pageSize = 10;
    private int maxValue = 1;
    public int PageSize 
    {
        get { return _pageSize; }
        set
        {
            if (value >= 1) 
            { 
                _pageSize = (value > int.MaxValue) ? maxValue : value;
            }
        }
    }
}

​ 對於控制器,我們只需要在獲得引數的時候新增兩個TouristRouteResourceParamaters的屬性即可。

 [HttpGet]
 [HttpHead]
 // api/touristroutes?keyword=xxx
 public async Task<IActionResult> GetTouristRoutes([FromQuery] TouristRouteResourceParamaters paramaters)
 {
     var touristRoutesFromRepo =
         await _touristRouteRepository.GetTouristRoutesAsync(
             paramaters.Keyword, 
             paramaters.RatingOperator,
             paramaters.RatingValue,
             paramaters.PageNumber,
             paramaters.PageSize
             ); 
     if (touristRoutesFromRepo == null || touristRoutesFromRepo.Count() <= 0)
     {
         return NotFound("旅遊路線不存在");
     }
     var touristRoutesDto = _mapper.Map<IEnumerable<TouristRouteDto>>(touristRoutesFromRepo);
     return Ok(touristRoutesDto);
 }

​ 在進行分頁操作的時候大體分為三步:1、計算需要調過的資料;2、調過之前的資料確定起始位置;3、從起始位置選擇以pageSize為大小的資料。

   /// <summary>
   /// 獲得所有的資訊
   /// </summary>
   /// <returns></returns>
   public async Task<IEnumerable<TouristRoute>> GetTouristRoutesAsync(string keyword, string operatorType, int? ratingValue, int pageSize, int pageNumber)
   {
       IQueryable<TouristRoute> result = _dbContext.TouristRoutes.Include(t => t.TouristRoutePictures);
       if (!string.IsNullOrWhiteSpace(keyword))
       {
           result = result.Where(t => t.Title.Contains(keyword));
       }

       if (ratingValue >= 0)
       {
           result = operatorType switch
           {
               "largerThan" => result.Where(t => t.Rating >= ratingValue),
               "lessThan" => result.Where(t => t.Rating < ratingValue),
               "equals" => result.Where(t => t.Rating == ratingValue)
           };
       }
       // 分頁操作
       var skip = (pageNumber - 1) * pageSize;
       result = result.Skip(skip);
       result =result.Take(pageSize);  
       return await result.ToListAsync();
   }

image-20240807220340257

2.模組化分頁

​ 模組化分頁是一種將分頁邏輯封裝到一個獨立模組中的技術。這種方法有許多優點,尤其是在處理大量資料時,它可以顯著提高系統的效能和可維護性。在 PaginationList<T> 建構函式中,AddRange(items) 被用來將從資料庫查詢到的分頁資料新增到當前的 PaginationList<T> 物件中。

​ 下面這個類用於實現分頁功能。它繼承自 List<T>,並新增了兩個額外的屬性:CurrentPagePageSize,用來儲存當前頁碼和每頁的記錄數。

namespace FakeXiecheng.Helper
{
    public class PaginationList<T> : List<T>
    {
        public int CurrentPage { get; set; }
        public int PageSize { get; set; }

        public PaginationList(int currentPage, int pageSize, List<T> items)
        {
            CurrentPage = currentPage;
            PageSize = pageSize;
            AddRange(items);
        }

        public static async Task<PaginationList<T>> CreateAsync(
            int currentPage, int pageSize, IQueryable<T> result)
        {
            // pagination
            // skip
            var skip = (currentPage - 1) * pageSize;
            result = result.Skip(skip);
            // 以pagesize為標準顯示一定量的資料
            result = result.Take(pageSize);

            // include vs join
            var items = await result.ToListAsync();

            return new PaginationList<T>(currentPage, pageSize, items);
        }
    }

}

​ 在需要分頁的地方使用工廠創造出一個PaginationList例項。

public async Task<PaginationList<TouristRoute>> GetTouristRoutesAsync(string keyword, string operatorType, int? ratingValue, int pageNumber, int pageSize)
{
    IQueryable<TouristRoute> result = _dbContext.TouristRoutes.Include(t => t.TouristRoutePictures);
    if (!string.IsNullOrWhiteSpace(keyword))
    {
        result = result.Where(t => t.Title.Contains(keyword));
    }
    if (ratingValue >= 0)
    {
        result = operatorType switch
        {
            "largerThan" => result.Where(t => t.Rating >= ratingValue),
            "lessThan" => result.Where(t => t.Rating < ratingValue),
            "equals" => result.Where(t => t.Rating == ratingValue)
        };
    }
    return await PaginationList<TouristRoute>.CreateAsync(pageNumber, pageSize, result);
}

3.複用分頁模組

​ 對於當前的專案來說,將分頁程式碼模組化最大的好處就是一次編寫,到處可以使用:

namespace FakeXiecheng.ResourceParameters
{
    public class PaginationResourceParamaters
    {
        private int _pageNumber = 1;
        public int PageNumber
        {
            get
            {
                return _pageNumber;
            }
            set
            {
                if (value >= 1)
                {
                    _pageNumber = value;
                }
            }
        }
        private int _pageSize = 10;
        const int maxPageSize = 50;
        public int PageSize
        {
            get
            {
                return _pageSize;
            }
            set
            {
                if (value >= 1)
                {
                    _pageSize = (value > maxPageSize) ? maxPageSize : value;
                }
            }
        }

    }
}
 public async Task<IActionResult> GetTouristRoutes([FromQuery] TouristRouteResourceParamaters paramaters,
     [FromQuery] PaginationResourceParamaters paramaters2)

​ 以獲取訂單GetOrdersByUserId為例,這是個同樣要求有分頁的操作

    [HttpGet]
    [Authorize(AuthenticationSchemes = "Bearer")]
    public async Task<IActionResult> GetOrders([FromQuery] PaginationResourceParamaters paramaters2)
    {
        // 1. 獲得當前使用者
        var userId = _httpContextAccessor
            .HttpContext.User.FindFirst(ClaimTypes.NameIdentifier).Value;

        // 2. 使用使用者id來獲取訂單歷史記錄
        var orders = await _touristRouteRepository.GetOrdersByUserId(userId, paramaters2.PageNumber, paramaters2.PageSize);

        return Ok(_mapper.Map<IEnumerable<OrderDto>>(orders));
    }
 public async Task<PaginationList<Order>> GetOrdersByUserId(string userId, int pageSize, int pageNumber)
 {
     IQueryable<Order> result =  _dbContext.Orders.Where(o => o.UserId == userId);
     return await PaginationList<Order>.CreateAsync(pageNumber, pageSize, result);
 }

image-20240809180947177

4.分頁導航

​ PC端商品、資訊內容的列表頁面,通常會有個分頁的功能,透過翻頁等操作,使用者可以跳轉到其他頁面檢視新的內容。頁碼展示包括當前頁碼展示、當前頁碼相鄰幾個頁碼的展示以及首末頁頁碼展示。

image-20240809233018149

​ 頁碼展示幫助使用者定位內容:例如使用者在某個商品的搜尋結果頁瀏覽時,看到第5頁,這時還是覺得第2頁的一件商品更想買,於是就可以透過點選頁碼2回到展示該商品的頁面;這就達到了透過頁碼快速定位商品位置的目的,而不用逐個商品逐個商品地往回去查詢該商品在哪裡。

分頁的子功能主要有頁碼展示、資料量展示以及翻頁操作,分別都有各自的作用,例如內容定位、對內容的預期把控等;我們在設計分頁功能時,可以根據業務需要來選擇不同的構成元素。

在下面的響應中,資料列表將會出現在響應主體中,而分頁的資訊與資料列表徹底分開,這是由於請求使用application/json,目的是獲取資源,而分頁資訊並不是資源,而是後設資料,所以以`metadata``的形式在header中輸出。

image-20240809181130755

​ 從本質上來說分頁導航屬於 api 成熟度 level3 級別,因為他實現了 API 的自我發現機制。

4.1改造分頁模組

​ 之前引入了PagianationList<T>工具類,裡面存放當前頁、單頁資料量和資料本體,現在對他進行改造,需要新增的資訊有:是否有上/下一頁、總頁數、(符合要求的)資料總量這四個屬性。

TotalCount我們可以使用內部提供的方法進行非同步獲得,有了總的資料量就能直接算出總的頁數,所以只把TotalCount作為引數傳入即可。

using Microsoft.EntityFrameworkCore;
namespace FakeXiecheng.Helper
{
    public class PaginationList<T> : List<T>
    {
        public int TotalPages { get; private set; }
        public int TotalCount { get; private set; }
        public bool HasPrevious => CurrentPage > 1;
        public bool HasNext => CurrentPage < TotalPages;

        public int CurrentPage { get; set; }
        public int PageSize { get; set; }
        public PaginationList(int totalCount, int currentPage, int pageSize, List<T> items)
        {
            CurrentPage = currentPage;
            PageSize = pageSize;
            AddRange(items);
            TotalCount = totalCount;
            TotalPages = (int)Math.Ceiling(totalCount / (double)pageSize);
        }
        public static async Task<PaginationList<T>> CreateAsync(
            int currentPage, int pageSize, IQueryable<T> result)
        {
            var totalCount = await result.CountAsync();
            // pagination
            // skip
            var skip = (currentPage - 1) * pageSize;
            result = result.Skip(skip);
            // 以pagesize為標準顯示一定量的資料
            result = result.Take(pageSize);
            // include vs join
            var items = await result.ToListAsync();
            return new PaginationList<T>(totalCount, currentPage, pageSize, items);
        }

    }

4.2建立分頁導航資訊

​ 確定分頁模組能夠提供我們需要的資訊之後下一步就是想辦法將這些資訊寫到Response之中

​ 使用 IUrlHelper 生成 URL 、,它提供了一種方便的方式在控制器或服務中生成與路由相關的 URL,為了使用這個東西,需要註冊IActionContextAccessor的服務,使得我們可以在應用程式的任何地方方便地訪問當前請求的上下文資訊。(尤其是我們需要處理與當前請求相關的業務邏輯)

builder.Services.AddSingleton<IActionContextAccessor, ActionContextAccessor>();

​ 注入的 IUrlHelper 是一個用於生成 URL 的幫助類,它提供了一種方便的方式在控制器或服務中生成與路由相關的 URL;

    private ITouristRouteRepository _touristRouteRepository;
    private readonly IMapper _mapper;
    private readonly IUrlHelper _urlHelper;

    public TouristRoutesController(
        ITouristRouteRepository touristRouteRepository,
        IMapper mapper,
        IUrlHelperFactory urlHelperFactory,
        IActionContextAccessor actionContextAccessor
    )
    {
        _touristRouteRepository = touristRouteRepository;
        _mapper = mapper;
        _urlHelper = urlHelperFactory.GetUrlHelper(actionContextAccessor.ActionContext);
    }

​ 建立GenerateTouristRouteResourceURL用於來生成完整的URL連結;

namespace FakeXiecheng.Helper
{
    public enum ResourceUriType
    {
        PreviousPage,
        NextPage
    }
}
    private string GenerateTouristRouteResourceURL(
        TouristRouteResourceParamaters paramaters,
        PaginationResourceParamaters paramaters2,
        ResourceUriType type
    )
{
    return type switch
    {
        ResourceUriType.PreviousPage => _urlHelper.Link("GetTouristRoutes",
            new
            {
                keyword = paramaters.Keyword,
                rating = paramaters.Rating,
                pageNumber = paramaters2.PageNumber - 1,
                pageSize = paramaters2.PageSize
            }),
        ResourceUriType.NextPage => _urlHelper.Link("GetTouristRoutes",
            new
            {
                keyword = paramaters.Keyword,
                rating = paramaters.Rating,
                pageNumber = paramaters2.PageNumber + 1,
                pageSize = paramaters2.PageSize
            }),
        _ => _urlHelper.Link("GetTouristRoutes",
            new
            {
                keyword = paramaters.Keyword,
                rating = paramaters.Rating,
                pageNumber = paramaters2.PageNumber,
                pageSize = paramaters2.PageSize
            })
    };
}

/

​ 改造控制器,這裡主要修改的內容是在獲得前/後一頁的URL連結之後使用Response.Headers.Add將資訊寫到返回的Header中。

/ api/touristRoutes?keyword=傳入的引數
[HttpGet(Name = "GetTouristRoutes")]
[HttpHead]
public async Task<IActionResult> GerTouristRoutes(
    [FromQuery] TouristRouteResourceParamaters paramaters,
    [FromQuery] PaginationResourceParamaters paramaters2
//[FromQuery] string keyword,
//string rating // 小於lessThan, 大於largerThan, 等於equalTo lessThan3, largerThan2, equalTo5 
)// FromQuery vs FromBody
{
    var touristRoutesFromRepo = await _touristRouteRepository
        .GetTouristRoutesAsync(
            paramaters.Keyword,
            paramaters.RatingOperator,
            paramaters.RatingValue,
            paramaters2.PageSize,
            paramaters2.PageNumber
        );
    if (touristRoutesFromRepo == null || touristRoutesFromRepo.Count() <= 0)
    {
        return NotFound("沒有旅遊路線");
    }
    var touristRoutesDto = _mapper.Map<IEnumerable<TouristRouteDto>>(touristRoutesFromRepo);

    var previousPageLink = touristRoutesFromRepo.HasPrevious
        ? GenerateTouristRouteResourceURL(
            paramaters, paramaters2, ResourceUriType.PreviousPage)
        : null;

    var nextPageLink = touristRoutesFromRepo.HasNext
        ? GenerateTouristRouteResourceURL(
            paramaters, paramaters2, ResourceUriType.NextPage)
        : null;

    // x-pagination
    var paginationMetadata = new
    {
        previousPageLink,
        nextPageLink,
        totalCount = touristRoutesFromRepo.TotalCount,
        pageSize = touristRoutesFromRepo.PageSize,
        currentPage = touristRoutesFromRepo.CurrentPage,
        totalPages = touristRoutesFromRepo.TotalPages
    };

    Response.Headers.Add("x-pagination",
        Newtonsoft.Json.JsonConvert.SerializeObject(paginationMetadata));

    return Ok(touristRoutesDto);
}

image-20240810012644173

第二章 資源排序

​ 以GerTouristRoutes為例,如果想要把排序的引數傳入後端,就需要在TouristRouteResourceParamaters中再新增一個String型別的資料來表示以什麼排序/升序或降序

 public string? OrderBy { get; set; }

​ 如果我們只想針對某個資料做排序像下面這樣寫死就行了,但是使用者只需要像後端傳送自己想要資料的請求就行了,而開發者要考慮的就多了。直接在下面程式碼中寫if或者switch判斷又顯得我們很呆,所以這時候就可以參考Mybatis中的動態sql。

image-20240811004136129

OrderByEF給我提供的,裡面又沒有辦法接受我們DTO傳過來的字串

​ 在Mybatis中,我們構造QueryWrapper來完成sql語句的構造,mybatis會按照約定自動將這個字串對映到entity上;在mybatis專案中,

if (!StringUtils.isEmpty(orderBy)) { queryWrapper.orderByDesc(orderBy);}

​ 走到這一步要求我們已經將url中的排序相關關鍵字全部拆分成約定的形式,比如說我們傳入了orderby=price desc,在上面這一行程式碼中,我們要求傳過來的這個orderBy引數的值已經是price這個字串;

​ 在.NET中,透過 LINQ 和表示式樹,開發者可以更加靈活地定義自己的對映規則,生成相應的SQL語句。這種方式雖然需要手動定義對映規則,但也提供了更大的靈活性和可擴充套件性,適用於更復雜的場景。

​ 在這裡我們需要自己定義自己的對映規則,讓(string)OrderBy欄位對映到實體上從而讓IQueryable生成相應的sql。

我們想要的效果就是result.ApplySort(orderBy,_mappingDict),方法引數的前者是從url獲得的排序字串,第二個則是對映到屍體上的對映法則。

​ 安裝依賴包 System.linq.dynamic.core

image-20240811012336955

1. 需求設計

需求:前端傳遞排序引數(如 orderby=price desc),後端需要根據這個引數對資料進行排序。由於 DTO 欄位和資料庫實體欄位可能不同,需要透過屬性對映將前端的排序欄位對映到實際的資料庫欄位上,然後對資料進行排序。

2. 服務設計

關鍵點:我們透過 PropertyMappingService 來處理 DTO 到 Model 的欄位對映,透過擴充套件方法 ApplySort<T>IQueryable 資料來源進行動態排序。

3. 具體實現

3.1 PropertyMappingService: IPropertyMappingService

作用:這是一個服務介面,用於管理 DTO 和實體類之間的屬性對映。透過面向介面的方式,我們可以將對映的具體實現與業務邏輯分離。

public interface IPropertyMappingService
{
    Dictionary<string, PropertyMappingValue> GetPropertyMapping<TSource, TDestination>();
}

實現類 PropertyMappingService

  • 私有成員 _propertyMappings:這是一個包含多個屬性對映的列表 (IList<IPropertyMapping>),用於管理不同 DTO 和 Model 之間的對映關係。
  • 建構函式:在建構函式中,我們將 DTO (TouristRouteDto) 和實體類 (TouristRoute) 的屬性對映關係新增到 _propertyMappings 列表中。
  • GetPropertyMapping<TSource, TDestination> 方法:該方法根據 DTO 和 Model 型別,返回對應的屬性對映字典 (Dictionary<string, PropertyMappingValue>),供後續的排序邏輯使用。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using FakeXiecheng.Dtos;
using FakeXiecheng.Models;

namespace FakeXiecheng.Services
{
    public class PropertyMappingService : IPropertyMappingService
    {
        private Dictionary<string, PropertyMappingValue> _touristRoutePropertyMapping =
           new Dictionary<string, PropertyMappingValue>(StringComparer.OrdinalIgnoreCase)
           {
               { "Id", new PropertyMappingValue(new List<string>(){ "Id" }) },
               { "Title", new PropertyMappingValue(new List<string>(){ "Title" })},
               { "Rating", new PropertyMappingValue(new List<string>(){ "Rating" })},
               { "OriginalPrice", new PropertyMappingValue(new List<string>(){ "OriginalPrice" })},
           };

        private IList<IPropertyMapping> _propertyMappings = new List<IPropertyMapping>();

        public PropertyMappingService()
        {
            _propertyMappings.Add(
                new PropertyMapping<TouristRouteDto, TouristRoute>(
                    _touristRoutePropertyMapping));
        }

        public Dictionary<string, PropertyMappingValue>
            GetPropertyMapping<TSource, TDestination>()
        {
            // 獲得匹配的對映物件
            var matchingMapping =
                _propertyMappings.OfType<PropertyMapping<TSource, TDestination>>();

            if (matchingMapping.Count() == 1)
            {
                return matchingMapping.First()._mappingDictionary;
            }

            throw new Exception(
                $"Cannot find exact property mapping instance for <{typeof(TSource)},{typeof(TDestination)}");

        }
    }
}

3.2 PropertyMapping<TSource, TDestination>: IPropertyMapping

作用:用於定義具體的屬性對映關係。

_mappingDictionary:這是一個字典型別 (Dictionary<string, PropertyMappingValue>),用於儲存 DTO 欄位和 Model 欄位的對映關係。鍵是 DTO 欄位名,值是 PropertyMappingValue,包含了對應的 Model 欄位名列表。

public class PropertyMapping<TSource, TDestination> : IPropertyMapping
{
    public Dictionary<string, PropertyMappingValue> _mappingDictionary { get; set; }

    public PropertyMapping(Dictionary<string, PropertyMappingValue> mappingDictionary)
    {
        _mappingDictionary = mappingDictionary;
    }
}
namespace FakeXiecheng.API.Services
{
    public class PropertyMapping<TSource, TDestination> : IPropertyMapping
    {
        public Dictionary<string, PropertyMappingValue> _mappingDictionary { get; set; }

        public PropertyMapping(Dictionary<string, PropertyMappingValue> mappingDictionary)
        {
            _mappingDictionary = mappingDictionary;
        }
    }
}

3.3 PropertyMappingValue

作用:用於儲存 DTO 欄位與 Model 欄位的對應關係。

  • DestinationProperties:這是一個字串列表,包含對應的 Model 欄位名。
namespace FakeXiecheng.API.Services
{
    public class PropertyMappingValue
    {
        public IEnumerable<string> DestinationProperties { get; private set; }
        public PropertyMappingValue(IEnumerable<string> destinationProperties)
        {
            DestinationProperties = destinationProperties;
        }
    }
}

3.4. 擴充套件方法實現排序

需求:前端傳入一個排序字串(如 "price desc"),需要將其應用到 IQueryable 資料來源上,並根據 DTO 和 Model 的對映關係進行排序。

ApplySort<T> 擴充套件方法

作用:將排序邏輯應用到 IQueryable<T> 資料來源上。

  • 引數說明
    • source:要進行排序的資料來源。
    • orderBy:排序字串,由前端傳入。
    • mappingDictionary:DTO 欄位與 Model 欄位的對映字典。
  • 邏輯步驟
    1. 檢查 sourcemappingDictionary 是否為 null
    2. 如果 orderBy 為空,直接返回未排序的資料來源。
    3. 解析 orderBy 字串,將其分割為多個排序條件。
    4. 對每個排序條件:
      • 提取屬性名和排序方向(升序或降序)。
      • 使用 mappingDictionary 將屬性名對映到實際的 Model 欄位名。
      • 將排序條件拼接成一個字串,用於 IQueryable 的排序。
    5. 使用生成的排序字串對 IQueryable 進行排序,並返回結果。
public static IQueryable<T> ApplySort<T>(
    this IQueryable<T> source,
    string orderBy,
    Dictionary<string, PropertyMappingValue> mappingDictionary
)
{
    // ...引數檢查...

    var orderByString = string.Empty;
    var orderByAfterSplit = orderBy.Split(',');

    foreach(var order in orderByAfterSplit)
    {
        var trimmedOrder = order.Trim();
        var orderDescending = trimmedOrder.EndsWith(" desc");
        var indexOfFirstSpace = trimmedOrder.IndexOf(" ");
        var propertyName = indexOfFirstSpace == -1 ? trimmedOrder : trimmedOrder.Remove(indexOfFirstSpace);

        if (!mappingDictionary.ContainsKey(propertyName))
        {
            throw new ArgumentException($"Key mapping for {propertyName} is missing");
        }

        var propertyMappingValue = mappingDictionary[propertyName];

        foreach(var destinationProperty in propertyMappingValue.DestinationProperties.Reverse())
        {
            orderByString = orderByString +
                (string.IsNullOrWhiteSpace(orderByString) ? string.Empty : ", ")
                + destinationProperty
                + (orderDescending ? " descending" : " ascending");
        }
    }

    return source.OrderBy(orderByString);
}

3.5 整體流程總結

從前端獲取排序引數:如 orderby=price desc

解析排序引數:在 ApplySort<T> 擴充套件方法中,解析出排序欄位和排序方向。

獲取屬性對映:透過 PropertyMappingService 獲取 DTO 到 Model 的屬性對映關係。

對映並排序:將解析出的排序欄位對映到實體類屬性上,然後透過擴充套件方法動態地對 IQueryable 資料來源進行排序。

返回排序結果:返回已經排序的查詢結果。

image-20240812000105833

4.排序引數的分頁導航

​ 在相應引數的頭部,排序引數並沒有放到響應中,在生成連結的時候加入這兩個引數即可。

        private string GenerateTouristRouteResourceURL(
            TouristRouteResourceParamaters paramaters,
            PaginationResourceParamaters paramaters2,
            ResourceUriType type
        )
    {
        return type switch
        {
            ResourceUriType.PreviousPage => _urlHelper.Link("GetTouristRoutes",
                new
                {
                    orderBy = paramaters.OrderBy,
                    keyword = paramaters.keyword,
                    rating = paramaters.Rating,
                    pageNumber = paramaters2.PageNumber - 1,
                    pageSize = paramaters2.PageSize
                }),
            ResourceUriType.NextPage => _urlHelper.Link("GetTouristRoutes",
                new
                {
                    orderBy = paramaters.OrderBy,
                    keyword = paramaters.keyword,
                    rating = paramaters.Rating,
                    pageNumber = paramaters2.PageNumber + 1,
                    pageSize = paramaters2.PageSize
                }),
            _ => _urlHelper.Link("GetTouristRoutes",
                new
                {
                    orderBy = paramaters.OrderBy,
                    keyword = paramaters.keyword,
                    rating = paramaters.Rating,
                    pageNumber = paramaters2.PageNumber,
                    pageSize = paramaters2.PageSize
                })
        };

image-20240812011212906

5.處理400級別的錯誤資訊

​ 在上面的排序中,對於不存在的排序欄位,請求仍然會返回500級別的錯誤,但是為了符合標準,這時候應該是返回400型別的錯誤並提示資訊。

image-20240812011504213

​ 也就是說在我們定義的字典中如果沒有使用者的輸入,就需要返回400型別的錯誤。

namespace FakeXiecheng.Services; 
public bool IsMappingExists<TSource, TDestination>(string fields)
        {
            var propertyMapping = GetPropertyMapping<TSource, TDestination>();

            if (string.IsNullOrWhiteSpace(fields))
            {
                return true;
            }

            //逗號來分隔欄位字串
            var fieldsAfterSplit = fields.Split(",");

            foreach(var field in fieldsAfterSplit)
            {
                // 去掉空格
                var trimmedField = field.Trim();
                // 獲得屬性名稱字串
                var indexOfFirstSpace = trimmedField.IndexOf(" ");
                var propertyName = indexOfFirstSpace == -1 ?
                    trimmedField : trimmedField.Remove(indexOfFirstSpace);

                if (!propertyMapping.ContainsKey(propertyName)) 
                {
                    return false;
                }
            }
            return true;
        }

​ 注入服務private readonly IPropertyMappingService _propertyMappingService;在控制器中先進行過濾即可。

if (!_propertyMappingService
.IsMappingExists<TouristRouteDto, TouristRoute>(
paramaters.OrderBy))
{
    return BadRequest("請輸入正確的排序引數");
}
var touristRoutesFromRepo = await _touristRouteRepository

image-20240812012459693

第三章 資料塑性

​ RESTful API的一個常見缺點是資料粒度過粗,即API可能會返回大量不必要的資料,這不僅影響了效能,還可能增加了網路傳輸的開銷。在RESTful API中,資料塑性指的是API所提供的資料結構能夠靈活適應不同的客戶端需求和應用場景。這種塑性體現在資料的可擴充套件性、可變形性以及在多種請求場景下的靈活應對能力。

​ 透過資料塑形,RESTful API可以更好地支援不同客戶端的需求,減少不必要的資料傳輸,提高整體效能,同時增強API的靈活性和適應性。這也是提升API資料塑性的重要途徑之一。

1.構建動態物件

​ 在處理RESTful API的響應資料時,傳統的固定DTO結構往往無法滿足多變的客戶端需求。為了提升API的靈活性,我們可以結合.NET中的ExpandoObject與反射機制,透過動態生成資料結構來實現定製化的資料塑形。

1.1擴充方法和動態檢查

​ 首先確定要對IEnumerable的方法進行擴充,傳入的是待處理的資料來源,我們希望該方法返回一個可以動態更改的ExpandoObject物件,需要處理的資料型別為泛型<T>,最後進行為空的異常處理。

namespace FakeXiecheng.Helper
{
    public static class IEnumerableExtensions
    {
        public static IEnumerable<ExpandoObject> ShapeData<TSource>(
            this IEnumerable<TSource> source,
            string fields
        )
        {
            if (source == null)
            {
                throw new ArgumentNullException(nameof(source));
            }

1.2透過反射獲得屬性

​ 建立一個List型別的容器放處理完的動態物件,若source為空說明不需要進行篩選欄位,直接將傳入類的所有欄位都放到要返回的物件propertyInfoList中即可;

​ 若要求對該資料進行塑性,則首先處理傳入後端的字串,基於約定處理成可識別的形式,使用反射機制獲取對應的欄位儲存到propertyInfo之中。


            var expandoObjectList = new List<ExpandoObject>();
            //避免在列表中遍歷資料,建立一個屬性資訊列表
            var propertyInfoList = new List<PropertyInfo>();
            if (string.IsNullOrWhiteSpace(fields)) 
            {
                // 希望返回動態型別物件ExpandoObject所有的屬性
                var propertyInfos = typeof(TSource)
                    .GetProperties(BindingFlags.IgnoreCase
                    | BindingFlags.Public | BindingFlags.Instance);
                propertyInfoList.AddRange(propertyInfos);
            }
            else
            {
                //逗號來分隔欄位字串
                var fieldsAfterSplit = fields.Split(',');
                foreach (var filed in fieldsAfterSplit)
                {
                    // 去掉首尾多餘的空格,獲得屬性名稱
                    var propertyName = filed.Trim();

                    var propertyInfo = typeof(TSource)
                        .GetProperty(propertyName, BindingFlags.IgnoreCase
                    | BindingFlags.Public | BindingFlags.Instance);

                    if (propertyInfo == null)
                    {
                        throw new Exception($"屬性 {propertyName} 找不到" +
                            $" {typeof(TSource)}");
                    }
                    propertyInfoList.Add(propertyInfo);
                }
            }

1.3遍歷並複製屬性到expendoObejct

​ 獲得對應的屬性和值之後,以字典的形式儲存到expandoObjectList中返回。

 foreach (TSource sourceObject in source)
            {
                // 建立動態型別物件, 建立資料塑性物件
                var dataShapedObject = new ExpandoObject();

                foreach (var propertyInfo in propertyInfoList)
                {
                    //獲得對應屬性的真實資料
                    var propertyValue = propertyInfo.GetValue(sourceObject);

                    ((IDictionary<string, object>)dataShapedObject)
                        .Add(propertyInfo.Name, propertyValue);
                }

                expandoObjectList.Add(dataShapedObject);
            }

            return expandoObjectList;
        }
    }
}

2.列表資料的塑性

​ 完成上面的處理之後首先更改TouristRouteResourceParamaters新增屬性Fields來代表需要篩選的資料,再將要返回dto的時候呼叫寫好的ShapeData方法對資料進行塑性。

return Ok(touristRoutesDto.ShapeData(paramaters.Fields));

​ 另外,我們還需要將這個資訊新增到返回體的Header之中。

 private string GenerateTouristRouteResourceURL{
	fileds = paramaters.Fields,
 }

image-20240814231131463

3.單一資源的塑性

​ 對於Api:GetTouristRoutes其返回的資料是列表型別的一組資料

​ 在IEnumerable中進行反射的操作是非常大的,透過一次性獲取所有所需的屬性資訊並將其儲存在 List<PropertyInfo> 中,可以避免在遍歷每個物件時重複進行反射操作。反射操作相對昂貴,因此減少其呼叫頻率有助於提升效能,所以我們建立了 var propertyInfoList = new List<PropertyInfo>();

namespace FakeXiecheng.Helper
{
    public static class ObjectExtensions
    {
        public static ExpandoObject ShapeData<TSource>(this TSource source,
             string fields)
        {
            if (source == null)
            {
                throw new ArgumentNullException(nameof(source));
            }

            var dataShapedObject = new ExpandoObject();

            if (string.IsNullOrWhiteSpace(fields))
            {
                // all public properties should be in the ExpandoObject 
                var propertyInfos = typeof(TSource)
                        .GetProperties(BindingFlags.IgnoreCase |
                        BindingFlags.Public | BindingFlags.Instance);

                foreach (var propertyInfo in propertyInfos)
                {
                    // get the value of the property on the source object
                    var propertyValue = propertyInfo.GetValue(source);

                    // add the field to the ExpandoObject
                    ((IDictionary<string, object>)dataShapedObject)
                        .Add(propertyInfo.Name, propertyValue);
                }

                return dataShapedObject;
            }

            // the field are separated by ",", so we split it.
            var fieldsAfterSplit = fields.Split(',');

            foreach (var field in fieldsAfterSplit)
            {
                // trim each field, as it might contain leading 
                // or trailing spaces. Can't trim the var in foreach,
                // so use another var.
                var propertyName = field.Trim();

                // use reflection to get the property on the source object
                // we need to include public and instance, b/c specifying a 
                // binding flag overwrites the already-existing binding flags.
                var propertyInfo = typeof(TSource)
                    .GetProperty(propertyName,
                    BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance);

                if (propertyInfo == null)
                {
                    throw new Exception($"Property {propertyName} wasn't found " +
                        $"on {typeof(TSource)}");
                }

                // get the value of the property on the source object
                var propertyValue = propertyInfo.GetValue(source);

                // add the field to the ExpandoObject
                ((IDictionary<string, object>)dataShapedObject)
                    .Add(propertyInfo.Name, propertyValue);
            }

            // return the list
            return dataShapedObject;
        }
    }
}

    [HttpGet("{touristRouteId}", Name = "GetTouristRouteById")]
    [HttpHead("{touristRouteId}")]
    public async Task<IActionResult> GetTouristRouteById(Guid touristRouteId, string fileds)
    {
        var touristRouteFromRepo = await _touristRouteRepository.GetTouristRouteAsync(touristRouteId);
        if (touristRouteFromRepo == null)
        {
            return NotFound($"旅遊路線{touristRouteId}找不到");
        }

        var touristRouteDto = _mapper.Map<TouristRouteDto>(touristRouteFromRepo);
        return Ok(touristRouteDto.ShapeData(fileds));
    }

image-20240814235654386

4.處理400級別錯誤

​ 對於不存在的欄位(比如說輸入一個itle)找不到對應的欄位應該返回400型別的錯誤,目前返回的是500

image-20240814235638671

​ 在Api:GerTouristRoutes中,對於引數的檢查都放在_propertyMappingService之中,所以可以直接把引數檢查放到該服務中:

        public bool IsPropertiesExists<T>(string fields)
        {
            if (string.IsNullOrWhiteSpace(fields))
            {
                return true;
            }

            //逗號來分隔欄位字串
            var fieldsAfterSplit = fields.Split(',');

            foreach (var field in fieldsAfterSplit)
            {
                // 獲得屬性名稱字串
                var propertyName = field.Trim();

                var propertyInfo = typeof(T)
                    .GetProperty(
                        propertyName,
                        BindingFlags.IgnoreCase | BindingFlags.Public
                        | BindingFlags.Instance
                    );
                // 如果T中沒找到對應的屬性
                if (propertyInfo == null)
                {
                    return false;
                }
            }
            return true;

        }

image-20240815005556828

image-20240815005644221

第四章 HATEOAS

​ 在現代的 Web 開發中,RESTful API 已經成為了與伺服器互動的標準方式。然而,傳統的 RESTful 設計常常要求客戶端對伺服器的結構和功能有嚴格的瞭解,這種緊耦合使得服務的演化和更新變得複雜。HATEOAS(Hypermedia As The Engine Of Application State)則提出了一種突破這種束縛的方式。HATEOAS 是 REST 架構的一個重要特性,它透過超媒體的使用,將應用的狀態和可用操作直接嵌入到資源的表示中,使得客戶端與伺服器之間的契約不再嚴格。

​ HATEOAS 的實現核心在於連結(Link)。透過在資源的響應中嵌入超媒體連結,客戶端可以動態地發現和訪問 API 的其他部分。這些連結包含三個重要元素:hrefrelmethod。其中,href 是指向相關資源的 URI,客戶端可以透過這個 URI 來檢索資源或改變應用狀態。rel 描述了當前資源和 URI 之間的關係,例如,self 表示當前資源的自我描述。method 則指明瞭對該 URI 進行操作時所需的 HTTP 方法。這種方式讓 API 的演化變得更加靈活,客戶端不再需要對伺服器的內部實現做出硬編碼,而是可以根據超媒體提供的資訊自適應地進行操作。

​ 比如說當引入了HATEOAS之後,對於 API: GET /touristRoutes/1,在返回的資訊中有了下面的資料:

rel: 表示當前連結的關係型別。self 表示該連結用於獲取當前資源自身的資訊;update 表示用於更新當前資源;delete 表示用於刪除當前資源;relatedRoutes 表示獲取與當前資源相關的其他資源。

href: 提供了資源的 URI,客戶端可以透過該 URI 執行相關操作。

method: 指定了需要使用的 HTTP 方法,如 GETPUTDELETE

{
  "id": 1,
  "title": "Great Wall Adventure",
  "links": [
    {
      "rel": "self",
      "href": "/touristRoutes/1",
      "method": "GET"
    },
    {
      "rel": "update",
      "href": "/touristRoutes/1",
      "method": "PUT"
    },
    {
      "rel": "delete",
      "href": "/touristRoutes/1",
      "method": "DELETE"
    },
    {
      "rel": "relatedRoutes",
      "href": "/touristRoutes?relatedTo=1",
      "method": "GET"
    }
  ]
}

​ 透過實現 HATEOAS,REST 服務可以更容易地進行演化和擴充套件。傳統的 RESTful 設計要求客戶端和伺服器之間保持嚴格的契約,這使得服務的更新和變更往往需要協調和修改客戶端程式碼。而 HATEOAS 透過將可用操作和資源的關係直接嵌入到響應中,打破了這種嚴格的契約限制。客戶端可以動態地根據服務提供的超媒體連結進行導航,從而適應 API 的變更。這種方法不僅提高了服務的靈活性,還大大簡化了客戶端的維護和更新工作。

​ 在 HATEOAS 模型中,每個資源不僅包含其自身的資料,還包括了一組與其他相關資源的連結。這些連結允許客戶端在不知道完整 API 結構的情況下,依據當前資源的狀態進行進一步的操作。這樣的設計理念使得 API 具有自描述的能力,客戶端可以透過分析響應中的連結資訊,動態地探索和操作資源。這種方法使得客戶端和伺服器之間的互動更加靈活,並且為 API 的演化提供了自然的支援。

1.使用HATOEAS處理單一資源

1.1處理GetTouristRouteById

​ 以API:GetTouristRouteById為例

LinkDto 類是一個簡單的資料傳輸物件(DTO),用於表示超媒體連結。也就是上面的json資料中可能會出現的東西,之後我們將對返回的Dto進行改造,使其包含LinkDto的內容。

namespace FakeXiecheng.Dtos
{
    public class LinkDto
    {
        // 連結的 URI
        public string Href { get; set; }
        // 描述了連結的關係型別,如 "self"、"update" 等
        public string Rel { get; set; }
        // 對該 URI 執行操作所需的 HTTP 方法
        public string Method { get; set; }

        public LinkDto(string href, string rel, string method)
        {
            Href = href;
            Rel = rel;
            Method = method;
        }
    }
}

CreateLinkForTouristRoute 方法生成一個包含多種連結的列表,這些連結允許客戶端對特定旅遊路線執行各種操作,每個 LinkDto 物件表示一個操作連結。

 private IEnumerable<LinkDto> CreateLinkForTouristRoute(
     Guid touristRouteId,
     string fields)
 {
     var links = new List<LinkDto>();

     links.Add(
         new LinkDto(
             Url.Link("GetTouristRouteById", new { touristRouteId, fields }),
             "self",
             "GET"
             )
         );

     // 更新
     links.Add(
         new LinkDto(
             Url.Link("UpdateTouristRoute", new { touristRouteId }),
             "update",
             "PUT"
             )
         );

     // 區域性更新 
     links.Add(
         new LinkDto(
             Url.Link("PartiallyUpdateTouristRoute", new { touristRouteId }),
             "partially_update",
             "PATCH")
         );

     // 刪除
     links.Add(
         new LinkDto(
             Url.Link("DeleteTouristRoute", new { touristRouteId }),
             "delete",
             "DELETE")
         );

     // 獲取路線圖片
     links.Add(
         new LinkDto(
             Url.Link("GetPictureListForTouristRoute", new { touristRouteId }),
             "get_pictures",
             "GET")
         );

     // 新增新圖片
     links.Add(
         new LinkDto(
             Url.Link("CreateTouristRoutePicture", new { touristRouteId }),
             "create_picture",
             "POST")
         );

     return links;
 }

​ 更改控制器的內容,主要在於使用:

​ 呼叫了 CreateLinkForTouristRoute 方法,傳入了 touristRouteIdfields 引數。CreateLinkForTouristRoute 方法生成了一組與該旅遊路線相關的超媒體連結,並返回一個 IEnumerable<LinkDto> 型別的集合。

​ 透過上面資源排序的內容我們知道ExpandoObject 是 C# 中的一個類,提供了一種在執行時動態新增和修改屬性的方式。它實現了 IDictionary<string, object> 介面,這意味著它可以被當作一個字典來操作。所以使用 as IDictionary<string, object>;操作將result轉化成字典型別方便之後我們進行.Add("links", linkDtos)

 // api/touristroutes/{touristRouteId}
 [HttpGet("{touristRouteId}", Name = "GetTouristRouteById")]
 [HttpHead("{touristRouteId}")]
 public async Task<IActionResult> GetTouristRouteById(Guid touristRouteId, string fields)
 {
     var touristRouteFromRepo = await _touristRouteRepository.GetTouristRouteAsync(touristRouteId);
     if (touristRouteFromRepo == null)
     {
         return NotFound($"旅遊路線{touristRouteId}找不到");
     }

     var touristRouteDto = _mapper.Map<TouristRouteDto>(touristRouteFromRepo);
     //return Ok(touristRouteDto.ShapeData(fields));
     var linkDtos = CreateLinkForTouristRoute(touristRouteId, fields);

     var result = touristRouteDto.ShapeData(fields)
         as IDictionary<string, object>;
     result.Add("links", linkDtos);

     return Ok(result);
 }

image-20240815015009873

1.2在POST請求中複用建立Link元件

​ 建立資源的響應不僅需要返回新建立資源的資料,還需附帶相關的操作連結。以下程式碼示例展示瞭如何處理 POST 請求以建立旅遊路線資源,並與之前獲取資源的操作相比,它不需要進行資料塑性,但是需要將result透過ShapeData方法轉化成字典:

 [HttpPost]
        [Authorize(AuthenticationSchemes = "Bearer")]
        [Authorize]
        public async Task<IActionResult> CreateTouristRoute([FromBody] TouristRouteForCreationDto touristRouteForCreationDto)
        {
            var touristRouteModel = _mapper.Map<TouristRoute>(touristRouteForCreationDto);
            _touristRouteRepository.AddTouristRoute(touristRouteModel);
            await _touristRouteRepository.SaveAsync();
            var touristRouteToReture = _mapper.Map<TouristRouteDto>(touristRouteModel);

            var links = CreateLinkForTouristRoute(touristRouteModel.Id, null);

            var result = touristRouteToReture.ShapeData(null)
                as IDictionary<string, object>;

            result.Add("links", links);
            
            return CreatedAtRoute(
                "GetTouristRouteById",
                new { touristRouteId = result["Id"] },
                result
            );
        }

image-20240815020131846

2.使用HATOEAS處理列表資源

​ 對於API: GerTouristRoutes加入HATEOAS支援,之前在生成 metadata 的時候使用到了ResourceUriType其中就包含了前一頁和後一頁的資源連結,而且當時也建立了相關的方法GenerateTouristRouteResourceURL來生成url,加入新的欄位表示當前頁對應links中的self

namespace FakeXiecheng.Helper
{
    public enum ResourceUriType
    {
        PreviousPage,
        NextPage,
        CurrnetPage
    }
}

​ 透過傳入相應引數呼叫GenerateTouristRouteResourceURL生成self的地址,且POST:api/touristRoutes也應該被加入。

private IEnumerable<LinkDto> CreateLinksForTouristRouteList(
            TouristRouteResourceParamaters paramaters,
            PaginationResourceParamaters paramaters2)
        {
            var links = new List<LinkDto>();
            // 新增self,自我連結
            links.Add(new LinkDto(
                    GenerateTouristRouteResourceURL(
                        paramaters, paramaters2, ResourceUriType.CurrnetPage),
                    "self",
                    "GET"
                ));

            // "api/touristRoutes"
            // 新增建立旅遊路線
            links.Add(new LinkDto(
                    Url.Link("CreateTouristRoute", null),
                    "create_tourist_route",
                    "POST"
                ));

            return links;
        }

​ 改造控制器,具體操作為建立集合級別的 HATEOAS 連結併為每個資源新增 HATEOAS 連結,最後構建相應結果返回。

 var shapedDtoList = touristRoutesDto.ShapeData(paramaters.Fields);

 var linkDto = CreateLinksForTouristRouteList(paramaters, paramaters2);

 var shapedDtoWithLinklist = shapedDtoList.Select(t =>
 {
     var touristRouteDictionary = t as IDictionary<string, object>;
     var links = CreateLinkForTouristRoute(
         (Guid)touristRouteDictionary["Id"], null);
     touristRouteDictionary.Add("links", links);
     return touristRouteDictionary;
 });

 var result = new
 {
     value = shapedDtoWithLinklist,
     links = linkDto
 };

 return Ok(result);

image-20240816011347300

3.給專案新增API根文件

​ API 根文件是至關重要的,因為它提供了對 API 功能的清晰概述,幫助開發者和使用者理解如何使用 API。它提高了 API 的自描述性,減少了開發和使用過程中的錯誤,並且使 API 的維護和擴充套件更加容易。過請求根文件的 URL,通常可以獲得一組操作連結。

namespace FakeXiecheng.Controllers
{
    [Route("api")]
    [ApiController]
    public class RootController : ControllerBase
    {
        [HttpGet(Name = "GetRoot")]
        public IActionResult GetRoot()
        {
            var links = new List<LinkDto>();

            // 自我連結
            links.Add(
                new LinkDto(
                    Url.Link("GetRoot", null),
                    "self",
                    "GET"
                ));

            // 一級連結 旅遊路線 “GET api/touristRoutes”
            links.Add(
                new LinkDto(
                    Url.Link("GetTouristRoutes", null),
                    "get_tourist_routes",
                    "GET"
                ));

            // 一級連結 旅遊路線 “POST api/touristRoutes”
            links.Add(
                new LinkDto(
                    Url.Link("CreateTouristRoute", null),
                    "create_tourist_route",
                    "POST"
                ));

            // 一級連結 購物車 “GET api/orders”
            links.Add(
                new LinkDto(
                    Url.Link("GetShoppingCart", null),
                    "get_shopping_cart",
                    "GET"
                ));

            // 一級連結 訂單 “GET api/shoppingCart”
            links.Add(
                new LinkDto(
                    Url.Link("GetOrders", null),
                    "get_orders",
                    "GET"
                ));

            return Ok(links);
        }
    }

}

image-20240816014225062

4.HATEOAS與請求媒體型別

​ 在處理 HATEOAS 時,當前面臨的問題是資源資料和操作連結的混合,這種做法可能違反了 RESTful 的設計原則,因為操作(即連結)與資源資料被混合在了一起。為了解決這個問題,我們可以使用內容協商(Content Negotiation),即客戶端透過 Accept 頭部請求不同的響應格式,從而確保響應資料的結構符合客戶端的需求。

​ 媒體型別(Media Types),也稱為 MIME 型別,是一種標準,用於表示文件、檔案或位元組流的性質和格式。它由主要類別(type)和子類別(subtype)組成,例如 application/json 表示標準 JSON 格式,而 application/vnd.arc.hateoas+json 是用於處理 HATEOAS 的自定義媒體型別。 在 .NET 中,MediaTypeHeaderValue 類可用於處理這些媒體型別,其中 SubType 屬性可以用來獲取子型別部分,從而幫助開發者根據不同的需求處理和解析響應資料。

image-20240816014450343

GET /api/touristRoutes/12345
Accept: application/vnd.arc.hateoas+json


  "Id": "12345",
  "Title": "Great Wall of China",
  "Description": "A historic landmark.",
  "links": [
    {
      "href": "/api/touristRoutes/12345",
      "rel": "self",
      "method": "GET"
    },
    
   。。。。。。
    {
      "href": "/api/touristRoutes/12345/pictures",
      "rel": "create_picture",
      "method": "POST"
    }
  ]
}

​ 在實現 HATEOAS (Hypermedia as the Engine of Application State) 時,通常需要使用特定的媒體型別來標識 HATEOAS 響應格式。透過配置媒體型別,伺服器能夠正確地處理和返回符合 HATEOAS 規範的響應資料,同時確保客戶端可以按照預期解析這些資料。

// 為 ASP.NET Core 的 MVC 配置支援特定的媒體型別
builder.Services.Configure<MvcOptions>(config =>
{
    var outputFormatter = config.OutputFormatters
        .OfType<NewtonsoftJsonOutputFormatter>()?.FirstOrDefault();

    if (outputFormatter != null)
    {
        outputFormatter.SupportedMediaTypes
        .Add("application/vnd.arc.hateoas+json");
    }
});

​ 針對控制器主要做了如下修改:

​ 新增了一個 FromHeader 屬性的引數 mediaType,用於從請求頭中獲取 Accept 媒體型別。這使得方法能夠根據客戶端請求的媒體型別來決定如何格式化響應;

​ 使用 MediaTypeHeaderValue.TryParse 方法解析 mediaType 引數,以確保它符合媒體型別的格式;

​ 根據解析出的媒體型別來決定響應的格式。如果 Accept 頭的媒體型別是 application/vnd.arc.hateoas+json,則將響應資料進行 HATEOAS 處理:如果媒體型別為 HATEOAS 特定型別,構造包含 HATEOAS 連結的響應;否則,僅返回簡單的形狀化資料。

// api/touristRoutes?keyword=傳入的引數
[HttpGet(Name = "GetTouristRoutes")]
[HttpHead]
public async Task<IActionResult> GerTouristRoutes(
    [FromQuery] TouristRouteResourceParamaters paramaters,
    [FromQuery] PaginationResourceParamaters paramaters2,
    [FromHeader(Name = "Accept")] string mediaType

//[FromQuery] string keyword,
//string rating // 小於lessThan, 大於largerThan, 等於equalTo lessThan3, largerThan2, equalTo5 
)// FromQuery vs FromBody
{
    if (!MediaTypeHeaderValue
           .TryParse(mediaType, out MediaTypeHeaderValue parsedMediatype))
    {
        return BadRequest();
    }

    if (!_propertyMappingService
    .IsMappingExists<TouristRouteDto, TouristRoute>(
    paramaters.OrderBy))
    {
        return BadRequest("請輸入正確的排序引數");
    }

    if (!_propertyMappingService
        .IsPropertiesExists<TouristRouteDto>(paramaters.Fields))
    {
        return BadRequest("請輸入正確的塑性引數");
    }
    var touristRoutesFromRepo = await _touristRouteRepository
        .GetTouristRoutesAsync(
            paramaters.keyword,
            paramaters.RatingOperator,
            paramaters.RatingValue,
            paramaters2.PageSize,
            paramaters2.PageNumber,
            paramaters.OrderBy
        );

    if (touristRoutesFromRepo == null || touristRoutesFromRepo.Count() <= 0)
    {
        return NotFound("沒有旅遊路線");
    }
    var touristRoutesDto = _mapper.Map<IEnumerable<TouristRouteDto>>(touristRoutesFromRepo);

    var previousPageLink = touristRoutesFromRepo.HasPrevious
        ? GenerateTouristRouteResourceURL(
            paramaters, paramaters2, ResourceUriType.PreviousPage)
        : null;

    var nextPageLink = touristRoutesFromRepo.HasNext
        ? GenerateTouristRouteResourceURL(
            paramaters, paramaters2, ResourceUriType.NextPage)
        : null;

    // x-pagination
    var paginationMetadata = new
    {
        previousPageLink,
        nextPageLink,
        totalCount = touristRoutesFromRepo.TotalCount,
        pageSize = touristRoutesFromRepo.PageSize,
        currentPage = touristRoutesFromRepo.CurrentPage,
        totalPages = touristRoutesFromRepo.TotalPages
    };

    Response.Headers.Add("x-pagination",
        Newtonsoft.Json.JsonConvert.SerializeObject(paginationMetadata));
    var shapedDtoList = touristRoutesDto.ShapeData(paramaters.Fields);
    if (parsedMediatype.MediaType == "application/vnd.arc.hateoas+json")
    {

        var linkDto = CreateLinksForTouristRouteList(paramaters, paramaters2);

        var shapedDtoWithLinklist = shapedDtoList.Select(t =>
        {
            var touristRouteDictionary = t as IDictionary<string, object>;
            var links = CreateLinkForTouristRoute(
                (Guid)touristRouteDictionary["Id"], null);
            touristRouteDictionary.Add("links", links);
            return touristRouteDictionary;
        });

        var result = new
        {
            value = shapedDtoWithLinklist,
            links = linkDto
        };

        return Ok(result);
    }
    return Ok(shapedDtoList);

    //return Ok(touristRoutesDto.ShapeData(paramaters.Fields));
}

image-20240816020246510

image-20240816020424662

第五章 部署

相關文章