如何在Asp.Net Core中集成Refit
在很多時(shí)候我們?cè)诓煌姆?wù)之間需要通過HttpClient進(jìn)行及時(shí)通訊,在我們的代碼中我們會(huì)創(chuàng)建自己的HttpClient對(duì)象然后去跨領(lǐng)域額進(jìn)行數(shù)據(jù)的交互,但是往往由于一個(gè)項(xiàng)目有多個(gè)人開發(fā)所以在開發(fā)中沒有人經(jīng)常會(huì)因?yàn)椴煌臉I(yè)務(wù)請(qǐng)求去寫不同的代碼,然后就會(huì)造成各種風(fēng)格的HttpClient的跨域請(qǐng)求,最重要的是由于每個(gè)人對(duì)HttpClient的理解程度不同所以寫出來的代碼可能質(zhì)量上會(huì)有參差不齊,即使代碼能夠達(dá)到要求往往也顯得非常臃腫,重復(fù)高我們?cè)谡浇榻BRefit這個(gè)項(xiàng)目之前,我們來看看我們?cè)陧?xiàng)目中常用的調(diào)用方式,后面再來介紹這種處理方式的弊端以及后面集成了Refit以后我們代碼的質(zhì)量能夠有哪些程度的提高。
一 常規(guī)創(chuàng)建方式
在常規(guī)的方式中我們一般使用IHttpClientFactory來創(chuàng)建HttpClient對(duì)象,然后使用這個(gè)對(duì)象來發(fā)送和接收消息,至于為什么要使用這個(gè)接口來創(chuàng)建HttpClient對(duì)象而不是使用using new HttpClient的原因請(qǐng)點(diǎn)擊這里了解更多的信息,我們先來看下面的這個(gè)例子。
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,或者接口服務(wù)的地址為空");
}
}
/// <summary>
/// Post請(qǐng)求返回實(shí)體
/// </summary>
/// <param name="relativeUrl">請(qǐng)求相對(duì)路徑</param>
/// <param name="postObj">請(qǐng)求數(shù)據(jù)</param>
/// <returns>實(shí)體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("找不到對(duì)應(yīng)的DcsParts服務(wù)");
}
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($"請(qǐng)求返回結(jié)果:{0} 請(qǐng)求內(nèi)容:{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("找不到對(duì)應(yīng)的DcsParts服務(wù)");
}
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對(duì)象
在這個(gè)過程中我們通過構(gòu)造函數(shù)來注入IHttpClientFactory接口,然后用這個(gè)接口的CreateClient方法來創(chuàng)建一個(gè)唯一的HttpClient對(duì)象,在這里我們一般都會(huì)同步注入ILogger接口來記錄日志信息從而便于我們排查線上問題,這里我們?cè)贑reateClient方法中傳入了一個(gè)字符串類型的參數(shù)用于標(biāo)記自己創(chuàng)建的HttpClient對(duì)象的唯一性。這里我們可以看到在構(gòu)造函數(shù)中我們會(huì)去判斷當(dāng)前創(chuàng)建的HttpClient的BaseAddress,如果沒有這個(gè)基地址那么程序會(huì)直接拋出錯(cuò)誤提示,那么問題來了我們的HttpClient的BaseAddress到底在哪里配置呢?熟悉Asp.Net Core機(jī)制的朋友肯定一下子就會(huì)想到在Startup類中配置,那么我們來看看需要怎么配置。
1.2 配置HttpClient的BaseAddress
public IServiceProvider ConfigureServices(IServiceCollection services) {
//dcs.part服務(wù)
services.AddHttpClient(PartsConsts.DcsPartClientName, config => {
config.BaseAddress = new Uri(_appConfiguration["DependencyServices:DcsParts"]);
config.Timeout = TimeSpan.FromSeconds(60);
});
}
這里我只是簡(jiǎn)要截取了一小段內(nèi)容,這里我們看到AddHttpClient的第一個(gè)參數(shù)也是一個(gè)字符串常量,這個(gè)常量應(yīng)該是和IHttpClientFactory的CreateClient的方法中的那個(gè)常量保持絕對(duì)的一致,只有這樣我們才能夠標(biāo)識(shí)唯一的標(biāo)識(shí)一個(gè)HttpClient對(duì)象,創(chuàng)建完了之后我們就能夠在這個(gè)里面去配置這個(gè)HttpClient的各種參數(shù)了,另外在上面的這段代碼中_appConfiguration這個(gè)對(duì)象是通過Startup的構(gòu)造函數(shù)注入的,具體的代碼請(qǐng)參考下面。
public Startup(IHostingEnvironment env) {
_appConfiguration = env.GetAppConfiguration();
Clock.Provider = ClockProviders.Local;
Environment = env;
Console.OutputEncoding = System.Text.Encoding.UTF8;
}
另外我們還需要配置一些HttpClient所必須的屬性包括基地址、超時(shí)時(shí)間......等等,當(dāng)然這個(gè)基地址我們是配置在appsetting.json中的,具體的配置如下所示。
"DependencyServices": {
"BlobStorage": "http://blob-storage/",
"DcsParts": "http://dcs-parts/",
"DmsAfterSales": "http://dms-after-sales/"
}
有了這些我們就能夠具備創(chuàng)建一個(gè)HttpClient對(duì)象的條件了,后面我們來看看我們?cè)趺词褂眠@個(gè)HttpClient進(jìn)行發(fā)送和接收數(shù)據(jù)。
1.3 HttpClient進(jìn)行數(shù)據(jù)的發(fā)送和接收
/// <summary>
/// Post請(qǐng)求返回實(shí)體
/// </summary>
/// <param name="relativeUrl">請(qǐng)求相對(duì)路徑</param>
/// <param name="postObj">請(qǐng)求數(shù)據(jù)</param>
/// <returns>實(shí)體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("找不到對(duì)應(yīng)的DcsParts服務(wù)");
}
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($"請(qǐng)求返回結(jié)果:{0} 請(qǐng)求內(nèi)容:{1}", response.StatusCode, postData);
}
return await Task.FromResult(result);
}
在上面的代碼中我們模擬了一個(gè)Post請(qǐng)求,請(qǐng)求完成以后我們?cè)偈褂肦eadAsAsync的方法來異步接收另外一個(gè)域中的數(shù)據(jù),然后我們根據(jù)返回的StatusCode來拋出不同的錯(cuò)誤提示,并記錄相關(guān)的日志信息并返回最終Post請(qǐng)求的結(jié)果,進(jìn)而完成整個(gè)過程,在這個(gè)中間我們發(fā)送請(qǐng)求的時(shí)候需要注意一下內(nèi)容:1 最終的完整版地址=BaseAddress+RelativeAddress,基地址是在appsetting.json中進(jìn)行配置的,RelativeAddress是我們請(qǐng)求不同域的時(shí)候的相對(duì)地址,這個(gè)需要我們根據(jù)實(shí)際的業(yè)務(wù)來進(jìn)行配置。2 請(qǐng)求的對(duì)象是我們將數(shù)據(jù)對(duì)象序列化成json后的結(jié)果,這兩點(diǎn)需要特別注意。
1.4 總結(jié)
通過上面的講述我們知道了如何完整的創(chuàng)建HttpClient以及通過創(chuàng)建的HttpClient如何收發(fā)數(shù)據(jù),但同時(shí)我們也發(fā)現(xiàn)了通過上面的方式我們的缺點(diǎn):如果一個(gè)業(yè)務(wù)中有大量的這種跨域請(qǐng)求整個(gè)代碼顯得非常臃腫并且由于不同開發(fā)人員的認(rèn)知不同最終導(dǎo)致很容易出問題,那么我們是否有辦法能夠去解決上面的問題呢?Refit庫(kù)的出現(xiàn)正好解決了這個(gè)問題,Refit通過這種申明式的方式能夠很大程度上讓代碼更加簡(jiǎn)練明了而且提供了更加豐富的功能。
二 使用Refit來創(chuàng)建HttpClient對(duì)象
2.1 引入Refit包
在我們的項(xiàng)目中我們可以通過 <PackageReference Include="Refit" Version="XXX" />來快速引用Refit包,引用的方式這里便不再贅述。
2.2 定義接口
我們將我們業(yè)務(wù)中涉及到的方法定義在一個(gè)接口中,就像下面這樣。
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($"業(yè)務(wù)校驗(yàn)失敗,{receivedBody.Message} ({From})", exception);
} else {
_logger.LogWarning(exception, "Call Dms After Sales API failed");
return new ApplicationException($"內(nèi)部調(diào)用失敗,{exception.Message} ({exception.StatusCode}) ({From})", exception);
}
}
private Exception WrapException(HttpRequestException exception) {
_logger.LogWarning(exception, "Call Dms After Sales API failed");
return new ApplicationException($"內(nèi)部調(diào)用失敗,{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);
}
}
}
在上面接口中定義好這個(gè)方法以后我們就可以直接在我們的領(lǐng)域類中引入這個(gè)接口IDmsAfterSalesApi ,然后就直接使用這個(gè)接口中的方法,講到這里便有疑問,這個(gè)接口的實(shí)現(xiàn)到底在哪里?這里當(dāng)我們定義好接口然后點(diǎn)擊里面的方法轉(zhuǎn)到實(shí)現(xiàn)的時(shí)候我們發(fā)現(xiàn)里面會(huì)轉(zhuǎn)到一個(gè)叫做RefitStubs.g.cs的類中,然后自動(dòng)的生成下面的方法。
/// <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);
}
}
這里面的核心是調(diào)用一個(gè)BuildRestResultFuncForMethod的方法,后面我們?cè)賮矸治鲞@里面到底是怎么實(shí)現(xiàn)的,這里我們首先把這整個(gè)使用流程說完,之前我們說過Refit的很多配置都是通過標(biāo)簽的方式來注入進(jìn)去的,這里包括請(qǐng)求類型、相對(duì)請(qǐng)求地址,那么我們的默認(rèn)超時(shí)時(shí)間和BaseAddress到底是怎樣來配置的呢?下面我們就來重點(diǎn)講述。
2.4 在Startup中配置基礎(chǔ)配置信息
public IServiceProvider ConfigureServices(IServiceCollection services) {
//refit dms after sales服務(wù)
services.AddRefitClient<IDmsAfterSalesApi>()
.ConfigureHttpClient(c => {
c.BaseAddress = new Uri(_appConfiguration["DependencyServices:DmsAfterSales"]);
c.Timeout = TimeSpan.FromMilliseconds(_appConfiguration.GetValue<int>("AppSettings:ServiceTimeOutMs"));
});
}
這里我們看到通過一個(gè)AddRefitClient方法我們就能夠去配置我們的基礎(chǔ)信息,講到這里我們是不是對(duì)整個(gè)過程都有一個(gè)清楚的認(rèn)識(shí)呢?通過上下兩種方式的對(duì)比,相信你對(duì)整個(gè)Refit的使用都有自己的理解。
2.5 注意事項(xiàng)
由于我們的Headers經(jīng)常需要我們?nèi)ヅ渲靡唤M數(shù)據(jù),那么我們應(yīng)該怎么配置多個(gè)項(xiàng)呢?
[Headers("User-Agent: Dms-Parts", "Content-Type: application/json")]
通過上面的方式我們能夠配置一組Headers,另外在很多的時(shí)候如果Headers里面沒有配置Content-Type那么很有可能會(huì)返回StatusCode=415 Unsupport Media Type這個(gè)類型的錯(cuò)誤信息,這個(gè)在使用的時(shí)候需要注意。
以上就是如何在Asp.Net Core中集成Refit的詳細(xì)內(nèi)容,更多關(guān)于Asp.Net Core中集成Refit的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
ASP.NET實(shí)現(xiàn)進(jìn)度條效果
這篇文章主要為大家詳細(xì)介紹了ASP.NET實(shí)現(xiàn)簡(jiǎn)單的進(jìn)度條效果,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-06-06
使用.NET?6開發(fā)TodoList應(yīng)用之引入數(shù)據(jù)存儲(chǔ)的思路詳解
在這篇文章中,我們僅討論如何實(shí)現(xiàn)數(shù)據(jù)存儲(chǔ)基礎(chǔ)設(shè)施的引入,具體的實(shí)體定義和操作后面專門來說。對(duì).NET?6開發(fā)TodoList引入數(shù)據(jù)存儲(chǔ)相關(guān)知識(shí)感興趣的朋友一起看看吧2021-12-12
ASP與ASP.NET互通COOKIES的一點(diǎn)經(jīng)驗(yàn)
ASP與ASP.NET互通COOKIES的一點(diǎn)經(jīng)驗(yàn)...2006-09-09
WPF中button按鈕同時(shí)點(diǎn)擊多次觸發(fā)click解決方法
這篇文章主要為大家詳細(xì)介紹了WPF中button按鈕同時(shí)點(diǎn)擊多次觸發(fā)click的解決方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-04-04
.NET?6實(shí)現(xiàn)滑動(dòng)驗(yàn)證碼的示例詳解
這篇文章主要為大家詳細(xì)介紹了如何利用.NET?6實(shí)現(xiàn)滑動(dòng)驗(yàn)證碼,文中的示例代碼講解詳細(xì),具有一定的借鑒價(jià)值,感興趣的小伙伴可以了解一下2022-11-11
ASP.NET MVC5網(wǎng)站開發(fā)我的咨詢列表及添加咨詢(十二)
這篇文章主要為大家詳細(xì)介紹了ASP.NET MVC5網(wǎng)站開發(fā)我的咨詢列表及添加咨詢,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2015-09-09
asp.net System.Net.Mail 發(fā)送郵件
一個(gè)師弟發(fā)了段代碼給我,說調(diào)試了很久發(fā)送郵件都沒有成功。自己使用過程中,也發(fā)現(xiàn)了很多問題,但最簡(jiǎn)單的問題是“發(fā)件方”地址根本不支持smtp發(fā)送郵件。2009-04-04
HttpWebRequest和HttpWebResponse用法小結(jié)
在每個(gè)系統(tǒng)出寫入報(bào)告錯(cuò)誤代碼(找個(gè)合理的理由,比如系統(tǒng)免費(fèi)升級(jí)) -> 自家服務(wù)器接收并處理錯(cuò)誤報(bào)告 -> 反饋用戶(解決掉BUG就行,不要太聲揚(yáng))2011-09-09

