diff --git a/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHostBuilder.cs b/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHostBuilder.cs index e90fdab63e70..c9971669d6c0 100644 --- a/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHostBuilder.cs +++ b/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHostBuilder.cs @@ -30,6 +30,7 @@ public sealed class WebAssemblyHostBuilder private Func _createServiceProvider; private RootTypeCache? _rootComponentCache; private string? _persistedState; + private ServiceProviderOptions? _serviceProviderOptions; /// /// Creates an instance of using the most common @@ -91,7 +92,16 @@ internal WebAssemblyHostBuilder(IInternalJSImportMethods jsMethods) _createServiceProvider = () => { - return Services.BuildServiceProvider(validateScopes: WebAssemblyHostEnvironmentExtensions.IsDevelopment(hostEnvironment)); + var isDevelopment = WebAssemblyHostEnvironmentExtensions.IsDevelopment(hostEnvironment); + + // Use custom options if provided, otherwise use defaults + var options = _serviceProviderOptions ?? new ServiceProviderOptions + { + ValidateScopes = isDevelopment, + ValidateOnBuild = isDevelopment + }; + + return Services.BuildServiceProvider(options); }; } @@ -276,6 +286,17 @@ public void ConfigureContainer(IServiceProviderFactory facto }; } + // In WebAssemblyHostBuilder class: + /// + /// Configures the service provider options for this host builder. + /// + /// The service provider options to use. + public WebAssemblyHostBuilder UseServiceProviderOptions(ServiceProviderOptions options) + { + _serviceProviderOptions = options ?? throw new ArgumentNullException(nameof(options)); + return this; + } + /// /// Builds a instance based on the configuration of this builder. /// diff --git a/src/Components/WebAssembly/WebAssembly/src/Hosting/WebassemblyHostBuilderExtensions.cs b/src/Components/WebAssembly/WebAssembly/src/Hosting/WebassemblyHostBuilderExtensions.cs new file mode 100644 index 000000000000..edfad727a34d --- /dev/null +++ b/src/Components/WebAssembly/WebAssembly/src/Hosting/WebassemblyHostBuilderExtensions.cs @@ -0,0 +1,44 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting; + +/// +/// Extension methods for configuring a . +/// +public static class WebAssemblyHostBuilderExtensions +{ + /// + /// Configures the default service provider for the WebAssembly host. + /// + /// The to configure. + /// A callback used to configure the . + /// The . + public static WebAssemblyHostBuilder UseDefaultServiceProvider( + this WebAssemblyHostBuilder builder, + Action configure) + { + return builder.UseDefaultServiceProvider((env, options) => configure(options)); + } + + /// + /// Configures the default service provider for the WebAssembly host. + /// + /// The to configure. + /// A callback used to configure the with access to the host environment. + /// The . + public static WebAssemblyHostBuilder UseDefaultServiceProvider( + this WebAssemblyHostBuilder builder, + Action configure) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(configure); + + var options = new ServiceProviderOptions(); + configure(builder.HostEnvironment, options); + + return builder.UseServiceProviderOptions(options); + } +} diff --git a/src/Components/WebAssembly/WebAssembly/src/PublicAPI.Unshipped.txt b/src/Components/WebAssembly/WebAssembly/src/PublicAPI.Unshipped.txt index f9836324fe1f..fc7b59f8f640 100644 --- a/src/Components/WebAssembly/WebAssembly/src/PublicAPI.Unshipped.txt +++ b/src/Components/WebAssembly/WebAssembly/src/PublicAPI.Unshipped.txt @@ -1,4 +1,6 @@ #nullable enable +Microsoft.AspNetCore.Components.WebAssembly.Hosting.WebAssemblyHostBuilder.UseServiceProviderOptions(Microsoft.Extensions.DependencyInjection.ServiceProviderOptions! options) -> Microsoft.AspNetCore.Components.WebAssembly.Hosting.WebAssemblyHostBuilder! +Microsoft.AspNetCore.Components.WebAssembly.Hosting.WebAssemblyHostBuilderExtensions Microsoft.AspNetCore.Components.WebAssembly.HotReload.WebAssemblyHotReload.Delta Microsoft.AspNetCore.Components.WebAssembly.HotReload.WebAssemblyHotReload.Delta.Delta() -> void Microsoft.AspNetCore.Components.WebAssembly.HotReload.WebAssemblyHotReload.Delta.ILDelta.get -> byte[]! @@ -17,4 +19,6 @@ Microsoft.AspNetCore.Components.WebAssembly.HotReload.WebAssemblyHotReload.LogEn Microsoft.AspNetCore.Components.WebAssembly.HotReload.WebAssemblyHotReload.LogEntry.Message.init -> void Microsoft.AspNetCore.Components.WebAssembly.HotReload.WebAssemblyHotReload.LogEntry.Severity.get -> int Microsoft.AspNetCore.Components.WebAssembly.HotReload.WebAssemblyHotReload.LogEntry.Severity.init -> void +static Microsoft.AspNetCore.Components.WebAssembly.Hosting.WebAssemblyHostBuilderExtensions.UseDefaultServiceProvider(this Microsoft.AspNetCore.Components.WebAssembly.Hosting.WebAssemblyHostBuilder! builder, System.Action! configure) -> Microsoft.AspNetCore.Components.WebAssembly.Hosting.WebAssemblyHostBuilder! +static Microsoft.AspNetCore.Components.WebAssembly.Hosting.WebAssemblyHostBuilderExtensions.UseDefaultServiceProvider(this Microsoft.AspNetCore.Components.WebAssembly.Hosting.WebAssemblyHostBuilder! builder, System.Action! configure) -> Microsoft.AspNetCore.Components.WebAssembly.Hosting.WebAssemblyHostBuilder! static Microsoft.AspNetCore.Components.WebAssembly.HotReload.WebAssemblyHotReload.ApplyHotReloadDeltas(Microsoft.AspNetCore.Components.WebAssembly.HotReload.WebAssemblyHotReload.Delta[]! deltas, int loggingLevel) -> Microsoft.AspNetCore.Components.WebAssembly.HotReload.WebAssemblyHotReload.LogEntry[]! diff --git a/src/Components/WebAssembly/WebAssembly/test/Hosting/WebAssemblyHostBuilderTest.cs b/src/Components/WebAssembly/WebAssembly/test/Hosting/WebAssemblyHostBuilderTest.cs index 2dec1e6bc51b..901712a21240 100644 --- a/src/Components/WebAssembly/WebAssembly/test/Hosting/WebAssemblyHostBuilderTest.cs +++ b/src/Components/WebAssembly/WebAssembly/test/Hosting/WebAssemblyHostBuilderTest.cs @@ -98,12 +98,9 @@ public void Build_InDevelopment_ConfiguresWithServiceProviderWithScopeValidation builder.Services.AddScoped(); builder.Services.AddSingleton(); - // Act - var host = builder.Build(); - - // Assert - Assert.NotNull(host.Services.GetRequiredService()); - Assert.Throws(() => host.Services.GetRequiredService()); + // Act & Assert + var exception = Assert.Throws(() => builder.Build()); + Assert.Contains("Cannot consume scoped service", exception.Message); } [Fact] @@ -248,4 +245,69 @@ public void Builder_SupportsConfiguringLogging() Assert.Equal(provider.Object, loggerProvider); } + + [Fact] + public void UseDefaultServiceProvider_DetectsCircularDependencies() + { + // Arrange + var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods()); + + // Add a circular dependency + builder.Services.AddScoped(); + builder.Services.AddScoped(); + + // Act + builder.UseDefaultServiceProvider(options => + { + options.ValidateOnBuild = true; + }); + + // Assert + var exception = Assert.Throws(() => builder.Build()); + Assert.Contains("circular dependency", exception.Message.ToLowerInvariant()); + } + + [Fact] + public void UseDefaultServiceProvider_EnvironmentOverload_WorksCorrectly() + { + // Arrange + var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(environment: "Development")); + + // Act + builder.UseDefaultServiceProvider((env, options) => + { + options.ValidateOnBuild = env.IsDevelopment(); + }); + + var host = builder.Build(); + + // Assert + Assert.NotNull(host); + } + + [Fact] + public void DefaultServiceProviderOptions_InDevelopment_ValidatesOnBuild() + { + // Arrange + var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(environment: "Development")); + + // Add a circular dependency - should throw due to default ValidateOnBuild=true in development + builder.Services.AddScoped(); + builder.Services.AddScoped(); + + // Act & Assert + var exception = Assert.Throws(() => builder.Build()); + Assert.Contains("circular dependency", exception.Message.ToLowerInvariant()); + } + + // Helper classes for testing circular dependencies + private class CircularServiceA + { + public CircularServiceA(CircularServiceB serviceB) { } + } + + private class CircularServiceB + { + public CircularServiceB(CircularServiceA serviceA) { } + } }