diff --git a/src/Nethermind/Nethermind.Core.Test/Container/OrderedComponentsTests.cs b/src/Nethermind/Nethermind.Core.Test/Container/OrderedComponentsTests.cs new file mode 100644 index 00000000000..5aa6cfa3dbd --- /dev/null +++ b/src/Nethermind/Nethermind.Core.Test/Container/OrderedComponentsTests.cs @@ -0,0 +1,76 @@ +// SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System; +using System.Collections.Generic; +using System.Linq; +using Autofac; +using FluentAssertions; +using Nethermind.Core.Container; +using NUnit.Framework; + +namespace Nethermind.Core.Test.Container; + +public class OrderedComponentsTests +{ + [Test] + public void TestNestedModuleConsistency() + { + using IContainer ctx = new ContainerBuilder() + .AddModule(new ModuleA()) + .AddLast(_ => new Item("4")) + .Build(); + + ctx.Resolve().Select(item => item.Name).Should().BeEquivalentTo(["1", "2", "3", "4"]); + ctx.Resolve>().Select(item => item.Name).Should().BeEquivalentTo(["1", "2", "3", "4"]); + ctx.Resolve>().Select(item => item.Name).Should().BeEquivalentTo(["1", "2", "3", "4"]); + } + + [Test] + public void TestAddFirst() + { + using IContainer ctx = new ContainerBuilder() + .AddLast(_ => new Item("2")) + .AddLast(_ => new Item("3")) + .AddFirst(_ => new Item("1")) + .Build(); + + ctx.Resolve().Select(item => item.Name).Should().BeEquivalentTo(["1", "2", "3"]); + } + + [Test] + public void TestDisallowIndividualRegistration() + { + Action act = () => new ContainerBuilder() + .AddLast(_ => new Item("1")) + .AddSingleton(_ => new Item("2")) + .Build(); + + act.Should().Throw(); + } + + private class ModuleA : Module + { + protected override void Load(ContainerBuilder builder) => + builder + .AddModule(new ModuleB()) + .AddLast(_ => new Item("3")); + } + + private class ModuleB : Module + { + protected override void Load(ContainerBuilder builder) => + builder + .AddModule(new ModuleC()) + .AddLast(_ => new Item("2")); + } + + + private class ModuleC : Module + { + protected override void Load(ContainerBuilder builder) => + builder + .AddLast(_ => new Item("1")); + } + private record Item(string Name); +} diff --git a/src/Nethermind/Nethermind.Core/Container/OrderedComponents.cs b/src/Nethermind/Nethermind.Core/Container/OrderedComponents.cs new file mode 100644 index 00000000000..4315b5c6f8a --- /dev/null +++ b/src/Nethermind/Nethermind.Core/Container/OrderedComponents.cs @@ -0,0 +1,22 @@ +// SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System.Collections.Generic; + +namespace Nethermind.Core.Container; + +public class OrderedComponents +{ + private IList _components = new List(); + public IEnumerable Components => _components; + + public void AddLast(T item) + { + _components.Add(item); + } + + public void AddFirst(T item) + { + _components.Insert(0, item); + } +} diff --git a/src/Nethermind/Nethermind.Core/Container/OrderedComponentsContainerBuilderExtensions.cs b/src/Nethermind/Nethermind.Core/Container/OrderedComponentsContainerBuilderExtensions.cs new file mode 100644 index 00000000000..be286cedbe8 --- /dev/null +++ b/src/Nethermind/Nethermind.Core/Container/OrderedComponentsContainerBuilderExtensions.cs @@ -0,0 +1,74 @@ +// SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System; +using System.Collections.Generic; +using System.Linq; +using Autofac; +using Autofac.Core; + +namespace Nethermind.Core.Container; + +/// +/// A set of dsl to register components where the order matter. Internally it has an +/// and uses decorators to add the items. The order of invocation matter, but there is explicit method like +/// allowing component that should appear first to appear first. +/// +public static class OrderedComponentsContainerBuilderExtensions +{ + public static ContainerBuilder AddLast(this ContainerBuilder builder, Func factory) => + builder + .EnsureOrderedComponents() + .AddDecorator>((ctx, orderedComponents) => + { + orderedComponents.AddLast(factory(ctx)); + return orderedComponents; + }); + + public static ContainerBuilder AddFirst(this ContainerBuilder builder, Func factory) => + builder + .EnsureOrderedComponents() + .AddDecorator>((ctx, orderedComponents) => + { + orderedComponents.AddFirst(factory(ctx)); + return orderedComponents; + }); + + private static ContainerBuilder EnsureOrderedComponents(this ContainerBuilder builder) + { + string registeredMarker = $"Registerd OrderedComponents For {typeof(T).Name}"; + if (!builder.Properties.TryAdd(registeredMarker, null)) + { + return builder; + } + + // Prevent registering separately which has no explicit ordering + builder.RegisterBuildCallback(scope => + { + if (scope.ComponentRegistry.ServiceRegistrationsFor(new TypedService(typeof(T))).Any()) + { + throw new InvalidOperationException( + $"Service of type {typeof(T).Name} must only be registered with one of DSL in {nameof(OrderedComponentsContainerBuilderExtensions)}"); + } + }); + + // Not a singleton which allow it to work seamlessly with scoped lifetime with additional component + builder.Add>(); + + builder + .Register((ctx) => ctx.Resolve>().Components) + .As>() + .Fixed(); + + builder.Register((ctx) => ctx.Resolve>().Components.ToArray()) + .As>() + .As() + .Fixed(); + + builder.Register((ctx) => ctx.Resolve>().Components.ToList()) + .As>() + .Fixed(); + + return builder; + } +} diff --git a/src/Nethermind/Nethermind.Core/ContainerBuilderExtensions.cs b/src/Nethermind/Nethermind.Core/ContainerBuilderExtensions.cs index e66ed9ec35d..b33a3e412c4 100644 --- a/src/Nethermind/Nethermind.Core/ContainerBuilderExtensions.cs +++ b/src/Nethermind/Nethermind.Core/ContainerBuilderExtensions.cs @@ -7,6 +7,7 @@ using Autofac; using Autofac.Builder; using Autofac.Core; +using Autofac.Core.Registration; using Autofac.Core.Resolving.Pipeline; using Autofac.Features.AttributeFilters; using Nethermind.Core.Container; @@ -478,6 +479,13 @@ private static Func CreateArgResolver(MethodInfo method } return (ctx) => ctx.Resolve(); } + + public static IRegistrationBuilder Fixed(this IRegistrationBuilder reg) + { + // Fixed registration is one where it is always the default. Can't be overridden by later registration. + reg.RegistrationData.Options |= RegistrationOptions.Fixed; + return reg; + } } /// diff --git a/src/Nethermind/Nethermind.JsonRpc/Modules/IContainerBuilderExtensions.cs b/src/Nethermind/Nethermind.JsonRpc/Modules/IContainerBuilderExtensions.cs index fb38935c7dd..29bd8e076b9 100644 --- a/src/Nethermind/Nethermind.JsonRpc/Modules/IContainerBuilderExtensions.cs +++ b/src/Nethermind/Nethermind.JsonRpc/Modules/IContainerBuilderExtensions.cs @@ -4,6 +4,7 @@ using System; using Autofac; using Nethermind.Core; +using Nethermind.Core.Container; namespace Nethermind.JsonRpc.Modules; @@ -19,7 +20,7 @@ public static ContainerBuilder RegisterSingletonJsonRpcModule(this Con public static ContainerBuilder RegisterSingletonJsonRpcModule(this ContainerBuilder builder) where T : IRpcModule { return builder - .AddSingleton((ctx) => + .AddLast((ctx) => { Lazy instance = ctx.Resolve>(); return new RpcModuleInfo(typeof(T), new LazyModulePool(new Lazy>(() => @@ -39,7 +40,7 @@ public static ContainerBuilder RegisterBoundedJsonRpcModule( return builder .AddSingleton() .AddSingleton, TFactory>() - .AddSingleton((ctx) => + .AddLast((ctx) => { Lazy> factory = ctx.Resolve>>(); return new RpcModuleInfo(typeof(T), new LazyModulePool(new Lazy>(() =>