diff --git a/.claude/settings.local.json b/.claude/settings.local.json
index 73eaf9da..e12dcfaf 100644
--- a/.claude/settings.local.json
+++ b/.claude/settings.local.json
@@ -28,7 +28,10 @@
"Bash(for dir in RCommon.*.Tests)",
"Bash(do echo \"=== $dir ===\")",
"Bash(done)",
- "Bash(git add:*)"
+ "Bash(git add:*)",
+ "Bash(findstr:*)",
+ "Bash(Select-String -Pattern \"warning CS8\")",
+ "Bash(Select-String -Pattern \"Build succeeded|Error\\\\\\(s\\\\\\)|Warning\\\\\\(s\\\\\\)|error CS\")"
]
}
}
diff --git a/.gitignore b/.gitignore
index e4e4b4f5..011ff5a6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -342,3 +342,6 @@ ASALocalRun/
# BeatPulse healthcheck temp database
healthchecksdb
/Src/RCommon.Persistence.EfCore/IEFCoreConfiguration.cs
+/.claude
+.claude/settings.local.json
+.claude/settings.local.json
diff --git a/Src/.editorconfig b/Src/.editorconfig
new file mode 100644
index 00000000..c85b034f
--- /dev/null
+++ b/Src/.editorconfig
@@ -0,0 +1,157 @@
+###############################
+# Core EditorConfig Options #
+###############################
+
+root = true
+
+# All files
+[*]
+indent_style = space
+
+# Code files
+[*.{cs,csx,vb,vbx}]
+indent_size = 4
+insert_final_newline = true
+charset = utf-8-bom
+
+###############################
+# .NET Coding Conventions #
+###############################
+
+###############################
+# .NET Coding Conventions #
+###############################
+
+[*.{cs,vb}]
+# Organize usings
+dotnet_sort_system_directives_first = true
+dotnet_separate_import_directive_groups = false
+
+# this. preferences
+dotnet_style_qualification_for_field = false:silent
+dotnet_style_qualification_for_property = false:silent
+dotnet_style_qualification_for_method = false:silent
+dotnet_style_qualification_for_event = false:silent
+
+# Language keywords vs BCL types preferences
+dotnet_style_predefined_type_for_locals_parameters_members = true:silent
+dotnet_style_predefined_type_for_member_access = true:silent
+
+# Parentheses preferences
+dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent
+dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent
+dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent
+dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent
+
+# Modifier preferences
+dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent
+dotnet_style_readonly_field = true:suggestion
+
+# Expression-level preferences
+dotnet_style_object_initializer = true:suggestion
+dotnet_style_collection_initializer = true:suggestion
+dotnet_style_explicit_tuple_names = true:suggestion
+dotnet_style_null_propagation = true:suggestion
+dotnet_style_coalesce_expression = true:suggestion
+dotnet_style_prefer_is_null_check_over_reference_equality_method = true:silent
+dotnet_style_prefer_inferred_tuple_names = true:suggestion
+dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
+dotnet_style_prefer_auto_properties = true:silent
+dotnet_style_prefer_conditional_expression_over_assignment = true:silent
+dotnet_style_prefer_conditional_expression_over_return = true:silent
+
+###############################
+# Naming Conventions #
+###############################
+
+# Style Definitions
+dotnet_naming_style.pascal_case_style.capitalization = pascal_case
+
+# Use PascalCase for constant fields
+dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion
+dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields
+dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style
+dotnet_naming_symbols.constant_fields.applicable_kinds = field
+dotnet_naming_symbols.constant_fields.applicable_accessibilities = *
+dotnet_naming_symbols.constant_fields.required_modifiers = const
+
+###############################
+# C# Code Style Rules #
+###############################
+
+[*.cs]
+# var preferences
+csharp_style_var_for_built_in_types = true:silent
+csharp_style_var_when_type_is_apparent = true:silent
+csharp_style_var_elsewhere = true:silent
+
+# Expression-bodied members
+csharp_style_expression_bodied_methods = false:silent
+csharp_style_expression_bodied_constructors = false:silent
+csharp_style_expression_bodied_operators = false:silent
+csharp_style_expression_bodied_properties = true:silent
+csharp_style_expression_bodied_indexers = true:silent
+csharp_style_expression_bodied_accessors = true:silent
+
+# Pattern-matching preferences
+csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
+csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
+
+# Null-checking preferences
+csharp_style_throw_expression = true:suggestion
+csharp_style_conditional_delegate_call = true:suggestion
+
+# Modifier preferences
+csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:suggestion
+
+# Expression-level preferences
+csharp_prefer_braces = true:silent
+csharp_style_deconstructed_variable_declaration = true:suggestion
+csharp_prefer_simple_default_expression = true:suggestion
+csharp_style_pattern_local_over_anonymous_function = true:suggestion
+csharp_style_inlined_variable_declaration = true:suggestion
+
+###############################
+# C# Formatting Rules #
+###############################
+
+# New line preferences
+csharp_new_line_before_open_brace = all
+csharp_new_line_before_else = true
+csharp_new_line_before_catch = true
+csharp_new_line_before_finally = true
+csharp_new_line_before_members_in_object_initializers = true
+csharp_new_line_before_members_in_anonymous_types = true
+csharp_new_line_between_query_expression_clauses = true
+
+# Indentation preferences
+csharp_indent_case_contents = true
+csharp_indent_switch_labels = true
+csharp_indent_labels = flush_left
+
+# Space preferences
+csharp_space_after_cast = false
+csharp_space_after_keywords_in_control_flow_statements = true
+csharp_space_between_method_call_parameter_list_parentheses = false
+csharp_space_between_method_declaration_parameter_list_parentheses = false
+csharp_space_between_parentheses = false
+csharp_space_before_colon_in_inheritance_clause = true
+csharp_space_after_colon_in_inheritance_clause = true
+csharp_space_around_binary_operators = before_and_after
+csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
+csharp_space_between_method_call_name_and_opening_parenthesis = false
+csharp_space_between_method_call_empty_parameter_list_parentheses = false
+csharp_space_after_comma = true
+csharp_space_after_dot = false
+
+# Wrapping preferences
+csharp_preserve_single_line_statements = true
+csharp_preserve_single_line_blocks = true
+
+##################################
+# Visual Basic Code Style Rules #
+##################################
+
+[*.vb]
+# Modifier preferences
+visual_basic_preferred_modifier_order = Partial,Default,Private,Protected,Public,Friend,NotOverridable,Overridable,MustOverride,Overloads,Overrides,MustInherit,NotInheritable,Static,Shared,Shadows,ReadOnly,WriteOnly,Dim,Const,WithEvents,Widening,Narrowing,Custom,Async:suggestion
diff --git a/Src/RCommon.ApplicationServices/Commands/CommandBus.cs b/Src/RCommon.ApplicationServices/Commands/CommandBus.cs
index 6e8b0f1c..cbe8af5a 100644
--- a/Src/RCommon.ApplicationServices/Commands/CommandBus.cs
+++ b/Src/RCommon.ApplicationServices/Commands/CommandBus.cs
@@ -39,16 +39,33 @@
namespace RCommon.ApplicationServices.Commands
{
+ ///
+ /// Default implementation of that dispatches commands to their registered handlers
+ /// using the dependency injection container.
+ ///
+ ///
+ /// The command bus resolves the appropriate from the service provider,
+ /// optionally validates the command via , and invokes the handler using
+ /// a dynamically compiled delegate. Compiled handler delegates can optionally be cached for improved performance.
+ ///
public class CommandBus : ICommandBus
{
private readonly ILogger _logger;
private readonly IServiceProvider _serviceProvider;
private readonly IValidationService _validationService;
private readonly IOptions _validationOptions;
- private ICacheService _cacheService;
+ private ICacheService? _cacheService;
private readonly CachingOptions _cachingOptions;
- public CommandBus(ILogger logger, IServiceProvider serviceProvider, IValidationService validationService,
+ ///
+ /// Initializes a new instance of .
+ ///
+ /// Logger for tracing command execution.
+ /// Service provider used to resolve command handlers.
+ /// Service used to validate commands before execution.
+ /// Options controlling whether command validation is enabled.
+ /// Options controlling whether dynamically compiled expressions are cached.
+ public CommandBus(ILogger logger, IServiceProvider serviceProvider, IValidationService validationService,
IOptions validationOptions, IOptions cachingOptions)
{
_logger = logger;
@@ -58,11 +75,13 @@ public CommandBus(ILogger logger, IServiceProvider serviceProvider,
_cachingOptions = cachingOptions.Value;
}
+ ///
public async Task DispatchCommandAsync(ICommand command, CancellationToken cancellationToken = default)
where TResult : IExecutionResult
{
if (command == null) throw new ArgumentNullException(nameof(command));
+ // Validate the command if validation is configured for commands
if (_validationOptions.Value != null && _validationOptions.Value.ValidateCommands)
{
// TODO: Would be nice to be able to take validation outcome and put in FailedExecutionResult. Need some casting magic
@@ -86,15 +105,25 @@ public async Task DispatchCommandAsync(ICommand comma
commandResult?.IsSuccess);
}
- return commandResult;
+ return commandResult!;
}
+ ///
+ /// Resolves and invokes the single registered handler for the given command.
+ ///
+ /// The execution result type returned by the handler.
+ /// The command to execute.
+ /// Token to observe for cancellation.
+ /// The result produced by the command handler.
+ /// Thrown when no handler is registered for the command type.
+ /// Thrown when more than one handler is registered for the command type.
private async Task ExecuteHandlerAsync(ICommand command, CancellationToken cancellationToken)
where TResult : IExecutionResult
{
var commandType = command.GetType();
var commandExecutionDetails = GetCommandExecutionDetails(commandType);
+ // Resolve all registered handlers for this command type and enforce exactly one
var commandHandlers = _serviceProvider.GetServices(commandExecutionDetails.CommandHandlerType)
.Cast()
.ToList();
@@ -114,14 +143,21 @@ private async Task ExecuteHandlerAsync(ICommand comma
var commandHandler = commandHandlers.Single();
+ // Invoke the handler via the dynamically compiled delegate
var task = (Task)commandExecutionDetails.Invoker(commandHandler, command, cancellationToken);
return await task;
}
+ ///
+ /// Holds the resolved handler type and the compiled delegate used to invoke it.
+ ///
private class CommandExecutionDetails
{
- public Type CommandHandlerType { get; set; }
- public Func Invoker { get; set; }
+ /// Gets or sets the closed generic handler interface type for the command.
+ public Type CommandHandlerType { get; set; } = default!;
+
+ /// Gets or sets the compiled delegate that invokes HandleAsync on the handler.
+ public Func Invoker { get; set; } = default!;
}
private const string NameOfExecuteCommand = nameof(
@@ -129,26 +165,42 @@ private class CommandExecutionDetails
IExecutionResult,
ICommand
>.HandleAsync);
+
+ ///
+ /// Gets the for the given command type, optionally retrieving
+ /// from cache if expression caching is enabled.
+ ///
+ /// The runtime type of the command being dispatched.
+ /// The execution details containing the handler type and compiled invoker delegate.
private CommandExecutionDetails GetCommandExecutionDetails(Type commandType)
{
+ // When caching is enabled, cache the compiled expression to avoid repeated reflection/compilation
if (_cachingOptions.CachingEnabled && _cachingOptions.CacheDynamicallyCompiledExpressions)
{
var cachingFactory = _serviceProvider.GetService>();
Guard.Against(cachingFactory == null, "We could not properly inject the caching factory: 'ICommonFactory>' into the CommandBus");
- _cacheService = cachingFactory.Create(ExpressionCachingStrategy.Default);
+ _cacheService = cachingFactory!.Create(ExpressionCachingStrategy.Default);
return _cacheService.GetOrCreate(CacheKey.With(GetType(), commandType.GetCacheKey()), () => this.BuildCommandDetails(commandType));
}
return this.BuildCommandDetails(commandType);
}
+ ///
+ /// Builds the by reflecting over the command type to determine
+ /// the handler interface and compiling a delegate for HandleAsync.
+ ///
+ /// The runtime type of the command being dispatched.
+ /// A new instance.
private CommandExecutionDetails BuildCommandDetails(Type commandType)
{
+ // Find the ICommand interface to extract the result type
var commandInterfaceType = commandType
.GetTypeInfo()
.GetInterfaces()
.Single(i => i.GetTypeInfo().IsGenericType && i.GetGenericTypeDefinition() == typeof(ICommand<>));
var commandTypes = commandInterfaceType.GetTypeInfo().GetGenericArguments();
+ // Construct the closed generic ICommandHandler type
var commandHandlerType = typeof(ICommandHandler<,>)
.MakeGenericType(commandTypes[0], commandType);
@@ -157,6 +209,7 @@ private CommandExecutionDetails BuildCommandDetails(Type commandType)
commandType.PrettyPrint(),
commandHandlerType.PrettyPrint());
+ // Compile a strongly-typed delegate for the handler's HandleAsync method
var invokeExecuteAsync = ReflectionHelper.CompileMethodInvocation>(
commandHandlerType, NameOfExecuteCommand);
diff --git a/Src/RCommon.ApplicationServices/Commands/ICommandBus.cs b/Src/RCommon.ApplicationServices/Commands/ICommandBus.cs
index bddd6257..16a13f0c 100644
--- a/Src/RCommon.ApplicationServices/Commands/ICommandBus.cs
+++ b/Src/RCommon.ApplicationServices/Commands/ICommandBus.cs
@@ -28,9 +28,23 @@
namespace RCommon.ApplicationServices.Commands
{
+ ///
+ /// Defines the contract for a command bus that dispatches commands to their corresponding handlers.
+ ///
+ ///
+ /// Commands represent intent to change state. The bus resolves the appropriate
+ /// and returns an .
+ ///
public interface ICommandBus
{
- Task DispatchCommandAsync(ICommand command, CancellationToken cancellationToken = default)
+ ///
+ /// Dispatches a command to its registered handler and returns the execution result.
+ ///
+ /// The type of execution result returned by the handler.
+ /// The command to dispatch.
+ /// Optional token to cancel the operation.
+ /// The execution result produced by the command handler.
+ Task DispatchCommandAsync(ICommand command, CancellationToken cancellationToken = default)
where TResult : IExecutionResult;
}
}
diff --git a/Src/RCommon.ApplicationServices/Commands/ICommandHandler.cs b/Src/RCommon.ApplicationServices/Commands/ICommandHandler.cs
index 28bd7197..9855275c 100644
--- a/Src/RCommon.ApplicationServices/Commands/ICommandHandler.cs
+++ b/Src/RCommon.ApplicationServices/Commands/ICommandHandler.cs
@@ -9,6 +9,9 @@
namespace RCommon.ApplicationServices.Commands
{
+ ///
+ /// Non-generic marker interface for all command handlers. Used for service resolution via reflection.
+ ///
public interface ICommandHandler
{
}
@@ -18,6 +21,12 @@ public interface ICommandHandler
public interface ICommandHandler : ICommandHandler
where TCommand: ICommand
{
+ ///
+ /// Handles the specified command asynchronously.
+ ///
+ /// The command to handle.
+ /// Token to observe for cancellation.
+ /// A task representing the asynchronous operation.
Task HandleAsync(TCommand command, CancellationToken cancellationToken);
}
@@ -28,6 +37,12 @@ public interface ICommandHandler : ICommandHandler
where TCommand : ICommand
where TResult : IExecutionResult
{
+ ///
+ /// Handles the specified command asynchronously and returns an execution result.
+ ///
+ /// The command to handle.
+ /// Token to observe for cancellation.
+ /// The execution result produced by handling the command.
Task HandleAsync(TCommand command, CancellationToken cancellationToken);
}
}
diff --git a/Src/RCommon.ApplicationServices/Commands/NoCommandHandlersException.cs b/Src/RCommon.ApplicationServices/Commands/NoCommandHandlersException.cs
index 9327b1b3..1a21fc1e 100644
--- a/Src/RCommon.ApplicationServices/Commands/NoCommandHandlersException.cs
+++ b/Src/RCommon.ApplicationServices/Commands/NoCommandHandlersException.cs
@@ -25,8 +25,19 @@
namespace RCommon.ApplicationServices.Commands
{
+ ///
+ /// Exception thrown when no command handlers are registered for a given command type.
+ ///
+ ///
+ /// This exception is raised by when it cannot resolve any
+ /// implementation for the dispatched command.
+ ///
public class NoCommandHandlersException : Exception
{
+ ///
+ /// Initializes a new instance of with the specified error message.
+ ///
+ /// A message describing which command type has no registered handlers.
public NoCommandHandlersException(string message) : base(message)
{
}
diff --git a/Src/RCommon.ApplicationServices/CqrsBuilder.cs b/Src/RCommon.ApplicationServices/CqrsBuilder.cs
index 0af33a80..8afde1f5 100644
--- a/Src/RCommon.ApplicationServices/CqrsBuilder.cs
+++ b/Src/RCommon.ApplicationServices/CqrsBuilder.cs
@@ -10,21 +10,34 @@
namespace RCommon.ApplicationServices
{
+ ///
+ /// Default implementation of that registers the core CQRS services
+ /// ( and ) into the dependency injection container.
+ ///
public class CqrsBuilder : ICqrsBuilder
{
+ ///
+ /// Initializes a new instance of and registers the default CQRS services.
+ ///
+ /// The RCommon builder providing access to the service collection.
public CqrsBuilder(IRCommonBuilder builder)
{
Services = builder.Services;
this.RegisterServices(Services);
}
+ ///
+ /// Registers the default and as transient services.
+ ///
+ /// The service collection to register into.
protected void RegisterServices(IServiceCollection services)
{
services.AddTransient();
services.AddTransient();
-
+
}
+ ///
public IServiceCollection Services { get; }
}
}
diff --git a/Src/RCommon.ApplicationServices/CqrsBuilderExtensions.cs b/Src/RCommon.ApplicationServices/CqrsBuilderExtensions.cs
index 941f98dd..33e5e0c2 100644
--- a/Src/RCommon.ApplicationServices/CqrsBuilderExtensions.cs
+++ b/Src/RCommon.ApplicationServices/CqrsBuilderExtensions.cs
@@ -37,8 +37,18 @@
namespace RCommon
{
+ ///
+ /// Extension methods for and that provide
+ /// fluent registration of CQRS infrastructure, command handlers, and query handlers.
+ ///
public static class CqrsBuilderExtensions
{
+ ///
+ /// Adds CQRS support using the specified implementation with default configuration.
+ ///
+ /// The implementation type to use.
+ /// The RCommon builder.
+ /// The for further chaining.
public static IRCommonBuilder WithCQRS(this IRCommonBuilder builder)
where T : ICqrsBuilder
{
@@ -46,16 +56,30 @@ public static IRCommonBuilder WithCQRS(this IRCommonBuilder builder)
return WithCQRS(builder, x => { });
}
+ ///
+ /// Adds CQRS support using the specified implementation and applies additional configuration.
+ ///
+ /// The implementation type to use.
+ /// The RCommon builder.
+ /// A delegate to configure the CQRS builder (e.g., register handlers).
+ /// The for further chaining.
public static IRCommonBuilder WithCQRS(this IRCommonBuilder builder, Action actions)
where T : ICqrsBuilder
{
- // Event Handling Configurations
- var cqrsBuilder = (T)Activator.CreateInstance(typeof(T), new object[] { builder });
+ // Instantiate the CQRS builder implementation, which registers core bus services in its constructor
+ var cqrsBuilder = (T)Activator.CreateInstance(typeof(T), new object[] { builder })!;
actions(cqrsBuilder);
return builder;
}
+ ///
+ /// Registers a query handler as a transient service for the specified query and result types.
+ ///
+ /// The query handler implementation type.
+ /// The query type.
+ /// The query result type.
+ /// The CQRS builder.
public static void AddQueryHandler(this ICqrsBuilder builder)
where TQueryHandler : class, IQueryHandler
where TQuery : IQuery
@@ -63,6 +87,14 @@ public static void AddQueryHandler(this ICqrsBui
builder.Services.AddTransient, TQueryHandler>();
}
+ ///
+ /// Registers a query handler as a transient service. This is an alias for
+ /// with a query-first type parameter order.
+ ///
+ /// The query type.
+ /// The query handler implementation type.
+ /// The query result type.
+ /// The CQRS builder.
public static void AddQuery(this ICqrsBuilder builder)
where TQueryHandler : class, IQueryHandler
where TQuery : IQuery
@@ -70,6 +102,13 @@ public static void AddQuery(this ICqrsBuilder bu
builder.Services.AddTransient, TQueryHandler>();
}
+ ///
+ /// Registers a command handler as a transient service for the specified command and result types.
+ ///
+ /// The command handler implementation type.
+ /// The command type.
+ /// The execution result type.
+ /// The CQRS builder.
public static void AddCommandHandler(this ICqrsBuilder builder)
where TCommandHandler : class, ICommandHandler
where TCommand : ICommand
@@ -78,6 +117,14 @@ public static void AddCommandHandler(this IC
builder.Services.AddTransient, TCommandHandler>();
}
+ ///
+ /// Registers a command handler as a transient service. This is an alias for
+ /// with a command-first type parameter order.
+ ///
+ /// The command type.
+ /// The command handler implementation type.
+ /// The execution result type.
+ /// The CQRS builder.
public static void AddCommand(this ICqrsBuilder builder)
where TCommandHandler : class, ICommandHandler
where TCommand : ICommand
@@ -86,27 +133,51 @@ public static void AddCommand(this ICqrsBuil
builder.Services.AddTransient, TCommandHandler>();
}
- public static void AddCommandHandlers(this ICqrsBuilder builder, Assembly fromAssembly, Predicate predicate = null)
+ ///
+ /// Scans the specified assembly for implementations
+ /// and registers them as transient services.
+ ///
+ /// The CQRS builder.
+ /// The assembly to scan for command handler types.
+ /// An optional predicate to filter which handler types to register.
+ ///
+ /// Types whose constructors accept an parameter are excluded
+ /// to avoid registering decorator types as base handlers.
+ ///
+ public static void AddCommandHandlers(this ICqrsBuilder builder, Assembly fromAssembly, Predicate? predicate = null)
{
predicate = predicate ?? (t => true);
var commandHandlerTypes = fromAssembly
.GetTypes()
.Where(t => t.GetTypeInfo().GetInterfaces().Any(IsCommandHandlerInterface))
+ // Exclude types that accept a command handler in their constructor (likely decorators)
.Where(t => !t.HasConstructorParameterOfType(IsCommandHandlerInterface))
.Where(t => predicate(t));
AddCommandHandlers(builder, commandHandlerTypes);
}
+ ///
+ /// Registers the specified command handler types as transient services.
+ ///
+ /// The CQRS builder.
+ /// The command handler types to register.
public static void AddCommandHandlers(this ICqrsBuilder builder, params Type[] commandHandlerTypes)
{
AddCommandHandlers(builder, (IEnumerable)commandHandlerTypes);
}
+ ///
+ /// Registers the specified command handler types as transient services.
+ ///
+ /// The CQRS builder.
+ /// The command handler types to register.
+ /// Thrown when a type does not implement .
public static void AddCommandHandlers(this ICqrsBuilder builder, IEnumerable commandHandlerTypes)
{
foreach (var commandHandlerType in commandHandlerTypes)
{
var t = commandHandlerType;
+ // Skip abstract types as they cannot be instantiated
if (t.GetTypeInfo().IsAbstract) continue;
var handlesCommandTypes = t
.GetTypeInfo()
@@ -118,6 +189,7 @@ public static void AddCommandHandlers(this ICqrsBuilder builder, IEnumerable).PrettyPrint()}'");
}
+ // Register the handler type for each command handler interface it implements
foreach (var handlesCommandType in handlesCommandTypes)
{
builder.Services.AddTransient(handlesCommandType, t);
@@ -125,33 +197,60 @@ public static void AddCommandHandlers(this ICqrsBuilder builder, IEnumerable
+ /// Determines whether the specified type is a closed generic form of .
+ ///
private static bool IsCommandHandlerInterface(this Type type)
{
return type.GetTypeInfo().IsGenericType && type.GetGenericTypeDefinition() == typeof(ICommandHandler<,>);
}
+ ///
+ /// Registers the specified query handler types as transient services.
+ ///
+ /// The CQRS builder.
+ /// The query handler types to register.
public static void AddQueryHandlers(this ICqrsBuilder builder, params Type[] queryHandlerTypes)
{
AddQueryHandlers(builder, (IEnumerable)queryHandlerTypes);
}
+ ///
+ /// Scans the specified assembly for implementations
+ /// and registers them as transient services.
+ ///
+ /// The CQRS builder.
+ /// The assembly to scan for query handler types.
+ /// An optional predicate to filter which handler types to register.
+ ///
+ /// Types whose constructors accept an parameter are excluded
+ /// to avoid registering decorator types as base handlers.
+ ///
public static void AddQueryHandlers(this ICqrsBuilder builder, Assembly fromAssembly,
- Predicate predicate = null)
+ Predicate? predicate = null)
{
predicate = predicate ?? (t => true);
var subscribeSynchronousToTypes = fromAssembly
.GetTypes()
.Where(t => t.GetTypeInfo().GetInterfaces().Any(IsQueryHandlerInterface))
+ // Exclude types that accept a query handler in their constructor (likely decorators)
.Where(t => !t.HasConstructorParameterOfType(IsQueryHandlerInterface))
.Where(t => predicate(t));
AddQueryHandlers(builder, subscribeSynchronousToTypes);
}
+ ///
+ /// Registers the specified query handler types as transient services.
+ ///
+ /// The CQRS builder.
+ /// The query handler types to register.
+ /// Thrown when a type does not implement .
public static void AddQueryHandlers(this ICqrsBuilder builder, IEnumerable queryHandlerTypes)
{
foreach (var queryHandlerType in queryHandlerTypes)
{
var t = queryHandlerType;
+ // Skip abstract types as they cannot be instantiated
if (t.GetTypeInfo().IsAbstract) continue;
var queryHandlerInterfaces = t
.GetTypeInfo()
@@ -163,6 +262,7 @@ public static void AddQueryHandlers(this ICqrsBuilder builder, IEnumerable
throw new ArgumentException($"Type '{t.PrettyPrint()}' is not an '{typeof(IQueryHandler<,>).PrettyPrint()}'");
}
+ // Register the handler type for each query handler interface it implements
foreach (var queryHandlerInterface in queryHandlerInterfaces)
{
builder.Services.AddTransient(queryHandlerInterface, t);
@@ -170,6 +270,9 @@ public static void AddQueryHandlers(this ICqrsBuilder builder, IEnumerable
}
}
+ ///
+ /// Determines whether the specified type is a closed generic form of .
+ ///
private static bool IsQueryHandlerInterface(this Type type)
{
return type.GetTypeInfo().IsGenericType && type.GetGenericTypeDefinition() == typeof(IQueryHandler<,>);
diff --git a/Src/RCommon.ApplicationServices/CqrsCachingOptions.cs b/Src/RCommon.ApplicationServices/CqrsCachingOptions.cs
index 69c8878e..1c6bb330 100644
--- a/Src/RCommon.ApplicationServices/CqrsCachingOptions.cs
+++ b/Src/RCommon.ApplicationServices/CqrsCachingOptions.cs
@@ -6,13 +6,27 @@
namespace RCommon.ApplicationServices
{
+ ///
+ /// Configuration options for caching behavior within the CQRS pipeline.
+ ///
+ ///
+ /// When is enabled, the command and query buses may cache
+ /// resolved handler metadata to reduce reflection overhead on subsequent dispatches.
+ /// Defaults to false.
+ ///
public class CqrsCachingOptions
{
+ ///
+ /// Initializes a new instance of with caching disabled.
+ ///
public CqrsCachingOptions()
{
this.UseCacheForHandlers = false;
}
+ ///
+ /// Gets or sets a value indicating whether resolved handler metadata should be cached.
+ ///
public bool UseCacheForHandlers { get; set; }
}
}
diff --git a/Src/RCommon.ApplicationServices/CqrsValidationOptions.cs b/Src/RCommon.ApplicationServices/CqrsValidationOptions.cs
index ef14d8b3..b5c069ec 100644
--- a/Src/RCommon.ApplicationServices/CqrsValidationOptions.cs
+++ b/Src/RCommon.ApplicationServices/CqrsValidationOptions.cs
@@ -7,15 +7,33 @@
namespace RCommon.ApplicationServices
{
+ ///
+ /// Configuration options that control whether automatic validation is applied to CQRS commands and queries.
+ ///
+ ///
+ /// When enabled, the and will invoke
+ /// before dispatching to handlers.
+ /// Both options default to false.
+ ///
public class CqrsValidationOptions
{
+ ///
+ /// Initializes a new instance of with validation disabled for both commands and queries.
+ ///
public CqrsValidationOptions()
{
ValidateQueries = false;
ValidateCommands = false;
}
+ ///
+ /// Gets or sets a value indicating whether queries should be validated before dispatch.
+ ///
public bool ValidateQueries { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether commands should be validated before dispatch.
+ ///
public bool ValidateCommands { get; set; }
}
}
diff --git a/Src/RCommon.ApplicationServices/ICqrsBuilder.cs b/Src/RCommon.ApplicationServices/ICqrsBuilder.cs
index 3eba4b2b..4a4404c1 100644
--- a/Src/RCommon.ApplicationServices/ICqrsBuilder.cs
+++ b/Src/RCommon.ApplicationServices/ICqrsBuilder.cs
@@ -2,8 +2,18 @@
namespace RCommon.ApplicationServices
{
+ ///
+ /// Defines the contract for a builder that configures CQRS (Command Query Responsibility Segregation) services.
+ ///
+ ///
+ /// Implementations register command and query bus infrastructure into the DI container.
+ /// Use extension methods on this interface (e.g., AddCommandHandler, AddQueryHandler) to register handlers.
+ ///
public interface ICqrsBuilder
{
+ ///
+ /// Gets the used to register CQRS-related services.
+ ///
IServiceCollection Services { get; }
}
}
\ No newline at end of file
diff --git a/Src/RCommon.ApplicationServices/IValidationBuilder.cs b/Src/RCommon.ApplicationServices/IValidationBuilder.cs
index 3fe67def..a7d1b5d7 100644
--- a/Src/RCommon.ApplicationServices/IValidationBuilder.cs
+++ b/Src/RCommon.ApplicationServices/IValidationBuilder.cs
@@ -7,8 +7,19 @@
namespace RCommon.ApplicationServices
{
+ ///
+ /// Defines the contract for a builder that configures validation services.
+ ///
+ ///
+ /// Implementations register and related services
+ /// into the DI container. Use to integrate
+ /// validation with the CQRS pipeline.
+ ///
public interface IValidationBuilder
{
+ ///
+ /// Gets the used to register validation-related services.
+ ///
IServiceCollection Services { get; }
}
}
diff --git a/Src/RCommon.ApplicationServices/InvalidCacheException.cs b/Src/RCommon.ApplicationServices/InvalidCacheException.cs
index 90281342..a08b20fa 100644
--- a/Src/RCommon.ApplicationServices/InvalidCacheException.cs
+++ b/Src/RCommon.ApplicationServices/InvalidCacheException.cs
@@ -6,11 +6,23 @@
namespace RCommon.ApplicationServices
{
+ ///
+ /// Exception thrown when the caching infrastructure is not properly configured or available.
+ ///
+ ///
+ /// This exception is raised by or
+ /// when expression caching is enabled but the required ICommonFactory<ExpressionCachingStrategy, ICacheService>
+ /// cannot be resolved from the service provider.
+ ///
public class InvalidCacheException : GeneralException
{
+ ///
+ /// Initializes a new instance of with the specified error message.
+ ///
+ /// A message describing the caching configuration problem.
public InvalidCacheException(string message):base(message)
{
-
+
}
}
}
diff --git a/Src/RCommon.ApplicationServices/Queries/IQueryBus.cs b/Src/RCommon.ApplicationServices/Queries/IQueryBus.cs
index dac776bd..0a9f7057 100644
--- a/Src/RCommon.ApplicationServices/Queries/IQueryBus.cs
+++ b/Src/RCommon.ApplicationServices/Queries/IQueryBus.cs
@@ -8,8 +8,22 @@
namespace RCommon.ApplicationServices.Queries
{
+ ///
+ /// Defines the contract for a query bus that dispatches queries to their corresponding handlers.
+ ///
+ ///
+ /// Queries represent requests for data that do not modify state. The bus resolves the appropriate
+ /// and returns the result.
+ ///
public interface IQueryBus
{
+ ///
+ /// Dispatches a query to its registered handler and returns the result.
+ ///
+ /// The type of result returned by the query handler.
+ /// The query to dispatch.
+ /// Optional token to cancel the operation.
+ /// The result produced by the query handler.
Task DispatchQueryAsync(IQuery query, CancellationToken cancellationToken = default);
}
}
diff --git a/Src/RCommon.ApplicationServices/Queries/IQueryHandler.cs b/Src/RCommon.ApplicationServices/Queries/IQueryHandler.cs
index a1cde1a7..58f7609a 100644
--- a/Src/RCommon.ApplicationServices/Queries/IQueryHandler.cs
+++ b/Src/RCommon.ApplicationServices/Queries/IQueryHandler.cs
@@ -8,13 +8,27 @@
namespace RCommon.ApplicationServices.Queries
{
+ ///
+ /// Non-generic marker interface for all query handlers. Used for service resolution via reflection.
+ ///
public interface IQueryHandler
{
}
+ ///
+ /// Defines the contract for a handler that processes a specific query type and returns a result.
+ ///
+ /// The query type to handle.
+ /// The type of result produced by handling the query.
public interface IQueryHandler : IQueryHandler
where TQuery : IQuery
{
+ ///
+ /// Handles the specified query asynchronously and returns the result.
+ ///
+ /// The query to handle.
+ /// Token to observe for cancellation.
+ /// The result produced by handling the query.
Task HandleAsync(TQuery query, CancellationToken cancellationToken);
}
}
diff --git a/Src/RCommon.ApplicationServices/Queries/QueryBus.cs b/Src/RCommon.ApplicationServices/Queries/QueryBus.cs
index c55bff38..9cbf09e7 100644
--- a/Src/RCommon.ApplicationServices/Queries/QueryBus.cs
+++ b/Src/RCommon.ApplicationServices/Queries/QueryBus.cs
@@ -36,12 +36,27 @@
namespace RCommon.ApplicationServices.Queries
{
+ ///
+ /// Default implementation of that dispatches queries to their registered handlers
+ /// using the dependency injection container.
+ ///
+ ///
+ /// The query bus resolves the appropriate from the service provider,
+ /// optionally validates the query via , and invokes the handler using
+ /// a dynamically compiled delegate. Compiled handler delegates can optionally be cached for improved performance.
+ ///
public class QueryBus : IQueryBus
{
+ ///
+ /// Holds the resolved handler type and the compiled delegate used to invoke it.
+ ///
private class HandlerFuncMapping
{
- public Type QueryHandlerType { get; set; }
- public Func HandlerFunc { get; set; }
+ /// Gets or sets the closed generic handler interface type for the query.
+ public Type QueryHandlerType { get; set; } = default!;
+
+ /// Gets or sets the compiled delegate that invokes HandleAsync on the handler.
+ public Func HandlerFunc { get; set; } = default!;
}
private readonly ILogger _logger;
@@ -49,8 +64,16 @@ private class HandlerFuncMapping
private readonly IValidationService _validationService;
private readonly IOptions _validationOptions;
private readonly CachingOptions _cachingOptions;
- private ICacheService _cacheService;
+ private ICacheService? _cacheService;
+ ///
+ /// Initializes a new instance of .
+ ///
+ /// Logger for tracing query execution.
+ /// Service provider used to resolve query handlers.
+ /// Service used to validate queries before execution.
+ /// Options controlling whether query validation is enabled.
+ /// Options controlling whether dynamically compiled expressions are cached.
public QueryBus(ILogger logger, IServiceProvider serviceProvider, IValidationService validationService,
IOptions validationOptions, IOptions cachingOptions)
{
@@ -61,8 +84,10 @@ public QueryBus(ILogger logger, IServiceProvider serviceProvider, IVal
_cachingOptions = cachingOptions.Value;
}
+ ///
public async Task DispatchQueryAsync(IQuery query, CancellationToken cancellationToken = default)
{
+ // Validate the query if validation is configured for queries
if (_validationOptions.Value != null && _validationOptions.Value.ValidateQueries)
{
// TODO: Would be nice to be able to take validation outcome and put in IQuery. Need some casting magic
@@ -82,32 +107,51 @@ public async Task DispatchQueryAsync(IQuery query, Ca
queryHandler.GetType().PrettyPrint());
}
+ // Invoke the handler via the dynamically compiled delegate
var task = (Task)handlerFunc.HandlerFunc(queryHandler, query, cancellationToken);
return await task.ConfigureAwait(false);
}
+ ///
+ /// Gets the for the given query type, optionally retrieving
+ /// from cache if expression caching is enabled.
+ ///
+ /// The runtime type of the query being dispatched.
+ /// The handler function mapping containing the handler type and compiled invoker delegate.
private HandlerFuncMapping GetHandlerFuncMapping(Type queryType)
{
+ // When caching is enabled, cache the compiled expression to avoid repeated reflection/compilation
if (_cachingOptions.CachingEnabled && _cachingOptions.CacheDynamicallyCompiledExpressions)
{
var cachingFactory = _serviceProvider.GetService>();
Guard.Against(cachingFactory == null, "We could not properly inject the caching factory: 'ICommonFactory>' into the QueryBus");
- _cacheService = cachingFactory.Create(ExpressionCachingStrategy.Default);
- return _cacheService.GetOrCreate(CacheKey.With(GetType(), queryType.GetCacheKey()),
+ _cacheService = cachingFactory!.Create(ExpressionCachingStrategy.Default);
+ return _cacheService.GetOrCreate(CacheKey.With(GetType(), queryType.GetCacheKey()),
() => this.BuildHandlerFuncMapping(queryType));
}
return this.BuildHandlerFuncMapping(queryType);
-
+
}
+ ///
+ /// Builds the by reflecting over the query type to determine
+ /// the handler interface and compiling a delegate for HandleAsync.
+ ///
+ /// The runtime type of the query being dispatched.
+ /// A new instance.
private HandlerFuncMapping BuildHandlerFuncMapping(Type queryType)
{
+ // Find the IQuery interface to extract the result type
var queryInterfaceType = queryType
.GetTypeInfo()
.GetInterfaces()
.Single(i => i.GetTypeInfo().IsGenericType && i.GetGenericTypeDefinition() == typeof(IQuery<>));
+
+ // Construct the closed generic IQueryHandler type
var queryHandlerType = typeof(IQueryHandler<,>).MakeGenericType(queryType, queryInterfaceType.GetTypeInfo().GetGenericArguments()[0]);
+
+ // Compile a strongly-typed delegate for the handler's HandleAsync method
var invokeExecuteQueryAsync = ReflectionHelper.CompileMethodInvocation>(
queryHandlerType,
"HandleAsync",
diff --git a/Src/RCommon.ApplicationServices/RCommon.ApplicationServices.csproj b/Src/RCommon.ApplicationServices/RCommon.ApplicationServices.csproj
index 4f303a75..ebc47211 100644
--- a/Src/RCommon.ApplicationServices/RCommon.ApplicationServices.csproj
+++ b/Src/RCommon.ApplicationServices/RCommon.ApplicationServices.csproj
@@ -2,6 +2,7 @@
net8.0;net9.0;net10.0
+ enableTrueRCommon.ApplicationServiceshttps://rcommon.com
diff --git a/Src/RCommon.ApplicationServices/README.md b/Src/RCommon.ApplicationServices/README.md
index 88074100..2c74cc78 100644
--- a/Src/RCommon.ApplicationServices/README.md
+++ b/Src/RCommon.ApplicationServices/README.md
@@ -1,3 +1,107 @@
# RCommon.ApplicationServices
-A cohesive set of infrastructure libraries for dotnet that utilizes abstractions for event handling, persistence, unit of work, mediator, distributed messaging, event bus, CQRS, email, and more
+Provides a CQRS (Command Query Responsibility Segregation) implementation with dedicated command and query buses, handler registration, and optional validation integration for the RCommon framework.
+
+## Features
+
+- **Command Bus** -- dispatches commands to a single registered `ICommandHandler` and returns an `IExecutionResult`
+- **Query Bus** -- dispatches queries to a single registered `IQueryHandler` and returns a typed result
+- **Validation pipeline** -- optionally validates commands and/or queries before handler execution via `IValidationService`
+- **Handler registration** -- register handlers individually or scan assemblies with automatic decorator exclusion
+- **Expression caching** -- dynamically compiled handler delegates can be cached for improved dispatch performance
+- **Fluent builder API** -- integrates with the `AddRCommon()` builder pattern for clean DI configuration
+
+## Installation
+
+```shell
+dotnet add package RCommon.ApplicationServices
+```
+
+## Usage
+
+```csharp
+using RCommon;
+using RCommon.ApplicationServices;
+
+// Configure CQRS in your DI setup
+services.AddRCommon(config =>
+{
+ config.WithCQRS(cqrs =>
+ {
+ // Register handlers individually
+ cqrs.AddCommandHandler();
+ cqrs.AddQueryHandler();
+
+ // Or scan an assembly for all handlers
+ cqrs.AddCommandHandlers(typeof(CreateOrderHandler).Assembly);
+ cqrs.AddQueryHandlers(typeof(GetOrderHandler).Assembly);
+ });
+});
+
+// Dispatch a command from your application layer
+public class OrderService
+{
+ private readonly ICommandBus _commandBus;
+ private readonly IQueryBus _queryBus;
+
+ public OrderService(ICommandBus commandBus, IQueryBus queryBus)
+ {
+ _commandBus = commandBus;
+ _queryBus = queryBus;
+ }
+
+ public async Task CreateOrderAsync(CreateOrderCommand command)
+ {
+ return await _commandBus.DispatchCommandAsync(command);
+ }
+
+ public async Task GetOrderAsync(GetOrderQuery query)
+ {
+ return await _queryBus.DispatchQueryAsync(query);
+ }
+}
+```
+
+### Enabling Validation
+
+```csharp
+services.AddRCommon(config =>
+{
+ config.WithValidation(validation =>
+ {
+ validation.UseWithCqrs(options =>
+ {
+ options.ValidateCommands = true;
+ options.ValidateQueries = true;
+ });
+ });
+});
+```
+
+## Key Types
+
+| Type | Description |
+|------|-------------|
+| `ICommandBus` | Dispatches commands to their registered handler and returns an `IExecutionResult` |
+| `IQueryBus` | Dispatches queries to their registered handler and returns a typed result |
+| `ICommandHandler` | Handles a specific command type and produces an execution result |
+| `IQueryHandler` | Handles a specific query type and produces a result |
+| `IValidationService` | Validates objects before dispatch; integrates with the CQRS pipeline |
+| `ValidationOutcome` | Contains a list of `ValidationFault` errors produced by validation |
+| `ValidationFault` | Describes a single validation failure with property name, message, and severity |
+| `CqrsValidationOptions` | Controls whether commands and/or queries are validated before dispatch |
+| `CqrsBuilder` | Default `ICqrsBuilder` implementation that registers `CommandBus` and `QueryBus` |
+
+## Documentation
+
+For full documentation, visit [rcommon.com](https://rcommon.com).
+
+## Related Packages
+
+- [RCommon.Core](https://www.nuget.org/packages/RCommon.Core) - Core abstractions and builder infrastructure
+- [RCommon.FluentValidation](https://www.nuget.org/packages/RCommon.FluentValidation) - FluentValidation-based `IValidationProvider` for CQRS pipeline integration
+- [RCommon.Models](https://www.nuget.org/packages/RCommon.Models) - `ICommand`, `IQuery`, and `IExecutionResult` model contracts
+
+## License
+
+Licensed under the [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0).
diff --git a/Src/RCommon.ApplicationServices/Validation/IValidationProvider.cs b/Src/RCommon.ApplicationServices/Validation/IValidationProvider.cs
index 1a8a2846..3c36872d 100644
--- a/Src/RCommon.ApplicationServices/Validation/IValidationProvider.cs
+++ b/Src/RCommon.ApplicationServices/Validation/IValidationProvider.cs
@@ -7,10 +7,26 @@
namespace RCommon.ApplicationServices.Validation
{
+ ///
+ /// Defines the contract for a validation provider that performs validation against a target object.
+ ///
+ ///
+ /// Implementations bridge to a specific validation library (e.g., FluentValidation).
+ /// The resolves an from the DI container
+ /// to perform the actual validation logic.
+ ///
public interface IValidationProvider
{
+ ///
+ /// Validates the specified target object asynchronously.
+ ///
+ /// The type of the object to validate.
+ /// The object to validate.
+ /// If true, a is thrown when validation fails.
+ /// Optional token to cancel the operation.
+ /// A containing any validation faults.
Task ValidateAsync(T target, bool throwOnFaults, CancellationToken cancellationToken = default)
where T : class;
-
+
}
}
diff --git a/Src/RCommon.ApplicationServices/Validation/IValidationService.cs b/Src/RCommon.ApplicationServices/Validation/IValidationService.cs
index b3dc5a78..b257c59d 100644
--- a/Src/RCommon.ApplicationServices/Validation/IValidationService.cs
+++ b/Src/RCommon.ApplicationServices/Validation/IValidationService.cs
@@ -3,8 +3,24 @@
namespace RCommon.ApplicationServices.Validation
{
+ ///
+ /// Defines the contract for a validation service that orchestrates object validation.
+ ///
+ ///
+ /// The validation service acts as a facade over , creating a DI scope
+ /// and delegating to the registered provider. It is used by
+ /// and when CQRS validation is enabled.
+ ///
public interface IValidationService
{
+ ///
+ /// Validates the specified target object asynchronously.
+ ///
+ /// The type of the object to validate.
+ /// The object to validate.
+ /// If true, a is thrown when validation fails. Defaults to false.
+ /// Optional token to cancel the operation.
+ /// A containing any validation faults.
Task ValidateAsync(T target, bool throwOnFaults = false, CancellationToken cancellationToken = default) where T : class;
}
}
\ No newline at end of file
diff --git a/Src/RCommon.ApplicationServices/Validation/ValidationException.cs b/Src/RCommon.ApplicationServices/Validation/ValidationException.cs
index fbbb82dc..56753306 100644
--- a/Src/RCommon.ApplicationServices/Validation/ValidationException.cs
+++ b/Src/RCommon.ApplicationServices/Validation/ValidationException.cs
@@ -35,30 +35,30 @@ public class ValidationException : Exception
public IEnumerable Errors { get; private set; }
///
- /// Creates a new ValidationException
+ /// Creates a new with a message and no errors.
///
- ///
+ /// The exception message.
public ValidationException(string message) : this(message, Enumerable.Empty())
{
}
///
- /// Creates a new ValidationException
+ /// Creates a new with a message and a collection of validation errors.
///
- ///
- ///
+ /// The exception message.
+ /// The collection of instances representing the validation failures.
public ValidationException(string message, IEnumerable errors) : base(message)
{
Errors = errors;
}
///
- /// Creates a new ValidationException
+ /// Creates a new with a message, validation errors, and an option to append the default error summary.
///
- ///
- ///
- /// appends default validation error message to message
+ /// The exception message.
+ /// The collection of instances representing the validation failures.
+ /// If true, appends the formatted validation error summary to .
public ValidationException(string message, IEnumerable errors, bool appendDefaultMessage)
: base(appendDefaultMessage ? $"{message} {BuildErrorMessage(errors)}" : message)
{
@@ -66,14 +66,20 @@ public ValidationException(string message, IEnumerable errors,
}
///
- /// Creates a new ValidationException
+ /// Creates a new from a collection of validation errors, using a generated error summary as the message.
///
- ///
+ /// The collection of instances representing the validation failures.
public ValidationException(IEnumerable errors) : base(BuildErrorMessage(errors))
{
Errors = errors;
}
+ ///
+ /// Builds a human-readable error message from a collection of instances,
+ /// listing each property name, error message, and severity.
+ ///
+ /// The validation faults to format.
+ /// A formatted string summarizing all validation failures.
private static string BuildErrorMessage(IEnumerable errors)
{
var arr = errors.Select(x => $"{Environment.NewLine} -- {x.PropertyName}: {x.ErrorMessage} Severity: {x.Severity.ToString()}");
diff --git a/Src/RCommon.ApplicationServices/Validation/ValidationFault.cs b/Src/RCommon.ApplicationServices/Validation/ValidationFault.cs
index dbbfd669..b87e876f 100644
--- a/Src/RCommon.ApplicationServices/Validation/ValidationFault.cs
+++ b/Src/RCommon.ApplicationServices/Validation/ValidationFault.cs
@@ -39,15 +39,20 @@ public ValidationFault()
///
/// Creates a new validation failure.
///
+ /// The name of the property that failed validation.
+ /// The error message describing the failure.
public ValidationFault(string propertyName, string errorMessage) : this(propertyName, errorMessage, null)
{
}
///
- /// Creates a new ValidationFailure.
+ /// Creates a new validation failure.
///
- public ValidationFault(string propertyName, string errorMessage, object attemptedValue)
+ /// The name of the property that failed validation.
+ /// The error message describing the failure.
+ /// The value that was rejected by the validation rule.
+ public ValidationFault(string propertyName, string errorMessage, object? attemptedValue)
{
PropertyName = propertyName;
ErrorMessage = errorMessage;
@@ -57,22 +62,22 @@ public ValidationFault(string propertyName, string errorMessage, object attempte
///
/// The name of the property.
///
- public string PropertyName { get; set; }
+ public string? PropertyName { get; set; }
///
/// The error message
///
- public string ErrorMessage { get; set; }
+ public string? ErrorMessage { get; set; }
///
/// The property value that caused the failure.
///
- public object AttemptedValue { get; set; }
+ public object? AttemptedValue { get; set; }
///
/// Custom state associated with the failure.
///
- public object CustomState { get; set; }
+ public object? CustomState { get; set; }
///
/// Custom severity level associated with the failure.
@@ -82,17 +87,17 @@ public ValidationFault(string propertyName, string errorMessage, object attempte
///
/// Gets or sets the error code.
///
- public string ErrorCode { get; set; }
+ public string? ErrorCode { get; set; }
///
/// Gets or sets the formatted message placeholder values.
///
- public Dictionary FormattedMessagePlaceholderValues { get; set; }
+ public Dictionary? FormattedMessagePlaceholderValues { get; set; }
///
/// Creates a textual representation of the failure.
///
- public override string ToString()
+ public override string? ToString()
{
return ErrorMessage;
}
diff --git a/Src/RCommon.ApplicationServices/Validation/ValidationOutcome.cs b/Src/RCommon.ApplicationServices/Validation/ValidationOutcome.cs
index 3d65ce5b..d616f7ac 100644
--- a/Src/RCommon.ApplicationServices/Validation/ValidationOutcome.cs
+++ b/Src/RCommon.ApplicationServices/Validation/ValidationOutcome.cs
@@ -57,7 +57,7 @@ public List Errors
///
/// The RuleSets that were executed during the validation run.
///
- public string[] RuleSetsExecuted { get; set; }
+ public string[]? RuleSetsExecuted { get; set; }
///
/// Creates a new ValidationResult
@@ -81,13 +81,13 @@ public ValidationOutcome(IEnumerable failures)
}
///
- /// Creates a new ValidationResult by combining several other ValidationResults.
+ /// Creates a new by combining the errors and rule sets from several other validation results.
///
- ///
+ /// The validation outcomes to merge into a single result.
public ValidationOutcome(IEnumerable otherResults)
{
_errors = otherResults.SelectMany(x => x.Errors).ToList();
- RuleSetsExecuted = otherResults.Where(x => x.RuleSetsExecuted != null).SelectMany(x => x.RuleSetsExecuted).Distinct().ToArray();
+ RuleSetsExecuted = otherResults.Where(x => x.RuleSetsExecuted != null).SelectMany(x => x.RuleSetsExecuted!).Distinct().ToArray();
}
internal ValidationOutcome(List errors)
@@ -123,10 +123,10 @@ public string ToString(string separator)
public IDictionary ToDictionary()
{
return Errors
- .GroupBy(x => x.PropertyName)
+ .GroupBy(x => x.PropertyName ?? string.Empty)
.ToDictionary(
g => g.Key,
- g => g.Select(x => x.ErrorMessage).ToArray()
+ g => g.Select(x => x.ErrorMessage ?? string.Empty).ToArray()
);
}
}
diff --git a/Src/RCommon.ApplicationServices/Validation/ValidationService.cs b/Src/RCommon.ApplicationServices/Validation/ValidationService.cs
index f3ac0b3d..ab5c785b 100644
--- a/Src/RCommon.ApplicationServices/Validation/ValidationService.cs
+++ b/Src/RCommon.ApplicationServices/Validation/ValidationService.cs
@@ -8,23 +8,37 @@
namespace RCommon.ApplicationServices.Validation
{
+ ///
+ /// Default implementation of that delegates validation
+ /// to a scoped .
+ ///
+ ///
+ /// A new DI scope is created for each validation call to ensure that scoped validation
+ /// providers (and their dependencies) are properly resolved and disposed.
+ ///
public class ValidationService : IValidationService
{
private readonly IServiceProvider _serviceProvider;
+ ///
+ /// Initializes a new instance of .
+ ///
+ /// The root service provider used to create scoped validation providers.
public ValidationService(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
+ ///
public async Task ValidateAsync(T target, bool throwOnFaults = false, CancellationToken cancellationToken = default)
where T : class
{
+ // Create a new scope so that scoped IValidationProvider instances are properly resolved
using (var scope = _serviceProvider.CreateScope())
{
var provider = scope.ServiceProvider.GetService();
- Guard.IsNotNull(provider, nameof(provider));
- var outcome = await provider.ValidateAsync(target, throwOnFaults, cancellationToken);
+ Guard.IsNotNull(provider!, nameof(provider));
+ var outcome = await provider!.ValidateAsync(target, throwOnFaults, cancellationToken);
return outcome;
}
}
diff --git a/Src/RCommon.ApplicationServices/ValidationBuilderExtensions.cs b/Src/RCommon.ApplicationServices/ValidationBuilderExtensions.cs
index 9cdf02c2..df50f837 100644
--- a/Src/RCommon.ApplicationServices/ValidationBuilderExtensions.cs
+++ b/Src/RCommon.ApplicationServices/ValidationBuilderExtensions.cs
@@ -10,26 +10,49 @@
namespace RCommon.ApplicationServices
{
+ ///
+ /// Extension methods for and that provide
+ /// fluent registration of validation infrastructure.
+ ///
public static class ValidationBuilderExtensions
{
+ ///
+ /// Adds validation support using the specified implementation with default configuration.
+ ///
+ /// The implementation type to use.
+ /// The RCommon builder.
+ /// The for further chaining.
public static IRCommonBuilder WithValidation(this IRCommonBuilder builder)
where T : IValidationBuilder
{
return WithValidation(builder, x => { });
}
+ ///
+ /// Adds validation support using the specified implementation and applies additional configuration.
+ ///
+ /// The implementation type to use.
+ /// The RCommon builder.
+ /// A delegate to configure the validation builder (e.g., register validation providers).
+ /// The for further chaining.
public static IRCommonBuilder WithValidation(this IRCommonBuilder builder, Action actions)
where T : IValidationBuilder
{
builder.Services.AddScoped();
- // Event Handling Configurations
- var mediatorConfig = (T)Activator.CreateInstance(typeof(T), new object[] { builder });
+ // Instantiate the validation builder implementation, which may register provider-specific services
+ var mediatorConfig = (T)Activator.CreateInstance(typeof(T), new object[] { builder })!;
actions(mediatorConfig);
return builder;
}
+ ///
+ /// Configures the validation builder to integrate with the CQRS pipeline by setting
+ /// (e.g., enabling command or query validation).
+ ///
+ /// The validation builder.
+ /// A delegate to configure .
public static void UseWithCqrs(this IValidationBuilder builder, Action options)
{
builder.Services.Configure(options);
diff --git a/Src/RCommon.Authorization.Web/Filters/AuthorizationHeaderParameterOperationFilter.cs b/Src/RCommon.Authorization.Web/Filters/AuthorizationHeaderParameterOperationFilter.cs
index 71a95727..1e2c8442 100644
--- a/Src/RCommon.Authorization.Web/Filters/AuthorizationHeaderParameterOperationFilter.cs
+++ b/Src/RCommon.Authorization.Web/Filters/AuthorizationHeaderParameterOperationFilter.cs
@@ -13,11 +13,24 @@
namespace RCommon.Authorization.Web.Filters
{
+ ///
+ /// A Swashbuckle that adds a required "Authorization" header parameter
+ /// to every Swagger/OpenAPI operation whose action pipeline includes an
+ /// and does not allow anonymous access.
+ ///
public class AuthorizationHeaderParameterOperationFilter : IOperationFilter
{
+ ///
+ /// Applies the filter to the given by inspecting the action's
+ /// filter pipeline for authorization requirements.
+ ///
+ /// The OpenAPI operation being processed.
+ /// The filter context containing API description metadata.
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
var filterPipeline = context.ApiDescription.ActionDescriptor.FilterDescriptors;
+
+ // Determine whether the action has authorization enforced and whether anonymous access is permitted.
var isAuthorized = filterPipeline.Select(filterInfo => filterInfo.Filter).Any(filter => filter is AuthorizeFilter);
var allowAnonymous = filterPipeline.Select(filterInfo => filterInfo.Filter).Any(filter => filter is IAllowAnonymousFilter);
@@ -30,6 +43,7 @@ public void Apply(OpenApiOperation operation, OperationFilterContext context)
operation.Parameters = new List();
#endif
+ // Add a required Authorization header so that Swagger UI prompts for a token.
operation.Parameters.Add(new OpenApiParameter
{
Name = "Authorization",
diff --git a/Src/RCommon.Authorization.Web/Filters/AuthorizeCheckOperationFilter.cs b/Src/RCommon.Authorization.Web/Filters/AuthorizeCheckOperationFilter.cs
index a044d8b5..0209ada3 100644
--- a/Src/RCommon.Authorization.Web/Filters/AuthorizeCheckOperationFilter.cs
+++ b/Src/RCommon.Authorization.Web/Filters/AuthorizeCheckOperationFilter.cs
@@ -13,19 +13,32 @@
namespace RCommon.Authorization.Web.Filters
{
+ ///
+ /// A Swashbuckle that adds 401/403 response codes and an OAuth2
+ /// security requirement to every Swagger/OpenAPI operation decorated with .
+ ///
public class AuthorizeCheckOperationFilter : IOperationFilter
{
+ ///
+ /// Applies the filter to the given by checking whether the
+ /// controller or action method is decorated with .
+ /// If so, 401 and 403 responses are added and an OAuth2 security requirement is attached.
+ ///
+ /// The OpenAPI operation being processed.
+ /// The filter context containing method and controller metadata.
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
- // Check for authorize attribute
- var hasAuthorize = context.MethodInfo.DeclaringType.GetCustomAttributes(true).OfType().Any() ||
+ // Check for authorize attribute on the controller type or the action method itself.
+ var hasAuthorize = (context.MethodInfo.DeclaringType?.GetCustomAttributes(true).OfType().Any() ?? false) ||
context.MethodInfo.GetCustomAttributes(true).OfType().Any();
if (!hasAuthorize) return;
- operation.Responses.TryAdd("401", new OpenApiResponse { Description = "Unauthorized" });
- operation.Responses.TryAdd("403", new OpenApiResponse { Description = "Forbidden" });
+ // Add standard authorization failure responses to the operation.
+ operation.Responses?.TryAdd("401", new OpenApiResponse { Description = "Unauthorized" });
+ operation.Responses?.TryAdd("403", new OpenApiResponse { Description = "Forbidden" });
+ // Attach an OAuth2 security requirement referencing the "api" scope.
#if NET10_0_OR_GREATER
var oAuthScheme = new OpenApiSecuritySchemeReference("oauth2");
diff --git a/Src/RCommon.Authorization.Web/RCommon.Authorization.Web.csproj b/Src/RCommon.Authorization.Web/RCommon.Authorization.Web.csproj
index 3922c2eb..c77bc9c7 100644
--- a/Src/RCommon.Authorization.Web/RCommon.Authorization.Web.csproj
+++ b/Src/RCommon.Authorization.Web/RCommon.Authorization.Web.csproj
@@ -2,6 +2,7 @@
net8.0;net9.0;net10.0
+ enableTrueRCommon.Authorization.Webhttps://rcommon.com
diff --git a/Src/RCommon.Authorization.Web/README.md b/Src/RCommon.Authorization.Web/README.md
index 6a5fa4d1..08c9cb37 100644
--- a/Src/RCommon.Authorization.Web/README.md
+++ b/Src/RCommon.Authorization.Web/README.md
@@ -1,3 +1,54 @@
# RCommon.Authorization.Web
-A cohesive set of infrastructure libraries for dotnet that utilizes abstractions for event handling, persistence, unit of work, mediator, distributed messaging, event bus, CQRS, email, and more
\ No newline at end of file
+Swagger/OpenAPI operation filters for ASP.NET Core that automatically surface authorization metadata in your API documentation, including required Authorization headers and OAuth2 security requirements.
+
+## Features
+
+- Automatically adds an `Authorization` header parameter to Swagger operations protected by `AuthorizeFilter`
+- Detects `[Authorize]` attribute on controllers and actions and adds 401/403 response codes to the OpenAPI spec
+- Attaches OAuth2 security requirements to authorized operations
+- Respects `[AllowAnonymous]` to skip authorization header injection
+- Compatible with Swashbuckle.AspNetCore across .NET 8, .NET 9, and .NET 10
+
+## Installation
+
+```shell
+dotnet add package RCommon.Authorization.Web
+```
+
+## Usage
+
+Register the operation filters when configuring Swagger in your ASP.NET Core application:
+
+```csharp
+using RCommon.Authorization.Web.Filters;
+
+builder.Services.AddSwaggerGen(options =>
+{
+ // Adds a required Authorization header to operations with AuthorizeFilter
+ options.OperationFilter();
+
+ // Adds 401/403 responses and OAuth2 security to operations with [Authorize]
+ options.OperationFilter();
+});
+```
+
+## Key Types
+
+| Type | Description |
+|------|-------------|
+| `AuthorizationHeaderParameterOperationFilter` | Adds a required `Authorization` header parameter to operations protected by `AuthorizeFilter` |
+| `AuthorizeCheckOperationFilter` | Adds 401/403 responses and an OAuth2 security requirement to operations decorated with `[Authorize]` |
+
+## Documentation
+
+For full documentation, visit [rcommon.com](https://rcommon.com).
+
+## Related Packages
+
+- [RCommon.Security](https://www.nuget.org/packages/RCommon.Security) - Core security abstractions
+- [RCommon.Core](https://www.nuget.org/packages/RCommon.Core) - Core abstractions and builder infrastructure
+
+## License
+
+Licensed under the [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0).
diff --git a/Src/RCommon.Caching/CacheKey.cs b/Src/RCommon.Caching/CacheKey.cs
index 666a793c..d455d5a4 100644
--- a/Src/RCommon.Caching/CacheKey.cs
+++ b/Src/RCommon.Caching/CacheKey.cs
@@ -25,10 +25,27 @@
namespace RCommon.Caching
{
+ ///
+ /// Represents a strongly-typed cache key with built-in validation and factory methods.
+ ///
+ ///
+ /// Cache keys are validated on construction to ensure they are non-empty and do not exceed
+ /// characters. Use the static factory
+ /// methods to create keys from component parts.
+ ///
public class CacheKey
{
+ ///
+ /// The maximum allowed length for a cache key value.
+ ///
public const int MaxLength = 256;
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The cache key string value.
+ /// Thrown when is null or empty.
+ /// Thrown when exceeds .
public CacheKey(string value)
{
if (string.IsNullOrEmpty(value))
@@ -37,11 +54,23 @@ public CacheKey(string value)
throw new ArgumentOutOfRangeException(nameof(value), value, $"Cache keys can maximum be '{MaxLength}' in length");
}
+ ///
+ /// Creates a by joining the specified key segments with a hyphen delimiter.
+ ///
+ /// The key segments to join.
+ /// A new composed of the joined segments.
public static CacheKey With(params string[] keys)
{
return new CacheKey(string.Join("-", keys));
}
+ ///
+ /// Creates a scoped to the specified owner type, using its cache key
+ /// representation as a prefix followed by the joined key segments.
+ ///
+ /// The type used to scope the cache key.
+ /// The key segments to join after the type prefix.
+ /// A new in the format TypeCacheKey:segment1-segment2.
public static CacheKey With(Type ownerType, params string[] keys)
{
return With($"{ownerType.GetCacheKey()}:{string.Join("-", keys)}");
diff --git a/Src/RCommon.Caching/CachingBuilderExtensions.cs b/Src/RCommon.Caching/CachingBuilderExtensions.cs
index 53788789..ca5a4637 100644
--- a/Src/RCommon.Caching/CachingBuilderExtensions.cs
+++ b/Src/RCommon.Caching/CachingBuilderExtensions.cs
@@ -8,34 +8,75 @@
namespace RCommon.Caching
{
+ ///
+ /// Extension methods on for registering caching infrastructure.
+ ///
public static class CachingBuilderExtensions
{
+ ///
+ /// Registers an implementation with default configuration.
+ ///
+ /// The concrete memory caching builder type to activate.
+ /// The RCommon builder instance.
+ /// The same for chaining.
public static IRCommonBuilder WithMemoryCaching(this IRCommonBuilder builder)
where T : IMemoryCachingBuilder
{
return WithMemoryCaching(builder, x => { });
}
+ ///
+ /// Registers an implementation and applies the specified configuration actions.
+ ///
+ /// The concrete memory caching builder type to activate.
+ /// The RCommon builder instance.
+ /// A delegate to configure the caching builder.
+ /// The same for chaining.
+ ///
+ /// The builder type is created via
+ /// and must have a constructor that accepts an .
+ ///
public static IRCommonBuilder WithMemoryCaching(this IRCommonBuilder builder, Action actions)
where T : IMemoryCachingBuilder
{
Guard.IsNotNull(actions, nameof(actions));
- var cachingConfig = (T)Activator.CreateInstance(typeof(T), new object[] { builder });
+ // Create the builder via reflection, passing the IRCommonBuilder to its constructor
+ var cachingConfig = (T)(Activator.CreateInstance(typeof(T), new object[] { builder })
+ ?? throw new InvalidOperationException($"Failed to create instance of {typeof(T).Name}."));
actions(cachingConfig);
return builder;
}
+ ///
+ /// Registers an implementation with default configuration.
+ ///
+ /// The concrete distributed caching builder type to activate.
+ /// The RCommon builder instance.
+ /// The same for chaining.
public static IRCommonBuilder WithDistributedCaching(this IRCommonBuilder builder)
where T : IDistributedCachingBuilder
{
return WithDistributedCaching(builder, x => { });
}
+ ///
+ /// Registers an implementation and applies the specified configuration actions.
+ ///
+ /// The concrete distributed caching builder type to activate.
+ /// The RCommon builder instance.
+ /// A delegate to configure the caching builder.
+ /// The same for chaining.
+ ///
+ /// The builder type is created via
+ /// and must have a constructor that accepts an .
+ ///
public static IRCommonBuilder WithDistributedCaching(this IRCommonBuilder builder, Action actions)
where T : IDistributedCachingBuilder
{
Guard.IsNotNull(actions, nameof(actions));
- var cachingConfig = (T)Activator.CreateInstance(typeof(T), new object[] { builder });
+ // Create the builder via reflection, passing the IRCommonBuilder to its constructor
+ var cachingConfig = (T)(Activator.CreateInstance(typeof(T), new object[] { builder })
+ ?? throw new InvalidOperationException($"Failed to create instance of {typeof(T).Name}."));
actions(cachingConfig);
return builder;
}
diff --git a/Src/RCommon.Caching/ExpressionCachingStrategy.cs b/Src/RCommon.Caching/ExpressionCachingStrategy.cs
index fb708bfe..5836f6f4 100644
--- a/Src/RCommon.Caching/ExpressionCachingStrategy.cs
+++ b/Src/RCommon.Caching/ExpressionCachingStrategy.cs
@@ -6,8 +6,18 @@
namespace RCommon.Caching
{
+ ///
+ /// Defines the strategy used when caching dynamically compiled expressions and lambdas.
+ ///
+ ///
+ /// Used as the key type for to resolve
+ /// the appropriate implementation at runtime.
+ ///
public enum ExpressionCachingStrategy
{
+ ///
+ /// The default expression caching strategy.
+ ///
Default
}
}
diff --git a/Src/RCommon.Caching/ICacheService.cs b/Src/RCommon.Caching/ICacheService.cs
index c1165c1e..c376abbc 100644
--- a/Src/RCommon.Caching/ICacheService.cs
+++ b/Src/RCommon.Caching/ICacheService.cs
@@ -6,9 +6,34 @@
namespace RCommon.Caching
{
+ ///
+ /// Provides a uniform interface for cache read-through (get-or-create) operations,
+ /// regardless of the underlying caching provider.
+ ///
+ ///
+ /// Implementations include InMemoryCacheService, DistributedMemoryCacheService,
+ /// and RedisCacheService.
+ ///
public interface ICacheService
{
+ ///
+ /// Returns the cached value for the specified key, or creates, caches, and returns a new value
+ /// using the provided factory delegate when no cached entry exists.
+ ///
+ /// The type of the cached data.
+ /// The cache key.
+ /// A factory delegate invoked to produce the value when the key is not found in cache.
+ /// The cached or newly created value of type .
TData GetOrCreate(object key, Func data);
+
+ ///
+ /// Asynchronously returns the cached value for the specified key, or creates, caches, and returns
+ /// a new value using the provided factory delegate when no cached entry exists.
+ ///
+ /// The type of the cached data.
+ /// The cache key.
+ /// A factory delegate invoked to produce the value when the key is not found in cache.
+ /// A task representing the cached or newly created value of type .
Task GetOrCreateAsync(object key, Func data);
}
}
diff --git a/Src/RCommon.Caching/IDistributedCachingBuilder.cs b/Src/RCommon.Caching/IDistributedCachingBuilder.cs
index ee5df102..fd6f64a6 100644
--- a/Src/RCommon.Caching/IDistributedCachingBuilder.cs
+++ b/Src/RCommon.Caching/IDistributedCachingBuilder.cs
@@ -7,8 +7,15 @@
namespace RCommon.Caching
{
+ ///
+ /// Defines the contract for configuring distributed caching services within the RCommon builder pipeline.
+ ///
+ ///
public interface IDistributedCachingBuilder
{
+ ///
+ /// Gets the used to register distributed caching dependencies.
+ ///
IServiceCollection Services { get; }
}
}
diff --git a/Src/RCommon.Caching/IMemoryCachingBuilder.cs b/Src/RCommon.Caching/IMemoryCachingBuilder.cs
index cacbefac..a5c86ca5 100644
--- a/Src/RCommon.Caching/IMemoryCachingBuilder.cs
+++ b/Src/RCommon.Caching/IMemoryCachingBuilder.cs
@@ -7,8 +7,15 @@
namespace RCommon.Caching
{
+ ///
+ /// Defines the contract for configuring in-memory caching services within the RCommon builder pipeline.
+ ///
+ ///
public interface IMemoryCachingBuilder
{
+ ///
+ /// Gets the used to register memory caching dependencies.
+ ///
IServiceCollection Services { get; }
}
}
diff --git a/Src/RCommon.Caching/RCommon.Caching.csproj b/Src/RCommon.Caching/RCommon.Caching.csproj
index 2bbf5c68..84c84e91 100644
--- a/Src/RCommon.Caching/RCommon.Caching.csproj
+++ b/Src/RCommon.Caching/RCommon.Caching.csproj
@@ -2,6 +2,7 @@
net8.0;net9.0;net10.0
+ enableTrueRCommon.Cachinghttps://rcommon.com
diff --git a/Src/RCommon.Caching/README.md b/Src/RCommon.Caching/README.md
index b0465bd7..fa033b34 100644
--- a/Src/RCommon.Caching/README.md
+++ b/Src/RCommon.Caching/README.md
@@ -1,3 +1,66 @@
- # RCommon.Caching
+# RCommon.Caching
-A cohesive set of infrastructure libraries for dotnet that utilizes abstractions for event handling, persistence, unit of work, mediator, distributed messaging, event bus, CQRS, email, and more
+Provides the core caching abstractions for RCommon, including the `ICacheService` interface, strongly-typed cache keys, and builder contracts for plugging in memory or distributed caching providers.
+
+## Features
+
+- `ICacheService` interface with generic `GetOrCreate` and `GetOrCreateAsync` methods (read-through / get-or-create pattern)
+- `CacheKey` value type with validation, max-length enforcement (256 chars), and factory methods for composite and type-scoped keys
+- `IMemoryCachingBuilder` and `IDistributedCachingBuilder` contracts for provider-agnostic DI configuration
+- `WithMemoryCaching` and `WithDistributedCaching` extension methods on `IRCommonBuilder` for fluent setup
+- `ExpressionCachingStrategy` enum for strategy-based resolution of cache services used to cache dynamically compiled expressions
+
+## Installation
+
+```shell
+dotnet add package RCommon.Caching
+```
+
+## Usage
+
+This package is typically consumed indirectly through a concrete provider such as `RCommon.MemoryCache` or `RCommon.RedisCache`. You can also program against the abstraction directly:
+
+```csharp
+// Inject ICacheService and use the get-or-create pattern
+public class ProductService
+{
+ private readonly ICacheService _cache;
+
+ public ProductService(ICacheService cache)
+ {
+ _cache = cache;
+ }
+
+ public async Task GetProductAsync(int id)
+ {
+ return await _cache.GetOrCreateAsync(
+ CacheKey.With("product", id.ToString()),
+ () => _productRepository.FindAsync(id));
+ }
+}
+```
+
+## Key Types
+
+| Type | Description |
+|------|-------------|
+| `ICacheService` | Core abstraction providing `GetOrCreate` and `GetOrCreateAsync` for read-through caching |
+| `CacheKey` | Strongly-typed cache key with validation, max-length enforcement, and static factory methods |
+| `IMemoryCachingBuilder` | Builder contract for configuring in-memory caching providers |
+| `IDistributedCachingBuilder` | Builder contract for configuring distributed caching providers |
+| `ExpressionCachingStrategy` | Strategy enum used to resolve the appropriate `ICacheService` for expression caching |
+| `CachingBuilderExtensions` | `WithMemoryCaching` and `WithDistributedCaching` extensions on `IRCommonBuilder` |
+
+## Documentation
+
+For full documentation, visit [rcommon.com](https://rcommon.com).
+
+## Related Packages
+
+- [RCommon.MemoryCache](https://www.nuget.org/packages/RCommon.MemoryCache) - In-process and distributed memory cache implementations of `ICacheService`
+- [RCommon.RedisCache](https://www.nuget.org/packages/RCommon.RedisCache) - Redis-backed distributed cache implementation of `ICacheService`
+- [RCommon.Persistence.Caching](https://www.nuget.org/packages/RCommon.Persistence.Caching) - Caching decorator repositories that layer caching over any persistence provider
+
+## License
+
+Licensed under the [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0).
diff --git a/Src/RCommon.Core/BaseApplicationException.cs b/Src/RCommon.Core/BaseApplicationException.cs
index e1952246..9706b40c 100644
--- a/Src/RCommon.Core/BaseApplicationException.cs
+++ b/Src/RCommon.Core/BaseApplicationException.cs
@@ -13,15 +13,24 @@
namespace RCommon
{
+ ///
+ /// Serves as the base class for application-specific exceptions, automatically capturing
+ /// environment information such as machine name, thread identity, and AppDomain name.
+ ///
+ ///
+ /// All constructors invoke to populate
+ /// diagnostic properties. Security exceptions during environment probing are silently
+ /// caught and the corresponding property is set to "Permission Denied".
+ ///
[Serializable]
public class BaseApplicationException : ApplicationException
{
#region Fields
- private string machineName;
- private string appDomainName;
- private string threadIdentity;
- private string windowsIdentity;
+ private string machineName = string.Empty;
+ private string appDomainName = string.Empty;
+ private string threadIdentity = string.Empty;
+ private string windowsIdentity = string.Empty;
private DateTime createdDateTime = DateTime.Now;
// Collection provided to store any extra information associated with the exception.
@@ -140,7 +149,7 @@ private void InitializeEnvironmentInformation()
{
if (Thread.CurrentPrincipal != null)
{
- threadIdentity = Thread.CurrentPrincipal.Identity.Name;
+ threadIdentity = Thread.CurrentPrincipal.Identity?.Name ?? string.Empty;
}
}
catch (SecurityException)
@@ -156,7 +165,7 @@ private void InitializeEnvironmentInformation()
{
if (Thread.CurrentPrincipal != null)
{
- windowsIdentity = Thread.CurrentPrincipal.Identity.Name;
+ windowsIdentity = Thread.CurrentPrincipal.Identity?.Name ?? string.Empty;
}
}
diff --git a/Src/RCommon.Core/CachingOptions.cs b/Src/RCommon.Core/CachingOptions.cs
index 9c20829e..4680be9c 100644
--- a/Src/RCommon.Core/CachingOptions.cs
+++ b/Src/RCommon.Core/CachingOptions.cs
@@ -6,15 +6,30 @@
namespace RCommon
{
+ ///
+ /// Configuration options for controlling caching behavior within RCommon.
+ /// Both options default to false (caching disabled).
+ ///
public class CachingOptions
{
+ ///
+ /// Initializes a new instance of with caching disabled by default.
+ ///
public CachingOptions()
{
this.CachingEnabled = false;
this.CacheDynamicallyCompiledExpressions = false;
}
+ ///
+ /// Gets or sets a value indicating whether caching is globally enabled.
+ ///
public bool CachingEnabled { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether dynamically compiled expressions (e.g., specification predicates)
+ /// should be cached to improve performance.
+ ///
public bool CacheDynamicallyCompiledExpressions { get; set; }
}
}
diff --git a/Src/RCommon.Core/Collections/IPaginatedList.cs b/Src/RCommon.Core/Collections/IPaginatedList.cs
index fc8f0b58..e9aa6818 100644
--- a/Src/RCommon.Core/Collections/IPaginatedList.cs
+++ b/Src/RCommon.Core/Collections/IPaginatedList.cs
@@ -5,13 +5,29 @@
namespace RCommon.Collections
{
+ ///
+ /// Represents a paginated list that extends with pagination metadata
+ /// such as page index, page size, total count, and navigation flags.
+ ///
+ /// The type of elements in the list.
public interface IPaginatedList : IList
{
+ /// Gets a value indicating whether there is a next page available.
bool HasNextPage { get; }
+
+ /// Gets a value indicating whether there is a previous page available.
bool HasPreviousPage { get; }
+
+ /// Gets the 1-based index of the current page.
int PageIndex { get; }
+
+ /// Gets the maximum number of items per page.
int PageSize { get; }
+
+ /// Gets the total number of items across all pages.
int TotalCount { get; }
+
+ /// Gets the total number of pages.
int TotalPages { get; }
}
}
diff --git a/Src/RCommon.Core/Collections/ListDictionary.cs b/Src/RCommon.Core/Collections/ListDictionary.cs
index 37e79c88..ffd92cf9 100644
--- a/Src/RCommon.Core/Collections/ListDictionary.cs
+++ b/Src/RCommon.Core/Collections/ListDictionary.cs
@@ -4,15 +4,30 @@
namespace RCommon.Collections
{
- public class ListDictionary : IEnumerable>>
+ ///
+ /// A dictionary that maps each key to a list of values, allowing multiple values per key.
+ /// Automatically creates an empty list when accessing a key that does not yet exist.
+ ///
+ /// The type of the dictionary keys.
+ /// The type of the values stored in the lists.
+ public class ListDictionary : IEnumerable>> where TKey : notnull
{
readonly Dictionary> innerValues = new Dictionary>();
+ ///
+ /// Gets the number of keys in the dictionary.
+ ///
public int Count
{
get { return innerValues.Count; }
}
+ ///
+ /// Gets or sets the list of values associated with the specified key.
+ /// If the key does not exist on get, an empty list is created and associated with the key.
+ ///
+ /// The key to look up.
+ /// The list of values for the specified key.
public List this[TKey key]
{
get
@@ -25,11 +40,17 @@ public List this[TKey key]
set { innerValues[key] = value; }
}
+ ///
+ /// Gets a collection of all keys in the dictionary.
+ ///
public ICollection Keys
{
get { return innerValues.Keys; }
}
+ ///
+ /// Gets a flattened list of all values across all keys.
+ ///
public List Values
{
get
@@ -43,18 +64,28 @@ public List Values
}
}
+ ///
+ /// Adds a key to the dictionary with an empty value list.
+ ///
+ /// The key to add. Must not be null.
public void Add(TKey key)
{
- Guard.IsNotNull(key, "key");
+ Guard.IsNotNull(key!, "key");
CreateNewList(key);
}
+ ///
+ /// Adds a value to the list associated with the specified key.
+ /// If the key does not exist, a new list is created first.
+ ///
+ /// The key to add the value under. Must not be null.
+ /// The value to add. Must not be null.
public void Add(TKey key,
TValue value)
{
- Guard.IsNotNull(key, "key");
- Guard.IsNotNull(value, "value");
+ Guard.IsNotNull(key!, "key");
+ Guard.IsNotNull(value!, "value");
if (innerValues.ContainsKey(key))
innerValues[key].Add(value);
@@ -65,18 +96,31 @@ public void Add(TKey key,
}
}
+ ///
+ /// Removes all keys and their associated value lists from the dictionary.
+ ///
public void Clear()
{
innerValues.Clear();
}
+ ///
+ /// Determines whether the dictionary contains the specified key.
+ ///
+ /// The key to locate. Must not be null.
+ /// true if the dictionary contains the key; otherwise, false.
public bool ContainsKey(TKey key)
{
- Guard.IsNotNull(key, "key");
+ Guard.IsNotNull(key!, "key");
return innerValues.ContainsKey(key);
}
+ ///
+ /// Determines whether any value list in the dictionary contains the specified value.
+ ///
+ /// The value to search for.
+ /// true if the value is found in any list; otherwise, false.
public bool ContainsValue(TValue value)
{
foreach (KeyValuePair> pair in innerValues)
@@ -86,6 +130,11 @@ public bool ContainsValue(TValue value)
return false;
}
+ ///
+ /// Creates a new empty value list and associates it with the specified key.
+ ///
+ /// The key to associate the new list with.
+ /// The newly created empty list.
List CreateNewList(TKey key)
{
List values = new List();
@@ -93,6 +142,11 @@ List CreateNewList(TKey key)
return values;
}
+ ///
+ /// Finds all values whose keys match the specified filter predicate.
+ ///
+ /// A predicate to filter keys.
+ /// An enumerable of values from all matching keys.
public IEnumerable FindByKey(Predicate keyFilter)
{
foreach (KeyValuePair> pair in this)
@@ -101,6 +155,12 @@ public IEnumerable FindByKey(Predicate keyFilter)
yield return value;
}
+ ///
+ /// Finds all values where both the key and value match the specified filter predicates.
+ ///
+ /// A predicate to filter keys.
+ /// A predicate to filter values within matching keys.
+ /// An enumerable of values matching both filters.
public IEnumerable FindByKeyAndValue(Predicate keyFilter,
Predicate valueFilter)
{
@@ -111,6 +171,11 @@ public IEnumerable FindByKeyAndValue(Predicate keyFilter,
yield return value;
}
+ ///
+ /// Finds all values across all keys that match the specified value filter predicate.
+ ///
+ /// A predicate to filter values.
+ /// An enumerable of matching values.
public IEnumerable FindByValue(Predicate valueFilter)
{
foreach (KeyValuePair> pair in this)
@@ -119,36 +184,52 @@ public IEnumerable FindByValue(Predicate valueFilter)
yield return value;
}
+ ///
public IEnumerator>> GetEnumerator()
{
return innerValues.GetEnumerator();
}
+ ///
IEnumerator IEnumerable.GetEnumerator()
{
return innerValues.GetEnumerator();
}
+ ///
+ /// Removes the key and its entire associated value list from the dictionary.
+ ///
+ /// The key to remove. Must not be null.
+ /// true if the key was found and removed; otherwise, false.
public bool Remove(TKey key)
{
- Guard.IsNotNull(key, "key");
+ Guard.IsNotNull(key!, "key");
return innerValues.Remove(key);
}
+ ///
+ /// Removes all occurrences of a specific value from the list associated with the specified key.
+ ///
+ /// The key whose value list to modify. Must not be null.
+ /// The value to remove. Must not be null.
public void Remove(TKey key,
TValue value)
{
- Guard.IsNotNull(key, "key");
- Guard.IsNotNull(value, "value");
+ Guard.IsNotNull(key!, "key");
+ Guard.IsNotNull(value!, "value");
if (innerValues.ContainsKey(key))
innerValues[key].RemoveAll(delegate (TValue item)
{
- return value.Equals(item);
+ return value!.Equals(item);
});
}
+ ///
+ /// Removes a specific value from all keys' value lists in the dictionary.
+ ///
+ /// The value to remove from all lists.
public void Remove(TValue value)
{
foreach (KeyValuePair> pair in innerValues)
diff --git a/Src/RCommon.Core/Collections/PaginatedList.cs b/Src/RCommon.Core/Collections/PaginatedList.cs
index 730f7939..c87a738b 100644
--- a/Src/RCommon.Core/Collections/PaginatedList.cs
+++ b/Src/RCommon.Core/Collections/PaginatedList.cs
@@ -5,28 +5,57 @@
namespace RCommon.Collections
{
+ ///
+ /// A concrete implementation of that extends
+ /// to provide pagination over a data source. Supports construction from ,
+ /// , and sources.
+ ///
+ /// The type of elements in the paginated list.
public class PaginatedList : List, IPaginatedList
{
+ ///
+ /// Initializes a new empty instance of .
+ ///
public PaginatedList()
{
}
+ /// Gets the 1-based index of the current page.
public int PageIndex { get; private set; }
+
+ /// Gets the maximum number of items per page.
public int PageSize { get; private set; }
+
+ /// Gets the total number of items across all pages.
public int TotalCount { get; private set; }
+
+ /// Gets the total number of pages.
public int TotalPages { get; private set; }
+ ///
+ /// Initializes a new instance of from an source.
+ ///
+ /// The queryable data source to paginate.
+ /// The 1-based page index, or null to default to the first page.
+ /// The number of items per page.
public PaginatedList(IQueryable source, int? pageIndex, int pageSize)
{
PageIndex = pageIndex ?? 1;
PageSize = pageSize;
TotalCount = source.Count();
+ // Calculate total pages using integer division, ensuring at least 1 page
TotalPages = ((TotalCount - 1) / PageSize) + 1;
this.AddRange(source.Skip((PageIndex - 1) * PageSize).Take(PageSize));
}
+ ///
+ /// Initializes a new instance of from an source.
+ ///
+ /// The list data source to paginate.
+ /// The 1-based page index, or null to default to the first page.
+ /// The number of items per page.
public PaginatedList(IList source, int? pageIndex, int pageSize)
{
PageIndex = pageIndex ?? 1;
@@ -37,6 +66,12 @@ public PaginatedList(IList source, int? pageIndex, int pageSize)
this.AddRange(source.Skip((PageIndex - 1) * PageSize).Take(PageSize));
}
+ ///
+ /// Initializes a new instance of from an source.
+ ///
+ /// The collection data source to paginate.
+ /// The 1-based page index, or null to default to the first page.
+ /// The number of items per page.
public PaginatedList(ICollection source, int? pageIndex, int pageSize)
{
PageIndex = pageIndex ?? 1;
@@ -48,6 +83,9 @@ public PaginatedList(ICollection source, int? pageIndex, int pageSize)
this.AddRange(source.Skip((PageIndex - 1) * PageSize).Take(PageSize));
}
+ ///
+ /// Gets a value indicating whether there is a previous page (i.e., is greater than 1).
+ ///
public bool HasPreviousPage
{
get
@@ -56,6 +94,9 @@ public bool HasPreviousPage
}
}
+ ///
+ /// Gets a value indicating whether there is a next page (i.e., is less than ).
+ ///
public bool HasNextPage
{
get
diff --git a/Src/RCommon.Core/CommonFactory.cs b/Src/RCommon.Core/CommonFactory.cs
index a568b07c..173cf3c5 100644
--- a/Src/RCommon.Core/CommonFactory.cs
+++ b/Src/RCommon.Core/CommonFactory.cs
@@ -6,20 +6,31 @@
namespace RCommon
{
+ ///
+ /// Default implementation of that uses a
+ /// delegate to create instances of .
+ ///
+ /// The type of object this factory creates.
public class CommonFactory : ICommonFactory
{
private readonly Func _initFunc;
+ ///
+ /// Initializes a new instance of with the specified initialization function.
+ ///
+ /// A delegate that creates new instances of .
public CommonFactory(Func initFunc)
{
_initFunc = initFunc;
}
+ ///
public TResult Create()
{
return _initFunc();
}
+ ///
public TResult Create(Action customize)
{
var concreteObject = _initFunc();
@@ -28,20 +39,32 @@ public TResult Create(Action customize)
}
}
+ ///
+ /// Default implementation of that uses a
+ /// delegate to create instances of from a single argument.
+ ///
+ /// The type of the input argument.
+ /// The type of object this factory creates.
public class CommonFactory : ICommonFactory
{
private readonly Func _initFunc;
+ ///
+ /// Initializes a new instance of with the specified initialization function.
+ ///
+ /// A delegate that creates new instances of from an argument of type .
public CommonFactory(Func initFunc)
{
_initFunc = initFunc;
}
+ ///
public TResult Create(T arg)
{
return _initFunc(arg);
}
+ ///
public TResult Create(T arg, Action customize)
{
var concreteObject = _initFunc(arg);
@@ -52,20 +75,33 @@ public TResult Create(T arg, Action customize)
}
+ ///
+ /// Default implementation of that uses a
+ /// delegate to create instances of from two arguments.
+ ///
+ /// The type of the first input argument.
+ /// The type of the second input argument.
+ /// The type of object this factory creates.
public class CommonFactory : ICommonFactory
{
private readonly Func _initFunc;
+ ///
+ /// Initializes a new instance of with the specified initialization function.
+ ///
+ /// A delegate that creates new instances of from arguments of type and .
public CommonFactory(Func initFunc)
{
_initFunc = initFunc;
}
+ ///
public TResult Create(T arg, T2 arg2)
{
return _initFunc(arg, arg2);
}
+ ///
public TResult Create(T arg, T2 arg2, Action customize)
{
var concreteObject = _initFunc(arg, arg2);
diff --git a/Src/RCommon.Core/Constants.cs b/Src/RCommon.Core/Constants.cs
index 1ef3e74d..63e99601 100644
--- a/Src/RCommon.Core/Constants.cs
+++ b/Src/RCommon.Core/Constants.cs
@@ -6,8 +6,15 @@
namespace RCommon
{
+ ///
+ /// Provides commonly used application-wide constant values for domain object identifiers,
+ /// versioning, delimiters, and paging defaults.
+ ///
public static class Constants
{
+ ///
+ /// Gets the current thread's culture information.
+ ///
public static CultureInfo CurrentCulture
{
get
@@ -16,18 +23,52 @@ public static CultureInfo CurrentCulture
}
}
+ ///
+ /// Default identifier value for a domain object that has not yet been persisted.
+ ///
public const int IdOfUnsavedDomainObject = 0;
+
+ ///
+ /// Sentinel identifier used for stub/mock domain objects in testing scenarios.
+ ///
public const int IdOfStubDomainObject = -999999999; // used as the Id of Stub objects, should be renamed to IdOfStubObject
- public const int IndexOfStubDomainObject = -1000; // a special Id to indicate a "Not-A-Domain-Object" (e.g.,
+
+ ///
+ /// Special index value indicating a non-domain object (e.g., artificial object types
+ /// such as ObjectDescriptor, ObjectAndTypeElement, or None-Entry items for combo boxes).
+ ///
+ public const int IndexOfStubDomainObject = -1000; // a special Id to indicate a "Not-A-Domain-Object" (e.g.,
// artificial object types such as: ObjectDescriptor, ObjectAndTypeElement, None-Entry Item (for combobox)
+
+ ///
+ /// Represents an invalid or uninitialized version number for optimistic concurrency control.
+ ///
public const int InvalidVersion = -1;
+ ///
+ /// The index of the first real (non-stub) object in a data list, typically following a placeholder entry.
+ ///
public static readonly int IndexOfFirstNonStubObjectInDataList = 1;
+ ///
+ /// The default delimiter character used for string splitting and joining operations.
+ ///
public static readonly char DefaultDelimiter = ',';
+
+ ///
+ /// The default number of items per page for paged queries.
+ ///
public static readonly int DefaultPageSize = 10;
+
+ ///
+ /// The default property name used to resolve singleton instances.
+ ///
public static readonly string DefaultSingletonPropertyName = "Singleton";
+
+ ///
+ /// The default property name used for optimistic concurrency version tracking.
+ ///
public static readonly string DefaultVersionPropertyName = "ObjectVersion";
}
}
diff --git a/Src/RCommon.Core/DisposableAction.cs b/Src/RCommon.Core/DisposableAction.cs
index 438a1ce6..449b0a50 100644
--- a/Src/RCommon.Core/DisposableAction.cs
+++ b/Src/RCommon.Core/DisposableAction.cs
@@ -25,6 +25,9 @@ public DisposeAction(Action action)
_action = action;
}
+ ///
+ /// Executes the action that was provided at construction time.
+ ///
public void Dispose()
{
_action();
diff --git a/Src/RCommon.Core/DisposableResource.cs b/Src/RCommon.Core/DisposableResource.cs
index 6d09ba1d..c67bd65b 100644
--- a/Src/RCommon.Core/DisposableResource.cs
+++ b/Src/RCommon.Core/DisposableResource.cs
@@ -7,13 +7,29 @@
namespace RCommon
{
+ ///
+ /// Abstract base class that implements the standard Dispose pattern for both synchronous
+ /// () and asynchronous () disposal.
+ ///
+ ///
+ /// Derived classes should override and/or
+ /// to release managed and unmanaged resources. The finalizer calls
+ /// with false to release unmanaged resources only.
+ ///
public abstract class DisposableResource : IDisposable, IAsyncDisposable
{
+ ///
+ /// Finalizer that invokes with false to release unmanaged resources.
+ ///
~DisposableResource()
{
Dispose(false);
}
+ ///
+ /// Performs application-defined tasks associated with freeing, releasing, or resetting resources.
+ /// Suppresses finalization after disposal.
+ ///
[DebuggerStepThrough]
public void Dispose()
{
@@ -21,16 +37,30 @@ public void Dispose()
GC.SuppressFinalize(this);
}
+ ///
+ /// Asynchronously performs application-defined tasks associated with freeing, releasing, or resetting resources.
+ /// Suppresses finalization after disposal.
+ ///
+ /// A representing the asynchronous dispose operation.
public async ValueTask DisposeAsync()
{
await this.DisposeAsync(true);
GC.SuppressFinalize(this);
}
+ ///
+ /// Releases resources. Override in derived classes to release managed resources when is true.
+ ///
+ /// true to release both managed and unmanaged resources; false to release only unmanaged resources.
protected virtual void Dispose(bool disposing)
{
}
+ ///
+ /// Asynchronously releases resources. Override in derived classes to release managed resources when is true.
+ ///
+ /// true to release both managed and unmanaged resources; false to release only unmanaged resources.
+ /// A representing the asynchronous dispose operation.
protected async virtual Task DisposeAsync(bool disposing)
{
diff --git a/Src/RCommon.Core/EventHandling/EventHandlingBuilderExtensions.cs b/Src/RCommon.Core/EventHandling/EventHandlingBuilderExtensions.cs
index add6f1e7..382899de 100644
--- a/Src/RCommon.Core/EventHandling/EventHandlingBuilderExtensions.cs
+++ b/Src/RCommon.Core/EventHandling/EventHandlingBuilderExtensions.cs
@@ -13,23 +13,46 @@
namespace RCommon
{
+ ///
+ /// Extension methods for configuring event handling on
+ /// and registering instances on .
+ ///
public static class EventHandlingBuilderExtensions
{
+ ///
+ /// Configures event handling using the specified builder with default settings.
+ ///
+ /// The implementation type to use.
+ /// The RCommon builder instance.
+ /// The for method chaining.
public static IRCommonBuilder WithEventHandling(this IRCommonBuilder builder)
where T : IEventHandlingBuilder
{
return WithEventHandling(builder, x => { });
}
+ ///
+ /// Configures event handling using the specified builder and applies custom configuration.
+ ///
+ /// The implementation type to use.
+ /// The RCommon builder instance.
+ /// An action to configure the event handling builder.
+ /// The for method chaining.
public static IRCommonBuilder WithEventHandling(this IRCommonBuilder builder, Action actions)
where T : IEventHandlingBuilder
{
// Event Handling Configurations
- var eventHandlingConfig = (T)Activator.CreateInstance(typeof(T), new object[] { builder });
+ var eventHandlingConfig = (T)Activator.CreateInstance(typeof(T), new object[] { builder })!;
actions(eventHandlingConfig);
return builder;
}
+ ///
+ /// Registers an of type as a singleton service
+ /// and associates it with the current builder type in the .
+ ///
+ /// The implementation type.
+ /// The event handling builder.
public static void AddProducer(this IEventHandlingBuilder builder)
where T : class, IEventProducer
{
@@ -40,6 +63,13 @@ public static void AddProducer(this IEventHandlingBuilder builder)
subscriptionManager?.AddProducerForBuilder(builder.GetType(), typeof(T));
}
+ ///
+ /// Registers an of type as a singleton service
+ /// using a factory delegate, and associates it with the current builder type.
+ ///
+ /// The implementation type.
+ /// The event handling builder.
+ /// A factory function to create the producer instance.
public static void AddProducer(this IEventHandlingBuilder builder, Func getProducer)
where T : class, IEventProducer
{
@@ -50,12 +80,21 @@ public static void AddProducer(this IEventHandlingBuilder builder, Func
+ /// Registers an existing instance as a singleton and associates it
+ /// with the current builder type. Also registers the producer as an
+ /// if it implements that interface.
+ ///
+ /// The implementation type.
+ /// The event handling builder.
+ /// The producer instance to register.
public static void AddProducer(this IEventHandlingBuilder builder, T producer)
where T : class, IEventProducer
{
builder.Services.TryAddSingleton(producer);
builder.Services.TryAddSingleton(sp => sp.GetRequiredService());
+ // Also register as IHostedService if the producer implements it
if (producer is IHostedService service)
{
builder.Services.TryAddSingleton(service);
@@ -70,6 +109,8 @@ public static void AddProducer(this IEventHandlingBuilder builder, T producer
/// Retrieves the singleton instance from the service collection
/// during configuration time (before the service provider is built).
///
+ /// The service collection to search.
+ /// The instance, or null if not registered.
public static EventSubscriptionManager? GetSubscriptionManager(this IServiceCollection services)
{
var descriptor = services.FirstOrDefault(d => d.ServiceType == typeof(EventSubscriptionManager));
diff --git a/Src/RCommon.Core/EventHandling/IEventBus.cs b/Src/RCommon.Core/EventHandling/IEventBus.cs
index c96eb56b..17034927 100644
--- a/Src/RCommon.Core/EventHandling/IEventBus.cs
+++ b/Src/RCommon.Core/EventHandling/IEventBus.cs
@@ -2,12 +2,36 @@
namespace RCommon.EventHandling
{
+ ///
+ /// Defines an in-process event bus for publishing events and subscribing event handlers.
+ ///
+ ///
public interface IEventBus
{
+ ///
+ /// Publishes an event to all registered subscribers of .
+ ///
+ /// The type of event to publish.
+ /// The event instance to publish.
+ /// A representing the asynchronous operation.
Task PublishAsync(TEvent @event);
+
+ ///
+ /// Subscribes a specific event handler to a specific event type.
+ ///
+ /// The type of event to subscribe to.
+ /// The handler type that implements .
+ /// The instance for method chaining.
IEventBus Subscribe()
where TEvent : class
where TEventHandler : class, Subscribers.ISubscriber;
+
+ ///
+ /// Automatically subscribes to all event types it handles
+ /// by discovering all interface implementations.
+ ///
+ /// The handler type whose implementations are auto-discovered.
+ /// The instance for method chaining.
IEventBus SubscribeAllHandledEvents() where TEventHandler : class;
}
}
\ No newline at end of file
diff --git a/Src/RCommon.Core/EventHandling/IEventHandlingBuilder.cs b/Src/RCommon.Core/EventHandling/IEventHandlingBuilder.cs
index d74bd689..3fa8a4e8 100644
--- a/Src/RCommon.Core/EventHandling/IEventHandlingBuilder.cs
+++ b/Src/RCommon.Core/EventHandling/IEventHandlingBuilder.cs
@@ -7,8 +7,15 @@
namespace RCommon.EventHandling
{
+ ///
+ /// Defines the base builder interface for configuring event handling infrastructure.
+ /// Provides access to the for service registration.
+ ///
public interface IEventHandlingBuilder
{
+ ///
+ /// Gets the used to register event handling services.
+ ///
IServiceCollection Services { get; }
}
}
diff --git a/Src/RCommon.Core/EventHandling/IInMemoryEventBusBuilder.cs b/Src/RCommon.Core/EventHandling/IInMemoryEventBusBuilder.cs
index f82742ac..97c78bf6 100644
--- a/Src/RCommon.Core/EventHandling/IInMemoryEventBusBuilder.cs
+++ b/Src/RCommon.Core/EventHandling/IInMemoryEventBusBuilder.cs
@@ -2,8 +2,14 @@
namespace RCommon.EventHandling
{
+ ///
+ /// Marker interface for the in-memory event bus builder. Extends
+ /// to provide a distinct builder type for configuring in-memory event handling with
+ /// .
+ ///
+ ///
public interface IInMemoryEventBusBuilder : IEventHandlingBuilder
{
-
+
}
}
diff --git a/Src/RCommon.Core/EventHandling/InMemoryEventBus.cs b/Src/RCommon.Core/EventHandling/InMemoryEventBus.cs
index 443870ea..aa224abe 100644
--- a/Src/RCommon.Core/EventHandling/InMemoryEventBus.cs
+++ b/Src/RCommon.Core/EventHandling/InMemoryEventBus.cs
@@ -34,17 +34,31 @@
namespace RCommon.EventHandling
{
+ ///
+ /// In-memory implementation of that resolves and invokes
+ /// handlers from the dependency injection container.
+ ///
+ ///
+ /// Subscriptions are registered directly into the at configuration time.
+ /// Publishing creates a new scope and resolves all handlers via reflection to support polymorphic event dispatch.
+ ///
public class InMemoryEventBus : IEventBus
{
private readonly IServiceCollection _services;
private readonly IServiceProvider _serviceProvider;
+ ///
+ /// Initializes a new instance of .
+ ///
+ /// The root service provider used to create scopes for event publishing.
+ /// The service collection for registering subscriber services at configuration time.
public InMemoryEventBus(IServiceProvider serviceProvider, IServiceCollection services)
{
_serviceProvider = serviceProvider;
_services = services;
}
+ ///
public IEventBus Subscribe()
where TEvent : class
where TEventHandler : class, ISubscriber
@@ -53,10 +67,16 @@ public IEventBus Subscribe()
return this;
}
+ ///
+ ///
+ /// Uses reflection to discover all interfaces on
+ /// and registers each as a scoped service.
+ ///
public IEventBus SubscribeAllHandledEvents()
where TEventHandler : class
{
Type implementationType = typeof(TEventHandler);
+ // Discover all ISubscriber<> interfaces implemented by the handler type
IEnumerable serviceTypes = implementationType
.GetInterfaces()
.Where(i => i.IsGenericType)
@@ -70,21 +90,33 @@ public IEventBus SubscribeAllHandledEvents()
return this;
}
+ ///
+ ///
+ /// Creates a new DI scope and uses reflection to resolve handlers for the runtime event type,
+ /// invoking on each handler sequentially.
+ ///
public async Task PublishAsync(TEvent @event)
{
using (IServiceScope scope = _serviceProvider.CreateScope())
{
- Type eventType = @event.GetType();
+ // Resolve handlers based on the runtime event type (not the compile-time generic parameter)
+ Type eventType = @event!.GetType();
Type openHandlerType = typeof(ISubscriber<>);
Type handlerType = openHandlerType.MakeGenericType(eventType);
- IEnumerable