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,20,30"
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 https://github.com/reactiveui/refit
来源: https://www.cnblogs.com/wzk153/p/12449585.html