Skip to content

Commit

Permalink
Add possibility to add AWS services to DI with a key (#3570)
Browse files Browse the repository at this point in the history
* Include newer versions of dependencies if target framework is .NET 8 to utilize newer framework features

* Add extensions to add AWS services with a key, to enable multiple registrations of the same type
  • Loading branch information
OleksiiZuiev authored Dec 13, 2024
1 parent dc31a6e commit 6215931
Show file tree
Hide file tree
Showing 4 changed files with 243 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,17 @@

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="2.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="2.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="2.0.0" />
</ItemGroup>

<ItemGroup Condition="'$(TargetFramework)' != 'net8.0'">
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="2.0.0" />
</ItemGroup>

<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.0" />
</ItemGroup>


<ItemGroup>
<ProjectReference Include="../../../sdk/src/Core/AWSSDK.Core.NetStandard.csproj" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,5 +162,78 @@ public static IServiceCollection TryAddAWSService<T>(this IServiceCollection col
collection.TryAdd(descriptor);
return collection;
}

#if NET8_0_OR_GREATER

/// <summary>
/// Adds the Amazon service client to the dependency injection framework with a key. The Amazon service client is not
/// created until it is requested. If the ServiceLifetime property is set to Singleton, the default, then the same
/// instance will be reused for the lifetime of the process and the object should not be disposed.
/// </summary>
/// <typeparam name="T">The AWS service interface, like IAmazonS3</typeparam>
/// <param name="collection"></param>
/// <param name="serviceKey">The key with which the service will be added in the dependency injection framework.</param>
/// <param name="lifetime">The lifetime of the service client created. The default is Singleton.</param>
/// <returns>Returns back the IServiceCollection to continue the fluent system of IServiceCollection.</returns>
public static IServiceCollection AddKeyedAWSService<T>(this IServiceCollection collection, object serviceKey, ServiceLifetime lifetime = ServiceLifetime.Singleton) where T : IAmazonService
{
return AddKeyedAWSService<T>(collection, serviceKey, null, lifetime);
}

/// <summary>
/// Adds the Amazon service client to the dependency injection framework with a key. The Amazon service client is not
/// created until it is requested. If the ServiceLifetime property is set to Singleton, the default, then the same
/// instance will be reused for the lifetime of the process and the object should not be disposed.
/// </summary>
/// <typeparam name="T">The AWS service interface, like IAmazonS3</typeparam>
/// <param name="collection"></param>
/// <param name="serviceKey">The key with which the service will be added in the dependency injection framework.</param>
/// <param name="options">The AWS options used to create the service client overriding the default AWS options added using AddDefaultAWSOptions.</param>
/// <param name="lifetime">The lifetime of the service client created. The default is Singleton.</param>
/// <returns>Returns back the IServiceCollection to continue the fluent system of IServiceCollection.</returns>
public static IServiceCollection AddKeyedAWSService<T>(this IServiceCollection collection, object serviceKey, AWSOptions options, ServiceLifetime lifetime = ServiceLifetime.Singleton) where T : IAmazonService
{
object Factory(IServiceProvider sp, object key) => new ClientFactory<T>(options).CreateServiceClient(sp);

var descriptor = new ServiceDescriptor(typeof(T), serviceKey, Factory, lifetime);
collection.Add(descriptor);
return collection;
}

/// <summary>
/// Adds the Amazon service client to the dependency injection framework with a key if the service type hasn't already been registered with the same key.
/// The Amazon service client is not created until it is requested. If the ServiceLifetime property is set to Singleton, the default, then the same
/// instance will be reused for the lifetime of the process and the object should not be disposed.
/// </summary>
/// <typeparam name="T">The AWS service interface, like IAmazonS3</typeparam>
/// <param name="collection"></param>
/// <param name="serviceKey">The key with which the service will be added in the dependency injection framework.</param>
/// <param name="lifetime">The lifetime of the service client created. The default is Singleton.</param>
/// <returns>Returns back the IServiceCollection to continue the fluent system of IServiceCollection.</returns>
public static IServiceCollection TryAddKeyedAWSService<T>(this IServiceCollection collection, object serviceKey, ServiceLifetime lifetime = ServiceLifetime.Singleton) where T : IAmazonService
{
return TryAddKeyedAWSService<T>(collection, serviceKey, null, lifetime);
}

/// <summary>
/// Adds the Amazon service client to the dependency injection framework with a key if the service type hasn't already been registered with the same key.
/// The Amazon service client is not created until it is requested. If the ServiceLifetime property is set to Singleton, the default, then the same
/// instance will be reused for the lifetime of the process and the object should not be disposed.
/// </summary>
/// <typeparam name="T">The AWS service interface, like IAmazonS3</typeparam>
/// <param name="collection"></param>
/// <param name="serviceKey">The key with which the service will be added in the dependency injection framework.</param>
/// <param name="options">The AWS options used to create the service client overriding the default AWS options added using AddDefaultAWSOptions.</param>
/// <param name="lifetime">The lifetime of the service client created. The default is Singleton.</param>
/// <returns>Returns back the IServiceCollection to continue the fluent system of IServiceCollection.</returns>
public static IServiceCollection TryAddKeyedAWSService<T>(this IServiceCollection collection, object serviceKey, AWSOptions options, ServiceLifetime lifetime = ServiceLifetime.Singleton) where T : IAmazonService
{
object Factory(IServiceProvider sp, object key) => new ClientFactory<T>(options).CreateServiceClient(sp);

var descriptor = new ServiceDescriptor(typeof(T), serviceKey, Factory, lifetime);
collection.TryAdd(descriptor);
return collection;
}
#endif
}
}
151 changes: 151 additions & 0 deletions extensions/test/NETCore.SetupTests/DependencyInjectionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,157 @@ public void InjectS3ClientWithFactoryBuiltConfig()
Assert.NotNull(controller.S3Client);
Assert.Equal(expectRegion, controller.S3Client.Config.RegionEndpoint);
}

