1. 程式人生 > 程式設計 >如何在Asp.Net Core中整合Refit

如何在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.
js
on; 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 pr
程式設計客棧
operties) { 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,"Dcs-Parts");
_httpClient.CancelPendingRequests();
_httpClient.DefaultRequestHeaders.Clear();
www.cppcns.comHttpContent 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) {
 http://www.cppcns.com;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}",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");
&nbswww.cppcns.comp;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) });
 www.cppcns.com;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的資料請關注我們其它相關文章!