欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

Asp.NetCoreWebApi - RESTful Api

程序员文章站 2022-04-09 19:51:28
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,比如: .
  • 传输的资源: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相关的包:
Asp.NetCoreWebApi - RESTful Api

为xxxx.api项目安装 pomelo.entityframeworkcore.mysql
Asp.NetCoreWebApi - RESTful Api

3.2.2. 建立entity和context

Asp.NetCoreWebApi - RESTful Api

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. 数据库迁移结果

Asp.NetCoreWebApi - RESTful Api

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

Asp.NetCoreWebApi - RESTful Api

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