1. rest
    1. 常用http动词
    1. webapi 在 asp.netcore 中的实现
    • 3.1. 创建webapi项目.
    • 3.2. 集成entity framework core操作mysql
      • 3.2.1. 安装相关的包(为xxxx.infrastructure项目安装)
      • 3.2.2. 建立entity和context
      • 3.2.3. configureservice中注入ef服务
      • 3.2.4. 迁移数据库
      • 3.2.5. 数据库迁移结果
      • 3.2.6. 为数据库创建种子数据
    1. 支持https
    1. 支持hsts
    1. 使用serillog
    • 6.1. 安装nuget包
    • 6.2. 添加代码
    • 6.3. 自行测试
    1. asp.netcore配置文件
    • 7.1. 默认配置文件
    • 7.2. 获得配置
    1. 自定义一个异常处理,exceptionhandler
    • 8.1. 弄一个类,写一个扩展方法处理异常
    • 8.2. 在configuration中使用扩展方法
    1. 实现数据接口类(resource),使用automapper在resource和entity中映射
    • 9.1. 为entity类创建对应的resource类
    • 9.2. 使用 automapper
    1. 使用fluentvalidation
    • 10.1. 安装nuget包
    • 10.2. 为每一个resource配置验证器
    1. 实现http get(翻页,过滤,排序)
    • 11.1. 资源命名
      • 11.1.1. 资源应该使用名词,例
      • 11.1.2. 资源命名层次结构
    • 11.2. 内容协商
    1. 翻页
    • 12.1. 构造翻页请求参数类
    • 12.2. repository实现支持翻页请求参数的方法
    • 12.3. 搜索(过滤)
    • 12.4. 排序
      • 12.4.1. 排序思路
    1. 资源塑形(resource shaping)
    1. hateoas
    • 14.1. 创建供应商特定媒体类型
      • 14.1.1. 判断media type类型

参考 :

  • asp.net core web api 开发-restful api实现
  • 理解http幂等性
  • 某站微软mvp杨旭的asp.netcore webapi的视频

1. rest

rest : 具象状态传输(representational state transfer,简称rest),是roy thomas fielding博士于2000年在他的博士论文 “architectural styles and the design of network-based software architectures” 中提出来的一种万维网软件架构风格。
目前在三种主流的web服务实现方案中,因为rest模式与复杂的soapxml-rpc相比更加简洁,越来越多的web服务开始采用rest风格设计和实现。例如,amazon.com提供接近rest风格的web服务执行图书查询;

符合rest设计风格的web api称为restful api。它从以下三个方面资源进行定义:

  • 直观简短的资源地址:uri,比如: .
  • 传输的资源:web服务接受与返回的互联网媒体类型,比如:json,xml,yaml等…
  • 对资源的操作:web服务在该资源上所支持的一系列请求方法(比如:post,get,put或delete).

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

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

不像基于soap的web服务,restful web服务并没有“正式”的标准。这是因为rest是一种架构,而soap只是一个协议。虽然rest不是一个标准,但大部分restful web服务实现会使用http、uri、json和xml等各种标准。

2. 常用http动词

括号中是相应的sql命令.

  • get(select) : 从服务器取出资源(一项或多项).
  • post(create) : 在服务器新建一个资源.
  • put(update) : 在服务器更新资源(客户端提供改变后的完整资源).
  • patch(update) : 在服务器更新资源(客户端提供改变的属性).
  • delete(delete) : 在服务器删除资源.

3. webapi 在 asp.netcore 中的实现

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

3.1. 创建webapi项目.

参考asp.net core webapi 开发-新建webapi项目.

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

3.2. 集成entity framework core操作mysql

3.2.1. 安装相关的包(为xxxx.infrastructure项目安装)

  • microsoft.entityframeworkcore.design
  • pomelo.entityframeworkcore.mysql

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

ps: mysql文档原文:

install the mysql.data.entityframeworkcore nuget package.
for ef core 1.1 only: if you plan to scaffold a database, install the mysql.data.entityframeworkcore.design nuget package as well.

efcore – mysql文档
mysql版本要求:
mysql版本要高于5.7
使用最新版本的mysql connector(2019 6/27 目前是8.x).

