From 0b80c853b640686085b931eb92f20be7ce6dbc3b Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Sun, 24 Nov 2024 01:14:10 -0700 Subject: [PATCH] Additional lifecycle registration changes (#1410) * Added service lifetime to Jobs client Signed-off-by: Whit Waldo * Added service lifetime to messaging client Signed-off-by: Whit Waldo * Added service lifetime to actors registration Signed-off-by: Whit Waldo * Added unit tests for DaprClient Signed-off-by: Whit Waldo * Minor naming tweaks Signed-off-by: Whit Waldo * Removed invalid using Signed-off-by: Whit Waldo * Added service lifetime tests for actors Signed-off-by: Whit Waldo * Added unit tests for jobs client lifecycle registrations Signed-off-by: Whit Waldo * Added unit tests for PubSub and lifecycle registration Signed-off-by: Whit Waldo * Fixed missing registration dependency Signed-off-by: Whit Waldo --------- Signed-off-by: Whit Waldo --- .../ActorsServiceCollectionExtensions.cs | 38 ++++- .../DaprJobsServiceCollectionExtensions.cs | 24 ++- ...ishSubscribeServiceCollectionExtensions.cs | 19 ++- ...aprActorServiceCollectionExtensionsTest.cs | 60 +++++++ .../DaprServiceCollectionExtensionsTest.cs | 158 ++++++++++++------ ...aprJobsServiceCollectionExtensionsTests.cs | 54 +++++- ...bscribeServiceCollectionExtensionsTests.cs | 76 +++++++++ 7 files changed, 360 insertions(+), 69 deletions(-) create mode 100644 test/Dapr.Actors.AspNetCore.Test/DaprActorServiceCollectionExtensionsTest.cs create mode 100644 test/Dapr.Messaging.Test/Extensions/PublishSubscribeServiceCollectionExtensionsTests.cs diff --git a/src/Dapr.Actors.AspNetCore/ActorsServiceCollectionExtensions.cs b/src/Dapr.Actors.AspNetCore/ActorsServiceCollectionExtensions.cs index 11f05f4c..9b80975d 100644 --- a/src/Dapr.Actors.AspNetCore/ActorsServiceCollectionExtensions.cs +++ b/src/Dapr.Actors.AspNetCore/ActorsServiceCollectionExtensions.cs @@ -34,17 +34,18 @@ namespace Microsoft.Extensions.DependencyInjection /// /// The . /// A delegate used to configure actor options and register actor types. - public static void AddActors(this IServiceCollection? services, Action? configure) + /// The lifetime of the registered services. + public static void AddActors(this IServiceCollection? services, Action? configure, ServiceLifetime lifetime = ServiceLifetime.Singleton) { ArgumentNullException.ThrowIfNull(services, nameof(services)); - // Routing and health checks are required dependencies. + // Routing, health checks and logging are required dependencies. services.AddRouting(); services.AddHealthChecks(); + services.AddLogging(); - services.TryAddSingleton(); - services.TryAddSingleton(s => - { + var actorRuntimeRegistration = new Func(s => + { var options = s.GetRequiredService>().Value; ConfigureActorOptions(s, options); @@ -53,11 +54,10 @@ namespace Microsoft.Extensions.DependencyInjection var proxyFactory = s.GetRequiredService(); return new ActorRuntime(options, loggerFactory, activatorFactory, proxyFactory); }); - - services.TryAddSingleton(s => + var proxyFactoryRegistration = new Func(serviceProvider => { - var options = s.GetRequiredService>().Value; - ConfigureActorOptions(s, options); + var options = serviceProvider.GetRequiredService>().Value; + ConfigureActorOptions(serviceProvider, options); var factory = new ActorProxyFactory() { @@ -72,6 +72,26 @@ namespace Microsoft.Extensions.DependencyInjection return factory; }); + switch (lifetime) + { + case ServiceLifetime.Scoped: + services.TryAddScoped(); + services.TryAddScoped(actorRuntimeRegistration); + services.TryAddScoped(proxyFactoryRegistration); + break; + case ServiceLifetime.Transient: + services.TryAddTransient(); + services.TryAddTransient(actorRuntimeRegistration); + services.TryAddTransient(proxyFactoryRegistration); + break; + default: + case ServiceLifetime.Singleton: + services.TryAddSingleton(); + services.TryAddSingleton(actorRuntimeRegistration); + services.TryAddSingleton(proxyFactoryRegistration); + break; + } + if (configure != null) { services.Configure(configure); diff --git a/src/Dapr.Jobs/Extensions/DaprJobsServiceCollectionExtensions.cs b/src/Dapr.Jobs/Extensions/DaprJobsServiceCollectionExtensions.cs index 67e71898..93265837 100644 --- a/src/Dapr.Jobs/Extensions/DaprJobsServiceCollectionExtensions.cs +++ b/src/Dapr.Jobs/Extensions/DaprJobsServiceCollectionExtensions.cs @@ -26,25 +26,40 @@ public static class DaprJobsServiceCollectionExtensions /// /// The . /// Optionally allows greater configuration of the . - public static IServiceCollection AddDaprJobsClient(this IServiceCollection serviceCollection, Action? configure = null) + /// The lifetime of the registered services. + public static IServiceCollection AddDaprJobsClient(this IServiceCollection serviceCollection, Action? configure = null, ServiceLifetime lifetime = ServiceLifetime.Singleton) { ArgumentNullException.ThrowIfNull(serviceCollection, nameof(serviceCollection)); //Register the IHttpClientFactory implementation serviceCollection.AddHttpClient(); - serviceCollection.TryAddSingleton(serviceProvider => + var registration = new Func(serviceProvider => { var httpClientFactory = serviceProvider.GetRequiredService(); var builder = new DaprJobsClientBuilder(); builder.UseHttpClientFactory(httpClientFactory); - + configure?.Invoke(builder); return builder.Build(); }); + switch (lifetime) + { + case ServiceLifetime.Scoped: + serviceCollection.TryAddScoped(registration); + break; + case ServiceLifetime.Transient: + serviceCollection.TryAddTransient(registration); + break; + case ServiceLifetime.Singleton: + default: + serviceCollection.TryAddSingleton(registration); + break; + } + return serviceCollection; } @@ -53,8 +68,9 @@ public static class DaprJobsServiceCollectionExtensions /// /// The . /// Optionally allows greater configuration of the using injected services. + /// The lifetime of the registered services. /// - public static IServiceCollection AddDaprJobsClient(this IServiceCollection serviceCollection, Action? configure) + public static IServiceCollection AddDaprJobsClient(this IServiceCollection serviceCollection, Action? configure, ServiceLifetime lifetime = ServiceLifetime.Singleton) { ArgumentNullException.ThrowIfNull(serviceCollection, nameof(serviceCollection)); diff --git a/src/Dapr.Messaging/PublishSubscribe/Extensions/PublishSubscribeServiceCollectionExtensions.cs b/src/Dapr.Messaging/PublishSubscribe/Extensions/PublishSubscribeServiceCollectionExtensions.cs index bc60c588..fe9b7c41 100644 --- a/src/Dapr.Messaging/PublishSubscribe/Extensions/PublishSubscribeServiceCollectionExtensions.cs +++ b/src/Dapr.Messaging/PublishSubscribe/Extensions/PublishSubscribeServiceCollectionExtensions.cs @@ -13,15 +13,16 @@ public static class PublishSubscribeServiceCollectionExtensions /// /// The . /// Optionally allows greater configuration of the using injected services. + /// The lifetime of the registered services. /// - public static IServiceCollection AddDaprPubSubClient(this IServiceCollection services, Action? configure = null) + public static IServiceCollection AddDaprPubSubClient(this IServiceCollection services, Action? configure = null, ServiceLifetime lifetime = ServiceLifetime.Singleton) { ArgumentNullException.ThrowIfNull(services, nameof(services)); //Register the IHttpClientFactory implementation services.AddHttpClient(); - services.TryAddSingleton(serviceProvider => + var registration = new Func(serviceProvider => { var httpClientFactory = serviceProvider.GetRequiredService(); @@ -33,6 +34,20 @@ public static class PublishSubscribeServiceCollectionExtensions return builder.Build(); }); + switch (lifetime) + { + case ServiceLifetime.Scoped: + services.TryAddScoped(registration); + break; + case ServiceLifetime.Transient: + services.TryAddTransient(registration); + break; + default: + case ServiceLifetime.Singleton: + services.TryAddSingleton(registration); + break; + } + return services; } } diff --git a/test/Dapr.Actors.AspNetCore.Test/DaprActorServiceCollectionExtensionsTest.cs b/test/Dapr.Actors.AspNetCore.Test/DaprActorServiceCollectionExtensionsTest.cs new file mode 100644 index 00000000..3255fb78 --- /dev/null +++ b/test/Dapr.Actors.AspNetCore.Test/DaprActorServiceCollectionExtensionsTest.cs @@ -0,0 +1,60 @@ +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Dapr.Actors.AspNetCore.Test; + +public sealed class DaprActorServiceCollectionExtensionsTest +{ + [Fact] + public void RegisterActorsClient_ShouldRegisterSingleton_WhenLifetimeIsSingleton() + { + var services = new ServiceCollection(); + + services.AddActors(options => { }, ServiceLifetime.Singleton); + var serviceProvider = services.BuildServiceProvider(); + + var daprClient1 = serviceProvider.GetService(); + var daprClient2 = serviceProvider.GetService(); + + Assert.NotNull(daprClient1); + Assert.NotNull(daprClient2); + + Assert.Same(daprClient1, daprClient2); + } + + [Fact] + public async Task RegisterActorsClient_ShouldRegisterScoped_WhenLifetimeIsScoped() + { + var services = new ServiceCollection(); + + services.AddActors(options => { }, ServiceLifetime.Scoped); + var serviceProvider = services.BuildServiceProvider(); + + await using var scope1 = serviceProvider.CreateAsyncScope(); + var daprClient1 = scope1.ServiceProvider.GetService(); + + await using var scope2 = serviceProvider.CreateAsyncScope(); + var daprClient2 = scope2.ServiceProvider.GetService(); + + Assert.NotNull(daprClient1); + Assert.NotNull(daprClient2); + Assert.NotSame(daprClient1, daprClient2); + } + + [Fact] + public void RegisterActorsClient_ShouldRegisterTransient_WhenLifetimeIsTransient() + { + var services = new ServiceCollection(); + + services.AddActors(options => { }, ServiceLifetime.Transient); + var serviceProvider = services.BuildServiceProvider(); + + var daprClient1 = serviceProvider.GetService(); + var daprClient2 = serviceProvider.GetService(); + + Assert.NotNull(daprClient1); + Assert.NotNull(daprClient2); + Assert.NotSame(daprClient1, daprClient2); + } +} diff --git a/test/Dapr.AspNetCore.Test/DaprServiceCollectionExtensionsTest.cs b/test/Dapr.AspNetCore.Test/DaprServiceCollectionExtensionsTest.cs index 4a340e22..2028a9fb 100644 --- a/test/Dapr.AspNetCore.Test/DaprServiceCollectionExtensionsTest.cs +++ b/test/Dapr.AspNetCore.Test/DaprServiceCollectionExtensionsTest.cs @@ -15,67 +15,120 @@ using System; using System.Text.Json; +using System.Threading.Tasks; using Dapr.Client; using Microsoft.Extensions.DependencyInjection; using Xunit; -namespace Dapr.AspNetCore.Test +namespace Dapr.AspNetCore.Test; + +public class DaprServiceCollectionExtensionsTest { - public class DaprServiceCollectionExtensionsTest + [Fact] + public void AddDaprClient_RegistersDaprClientOnlyOnce() { - [Fact] - public void AddDaprClient_RegistersDaprClientOnlyOnce() - { - var services = new ServiceCollection(); + var services = new ServiceCollection(); - var clientBuilder = new Action( - builder => builder.UseJsonSerializationOptions( - new JsonSerializerOptions() - { - PropertyNameCaseInsensitive = false - } - ) - ); - - // register with JsonSerializerOptions.PropertyNameCaseInsensitive = true (default) - services.AddDaprClient(); - - // register with PropertyNameCaseInsensitive = false - services.AddDaprClient(clientBuilder); - - var serviceProvider = services.BuildServiceProvider(); - - DaprClientGrpc? daprClient = serviceProvider.GetService() as DaprClientGrpc; - - Assert.NotNull(daprClient); - Assert.True(daprClient?.JsonSerializerOptions.PropertyNameCaseInsensitive); - } - - [Fact] - public void AddDaprClient_RegistersUsingDependencyFromIServiceProvider() - { - - var services = new ServiceCollection(); - services.AddSingleton(); - services.AddDaprClient((provider, builder) => - { - var configProvider = provider.GetRequiredService(); - var caseSensitivity = configProvider.GetCaseSensitivity(); - - builder.UseJsonSerializationOptions(new JsonSerializerOptions + var clientBuilder = new Action( + builder => builder.UseJsonSerializationOptions( + new JsonSerializerOptions() { - PropertyNameCaseInsensitive = caseSensitivity - }); + PropertyNameCaseInsensitive = false + } + ) + ); + + // register with JsonSerializerOptions.PropertyNameCaseInsensitive = true (default) + services.AddDaprClient(); + + // register with PropertyNameCaseInsensitive = false + services.AddDaprClient(clientBuilder); + + var serviceProvider = services.BuildServiceProvider(); + + DaprClientGrpc? daprClient = serviceProvider.GetService() as DaprClientGrpc; + + Assert.NotNull(daprClient); + Assert.True(daprClient?.JsonSerializerOptions.PropertyNameCaseInsensitive); + } + + [Fact] + public void AddDaprClient_RegistersUsingDependencyFromIServiceProvider() + { + + var services = new ServiceCollection(); + services.AddSingleton(); + services.AddDaprClient((provider, builder) => + { + var configProvider = provider.GetRequiredService(); + var caseSensitivity = configProvider.GetCaseSensitivity(); + + builder.UseJsonSerializationOptions(new JsonSerializerOptions + { + PropertyNameCaseInsensitive = caseSensitivity }); + }); - var serviceProvider = services.BuildServiceProvider(); + var serviceProvider = services.BuildServiceProvider(); - DaprClientGrpc? client = serviceProvider.GetRequiredService() as DaprClientGrpc; + DaprClientGrpc? client = serviceProvider.GetRequiredService() as DaprClientGrpc; - //Registers with case-insensitive as true by default, but we set as false above - Assert.NotNull(client); - Assert.False(client?.JsonSerializerOptions.PropertyNameCaseInsensitive); - } + //Registers with case-insensitive as true by default, but we set as false above + Assert.NotNull(client); + Assert.False(client?.JsonSerializerOptions.PropertyNameCaseInsensitive); + } + + [Fact] + public void RegisterClient_ShouldRegisterSingleton_WhenLifetimeIsSingleton() + { + var services = new ServiceCollection(); + + services.AddDaprClient(options => { }, ServiceLifetime.Singleton); + var serviceProvider = services.BuildServiceProvider(); + + var daprClient1 = serviceProvider.GetService(); + var daprClient2 = serviceProvider.GetService(); + + Assert.NotNull(daprClient1); + Assert.NotNull(daprClient2); + + Assert.Same(daprClient1, daprClient2); + } + + [Fact] + public async Task RegisterDaprClient_ShouldRegisterScoped_WhenLifetimeIsScoped() + { + var services = new ServiceCollection(); + + services.AddDaprClient(options => { }, ServiceLifetime.Scoped); + var serviceProvider = services.BuildServiceProvider(); + + await using var scope1 = serviceProvider.CreateAsyncScope(); + var daprClient1 = scope1.ServiceProvider.GetService(); + + await using var scope2 = serviceProvider.CreateAsyncScope(); + var daprClient2 = scope2.ServiceProvider.GetService(); + + Assert.NotNull(daprClient1); + Assert.NotNull(daprClient2); + Assert.NotSame(daprClient1, daprClient2); + } + + [Fact] + public void RegisterDaprClient_ShouldRegisterTransient_WhenLifetimeIsTransient() + { + var services = new ServiceCollection(); + + services.AddDaprClient(options => { }, ServiceLifetime.Transient); + var serviceProvider = services.BuildServiceProvider(); + + var daprClient1 = serviceProvider.GetService(); + var daprClient2 = serviceProvider.GetService(); + + Assert.NotNull(daprClient1); + Assert.NotNull(daprClient2); + Assert.NotSame(daprClient1, daprClient2); + } #if NET8_0_OR_GREATER @@ -96,9 +149,8 @@ namespace Dapr.AspNetCore.Test } #endif - private class TestConfigurationProvider - { - public bool GetCaseSensitivity() => false; - } + private class TestConfigurationProvider + { + public bool GetCaseSensitivity() => false; } } diff --git a/test/Dapr.Jobs.Test/Extensions/DaprJobsServiceCollectionExtensionsTests.cs b/test/Dapr.Jobs.Test/Extensions/DaprJobsServiceCollectionExtensionsTests.cs index 34d900ae..281477d4 100644 --- a/test/Dapr.Jobs.Test/Extensions/DaprJobsServiceCollectionExtensionsTests.cs +++ b/test/Dapr.Jobs.Test/Extensions/DaprJobsServiceCollectionExtensionsTests.cs @@ -13,9 +13,9 @@ using System; using System.Net.Http; +using System.Threading.Tasks; using Dapr.Jobs.Extensions; using Microsoft.Extensions.DependencyInjection; -using Xunit; namespace Dapr.Jobs.Test.Extensions; @@ -77,6 +77,58 @@ public class DaprJobsServiceCollectionExtensionsTest Assert.Equal("dapr-api-token", client.apiTokenHeader.Value.Key); Assert.Equal("abcdef", client.apiTokenHeader.Value.Value); } + + [Fact] + public void RegisterJobsClient_ShouldRegisterSingleton_WhenLifetimeIsSingleton() + { + var services = new ServiceCollection(); + + services.AddDaprJobsClient(options => { }, ServiceLifetime.Singleton); + var serviceProvider = services.BuildServiceProvider(); + + var daprJobsClient1 = serviceProvider.GetService(); + var daprJobsClient2 = serviceProvider.GetService(); + + Assert.NotNull(daprJobsClient1); + Assert.NotNull(daprJobsClient2); + + Assert.Same(daprJobsClient1, daprJobsClient2); + } + + [Fact] + public async Task RegisterJobsClient_ShouldRegisterScoped_WhenLifetimeIsScoped() + { + var services = new ServiceCollection(); + + services.AddDaprJobsClient(options => { }, ServiceLifetime.Scoped); + var serviceProvider = services.BuildServiceProvider(); + + await using var scope1 = serviceProvider.CreateAsyncScope(); + var daprJobsClient1 = scope1.ServiceProvider.GetService(); + + await using var scope2 = serviceProvider.CreateAsyncScope(); + var daprJobsClient2 = scope2.ServiceProvider.GetService(); + + Assert.NotNull(daprJobsClient1); + Assert.NotNull(daprJobsClient2); + Assert.NotSame(daprJobsClient1, daprJobsClient2); + } + + [Fact] + public void RegisterJobsClient_ShouldRegisterTransient_WhenLifetimeIsTransient() + { + var services = new ServiceCollection(); + + services.AddDaprJobsClient(options => { }, ServiceLifetime.Transient); + var serviceProvider = services.BuildServiceProvider(); + + var daprJobsClient1 = serviceProvider.GetService(); + var daprJobsClient2 = serviceProvider.GetService(); + + Assert.NotNull(daprJobsClient1); + Assert.NotNull(daprJobsClient2); + Assert.NotSame(daprJobsClient1, daprJobsClient2); + } private class TestSecretRetriever { diff --git a/test/Dapr.Messaging.Test/Extensions/PublishSubscribeServiceCollectionExtensionsTests.cs b/test/Dapr.Messaging.Test/Extensions/PublishSubscribeServiceCollectionExtensionsTests.cs new file mode 100644 index 00000000..d239fb86 --- /dev/null +++ b/test/Dapr.Messaging.Test/Extensions/PublishSubscribeServiceCollectionExtensionsTests.cs @@ -0,0 +1,76 @@ +using Dapr.Messaging.PublishSubscribe; +using Dapr.Messaging.PublishSubscribe.Extensions; +using Microsoft.Extensions.DependencyInjection; + +namespace Dapr.Messaging.Test.Extensions; + +public sealed class PublishSubscribeServiceCollectionExtensionsTests +{ + [Fact] + public void AddDaprPubSubClient_RegistersIHttpClientFactory() + { + var services = new ServiceCollection(); + + services.AddDaprPubSubClient(); + + var serviceProvider = services.BuildServiceProvider(); + + var httpClientFactory = serviceProvider.GetService(); + Assert.NotNull(httpClientFactory); + + var daprPubSubClient = serviceProvider.GetService(); + Assert.NotNull(daprPubSubClient); + } + + [Fact] + public void RegisterPubsubClient_ShouldRegisterSingleton_WhenLifetimeIsSingleton() + { + var services = new ServiceCollection(); + + services.AddDaprPubSubClient(lifetime: ServiceLifetime.Singleton); + var serviceProvider = services.BuildServiceProvider(); + + var daprPubSubClient1 = serviceProvider.GetService(); + var daprPubSubClient2 = serviceProvider.GetService(); + + Assert.NotNull(daprPubSubClient1); + Assert.NotNull(daprPubSubClient2); + + Assert.Same(daprPubSubClient1, daprPubSubClient2); + } + + [Fact] + public async Task RegisterPubsubClient_ShouldRegisterScoped_WhenLifetimeIsScoped() + { + var services = new ServiceCollection(); + + services.AddDaprPubSubClient(lifetime: ServiceLifetime.Scoped); + var serviceProvider = services.BuildServiceProvider(); + + await using var scope1 = serviceProvider.CreateAsyncScope(); + var daprPubSubClient1 = scope1.ServiceProvider.GetService(); + + await using var scope2 = serviceProvider.CreateAsyncScope(); + var daprPubSubClient2 = scope2.ServiceProvider.GetService(); + + Assert.NotNull(daprPubSubClient1); + Assert.NotNull(daprPubSubClient2); + Assert.NotSame(daprPubSubClient1, daprPubSubClient2); + } + + [Fact] + public void RegisterPubsubClient_ShouldRegisterTransient_WhenLifetimeIsTransient() + { + var services = new ServiceCollection(); + + services.AddDaprPubSubClient(lifetime: ServiceLifetime.Transient); + var serviceProvider = services.BuildServiceProvider(); + + var daprPubSubClient1 = serviceProvider.GetService(); + var daprPubSubClient2 = serviceProvider.GetService(); + + Assert.NotNull(daprPubSubClient1); + Assert.NotNull(daprPubSubClient2); + Assert.NotSame(daprPubSubClient1, daprPubSubClient2); + } +}