diff --git a/Directory.Packages.props b/Directory.Packages.props
index a9732615..6dd2e2ac 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -15,6 +15,7 @@
+
diff --git a/src/AwsLambda.Host/AwsLambda.Host.csproj b/src/AwsLambda.Host/AwsLambda.Host.csproj
index f7656c92..a479e9c5 100644
--- a/src/AwsLambda.Host/AwsLambda.Host.csproj
+++ b/src/AwsLambda.Host/AwsLambda.Host.csproj
@@ -18,6 +18,7 @@
+
diff --git a/src/AwsLambda.Host/Builder/Extensions/LambdaHttpClientServiceCollectionExtensions.cs b/src/AwsLambda.Host/Builder/Extensions/LambdaHttpClientServiceCollectionExtensions.cs
new file mode 100644
index 00000000..20c65192
--- /dev/null
+++ b/src/AwsLambda.Host/Builder/Extensions/LambdaHttpClientServiceCollectionExtensions.cs
@@ -0,0 +1,107 @@
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+
+namespace AwsLambda.Host.Builder.Extensions;
+
+///
+/// Extension methods for configuring the Lambda bootstrap HTTP client in an
+/// .
+///
+public static class LambdaHttpClientServiceCollectionExtensions
+{
+ extension(IServiceCollection services)
+ {
+ /// Adds a custom HTTP client instance for the Lambda bootstrap runtime API.
+ /// The type of to register.
+ /// The pre-configured instance.
+ /// The for chaining.
+ ///
+ /// Registers a keyed singleton that will be used by the Lambda
+ /// bootstrap to communicate with the Lambda runtime API. This overload is useful for testing
+ /// scenarios where you want to inject a mock or fake HTTP client.
+ ///
+ public IServiceCollection AddLambdaBootstrapHttpClient(T client)
+ where T : HttpClient
+ {
+ ArgumentNullException.ThrowIfNull(services);
+
+ services.AddKeyedSingleton(typeof(ILambdaBootstrapOrchestrator), client);
+
+ return services;
+ }
+
+ ///
+ /// Adds a factory for creating a custom HTTP client for the Lambda bootstrap runtime API.
+ ///
+ ///
+ /// A factory function that creates the instance. The function
+ /// receives the and service key.
+ ///
+ /// The for chaining.
+ ///
+ ///
+ /// Registers a keyed singleton factory that will be used by the
+ /// Lambda bootstrap to communicate with the Lambda runtime API. This overload provides
+ /// access to the dependency injection container, allowing you to resolve other services
+ /// when constructing the HTTP client.
+ ///
+ ///
+ public IServiceCollection AddLambdaBootstrapHttpClient(
+ Func factory
+ )
+ {
+ ArgumentNullException.ThrowIfNull(services);
+
+ services.AddKeyedSingleton(typeof(ILambdaBootstrapOrchestrator), factory);
+
+ return services;
+ }
+
+ ///
+ /// Attempts to add a custom HTTP client instance for the Lambda bootstrap runtime API if one is
+ /// not already registered.
+ ///
+ /// The type of to register.
+ /// The pre-configured instance.
+ ///
+ /// Registers a keyed singleton that will be used by the Lambda
+ /// bootstrap to communicate with the Lambda runtime API, but only if a keyed HTTP client has
+ /// not already been registered. This overload is useful for testing scenarios where you want
+ /// to inject a mock or fake HTTP client without overwriting user-supplied configurations.
+ ///
+ public void TryAddLambdaBootstrapHttpClient(T client)
+ where T : HttpClient
+ {
+ ArgumentNullException.ThrowIfNull(services);
+
+ services.TryAddKeyedSingleton(typeof(ILambdaBootstrapOrchestrator), client);
+ }
+
+ ///
+ /// Attempts to add a factory for creating a custom HTTP client for the Lambda bootstrap runtime
+ /// API if one is not already registered.
+ ///
+ ///
+ /// A factory function that creates the instance. The function
+ /// receives the and service key.
+ ///
+ ///
+ ///
+ /// Registers a keyed singleton factory that will be used by the
+ /// Lambda bootstrap to communicate with the Lambda runtime API, but only if a keyed HTTP
+ /// client has not already been registered.
+ ///
+ ///
+ public void TryAddLambdaBootstrapHttpClient(
+ Func factory
+ )
+ {
+ ArgumentNullException.ThrowIfNull(services);
+
+ services.TryAddKeyedSingleton(
+ typeof(ILambdaBootstrapOrchestrator),
+ factory
+ );
+ }
+ }
+}
diff --git a/src/AwsLambda.Host/Core/Options/LambdaHostOptions.cs b/src/AwsLambda.Host/Core/Options/LambdaHostOptions.cs
index c6229f5b..953c05cc 100644
--- a/src/AwsLambda.Host/Core/Options/LambdaHostOptions.cs
+++ b/src/AwsLambda.Host/Core/Options/LambdaHostOptions.cs
@@ -1,4 +1,5 @@
using Amazon.Lambda.RuntimeSupport.Bootstrap;
+using AwsLambda.Host.Builder.Extensions;
namespace AwsLambda.Host.Options;
@@ -8,9 +9,17 @@ public class LambdaHostOptions
/// Gets or sets an optional custom HTTP client for the Lambda bootstrap.
///
/// When null, the bootstrap will create its own HTTP client for communicating with the Lambda
- /// runtime API. Provide a custom client to control connection pooling, timeout settings, or other
- /// HTTP-level behaviors.
+ /// runtime API. This property will be overridden by keyed service registrations from
+ /// or
+ ///
+ /// extension methods.
///
+ [Obsolete(
+ "This property will be removed in version 2.0.0. Use "
+ + "AddLambdaBootstrapHttpClient() or TryAddLambdaBootstrapHttpClient() extension methods "
+ + "instead.",
+ false
+ )]
public HttpClient? BootstrapHttpClient { get; set; } = null;
/// Gets or sets the options for configuring the Lambda bootstrap behavior.
diff --git a/src/AwsLambda.Host/Runtime/LambdaBootstrapAdapter.cs b/src/AwsLambda.Host/Runtime/LambdaBootstrapAdapter.cs
index c6f20536..6dc13276 100644
--- a/src/AwsLambda.Host/Runtime/LambdaBootstrapAdapter.cs
+++ b/src/AwsLambda.Host/Runtime/LambdaBootstrapAdapter.cs
@@ -1,5 +1,6 @@
using Amazon.Lambda.Core;
using Amazon.Lambda.RuntimeSupport;
+using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
namespace AwsLambda.Host.Runtime;
@@ -10,12 +11,23 @@ namespace AwsLambda.Host.Runtime;
///
internal sealed class LambdaBootstrapAdapter : ILambdaBootstrapOrchestrator
{
+ private readonly HttpClient? _httpClient;
private readonly LambdaHostOptions _settings;
- public LambdaBootstrapAdapter(IOptions lambdaHostSettings)
+ public LambdaBootstrapAdapter(
+ IOptions lambdaHostSettings,
+ [FromKeyedServices(typeof(ILambdaBootstrapOrchestrator))] HttpClient? httpClient = null
+ )
{
ArgumentNullException.ThrowIfNull(lambdaHostSettings);
+ // TODO: Remove this check once ILambdaBootstrapOrchestrator.BootstrapHttpClient is removed.
+ // until ILambdaBootstrapOrchestrator.BootstrapHttpClient is removed, we need to check for
+ // it if the keyed service is NOT present.
+#pragma warning disable CS0618 // Type or member is obsolete
+ _httpClient = httpClient ?? lambdaHostSettings.Value.BootstrapHttpClient;
+#pragma warning restore CS0618 // Type or member is obsolete
+
_settings = lambdaHostSettings.Value;
}
@@ -32,10 +44,10 @@ CancellationToken stoppingToken
using var wrappedHandler = HandlerWrapper.GetHandlerWrapper(handler);
// Create the bootstrap based on configuration.
- using var bootstrap = _settings.BootstrapHttpClient is null
+ using var bootstrap = _httpClient is null
? new LambdaBootstrap(wrappedHandler, _settings.BootstrapOptions, convertedInitializer)
: new LambdaBootstrap(
- _settings.BootstrapHttpClient,
+ _httpClient,
wrappedHandler,
_settings.BootstrapOptions,
convertedInitializer
diff --git a/tests/AwsLambda.Host.UnitTests/Builder/Extensions/LambdaHttpClientServiceCollectionExtensionsTests.cs b/tests/AwsLambda.Host.UnitTests/Builder/Extensions/LambdaHttpClientServiceCollectionExtensionsTests.cs
new file mode 100644
index 00000000..7eb29534
--- /dev/null
+++ b/tests/AwsLambda.Host.UnitTests/Builder/Extensions/LambdaHttpClientServiceCollectionExtensionsTests.cs
@@ -0,0 +1,237 @@
+using AwsLambda.Host.Builder.Extensions;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace AwsLambda.Host.UnitTests.Builder.Extensions;
+
+[TestSubject(typeof(LambdaHttpClientServiceCollectionExtensions))]
+public class LambdaHttpClientServiceCollectionExtensionsTests
+{
+ [Fact]
+ public void AddLambdaBootstrapHttpClient_WithNullServiceCollection_ThrowsArgumentNullException()
+ {
+ // Act
+ var act = () => ((IServiceCollection)null!).AddLambdaBootstrapHttpClient(new HttpClient());
+
+ // Assert
+ act.Should().ThrowExactly();
+ }
+
+ [Fact]
+ public void AddLambdaBootstrapHttpClient_WithNullClient_ThrowsArgumentNullException()
+ {
+ // Arrange
+ var serviceCollection = new ServiceCollection();
+
+ // Act
+ var act = () => serviceCollection.AddLambdaBootstrapHttpClient((HttpClient)null!);
+
+ // Assert
+ act.Should().ThrowExactly();
+ }
+
+ [Fact]
+ public void AddLambdaBootstrapHttpClient_WithValidClient_ReturnsServiceCollection()
+ {
+ // Arrange
+ var serviceCollection = new ServiceCollection();
+ var httpClient = new HttpClient();
+
+ // Act
+ var result = serviceCollection.AddLambdaBootstrapHttpClient(httpClient);
+
+ // Assert
+ result.Should().BeSameAs(serviceCollection);
+ }
+
+ [Fact]
+ public void AddLambdaBootstrapHttpClient_WithValidClient_RegistersKeyedSingleton()
+ {
+ // Arrange
+ var serviceCollection = new ServiceCollection();
+ var httpClient = new HttpClient();
+
+ // Act
+ serviceCollection.AddLambdaBootstrapHttpClient(httpClient);
+ var provider = serviceCollection.BuildServiceProvider();
+
+ // Assert
+ var registeredClient = provider.GetKeyedService(
+ typeof(ILambdaBootstrapOrchestrator)
+ );
+ registeredClient.Should().BeSameAs(httpClient);
+ }
+
+ [Fact]
+ public void AddLambdaBootstrapHttpClient_WithNullFactory_ThrowsArgumentNullException()
+ {
+ // Arrange
+ var serviceCollection = new ServiceCollection();
+
+ // Act
+ var act = () => serviceCollection.AddLambdaBootstrapHttpClient(null!);
+
+ // Assert
+ act.Should().ThrowExactly();
+ }
+
+ [Fact]
+ public void AddLambdaBootstrapHttpClient_WithValidFactory_ReturnsServiceCollection()
+ {
+ // Arrange
+ var serviceCollection = new ServiceCollection();
+
+ // Act
+ var result = serviceCollection.AddLambdaBootstrapHttpClient((sp, key) => new HttpClient());
+
+ // Assert
+ result.Should().BeSameAs(serviceCollection);
+ }
+
+ [Fact]
+ public void AddLambdaBootstrapHttpClient_WithValidFactory_RegistersKeyedSingleton()
+ {
+ // Arrange
+ var serviceCollection = new ServiceCollection();
+ var httpClient = new HttpClient();
+
+ // Act
+ serviceCollection.AddLambdaBootstrapHttpClient((sp, key) => httpClient);
+ var provider = serviceCollection.BuildServiceProvider();
+
+ // Assert
+ var registeredClient = provider.GetKeyedService(
+ typeof(ILambdaBootstrapOrchestrator)
+ );
+ registeredClient.Should().BeSameAs(httpClient);
+ }
+
+ [Fact]
+ public void TryAddLambdaBootstrapHttpClient_WithNullServiceCollection_ThrowsArgumentNullException()
+ {
+ // Act
+ var act = () =>
+ ((IServiceCollection)null!).TryAddLambdaBootstrapHttpClient(new HttpClient());
+
+ // Assert
+ act.Should().ThrowExactly();
+ }
+
+ [Fact]
+ public void TryAddLambdaBootstrapHttpClient_WithNullClient_ThrowsArgumentNullException()
+ {
+ // Arrange
+ var serviceCollection = new ServiceCollection();
+
+ // Act
+ var act = () => serviceCollection.TryAddLambdaBootstrapHttpClient((HttpClient)null!);
+
+ // Assert
+ act.Should().ThrowExactly();
+ }
+
+ [Fact]
+ public void TryAddLambdaBootstrapHttpClient_WithValidClient_RegistersKeyedSingleton()
+ {
+ // Arrange
+ var serviceCollection = new ServiceCollection();
+ var httpClient = new HttpClient();
+
+ // Act
+ serviceCollection.TryAddLambdaBootstrapHttpClient(httpClient);
+ var provider = serviceCollection.BuildServiceProvider();
+
+ // Assert
+ var registeredClient = provider.GetKeyedService(
+ typeof(ILambdaBootstrapOrchestrator)
+ );
+ registeredClient.Should().BeSameAs(httpClient);
+ }
+
+ [Fact]
+ public void TryAddLambdaBootstrapHttpClient_WhenAlreadyRegistered_DoesNotOverwrite()
+ {
+ // Arrange
+ var serviceCollection = new ServiceCollection();
+ var firstClient = new HttpClient();
+ var secondClient = new HttpClient();
+
+ // Act
+ serviceCollection.TryAddLambdaBootstrapHttpClient(firstClient);
+ serviceCollection.TryAddLambdaBootstrapHttpClient(secondClient);
+ var provider = serviceCollection.BuildServiceProvider();
+
+ // Assert
+ var registeredClient = provider.GetKeyedService(
+ typeof(ILambdaBootstrapOrchestrator)
+ );
+ registeredClient.Should().BeSameAs(firstClient);
+ }
+
+ [Fact]
+ public void TryAddLambdaBootstrapHttpClient_WithNullFactory_ThrowsArgumentNullException()
+ {
+ // Arrange
+ var serviceCollection = new ServiceCollection();
+
+ // Act
+ var act = () => serviceCollection.TryAddLambdaBootstrapHttpClient(null!);
+
+ // Assert
+ act.Should().ThrowExactly();
+ }
+
+ [Fact]
+ public void TryAddLambdaBootstrapHttpClient_WithValidFactory_RegistersKeyedSingleton()
+ {
+ // Arrange
+ var serviceCollection = new ServiceCollection();
+ var httpClient = new HttpClient();
+
+ // Act
+ serviceCollection.TryAddLambdaBootstrapHttpClient((sp, key) => httpClient);
+ var provider = serviceCollection.BuildServiceProvider();
+
+ // Assert
+ var registeredClient = provider.GetKeyedService(
+ typeof(ILambdaBootstrapOrchestrator)
+ );
+ registeredClient.Should().BeSameAs(httpClient);
+ }
+
+ [Fact]
+ public void TryAddLambdaBootstrapHttpClient_WithFactory_WhenAlreadyRegistered_DoesNotOverwrite()
+ {
+ // Arrange
+ var serviceCollection = new ServiceCollection();
+ var firstClient = new HttpClient();
+ var secondClient = new HttpClient();
+
+ // Act
+ serviceCollection.TryAddLambdaBootstrapHttpClient((sp, key) => firstClient);
+ serviceCollection.TryAddLambdaBootstrapHttpClient((sp, key) => secondClient);
+ var provider = serviceCollection.BuildServiceProvider();
+
+ // Assert
+ var registeredClient = provider.GetKeyedService(
+ typeof(ILambdaBootstrapOrchestrator)
+ );
+ registeredClient.Should().BeSameAs(firstClient);
+ }
+
+ [Fact]
+ public void AddLambdaBootstrapHttpClient_EnablesMethodChaining()
+ {
+ // Arrange
+ var serviceCollection = new ServiceCollection();
+ var firstClient = new HttpClient();
+ var secondClient = new HttpClient();
+
+ // Act
+ var result = serviceCollection
+ .AddLambdaBootstrapHttpClient(firstClient)
+ .AddLambdaBootstrapHttpClient((sp, key) => secondClient);
+
+ // Assert
+ result.Should().BeSameAs(serviceCollection);
+ }
+}