在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這個類型的錯誤信息,這個在使用的時候需要注意。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM