Skip to content

Commit 843ff0d

Browse files
Added .editorconfig to help keep code clean based on rules. Added comments to all methods and complex code. Updated READMEs. Added stats to main README. (#156)
Added .editorconfig to help keep code clean based on rules. Added comments to all methods and complex code. Updated READMEs. Added stats to main README. Co-authored-by: jasonmwebb-lv <jason.webb@leadventure.com>
1 parent ca8b774 commit 843ff0d

335 files changed

Lines changed: 8791 additions & 596 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.claude/settings.local.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,10 @@
2828
"Bash(for dir in RCommon.*.Tests)",
2929
"Bash(do echo \"=== $dir ===\")",
3030
"Bash(done)",
31-
"Bash(git add:*)"
31+
"Bash(git add:*)",
32+
"Bash(findstr:*)",
33+
"Bash(Select-String -Pattern \"warning CS8\")",
34+
"Bash(Select-String -Pattern \"Build succeeded|Error\\\\\\(s\\\\\\)|Warning\\\\\\(s\\\\\\)|error CS\")"
3235
]
3336
}
3437
}

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,3 +342,6 @@ ASALocalRun/
342342
# BeatPulse healthcheck temp database
343343
healthchecksdb
344344
/Src/RCommon.Persistence.EfCore/IEFCoreConfiguration.cs
345+
/.claude
346+
.claude/settings.local.json
347+
.claude/settings.local.json

Src/.editorconfig

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
###############################
2+
# Core EditorConfig Options #
3+
###############################
4+
5+
root = true
6+
7+
# All files
8+
[*]
9+
indent_style = space
10+
11+
# Code files
12+
[*.{cs,csx,vb,vbx}]
13+
indent_size = 4
14+
insert_final_newline = true
15+
charset = utf-8-bom
16+
17+
###############################
18+
# .NET Coding Conventions #
19+
###############################
20+
21+
###############################
22+
# .NET Coding Conventions #
23+
###############################
24+
25+
[*.{cs,vb}]
26+
# Organize usings
27+
dotnet_sort_system_directives_first = true
28+
dotnet_separate_import_directive_groups = false
29+
30+
# this. preferences
31+
dotnet_style_qualification_for_field = false:silent
32+
dotnet_style_qualification_for_property = false:silent
33+
dotnet_style_qualification_for_method = false:silent
34+
dotnet_style_qualification_for_event = false:silent
35+
36+
# Language keywords vs BCL types preferences
37+
dotnet_style_predefined_type_for_locals_parameters_members = true:silent
38+
dotnet_style_predefined_type_for_member_access = true:silent
39+
40+
# Parentheses preferences
41+
dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent
42+
dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent
43+
dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent
44+
dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent
45+
46+
# Modifier preferences
47+
dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent
48+
dotnet_style_readonly_field = true:suggestion
49+
50+
# Expression-level preferences
51+
dotnet_style_object_initializer = true:suggestion
52+
dotnet_style_collection_initializer = true:suggestion
53+
dotnet_style_explicit_tuple_names = true:suggestion
54+
dotnet_style_null_propagation = true:suggestion
55+
dotnet_style_coalesce_expression = true:suggestion
56+
dotnet_style_prefer_is_null_check_over_reference_equality_method = true:silent
57+
dotnet_style_prefer_inferred_tuple_names = true:suggestion
58+
dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
59+
dotnet_style_prefer_auto_properties = true:silent
60+
dotnet_style_prefer_conditional_expression_over_assignment = true:silent
61+
dotnet_style_prefer_conditional_expression_over_return = true:silent
62+
63+
###############################
64+
# Naming Conventions #
65+
###############################
66+
67+
# Style Definitions
68+
dotnet_naming_style.pascal_case_style.capitalization = pascal_case
69+
70+
# Use PascalCase for constant fields
71+
dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion
72+
dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields
73+
dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style
74+
dotnet_naming_symbols.constant_fields.applicable_kinds = field
75+
dotnet_naming_symbols.constant_fields.applicable_accessibilities = *
76+
dotnet_naming_symbols.constant_fields.required_modifiers = const
77+
78+
###############################
79+
# C# Code Style Rules #
80+
###############################
81+
82+
[*.cs]
83+
# var preferences
84+
csharp_style_var_for_built_in_types = true:silent
85+
csharp_style_var_when_type_is_apparent = true:silent
86+
csharp_style_var_elsewhere = true:silent
87+
88+
# Expression-bodied members
89+
csharp_style_expression_bodied_methods = false:silent
90+
csharp_style_expression_bodied_constructors = false:silent
91+
csharp_style_expression_bodied_operators = false:silent
92+
csharp_style_expression_bodied_properties = true:silent
93+
csharp_style_expression_bodied_indexers = true:silent
94+
csharp_style_expression_bodied_accessors = true:silent
95+
96+
# Pattern-matching preferences
97+
csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
98+
csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
99+
100+
# Null-checking preferences
101+
csharp_style_throw_expression = true:suggestion
102+
csharp_style_conditional_delegate_call = true:suggestion
103+
104+
# Modifier preferences
105+
csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:suggestion
106+
107+
# Expression-level preferences
108+
csharp_prefer_braces = true:silent
109+
csharp_style_deconstructed_variable_declaration = true:suggestion
110+
csharp_prefer_simple_default_expression = true:suggestion
111+
csharp_style_pattern_local_over_anonymous_function = true:suggestion
112+
csharp_style_inlined_variable_declaration = true:suggestion
113+
114+
###############################
115+
# C# Formatting Rules #
116+
###############################
117+
118+
# New line preferences
119+
csharp_new_line_before_open_brace = all
120+
csharp_new_line_before_else = true
121+
csharp_new_line_before_catch = true
122+
csharp_new_line_before_finally = true
123+
csharp_new_line_before_members_in_object_initializers = true
124+
csharp_new_line_before_members_in_anonymous_types = true
125+
csharp_new_line_between_query_expression_clauses = true
126+
127+
# Indentation preferences
128+
csharp_indent_case_contents = true
129+
csharp_indent_switch_labels = true
130+
csharp_indent_labels = flush_left
131+
132+
# Space preferences
133+
csharp_space_after_cast = false
134+
csharp_space_after_keywords_in_control_flow_statements = true
135+
csharp_space_between_method_call_parameter_list_parentheses = false
136+
csharp_space_between_method_declaration_parameter_list_parentheses = false
137+
csharp_space_between_parentheses = false
138+
csharp_space_before_colon_in_inheritance_clause = true
139+
csharp_space_after_colon_in_inheritance_clause = true
140+
csharp_space_around_binary_operators = before_and_after
141+
csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
142+
csharp_space_between_method_call_name_and_opening_parenthesis = false
143+
csharp_space_between_method_call_empty_parameter_list_parentheses = false
144+
csharp_space_after_comma = true
145+
csharp_space_after_dot = false
146+
147+
# Wrapping preferences
148+
csharp_preserve_single_line_statements = true
149+
csharp_preserve_single_line_blocks = true
150+
151+
##################################
152+
# Visual Basic Code Style Rules #
153+
##################################
154+
155+
[*.vb]
156+
# Modifier preferences
157+
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

