在ASP.NET Core 中实现RESTFull库的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,后者根据用户的需求对结果进行解析的过程。安装组件命令行:

<code>Install-Package refit/<code>

代码例子:

<code>[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>>> Get()

{
var result = await new GitHubApi().GetUser();
return new string[] { result.id.Value.ToString(), result.login };
}/<actionresult>/<igithubapi>/<user>/<user>/<code>

注:接口中Headers、Get这些属性叫做Refit的特性。定义上面的一个IGitHubApi的REST API接口,该接口定义了一个函数GetUser,该函数会通过HTTP GET请求去访问服务器的/users/{user}路径把返回的结果封装为User POCO对象并返回。其中URL路径中的{user}的值为GetUser函数中的参数user的取值,这里赋值为octocat。然后通过RestService类来生成一个IGitHubApi接口的实现并供HttpClient调用。

在ASP.NET Core 中实现RESTFull库的Refit


2.API属性

每个方法必须具有提供请求URL和HTTP属性。HTTP属性有六个内置注释:Get, Post, Put, Delete, Patch and Head,例:

<code>[Get("/users/list")]/<code>

您还可以在请求URL中指定查询参数:

<code>[Get("/users/list?sort=desc")]/<code>

还可以使用相对URL上的替换块和参数来动态请求资源。替换块是由{and,即&}包围的字母数字字符串。如果参数名称与URL路径中的名称不匹配,请使用AliasAs属性,例:

<code>[Get("/group/{id}/users")]
Task<list>> GroupList([AliasAs("id")] int groupId);/<list>/<code>

请求URL还可以将替换块绑定到自定义对象,例:

<code>[Get("/group/{request.groupId}/users/{request.userId}")]
Task<list>> GroupList(UserGroupRequest request);
class UserGroupRequest{
int groupId { get;set; }
int userId { get;set; }
}/<list>/<code>

未指定为URL替换的参数将自动用作查询参数。这与Retrofit不同,在Retrofit中,必须明确指定所有参数,例:

<code>[Get("/group/{id}/users")]
Task<list>> GroupList([AliasAs("id")] int groupId, [AliasAs("sort")] string sortOrder);

GroupList(4, "desc");/<list>/<code>

输出结果:"/group/4/users?sort=desc"

3.动态查询字符串参数(Dynamic Querystring Parameters)

方法还可以传递自定义对象,把对象属性追加到查询字符串参数当中,例如:

<code>public class MyQueryParams
{
[AliasAs("order")]
public string SortOrder { get; set; }
public int Limit { get; set; }
}
[Get("/group/{id}/users")]
Task<list>> GroupList([AliasAs("id")] int groupId, MyQueryParams params);
[Get("/group/{id}/users")]
Task<list>> GroupListWithAttribute([AliasAs("id")] int groupId, [Query(".","search")]MyQueryParams params);
params.SortOrder = "desc";
params.Limit = 10;
GroupList(4, params)/<list>/<list>/<code>

输出结果:"/group/4/users?order=desc&Limit=10"

<code>GroupListWithAttribute(4, params)/<code>

输出结果:"/group/4/users?search.order=desc&search.Limit=10"您还可以使用[Query]指定querystring参数,并将其在非GET请求中扁平化,类似于:

<code>[Post("/statuses/update.json")]
Task<tweet> PostTweet([Query]TweetParams params);/<tweet>/<code>

5.集合作为查询字符串参数(Collections as Querystring Parameters)

方法除了支持传递自定义对象查询,还支持集合查询的,例:

<code>[Get("/users/list")]
Task Search([Query(CollectionFormat.Multi)]int[] ages);
Search(new [] {10, 20, 30})/<code>

输出结果:"/users/list?ages=10&ages=20&ages=30"

<code>[Get("/users/list")]
Task Search([Query(CollectionFormat.Csv)]int[] ages);
Search(new [] {10, 20, 30})/<code>

输出结果:"/users/list?ages=10%2C20%2C30"

6.转义符查询字符串参数(Unescape Querystring Parameters)

使用QueryUriFormat属性指定查询参数是否应转义网址,例:

<code>[Get("/query")]
[QueryUriFormat(UriFormat.Unescaped)]
Task Query(string q);
Query("Select+Id,Name+From+Account")/<code>

输出结果:"/query?q=Select+Id,Name+From+Account"

7.Body内容

通过使用Body属性,可以把自定义对象参数追加到HTTP请求Body当中。

<code>[Post("/users/new")]
Task CreateUser([Body] User user)/<code>

根据参数的类型,提供Body数据有四种可能性:●如果类型为Stream,则内容将通过StreamContent流形式传输。●如果类型为string,则字符串将直接用作内容,除非[Body(BodySerializationMethod.Json)]设置了字符串,否则将其作为StringContent。●如果参数具有属性[Body(BodySerializationMethod.UrlEncoded)],则内容将被URL编码。●对于所有其他类型,将使用RefitSettings中指定的内容序列化程序将对象序列化(默认为JSON)。●缓冲和Content-Length头默认情况下,Refit重新调整流式传输正文内容而不缓冲它。例如,这意味着您可以从磁盘流式传输文件,而不会产生将整个文件加载到内存中的开销。这样做的缺点是没有在请求上设置内容长度头(Content-Length)。如果您的API需要您随请求发送一个内容长度头,您可以通过将[Body]属性的缓冲参数设置为true来禁用此流行为:

<code>Task CreateUser([Body(buffered: true)] User user);/<code>

7.1.JSON内容

使用Json.NET对JSON请求和响应进行序列化/反序列化。默认情况下,Refit将使用可以通过设置Newtonsoft.Json.JsonConvert.DefaultSettings进行配置的序列化器设置:

<code>JsonConvert.DefaultSettings =
() => new JsonSerializerSettings() {
ContractResolver = new CamelCasePropertyNamesContractResolver(),
Converters = {new StringEnumConverter()}
};
//Serialized as: {"day":"Saturday"}
await PostSomeStuff(new { Day = DayOfWeek.Saturday });/<code>

由于默认静态配置是全局设置,它们将影响您的整个应用程序。有时候我们只想要对某些特定API进行设置,您可以选择使用RefitSettings属性,以允许您指定所需的序列化程序进行设置,这使您可以为单独的API设置不同的序列化程序设置:

<code>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()
}
)});/<iotherapi>/<igithubapi>/<code>

