REST & x5E38;& x7528;http& x52A8;& x8BCD; WebApi & x5728; Asp.NetCore & x4E2D;& x7684;& x5B9E;& x73B0; 3.1. & x521B;& x5EFA;WebApi& x9879;& x76EE;. 3. ...
-
- 3.1. 创建WebApi项目.
- 3.2. 集成Entity Framework Core操作Mysql
- 3.2.1. 安装相关的包(为Xxxx.Infrastructure项目安装)
- 3.2.2. 建立Entity和Context
- 3.2.3. ConfigureService中注入EF服务
- 3.2.4. 迁移数据库
- 3.2.5. 数据库迁移结果
- 3.2.6. 为数据库创建种子数据
-
- 8.1. 弄一个类,写一个扩展方法处理异常
- 8.2. 在Configuration中使用扩展方法
-
- 9.1. 为Entity类创建对应的Resource类
- 9.2. 使用 AutoMapper
-
- 10.1. 安装Nuget包
- 10.2. 为每一个Resource配置验证器
-
- 11.1. 资源命名
- 11.1.1. 资源应该使用名词,例
- 11.1.2. 资源命名层次结构
- 11.2. 内容协商
- 11.1. 资源命名
-
- 12.1. 构造翻页请求参数类
- 12.2. Repository实现支持翻页请求参数的方法
- 12.3. 搜索(过滤)
- 12.4. 排序
- 12.4.1. 排序思路
-
- 14.1. 创建供应商特定媒体类型
- 14.1.1. 判断Media Type类型
- 14.1. 创建供应商特定媒体类型
参考 :
1. REST
REST : 具象状态传输(Representational State Transfer,简称REST),是Roy Thomas Fielding博士于2000年在他的博士论文 "Architectural Styles and the Design of Network-based Software Architectures" 中提出来的一种万维网软件架构风格。
目前在三种主流的Web服务实现方案中,因为REST模式与复杂的SOAP和XML-RPC相比更加简洁,越来越多的web服务开始采用REST风格设计和实现。例如,Amazon.com提供接近REST风格的Web服务执行图书查询;
符合REST设计风格的Web API称为RESTful API。它从以下三个方面资源进行定义:
- 直观简短的资源地址:URI,比如:http://example.com/resources/ .
- 传输的资源:Web服务接受与返回的互联网媒体类型,比如:JSON,XML,YAML等...
- 对资源的操作:Web服务在该资源上所支持的一系列请求方法(比如:POST,GET,PUT或DELETE).
PUT和DELETE方法是幂等方法.GET方法是安全方法(不会对服务器端有修改,因此当然也是幂等的).
ps 关于幂等方法 :
看这篇 理解HTTP幂等性.
简单说,客户端多次请求服务端返回的结果都相同,那么就说这个操作是幂等的.(个人理解,详细的看上面给的文章)
不像基于SOAP的Web服务,RESTful Web服务并没有“正式”的标准。这是因为REST是一种架构,而SOAP只是一个协议。虽然REST不是一个标准,但大部分RESTful Web服务实现会使用HTTP、URI、JSON和XML等各种标准。
2. 常用http动词
括号中是相应的SQL命令.
- GET(SELECT) : 从服务器取出资源(一项或多项).
- POST(CREATE) : 在服务器新建一个资源.
- PUT(UPDATE) : 在服务器更新资源(客户端提供改变后的完整资源).
- PATCH(UPDATE) : 在服务器更新资源(客户端提供改变的属性).
- DELETE(DELETE) : 在服务器删除资源.
3. WebApi 在 Asp.NetCore 中的实现
这里以用户增删改查为例.
3.1. 创建WebApi项目.
参考ASP.NET Core WebAPI 开发-新建WebAPI项目.
注意,本文建立的Asp.NetCore WebApi项目选择.net core版本是2.2,不建议使用其他版本,2.1版本下会遇到依赖文件冲突问题!所以一定要选择2.2版本的.net core.
3.2. 集成Entity Framework Core操作Mysql
3.2.1. 安装相关的包(为Xxxx.Infrastructure项目安装)
- Microsoft.EntityFrameworkCore.Design
- Pomelo.EntityFrameworkCore.MySql
这里注意一下,Mysql官方的包是 MySql.Data.EntityFrameworkCore
,但是这个包有bug,我在github上看到有人说有替代方案 - Pomelo.EntityFrameworkCore.MySql
,经过尝试,后者比前者好用.所有这里就选择后者了.使用前者的话可能会导致数据库迁移失败(Update的时候).
PS: Mysql文档原文:
Install the MySql.Data.EntityFrameworkCore NuGet package.
For EF Core 1.1 only: If you plan to scaffold a database, install the MySql.Data.EntityFrameworkCore.Design NuGet package as well.EFCore - MySql文档
Mysql版本要求:
Mysql版本要高于5.7
使用最新版本的Mysql Connector(2019 6/27 目前是8.x).
为Xxxx.Infrastructure项目安装EFCore相关的包:
为Xxxx.Api项目安装 Pomelo.EntityFrameworkCore.MySql
3.2.2. 建立Entity和Context
ApiUser
namespace ApiStudy.Core.Entities
{
using System;
public class ApiUser
{
public Guid Guid { get; set; }
public string Name { get; set; }
public string Passwd { get; set; }
public DateTime RegistrationDate { get; set; }
public DateTime Birth { get; set; }
public string ProfilePhotoUrl { get; set; }
public string PhoneNumber { get; set; }
public string Email { get; set; }
}
}
UserContext
namespace ApiStudy.Infrastructure.Database
{
using ApiStudy.Core.Entities;
using Microsoft.EntityFrameworkCore;
public class UserContext:DbContext
{
public UserContext(DbContextOptions<UserContext> options): base(options)
{
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<ApiUser>().HasKey(u => u.Guid);
base.OnModelCreating(modelBuilder);
}
public DbSet<ApiUser> ApiUsers { get; set; }
}
}
3.2.3. ConfigureService中注入EF服务
services.AddDbContext<UserContext>(options =>
{
string connString = "Server=Xxx:xxx:xxx:xxx;Database=Xxxx;Uid=root;Pwd=Xxxxx; ";
options.UseMySQL(connString);
});
3.2.4. 迁移数据库
- 在Tools > NuGet Package Manager > Package Manager Console输入命令.
- Add-Migration Xxx 添加迁移.
PS : 如果迁移不想要,使用 Remove-Migration 命令删除迁移. - Update-Database 更新到数据库.
3.2.5. 数据库迁移结果
3.2.6. 为数据库创建种子数据
-
写一个创建种子数据的类
UserContextSeed
namespace ApiStudy.Infrastructure.Database { using ApiStudy.Core.Entities; using Microsoft.Extensions.Logging; using System; using System.Linq; using System.Threading.Tasks; public class UserContextSeed { public static async Task SeedAsync(UserContext context,ILoggerFactory loggerFactory) { try { if (!context.ApiUsers.Any()) { context.ApiUsers.AddRange( new ApiUser { Guid = Guid.NewGuid(), Name = "la", Birth = new DateTime(1998, 11, 29), RegistrationDate = new DateTime(2019, 6, 28), Passwd = "123587", ProfilePhotoUrl = "https://www.laggage.top/", PhoneNumber = "10086", Email = "[email protected]" }, new ApiUser { Guid = Guid.NewGuid(), Name = "David", Birth = new DateTime(1995, 8, 29), RegistrationDate = new DateTime(2019, 3, 28), Passwd = "awt87495987", ProfilePhotoUrl = "https://www.laggage.top/", PhoneNumber = "1008611", Email = "[email protected]" }, new ApiUser { Guid = Guid.NewGuid(), Name = "David", Birth = new DateTime(2001, 8, 19), RegistrationDate = new DateTime(2019, 4, 25), Passwd = "awt87495987", ProfilePhotoUrl = "https://www.laggage.top/", PhoneNumber = "1008611", Email = "[email protected]" }, new ApiUser { Guid = Guid.NewGuid(), Name = "Linus", Birth = new DateTime(1999, 10, 26), RegistrationDate = new DateTime(2018, 2, 8), Passwd = "awt87495987", ProfilePhotoUrl = "https://www.laggage.top/", PhoneNumber = "17084759987", Email = "[email protected]" }, new ApiUser { Guid = Guid.NewGuid(), Name = "YouYou", Birth = new DateTime(1992, 1, 26), RegistrationDate = new DateTime(2015, 7, 8), Passwd = "grwe874864987", ProfilePhotoUrl = "https://www.laggage.top/", PhoneNumber = "17084759987", Email = "[email protected]" }, new ApiUser { Guid = Guid.NewGuid(), Name = "小白", Birth = new DateTime(1997, 9, 30), RegistrationDate = new DateTime(2018, 11, 28), Passwd = "gewa749864", ProfilePhotoUrl = "https://www.laggage.top/", PhoneNumber = "17084759987", Email = "[email protected]" }); await context.SaveChangesAsync(); } } catch(Exception ex) { ILogger logger = loggerFactory.CreateLogger<UserContextSeed>(); logger.LogError(ex, "Error occurred while seeding database"); } } } }
-
修改Program.Main方法
Program.Main
IWebHost host = CreateWebHostBuilder(args).Build(); using (IServiceScope scope = host.Services.CreateScope()) { IServiceProvider provider = scope.ServiceProvider; UserContext userContext = provider.GetService<UserContext>(); ILoggerFactory loggerFactory = provider.GetService<ILoggerFactory>(); UserContextSeed.SeedAsync(userContext, loggerFactory).Wait(); } host.Run();
这个时候运行程序会出现异常,打断点看一下异常信息:Data too long for column 'Guid' at row 1
可以猜到,Mysql的varbinary(16)放不下C# Guid.NewGuid()方法生成的Guid,所以配置一下数据库Guid字段类型为varchar(256)可以解决问题.
解决方案:
修改 UserContext.OnModelCreating 方法
配置一下 ApiUser.Guid 属性到Mysql数据库的映射:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<ApiUser>().Property(p => p.Guid)
.HasColumnType("nvarchar(256)");
modelBuilder.Entity<ApiUser>().HasKey(u => u.Guid);
base.OnModelCreating(modelBuilder);
}
4. 支持https
将所有http请求全部映射到https
Startup中:
ConfigureServices方法注册,并配置端口和状态码等:
services.AddHttpsRedirection(…)
services.AddHttpsRedirection(options =>
{
options.RedirectStatusCode = StatusCodes.Status307TemporaryRedirect;
options.HttpsPort = 5001;
});
Configure方法使用该中间件:
app.UseHttpsRedirection()
5. 支持HSTS
ConfigureServices方法注册
看官方文档
services.AddHsts(options =>
{
options.Preload = true;
options.IncludeSubDomains = true;
options.MaxAge = TimeSpan.FromDays(60);
options.ExcludedHosts.Add("example.com");
options.ExcludedHosts.Add("www.example.com");
});
Configure方法配置中间件管道
app.UseHsts();
注意 app.UseHsts() 方法最好放在 app.UseHttps() 方法之后.
6. 使用SerilLog
有关日志的微软官方文档
SerilLog github仓库
该github仓库上有详细的使用说明.
使用方法:
6.1. 安装nuget包
- Serilog.AspNetCore
- Serilog.Sinks.Console
6.2. 添加代码
Program.Main方法中:
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug()
.MinimumLevel.Override("Microsoft", LogEventLevel.Information)
.Enrich.FromLogContext()
.WriteTo.Console()
.CreateLogger();
修改Program.CreateWebHostBuilder(...)
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.UseSerilog(); // <-- Add this line;
}
6.3. 自行测试
7. Asp.NetCore配置文件
7.1. 默认配置文件
默认 appsettings.json
ConfigurationBuilder().AddJsonFile("appsettings.json").Build()-->IConfigurationRoot(IConfiguration)
7.2. 获得配置
IConfiguration[“Key:ChildKey”]
针对”ConnectionStrings:xxx”,可以使用IConfiguration.GetConnectionString(“xxx”)
private static IConfiguration Configuration { get; set; }
public StartupDevelopment(IConfiguration config)
{
Configuration = config;
}
...
Configuration[“Key:ChildKey”]
8. 自定义一个异常处理,ExceptionHandler
8.1. 弄一个类,写一个扩展方法处理异常
namespace ApiStudy.Api.Extensions
{
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using System;
public static class ExceptionHandlingExtensions
{
public static void UseCustomExceptionHandler(this IApplicationBuilder app,ILoggerFactory loggerFactory)
{
app.UseExceptionHandler(
builder => builder.Run(async context =>
{
context.Response.StatusCode = StatusCodes.Status500InternalServerError;
context.Response.ContentType = "application/json";
Exception ex = context.Features.Get<Exception>();
if (!(ex is null))
{
ILogger logger = loggerFactory.CreateLogger("ApiStudy.Api.Extensions.ExceptionHandlingExtensions");
logger.LogError(ex, "Error occurred.");
}
await context.Response.WriteAsync(ex?.Message ?? "Error occurred, but cannot get exception message.For more detail, go to see the log.");
}));
}
}
}
8.2. 在Configuration中使用扩展方法
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory)
{
app.UseCustomExceptionHandler(loggerFactory); //modified code
//app.UseDeveloperExceptionPage();
app.UseHsts();
app.UseHttpsRedirection();
app.UseMvc(); //使用默认路由
}
9. 实现数据接口类(Resource),使用AutoMapper在Resource和Entity中映射
9.1. 为Entity类创建对应的Resource类
ApiUserResource
namespace ApiStudy.Infrastructure.Resources
{
using System;
public class ApiUserResource
{
public Guid Guid { get; set; }
public string Name { get; set; }
//public string Passwd { get; set; }
public DateTime RegistrationDate { get; set; }
public DateTime Birth { get; set; }
public string ProfilePhotoUrl { get; set; }
public string PhoneNumber { get; set; }
public string Email { get; set; }
}
}
9.2. 使用 AutoMapper
-
添加nuget包
AutoMapper
AutoMapper.Extensions.Microsoft.DependencyInjection
-
配置映射
可以创建Profile
CreateMap<TSource,TDestination>()MappingProfile
namespace ApiStudy.Api.Extensions { using ApiStudy.Core.Entities; using ApiStudy.Infrastructure.Resources; using AutoMapper; using System; using System.Text; public class MappingProfile : Profile { public MappingProfile() { CreateMap<ApiUser, ApiUserResource>() .ForMember( d => d.Passwd, opt => opt.AddTransform(s => Convert.ToBase64String(Encoding.Default.GetBytes(s)))); CreateMap<ApiUserResource, ApiUser>() .ForMember( d => d.Passwd, opt => opt.AddTransform(s => Encoding.Default.GetString(Convert.FromBase64String(s)))); } } }
-
注入服务
services.AddAutoMapper()
10. 使用FluentValidation
10.1. 安装Nuget包
- FluentValidation
- FluentValidation.AspNetCore
10.2. 为每一个Resource配置验证器
-
继承于AbstractValidator
ApiUserResourceValidator
namespace ApiStudy.Infrastructure.Resources { using FluentValidation; public class ApiUserResourceValidator : AbstractValidator<ApiUserResource> { public ApiUserResourceValidator() { RuleFor(s => s.Name) .MaximumLength(80) .WithName("用户名") .WithMessage("{PropertyName}的最大长度为80") .NotEmpty() .WithMessage("{PropertyName}不能为空!"); } } }
-
注册到容器:services.AddTransient<>()
services.AddTransient<IValidator<ApiUserResource>, ApiUserResourceValidator>();
11. 实现Http Get(翻页,过滤,排序)
基本的Get实现
[HttpGet]
public async Task<IActionResult> Get()
{
IEnumerable<ApiUser> apiUsers = await _apiUserRepository.GetAllApiUsersAsync();
IEnumerable<ApiUserResource> apiUserResources =
_mapper.Map<IEnumerable<ApiUser>,IEnumerable<ApiUserResource>>(apiUsers);
return Ok(apiUserResources);
}
[HttpGet("{guid}")]
public async Task<IActionResult> Get(string guid)
{
ApiUser apiUser = await _apiUserRepository.GetApiUserByGuidAsync(Guid.Parse(guid));
if (apiUser is null) return NotFound();
ApiUserResource apiUserResource = _mapper.Map<ApiUser,ApiUserResource>(apiUser);
return Ok(apiUserResource);
}
11.1. 资源命名
11.1.1. 资源应该使用名词,例
- api/getusers就是不正确的.
- GET api/users就是正确的
11.1.2. 资源命名层次结构
- 例如
api/department/{departmentId}/emoloyees
, 这就表示了department
(部门)和员工
(employee)之前是主从关系. - 而
api/department/{departmentId}/emoloyees/{employeeId}
,就表示了该部门下的某个员
工.
11.2. 内容协商
ASP.NET Core支持输出和输入两种格式化器.
- 用于输出的media type放在Accept Header里,表示客户端接受这种格式的输出.
- 用于输入的media type放Content-Type Header里,表示客户端传进来的数据是这种格式.
- ReturnHttpNotAcceptable设为true,如果客户端请求不支持的数据格式,就会返回406.
services.AddMvc(options => { options.ReturnHttpNotAcceptable = true; });
- 支持输出XML格式:
options.OutputFormatters.Add(newXmlDataContractSerializerOutputFormatter());
12. 翻页
12.1. 构造翻页请求参数类
QueryParameters
namespace ApiStudy.Core.Entities
{
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.CompilerServices;
public abstract class QueryParameters : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private const int DefaultPageSize = 10;
private const int DefaultMaxPageSize = 100;
private int _pageIndex = 1;
public virtual int PageIndex
{
get => _pageIndex;
set => SetField(ref _pageIndex, value);
}
private int _pageSize = DefaultPageSize;
public virtual int PageSize
{
get => _pageSize;
set => SetField(ref _pageSize, value);
}
private int _maxPageSize = DefaultMaxPageSize;
public virtual int MaxPageSize
{
get => _maxPageSize;
set => SetField(ref _maxPageSize, value);
}
public string OrderBy { get; set; }
public string Fields { get; set; }
protected void SetField<TField>(
ref TField field,in TField newValue,[CallerMemberName] string propertyName = null)
{
if (EqualityComparer<TField>.Default.Equals(field, newValue))
return;
field = newValue;
if (propertyName == nameof(PageSize) || propertyName == nameof(MaxPageSize)) SetPageSize();
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
private void SetPageSize()
{
if (_maxPageSize <= 0) _maxPageSize = DefaultMaxPageSize;
if (_pageSize <= 0) _pageSize = DefaultPageSize;
_pageSize = _pageSize > _maxPageSize ? _maxPageSize : _pageSize;
}
}
}
ApiUserParameters
namespace ApiStudy.Core.Entities
{
public class ApiUserParameters:QueryParameters
{
public string UserName { get; set; }
}
}
12.2. Repository实现支持翻页请求参数的方法
Repository相关代码
/*----- ApiUserRepository -----*/
public PaginatedList<ApiUser> GetAllApiUsers(ApiUserParameters parameters)
{
return new PaginatedList<ApiUser>(
parameters.PageIndex,
parameters.PageSize,
_context.ApiUsers.Count(),
_context.ApiUsers.Skip(parameters.PageIndex * parameters.PageSize)
.Take(parameters.PageSize));
}
public Task<PaginatedList<ApiUser>> GetAllApiUsersAsync(ApiUserParameters parameters)
{
return Task.Run(() => GetAllApiUsers(parameters));
}
/*----- IApiUserRepository -----*/
PaginatedList<ApiUser> GetAllApiUsers(ApiUserParameters parameters);
Task<PaginatedList<ApiUser>> GetAllApiUsersAsync(ApiUserParameters parameters);
UserController部分代码
...
[HttpGet(Name = "GetAllApiUsers")]
public async Task<IActionResult> GetAllApiUsers(ApiUserParameters parameters)
{
PaginatedList<ApiUser> apiUsers = await _apiUserRepository.GetAllApiUsersAsync(parameters);
IEnumerable<ApiUserResource> apiUserResources =
_mapper.Map<IEnumerable<ApiUser>,IEnumerable<ApiUserResource>>(apiUsers);
var meta = new
{
PageIndex = apiUsers.PageIndex,
PageSize = apiUsers.PageSize,
PageCount = apiUsers.PageCount,
TotalItemsCount = apiUsers.TotalItemsCount,
NextPageUrl = CreateApiUserUrl(parameters, ResourceUriType.NextPage),
PreviousPageUrl = CreateApiUserUrl(parameters, ResourceUriType.PreviousPage)
};
Response.Headers.Add(
"X-Pagination",
JsonConvert.SerializeObject(
meta,
new JsonSerializerSettings
{ ContractResolver = new CamelCasePropertyNamesContractResolver() }));
return Ok(apiUserResources);
}
...
private string CreateApiUserUrl(ApiUserParameters parameters,ResourceUriType uriType)
{
var param = new ApiUserParameters
{
PageIndex = parameters.PageIndex,
PageSize = parameters.PageSize
};
switch (uriType)
{
case ResourceUriType.PreviousPage:
param.PageIndex--;
break;
case ResourceUriType.NextPage:
param.PageIndex++;
break;
case ResourceUriType.CurrentPage:
break;
default:break;
}
return Url.Link("GetAllApiUsers", parameters);
}
PS注意,为HttpGet方法添加参数的话,在.net core2.2版本下,去掉那个ApiUserController上的 [ApiController());] 特性,否则参数传不进来..net core3.0中据说已经修复这个问题.
12.3. 搜索(过滤)
修改Repository代码:
public PaginatedList<ApiUser> GetAllApiUsers(ApiUserParameters parameters)
{
IQueryable<ApiUser> query = _context.ApiUsers.AsQueryable();
query = query.Skip(parameters.PageIndex * parameters.PageSize)
.Take(parameters.PageSize);
if (!string.IsNullOrEmpty(parameters.UserName))
query = _context.ApiUsers.Where(
x => StringComparer.OrdinalIgnoreCase.Compare(x.Name, parameters.UserName) == 0);
return new PaginatedList<ApiUser>(
parameters.PageIndex,
parameters.PageSize,
query.Count(),
query);
}
12.4. 排序
12.4.1. 排序思路
- 需要安装System.Linq.Dynamic.Core
思路:
- PropertyMappingContainer
- PropertyMapping(ApiUserPropertyMapping)
- MappedProperty
- PropertyMapping(ApiUserPropertyMapping)
MappedProperty
namespace ApiStudy.Infrastructure.Services
{
public struct MappedProperty
{
public MappedProperty(string name, bool revert = false)
{
Name = name;
Revert = revert;
}
public string Name { get; set; }
public bool Revert { get; set; }
}
}
IPropertyMapping
namespace ApiStudy.Infrastructure.Services
{
using System.Collections.Generic;
public interface IPropertyMapping
{
Dictionary<string, List<MappedProperty>> MappingDictionary { get; }
}
}
PropertyMapping
namespace ApiStudy.Infrastructure.Services
{
using System.Collections.Generic;
public abstract class PropertyMapping<TSource,TDestination> : IPropertyMapping
{
public Dictionary<string, List<MappedProperty>> MappingDictionary { get; }
public PropertyMapping(Dictionary<string, List<MappedProperty>> MappingDict)
{
MappingDictionary = MappingDict;
}
}
}
IPropertyMappingContainer
namespace ApiStudy.Infrastructure.Services
{
public interface IPropertyMappingContainer
{
void Register<T>() where T : IPropertyMapping, new();
IPropertyMapping Resolve<TSource, TDestination>();
bool ValidateMappingExistsFor<TSource, TDestination>(string fields);
}
}
PropertyMappingContainer
namespace ApiStudy.Infrastructure.Services
{
using System;
using System.Linq;
using System.Collections.Generic;
public class PropertyMappingContainer : IPropertyMappingContainer
{
protected internal readonly IList<IPropertyMapping> PropertyMappings = new List<IPropertyMapping>();
public void Register<T>() where T : IPropertyMapping, new()
{
if (PropertyMappings.Any(x => x.GetType() == typeof(T))) return;
PropertyMappings.Add(new T());
}
public IPropertyMapping Resolve<TSource,TDestination>()
{
IEnumerable<PropertyMapping<TSource, TDestination>> result = PropertyMappings.OfType<PropertyMapping<TSource,TDestination>>();
if (result.Count() > 0)
return result.First();
throw new InvalidCastException(
string.Format( "Cannot find property mapping instance for {0}, {1}", typeof(TSource), typeof(TDestination)));
}
public bool ValidateMappingExistsFor<TSource, TDestination>(string fields)
{
if (string.IsNullOrEmpty(fields)) return true;
IPropertyMapping propertyMapping = Resolve<TSource, TDestination>();
string[] splitFields = fields.Split(',');
foreach(string property in splitFields)
{
string trimmedProperty = property.Trim();
int indexOfFirstWhiteSpace = trimmedProperty.IndexOf(' ');
string propertyName = indexOfFirstWhiteSpace <= 0 ? trimmedProperty : trimmedProperty.Remove(indexOfFirstWhiteSpace);
if (!propertyMapping.MappingDictionary.Keys.Any(x => string.Equals(propertyName,x,StringComparison.OrdinalIgnoreCase))) return false;
}
return true;
}
}
}
QueryExtensions
namespace ApiStudy.Infrastructure.Extensions
{
using ApiStudy.Infrastructure.Services;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Dynamic.Core;
public static class QueryExtensions
{
public static IQueryable<T> ApplySort<T>(
this IQueryable<T> data,in string orderBy,in IPropertyMapping propertyMapping)
{
if (data == null) throw new ArgumentNullException(nameof(data));
if (string.IsNullOrEmpty(orderBy)) return data;
string[] splitOrderBy = orderBy.Split(',');
foreach(string property in splitOrderBy)
{
string trimmedProperty = property.Trim();
int indexOfFirstSpace = trimmedProperty.IndexOf(' ');
bool desc = trimmedProperty.EndsWith(" desc");
string propertyName = indexOfFirstSpace > 0 ? trimmedProperty.Remove(indexOfFirstSpace) : trimmedProperty;
propertyName = propertyMapping.MappingDictionary.Keys.FirstOrDefault(
x => string.Equals(x, propertyName, StringComparison.OrdinalIgnoreCase)); //ignore case of sort property
if (!propertyMapping.MappingDictionary.TryGetValue(
propertyName, out List<MappedProperty> mappedProperties))
throw new InvalidCastException($"key mapping for {propertyName} is missing");
mappedProperties.Reverse();
foreach(MappedProperty mappedProperty in mappedProperties)
{
if (mappedProperty.Revert) desc = !desc;
data = data.OrderBy($"{mappedProperty.Name} {(desc ? "descending" : "ascending")} ");
}
}
return data;
}
}
}
UserController 部分代码
[HttpGet(Name = "GetAllApiUsers")]
public async Task<IActionResult> GetAllApiUsers(ApiUserParameters parameters)
{
if (!_propertyMappingContainer.ValidateMappingExistsFor<ApiUserResource, ApiUser>(parameters.OrderBy))
return BadRequest("can't find fields for sorting.");
PaginatedList<ApiUser> apiUsers = await _apiUserRepository.GetAllApiUsersAsync(parameters);
IEnumerable<ApiUserResource> apiUserResources =
_mapper.Map<IEnumerable<ApiUser>, IEnumerable<ApiUserResource>>(apiUsers);
IEnumerable<ApiUserResource> sortedApiUserResources =
apiUserResources.AsQueryable().ApplySort(
parameters.OrderBy, _propertyMappingContainer.Resolve<ApiUserResource, ApiUser>());
var meta = new
{
apiUsers.PageIndex,
apiUsers.PageSize,
apiUsers.PageCount,
apiUsers.TotalItemsCount,
PreviousPageUrl = apiUsers.HasPreviousPage ? CreateApiUserUrl(parameters, ResourceUriType.PreviousPage) : string.Empty,
NextPageUrl = apiUsers.HasNextPage ? CreateApiUserUrl(parameters, ResourceUriType.NextPage) : string.Empty,
};
Response.Headers.Add(
"X-Pagination",
JsonConvert.SerializeObject(
meta,
new JsonSerializerSettings
{ ContractResolver = new CamelCasePropertyNamesContractResolver() }));
return Ok(sortedApiUserResources);
}
private string CreateApiUserUrl(ApiUserParameters parameters, ResourceUriType uriType)
{
var param = new {
parameters.PageIndex,
parameters.PageSize
};
switch (uriType)
{
case ResourceUriType.PreviousPage:
param = new
{
PageIndex = parameters.PageIndex - 1,
parameters.PageSize
};
break;
case ResourceUriType.NextPage:
param = new
{
PageIndex = parameters.PageIndex + 1,
parameters.PageSize
};
break;
case ResourceUriType.CurrentPage:
break;
default: break;
}
return Url.Link("GetAllApiUsers", param);
}
13. 资源塑形(Resource shaping)
返回 资源的指定字段
ApiStudy.Infrastructure.Extensions.TypeExtensions
namespace ApiStudy.Infrastructure.Extensions
{
using System;
using System.Collections.Generic;
using System.Reflection;
public static class TypeExtensions
{
public static IEnumerable<PropertyInfo> GetProeprties(this Type source, string fields = null)
{
List<PropertyInfo> propertyInfoList = new List<PropertyInfo>();
if (string.IsNullOrEmpty(fields))
{
propertyInfoList.AddRange(source.GetProperties(BindingFlags.Public | BindingFlags.Instance));
}
else
{
string[] properties = fields.Trim().Split(',');
foreach (string propertyName in properties)
{
propertyInfoList.Add(
source.GetProperty(
propertyName.Trim(),
BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase));
}
}
return propertyInfoList;
}
}
}
ApiStudy.Infrastructure.Extensions.ObjectExtensions
namespace ApiStudy.Infrastructure.Extensions
{
using System.Collections.Generic;
using System.Dynamic;
using System.Linq;
using System.Reflection;
public static class ObjectExtensions
{
public static ExpandoObject ToDynamicObject(this object source, in string fields = null)
{
List<PropertyInfo> propertyInfoList = source.GetType().GetProeprties(fields).ToList();
ExpandoObject expandoObject = new ExpandoObject();
foreach (PropertyInfo propertyInfo in propertyInfoList)
{
try
{
(expandoObject as IDictionary<string, object>).Add(
propertyInfo.Name, propertyInfo.GetValue(source));
}
catch { continue; }
}
return expandoObject;
}
internal static ExpandoObject ToDynamicObject(this object source, in IEnumerable<PropertyInfo> propertyInfos, in string fields = null)
{
ExpandoObject expandoObject = new ExpandoObject();
foreach (PropertyInfo propertyInfo in propertyInfos)
{
try
{
(expandoObject as IDictionary<string, object>).Add(
propertyInfo.Name, propertyInfo.GetValue(source));
}
catch { continue; }
}
return expandoObject;
}
}
}
ApiStudy.Infrastructure.Extensions.IEnumerableExtensions
namespace ApiStudy.Infrastructure.Extensions
{
using System;
using System.Collections.Generic;
using System.Dynamic;
using System.Linq;
using System.Reflection;
public static class IEnumerableExtensions
{
public static IEnumerable<ExpandoObject> ToDynamicObject<T>(
this IEnumerable<T> source,in string fields = null)
{
if (source == null) throw new ArgumentNullException(nameof(source));
List<ExpandoObject> expandoObejctList = new List<ExpandoObject>();
List<PropertyInfo> propertyInfoList = typeof(T).GetProeprties(fields).ToList();
foreach(T x in source)
{
expandoObejctList.Add(x.ToDynamicObject(propertyInfoList, fields));
}
return expandoObejctList;
}
}
}
ApiStudy.Infrastructure.Services.TypeHelperServices
namespace ApiStudy.Infrastructure.Services
{
using System.Reflection;
public class TypeHelperServices : ITypeHelperServices
{
public bool HasProperties<T>(string fields)
{
if (string.IsNullOrEmpty(fields)) return true;
string[] splitFields = fields.Split(',');
foreach(string splitField in splitFields)
{
string proeprtyName = splitField.Trim();
PropertyInfo propertyInfo = typeof(T).GetProperty(
proeprtyName, BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
if (propertyInfo == null) return false;
}
return true;
}
}
}
UserContext.GetAllApiUsers(), UserContext.Get()
[HttpGet(Name = "GetAllApiUsers")]
public async Task<IActionResult> GetAllApiUsers(ApiUserParameters parameters)
{
//added code
if (!_typeHelper.HasProperties<ApiUserResource>(parameters.Fields))
return BadRequest("fields not exist.");
if (!_propertyMappingContainer.ValidateMappingExistsFor<ApiUserResource, ApiUser>(parameters.OrderBy))
return BadRequest("can't find fields for sorting.");
PaginatedList<ApiUser> apiUsers = await _apiUserRepository.GetAllApiUsersAsync(parameters);
IEnumerable<ApiUserResource> apiUserResources =
_mapper.Map<IEnumerable<ApiUser>, IEnumerable<ApiUserResource>>(apiUsers);
IEnumerable<ApiUserResource> sortedApiUserResources =
apiUserResources.AsQueryable().ApplySort(
parameters.OrderBy, _propertyMappingContainer.Resolve<ApiUserResource, ApiUser>());
//modified code
IEnumerable<ExpandoObject> sharpedApiUserResources =
sortedApiUserResources.ToDynamicObject(parameters.Fields);
var meta = new
{
apiUsers.PageIndex,
apiUsers.PageSize,
apiUsers.PageCount,
apiUsers.TotalItemsCount,
PreviousPageUrl = apiUsers.HasPreviousPage ? CreateApiUserUrl(parameters, ResourceUriType.PreviousPage) : string.Empty,
NextPageUrl = apiUsers.HasNextPage ? CreateApiUserUrl(parameters, ResourceUriType.NextPage) : string.Empty,
};
Response.Headers.Add(
"X-Pagination",
JsonConvert.SerializeObject(
meta,
new JsonSerializerSettings
{ ContractResolver = new CamelCasePropertyNamesContractResolver() }));
//modified code
return Ok(sharpedApiUserResources);
}
配置返回的json名称风格为CamelCase
StartupDevelopment.ConfigureServices
services.AddMvc(options =>
{
options.ReturnHttpNotAcceptable = true;
options.OutputFormatters.Add(new XmlDataContractSerializerOutputFormatter());
})
.AddJsonOptions(options =>
{
//added code
options.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
});
14. HATEOAS
REST里最复杂的约束,构建成熟RESTAPI的核心
- 可进化性,自我描述
- 超媒体(Hypermedia,例如超链接)驱动如何消
费和使用API
UserContext
private IEnumerable<LinkResource> CreateLinksForApiUser(string guid,string fields = null)
{
List<LinkResource> linkResources = new List<LinkResource>();
if (string.IsNullOrEmpty(fields))
{
linkResources.Add(
new LinkResource(Url.Link("GetApiUser", new { guid }), "self", "get"));
}
else
{
linkResources.Add(
new LinkResource(Url.Link("GetApiUser", new { guid, fields }), "self", "get"));
}
linkResources.Add(
new LinkResource(Url.Link("DeleteApiUser", new { guid }), "self", "Get"));
return linkResources;
}
private IEnumerable<LinkResource> CreateLinksForApiUsers(ApiUserParameters parameters,bool hasPrevious,bool hasNext)
{
List<LinkResource> resources = new List<LinkResource>();
resources.Add(
new LinkResource(
CreateApiUserUrl(parameters,ResourceUriType.CurrentPage),
"current_page", "get"));
if (hasPrevious)
resources.Add(
new LinkResource(
CreateApiUserUrl(parameters, ResourceUriType.PreviousPage),
"previous_page", "get"));
if (hasNext)
resources.Add(
new LinkResource(
CreateApiUserUrl(parameters, ResourceUriType.NextPage),
"next_page", "get"));
return resources;
}
[HttpGet(Name = "GetAllApiUsers")]
public async Task<IActionResult> GetAllApiUsers(ApiUserParameters parameters)
{
if (!_typeHelper.HasProperties<ApiUserResource>(parameters.Fields))
return BadRequest("fields not exist.");
if (!_propertyMappingContainer.ValidateMappingExistsFor<ApiUserResource, ApiUser>(parameters.OrderBy))
return BadRequest("can't find fields for sorting.");
PaginatedList<ApiUser> apiUsers = await _apiUserRepository.GetAllApiUsersAsync(parameters);
IEnumerable<ApiUserResource> apiUserResources =
_mapper.Map<IEnumerable<ApiUser>, IEnumerable<ApiUserResource>>(apiUsers);
IEnumerable<ApiUserResource> sortedApiUserResources =
apiUserR