为xxxx.infrastructure项目安装efcore相关的包:

为xxxx.api项目安装 pomelo.entityframeworkcore.mysql

3.2.2. 建立entity和context

apiuser

namespace apistudy.core.entities
{
    using system;

    public class apiuser
    {
        public guid guid { get; set; }
        public string name { get; set; }
        public string passwd { get; set; }
        public datetime registrationdate { get; set; }
        public datetime birth { get; set; }
        public string profilephotourl { get; set; }
        public string phonenumber { get; set; }
        public string email { get; set; }
    }
}

usercontext

namespace apistudy.infrastructure.database
{
    using apistudy.core.entities;
    using microsoft.entityframeworkcore;

    public class usercontext:dbcontext
    {
        public usercontext(dbcontextoptions<usercontext> options): base(options)
        {
        }

        protected override void onmodelcreating(modelbuilder modelbuilder)
        {
            modelbuilder.entity<apiuser>().haskey(u => u.guid);

            base.onmodelcreating(modelbuilder);
        }

        public dbset<apiuser> apiusers { get; set; }
    }
}

3.2.3. configureservice中注入ef服务

services.adddbcontext<usercontext>(options =>
            {
                string connstring = "server=xxx:xxx:xxx:xxx;database=xxxx;uid=root;pwd=xxxxx; ";
                options.usemysql(connstring);
            });

3.2.4. 迁移数据库

  • 在tools > nuget package manager > package manager console输入命令.
  • add-migration xxx 添加迁移.
    ps : 如果迁移不想要,使用 remove-migration 命令删除迁移.
  • update-database 更新到数据库.

3.2.5. 数据库迁移结果

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

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

    usercontextseed

    namespace apistudy.infrastructure.database
    {
        using apistudy.core.entities;
        using microsoft.extensions.logging;
        using system;
        using system.linq;
        using system.threading.tasks;
    
        public class usercontextseed
        {
            public static async task seedasync(usercontext context,iloggerfactory loggerfactory)
            {
                try
                {
                    if (!context.apiusers.any())
                    {
                        context.apiusers.addrange(
                            new apiuser
                            {
                                guid = guid.newguid(),
                                name = "la",
                                birth = new datetime(1998, 11, 29),
                                registrationdate = new datetime(2019, 6, 28),
                                passwd = "123587",
                                profilephotourl = "https://www.laggage.top/",
                                phonenumber = "10086",
                                email = "yu@outlook.com"
                            },
                            new apiuser
                            {
                                guid = guid.newguid(),
                                name = "david",
                                birth = new datetime(1995, 8, 29),
                                registrationdate = new datetime(2019, 3, 28),
                                passwd = "awt87495987",
                                profilephotourl = "https://www.laggage.top/",
                                phonenumber = "1008611",
                                email = "david@outlook.com"
                            },
                            new apiuser
                            {
                                guid = guid.newguid(),
                                name = "david",
                                birth = new datetime(2001, 8, 19),
                                registrationdate = new datetime(2019, 4, 25),
                                passwd = "awt87495987",
                                profilephotourl = "https://www.laggage.top/",
                                phonenumber = "1008611",
                                email = "david@outlook.com"
                            },
                            new apiuser
                            {
                                guid = guid.newguid(),
                                name = "linus",
                                birth = new datetime(1999, 10, 26),
                                registrationdate = new datetime(2018, 2, 8),
                                passwd = "awt87495987",
                                profilephotourl = "https://www.laggage.top/",
                                phonenumber = "17084759987",
                                email = "linus@outlook.com"
                            },
                            new apiuser
                            {
                                guid = guid.newguid(),
                                name = "youyou",
                                birth = new datetime(1992, 1, 26),
                                registrationdate = new datetime(2015, 7, 8),
                                passwd = "grwe874864987",
                                profilephotourl = "https://www.laggage.top/",
                                phonenumber = "17084759987",
                                email = "youyou@outlook.com"
                            },
                            new apiuser
                            {
                                guid = guid.newguid(),
                                name = "小白",
                                birth = new datetime(1997, 9, 30),
                                registrationdate = new datetime(2018, 11, 28),
                                passwd = "gewa749864",
                                profilephotourl = "https://www.laggage.top/",
                                phonenumber = "17084759987",
                                email = "baibai@outlook.com"
                            });
    
                        await context.savechangesasync();
                    }
                }
                catch(exception ex)
                {
                    ilogger logger = loggerfactory.createlogger<usercontextseed>();
                    logger.logerror(ex, "error occurred while seeding database");
                }
            }
        }
    }
    
    
  • 修改program.main方法

    program.main

    iwebhost host = createwebhostbuilder(args).build();
    
    using (iservicescope scope = host.services.createscope())
    {
        iserviceprovider provider = scope.serviceprovider;
        usercontext usercontext = provider.getservice<usercontext>();
        iloggerfactory loggerfactory = provider.getservice<iloggerfactory>();
        usercontextseed.seedasync(usercontext, loggerfactory).wait();
    }
    
    host.run();
    

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

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

