Asp.NetCoreWebApi - RESTful Api
-
- 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. 为数据库创建种子数据
-
- 8.1. 弄一个类,写一个扩展方法处理异常
- 8.2. 在configuration中使用扩展方法
-
- 9.1. 为entity类创建对应的resource类
- 9.2. 使用 automapper
-
- 10.1. 安装nuget包
- 10.2. 为每一个resource配置验证器
-
- 11.1. 资源命名
- 11.1.1. 资源应该使用名词,例
- 11.1.2. 资源命名层次结构
- 11.2. 内容协商
- 11.1. 资源命名
-
- 12.1. 构造翻页请求参数类
- 12.2. repository实现支持翻页请求参数的方法
- 12.3. 搜索(过滤)
- 12.4. 排序
- 12.4.1. 排序思路
-
- 14.1. 创建供应商特定媒体类型
- 14.1.1. 判断media type类型
- 14.1. 创建供应商特定媒体类型
参考 :
1. rest
rest : 具象状态传输(representational state transfer,简称rest),是roy thomas fielding博士于2000年在他的博士论文 "architectural styles and the design of network-based software architectures" 中提出来的一种万维网软件架构风格。
目前在三种主流的web服务实现方案中,因为rest模式与复杂的soap和xml-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
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
- propertymapping(apiuserpropertymapping)
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); }