From e038a0838d749bc218877a2af8d5748b56a1421f Mon Sep 17 00:00:00 2001 From: Ferenc Czirok Date: Thu, 12 Oct 2023 10:11:32 +0200 Subject: [PATCH] Service duplication fix (#224) Fixes when multiple nuget packages or project references contain Blazored.LocalStorage and call AddBlazoredLocalStorage(). No error occurs if it is added more than once, but the services are only needed once. --- .../ServiceCollectionExtensions.cs | 40 +++++---- .../ServiceCollectionExtensionsTest.cs | 81 +++++++++++++++++++ 2 files changed, 106 insertions(+), 15 deletions(-) create mode 100644 tests/Blazored.LocalStorage.Tests/LocalStorageServiceTests/ServiceCollectionExtensionsTest.cs diff --git a/src/Blazored.LocalStorage/ServiceCollectionExtensions.cs b/src/Blazored.LocalStorage/ServiceCollectionExtensions.cs index c561126..e59eb97 100644 --- a/src/Blazored.LocalStorage/ServiceCollectionExtensions.cs +++ b/src/Blazored.LocalStorage/ServiceCollectionExtensions.cs @@ -1,9 +1,12 @@ -using System; +using System; using System.Diagnostics.CodeAnalysis; +using System.Linq; using Blazored.LocalStorage.JsonConverters; using Blazored.LocalStorage.Serialization; using Blazored.LocalStorage.StorageOptions; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; namespace Blazored.LocalStorage { @@ -15,18 +18,22 @@ public static IServiceCollection AddBlazoredLocalStorage(this IServiceCollection public static IServiceCollection AddBlazoredLocalStorage(this IServiceCollection services, Action configure) { - return services - .AddScoped() - .AddScoped() - .AddScoped() - .AddScoped() - .Configure(configureOptions => + services.TryAddScoped(); + services.TryAddScoped(); + services.TryAddScoped(); + services.TryAddScoped(); + if (services.All(serviceDescriptor => serviceDescriptor.ServiceType != typeof(IConfigureOptions))) + { + services.Configure(configureOptions => { configure?.Invoke(configureOptions); configureOptions.JsonSerializerOptions.Converters.Add(new TimespanJsonConverter()); }); + } + + return services; } - + /// /// Registers the Blazored LocalStorage services as singletons. This should only be used in Blazor WebAssembly applications. /// Using this in Blazor Server applications will cause unexpected and potentially dangerous behaviour. @@ -34,7 +41,7 @@ public static IServiceCollection AddBlazoredLocalStorage(this IServiceCollection /// public static IServiceCollection AddBlazoredLocalStorageAsSingleton(this IServiceCollection services) => AddBlazoredLocalStorageAsSingleton(services, null); - + /// /// Registers the Blazored LocalStorage services as singletons. This should only be used in Blazor WebAssembly applications. /// Using this in Blazor Server applications will cause unexpected and potentially dangerous behaviour. @@ -44,16 +51,19 @@ public static IServiceCollection AddBlazoredLocalStorageAsSingleton(this IServic /// public static IServiceCollection AddBlazoredLocalStorageAsSingleton(this IServiceCollection services, Action configure) { - return services - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .Configure(configureOptions => + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + if (services.All(serviceDescriptor => serviceDescriptor.ServiceType != typeof(IConfigureOptions))) + { + services.Configure(configureOptions => { configure?.Invoke(configureOptions); configureOptions.JsonSerializerOptions.Converters.Add(new TimespanJsonConverter()); }); + } + return services; } } } diff --git a/tests/Blazored.LocalStorage.Tests/LocalStorageServiceTests/ServiceCollectionExtensionsTest.cs b/tests/Blazored.LocalStorage.Tests/LocalStorageServiceTests/ServiceCollectionExtensionsTest.cs new file mode 100644 index 0000000..73a21b4 --- /dev/null +++ b/tests/Blazored.LocalStorage.Tests/LocalStorageServiceTests/ServiceCollectionExtensionsTest.cs @@ -0,0 +1,81 @@ +using System; +using System.Linq; +using Blazored.LocalStorage.Serialization; +using Blazored.LocalStorage.StorageOptions; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Xunit; + +namespace Blazored.LocalStorage.Tests.LocalStorageServiceTests; + +public class ServiceCollectionExtensionsTest +{ + [Fact] + public void Scoped() + { + // Arrange + var services = new ServiceCollection(); + + // Act + services.AddBlazoredLocalStorage(); + services.AddBlazoredLocalStorage(); + services.AddBlazoredLocalStorage(); + services.AddBlazoredLocalStorage(); + + // Assert + AssertEqual(services, typeof(IJsonSerializer), typeof(SystemTextJsonSerializer), ServiceLifetime.Scoped); + AssertEqual(services, typeof(IStorageProvider), typeof(BrowserStorageProvider), ServiceLifetime.Scoped); + AssertEqual(services, typeof(ILocalStorageService), typeof(LocalStorageService), ServiceLifetime.Scoped); + AssertEqual(services, typeof(ISyncLocalStorageService), typeof(LocalStorageService), ServiceLifetime.Scoped); + AssertEqualConfigureOptions(services, typeof(IConfigureOptions), ServiceLifetime.Singleton); + } + + [Fact] + public void Singleton() + { + // Arrange + var services = new ServiceCollection(); + + // Act + services.AddBlazoredLocalStorageAsSingleton(); + services.AddBlazoredLocalStorageAsSingleton(); + services.AddBlazoredLocalStorageAsSingleton(); + services.AddBlazoredLocalStorageAsSingleton(); + + // Assert + AssertEqual(services, typeof(IJsonSerializer), typeof(SystemTextJsonSerializer), ServiceLifetime.Singleton); + AssertEqual(services, typeof(IStorageProvider), typeof(BrowserStorageProvider), ServiceLifetime.Singleton); + AssertEqual(services, typeof(ILocalStorageService), typeof(LocalStorageService), ServiceLifetime.Singleton); + AssertEqual(services, typeof(ISyncLocalStorageService), typeof(LocalStorageService), ServiceLifetime.Singleton); + AssertEqualConfigureOptions(services, typeof(IConfigureOptions), ServiceLifetime.Singleton); + } + + + static void AssertEqual( + IServiceCollection services, + Type serviceType, + Type implementationType, + ServiceLifetime serviceLifetime + ) + { + var descriptors = services.Where(serviceDescriptor => serviceDescriptor.ServiceType == serviceType).ToArray(); + Assert.Single(descriptors); + var descriptor = descriptors.Single(); + Assert.Equal(serviceType, descriptor.ServiceType); + Assert.Equal(implementationType, descriptor.ImplementationType); + Assert.Equal(serviceLifetime, descriptor.Lifetime); + } + + static void AssertEqualConfigureOptions( + IServiceCollection services, + Type serviceType, + ServiceLifetime serviceLifetime + ) + { + var descriptors = services.Where(serviceDescriptor => serviceDescriptor.ServiceType == serviceType).ToArray(); + Assert.Single(descriptors); + var descriptor = descriptors.Single(); + Assert.Equal(serviceType, descriptor.ServiceType); + Assert.Equal(serviceLifetime, descriptor.Lifetime); + } +}