• Asp.NetCoreWebApi


    参考文章


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

    ConfigureService中注入EF服务

    services.AddDbContext<UserContext>(options =>
                {
                    string connString = "Server=Xxx:xxx:xxx:xxx;Database=Xxxx;Uid=root;Pwd=Xxxxx; ";
                    options.UseMySQL(connString);
                });
    

    数据库连接字符串也可以在appSettings.json中配置 image 然后通过 Configuration.GetConnectionString("mysql") 来获得连接字符串.

    迁移数据库

    • 在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”]
    
    

    全局异常处理

    异常处理官方文档

    下面按照我的想法实现一个全局异常处理,

    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 = "text/plain";
    
                        Exception ex = context.Features.Get<IExceptionHandlerFeature>().Error;
                        if (!(ex is null))
                        {Errors
                            ILogger logger = loggerFactory.CreateLogger("ApiStudy.Api.Extensions.ExceptionHandlingExtensions");
                            logger.LogError(
                                $"Encounter error while handling request. ExceptionMsgs: {exMsgs}");
                        }
                        await context.Response.WriteAsync(ex?.Message ?? "Error occurred, but cannot get exception message.For more detail, go to see the log.");
                    }));
            }
    
            private static Task<string> FlattenExceptionMsgAsync(Exception ex)
            {
                return Task.Run(() =>
                {
                    var sb = new StringBuilder();
                    Exception t = ex;
                    while(!(t is null))
                    {
                        sb.AppendLine($"ExceptoinType-{ex.GetType().Name}: {ex.Message}");
                        t = ex.InnerException;
                    };
                    return sb.ToString();
                });
            }
        }
    }
    

    再Startup.Configure中使用这个异常处理程序

    public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory, IWebHostEnviroment env)
    {
        if (env.IsDevelopment()) 
        {
            app.UseCustomExceptionHandler(loggerFactory);  //modified code
        }
    
        //app.UseDeveloperExceptionPage();
        app.UseHsts();
        app.UseHttpsRedirection();
    
        app.UseMvc(); //使用默认路由
    }
    

    实现数据接口类(Resource),使用AutoMapper在Resource和Entity中映射

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

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

    资源命名

    资源应该使用名词,例

    • api/getusers就是不正确的.
    • GET api/users就是正确的

    资源命名层次结构

    • 例如api/department/{departmentId}/emoloyees, 这就表示了 department (部门)和员工 (employee)之前是主从关系.
    • api/department/{departmentId}/emoloyees/{employeeId},就表示了该部门下的某个员 工.

    内容协商

    ASP.NET Core支持输出和输入两种格式化器.

    • 用于输出的media type放在Accept Header里,表示客户端接受这种格式的输出.
    • 用于输入的media type放Content-Type Header里,表示客户端传进来的数据是这种格式.
    • ReturnHttpNotAcceptable设为true,如果客户端请求不支持的数据格式,就会返回406.
      services.AddMvc(options =>
      {
          options.ReturnHttpNotAcceptable = true;
      });
      
    • 支持输出XML格式:options.OutputFormatters.Add(newXmlDataContractSerializerOutputFormatter());

    翻页

    构造翻页请求参数类

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

    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中据说已经修复这个问题.

    搜索(过滤)

    修改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
    namespace ApiStudy.Infrastructure.Services
    {
        public struct MappedProperty
        {
            public MappedProperty(string name, bool revert = false)
            {
                Name = name;
                Revert = revert;
            }
    
            public string Name { get; set; }
            public bool Revert { get; set; }
        }
    }
    
    IPropertyMapping
    namespace ApiStudy.Infrastructure.Services
    {
        using System.Collections.Generic;
    
        public interface IPropertyMapping
        {
            Dictionary<string, List<MappedProperty>> MappingDictionary { get; }
        }
    }
    
    PropertyMapping
    namespace ApiStudy.Infrastructure.Services
    {
        using System.Collections.Generic;
    
        public abstract class PropertyMapping<TSource,TDestination> : IPropertyMapping
        {
            public Dictionary<string, List<MappedProperty>> MappingDictionary { get; }
    
            public PropertyMapping(Dictionary<string, List<MappedProperty>> MappingDict)
            {
                MappingDictionary = MappingDict;
            }
        }
    }
    
    IPropertyMappingContainer
    namespace ApiStudy.Infrastructure.Services
    {
        public interface IPropertyMappingContainer
        {
            void Register<T>() where T : IPropertyMapping, new();
            IPropertyMapping Resolve<TSource, TDestination>();
            bool ValidateMappingExistsFor<TSource, TDestination>(string fields);
        }
    }
    
    PropertyMappingContainer
    namespace ApiStudy.Infrastructure.Services
    {
        using System;
        using System.Linq;
        using System.Collections.Generic;
    
        public class PropertyMappingContainer : IPropertyMappingContainer
        {
            protected internal readonly IList<IPropertyMapping> PropertyMappings = new List<IPropertyMapping>();
    
            public void Register<T>() where T : IPropertyMapping, new()
            {
                if (PropertyMappings.Any(x => x.GetType() == typeof(T))) return;
                PropertyMappings.Add(new T());
            }
    
            public IPropertyMapping Resolve<TSource,TDestination>()
            {
                IEnumerable<PropertyMapping<TSource, TDestination>> result = PropertyMappings.OfType<PropertyMapping<TSource,TDestination>>();
                if (result.Count() > 0)
                    return result.First();
                throw new InvalidCastException(
                   string.Format( "Cannot find property mapping instance for {0}, {1}", typeof(TSource), typeof(TDestination)));
            }
    
            public bool ValidateMappingExistsFor<TSource, TDestination>(string fields)
            {
                if (string.IsNullOrEmpty(fields)) return true;
    
                IPropertyMapping propertyMapping = Resolve<TSource, TDestination>();
    
                string[] splitFields = fields.Split(',');
    
                foreach(string property in splitFields)
                {
                    string trimmedProperty = property.Trim();
                    int indexOfFirstWhiteSpace = trimmedProperty.IndexOf(' ');
                    string propertyName = indexOfFirstWhiteSpace <= 0 ? trimmedProperty : trimmedProperty.Remove(indexOfFirstWhiteSpace);
                    
                    if (!propertyMapping.MappingDictionary.Keys.Any(x => string.Equals(propertyName,x,StringComparison.OrdinalIgnoreCase))) return false;
                }
                return true;
            }
        }
    }
    
    QueryExtensions
    namespace ApiStudy.Infrastructure.Extensions
    {
        using ApiStudy.Infrastructure.Services;
        using System;
        using System.Collections.Generic;
        using System.Linq;
        using System.Linq.Dynamic.Core;
    
        public static class QueryExtensions
        {
            public static IQueryable<T> ApplySort<T>(
               this IQueryable<T> data,in string orderBy,in IPropertyMapping propertyMapping)
            {
                if (data == null) throw new ArgumentNullException(nameof(data));
                if (string.IsNullOrEmpty(orderBy)) return data;
    
                string[] splitOrderBy = orderBy.Split(',');
                foreach(string property in splitOrderBy)
                {
                    string trimmedProperty = property.Trim();
                    int indexOfFirstSpace = trimmedProperty.IndexOf(' ');
                    bool desc = trimmedProperty.EndsWith(" desc");
                    string propertyName = indexOfFirstSpace > 0 ? trimmedProperty.Remove(indexOfFirstSpace) : trimmedProperty;
                    propertyName = propertyMapping.MappingDictionary.Keys.FirstOrDefault(
                        x => string.Equals(x, propertyName, StringComparison.OrdinalIgnoreCase)); //ignore case of sort property
    
                    if (!propertyMapping.MappingDictionary.TryGetValue(
                        propertyName, out List<MappedProperty> mappedProperties))
                        throw new InvalidCastException($"key mapping for {propertyName} is missing");
    
                    mappedProperties.Reverse();
                    foreach(MappedProperty mappedProperty in mappedProperties)
                    {
                        if (mappedProperty.Revert) desc = !desc;
                        data = data.OrderBy($"{mappedProperty.Name} {(desc ? "descending" : "ascending")} ");
                    }
                }
                return data;
            }
        }
    }
    
    UserController 部分代码
    [HttpGet(Name = "GetAllApiUsers")]
    public async Task<IActionResult> GetAllApiUsers(ApiUserParameters parameters)
    {
        if (!_propertyMappingContainer.ValidateMappingExistsFor<ApiUserResource, ApiUser>(parameters.OrderBy))
            return BadRequest("can't find fields for sorting.");
    
        PaginatedList<ApiUser> apiUsers = await _apiUserRepository.GetAllApiUsersAsync(parameters);
    
        IEnumerable<ApiUserResource> apiUserResources =
            _mapper.Map<IEnumerable<ApiUser>, IEnumerable<ApiUserResource>>(apiUsers);
    
        IEnumerable<ApiUserResource> sortedApiUserResources =
            apiUserResources.AsQueryable().ApplySort(
                parameters.OrderBy, _propertyMappingContainer.Resolve<ApiUserResource, ApiUser>());
    
        var meta = new
        {
            apiUsers.PageIndex,
            apiUsers.PageSize,
            apiUsers.PageCount,
            apiUsers.TotalItemsCount,
            PreviousPageUrl = apiUsers.HasPreviousPage ? CreateApiUserUrl(parameters, ResourceUriType.PreviousPage) : string.Empty,
            NextPageUrl = apiUsers.HasNextPage ? CreateApiUserUrl(parameters, ResourceUriType.NextPage) : string.Empty,
        };
        Response.Headers.Add(
            "X-Pagination",
            JsonConvert.SerializeObject(
                meta,
                new JsonSerializerSettings
                { ContractResolver = new CamelCasePropertyNamesContractResolver() }));
        return Ok(sortedApiUserResources);
    }
    
    private string CreateApiUserUrl(ApiUserParameters parameters, ResourceUriType uriType)
    {
        var param = new {
            parameters.PageIndex,
            parameters.PageSize
        };
        switch (uriType)
        {
            case ResourceUriType.PreviousPage:
                param = new
                {
                    PageIndex = parameters.PageIndex - 1,
                    parameters.PageSize
                };
                break;
            case ResourceUriType.NextPage:
                param = new
                {
                    PageIndex = parameters.PageIndex + 1,
                    parameters.PageSize
                };
                break;
            case ResourceUriType.CurrentPage:
                break;
            default: break;
        }
        return Url.Link("GetAllApiUsers", param);
    }
    

    资源塑形(Resource shaping)

    返回 资源的指定字段

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

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

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

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

    创建供应商特定媒体类型

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

    Post添加资源

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

    安全性和幂等性

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

    代码实现

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

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

    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}不能为空!");
            }
        }
    }
    

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

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

    Http常用方法总结

    • GET(获取资源):
      • GET api/countries,返回200,集合数据;找不到数据返回404。
      • GET api/countries/,返回200,单个数据;找不到返回404.
    • DELETE(删除资源)
      • DELETE api/countries/,成功204;没找到资源404。
      • DELETE api/countries,很少用,也是204或者404.
    • POST (创建资源):
      • POST api/countries,成功返回201和单个数据;如果资源没有创建则返回404
      • POST api/countries/,肯定不会成功,返回404或409.
      • POST api/countrycollections,成功返回201和集合;没创建资源则返回404
    • PUT (整体更新):
      • PUT api/countries/,成功可以返回200,204;没找到资源则返回404
      • PUT api/countries,集合操作很少见,返回200,204或404
    • PATCH(局部更新):
      • PATCHapi/countries/,200单个数据,204或者404
      • PATCHapi/countries,集合操作很少见,返回200集合,204或404.
  • 相关阅读:
    openJudge计算概论-谁考了第k名
    OpenJudge计算概论-求平均年龄
    OpenJudge计算概论-能被3,5,7整除的数
    OpenJudge计算概论-计算书费
    OpenJudge计算概论-计算三角形面积【海伦公式】
    OpenWrt 中安装配置Transmission
    OpenWrt中wifidog的配置及各节点页面参数
    Linux中后台执行任务
    通过ionice和nice降低shell脚本运行的优先级
    OpenWrt中对USB文件系统的操作, 以及读写性能测试
  • 原文地址:https://www.cnblogs.com/Laggage/p/11117768.html
Copyright © 2020-2023  润新知