从SpringBoot到DotNet_4.完善篇
第一章 分页
在开发 RESTful API 时,分页功能是非常常见的需求,尤其是在处理大量数据时。分页不仅能提高查询效率,还能改善用户体验,让前端能够逐步加载数据而不是一次性全部加载。
在第一章中,我们将探讨如何实现分页功能,并且确保分页参数通过查询字符串传递、设置合理的默认值和阈限值,并在数据库中实现高效分页。
1.简单分页
对参数进行改造,添加pageNumber
和pageSize
两个字段,并设定默认值,同时为了性能保证,当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();
}
2.模组化分页
模组化分页是一种将分页逻辑封装到一个独立模块中的技术。这种方法有许多优点,尤其是在处理大量数据时,它可以显著提高系统的性能和可维护性。在 PaginationList<T>
构造函数中,AddRange(items)
被用来将从数据库查询到的分页数据添加到当前的 PaginationList<T>
对象中。
下面这个类用于实现分页功能。它继承自 List<T>
,并添加了两个额外的属性:CurrentPage
和 PageSize
,用来存储当前页码和每页的记录数。
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);
}
4.分页导航
PC端商品、资讯内容的列表页面,通常会有个分页的功能,通过翻页等操作,用户可以跳转到其他页面查看新的内容。页码展示包括当前页码展示、当前页码相邻几个页码的展示以及首末页页码展示。
页码展示帮助用户定位内容:例如用户在某个商品的搜索结果页浏览时,看到第5页,这时还是觉得第2页的一件商品更想买,于是就可以通过点击页码2回到展示该商品的页面;这就达到了通过页码快速定位商品位置的目的,而不用逐个商品逐个商品地往回去查找该商品在哪里。
分页的子功能主要有页码展示、数据量展示以及翻页操作,分别都有各自的作用,例如内容定位、对内容的预期把控等;我们在设计分页功能时,可以根据业务需要来选择不同的构成元素。
在下面的响应中,数据列表将会出现在响应主体中,而分页的信息与数据列表彻底分开,这是由于请求使用application/json
,目的是获取资源,而分页信息并不是资源,而是元数据,所以以`metadata``的形式在header中输出。
从本质上来说分页导航属于 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);
}
第二章 资源排序
以GerTouristRoutes
为例,如果想要把排序的参数传入后端,就需要在TouristRouteResourceParamaters
中再添加一个String
类型的数据来表示以什么排序/升序或降序
public string? OrderBy { get; set; }
如果我们只想针对某个数据做排序像下面这样写死就行了,但是用户只需要像后端发送自己想要数据的请求就行了,而开发者要考虑的就多了。直接在下面代码中写if
或者switch
判断又显得我们很呆,所以这时候就可以参考Mybatis
中的动态sql。
OrderBy
是EF
给我提供的,里面又没有办法接受我们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
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 字段的映射字典。
- 逻辑步骤:
- 检查
source
、mappingDictionary
是否为null
。 - 如果
orderBy
为空,直接返回未排序的数据源。 - 解析
orderBy
字符串,将其分割为多个排序条件。 - 对每个排序条件:
- 提取属性名和排序方向(升序或降序)。
- 使用
mappingDictionary
将属性名映射到实际的 Model 字段名。 - 将排序条件拼接成一个字符串,用于
IQueryable
的排序。
- 使用生成的排序字符串对
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
数据源进行排序。
返回排序结果:返回已经排序的查询结果。
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
})
};
5.处理400级别的错误信息
在上面的排序中,对于不存在的排序字段,请求仍然会返回500
级别的错误,但是为了符合标准,这时候应该是返回400
类型的错误并提示信息。
也就是说在我们定义的字典中如果没有用户的输入,就需要返回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
第三章 数据塑性
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,
}
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));
}
4.处理400级别错误
对于不存在的字段(比如说输入一个itle
)找不到对应的字段应该返回400
类型的错误,目前返回的是500
。
在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;
}
第四章 HATEOAS
在现代的 Web 开发中,RESTful API 已经成为了与服务器交互的标准方式。然而,传统的 RESTful 设计常常要求客户端对服务器的结构和功能有严格的了解,这种紧耦合使得服务的演化和更新变得复杂。HATEOAS(Hypermedia As The Engine Of Application State)则提出了一种突破这种束缚的方式。HATEOAS 是 REST 架构的一个重要特性,它通过超媒体的使用,将应用的状态和可用操作直接嵌入到资源的表示中,使得客户端与服务器之间的契约不再严格。
HATEOAS 的实现核心在于链接(Link)。通过在资源的响应中嵌入超媒体链接,客户端可以动态地发现和访问 API 的其他部分。这些链接包含三个重要元素:href
、rel
和 method
。其中,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 方法,如 GET
、PUT
或 DELETE
。
{
"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
方法,传入了 touristRouteId
和 fields
参数。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);
}
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
);
}
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);
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);
}
}
}
4.HATEOAS与请求媒体类型
在处理 HATEOAS 时,当前面临的问题是资源数据和操作链接的混合,这种做法可能违反了 RESTful 的设计原则,因为操作(即链接)与资源数据被混合在了一起。为了解决这个问题,我们可以使用内容协商(Content Negotiation),即客户端通过 Accept
头部请求不同的响应格式,从而确保响应数据的结构符合客户端的需求。
媒体类型(Media Types),也称为 MIME 类型,是一种标准,用于表示文档、文件或字节流的性质和格式。它由主要类别(type)和子类别(subtype)组成,例如 application/json
表示标准 JSON 格式,而 application/vnd.arc.hateoas+json
是用于处理 HATEOAS 的自定义媒体类型。 在 .NET 中,MediaTypeHeaderValue
类可用于处理这些媒体类型,其中 SubType
属性可以用来获取子类型部分,从而帮助开发者根据不同的需求处理和解析响应数据。
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));
}