Asp.NetCoreWebApi - RESTful Api

来源:https://www.cnblogs.com/Laggage/archive/2019/07/02/11117768.html
-Advertisement-
Play Games

REST & x5E38;& x7528;http& x52A8;& x8BCD; WebApi & x5728; Asp.NetCore & x4E2D;& x7684;& x5B9E;& x73B0; 3.1. & x521B;& x5EFA;WebApi& x9879;& x76EE;. 3. ...


参考 :

1. REST

REST : 具象状态传输(Representational State Transfer,简称REST),是Roy Thomas Fielding博士于2000年在他的博士论文 "Architectural Styles and the Design of Network-based Software Architectures" 中提出来的一种万维网软件架构风格。
目前在三种主流的Web服务实现方案中,因为REST模式与复杂的SOAPXML-RPC相比更加简洁,越来越多的web服务开始采用REST风格设计和实现。例如,Amazon.com提供接近REST风格的Web服务执行图书查询;

符合REST设计风格的Web API称为RESTful API。它从以下三个方面资源进行定义:

  • 直观简短的资源地址:URI,比如:http://example.com/resources/ .
  • 传输的资源:Web服务接受与返回的互联网媒体类型,比如:JSON,XML,YAML等...
  • 对资源的操作:Web服务在该资源上所支持的一系列请求方法(比如:POST,GET,PUT或DELETE).

PUT和DELETE方法是幂等方法.GET方法是安全方法(不会对服务器端有修改,因此当然也是幂等的).

ps 关于幂等方法 :
看这篇 理解HTTP幂等性.
简单说,客户端多次请求服务端返回的结果都相同,那么就说这个操作是幂等的.(个人理解,详细的看上面给的文章)

不像基于SOAP的Web服务,RESTful Web服务并没有“正式”的标准。这是因为REST是一种架构,而SOAP只是一个协议。虽然REST不是一个标准,但大部分RESTful Web服务实现会使用HTTP、URI、JSON和XML等各种标准。

2. 常用http动词

括号中是相应的SQL命令.

  • GET(SELECT) : 从服务器取出资源(一项或多项).
  • POST(CREATE) : 在服务器新建一个资源.
  • PUT(UPDATE) : 在服务器更新资源(客户端提供改变后的完整资源).
  • PATCH(UPDATE) : 在服务器更新资源(客户端提供改变的属性).
  • DELETE(DELETE) : 在服务器删除资源.

3. WebApi 在 Asp.NetCore 中的实现

这里以用户增删改查为例.

3.1. 创建WebApi项目.

参考ASP.NET Core WebAPI 开发-新建WebAPI项目.

注意,本文建立的Asp.NetCore WebApi项目选择.net core版本是2.2,不建议使用其他版本,2.1版本下会遇到依赖文件冲突问题!所以一定要选择2.2版本的.net core.

3.2. 集成Entity Framework Core操作Mysql

3.2.1. 安装相关的包(为Xxxx.Infrastructure项目安装)

  • Microsoft.EntityFrameworkCore.Design
  • Pomelo.EntityFrameworkCore.MySql

这里注意一下,Mysql官方的包是 MySql.Data.EntityFrameworkCore,但是这个包有bug,我在github上看到有人说有替代方案 - Pomelo.EntityFrameworkCore.MySql,经过尝试,后者比前者好用.所有这里就选择后者了.使用前者的话可能会导致数据库迁移失败(Update的时候).

PS: Mysql文档原文:

Install the MySql.Data.EntityFrameworkCore NuGet package.
For EF Core 1.1 only: If you plan to scaffold a database, install the MySql.Data.EntityFrameworkCore.Design NuGet package as well.

EFCore - MySql文档
Mysql版本要求:
Mysql版本要高于5.7
使用最新版本的Mysql Connector(2019 6/27 目前是8.x).

为Xxxx.Infrastructure项目安装EFCore相关的包:

为Xxxx.Api项目安装 Pomelo.EntityFrameworkCore.MySql

3.2.2. 建立Entity和Context

ApiUser
namespace ApiStudy.Core.Entities
{
    using System;

    public class ApiUser
    {
        public Guid Guid { get; set; }
        public string Name { get; set; }
        public string Passwd { get; set; }
        public DateTime RegistrationDate { get; set; }
        public DateTime Birth { get; set; }
        public string ProfilePhotoUrl { get; set; }
        public string PhoneNumber { get; set; }
        public string Email { get; set; }
    }
}
UserContext
namespace ApiStudy.Infrastructure.Database
{
    using ApiStudy.Core.Entities;
    using Microsoft.EntityFrameworkCore;

    public class UserContext:DbContext
    {
        public UserContext(DbContextOptions<UserContext> options): base(options)
        {
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<ApiUser>().HasKey(u => u.Guid);

            base.OnModelCreating(modelBuilder);
        }

        public DbSet<ApiUser> ApiUsers { get; set; }
    }
}

3.2.3. ConfigureService中注入EF服务

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

