如何在Asp.Net Core中集成Refit

  在很多时候我们在不同的服务之间需要通过HttpClient进行及时通讯,在我们的代码中我们会创建自己的HttpClient对象然后去跨领域额进行数据的交互,但是往往由于一个项目有多个人开发所以在开发中没有人经常会因为不同的业务请求去写不同的代码,然后就会造成各种风格的HttpClient的跨域请求,最重要的是由于每个人对HttpClient的理解程度不同所以写出来的代码可能质量上会有参差不齐,即使代码能够达到要求往往也显得非常臃肿,重复高我们在正式介绍Refit这个项目之前,我们来看看我们在项目中常用的调用方式,后面再来介绍这种处理方式的弊端以及后面集成了Refit以后我们代码的质量能够有哪些程度的提高。

  一  常规创建方式

  在常规的方式中我们一般使用IHttpClientFactory来创建HttpClient对象,然后使用这个对象来发送和接收消息,至于为什么要使用这个接口来创建HttpClient对象而不是使用using new HttpClient的原因请点击这里了解更多的信息,我们先来看下面的这个例子。

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
using System.Web;
using Abp.Domain.Services;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
 
namespace Sunlight.Dms.Parts.Domain.Web {
    /// <summary>
    /// HttpClient的帮助类
    /// </summary>
    public class DcsPartClientService : DomainService {
        private readonly HttpClient _httpClient;
        private readonly ILogger<DcsPartClientService> _loggerHelper;
 
        public DcsPartClientService(IHttpClientFactory httpClientFactory,
                                    ILogger<DcsPartClientService> loggerHelper) {
            _loggerHelper = loggerHelper;
            _httpClient = httpClientFactory.CreateClient(PartsConsts.DcsPartClientName);
            if (_httpClient.BaseAddress == null) {
                throw new ArgumentNullException(nameof(httpClientFactory), $"没有配置名称为 {PartsConsts.DcsPartClientName} 的HttpClient,或者接口服务的地址为空");
            }
        }
 
        /// <summary>
        /// Post请求返回实体
        /// </summary>
        /// <param name="relativeUrl">请求相对路径</param>
        /// <param name="postObj">请求数据</param>
        /// <returns>实体T</returns>
        public async Task<List<T>> PostResponse<T>(string relativeUrl, object postObj) where T : class {
 
            var postData = JsonConvert.SerializeObject(postObj);
 
            _httpClient.DefaultRequestHeaders.Add("user-agent", "Dcs-Parts");
            _httpClient.CancelPendingRequests();
            _httpClient.DefaultRequestHeaders.Clear();
            HttpContent httpContent = new StringContent(postData);
 
            httpContent.Headers.ContentType = new MediaTypeHeaderValue("application/json");
            var result = default(List<T>);
            var response = await _httpClient.PostAsync(_httpClient.BaseAddress + relativeUrl, httpContent);
            if (response.StatusCode == HttpStatusCode.NotFound) {
                throw new ValidationException("找不到对应的DcsParts服务");
            }
            var responseContent = await response.Content.ReadAsAsync<ReceiveResponseBody<List<T>>>();
            if (response.IsSuccessStatusCode) {
                result = responseContent?.Payload;
            } else {
                if (!string.IsNullOrWhiteSpace(responseContent?.Message)) {
                    throw new ValidationException(responseContent.Message);
                }
 
                _loggerHelper.LogDebug($"请求返回结果:{0} 请求内容:{1}", response.StatusCode, postData);
            }
 
            return await Task.FromResult(result);
        }
 
        public async Task<List<T>> GetResponse<T>(string relativeUrl, object queryObj) where T : class {
            var queryData = ModelToUriQueryParam(queryObj);
            _httpClient.DefaultRequestHeaders.Add("user-agent", "Dcs-Parts");
            _httpClient.CancelPendingRequests();
            _httpClient.DefaultRequestHeaders.Clear();
            _httpClient.DefaultRequestHeaders.Add("accept", "application/json");
 
            var result = default(List<T>);
            var response = await _httpClient.GetAsync(_httpClient.BaseAddress + relativeUrl + queryData);
            if (response.StatusCode == HttpStatusCode.NotFound) {
                throw new ValidationException("找不到对应的DcsParts服务");
            }
            var responseContent = await response.Content.ReadAsAsync<ReceiveResponseBody<List<T>>>();
            if (response.IsSuccessStatusCode) {
                result = responseContent?.Payload;
            } else {
                if (!string.IsNullOrWhiteSpace(responseContent?.Message)) {
                    throw new ValidationException(responseContent.Message);
                }
            }
 
            return await Task.FromResult(result);
        }
 
