Asp.NetCoreWebApi - RESTful Api

    1. REST
    1. 常用http动词
    1. WebApi 在 Asp.NetCore 中的实现
    • 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. 为数据库创建种子数据
    1. 支持https
    1. 支持HSTS
    1. 使用SerilLog
    • 6.1. 安装nuget包
    • 6.2. 添加代码
    • 6.3. 自行测试
    1. Asp.NetCore配置文件
    • 7.1. 默认配置文件
    • 7.2. 获得配置
    1. 自定义一个异常处理,ExceptionHandler
    • 8.1. 弄一个类,写一个扩展方法处理异常
    • 8.2. 在Configuration中使用扩展方法
    1. 实现数据接口类(Resource),使用AutoMapper在Resource和Entity中映射
    • 9.1. 为Entity类创建对应的Resource类
    • 9.2. 使用 AutoMapper
    1. 使用FluentValidation
    • 10.1. 安装Nuget包
    • 10.2. 为每一个Resource配置验证器
    1. 实现Http Get(翻页,过滤,排序)
    • 11.1. 资源命名
      • 11.1.1. 资源应该使用名词,例
      • 11.1.2. 资源命名层次结构
    • 11.2. 内容协商
    1. 翻页
    • 12.1. 构造翻页请求参数类
    • 12.2. Repository实现支持翻页请求参数的方法
    • 12.3. 搜索(过滤)
    • 12.4. 排序
      • 12.4.1. 排序思路
    1. 资源塑形(Resource shaping)
    1. HATEOAS
    • 14.1. 创建供应商特定媒体类型
      • 14.1.1. 判断Media Type类型
    1. Post添加资源
    • 15.1. 安全性和幂等性
    • 15.2. 代码实现
    1. Delete
    1. PUT & PATCH
    • 17.1. PUT 整体更新
    • 17.2. PATCH
    1. Http常用方法总结

参考 :

  • ASP.NET Core Web API 开发-RESTful API实现
  • 理解HTTP幂等性
  • 某站微软Mvp杨旭的Asp.NetCore WebApi的视频

1. REST

REST : 具象状态传输(Representational State Transfer,简称REST),是Roy Thomas Fielding博士于2000年在他的博士论文 "Architectural Styles and the Design of Network-based Software Architectures" 中提出来的一种万维网软件架构风格。
目前在三种主流的Web服务实现方案中,因为REST模式与复杂的SOAPXML-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 = "yu@outlook.com"},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 = "David@outlook.com"},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 = "David@outlook.com"},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 = "Linus@outlook.com"},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 = "YouYou@outlook.com"},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 = "BaiBai@outlook.com"});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

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
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 propertyif (!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 codeif (!_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 codeIEnumerable<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 codereturn Ok(sharpedApiUserResources);
}

配置返回的json名称风格为CamelCase

StartupDevelopment.ConfigureServices
services.AddMvc(options =>{options.ReturnHttpNotAcceptable = true;options.OutputFormatters.Add(new XmlDataContractSerializerOutputFormatter());}).AddJsonOptions(options =>{//added codeoptions.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 =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);
}

14.1. 创建供应商特定媒体类型

  • application/vnd.mycompany.hateoas+json
    • vnd是vendor的缩写,这一条是mime type的原则,表示这个媒体类型是供应商特定的
    • 自定义的标识,也可能还包括额外的值,这里我是用的是公司名,随后是hateoas表示返回的响应里面要
      包含链接
    • “+json”
  • 在Startup里注册.

14.1.1. 判断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中注册这个媒体类型,上面的代码给出了具体操作.

UserContext
[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约束.
RequestHeaderMatchingMediaTypeAttribute
[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;
}
UserContext
[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);
}

15. Post添加资源

Post - 不安全,非幂等
要返回添加好的资源,并且返回头中有获得新创建资源的连接.

15.1. 安全性和幂等性

  • 安全性是指方法执行后并不会改变资源的表述
  • 幂等性是指方法无论执行多少次都会得到同样
    的结果

15.2. 代码实现

StartUp中注册Fluent,用于验证

services.AddMvc(...).AddFluentValidation();services.AddTransient<IValidator<ApiUserAddResource>, ApiUserAddResourceValidator>();
ApiStudy.Infrastructure.Resources.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}不能为空!");}}
}
UserContext.AddApiUser()
[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);
}

16. Delete

  • 参数 : ID
  • 幂等的
    • 多次请求的副作用和单次请求的副作用是一样的.每次发送了DELETE请求之后,服务器的状态都是一样的.
  • 不安全
ApiStudy.Api.Controllers.UserController
[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();
}

17. PUT & PATCH

相关类:

