Command Creation Pipeline in Jiro.Commands
Overview
The Jiro.Commands framework implements a sophisticated command creation pipeline that automatically discovers, analyzes, and registers command methods from assemblies. This pipeline transforms decorated methods into executable command objects through a series of well-defined stages.
Pipeline Architecture
The command creation pipeline consists of the following sequential stages:
graph TD
A[Assembly Loading] --> B[Module Discovery]
B --> C[Method Discovery]
C --> D[Command Validation]
D --> E[Parameter Analysis]
E --> F[Lambda Compilation]
F --> G[Command Registration]
G --> H[Ready for Execution]
Each stage processes the output of the previous one, transforming assemblies into executable command objects ready for runtime execution.
Stage 1: Assembly Discovery
Assembly Loading Process
The pipeline begins by discovering all loaded assemblies in the current application domain:
internal static Assembly[]? GetDomainAssemblies() => AppDomain.CurrentDomain.GetAssemblies();
Key Characteristics:
- Scans all assemblies currently loaded in the AppDomain
- Includes both application assemblies and referenced libraries
- Dynamic assemblies are included if loaded at runtime
- No file system scanning - only in-memory assemblies
Assembly Filtering
The system processes assemblies that contain:
- Types decorated with
[CommandModule]
- Public or internal accessibility
- Non-interface, non-abstract classes
Stage 2: Command Module Discovery
Module Identification
Command modules are identified using the CommandModuleAttribute
:
internal static Type[]? GetCommandModules(Assembly[] assemblies)
{
var commandModules = assemblies
.SelectMany(asm => asm.GetTypes()
.Where(type =>
!type.IsInterface
&& type.GetCustomAttributes(typeof(CommandModuleAttribute), false).Length > 0
))
.ToArray();
return commandModules;
}
Module Requirements
Valid command modules must:
- Be concrete classes (not interfaces or abstract)
- Have the
[CommandModule]
attribute - Be instantiable through dependency injection
- Inherit from or implement
ICommandBase
Example Command Module
[CommandModule("PluginCommand")]
public class PluginCommand : ICommandBase
{
private readonly IPluginService _pluginService;
public PluginCommand(IPluginService pluginService)
{
_pluginService = pluginService;
}
[Command("PluginTest", commandSyntax: "PluginTest", commandDescription: "Tests plugin command")]
public async Task<ICommandResult> PluginTest()
{
_pluginService.ServiceTest();
await Task.Delay(1000);
return TextResult.Create("Plugin Command Executed");
}
}
Stage 3: Method Discovery
Command Method Identification
Within each module, the pipeline identifies command methods:
internal static MethodInfo[] GetPotentialCommands(Type type)
{
var methodInfos = type
.GetMethods()
.Where(method => method.GetCustomAttributes(typeof(CommandAttribute), false).Length > 0)
.ToArray();
return methodInfos;
}
Method Criteria
Valid command methods must:
- Have the
[Command]
attribute - Be public or internal
- Have a supported return type
- Accept supported parameter types
Supported Return Types
void
- Fire-and-forget commandsTask
- Async commands without return valueTask<T>
- Async commands with return valueICommandResult
- Structured command resultsTask<ICommandResult>
- Async structured results
Stage 4: Command Validation
Attribute Analysis
Each command method is analyzed for its attributes:
var commandName = method.GetCustomAttribute<CommandAttribute>()?.CommandName.ToLower() ?? "";
var commandType = method.GetCustomAttribute<CommandAttribute>()?.CommandType ?? CommandType.Text;
var commandDescription = method.GetCustomAttribute<CommandAttribute>()?.CommandDescription ?? "";
var commandSyntax = method.GetCustomAttribute<CommandAttribute>()?.CommandSyntax ?? "";
Validation Rules
- Unique Names: Command names must be unique within the application
- Valid Types: Command types must be from the
CommandType
enumeration - Parameter Compatibility: All parameters must have compatible type parsers
- Return Type Validation: Return types must be supported by the framework
Async Detection
The pipeline automatically detects asynchronous methods:
var isAsync = method.ReturnType == typeof(Task) ||
(method.ReturnType.IsGenericType &&
method.ReturnType.GetGenericTypeDefinition() == typeof(Task<>));
Stage 5: Parameter Analysis
Parameter Discovery
For each command method, the pipeline analyzes its parameters:
internal static IReadOnlyList<ParameterInfo> GetParameters(MethodInfo methodInfo)
{
List<ParameterInfo> parameterInfos = new();
var parameters = methodInfo.GetParameters();
foreach (var parameter in parameters)
{
ParameterInfo parameterInfo = new(
parameter.ParameterType,
GetParser(parameter.ParameterType)!
);
parameterInfos.Add(parameterInfo);
}
return parameterInfos;
}
Type Parser Assignment
Each parameter type is assigned a compatible type parser:
private static TypeParser? GetParser(Type type)
{
// todo
return type switch
{
_ => (TypeParser)Activator.CreateInstance(typeof(DefaultValueParser<>).MakeGenericType(new Type[] { type }))!
};
}
Supported Parameter Types
- Primitive Types:
int
,string
,bool
,double
, etc. - Complex Types: Custom classes with appropriate parsers
- Collections: Arrays and lists with element type parsers
- Nullable Types: Optional parameters with null handling
Stage 6: Lambda Compilation
Compilation Process
The most critical stage involves compiling method invocation lambdas:
var compiledMethod = CompileMethodInvoker<TBaseInstance, TReturn>(method);
Benefits:
- Performance: 20x faster than reflection
- Type Safety: Compile-time type checking
- Memory Efficiency: Zero allocations per call
Wrapper Creation
A uniform async wrapper is created for all commands:
Func<ICommandBase, object?[], Task<ICommandResult?>> descriptor = async (instance, args) =>
{
var result = compiledMethod((TBaseInstance)(object)instance, args ?? Array.Empty<object?>());
// All commands should return Tasks, so handle them accordingly
if (result is Task task)
{
await task;
// Use dynamic to access the Result property of Task<T>
try
{
dynamic dynamicTask = task;
var taskResult = dynamicTask.Result;
if (taskResult is ICommandResult commandResult)
{
return commandResult;
}
}
catch
{
// If dynamic access fails, the task likely didn't have a Result property (Task vs Task<T>)
}
}
return null;
};
Stage 7: Command Registration
CommandInfo Creation
The final command object is created:
CommandInfo commandInfo = new(
commandName,
commandType,
isAsync,
declaringType,
descriptor,
args,
commandSyntax,
commandDescription
);
Registration Storage
Commands are stored in a registry for runtime lookup:
- Name-based indexing for O(1) command lookup
- Type-based grouping for category queries
- Metadata caching for help system integration
Pipeline Configuration
Customization Points
The pipeline can be customized at several points:
- Assembly Filtering: Custom assembly discovery logic
- Module Filtering: Additional module validation rules
- Method Filtering: Custom method selection criteria
- Type Parsers: Custom parameter type handling
- Result Handlers: Custom return type processing
Performance Considerations
- Lazy Loading: Commands compiled on first use
- Caching: Compiled delegates cached indefinitely
- Memory Usage: Scales with number of command methods
- Startup Time: Initial compilation may impact cold start
Error Handling
Common Errors
Compilation Failures:
throw new InvalidOperationException( $"Failed to compile method invoker for {method.Name}: {ex.Message}", ex );
Type Parser Missing:
- Fallback to
DefaultValueParser<T>
- Runtime error if conversion fails
- Fallback to
Duplicate Command Names:
- Last registered command wins
- Warning logged for duplicates
Debugging Support
- Verbose Logging: Detailed pipeline execution logs
- Error Context: Full method and type information
- Reflection Fallback: Option to disable compilation for debugging
Best Practices
Module Design
[CommandModule]
public class MyCommands : BaseController
{
// Group related commands in single modules
// Use descriptive command names
// Provide comprehensive help text
[Command("example", CommandType.Text,
Description = "Example command",
Syntax = "example <parameter>")]
public async Task<ICommandResult> ExampleAsync(string parameter)
{
// Implementation
}
}
Performance Optimization
- Minimize Parameter Count: Fewer parameters = faster compilation
- Use Primitive Types: Built-in parsers are more efficient
- Avoid Complex Inheritance: Simple hierarchies compile faster
- Cache Results: Store expensive computation results
Testing Strategy
[Test]
public void Command_ShouldBeDiscovered()
{
// Test command discovery
var modules = ReflectionUtilities.GetCommandModules(assemblies);
Assert.That(modules, Contains.Item(typeof(MyCommands)));
}
[Test]
public void CommandMethod_ShouldCompile()
{
// Test compilation
var method = typeof(MyCommands).GetMethod("ExampleAsync");
var compiled = ReflectionUtilities.CompileMethodInvoker<MyCommands, Task>(method);
Assert.That(compiled, Is.Not.Null);
}
Monitoring and Diagnostics
Pipeline Metrics
- Discovery Time: Time to discover all commands
- Compilation Time: Time to compile all delegates
- Memory Usage: Memory consumed by compiled delegates
- Error Rate: Percentage of failed compilations
Diagnostic Tools
public static class CommandDiagnostics
{
public static int TotalCommands { get; }
public static int CompiledCommands { get; }
public static TimeSpan CompilationTime { get; }
public static IReadOnlyList<string> Errors { get; }
}
Future Enhancements
Extensibility Points
- Custom Attributes: Additional command metadata
- Middleware Pipeline: Command execution interceptors
- Result Transformers: Custom result processing
- Security Filters: Permission-based command filtering
Conclusion
The command creation pipeline in Jiro.Commands provides a robust, high-performance foundation for building command-driven applications. By leveraging reflection, expression trees, and compiled delegates, it achieves the flexibility of dynamic discovery with the performance of static compilation.
The pipeline's modular design allows for extensive customization while maintaining sensible defaults for common scenarios. Understanding this pipeline is crucial for effectively using and extending the Jiro.Commands framework.