• 针对.NET Core, Xamarin以及.NET的自动类型安全Rest库: Refit


    本文大部分内容是针对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
    }
    // ...
    
  • 相关阅读:
    EffectiveJava(18)接口优先于抽象类
    EffectiveJava(17)要么为继承而设计,要么禁止继承
    EffectiveJava(16)复合优先于继承
    EffectiveJava(15)强化对象和域的不可变性
    EffectiveJava(14)在公有类中使用访问方法而非公有域
    EffectiveJava(13)使类和成员的可访问性最小化
    CentOS 6 安装 MySQL 8.0.+
    Gradle sync failed: Cannot set the value of read-only property 'outputFile'
    Error:Execution failed for task ':xutils:mergeDebugAndroidTestResources'. > No slave process to proc
    Error:All flavors must now belong to a named flavor dimension.
  • 原文地址:https://www.cnblogs.com/lwqlun/p/10817537.html
Copyright © 2020-2023  润新知