        private string ModelToUriQueryParam<T>(T t, string url = "") {
            var properties = t.GetType().GetProperties();
            var sb = new StringBuilder();
            sb.Append(url);
            sb.Append("?");
            foreach (var p in properties) {
                var v = p.GetValue(t, null);
                if (v == null)
                    continue;
 
                sb.Append(p.Name);
                sb.Append("=");
                sb.Append(HttpUtility.UrlEncode(v.ToString()));
                sb.Append("&");
            }
 
            sb.Remove(sb.Length - 1, 1);
 
            return sb.ToString();
        }
    }
 
    public class ReceiveResponseBody<T> where T : class {
        public string Message { get; set; }
 
        public T Payload { get; set; }
    }
 
    public class ReceiveResponseBody {
        public string Message { get; set; }
    }
 
 
}

  1.1 注入IHttpClientFactory对象

  在这个过程中我们通过构造函数来注入IHttpClientFactory接口,然后用这个接口的CreateClient方法来创建一个唯一的HttpClient对象,在这里我们一般都会同步注入ILogger接口来记录日志信息从而便于我们排查线上问题,这里我们在CreateClient方法中传入了一个字符串类型的参数用于标记自己创建的HttpClient对象的唯一性。这里我们可以看到在构造函数中我们会去判断当前创建的HttpClient的BaseAddress,如果没有这个基地址那么程序会直接抛出错误提示,那么问题来了我们的HttpClient的BaseAddress到底在哪里配置呢?熟悉Asp.Net Core机制的朋友肯定一下子就会想到在Startup类中配置,那么我们来看看需要怎么配置。

  1.2 配置HttpClient的BaseAddress  

public IServiceProvider ConfigureServices(IServiceCollection services) {
          //dcs.part服务
          services.AddHttpClient(PartsConsts.DcsPartClientName, config => {
              config.BaseAddress = new Uri(_appConfiguration["DependencyServices:DcsParts"]);
              config.Timeout = TimeSpan.FromSeconds(60);
          });     
      }  

  这里我只是简要截取了一小段内容,这里我们看到AddHttpClient的第一个参数也是一个字符串常量,这个常量应该是和IHttpClientFactory的CreateClient的方法中的那个常量保持绝对的一致,只有这样我们才能够标识唯一的标识一个HttpClient对象,创建完了之后我们就能够在这个里面去配置这个HttpClient的各种参数了,另外在上面的这段代码中_appConfiguration这个对象是通过Startup的构造函数注入的,具体的代码请参考下面。

public Startup(IHostingEnvironment env) {
            _appConfiguration = env.GetAppConfiguration();
            Clock.Provider = ClockProviders.Local;
            Environment = env;
            Console.OutputEncoding = System.Text.Encoding.UTF8;
        }  

  另外我们还需要配置一些HttpClient所必须的属性包括基地址、超时时间......等等,当然这个基地址我们是配置在appsetting.json中的,具体的配置如下所示。

"DependencyServices": {
   "BlobStorage": "http://blob-storage/",
   "DcsParts": "http://dcs-parts/",
   "DmsAfterSales": "http://dms-after-sales/"
 }

  有了这些我们就能够具备创建一个HttpClient对象的条件了,后面我们来看看我们怎么使用这个HttpClient进行发送和接收数据。

  1.3 HttpClient进行数据的发送和接收