还可以使用Json.NET的JsonProperty属性来自定义属性序列化/反序列化:

<code>public class Foo
{
//像[AliasAs(“ b”)]一样会在表单中发布
[JsonProperty(PropertyName="b")]
public string Bar { get; set; }
} /<code>

7.2XML内容

XML请求和响应使用System.XML.Serialization.XmlSerializer进行序列化/反序列化。默认情况下,Refit只会使用JSON将内容序列化,若要使用XML内容,请将ContentSerializer配置为使用XmlContentSerializer:

<code>var gitHubApi = RestService.For<ixmlapi>("https://www.w3.org/XML",
new RefitSettings {
ContentSerializer = new XmlContentSerializer()
});/<ixmlapi>/<code>

属性序列化/反序列化可以使用System.Xml.serialization命名空间中的属性进行自定义:

<code>public class Foo
{
[XmlElement(Namespace = "https://www.w3.org/XML")]
public string Bar { get; set; }
}/<code>

System.Xml.Serialization.XmlSerializer提供了许多序列化选项,可以通过向XmlContentSerializer构造函数提供XmlContentSerializer设置来设置这些选项:

<code>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
}
}

}
)
});/<ixmlapi>/<code>

7.3.表单发布(Form posts)

对于以表单形式发布(即序列化为application/x-www-form-urlencoded)的API,请使用初始化Body属性BodySerializationMethod.UrlEncoded属性,参数可以是IDictionary字典,例:

<code>public interface IMeasurementProtocolApi
{
[Post("/collect")]
Task Collect([Body(BodySerializationMethod.UrlEncoded)] Dictionary<string> data);
}
var data = new Dictionary<string> {
{"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);/<string>/<string>/<code>

如果我们传递对象跟请求表单中字段名称不一致时,可在对象属性名称上加入[AliasAs("你定义字段名称")] 属性,那么加入属性的对象字段都将会被序列化为请求中的表单字段:

<code>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);/<code>

8.设置请求头

8.1静态头(Static headers)

您可以为将headers属性应用于方法的请求设置一个或多个静态请求头:

<code>[Headers("User-Agent: Awesome Octocat App")]
[Get("/users/{user}")]
Task<user> GetUser(string user);/<user>/<code>

通过将headers属性应用于接口,还可以将静态头添加到API中的每个请求:

<code>[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);
}/<user>/<code>

8.2动态头(Dynamic headers)

如果需要在运行时设置头的内容,则可以通过将头属性应用于参数来向请求添加具有动态值的头:

<code>[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"); /<user>/<code>

使用头的最常见原因是为了授权。而现在大多数API使用一些oAuth风格的访问令牌,这些访问令牌会过期,刷新寿命更长的令牌。封装这些类型的令牌使用的一种方法是,可以插入自定义的HttpClientHandler。这样做有两个类:一个是AuthenticatedHttpClientHandler,它接受一个Func<task>>参数,在这个参数中可以生成签名,而不必知道请求。另一个是authenticatedparameteredhttpclienthandler,它接受一个Func<httprequestmessage>>参数,其中签名需要有关请求的信息(参见前面关于Twitter的API的注释),例如:/<httprequestmessage>/<task>

<code>class AuthenticatedHttpClientHandler : HttpClientHandler
{
private readonly Func<task>> getToken;
public AuthenticatedHttpClientHandler(Func<task>> 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);
}
}/<httpresponsemessage>/<task>/<task>/<code>

或者:

<code>class AuthenticatedParameterizedHttpClientHandler : DelegatingHandler
{
readonly Func<httprequestmessage>> getToken;
public AuthenticatedParameterizedHttpClientHandler(Func<httprequestmessage>> 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);
}
}/<httpresponsemessage>/<httprequestmessage>/<httprequestmessage>/<code>

虽然HttpClient包含一个几乎相同的方法签名,但使用方式不同。重新安装未调用HttpClient.SendAsync。必须改为修改HttpClientHandler。此类的用法与此类似(示例使用ADAL库来管理自动令牌刷新,但主体用于Xamarin.Auth或任何其他库:

<code>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();
}/<location>/<foobar>/<imyrestservice>/<string>/<code>

在上面的示例中,每当调用需要身份验证的方法时,AuthenticatedHttpClientHandler将尝试获取新的访问令牌。由应用程序提供,检查现有访问令牌的过期时间,并在需要时获取新的访问令牌。


分享到:


相關文章: