C# - 基于.NetCore开发的“寸金游”(后端开发)(第三部分)
十五、【RESTful技能进阶】数据排序
- 进行简单排序
- 通过属性(property)的隐射,实现动态化排序
- 结合分页功能,返回合理的http响应
给列表资源排序
举例
数据排序入门
添加处理排序的成员变量(TouristRouteResourceParamaters)
public string OrderBy { get; set; }
参数处理(TouristRoutesController.cs)
- 加上排序的参数。
paramaters.OrderBy
var touristRoutesFromRepo = await _touristRouteRepository
.GetTouristRoutesAsync(
paramaters.Keyword,
paramaters.RatingOperator,
paramaters.RatingValue,
paramaters2.PageSize,
paramaters2.PageNumber,
paramaters.OrderBy
);
修改接口函数ITouristRouteRepository.cs 和TouristRouteRepository.cs
Task<PaginationList<TouristRoute>> GetTouristRoutesAsync(
string keyword, string ratingOperator, int? ratingValue
//分页
, int pageSize, int pageNumber, string orderBy);
public async Task<PaginationList<TouristRoute>> GetTouristRoutesAsync(
string keyword,
string ratingOperator,
int? ratingValue,
//分页
int pageSize,
int pageNumber,
string orderBy
)
if (!string.IsNullOrWhiteSpace(orderBy))
{
if(orderBy.ToLowerInvariant() == "originalprice")
{
result = result.OrderBy(t => t.OriginalPrice);
}
//result.ApplySort(orderBy, _mappingDictionary);
}
修改TouristRouteDto.cs
public decimal OriginalPrice { get; set; }
public double? DiscountPresent { get; set; }
测试 - 请求https://localhost:44381/api/touristRoutes?orderby=originalPrice
安装插件system.linq.aynamic
- 按照字符串排序
ApplySort
-
result.ApplySort(orderBy, _mappingDictionary);
if (!string.IsNullOrWhiteSpace(orderBy))
{
if (orderBy.ToLowerInvariant() == "originalprice")
{
result = result.OrderBy(t => t.OriginalPrice);
}
result.ApplySort(orderBy, _mappingDictionary);
}
属性(Property) 映射服务
属性映射服务PropertyMappingService
创建属性(Property) 映射服务
新建PropertyMappingService.cs
namespace _04NET___CJ_ASP_Travel4.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)}");
}
}
}
新建PropertyMappingValue.cs
namespace _04NET___CJ_ASP_Travel4.Services
{
public class PropertyMappingValue
{
public IEnumerable<string> DestinationProperties { get; private set; }
public PropertyMappingValue(IEnumerable<string> destinationProperties)
{
DestinationProperties = destinationProperties;
}
}
}
新建PropertyMapping.cs
namespace _04NET___CJ_ASP_Travel4.Services
{
public class PropertyMapping<TSource, TDestination> : IPropertyMapping
{
public Dictionary<string, PropertyMappingValue> _mappingDictionary { get; set; }
public PropertyMapping(Dictionary<string, PropertyMappingValue> mappingDictionary)
{
_mappingDictionary = mappingDictionary;
}
}
}
新增接口IPropertyMapping.cs
namespace _04NET___CJ_ASP_Travel4.Services
{
public interface IPropertyMapping
{
}
}
新增接口IPropertyMappingService.cs
namespace _04NET___CJ_ASP_Travel4.Services
{
public interface IPropertyMappingService
{
Dictionary<string, PropertyMappingValue> GetPropertyMapping<TSource, TDestination>();
}
}
注册依赖服务(Startup.cs)ConfigureServices
services.AddTransient<IPropertyMappingService, PropertyMappingService>();
映射字典
注入属性映射服务依赖TouristRouteRepository.cs
private readonly IPropertyMappingService _propertyMappingService;
public TouristRouteRepository(
AppDbContext appDbContext,
IPropertyMappingService propertyMappingService
)
{
_context = appDbContext;
_propertyMappingService = propertyMappingService;
}
if (!string.IsNullOrWhiteSpace(orderBy))
{
var touristRouteMappingDictionary = _propertyMappingService
.GetPropertyMapping<TouristRouteDto, TouristRoute>();
result = result.ApplySort(orderBy, touristRouteMappingDictionary);
}
新建Helper - IQueryableExtensions.cs
namespace _04NET___CJ_ASP_Travel4.Helper
{
public static class IQueryableExtensions
{
public static IQueryable<T> ApplySort<T>(
this IQueryable<T> source,
string orderBy,
Dictionary<string, PropertyMappingValue> mappingDictionary
)
{
if (source == null)
{
throw new ArgumentNullException("source");
}
if (mappingDictionary == null)
{
throw new ArgumentNullException("mappingDictionary");
}
if (string.IsNullOrWhiteSpace(orderBy))
{
return source;
}
var orderByString = string.Empty;
var orderByAfterSplit = orderBy.Split(',');
foreach(var order in orderByAfterSplit)
{
var trimmedOrder = order.Trim();
// 通过字符串“ desc”来判断升序还是降序
var orderDescending = trimmedOrder.EndsWith(" desc");
// 删除升序或降序字符串 " asc" or " 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];
if (propertyMappingValue == null)
{
throw new ArgumentNullException("propertyMappingValue");
}
foreach(var destinationProperty in
propertyMappingValue.DestinationProperties.Reverse())
{
// 给IQueryable 添加排序字符串
orderByString = orderByString +
(string.IsNullOrWhiteSpace(orderByString) ? string.Empty : ", ")
+ destinationProperty
+ (orderDescending ? " descending" : " ascending");
}
}
return source.OrderBy(orderByString);
}
}
}
测试 - 请求https://localhost:44381/api/touristRoutes?orderby=originalPrice desc
- 降序排序
排序参数的分页导航
修改TouristRoutesController.cs中的GenerateTouristRouteResourceURL
方法
- 加上排序参数
orderBy = paramaters.OrderBy,
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
})
};
测试 - 请求https://localhost:44381/api/touristRoutes?orderby=rating desc,originalPrice desc
在header中就有了自定义头部
{"previousPageLink":null,
"nextPageLink":"https://localhost:44381/api/TouristRoutes?orderBy=rating%20desc,originalPrice%20desc&pageNumber=2&pageSize=10","totalCount":16,
"pageSize":10,
"currentPage":1,
"totalPages":2}
处理400级别错误信息
PropertyMappingService.cs信新增IsMappingExists
方法
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;
}
在IPropertyMappingService.cs中将函数声明添加
bool IsMappingExists<TSource, TDestination>(string fields);
修改TouristRoutesController.cs的GerTouristRoutes
方法
private readonly IPropertyMappingService _propertyMappingService;
public TouristRoutesController(
ITouristRouteRepository touristRouteRepository,
IMapper mapper,
IUrlHelperFactory urlHelperFactory,
IActionContextAccessor actionContextAccessor,
IPropertyMappingService propertyMappingService
)
{
_touristRouteRepository = touristRouteRepository;
_mapper = mapper;
_urlHelper = urlHelperFactory.GetUrlHelper(actionContextAccessor.ActionContext);
_propertyMappingService = propertyMappingService;
}
if (!_propertyMappingService
.IsMappingExists<TouristRouteDto, TouristRoute>(
paramaters.OrderBy))
{
return BadRequest("请输入正确的排序参数");
}
十六、【RESTful技能进阶】数据塑形
- RESTFul API的一大缺点:粒度太粗。
- 使用数据塑形的方法来解决数据粒度太粗的问题。
目标
什么是数据塑形
- 定制化选择性后端输出数据的技术。
处理动态类型对象
ExpandoObject
新建Helper - IEnumerableExtensions.cs
namespace _04NET___CJ_ASP_Travel4.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));
}
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);
}
}
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;
}
}
}
列表数据的塑形
修改TouristRouteResourceParamaters.cs
添加新的字符串类型成员变量
public string Fields { get; set; }
修改TouristRoutesController.cs的GerTouristRoutes
方法
var vvvvv = touristRoutesDto.ShapeData(paramaters.Fields);
return Ok(touristRoutesDto.ShapeData(paramaters.Fields));
修改TouristRoutesController.cs的GenerateTouristRouteResourceURL
方法
- 都加上
fields = paramaters.Fields,
return type switch
{
ResourceUriType.PreviousPage => _urlHelper.Link("GetTouristRoutes",
new
{
fields = paramaters.Fields,
orderBy = paramaters.OrderBy,
keyword = paramaters.Keyword,
rating = paramaters.Rating,
pageNumber = paramaters2.PageNumber - 1,
pageSize = paramaters2.PageSize
}),
ResourceUriType.NextPage => _urlHelper.Link("GetTouristRoutes",
new
{
fields = paramaters.Fields,
orderBy = paramaters.OrderBy,
keyword = paramaters.Keyword,
rating = paramaters.Rating,
pageNumber = paramaters2.PageNumber + 1,
pageSize = paramaters2.PageSize
}),
_ => _urlHelper.Link("GetTouristRoutes",
new
{
fields = paramaters.Fields,
orderBy = paramaters.OrderBy,
keyword = paramaters.Keyword,
rating = paramaters.Rating,
pageNumber = paramaters2.PageNumber,
pageSize = paramaters2.PageSize
})
};
测试 - 请求https://localhost:44381/api/touristRoutes?fields=id,title
单一资源的塑形
新建Helper - ObjectExtensions.cs
namespace _04NET___CJ_ASP_Travel4.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;
}
}
}
单一资源的塑形 - 修改TouristRoutesController.cs的GetTouristRouteById
方法
public async Task<IActionResult> GetTouristRouteById
(Guid touristRouteId, string fields)
//省略------------
return Ok(touristRouteDto.ShapeData(fields));
处理400级别错误
IPropertyMappingService.cs
bool IsPropertiesExists<T>(string fields);
实现接口(PropertyMappingService.cs)
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;
}
修改TouristRoutesController.cs的GerTouristRoutes
方法
if (!_propertyMappingService
.IsPropertiesExists<TouristRouteDto>(paramaters.Fields))
{
return BadRequest("请输入正确的塑性参数");
}
十七、 【极致RESTful风格】HATEOAS API的超媒体进化
目标
HATEOAS
- 超媒体即应用状态引擎
- 打破了客户端和服务器之间严格的契约
- REST服务本身的演化和更新也变得更加容易
HATEOAS示例
link是HATEOAS的核心
使用HATOEAS处理单一资源
新建类LinkDto.cs
namespace _04NET___CJ_ASP_Travel4.Dtos
{
public class LinkDto
{
public string Href { get; set; }
public string Rel { get; set; }
public string Method { get; set; }
public LinkDto(string href, string rel, string method)
{
Href = href;
Rel = rel;
Method = method;
}
}
}
修改TouristRoutesController.cs的GetTouristRouteById
方法
var touristRouteDto = _mapper.Map<TouristRouteDto>(touristRouteFromRepo);
var linkDtos = CreateLinkForTouristRoute(touristRouteId, fields);
var result = touristRouteDto.ShapeData(fields)
as IDictionary<string, object>;
result.Add("links", linkDtos);
return Ok(result);
CreateLinkForTouristRoute
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;
}
加上函数名称UpdateTouristRoute
-
[HttpPut("{touristRouteId}", Name = "UpdateTouristRoute")]
[HttpPut("{touristRouteId}", Name = "UpdateTouristRoute")]
[Authorize(AuthenticationSchemes = "Bearer")]
[Authorize(Roles = "Admin")]
public async Task<IActionResult> UpdateTouristRoute
DeleteTouristRoute和PartiallyUpdateTouristRoute也要加上函数名称
-
[HttpPut("{touristRouteId}", Name = "UpdateTouristRoute")]
给两个action函数加上函数名称(TouristRoutePicturesController.cs)
[HttpGet("{pictureId}", Name = "GetPicture")]
//分隔线-------------------------------------
[HttpPost(Name = "CreateTouristRoutePicture")]
[Authorize(AuthenticationSchemes = "Bearer")]
[Authorize(Roles = "Admin")]
在POST请求中复用创建link组件
修改TouristRoutesController.cs的CreateTouristRoute
方法
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
);
使用HATOEAS处理列表资源
新建私有方法CreateLinksForTouristRouteList
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;
}
增加枚举类型CurrnetPage(ResourceUriType.cs)
namespace _04NET___CJ_ASP_Travel4.Helper
{
public enum ResourceUriType
{
PreviousPage,
NextPage,
CurrnetPage
}
}
给CreateTouristRoute方法添加Name
-
[HttpPost(Name = "CreateTouristRoute")]
[HttpPost(Name = "CreateTouristRoute")]
[Authorize(AuthenticationSchemes = "Bearer")]
[Authorize]
public async Task<IActionResult> CreateTouristRoute([FromBody] TouristRouteForCreationDto touristRouteForCreationDto)
修改TouristRoutesController.cs的GetTouristRoutes
方法
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);
给项目添加API根文档
新建RootController.cs
namespace _04NET___CJ_ASP_Travel4.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);
}
}
}
ShoppingCartController.cs
给GetShoppingCart方法加上Name
-
[HttpGet(Name = "GetShoppingCart")]
[HttpGet(Name = "GetShoppingCart")]
[Authorize(AuthenticationSchemes = "Bearer")]
public async Task<IActionResult> GetShoppingCart()
OrdersController.cs
给GetOrders方法加上Name
-
[HttpGet(Name = "GetOrders")]
[HttpGet(Name = "GetOrders")]
[Authorize(AuthenticationSchemes = "Bearer")]
public async Task<IActionResult> GetOrders([FromQuery] PaginationResourceParamaters paramaters)
访问RootController.cs的根目录获得api的使用方式
[
{
// 自我链接
"href": "https://localhost:44381/api",
"rel": "self",
"method": "GET"
},
{
// 一级链接 旅游路线 “GET api/touristRoutes”
"href": "https://localhost:44381/api/TouristRoutes",
"rel": "get_tourist_routes",
"method": "GET"
},
{
// 一级链接 旅游路线 “POST api/touristRoutes”
"href": "https://localhost:44381/api/TouristRoutes",
"rel": "create_tourist_route",
"method": "POST"
},
{
// 一级链接 订单 “GET api/shoppingCart”
"href": "https://localhost:44381/api/shoppingCart",
"rel": "get_shopping_cart",
"method": "GET"
},
{
// 一级链接 购物车 “GET api/orders”
"href": "https://localhost:44381/api/orders",
"rel": "get_orders",
"method": "GET"
}
]
媒体类型
存在问题
媒体类型
通用结构
常见类型
自定义媒体类型
HATOEAS与请求媒体类型
内容协商,配置自定义Content-Type
Vendor-specific media type供应商特定媒体类型
GerTouristRoutes方法添加Produces
// api/touristRoutes?keyword=传入的参数
// 1. application/json -> 旅游路线资源
// 2. application/vnd.aleks.hateoas+json
// 3. application/vnd.aleks.touristRoute.simplify+json -> 输出简化版资源数据
// 4. application/vnd.aleks.touristRoute.simplify.hateoas+json -> 输出简化版hateoas超媒体资源数据
[Produces(
"application/json",
"application/vnd.aleks.hateoas+json",
"application/vnd.aleks.touristRoute.simplify+json",
"application/vnd.aleks.touristRoute.simplify.hateoas+json"
)]
[HttpGet(Name = "GetTouristRoutes")]
[HttpHead]
public async Task<IActionResult> GerTouristRoutes
Startup.cs
配置MvcOptions
services.Configure<MvcOptions>(config =>
{
var outputFormatter = config.OutputFormatters
.OfType<NewtonsoftJsonOutputFormatter>()?.FirstOrDefault();
if (outputFormatter != null)
{
outputFormatter.SupportedMediaTypes
.Add("application/vnd.aleks.hateoas+json");
}
});
完整TouristRoutesController.cs代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using AutoMapper;
using Microsoft.AspNetCore.JsonPatch;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.Net.Http.Headers;
using System.Dynamic;
using _04NET___CJ_ASP_Travel4.Dtos;
using _04NET___CJ_ASP_Travel4.Helper;
using _04NET___CJ_ASP_Travel4.Models;
using _04NET___CJ_ASP_Travel4.ResourceParameters;
using _04NET___CJ_ASP_Travel4.Services;
using _04NET___CJ_ASP_Travel4.API.Dtos;
namespace FakeXiecheng.API.Controllers
{
[Route("api/[controller]")] // api/touristroute
[ApiController]
public class TouristRoutesController : ControllerBase
{
private ITouristRouteRepository _touristRouteRepository;
private readonly IMapper _mapper;
private readonly IUrlHelper _urlHelper;
private readonly IPropertyMappingService _propertyMappingService;
public TouristRoutesController(
ITouristRouteRepository touristRouteRepository,
IMapper mapper,
IUrlHelperFactory urlHelperFactory,
IActionContextAccessor actionContextAccessor,
IPropertyMappingService propertyMappingService
)
{
_touristRouteRepository = touristRouteRepository;
_mapper = mapper;
_urlHelper = urlHelperFactory.GetUrlHelper(actionContextAccessor.ActionContext);
_propertyMappingService = propertyMappingService;
}
private string GenerateTouristRouteResourceURL(
TouristRouteResourceParamaters paramaters,
PaginationResourceParamaters paramaters2,
ResourceUriType type
)
{
return type switch
{
ResourceUriType.PreviousPage => _urlHelper.Link("GetTouristRoutes",
new
{
fields = paramaters.Fields,
orderBy = paramaters.OrderBy,
keyword = paramaters.Keyword,
rating = paramaters.Rating,
pageNumber = paramaters2.PageNumber - 1,
pageSize = paramaters2.PageSize
}),
ResourceUriType.NextPage => _urlHelper.Link("GetTouristRoutes",
new
{
fields = paramaters.Fields,
orderBy = paramaters.OrderBy,
keyword = paramaters.Keyword,
rating = paramaters.Rating,
pageNumber = paramaters2.PageNumber + 1,
pageSize = paramaters2.PageSize
}),
_ => _urlHelper.Link("GetTouristRoutes",
new
{
fields = paramaters.Fields,
orderBy = paramaters.OrderBy,
keyword = paramaters.Keyword,
rating = paramaters.Rating,
pageNumber = paramaters2.PageNumber,
pageSize = paramaters2.PageSize
})
};
}
// api/touristRoutes?keyword=传入的参数
// 1. application/json -> 旅游路线资源
// 2. application/vnd.aleks.hateoas+json
// 3. application/vnd.aleks.touristRoute.simplify+json -> 输出简化版资源数据
// 4. application/vnd.aleks.touristRoute.simplify.hateoas+json -> 输出简化版hateoas超媒体资源数据
[Produces(
"application/json",
"application/vnd.aleks.hateoas+json",
"application/vnd.aleks.touristRoute.simplify+json",
"application/vnd.aleks.touristRoute.simplify.hateoas+json"
)]
[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 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));
bool isHateoas = parsedMediatype.SubTypeWithoutSuffix
.EndsWith("hateoas", StringComparison.InvariantCultureIgnoreCase);
var primaryMediaType = isHateoas
? parsedMediatype.SubTypeWithoutSuffix
.Substring(0, parsedMediatype.SubTypeWithoutSuffix.Length - 8)
: parsedMediatype.SubTypeWithoutSuffix;
//var touristRoutesDto = _mapper.Map<IEnumerable<TouristRouteDto>>(touristRoutesFromRepo);
//var shapedDtoList = touristRoutesDto.ShapeData(paramaters.Fields);
IEnumerable<object> touristRoutesDto;
IEnumerable<ExpandoObject> shapedDtoList;
if (primaryMediaType == "vnd.aleks.touristRoute.simplify")
{
touristRoutesDto = _mapper
.Map<IEnumerable<TouristRouteSimplifyDto>>(touristRoutesFromRepo);
shapedDtoList = ((IEnumerable<TouristRouteSimplifyDto>)touristRoutesDto)
.ShapeData(paramaters.Fields);
}
else
{
touristRoutesDto = _mapper
.Map<IEnumerable<TouristRouteDto>>(touristRoutesFromRepo);
shapedDtoList =
((IEnumerable<TouristRouteDto>)touristRoutesDto)
.ShapeData(paramaters.Fields);
}
if (isHateoas)
{
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);
}
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;
}
// api/touristroutes/{touristRouteId}
[HttpGet("{touristRouteId}", Name = "GetTouristRouteById")]
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);
}
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;
}
[HttpPost(Name = "CreateTouristRoute")]
[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
);
}
[HttpPut("{touristRouteId}", Name = "UpdateTouristRoute")]
[Authorize(AuthenticationSchemes = "Bearer")]
[Authorize(Roles = "Admin")]
public async Task<IActionResult> UpdateTouristRoute(
[FromRoute] Guid touristRouteId,
[FromBody] TouristRouteForUpdateDto touristRouteForUpdateDto
)
{
if (!(await _touristRouteRepository.TouristRouteExistsAsync(touristRouteId)))
{
return NotFound("旅游路线找不到");
}
var touristRouteFromRepo = await _touristRouteRepository.GetTouristRouteAsync(touristRouteId);
// 1. 映射dto
// 2. 更新dto
// 3. 映射model
_mapper.Map(touristRouteForUpdateDto, touristRouteFromRepo);
await _touristRouteRepository.SaveAsync();
return NoContent();
}
[HttpPatch("{touristRouteId}", Name = "PartiallyUpdateTouristRoute")]
[Authorize(AuthenticationSchemes = "Bearer")]
[Authorize(Roles = "Admin")]
public async Task<IActionResult> PartiallyUpdateTouristRoute(
[FromRoute] Guid touristRouteId,
[FromBody] JsonPatchDocument<TouristRouteForUpdateDto> patchDocument
)
{
if (!(await _touristRouteRepository.TouristRouteExistsAsync(touristRouteId)))
{
return NotFound("旅游路线找不到");
}
var touristRouteFromRepo = await _touristRouteRepository.GetTouristRouteAsync(touristRouteId);
var touristRouteToPatch = _mapper.Map<TouristRouteForUpdateDto>(touristRouteFromRepo);
patchDocument.ApplyTo(touristRouteToPatch, ModelState);
if (!TryValidateModel(touristRouteToPatch))
{
return ValidationProblem(ModelState);
}
_mapper.Map(touristRouteToPatch, touristRouteFromRepo);
await _touristRouteRepository.SaveAsync();
return NoContent();
}
[HttpDelete("{touristRouteId}", Name = "DeleteTouristRoute")]
[Authorize(AuthenticationSchemes = "Bearer")]
[Authorize(Roles = "Admin")]
public async Task<IActionResult> DeleteTouristRoute([FromRoute] Guid touristRouteId)
{
if (!(await _touristRouteRepository.TouristRouteExistsAsync(touristRouteId)))
{
return NotFound("旅游路线找不到");
}
var touristRoute = await _touristRouteRepository.GetTouristRouteAsync(touristRouteId);
_touristRouteRepository.DeleteTouristRoute(touristRoute);
await _touristRouteRepository.SaveAsync();
return NoContent();
}
[HttpDelete("({touristIDs})")]
[Authorize(Roles = "Admin")]
public async Task<IActionResult> DeleteByIDs(
[ModelBinder(BinderType = typeof(ArrayModelBinder))][FromRoute] IEnumerable<Guid> touristIDs)
{
if (touristIDs == null)
{
return BadRequest();
}
var touristRoutesFromRepo = await _touristRouteRepository.GetTouristRoutesByIDListAsync(touristIDs);
_touristRouteRepository.DeleteTouristRoutes(touristRoutesFromRepo);
await _touristRouteRepository.SaveAsync();
return NoContent();
}
}
}
十八、【项目上线】容器化、测试、和部署
回顾
Comments | NOTHING