Skip to content

Commit f469cdc

Browse files
authored
Add UseDefaultServiceProvider extension methods to fix circular DI dependencies (#62629)
1 parent f7a9e67 commit f469cdc

File tree

4 files changed

+138
-7
lines changed

4 files changed

+138
-7
lines changed

src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHostBuilder.cs

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ public sealed class WebAssemblyHostBuilder
3030
private Func<IServiceProvider> _createServiceProvider;
3131
private RootTypeCache? _rootComponentCache;
3232
private string? _persistedState;
33+
private ServiceProviderOptions? _serviceProviderOptions;
3334

3435
/// <summary>
3536
/// Creates an instance of <see cref="WebAssemblyHostBuilder"/> using the most common
@@ -91,7 +92,16 @@ internal WebAssemblyHostBuilder(IInternalJSImportMethods jsMethods)
9192

9293
_createServiceProvider = () =>
9394
{
94-
return Services.BuildServiceProvider(validateScopes: WebAssemblyHostEnvironmentExtensions.IsDevelopment(hostEnvironment));
95+
var isDevelopment = WebAssemblyHostEnvironmentExtensions.IsDevelopment(hostEnvironment);
96+
97+
// Use custom options if provided, otherwise use defaults
98+
var options = _serviceProviderOptions ?? new ServiceProviderOptions
99+
{
100+
ValidateScopes = isDevelopment,
101+
ValidateOnBuild = isDevelopment
102+
};
103+
104+
return Services.BuildServiceProvider(options);
95105
};
96106
}
97107

@@ -276,6 +286,17 @@ public void ConfigureContainer<TBuilder>(IServiceProviderFactory<TBuilder> facto
276286
};
277287
}
278288

289+
// In WebAssemblyHostBuilder class:
290+
/// <summary>
291+
/// Configures the service provider options for this host builder.
292+
/// </summary>
293+
/// <param name="options">The service provider options to use.</param>
294+
public WebAssemblyHostBuilder UseServiceProviderOptions(ServiceProviderOptions options)
295+
{
296+
_serviceProviderOptions = options ?? throw new ArgumentNullException(nameof(options));
297+
return this;
298+
}
299+
279300
/// <summary>
280301
/// Builds a <see cref="WebAssemblyHost"/> instance based on the configuration of this builder.
281302
/// </summary>
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Microsoft.Extensions.DependencyInjection;
5+
6+
namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting;
7+
8+
/// <summary>
9+
/// Extension methods for configuring a <see cref="WebAssemblyHostBuilder"/>.
10+
/// </summary>
11+
public static class WebAssemblyHostBuilderExtensions
12+
{
13+
/// <summary>
14+
/// Configures the default service provider for the WebAssembly host.
15+
/// </summary>
16+
/// <param name="builder">The <see cref="WebAssemblyHostBuilder"/> to configure.</param>
17+
/// <param name="configure">A callback used to configure the <see cref="ServiceProviderOptions"/>.</param>
18+
/// <returns>The <see cref="WebAssemblyHostBuilder"/>.</returns>
19+
public static WebAssemblyHostBuilder UseDefaultServiceProvider(
20+
this WebAssemblyHostBuilder builder,
21+
Action<ServiceProviderOptions> configure)
22+
{
23+
return builder.UseDefaultServiceProvider((env, options) => configure(options));
24+
}
25+
26+
/// <summary>
27+
/// Configures the default service provider for the WebAssembly host.
28+
/// </summary>
29+
/// <param name="builder">The <see cref="WebAssemblyHostBuilder"/> to configure.</param>
30+
/// <param name="configure">A callback used to configure the <see cref="ServiceProviderOptions"/> with access to the host environment.</param>
31+
/// <returns>The <see cref="WebAssemblyHostBuilder"/>.</returns>
32+
public static WebAssemblyHostBuilder UseDefaultServiceProvider(
33+
this WebAssemblyHostBuilder builder,
34+
Action<IWebAssemblyHostEnvironment, ServiceProviderOptions> configure)
35+
{
36+
ArgumentNullException.ThrowIfNull(builder);
37+
ArgumentNullException.ThrowIfNull(configure);
38+
39+
var options = new ServiceProviderOptions();
40+
configure(builder.HostEnvironment, options);
41+
42+
return builder.UseServiceProviderOptions(options);
43+
}
44+
}