Src/RCommon.ApplicationServices/Commands/CommandBus.cs

Lines changed: 59 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,16 +39,33 @@
3939

4040
namespace RCommon.ApplicationServices.Commands
4141
{
42+
/// <summary>
43+
/// Default implementation of <see cref="ICommandBus"/> that dispatches commands to their registered handlers
44+
/// using the dependency injection container.
45+
/// </summary>
46+
/// <remarks>
47+
/// The command bus resolves the appropriate <see cref="ICommandHandler{TResult, TCommand}"/> from the service provider,
48+
/// optionally validates the command via <see cref="IValidationService"/>, and invokes the handler using
49+
/// a dynamically compiled delegate. Compiled handler delegates can optionally be cached for improved performance.
50+
/// </remarks>
4251
public class CommandBus : ICommandBus
4352
{
4453
private readonly ILogger<CommandBus> _logger;
4554
private readonly IServiceProvider _serviceProvider;
4655
private readonly IValidationService _validationService;
4756
private readonly IOptions<CqrsValidationOptions> _validationOptions;
48-
private ICacheService _cacheService;
57+
private ICacheService? _cacheService;
4958
private readonly CachingOptions _cachingOptions;
5059

51-
public CommandBus(ILogger<CommandBus> logger, IServiceProvider serviceProvider, IValidationService validationService,
60+
/// <summary>
61+
/// Initializes a new instance of <see cref="CommandBus"/>.
62+
/// </summary>
63+
/// <param name="logger">Logger for tracing command execution.</param>
64+
/// <param name="serviceProvider">Service provider used to resolve command handlers.</param>
65+
/// <param name="validationService">Service used to validate commands before execution.</param>
66+
/// <param name="validationOptions">Options controlling whether command validation is enabled.</param>
67+
/// <param name="cachingOptions">Options controlling whether dynamically compiled expressions are cached.</param>
68+
public CommandBus(ILogger<CommandBus> logger, IServiceProvider serviceProvider, IValidationService validationService,
5269
IOptions<CqrsValidationOptions> validationOptions, IOptions<CachingOptions> cachingOptions)
5370
{
5471
_logger = logger;
@@ -58,11 +75,13 @@ public CommandBus(ILogger<CommandBus> logger, IServiceProvider serviceProvider,
5875
_cachingOptions = cachingOptions.Value;
5976
}
6077

78+
/// <inheritdoc />
6179
public async Task<TResult> DispatchCommandAsync<TResult>(ICommand<TResult> command, CancellationToken cancellationToken = default)
6280
where TResult : IExecutionResult
6381
{
6482
if (command == null) throw new ArgumentNullException(nameof(command));
6583

84+
// Validate the command if validation is configured for commands
6685
if (_validationOptions.Value != null && _validationOptions.Value.ValidateCommands)
6786
{
6887
// 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<TResult> DispatchCommandAsync<TResult>(ICommand<TResult> comma
86105
commandResult?.IsSuccess);
87106
}
88107

89-
return commandResult;
108+
return commandResult!;
90109
}
91110

111+
/// <summary>
112+
/// Resolves and invokes the single registered handler for the given command.
113+
/// </summary>
114+
/// <typeparam name="TResult">The execution result type returned by the handler.</typeparam>
115+
/// <param name="command">The command to execute.</param>
116+
/// <param name="cancellationToken">Token to observe for cancellation.</param>
117+
/// <returns>The result produced by the command handler.</returns>
118+
/// <exception cref="NoCommandHandlersException">Thrown when no handler is registered for the command type.</exception>
119+
/// <exception cref="InvalidOperationException">Thrown when more than one handler is registered for the command type.</exception>
92120
private async Task<TResult> ExecuteHandlerAsync<TResult>(ICommand<TResult> command, CancellationToken cancellationToken)
93121
where TResult : IExecutionResult
94122
{
95123
var commandType = command.GetType();
96124
var commandExecutionDetails = GetCommandExecutionDetails(commandType);
97125

126+
// Resolve all registered handlers for this command type and enforce exactly one
98127
var commandHandlers = _serviceProvider.GetServices(commandExecutionDetails.CommandHandlerType)
99128
.Cast<ICommandHandler>()
100129
.ToList();
@@ -114,41 +143,64 @@ private async Task<TResult> ExecuteHandlerAsync<TResult>(ICommand<TResult> comma
114143

115144
var commandHandler = commandHandlers.Single();
116145

146+
// Invoke the handler via the dynamically compiled delegate
117147
var task = (Task<TResult>)commandExecutionDetails.Invoker(commandHandler, command, cancellationToken);
118148
return await task;
119149
}
120150

151+
/// <summary>
152+
/// Holds the resolved handler type and the compiled delegate used to invoke it.
153+
/// </summary>
121154
private class CommandExecutionDetails
122155
{
123-
public Type CommandHandlerType { get; set; }
124-
public Func<ICommandHandler, ICommand, CancellationToken, Task> Invoker { get; set; }
156+
/// <summary>Gets or sets the closed generic handler interface type for the command.</summary>
157+
public Type CommandHandlerType { get; set; } = default!;
158+
159+
/// <summary>Gets or sets the compiled delegate that invokes <c>HandleAsync</c> on the handler.</summary>
160+
public Func<ICommandHandler, ICommand, CancellationToken, Task> Invoker { get; set; } = default!;
125161
}
126162

127163
private const string NameOfExecuteCommand = nameof(
128164
ICommandHandler<
129165
IExecutionResult,
130166
ICommand<IExecutionResult>
131167
>.HandleAsync);
168+
169+
/// <summary>
170+
/// Gets the <see cref="CommandExecutionDetails"/> for the given command type, optionally retrieving
171+
/// from cache if expression caching is enabled.
172+
/// </summary>
173+
/// <param name="commandType">The runtime type of the command being dispatched.</param>
174+
/// <returns>The execution details containing the handler type and compiled invoker delegate.</returns>
132175
private CommandExecutionDetails GetCommandExecutionDetails(Type commandType)
133176
{
177+
// When caching is enabled, cache the compiled expression to avoid repeated reflection/compilation
134178
if (_cachingOptions.CachingEnabled && _cachingOptions.CacheDynamicallyCompiledExpressions)
135179
{
136180
var cachingFactory = _serviceProvider.GetService<ICommonFactory<ExpressionCachingStrategy, ICacheService>>();
137181
Guard.Against<InvalidCacheException>(cachingFactory == null, "We could not properly inject the caching factory: 'ICommonFactory<ExpressionCachingStrategy, ICacheService>>' into the CommandBus");
138-
_cacheService = cachingFactory.Create(ExpressionCachingStrategy.Default);
182+
_cacheService = cachingFactory!.Create(ExpressionCachingStrategy.Default);
139183
return _cacheService.GetOrCreate(CacheKey.With(GetType(), commandType.GetCacheKey()), () => this.BuildCommandDetails(commandType));
140184
}
141185
return this.BuildCommandDetails(commandType);
142186
}
143187

188+
/// <summary>
189+
/// Builds the <see cref="CommandExecutionDetails"/> by reflecting over the command type to determine
190+
/// the handler interface and compiling a delegate for <c>HandleAsync</c>.
191+
/// </summary>
192+
/// <param name="commandType">The runtime type of the command being dispatched.</param>
193+
/// <returns>A new <see cref="CommandExecutionDetails"/> instance.</returns>
144194
private CommandExecutionDetails BuildCommandDetails(Type commandType)
145195
{
196+
// Find the ICommand<TResult> interface to extract the result type
146197
var commandInterfaceType = commandType
147198
.GetTypeInfo()
148199
.GetInterfaces()
149200
.Single(i => i.GetTypeInfo().IsGenericType && i.GetGenericTypeDefinition() == typeof(ICommand<>));
150201
var commandTypes = commandInterfaceType.GetTypeInfo().GetGenericArguments();
151202

203+
// Construct the closed generic ICommandHandler<TResult, TCommand> type
152204
var commandHandlerType = typeof(ICommandHandler<,>)
153205
.MakeGenericType(commandTypes[0], commandType);
154206

@@ -157,6 +209,7 @@ private CommandExecutionDetails BuildCommandDetails(Type commandType)
157209
commandType.PrettyPrint(),
158210
commandHandlerType.PrettyPrint());
159211

212+
// Compile a strongly-typed delegate for the handler's HandleAsync method
160213
var invokeExecuteAsync = ReflectionHelper.CompileMethodInvocation<Func<ICommandHandler, ICommand, CancellationToken, Task>>(
161214
commandHandlerType, NameOfExecuteCommand);
162215

Src/RCommon.ApplicationServices/Commands/ICommandBus.cs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,23 @@
2828

2929
namespace RCommon.ApplicationServices.Commands
3030
{
31+
/// <summary>
32+
/// Defines the contract for a command bus that dispatches commands to their corresponding handlers.
33+
/// </summary>
34+
/// <remarks>
35+
/// Commands represent intent to change state. The bus resolves the appropriate
36+
/// <see cref="ICommandHandler{TResult, TCommand}"/> and returns an <see cref="IExecutionResult"/>.
37+
/// </remarks>
3138
public interface ICommandBus
3239
{
33-
Task<TResult> DispatchCommandAsync<TResult>(ICommand<TResult> command, CancellationToken cancellationToken = default)
40+
/// <summary>
41+
/// Dispatches a command to its registered handler and returns the execution result.
42+
/// </summary>
43+
/// <typeparam name="TResult">The type of execution result returned by the handler.</typeparam>
44+
/// <param name="command">The command to dispatch.</param>
45+
/// <param name="cancellationToken">Optional token to cancel the operation.</param>
46+
/// <returns>The execution result produced by the command handler.</returns>
47+
Task<TResult> DispatchCommandAsync<TResult>(ICommand<TResult> command, CancellationToken cancellationToken = default)
3448
where TResult : IExecutionResult;
3549
}
3650
}

Src/RCommon.ApplicationServices/Commands/ICommandHandler.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99

1010
namespace RCommon.ApplicationServices.Commands
1111
{
12+
/// <summary>
13+
/// Non-generic marker interface for all command handlers. Used for service resolution via reflection.
14+
/// </summary>
1215
public interface ICommandHandler
1316
{
1417
}
@@ -18,6 +21,12 @@ public interface ICommandHandler
1821
public interface ICommandHandler<in TCommand> : ICommandHandler
1922
where TCommand: ICommand
2023
{
24+
/// <summary>
25+
/// Handles the specified command asynchronously.
26+
/// </summary>
27+
/// <param name="command">The command to handle.</param>
28+
/// <param name="cancellationToken">Token to observe for cancellation.</param>
29+
/// <returns>A task representing the asynchronous operation.</returns>
2130
Task HandleAsync(TCommand command, CancellationToken cancellationToken);
2231
}
2332

@@ -28,6 +37,12 @@ public interface ICommandHandler<TResult, in TCommand> : ICommandHandler
2837
where TCommand : ICommand<TResult>
2938
where TResult : IExecutionResult
3039
{
40+
/// <summary>
41+
/// Handles the specified command asynchronously and returns an execution result.
42+
/// </summary>
43+
/// <param name="command">The command to handle.</param>
44+
/// <param name="cancellationToken">Token to observe for cancellation.</param>
45+
/// <returns>The execution result produced by handling the command.</returns>
3146
Task<TResult> HandleAsync(TCommand command, CancellationToken cancellationToken);
3247
}
3348
}

0 commit comments

Comments
 (0)