解决方案:
修改 usercontext.onmodelcreating 方法
配置一下 apiuser.guid 属性到mysql数据库的映射:

protected override void onmodelcreating(modelbuilder modelbuilder)
{
    modelbuilder.entity<apiuser>().property(p => p.guid)
        .hascolumntype("nvarchar(256)");
    modelbuilder.entity<apiuser>().haskey(u => u.guid);
    
    base.onmodelcreating(modelbuilder);
}

4. 支持https

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

startup中:
configureservices方法注册,并配置端口和状态码等:
services.addhttpsredirection(…)

services.addhttpsredirection(options =>
                {
                    options.redirectstatuscode = statuscodes.status307temporaryredirect;
                    options.httpsport = 5001;
                });

configure方法使用该中间件:

app.usehttpsredirection()

5. 支持hsts

configureservices方法注册
看官方文档

services.addhsts(options =>
{
    options.preload = true;
    options.includesubdomains = true;
    options.maxage = timespan.fromdays(60);
    options.excludedhosts.add("example.com");
    options.excludedhosts.add("www.example.com");
});

configure方法配置中间件管道

app.usehsts();

注意 app.usehsts() 方法最好放在 app.usehttps() 方法之后.

6. 使用serillog

有关日志的微软官方文档

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

使用方法:

6.1. 安装nuget包

  • serilog.aspnetcore
  • serilog.sinks.console

6.2. 添加代码

program.main方法中:

log.logger = new loggerconfiguration()
            .minimumlevel.debug()
            .minimumlevel.override("microsoft", logeventlevel.information)
            .enrich.fromlogcontext()
            .writeto.console()
            .createlogger();

修改program.createwebhostbuilder(…)

 public static iwebhostbuilder createwebhostbuilder(string[] args) =>
            webhost.createdefaultbuilder(args)
                .usestartup<startup>()
                .useserilog(); // <-- add this line;
 }

6.3. 自行测试

7. asp.netcore配置文件

7.1. 默认配置文件

默认 appsettings.json
configurationbuilder().addjsonfile(“appsettings.json”).build()–>iconfigurationroot(iconfiguration)

7.2. 获得配置

iconfiguration[“key:childkey”]
针对”connectionstrings:xxx”,可以使用iconfiguration.getconnectionstring(“xxx”)

private static iconfiguration configuration { get; set; }

public startupdevelopment(iconfiguration config)
{
    configuration = config;
}

...

configuration[“key:childkey”]

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

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

namespace apistudy.api.extensions
{
    using microsoft.aspnetcore.builder;
    using microsoft.aspnetcore.http;
    using microsoft.extensions.logging;
    using system;

    public static class exceptionhandlingextensions
    {
        public static void usecustomexceptionhandler(this iapplicationbuilder app,iloggerfactory loggerfactory)
        {
            app.useexceptionhandler(
                builder => builder.run(async context =>
                {
                    context.response.statuscode = statuscodes.status500internalservererror;
                    context.response.contenttype = "application/json";

                    exception ex = context.features.get<exception>();
                    if (!(ex is null))
                    {
                        ilogger logger = loggerfactory.createlogger("apistudy.api.extensions.exceptionhandlingextensions");
                        logger.logerror(ex, "error occurred.");
                    }
                    await context.response.writeasync(ex?.message ?? "error occurred, but cannot get exception message.for more detail, go to see the log.");
                }));
        }
    }
}

