Refit 是一個類型安全的 REST 開源庫,是一套基於 RESTful 架構的 .NET 客戶端實現,內部使用 HttpClient 類封裝,可通過 Refit 更加簡單安全地訪問 Web API 接口,要使用 Refit 框架,只需要在項目中通過 NuGet 包安裝器安裝即可。
Install-Package refit
使用方法很簡單:
public interface IGitHubApi
{
[Get("/users/{userid}")]
Task<User> GetUser(string userid);
}
以上方法定義一個 REST API 接口,該接口定義了 GetUser 函數,該函數通過 HTTP GET 請求去訪問服務器的 /users/{userid} 路徑並把返回的結果封裝為 User 對象返回,其中 URL 路徑中 {userid} 的值為 GetUser 函數的 userid 參數取值,然后,通過 RestService 類生成 IGitHubApi 的代理實現,通過代理直接調用 Web API 接口。
var gitHubApi = RestService.For<IGitHubApi>("https://api.xcode.me");
var octocat = await gitHubApi.GetUser("xcode");
API Attributes特性
通過 Attribute 特性標記,指定請求方法和相對 URL 地址,內置支持 Get、Post、Put、Delete 和 Head 方法。
[Get("/users/list")]
也可以在 URL 中指定查詢參數:
[Get("/users/list?sort=desc")]
方法中的 URL 地址可以使用占位符,占位符是由 {} 包圍的字符串,如果函數參數與 URL 路徑中的占位符名稱不同,可使用 AliasAs 指定別名。
[Get("/group/{id}/users")]
Task<List<User>> GroupList([AliasAs("id")] int groupId)
值得注意的是,參數名稱和 URL 參數占位符不區分大小寫,如果一個函數參數未被 URL 占位符所使用,那么它將自動被當作 QueryString 查詢字符串來使用。
[Get("/group/{id}/users")]
Task<List<User>> GroupList([AliasAs("id")] int groupId, [AliasAs("sort")] string sortOrder);
當我們調用 GroupList 方法時,相當於請求 "/group/4/users?sort=desc"這個地址,其中 sort 參數被當作 GET 參數自動使用。
Dynamic Querystring Parameters
函數參數可傳遞對象,對象的字段屬性將被自動追加到 Querystring 查詢字符串。
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 param);
[Get("/group/{id}/users")]
Task<List<User>> GroupListWithAttribute([AliasAs("id")] int groupId, [Query(".","search")] MyQueryParams param);
param.SortOrder = "desc";
param.Limit = 10;
GroupList(4, param)
>>> "/group/4/users?order=desc&Limit=10"
GroupListWithAttribute(4, param)
>>> "/group/4/users?search.order=desc&search.Limit=10"
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"
Body內容
通過使用 BodyAttribute 特性,將函數參數追加到 HTTP 請求的 Body 部分。
[Post("/users/new")]
Task CreateUser([Body] User user);
根據參數的類型,提供 Body 數據有四種可能:如果類型為 Stream 流類型,則內容將通過 StreamContent 流式傳輸。如果類型是 String 字符串類型,則該字符串將直接用作內容。如果參數具有 [Body(BodySerializationMethod.UrlEncoded)] 屬性,內容將被 URL 編碼后使用。對於以上除外的其它類型,對象將被序列化為 JSON 傳輸。
JSON內容
基於 JSON 的請求和響應,內部使用 JSON.NET 框架進行序列化和反序列化,默認情況下,Refit 框架將使用 JsonConvert.DefaultSettings 來配置序列化器的行為:
JsonConvert.DefaultSettings =
() => new JsonSerializerSettings() {
ContractResolver = new CamelCasePropertyNamesContractResolver(),
Converters = {new StringEnumConverter()}
};
// Serialized as: {"day":"Saturday"}
await PostSomeStuff(new { Day = DayOfWeek.Saturday });
因為靜態屬性 DefaultSettings 是全局設置,它會影響整個應用程序,有些時候,我們希望對某些 API 請求使用特定序列化設置,可以使用 RefitSettings 來指定。
var gitHubApi = RestService.For<IGitHubApi>("https://api.xcode.me",
new RefitSettings {
JsonSerializerSettings = new JsonSerializerSettings {
ContractResolver = new SnakeCasePropertyNamesContractResolver()
}
});
var otherApi = RestService.For<IOtherApi>("https://api.xcode.me",
new RefitSettings {
JsonSerializerSettings = new JsonSerializerSettings {
ContractResolver = new CamelCasePropertyNamesContractResolver()
}
});
對象屬性的序列化行為可以通過 JSON.NET 框架本身的 JsonPropertyAttribute 特性定制:
public class Foo
{
// Works like [AliasAs("b")] would in form posts (see below)
[JsonProperty(PropertyName="b")]
public string Bar { get; set; }
}
Form posts
對於采用表單提交數據(application/x-www-form-urlencoded)的 API 接口,使用 BodySerializationMethod.UrlEncoded 初始化 BodyAttribute 特性,參數可以是一個 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);
設置靜態請求頭
您可以使用 HeadersAttribute 特性設置一個或多個 HTTP 靜態請求標頭:
[Headers("User-Agent: Awesome Octocat App")]
[Get("/users/{user}")]
Task<User> GetUser(string user);
也可以通過將 HeadersAttribute 特性應用於接口,這將影響該接口中的所有請求方法:
[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);
}
設置動態請求頭
如果請求頭需要在運行時設置,則可以通過將 HeaderAttribute 特性應用到函數參數,從而為請求頭添加動態值。
[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 協議通過訪問令牌授權,申請訪問令牌,即可訪問 API 接口,訪問令牌到期后需要刷新令牌,取得更長壽命的令牌,封裝這些令牌的操作,可通過自定義 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 包含幾乎相同的方法簽名, 但使用方式不同,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 void 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 將嘗試獲取一個新的訪問令牌。 由應用程序提供一個,檢查現有訪問令牌的到期時間,並在需要時獲取新的訪問令牌。
重新定義標頭
當定義 HTTP 標頭時,對於多次設置同名的標頭,這些重名的標頭不會相互覆蓋,都將被添加到請求頭中,值得注意的是,標頭設置的優先級不同時,重新定義標頭將被替換,它們的優先級是:
1、接口上的 Headers 特性(最低優先級),2、方法上的 Headers 特性,3、方法參數上的 Header 特性(最高優先級)
[Headers("X-Emoji: :rocket:")]
public interface IGitHubApi
{
[Get("/users/list")]
Task<List> GetUsers();
[Get("/users/{user}")]
[Headers("X-Emoji: :smile_cat:")]
Task<User> GetUser(string user);
[Post("/users/new")]
[Headers("X-Emoji: :metal:")]
Task CreateUser([Body] User user, [Header("X-Emoji")] string emoji);
}
// X-Emoji: :rocket:
var users = await GetUsers();
// X-Emoji: :smile_cat:
var user = await GetUser("octocat");
// X-Emoji: :trollface:
await CreateUser(user, ":trollface:");
刪除標頭
當使用 HeadersAttribute 不提供值或者提供的值為 null 時, 請求表頭將被自動移除。
[Headers("X-Emoji: :rocket:")]
public interface IGitHubApi
{
[Get("/users/list")]
[Headers("X-Emoji")] // Remove the X-Emoji header
Task<List> GetUsers();
[Get("/users/{user}")]
[Headers("X-Emoji:")] // Redefine the X-Emoji header as empty
Task<User> GetUser(string user);
[Post("/users/new")]
Task CreateUser([Body] User user, [Header("X-Emoji")] string emoji);
}
// No X-Emoji header
var users = await GetUsers();
// X-Emoji:
var user = await GetUser("octocat");
// No X-Emoji header
await CreateUser(user, null);
// X-Emoji:
await CreateUser(user, "");
Multipart uploads
Refit 框架也支持字節流和文件流的上傳:
public interface ISomeApi
{
[Multipart]
[Post("/users/{id}/photo")]
Task UploadPhoto(int id, [AliasAs("myPhoto")] StreamPart stream);
someApiInstance.UploadPhoto(id, new StreamPart(myPhotoStream, "photo.jpg", "image/jpeg"));
}
響應處理
為了提高性能 Refit 只支持方法返回 Task 和 IObservable 類型,如需同步請求,可使用 async 和 await 異步技術。
[Post("/users/new")]
Task CreateUser([Body] User user);
// This will throw if the network call fails
await CreateUser(someUser);
如果類型參數為 HttpResponseMessage 或 string 類型,可以通過重載函數來分別返回。
// Returns the content as a string (i.e. the JSON data)
[Get("/users/{user}")]
Task<string> GetUser(string user);
// Returns the raw response, as an IObservable that can be used with the
// Reactive Extensions
[Get("/users/{user}")]
IObservable<HttpResponseMessage> GetUser(string user);
使用通用接口
有的 Web API 擁有一整套基於 CRUD 操作的 REST 服務,Refit 允許您使用通用泛型定義接口:
public interface IReallyExcitingCrudApi<T, in TKey> where T : class
{
[Post("")]
Task<T> Create([Body] T payload);
[Get("")]
Task<List<T>> ReadAll();
[Get("/{key}")]
Task<T> ReadOne(TKey key);
[Put("/{key}")]
Task Update(TKey key, [Body]T payload);
[Delete("/{key}")]
Task Delete(TKey key);
}
可以這樣來調用以上接口封裝:
// The "/users" part here is kind of important if you want it to work for more
// than one type (unless you have a different domain for each type)
var api = RestService.For<IReallyExcitingCrudApi<User, string>>("http://api.xcode.me/users");
Using HttpClientFactory
在 ASP.Net Core 2.1 中,可通過 Refix 框架提供的擴展方法注入類型客戶端:
public void ConfigureServices(IServiceCollection services)
{
services.AddHttpClient("hello", c =>
{
c.BaseAddress = new Uri("http://api.xcode.me");
})
.AddTypedClient(c => Refit.RestService.For<IHelloClient>(c));
services.AddMvc();
}
除此之外,還支持通過 HttpClientFactory 創建請求代理,Refit 為此提供擴展,用此擴展前需要通過 NuGet 引用以下包,
Install-Package Refit.HttpClientFactory
引用程序包后,可以這樣來配置:
services.AddRefitClient<IWebApi>()
.ConfigureHttpClient(c => c.BaseAddress = new Uri("https://api.xcode.me"));
// Add additional IHttpClientBuilder chained methods as required here:
// .AddHttpMessageHandler<MyHandler>()
// .SetHandlerLifetime(TimeSpan.FromMinutes(2));
也可以通過 RefitSettings 設置行為:
var settings = new RefitSettings();
// Configure refit settings here
services.AddRefitClient<IWebApi>(settings)
.ConfigureHttpClient(c => c.BaseAddress = new Uri("https://api.xcode.me"));
// Add additional IHttpClientBuilder chained methods as required here:
// .AddHttpMessageHandler<MyHandler>()
// .SetHandlerLifetime(TimeSpan.FromMinutes(2));
然后, 您可以使用構造函數將請求接口注入到控制器之中:
public class HomeController : Controller
{
public HomeController(IWebApi webApi)
{
_webApi = webApi;
}
private readonly IWebApi _webApi;
public async Task<IActionResult> Index(CancellationToken cancellationToken)
{
var thing = await _webApi.GetSomethingWeNeed(cancellationToken);
return View(thing);
}
}
