diff --git a/docs/docs/how-to/parallelization.md b/docs/docs/how-to/parallelization.md
index d52c0cec151..4be03f4f820 100644
--- a/docs/docs/how-to/parallelization.md
+++ b/docs/docs/how-to/parallelization.md
@@ -121,7 +121,7 @@ public class BuildProjectModule : Module
public record MyParallelLimit : IParallelLimit
{
- public int Limit => 2;
+ public static int Limit => 2;
}
```
diff --git a/src/ModularPipelines/Attributes/ParallelLimiterAttribute.cs b/src/ModularPipelines/Attributes/ParallelLimiterAttribute.cs
index ade3becff21..08f697750b3 100644
--- a/src/ModularPipelines/Attributes/ParallelLimiterAttribute.cs
+++ b/src/ModularPipelines/Attributes/ParallelLimiterAttribute.cs
@@ -1,27 +1,56 @@
-using ModularPipelines.Interfaces;
+using ModularPipelines.Helpers;
+using ModularPipelines.Interfaces;
+using Semaphores;
namespace ModularPipelines.Attributes;
+///
+/// Specifies a parallel execution limit for a module using a strongly-typed limit class.
+///
+/// The type implementing .
[AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Class | AttributeTargets.Method)]
public sealed class ParallelLimiterAttribute : ParallelLimiterAttribute
- where TParallelLimit : IParallelLimit, new()
+ where TParallelLimit : IParallelLimit
{
public ParallelLimiterAttribute() : base(typeof(TParallelLimit))
{
}
+
+ ///
+ internal override AsyncSemaphore GetLock(IParallelLimitProvider provider)
+ {
+ return provider.GetLock();
+ }
}
-public class ParallelLimiterAttribute : Attribute
+///
+/// Base attribute for specifying parallel execution limits.
+///
+public abstract class ParallelLimiterAttribute : Attribute
{
+ ///
+ /// Gets the type implementing .
+ ///
public Type Type { get; }
- public ParallelLimiterAttribute(Type type)
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The type implementing .
+ protected ParallelLimiterAttribute(Type type)
{
if (!type.IsAssignableTo(typeof(IParallelLimit)))
{
- throw new Exception("Type must be of IParallelLimit");
+ throw new ArgumentException("Type must implement IParallelLimit", nameof(type));
}
Type = type;
}
+
+ ///
+ /// Gets the semaphore lock from the provider without reflection.
+ ///
+ /// The parallel limit provider.
+ /// The semaphore for this limit type.
+ internal abstract AsyncSemaphore GetLock(IParallelLimitProvider provider);
}
\ No newline at end of file
diff --git a/src/ModularPipelines/Engine/Execution/ExecutionContextFactory.cs b/src/ModularPipelines/Engine/Execution/ExecutionContextFactory.cs
new file mode 100644
index 00000000000..84ccd2d4b9d
--- /dev/null
+++ b/src/ModularPipelines/Engine/Execution/ExecutionContextFactory.cs
@@ -0,0 +1,68 @@
+using System.Collections.Concurrent;
+using System.Linq.Expressions;
+using ModularPipelines.Models;
+using ModularPipelines.Modules;
+
+namespace ModularPipelines.Engine.Execution;
+
+///
+/// Factory for creating ModuleExecutionContext instances without reflection.
+///
+///
+/// Replaces Activator.CreateInstance with compiled expression trees for better performance.
+///
+internal static class ExecutionContextFactory
+{
+ ///
+ /// Delegate signature for creating a ModuleExecutionContext.
+ ///
+ internal delegate ModuleExecutionContext CreateContextDelegate(IModule module, Type moduleType);
+
+ private static readonly ConcurrentDictionary ContextFactoryCache = new();
+
+ ///
+ /// Creates a ModuleExecutionContext for the specified module.
+ ///
+ /// The module instance.
+ /// The type of the module.
+ /// A typed ModuleExecutionContext.
+ public static ModuleExecutionContext Create(IModule module, Type moduleType)
+ {
+ var resultType = module.ResultType;
+ var factory = ContextFactoryCache.GetOrAdd(resultType, CreateFactory);
+ return factory(module, moduleType);
+ }
+
+ private static CreateContextDelegate CreateFactory(Type resultType)
+ {
+ // Parameters
+ var moduleParam = Expression.Parameter(typeof(IModule), "module");
+ var moduleTypeParam = Expression.Parameter(typeof(Type), "moduleType");
+
+ // Get the generic types
+ var contextType = typeof(ModuleExecutionContext<>).MakeGenericType(resultType);
+ var typedModuleType = typeof(Module<>).MakeGenericType(resultType);
+
+ // Find the constructor: ModuleExecutionContext(Module module, Type moduleType)
+ var constructor = contextType.GetConstructor(new[] { typedModuleType, typeof(Type) })
+ ?? throw new InvalidOperationException(
+ $"Could not find constructor for {contextType.Name} with (Module<{resultType.Name}>, Type) parameters.");
+
+ // Cast module to Module
+ var castModule = Expression.Convert(moduleParam, typedModuleType);
+
+ // Create new ModuleExecutionContext((Module)module, moduleType)
+ var newContext = Expression.New(constructor, castModule, moduleTypeParam);
+
+ // Cast to base type
+ var castToBase = Expression.Convert(newContext, typeof(ModuleExecutionContext));
+
+ // Create and compile the lambda
+ var lambda = Expression.Lambda(
+ castToBase,
+ moduleParam,
+ moduleTypeParam);
+
+ return lambda.Compile();
+ }
+}
diff --git a/src/ModularPipelines/Engine/Execution/ModuleExecutionDelegateFactory.cs b/src/ModularPipelines/Engine/Execution/ModuleExecutionDelegateFactory.cs
new file mode 100644
index 00000000000..3adcec6b793
--- /dev/null
+++ b/src/ModularPipelines/Engine/Execution/ModuleExecutionDelegateFactory.cs
@@ -0,0 +1,113 @@
+using System.Collections.Concurrent;
+using System.Linq.Expressions;
+using System.Reflection;
+using ModularPipelines.Context;
+using ModularPipelines.Models;
+using ModularPipelines.Modules;
+
+namespace ModularPipelines.Engine.Execution;
+
+///
+/// Factory for creating cached delegates to execute modules without runtime reflection.
+///
+///
+/// This class replaces the reflection-heavy pattern of using MakeGenericMethod and GetProperty("Result")
+/// with compiled expression trees that are cached per result type.
+///
+internal static class ModuleExecutionDelegateFactory
+{
+ ///
+ /// Delegate signature for executing a module and returning its result.
+ ///
+ internal delegate Task ExecuteModuleDelegate(
+ IModuleExecutionPipeline pipeline,
+ IModule module,
+ ModuleExecutionContext executionContext,
+ IModuleContext moduleContext,
+ CancellationToken cancellationToken);
+
+ private static readonly ConcurrentDictionary ExecutorCache = new();
+
+ ///
+ /// Gets a cached delegate for executing a module with the specified result type.
+ ///
+ /// The result type of the module (T in Module<T>).
+ /// A delegate that executes the module and returns its result.
+ public static ExecuteModuleDelegate GetExecutor(Type resultType)
+ {
+ return ExecutorCache.GetOrAdd(resultType, CreateExecutor);
+ }
+
+ private static ExecuteModuleDelegate CreateExecutor(Type resultType)
+ {
+ // Parameters for the delegate
+ var pipelineParam = Expression.Parameter(typeof(IModuleExecutionPipeline), "pipeline");
+ var moduleParam = Expression.Parameter(typeof(IModule), "module");
+ var contextParam = Expression.Parameter(typeof(ModuleExecutionContext), "executionContext");
+ var moduleContextParam = Expression.Parameter(typeof(IModuleContext), "moduleContext");
+ var cancellationTokenParam = Expression.Parameter(typeof(CancellationToken), "cancellationToken");
+
+ // Get the generic types
+ var moduleType = typeof(Module<>).MakeGenericType(resultType);
+ var executionContextType = typeof(ModuleExecutionContext<>).MakeGenericType(resultType);
+ var moduleResultType = typeof(ModuleResult<>).MakeGenericType(resultType);
+ var taskType = typeof(Task<>).MakeGenericType(moduleResultType);
+
+ // Cast module to Module
+ var castModule = Expression.Convert(moduleParam, moduleType);
+
+ // Cast executionContext to ModuleExecutionContext
+ var castContext = Expression.Convert(contextParam, executionContextType);
+
+ // Get the ExecuteAsync method
+ var executeMethod = typeof(IModuleExecutionPipeline)
+ .GetMethod(nameof(IModuleExecutionPipeline.ExecuteAsync))!
+ .MakeGenericMethod(resultType);
+
+ // Call pipeline.ExecuteAsync(module, executionContext, moduleContext, cancellationToken)
+ var callExecute = Expression.Call(
+ pipelineParam,
+ executeMethod,
+ castModule,
+ castContext,
+ moduleContextParam,
+ cancellationTokenParam);
+
+ // We need to create an async wrapper that awaits the task and casts the result to IModuleResult
+ // Since Expression trees can't directly represent async/await, we'll use a helper method
+ var helperMethod = typeof(ModuleExecutionDelegateFactory)
+ .GetMethod(nameof(ExecuteAndCastAsync), BindingFlags.NonPublic | BindingFlags.Static)!
+ .MakeGenericMethod(resultType);
+
+ var callHelper = Expression.Call(
+ helperMethod,
+ pipelineParam,
+ castModule,
+ castContext,
+ moduleContextParam,
+ cancellationTokenParam);
+
+ // Create and compile the lambda
+ var lambda = Expression.Lambda(
+ callHelper,
+ pipelineParam,
+ moduleParam,
+ contextParam,
+ moduleContextParam,
+ cancellationTokenParam);
+
+ return lambda.Compile();
+ }
+
+ private static async Task ExecuteAndCastAsync(
+ IModuleExecutionPipeline pipeline,
+ Module module,
+ ModuleExecutionContext executionContext,
+ IModuleContext moduleContext,
+ CancellationToken cancellationToken)
+ {
+ var result = await pipeline.ExecuteAsync(module, executionContext, moduleContext, cancellationToken)
+ .ConfigureAwait(false);
+ return result;
+ }
+}
diff --git a/src/ModularPipelines/Engine/Execution/ModuleResultFactory.cs b/src/ModularPipelines/Engine/Execution/ModuleResultFactory.cs
new file mode 100644
index 00000000000..b6f04651cbd
--- /dev/null
+++ b/src/ModularPipelines/Engine/Execution/ModuleResultFactory.cs
@@ -0,0 +1,110 @@
+using System.Collections.Concurrent;
+using System.Linq.Expressions;
+using System.Reflection;
+using ModularPipelines.Models;
+using ModularPipelines.Modules;
+
+namespace ModularPipelines.Engine.Execution;
+
+///
+/// Factory for creating ModuleResult instances without reflection.
+///
+///
+/// Replaces reflection-based constructor invocation with compiled expression trees.
+///
+internal static class ModuleResultFactory
+{
+ ///
+ /// Delegate for creating a ModuleResult with a null value (for skipped modules).
+ ///
+ internal delegate IModuleResult CreateSkippedResultDelegate(ModuleExecutionContext executionContext);
+
+ ///
+ /// Delegate for creating a ModuleResult with an exception.
+ ///
+ internal delegate IModuleResult CreateExceptionResultDelegate(Exception exception, ModuleExecutionContext executionContext);
+
+ private static readonly ConcurrentDictionary SkippedResultCache = new();
+ private static readonly ConcurrentDictionary ExceptionResultCache = new();
+
+ ///
+ /// Creates a skipped ModuleResult for the specified result type.
+ ///
+ public static IModuleResult CreateSkipped(Type resultType, ModuleExecutionContext executionContext)
+ {
+ var factory = SkippedResultCache.GetOrAdd(resultType, CreateSkippedFactory);
+ return factory(executionContext);
+ }
+
+ ///
+ /// Creates an exception ModuleResult for the specified result type.
+ ///
+ public static IModuleResult CreateException(Type resultType, Exception exception, ModuleExecutionContext executionContext)
+ {
+ var factory = ExceptionResultCache.GetOrAdd(resultType, CreateExceptionFactory);
+ return factory(exception, executionContext);
+ }
+
+ private static CreateSkippedResultDelegate CreateSkippedFactory(Type resultType)
+ {
+ var contextParam = Expression.Parameter(typeof(ModuleExecutionContext), "executionContext");
+ var resultGenericType = typeof(ModuleResult<>).MakeGenericType(resultType);
+ var typedContextType = typeof(ModuleExecutionContext<>).MakeGenericType(resultType);
+
+ // Cast to typed context
+ var castContext = Expression.Convert(contextParam, typedContextType);
+
+ // Find the internal constructor: ModuleResult(T? value, ModuleExecutionContext context)
+ var constructor = resultGenericType.GetConstructor(
+ BindingFlags.NonPublic | BindingFlags.Instance,
+ null,
+ new[] { resultType, typeof(ModuleExecutionContext) },
+ null);
+
+ if (constructor == null)
+ {
+ throw new InvalidOperationException(
+ $"Could not find internal constructor for ModuleResult<{resultType.Name}>(T?, ModuleExecutionContext)");
+ }
+
+ // Create: new ModuleResult(default(T), executionContext)
+ var defaultValue = Expression.Default(resultType);
+ var newResult = Expression.New(constructor, defaultValue, contextParam);
+
+ // Cast to IModuleResult
+ var castToInterface = Expression.Convert(newResult, typeof(IModuleResult));
+
+ var lambda = Expression.Lambda(castToInterface, contextParam);
+ return lambda.Compile();
+ }
+
+ private static CreateExceptionResultDelegate CreateExceptionFactory(Type resultType)
+ {
+ var exceptionParam = Expression.Parameter(typeof(Exception), "exception");
+ var contextParam = Expression.Parameter(typeof(ModuleExecutionContext), "executionContext");
+ var resultGenericType = typeof(ModuleResult<>).MakeGenericType(resultType);
+
+ // Find the internal constructor: ModuleResult(Exception exception, ModuleExecutionContext context)
+ // Note: The constructor takes the base class ModuleExecutionContext, not ModuleExecutionContext
+ var constructor = resultGenericType.GetConstructor(
+ BindingFlags.NonPublic | BindingFlags.Instance,
+ null,
+ new[] { typeof(Exception), typeof(ModuleExecutionContext) },
+ null);
+
+ if (constructor == null)
+ {
+ throw new InvalidOperationException(
+ $"Could not find internal constructor for ModuleResult<{resultType.Name}>(Exception, ModuleExecutionContext)");
+ }
+
+ // Create: new ModuleResult(exception, executionContext)
+ var newResult = Expression.New(constructor, exceptionParam, contextParam);
+
+ // Cast to IModuleResult
+ var castToInterface = Expression.Convert(newResult, typeof(IModuleResult));
+
+ var lambda = Expression.Lambda(castToInterface, exceptionParam, contextParam);
+ return lambda.Compile();
+ }
+}
diff --git a/src/ModularPipelines/Engine/Execution/ModuleResultRegistrar.cs b/src/ModularPipelines/Engine/Execution/ModuleResultRegistrar.cs
index 0b5ffd7df35..421757ddac3 100644
--- a/src/ModularPipelines/Engine/Execution/ModuleResultRegistrar.cs
+++ b/src/ModularPipelines/Engine/Execution/ModuleResultRegistrar.cs
@@ -27,20 +27,13 @@ public void RegisterTerminatedResult(IModule module, Type moduleType, Exception
{
var resultType = module.ResultType;
- // Create execution context with PipelineTerminated status
- var contextType = typeof(ModuleExecutionContext<>).MakeGenericType(resultType);
- var executionContext = (ModuleExecutionContext)Activator.CreateInstance(contextType, module, moduleType)!;
+ // Create execution context with PipelineTerminated status using compiled delegate factory
+ var executionContext = ExecutionContextFactory.Create(module, moduleType);
executionContext.Status = Enums.Status.PipelineTerminated;
executionContext.Exception = exception;
- // Create ModuleResult with the exception
- var resultGenericType = typeof(ModuleResult<>).MakeGenericType(resultType);
- var result = (IModuleResult)Activator.CreateInstance(
- resultGenericType,
- System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance,
- null,
- new object[] { exception, executionContext },
- null)!;
+ // Create ModuleResult with the exception using compiled delegate factory
+ var result = ModuleResultFactory.CreateException(resultType, exception, executionContext);
_resultRegistry.RegisterResult(moduleType, result);
}
diff --git a/src/ModularPipelines/Engine/Execution/ModuleRunner.cs b/src/ModularPipelines/Engine/Execution/ModuleRunner.cs
index 9a0afcb4f08..00c920f17e4 100644
--- a/src/ModularPipelines/Engine/Execution/ModuleRunner.cs
+++ b/src/ModularPipelines/Engine/Execution/ModuleRunner.cs
@@ -147,18 +147,20 @@ private async Task ExecuteModuleWithPipeline(ModuleState moduleState, IServicePr
var logger = GetOrCreateLogger(moduleType, scopedServiceProvider);
var moduleContext = new ModuleContext(pipelineContext, module, executionContext, logger);
- // Set up logging - use try/finally to ensure cleanup of AsyncLocal context
- // Assignment MUST be inside try block to guarantee cleanup even if an exception
+ // Set up logging and module type context - use try/finally to ensure cleanup of AsyncLocal context
+ // Assignments MUST be inside try block to guarantee cleanup even if an exception
// occurs immediately after assignment
try
{
ModuleLogger.Values.Value = logger;
+ ModuleLogger.CurrentModuleType.Value = moduleType;
await ExecuteModuleLifecycle(moduleState, scopedServiceProvider, pipelineContext, executionContext, moduleContext, cancellationToken).ConfigureAwait(false);
}
finally
{
// Clear AsyncLocal to prevent potential leaks in edge cases (thread pool reuse, long-running contexts)
ModuleLogger.Values.Value = null;
+ ModuleLogger.CurrentModuleType.Value = null;
}
}
@@ -259,9 +261,8 @@ private async Task ExecuteModuleLifecycle(
private ModuleExecutionContext CreateExecutionContext(IModule module, Type moduleType)
{
- var resultType = module.ResultType;
- var contextType = typeof(ModuleExecutionContext<>).MakeGenericType(resultType);
- return (ModuleExecutionContext)Activator.CreateInstance(contextType, module, moduleType)!;
+ // Use compiled delegate factory instead of Activator.CreateInstance
+ return ExecutionContextFactory.Create(module, moduleType);
}
private async Task ExecuteTypedModule(
@@ -270,26 +271,10 @@ private async Task ExecuteTypedModule(
IModuleContext moduleContext,
CancellationToken cancellationToken)
{
- var resultType = module.ResultType;
-
- var executeMethodInfo = typeof(IModuleExecutionPipeline).GetMethod(nameof(IModuleExecutionPipeline.ExecuteAsync))
- ?? throw new InvalidOperationException($"Method '{nameof(IModuleExecutionPipeline.ExecuteAsync)}' not found on type '{nameof(IModuleExecutionPipeline)}'.");
-
- var executeMethod = executeMethodInfo.MakeGenericMethod(resultType);
-
- var invokeResult = executeMethod.Invoke(_executionPipeline, new object[] { module, executionContext, moduleContext, cancellationToken })
- ?? throw new InvalidOperationException($"Invocation of '{nameof(IModuleExecutionPipeline.ExecuteAsync)}' returned null.");
-
- var task = (Task)invokeResult;
- await task.ConfigureAwait(false);
-
- var resultProperty = task.GetType().GetProperty("Result")
- ?? throw new InvalidOperationException($"Property 'Result' not found on task type '{task.GetType().Name}'.");
-
- var resultValue = resultProperty.GetValue(task)
- ?? throw new InvalidOperationException($"Property 'Result' returned null for task type '{task.GetType().Name}'.");
-
- return (IModuleResult)resultValue;
+ // Use compiled delegate instead of MakeGenericMethod + Invoke + GetProperty("Result")
+ var executor = ModuleExecutionDelegateFactory.GetExecutor(module.ResultType);
+ return await executor(_executionPipeline, module, executionContext, moduleContext, cancellationToken)
+ .ConfigureAwait(false);
}
private IModuleLogger GetOrCreateLogger(Type moduleType, IServiceProvider scopedServiceProvider)
diff --git a/src/ModularPipelines/Engine/Execution/ParallelLimitHandler.cs b/src/ModularPipelines/Engine/Execution/ParallelLimitHandler.cs
index f1a9c5bfeb9..2101111c9fe 100644
--- a/src/ModularPipelines/Engine/Execution/ParallelLimitHandler.cs
+++ b/src/ModularPipelines/Engine/Execution/ParallelLimitHandler.cs
@@ -2,7 +2,6 @@
using Microsoft.Extensions.Logging;
using ModularPipelines.Attributes;
using ModularPipelines.Helpers;
-using ModularPipelines.Interfaces;
using ModularPipelines.Logging;
namespace ModularPipelines.Engine.Execution;
@@ -26,17 +25,18 @@ public ParallelLimitHandler(
///
public async Task AcquireParallelLimitAsync(Type moduleType)
{
- var parallelLimitAttributeType =
- moduleType.GetCustomAttributes().FirstOrDefault()?.Type;
+ var parallelLimiterAttribute =
+ moduleType.GetCustomAttributes().FirstOrDefault();
- if (parallelLimitAttributeType != null)
+ if (parallelLimiterAttribute != null)
{
_logger.LogDebug(
"Module {ModuleName} acquiring parallel limit from {LimiterType}",
MarkupFormatter.FormatModuleName(moduleType.Name),
- parallelLimitAttributeType.Name);
+ parallelLimiterAttribute.Type.Name);
- return await _parallelLimitProvider.GetLock(parallelLimitAttributeType).WaitAsync().ConfigureAwait(false);
+ // Use the attribute's GetLock method to avoid reflection on IParallelLimit
+ return await parallelLimiterAttribute.GetLock(_parallelLimitProvider).WaitAsync().ConfigureAwait(false);
}
return NoOpDisposable.Instance;
diff --git a/src/ModularPipelines/Engine/Execution/ResultRepositoryDelegateFactory.cs b/src/ModularPipelines/Engine/Execution/ResultRepositoryDelegateFactory.cs
new file mode 100644
index 00000000000..290fb3634fe
--- /dev/null
+++ b/src/ModularPipelines/Engine/Execution/ResultRepositoryDelegateFactory.cs
@@ -0,0 +1,72 @@
+using System.Collections.Concurrent;
+using System.Linq.Expressions;
+using System.Reflection;
+using ModularPipelines.Context;
+using ModularPipelines.Models;
+using ModularPipelines.Modules;
+
+namespace ModularPipelines.Engine.Execution;
+
+///
+/// Factory for creating cached delegates to call IModuleResultRepository methods without reflection.
+///
+internal static class ResultRepositoryDelegateFactory
+{
+ ///
+ /// Delegate for calling GetResultAsync on a repository.
+ ///
+ internal delegate Task GetResultDelegate(
+ IModuleResultRepository repository,
+ IModule module,
+ IPipelineContext context);
+
+ private static readonly ConcurrentDictionary GetResultCache = new();
+
+ ///
+ /// Gets a cached delegate for calling GetResultAsync with the specified result type.
+ ///
+ public static GetResultDelegate GetResultDelegateFor(Type resultType)
+ {
+ return GetResultCache.GetOrAdd(resultType, CreateGetResultDelegate);
+ }
+
+ private static GetResultDelegate CreateGetResultDelegate(Type resultType)
+ {
+ var repositoryParam = Expression.Parameter(typeof(IModuleResultRepository), "repository");
+ var moduleParam = Expression.Parameter(typeof(IModule), "module");
+ var contextParam = Expression.Parameter(typeof(IPipelineContext), "context");
+
+ // Get types
+ var moduleType = typeof(Module<>).MakeGenericType(resultType);
+
+ // Cast module to Module
+ var castModule = Expression.Convert(moduleParam, moduleType);
+
+ // Get the GetResultAsync method
+ var method = typeof(IModuleResultRepository)
+ .GetMethod(nameof(IModuleResultRepository.GetResultAsync))!
+ .MakeGenericMethod(resultType);
+
+ // Call: repository.GetResultAsync((Module)module, context)
+ var callMethod = Expression.Call(repositoryParam, method, castModule, contextParam);
+
+ // We need an async helper since expression trees can't represent async
+ var helperMethod = typeof(ResultRepositoryDelegateFactory)
+ .GetMethod(nameof(GetResultAndCastAsync), BindingFlags.NonPublic | BindingFlags.Static)!
+ .MakeGenericMethod(resultType);
+
+ var callHelper = Expression.Call(helperMethod, repositoryParam, castModule, contextParam);
+
+ var lambda = Expression.Lambda(callHelper, repositoryParam, moduleParam, contextParam);
+ return lambda.Compile();
+ }
+
+ private static async Task GetResultAndCastAsync(
+ IModuleResultRepository repository,
+ Module module,
+ IPipelineContext context)
+ {
+ var result = await repository.GetResultAsync(module, context).ConfigureAwait(false);
+ return result;
+ }
+}
diff --git a/src/ModularPipelines/Engine/Executors/IgnoredModuleResultRegistrar.cs b/src/ModularPipelines/Engine/Executors/IgnoredModuleResultRegistrar.cs
index 0e777620fc6..a5e80b3e9a6 100644
--- a/src/ModularPipelines/Engine/Executors/IgnoredModuleResultRegistrar.cs
+++ b/src/ModularPipelines/Engine/Executors/IgnoredModuleResultRegistrar.cs
@@ -1,6 +1,6 @@
-using System.Reflection;
using Microsoft.Extensions.Logging;
using ModularPipelines.Context;
+using ModularPipelines.Engine.Execution;
using ModularPipelines.Enums;
using ModularPipelines.Helpers;
using ModularPipelines.Models;
@@ -46,7 +46,7 @@ public async Task RegisterIgnoredModuleResultsAsync(IReadOnlyList
// For ignored modules, always check for historical data if a repository is configured
if (_resultRepository.GetType() != typeof(NoOpModuleResultRepository))
{
- var historicalResult = await TryGetHistoricalResultAsync(module, moduleType, resultType, pipelineContext).ConfigureAwait(false);
+ var historicalResult = await TryGetHistoricalResultAsync(module, resultType, pipelineContext).ConfigureAwait(false);
if (historicalResult != null)
{
// Update the status to UsedHistory since we're using a cached result
@@ -65,67 +65,35 @@ public async Task RegisterIgnoredModuleResultsAsync(IReadOnlyList
_logger.LogDebug("Registering skipped result for ignored module {ModuleName}",
MarkupFormatter.FormatModuleName(moduleType.Name));
- // Create execution context with Skipped status
- var contextType = typeof(ModuleExecutionContext<>).MakeGenericType(resultType);
- var executionContext = (ModuleExecutionContext) Activator.CreateInstance(contextType, module, moduleType)!;
+ // Create execution context with Skipped status using compiled delegate factory
+ var executionContext = ExecutionContextFactory.Create(module, moduleType);
executionContext.Status = Status.Skipped;
executionContext.SkipResult = ignoredModule.SkipDecision;
- // Create ModuleResult with the skipped status using the value constructor (T?, ModuleExecutionContext)
- var resultGenericType = typeof(ModuleResult<>).MakeGenericType(resultType);
- var constructor = resultGenericType.GetConstructor(
- BindingFlags.NonPublic | BindingFlags.Instance,
- null,
- new[] { resultType, typeof(ModuleExecutionContext) },
- null);
-
- if (constructor == null)
- {
- _logger.LogWarning("Could not find constructor for ModuleResult<{ResultType}>", resultType.Name);
- continue;
- }
-
- var result = (IModuleResult) constructor.Invoke(new object?[] { null, executionContext })!;
+ // Create ModuleResult with the skipped status using compiled delegate factory
+ var result = ModuleResultFactory.CreateSkipped(resultType, executionContext);
_resultRegistry.RegisterResult(moduleType, result);
}
}
///
- /// Attempts to get a historical result for a module using reflection to call the generic GetResultAsync method.
+ /// Attempts to get a historical result for a module using compiled delegates to call the generic GetResultAsync method.
///
private async Task TryGetHistoricalResultAsync(
IModule module,
- Type moduleType,
Type resultType,
IPipelineContext pipelineContext)
{
try
{
- // Get the generic GetResultAsync method
- var getResultAsyncMethod = typeof(IModuleResultRepository)
- .GetMethod(nameof(IModuleResultRepository.GetResultAsync))!
- .MakeGenericMethod(resultType);
-
- // Invoke the method: Task?> GetResultAsync(Module module, IPipelineHookContext pipelineContext)
- var task = (Task?) getResultAsyncMethod.Invoke(_resultRepository, new object[] { module, pipelineContext });
-
- if (task == null)
- {
- return null;
- }
-
- await task.ConfigureAwait(false);
-
- // Get the Result property from the completed Task?>
- var resultProperty = task.GetType().GetProperty("Result");
- var historicalResult = resultProperty?.GetValue(task) as IModuleResult;
-
- return historicalResult;
+ // Use compiled delegate instead of MakeGenericMethod + Invoke + GetProperty("Result")
+ var getResultDelegate = ResultRepositoryDelegateFactory.GetResultDelegateFor(resultType);
+ return await getResultDelegate(_resultRepository, module, pipelineContext).ConfigureAwait(false);
}
catch (Exception ex)
{
- _logger.LogWarning(ex, "Failed to get historical result for module {ModuleName}", moduleType.Name);
+ _logger.LogWarning(ex, "Failed to get historical result for module {ModuleName}", module.GetType().Name);
return null;
}
}
diff --git a/src/ModularPipelines/Engine/OptionsProvider.cs b/src/ModularPipelines/Engine/OptionsProvider.cs
index d2ef3b9ca49..9b278846c6a 100644
--- a/src/ModularPipelines/Engine/OptionsProvider.cs
+++ b/src/ModularPipelines/Engine/OptionsProvider.cs
@@ -1,4 +1,5 @@
-using System.Reflection;
+using System.Collections.Concurrent;
+using System.Linq.Expressions;
using Microsoft.Extensions.Options;
using ModularPipelines.DependencyInjection;
@@ -6,6 +7,12 @@ namespace ModularPipelines.Engine;
internal class OptionsProvider : IOptionsProvider
{
+ ///
+ /// Cache of compiled property accessors for IOptions<T>.Value.
+ /// Avoids repeated reflection for property access.
+ ///
+ private static readonly ConcurrentDictionary> ValueGetterCache = new();
+
private readonly IPipelineServiceContainerWrapper _pipelineServiceContainerWrapper;
private readonly IServiceProvider _serviceProvider;
@@ -44,10 +51,26 @@ public OptionsProvider(IPipelineServiceContainerWrapper pipelineServiceContainer
continue;
}
- var valueProperty = option.GetType().GetProperty("Value", BindingFlags.Public | BindingFlags.Instance)
- ?? throw new InvalidOperationException($"Property 'Value' not found on type '{option.GetType().Name}'.");
-
- yield return valueProperty.GetValue(option);
+ // Use cached compiled delegate instead of reflection
+ var getter = ValueGetterCache.GetOrAdd(option.GetType(), CreateValueGetter);
+ yield return getter(option);
}
}
+
+ ///
+ /// Creates a compiled delegate to access the Value property of an IOptions<T> instance.
+ ///
+ private static Func