3.2.4. 迁移数据库

  • 在Tools > NuGet Package Manager > Package Manager Console输入命令.
  • Add-Migration Xxx 添加迁移.
    PS : 如果迁移不想要,使用 Remove-Migration 命令删除迁移.
  • Update-Database 更新到数据库.

3.2.5. 数据库迁移结果

3.2.6. 为数据库创建种子数据

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

    UserContextSeed
    namespace ApiStudy.Infrastructure.Database
    {
        using ApiStudy.Core.Entities;
        using Microsoft.Extensions.Logging;
        using System;
        using System.Linq;
        using System.Threading.Tasks;
    
        public class UserContextSeed
        {
            public static async Task SeedAsync(UserContext context,ILoggerFactory loggerFactory)
            {
                try
                {
                    if (!context.ApiUsers.Any())
                    {
                        context.ApiUsers.AddRange(
                            new ApiUser
                            {
                                Guid = Guid.NewGuid(),
                                Name = "la",
                                Birth = new DateTime(1998, 11, 29),
                                RegistrationDate = new DateTime(2019, 6, 28),
                                Passwd = "123587",
                                ProfilePhotoUrl = "https://www.laggage.top/",
                                PhoneNumber = "10086",
                                Email = "[email protected]"
                            },
                            new ApiUser
                            {
                                Guid = Guid.NewGuid(),
                                Name = "David",
                                Birth = new DateTime(1995, 8, 29),
                                RegistrationDate = new DateTime(2019, 3, 28),
                                Passwd = "awt87495987",
                                ProfilePhotoUrl = "https://www.laggage.top/",
                                PhoneNumber = "1008611",
                                Email = "[email protected]"
                            },
                            new ApiUser
                            {
                                Guid = Guid.NewGuid(),
                                Name = "David",
                                Birth = new DateTime(2001, 8, 19),
                                RegistrationDate = new DateTime(2019, 4, 25),
                                Passwd = "awt87495987",
                                ProfilePhotoUrl = "https://www.laggage.top/",
                                PhoneNumber = "1008611",
                                Email = "[email protected]"
                            },
                            new ApiUser
                            {
                                Guid = Guid.NewGuid(),
                                Name = "Linus",
                                Birth = new DateTime(1999, 10, 26),
                                RegistrationDate = new DateTime(2018, 2, 8),
                                Passwd = "awt87495987",
                                ProfilePhotoUrl = "https://www.laggage.top/",
                                PhoneNumber = "17084759987",
                                Email = "[email protected]"
                            },
                            new ApiUser
                            {
                                Guid = Guid.NewGuid(),
                                Name = "YouYou",
                                Birth = new DateTime(1992, 1, 26),
                                RegistrationDate = new DateTime(2015, 7, 8),
                                Passwd = "grwe874864987",
                                ProfilePhotoUrl = "https://www.laggage.top/",
                                PhoneNumber = "17084759987",
                                Email = "[email protected]"
                            },
                            new ApiUser
                            {
                                Guid = Guid.NewGuid(),
                                Name = "小白",
                                Birth = new DateTime(1997, 9, 30),
                                RegistrationDate = new DateTime(2018, 11, 28),
                                Passwd = "gewa749864",
                                ProfilePhotoUrl = "https://www.laggage.top/",
                                PhoneNumber = "17084759987",
                                Email = "[email protected]"
                            });
    
                        await context.SaveChangesAsync();
                    }
                }
                catch(Exception ex)
                {
                    ILogger logger = loggerFactory.CreateLogger<UserContextSeed>();
                    logger.LogError(ex, "Error occurred while seeding database");
                }
            }
        }
    }
    
    
  • 修改Program.Main方法

    Program.Main
    IWebHost host = CreateWebHostBuilder(args).Build();
    
    using (IServiceScope scope = host.Services.CreateScope())
    {
        IServiceProvider provider = scope.ServiceProvider;
        UserContext userContext = provider.GetService<UserContext>();
        ILoggerFactory loggerFactory = provider.GetService<ILoggerFactory>();
        UserContextSeed.SeedAsync(userContext, loggerFactory).Wait();
    }
    
    host.Run();
    

这个时候运行程序会出现异常,打断点看一下异常信息:Data too long for column 'Guid' at row 1

可以猜到,Mysql的varbinary(16)放不下C# Guid.NewGuid()方法生成的Guid,所以配置一下数据库Guid字段类型为varchar(256)可以解决问题.

解决方案:
修改 UserContext.OnModelCreating 方法
配置一下 ApiUser.Guid 属性到Mysql数据库的映射:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<ApiUser>().Property(p => p.Guid)
        .HasColumnType("nvarchar(256)");
    modelBuilder.Entity<ApiUser>().HasKey(u => u.Guid);
    
    base.OnModelCreating(modelBuilder);
}

4. 支持https

将所有http请求全部映射到https

Startup中:
ConfigureServices方法注册,并配置端口和状态码等:
services.AddHttpsRedirection(…)

services.AddHttpsRedirection(options =>
                {
                    options.RedirectStatusCode = StatusCodes.Status307TemporaryRedirect;
                    options.HttpsPort = 5001;
                });

Configure方法使用该中间件:

app.UseHttpsRedirection()

5. 支持HSTS

ConfigureServices方法注册
官方文档

services.AddHsts(options =>
{
    options.Preload = true;
    options.IncludeSubDomains = true;
    options.MaxAge = TimeSpan.FromDays(60);
    options.ExcludedHosts.Add("example.com");
    options.ExcludedHosts.Add("www.example.com");
});

Configure方法配置中间件管道

app.UseHsts();

注意 app.UseHsts() 方法最好放在 app.UseHttps() 方法之后.

6. 使用SerilLog

有关日志的微软官方文档

SerilLog github仓库
该github仓库上有详细的使用说明.

使用方法:

6.1. 安装nuget包

  • Serilog.AspNetCore
  • Serilog.Sinks.Console

6.2. 添加代码

Program.Main方法中:

Log.Logger = new LoggerConfiguration()
            .MinimumLevel.Debug()
            .MinimumLevel.Override("Microsoft", LogEventLevel.Information)
            .Enrich.FromLogContext()
            .WriteTo.Console()
            .CreateLogger();

修改Program.CreateWebHostBuilder(...)

 public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
            WebHost.CreateDefaultBuilder(args)
                .UseStartup<Startup>()
                .UseSerilog(); // <-- Add this line;
 }

6.3. 自行测试

7. Asp.NetCore配置文件

7.1. 默认配置文件

默认 appsettings.json
ConfigurationBuilder().AddJsonFile("appsettings.json").Build()-->IConfigurationRoot(IConfiguration)

7.2. 获得配置

IConfiguration[“Key:ChildKey”]
针对”ConnectionStrings:xxx”,可以使用IConfiguration.GetConnectionString(“xxx”)

private static IConfiguration Configuration { get; set; }

public StartupDevelopment(IConfiguration config)
{
    Configuration = config;
}

...

Configuration[“Key:ChildKey”]

8. 自定义一个异常处理,ExceptionHandler

8.1. 弄一个类,写一个扩展方法处理异常

namespace ApiStudy.Api.Extensions
{
    using Microsoft.AspNetCore.Builder;
    using Microsoft.AspNetCore.Http;
    using Microsoft.Extensions.Logging;
    using System;

    public static class ExceptionHandlingExtensions
    {
        public static void UseCustomExceptionHandler(this IApplicationBuilder app,ILoggerFactory loggerFactory)
        {
            app.UseExceptionHandler(
                builder => builder.Run(async context =>
                {
                    context.Response.StatusCode = StatusCodes.Status500InternalServerError;
                    context.Response.ContentType = "application/json";

                    Exception ex = context.Features.Get<Exception>();
                    if (!(ex is null))
                    {
                        ILogger logger = loggerFactory.CreateLogger("ApiStudy.Api.Extensions.ExceptionHandlingExtensions");
                        logger.LogError(ex, "Error occurred.");
                    }
                    await context.Response.WriteAsync(ex?.Message ?? "Error occurred, but cannot get exception message.For more detail, go to see the log.");
                }));
        }
    }
}

8.2. 在Configuration中使用扩展方法

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory)
{
    app.UseCustomExceptionHandler(loggerFactory);  //modified code

    //app.UseDeveloperExceptionPage();
    app.UseHsts();
    app.UseHttpsRedirection();

    app.UseMvc(); //使用默认路由
}

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

9.1. 为Entity类创建对应的Resource类

ApiUserResource
namespace ApiStudy.Infrastructure.Resources
{
    using System;

    public class ApiUserResource
    {
        public Guid Guid { get; set; }
        public string Name { get; set; }
        //public string Passwd { get; set; }
        public DateTime RegistrationDate { get; set; }
        public DateTime Birth { get; set; }
        public string ProfilePhotoUrl { get; set; }
        public string PhoneNumber { get; set; }
        public string Email { get; set; }
    }
}

9.2. 使用 AutoMapper

  • 添加nuget包
    AutoMapper
    AutoMapper.Extensions.Microsoft.DependencyInjection

  • 配置映射
    可以创建Profile
    CreateMap<TSource,TDestination>()

    MappingProfile
    namespace ApiStudy.Api.Extensions
    {
        using ApiStudy.Core.Entities;
        using ApiStudy.Infrastructure.Resources;
        using AutoMapper;
        using System;
        using System.Text;
    
        public class MappingProfile : Profile
        {
            public MappingProfile()
            {
                CreateMap<ApiUser, ApiUserResource>()
                    .ForMember(
                    d => d.Passwd, 
                    opt => opt.AddTransform(s => Convert.ToBase64String(Encoding.Default.GetBytes(s))));
    
                CreateMap<ApiUserResource, ApiUser>()
                    .ForMember(
                    d => d.Passwd,
                    opt => opt.AddTransform(s => Encoding.Default.GetString(Convert.FromBase64String(s))));
            }
        }
    }
    
    
  • 注入服务
    services.AddAutoMapper()

10. 使用FluentValidation

FluentValidation官网

10.1. 安装Nuget包

  • FluentValidation
  • FluentValidation.AspNetCore

