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); } }