针对.NET Core, Xamarin以及.NET的自动类型安全Rest库: Refit
本文大部分内容是针对Refit官网的翻译。
Refit是一个类似于Retrofit的Restful Api库,使用它,你可以将你的Restful Api定义在接口中。
例如:
public interface IGitHubApi
{
[Get("/users/{user}")]
Task<User> GetUser(string user);
}
这里RestService
类生成了一个IGitHubApi
接口的实现,它使用HttpClient
来进行api调用。
var gitHubApi = RestService.For<IGitHubApi>("https://api.github.com");
var octocat = await gitHubApi.GetUser("octocat");
Refit可以在哪些地方使用?
当前Refit支持一下平台。
- UWP
- Xamarin.Android
- Xamarin.Mac
- Xamarin.iOS
- Desktop .NET 4.6.1
- .NET Core
.NET Core的注意事项:
对于.NET Core的构建时支持(Build-Time support), 你必须使用.NET Core 2.x SDK。你可以针对所有的支持平台构建你的库,只要构建时使用2.x SDK即可。
API属性
基本用法
针对每个方法都必须提供一个HTTP属性,这个属性指定了请求的方式和相关的URL。这里有6种内置的批注:Get, Post, Put, Delete, Patch和Head。在批注中需要指定资源对应的URL。
[Get("/users/list")]
你同样可以指定URL中的查询字符串。
[Get("/users/list?sort=desc")]
动态URL
你还可以使用可替换块(replacement block)和方法参数创建动态URL。这里可替换块是一个被大括号包裹的字符串变量。
[Get("/group/{id}/users")]
Task<List<User>> GroupList([AliasAs("id")] int groupId);
URL中没有指定的参数,就会自动作为URL的查询字符串。这与Retrofit不同,在Retrofit中所有参数都必须显示指定。
[Get("/group/{id}/users")]
Task<List<User>> GroupList([AliasAs("id")] int groupId, [AliasAs("sort")] string sortOrder);
这里当调用GroupList(4, "desc");
方法时,调用API会是"/group/4/users?sort=desc"
。
回转路由语法
回转路由参数语法:使用双星号的捕获所有参数(catch-all parameter)且不会对"/"进行编码,
在生成链接的过程, 路由系统将编码双星号捕获的全部参数(catch-all parameter),而不会编码"/"。
[Get("/search/{**page}")]
Task<List<Page>> Search(string page);
回转路由参数必须是字符串
这里当调用Search("admin/products");
时,生成的连接是"/search/admin/products"
动态查询字符串参数
当你指定一个对象作为查询参数的时候,所有非空的public属性将被用作查询参数。使用Query
特性将改变默认的行为,它会扁平化你的查询字符串对象。如果使用Query
特性,你还可以针对扁平化查询字符串对象添加指定的分隔符和前缀。
例:
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
特性同样可以指定查询字符串中应该如何格式化集合对象。
例:
[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"
正文内容
在你的方法签名中,你还可以将使用Body
特性将参数中的一个标记为正文内容。
[Post("/users/new")]
Task CreateUser([Body] User user);
这里Refit支持4种请求体数据
- 如果正文内容类型是
Stream
, 其内容会包裹在一个StreamContent
对象中。 - 如果正文内容类型是
string
, 其内容会直接用作正文内容。当指定当前参数拥有特性[Body(BodySerializationMethod.Json)]
时,它会被包裹在一个StringContent
对象中。 - 如果当前参数拥有特性
[Body(BodySerializationMethod.UrlEncoded)]
, 其内容会被URL编码。 - 针对其他类型,当前指定的参数会被默认序列化成JSON。
缓冲及Content-Header头部设置
默认情况下,Refit会流式传输正文内容,而不会缓冲它。这意味着,你可以从磁盘流式传输文件,而不产生将整个文件加载到内存中的开销。这样做的缺点是,请求头部没有设置Content-Length
。如果你的API需要发送一个请求并指定Content-Length
请求头,则需要将Body
特性的buffered
参数设置为true。
Task CreateUser([Body(buffered: true)] User user);
Json内容
JSON请求和响应可以使用Json.NET来序列化和反序列化,默认情况下,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使用独立的配置。当使用Refit生成一个接口对象的时候,你可以传入一个RefitSettings
参数,这个参数可以指定你使用的JSON序列化配置。
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
{
// Works like [AliasAs("b")] would in form posts (see below)
[JsonProperty(PropertyName="b")]
public string Bar { get; set; }
}
Xml内容
针对XML请求和响应的序列化和反序列化,Refit使用了System.Xml.Serialization.XmlSerializer
。默认情况下, Refit会使用JSON内容序列化器,如果想要使用XML内容序列化器,你需要将RefitSetting
的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
提供了多种序列化方式,你可以通过在XmlContentSerialier
对象的构造函数中指定一个XmlContentSerializerSettings
对象类进行配置。
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
}
}
}
)
});
表单Post
针对采用表单Post的API( 正文会被序列化成application/x-www-form-urlencoded ), 我们可以将指定参数的正文特性指定为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"},
};
// 序列化为: v=1&tid=UA-1234-5&cid=d1e9ea6b-2e8b-4699-93e0-0bcbd26c206c&t=event
await api.Collect(data);
当然参数也可以是一个普通对象,Refit会将对象中所有public, 可读取的属性序列化成表单字段。当然这里你可以使用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"
};
// 序列化为: v=1&tid=UA-1234-5&cid=d1e9ea6b-2e8b-4699-93e0-0bcbd26c206c&t=event
await api.Collect(measurement);
如果当前属性同时指定了[JsonProperty(PropertyName)]
和AliasAs()
, Refit会优先使用AliasAs()
中指定的名称。这意味着,以下类型会被序列化成one=value1&two=value2
public class SomeObject
{
[JsonProperty(PropertyName = "one")]
public string FirstProperty { get; set; }
[JsonProperty(PropertyName = "notTwo")]
[AliasAs("two")]
public string SecondProperty { get; set; }
}
注意:
AliasAs
只能应用在请求参数和Form正文Post中,不能应用于响应对象。如果要为响应对象属性起别名,你依然需要使用[JsonProperty("full-property-name")]
设置请求Header
静态头
你可以使用Headers
特性指定一个或多个静态的请求头。
[Headers("User-Agent: Awesome Octocat App")]
[Get("/users/{user}")]
Task<User> GetUser(string user);
为了简便使用,你也可以将Headers
特性放在接口定义上,从而使当前接口中定义的所有Rest请求都添加相同的静态头。
[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);
}
动态头
如果头部内容需要在运行时动态设置,你可以在方法签名处,使用Header
特性指定一个动态头部参数,你可以在调用Api时,为这个参数指定一个dynamic
类型的值,从而实现动态头。
[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");
授权(动态头的升级版)
使用请求头的最常见场景就是授权。当今绝大多数的API都是使用OAuth, 它会提供一个带过期时间的access token和一个负责刷新access token的refresh token。
为了封装这些授权令牌的使用,我们可以自定义一个HttpClientHandler
。
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);
}
}
虽然HttpClient
包含了几乎相同的方法签名,但是它的使用方式不同。Refit不会调用HttpClient.SendAsync
方法,这里必须使用自定义的HttpClientHandler
替换它。
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
会尝试获取一个新的access token。 这里程序会检查access token是否到期,并在需要时获取新的令牌。
分段上传
当一个接口方法被指定为[Multipart]
, 这意味着当前Api提交的内容中包含分段内容类型。针对分段方法,Refit当前支持一下几种参数类型
- 字符串
- 二进制数组
- Stream流
- FileInfo
这里参数名会作为分段数据的字段名。当然你可以用AliasAs
特性复写它。
为了给二进制数组,Stream流以及FileInfo参数的内容指定文件名和内容类型,我们必须要使用封装类。Refit中默认的封装类有3种,ByteArrarPart
, StreamPart
, FileInfoPart
。
public interface ISomeApi
{
[Multipart]
[Post("/users/{id}/photo")]
Task UploadPhoto(int id, [AliasAs("myPhoto")] StreamPart stream);
}
为了将一个Stream流对象传递给以上定义的方法,我们需要构建一个StreamObject
对象:
someApiInstance.UploadPhoto(id, new StreamPart(myPhotoStream, "photo.jpg", "image/jpeg"));
异常处理
为了封装可能来自服务的任何异常,你可以捕获包含请求和响应信息的ApiException
。 Refit还支持捕获由于不良请求而引发的验证异常,以解决问题详细信息。 有关验证异常的问题详细信息的特定信息,只需捕获ValidationApiException
:
// ...
try
{
var result = await awesomeApi.GetFooAsync("bar");
}
catch (ValidationApiException validationException)
{
// handle validation here by using validationException.Content,
// which is type of ProblemDetails according to RFC 7807
}
catch (ApiException exception)
{
// other exception handling
}
// ...
针对.NET Core, Xamarin以及.NET的自动类型安全Rest库: Refit的更多相关文章
- (27)ASP.NET Core .NET标准REST库Refit
1.简介 Refit是一个受到Square的Retrofit库(Java)启发的自动类型安全REST库.通过HttpClient网络请求(POST,GET,PUT,DELETE等封装)把REST AP ...
- 微软:正式发布针对 .NET Core的 Winform 设计器
转载请注明出处:葡萄城官网,葡萄城为开发者提供专业的开发工具.解决方案和服务,赋能开发者. 原文出处:https://devblogs.microsoft.com/dotnet/windows-for ...
- iOS8 Core Image In Swift:自动改善图像以及内置滤镜的使用
iOS8 Core Image In Swift:自动改善图像以及内置滤镜的使用 iOS8 Core Image In Swift:更复杂的滤镜 iOS8 Core Image In Swift:人脸 ...
- 基于TeamCity的asp.net mvc/core,Vue 持续集成与自动部署
一 Web Server(Windows)端的配置 1.配置IIS,重要的是管理服务 1.1 配置FTP(前端NPM项目需要) 该步骤略,如果是在阿里云ESC上,需要开启端口21(用来FTP认证握手) ...
- # .NET Core下操作Git,自动提交代码到
.NET Core下操作Git,自动提交代码到 转自博客园(阿星Plus) .NET Core 3.0 预览版发布已经好些时日了,博客园也已将其用于生产环境中,可见 .NET Core 日趋成熟 回归 ...
- 【.Net Core】编译时禁止自动生成netcoreapp文件夹
原文:[.Net Core]编译时禁止自动生成netcoreapp文件夹 每次在编译生成文件时,VS都会自动在<OutputPath>属性指定的路劲后再追加一个用NetCore命名的文件夹 ...
- 自动类型安全的.NET标准REST库refit
在SCOTT HANSELMAN 博客上看到一个好东西<Exploring refit, an automatic type-safe REST library for .NET Standar ...
- 自动调参库hyperopt+lightgbm 调参demo
在此之前,调参要么网格调参,要么随机调参,要么肉眼调参.虽然调参到一定程度,进步有限,但仍然很耗精力. 自动调参库hyperopt可用tpe算法自动调参,实测强于随机调参. hyperopt 需要自己 ...
- 自动检测GD库支持的图像类型
以下代码通过自动检测GD库支持的图像类型 来写出移直性更好的PHP代码 <?php if(function_exists("imagegif")){ header(" ...
随机推荐
- 防止ssh暴力破解的小工具denyhosts
DenyHosts 简介 DenyHosts 是 Python 语言写的一个程序软件,运行于 Linux 上预防 SSH 暴力破解的,它会分析 sshd 的日志文件(/var/log/secure), ...
- Zookeeper集群的"脑裂"问题处理 - 运维总结
关于集群中的"脑裂"问题,之前已经在这里详细介绍过,下面重点说下Zookeeper脑裂问题的处理办法.ooKeeper是用来协调(同步)分布式进程的服务,提供了一个简单高性能的协调 ...
- tomcat运行一段时间后报错"Too many open files"
tomcat运行一段时间后报打开太多文件错误:Too many open files 查看当前进程的文件打开数: lsof -n |awk '{print $2}'|sort|uniq -c |so ...
- 【Linux】文本处理工具介绍
文本处理工具介绍 grep.sed和awk都是文本处理工具,各自都有各自的优缺点,一种文本处理命令是不能被另一个完全替换的.相比较而言,sed和awk功能更强大,且已独立成一种语言来介绍. grep: ...
- 【shell脚本】优化内核参数===
一.Linux内核参数优化 Sysctl命令用来配置与显示在/proc/sys目录中的内核参数.如果想使参数长期保存,可以通过编辑/etc/sysctl.conf文件来实现. 命令格式: sysct ...
- allure定制化输出测试报告,让报告锦上添花!
一.定制化后的效果展示 用两张图展示效果: 二.注意别踩坑 allure定制化想必大部分情况都会去选择pip install pytest-allure-adaptor这个插件,安装完成后,运行定制化 ...
- copy-and-swap idiom
This answer is from https://stackoverflow.com/a/3279550/10133369 Overview Why do we need the copy-an ...
- 面试官:你看过Redis数据结构底层实现吗?
面试中,redis也是很受面试官亲睐的一部分.我向在这里讲的是redis的底层数据结构,而不是你理解的五大数据结构.你有没有想过redis底层是怎样的数据结构呢,他们和我们java中的HashMap. ...
- ABP开发框架前后端开发系列---(6)ABP基础接口处理和省份城市行政区管理模块的开发
最近没有更新ABP框架的相关文章,一直在研究和封装相关的接口,总算告一段落,开始继续整理下开发心得.上次我在随笔<ABP开发框架前后端开发系列---(5)Web API调用类在Winform项目 ...
- OpenGL入门1.7:摄像机
每一个小步骤的源码都放在了Github 的内容为插入注释,可以先跳过 前言 我们已经知道了何为观察矩阵以及如何使用观察矩阵移动场景(我们向后移动了一点) OpenGL本身没有摄像机(Camera)的概 ...