ApiStudy.Infrastructure.Resources.ApiUserAddOrUpdateResource
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; }}
}
ApiStudy.Infrastructure.Resources.ApiUserAddResource
namespace ApiStudy.Infrastructure.Resources
{public class ApiUserAddResource:ApiUserAddOrUpdateResource{}
}
ApiStudy.Infrastructure.Resources.ApiUserUpdateResource
namespace ApiStudy.Infrastructure.Resources
{public class ApiUserUpdateResource : ApiUserAddOrUpdateResource{}
}
ApiStudy.Infrastructure.Resources.ApiUserAddOrUpdateResourceValidator
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}不能为空!");}}
}

17.1. PUT 整体更新

  • 返回204
  • 参数
    • ID,
    • [FromBody]XxxxUpdateResource
ApiStudy.Api.Controllers.UpdateApiUser
[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();
}

17.2. 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"},测试目标属性的值和指定的值是一样的。
  • path,资源的属性名
    • 可以有层级结构
  • value 更新的值

[
{
"op":"replace",
"path":"/name",
"value":"阿黄"
},
{
"op":"remove",
"path":"/email"
}
]

ApiStudy.Api.Controllers.UserContext.UpdateApiUser
[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();
}

18. 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.

转载于:https://www.cnblogs.com/Laggage/p/11117768.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/392952.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

android动画影子效果,Android TV常用动画的效果,View选中变大且有阴影(手机也能用)...

因为电视屏幕比较大&#xff0c;而我们看电视时距离电视有一定距离&#xff0c;这样就需要动画效果比较明显&#xff0c;这个动画就是应用最广泛的&#xff0c;因为很酷&#xff0c;呵呵&#xff0c;你懂得&#xff0c;看了就知道。效果如下图&#xff1a;public class MainAct…

linux 高可用----keepalived+lvs

什么是高可用&#xff1f; HA&#xff08;high availability&#xff09;即高可用性&#xff1b;就是在高可用集群中发生单点故障时&#xff0c;能够自动转移资源并切换服务&#xff0c;以保证服务一直在线的机制。 LVS LVS&#xff1a;&#xff08;linux virtual server&#…

用户配置相关文件

用户配置相关文件小总结 /etc/passwd 记录用户相关的信息 /etc/shadow 密码影子文件 /etc/group 记录用户组相关的信息 /etc/gshadow 密码影子文件&#xff08;组密码&#xff09; /etc/passwd 文件中各段的内容 第1段&#xff1a;用户名 第…

华为5c android n风格,华为荣耀畅玩5C的屏幕怎么样

华为荣耀畅玩5C的屏幕怎么样屏幕方面&#xff0c;华为荣耀畅玩5C采用了5.2英寸1080P级别GFF贴合屏幕&#xff0c;塑料边框采用了弧面状的设计&#xff0c;握感比较舒适。华为荣耀畅玩5C采用了双主天线的设计&#xff0c;分别在上下的塑料区域。此外&#xff0c;边框以及后盖的上…

spring解析配置文件(三)

一、从XmlBeanDefinitionReader的registerBeanDefinitions&#xff08;doc,resource&#xff09;开始 1 protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource) 2 throws BeanDefinitionStoreException { 3 try { 4 …

使用kibana和elasticsearch日志实时绘制图表

前言&#xff1a; 此文接的是上篇&#xff0c;上次的内容是&#xff0c;用python操作elasticsearch存储&#xff0c;实现数据的插入和查询。 估计有些人一看我的标题&#xff0c;以为肯定是 logstash kibana elasticsearch的组合。这三个家伙也确实总是勾搭在一块。 其实logst…

android 网络调试 源代码,Android源代码调试环境搭建

我们在调试Android应用程序的时候&#xff0c;有时候遇到一些莫名其妙的问题&#xff0c;因此我们需要查看Android内部是如何调用的。我们都知道Android是一个伟大的开源项目&#xff0c;因此debug的时候肯定是支持源代码级别调试的。采用源代码调试&#xff0c;一方面有利于发…

如何使用HTTP压缩优化服务器

鉴于互联网上的宽带有限&#xff0c;网络管理人员任何旨在加速接入速度的努力都是有价值的。其中的一个方法就是通过HTTP压缩技术实现接入速度的加速&#xff0c;它通过减少在服务器和客户端之间传输的数据量&#xff0c;显著地提高网站的性能。数据压缩本身并不新鲜。但是&…

OpenGL研究3.0 多边形区域填充

OpenGL研究3.0 多边形区域填充 DionysosLai(906391500qq.com)2014-06-22 所谓多边形区域填充。就是将多边形内部区域&#xff0c;所有已相同色块填充。注意&#xff1a;这里讨论的多边形是简单多边形&#xff08;即不考虑诸如五角星这样的相交多边形&#xff09;。简单多边形&a…

ping、网络抖动与丢包