10.2. 为每一个Resource配置验证器

  • 继承于AbstractValidator

    ApiUserResourceValidator
    namespace ApiStudy.Infrastructure.Resources
    {
        using FluentValidation;
    
        public class ApiUserResourceValidator : AbstractValidator<ApiUserResource>
        {
            public ApiUserResourceValidator()
            {
                RuleFor(s => s.Name)
                    .MaximumLength(80)
                    .WithName("用户名")
                    .WithMessage("{PropertyName}的最大长度为80")
                    .NotEmpty()
                    .WithMessage("{PropertyName}不能为空!");
            }
        }
    }
    
  • 注册到容器:services.AddTransient<>()
    services.AddTransient<IValidator<ApiUserResource>, ApiUserResourceValidator>();

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

基本的Get实现
[HttpGet]
public async Task<IActionResult> Get()
{
    IEnumerable<ApiUser> apiUsers = await _apiUserRepository.GetAllApiUsersAsync();

    IEnumerable<ApiUserResource> apiUserResources = 
        _mapper.Map<IEnumerable<ApiUser>,IEnumerable<ApiUserResource>>(apiUsers);

    return Ok(apiUserResources);
}

[HttpGet("{guid}")]
public async Task<IActionResult> Get(string guid)
{
    ApiUser apiUser = await _apiUserRepository.GetApiUserByGuidAsync(Guid.Parse(guid));

    if (apiUser is null) return NotFound();

    ApiUserResource apiUserResource = _mapper.Map<ApiUser,ApiUserResource>(apiUser);

    return Ok(apiUserResource);
}

11.1. 资源命名

11.1.1. 资源应该使用名词,例

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

11.1.2. 资源命名层次结构

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

11.2. 内容协商

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

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

12. 翻页

12.1. 构造翻页请求参数类

QueryParameters
namespace ApiStudy.Core.Entities
{
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Runtime.CompilerServices;

    public abstract class QueryParameters : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        private const int DefaultPageSize = 10;
        private const int DefaultMaxPageSize = 100;

        private int _pageIndex = 1;
        public virtual int PageIndex
        {
            get => _pageIndex;
            set => SetField(ref _pageIndex, value);
        }

        private int _pageSize = DefaultPageSize;
        public virtual int PageSize
        {
            get => _pageSize;
            set => SetField(ref _pageSize, value);
        }

        private int _maxPageSize = DefaultMaxPageSize;
        public virtual int MaxPageSize
        {
            get => _maxPageSize;
            set => SetField(ref _maxPageSize, value);
        }

        public string OrderBy { get; set; }
        public string Fields { get; set; }

        protected void SetField<TField>(
            ref TField field,in TField newValue,[CallerMemberName] string propertyName = null)
        {
            if (EqualityComparer<TField>.Default.Equals(field, newValue))
                return;
            field = newValue;
            if (propertyName == nameof(PageSize) || propertyName == nameof(MaxPageSize)) SetPageSize();
            
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
            
        }

        private void SetPageSize()
        {
            if (_maxPageSize <= 0) _maxPageSize = DefaultMaxPageSize;
            if (_pageSize <= 0) _pageSize = DefaultPageSize;
            _pageSize = _pageSize > _maxPageSize ? _maxPageSize : _pageSize;
        }
    }
}
ApiUserParameters
namespace ApiStudy.Core.Entities
{
    public class ApiUserParameters:QueryParameters
    {
        public string UserName { get; set; }
    }
}

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

Repository相关代码
/*----- ApiUserRepository -----*/
public PaginatedList<ApiUser> GetAllApiUsers(ApiUserParameters parameters)
{
    return new PaginatedList<ApiUser>(
        parameters.PageIndex,
        parameters.PageSize,
        _context.ApiUsers.Count(),
        _context.ApiUsers.Skip(parameters.PageIndex * parameters.PageSize)
        .Take(parameters.PageSize));
}

public Task<PaginatedList<ApiUser>> GetAllApiUsersAsync(ApiUserParameters parameters)
{
    return Task.Run(() => GetAllApiUsers(parameters));
}

/*----- IApiUserRepository -----*/
PaginatedList<ApiUser> GetAllApiUsers(ApiUserParameters parameters);
Task<PaginatedList<ApiUser>> GetAllApiUsersAsync(ApiUserParameters parameters);

UserController部分代码
...

[HttpGet(Name = "GetAllApiUsers")]
public async Task<IActionResult> GetAllApiUsers(ApiUserParameters parameters)
{
    PaginatedList<ApiUser> apiUsers = await _apiUserRepository.GetAllApiUsersAsync(parameters);

    IEnumerable<ApiUserResource> apiUserResources = 
        _mapper.Map<IEnumerable<ApiUser>,IEnumerable<ApiUserResource>>(apiUsers);

    var meta = new
    {
        PageIndex = apiUsers.PageIndex,
        PageSize = apiUsers.PageSize,
        PageCount = apiUsers.PageCount,
        TotalItemsCount = apiUsers.TotalItemsCount,
        NextPageUrl = CreateApiUserUrl(parameters, ResourceUriType.NextPage),
        PreviousPageUrl = CreateApiUserUrl(parameters, ResourceUriType.PreviousPage)
    };
    Response.Headers.Add(
        "X-Pagination",
        JsonConvert.SerializeObject(
            meta, 
            new JsonSerializerSettings
            { ContractResolver = new CamelCasePropertyNamesContractResolver() }));
    return Ok(apiUserResources);
}

