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,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<Item[]>().Select(item => item.Name).Should().BeEquivalentTo(["1", "2", "3", "4"]);
ctx.Resolve<IEnumerable<Item>>().Select(item => item.Name).Should().BeEquivalentTo(["1", "2", "3", "4"]);
ctx.Resolve<IReadOnlyList<Item>>().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<Item[]>().Select(item => item.Name).Should().BeEquivalentTo(["1", "2", "3"]);
}

[Test]
public void TestDisallowIndividualRegistration()
{
Action act = () => new ContainerBuilder()
.AddLast(_ => new Item("1"))
.AddSingleton<Item>(_ => new Item("2"))
.Build();

act.Should().Throw<InvalidOperationException>();
}

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);
}
22 changes: 22 additions & 0 deletions src/Nethermind/Nethermind.Core/Container/OrderedComponents.cs
Original file line number Diff line number Diff line change
@@ -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<T>
{
private IList<T> _components = new List<T>();
public IEnumerable<T> Components => _components;

public void AddLast(T item)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just wondering, this API isn't enforcing which will be the last element? In case of two consecutive AddLast calls, the first call becomes effectively invalid and it may get overridden. Would it make sense to somehow enforce calling these functions only once?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ow, I did not expect that use case.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess that can make sense for other use case, but in this case, there are other rpc module also. We just need taiko to run after merge.

{
_components.Add(item);
}

public void AddFirst(T item)
{
_components.Insert(0, item);
}
}
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// A set of dsl to register components where the order matter. Internally it has an <see cref="OrderedComponents{T}"/>
/// and uses decorators to add the items. The order of invocation matter, but there is explicit method like <see cref="AddFirst{T}"/>
/// allowing component that should appear first to appear first.
/// </summary>
public static class OrderedComponentsContainerBuilderExtensions
{
public static ContainerBuilder AddLast<T>(this ContainerBuilder builder, Func<IComponentContext, T> factory) =>
builder
.EnsureOrderedComponents<T>()
.AddDecorator<OrderedComponents<T>>((ctx, orderedComponents) =>
{
orderedComponents.AddLast(factory(ctx));
return orderedComponents;
});

public static ContainerBuilder AddFirst<T>(this ContainerBuilder builder, Func<IComponentContext, T> factory) =>
builder
.EnsureOrderedComponents<T>()
.AddDecorator<OrderedComponents<T>>((ctx, orderedComponents) =>
{
orderedComponents.AddFirst(factory(ctx));
return orderedComponents;
});

private static ContainerBuilder EnsureOrderedComponents<T>(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<OrderedComponents<T>>();

builder
.Register((ctx) => ctx.Resolve<OrderedComponents<T>>().Components)
.As<IEnumerable<T>>()
.Fixed();

builder.Register((ctx) => ctx.Resolve<OrderedComponents<T>>().Components.ToArray())
.As<IReadOnlyList<T>>()
.As<T[]>()
.Fixed();

builder.Register((ctx) => ctx.Resolve<OrderedComponents<T>>().Components.ToList())
.As<IList<T>>()
.Fixed();

return builder;
}
}
8 changes: 8 additions & 0 deletions src/Nethermind/Nethermind.Core/ContainerBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -478,6 +479,13 @@ private static Func<IComponentContext, T> CreateArgResolver<T>(MethodInfo method
}
return (ctx) => ctx.Resolve<T>();
}

public static IRegistrationBuilder<T, TAct, TStyle> Fixed<T, TAct, TStyle>(this IRegistrationBuilder<T, TAct, TStyle> 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;
}
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System;
using Autofac;
using Nethermind.Core;
using Nethermind.Core.Container;

namespace Nethermind.JsonRpc.Modules;

Expand All @@ -19,7 +20,7 @@ public static ContainerBuilder RegisterSingletonJsonRpcModule<T, TImpl>(this Con
public static ContainerBuilder RegisterSingletonJsonRpcModule<T>(this ContainerBuilder builder) where T : IRpcModule
{
return builder
.AddSingleton<RpcModuleInfo>((ctx) =>
.AddLast<RpcModuleInfo>((ctx) =>
{
Lazy<T> instance = ctx.Resolve<Lazy<T>>();
return new RpcModuleInfo(typeof(T), new LazyModulePool<T>(new Lazy<IRpcModulePool<T>>(() =>
Expand All @@ -39,7 +40,7 @@ public static ContainerBuilder RegisterBoundedJsonRpcModule<T, TFactory>(
return builder
.AddSingleton<TFactory>()
.AddSingleton<IRpcModuleFactory<T>, TFactory>()
.AddSingleton<RpcModuleInfo>((ctx) =>
.AddLast<RpcModuleInfo>((ctx) =>
{
Lazy<IRpcModuleFactory<T>> factory = ctx.Resolve<Lazy<IRpcModuleFactory<T>>>();
return new RpcModuleInfo(typeof(T), new LazyModulePool<T>(new Lazy<IRpcModulePool<T>>(() =>
Expand Down