基本概念&#xff1a; ping: PING指一个数据包从用户的设备发送到测速点&#xff0c;然后再立即从测速点返回用户设备的来回时间。也就是俗称的“网络延迟” 一般以毫秒&#xff08;ms&#xff09;计算 一般PING在0~100ms都是正常的速度&#xff0c;不会有较为明显的卡顿。 测试…

[luoguP2801] 教主的魔法(二分 + 分块)

传送门 以为对于这类问题线段树都能解决&#xff0c;分块比线段树菜&#xff0c;结果培训完才知道线段树是一种特殊的分块方法&#xff0c;有的分块的题线段树不能做&#xff0c;看来分块还是有必要学的。 对于这个题&#xff0c;先分块&#xff0c;然后另开一个数组对于每个块…

鸿蒙系统适配开发,捕获科技拟建立鸿蒙开发组 为区块链钱包客户适配鸿蒙系统做筹备...

遭遇美国“实体清单”封杀的第85天&#xff0c;华为“鸿蒙”横空出世&#xff01;8月9日下午&#xff0c;在华为全球开发者大会上&#xff0c;当余承东正式宣布鸿蒙系统(Harmony OS)发布的时候&#xff0c;全场掌声雷动&#xff01;世界上第一个由中国企业自主研发的全平台微内…

关于VCP(Virtual Com Port)拓展的调试经历(一)

* The Overview 前日&#xff0c;接到老板部署的任务&#xff0c;将现有的基于STM32L151与L432的LoRaWAN程序中添加USB CDC(Communication Device Class)功能&#xff0c;并枚举为VCP(Virtual Com Port)用以替代以往的串口打印。很疑惑为什么以前架构代码的时候没有添加进去。。…

三星s6 android 8.0,再见Android 8.0,三星s6全系列系统都停止了,第一代国王已经倒下了吗?...

对于Android用户而言&#xff0c;最令人兴奋的事情是系统更新&#xff0c;因为该更新意味着更流畅的体验和更加用户友好的功能. 但是&#xff0c;旧的三星S6并不是那么幸运&#xff0c;并且不再错过Android 8.0.三星s6的全系列指的是三星s6&#xff0c;三星s6 edge&#xff0c;…

Exchange 2010无法安装问题解决方法

当你在活动目录(AD)森林中安装多台全局编录服务器(GC)之后,默认情况下你会发现在AD站点里面自动生成二条站点连接,从上面的截图可以看到目前在AD森林的Default-First-Site-Name(默认站点)里面有6台GC。 从上面的截图可以看到目前只有一台叫做Sh-Site1GC(全局编录服务器)是处于运…

一加6android9玩飞车掉,解锁新速度:一加6T深度评测

解锁新速度&#xff1a;一加6T深度评测2019-11-02 14:28:595点赞2收藏4评论创作立场声明&#xff1a;我们只谈智能硬件&#xff0c;向改变生活的智能硬件Say“嗨”&#xff01;作为安卓旗舰机成员&#xff0c;一加这个品牌在玩机一类的同学手里可是大放光彩&#xff0c;各种刷机…

探讨跨域请求资源的几种方式

[转自&#xff1a;http://www.cnblogs.com/dojo-lzz/p/4265637.html] 什么是跨域JSONPproxy代理corsxdr由于浏览器同源策略&#xff0c;凡是发送请求url的协议、域名、端口三者之间任意一与当前页面地址不同即为跨域。具体可以查看下表&#xff08;来源&#xff09; JSONP 这种…

python处理excel文件(xls和xlsx)

一、xlrd和xlwt 使用之前需要需要先安装&#xff0c;windows上如果直接在cmd中运行python则需要先执行pip3 install xlrd和pip3 install xlwt&#xff0c;如果使用pycharm则需要在项目的解释器中安装这两个模块&#xff0c;File-Settings-Project:layout-Project Interpreter&a…

Mina、Netty、Twisted一起学(五):整合protobuf

protobuf是谷歌的Protocol Buffers的简称&#xff0c;用于结构化数据和字节码之间互相转换&#xff08;序列化、反序列化&#xff09;&#xff0c;一般应用于网络传输&#xff0c;可支持多种编程语言。protobuf怎样使用这里不再介绍&#xff0c;本文主要介绍在MINA、Netty、Twi…

2021年南宁二中高考成绩查询,2021广西高考圆满结束,6月23日可查询成绩

6月8日下午&#xff0c;2021年高考统考圆满结束。今年广西参加高考统考考生人数40.05万余人&#xff0c;比2020年增加了2.2万人。我区预计6月23日可查询高考成绩&#xff0c;6月24日起可陆续填报志愿&#xff0c;我区的网上咨询会将于6月25日至27日举办。▲高考结束&#xff0c…