...

private string CreateApiUserUrl(ApiUserParameters parameters,ResourceUriType uriType)
{
    var param = new ApiUserParameters
    {
        PageIndex = parameters.PageIndex,
        PageSize = parameters.PageSize
    };
    switch (uriType)
    {
        case ResourceUriType.PreviousPage:
            param.PageIndex--;
            break;
        case ResourceUriType.NextPage:
            param.PageIndex++;
            break;
        case ResourceUriType.CurrentPage:
            break;
        default:break;
    }
    return Url.Link("GetAllApiUsers", parameters);
}

PS注意,为HttpGet方法添加参数的话,在.net core2.2版本下,去掉那个ApiUserController上的 [ApiController());] 特性,否则参数传不进来..net core3.0中据说已经修复这个问题.

12.3. 搜索(过滤)

修改Repository代码:

 public PaginatedList<ApiUser> GetAllApiUsers(ApiUserParameters parameters)
{
    IQueryable<ApiUser> query = _context.ApiUsers.AsQueryable();
    query = query.Skip(parameters.PageIndex * parameters.PageSize)
            .Take(parameters.PageSize);

    if (!string.IsNullOrEmpty(parameters.UserName))
        query = _context.ApiUsers.Where(
            x => StringComparer.OrdinalIgnoreCase.Compare(x.Name, parameters.UserName) == 0);

    return new PaginatedList<ApiUser>(
        parameters.PageIndex,
        parameters.PageSize,
        query.Count(),
        query);
}

12.4. 排序

12.4.1. 排序思路

  • 需要安装System.Linq.Dynamic.Core

思路:

  • PropertyMappingContainer
    • PropertyMapping(ApiUserPropertyMapping)
      • MappedProperty
MappedProperty
namespace ApiStudy.Infrastructure.Services
{
    public struct MappedProperty
    {
        public MappedProperty(string name, bool revert = false)
        {
            Name = name;
            Revert = revert;
        }

        public string Name { get; set; }
        public bool Revert { get; set; }
    }
}
IPropertyMapping
namespace ApiStudy.Infrastructure.Services
{
    using System.Collections.Generic;

    public interface IPropertyMapping
    {
        Dictionary<string, List<MappedProperty>> MappingDictionary { get; }
    }
}
PropertyMapping
namespace ApiStudy.Infrastructure.Services
{
    using System.Collections.Generic;

    public abstract class PropertyMapping<TSource,TDestination> : IPropertyMapping
    {
        public Dictionary<string, List<MappedProperty>> MappingDictionary { get; }

        public PropertyMapping(Dictionary<string, List<MappedProperty>> MappingDict)
        {
            MappingDictionary = MappingDict;
        }
    }
}
IPropertyMappingContainer
namespace ApiStudy.Infrastructure.Services
{
    public interface IPropertyMappingContainer
    {
        void Register<T>() where T : IPropertyMapping, new();
        IPropertyMapping Resolve<TSource, TDestination>();
        bool ValidateMappingExistsFor<TSource, TDestination>(string fields);
    }
}
PropertyMappingContainer
namespace ApiStudy.Infrastructure.Services
{
    using System;
    using System.Linq;
    using System.Collections.Generic;

    public class PropertyMappingContainer : IPropertyMappingContainer
    {
        protected internal readonly IList<IPropertyMapping> PropertyMappings = new List<IPropertyMapping>();

        public void Register<T>() where T : IPropertyMapping, new()
        {
            if (PropertyMappings.Any(x => x.GetType() == typeof(T))) return;
            PropertyMappings.Add(new T());
        }

        public IPropertyMapping Resolve<TSource,TDestination>()
        {
            IEnumerable<PropertyMapping<TSource, TDestination>> result = PropertyMappings.OfType<PropertyMapping<TSource,TDestination>>();
            if (result.Count() > 0)
                return result.First();
            throw new InvalidCastException(
               string.Format( "Cannot find property mapping instance for {0}, {1}", typeof(TSource), typeof(TDestination)));
        }

        public bool ValidateMappingExistsFor<TSource, TDestination>(string fields)
        {
            if (string.IsNullOrEmpty(fields)) return true;

            IPropertyMapping propertyMapping = Resolve<TSource, TDestination>();

            string[] splitFields = fields.Split(',');

            foreach(string property in splitFields)
            {
                string trimmedProperty = property.Trim();
                int indexOfFirstWhiteSpace = trimmedProperty.IndexOf(' ');
                string propertyName = indexOfFirstWhiteSpace <= 0 ? trimmedProperty : trimmedProperty.Remove(indexOfFirstWhiteSpace);
                
                if (!propertyMapping.MappingDictionary.Keys.Any(x => string.Equals(propertyName,x,StringComparison.OrdinalIgnoreCase))) return false;
            }
            return true;
        }
    }
}
QueryExtensions
namespace ApiStudy.Infrastructure.Extensions
{
    using ApiStudy.Infrastructure.Services;
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Linq.Dynamic.Core;

