- 参考文章
- REST
- 常用http动词
- WebApi 在 Asp.NetCore 中的实现
- 创建WebApi项目.
- 集成Entity Framework Core操作Mysql
- 安装相关的包(为Xxxx.Infrastructure项目安装)
- 建立Entity和Context
- ConfigureService中注入EF服务
- 迁移数据库
- 迁移数据库失败, 提示
Unable to create an object of type '
Context'. For the different patterns supported at design time, see https://go.microsoft.com/fwlink/?linkid=851728 - 数据库迁移结果
- 3.2.6. 为数据库创建种子数据
- 支持https
- 支持HSTS
- 使用SerilLog
- 安装nuget包
- 添加代码
- 自行测试
- Asp.NetCore配置文件
- 默认配置文件
- 获得配置
- 自定义一个异常处理,ExceptionHandler
- 弄一个类,写一个扩展方法处理异常
- 在Configuration中使用扩展方法
- 实现数据接口类(Resource),使用AutoMapper在Resource和Entity中映射
- 9.1. 为Entity类创建对应的Resource类
- 使用 AutoMapper
- 使用FluentValidation
- 安装Nuget包
- 为每一个Resource配置验证器
- 实现Http Get(翻页,过滤,排序)
- 资源命名
- 资源应该使用名词,例
- 资源命名层次结构
- 内容协商
- 资源命名
- 翻页
- 构造翻页请求参数类
- Repository实现支持翻页请求参数的方法
- 搜索(过滤)
- 排序
- >排序思路
- 资源塑形(Resource shaping)
- HATEOAS
- 创建供应商特定媒体类型
- 判断Media Type类型
- 创建供应商特定媒体类型
- Post添加资源
- 安全性和幂等性
- 代码实现
- Delete
- PUT & PATCH
- PUT 整体更新
- PATCH
- Http常用方法总结
参考文章
- ASP.NET Core Web API 开发-RESTful API实现
- 理解HTTP幂等性
- 某站微软Mvp杨旭的Asp.NetCore WebApi的视频
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等各种标准。
常用http动词
括号中是相应的SQL命令.
- GET(SELECT) : 从服务器取出资源(一项或多项).
- POST(CREATE) : 在服务器新建一个资源.
- PUT(UPDATE) : 在服务器更新资源(客户端提供改变后的完整资源).
- PATCH(UPDATE) : 在服务器更新资源(客户端提供改变的属性).
- DELETE(DELETE) : 在服务器删除资源.
WebApi 在 Asp.NetCore 中的实现
这里以用户增删改查为例.
创建WebApi项目.
参考ASP.NET Core WebAPI 开发-新建WebAPI项目.
注意,本文建立的Asp.NetCore WebApi项目选择.net core版本是2.2,不建议使用其他版本,2.1版本下会遇到依赖文件冲突问题!所以一定要选择2.2版本的.net core.
集成Entity Framework Core操作Mysql
安装相关的包(为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
建立Entity和Context
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; } } }
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; } } }
ConfigureService中注入EF服务
services.AddDbContext<UserContext>(options => { string connString = "Server=Xxx:xxx:xxx:xxx;Database=Xxxx;Uid=root;Pwd=Xxxxx; "; options.UseMySQL(connString); });
迁移数据库
- 在Tools > NuGet Package Manager > Package Manager Console输入命令.
- Add-Migration Xxx 添加迁移.
PS : 如果迁移不想要,使用 Remove-Migration 命令删除迁移. - Update-Database 更新到数据库.
迁移数据库失败, 提示 Unable to create an object of type 'Context'. For the different patterns supported at design time, see https://go.microsoft.com/fwlink/?linkid=851728
原因应该是EfCore迁移工具不知道如何创建 DbContext
导致的.
解决方案
在DbContext
所在的项目下新建一个类:
////// 设计时DbContext的创建, 告诉EF Core迁移工具如何创建DbContext /// public class <Xxxx>ContextFactory : IDesignTimeDbContextFactory<<Xxxx>Context> { public <Xxxx>Context CreateDbContext(string[] args) { var optionsBuilder = new DbContextOptionsBuilder<<Xxxx>Context>(); optionsBuilder.UseMySql( @"Server=[服务器ip];Database=[数据库]];Uid=[用户名];Pwd=[密码];"); return new <Xxxx>Context(optionsBuilder.Options); } }
数据库迁移结果
3.2.6. 为数据库创建种子数据
-
写一个创建种子数据的类
UserContextSeednamespace 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.MainIWebHost 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().Property(p => p.Guid)
.HasColumnType("nvarchar(256)");
modelBuilder.Entity().HasKey(u => u.Guid);
base.OnModelCreating(modelBuilder);
}
支持https
将所有http请求全部映射到https
Startup中:
ConfigureServices方法注册,并配置端口和状态码等:
services.AddHttpsRedirection(…)
services.AddHttpsRedirection(options =>
{
options.RedirectStatusCode = StatusCodes.Status307TemporaryRedirect;
options.HttpsPort = 5001;
});
Configure方法使用该中间件:
app.UseHttpsRedirection()
支持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() 方法之后.
使用SerilLog
有关日志的微软官方文档
SerilLog github仓库
该github仓库上有详细的使用说明.
使用方法:
安装nuget包
- Serilog.AspNetCore
- Serilog.Sinks.Console
添加代码
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; }
自行测试
Asp.NetCore配置文件
默认配置文件
默认 appsettings.json
ConfigurationBuilder().AddJsonFile("appsettings.json").Build()-->IConfigurationRoot(IConfiguration)
获得配置
IConfiguration[“Key:ChildKey”]
针对”ConnectionStrings:xxx”,可以使用IConfiguration.GetConnectionString(“xxx”)
private static IConfiguration Configuration { get; set; }
public StartupDevelopment(IConfiguration config)
{
Configuration = config;
}
...
Configuration[“Key:ChildKey”]
自定义一个异常处理,ExceptionHandler
弄一个类,写一个扩展方法处理异常
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."); })); } } }
在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(); //使用默认路由 }
实现数据接口类(Resource),使用AutoMapper在Resource和Entity中映射
9.1. 为Entity类创建对应的Resource类
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; } } }
使用 AutoMapper
-
添加nuget包
AutoMapper
AutoMapper.Extensions.Microsoft.DependencyInjection
-
配置映射
可以创建Profile
CreateMap() MappingProfilenamespace 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()
使用FluentValidation
FluentValidation官网
安装Nuget包
- FluentValidation
- FluentValidation.AspNetCore
为每一个Resource配置验证器
-
继承于AbstractValidator
ApiUserResourceValidatornamespace 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
, ApiUserResourceValidator>();
实现Http 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); }
资源命名
资源应该使用名词,例
- api/getusers就是不正确的.
- GET api/users就是正确的
资源命名层次结构
- 例如
api/department/{departmentId}/emoloyees
, 这就表示了department
(部门)和员工
(employee)之前是主从关系. - 而
api/department/{departmentId}/emoloyees/{employeeId}
,就表示了该部门下的某个员
工.
内容协商
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());
翻页
构造翻页请求参数类
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; } } }
namespace ApiStudy.Core.Entities { public class ApiUserParameters:QueryParameters { public string UserName { get; set; } } }
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);
... [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中据说已经修复这个问题.
搜索(过滤)
修改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); }
排序
>排序思路
- 需要安装System.Linq.Dynamic.Core
思路:
- PropertyMappingContainer
- PropertyMapping(ApiUserPropertyMapping)
- MappedProperty
- PropertyMapping(ApiUserPropertyMapping)
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; } } }
namespace ApiStudy.Infrastructure.Services { using System.Collections.Generic; public interface IPropertyMapping { Dictionary<string, List<MappedProperty>> MappingDictionary { get; } } }
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; } } }
namespace ApiStudy.Infrastructure.Services { public interface IPropertyMappingContainer { void Register<T>() where T : IPropertyMapping, new(); IPropertyMapping Resolve<TSource, TDestination>(); bool ValidateMappingExistsFor<TSource, TDestination>(string fields); } }
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; } } }
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; } } }
[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); }
资源塑形(Resource shaping)
返回 资源的指定字段
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; } } }
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; } } }
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; } } }
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; } } }
[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
services.AddMvc(options => { options.ReturnHttpNotAcceptable = true; options.OutputFormatters.Add(new XmlDataContractSerializerOutputFormatter()); }) .AddJsonOptions(options => { //added code options.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver(); });
HATEOAS
REST里最复杂的约束,构建成熟RESTAPI的核心
- 可进化性,自我描述
- 超媒体(Hypermedia,例如超链接)驱动如何消
费和使用API
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 = apiUserResources.AsQueryable().ApplySort( parameters.OrderBy, _propertyMappingContainer.Resolve<ApiUserResource, ApiUser>()); IEnumerable<ExpandoObject> shapedApiUserResources = sortedApiUserResources.ToDynamicObject(parameters.Fields); IEnumerable<ExpandoObject> shapedApiUserResourcesWithLinks = shapedApiUserResources.Select( x => { IDictionary<string, object> dict = x as IDictionary<string, object>; if(dict.Keys.Contains("guid")) dict.Add("links", CreateLinksForApiUser(dict["guid"] as string)); return dict as ExpandoObject; }); var result = new { value = shapedApiUserResourcesWithLinks, links = CreateLinksForApiUsers(parameters, apiUsers.HasPreviousPage, apiUsers.HasNextPage) }; 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(result); }
创建供应商特定媒体类型
- application/vnd.mycompany.hateoas+json
- vnd是vendor的缩写,这一条是mime type的原则,表示这个媒体类型是供应商特定的
- 自定义的标识,也可能还包括额外的值,这里我是用的是公司名,随后是hateoas表示返回的响应里面要
包含链接 - “+json”
- 在Startup里注册.
判断Media Type类型
- [FromHeader(Name = "Accept")] stringmediaType
//Startup.ConfigureServices 中注册媒体类型 services.AddMvc(options => { options.ReturnHttpNotAcceptable = true; //options.OutputFormatters.Add(new XmlDataContractSerializerOutputFormatter()); JsonOutputFormatter formatter = options.OutputFormatters.OfType<JsonOutputFormatter>().FirstOrDefault(); formatter.SupportedMediaTypes.Add("application/vnd.laggage.hateoas+json"); }) // get方法中判断媒体类型 if (mediaType == "application/json") return Ok(shapedApiUserResources); else if (mediaType == "application/vnd.laggage.hateoas+json") { ... return; }
注意,要是的 Action 认识 application/vnd.laggage.hateoss+json ,需要在Startup.ConfigureServices中注册这个媒体类型,上面的代码给出了具体操作.
[HttpGet(Name = "GetAllApiUsers")] public async Task<IActionResult> GetAllApiUsers(ApiUserParameters parameters,[FromHeader(Name = "Accept")] string mediaType) { 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>()); IEnumerable<ExpandoObject> shapedApiUserResources = sortedApiUserResources.ToDynamicObject(parameters.Fields); if (mediaType == "application/json") return Ok(shapedApiUserResources); else if (mediaType == "application/vnd.laggage.hateoas+json") { IEnumerable<ExpandoObject> shapedApiUserResourcesWithLinks = shapedApiUserResources.Select( x => { IDictionary<string, object> dict = x as IDictionary<string, object>; if (dict.Keys.Contains("guid")) dict.Add("links", CreateLinksForApiUser( dict.FirstOrDefault( a => string.Equals( a.Key,"guid",StringComparison.OrdinalIgnoreCase)) .Value.ToString())); return dict as ExpandoObject; }); var result = new { value = shapedApiUserResourcesWithLinks, links = CreateLinksForApiUsers(parameters, apiUsers.HasPreviousPage, apiUsers.HasNextPage) }; var meta = new { apiUsers.PageIndex, apiUsers.PageSize, apiUsers.PageCount, apiUsers.TotalItemsCount, }; Response.Headers.Add( "X-Pagination", JsonConvert.SerializeObject( meta, new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() })); return Ok(result); } return NotFound($"Can't find resources for the given media type: [{mediaType}]."); } [HttpGet("{guid}",Name = "GetApiUser")] public async Task<IActionResult> Get(string guid, [FromHeader(Name = "Accept")] string mediaType , string fields = null) { if (!_typeHelper.HasProperties<ApiUserResource>(fields)) return BadRequest("fields not exist."); ApiUser apiUser = await _apiUserRepository.GetApiUserByGuidAsync(Guid.Parse(guid)); if (apiUser is null) return NotFound(); ApiUserResource apiUserResource = _mapper.Map<ApiUser, ApiUserResource>(apiUser); ExpandoObject shapedApiUserResource = apiUserResource.ToDynamicObject(fields); if (mediaType == "application/json") return Ok(shapedApiUserResource); else if(mediaType == "application/vnd.laggage.hateoas+json") { IDictionary<string, object> shapedApiUserResourceWithLink = shapedApiUserResource as IDictionary<string, object>; shapedApiUserResourceWithLink.Add("links", CreateLinksForApiUser(guid, fields)); return Ok(shapedApiUserResourceWithLink); } return NotFound(@"Can't find resource for the given media type: [{mediaType}]."); }
- 自定义Action约束.
[AttributeUsage(AttributeTargets.All, Inherited = true, AllowMultiple = true)] public class RequestHeaderMatchingMediaTypeAttribute : Attribute, IActionConstraint { private readonly string _requestHeaderToMatch; private readonly string[] _mediaTypes; public RequestHeaderMatchingMediaTypeAttribute(string requestHeaderToMatch, string[] mediaTypes) { _requestHeaderToMatch = requestHeaderToMatch; _mediaTypes = mediaTypes; } public bool Accept(ActionConstraintContext context) { var requestHeaders = context.RouteContext.HttpContext.Request.Headers; if (!requestHeaders.ContainsKey(_requestHeaderToMatch)) { return false; } foreach (var mediaType in _mediaTypes) { var mediaTypeMatches = string.Equals(requestHeaders[_requestHeaderToMatch].ToString(), mediaType, StringComparison.OrdinalIgnoreCase); if (mediaTypeMatches) { return true; } } return false; } public int Order { get; } = 0; }
[HttpGet(Name = "GetAllApiUsers")] [RequestHeaderMatchingMediaType("Accept",new string[] { "application/vnd.laggage.hateoas+json" })] public async Task<IActionResult> GetHateoas(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 = apiUserResources.AsQueryable().ApplySort( parameters.OrderBy, _propertyMappingContainer.Resolve<ApiUserResource, ApiUser>()); IEnumerable<ExpandoObject> shapedApiUserResources = sortedApiUserResources.ToDynamicObject(parameters.Fields); IEnumerable<ExpandoObject> shapedApiUserResourcesWithLinks = shapedApiUserResources.Select( x => { IDictionary<string, object> dict = x as IDictionary<string, object>; if (dict.Keys.Contains("guid")) dict.Add("links", CreateLinksForApiUser( dict.FirstOrDefault( a => string.Equals( a.Key,"guid",StringComparison.OrdinalIgnoreCase)) .Value.ToString())); return dict as ExpandoObject; }); var result = new { value = shapedApiUserResourcesWithLinks, links = CreateLinksForApiUsers(parameters, apiUsers.HasPreviousPage, apiUsers.HasNextPage) }; var meta = new { apiUsers.PageIndex, apiUsers.PageSize, apiUsers.PageCount, apiUsers.TotalItemsCount, }; Response.Headers.Add( "X-Pagination", JsonConvert.SerializeObject( meta, new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() })); return Ok(result); } [HttpGet(Name = "GetAllApiUsers")] [RequestHeaderMatchingMediaType("Accept",new string[] { "application/json" })] public async Task<IActionResult> Get(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 = apiUserResources.AsQueryable().ApplySort( parameters.OrderBy, _propertyMappingContainer.Resolve<ApiUserResource, ApiUser>()); IEnumerable<ExpandoObject> shapedApiUserResources = sortedApiUserResources.ToDynamicObject(parameters.Fields); return Ok(shapedApiUserResources); } [HttpGet("{guid}", Name = "GetApiUser")] [RequestHeaderMatchingMediaType("Accept", new string[] { "application/vnd.laggage.hateoas+json" })] public async Task<IActionResult> GetHateoas(string guid, string fields = null) { if (!_typeHelper.HasProperties<ApiUserResource>(fields)) return BadRequest("fields not exist."); ApiUser apiUser = await _apiUserRepository.GetApiUserByGuidAsync(Guid.Parse(guid)); if (apiUser is null) return NotFound(); ApiUserResource apiUserResource = _mapper.Map<ApiUser, ApiUserResource>(apiUser); ExpandoObject shapedApiUserResource = apiUserResource.ToDynamicObject(fields); IDictionary<string, object> shapedApiUserResourceWithLink = shapedApiUserResource as IDictionary<string, object>; shapedApiUserResourceWithLink.Add("links", CreateLinksForApiUser(guid, fields)); return Ok(shapedApiUserResourceWithLink); } [HttpGet("{guid}", Name = "GetApiUser")] [RequestHeaderMatchingMediaType("Accept", new string[] { "application/json" })] public async Task<IActionResult> Get(string guid, string fields = null) { if (!_typeHelper.HasProperties<ApiUserResource>(fields)) return BadRequest("fields not exist."); ApiUser apiUser = await _apiUserRepository.GetApiUserByGuidAsync(Guid.Parse(guid)); if (apiUser is null) return NotFound(); ApiUserResource apiUserResource = _mapper.Map<ApiUser, ApiUserResource>(apiUser); ExpandoObject shapedApiUserResource = apiUserResource.ToDynamicObject(fields); return Ok(shapedApiUserResource); }
Post添加资源
Post - 不安全,非幂等
要返回添加好的资源,并且返回头中有获得新创建资源的连接.
安全性和幂等性
代码实现
StartUp中注册Fluent,用于验证
services.AddMvc(...) .AddFluentValidation(); services.AddTransient<IValidator<ApiUserAddResource>, ApiUserAddResourceValidator>();
namespace ApiStudy.Infrastructure.Resources { using FluentValidation; public class ApiUserAddResourceValidator : AbstractValidator<ApiUserAddResource> { public ApiUserAddResourceValidator() { RuleFor(x => x.Name) .MaximumLength(20) .WithName("用户名") .WithMessage("{PropertyName}的最大长度为20!") .NotNull() .WithMessage("{PropertyName}是必填的!") .NotEmpty() .WithMessage("{PropertyName}不能为空!"); RuleFor(x => x.Passwd) .NotNull() .WithName("密码") .WithMessage("{PropertyName}是必填的!") .MinimumLength(6) .WithMessage("{PropertyName}的最小长度是6") .MaximumLength(16) .WithMessage("{PropertyName}的最大长度是16"); RuleFor(x => x.PhoneNumber) .NotNull() .WithName("电话") .WithMessage("{PropertyName}是必填的!") .NotEmpty() .WithMessage("{PropertyName}不能为空!"); } } }
[HttpPost(Name = "CreateApiUser")] [RequestHeaderMatchingMediaType("Content-Type",new string[] { "application/vnd.laggage.create.apiuser+json" })] [RequestHeaderMatchingMediaType("Accept",new string[] { "application/vnd.laggage.hateoas+json" })] public async Task<IActionResult> AddUser([FromBody] ApiUserAddResource apiUser) { if (!ModelState.IsValid) return UnprocessableEntity(ModelState); ApiUser newUser = _mapper.Map<ApiUser>(apiUser); newUser.Guid = Guid.NewGuid(); newUser.ProfilePhotoUrl = $"www.eample.com/photo/{newUser.Guid}"; newUser.RegistrationDate = DateTime.Now; await _apiUserRepository.AddApiUserAsync(newUser); if (!await _unitOfWork.SaveChangesAsync()) throw new Exception("Failed to save changes"); IDictionary<string, object> shapedUserResource = _mapper.Map<ApiUserResource>(newUser) .ToDynamicObject() as IDictionary<string, object>; IEnumerable<LinkResource> links = CreateLinksForApiUser(newUser.Guid.ToString()); shapedUserResource.Add("links", links); return CreatedAtRoute("GetApiUser",new { newUser.Guid }, shapedUserResource); }
Delete
- 参数 : ID
- 幂等的
- 多次请求的副作用和单次请求的副作用是一样的.每次发送了DELETE请求之后,服务器的状态都是一样的.
- 不安全
[HttpDelete("{guid}",Name = "DeleteApiUser")] public async Task<IActionResult> DeleteApiUser(string guid) { ApiUser userToDelete = await _apiUserRepository.GetApiUserByGuidAsync(new Guid(guid)); if (userToDelete == null) return NotFound(); await _apiUserRepository.DeleteApiUserAsync(userToDelete); if (!await _unitOfWork.SaveChangesAsync()) throw new Exception("Failed to delete apiUser"); return NoContent(); }
PUT & PATCH
相关类:
namespace ApiStudy.Infrastructure.Resources { using System; public abstract class ApiUserAddOrUpdateResource { public string Name { get; set; } public string Passwd { get; set; } public DateTime Birth { get; set; } public string PhoneNumber { get; set; } public string Email { get; set; } } }
namespace ApiStudy.Infrastructure.Resources { public class ApiUserAddResource:ApiUserAddOrUpdateResource { } }
namespace ApiStudy.Infrastructure.Resources { public class ApiUserUpdateResource : ApiUserAddOrUpdateResource { } }
namespace ApiStudy.Infrastructure.Resources { using FluentValidation; public class ApiUserAddOrUpdateResourceValidator<T> : AbstractValidator<T> where T: ApiUserAddOrUpdateResource { public ApiUserAddOrUpdateResourceValidator() { RuleFor(x => x.Name) .MaximumLength(20) .WithName("用户名") .WithMessage("{PropertyName}的最大长度为20!") .NotNull() .WithMessage("{PropertyName}是必填的!") .NotEmpty() .WithMessage("{PropertyName}不能为空!"); RuleFor(x => x.Passwd) .NotNull() .WithName("密码") .WithMessage("{PropertyName}是必填的!") .MinimumLength(6) .WithMessage("{PropertyName}的最小长度是6") .MaximumLength(16) .WithMessage("{PropertyName}的最大长度是16"); RuleFor(x => x.PhoneNumber) .NotNull() .WithName("电话") .WithMessage("{PropertyName}是必填的!") .NotEmpty() .WithMessage("{PropertyName}不能为空!"); } } }
PUT 整体更新
- 返回204
- 参数
- ID,
- [FromBody]XxxxUpdateResource
[HttpPut("{guid}",Name = "PutApiUser")] public async Task<IActionResult> UpdateApiUser(string guid,[FromBody] ApiUserUpdateResource apiUserUpdateResource) { if (!ModelState.IsValid) return BadRequest(ModelState); ApiUser userToUpdate = await _apiUserRepository.GetApiUserByGuidAsync(new Guid(guid)); if (userToUpdate == null) return NotFound(); _mapper.Map(apiUserUpdateResource, userToUpdate); if (!await _unitOfWork.SaveChangesAsync()) throw new Exception("Failed to update Entity of ApiUser"); return NoContent(); }
PATCH
- Content-Type
- application/json-patch+json
- 返回204
- 参数
- ID
- [FromBody] JsonPatchDocument
- op操作
- 添加:{“op”: "add", "path": "/xxx", "value": "xxx"},如果该属性不存,那么就添加该属性,如
果属性存在,就改变属性的值。这个对静态类型不适用。 - 删除:{“op”: "remove", "path": "/xxx"},删除某个属性,或把它设为默认值(例如空值)。
- 替换:{“op”: "replace", "path": "/xxx", "value": "xxx"},改变属性的值,也可以理解为先执行
了删除,然后进行添加。 - 复制:{“op”: "copy", "from": "/xxx", "path": "/yyy"},把某个属性的值赋给目标属性。
- 移动:{“op”: "move", "from": "/xxx", "path": "/yyy"},把源属性的值赋值给目标属性,并把源
属性删除或设成默认值。 - 测试:{“op”: "test", "path": "/xxx", "value": "xxx"},测试目标属性的值和指定的值是一样的。
- 添加:{“op”: "add", "path": "/xxx", "value": "xxx"},如果该属性不存,那么就添加该属性,如
- path,资源的属性名
- 可以有层级结构
- value 更新的值
[ { "op":"replace", "path":"/name", "value":"阿黄" }, { "op":"remove", "path":"/email" } ]
[HttpPatch("{guid}",Name = "PatchApiUser")] [RequestHeaderMatchingMediaType("Content-Type",new string[] { "application/vnd.laggage.patch.apiuser+json" })] public async Task<IActionResult> UpdateApiUser( string guid,[FromBody] JsonPatchDocument<ApiUserUpdateResource> userUpdateDoc) { if (userUpdateDoc == null) return BadRequest(); ApiUser userToUpdate = await _apiUserRepository.GetApiUserByGuidAsync(new Guid(guid)); if (userToUpdate is null) return NotFound(); ApiUserUpdateResource userToUpdateResource = _mapper.Map<ApiUserUpdateResource>(userToUpdate); userUpdateDoc.ApplyTo(userToUpdateResource); _mapper.Map(userToUpdateResource, userToUpdate); if (!await _unitOfWork.SaveChangesAsync()) throw new Exception("Failed to update Entity of ApiUser"); return NoContent(); }
Http常用方法总结
- GET(获取资源):
- GET api/countries,返回200,集合数据;找不到数据返回404。
- GET api/countries/{id},返回200,单个数据;找不到返回404.
- DELETE(删除资源)
- DELETE api/countries/{id},成功204;没找到资源404。
- DELETE api/countries,很少用,也是204或者404.
- POST (创建资源):
- POST api/countries,成功返回201和单个数据;如果资源没有创建则返回404
- POST api/countries/{id},肯定不会成功,返回404或409.
- POST api/countrycollections,成功返回201和集合;没创建资源则返回404
- PUT (整体更新):
- PUT api/countries/{id},成功可以返回200,204;没找到资源则返回404
- PUT api/countries,集合操作很少见,返回200,204或404
- PATCH(局部更新):
- PATCHapi/countries/{id},200单个数据,204或者404
- PATCHapi/countries,集合操作很少见,返回200集合,204或404.