diff --git a/CHANGELOG.md b/CHANGELOG.md
index ab0f9f47..e036706a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -168,3 +168,12 @@ Fixed handling of No Content responses
### Added
- Added basic ability to modify the HttpClient in a request
+
+## 2.3.0
+
+### Added
+
+- Added ability to scope request modifiers by using the `.WithRequestModifier()` method instead of the `.ModifyRequest` property.
+ This will allow consumers to modify a single request without affecting any other consumers of the client. It also allows for
+ multiple modifiers to be added. For example, a modifier could be added at the global level that applies to all requests and then
+ another modifier can be added for a single request.
diff --git a/ShipEngineSDK.Test/Helpers/MockShipEngineFixture.cs b/ShipEngineSDK.Test/Helpers/MockShipEngineFixture.cs
index 158a00b7..4de07ebd 100644
--- a/ShipEngineSDK.Test/Helpers/MockShipEngineFixture.cs
+++ b/ShipEngineSDK.Test/Helpers/MockShipEngineFixture.cs
@@ -75,11 +75,13 @@ public void AssertRequest(HttpMethod method, string path, int numberOfCalls = 1)
/// The HTTP path.
/// The status code to return.
/// The response body to return.
- public string StubRequest(HttpMethod method, string path, HttpStatusCode status, string response)
+ public string StubRequest(HttpMethod method, string path, HttpStatusCode status = HttpStatusCode.OK, string response = null)
{
var requestId = Guid.NewGuid().ToString();
- var responseMessage = new HttpResponseMessage(status);
- responseMessage.Content = new StringContent(response ?? "");
+ var responseMessage = new HttpResponseMessage(status)
+ {
+ Content = new StringContent(response ?? "")
+ };
responseMessage.Headers.Add("x-shipengine-requestid", requestId);
responseMessage.Headers.Add("request-id", requestId);
diff --git a/ShipEngineSDK.Test/ShipEngineClientTests.cs b/ShipEngineSDK.Test/ShipEngineClientTests.cs
index 0024f1f7..601988db 100644
--- a/ShipEngineSDK.Test/ShipEngineClientTests.cs
+++ b/ShipEngineSDK.Test/ShipEngineClientTests.cs
@@ -1,10 +1,13 @@
namespace ShipEngineTest
{
+ using Moq;
+ using Moq.Protected;
using ShipEngineSDK;
using ShipEngineSDK.VoidLabelWithLabelId;
using System;
using System.Linq;
using System.Net.Http;
+ using System.Threading;
using System.Threading.Tasks;
using Xunit;
@@ -173,7 +176,7 @@ public async Task SuccessResponseWithNullStringContentThrowsShipEngineExceptionW
var mockShipEngineFixture = new MockShipEngineFixture(config);
var shipengine = mockShipEngineFixture.ShipEngine;
- // this scenario is similar to unparseable JSON - except that it is valid JSON
+ // this scenario is similar to unparsable JSON - except that it is valid JSON
var responseBody = @"null";
var requestId = mockShipEngineFixture.StubRequest(HttpMethod.Post, "/v1/something", System.Net.HttpStatusCode.OK,
responseBody);
@@ -197,7 +200,7 @@ public async Task SuccessResponseWhenStringRequestedReturnsUnparsedString()
var mockShipEngineFixture = new MockShipEngineFixture(config);
var shipengine = mockShipEngineFixture.ShipEngine;
- // this scenario is similar to unparseable JSON - except that it is valid JSON
+ // this scenario is similar to unparsable JSON - except that it is valid JSON
var responseBody = @"The Response";
mockShipEngineFixture.StubRequest(HttpMethod.Delete, "/v1/something", System.Net.HttpStatusCode.OK,
responseBody);
@@ -214,14 +217,147 @@ public async Task SuccessResponseWithNoContentCanBeReturnedIfStringRequested()
var mockShipEngineFixture = new MockShipEngineFixture(config);
var shipengine = mockShipEngineFixture.ShipEngine;
- // this scenario is similar to unparseable JSON - except that it is valid JSON
- string responseBody = null;
- mockShipEngineFixture.StubRequest(HttpMethod.Delete, "/v1/something", System.Net.HttpStatusCode.OK,
- responseBody);
+ // this scenario is similar to unparsable JSON - except that it is valid JSON
+ mockShipEngineFixture.StubRequest(HttpMethod.Delete, "/v1/something", System.Net.HttpStatusCode.OK, null);
var result = await shipengine.SendHttpRequestAsync(HttpMethod.Delete, "/v1/something", "",
mockShipEngineFixture.HttpClient, config);
- Assert.Null(responseBody);
+ Assert.Empty(result);
+ }
+
+ [Fact]
+ public void WithRequestModifierDoesNotCreateNewHttpClient()
+ {
+ var config = new Config(apiKey: "test", timeout: TimeSpan.FromSeconds(0.5));
+ var mockShipEngineFixture = new MockShipEngineFixture(config);
+ var httpClient = mockShipEngineFixture.HttpClient;
+
+ var originalShipEngine = mockShipEngineFixture.ShipEngine;
+
+ var newShipEngine = originalShipEngine.WithRequestModifier(x => x.Headers.Add("X-Test-Header", "Test"));
+
+ Assert.Same(config, newShipEngine._config);
+ Assert.Same(httpClient, newShipEngine._client);
+ Assert.NotSame(originalShipEngine, newShipEngine);
+ }
+
+ [Fact]
+ public void ModifyRequestDoesNotCreateNewHttpClientNorShipEngineInstance()
+ {
+ var config = new Config(apiKey: "test", timeout: TimeSpan.FromSeconds(0.5));
+ var mockShipEngineFixture = new MockShipEngineFixture(config);
+ var httpClient = mockShipEngineFixture.HttpClient;
+
+ var originalShipEngine = mockShipEngineFixture.ShipEngine;
+
+ var newShipEngine = originalShipEngine.ModifyRequest(x => x.Headers.Add("X-Test-Header", "Test"));
+
+ Assert.Same(config, newShipEngine._config);
+ Assert.Same(httpClient, newShipEngine._client);
+ Assert.Same(originalShipEngine, newShipEngine);
+ }
+
+ [Fact]
+ public async Task WithSingleRequestModifierAppliesBeforeRequest()
+ {
+ var config = new Config(apiKey: "test", timeout: TimeSpan.FromSeconds(0.5));
+ var mockShipEngineFixture = new MockShipEngineFixture(config);
+ var shipengine = mockShipEngineFixture.ShipEngine.WithRequestModifier(x => x.Headers.Add("X-Test-Header", "Test"));
+ mockShipEngineFixture.StubRequest(HttpMethod.Get, "/foo");
+
+ await shipengine.SendHttpRequestAsync(HttpMethod.Get, "/foo", "", mockShipEngineFixture.HttpClient, config);
+
+ mockShipEngineFixture.MockHandler.Protected()
+ .Verify(
+ "SendAsync",
+ Times.Once(),
+ ItExpr.Is(m => m.Headers.Any(x => x.Key == "X-Test-Header")),
+ ItExpr.IsAny());
+ }
+
+ [Fact]
+ public async Task WithRequestModifierDoesNotAffectOriginalClient()
+ {
+ var config = new Config(apiKey: "test", timeout: TimeSpan.FromSeconds(0.5));
+ var mockShipEngineFixture = new MockShipEngineFixture(config);
+ var shipengine = mockShipEngineFixture.ShipEngine;
+ var modifiedShipEngine = shipengine.WithRequestModifier(x => x.Headers.Add("X-Test-Header", "Test"));
+ mockShipEngineFixture.StubRequest(HttpMethod.Get, "/foo");
+
+ await shipengine.SendHttpRequestAsync(HttpMethod.Get, "/foo", "", mockShipEngineFixture.HttpClient, config);
+
+ mockShipEngineFixture.MockHandler.Protected()
+ .Verify(
+ "SendAsync",
+ Times.Once(),
+ ItExpr.Is(m => !m.Headers.Any(x => x.Key == "X-Test-Header")),
+ ItExpr.IsAny());
+ }
+
+ [Fact]
+ public async Task WithTwoRequestModifierAppliesBeforeRequest()
+ {
+ var config = new Config(apiKey: "test", timeout: TimeSpan.FromSeconds(0.5));
+ var mockShipEngineFixture = new MockShipEngineFixture(config);
+ var shipengine = mockShipEngineFixture.ShipEngine
+ .WithRequestModifier(x =>
+ {
+ x.Headers.Add("X-Test-Header", "Test 1");
+ x.Headers.Add("X-Second-Header", "Test 2");
+ })
+ .WithRequestModifier(x => x.Headers.Remove("X-Test-Header"));
+ mockShipEngineFixture.StubRequest(HttpMethod.Get, "/foo");
+
+ await shipengine.SendHttpRequestAsync(HttpMethod.Get, "/foo", "", mockShipEngineFixture.HttpClient, config);
+
+ mockShipEngineFixture.MockHandler.Protected()
+ .Verify(
+ "SendAsync",
+ Times.Once(),
+ ItExpr.Is(m =>
+ !m.Headers.Any(x => x.Key == "X-Test-Header") &&
+ m.Headers.Any(x => x.Key == "X-Second-Header")),
+ ItExpr.IsAny());
+ }
+
+ [Fact]
+ public async Task ModifyRequestAppliesBeforeRequest()
+ {
+ var config = new Config(apiKey: "test", timeout: TimeSpan.FromSeconds(0.5));
+ var mockShipEngineFixture = new MockShipEngineFixture(config);
+ var shipengine = mockShipEngineFixture.ShipEngine.ModifyRequest(x => x.Headers.Add("X-Test-Header", "Test"));
+ mockShipEngineFixture.StubRequest(HttpMethod.Get, "/foo");
+
+ await shipengine.SendHttpRequestAsync(HttpMethod.Get, "/foo", "", mockShipEngineFixture.HttpClient, config);
+
+ mockShipEngineFixture.MockHandler.Protected()
+ .Verify(
+ "SendAsync",
+ Times.Once(),
+ ItExpr.Is(m => m.Headers.Any(x => x.Key == "X-Test-Header")),
+ ItExpr.IsAny());
+ }
+
+ [Fact]
+ public async Task ModifyRequestReplacesExistingModifiersAppliesBeforeRequest()
+ {
+ var config = new Config(apiKey: "test", timeout: TimeSpan.FromSeconds(0.5));
+ var mockShipEngineFixture = new MockShipEngineFixture(config);
+ var shipengine = mockShipEngineFixture.ShipEngine
+ .WithRequestModifier(x => x.Headers.Add("X-Test-Header", "Test 1"))
+ .ModifyRequest(x => x.Headers.Add("X-Second-Header", "Test 2"));
+ mockShipEngineFixture.StubRequest(HttpMethod.Get, "/foo");
+
+ await shipengine.SendHttpRequestAsync(HttpMethod.Get, "/foo", "", mockShipEngineFixture.HttpClient, config);
+
+ mockShipEngineFixture.MockHandler.Protected()
+ .Verify(
+ "SendAsync",
+ Times.Once(),
+ ItExpr.Is(m =>
+ m.Headers.Any(x => x.Key == "X-Second-Header") &&
+ !m.Headers.Any(x => x.Key == "X-Test-Header")),
+ ItExpr.IsAny());
}
}
}
\ No newline at end of file
diff --git a/ShipEngineSDK/ShipEngine.cs b/ShipEngineSDK/ShipEngine.cs
index 13dc1e37..a5ef41e3 100644
--- a/ShipEngineSDK/ShipEngine.cs
+++ b/ShipEngineSDK/ShipEngine.cs
@@ -1,14 +1,13 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using ShipEngineSDK.Common;
-using ShipEngineSDK.Manifests;
using System;
using System.Collections.Generic;
+using System.Linq;
using System.Net.Http;
using System.Runtime.CompilerServices;
using System.Text.Json;
using System.Threading.Tasks;
-using Result = ShipEngineSDK.ValidateAddresses.Result;
[assembly: InternalsVisibleTo("ShipEngineSDK.Test")]
@@ -225,14 +224,40 @@ public ShipEngine(HttpClient httpClient) : base()
_client = httpClient;
}
+ ///
+ /// Copy constructor that adds a request modifier to the existing collection
+ ///
+ /// Client to use for requests
+ /// Config to use for the requests
+ /// List of request modifiers to use
+ private ShipEngine(HttpClient client, Config config, IEnumerable> requestModifiers) :
+ base(requestModifiers)
+ {
+ _client = client;
+ _config = config;
+ }
+
+ ///
+ /// Gets a new instance of the ShipEngine client with the provided request modifier added to the collection
+ ///
+ /// Request modifier that will be added
+ /// A new instance of the ShipEngine client
+ /// The existing ShipEngine client is not modified
+ public ShipEngine WithRequestModifier(Action modifier) =>
+ new(_client, _config, requestModifiers.Append(modifier));
+
///
/// Modifies the request before it is sent to the ShipEngine API
///
- ///
- ///
+ /// Request modifier that will be used
+ /// The current instance of the ShipEngine client
+ ///
+ /// This method modifies the existing ShipEngine client and will replace any existing request modifiers with the one provided.
+ /// If you want to add a request modifier to the existing collection, use the WithRequestModifier method.
+ ///
public ShipEngine ModifyRequest(Action modifyRequest)
{
- base.ModifyRequest = modifyRequest;
+ requestModifiers = [modifyRequest];
return this;
}
diff --git a/ShipEngineSDK/ShipEngineClient.cs b/ShipEngineSDK/ShipEngineClient.cs
index 62163ccb..5ab37618 100644
--- a/ShipEngineSDK/ShipEngineClient.cs
+++ b/ShipEngineSDK/ShipEngineClient.cs
@@ -1,5 +1,6 @@
using ShipEngineSDK.Common;
using System;
+using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
@@ -19,6 +20,20 @@ namespace ShipEngineSDK
///
public class ShipEngineClient
{
+ ///
+ /// Default constructor
+ ///
+ public ShipEngineClient() { }
+
+ ///
+ /// Constructor that takes a collection of request modifiers to apply to the request before it is sent
+ ///
+ /// Collection of modifiers to be used for each request
+ protected ShipEngineClient(IEnumerable> requestModifiers)
+ {
+ this.requestModifiers = requestModifiers;
+ }
+
///
/// Options for serializing the method call params to JSON.
/// A separate inline setting is used for deserializing the response
@@ -46,9 +61,13 @@ public class ShipEngineClient
public CancellationToken CancellationToken { get; set; }
///
- /// Modifies the client request before it is sent
+ /// Collections of request modifiers to apply to the request before it is sent
///
- public Action? ModifyRequest { get; set; }
+ ///
+ /// This is a collection instead of a single action so that modifiers can be added at multiple levels.
+ /// For example, a consumer could add a modifier at the client level, and then add another at the method level.
+ ///
+ protected IEnumerable> requestModifiers = [];
///
/// Sets the HttpClient User agent, the json media type, and the API key to be used
@@ -209,7 +228,10 @@ public virtual async Task SendHttpRequestAsync(HttpMethod method, string p
try
{
var request = BuildRequest(method, path, jsonContent);
- ModifyRequest?.Invoke(request);
+ foreach (var modifier in requestModifiers ?? [])
+ {
+ modifier?.Invoke(request);
+ }
response = await client.SendAsync(request, cancellationToken);
var deserializedResult = await DeserializedResultOrThrow(response);
diff --git a/ShipEngineSDK/ShipEngineSDK.csproj b/ShipEngineSDK/ShipEngineSDK.csproj
index 81b539b9..b43c5f0c 100644
--- a/ShipEngineSDK/ShipEngineSDK.csproj
+++ b/ShipEngineSDK/ShipEngineSDK.csproj
@@ -4,7 +4,7 @@
ShipEngine
sdk;rest;api;shipping;rates;label;tracking;cost;address;validation;normalization;fedex;ups;usps;
- 2.2.1
+ 2.3.0
ShipEngine
ShipEngine
The official ShipEngine C# SDK for .NET
diff --git a/package.json b/package.json
index 2239a9bb..94ef989e 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "shipengine-dotnet",
- "version": "2.2.1",
+ "version": "2.3.0",
"description": "Package primarily used to generate the API and models from OpenApi spec\"",
"main": "index.js",
"directories": {