数据的塑形:
允许api的消费者选择返回的资源的字段
/api.companies?fields=id,name
针对资源的字段,而不是其他更低层次的对象的字段。
创建自定义方法IEnumerableExtensions:针对集合的,这里使用反射+扩展方法,就可以对这种问题处理了
using Microsoft.AspNetCore.Mvc.Filters; using System; using System.Collections.Generic; using System.Dynamic; using System.Linq; using System.Reflection; using System.Threading.Tasks; namespace Rountine.API.Helpers { public static class IEnumerableExtensions { public static IEnumerable<ExpandoObject> ShapeData<TSource>(this IEnumerable<TSource> sources, string fields) { if (sources == null) { throw new ArgumentException(nameof(sources)); } var expandoObject = new List<ExpandoObject>(sources.Count()); var propertyInfoList = new List<PropertyInfo>(); if (string.IsNullOrEmpty(fields)) { var properTyInfos = typeof(TSource).GetProperties(BindingFlags.Public | BindingFlags.Instance); propertyInfoList.AddRange(properTyInfos); } else { var fieldsAfterSplit = fields.Split(","); foreach (var field in fieldsAfterSplit) { var propertyName = field.Trim(); var propertyInfo = typeof(TSource).GetProperty(propertyName,BindingFlags.IgnoreCase| BindingFlags.Public|BindingFlags.Instance); if (propertyInfo == null) { throw new Exception($"Property:{propertyName}没找到:{typeof(TSource)}"); } propertyInfoList.Add(propertyInfo); } } foreach (TSource obj in sources) { var shapedObj = new ExpandoObject(); foreach (var propertyInfo in propertyInfoList) { var propertyValue = propertyInfo.GetValue(obj); ((IDictionary<string, object>)shapedObj).Add(propertyInfo.Name,propertyValue); } expandoObject.Add(shapedObj); } return expandoObject; } } }
修改controller:
[HttpGet(Name =nameof(GetCompanies))] public async Task<IActionResult> GetCompanies([FromQuery] CompanyDtoParameters parameters) { var companies = await _companyRepository.GetCompaniesAsync(parameters); var previousPageLink = companies.HasPrevious ? CreateCompaniesResourceUri(parameters, ResourceUriType.PreviousPage) : null; var nextPageLink = companies.HasNext ? CreateCompaniesResourceUri(parameters, ResourceUriType.NextPage) : null; var paginationMetadate = new { totalCount= companies.ToTalCount, pageSize=companies.PageSize, currentPage=companies.CurrentPage, totalPages=companies.ToTalPages, previousPageLink, nextPageLink }; Response.Headers.Add("X-Pagination", JsonSerializer.Serialize(paginationMetadate, new JsonSerializerOptions { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }) ) ; var companyDto = _mapper.Map<IEnumerable<CompanyDto>>(companies); return Ok(companyDto.ShapeData(parameters.Fields)); }
postman:
针对对象ObjectExtensions:
using System; using System.Collections.Generic; using System.Dynamic; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Threading.Tasks; namespace Rountine.API.Helpers { public static class ObjectExtensions { public static ExpandoObject shapeData<TSource>(this TSource source, string fields) { if (source == null) { throw new ArgumentNullException(nameof(source)); } var expandoObject = new ExpandoObject(); if (string.IsNullOrEmpty(fields)) { var propertyInfos = typeof(TSource).GetProperties(BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance); foreach (var propertyInfo in propertyInfos) { var propertyValue = propertyInfo.GetValue(source); ((IDictionary<string, object>)expandoObject).Add(propertyInfo.Name, propertyValue); } } else { var fieldsAfterSplit = fields.Split(","); foreach (var field in fieldsAfterSplit) { var propertyName = field.Trim(); var propertyInfo = typeof(TSource).GetProperty(propertyName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.IgnoreCase); if (propertyInfo == null) { throw new Exception(); } var propertyValue = propertyInfo.GetValue(source); ((IDictionary<string, object>)expandoObject).Add(propertyInfo.Name, propertyValue); } } return expandoObject; } } }
hatos
Hypermedia as the engine of application state
hateoas是rest风格架构中最复杂的约束,也是构建成熟rest服务核心,它的重要性是打破客户端和服务器之间严格的契约,使得客户端能智能自适应,
而rest本身进化更新跟冗余
有助于进化和自我描述
超媒体提如何驱动
支持hateos两种办法
静态类型方案:需要基类(包含link)和包装类,也就是返回的资源里面都含有link,通过集成同一个基类实现。
动态类型方案:需要使用匿名类型或者ExpandoObject等,对于单个资源可以使用ExpandoObject,而对于集合使用匿名类。
单个资源的hateos支持:
LinkDto:
using Microsoft.EntityFrameworkCore.Storage.ValueConversion.Internal;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Rountine.API.Models
{
public class LinkDto
{
public string Href { get; }
public string Rel { get; }
public string Method { get; }
public LinkDto(string href,string rel,string method)
{
Href = href;
Rel = rel;
Method = method;
}
}
}
CompainesController:
[HttpGet("{companyId}", Name = nameof(GetCompany))] public async Task<IActionResult> GetCompany(Guid companyId, string field) { var company = await _companyRepository.GetCompanyAsync(companyId); if (company == null) { return NotFound(); } var companyDto = _mapper.Map<CompanyDto>(company); var links = CreatLinkForCompany(companyId, field); var linkedResoruce = companyDto.shapeData(field) as IDictionary<string, object>; linkedResoruce.Add("links",links); return Ok(linkedResoruce); } private IEnumerable<LinkDto> CreatLinkForCompany(Guid companyId, String field) { var links = new List<LinkDto>(); if (string.IsNullOrEmpty(field)) { links.Add(new LinkDto(Url.Link(nameof(GetCompany), new { companyId }), "self", "GET")); } else { links.Add(new LinkDto(Url.Link(nameof(GetCompany), new { companyId, field }), "self", "GET")); } return links; }
postman:
集合资源的hateos支持:
[HttpGet(Name = nameof(GetCompanies))] public async Task<IActionResult> GetCompanies([FromQuery] CompanyDtoParameters parameters) { var companies = await _companyRepository.GetCompaniesAsync(parameters); var previousPageLink = companies.HasPrevious ? CreateCompaniesResourceUri(parameters, ResourceUriType.PreviousPage) : null; var nextPageLink = companies.HasNext ? CreateCompaniesResourceUri(parameters, ResourceUriType.NextPage) : null; var paginationMetadate = new { totalCount = companies.ToTalCount, pageSize = companies.PageSize, currentPage = companies.CurrentPage, totalPages = companies.ToTalPages, previousPageLink, nextPageLink }; Response.Headers.Add("X-Pagination", JsonSerializer.Serialize(paginationMetadate, new JsonSerializerOptions { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping })); var companyDto = _mapper.Map<IEnumerable<CompanyDto>>(companies); var shape=companyDto.ShapeData(parameters.Fields); var links = createLinksForCompany(parameters); var shapedCompaniesWithLinks = shape.Select(c => { var companyDict = c as IDictionary<string, Object>; var companyLinks = CreatLinkForCompany((Guid)companyDict["Id"],null); companyDict.Add("links",companyLinks); return companyDict; }); var linkedCollectionResource = new { value = shapedCompaniesWithLinks, links }; return Ok(linkedCollectionResource); } private IEnumerable<LinkDto> createLinksForCompany(CompanyDtoParameters parameters) { var links = new List<LinkDto>(); links.Add(new LinkDto(CreateCompaniesResourceUri(parameters, ResourceUriType.Currentpage), "self", "GET")); return links; }
响应: