Refit 使用详解

Git官网:https://github.com/reactiveui/refit

Refit 是一个针对 .NET 应用程序的 REST API 客户端库,它通过接口定义 API 调用,从而简化与 RESTful 服务的交互。其核心理念是利用声明性编程的方式来创建 HttpClient 客户端,使得 API 调用更加简洁和易于维护。

目前Refit所支持的平台如下:

  • .NET 6 / 8
  • Blazor
  • Desktop .NET 4.6.1
  • UWP
  • Xamarin.Android
  • Xamarin.Mac
  • Xamarin.iOS
  • Uno Platform

简单使用

依赖库的安装

可以从Nuget中直接下载依赖库,在Nuget中搜索Refit可以看到有两个相关的依赖库可以下载,分别是RefitRefit.HttpClientFactory
在这里插入图片描述
Refit 是一个用于 .NET 的库,它通过接口定义 REST API 调用,允许开发者以声明性方式创建 HTTP 客户端。Refit 负责自动序列化和反序列化请求和响应,并简化与 RESTful 服务的交互。其核心功能如下:

  • 提供强类型的 API 调用
  • 自动处理 JSON 和其他格式的序列化
  • 支持异步调用和中间件

Refit.HttpClientFactory 是一个扩展库,它与 ASP.NET Core 的 HttpClientFactory 结合使用,提供了使用 Refit 创建 API 客户端的便利方式。其核心功能如下:

  • 使用 ASP.NET Core 的 HttpClientFactory 管管理 HttpClient 的生命周期,避免了潜在的性能问题(如 DNS 缓存问题)和过度创建 HttpClient 实例
  • 主要用于 ASP.NET Core 应用,依赖于 ASP.NET Core 的 HttpClientFactory,可以更好地与 ASP.NET Core 的依赖注入框架集成

可以根据自己的项目选择使用,如果是ASP.NET Core 应用中使用 Refit,那么直接安装Refit.HttpClientFactory就可以了,如果是一些其他项目,例如WPF啥的,那么安装Refit 然后自己管理HttpClient实例就可以了。

定义接口

要使用Refit,第一步先要根据要请求的RestAPI进行接口的定义。

  • 示例

    public interface IGitHubApi
    {
        [Get("/users/{user}")]
        Task<User> GetUser(string user);
    }
    
    public class User
    {
        public string Id { get; set; }
        public string Name { get; set; }
    }
    

进行Http请求

Refit提供了RestService服务,可以帮助我们生成指定接口的实现,其内部调用了HttpClient进行Http请求

  • 示例

    var gitHubApi = RestService.For<IGitHubApi>("https://api.github.com");
    var octocat = await gitHubApi.GetUser("octocat");
    

Asp.Net Core中的注册

如果是Asp.Net Core项目,Refit支持通过HttpClientFactory进行注册(安装Refit.HttpClientFactory),后面直接使用我们定义的接口进行依赖注入直接使用就可以了

  • 示例-Program.cs

    ......
    services
        .AddRefitClient<IGitHubApi>()
        .ConfigureHttpClient(c => c.BaseAddress = new Uri("https://api.github.com"));
    ......
    

使用详解

一、路由特性

在进行接口定义时,每个方法都必须有一个HTTP特性,该特性提供请求方式和相对URL。

Refit提供了六个内置特性:GetPostPutDeletePatchHead,资源的相对URL在特性中指定。

  • 示例

    public interface IGitHubApi
    {
        [Get("/users/list")]
        Task<List<User>> GetUserList();
        
        //Get请求可以直接在请求地址中携带一些参数
        [Get("/users/list?sort=desc")]
        Task<List<User>> GetUserListWithSort();
    }
    

参数占位符

Refit的方法特性支持使用参数占位符,通过{}占位符可以将URL模板中的指定参数与参数列表中对应名称的参数进行关联

  • 示例

    public interface IGitHubApi
    {
        [Get("/users/{user}")]
        Task<User> GetUser(string user);
    }
    

如果参数列表中的参数名称与ULR模板中{}所指定的参数名称不一致,可以在参数列表中使用AliasAs(paramName)进行指定

  • 示例

    public interface IGitHubApi
    {
        [Get("/group/{id}/users")]
    	Task<List<User>> GroupList([AliasAs("id")] int groupId);
    }
    

{}中还可以直接访问参数列表中的对象成员

  • 示例

    
    public interface IGitHubApi
    {
        [Get("/group/{request.groupId}/users/{request.userId}")]
     	Task<List<User>> GroupList(UserGroupRequest request);
    }
    
    class UserGroupRequest{
        int groupId { get;set; }
        int userId { get;set; }
    }
    

需要注意的是,方法的参数列表中未匹配的参数一律会作为请求参数

  • 示例

    public interface IGitHubApi
    {
        [Get("/group/{id}/users")]
    	Task<List<User>> GroupListA([AliasAs("id")] int groupId, [AliasAs("sort")] string sortOrder);
        
        [Get("/group/{id}/users")]
    	Task<List<User>> GroupListB([AliasAs("id")] int groupId, string sortOrder);
    }
    
    GroupListA(4, "desc");   //请求路径为 /group/4/users?sort=desc 
    GroupListB(4, "desc");   //请求路径为 /group/4/users?sortOrder=desc 
    

路由转换

在声明方法时,如果希望方法的参数作为URL的一部分,那么可以使用{** paramName}进行匹配,使用**可以将匹配到的参数中所携带的斜杠 / 保持原样,不进行编码

  • 示例

    public interface IGitHubApi
    {
        [Get("/search/{**page}")]
    	Task<List<Page>> Search(string page);
    }
    
    Search("admin/products"); // 请求的URL为  /search/admin/products
    

需要注意的是{** paramName}所匹配的参数,其类型必须为string

二、查询字符串

这里指出的查询字符串是指在请求的URL中通过?分隔开的查询参数

1、动态查询字符串

对象作为查询字符串

在Get请求中,如果使用一个引用类型作为方法参数,那么类型对象中所有公共且不为null的属性将为自动成为查询字符串。

  • 自定义类型中,可以使用[AliasAs]特性设置属性序列化和反序列化时键的名称。如果不设置,默认会使用属性名

  • 自定义类中可以使用[Query]特性设置指定属性的前缀和前缀与参数名之间的分隔符。不过通常更多是在接口的参数列表中使用,对类型的所有属性统一设置

  • 枚举类型则可以使用[EnumMember]特性设置序列化和序列化时的对应值

  • 示例

    public class MyQueryParams
    {
        [AliasAs("order")]
        public string SortOrder { get; set; }
    
        public int Limit { get; set; }
    
        public KindOptions Kind { get; set; }
    }
    
    public enum KindOptions
    {
        Foo,
        [EnumMember(Value = "bar")] //定义序列化和反序列化时对应的值
        Bar
    }
    
    public interface IGitHubApi
    {
        [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);
    }
    
    var myParams = new MyQueryParams();
    myParams.SortOrder = "desc";
    myParams.Limit = 10;
    myParams.Kind = KindOptions.Bar;
    
    GroupList(4, myParams)               //请求的相对URL为 /group/4/users?order=desc&objA.Limit=10&Kind=bar
    GroupListWithAttribute(4, myParams)  //请求的相对URL为/group/4/users?search.order=desc&search.Limit=10&search.Kind=bar
    

字典作为查询字符串

在Get请求中,可以使用字典Dictionary作为参数,其内容会自动序列化为查询字符串,只是无法通过[AliasAs]去指定键名

在非Get请求中的查询字符串

在非Get请求中,如果希望将指定的参数对象作为查询字符串,可以使用[Query]特性来声明对参数进行扁平化处理。实际上不论是Get还是非Get请求,如果要将方法参数对象作为查询字符串,那么建议在参数列表中都使用[Query]特性进行声明。

  • 示例

    [Post("/statuses/update.json")]
    Task<Tweet> PostTweet([Query]TweetParams myParams);
    

2、集合参数作为查询字符串