    public static class QueryExtensions
    {
        public static IQueryable<T> ApplySort<T>(
           this IQueryable<T> data,in string orderBy,in IPropertyMapping propertyMapping)
        {
            if (data == null) throw new ArgumentNullException(nameof(data));
            if (string.IsNullOrEmpty(orderBy)) return data;

            string[] splitOrderBy = orderBy.Split(',');
            foreach(string property in splitOrderBy)
            {
                string trimmedProperty = property.Trim();
                int indexOfFirstSpace = trimmedProperty.IndexOf(' ');
                bool desc = trimmedProperty.EndsWith(" desc");
                string propertyName = indexOfFirstSpace > 0 ? trimmedProperty.Remove(indexOfFirstSpace) : trimmedProperty;
                propertyName = propertyMapping.MappingDictionary.Keys.FirstOrDefault(
                    x => string.Equals(x, propertyName, StringComparison.OrdinalIgnoreCase)); //ignore case of sort property

                if (!propertyMapping.MappingDictionary.TryGetValue(
                    propertyName, out List<MappedProperty> mappedProperties))
                    throw new InvalidCastException($"key mapping for {propertyName} is missing");

                mappedProperties.Reverse();
                foreach(MappedProperty mappedProperty in mappedProperties)
                {
                    if (mappedProperty.Revert) desc = !desc;
                    data = data.OrderBy($"{mappedProperty.Name} {(desc ? "descending" : "ascending")} ");
                }
            }
            return data;
        }
    }
}
UserController 部分代码
[HttpGet(Name = "GetAllApiUsers")]
public async Task<IActionResult> GetAllApiUsers(ApiUserParameters parameters)
{
    if (!_propertyMappingContainer.ValidateMappingExistsFor<ApiUserResource, ApiUser>(parameters.OrderBy))
        return BadRequest("can't find fields for sorting.");

    PaginatedList<ApiUser> apiUsers = await _apiUserRepository.GetAllApiUsersAsync(parameters);

    IEnumerable<ApiUserResource> apiUserResources =
        _mapper.Map<IEnumerable<ApiUser>, IEnumerable<ApiUserResource>>(apiUsers);

    IEnumerable<ApiUserResource> sortedApiUserResources =
        apiUserResources.AsQueryable().ApplySort(
            parameters.OrderBy, _propertyMappingContainer.Resolve<ApiUserResource, ApiUser>());

    var meta = new
    {
        apiUsers.PageIndex,
        apiUsers.PageSize,
        apiUsers.PageCount,
        apiUsers.TotalItemsCount,
        PreviousPageUrl = apiUsers.HasPreviousPage ? CreateApiUserUrl(parameters, ResourceUriType.PreviousPage) : string.Empty,
        NextPageUrl = apiUsers.HasNextPage ? CreateApiUserUrl(parameters, ResourceUriType.NextPage) : string.Empty,
    };
    Response.Headers.Add(
        "X-Pagination",
        JsonConvert.SerializeObject(
            meta,
            new JsonSerializerSettings
            { ContractResolver = new CamelCasePropertyNamesContractResolver() }));
    return Ok(sortedApiUserResources);
}

private string CreateApiUserUrl(ApiUserParameters parameters, ResourceUriType uriType)
{
    var param = new {
        parameters.PageIndex,
        parameters.PageSize
    };
    switch (uriType)
    {
        case ResourceUriType.PreviousPage:
            param = new
            {
                PageIndex = parameters.PageIndex - 1,
                parameters.PageSize
            };
            break;
        case ResourceUriType.NextPage:
            param = new
            {
                PageIndex = parameters.PageIndex + 1,
                parameters.PageSize
            };
            break;
        case ResourceUriType.CurrentPage:
            break;
        default: break;
    }
    return Url.Link("GetAllApiUsers", param);
}

13. 资源塑形(Resource shaping)

返回 资源的指定字段

ApiStudy.Infrastructure.Extensions.TypeExtensions
namespace ApiStudy.Infrastructure.Extensions
{
    using System;
    using System.Collections.Generic;
    using System.Reflection;

    public static class TypeExtensions
    {
        public static IEnumerable<PropertyInfo> GetProeprties(this Type source, string fields = null)
        {
            List<PropertyInfo> propertyInfoList = new List<PropertyInfo>();
            if (string.IsNullOrEmpty(fields))
            {
                propertyInfoList.AddRange(source.GetProperties(BindingFlags.Public | BindingFlags.Instance));
            }
            else
            {
                string[] properties = fields.Trim().Split(',');
                foreach (string propertyName in properties)
                {
                    propertyInfoList.Add(
                        source.GetProperty(
                        propertyName.Trim(),
                        BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase));
                }
            }
            return propertyInfoList;
        }
    }
}
ApiStudy.Infrastructure.Extensions.ObjectExtensions
namespace ApiStudy.Infrastructure.Extensions
{
    using System.Collections.Generic;
    using System.Dynamic;
    using System.Linq;
    using System.Reflection;

