• Refit-使用强类型方式访问WebApi


    Refit可以进行强类型的客户端调用

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

    参考:https://www.xcode.me/post/5918
  • 相关阅读:
    socket编程中最常用的几个数据类型和转换函数
    windows下给QT可执行文件(exe)一个图标
    fseek函数与ftell函数使用例程
    LINUX C例程1:sscanf的用法
    Linux进程控制——exec函数族
    Linux查看文件编码格式及文件编码转换
    oracle易忘函数用法(1)
    Oracle VARRAY的实际应用简介
    oracle 存储过程的基本语法 及注意事项
    何将ext中的FormPanel中,所有组件都居中放置?
  • 原文地址:https://www.cnblogs.com/fanfan-90/p/12950584.html
Copyright © 2020-2023  润新知