8.2. 在configuration中使用扩展方法

// this method gets called by the runtime. use this method to configure the http request pipeline.
public void configure(iapplicationbuilder app, iloggerfactory loggerfactory)
{
    app.usecustomexceptionhandler(loggerfactory);  //modified code

    //app.usedeveloperexceptionpage();
    app.usehsts();
    app.usehttpsredirection();

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

9. 实现数据接口类(resource),使用automapper在resource和entity中映射

9.1. 为entity类创建对应的resource类

apiuserresource

namespace apistudy.infrastructure.resources
{
    using system;

    public class apiuserresource
    {
        public guid guid { get; set; }
        public string name { get; set; }
        //public string passwd { get; set; }
        public datetime registrationdate { get; set; }
        public datetime birth { get; set; }
        public string profilephotourl { get; set; }
        public string phonenumber { get; set; }
        public string email { get; set; }
    }
}

9.2. 使用 automapper

  • 添加nuget包
    automapper
    automapper.extensions.microsoft.dependencyinjection

  • 配置映射
    可以创建profile
    createmap<tsource,tdestination>()

    mappingprofile

    namespace apistudy.api.extensions
    {
        using apistudy.core.entities;
        using apistudy.infrastructure.resources;
        using automapper;
        using system;
        using system.text;
    
        public class mappingprofile : profile
        {
            public mappingprofile()
            {
                createmap<apiuser, apiuserresource>()
                    .formember(
                    d => d.passwd, 
                    opt => opt.addtransform(s => convert.tobase64string(encoding.default.getbytes(s))));
    
                createmap<apiuserresource, apiuser>()
                    .formember(
                    d => d.passwd,
                    opt => opt.addtransform(s => encoding.default.getstring(convert.frombase64string(s))));
            }
        }
    }
    
    
  • 注入服务
    services.addautomapper()

10. 使用fluentvalidation

fluentvalidation官网

10.1. 安装nuget包

  • fluentvalidation
  • fluentvalidation.aspnetcore

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

  • 继承于abstractvalidator

    apiuserresourcevalidator

    namespace apistudy.infrastructure.resources
    {
        using fluentvalidation;
    
        public class apiuserresourcevalidator : abstractvalidator<apiuserresource>
        {
            public apiuserresourcevalidator()
            {
                rulefor(s => s.name)
                    .maximumlength(80)
                    .withname("用户名")
                    .withmessage("{propertyname}的最大长度为80")
                    .notempty()
                    .withmessage("{propertyname}不能为空!");
            }
        }
    }
    
  • 注册到容器:services.addtransient<>()
    services.addtransient<ivalidator<apiuserresource>, apiuserresourcevalidator>();

11. 实现http get(翻页,过滤,排序)

基本的get实现

[httpget]
public async task<iactionresult> get()
{
    ienumerable<apiuser> apiusers = await _apiuserrepository.getallapiusersasync();

    ienumerable<apiuserresource> apiuserresources = 
        _mapper.map<ienumerable<apiuser>,ienumerable<apiuserresource>>(apiusers);

    return ok(apiuserresources);
}

[httpget("{guid}")]
public async task<iactionresult> get(string guid)
{
    apiuser apiuser = await _apiuserrepository.getapiuserbyguidasync(guid.parse(guid));

    if (apiuser is null) return notfound();

    apiuserresource apiuserresource = _mapper.map<apiuser,apiuserresource>(apiuser);

    return ok(apiuserresource);
}

11.1. 资源命名

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

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

11.1.2. 资源命名层次结构

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

11.2. 内容协商

asp.net core支持输出和输入两种格式化器.

  • 用于输出的media type放在accept header里,表示客户端接受这种格式的输出.
  • 用于输入的media type放content-type header里,表示客户端传进来的数据是这种格式.
  • returnhttpnotacceptable设为true,如果客户端请求不支持的数据格式,就会返回406.
    services.addmvc(options =>
    {
        options.returnhttpnotacceptable = true;
    });
    
  • 支持输出xml格式:options.outputformatters.add(newxmldatacontractserializeroutputformatter());

12. 翻页

12.1. 构造翻页请求参数类

queryparameters

namespace apistudy.core.entities
{
    using system.collections.generic;
    using system.componentmodel;
    using system.runtime.compilerservices;

    public abstract class queryparameters : inotifypropertychanged
    {
        public event propertychangedeventhandler propertychanged;

        private const int defaultpagesize = 10;
        private const int defaultmaxpagesize = 100;

        private int _pageindex = 1;
        public virtual int pageindex
        {
            get => _pageindex;
            set => setfield(ref _pageindex, value);
        }

        private int _pagesize = defaultpagesize;
        public virtual int pagesize
        {
            get => _pagesize;
            set => setfield(ref _pagesize, value);
        }

        private int _maxpagesize = defaultmaxpagesize;
        public virtual int maxpagesize
        {
            get => _maxpagesize;
            set => setfield(ref _maxpagesize, value);
        }

        public string orderby { get; set; }
        public string fields { get; set; }

        protected void setfield<tfield>(
            ref tfield field,in tfield newvalue,[callermembername] string propertyname = null)
        {
            if (equalitycomparer<tfield>.default.equals(field, newvalue))
                return;
            field = newvalue;
            if (propertyname == nameof(pagesize) || propertyname == nameof(maxpagesize)) setpagesize();
            
            propertychanged?.invoke(this, new propertychangedeventargs(propertyname));
            
        }

        private void setpagesize()
        {
            if (_maxpagesize <= 0) _maxpagesize = defaultmaxpagesize;
            if (_pagesize <= 0) _pagesize = defaultpagesize;
            _pagesize = _pagesize > _maxpagesize ? _maxpagesize : _pagesize;
        }
    }
}

apiuserparameters

namespace apistudy.core.entities
{
    public class apiuserparameters:queryparameters
    {
        public string username { get; set; }
    }
}

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

repository相关代码

/*----- apiuserrepository -----*/
public paginatedlist<apiuser> getallapiusers(apiuserparameters parameters)
{
    return new paginatedlist<apiuser>(
        parameters.pageindex,
        parameters.pagesize,
        _context.apiusers.count(),
        _context.apiusers.skip(parameters.pageindex * parameters.pagesize)
        .take(parameters.pagesize));
}

public task<paginatedlist<apiuser>> getallapiusersasync(apiuserparameters parameters)
{
    return task.run(() => getallapiusers(parameters));
}

/*----- iapiuserrepository -----*/
paginatedlist<apiuser> getallapiusers(apiuserparameters parameters);
task<paginatedlist<apiuser>> getallapiusersasync(apiuserparameters parameters);

usercontroller部分代码

...

[httpget(name = "getallapiusers")]
public async task<iactionresult> getallapiusers(apiuserparameters parameters)
{
    paginatedlist<apiuser> apiusers = await _apiuserrepository.getallapiusersasync(parameters);

    ienumerable<apiuserresource> apiuserresources = 
        _mapper.map<ienumerable<apiuser>,ienumerable<apiuserresource>>(apiusers);

    var meta = new
    {
        pageindex = apiusers.pageindex,
        pagesize = apiusers.pagesize,
        pagecount = apiusers.pagecount,
        totalitemscount = apiusers.totalitemscount,
        nextpageurl = createapiuserurl(parameters, resourceuritype.nextpage),
        previouspageurl = createapiuserurl(parameters, resourceuritype.previouspage)
    };
    response.headers.add(
        "x-pagination",
        jsonconvert.serializeobject(
            meta, 
            new jsonserializersettings
            { contractresolver = new camelcasepropertynamescontractresolver() }));
    return ok(apiuserresources);
}

...

private string createapiuserurl(apiuserparameters parameters,resourceuritype uritype)
{
    var param = new apiuserparameters
    {
        pageindex = parameters.pageindex,
        pagesize = parameters.pagesize
    };
    switch (uritype)
    {
        case resourceuritype.previouspage:
            param.pageindex--;
            break;
        case resourceuritype.nextpage:
            param.pageindex++;
            break;
        case resourceuritype.currentpage:
            break;
        default:break;
    }
    return url.link("getallapiusers", parameters);
}

ps注意,为httpget方法添加参数的话, 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 =
        apiuserresources.asqueryable().applysort(
            parameters.orderby, _propertymappingcontainer.resolve<apiuserresource, apiuser>());

    ienumerable<expandoobject> shapedapiuserresources =
        sortedapiuserresources.todynamicobject(parameters.fields);

    ienumerable<expandoobject> shapedapiuserresourceswithlinks = shapedapiuserresources.select(
        x =>
        {
            idictionary<string, object> dict = x as idictionary<string, object>;
            if(dict.keys.contains("guid"))
                dict.add("links", createlinksforapiuser(dict["guid"] as string));
            return dict as expandoobject;
        });

    var result = new
    {
        value = shapedapiuserresourceswithlinks,
        links = createlinksforapiusers(parameters, apiusers.haspreviouspage, apiusers.hasnextpage)
    };

    var meta = new
    {
        apiusers.pageindex,
        apiusers.pagesize,
        apiusers.pagecount,
        apiusers.totalitemscount,
        //previouspageurl = apiusers.haspreviouspage ? createapiuserurl(parameters, resourceuritype.previouspage) : string.empty,
        //nextpageurl = apiusers.hasnextpage ? createapiuserurl(parameters, resourceuritype.nextpage) : string.empty,
    };
    response.headers.add(
        "x-pagination",
        jsonconvert.serializeobject(
            meta,
            new jsonserializersettings
            { contractresolver = new camelcasepropertynamescontractresolver() }));
    return ok(result);
}

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

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

