Asp.NetCoreWebApi - RESTful Api

时间:2019-10-14
本文章向大家介绍Asp.NetCoreWebApi - RESTful Api,主要包括Asp.NetCoreWebApi - RESTful Api使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。
目录 - [参考文章](#参考文章) - [REST](#rest) - [常用http动词](#常用http动词) - [WebApi 在 Asp.NetCore 中的实现](#webapi-在-aspnetcore-中的实现) - [创建WebApi项目.](#创建webapi项目) - [集成Entity Framework Core操作Mysql](#集成entity-framework-core操作mysql) - [安装相关的包(为Xxxx.Infrastructure项目安装)](#安装相关的包为xxxxinfrastructure项目安装) - [建立Entity和Context](#建立entity和context) - [ConfigureService中注入EF服务](#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`](#迁移数据库失败-提示-unable-to-create-an-object-of-type-xxxxcontext-for-the-different-patterns-supported-at-design-time-see-httpsgomicrosoftcomfwlinklinkid851728) - [数据库迁移结果](#数据库迁移结果) - [为数据库创建种子数据](#为数据库创建种子数据) - [支持https](#支持https) - [支持HSTS](#支持hsts) - [使用SerilLog](#使用serillog) - [安装nuget包](#安装nuget包) - [添加代码](#添加代码) - [自行测试](#自行测试) - [Asp.NetCore配置文件](#aspnetcore配置文件) - [默认配置文件](#默认配置文件) - [获得配置](#获得配置) - [自定义一个异常处理,ExceptionHandler](#自定义一个异常处理exceptionhandler) - [弄一个类,写一个扩展方法处理异常](#弄一个类写一个扩展方法处理异常) - [在Configuration中使用扩展方法](#在configuration中使用扩展方法) - [实现数据接口类(Resource),使用AutoMapper在Resource和Entity中映射](#实现数据接口类resource使用automapper在resource和entity中映射) - [为Entity类创建对应的Resource类](#为entity类创建对应的resource类) - [使用 AutoMapper](#使用-automapper) - [使用FluentValidation](#使用fluentvalidation) - [安装Nuget包](#安装nuget包) - [为每一个Resource配置验证器](#为每一个resource配置验证器) - [实现Http Get(翻页,过滤,排序)](#实现http-get翻页过滤排序) - [资源命名](#资源命名) - [资源应该使用名词,例](#资源应该使用名词例) - [资源命名层次结构](#资源命名层次结构) - [内容协商](#内容协商) - [翻页](#翻页) - [构造翻页请求参数类](#构造翻页请求参数类) - [Repository实现支持翻页请求参数的方法](#repository实现支持翻页请求参数的方法) - [搜索(过滤)](#搜索过滤) - [排序](#排序) - [>排序思路](#排序思路) - [资源塑形(Resource shaping)](#资源塑形resource-shaping) - [HATEOAS](#hateoas) - [创建供应商特定媒体类型](#创建供应商特定媒体类型) - [判断Media Type类型](#判断media-type类型) - [Post添加资源](#post添加资源) - [安全性和幂等性](#安全性和幂等性) - [代码实现](#代码实现) - [Delete](#delete) - [PUT & PATCH](#put--patch) - [PUT 整体更新](#put-整体更新) - [PATCH](#patch) - [Http常用方法总结](#http常用方法总结)

参考文章


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等各种标准。

常用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

ApiUser ```CSharp 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 ```CSharp namespace ApiStudy.Infrastructure.Database { using ApiStudy.Core.Entities; using Microsoft.EntityFrameworkCore; public class UserContext:DbContext { public UserContext(DbContextOptions options): base(options) { } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity().HasKey(u => u.Guid); base.OnModelCreating(modelBuilder); } public DbSet 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 '<Xxxx>Context'. For the different patterns supported at design time, see https://go.microsoft.com/fwlink/?linkid=851728

原因应该是EfCore迁移工具不知道如何创建 DbContext 导致的.

解决方案

DbContext所在的项目下新建一个类:

/// <summary>
/// 设计时DbContext的创建, 告诉EF Core迁移工具如何创建DbContext
/// </summary>
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);
    }
}

数据库迁移结果

为数据库创建种子数据

  • 写一个创建种子数据的类


    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);
}

支持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中映射

为Entity类创建对应的Resource类

