参考 :
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。它从以下三个方面资源进行定义:
put和delete方法是幂等方法.get方法是安全方法(不会对服务器端有修改,因此当然也是幂等的).
ps 关于幂等方法 :
看这篇 理解http幂等性.
简单说,客户端多次请求服务端返回的结果都相同,那么就说这个操作是幂等的.(个人理解,详细的看上面给的文章)
不像基于soap的web服务,restful web服务并没有“正式”的标准。这是因为rest是一种架构,而soap只是一个协议。虽然rest不是一个标准,但大部分restful web服务实现会使用http、uri、json和xml等各种标准。
括号中是相应的sql命令.
这里以用户增删改查为例.
参考asp.net core webapi 开发-新建webapi项目.
注意,本文建立的asp.netcore core版本是2.2,不建议使用其他版本,2.1版本下会遇到依赖文件冲突问题! core.
这里注意一下,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
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; } } }
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; } } }
services.adddbcontext<usercontext>(options => { string connstring = "server=xxx:xxx:xxx:xxx;database=xxxx;uid=root;pwd=xxxxx; "; options.usemysql(connstring); });
写一个创建种子数据的类
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方法
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); }
将所有http请求全部映射到https
startup中:
configureservices方法注册,并配置端口和状态码等:
services.addhttpsredirection(…)
services.addhttpsredirection(options => { options.redirectstatuscode = statuscodes.status307temporaryredirect; options.httpsport = 5001; });
configure方法使用该中间件:
app.usehttpsredirection()
configureservices方法注册
看官方文档
services.addhsts(options => { options.preload = true; options.includesubdomains = true; options.maxage = timespan.fromdays(60); options.excludedhosts.add("example.com"); options.excludedhosts.add("www.example.com"); });
configure方法配置中间件管道
app.usehsts();
注意 app.usehsts() 方法最好放在 app.usehttps() 方法之后.
有关日志的微软官方文档
serillog github仓库
该github仓库上有详细的使用说明.
使用方法:
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; }
默认 appsettings.json
configurationbuilder().addjsonfile("appsettings.json").build()-->iconfigurationroot(iconfiguration)
iconfiguration[“key:childkey”]
针对”connectionstrings:xxx”,可以使用iconfiguration.getconnectionstring(“xxx”)
private static iconfiguration configuration { get; set; } public startupdevelopment(iconfiguration config) { configuration = config; } ... configuration[“key:childkey”]
namespace apistudy.api.extensions { using microsoft.aspnetcore.builder; using microsoft.aspnetcore.http; using microsoft.extensions.logging; using system; public static class exceptionhandlingextensions { public static void usecustomexceptionhandler(this iapplicationbuilder app,iloggerfactory loggerfactory) { app.useexceptionhandler( builder => builder.run(async context => { context.response.statuscode = statuscodes.status500internalservererror; context.response.contenttype = "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."); })); } } }
// 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(); //使用默认路由 }
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; } } }
添加nuget包
automapper
automapper.extensions.microsoft.dependencyinjection
配置映射
可以创建profile
createmap<tsource,tdestination>()
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()
继承于abstractvalidator
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>();
[httpget] public async task<iactionresult> get() { ienumerable<apiuser> apiusers = await _apiuserrepository.getallapiusersasync(); ienumerable<apiuserresource> apiuserresources = _mapper.map<ienumerable<apiuser>,ienumerable<apiuserresource>>(apiusers); return ok(apiuserresources); } [httpget("{guid}")] public async task<iactionresult> get(string guid) { apiuser apiuser = await _apiuserrepository.getapiuserbyguidasync(guid.parse(guid)); if (apiuser is null) return notfound(); apiuserresource apiuserresource = _mapper.map<apiuser,apiuserresource>(apiuser); return ok(apiuserresource); }
api/department/{departmentid}/emoloyees
, 这就表示了 department
(部门)和员工api/department/{departmentid}/emoloyees/{employeeid}
,就表示了该部门下的某个员core支持输出和输入两种格式化器.
services.addmvc(options => { options.returnhttpnotacceptable = true; });
options.outputformatters.add(newxmldatacontractserializeroutputformatter());
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; } } }
namespace apistudy.core.entities { public class apiuserparameters:queryparameters { public string username { get; set; } } }
/*----- 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);
... [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中据说已经修复这个问题.
修改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); }
思路:
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; } } }
namespace apistudy.infrastructure.services { using system.collections.generic; public interface ipropertymapping { dictionary<string, list<mappedproperty>> mappingdictionary { get; } } }
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; } } }
namespace apistudy.infrastructure.services { public interface ipropertymappingcontainer { void register<t>() where t : ipropertymapping, new(); ipropertymapping resolve<tsource, tdestination>(); bool validatemappingexistsfor<tsource, tdestination>(string fields); } }
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; } } }
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; } } }
[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); }
返回 资源的指定字段
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; } } }
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; } } }
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; } } }
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; } } }
[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
services.addmvc(options => { options.returnhttpnotacceptable = true; options.outputformatters.add(new xmldatacontractserializeroutputformatter()); }) .addjsonoptions(options => { //added code options.serializersettings.contractresolver = new camelcasepropertynamescontractresolver(); });
rest里最复杂的约束,构建成熟restapi的核心
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); }
//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中注册这个媒体类型,上面的代码给出了具体操作.
[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}]."); }
[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; }
[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); }
如对本文有疑问, 点击进行留言回复!!
网友评论