Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,14 @@ public static IUmbracoBuilder AddMvcAndRazor(this IUmbracoBuilder builder, Actio
/// </summary>
public static IUmbracoBuilder AddWebComponents(this IUmbracoBuilder builder)
{
// Idempotency check - safe to call multiple times.
if (builder.Services.Any(s => s.ServiceType == typeof(AddWebComponentsMarker)))
{
return builder;
}

builder.Services.AddSingleton<AddWebComponentsMarker>();

// Add service session
// This can be overwritten by the user by adding their own call to AddSession
// since the last call of AddSession take precedence
Expand Down Expand Up @@ -401,4 +409,11 @@ private static IHostingEnvironment GetTemporaryHostingEnvironment(
private sealed class AddCoreMarker
{
}

/// <summary>
/// Marker class to ensure AddWebComponents is only executed once.
/// </summary>
private sealed class AddWebComponentsMarker
{
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using NUnit.Framework;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Composing;
Expand Down Expand Up @@ -374,4 +375,51 @@ public void DeliveryOnlyScenario_RegistersAllRequiredServices()
Is.False,
"IBackOfficeEnabledMarker should NOT be registered in delivery-only scenario");
}

/// <summary>
/// Verifies that calling AddWebComponents() explicitly after AddBackOffice() is safe.
/// Regression test for https://github.com/umbraco/Umbraco-CMS/issues/22344.
/// </summary>
[Test]
public void AddWebComponents_IsIdempotent()
{
// Arrange
var services = new ServiceCollection();
var configuration = new ConfigurationBuilder()
.AddInMemoryCollection(InMemoryConfiguration)
.Build();

services.AddSingleton<IConfiguration>(configuration);

TypeLoader typeLoader = services.AddTypeLoader(
GetType().Assembly,
TestHelper.ConsoleLoggerFactory,
configuration);

var builder = new UmbracoBuilder(
services,
configuration,
typeLoader,
TestHelper.ConsoleLoggerFactory,
TestHelper.Profiler,
AppCaches.NoCache);

// Act - This is the exact chain from the issue that caused duplicate health check crash.
// AddBackOffice() calls AddCore() which calls AddWebComponents() internally,
// so the explicit AddWebComponents() call is a duplicate.
builder
.AddBackOffice()
.AddWebsite()
.AddDeliveryApi()
.AddWebComponents()
.AddUmbracoSqlServerSupport()
.AddUmbracoSqliteSupport();

builder.Build();

// Assert - Verify health checks can be resolved without duplicate name errors.
// The duplicate "umbraco-ready" registration caused ArgumentException at host startup.
var provider = services.BuildServiceProvider();
Assert.DoesNotThrow(() => provider.GetRequiredService<HealthCheckService>());
}
}
Loading