#if NET8_0_OR_GREATER

[Fact]
public void InjectKeyedS3ClientWithDefaultConfig()
{
var builder = new ConfigurationBuilder();
builder.AddJsonFile("./TestFiles/GetClientConfigSettingsTest.json");

IConfiguration config = builder.Build();

ServiceCollection services = new ServiceCollection();
services.AddDefaultAWSOptions(config.GetAWSOptions());
services.AddKeyedAWSService<IAmazonS3>(TestControllerKeyed.Key);

var serviceProvider = services.BuildServiceProvider();

var controller = ActivatorUtilities.CreateInstance<TestControllerKeyed>(serviceProvider);
Assert.NotNull(controller.S3Client);
Assert.Equal(RegionEndpoint.USWest2, controller.S3Client.Config.RegionEndpoint);
}

[Fact]
public void InjectKeyedS3ClientWithoutDefaultConfig()
{
ServiceCollection services = new ServiceCollection();
services.AddKeyedAWSService<IAmazonS3>(TestControllerKeyed.Key, new AWSOptions {Region = RegionEndpoint.USEast1 });

var serviceProvider = services.BuildServiceProvider();

var controller = ActivatorUtilities.CreateInstance<TestControllerKeyed>(serviceProvider);
Assert.NotNull(controller.S3Client);
Assert.Equal(RegionEndpoint.USEast1, controller.S3Client.Config.RegionEndpoint);
}

[Fact]
public void InjectKeyedS3ClientWithOverridingConfig()
{
var builder = new ConfigurationBuilder();
builder.AddJsonFile("./TestFiles/GetClientConfigSettingsTest.json");

IConfiguration config = builder.Build();

ServiceCollection services = new ServiceCollection();
services.AddDefaultAWSOptions(config.GetAWSOptions());
services.AddKeyedAWSService<IAmazonS3>(TestControllerKeyed.Key, new AWSOptions {Region = RegionEndpoint.EUCentral1 });

var serviceProvider = services.BuildServiceProvider();

var controller = ActivatorUtilities.CreateInstance<TestControllerKeyed>(serviceProvider);
Assert.NotNull(controller.S3Client);
Assert.Equal(RegionEndpoint.EUCentral1, controller.S3Client.Config.RegionEndpoint);
}