如果方法参数是一个集合对象,并且希望其内容作为查询字符串来使用,可以在参数列表中使用[Query]特性配合CollectionFormat枚举对集合进行格式化设置,其有效的格式如下:

  • CollectionFormat.Multi:根据参数名称,将集合中的每一个元素作为单独的查询字符串参数

  • CollectionFormat.Csv:将集合中的所有元素使用逗号连接,作为一个查询字符串

  • CollectionFormat.Ssv:将集合中的所有元素使用空格连接,作为一个查询字符串

  • CollectionFormat.Tsv:将集合中的所有元素使用制表符(\t)连接,作为一个查询字符串

  • CollectionFormat.Pipes:将集合中的所有元素使用竖线(|)连接,作为一个查询字符串

  • 示例

    [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"  
    

如果向进行全局设置,可以在创建接口对象或进行容器注册时进行设置

  • 示例

    var gitHubApi = RestService.For<IGitHubApi>("https://api.github.com",
        new RefitSettings {
            CollectionFormat = CollectionFormat.Multi
        });
    

3、查询字符串参数解码

解码查询字符串其实就是不对查询字符串进行编码,保留原有的字符串内容,refit支持在进行接口方法定义时,通过使用[QueryUriFormat(UriFormat.Unescaped)]特性声明不对此方法中的查询字符串内容进行编码

  • 示例

    [Get("/query")]
    [QueryUriFormat(UriFormat.Unescaped)]
    Task Query(string q);
    
    Query("Select+Id,Name+From+Account") // 请求的相对URL为 /query?q=Select+Id,Name+From+Account
    

4、URL参数的自定义格式化

Refit提供了IUrlParameterFormatter接口来帮助我们对URL参数进行自定义的格式化,这在我们需要对日期、数值等数据进行格式化时十分有效。

实现接口

只需要实现IUrlParameterFormatter接口,并实现Format()方法,在方法中进行自定义格式化即可

  • 示例

    //对日期进行格式化
    public class CustomDateUrlParameterFormatter : IUrlParameterFormatter
    {
        public string? Format(object? value, ICustomAttributeProvider attributeProvider, Type type)
        {
            if (value is DateTime dt)
            {
                return dt.ToString("yyyyMMdd");
            }
    
            return value?.ToString();
        }
    }
    //对字典进行格式化
    public class CustomDictionaryKeyFormatter : IUrlParameterFormatter
    {
        public string? Format(object? value, ICustomAttributeProvider attributeProvider, Type type)
        {
            // Handle dictionary keys
            if (attributeProvider is PropertyInfo prop && prop.PropertyType.IsGenericType && prop.PropertyType.GetGenericTypeDefinition() == typeof(Dictionary<,>))
            {
                // Custom formatting logic for dictionary keys
                return value?.ToString().ToUpperInvariant();
            }
    
            return value?.ToString();
        }
    }
    

使用自定义格式化

在创建接口对象或进行容器注册时进行设置即可

  • 示例

    var gitHubApi = RestService.For<IGitHubApi>("https://localhost:7234",new RefitSettings
    {
        UrlParameterFormatter = new CustomDateUrlParameterFormatter()
    });
    

三、请求体(Body)

在进行Post请求时,是需要携带请求体的,此时可以在声明方法时,在参数列表中对希望作为请求体的参数使用[Body]特性。

  • 示例

    [Post("/users/new")]
    Task CreateUser([Body] User user);
    

Refit对请求体的处理主要有如下四种情况

  • 当参数为Stream类型,那么数据会通过 StreamContent 进行流式传输
  • 当参数为string类型,默认情况下会直接将参数作为请求体(也就是Content-Type=text/plain);如果使用了特性 [Body(BodySerializationMethod.Json)],字符串将被作为StringContent 发送,并以 JSON 格式提供(Content-Type = application/json),这通常适用于需要符合 JSON 格式的 API 接口
  • 使用了[Body(BodySerializationMethod.UrlEncoded)]特性的参数,其内容将被 URL 编码。这种情况通常用于表单提交(Content-Type=application/x-www-form-urlencoded
  • 当参数是其他类型(即不属于上面的三种情况)时,则会使用 RefitSettings 中指定的内容序列化器进行序列化,默认情况下是 JSON 格式(也就是[Body(BodySerializationMethod.Json)]

1、流缓存及Content-Length

默认情况下,Refit对请求体进行流处理时,是不进行缓存的(针对所有请求体,不仅仅是文件),这意味着可以从磁盘流式传输文件,而不会产生将整个文件加载到内存中的开销。这样做的缺点是请求上没有设置Content-Length报头。如果需要Refit在进行API请求中发送一个Content-Length头,可以使用[Body(buffered:true)]特性来开启缓存

  • 原因在于,要得到Content-Length就必须先把要进行传输的文件加载到内存中才能正确的计算出来

  • 示例

    这里只是以文件上传为例子,实际上refit对所有的请求体默认都是不进行缓存的

    public interface IMyApi  
    {  
        [Post("/upload")]  
        Task UploadFile([Body(buffered: true)] Stream fileStream);  
    }  
    
    // 使用时  
    var api = RestService.For<IMyApi>("https://example.com");  
    using (var fileStream = File.OpenRead("largefile.txt"))  
    {  
        await api.UploadFile(fileStream); // Content-Length 会被设置  
    }  
    

2、Json的序列化管理

关于Json的请求和响应,Refit使用IHttpContentSerializer接口对象其进行序列化和反序列化。

Refit提供了两种Json序列化的实现方式:

  • SystemTextJsonContentSerializer:这是默认的 JSON 序列化器,基于 .NET 内置的 System.Text.Json 库。这个实现专注于高性能和低内存占用,适合对性能有严格要求的应用(默认)
  • NewtonsoftJsonContentSerializer:基于流行的 Newtonsoft.Json 库,这个实现更灵活,可以处理更多复杂的序列化需求,支持配置选项和定制化。

使用Newtonsoft.Json进行序列化

由于默认使用的是SystemTextJsonContentSerializer,所以这里学习一下如何使用NewtonsoftJsonContentSerializer就好了。

如果使用NewtonsoftJsonContentSerializer进行序列化,需要在项目中安装Refit.Newtonsoft.Json依赖库

在这里插入图片描述
然后在创建Refit接口对象或进行容器注册时进行设置即可

  • 示例

    var gitHubApi = RestService.For<IGitHubApi>("https://localhost:7234", new RefitSettings()
    {
        ContentSerializer = new NewtonsoftJsonContentSerializer()
    });
    

使用Newtonsoft.Json,可以通过JsonConvert.DefaultSettings对其默认的序列化行为进行全局配置。

  • 示例

    JsonConvert.DefaultSettings = () => new JsonSerializerSettings()
    {
        ContractResolver = new CamelCasePropertyNamesContractResolver(),
        Converters = { new StringEnumConverter() }
    };
    

也可以在创建Refit接口对象或进行容器注册针对指定的API接口进行配置

  • 示例

    var gitHubApi = RestService.For<IGitHubApi>("https://api.github.com",
        new RefitSettings {
            ContentSerializer = new NewtonsoftJsonContentSerializer(
                new JsonSerializerSettings { ContractResolver = new SnakeCasePropertyNamesContractResolver()
            }
        )});
    
    var otherApi = RestService.For<IOtherApi>("https://api.example.com",
        new RefitSettings {
            ContentSerializer = new NewtonsoftJsonContentSerializer(
                new JsonSerializerSettings {
                    ContractResolver = new CamelCasePropertyNamesContractResolver()
            }
        )});
    

属性的序列化别名设置

如果使用的是Newtonsoft.Json,那么在类型的属性上,可以通过[JsonProperty]对属性进行自定义序列化设置,例如[JsonProperty(PropertyName="b"),其效果与[AliasAs("b")]一样

  • 示例

    public class Foo
    {
        // Works like [AliasAs("b")] would in form posts (see below)
        [JsonProperty(PropertyName="b")]
        public string Bar { get; set; }
    }
    

如果使用的是默认的System.Text.Json,则可以使用[JsonPropertyName]进行属性序列化别名的设置

  • 示例

    public class User  
    {  
        [JsonPropertyName("full_name")]  
        public string FullName { get; set; }  
    
        public int Age { get; set; }  
    }
    

3、XML序列化管理

Refit默认使用Json进行序列化,因此如果不进行配置,所有的请求和响应都会被处理为Json格式。

Refit的XML序列化使用System.Xml.Serialization.XmlSerializer,要配置XML序列化,需要将ContentSerializer设置为XmlContentSerializer对象

  • 示例

    var gitHubApi = RestService.For<IXmlApi>("https://www.w3.org/XML",
        new RefitSettings {
            ContentSerializer = new XmlContentSerializer()
        });
    

System.Xml.Serialization.XmlSerializer提供了许多序列化配置项,可以在创建或注册Refit接口时候进行设置。

  • 示例

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

此外,System.Xml.Serialization.XmlSerializer还提供了一些可以对类型属性的序列化进行自定义设置的特性

  • 示例

    public class Foo
    {
        [XmlElement(Namespace = "https://www.w3.org/XML")]
        public string Bar { get; set; }
    }
    

4、表单提交

如果需要进行表单提交,那么需要使用[Body(BodySerializationMethod.UrlEncoded)]特性对方法参数进行声明,其会使用application/x-www-form-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"},
    };
    
    // Serialized as: v=1&tid=UA-1234-5&cid=d1e9ea6b-2e8b-4699-93e0-0bcbd26c206c&t=event
    await api.Collect(data);
    

object类型参数

可以使用object类型作为参数进行表单提交,其公开可读的属性会被序列化为表单字段。且类型中的属性同样支持使用[AliasAs("whatever")][JsonProperty(PropertyName = "whatever")][JsonPropertyName("whatever")]进行属性的别名设置,但是[AliasAs("whatever")]的优先级更高

  • 示例

    public interface IMeasurementProtocolApi
    {
        [Post("/collect")]
        Task Collect([Body(BodySerializationMethod.UrlEncoded)] Measurement measurement);
    }
    
    public class Measurement
    {
        public int v { get { return 1; } }
    
        [AliasAs("tid")]
        public string WebPropertyId { get; set; }
    
    		[JsonProperty(PropertyName = "one")]
        [AliasAs("cid")]
        public Guid ClientId { get; set; }
    
        [JsonProperty(PropertyName = "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);
    

需要注意的是,[AliasAs]特性对查询字符串参数和表单数据POST 请求的序列化有效,但对响应数据的反序列化无效,因此如果响应内容中的字段名称与接纳类型的属性名不同时,还是需要使用[JsonProperty][JsonPropertyName]特性进行别名指定。

四、请求头设置

1、静态请求头

Refit提供了[Headers]特性,可以在声明接口方法时,设置一个或多个请求头信息。

  • 示例

    public interface IGitHubApi
    {
        [Headers("User-Agent: Awesome Octocat App")]
        [Get("/users/{user}")]
        Task<User> GetUser(string user);
    }
    

如果希望对接口中的所有方法都设置同样的头信息,那么直接在接口上使用[Headers]特性就可以了

  • 示例

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

2、动态请求头

如果请求头中的信息需要根据运行时动态变化,那么可以将[Headers]特性直接在参数列表中对参数使用即可。

  • 示例

    [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");
    

批量请求头

如果需要设置多个动态请求头,可以使用IDictionary<string, string>类型作为参数,并对其使用[HeaderCollection]特性。

  • 示例

    [Get("/users/{user}")]
    Task<User> GetUser(string user, [HeaderCollection] IDictionary<string, string> headers);
    
    var headers = new Dictionary<string, string> {{"Authorization","Bearer tokenGoesHere"}, {"X-Tenant-Id","123"}};
    var user = await GetUser("octocat", headers);
    

3、授权头信息

添加授权头信息是很常见的操作,因此Refit提供了[Authorize]特性专门用于声明授权令牌参数,并且可以设置授权策略,例如JWT可以使用[Authorize("Bearer")]

  • 示例

    [Get("/users/{user}")]
    Task<User> GetUser(string user, [Authorize("Bearer")] string token);
    
    // Will add the header "Authorization: Bearer OAUTH-TOKEN}" to the request
    var user = await GetUser("octocat", "OAUTH-TOKEN");
    

授权令牌简化方案

通常,在API请求中携带授权令牌是很常见的需求,如果对每一个方法都要传入授权令牌,并对参数使用[Authorize("Bearer")]就太麻烦了,针对此,Refit提供了更为简介的方案。

  • 第一步,在创建Refit接口或注册时,通过RefitSettings,配置AuthorizationHeaderValueGetter委托,每次需要授权头信时候都会从此委托中获取授权令牌信息

  • 第二步,在需要携带授权令牌的接口或方法上使用[Headers("Authorization: Bearer")]特性即可

  • 示例

    [Headers("Authorization: Bearer")]
    public interface IGitHubApi
    {
        [Get("/users/{user}")]
        Task<string> GetUser(string user);
    }
    
    var gitHubApi = RestService.For<IGitHubApi>("https://localhost:7234", new RefitSettings()
    {
        AuthorizationHeaderValueGetter = (request,token) => Task.FromResult("返回授权令牌")
    });
    

4、请求头的全局配置

如果有一些头部信息是必须在所有的请求都携带的,对每一个接口或方法都进行配置未免显得繁琐和冗余,此时可以借助DelegatingHandler中间件,在 HTTP 请求和响应的处理管道中插入自定义逻辑,通过继承DelegatingHandler并重写SendAsync()方法,在SendAsync()方法中就可以在每个请求中自动添加所需的头部,而不需要在每个 API 方法中手动添加。对接口或方法特有的请求头,再另外使用特性去配置即可。

  • DelegatingHandler 是 .NET 中用于处理 HTTP 请求和响应的一个抽象类,属于 System.Net.Http 命名空间。它可以在 HTTP 请求的处理管道中插入自定义逻辑,是实现 HTTP 客户端自定义行为的一个重要组件

实现DelegatingHandler

这里假设我们在项目中已经实现了ITenantProvider 接口来获取当前租户的信息,以及一个 IAuthTokenStore接口来获取授权令牌,可以进行如下DelegatingHandler实现。

  • 示例

    public class CustomHeaderHandler : DelegatingHandler  
    {  
        private readonly ITenantProvider _tenantProvider;  
        private readonly IAuthTokenStore _authTokenStore;  
    
        public CustomHeaderHandler(ITenantProvider tenantProvider, IAuthTokenStore authTokenStore)  
        {  
            _tenantProvider = tenantProvider;  
            _authTokenStore = authTokenStore;  
        }  
    
        protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)  
        {  
            // 获取租户 ID 和授权令牌  
            var tenantId = _tenantProvider.GetCurrentTenantId();  
            var authToken = await _authTokenStore.GetAuthTokenAsync();  
    
            // 添加头部  
            request.Headers.Add("X-Tenant-Id", tenantId);  
            request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", authToken);  
    
            // 继续处理请求  
            return await base.SendAsync(request, cancellationToken).ConfigureAwait(false);  
        }  
    }  
    

定义API接口

  • 示例

    public interface IGitHubApi
    {
        [Get("/users/{user}")]
        Task<User> GetUser(string user);
    }
    
    public class User
    {
        public string Id { get; set; }
        public string Name { get; set; }
    }
    

注册中间件及API接口

  • 示例-Program.cs

    ......
    builder.Services.AddTransient<ITenantProvider, TenantProvider>();
    builder.Services.AddTransient<IAuthTokenStore, AuthTokenStore>();
    builder.Services.AddTransient<CustomHeaderHandler>();
    
    builder.Services.AddRefitClient<IGitHubApi>()
            .ConfigureHttpClient(c => c.BaseAddress = new Uri("https://api.example.com"))
            .AddHttpMessageHandler<CustomHeaderHandler>();
    ......
    

如果没有使用依赖容器,那么直接在创建Refit接口时候传入对应的接口对象即可

  • 示例

    var api = RestService.For<IGitHubApi>(new HttpClient(new CustomHeaderHandler(tenantProvider, authTokenStore))
        {
            BaseAddress = new Uri("https://api.example.com")
        }
    );
    

5、请求头的覆盖

在Refit中,如果同一个请求上对同一个请求头设置了多次,那么会根据如下优先级对请求头进行覆盖(仅覆盖同名的请求头):

  • 在接口上使用[Headers]特性进行设置(最低优先级)

  • 在方法上使用[Headers]特性进行设置

  • 在方法的参数上使用[Headers][HeaderCollection]进行设置

  • 示例

    [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:");
    

6、删除请求头

如果希望对某个接口或方法删除指定的请求头,可以通过如下两种方式:

  • 静态请求头的方式设置请求头,且不设置请求头的值
  • 动态请求头的方式设置请求头,且将值设置为null

需要注意删除和设置为空""的区别

  • 示例

    [Headers("X-Emoji: :rocket:")]
    public interface IGitHubApi
    {
        [Get("/users/list")]
        [Headers("X-Emoji")] // 删除 X-Emoji 请求头
        Task<List> GetUsers();
    
        [Get("/users/{user}")]
        [Headers("X-Emoji:")] // 设置 X-Emoji 请求头为 ""
        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, "");
    

五、中间件的数据传递

1、参数数据传输

如果有些运行过程中的数据需要传递给DelegatingHandler中间件,需要对方法参数使用[Property]特性进行声明。Refit会将使用[Property]特性进行声明的方法参数传入到HttpRequestMessage.PropertiesHttpRequestMessage.Options

  • NET 5 以后HttpRequestMessage.Properties已被标记为过时,Refit会将数据放入到HttpRequestMessage.Options

声明需要传递的参数

  • 示例

    public interface IGitHubApi
    {
        [Post("/users/new")]
        Task CreateUser([Body] User user, [Property("SomeKey")] string someValue);
    
        [Post("/users/new")]
        Task CreateUser([Body] User user, [Property] string someOtherKey);
    }
    

读取传递的数据

DelegatingHandler中间件中进行读取

  • 示例

    class RequestPropertyHandler : DelegatingHandler
    {
        public RequestPropertyHandler(HttpMessageHandler innerHandler = null) : base(innerHandler ?? new HttpClientHandler()) {}
    
        protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            // See if the request has a the property
            if(request.Properties.ContainsKey("SomeKey"))
            {
                var someProperty = request.Properties["SomeKey"];
                //do stuff
            }
    
            if(request.Properties.ContainsKey("someOtherKey"))
            {
                var someOtherProperty = request.Properties["someOtherKey"];
                //do stuff
            }
    
            return await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
        }
    }
    

注册中间件

这里多提一嘴,记得向依赖容器注册中间件

  • 示例-Program.cs

    ......
    builder.Services.AddTransient<RequestPropertyHandler>();
    
    builder.Services.AddRefitClient<IGitHubApi>()
            .ConfigureHttpClient(c => c.BaseAddress = new Uri("https://api.example.com"))
            .AddHttpMessageHandler<RequestPropertyHandler>();
    ......
    

如果没有使用依赖容器,那么则记得要在创建Refit接口时使用中间件

  • 示例

    var api = RestService.For<IGitHubApi>(new HttpClient(new RequestPropertyHandler())
        {
            BaseAddress = new Uri("https://api.example.com")
        }
    );
    

2、接口及方法信息的获取

这里指的是为了使用Refit所定义的接口及其方法。

在实际开发时,有时候可能需要知道当前所调用的方法是来自于哪个接口,特别是在使用的接口继承了某个公共接口的情况时。例如下列情况:

  • 示例

    public interface IGetAPI<TEntity>
    {
        [Get("/{key}")]
        Task<TEntity> Get(long key);
    }
    
    public interface IUsersAPI : IGetAPI<User>
    {
    }
    
    public interface IOrdersAPI : IGetAPI<Order>
    {
    }
    

获取接口信息

Refit提供了HttpRequestMessageOptions.InterfaceType静态字符串专门用于从HttpRequestMessage.PropertiesHttpRequestMessage.Options中获取对应接口的Type类型

  • .Net5之后,从HttpRequestMessage.Properties改为HttpRequestMessage.Options

通过获取接口信息,可以在中间件中进行一些业务处理,例如更换访问的URL

  • 示例

    class RequestPropertyHandler : DelegatingHandler
    {
        public RequestPropertyHandler(HttpMessageHandler innerHandler = null) : base(innerHandler ?? new HttpClientHandler()) {}
    
        protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
    		    //通过 HttpMessageRequestOptions.InterfaceType 获取到接口的Type类型
            Type interfaceType = (Type)request.Properties[HttpMessageRequestOptions.InterfaceType];
    
            var builder = new UriBuilder(request.RequestUri);
            // Alter the Path in some way based on the interface or an attribute on it
            builder.Path = $"/{interfaceType.Name}{builder.Path}";
            // Set the new Uri on the outgoing message
            request.RequestUri = builder.Uri;
    
            return await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
        }
    }
    

获取方法信息

Refit提供了HttpRequestMessageOptions.RestMethodInfo静态字符串专门用于从HttpRequestMessage.PropertiesHttpRequestMessage.Options中获取对应方法的RestMethodInfo类型,所有方法所相关的信息都封装在RestMethodInfo中,特别是在需要使用反射时,可以访问到完整的MethodInfo对象。

  • 示例

    class RequestPropertyHandler : DelegatingHandler
    {
        public RequestPropertyHandler(HttpMessageHandler innerHandler = null) : base(innerHandler ?? new HttpClientHandler()) {}
    
        protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            // Get the method info
            if (request.Options.TryGetValue(HttpRequestMessageOptions.RestMethodInfo,out RestMethodInfo restMethodInfo))
            {
                var builder = new UriBuilder(request.RequestUri);
                // Alter the Path in some way based on the method info or an attribute on it
                builder.Path = $"/{restMethodInfo.MethodInfo.Name}{builder.Path}";
                // Set the new Uri on the outgoing message
                request.RequestUri = builder.Uri;
            }
    
            return await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
        }
    }
    

六、Multipart uploads

Multipart uploads指的是多部份上传,类似于表单中携带一个或多个文件进行上传的情况。

Refit支持使用[Multipart]特性声明方法,使用[Multipart]特性的方法将会以Content-Type=multipart/form-data类型提交。

多部份上传所支持的参数类型

此时该方法所支持的参数类型如下:

  • string:字符串类型,参数名将用作表单数据的名称,字符串值作为其值
  • byte[]:字节数组,通常用于已经将内容加载到内存中的情况
  • Stream:流,用于处理文件或数据流,常用于大文件
  • FileInfo:文件信息,表示文件的元数据,会自动根据文件信息获取文件进行上传

字段名称的优先级

在多部份上传的数据中,字段名称的优先级如下:

  • multipartItem.Name:如果在运行时动态的指定了名称并且不为 null,则优先使用此名称,可以在执行时为表单数据部分命名
  • [AliasAs] 特性:可用于装饰方法签名中的流参数,为其提供一个静态名称
  • MultipartItem参数名称:这是默认的字段名称,直接根据方法签名中定义的参数名称使用

边界设置

边界指的是在多部份上传时,对不同部分进行分割的标识符,可以通过[Multipart(boundaryText)]进行设置,如果没有设置则默认使用----MyGreatBoundary

  • 边界效果示意

    ------MyGreatBoundary  
    Content-Disposition: form-data; name="field1"  
    
    value1  
    ------MyGreatBoundary  
    Content-Disposition: form-data; name="file"; filename="example.txt"  
    Content-Type: text/plain  
    
    (文件内容)  
    ------MyGreatBoundary--  
    

边界字符串可以帮助服务器解析请求中的不同部分,服务器用边界来识别这些部分的开始和结束。

文件名和内容类型的指定

对于 byte[]StreamFileInfo 参数,必须使用包装类来指定文件名和内容类型。具体的包装类包括:

  • ByteArrayPart:用于字节数组

  • StreamPart:用于流

  • FileInfoPart:用于文件信息

  • 示例-Stream

    public interface IMyApi  
    {  
        [Multipart]  
        [Post("/users/textFile")]
        Task UploadFile([AliasAs("file")] StreamPart fileStream, string description);  
    }  
    
    // 调用示例  
    using (var stream = File.OpenRead("path/to/largeFile.txt"))  
    {  
        var part = new StreamPart(stream, "largeFile.txt", "text/plain");  
        await api.UploadFile(part, "Large file description");  
    }  
    
  • 示例-byte[]

    public interface IMyApi  
    {  
        [Multipart]  
        [Post("/users/textFile")]
        Task UploadFile([AliasAs("file")] ByteArrayPart file,string description);  
    }  
    
    // 调用示例  
    var byteArray = File.ReadAllBytes("path/to/file.txt");  
    var part = new ByteArrayPart(byteArray, "file.txt", "text/plain");  
    await api.UploadFile(part, "File description");  
    
  • 示例-FileInfo

    public interface IMyApi  
    {  
        [Multipart]  
        [Post("/users/textFile")]
        Task UploadFile([AliasAs("file")]FileInfoPart file,string description);  
    }  
    
    // 调用示例  
    var fileInfo = new FileInfo("path/to/file.txt");  
    var part = new FileInfoPart(fileInfo);  
    await api.UploadFile(part, "File description");  
    

七、响应类型

在Refit中,所有的网络请求必须是异步的,所有请求都要返回一个 Task(表示正在进行的异步操作)或 IObservable(用于响应式编程)。

返回Task

如果方法返回 Task 而不带类型参数,这意味着调用只关注请求是否成功,而不关心返回的具体内容。示例中 CreateUser 方法创建用户,但不返回任何数据,只确认请求是否成功。

  • 示例

    [Post("/users/new")]  
    Task CreateUser([Body] User user);  
    

返回Task<T>

如果返回类型是Task<T>,则表示可以接收响应的内容,通常是从服务器返回的 JSON 数据(自动进行反序列化),或是一些基础的基本类型,例如stringint等等,这种情况下也是不关注响应的元数据的。

  • 示例

    // 获取用户内容作为字符串  
    [Get("/users/{user}")]  
    Task<string> GetUser(string user); 
    

返回ApiResponse<T>

使用 ApiResponse<T> 作为返回类型能够获取请求和响应的元数据,例如 HTTP 状态码、响应头等,同时也能获得反序列化后的内容

  • 通过ApiResponse<T>response.StatusCode获取状态码

  • 通过ApiResponse<T>IsSuccessful 属性来判断请求是否成功,并进一步处理响应

  • 通过ApiResponse<T>Content属性可以获取到T对象

  • 通过ApiResponse<T>Headers属性可以获取到响应头信息

  • 通过ApiResponse<T>Headers.Server属性可以获取到服务器信息

  • 示例

    [Get("/users/{user}")]
    Task<ApiResponse<User>> GetUser(string user);
    
    var response = await gitHubApi.GetUser("octocat");
    var httpStatus = response.StatusCode;
    
    if(response.IsSuccessful)
    {
        //YAY! Do the thing...
    }
    
    var serverHeaderValue = response.Headers.Server != null ? response.Headers.Server.ToString() : string.Empty;
    
    var customHeaderValue = string.Join(',', response.Headers.GetValues("A-Custom-Header"));
    
    foreach(var header in response.Headers)
    {
        var headerName = header.Key;
        var headerValue = string.Join(',', header.Value);
    }
    
    var user = response.Content;
    

返回IObservable<HttpResponseMessage>

当发出网络请求并返回一个 IObservable<HttpResponseMessage> 时,将接收到一个包含 HTTP 响应的对象,这个对象可以被用来获取请求的详细信息,比如状态码、响应头和响应体等。

由于很少用到IObservable<HttpResponseMessage>类型,这里就不展开说明了。哪天用到了再补充IObservable接口的用法

八、接口的使用

1、泛型接口

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);
    }
    
    var api = RestService.For<IReallyExcitingCrudApi<User, string>>("http://api.example.com/users");
    

2、接口继承

Refit支持接口继承,从而避免重复声明一样的方法

  • 示例

    public interface IBaseService
    {
        [Get("/resources")]
        Task<Resource> GetResource(string id);
    }
    
    public interface IDerivedServiceA : IBaseService
    {
        [Delete("/resources")]
        Task DeleteResource(string id);
    }
    
    public interface IDerivedServiceB : IBaseService
    {
        [Post("/resources")]
        Task<string> AddResource([Body] Resource resource);
    }
    

需要注意的是,使用继承时,请求头的配置也会被继承。

  • 示例

    [Headers("User-Agent: AAA")]
    public interface IAmInterfaceA
    {
        [Get("/get?result=Ping")]
        Task<string> Ping();
    }
    
    [Headers("User-Agent: BBB")]
    public interface IAmInterfaceB : IAmInterfaceA
    {
        [Get("/get?result=Pang")]
        [Headers("User-Agent: PANG")]
        Task<string> Pang();
    
        [Get("/get?result=Foo")]
        Task<string> Foo();
    }
    
    public interface IAmInterfaceC : IAmInterfaceA, IAmInterfaceB
    {
        [Get("/get?result=Foo")]
        Task<string> Foo();
    }
    

上述示例中:

  • IAmInterfaceB 的Ping()和Foo()方法将会使用 User-Agent: BBB 请求头
  • IAmInterfaceB 的Pang()方法将会使用 User-Agent: PANG 请求头
  • 如果IAmInterfaceB 接口上没有设定请求头,那么Foo()方法将会使用 User-Agent: AAA 请求头
  • IAmInterfaceC 的 Foo()方法会先查看IAmInterfaceA所定义的请求头,如果有则直接使用,如果没有则查看IAmInterfaceB的请求头,这个跟继承的顺序有关,按顺序往下,找到第一个就直接使用

3、接口的默认实现

从C# 8.0开始,接口中可以定义默认实现方法。Refit支持在接口中通过默认实现为接口提供额外的逻辑。

  • 示例

    public interface IApiClient
    {
        // implemented by Refit but not exposed publicly
        [Get("/get")]
        internal Task<string> GetInternal();
        // Publicly available with added logic applied to the result from the API call
        public async Task<string> Get()
            => FormatResponse(await GetInternal());
        private static String FormatResponse(string response)
            => $"The response is: {response}";
    }
    

九、使用HttpClientFactory

Refit对ASP.Net Core 2.1 以后出现的HttpClientFactory有着一流的支持。

在ASP.Net Core中使用Refit,直接安装Refit.HttpClientFactory依赖库即可。
在这里插入图片描述
注册Refit接口

ASP.Net Core 项目中,可以在Program.cs中,通过AddRefitClient<IWebApi>()注册Refit接口并进行基RUL的配置

  • 示例-Program.cs

    ......
    builder.Services.AddRefitClient<IGitHubApi>()
            .ConfigureHttpClient(c => c.BaseAddress = new Uri("https://api.example.com"));
            //.AddHttpMessageHandler<MyHandler>()            //添加中间件
            //.SetHandlerLifetime(TimeSpan.FromMinutes(2));  //设置中间件的声明周期
    ......
    

进行Refit配置

如果需要对Refit进行自定义设置,可以有如下两种方式:

  • 先进行RefitSettings对象的配置,然后再直接传入到AddRefitClient<IWebApi>(RefitSettings settings)方法中

  • 示例-Program.cs

    var settings = new RefitSettings();
    builder.Services.AddRefitClient<IWebApi>(settings)
            .ConfigureHttpClient(c => c.BaseAddress = new Uri("https://api.example.com"));
            // Add additional IHttpClientBuilder chained methods as required here:
            // .AddHttpMessageHandler<MyHandler>()
            // .SetHandlerLifetime(TimeSpan.FromMinutes(2));
    
  • 使用AddRefitClient<IWebApi>(Func<IServiceProvider, RefitSettings?>? settingsAction)方法获取容器注入的服务提供对象后再进行配置

  • 示例-Program.cs

    ......
    //从容器中注入服务提供对象后在进行设置
    builder.Services.AddRefitClient<IWebApi>(provider => new RefitSettings() { /* configure settings */ })
            .ConfigureHttpClient(c => c.BaseAddress = new Uri("https://api.example.com"));
            // .AddHttpMessageHandler<MyHandler>()
            // .SetHandlerLifetime(TimeSpan.FromMinutes(2));
    ......
    

需要注意的是,RefitSettings中的一些属性会被忽略,因为HttpClientHttpClientHandlers是由HttpClientFactory管理而不是Refit。

依赖注入获得API接口对象

注册完成后,就可以再需要使用的地方通过依赖注入获得Refit接口对象了。

  • 示例

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

十、异常处理

1、Refit对异常的处理

Refit对于异常处理的方式会根据方法返回的类型而有所不同,例如返回Task<T>Task<IApiResponse>Task<IApiResponse<T>>Task<ApiResponse<T>>等类型。

返回 Task<T>

如果接口方法返回 Task<T>,Refit将会抛出任何由ExceptionFactory产生的 ApiException异常或在尝试反序列化为 Task<T> 时产生的异常。

  • 示例

    try
    {
       var result = await awesomeApi.GetFooAsync("bar");
    }
    catch (ApiException exception)
    {
       // 异常处理逻辑  
    }
    

返回 Task<IApiResponse>Task<IApiResponse<T>> 或 Task<ApiResponse<T>>

当接口方法返回Task<IApiResponse>Task<IApiResponse<T>> 或 Task<ApiResponse<T>>时,Refit将捕获所有由ExceptionFactory产生的ApiException异常以及在尝试反序列化ApiResponse<T> 时发生的异常,并将捕获到的异常填充到 ApiResponse<T> 的 Error 属性中,而不会直接抛出。

  • 示例

    var response = await _myRefitClient.GetSomeStuff();  
    if (response.IsSuccessful)  
    {  
        // 执行业务逻辑  
    }  
    else  
    {  
        _logger.LogError(response.Error, response.Error.Content);  
    }  
    

需要注意的是,ApiResponse<T>IsSuccessful所检查的是状态是否在200~299内且没有任何其他异常(例如反序列化时产生的异常),如果只是想检查HTTP响应状态,可以使用IsSuccessStatusCode属性。

2、自定义异常工厂

Refit允许用户提供自定义的异常处理逻辑。

处理响应的自定义异常工厂

如果是想自定义处理响应时发生异常,可以通过设置RefitSettings 中的ExceptionFactory属性来实现,例如可以选择在处理响应时忽略所有异常。

  • 示例

    var nullTask = Task.FromResult<Exception>(null);  
    
    var gitHubApi = RestService.For<IGitHubApi>("https://api.github.com",  
        new RefitSettings {  
            ExceptionFactory = httpResponse => nullTask;  
        });  
    

处理反序列化的自定义异常工厂

如果希望自定义处理反序列化时产生的异常,可以通过设置RefitSettings 中的DeserializationExceptionFactory属性来实现

  • 示例

    var nullTask = Task.FromResult<Exception>(null);
    
    var gitHubApi = RestService.For<IGitHubApi>("https://api.github.com",
        new RefitSettings {
            DeserializationExceptionFactory = (httpResponse, exception) => nullTask;
        });
    
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

SchuylerEX

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值