/// <summary>
       /// Post请求返回实体
       /// </summary>
       /// <param name="relativeUrl">请求相对路径</param>
       /// <param name="postObj">请求数据</param>
       /// <returns>实体T</returns>
       public async Task<List<T>> PostResponse<T>(string relativeUrl, object postObj) where T : class {
 
           var postData = JsonConvert.SerializeObject(postObj);
 
           _httpClient.DefaultRequestHeaders.Add("user-agent", "Dcs-Parts");
           _httpClient.CancelPendingRequests();
           _httpClient.DefaultRequestHeaders.Clear();
           HttpContent httpContent = new StringContent(postData);
 
           httpContent.Headers.ContentType = new MediaTypeHeaderValue("application/json");
           var result = default(List<T>);
           var response = await _httpClient.PostAsync(_httpClient.BaseAddress + relativeUrl, httpContent);
           if (response.StatusCode == HttpStatusCode.NotFound) {
               throw new ValidationException("找不到对应的DcsParts服务");
           }
           var responseContent = await response.Content.ReadAsAsync<ReceiveResponseBody<List<T>>>();
           if (response.IsSuccessStatusCode) {
               result = responseContent?.Payload;
           } else {
               if (!string.IsNullOrWhiteSpace(responseContent?.Message)) {
                   throw new ValidationException(responseContent.Message);
               }
 
               _loggerHelper.LogDebug($"请求返回结果:{0} 请求内容:{1}", response.StatusCode, postData);
           }
 
           return await Task.FromResult(result);
       }

  在上面的代码中我们模拟了一个Post请求,请求完成以后我们再使用ReadAsAsync的方法来异步接收另外一个域中的数据,然后我们根据返回的StatusCode来抛出不同的错误提示,并记录相关的日志信息并返回最终Post请求的结果,进而完成整个过程,在这个中间我们发送请求的时候需要注意一下内容:1 最终的完整版地址=BaseAddress+RelativeAddress,基地址是在appsetting.json中进行配置的,RelativeAddress是我们请求不同域的时候的相对地址,这个需要我们根据实际的业务来进行配置。2 请求的对象是我们将数据对象序列化成json后的结果,这两点需要特别注意。

  1.4 总结

  通过上面的讲述我们知道了如何完整的创建HttpClient以及通过创建的HttpClient如何收发数据,但同时我们也发现了通过上面的方式我们的缺点:如果一个业务中有大量的这种跨域请求整个代码显得非常臃肿并且由于不同开发人员的认知不同最终导致很容易出问题,那么我们是否有办法能够去解决上面的问题呢?Refit库的出现正好解决了这个问题,Refit通过这种申明式的方式能够很大程度上让代码更加简练明了而且提供了更加丰富的功能。

  二  使用Refit来创建HttpClient对象

  2.1 引入Refit包

  在我们的项目中我们可以通过   <PackageReference Include="Refit" Version="XXX" />来快速引用Refit包,引用的方式这里便不再赘述。

  2.2 定义接口

  我们将我们业务中涉及到的方法定义在一个接口中,就像下面这样。

public interface IDmsAfterSalesApi {
 
       [Headers("User-Agent: Dms-Parts")]
       [Post("/internal/api/v1/customerAccounts/update")]
       Task<ResponseBody> UpdateCustomerAmount([Body]PartRetailSettlementModel input);
 
       [Headers("User-Agent: Dms-Parts")]
       [Post("/internal/api/v1/repairShortagePart/checkCustomerAccount")]
       Task<RepairShortagePartResponseBody> RepairShortagePartCheckCustomerAccount([Body]RepairShortagePartModel input);
 
       [Headers("User-Agent: Dms-Parts")]
       [Post("/internal/api/v1/vehiclesAndMemberCode/forCoupons")]
       Task<GetMemberCodeBrandCodeForVehicleBody> GetMemberCodeBrandCodeForVehicle(Guid vehicleId);
   }

  2.3 注入接口并使用接口中的方法

public class DmsAfterSalesClientService : DomainService {
    private readonly IDmsAfterSalesApi _api;
    private readonly ILogger<DcsPartClientService> _logger;
    private const string From = "Dms After Sales";
 
    public DmsAfterSalesClientService(IDmsAfterSalesApi api, ILogger<DcsPartClientService> logger) {
        _api = api;
        _logger = logger;
    }
 
    private async Task<Exception> WrapException(ApiException exception) {
        if (exception.StatusCode == System.Net.HttpStatusCode.BadRequest) {
            var receivedBody = await exception.GetContentAsAsync<ResponseBody>();
            return new ValidationException($"业务校验失败,{receivedBody.Message} ({From})", exception);
        } else {
            _logger.LogWarning(exception, "Call Dms After Sales API failed");
            return new ApplicationException($"内部调用失败,{exception.Message} ({exception.StatusCode}) ({From})", exception);
        }
    }
 