    public static class ObjectExtensions
    {
        public static ExpandoObject ToDynamicObject(this object source, in string fields = null)
        {
            List<PropertyInfo> propertyInfoList = source.GetType().GetProeprties(fields).ToList();

            ExpandoObject expandoObject = new ExpandoObject();
            foreach (PropertyInfo propertyInfo in propertyInfoList)
            {
                try
                {
                    (expandoObject as IDictionary<string, object>).Add(
                    propertyInfo.Name, propertyInfo.GetValue(source));
                }
                catch { continue; }
            }
            return expandoObject;
        }

        internal static ExpandoObject ToDynamicObject(this object source, in IEnumerable<PropertyInfo> propertyInfos, in string fields = null)
        {
            ExpandoObject expandoObject = new ExpandoObject();
            foreach (PropertyInfo propertyInfo in propertyInfos)
            {
                try
                {
                    (expandoObject as IDictionary<string, object>).Add(
                    propertyInfo.Name, propertyInfo.GetValue(source));
                }
                catch { continue; }
            }
            return expandoObject;
        }
    }
}
ApiStudy.Infrastructure.Extensions.IEnumerableExtensions
namespace ApiStudy.Infrastructure.Extensions
{
    using System;
    using System.Collections.Generic;
    using System.Dynamic;
    using System.Linq;
    using System.Reflection;

    public static class IEnumerableExtensions
    {
        public static IEnumerable<ExpandoObject> ToDynamicObject<T>(
            this IEnumerable<T> source,in string fields = null)
        {
            if (source == null) throw new ArgumentNullException(nameof(source));

            List<ExpandoObject> expandoObejctList = new List<ExpandoObject>();
            List<PropertyInfo> propertyInfoList = typeof(T).GetProeprties(fields).ToList();
            foreach(T x in source)
            {
                expandoObejctList.Add(x.ToDynamicObject(propertyInfoList, fields));
            }
            return expandoObejctList;
        }
    }
}
ApiStudy.Infrastructure.Services.TypeHelperServices
namespace ApiStudy.Infrastructure.Services
{
    using System.Reflection;

    public class TypeHelperServices : ITypeHelperServices
    {
        public bool HasProperties<T>(string fields)
        {
            if (string.IsNullOrEmpty(fields)) return true;

            string[] splitFields = fields.Split(',');
            foreach(string splitField in splitFields)
            {
                string proeprtyName = splitField.Trim();
                PropertyInfo propertyInfo = typeof(T).GetProperty(
                    proeprtyName, BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
                if (propertyInfo == null) return false;
            }
            return true;
        }
    }
}
UserContext.GetAllApiUsers(), UserContext.Get()
[HttpGet(Name = "GetAllApiUsers")]
public async Task<IActionResult> GetAllApiUsers(ApiUserParameters parameters)
{
    //added code
    if (!_typeHelper.HasProperties<ApiUserResource>(parameters.Fields))
        return BadRequest("fields not exist.");

    if (!_propertyMappingContainer.ValidateMappingExistsFor<ApiUserResource, ApiUser>(parameters.OrderBy))
        return BadRequest("can't find fields for sorting.");

    PaginatedList<ApiUser> apiUsers = await _apiUserRepository.GetAllApiUsersAsync(parameters);

    IEnumerable<ApiUserResource> apiUserResources =
        _mapper.Map<IEnumerable<ApiUser>, IEnumerable<ApiUserResource>>(apiUsers);

    IEnumerable<ApiUserResource> sortedApiUserResources =
        apiUserResources.AsQueryable().ApplySort(
            parameters.OrderBy, _propertyMappingContainer.Resolve<ApiUserResource, ApiUser>());

    //modified code
    IEnumerable<ExpandoObject> sharpedApiUserResources =
        sortedApiUserResources.ToDynamicObject(parameters.Fields);

    var meta = new
    {
        apiUsers.PageIndex,
        apiUsers.PageSize,
        apiUsers.PageCount,
        apiUsers.TotalItemsCount,
        PreviousPageUrl = apiUsers.HasPreviousPage ? CreateApiUserUrl(parameters, ResourceUriType.PreviousPage) : string.Empty,
        NextPageUrl = apiUsers.HasNextPage ? CreateApiUserUrl(parameters, ResourceUriType.NextPage) : string.Empty,
    };
    Response.Headers.Add(
        "X-Pagination",
        JsonConvert.SerializeObject(
            meta,
            new JsonSerializerSettings
            { ContractResolver = new CamelCasePropertyNamesContractResolver() }));
    //modified code
    return Ok(sharpedApiUserResources);
}

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

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

14. HATEOAS

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

