(27)ASP.NET Core .NET标准REST库Refit
1.简介
refit是一个受到square的retrofit库(java)启发的自动类型安全rest库。通过httpclient网络请求(post,get,put,delete等封装)把rest api返回的数据转化为poco(plain ordinary c# object,简单c#对象) to json。我们的应用程序通过refit请求网络,实际上是使用refit接口层封装请求参数、header、url等信息,之后由httpclient完成后续的请求操作,在服务端返回数据之后,httpclient将原始的结果交给refit,后者根据用户的需求对结果进行解析的过程。安装组件命令行:
install-package refit
代码例子:
[headers("user-agent: refit integration tests")]//这里因为目标源是githubapi,所以一定要加入这个静态请求标头信息,让其这是一个测试请求,不然会返回数据异常。 public interface igithubapi { [get("/users/{user}")] task<user> getuser(string user); } public class githubapi { public async task<user> getuser() { var githubapi = restservice.for<igithubapi>("https://api.github.com"); var octocat = await githubapi.getuser("octocat"); return octocat; } } public class user { public string login { get; set; } public int? id { get; set; } public string url { get; set; } } [httpget] public async task<actionresult<ienumerable<string>>> get() { var result = await new githubapi().getuser(); return new string[] { result.id.value.tostring(), result.login }; }
注:接口中headers、get这些属性叫做refit的特性。
定义上面的一个igithubapi的rest api接口,该接口定义了一个函数getuser,该函数会通过http get请求去访问服务器的/users/{user}路径把返回的结果封装为user poco对象并返回。其中url路径中的{user}的值为getuser函数中的参数user的取值,这里赋值为octocat。然后通过restservice类来生成一个igithubapi接口的实现并供httpclient调用。
2.api属性
每个方法必须具有提供请求url和http属性。http属性有六个内置注释:get, post, put, delete, patch and head,例:
[get("/users/list")]
您还可以在请求url中指定查询参数:
[get("/users/list?sort=desc")]
还可以使用相对url上的替换块和参数来动态请求资源。替换块是由{and,即&}包围的字母数字字符串。如果参数名称与url路径中的名称不匹配,请使用aliasas属性,例:
[get("/group/{id}/users")] task<list<user>> grouplist([aliasas("id")] int groupid);
请求url还可以将替换块绑定到自定义对象,例:
[get("/group/{request.groupid}/users/{request.userid}")] task<list<user>> grouplist(usergrouprequest request); class usergrouprequest{ int groupid { get;set; } int userid { get;set; } }
未指定为url替换的参数将自动用作查询参数。这与retrofit不同,在retrofit中,必须明确指定所有参数,例:
[get("/group/{id}/users")] task<list<user>> grouplist([aliasas("id")] int groupid, [aliasas("sort")] string sortorder); grouplist(4, "desc");
输出结果:"/group/4/users?sort=desc"
3.动态查询字符串参数(dynamic querystring parameters)
方法还可以传递自定义对象,把对象属性追加到查询字符串参数当中,例如:
public class myqueryparams { [aliasas("order")] public string sortorder { get; set; } public int limit { get; set; } } [get("/group/{id}/users")] task<list<user>> grouplist([aliasas("id")] int groupid, myqueryparams params); [get("/group/{id}/users")] task<list<user>> grouplistwithattribute([aliasas("id")] int groupid, [query(".","search")]myqueryparams params); params.sortorder = "desc"; params.limit = 10; grouplist(4, params)
输出结果:"/group/4/users?order=desc&limit=10"
grouplistwithattribute(4, params)
输出结果:"/group/4/users?search.order=desc&search.limit=10"
您还可以使用[query]指定querystring参数,并将其在非get请求中扁平化,类似于:
[post("/statuses/update.json")] task<tweet> posttweet([query]tweetparams params);
5.集合作为查询字符串参数(collections as querystring parameters)
方法除了支持传递自定义对象查询,还支持集合查询的,例:
[get("/users/list")] task search([query(collectionformat.multi)]int[] ages); search(new [] {10, 20, 30})
输出结果:"/users/list?ages=10&ages=20&ages=30"
[get("/users/list")] task search([query(collectionformat.csv)]int[] ages); search(new [] {10, 20, 30})
输出结果:"/users/list?ages=10%2c20%2c30"
6.转义符查询字符串参数(unescape querystring parameters)
使用queryuriformat属性指定查询参数是否应转义网址,例:
[get("/query")] [queryuriformat(uriformat.unescaped)] task query(string q); query("select+id,name+from+account")
输出结果:"/query?q=select+id,name+from+account"
7.body内容
通过使用body属性,可以把自定义对象参数追加到http请求body当中。
[post("/users/new")] task createuser([body] user user)
根据参数的类型,提供body数据有四种可能性:
●如果类型为stream,则内容将通过streamcontent流形式传输。
●如果类型为string,则字符串将直接用作内容,除非[body(bodyserializationmethod.json)]设置了字符串,否则将其作为stringcontent。
●如果参数具有属性[body(bodyserializationmethod.urlencoded)],则内容将被url编码。
●对于所有其他类型,将使用refitsettings中指定的内容序列化程序将对象序列化(默认为json)。
●缓冲和content-length头
默认情况下,refit重新调整流式传输正文内容而不缓冲它。例如,这意味着您可以从磁盘流式传输文件,而不会产生将整个文件加载到内存中的开销。这样做的缺点是没有在请求上设置内容长度头(content-length)。如果您的api需要您随请求发送一个内容长度头,您可以通过将[body]属性的缓冲参数设置为true来禁用此流行为:
task createuser([body(buffered: true)] user user);
7.1.json内容
使用json.net对json请求和响应进行序列化/反序列化。默认情况下,refit将使用可以通过设置newtonsoft.json.jsonconvert.defaultsettings进行配置的序列化器设置:
jsonconvert.defaultsettings = () => new jsonserializersettings() { contractresolver = new camelcasepropertynamescontractresolver(), converters = {new stringenumconverter()} }; //serialized as: {"day":"saturday"} await postsomestuff(new { day = dayofweek.saturday });
由于默认静态配置是全局设置,它们将影响您的整个应用程序。有时候我们只想要对某些特定api进行设置,您可以选择使用refitsettings属性,以允许您指定所需的序列化程序进行设置,这使您可以为单独的api设置不同的序列化程序设置:
var githubapi = restservice.for<igithubapi>("https://api.github.com", new refitsettings { contentserializer = new jsoncontentserializer( new jsonserializersettings { contractresolver = new snakecasepropertynamescontractresolver() } )}); var otherapi = restservice.for<iotherapi>("https://api.example.com", new refitsettings { contentserializer = new jsoncontentserializer( new jsonserializersettings { contractresolver = new camelcasepropertynamescontractresolver() } )});
还可以使用json.net的jsonproperty属性来自定义属性序列化/反序列化:
public class foo { //像[aliasas(“ b”)]一样会在表单中发布 [jsonproperty(propertyname="b")] public string bar { get; set; } }
7.2xml内容
xml请求和响应使用system.xml.serialization.xmlserializer进行序列化/反序列化。默认情况下,refit只会使用json将内容序列化,若要使用xml内容,请将contentserializer配置为使用xmlcontentserializer:
var githubapi = restservice.for<ixmlapi>("https://www.w3.org/xml", new refitsettings { contentserializer = new xmlcontentserializer() });
属性序列化/反序列化可以使用system.xml.serialization命名空间中的属性进行自定义:
public class foo { [xmlelement(namespace = "https://www.w3.org/xml")] public string bar { get; set; } }
system.xml.serialization.xmlserializer提供了许多序列化选项,可以通过向xmlcontentserializer构造函数提供xmlcontentserializer设置来设置这些选项:
var githubapi = restservice.for<ixmlapi>("https://www.w3.org/xml", new refitsettings { contentserializer = new xmlcontentserializer( new xmlcontentserializersettings { xmlreaderwritersettings = new xmlreaderwritersettings() { readersettings = new xmlreadersettings { ignorewhitespace = true } } } ) });
7.3.表单发布(form posts)
对于以表单形式发布(即序列化为application/x-www-form-urlencoded)的api,请使用初始化body属性bodyserializationmethod.urlencoded属性,参数可以是idictionary字典,例:
public interface imeasurementprotocolapi { [post("/collect")] task collect([body(bodyserializationmethod.urlencoded)] dictionary<string, object> data); } var data = new dictionary<string, object> { {"v", 1}, {"tid", "ua-1234-5"}, {"cid", new guid("d1e9ea6b-2e8b-4699-93e0-0bcbd26c206c")}, {"t", "event"}, }; // serialized as: v=1&tid=ua-1234-5&cid=d1e9ea6b-2e8b-4699-93e0-0bcbd26c206c&t=event await api.collect(data);
如果我们传递对象跟请求表单中字段名称不一致时,可在对象属性名称上加入[aliasas("你定义字段名称")] 属性,那么加入属性的对象字段都将会被序列化为请求中的表单字段:
public interface imeasurementprotocolapi { [post("/collect")] task collect([body(bodyserializationmethod.urlencoded)] measurement measurement); } public class measurement { // properties can be read-only and [aliasas] isn't required public int v { get { return 1; } } [aliasas("tid")] public string webpropertyid { get; set; } [aliasas("cid")] public guid clientid { get; set; } [aliasas("t")] public string type { get; set; } public object ignoreme { private get; set; } } var measurement = new measurement { webpropertyid = "ua-1234-5", clientid = new guid("d1e9ea6b-2e8b-4699-93e0-0bcbd26c206c"), type = "event" }; // serialized as: v=1&tid=ua-1234-5&cid=d1e9ea6b-2e8b-4699-93e0-0bcbd26c206c&t=event await api.collect(measurement);
8.设置请求头
8.1静态头(static headers)
您可以为将headers属性应用于方法的请求设置一个或多个静态请求头:
[headers("user-agent: awesome octocat app")] [get("/users/{user}")] task<user> getuser(string user);
通过将headers属性应用于接口,还可以将静态头添加到api中的每个请求:
[headers("user-agent: awesome octocat app")] public interface igithubapi { [get("/users/{user}")] task<user> getuser(string user); [post("/users/new")] task createuser([body] user user); }
8.2动态头(dynamic headers)
如果需要在运行时设置头的内容,则可以通过将头属性应用于参数来向请求添加具有动态值的头:
[get("/users/{user}")] task<user> getuser(string user, [header("authorization")] string authorization); // will add the header "authorization: token oauth-token" to the request var user = await getuser("octocat", "token oauth-token");
8.3授权(动态头redux)
使用头的最常见原因是为了授权。而现在大多数api使用一些oauth风格的访问令牌,这些访问令牌会过期,刷新寿命更长的令牌。封装这些类型的令牌使用的一种方法是,可以插入自定义的httpclienthandler。这样做有两个类:一个是authenticatedhttpclienthandler,它接受一个func<task<string>>参数,在这个参数中可以生成签名,而不必知道请求。另一个是authenticatedparameteredhttpclienthandler,它接受一个func<httprequestmessage,task<string>>参数,其中签名需要有关请求的信息(参见前面关于twitter的api的注释),
例如:
class authenticatedhttpclienthandler : httpclienthandler { private readonly func<task<string>> gettoken; public authenticatedhttpclienthandler(func<task<string>> gettoken) { if (gettoken == null) throw new argumentnullexception(nameof(gettoken)); this.gettoken = gettoken; } protected override async task<httpresponsemessage> sendasync(httprequestmessage request, cancellationtoken cancellationtoken) { // see if the request has an authorize header var auth = request.headers.authorization; if (auth != null) { var token = await gettoken().configureawait(false); request.headers.authorization = new authenticationheadervalue(auth.scheme, token); } return await base.sendasync(request, cancellationtoken).configureawait(false); } }
或者:
class authenticatedparameterizedhttpclienthandler : delegatinghandler { readonly func<httprequestmessage, task<string>> gettoken; public authenticatedparameterizedhttpclienthandler(func<httprequestmessage, task<string>> gettoken, httpmessagehandler innerhandler = null) : base(innerhandler ?? new httpclienthandler()) { this.gettoken = gettoken ?? throw new argumentnullexception(nameof(gettoken)); } protected override async task<httpresponsemessage> sendasync(httprequestmessage request, cancellationtoken cancellationtoken) { // see if the request has an authorize header var auth = request.headers.authorization; if (auth != null) { var token = await gettoken(request).configureawait(false); request.headers.authorization = new authenticationheadervalue(auth.scheme, token); } return await base.sendasync(request, cancellationtoken).configureawait(false); } }
虽然httpclient包含一个几乎相同的方法签名,但使用方式不同。重新安装未调用httpclient.sendasync。必须改为修改httpclienthandler。此类的用法与此类似(示例使用adal库来管理自动令牌刷新,但主体用于xamarin.auth或任何其他库:
class loginviewmodel { authenticationcontext context = new authenticationcontext(...); private async task<string> gettoken() { // the acquiretokenasync call will prompt with a ui if necessary // or otherwise silently use a refresh token to return // a valid access token var token = await context.acquiretokenasync("http://my.service.uri/app", "clientid", new uri("callback://complete")); return token; } public async task loginandcallapi() { var api = restservice.for<imyrestservice>(new httpclient(new authenticatedhttpclienthandler(gettoken)) { baseaddress = new uri("https://the.end.point/") }); var location = await api.getlocationofrebelbase(); } } interface imyrestservice { [get("/getpublicinfo")] task<foobar> somepublicmethod(); [get("/secretstuff")] [headers("authorization: bearer")] task<location> getlocationofrebelbase(); }
在上面的示例中,每当调用需要身份验证的方法时,authenticatedhttpclienthandler将尝试获取新的访问令牌。由应用程序提供,检查现有访问令牌的过期时间,并在需要时获取新的访问令牌。
参考文献:
refit
上一篇: 新零售变革,云集品紧握时代发展趋势