Module.Apply() enumerated _features.Values directly while calling feature.Apply(). A feature whose Apply() introduces another feature — Module.Configure<T>() directly, or via a helper such as AddActivity<T>() which configures WorkflowManagementFeature — mutated that collection mid-enumeration and threw "Collection was modified; enumeration operation may not execute", naming nothing about features. Whether it fired depended on whether the other feature happened to be installed already, so a module built or did not based on unrelated host config. The module already treats introduction-during-apply as supported: the ConfigureFeature loop iterates a snapshot for exactly this reason, and Configure<T>() has an _isApplying branch that creates, resolves and configures a feature introduced mid-Apply. Only the final apply loop missed the same treatment, so make it tolerant rather than diagnose a constraint the code does not hold. The apply loop now runs in rounds until no new features appear, each round topologically sorted so a late feature's dependencies apply before it. Hosted services are registered in a single pass after that loop, then moved back to the index the block previously occupied: registering late is needed so features contributed during Apply() are included and ordered by priority, while keeping the position matters because features register hosted services directly from Apply() — WorkflowRuntimeFeature adds DrainOrchestratorHostedService that way — and module-managed services must keep starting first, or a priority such as ActivateTenants at -1 would silently start ordering after them. Adds Elsa.Features.UnitTests, covering the introduced feature applying, a three-deep introduction chain, dependency ordering, hosted service registration and priority ordering for late arrivals, the installed- feature registry, and no double-apply, plus guards for pre-existing ordering behaviour. Closes #7944 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
59 lines
2.2 KiB
C#
59 lines
2.2 KiB
C#
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.Hosting;
|
|
|
|
namespace Elsa.Features.Services;
|
|
|
|
/// <summary>
|
|
/// A thin abstraction on top of <see cref="IServiceCollection"/> to help organize features and dependencies.
|
|
/// </summary>
|
|
public interface IModule
|
|
{
|
|
/// <summary>
|
|
/// The service collection being populated.
|
|
/// </summary>
|
|
IServiceCollection Services { get; }
|
|
|
|
/// <summary>
|
|
/// A dictionary into which features can stash away values for later use.
|
|
/// </summary>
|
|
IDictionary<object, object> Properties { get; }
|
|
|
|
/// <summary>
|
|
/// Returns true if a feature of the specified type has been configured.
|
|
/// </summary>
|
|
bool HasFeature<T>() where T : class, IFeature;
|
|
|
|
/// <summary>
|
|
/// Returns true if a feature of the specified type has been configured.
|
|
/// </summary>
|
|
bool HasFeature(Type featureType);
|
|
|
|
/// <summary>
|
|
/// Creates and configures a feature of the specified type.
|
|
/// </summary>
|
|
T Configure<T>(Action<T>? configure = null) where T : class, IFeature;
|
|
|
|
/// <summary>
|
|
/// Creates and configures a feature of the specified type.
|
|
/// </summary>
|
|
T Configure<T>(Func<IModule, T> factory, Action<T>? configure = null) where T : class, IFeature;
|
|
|
|
/// <summary>
|
|
/// Configures a <see cref="IHostedService"/> using an optional priority to control in which order it will be registered with the service container.
|
|
/// </summary>
|
|
IModule ConfigureHostedService<T>(int priority = 0) where T : class, IHostedService;
|
|
|
|
/// <summary>
|
|
/// Configures a <see cref="IHostedService"/> using an optional priority to control in which order it will be registered with the service container.
|
|
/// </summary>
|
|
IModule ConfigureHostedService(Type hostedServiceType, int priority = 0);
|
|
|
|
/// <summary>
|
|
/// Will apply all configured features, causing the <see cref="Services"/> collection to be populated.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Features are allowed to introduce additional features from their own <see cref="IFeature.Apply"/> method, e.g. by calling <see cref="Configure{T}(Action{T})"/>.
|
|
/// Those features are configured and applied as part of the same call.
|
|
/// </remarks>
|
|
void Apply();
|
|
} |