  • 可进化性,自我描述
  • 超媒体(Hypermedia,例如超链接)驱动如何消
    费和使用API
UserContext
private IEnumerable<LinkResource> CreateLinksForApiUser(string guid,string fields = null)
{
    List<LinkResource> linkResources = new List<LinkResource>();
    if (string.IsNullOrEmpty(fields))
    {
        linkResources.Add(
            new LinkResource(Url.Link("GetApiUser", new { guid }), "self", "get"));
    }
    else
    {
        linkResources.Add(
            new LinkResource(Url.Link("GetApiUser", new { guid, fields }), "self", "get"));
    }

    linkResources.Add(
            new LinkResource(Url.Link("DeleteApiUser", new { guid }), "self", "Get"));
    return linkResources;
}

private IEnumerable<LinkResource> CreateLinksForApiUsers(ApiUserParameters parameters,bool hasPrevious,bool hasNext)
{
    List<LinkResource> resources = new List<LinkResource>();

    resources.Add(
            new LinkResource(
                CreateApiUserUrl(parameters,ResourceUriType.CurrentPage),
                "current_page", "get"));
    if (hasPrevious)
        resources.Add(
            new LinkResource(
                CreateApiUserUrl(parameters, ResourceUriType.PreviousPage),
                "previous_page", "get"));
    if (hasNext)
        resources.Add(
            new LinkResource(
                CreateApiUserUrl(parameters, ResourceUriType.NextPage),
                "next_page", "get"));

    return resources;
}

[HttpGet(Name = "GetAllApiUsers")]
public async Task<IActionResult> GetAllApiUsers(ApiUserParameters parameters)
{
    if (!_typeHelper.HasProperties<ApiUserResource>(parameters.Fields))
        return BadRequest("fields not exist.");

    if (!_propertyMappingContainer.ValidateMappingExistsFor<ApiUserResource, ApiUser>(parameters.OrderBy))
        return BadRequest("can't find fields for sorting.");

    PaginatedList<ApiUser> apiUsers = await _apiUserRepository.GetAllApiUsersAsync(parameters);

    IEnumerable<ApiUserResource> apiUserResources =
        _mapper.Map<IEnumerable<ApiUser>, IEnumerable<ApiUserResource>>(apiUsers);

    IEnumerable<ApiUserResource> sortedApiUserResources =
        apiUserR

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 一、NoSQL簡介 1、NoSQL 概念 NoSQL( Not Only SQL ),意即"不僅僅是SQL"。對不同於傳統的關係型資料庫的資料庫管理系統的統稱。NoSQL用於超大規模數據的存儲。這些類型的數據存儲不需要固定的模式,無需多餘操作就可以橫向擴展。 2、NoSQL的優點/缺點 二、Mong ...
  • Spring容器是一個大工廠,負責創建、管理所有的Bean。 Spring容器支持2種格式的配置文件:xml文件、properties文件,最常用的是xml文件。 Bean在xml文件中的配置 <beans> 根元素,可包含多個<bean>元素,一個<bean>即一個Bean的配置。 <bean> ...
  • 今天我開始重新一點一滴的學習python,雖然之前也學習過python,但是當時的學習比較雜亂,知識點較為凌亂,所以感覺學習效果不是很好,寫一些程式或者是演算法都不能夠得心應手,因此決定重新學習學習python,因為之前有學習過C語言和JAVA的原因,學習過程比較快,但是還是要告誡一些初學者或者是其他 ...
  • Day1 1.電腦基礎 1. 什麼是電腦 輸入輸出設備 CPU(中央處理器):處理各種數據,相當於人的大腦 記憶體:存儲數據,相當於人的臨時記憶 硬碟:存儲數據,相當於人的永久記憶 2. 什麼是操作系統 控制電腦硬體工作的流程 軟體 3. 什麼是應用程式 安裝在操作系統之上的軟體 2.Pytho ...
  • [TOC] 一、源碼下載 Qt庫封裝了很多很控制項,種類也比較多,其中容器控制項包括:表格、樹和列表。 使用過QtDesigner的同學應該都知道,這個工具中有一個屬性編輯器,是一個表格樹控制項,就像vs中控制項屬性面板一樣。 今天我們就來介紹一款使用QTreeWidget封裝的表格樹控制項QtTreePro ...
  • 前言 只有光頭才能變強。 文本已收錄至我的GitHub倉庫,歡迎Star: "https://github.com/ZhongFuCheng3y/3y" 回顧上一篇: "《大型網站系統與Java中間件》讀書筆記(一)" 這周周末讀了第四章,現在過來做做筆記,希望能幫助到大家。 註:在看這篇文章之前, ...
  • 微信公眾號掃一掃功能提示:10003 redirect_uri功能變數名稱與後臺不一致 Senparc.Weixin組件很好用,但一個坑,不知道這和個是否有關。。 先說明下環境,centos+.net core 2.2 .netcore 直接dotnet run ,用nohup運行起來,配置埠為80,Us ...
  • Redis 是一個開源的使用 ANSI C語言編寫的支持網路、可基於記憶體也可持久化的日誌型、Key Value 資料庫。 常用它來存儲緩存數據,能非常輕鬆的實現緩存過期刷新機制。 多種語言都可以連接到 Redis 資料庫伺服器,本文將推薦一個非常簡潔的 C 連接 Redis 資料庫的開源項目。 一般 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...