14.1.1. 判断media type类型

  • [fromheader(name = “accept”)] stringmediatype
//startup.configureservices 中注册媒体类型
services.addmvc(options =>
    {
        options.returnhttpnotacceptable = true;
        //options.outputformatters.add(new xmldatacontractserializeroutputformatter());
        jsonoutputformatter formatter = options.outputformatters.oftype<jsonoutputformatter>().firstordefault();
        formatter.supportedmediatypes.add("application/vnd.laggage.hateoas+json");
    })

// get方法中判断媒体类型
if (mediatype == "application/json") 
    return ok(shapedapiuserresources);
else if (mediatype == "application/vnd.laggage.hateoas+json")
{
    ...
    return;
}

注意,要是的 action 认识 application/vnd.laggage.hateoss+json ,需要在startup.configureservices中注册这个媒体类型,上面的代码给出了具体操作.

usercontext

[httpget(name = "getallapiusers")]
public async task<iactionresult> getallapiusers(apiuserparameters parameters,[fromheader(name = "accept")] string mediatype)
{
    if (!_typehelper.hasproperties<apiuserresource>(parameters.fields))
        return badrequest("fields not exist.");

    if (!_propertymappingcontainer.validatemappingexistsfor<apiuserresource, apiuser>(parameters.orderby))
        return badrequest("can't find fields for sorting.");

    paginatedlist<apiuser> apiusers = await _apiuserrepository.getallapiusersasync(parameters);

    ienumerable<apiuserresource> apiuserresources =
        _mapper.map<ienumerable<apiuser>, ienumerable<apiuserresource>>(apiusers);

    ienumerable<apiuserresource> sortedapiuserresources =
        apiuserresources.asqueryable().applysort(
            parameters.orderby, _propertymappingcontainer.resolve<apiuserresource, apiuser>());

    ienumerable<expandoobject> shapedapiuserresources =
        sortedapiuserresources.todynamicobject(parameters.fields);

    if (mediatype == "application/json") return ok(shapedapiuserresources);
    else if (mediatype == "application/vnd.laggage.hateoas+json")
    {
        ienumerable<expandoobject> shapedapiuserresourceswithlinks = shapedapiuserresources.select(
            x =>
            {
                idictionary<string, object> dict = x as idictionary<string, object>;
                if (dict.keys.contains("guid"))
                    dict.add("links", createlinksforapiuser(dict["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,
        };
        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["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,
    };
    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);
}