src/Components/WebAssembly/WebAssembly/src/PublicAPI.Unshipped.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
#nullable enable
2+
Microsoft.AspNetCore.Components.WebAssembly.Hosting.WebAssemblyHostBuilder.UseServiceProviderOptions(Microsoft.Extensions.DependencyInjection.ServiceProviderOptions! options) -> Microsoft.AspNetCore.Components.WebAssembly.Hosting.WebAssemblyHostBuilder!
3+
Microsoft.AspNetCore.Components.WebAssembly.Hosting.WebAssemblyHostBuilderExtensions
24
Microsoft.AspNetCore.Components.WebAssembly.HotReload.WebAssemblyHotReload.Delta
35
Microsoft.AspNetCore.Components.WebAssembly.HotReload.WebAssemblyHotReload.Delta.Delta() -> void
46
Microsoft.AspNetCore.Components.WebAssembly.HotReload.WebAssemblyHotReload.Delta.ILDelta.get -> byte[]!
@@ -17,4 +19,6 @@ Microsoft.AspNetCore.Components.WebAssembly.HotReload.WebAssemblyHotReload.LogEn
1719
Microsoft.AspNetCore.Components.WebAssembly.HotReload.WebAssemblyHotReload.LogEntry.Message.init -> void
1820
Microsoft.AspNetCore.Components.WebAssembly.HotReload.WebAssemblyHotReload.LogEntry.Severity.get -> int
1921
Microsoft.AspNetCore.Components.WebAssembly.HotReload.WebAssemblyHotReload.LogEntry.Severity.init -> void
22+
static Microsoft.AspNetCore.Components.WebAssembly.Hosting.WebAssemblyHostBuilderExtensions.UseDefaultServiceProvider(this Microsoft.AspNetCore.Components.WebAssembly.Hosting.WebAssemblyHostBuilder! builder, System.Action<Microsoft.AspNetCore.Components.WebAssembly.Hosting.IWebAssemblyHostEnvironment!, Microsoft.Extensions.DependencyInjection.ServiceProviderOptions!>! configure) -> Microsoft.AspNetCore.Components.WebAssembly.Hosting.WebAssemblyHostBuilder!
23+
static Microsoft.AspNetCore.Components.WebAssembly.Hosting.WebAssemblyHostBuilderExtensions.UseDefaultServiceProvider(this Microsoft.AspNetCore.Components.WebAssembly.Hosting.WebAssemblyHostBuilder! builder, System.Action<Microsoft.Extensions.DependencyInjection.ServiceProviderOptions!>! configure) -> Microsoft.AspNetCore.Components.WebAssembly.Hosting.WebAssemblyHostBuilder!
2024
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[]!

src/Components/WebAssembly/WebAssembly/test/Hosting/WebAssemblyHostBuilderTest.cs

Lines changed: 68 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -98,12 +98,9 @@ public void Build_InDevelopment_ConfiguresWithServiceProviderWithScopeValidation
9898
builder.Services.AddScoped<StringBuilder>();
9999
builder.Services.AddSingleton<TestServiceThatTakesStringBuilder>();
100100

101-
// Act
102-
var host = builder.Build();
103-
104-
// Assert
105-
Assert.NotNull(host.Services.GetRequiredService<StringBuilder>());
106-
Assert.Throws<InvalidOperationException>(() => host.Services.GetRequiredService<TestServiceThatTakesStringBuilder>());
101+
// Act & Assert
102+
var exception = Assert.Throws<AggregateException>(() => builder.Build());
103+
Assert.Contains("Cannot consume scoped service", exception.Message);
107104
}
108105

109106
[Fact]
@@ -248,4 +245,69 @@ public void Builder_SupportsConfiguringLogging()
248245
Assert.Equal<ILoggerProvider>(provider.Object, loggerProvider);
249246

250247
}
248+
249+
[Fact]
250+
public void UseDefaultServiceProvider_DetectsCircularDependencies()
251+
{
252+
// Arrange
253+
var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods());
254+
255+
// Add a circular dependency
256+
builder.Services.AddScoped<CircularServiceA>();
257+
builder.Services.AddScoped<CircularServiceB>();
258+
259+
// Act
260+
builder.UseDefaultServiceProvider(options =>
261+
{
262+
options.ValidateOnBuild = true;
263+
});
264+
265+
// Assert
266+
var exception = Assert.Throws<AggregateException>(() => builder.Build());
267+
Assert.Contains("circular dependency", exception.Message.ToLowerInvariant());
268+
}
269+
270+
[Fact]
271+
public void UseDefaultServiceProvider_EnvironmentOverload_WorksCorrectly()
272+
{
273+
// Arrange
274+
var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(environment: "Development"));
275+
276+
// Act
277+
builder.UseDefaultServiceProvider((env, options) =>
278+
{
279+
options.ValidateOnBuild = env.IsDevelopment();
280+
});
281+
282+
var host = builder.Build();
283+
284+
// Assert
285+
Assert.NotNull(host);
286+
}
287+
288+
[Fact]
289+
public void DefaultServiceProviderOptions_InDevelopment_ValidatesOnBuild()
290+
{
291+
// Arrange
292+
var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(environment: "Development"));
293+
294+
// Add a circular dependency - should throw due to default ValidateOnBuild=true in development
295+
builder.Services.AddScoped<CircularServiceA>();
296+
builder.Services.AddScoped<CircularServiceB>();
297+
298+
// Act & Assert
299+
var exception = Assert.Throws<AggregateException>(() => builder.Build());
300+
Assert.Contains("circular dependency", exception.Message.ToLowerInvariant());
301+
}
302+
303+
// Helper classes for testing circular dependencies
304+
private class CircularServiceA
305+
{
306+
public CircularServiceA(CircularServiceB serviceB) { }
307+
}
308+
309+
private class CircularServiceB
310+
{
311+
public CircularServiceB(CircularServiceA serviceA) { }
312+
}
251313
}

0 commit comments

Comments
 (0)