Skip to content

Commit ae34fe5

Browse files
thomhurstclaude
andcommitted
refactor: Reduce reflection usage with compiled delegates and static abstracts (#1424)
Replace runtime reflection patterns with compile-time alternatives for improved performance: 1. **ParallelLimitProvider**: Use static abstract interface members (.NET 7+) instead of Activator.CreateInstance. IParallelLimit.Limit is now a static abstract property. 2. **ModuleRunner**: Create compiled delegate factories using expression trees: - ExecutionContextFactory: Cached delegates for ModuleExecutionContext<T> creation - ModuleExecutionDelegateFactory: Cached delegates for ExecuteAsync<T> calls - ModuleResultFactory: Cached delegates for ModuleResult<T> creation - ResultRepositoryDelegateFactory: Cached delegates for result retrieval 3. **ModuleLoggerProvider**: Use AsyncLocal<Type?> to track current module type, avoiding expensive stack trace inspection in most logging scenarios. 4. **OptionsProvider**: Cache compiled property accessors using expression trees, replacing GetProperty("Value") reflection on the hot path. BREAKING CHANGE: IParallelLimit.Limit changed from instance property to static abstract. Implementations must update from `public int Limit => N;` to `public static int Limit => N;` Closes #1424 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 8cbef48 commit ae34fe5

17 files changed

Lines changed: 503 additions & 112 deletions

docs/docs/how-to/parallelization.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ public class BuildProjectModule : Module
121121

122122
public record MyParallelLimit : IParallelLimit
123123
{
124-
public int Limit => 2;
124+
public static int Limit => 2;
125125
}
126126
```
127127

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,56 @@
1-
using ModularPipelines.Interfaces;
1+
using ModularPipelines.Helpers;
2+
using ModularPipelines.Interfaces;
3+
using Semaphores;
24

35
namespace ModularPipelines.Attributes;
46

7+
/// <summary>
8+
/// Specifies a parallel execution limit for a module using a strongly-typed limit class.
9+
/// </summary>
10+
/// <typeparam name="TParallelLimit">The type implementing <see cref="IParallelLimit"/>.</typeparam>
511
[AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Class | AttributeTargets.Method)]
612
public sealed class ParallelLimiterAttribute<TParallelLimit> : ParallelLimiterAttribute
7-
where TParallelLimit : IParallelLimit, new()
13+
where TParallelLimit : IParallelLimit
814
{
915
public ParallelLimiterAttribute() : base(typeof(TParallelLimit))
1016
{
1117
}
18+
19+
/// <inheritdoc />
20+
public override AsyncSemaphore GetLock(IParallelLimitProvider provider)
21+
{
22+
return provider.GetLock<TParallelLimit>();
23+
}
1224
}
1325

14-
public class ParallelLimiterAttribute : Attribute
26+
/// <summary>
27+
/// Base attribute for specifying parallel execution limits.
28+
/// </summary>
29+
public abstract class ParallelLimiterAttribute : Attribute
1530
{
31+
/// <summary>
32+
/// Gets the type implementing <see cref="IParallelLimit"/>.
33+
/// </summary>
1634
public Type Type { get; }
1735

18-
public ParallelLimiterAttribute(Type type)
36+
/// <summary>
37+
/// Initializes a new instance of the <see cref="ParallelLimiterAttribute"/> class.
38+
/// </summary>
39+
/// <param name="type">The type implementing <see cref="IParallelLimit"/>.</param>
40+
protected ParallelLimiterAttribute(Type type)
1941
{
2042
if (!type.IsAssignableTo(typeof(IParallelLimit)))
2143
{
22-
throw new Exception("Type must be of IParallelLimit");
44+
throw new ArgumentException("Type must implement IParallelLimit", nameof(type));
2345
}
2446

2547
Type = type;
2648
}
49+
50+
/// <summary>
51+
/// Gets the semaphore lock from the provider without reflection.
52+
/// </summary>
53+
/// <param name="provider">The parallel limit provider.</param>
54+
/// <returns>The semaphore for this limit type.</returns>
55+
public abstract AsyncSemaphore GetLock(IParallelLimitProvider provider);
2756
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
using System.Collections.Concurrent;
2+
using System.Linq.Expressions;
3+
using ModularPipelines.Models;
4+
using ModularPipelines.Modules;
5+
6+
namespace ModularPipelines.Engine.Execution;
7+
8+
/// <summary>
9+
/// Factory for creating ModuleExecutionContext instances without reflection.
10+
/// </summary>
11+
/// <remarks>
12+
/// Replaces Activator.CreateInstance with compiled expression trees for better performance.
13+
/// </remarks>
14+
internal static class ExecutionContextFactory
15+
{
16+
/// <summary>
17+
/// Delegate signature for creating a ModuleExecutionContext.
18+
/// </summary>
19+
internal delegate ModuleExecutionContext CreateContextDelegate(IModule module, Type moduleType);
20+
21+
private static readonly ConcurrentDictionary<Type, CreateContextDelegate> ContextFactoryCache = new();
22+
23+
/// <summary>
24+
/// Creates a ModuleExecutionContext for the specified module.
25+
/// </summary>
26+
/// <param name="module">The module instance.</param>
27+
/// <param name="moduleType">The type of the module.</param>
28+
/// <returns>A typed ModuleExecutionContext.</returns>
29+
public static ModuleExecutionContext Create(IModule module, Type moduleType)
30+
{
31+
var resultType = module.ResultType;
32+
var factory = ContextFactoryCache.GetOrAdd(resultType, CreateFactory);
33+
return factory(module, moduleType);
34+
}
35+
36+
private static CreateContextDelegate CreateFactory(Type resultType)
37+
{
38+
// Parameters
39+
var moduleParam = Expression.Parameter(typeof(IModule), "module");
40+
var moduleTypeParam = Expression.Parameter(typeof(Type), "moduleType");
41+
42+
// Get the generic types
43+
var contextType = typeof(ModuleExecutionContext<>).MakeGenericType(resultType);
44+
var typedModuleType = typeof(Module<>).MakeGenericType(resultType);
45+
46+
// Find the constructor: ModuleExecutionContext<T>(Module<T> module, Type moduleType)
47+
var constructor = contextType.GetConstructor(new[] { typedModuleType, typeof(Type) })
48+
?? throw new InvalidOperationException(
49+
$"Could not find constructor for {contextType.Name} with (Module<{resultType.Name}>, Type) parameters.");
50+
51+
// Cast module to Module<T>
52+
var castModule = Expression.Convert(moduleParam, typedModuleType);
53+
54+
// Create new ModuleExecutionContext<T>((Module<T>)module, moduleType)
55+
var newContext = Expression.New(constructor, castModule, moduleTypeParam);
56+
57+
// Cast to base type
58+
var castToBase = Expression.Convert(newContext, typeof(ModuleExecutionContext));
59+
60+
// Create and compile the lambda
61+
var lambda = Expression.Lambda<CreateContextDelegate>(
62+
castToBase,
63+
moduleParam,
64+
moduleTypeParam);
65+
66+
return lambda.Compile();
67+
}
68+
}
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
using System.Collections.Concurrent;
2+
using System.Linq.Expressions;
3+
using System.Reflection;
4+
using ModularPipelines.Context;
5+
using ModularPipelines.Models;
6+
using ModularPipelines.Modules;
7+
8+
namespace ModularPipelines.Engine.Execution;
9+
10+
/// <summary>
11+
/// Factory for creating cached delegates to execute modules without runtime reflection.
12+
/// </summary>
13+
/// <remarks>
14+
/// This class replaces the reflection-heavy pattern of using MakeGenericMethod and GetProperty("Result")
15+
/// with compiled expression trees that are cached per result type.
16+
/// </remarks>
17+
internal static class ModuleExecutionDelegateFactory
18+
{
19+
/// <summary>
20+
/// Delegate signature for executing a module and returning its result.
21+
/// </summary>
22+
internal delegate Task<IModuleResult> ExecuteModuleDelegate(
23+
IModuleExecutionPipeline pipeline,
24+
IModule module,
25+
ModuleExecutionContext executionContext,
26+
IModuleContext moduleContext,
27+
CancellationToken cancellationToken);
28+
29+
private static readonly ConcurrentDictionary<Type, ExecuteModuleDelegate> ExecutorCache = new();
30+
31+
/// <summary>
32+
/// Gets a cached delegate for executing a module with the specified result type.
33+
/// </summary>
34+
/// <param name="resultType">The result type of the module (T in Module&lt;T&gt;).</param>
35+
/// <returns>A delegate that executes the module and returns its result.</returns>
36+
public static ExecuteModuleDelegate GetExecutor(Type resultType)
37+
{
38+
return ExecutorCache.GetOrAdd(resultType, CreateExecutor);
39+
}
40+
41+
private static ExecuteModuleDelegate CreateExecutor(Type resultType)
42+
{
43+
// Parameters for the delegate
44+
var pipelineParam = Expression.Parameter(typeof(IModuleExecutionPipeline), "pipeline");
45+
var moduleParam = Expression.Parameter(typeof(IModule), "module");
46+
var contextParam = Expression.Parameter(typeof(ModuleExecutionContext), "executionContext");
47+
var moduleContextParam = Expression.Parameter(typeof(IModuleContext), "moduleContext");
48+
var cancellationTokenParam = Expression.Parameter(typeof(CancellationToken), "cancellationToken");
49+
50+
// Get the generic types
51+
var moduleType = typeof(Module<>).MakeGenericType(resultType);
52+
var executionContextType = typeof(ModuleExecutionContext<>).MakeGenericType(resultType);
53+
var moduleResultType = typeof(ModuleResult<>).MakeGenericType(resultType);
54+
var taskType = typeof(Task<>).MakeGenericType(moduleResultType);
55+
56+
// Cast module to Module<T>
57+
var castModule = Expression.Convert(moduleParam, moduleType);
58+
59+
// Cast executionContext to ModuleExecutionContext<T>
60+
var castContext = Expression.Convert(contextParam, executionContextType);
61+
62+
// Get the ExecuteAsync method
63+
var executeMethod = typeof(IModuleExecutionPipeline)
64+
.GetMethod(nameof(IModuleExecutionPipeline.ExecuteAsync))!
65+
.MakeGenericMethod(resultType);
66+
67+
// Call pipeline.ExecuteAsync<T>(module, executionContext, moduleContext, cancellationToken)
68+
var callExecute = Expression.Call(
69+
pipelineParam,
70+
executeMethod,
71+
castModule,
72+
castContext,
73+
moduleContextParam,
74+
cancellationTokenParam);
75+
76+
// We need to create an async wrapper that awaits the task and casts the result to IModuleResult
77+
// Since Expression trees can't directly represent async/await, we'll use a helper method
78+
var helperMethod = typeof(ModuleExecutionDelegateFactory)
79+
.GetMethod(nameof(ExecuteAndCastAsync), BindingFlags.NonPublic | BindingFlags.Static)!
80+
.MakeGenericMethod(resultType);
81+
82+
var callHelper = Expression.Call(
83+
helperMethod,
84+
pipelineParam,
85+
castModule,
86+
castContext,
87+
moduleContextParam,
88+
cancellationTokenParam);
89+
90+
// Create and compile the lambda
91+
var lambda = Expression.Lambda<ExecuteModuleDelegate>(
92+
callHelper,
93+
pipelineParam,
94+
moduleParam,
95+
contextParam,
96+
moduleContextParam,
97+
cancellationTokenParam);
98+
99+
return lambda.Compile();
100+
}
101+
102+
private static async Task<IModuleResult> ExecuteAndCastAsync<T>(
103+
IModuleExecutionPipeline pipeline,
104+
Module<T> module,
105+
ModuleExecutionContext<T> executionContext,
106+
IModuleContext moduleContext,
107+
CancellationToken cancellationToken)
108+
{
109+
var result = await pipeline.ExecuteAsync(module, executionContext, moduleContext, cancellationToken)
110+
.ConfigureAwait(false);
111+
return result;
112+
}
113+
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
using System.Collections.Concurrent;
2+
using System.Linq.Expressions;
3+
using System.Reflection;
4+
using ModularPipelines.Models;
5+
using ModularPipelines.Modules;
6+
7+
namespace ModularPipelines.Engine.Execution;
8+
9+
/// <summary>
10+
/// Factory for creating ModuleResult instances without reflection.
11+
/// </summary>
12+
/// <remarks>
13+
/// Replaces reflection-based constructor invocation with compiled expression trees.
14+
/// </remarks>
15+
internal static class ModuleResultFactory
16+
{
17+
/// <summary>
18+
/// Delegate for creating a ModuleResult with a null value (for skipped modules).
19+
/// </summary>
20+
internal delegate IModuleResult CreateSkippedResultDelegate(ModuleExecutionContext executionContext);
21+
22+
/// <summary>
23+
/// Delegate for creating a ModuleResult with an exception.
24+
/// </summary>
25+
internal delegate IModuleResult CreateExceptionResultDelegate(Exception exception, ModuleExecutionContext executionContext);
26+
27+
private static readonly ConcurrentDictionary<Type, CreateSkippedResultDelegate> SkippedResultCache = new();
28+
private static readonly ConcurrentDictionary<Type, CreateExceptionResultDelegate> ExceptionResultCache = new();
29+
30+
/// <summary>
31+
/// Creates a skipped ModuleResult for the specified result type.
32+
/// </summary>
33+
public static IModuleResult CreateSkipped(Type resultType, ModuleExecutionContext executionContext)
34+
{
35+
var factory = SkippedResultCache.GetOrAdd(resultType, CreateSkippedFactory);
36+
return factory(executionContext);
37+
}
38+
39+
/// <summary>
40+
/// Creates an exception ModuleResult for the specified result type.
41+
/// </summary>
42+
public static IModuleResult CreateException(Type resultType, Exception exception, ModuleExecutionContext executionContext)
43+
{
44+
var factory = ExceptionResultCache.GetOrAdd(resultType, CreateExceptionFactory);
45+
return factory(exception, executionContext);
46+
}
47+
48+
private static CreateSkippedResultDelegate CreateSkippedFactory(Type resultType)
49+
{
50+
var contextParam = Expression.Parameter(typeof(ModuleExecutionContext), "executionContext");
51+
var resultGenericType = typeof(ModuleResult<>).MakeGenericType(resultType);
52+
var typedContextType = typeof(ModuleExecutionContext<>).MakeGenericType(resultType);
53+
54+
// Cast to typed context
55+
var castContext = Expression.Convert(contextParam, typedContextType);
56+
57+
// Find the internal constructor: ModuleResult<T>(T? value, ModuleExecutionContext context)
58+
var constructor = resultGenericType.GetConstructor(
59+
BindingFlags.NonPublic | BindingFlags.Instance,
60+
null,
61+
new[] { resultType, typeof(ModuleExecutionContext) },
62+
null);
63+
64+
if (constructor == null)
65+
{
66+
throw new InvalidOperationException(
67+
$"Could not find internal constructor for ModuleResult<{resultType.Name}>(T?, ModuleExecutionContext)");
68+
}
69+
70+
// Create: new ModuleResult<T>(default(T), executionContext)
71+
var defaultValue = Expression.Default(resultType);
72+
var newResult = Expression.New(constructor, defaultValue, contextParam);
73+
74+
// Cast to IModuleResult
75+
var castToInterface = Expression.Convert(newResult, typeof(IModuleResult));
76+
77+
var lambda = Expression.Lambda<CreateSkippedResultDelegate>(castToInterface, contextParam);
78+
return lambda.Compile();
79+
}
80+
81+
private static CreateExceptionResultDelegate CreateExceptionFactory(Type resultType)
82+
{
83+
var exceptionParam = Expression.Parameter(typeof(Exception), "exception");
84+
var contextParam = Expression.Parameter(typeof(ModuleExecutionContext), "executionContext");
85+
var resultGenericType = typeof(ModuleResult<>).MakeGenericType(resultType);
86+
87+
// Find the internal constructor: ModuleResult<T>(Exception exception, ModuleExecutionContext context)
88+
// Note: The constructor takes the base class ModuleExecutionContext, not ModuleExecutionContext<T>
89+
var constructor = resultGenericType.GetConstructor(
90+
BindingFlags.NonPublic | BindingFlags.Instance,
91+
null,
92+
new[] { typeof(Exception), typeof(ModuleExecutionContext) },
93+
null);
94+
95+
if (constructor == null)
96+
{
97+
throw new InvalidOperationException(
98+
$"Could not find internal constructor for ModuleResult<{resultType.Name}>(Exception, ModuleExecutionContext)");
99+
}
100+
101+
// Create: new ModuleResult<T>(exception, executionContext)
102+
var newResult = Expression.New(constructor, exceptionParam, contextParam);
103+
104+
// Cast to IModuleResult
105+
var castToInterface = Expression.Convert(newResult, typeof(IModuleResult));
106+
107+
var lambda = Expression.Lambda<CreateExceptionResultDelegate>(castToInterface, exceptionParam, contextParam);
108+
return lambda.Compile();
109+
}
110+
}

src/ModularPipelines/Engine/Execution/ModuleResultRegistrar.cs

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,20 +27,13 @@ public void RegisterTerminatedResult(IModule module, Type moduleType, Exception
2727
{
2828
var resultType = module.ResultType;
2929

30-
// Create execution context with PipelineTerminated status
31-
var contextType = typeof(ModuleExecutionContext<>).MakeGenericType(resultType);
32-
var executionContext = (ModuleExecutionContext)Activator.CreateInstance(contextType, module, moduleType)!;
30+
// Create execution context with PipelineTerminated status using compiled delegate factory
31+
var executionContext = ExecutionContextFactory.Create(module, moduleType);
3332
executionContext.Status = Enums.Status.PipelineTerminated;
3433
executionContext.Exception = exception;
3534

36-
// Create ModuleResult<T> with the exception
37-
var resultGenericType = typeof(ModuleResult<>).MakeGenericType(resultType);
38-
var result = (IModuleResult)Activator.CreateInstance(
39-
resultGenericType,
40-
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance,
41-
null,
42-
new object[] { exception, executionContext },
43-
null)!;
35+
// Create ModuleResult<T> with the exception using compiled delegate factory
36+
var result = ModuleResultFactory.CreateException(resultType, exception, executionContext);
4437

4538
_resultRegistry.RegisterResult(moduleType, result);
4639
}

0 commit comments

Comments
 (0)