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
@@ -0,0 +1,26 @@
using Microsoft.AspNetCore.Builder;
using Umbraco.Cms.Web.Common.ApplicationBuilder;

namespace Umbraco.Extensions;

/// <summary>
/// <see cref="IApplicationBuilder" /> extensions for the Umbraco Delivery API.
/// </summary>
public static class DeliveryApiApplicationBuilderExtensions
{
/// <summary>
/// Sets up routes for the Umbraco Delivery API.
/// </summary>
/// <remarks>
/// This method maps attribute-routed controllers including the Delivery API endpoints.
/// Call this when using <c>AddDeliveryApi()</c> without <c>AddBackOffice()</c>, as the
/// backoffice endpoints normally handle the controller mapping.
/// </remarks>
/// <param name="builder">The Umbraco endpoint builder context.</param>
/// <returns>The <see cref="IUmbracoEndpointBuilderContext" /> for chaining.</returns>
public static IUmbracoEndpointBuilderContext UseDeliveryApiEndpoints(this IUmbracoEndpointBuilderContext builder)
{
builder.EndpointRouteBuilder.MapControllers();
return builder;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,27 @@
using Umbraco.Cms.Core.DeliveryApi;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Notifications;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Infrastructure.Security;
using Umbraco.Cms.Web.Common.ApplicationBuilder;

namespace Umbraco.Extensions;

public static class UmbracoBuilderExtensions
{
/// <summary>
/// Add services for the Umbraco Delivery API (headless content delivery).
/// </summary>
/// <remarks>
/// This method assumes that either <c>AddBackOffice()</c> or <c>AddCore()</c> has already been called.
/// It registers Delivery API-specific services such as controllers, output caching, and member authentication.
/// </remarks>
/// <param name="builder">The Umbraco builder.</param>
/// <returns>The Umbraco builder.</returns>
public static IUmbracoBuilder AddDeliveryApi(this IUmbracoBuilder builder)
{
// Delivery API supports member authentication for protected content
builder.AddMembersIdentity();

builder.Services.AddScoped<IRequestStartItemProvider, RequestStartItemProvider>();
builder.Services.AddScoped<RequestContextOutputExpansionStrategy>();
builder.Services.AddScoped<RequestContextOutputExpansionStrategyV2>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Hosting;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Infrastructure.DependencyInjection;
using Umbraco.Cms.Web.Common.Hosting;

namespace Umbraco.Extensions;
Expand All @@ -15,28 +14,29 @@ namespace Umbraco.Extensions;
public static partial class UmbracoBuilderExtensions
{
/// <summary>
/// Adds all required components to run the Umbraco back office
/// Adds all required components to run the Umbraco back office.
/// </summary>
public static IUmbracoBuilder
AddBackOffice(this IUmbracoBuilder builder, Action<IMvcBuilder>? configureMvc = null) => builder
.AddConfiguration()
.AddUmbracoCore()
.AddWebComponents()
.AddHelpers()
.AddBackOfficeCore()
.AddBackOfficeIdentity()
.AddBackOfficeAuthentication()
.AddTokenRevocation()
.AddMembersIdentity()
.AddUmbracoProfiler()
.AddMvcAndRazor(configureMvc)
.AddBackgroundJobs()
.AddUmbracoHybridCache()
.AddDistributedCache()
.AddCoreNotifications();
/// <remarks>
/// This method calls <c>AddCore()</c> internally to register all core services,
/// then adds backoffice-specific services on top.
/// </remarks>
/// <param name="builder">The Umbraco builder.</param>
/// <param name="configureMvc">Optional action to configure the MVC builder.</param>
/// <returns>The Umbraco builder.</returns>
public static IUmbracoBuilder AddBackOffice(this IUmbracoBuilder builder, Action<IMvcBuilder>? configureMvc = null) =>
builder
.AddCore(configureMvc) // All core services
.AddBackOfficeCore() // Backoffice-specific: IBackOfficePathGenerator
.AddBackOfficeIdentity() // Backoffice user identity
.AddBackOfficeAuthentication() // OpenIddict, authorization policies
.AddTokenRevocation() // Token cleanup handlers
.AddMembersIdentity(); // Member identity (also needed for backoffice admin)

public static IUmbracoBuilder AddBackOfficeCore(this IUmbracoBuilder builder)
{
// Register marker indicating backoffice is enabled.
builder.Services.AddSingleton<IBackOfficeEnabledMarker, BackOfficeEnabledMarker>();

builder.Services.AddUnique<IBackOfficePathGenerator, UmbracoBackOfficePathGenerator>();
builder.Services.AddUnique<IPhysicalFileSystem>(factory =>
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,8 @@ private static BackOfficeIdentityBuilder BuildUmbracoBackOfficeIdentity(this IUm
services.TryAddScoped<IIpResolver, AspNetCoreIpResolver>();
services.TryAddSingleton<IBackOfficeExternalLoginProviders, BackOfficeExternalLoginProviders>();
// We need to know in the core services if local logins is denied, so we register the providers with a core friendly interface.
services.TryAddSingleton<ILocalLoginSettingProvider, BackOfficeExternalLoginProviders>();
// Use AddUnique to replace the default NoopLocalLoginSettingProvider registered in core services.
services.AddUnique<ILocalLoginSettingProvider, BackOfficeExternalLoginProviders>();
services.TryAddSingleton<IBackOfficeTwoFactorOptions, DefaultBackOfficeTwoFactorOptions>();
services.AddTransient<IDetailedTelemetryProvider, ExternalLoginTelemetryProvider>();

Expand Down
10 changes: 9 additions & 1 deletion src/Umbraco.Cms.Api.Management/ManagementApiComposer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,15 @@ namespace Umbraco.Cms.Api.Management;

public class ManagementApiComposer : IComposer
{
public void Compose(IUmbracoBuilder builder) =>
public void Compose(IUmbracoBuilder builder)
{
// Only register Management API services if backoffice is enabled.
if (builder.Services.Any(s => s.ServiceType == typeof(IBackOfficeEnabledMarker)) is false)
{
return;
}

builder.AddUmbracoManagementApi();
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,10 @@ private static IUmbracoBuilder AddInMemoryModelsRazorEngine(this IUmbracoBuilder
// Since these services expect the ModelsMode to be InMemoryAuto
if (builder.Config.GetModelsMode() == ModelsModeConstants.InMemoryAuto)
{
// Ensure ModelsBuilder services (including UmbracoServices) are registered.
// This is normally done by AddWebsite(), but in a delivery-API-only setup
// it may not have been called. AddModelsBuilder() is idempotent.
builder.AddModelsBuilder();
builder.Services.AddSingleton<UmbracoRazorReferenceManager>();
builder.Services.AddSingleton<CompilationOptionsProvider>();
builder.Services.AddSingleton<IViewCompilerProvider, UmbracoViewCompilerProvider>();
Expand Down
12 changes: 12 additions & 0 deletions src/Umbraco.Core/DependencyInjection/IBackOfficeEnabledMarker.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace Umbraco.Cms.Core.DependencyInjection;

/// <summary>
/// Marker interface indicating the backoffice is enabled.
/// Used to conditionally register Management API controllers and services.
/// </summary>
public interface IBackOfficeEnabledMarker { }

/// <summary>
/// Marker class implementation for <see cref="IBackOfficeEnabledMarker"/>.
/// </summary>
public sealed class BackOfficeEnabledMarker : IBackOfficeEnabledMarker { }
14 changes: 14 additions & 0 deletions src/Umbraco.Core/Security/NoopLocalLoginSettingProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
namespace Umbraco.Cms.Core.Security;

/// <summary>
/// A default implementation of <see cref="ILocalLoginSettingProvider"/> that always allows local login.
/// </summary>
/// <remarks>
/// This is used when the backoffice is not registered, as the backoffice provides its own implementation
/// that checks external login provider settings.
/// </remarks>
public sealed class NoopLocalLoginSettingProvider : ILocalLoginSettingProvider
{
/// <inheritdoc />
public bool HasDenyLocalLogin() => false;
}
18 changes: 18 additions & 0 deletions src/Umbraco.Core/Services/NoopConflictingRouteService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
namespace Umbraco.Cms.Core.Services;

/// <summary>
/// A default implementation of <see cref="IConflictingRouteService"/> that never reports conflicts.
/// </summary>
/// <remarks>
/// This is used when the backoffice is not registered, as route conflict detection
/// is primarily relevant for backoffice controller routes.
/// </remarks>
public sealed class NoopConflictingRouteService : IConflictingRouteService
{
/// <inheritdoc />
public bool HasConflictingRoutes(out string controllerName)
{
controllerName = string.Empty;
return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
using Umbraco.Cms.Infrastructure.Serialization;
using Umbraco.Cms.Infrastructure.Services.Implement;
using Umbraco.Extensions;
using Microsoft.Extensions.DependencyInjection.Extensions;
using IScopeProvider = Umbraco.Cms.Infrastructure.Scoping.IScopeProvider;

namespace Umbraco.Cms.Infrastructure.DependencyInjection;
Expand Down Expand Up @@ -248,6 +249,18 @@

builder.Services.AddUnique<IPasswordChanger<BackOfficeIdentityUser>, PasswordChanger<BackOfficeIdentityUser>>();
builder.Services.AddUnique<IPasswordChanger<MemberIdentityUser>, PasswordChanger<MemberIdentityUser>>();

// Register default local login setting provider (allows local login by default).
// This will be replaced by the backoffice implementation when AddBackOffice() is called.
builder.Services.TryAddSingleton<ILocalLoginSettingProvider, NoopLocalLoginSettingProvider>();

// Register default conflicting route service (no conflicts when backoffice not enabled).
// This will be replaced by the backoffice implementation when AddBackOffice() is called.
builder.Services.TryAddSingleton<IConflictingRouteService, NoopConflictingRouteService>();

// Register URL assembler (needed by DefaultMediaUrlProvider).
builder.Services.TryAddTransient<IUrlAssembler, DefaultUrlAssembler>();

Check warning on line 263 in src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs

View check run for this annotation

CodeScene Delta Analysis / CodeScene Code Health Review (main)

❌ Getting worse: Large Method

AddCoreInitialServices increases from 120 to 123 lines of code, threshold = 70. Large functions with many lines of code are generally harder to understand and lower the code health. Avoid adding more lines to this function.
builder.Services.AddTransient<IMemberEditingService, MemberEditingService>();

builder.Services.AddSingleton<IBlockEditorElementTypeCache, BlockEditorElementTypeCache>();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Services;
using Umbraco.Extensions;

Expand Down Expand Up @@ -73,7 +74,11 @@ public void RegisterDefaultRequiredMiddleware()

AppBuilder.UseUmbracoMediaFileProvider();

AppBuilder.UseUmbracoBackOfficeRewrites();
// Only use backoffice rewrites if backoffice is enabled
if (ApplicationServices.GetService<IBackOfficeEnabledMarker>() is not null)
{
AppBuilder.UseUmbracoBackOfficeRewrites();
}

AppBuilder.UseStaticFiles();

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.Data.Common;
using System.Net.Http.Headers;
using System.Reflection;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.DataProtection.Infrastructure;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
Expand Down Expand Up @@ -60,6 +61,7 @@
using Umbraco.Cms.Web.Common.Security;
using Umbraco.Cms.Web.Common.Templates;
using Umbraco.Cms.Web.Common.UmbracoContext;
using Umbraco.Cms.Web.Common.Authorization;
using IHostingEnvironment = Umbraco.Cms.Core.Hosting.IHostingEnvironment;

namespace Umbraco.Extensions;
Expand All @@ -71,6 +73,59 @@ namespace Umbraco.Extensions;
/// </summary>
public static partial class UmbracoBuilderExtensions
{
/// <summary>
/// Adds all core Umbraco services required to run without the backoffice.
/// Use this for delivery-only scenarios (Delivery API, Website) without the management backoffice.
/// For full Umbraco with backoffice, use <c>AddBackOffice()</c> instead.
/// </summary>
/// <remarks>
/// This method is idempotent - calling it multiple times has no effect after the first call.
/// The individual service registration methods are also idempotent, so calling both
/// <c>AddBackOffice()</c> and <see cref="AddCore"/> is safe (though not expected).
/// </remarks>
/// <param name="builder">The Umbraco builder.</param>
/// <param name="configureMvc">Optional action to configure the MVC builder.</param>
/// <returns>The Umbraco builder.</returns>
public static IUmbracoBuilder AddCore(this IUmbracoBuilder builder, Action<IMvcBuilder>? configureMvc = null)
{
// Idempotency check - safe to call multiple times.
if (builder.Services.Any(s => s.ServiceType == typeof(AddCoreMarker)))
{
return builder;
}

builder.Services.AddSingleton<AddCoreMarker>();

// Register the feature authorization handler and policy.
// This enables the UmbracoFeatureEnabled policy used by Delivery API and other controllers.
// Use TryAddEnumerable because IAuthorizationHandler is a multi-registration service -
// TryAddSingleton would skip registration if ANY handler exists, not just this specific one.
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IAuthorizationHandler, FeatureAuthorizeHandler>());
builder.Services.AddAuthorization(options =>
{
// Only add the policy if it doesn't already exist.
if (options.GetPolicy(AuthorizationPolicies.UmbracoFeatureEnabled) is null)
{
options.AddPolicy(AuthorizationPolicies.UmbracoFeatureEnabled, policy =>
{
policy.Requirements.Add(new FeatureAuthorizeRequirement());
});
}
});
Comment thread
AndyButland marked this conversation as resolved.

return builder
.AddConfiguration()
.AddUmbracoCore()
.AddWebComponents()
.AddHelpers()
.AddUmbracoProfiler()
.AddMvcAndRazor(configureMvc)
.AddBackgroundJobs()
.AddUmbracoHybridCache()
.AddDistributedCache()
.AddCoreNotifications();
}

/// <summary>
/// Creates an <see cref="IUmbracoBuilder" /> and registers basic Umbraco services
/// </summary>
Expand Down Expand Up @@ -207,6 +262,10 @@ private static IUmbracoBuilder AddHttpClients(this IUmbracoBuilder builder)

public static IUmbracoBuilder AddMvcAndRazor(this IUmbracoBuilder builder, Action<IMvcBuilder>? mvcBuilding = null)
{
// NOTE: AddControllersWithViews() is already idempotent for service registration.
// We intentionally do NOT add an idempotency check here because the mvcBuilding callback
// may contain important configuration (e.g., Razor runtime compilation) that needs to
// be applied even if MVC services were already registered by a previous call.
// TODO: We need to figure out if we can work around this because calling AddControllersWithViews modifies the global app and order is very important
// this will directly affect developers who need to call that themselves.
IMvcBuilder mvcBuilder = builder.Services.AddControllersWithViews();
Expand Down Expand Up @@ -335,4 +394,11 @@ private static IHostingEnvironment GetTemporaryHostingEnvironment(
wrappedWebRoutingSettings,
webHostEnvironment);
}

/// <summary>
/// Marker class to ensure AddCore is only executed once.
/// </summary>
private sealed class AddCoreMarker
{
}
}
12 changes: 12 additions & 0 deletions src/Umbraco.Web.UI/Composers/ControllersAsServicesComposer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using Umbraco.Cms.Web.Website.Controllers;
using Umbraco.Extensions;
using IHostingEnvironment = Umbraco.Cms.Core.Hosting.IHostingEnvironment;
using IBackOfficeEnabledMarker = Umbraco.Cms.Core.DependencyInjection.IBackOfficeEnabledMarker;

namespace Umbraco.Cms.Web.UI.Composers
{
Expand Down Expand Up @@ -53,8 +54,19 @@ public static IMvcBuilder AddControllersAsServicesWithoutChangingActivator(this
var feature = new ControllerFeature();
builder.PartManager.PopulateFeature(feature);

// Check if backoffice is enabled via marker interface.
bool backofficeEnabled = builder.Services
.Any(s => s.ServiceType == typeof(IBackOfficeEnabledMarker));

foreach (Type controller in feature.Controllers.Select(c => c.AsType()))
{
// Skip Management API controllers if backoffice not enabled.
if (backofficeEnabled is false &&
controller.Assembly.GetName().Name?.StartsWith("Umbraco.Cms.Api.Management", StringComparison.Ordinal) == true)
{
continue;
}

builder.Services.TryAddTransient(controller, controller);
}

Expand Down
Loading
Loading