如何在Asp.Net Core中集成Refit
在很多时候我们在不同的服务之间需要通过HttpClient进行及时通讯,在我们的代码中我们会创建自己的HttpClient对象然后去跨领域额进行数据的交互,但是往往由于一个项目有多个人开发所以在开发中没有人经常会因为不同的业务请求去写不同的代码,然后就会造成各种风格的HttpClient的跨域请求,最重要的是由于每个人对HttpClient的理解程度不同所以写出来的代码可能质量上会有参差不齐,即使代码能够达到要求往往也显得非常臃肿,重复高我们在正式介绍Refit这个项目之前,我们来看看我们在项目中常用的调用方式,后面再来介绍这种处理方式的弊端以及后面集成了Refit以后我们代码的质量能够有哪些程度的提高。
一 常规创建方式
在常规的方式中我们一般使用IHttpClientFactory来创建HttpClient对象,然后使用这个对象来发送和接收消息,至于为什么要使用这个接口来创建HttpClient对象而不是使用usingnewHttpClient的原因请点击这里了解更多的信息,我们先来看下面的这个例子。
usingSystem;
usingSystem.Collections.Generic;
usingSystem.ComponentModel.DataAnnotations;
usingSystem.Net;
usingSystem.Net.Http;
usingSystem.Net.Http.Headers;
usingSystem.Text;
usingSystem.Threading.Tasks;
usingSystem.Web;
usingAbp.Domain.Services;
usingMicrosoft.Extensions.Logging;
usingNewtonsoft.Json;
namespaceSunlight.Dms.Parts.Domain.Web{
///
///HttpClient的帮助类
///
publicclassDcsPartClientService:DomainService{
privatereadonlyHttpClient_httpClient;
privatereadonlyILogger_loggerHelper;
publicDcsPartClientService(IHttpClientFactoryhttpClientFactory,
ILoggerloggerHelper){
_loggerHelper=loggerHelper;
_httpClient=httpClientFactory.CreateClient(PartsConsts.DcsPartClientName);
if(_httpClient.BaseAddress==null){
thrownewArgumentNullException(nameof(httpClientFactory),$"没有配置名称为{PartsConsts.DcsPartClientName}的HttpClient,或者接口服务的地址为空");
}
}
///
///Post请求返回实体
///
///请求相对路径
///请求数据
///实体T
publicasyncTask>PostResponse(stringrelativeUrl,objectpostObj)whereT:class{
varpostData=JsonConvert.SerializeObject(postObj);
_httpClient.DefaultRequestHeaders.Add("user-agent","Dcs-Parts");
_httpClient.CancelPendingRequests();
_httpClient.DefaultRequestHeaders.Clear();
HttpContenthttpContent=newStringContent(postData);
httpContent.Headers.ContentType=newMediaTypeHeaderValue("application/json");
varresult=default(List);
varresponse=await_httpClient.PostAsync(_httpClient.BaseAddress+relativeUrl,httpContent);
if(response.StatusCode==HttpStatusCode.NotFound){
thrownewValidationException("找不到对应的DcsParts服务");
}
varresponseContent=awaitresponse.Content.ReadAsAsync>>();
if(response.IsSuccessStatusCode){
result=responseContent?.Payload;
}else{
if(!string.IsNullOrWhiteSpace(responseContent?.Message)){
thrownewValidationException(responseContent.Message);
}
_loggerHelper.LogDebug($"请求返回结果:{0}请求内容:{1}",response.StatusCode,postData);
}
returnawaitTask.FromResult(result);
}
publicasyncTask>GetResponse(stringrelativeUrl,objectqueryObj)whereT:class{
varqueryData=ModelToUriQueryParam(queryObj);
_httpClient.DefaultRequestHeaders.Add("user-agent","Dcs-Parts");
_httpClient.CancelPendingRequests();
_httpClient.DefaultRequestHeaders.Clear();
_httpClient.DefaultRequestHeaders.Add("accept","application/json");
varresult=default(List);
varresponse=await_httpClient.GetAsync(_httpClient.BaseAddress+relativeUrl+queryData);
if(response.StatusCode==HttpStatusCode.NotFound){
thrownewValidationException("找不到对应的DcsParts服务");
}
varresponseContent=awaitresponse.Content.ReadAsAsync>>();
if(response.IsSuccessStatusCode){
result=responseContent?.Payload;
}else{
if(!string.IsNullOrWhiteSpace(responseContent?.Message)){
thrownewValidationException(responseContent.Message);
}
}
returnawaitTask.FromResult(result);
}
privatestringModelToUriQueryParam(Tt,stringurl=""){
varproperties=t.GetType().GetProperties();
varsb=newStringBuilder();
sb.Append(url);
sb.Append("?");
foreach(varpinproperties){
varv=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);
returnsb.ToString();
}
}
publicclassReceiveResponseBodywhereT:class{
publicstringMessage{get;set;}
publicTPayload{get;set;}
}
publicclassReceiveResponseBody{
publicstringMessage{get;set;}
}
}
1.1注入IHttpClientFactory对象
在这个过程中我们通过构造函数来注入IHttpClientFactory接口,然后用这个接口的CreateClient方法来创建一个唯一的HttpClient对象,在这里我们一般都会同步注入ILogger接口来记录日志信息从而便于我们排查线上问题,这里我们在CreateClient方法中传入了一个字符串类型的参数用于标记自己创建的HttpClient对象的唯一性。这里我们可以看到在构造函数中我们会去判断当前创建的HttpClient的BaseAddress,如果没有这个基地址那么程序会直接抛出错误提示,那么问题来了我们的HttpClient的BaseAddress到底在哪里配置呢?熟悉Asp.NetCore机制的朋友肯定一下子就会想到在Startup类中配置,那么我们来看看需要怎么配置。
1.2配置HttpClient的BaseAddress
publicIServiceProviderConfigureServices(IServiceCollectionservices){
//dcs.part服务
services.AddHttpClient(PartsConsts.DcsPartClientName,config=>{
config.BaseAddress=newUri(_appConfiguration["DependencyServices:DcsParts"]);
config.Timeout=TimeSpan.FromSeconds(60);
});
}
这里我只是简要截取了一小段内容,这里我们看到AddHttpClient的第一个参数也是一个字符串常量,这个常量应该是和IHttpClientFactory的CreateClient的方法中的那个常量保持绝对的一致,只有这样我们才能够标识唯一的标识一个HttpClient对象,创建完了之后我们就能够在这个里面去配置这个HttpClient的各种参数了,另外在上面的这段代码中_appConfiguration这个对象是通过Startup的构造函数注入的,具体的代码请参考下面。
publicStartup(IHostingEnvironmentenv){
_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.3HttpClient进行数据的发送和接收
//////Post请求返回实体 /// ///请求相对路径 /// 请求数据 /// 实体T publicasyncTask>PostResponse
(stringrelativeUrl,objectpostObj)whereT:class{ varpostData=JsonConvert.SerializeObject(postObj); _httpClient.DefaultRequestHeaders.Add("user-agent","Dcs-Parts"); _httpClient.CancelPendingRequests(); _httpClient.DefaultRequestHeaders.Clear(); HttpContenthttpContent=newStringContent(postData); httpContent.Headers.ContentType=newMediaTypeHeaderValue("application/json"); varresult=default(List ); varresponse=await_httpClient.PostAsync(_httpClient.BaseAddress+relativeUrl,httpContent); if(response.StatusCode==HttpStatusCode.NotFound){ thrownewValidationException("找不到对应的DcsParts服务"); } varresponseContent=awaitresponse.Content.ReadAsAsync >>(); if(response.IsSuccessStatusCode){ result=responseContent?.Payload; }else{ if(!string.IsNullOrWhiteSpace(responseContent?.Message)){ thrownewValidationException(responseContent.Message); } _loggerHelper.LogDebug($"请求返回结果:{0}请求内容:{1}",response.StatusCode,postData); } returnawaitTask.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包
在我们的项目中我们可以通过
2.2定义接口
我们将我们业务中涉及到的方法定义在一个接口中,就像下面这样。
publicinterfaceIDmsAfterSalesApi{
[Headers("User-Agent:Dms-Parts")]
[Post("/internal/api/v1/customerAccounts/update")]
TaskUpdateCustomerAmount([Body]PartRetailSettlementModelinput);
[Headers("User-Agent:Dms-Parts")]
[Post("/internal/api/v1/repairShortagePart/checkCustomerAccount")]
TaskRepairShortagePartCheckCustomerAccount([Body]RepairShortagePartModelinput);
[Headers("User-Agent:Dms-Parts")]
[Post("/internal/api/v1/vehiclesAndMemberCode/forCoupons")]
TaskGetMemberCodeBrandCodeForVehicle(GuidvehicleId);
}
2.3注入接口并使用接口中的方法
publicclassDmsAfterSalesClientService:DomainService{
privatereadonlyIDmsAfterSalesApi_api;
privatereadonlyILogger_logger;
privateconststringFrom="DmsAfterSales";
publicDmsAfterSalesClientService(IDmsAfterSalesApiapi,ILoggerlogger){
_api=api;
_logger=logger;
}
privateasyncTaskWrapException(ApiExceptionexception){
if(exception.StatusCode==System.Net.HttpStatusCode.BadRequest){
varreceivedBody=awaitexception.GetContentAsAsync();
returnnewValidationException($"业务校验失败,{receivedBody.Message}({From})",exception);
}else{
_logger.LogWarning(exception,"CallDmsAfterSalesAPIfailed");
returnnewApplicationException($"内部调用失败,{exception.Message}({exception.StatusCode})({From})",exception);
}
}
privateExceptionWrapException(HttpRequestExceptionexception){
_logger.LogWarning(exception,"CallDmsAfterSalesAPIfailed");
returnnewApplicationException($"内部调用失败,{exception.Message}({From})",exception);
}
publicasyncTaskUpdateCustomerAmount([Body]PartRetailSettlementModelinput){
try{
await_api.UpdateCustomerAmount(input);
}catch(ApiExceptionex){
throwawaitWrapException(ex);
}catch(HttpRequestExceptionex){
throwWrapException(ex);
}
}
publicasyncTaskRepairShortagePartCheckCustomerAccount([Body]RepairShortagePartModelinput){
try{
varresult=await_api.RepairShortagePartCheckCustomerAccount(input);
returnresult.Payload.BalanceAmount;
}catch(ApiExceptionex){
throwawaitWrapException(ex);
}catch(HttpRequestExceptionex){
throwWrapException(ex);
}
}
publicasyncTaskGetMemberCodeBrandCodeForVehicle([Body]GuidvehicleId){
try{
varresult=await_api.GetMemberCodeBrandCodeForVehicle(vehicleId);
returnresult.Payload;
}catch(ApiExceptionex){
throwawaitWrapException(ex);
}catch(HttpRequestExceptionex){
throwWrapException(ex);
}
}
}
在上面接口中定义好这个方法以后我们就可以直接在我们的领域类中引入这个接口IDmsAfterSalesApi,然后就直接使用这个接口中的方法,讲到这里便有疑问,这个接口的实现到底在哪里?这里当我们定义好接口然后点击里面的方法转到实现的时候我们发现里面会转到一个叫做RefitStubs.g.cs的类中,然后自动的生成下面的方法。
///[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] [global::System.Diagnostics.DebuggerNonUserCode] [Preserve] [global::System.Reflection.Obfuscation(Exclude=true)] partialclassAutoGeneratedIDmsAfterSalesApi:IDmsAfterSalesApi { /// publicHttpClientClient{get;protectedset;} readonlyIRequestBuilderrequestBuilder; /// publicAutoGeneratedIDmsAfterSalesApi(HttpClientclient,IRequestBuilderrequestBuilder) { Client=client; this.requestBuilder=requestBuilder; } /// Task IDmsAfterSalesApi.UpdateCustomerAmount(PartRetailSettlementModelinput) { vararguments=newobject[]{input}; varfunc=requestBuilder.BuildRestResultFuncForMethod("UpdateCustomerAmount",newType[]{typeof(PartRetailSettlementModel)}); return(Task )func(Client,arguments); } /// Task IDmsAfterSalesApi.RepairShortagePartCheckCustomerAccount(RepairShortagePartModelinput) { vararguments=newobject[]{input}; varfunc=requestBuilder.BuildRestResultFuncForMethod("RepairShortagePartCheckCustomerAccount",newType[]{typeof(RepairShortagePartModel)}); return(Task )func(Client,arguments); } /// Task IDmsAfterSalesApi.GetMemberCodeBrandCodeForVehicle(GuidvehicleId) { vararguments=newobject[]{vehicleId}; varfunc=requestBuilder.BuildRestResultFuncForMethod("GetMemberCodeBrandCodeForVehicle",newType[]{typeof(Guid)}); return(Task )func(Client,arguments); } }
这里面的核心是调用一个BuildRestResultFuncForMethod的方法,后面我们再来分析这里面到底是怎么实现的,这里我们首先把这整个使用流程说完,之前我们说过Refit的很多配置都是通过标签的方式来注入进去的,这里包括请求类型、相对请求地址,那么我们的默认超时时间和BaseAddress到底是怎样来配置的呢?下面我们就来重点讲述。
2.4 在Startup中配置基础配置信息
publicIServiceProviderConfigureServices(IServiceCollectionservices){
//refitdmsaftersales服务
services.AddRefitClient()
.ConfigureHttpClient(c=>{
c.BaseAddress=newUri(_appConfiguration["DependencyServices:DmsAfterSales"]);
c.Timeout=TimeSpan.FromMilliseconds(_appConfiguration.GetValue("AppSettings:ServiceTimeOutMs"));
});
}
这里我们看到通过一个AddRefitClient方法我们就能够去配置我们的基础信息,讲到这里我们是不是对整个过程都有一个清楚的认识呢?通过上下两种方式的对比,相信你对整个Refit的使用都有自己的理解。
2.5注意事项
由于我们的Headers经常需要我们去配置一组数据,那么我们应该怎么配置多个项呢?
[Headers("User-Agent:Dms-Parts","Content-Type:application/json")]
通过上面的方式我们能够配置一组Headers,另外在很多的时候如果Headers里面没有配置Content-Type那么很有可能会返回StatusCode=415UnsupportMediaType这个类型的错误信息,这个在使用的时候需要注意。
以上就是如何在Asp.NetCore中集成Refit的详细内容,更多关于Asp.NetCore中集成Refit的资料请关注毛票票其它相关文章!
声明:本文内容来源于网络,版权归原作者所有,内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:czq8825#qq.com(发邮件时,请将#更换为@)进行举报,并提供相关证据,一经查实,本站将立刻删除涉嫌侵权内容。