1. 程式人生 > >針對.NET Core, Xamarin以及.NET的自動類型安全Rest庫: Refit

針對.NET Core, Xamarin以及.NET的自動類型安全Rest庫: Refit

secret eap 指定 pla 路由 read json 註意事項 parameter

本文大部分內容是針對Refit官網的翻譯。

官網地址: https://github.com/reactiveui/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內容序列化器,你需要將RefitSettingContentSerializer屬性指定為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