    private Exception WrapException(HttpRequestException exception) {
        _logger.LogWarning(exception, "Call Dms After Sales API failed");
        return new ApplicationException($"内部调用失败,{exception.Message} ({From})", exception);
    }
 
    public async Task UpdateCustomerAmount([Body] PartRetailSettlementModel input) {
        try {
            await _api.UpdateCustomerAmount(input);
        } catch (ApiException ex) {
            throw await WrapException(ex);
        } catch (HttpRequestException ex) {
            throw WrapException(ex);
        }
    }
 
    public async Task<decimal> RepairShortagePartCheckCustomerAccount([Body] RepairShortagePartModel input) {
        try {
            var result = await _api.RepairShortagePartCheckCustomerAccount(input);
            return result.Payload.BalanceAmount;
        } catch (ApiException ex) {
            throw await WrapException(ex);
        } catch (HttpRequestException ex) {
            throw WrapException(ex);
        }
    }
 
    public async Task<GetMemberCodeBrandCodeForVehicleOutput> GetMemberCodeBrandCodeForVehicle([Body]Guid vehicleId) {
        try {
            var result = await _api.GetMemberCodeBrandCodeForVehicle(vehicleId);
            return result.Payload;
        } catch (ApiException ex) {
            throw await WrapException(ex);
        } catch (HttpRequestException ex) {
            throw WrapException(ex);
        }
    }
}

  在上面接口中定义好这个方法以后我们就可以直接在我们的领域类中引入这个接口IDmsAfterSalesApi ,然后就直接使用这个接口中的方法,讲到这里便有疑问,这个接口的实现到底在哪里?这里当我们定义好接口然后点击里面的方法转到实现的时候我们发现里面会转到一个叫做RefitStubs.g.cs的类中,然后自动的生成下面的方法。