ApiUserResource ```CSharp 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<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()

使用FluentValidation

FluentValidation官网

安装Nuget包

  • FluentValidation
  • FluentValidation.AspNetCore

为每一个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>();

实现Http Get(翻页,过滤,排序)

基本的Get实现 ```CSharp [HttpGet] public async Task Get() { IEnumerable apiUsers = await _apiUserRepository.GetAllApiUsersAsync(); IEnumerable apiUserResources = _mapper.Map,IEnumerable>(apiUsers); return Ok(apiUserResources); } [HttpGet("{guid}")] public async Task Get(string guid) { ApiUser apiUser = await _apiUserRepository.GetApiUserByGuidAsync(Guid.Parse(guid)); if (apiUser is null) return NotFound(); ApiUserResource apiUserResource = _mapper.Map(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());

翻页

构造翻页请求参数类

QueryParameters ```CSharp 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( ref TField field,in TField newValue,[CallerMemberName] string propertyName = null) { if (EqualityComparer.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 ```CSharp namespace ApiStudy.Core.Entities { public class ApiUserParameters:QueryParameters { public string UserName { get; set; } } } ```

Repository实现支持翻页请求参数的方法

Repository相关代码 ```CSharp /*----- ApiUserRepository -----*/ public PaginatedList GetAllApiUsers(ApiUserParameters parameters) { return new PaginatedList( parameters.PageIndex, parameters.PageSize, _context.ApiUsers.Count(), _context.ApiUsers.Skip(parameters.PageIndex * parameters.PageSize) .Take(parameters.PageSize)); } public Task> GetAllApiUsersAsync(ApiUserParameters parameters) { return Task.Run(() => GetAllApiUsers(parameters)); } /*----- IApiUserRepository -----*/ PaginatedList GetAllApiUsers(ApiUserParameters parameters); Task> GetAllApiUsersAsync(ApiUserParameters parameters); ```
UserController部分代码 ```CSharp ... [HttpGet(Name = "GetAllApiUsers")] public async Task GetAllApiUsers(ApiUserParameters parameters) { PaginatedList apiUsers = await _apiUserRepository.GetAllApiUsersAsync(parameters); IEnumerable apiUserResources = _mapper.Map,IEnumerable>(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
MappedProperty ```CSharp 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 ```CSharp namespace ApiStudy.Infrastructure.Services { using System.Collections.Generic; public interface IPropertyMapping { Dictionary> MappingDictionary { get; } } } ```
PropertyMapping ```CSharp namespace ApiStudy.Infrastructure.Services { using System.Collections.Generic; public abstract class PropertyMapping : IPropertyMapping { public Dictionary> MappingDictionary { get; } public PropertyMapping(Dictionary> MappingDict) { MappingDictionary = MappingDict; } } } ```
IPropertyMappingContainer ```CSharp namespace ApiStudy.Infrastructure.Services { public interface IPropertyMappingContainer { void Register() where T : IPropertyMapping, new(); IPropertyMapping Resolve(); bool ValidateMappingExistsFor(string fields); } } ```
PropertyMappingContainer ```CSharp namespace ApiStudy.Infrastructure.Services { using System; using System.Linq; using System.Collections.Generic; public class PropertyMappingContainer : IPropertyMappingContainer { protected internal readonly IList PropertyMappings = new List(); public void Register() where T : IPropertyMapping, new() { if (PropertyMappings.Any(x => x.GetType() == typeof(T))) return; PropertyMappings.Add(new T()); } public IPropertyMapping Resolve() { IEnumerable> result = PropertyMappings.OfType>(); 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(string fields) { if (string.IsNullOrEmpty(fields)) return true; IPropertyMapping propertyMapping = Resolve(); 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 ```CSharp 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 ApplySort( this IQueryable 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 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 部分代码 ```CSharp [HttpGet(Name = "GetAllApiUsers")] public async Task GetAllApiUsers(ApiUserParameters parameters) { if (!_propertyMappingContainer.ValidateMappingExistsFor(parameters.OrderBy)) return BadRequest("can't find fields for sorting."); PaginatedList apiUsers = await _apiUserRepository.GetAllApiUsersAsync(parameters); IEnumerable apiUserResources = _mapper.Map, IEnumerable>(apiUsers); IEnumerable sortedApiUserResources = apiUserResources.AsQueryable().ApplySort( parameters.OrderBy, _propertyMappingContainer.Resolve()); 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)

返回 资源的指定字段

ApiStudy.Infrastructure.Extensions.TypeExtensions ```CSharp namespace ApiStudy.Infrastructure.Extensions { using System; using System.Collections.Generic; using System.Reflection; public static class TypeExtensions { public static IEnumerable GetProeprties(this Type source, string fields = null) { List propertyInfoList = new List(); 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 ```CSharp 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 propertyInfoList = source.GetType().GetProeprties(fields).ToList(); ExpandoObject expandoObject = new ExpandoObject(); foreach (PropertyInfo propertyInfo in propertyInfoList) { try { (expandoObject as IDictionary).Add( propertyInfo.Name, propertyInfo.GetValue(source)); } catch { continue; } } return expandoObject; } internal static ExpandoObject ToDynamicObject(this object source, in IEnumerable propertyInfos, in string fields = null) { ExpandoObject expandoObject = new ExpandoObject(); foreach (PropertyInfo propertyInfo in propertyInfos) { try { (expandoObject as IDictionary).Add( propertyInfo.Name, propertyInfo.GetValue(source)); } catch { continue; } } return expandoObject; } } } ```
ApiStudy.Infrastructure.Extensions.IEnumerableExtensions ```CSharp 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 ToDynamicObject( this IEnumerable source,in string fields = null) { if (source == null) throw new ArgumentNullException(nameof(source)); List expandoObejctList = new List(); List propertyInfoList = typeof(T).GetProeprties(fields).ToList(); foreach(T x in source) { expandoObejctList.Add(x.ToDynamicObject(propertyInfoList, fields)); } return expandoObejctList; } } } ```
ApiStudy.Infrastructure.Services.TypeHelperServices ```CSharp namespace ApiStudy.Infrastructure.Services { using System.Reflection; public class TypeHelperServices : ITypeHelperServices { public bool HasProperties(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() ```CSharp [HttpGet(Name = "GetAllApiUsers")] public async Task GetAllApiUsers(ApiUserParameters parameters) { //added code if (!_typeHelper.HasProperties(parameters.Fields)) return BadRequest("fields not exist."); if (!_propertyMappingContainer.ValidateMappingExistsFor(parameters.OrderBy)) return BadRequest("can't find fields for sorting."); PaginatedList apiUsers = await _apiUserRepository.GetAllApiUsersAsync(parameters); IEnumerable apiUserResources = _mapper.Map, IEnumerable>(apiUsers); IEnumerable sortedApiUserResources = apiUserResources.AsQueryable().ApplySort( parameters.OrderBy, _propertyMappingContainer.Resolve()); //modified code IEnumerable sharpedApiUserResources = sortedApiUserResources.ToDynamicObject(parameters.Fields); var meta = new { apiUsers.PageIndex, apiUsers.PageSize, apiUsers.PageCount, apiUsers.TotalItemsCount, PreviousPageUrl = apiUsers.HasPreviousPage ? CreateApiUserUrl(parameters, ResourceUriType.PreviousPage) : string.Empty, NextPageUrl = apiUsers.HasNextPage ? CreateApiUserUrl(parameters, ResourceUriType.NextPage) : string.Empty, }; Response.Headers.Add( "X-Pagination", JsonConvert.SerializeObject( meta, new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() })); //modified code return Ok(sharpedApiUserResources); } ```

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


StartupDevelopment.ConfigureServices

services.AddMvc(options =>
    {
        options.ReturnHttpNotAcceptable = true;
        options.OutputFormatters.Add(new XmlDataContractSerializerOutputFormatter());
    })
        .AddJsonOptions(options =>
        {
            //added code
            options.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
        });

HATEOAS

REST里最复杂的约束,构建成熟RESTAPI的核心

  • 可进化性,自我描述
  • 超媒体(Hypermedia,例如超链接)驱动如何消
    费和使用API
UserContext ```CSharp private IEnumerable CreateLinksForApiUser(string guid,string fields = null) { List linkResources = new List(); 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 CreateLinksForApiUsers(ApiUserParameters parameters,bool hasPrevious,bool hasNext) { List resources = new List(); 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 GetAllApiUsers(ApiUserParameters parameters) { if (!_typeHelper.HasProperties(parameters.Fields)) return BadRequest("fields not exist."); if (!_propertyMappingContainer.ValidateMappingExistsFor(parameters.OrderBy)) return BadRequest("can't find fields for sorting."); PaginatedList apiUsers = await _apiUserRepository.GetAllApiUsersAsync(parameters); IEnumerable apiUserResources = _mapper.Map, IEnumerable>(apiUsers); IEnumerable sortedApiUserResources = apiUserResources.AsQueryable().ApplySort( parameters.OrderBy, _propertyMappingContainer.Resolve()); IEnumerable shapedApiUserResources = sortedApiUserResources.ToDynamicObject(parameters.Fields); IEnumerable shapedApiUserResourcesWithLinks = shapedApiUserResources.Select( x => { IDictionary dict = x as IDictionary; 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中注册这个媒体类型,上面的代码给出了具体操作.

UserContext ```CSharp [HttpGet(Name = "GetAllApiUsers")] public async Task GetAllApiUsers(ApiUserParameters parameters,[FromHeader(Name = "Accept")] string mediaType) { if (!_typeHelper.HasProperties(parameters.Fields)) return BadRequest("fields not exist."); if (!_propertyMappingContainer.ValidateMappingExistsFor(parameters.OrderBy)) return BadRequest("can't find fields for sorting."); PaginatedList apiUsers = await _apiUserRepository.GetAllApiUsersAsync(parameters); IEnumerable apiUserResources = _mapper.Map, IEnumerable>(apiUsers); IEnumerable sortedApiUserResources = apiUserResources.AsQueryable().ApplySort( parameters.OrderBy, _propertyMappingContainer.Resolve()); IEnumerable shapedApiUserResources = sortedApiUserResources.ToDynamicObject(parameters.Fields); if (mediaType == "application/json") return Ok(shapedApiUserResources); else if (mediaType == "application/vnd.laggage.hateoas+json") { IEnumerable shapedApiUserResourcesWithLinks = shapedApiUserResources.Select( x => { IDictionary dict = x as IDictionary; 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 Get(string guid, [FromHeader(Name = "Accept")] string mediaType , string fields = null) { if (!_typeHelper.HasProperties(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); ExpandoObject shapedApiUserResource = apiUserResource.ToDynamicObject(fields); if (mediaType == "application/json") return Ok(shapedApiUserResource); else if(mediaType == "application/vnd.laggage.hateoas+json") { IDictionary shapedApiUserResourceWithLink = shapedApiUserResource as IDictionary; shapedApiUserResourceWithLink.Add("links", CreateLinksForApiUser(guid, fields)); return Ok(shapedApiUserResourceWithLink); } return NotFound(@"Can't find resource for the given media type: [{mediaType}]."); } ```


  • 自定义Action约束.
RequestHeaderMatchingMediaTypeAttribute ```CSharp [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 ```CSharp [HttpGet(Name = "GetAllApiUsers")] [RequestHeaderMatchingMediaType("Accept",new string[] { "application/vnd.laggage.hateoas+json" })] public async Task GetHateoas(ApiUserParameters parameters) { if (!_typeHelper.HasProperties(parameters.Fields)) return BadRequest("fields not exist."); if (!_propertyMappingContainer.ValidateMappingExistsFor(parameters.OrderBy)) return BadRequest("can't find fields for sorting."); PaginatedList apiUsers = await _apiUserRepository.GetAllApiUsersAsync(parameters); IEnumerable apiUserResources = _mapper.Map, IEnumerable>(apiUsers); IEnumerable sortedApiUserResources = apiUserResources.AsQueryable().ApplySort( parameters.OrderBy, _propertyMappingContainer.Resolve()); IEnumerable shapedApiUserResources = sortedApiUserResources.ToDynamicObject(parameters.Fields); IEnumerable shapedApiUserResourcesWithLinks = shapedApiUserResources.Select( x => { IDictionary dict = x as IDictionary; 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 Get(ApiUserParameters parameters) { if (!_typeHelper.HasProperties(parameters.Fields)) return BadRequest("fields not exist."); if (!_propertyMappingContainer.ValidateMappingExistsFor(parameters.OrderBy)) return BadRequest("can't find fields for sorting."); PaginatedList apiUsers = await _apiUserRepository.GetAllApiUsersAsync(parameters); IEnumerable apiUserResources = _mapper.Map, IEnumerable>(apiUsers); IEnumerable sortedApiUserResources = apiUserResources.AsQueryable().ApplySort( parameters.OrderBy, _propertyMappingContainer.Resolve()); IEnumerable shapedApiUserResources = sortedApiUserResources.ToDynamicObject(parameters.Fields); return Ok(shapedApiUserResources); } [HttpGet("{guid}", Name = "GetApiUser")] [RequestHeaderMatchingMediaType("Accept", new string[] { "application/vnd.laggage.hateoas+json" })] public async Task GetHateoas(string guid, string fields = null) { if (!_typeHelper.HasProperties(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); ExpandoObject shapedApiUserResource = apiUserResource.ToDynamicObject(fields); IDictionary shapedApiUserResourceWithLink = shapedApiUserResource as IDictionary; shapedApiUserResourceWithLink.Add("links", CreateLinksForApiUser(guid, fields)); return Ok(shapedApiUserResourceWithLink); } [HttpGet("{guid}", Name = "GetApiUser")] [RequestHeaderMatchingMediaType("Accept", new string[] { "application/json" })] public async Task Get(string guid, string fields = null) { if (!_typeHelper.HasProperties(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); ExpandoObject shapedApiUserResource = apiUserResource.ToDynamicObject(fields); return Ok(shapedApiUserResource); } ```

Post添加资源

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

安全性和幂等性

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

代码实现

StartUp中注册Fluent,用于验证

services.AddMvc(...)
        .AddFluentValidation();

services.AddTransient<IValidator<ApiUserAddResource>, ApiUserAddResourceValidator>();
ApiStudy.Infrastructure.Resources.ApiUserAddResourceValidator ```CSharp namespace ApiStudy.Infrastructure.Resources { using FluentValidation; public class ApiUserAddResourceValidator : AbstractValidator { 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() ```CSharp [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 AddUser([FromBody] ApiUserAddResource apiUser) { if (!ModelState.IsValid) return UnprocessableEntity(ModelState); ApiUser newUser = _mapper.Map(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 shapedUserResource = _mapper.Map(newUser) .ToDynamicObject() as IDictionary; IEnumerable links = CreateLinksForApiUser(newUser.Guid.ToString()); shapedUserResource.Add("links", links); return CreatedAtRoute("GetApiUser",new { newUser.Guid }, shapedUserResource); } ```

Delete

  • 参数 : ID
  • 幂等的
    • 多次请求的副作用和单次请求的副作用是一样的.每次发送了DELETE请求之后,服务器的状态都是一样的.
  • 不安全
ApiStudy.Api.Controllers.UserController ```CSharp [HttpDelete("{guid}",Name = "DeleteApiUser")] public async Task 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

相关类:


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 ```CSharp namespace ApiStudy.Infrastructure.Resources { public class ApiUserAddResource:ApiUserAddOrUpdateResource { } } ```
ApiStudy.Infrastructure.Resources.ApiUserUpdateResource ```CSharp namespace ApiStudy.Infrastructure.Resources { public class ApiUserUpdateResource : ApiUserAddOrUpdateResource { } } ```
ApiStudy.Infrastructure.Resources.ApiUserAddOrUpdateResourceValidator ```CSharp namespace ApiStudy.Infrastructure.Resources { using FluentValidation; public class ApiUserAddOrUpdateResourceValidator : AbstractValidator 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
ApiStudy.Api.Controllers.UpdateApiUser ```CSharp [HttpPut("{guid}",Name = "PutApiUser")] public async Task 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"},测试目标属性的值和指定的值是一样的。
  • path,资源的属性名
    • 可以有层级结构
  • value 更新的值
[
    {
        "op":"replace",
        "path":"/name",
        "value":"阿黄"
    },
    {
        "op":"remove",
        "path":"/email"
    }
]
ApiStudy.Api.Controllers.UserContext.UpdateApiUser ```CSharp [HttpPatch("{guid}",Name = "PatchApiUser")] [RequestHeaderMatchingMediaType("Content-Type",new string[] { "application/vnd.laggage.patch.apiuser+json" })] public async Task UpdateApiUser( string guid,[FromBody] JsonPatchDocument userUpdateDoc) { if (userUpdateDoc == null) return BadRequest(); ApiUser userToUpdate = await _apiUserRepository.GetApiUserByGuidAsync(new Guid(guid)); if (userToUpdate is null) return NotFound(); ApiUserUpdateResource userToUpdateResource = _mapper.Map(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.

原文地址:https://www.cnblogs.com/Laggage/p/11117768.html