From e3b660fbf2d0bb75c2171945c203ebbe90798c92 Mon Sep 17 00:00:00 2001 From: Joe Pill Date: Thu, 27 Jun 2024 09:08:15 -0500 Subject: [PATCH] feat: add method to initialize SDK with an httpClient --- ShipEngine/PublicAPI.Shipped.txt | 9 ++++- ShipEngine/ShipEngine.cs | 58 +++++++++++++++++++++++++++----- ShipEngine/ShipEngine.csproj | 5 ++- ShipEngine/ShipEngineClient.cs | 53 ++++++++++++++++++----------- 4 files changed, 95 insertions(+), 30 deletions(-) diff --git a/ShipEngine/PublicAPI.Shipped.txt b/ShipEngine/PublicAPI.Shipped.txt index 76430014..a6e070f2 100644 --- a/ShipEngine/PublicAPI.Shipped.txt +++ b/ShipEngine/PublicAPI.Shipped.txt @@ -4,7 +4,6 @@ override ShipEngineSDK.Common.MonetaryValueConverter.Write(System.Text.Json.Utf8 readonly ShipEngineSDK.Config.ApiKey -> string! readonly ShipEngineSDK.Config.Retries -> int readonly ShipEngineSDK.Config.Timeout -> System.TimeSpan -readonly ShipEngineSDK.ShipEngineClient.JsonSerializerOptions -> System.Text.Json.JsonSerializerOptions! ShipEngineSDK.Common.Address ShipEngineSDK.Common.Address.Address() -> void ShipEngineSDK.Common.Address.AddressLine1.get -> string? @@ -1171,12 +1170,14 @@ ShipEngineSDK.ShipEngine.CreateLabelFromShipmentDetails(ShipEngineSDK.CreateLabe ShipEngineSDK.ShipEngine.CreateLabelFromShipmentDetails(ShipEngineSDK.CreateLabelFromShipmentDetails.Params! labelParams, ShipEngineSDK.Config! methodConfig) -> System.Threading.Tasks.Task! ShipEngineSDK.ShipEngine.CreateManifest(ShipEngineSDK.Config! methodConfig, ShipEngineSDK.Manifests.Params! manifestParams) -> System.Threading.Tasks.Task! ShipEngineSDK.ShipEngine.CreateManifest(ShipEngineSDK.Manifests.Params! manifestParams) -> System.Threading.Tasks.Task! +ShipEngineSDK.ShipEngine.Dispose() -> void ShipEngineSDK.ShipEngine.GetRatesWithShipmentDetails(ShipEngineSDK.GetRatesWithShipmentDetails.Params! rateParams) -> System.Threading.Tasks.Task! ShipEngineSDK.ShipEngine.GetRatesWithShipmentDetails(ShipEngineSDK.GetRatesWithShipmentDetails.Params! rateParams, ShipEngineSDK.Config! methodConfig) -> System.Threading.Tasks.Task! ShipEngineSDK.ShipEngine.ListCarriers() -> System.Threading.Tasks.Task! ShipEngineSDK.ShipEngine.ListCarriers(ShipEngineSDK.Config! methodConfig) -> System.Threading.Tasks.Task! ShipEngineSDK.ShipEngine.ShipEngine(ShipEngineSDK.Config! config) -> void ShipEngineSDK.ShipEngine.ShipEngine(string! apiKey) -> void +ShipEngineSDK.ShipEngine.ShipEngine(System.Net.Http.HttpClient! httpClient) -> void ShipEngineSDK.ShipEngine.TrackUsingCarrierCodeAndTrackingNumber(string! trackingNumber, string! carrierCode) -> System.Threading.Tasks.Task! ShipEngineSDK.ShipEngine.TrackUsingCarrierCodeAndTrackingNumber(string! trackingNumber, string! carrierCode, ShipEngineSDK.Config! methodConfig) -> System.Threading.Tasks.Task! ShipEngineSDK.ShipEngine.TrackUsingLabelId(string! labelId) -> System.Threading.Tasks.Task! @@ -1189,6 +1190,8 @@ ShipEngineSDK.ShipEngine._client -> System.Net.Http.HttpClient! ShipEngineSDK.ShipEngine._config -> ShipEngineSDK.Config! ShipEngineSDK.ShipEngineClient ShipEngineSDK.ShipEngineClient.ShipEngineClient() -> void +ShipEngineSDK.ShipEngineClient.CancellationToken.get -> System.Threading.CancellationToken +ShipEngineSDK.ShipEngineClient.CancellationToken.set -> void ShipEngineSDK.ShipEngineException ShipEngineSDK.ShipEngineException.ErrorCode.get -> ShipEngineSDK.ErrorCode ShipEngineSDK.ShipEngineException.ErrorCode.set -> void @@ -1200,6 +1203,7 @@ ShipEngineSDK.ShipEngineException.RequestId.get -> string? ShipEngineSDK.ShipEngineException.ResponseMessage.get -> System.Net.Http.HttpResponseMessage? ShipEngineSDK.ShipEngineException.ResponseMessage.set -> void ShipEngineSDK.ShipEngineException.ShipEngineException(string! message, ShipEngineSDK.ErrorSource errorSource = ShipEngineSDK.ErrorSource.Shipengine, ShipEngineSDK.ErrorType errorType = ShipEngineSDK.ErrorType.System, ShipEngineSDK.ErrorCode errorCode = ShipEngineSDK.ErrorCode.Unspecified, System.Net.Http.HttpResponseMessage? responseMessage = null, string? requestID = null) -> void +ShipEngineSDK.ShipEngineExtensions ShipEngineSDK.ShipEngineMock ShipEngineSDK.ShipEngineMock.ShipEngineMock() -> void ShipEngineSDK.TrackUsingCarrierCodeAndTrackingNumber.Result @@ -1310,7 +1314,10 @@ ShipEngineSDK.VoidLabelWithLabelId.Result.Approved.set -> void ShipEngineSDK.VoidLabelWithLabelId.Result.Message.get -> string? ShipEngineSDK.VoidLabelWithLabelId.Result.Message.set -> void ShipEngineSDK.VoidLabelWithLabelId.Result.Result() -> void +static readonly ShipEngineSDK.ShipEngineClient.JsonSerializerOptions -> System.Text.Json.JsonSerializerOptions! static ShipEngineSDK.ShipEngineClient.ConfigureHttpClient(ShipEngineSDK.Config! config, System.Net.Http.HttpClient! client) -> System.Net.Http.HttpClient! +static ShipEngineSDK.ShipEngineClient.ConfigureHttpClient(System.Net.Http.HttpClient! client, string! apiKey, System.Uri? baseUri, System.TimeSpan? timeout = null) -> System.Net.Http.HttpClient! +static ShipEngineSDK.ShipEngineExtensions.AddShipEngine(this Microsoft.Extensions.Hosting.IHostApplicationBuilder! builder, System.Action? configureClient = null) -> Microsoft.Extensions.Hosting.IHostApplicationBuilder! virtual ShipEngineSDK.ShipEngineClient.SendHttpRequestAsync(System.Net.Http.HttpMethod! method, string! path, string? jsonContent, System.Net.Http.HttpClient! client, ShipEngineSDK.Config! config) -> System.Threading.Tasks.Task! virtual ShipEngineSDK.ShipEngineMock.CreateLabelFromRate(ShipEngineSDK.CreateLabelFromRate.Params! createLabelFromRateParams) -> System.Threading.Tasks.Task! virtual ShipEngineSDK.ShipEngineMock.CreateLabelFromRate(ShipEngineSDK.CreateLabelFromRate.Params! createLabelFromRateParams, ShipEngineSDK.Config! methodConfig) -> System.Threading.Tasks.Task! diff --git a/ShipEngine/ShipEngine.cs b/ShipEngine/ShipEngine.cs index bb7fad52..c6da51cb 100644 --- a/ShipEngine/ShipEngine.cs +++ b/ShipEngine/ShipEngine.cs @@ -1,10 +1,12 @@ -using ShipEngineSDK.Common; -using ShipEngineSDK.Manifests; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using Result = ShipEngineSDK.ValidateAddresses.Result; +using ShipEngineSDK.Common; +using ShipEngineSDK.Manifests; +using System; using System.Collections.Generic; using System.Net.Http; using System.Text.Json; -using System.Text.Json.Serialization; using System.Threading.Tasks; namespace ShipEngineSDK @@ -345,10 +347,35 @@ public virtual Task> ValidateAddresses(List
addresses, Con } } + /// + /// Extension method to allow customized client configuration + /// + public static class ShipEngineExtensions + { + /// + /// Adds ShipEngine to the host builder and configures the client. + /// + /// + /// + /// + public static IHostApplicationBuilder AddShipEngine(this IHostApplicationBuilder builder, Action? configureClient = null) + { + builder.Services.AddHttpClient(c => + { + var baseUri = builder.Configuration["ShipEngine:BaseUrl"] ?? "https://api.shipengine.com"; + var apiKey = builder.Configuration["ShipEngine:ApiKey"] ?? ""; + ShipEngineClient.ConfigureHttpClient(c, apiKey, new Uri(baseUri)); + configureClient?.Invoke(c); + }); + + return builder; + } + } + /// /// Contains methods for interacting with the ShipEngine API. /// - public class ShipEngine : ShipEngineClient, IShipEngine + public class ShipEngine : ShipEngineClient, IDisposable, IShipEngine { /// /// Global HttpClient for ShipEngine instance. @@ -366,9 +393,8 @@ public class ShipEngine : ShipEngineClient, IShipEngine /// Api Key associated with the ShipEngine account you want to use public ShipEngine(string apiKey) : base() { - var client = new HttpClient(); _config = new Config(apiKey); - _client = ConfigureHttpClient(_config, client); + _client = ConfigureHttpClient(_config, new HttpClient()); } /// @@ -377,9 +403,25 @@ public ShipEngine(string apiKey) : base() /// Config object containing custom configurations public ShipEngine(Config config) : base() { - var client = new HttpClient(); this._config = config; - _client = ConfigureHttpClient(config, client); + _client = ConfigureHttpClient(config, new HttpClient()); + } + + /// + /// Initialize the ShipEngine SDK with an httpClient object + /// + /// HttpClient object to be used for ShipEngine API calls. We expect the httpClient has already been configured with ConfigureHttpClient + public ShipEngine(HttpClient httpClient) : base() + { + _client = httpClient; + } + + /// + /// Dispose of the ShipEngine client + /// + public void Dispose() + { + _client.Dispose(); } /// diff --git a/ShipEngine/ShipEngine.csproj b/ShipEngine/ShipEngine.csproj index ae9602f4..c1aaa0b7 100644 --- a/ShipEngine/ShipEngine.csproj +++ b/ShipEngine/ShipEngine.csproj @@ -28,9 +28,12 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + + + - + diff --git a/ShipEngine/ShipEngineClient.cs b/ShipEngine/ShipEngineClient.cs index 09a6c44f..68857e0d 100644 --- a/ShipEngine/ShipEngineClient.cs +++ b/ShipEngine/ShipEngineClient.cs @@ -6,6 +6,7 @@ using System.Net.Http.Headers; using System.Text.Json; using System.Text.Json.Serialization; +using System.Threading; using System.Threading.Tasks; namespace ShipEngineSDK @@ -21,25 +22,22 @@ public class ShipEngineClient /// Options for serializing the method call params to JSON. /// A separate inline setting is used for deserializing the response /// - protected readonly JsonSerializerOptions JsonSerializerOptions; - - /// - /// Constructor for ShipEngineClient - /// - public ShipEngineClient() + protected static readonly JsonSerializerOptions JsonSerializerOptions = new JsonSerializerOptions { - JsonSerializerOptions = new JsonSerializerOptions - { - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, - PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, - PropertyNameCaseInsensitive = true, - WriteIndented = true, - Converters = { new JsonStringEnumMemberConverter() } - }; - } + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, + PropertyNameCaseInsensitive = true, + WriteIndented = true, + Converters = { new JsonStringEnumMemberConverter() } + }; private const string JsonMediaType = "application/json"; + /// + /// Token to cancel the request + /// + public CancellationToken CancellationToken { get; set; } + /// /// Sets the HttpClient User agent, the json media type, and the API key to be used /// for all ShipEngine API calls unless overrwritten at the method level. @@ -71,9 +69,24 @@ public static HttpClient ConfigureHttpClient(Config config, HttpClient client) return client; } - private async Task DeserializedResultOrThrow(HttpResponseMessage response) + /// + /// Sets the HttpClient User agent, the json media type, and the API key to be used + /// for all ShipEngine API calls unless overwritten at the method level. + /// + /// The HttpClient to be configured + /// The API key to be used for all ShipEngine API calls + /// The base URI for the ShipEngine API + /// The timeout for the ShipEngine API Calls + /// + public static HttpClient ConfigureHttpClient(HttpClient client, string apiKey, Uri? baseUri, TimeSpan? timeout = null) { + var config = new Config(apiKey, timeout); + client.BaseAddress = baseUri ?? new Uri("https://api.shipengine.com"); + return ConfigureHttpClient(config, client); + } + private async Task DeserializedResultOrThrow(HttpResponseMessage response) + { var contentString = await response.Content.ReadAsStringAsync(); if (!response.IsSuccessStatusCode) @@ -135,7 +148,7 @@ public virtual async Task SendHttpRequestAsync(HttpMethod method, string p try { var request = BuildRequest(method, path, jsonContent); - var streamTask = client.SendAsync(request); + var streamTask = client.SendAsync(request, CancellationToken); response = await streamTask; var deserializedResult = await DeserializedResultOrThrow(response); @@ -202,10 +215,10 @@ private async Task WaitAndRetry(HttpResponseMessage? response, Config config, Sh ); } - await Task.Delay((int)retryAfter * 1000).ConfigureAwait(false); + await Task.Delay((int)retryAfter * 1000, CancellationToken).ConfigureAwait(false); } - private HttpRequestMessage BuildRequest(HttpMethod method, string path, string? jsonContent) + private static HttpRequestMessage BuildRequest(HttpMethod method, string path, string? jsonContent) { var request = new HttpRequestMessage(method, path); @@ -217,7 +230,7 @@ private HttpRequestMessage BuildRequest(HttpMethod method, string path, string? return request; } - private bool ShouldRetry( + private static bool ShouldRetry( int numRetries, HttpStatusCode? statusCode, HttpHeaders? headers,