/// <inheritdoc />
[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
[global::System.Diagnostics.DebuggerNonUserCode]
[Preserve]
[global::System.Reflection.Obfuscation(Exclude=true)]
partial class AutoGeneratedIDmsAfterSalesApi : IDmsAfterSalesApi
{
    /// <inheritdoc />
    public HttpClient Client { get; protected set; }
    readonly IRequestBuilder requestBuilder;
 
    /// <inheritdoc />
    public AutoGeneratedIDmsAfterSalesApi(HttpClient client, IRequestBuilder requestBuilder)
    {
        Client = client;
        this.requestBuilder = requestBuilder;
    }
 
    /// <inheritdoc />
    Task<ResponseBody> IDmsAfterSalesApi.UpdateCustomerAmount(PartRetailSettlementModel input)
    {
        var arguments = new object[] { input };
        var func = requestBuilder.BuildRestResultFuncForMethod("UpdateCustomerAmount", new Type[] { typeof(PartRetailSettlementModel) });
        return (Task<ResponseBody>)func(Client, arguments);
    }
 
    /// <inheritdoc />
    Task<RepairShortagePartResponseBody> IDmsAfterSalesApi.RepairShortagePartCheckCustomerAccount(RepairShortagePartModel input)
    {
        var arguments = new object[] { input };
        var func = requestBuilder.BuildRestResultFuncForMethod("RepairShortagePartCheckCustomerAccount", new Type[] { typeof(RepairShortagePartModel) });
        return (Task<RepairShortagePartResponseBody>)func(Client, arguments);
    }
 
    /// <inheritdoc />
    Task<GetMemberCodeBrandCodeForVehicleBody> IDmsAfterSalesApi.GetMemberCodeBrandCodeForVehicle(Guid vehicleId)
    {
        var arguments = new object[] { vehicleId };
        var func = requestBuilder.BuildRestResultFuncForMethod("GetMemberCodeBrandCodeForVehicle", new Type[] { typeof(Guid) });
        return (Task<GetMemberCodeBrandCodeForVehicleBody>)func(Client, arguments);
    }
}  

  这里面的核心是调用一个BuildRestResultFuncForMethod的方法,后面我们再来分析这里面到底是怎么实现的,这里我们首先把这整个使用流程说完,之前我们说过Refit的很多配置都是通过标签的方式来注入进去的,这里包括请求类型、相对请求地址,那么我们的默认超时时间和BaseAddress到底是怎样来配置的呢?下面我们就来重点讲述。

  2.4  在Startup中配置基础配置信息

public IServiceProvider ConfigureServices(IServiceCollection services) {
          //refit dms after sales服务
          services.AddRefitClient<IDmsAfterSalesApi>()
              .ConfigureHttpClient(c => {
                  c.BaseAddress = new Uri(_appConfiguration["DependencyServices:DmsAfterSales"]);
                  c.Timeout = TimeSpan.FromMilliseconds(_appConfiguration.GetValue<int>("AppSettings:ServiceTimeOutMs"));
              });
      }

  这里我们看到通过一个AddRefitClient方法我们就能够去配置我们的基础信息,讲到这里我们是不是对整个过程都有一个清楚的认识呢?通过上下两种方式的对比,相信你对整个Refit的使用都有自己的理解。

  2.5 注意事项

  由于我们的Headers经常需要我们去配置一组数据,那么我们应该怎么配置多个项呢?

[Headers("User-Agent: Dms-Parts", "Content-Type: application/json")]

  通过上面的方式我们能够配置一组Headers,另外在很多的时候如果Headers里面没有配置Content-Type那么很有可能会返回StatusCode=415 Unsupport Media Type这个类型的错误信息,这个在使用的时候需要注意。

以上就是如何在Asp.Net Core中集成Refit的详细内容,更多关于Asp.Net Core中集成Refit的资料请关注我们其它相关文章!

(0)

相关推荐

  • asp.net core集成CKEditor实现图片上传功能的示例代码

    背景 本文为大家分享了asp.net core 如何集成CKEditor ,并实现图片上传功能的具体方法,供大家参考,具体内容如下. 准备工作 1.visual studio 2019 开发环境 2.net core 2.0 及以上版本 实现方法 1.新建asp.net core web项目 2.下载CKEditor 这里我们新建了一个系统自带的样本项目,去 CKEditor官网下载一个版本,解压后拷贝大wwwroot中 3.增加图片上传控制器 @using CompanyName.Projec

  • ASP.NET Core WebApi版本控制的实现

    前言: 在日常项目开发中,随着项目需求不断的累加.不断的迭代:项目服务接口需要向下兼容历史版本:前些时候就因为Api接口为做版本管理导致接口对低版本兼容处理不友好. 最近就像了解下如何实现WebApi版本控制,那么版本控制有什么好处呢? WebApi版本控制的好处 有助于及时推出功能, 而不会破坏现有系统,兼容性处理更友好. 它还可以帮助为选定的客户提供额外的功能. 接下来就来实现版本控制以及在Swagger UI中接入WebApi版本 一.WebApi版本控制实现 通过Microsoft.As

  • 详解如何在ASP.NET Core中编写高效的控制器

    通过遵循最佳实践,可以编写更好的控制器.所谓的"瘦"控制器(指代码更少.职责更少的控制器)更容易阅读和维护.而且,一旦你的控制器很瘦,可能就不需要对它们进行太多测试了.相反,你可以专注于测试业务逻辑和数据访问代码.瘦控制器的另一个优点是,它更容易维护控制器的多个版本. 这篇文章讨论了使控制器变胖的坏习惯,然后探索了使控制器变瘦和易于管理的方法.我列出编写控制器的最佳实践可能并不全面,但我已经讨论了最重要的一些,并在适当的情况下提供了相关的源代码.在接下来的几节中,我们将研究什么是胖控制

  • Asp.Net Core中发送Email的完整步骤

    前言 在项目开发中常常会需要做发送 Email 的功能,在 ASP.NET Core 中你可以用 MailKit 来实现 Email 的发送,MailKit 是一个开源的客户端库,可用在 Windows,Linux 或者 Mac 上,本篇文章就来讨论在 ASP.NET Core 中去实现. 安装 MailKit 要想使用 MailKit,你可以使用 Visual Studio 2019 中的 NuGet package manager 可视化界面进行安装,或者通过 NuGet package m

  • ASP.NET Core中如何实现重定向详解

    前言 ASP.NET Core 是一个跨平台,开源的,轻量级的,模块化的,用于构建高性能的 web 开发框架, ASP.NET Core MVC 内置了多种方式将一个 request 请求跳转到指定的url,这篇文章我们就来讨论如何去实现. 理解 RedirectActionResult ASP.NET Core MVC 中内置了几种 Redirect,比如说:RedirectResult, RedirectToActionResult, RedirectToRouteResult 和 Loca

  • 如何在ASP.NET Core中使用Session的示例代码

    ASP.NET Core 是一个跨平台,开源的,轻量级,高性能 并且 高度模块化的web框架,Session 可以实现用户信息存储从而可以在同一个客户端的多次请求之间实现用户追踪,在 ASP.Net Core 中可以使用 Microsoft.AspNetCore.Session 中间件来启用 Session 机制. 中间件的价值在于可以在 request -> response 的过程中做一些定制化的操作,比如说:监视数据,切换路由,修改流转过程中的消息体,通常来说:中间件是以链式的方式灌入到

  • ASP.NET Core对不同类型的用户进行区别限流详解

    前言 老板提出了一个新需求,从某某天起,免费用户每天只能查询100次,收费用户100W次. 这是一个限流问题,聪明的你也一定想到了如何去做:记录用户每一天的查询次数,然后根据当前用户的类型使用不同的数字做比较,超过指定的数字就返回错误. 嗯,原理就是这么简单.不过真正写起来还要考虑更多问题: 统计数据的数据结构是什么样的?字典 or 行记录? 统计数据记录到哪里?内存 or MySQL or Redis? 分布式应用怎么精确计数?分布式锁 or 队列 or 事务? 吞吐量比较大时如何扛得住?内存

  • ASP.NET Core中实现全局异常拦截的完整步骤

    前言 异常是一种运行时错误,当异常没有得到适当的处理,很可能会导致你的程序意外终止,这篇就来讨论一下如何在 ASP.Net Core MVC 中实现全局异常处理,我会用一些 样例代码 和 截图 来说明这些概念. 全局异常处理 其实在 ASP.Net Core MVC 框架中已经有了全局异常处理的机制,你可以在一个中心化的地方使用 全局异常处理中间件 来进行异常拦截,如果不用这种中心化方式的话,你就只能在 Controller 或者 Action 作用域上单独处理,这会导致异常处理代码零散在项目各

  • 如何在ASP.Net Core使用分布式缓存的实现

    ASP.Net Core 提供了多种类型的缓存,除了内存缓存和响应缓存之外,还提供了对 分布式缓存 的支持.在之前的一篇文章中,我讨论了 ASP.Net Core 的内存缓存.在本文中,我们将讨论如何在 ASP.Net Core 中使用分布式缓存,本篇就拿 Redis 和 SQL Server 作为演示. 什么是分布式缓存 分布式缓存 可用于提高应用程序的性能和可伸缩性,通常 分布式缓存 被多个应用服务器共享,在分布式缓存中,缓存的数据不会落在某些个别的web服务器内存中,这些缓存数据采用集中化

  • ASP.NET Core 使用Cookie验证身份的示例代码

    ASP.NET Core 1.x提供了通过Cookie 中间件将用户主体序列化为一个加密的Cookie,然后在后续请求中验证Cookie并重新创建主体,并将其分配给HttpContext.User属性.如果您要提供自己的登录界面和用户数据库,可以使用作为独立功能的Cookie中间件. ASP.NET Core 2.x的一个主要变化是不再存在Cookie中间件.取而代之的是在Startup.cs文件中的Configure方法中的调用UseAuthentication方法会添加设置HttpConte

  • 详解如何在ASP.NET Core中使用IHttpClientFactory

    利用IHttpClientFactory可以无缝创建HttpClient实例,避免手动管理它们的生命周期. 当使用ASP.Net Core开发应用程序时,可能经常需要通过HttpClient调用WebAPI的方法以检查终结点是否正常工作.要实现这一点,通常需要实例化HttpClient并使用该实例来调用你的方法.但是直接使用HttpClient也有一些缺点,主要与手动管理实例的生命周期有关. 你可以使用IHttpClientFactory创建HttpClient来避免这些问题.IHttpClie

随机推荐