[Fact]
public void TryAddKeyedCanRegisterWithDefaultConfig()
{
var builder = new ConfigurationBuilder();
builder.AddJsonFile("./TestFiles/GetClientConfigSettingsTest.json");

IConfiguration config = builder.Build();

ServiceCollection services = new ServiceCollection();
services.AddDefaultAWSOptions(config.GetAWSOptions());
services.TryAddKeyedAWSService<IAmazonS3>(TestControllerKeyed.Key);

var serviceProvider = services.BuildServiceProvider();

var controller = ActivatorUtilities.CreateInstance<TestControllerKeyed>(serviceProvider);
Assert.NotNull(controller.S3Client);
Assert.Equal(RegionEndpoint.USWest2, controller.S3Client.Config.RegionEndpoint);
}

[Fact]
public void TryAddKeyedCanRegisterWithoutDefaultConfig()
{
ServiceCollection services = new ServiceCollection();
services.AddKeyedAWSService<IAmazonS3>(TestControllerKeyed.Key, new AWSOptions {Region = RegionEndpoint.USEast1 });

var serviceProvider = services.BuildServiceProvider();

var controller = ActivatorUtilities.CreateInstance<TestControllerKeyed>(serviceProvider);
Assert.NotNull(controller.S3Client);
Assert.Equal(RegionEndpoint.USEast1, controller.S3Client.Config.RegionEndpoint);
}

[Fact]
public void TryAddKeyedServiceDontOverrideWhenAlreadySetup()
{
var builder = new ConfigurationBuilder();
builder.AddJsonFile("./TestFiles/GetClientConfigSettingsTest.json");

IConfiguration config = builder.Build();

ServiceCollection services = new ServiceCollection();

services.AddDefaultAWSOptions(config.GetAWSOptions());
services.AddKeyedAWSService<IAmazonS3>(TestControllerKeyed.Key, new AWSOptions { Region = RegionEndpoint.EUWest1 });
services.TryAddKeyedAWSService<IAmazonS3>(TestControllerKeyed.Key);
services.TryAddKeyedAWSService<IAmazonS3>(TestControllerKeyed.Key, new AWSOptions { Region = RegionEndpoint.EUCentral1 });

var serviceProvider = services.BuildServiceProvider();

var controller = ActivatorUtilities.CreateInstance<TestControllerKeyed>(serviceProvider);
Assert.Equal(RegionEndpoint.EUWest1, controller.S3Client.Config.RegionEndpoint);
}

[Fact]
public void InjectMultipleS3Clients()
{
ServiceCollection services = new ServiceCollection();
services.AddKeyedAWSService<IAmazonS3>(TestControllerMultiKeyed.Key1, new AWSOptions { Region = RegionEndpoint.USEast1 });
services.AddKeyedAWSService<IAmazonS3>(TestControllerMultiKeyed.Key2, new AWSOptions { Region = RegionEndpoint.USWest2 });

var serviceProvider = services.BuildServiceProvider();

var controller = ActivatorUtilities.CreateInstance<TestControllerMultiKeyed>(serviceProvider);
Assert.NotNull(controller.S3Client1);
Assert.NotNull(controller.S3Client2);
Assert.Equal(RegionEndpoint.USEast1, controller.S3Client1.Config.RegionEndpoint);
Assert.Equal(RegionEndpoint.USWest2, controller.S3Client2.Config.RegionEndpoint);
}

public class TestControllerKeyed
{
public const string Key = "key";

public IAmazonS3 S3Client { get; private set; }
public TestControllerKeyed([FromKeyedServices(Key)] IAmazonS3 s3Client)
{
S3Client = s3Client;
}
}

public class TestControllerMultiKeyed
{
public const string Key1 = "key1";
public const string Key2 = "key2";

public IAmazonS3 S3Client1 { get; private set; }
public IAmazonS3 S3Client2 { get; private set; }
public TestControllerMultiKeyed(
[FromKeyedServices(Key1)] IAmazonS3 s3Client1,
[FromKeyedServices(Key2)] IAmazonS3 s3Client2)
{
S3Client1 = s3Client1;
S3Client2 = s3Client2;
}

}
#endif

public class TestController
{
Expand Down
11 changes: 10 additions & 1 deletion extensions/test/NETCore.SetupTests/NETCore.SetupTests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,19 @@
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
<PackageReference Include="Moq" Version="4.8.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="2.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="2.0.0" />
</ItemGroup>

<ItemGroup Condition="'$(TargetFramework)' != 'net8.0'">
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="2.0.0" />
<PackageReference Include="xunit" Version="2.4.2" />
</ItemGroup>

<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
</ItemGroup>



</Project>

0 comments on commit 6215931

Please sign in to comment.