From 247c7a5e47b6e454898cde365091b320b98ed531 Mon Sep 17 00:00:00 2001 From: jasonmwebb-lv Date: Sun, 8 Feb 2026 17:12:23 -0700 Subject: [PATCH] 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. 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. --- .claude/settings.local.json | 5 +- .gitignore | 3 + Src/.editorconfig | 157 ++++++++++++++++++ .../Commands/CommandBus.cs | 65 +++++++- .../Commands/ICommandBus.cs | 16 +- .../Commands/ICommandHandler.cs | 15 ++ .../Commands/NoCommandHandlersException.cs | 11 ++ .../CqrsBuilder.cs | 15 +- .../CqrsBuilderExtensions.cs | 111 ++++++++++++- .../CqrsCachingOptions.cs | 14 ++ .../CqrsValidationOptions.cs | 18 ++ .../ICqrsBuilder.cs | 10 ++ .../IValidationBuilder.cs | 11 ++ .../InvalidCacheException.cs | 14 +- .../Queries/IQueryBus.cs | 14 ++ .../Queries/IQueryHandler.cs | 14 ++ .../Queries/QueryBus.cs | 56 ++++++- .../RCommon.ApplicationServices.csproj | 1 + Src/RCommon.ApplicationServices/README.md | 106 +++++++++++- .../Validation/IValidationProvider.cs | 18 +- .../Validation/IValidationService.cs | 16 ++ .../Validation/ValidationException.cs | 28 ++-- .../Validation/ValidationFault.cs | 23 ++- .../Validation/ValidationOutcome.cs | 12 +- .../Validation/ValidationService.cs | 18 +- .../ValidationBuilderExtensions.cs | 27 ++- ...orizationHeaderParameterOperationFilter.cs | 14 ++ .../Filters/AuthorizeCheckOperationFilter.cs | 21 ++- .../RCommon.Authorization.Web.csproj | 1 + Src/RCommon.Authorization.Web/README.md | 53 +++++- Src/RCommon.Caching/CacheKey.cs | 29 ++++ .../CachingBuilderExtensions.cs | 45 ++++- .../ExpressionCachingStrategy.cs | 10 ++ Src/RCommon.Caching/ICacheService.cs | 25 +++ .../IDistributedCachingBuilder.cs | 7 + Src/RCommon.Caching/IMemoryCachingBuilder.cs | 7 + Src/RCommon.Caching/RCommon.Caching.csproj | 1 + Src/RCommon.Caching/README.md | 67 +++++++- Src/RCommon.Core/BaseApplicationException.cs | 21 ++- Src/RCommon.Core/CachingOptions.cs | 15 ++ .../Collections/IPaginatedList.cs | 16 ++ .../Collections/ListDictionary.cs | 99 ++++++++++- Src/RCommon.Core/Collections/PaginatedList.cs | 41 +++++ Src/RCommon.Core/CommonFactory.cs | 36 ++++ Src/RCommon.Core/Constants.cs | 43 ++++- Src/RCommon.Core/DisposableAction.cs | 3 + Src/RCommon.Core/DisposableResource.cs | 30 ++++ .../EventHandlingBuilderExtensions.cs | 43 ++++- Src/RCommon.Core/EventHandling/IEventBus.cs | 24 +++ .../EventHandling/IEventHandlingBuilder.cs | 7 + .../EventHandling/IInMemoryEventBusBuilder.cs | 8 +- .../EventHandling/InMemoryEventBus.cs | 44 ++++- .../EventHandling/InMemoryEventBusBuilder.cs | 11 +- .../InMemoryEventBusBuilderExtensions.cs | 19 +++ .../Producers/EventProductionException.cs | 21 ++- .../EventHandling/Producers/IEventProducer.cs | 14 +- .../EventHandling/Producers/IEventRouter.cs | 9 +- .../InMemoryTransactionalEventRouter.cs | 35 +++- .../PublishWithEventBusEventProducer.cs | 16 ++ .../UnsupportedEventProducerException.cs | 11 +- .../IDynamicDistributedEventHandler.cs | 9 + .../EventHandling/Subscribers/ISubscriber.cs | 11 ++ .../Extensions/CollectionExtensions.cs | 100 +++++++++-- .../Extensions/DateTimeExtensions.cs | 38 +++++ .../Extensions/DictionaryExtensions.cs | 35 ++-- .../Extensions/ExpressionExtensions.cs | 57 +++++++ Src/RCommon.Core/Extensions/GuidExtensions.cs | 8 + .../Extensions/IDataReaderExtensions.cs | 35 +++- .../Extensions/ILoggerExtensions.cs | 50 +++++- .../Extensions/IQueryableExtensions.cs | 78 ++++++++- .../Extensions/ObjectExtensions.cs | 40 +++-- .../Extensions/ServiceCollectionExtensions.cs | 50 +++++- .../Extensions/StreamExtensions.cs | 24 +++ .../Extensions/StringExtensions.cs | 156 +++++++++++++++-- Src/RCommon.Core/Extensions/TypeExtensions.cs | 48 ++++++ Src/RCommon.Core/GeneralException.cs | 62 ++++++- Src/RCommon.Core/Guard.cs | 138 ++++++++++++++- Src/RCommon.Core/ICommonFactory.cs | 58 +++++++ Src/RCommon.Core/IPagedSpecification.cs | 19 +++ Src/RCommon.Core/IRCommonBuilder.cs | 39 +++++ Src/RCommon.Core/ISystemTime.cs | 5 + Src/RCommon.Core/ISystemTimeOptions.cs | 8 + Src/RCommon.Core/InvalidArgumentException.cs | 17 ++ Src/RCommon.Core/Linq/Linq.cs | 18 +- Src/RCommon.Core/Linq/PredicateBuilder.cs | 37 +++++ Src/RCommon.Core/PagedSpecification.cs | 21 ++- Src/RCommon.Core/RCommon.Core.csproj | 1 + Src/RCommon.Core/RCommonBuilder.cs | 26 ++- Src/RCommon.Core/RCommonBuilderException.cs | 10 ++ Src/RCommon.Core/README.md | 72 +++++++- .../Reflection/ObjectGraphWalker.cs | 50 +++++- .../Reflection/ReflectionHelper.cs | 44 +++-- Src/RCommon.Core/SequentialGuidGenerator.cs | 22 +++ .../SequentialGuidGeneratorOptions.cs | 4 + Src/RCommon.Core/SimpleGuidGenerator.cs | 4 + Src/RCommon.Core/Specification.cs | 4 +- Src/RCommon.Core/SystemTime.cs | 25 +++ Src/RCommon.Core/SystemTimeOptions.cs | 10 +- Src/RCommon.Core/Threading/AsyncHelper.cs | 4 + Src/RCommon.Core/Util/ImageHelper.cs | 21 +++ Src/RCommon.Core/Util/Inflector.cs | 49 +++++- Src/RCommon.Dapper/Crud/DapperRepository.cs | 46 ++++- .../DapperFluentMappingsException.cs | 13 +- .../DapperPersistenceBuilder.cs | 41 ++++- Src/RCommon.Dapper/IDapperBuilder.cs | 14 ++ Src/RCommon.Dapper/RCommon.Dapper.csproj | 1 + Src/RCommon.Dapper/README.md | 97 ++++++++++- Src/RCommon.EfCore/Crud/EFCoreRepository.cs | 108 +++++++++--- .../EFCorePerisistenceBuilder.cs | 29 +++- .../IEFCorePersistenceBuilder.cs | 14 ++ Src/RCommon.EfCore/RCommonDbContext.cs | 17 ++ Src/RCommon.EfCore/README.md | 95 ++++++++++- Src/RCommon.Emailing/EmailEventArgs.cs | 10 ++ .../EmailingBuilderExtensions.cs | 11 +- Src/RCommon.Emailing/IEmailService.cs | 16 ++ Src/RCommon.Emailing/README.md | 89 +++++++++- Src/RCommon.Emailing/Smtp/SmtpEmailService.cs | 21 ++- .../Smtp/SmtpEmailSettings.cs | 44 ++++- Src/RCommon.Entities/AuditedEntity.cs | 31 ++++ Src/RCommon.Entities/BusinessEntity.cs | 70 ++++++-- .../EntityNotFoundException.cs | 21 ++- Src/RCommon.Entities/IAuditedEntity.cs | 35 +++- Src/RCommon.Entities/IBusinessEntity.cs | 24 ++- Src/RCommon.Entities/IEntityEventTracker.cs | 7 +- Src/RCommon.Entities/ITrackedEntity.cs | 8 + .../InMemoryEntityEventTracker.cs | 31 +++- Src/RCommon.Entities/RCommon.Entities.csproj | 1 + Src/RCommon.Entities/README.md | 91 +++++++++- .../TransactionalEventsChangedEventArgs.cs | 17 ++ .../TransactionalEventsClearedEventArgs.cs | 12 ++ .../FluentValidationBuilder.cs | 18 ++ .../FluentValidationBuilderExtensions.cs | 43 ++++- .../FluentValidationProvider.cs | 51 +++++- .../IFluentValidationBuilder.cs | 11 +- Src/RCommon.FluentValidation/README.md | 103 +++++++++++- .../ValidationBuilderExtensions.cs | 29 +++- Src/RCommon.Json/IJsonBuilder.cs | 9 + Src/RCommon.Json/IJsonSerializer.cs | 37 ++++- Src/RCommon.Json/JsonBuilderExtensions.cs | 65 +++++++- Src/RCommon.Json/JsonDeserializeOptions.cs | 15 +- Src/RCommon.Json/JsonSerializeOptions.cs | 18 ++ Src/RCommon.Json/RCommon.Json.csproj | 1 + Src/RCommon.Json/README.md | 85 +++++++++- Src/RCommon.JsonNet/IJsonNetBuilder.cs | 5 + .../IJsonNetBuilderExtensions.cs | 9 + Src/RCommon.JsonNet/JsonNetBuilder.cs | 16 ++ Src/RCommon.JsonNet/JsonNetSerializer.cs | 31 +++- Src/RCommon.JsonNet/RCommon.JsonNet.csproj | 1 + Src/RCommon.JsonNet/README.md | 78 ++++++++- Src/RCommon.Linq2Db/Crud/Linq2DbRepository.cs | 85 +++++++++- .../ILinq2DbPersistenceBuilder.cs | 14 ++ .../Linq2DbPersistenceBuilder.cs | 32 +++- Src/RCommon.Linq2Db/RCommonDataConnection.cs | 20 ++- Src/RCommon.Linq2Db/README.md | 102 +++++++++++- .../IMassTransitEventHandlingBuilder.cs | 7 +- .../MassTransitEventHandlingBuilder.cs | 12 +- ...ssTransitEventHandlingBuilderExtensions.cs | 43 ++++- .../PublishWithMassTransitEventProducer.cs | 18 ++ .../SendWithMassTransitEventProducer.cs | 19 +++ Src/RCommon.MassTransit/README.md | 88 +++++++++- .../Subscribers/IMassTransitEventHandler.cs | 9 +- .../Subscribers/MassTransitEventHandler.cs | 14 ++ Src/RCommon.Mediator/IMediatorAdapter.cs | 30 +++- Src/RCommon.Mediator/IMediatorBuilder.cs | 11 ++ Src/RCommon.Mediator/IMediatorService.cs | 30 ++++ .../MediatorBuilderExtensions.cs | 29 +++- Src/RCommon.Mediator/MediatorService.cs | 15 ++ Src/RCommon.Mediator/README.md | 103 +++++++++++- .../Subscribers/IAppNotification.cs | 8 + .../Subscribers/IAppRequest.cs | 15 ++ .../Subscribers/IAppRequestHandler.cs | 29 ++++ .../Behaviors/LoggingBehavior.cs | 24 +++ .../Behaviors/UnitOfWorkBehavior.cs | 26 +++ .../Behaviors/ValidatorBehavior.cs | 28 ++++ Src/RCommon.Mediatr/IMediatRBuilder.cs | 15 ++ .../IMediatREventHandlingBuilder.cs | 6 +- Src/RCommon.Mediatr/MediatRAdapter.cs | 19 +++ Src/RCommon.Mediatr/MediatRBuilder.cs | 15 ++ .../MediatRBuilderExtensions.cs | 46 ++++- .../MediatREventHandlingBuilder.cs | 15 +- .../MediatREventHandlingBuilderExtensions.cs | 36 +++- .../PublishWithMediatREventProducer.cs | 19 +++ .../Producers/SendWithMediatREventProducer.cs | 19 +++ Src/RCommon.Mediatr/README.md | 99 ++++++++++- .../Subscribers/IMediatRNotification.cs | 11 ++ .../Subscribers/IMediatRRequest.cs | 18 +- .../Subscribers/MediatREventHandler.cs | 26 ++- .../Subscribers/MediatRNotification.cs | 10 ++ .../Subscribers/MediatRNotificationHandler.cs | 27 ++- .../Subscribers/MediatRRequest.cs | 21 +++ .../Subscribers/MediatRRequestHandler.cs | 44 ++++- .../DistributedMemoryCacheBuilder.cs | 19 +++ .../DistributedMemoryCacheService.cs | 40 +++-- .../IDistributedMemoryCachingBuilder.cs | 7 + ...stributedMemoryCachingBuilderExtensions.cs | 26 ++- .../IInMemoryCachingBuilder.cs | 7 + .../IInMemoryCachingBuilderExtensions.cs | 26 ++- .../InMemoryCacheService.cs | 20 ++- .../InMemoryCachingBuilder.cs | 20 ++- .../RCommon.MemoryCache.csproj | 1 + Src/RCommon.MemoryCache/README.md | 67 +++++++- Src/RCommon.Models/Commands/CommandResult.cs | 14 ++ Src/RCommon.Models/Commands/ICommand.cs | 14 ++ Src/RCommon.Models/Commands/ICommandResult.cs | 10 ++ Src/RCommon.Models/Events/IAsyncEvent.cs | 10 ++ .../Events/ISerializableEvent.cs | 6 + Src/RCommon.Models/Events/ISyncEvent.cs | 10 ++ .../ExecutionResults/ExecutionResult.cs | 38 +++++ .../ExecutionResults/FailedExecutionResult.cs | 21 +++ .../ExecutionResults/IExecutionResult.cs | 10 ++ .../SuccessExecutionResult.cs | 14 ++ Src/RCommon.Models/IModel.cs | 5 + Src/RCommon.Models/IPaginatedListRequest.cs | 24 ++- .../ISearchPaginatedListRequest.cs | 11 +- Src/RCommon.Models/PaginatedListModel.cs | 114 ++++++++++++- Src/RCommon.Models/PaginatedListRequest.cs | 19 ++- Src/RCommon.Models/Queries/IQuery.cs | 10 ++ Src/RCommon.Models/RCommon.Models.csproj | 1 + Src/RCommon.Models/README.md | 86 +++++++++- .../SearchPaginatedListRequest.cs | 14 +- Src/RCommon.Models/SortDirectionEnum.cs | 16 +- .../IPersistenceBuilderExtensions.cs | 39 ++++- .../README.md | 55 +++++- .../IPersistenceBuilderExtensions.cs | 25 ++- .../README.md | 62 ++++++- .../Crud/CachingGraphRepository.cs | 55 +++++- .../Crud/CachingLinqRepository.cs | 53 +++++- .../Crud/CachingSqlMapperRepository.cs | 38 ++++- .../Crud/ICachingGraphRepository.cs | 43 +++++ .../Crud/ICachingLinqRepository.cs | 43 +++++ .../Crud/ICachingSqlMapperRepository.cs | 23 +++ .../IPersistenceBuilderExtensions.cs | 17 ++ .../PersistenceCachingStrategy.cs | 10 ++ .../RCommon.Persistence.Caching.csproj | 1 + Src/RCommon.Persistence.Caching/README.md | 82 ++++++++- .../Crud/GraphRepositoryBase.cs | 18 +- .../Crud/IEagerLoadableQueryable.cs | 15 ++ .../Crud/IGraphRepository.cs | 15 +- .../Crud/ILinqRepository.cs | 67 +++++++- .../Crud/IReadOnlyRepository.cs | 64 ++++++- .../Crud/ISqlMapperRepository.cs | 14 +- .../Crud/IWriteOnlyRepository.cs | 7 + .../Crud/LinqRepositoryBase.cs | 80 ++++++++- .../Crud/RepositoryException.cs | 12 ++ .../Crud/SqlRepositoryBase.cs | 75 ++++++++- Src/RCommon.Persistence/DataStoreFactory.cs | 15 +- .../DataStoreFactoryOptions.cs | 17 ++ .../DataStoreNotFoundException.cs | 7 + Src/RCommon.Persistence/DataStoreValue.cs | 25 +++ .../DefaultDataStoreOptions.cs | 12 +- Src/RCommon.Persistence/IDataStore.cs | 13 +- Src/RCommon.Persistence/INamedDataSource.cs | 10 ++ .../IPersistenceBuilder.cs | 13 ++ Src/RCommon.Persistence/IReadModel.cs | 7 + Src/RCommon.Persistence/IScopedDataStore.cs | 7 + .../PersistenceBuilderExtensions.cs | 32 +++- .../PersistenceException.cs | 14 +- .../RCommon.Persistence.csproj | 1 + Src/RCommon.Persistence/README.md | 100 ++++++++++- Src/RCommon.Persistence/Sql/IRDbConnection.cs | 8 +- Src/RCommon.Persistence/Sql/RDbConnection.cs | 32 +++- .../Sql/RDbConnectionException.cs | 10 +- .../Sql/RDbConnectionOptions.cs | 17 +- .../Transactions/DefaultUnitOfWorkBuilder.cs | 9 +- .../Transactions/IUnitOfWork.cs | 33 ++++ .../Transactions/IUnitOfWorkBuilder.cs | 10 ++ .../Transactions/IUnitOfWorkFactory.cs | 21 +++ .../Transactions/TransactionScopeHelper.cs | 17 ++ .../Transactions/UnitOfWork.cs | 42 +++++ .../Transactions/UnitOfWorkException.cs | 8 + .../Transactions/UnitOfWorkFactory.cs | 20 ++- .../Transactions/UnitOfWorkSettings.cs | 10 +- .../Transactions/UnitOfWorkState.cs | 22 +++ .../UnsupportedDataStoreException.cs | 10 ++ .../IRedisCachingBuilder.cs | 7 + .../IRedisCachingBuilderExtensions.cs | 20 ++- .../RCommon.RedisCache.csproj | 1 + Src/RCommon.RedisCache/README.md | 60 ++++++- Src/RCommon.RedisCache/RedisCacheService.cs | 40 +++-- Src/RCommon.RedisCache/RedisCachingBuilder.cs | 19 ++- .../Authorization/AuthorizationException.cs | 10 +- .../Claims/ClaimTypesConst.cs | 4 + .../Claims/CurrentPrincipalAccessorBase.cs | 30 +++- .../CurrentPrincipalAccessorExtensions.cs | 22 +++ .../Claims/ICurrentPrincipalAccessor.cs | 13 +- .../Claims/ThreadCurrentPrincipalAccessor.cs | 11 +- .../ClaimsIdentityExtensions.cs | 60 ++++++- Src/RCommon.Security/Clients/CurrentClient.cs | 12 +- .../Clients/ICurrentClient.cs | 13 +- Src/RCommon.Security/RCommon.Security.csproj | 1 + Src/RCommon.Security/README.md | 102 +++++++++++- .../SecurityConfigurationExtensions.cs | 11 +- Src/RCommon.Security/Users/CurrentUser.cs | 24 ++- .../Users/CurrentUserExtensions.cs | 26 ++- Src/RCommon.Security/Users/ICurrentUser.cs | 36 +++- Src/RCommon.SendGrid/README.md | 83 ++++++++- Src/RCommon.SendGrid/SendGridEmailService.cs | 42 ++++- Src/RCommon.SendGrid/SendGridEmailSettings.cs | 19 ++- ...SendGridEmailingConfigurationExtensions.cs | 11 +- .../ITextJsonBuilder.cs | 5 + .../ITextJsonBuilderExtensions.cs | 9 + .../JsonByteEnumConverter.cs | 27 ++- .../JsonIntEnumConverter.cs | 27 ++- .../RCommon.SystemTextJson.csproj | 13 +- Src/RCommon.SystemTextJson/README.md | 84 +++++++++- Src/RCommon.SystemTextJson/TextJsonBuilder.cs | 16 ++ .../TextJsonSerializer.cs | 33 +++- Src/RCommon.Web/RCommon.Web.csproj | 1 + Src/RCommon.Web/README.md | 35 +++- .../IWolverineEventHandlingBuilder.cs | 4 + .../PublishWithWolverineEventProducer.cs | 19 +++ .../SendWithWolverineEventProducer.cs | 19 +++ Src/RCommon.Wolverine/README.md | 87 +++++++++- .../Subscribers/IWolverineEventHandler.cs | 13 ++ .../Subscribers/WolverineEventHandler.cs | 11 ++ .../WolverineEventHandlingBuilder.cs | 9 + ...WolverineEventHandlingBuilderExtensions.cs | 18 +- .../RCommon.ApplicationServices.Tests.csproj | 12 -- .../EntityNotFoundExceptionTests.cs | 2 +- .../DefaultDataStoreOptionsTests.cs | 2 +- .../RCommon.Persistence.Tests.csproj | 12 -- .../RDbConnectionOptionsTests.cs | 4 +- .../RDbConnectionTests.cs | 4 +- .../RCommon.TestBase.Data.csproj | 3 - Tests/RCommon.TestBase.Data/TestRepository.cs | 4 +- Tests/RCommon.TestBase/Entities/Customer.cs | 14 +- Tests/RCommon.TestBase/Entities/Department.cs | 2 +- .../Entities/MonthlySalesSummary.cs | 6 +- Tests/RCommon.TestBase/Entities/Order.cs | 2 +- Tests/RCommon.TestBase/Entities/OrderItem.cs | 6 +- Tests/RCommon.TestBase/Entities/Product.cs | 4 +- .../RCommon.TestBase/Entities/SalesPerson.cs | 8 +- .../Entities/SalesTerritory.cs | 4 +- .../RCommon.TestBase/RCommon.TestBase.csproj | 3 - Tests/RCommon.TestBase/TestBootstrapper.cs | 6 +- 335 files changed, 8791 insertions(+), 596 deletions(-) create mode 100644 Src/.editorconfig 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 + enable True RCommon.ApplicationServices https://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 + enable True RCommon.Authorization.Web https://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 + enable True RCommon.Caching https://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 handlers = scope.ServiceProvider.GetServices(handlerType); - foreach (object handler in handlers) + IEnumerable handlers = scope.ServiceProvider.GetServices(handlerType); + foreach (object? handler in handlers) { - object result = handlerType + if (handler == null) continue; + + // Invoke HandleAsync via reflection to support polymorphic dispatch + object? result = handlerType .GetTypeInfo() .GetDeclaredMethod(nameof(ISubscriber.HandleAsync)) - .Invoke(handler, new object[] { @event, CancellationToken.None}); - await (Task)result; + ?.Invoke(handler, new object[] { @event, CancellationToken.None}); + if (result is Task task) + { + await task; + } } } } diff --git a/Src/RCommon.Core/EventHandling/InMemoryEventBusBuilder.cs b/Src/RCommon.Core/EventHandling/InMemoryEventBusBuilder.cs index 05697743..f29a4586 100644 --- a/Src/RCommon.Core/EventHandling/InMemoryEventBusBuilder.cs +++ b/Src/RCommon.Core/EventHandling/InMemoryEventBusBuilder.cs @@ -7,15 +7,24 @@ namespace RCommon.EventHandling { + /// + /// Default implementation of that configures + /// in-memory event handling by exposing the from the parent + /// . + /// public class InMemoryEventBusBuilder : IInMemoryEventBusBuilder { - + /// + /// Initializes a new instance of using the parent builder's service collection. + /// + /// The parent whose will be used. public InMemoryEventBusBuilder(IRCommonBuilder builder) { Services = builder.Services; } + /// public IServiceCollection Services { get; } } } diff --git a/Src/RCommon.Core/EventHandling/InMemoryEventBusBuilderExtensions.cs b/Src/RCommon.Core/EventHandling/InMemoryEventBusBuilderExtensions.cs index 5e59511b..11dce2d8 100644 --- a/Src/RCommon.Core/EventHandling/InMemoryEventBusBuilderExtensions.cs +++ b/Src/RCommon.Core/EventHandling/InMemoryEventBusBuilderExtensions.cs @@ -12,8 +12,19 @@ namespace RCommon.EventHandling { + /// + /// Extension methods for to register event subscribers + /// and associate them with the correct event producers via the . + /// public static class InMemoryEventBusBuilderExtensions { + /// + /// Registers a scoped subscriber for the specified event type and records the event-to-producer + /// subscription so the routes this event only to the correct producers. + /// + /// The event type to subscribe to. Must implement . + /// The subscriber type that handles . + /// The in-memory event bus builder. public static void AddSubscriber(this IInMemoryEventBusBuilder builder) where TEvent : class, ISerializableEvent where TEventHandler : class, ISubscriber @@ -25,6 +36,14 @@ public static void AddSubscriber(this IInMemoryEventBusBu subscriptionManager?.AddSubscription(builder.GetType(), typeof(TEvent)); } + /// + /// Registers a scoped subscriber for the specified event type using a factory delegate, + /// and records the event-to-producer subscription for correct event routing. + /// + /// The event type to subscribe to. Must implement . + /// The subscriber type that handles . + /// The in-memory event bus builder. + /// A factory function to create the subscriber instance. public static void AddSubscriber(this IInMemoryEventBusBuilder builder, Func getSubscriber) where TEvent : class, ISerializableEvent where TEventHandler : class, ISubscriber diff --git a/Src/RCommon.Core/EventHandling/Producers/EventProductionException.cs b/Src/RCommon.Core/EventHandling/Producers/EventProductionException.cs index b2a2821f..9763a82d 100644 --- a/Src/RCommon.Core/EventHandling/Producers/EventProductionException.cs +++ b/Src/RCommon.Core/EventHandling/Producers/EventProductionException.cs @@ -6,18 +6,37 @@ namespace RCommon.EventHandling.Producers { + /// + /// Exception thrown when an error occurs during event production through an + /// or the . + /// public class EventProductionException : GeneralException { + /// + /// Initializes a new instance of with the specified message. + /// + /// The error message. public EventProductionException(string message) : base(message) { - + } + /// + /// Initializes a new instance of with a parameterized message. + /// + /// The message format string. + /// Parameters to format into the message string. public EventProductionException(string message, object[] @params) : base(message, @params) { } + /// + /// Initializes a new instance of with a parameterized message and inner exception. + /// + /// The message format string. + /// The inner exception that caused this error. + /// Parameters to format into the message string. public EventProductionException(string message, Exception exception, object[] @params) : base(message, exception, @params) { diff --git a/Src/RCommon.Core/EventHandling/Producers/IEventProducer.cs b/Src/RCommon.Core/EventHandling/Producers/IEventProducer.cs index d06fb2cb..c9f06a88 100644 --- a/Src/RCommon.Core/EventHandling/Producers/IEventProducer.cs +++ b/Src/RCommon.Core/EventHandling/Producers/IEventProducer.cs @@ -8,9 +8,21 @@ namespace RCommon.EventHandling.Producers { + /// + /// Defines a producer responsible for dispatching serializable events to their destination + /// (e.g., in-memory bus, message broker, or external system). + /// + /// public interface IEventProducer { - Task ProduceEventAsync(TEvent @event, CancellationToken cancellationToken = default) + /// + /// Produces (dispatches) the specified event asynchronously. + /// + /// The type of event to produce. Must implement . + /// The event instance to produce. + /// A token to observe for cancellation requests. + /// A representing the asynchronous produce operation. + Task ProduceEventAsync(TEvent @event, CancellationToken cancellationToken = default) where TEvent : ISerializableEvent; } } diff --git a/Src/RCommon.Core/EventHandling/Producers/IEventRouter.cs b/Src/RCommon.Core/EventHandling/Producers/IEventRouter.cs index c6e036b5..486eaf82 100644 --- a/Src/RCommon.Core/EventHandling/Producers/IEventRouter.cs +++ b/Src/RCommon.Core/EventHandling/Producers/IEventRouter.cs @@ -4,18 +4,23 @@ namespace RCommon.EventHandling.Producers { + /// + /// Defines a router responsible for storing transactional events and dispatching them to the appropriate + /// instances when ready. + /// + /// public interface IEventRouter { /// /// Adds a serializable event to the transactional event store so that it can be published when the consumer is ready. /// - /// + /// The event to add to the transactional store. void AddTransactionalEvent(ISerializableEvent serializableEvent); /// /// Adds a collection of serializable events to the transactional event store so that it can be published when the consumer is ready. /// - /// + /// The collection of events to add to the transactional store. void AddTransactionalEvents(IEnumerable serializableEvents); /// diff --git a/Src/RCommon.Core/EventHandling/Producers/InMemoryTransactionalEventRouter.cs b/Src/RCommon.Core/EventHandling/Producers/InMemoryTransactionalEventRouter.cs index a9d7c881..7cf810cb 100644 --- a/Src/RCommon.Core/EventHandling/Producers/InMemoryTransactionalEventRouter.cs +++ b/Src/RCommon.Core/EventHandling/Producers/InMemoryTransactionalEventRouter.cs @@ -22,6 +22,12 @@ public class InMemoryTransactionalEventRouter : IEventRouter private readonly EventSubscriptionManager _subscriptionManager; private ConcurrentQueue _storedTransactionalEvents; + /// + /// Initializes a new instance of . + /// + /// The service provider used to resolve instances. + /// The logger for diagnostic output. + /// The manager that tracks event-to-producer subscriptions for filtering. public InMemoryTransactionalEventRouter(IServiceProvider serviceProvider, ILogger logger, EventSubscriptionManager subscriptionManager) { @@ -31,6 +37,7 @@ public InMemoryTransactionalEventRouter(IServiceProvider serviceProvider, ILogge _storedTransactionalEvents = new ConcurrentQueue(); } + /// public async Task RouteEventsAsync(IEnumerable transactionalEvents) { try @@ -85,6 +92,11 @@ public async Task RouteEventsAsync(IEnumerable transactional } } + /// + /// Dispatches async events to their filtered producers concurrently using . + /// + /// The async events to produce. + /// All registered event producers (will be filtered per event). private async Task ProduceAsyncEvents(IEnumerable asyncEvents, IEnumerable eventProducers) { var eventTaskList = new List(); @@ -99,6 +111,11 @@ private async Task ProduceAsyncEvents(IEnumerable asyncEvent await Task.WhenAll(eventTaskList); } + /// + /// Dispatches sync events to their filtered producers sequentially, awaiting each before proceeding. + /// + /// The synchronous events to produce. + /// All registered event producers (will be filtered per event). private async Task ProduceSyncEvents(IEnumerable syncEvents, IEnumerable eventProducers) { foreach (var @event in syncEvents) @@ -129,12 +146,19 @@ public async Task RouteEventsAsync() } } + /// + /// Removes a batch of events from the concurrent queue with retry logic. + /// Each event is attempted up to 4 times before throwing an . + /// + /// The events to remove from the queue. + /// Thrown if an event cannot be dequeued after 4 attempts. private void RemoveEvents(IEnumerable events) { foreach (var @event in events) { var item = @event; + // Retry dequeue up to 4 times to handle ConcurrentQueue contention for (int i = 1; i <= 4; i++) // Try 4 times { if (!RemoveEvent(item)) @@ -151,22 +175,29 @@ private void RemoveEvents(IEnumerable events) throw new EventProductionException($"Could not Dequeue event {item}"); } } - + } } + /// + /// Attempts to dequeue a single event from the concurrent queue. + /// + /// The event to dequeue (used as output parameter for the dequeued item). + /// true if the dequeue was successful; otherwise false. private bool RemoveEvent(ISerializableEvent @event) { - bool success = _storedTransactionalEvents.TryDequeue(out @event); + bool success = _storedTransactionalEvents.TryDequeue(out ISerializableEvent? _); return success; } + /// public void AddTransactionalEvent(ISerializableEvent serializableEvent) { Guard.IsNotNull(serializableEvent, nameof(serializableEvent)); _storedTransactionalEvents.Enqueue(serializableEvent); } + /// public void AddTransactionalEvents(IEnumerable serializableEvents) { Guard.IsNotNull(serializableEvents, nameof(serializableEvents)); diff --git a/Src/RCommon.Core/EventHandling/Producers/PublishWithEventBusEventProducer.cs b/Src/RCommon.Core/EventHandling/Producers/PublishWithEventBusEventProducer.cs index 803a97ee..3ff47304 100644 --- a/Src/RCommon.Core/EventHandling/Producers/PublishWithEventBusEventProducer.cs +++ b/Src/RCommon.Core/EventHandling/Producers/PublishWithEventBusEventProducer.cs @@ -9,12 +9,23 @@ namespace RCommon.EventHandling.Producers { + /// + /// An implementation that produces events by publishing them + /// through the . Consults the + /// to determine whether this producer should handle a given event type. + /// public class PublishWithEventBusEventProducer : IEventProducer { private readonly IEventBus _eventBus; private readonly ILogger _logger; private readonly EventSubscriptionManager _subscriptionManager; + /// + /// Initializes a new instance of . + /// + /// The event bus used to publish events to subscribers. + /// The logger for diagnostic output. + /// The subscription manager that determines which events this producer handles. public PublishWithEventBusEventProducer(IEventBus eventBus, ILogger logger, EventSubscriptionManager subscriptionManager) { @@ -23,6 +34,11 @@ public PublishWithEventBusEventProducer(IEventBus eventBus, ILogger + /// + /// Before publishing, this producer checks the to determine + /// if it should handle the event. If not subscribed, the event is silently skipped. + /// public async Task ProduceEventAsync(T @event, CancellationToken cancellationToken = default) where T : ISerializableEvent { diff --git a/Src/RCommon.Core/EventHandling/Producers/UnsupportedEventProducerException.cs b/Src/RCommon.Core/EventHandling/Producers/UnsupportedEventProducerException.cs index 34ce9037..2fd6b634 100644 --- a/Src/RCommon.Core/EventHandling/Producers/UnsupportedEventProducerException.cs +++ b/Src/RCommon.Core/EventHandling/Producers/UnsupportedEventProducerException.cs @@ -6,13 +6,20 @@ namespace RCommon.EventHandling.Producers { + /// + /// Exception thrown when an attempt is made to use an that is not + /// supported or not properly configured for the current event handling pipeline. + /// public class UnsupportedEventProducerException : ApplicationException { - + /// + /// Initializes a new instance of with the specified message. + /// + /// The error message describing the unsupported producer. public UnsupportedEventProducerException(string message) : base(message) { } - + } } diff --git a/Src/RCommon.Core/EventHandling/Subscribers/IDynamicDistributedEventHandler.cs b/Src/RCommon.Core/EventHandling/Subscribers/IDynamicDistributedEventHandler.cs index baa947ab..bcd36127 100644 --- a/Src/RCommon.Core/EventHandling/Subscribers/IDynamicDistributedEventHandler.cs +++ b/Src/RCommon.Core/EventHandling/Subscribers/IDynamicDistributedEventHandler.cs @@ -2,8 +2,17 @@ namespace RCommon.EventHandling.Subscribers { + /// + /// Defines a handler for distributed events that accepts dynamically-typed event data, + /// allowing handling of events without compile-time knowledge of their concrete type. + /// public interface IDynamicDistributedEventHandler { + /// + /// Handles a distributed event with dynamically-typed event data. + /// + /// The event data as a dynamic object. + /// A representing the asynchronous handling operation. Task Handle(dynamic eventData); } } diff --git a/Src/RCommon.Core/EventHandling/Subscribers/ISubscriber.cs b/Src/RCommon.Core/EventHandling/Subscribers/ISubscriber.cs index 3fa16853..cf9897d6 100644 --- a/Src/RCommon.Core/EventHandling/Subscribers/ISubscriber.cs +++ b/Src/RCommon.Core/EventHandling/Subscribers/ISubscriber.cs @@ -10,8 +10,19 @@ namespace RCommon.EventHandling.Subscribers { + /// + /// Defines a strongly-typed event subscriber that handles events of type . + /// Implementations are resolved from the DI container when events are published via the . + /// + /// The type of event this subscriber handles. public interface ISubscriber { + /// + /// Handles the specified event asynchronously. + /// + /// The event instance to handle. + /// A token to observe for cancellation requests. + /// A representing the asynchronous handling operation. public Task HandleAsync(TEvent @event, CancellationToken cancellationToken = default); } } diff --git a/Src/RCommon.Core/Extensions/CollectionExtensions.cs b/Src/RCommon.Core/Extensions/CollectionExtensions.cs index 61fb1cca..bec0ac7d 100644 --- a/Src/RCommon.Core/Extensions/CollectionExtensions.cs +++ b/Src/RCommon.Core/Extensions/CollectionExtensions.cs @@ -17,6 +17,9 @@ namespace RCommon /// public static class CollectionExtensions { + /// + /// The default comma delimiter character used by delimited string methods. + /// public static readonly char CommaDelimiter = ','; /// @@ -25,7 +28,7 @@ public static class CollectionExtensions /// The list of elements to create delimited string from /// The string consisting of comma-separated elements (using the ToString() method) from the input list /// if is null - public static string GetCommaDelimitedString(this IEnumerable source) + public static string? GetCommaDelimitedString(this IEnumerable source) { return source.GetDelimitedString(CommaDelimiter); } @@ -38,7 +41,7 @@ public static string GetCommaDelimitedString(this IEnumerable source) /// The delegate to return data to be extracted from each element /// comma-delimited string /// if is null - public static string GetCommaDelimitedString(this IEnumerable source, + public static string? GetCommaDelimitedString(this IEnumerable source, Func funcToGetString) { return source.GetDelimitedString(funcToGetString, CommaDelimiter, false, true); @@ -52,9 +55,9 @@ public static string GetCommaDelimitedString(this IEnumerable source, /// The list of elements to create delimited string from /// The string consisting of delimiter-separated elements (using the ToString() method) from the input list /// if is null - public static string GetDelimitedString(this IEnumerable source, char delimiter) + public static string? GetDelimitedString(this IEnumerable source, char delimiter) { - return source.GetDelimitedString(t => t.ToString(), delimiter, false, true); + return source.GetDelimitedString(t => t?.ToString() ?? string.Empty, delimiter, false, true); } /// @@ -68,7 +71,7 @@ public static string GetDelimitedString(this IEnumerable source, char deli /// The string consisting of delimiter-separated elements (using the ToString() method) from the input list /// if is null /// if is null - public static string GetDelimitedString(this IEnumerable source, + public static string? GetDelimitedString(this IEnumerable source, Func funcToGetString, char delimiter, bool addLeadingDelimiter, bool removeTrailingDelimiter) { @@ -109,7 +112,7 @@ public static IList Copy(this IList list) /// /// A boolean indicating if the original order in the collectionWithPossibleDuplicates should be retained /// - public static IList ConvertToListWithNoDuplicates( + public static IList? ConvertToListWithNoDuplicates( this IEnumerable collectionWithPossibleDuplicates, bool retainOriginalOrder) { if (collectionWithPossibleDuplicates == null) return null; @@ -164,7 +167,7 @@ public static IList ConvertToList(this System.Collections.IEnumerable obje /// /// /// - public static T[] ConvertToArray(this IEnumerable source) + public static T[]? ConvertToArray(this IEnumerable source) { if (source == null) return null; @@ -256,6 +259,10 @@ public static void TryForEach(this IEnumerable collection, Action actio + /// + /// Helper method placeholder that always returns true. Used internally as a default predicate. + /// + /// Always returns true. private static bool ForEachHelper() { return true; @@ -279,6 +286,12 @@ public static void TryForEach(this IEnumerator enumerator, Action actio } } + /// + /// Determines whether the collection is null or contains no elements. + /// + /// The type of the elements in the collection. + /// The collection to check. + /// true if the collection is null or empty; otherwise, false. [DebuggerStepThrough] public static bool IsNullOrEmpty(this ICollection collection) { @@ -286,6 +299,12 @@ public static bool IsNullOrEmpty(this ICollection collection) } + /// + /// Delegate used to decide whether an item should be removed from a collection. + /// + /// The type of item to evaluate. + /// The item to evaluate. + /// true if the item should be removed; otherwise, false. public delegate bool Decide(T item); /// @@ -353,6 +372,12 @@ public static void RemoveItems(this ICollection collection, Action acti } + /// + /// Converts an to a by reflecting over the properties of the first element. + /// + /// The list of objects to convert. Must contain at least one element. + /// A populated with data from the list. + /// Thrown when is null. public static DataTable ToDataTable(this IList alist) { DataTable dt = new DataTable(); @@ -361,9 +386,9 @@ public static DataTable ToDataTable(this IList alist) { throw new FormatException("Parameter ArrayList empty"); } - dt.TableName = alist[0].GetType().Name; + dt.TableName = alist[0]!.GetType().Name; DataRow dr; - System.Reflection.PropertyInfo[] propInfo = alist[0].GetType().GetProperties(); + System.Reflection.PropertyInfo[] propInfo = alist[0]!.GetType().GetProperties(); for (int i = 0; i < propInfo.Length; i++) { dt.Columns.Add(propInfo[i].Name, propInfo[i].PropertyType); @@ -374,9 +399,9 @@ public static DataTable ToDataTable(this IList alist) dr = dt.NewRow(); for (int i = 0; i < propInfo.Length; i++) { - object tempObject = alist[row]; + object? tempObject = alist[row]; - object t = propInfo[i].GetValue(tempObject, null); + object? t = propInfo[i].GetValue(tempObject, null); /*object t =tempObject.GetType().InvokeMember(propInfo[i].Name, R.BindingFlags.GetProperty , null,tempObject , new object [] {});*/ if (t != null) @@ -387,6 +412,13 @@ public static DataTable ToDataTable(this IList alist) return dt; } + /// + /// Converts an to a , filtering columns to only those whose names appear in . + /// + /// The list of objects to convert. Must contain at least one element. + /// An of column names to include in the resulting . + /// A populated with the filtered column data from the list. + /// Thrown when is null. public static DataTable ToDataTable(this IList alist, ArrayList alColNames) { DataTable dt = new DataTable(); @@ -395,14 +427,14 @@ public static DataTable ToDataTable(this IList alist, ArrayList alColNames) { throw new FormatException("Parameter ArrayList empty"); } - dt.TableName = alist[0].GetType().Name; + dt.TableName = alist[0]!.GetType().Name; DataRow dr; - System.Reflection.PropertyInfo[] propInfo = alist[0].GetType().GetProperties(); + System.Reflection.PropertyInfo[] propInfo = alist[0]!.GetType().GetProperties(); for (int i = 0; i < propInfo.Length; i++) { for (int j = 0; j < alColNames.Count; j++) { - if (alColNames[j].ToString() == propInfo[i].Name) + if (alColNames[j]?.ToString() == propInfo[i].Name) { dt.Columns.Add(propInfo[i].Name, propInfo[i].PropertyType); break; @@ -415,9 +447,9 @@ public static DataTable ToDataTable(this IList alist, ArrayList alColNames) dr = dt.NewRow(); for (int i = 0; i < dt.Columns.Count; i++) { - object tempObject = alist[row]; + object? tempObject = alist[row]; - object t = propInfo[i].GetValue(tempObject, null); + object? t = propInfo[i].GetValue(tempObject, null); /*object t =tempObject.GetType().InvokeMember(propInfo[i].Name, R.BindingFlags.GetProperty , null,tempObject , new object [] {});*/ if (t != null) @@ -428,6 +460,15 @@ public static DataTable ToDataTable(this IList alist, ArrayList alColNames) return dt; } + /// + /// Conditionally filters an based on a boolean condition. + /// If the condition is false, the source is returned unfiltered. + /// + /// The type of elements in the source. + /// The source enumerable to filter. + /// If true, the predicate is applied; otherwise, the source is returned as-is. + /// The filter predicate to apply when is true. + /// A filtered or unfiltered enumerable depending on the condition. public static IEnumerable WhereIf(this IEnumerable source, bool condition, Func predicate) { if (condition) @@ -436,6 +477,15 @@ public static IEnumerable WhereIf(this IEnumerable so return source; } + /// + /// Conditionally filters an based on a boolean condition, using an index-aware predicate. + /// If the condition is false, the source is returned unfiltered. + /// + /// The type of elements in the source. + /// The source enumerable to filter. + /// If true, the predicate is applied; otherwise, the source is returned as-is. + /// The index-aware filter predicate to apply when is true. + /// A filtered or unfiltered enumerable depending on the condition. public static IEnumerable WhereIf(this IEnumerable source, bool condition, Func predicate) { if (condition) @@ -444,6 +494,15 @@ public static IEnumerable WhereIf(this IEnumerable so return source; } + /// + /// Converts an to an with the specified page index and size. + /// + /// The type of elements in the collection. + /// The source collection to paginate. + /// The 1-based page index, or null to default to the first page. + /// The number of items per page. Must be greater than zero. + /// A paginated list containing the items for the requested page. + /// public static IPaginatedList ToPaginatedList(this ICollection query, int? pageIndex, int pageSize) { Guard.IsNotNegativeOrZero(pageSize, "pageSize"); @@ -451,6 +510,15 @@ public static IPaginatedList ToPaginatedList(this ICollection query, in return new PaginatedList(query, pageIndex, pageSize); } + /// + /// Converts an to an with the specified page index and size. + /// + /// The type of elements in the list. + /// The source list to paginate. + /// The 1-based page index, or null to default to the first page. + /// The number of items per page. Must be greater than zero. + /// A paginated list containing the items for the requested page. + /// public static IPaginatedList ToPaginatedList(this IList query, int? pageIndex, int pageSize) { Guard.IsNotNegativeOrZero(pageSize, "pageSize"); diff --git a/Src/RCommon.Core/Extensions/DateTimeExtensions.cs b/Src/RCommon.Core/Extensions/DateTimeExtensions.cs index 6c91df44..9dc5e974 100644 --- a/Src/RCommon.Core/Extensions/DateTimeExtensions.cs +++ b/Src/RCommon.Core/Extensions/DateTimeExtensions.cs @@ -6,28 +6,60 @@ namespace RCommon { + /// + /// Provides extension methods for operations including validation, + /// date range helpers, and human-readable formatting. + /// public static class DateTimeExtension { + /// + /// The minimum valid date (January 1, 1900) used by . + /// private static readonly DateTime MinDate = new DateTime(1900, 1, 1); + + /// + /// The maximum valid date (December 31, 9999 23:59:59.999) used by . + /// private static readonly DateTime MaxDate = new DateTime(9999, 12, 31, 23, 59, 59, 999); + /// + /// Determines whether the falls within the valid range (1900-01-01 to 9999-12-31). + /// + /// The to validate. + /// true if the date is within the valid range; otherwise, false. [DebuggerStepThrough] public static bool IsValid(this DateTime target) { return (target >= MinDate) && (target <= MaxDate); } + /// + /// Returns a representing the first day of the month for the given date. + /// + /// The source date. + /// A set to the first day of the month. public static DateTime FirstDayOfMonth(this DateTime dt) { return new DateTime(dt.Year, dt.Month, 1); } + /// + /// Returns a representing the last day of the month for the given date. + /// + /// The source date. + /// A set to the last day of the month. public static DateTime LastDayOfMonth(this DateTime dt) { return new DateTime(dt.Year, dt.Month, DateTime.DaysInMonth(dt.Year, dt.Month)); } + /// + /// Converts a to a human-readable relative time string (e.g., "5 minutes ago"). + /// For future dates, delegates to . + /// + /// The to humanize. + /// A human-readable string representing the relative time from now. public static string Humanize(this DateTime target) { @@ -55,6 +87,12 @@ public static string Humanize(this DateTime target) return sb.ToString(); } + /// + /// Converts a to a human-readable relative date group string + /// (e.g., "Today", "Yesterday", "Next week", "Last month"). + /// + /// The to humanize. + /// A descriptive string indicating the relative date grouping. public static string HumanizeDate(this DateTime date) { DateTime dateNow = DateTime.Now; diff --git a/Src/RCommon.Core/Extensions/DictionaryExtensions.cs b/Src/RCommon.Core/Extensions/DictionaryExtensions.cs index 44c9c171..9e0c2dc1 100644 --- a/Src/RCommon.Core/Extensions/DictionaryExtensions.cs +++ b/Src/RCommon.Core/Extensions/DictionaryExtensions.cs @@ -7,6 +7,9 @@ namespace RCommon { + /// + /// Provides extension methods for dictionary types including safe value retrieval and get-or-add semantics. + /// public static class DictionaryExtensions { @@ -18,12 +21,11 @@ public static class DictionaryExtensions /// Key /// Value of the key (or default value if key not exists) /// True if key does exists in the dictionary - internal static bool TryGetValue(this IDictionary dictionary, string key, out T value) + internal static bool TryGetValue(this IDictionary dictionary, string key, out T? value) { - object valueObj; - if (dictionary.TryGetValue(key, out valueObj) && valueObj is T) + if (dictionary.TryGetValue(key, out object? valueObj) && valueObj is T typedValue) { - value = (T)valueObj; + value = typedValue; return true; } @@ -39,10 +41,10 @@ internal static bool TryGetValue(this IDictionary dictionary, /// Type of the key /// Type of the value /// Value if found, default if can not found. - public static TValue GetOrDefault(this Dictionary dictionary, TKey key) + public static TValue? GetOrDefault(this Dictionary dictionary, TKey key) + where TKey : notnull { - TValue obj; - return dictionary.TryGetValue(key, out obj) ? obj : default; + return dictionary.TryGetValue(key, out TValue? obj) ? obj : default; } /// @@ -53,9 +55,9 @@ public static TValue GetOrDefault(this Dictionary di /// Type of the key /// Type of the value /// Value if found, default if can not found. - public static TValue GetOrDefault(this IDictionary dictionary, TKey key) + public static TValue? GetOrDefault(this IDictionary dictionary, TKey key) { - return dictionary.TryGetValue(key, out var obj) ? obj : default; + return dictionary.TryGetValue(key, out TValue? obj) ? obj : default; } /// @@ -66,9 +68,9 @@ public static TValue GetOrDefault(this IDictionary d /// Type of the key /// Type of the value /// Value if found, default if can not found. - public static TValue GetOrDefault(this IReadOnlyDictionary dictionary, TKey key) + public static TValue? GetOrDefault(this IReadOnlyDictionary dictionary, TKey key) { - return dictionary.TryGetValue(key, out var obj) ? obj : default; + return dictionary.TryGetValue(key, out TValue? obj) ? obj : default; } /// @@ -79,9 +81,10 @@ public static TValue GetOrDefault(this IReadOnlyDictionaryType of the key /// Type of the value /// Value if found, default if can not found. - public static TValue GetOrDefault(this ConcurrentDictionary dictionary, TKey key) + public static TValue? GetOrDefault(this ConcurrentDictionary dictionary, TKey key) + where TKey : notnull { - return dictionary.TryGetValue(key, out var obj) ? obj : default; + return dictionary.TryGetValue(key, out TValue? obj) ? obj : default; } /// @@ -95,10 +98,9 @@ public static TValue GetOrDefault(this ConcurrentDictionaryValue if found, default if can not found. public static TValue GetOrAdd(this IDictionary dictionary, TKey key, Func factory) { - TValue obj; - if (dictionary.TryGetValue(key, out obj)) + if (dictionary.TryGetValue(key, out TValue? obj)) { - return obj; + return obj!; } return dictionary[key] = factory(key); @@ -128,6 +130,7 @@ public static TValue GetOrAdd(this IDictionary dicti /// Type of the value /// Value if found, default if can not found. public static TValue GetOrAdd(this ConcurrentDictionary dictionary, TKey key, Func factory) + where TKey : notnull { return dictionary.GetOrAdd(key, k => factory()); } diff --git a/Src/RCommon.Core/Extensions/ExpressionExtensions.cs b/Src/RCommon.Core/Extensions/ExpressionExtensions.cs index 40370c29..d91f9d66 100644 --- a/Src/RCommon.Core/Extensions/ExpressionExtensions.cs +++ b/Src/RCommon.Core/Extensions/ExpressionExtensions.cs @@ -6,30 +6,87 @@ namespace RCommon { + /// + /// Provides extension methods to compile and invoke trees in a single call. + /// + /// + /// Each invocation compiles the expression tree, which can be expensive. Consider caching the compiled + /// delegate if performance is critical. + /// public static class ExpressionExtensions { + /// + /// Compiles and invokes a parameterless expression. + /// + /// The return type of the expression. + /// The expression to compile and invoke. + /// The result of invoking the compiled expression. public static TResult Invoke(this Expression> expr) { return expr.Compile().Invoke(); } + /// + /// Compiles and invokes an expression with one argument. + /// + /// The type of the first argument. + /// The return type of the expression. + /// The expression to compile and invoke. + /// The first argument. + /// The result of invoking the compiled expression. public static TResult Invoke(this Expression> expr, T1 arg1) { return expr.Compile().Invoke(arg1); } + /// + /// Compiles and invokes an expression with two arguments. + /// + /// The type of the first argument. + /// The type of the second argument. + /// The return type of the expression. + /// The expression to compile and invoke. + /// The first argument. + /// The second argument. + /// The result of invoking the compiled expression. public static TResult Invoke(this Expression> expr, T1 arg1, T2 arg2) { return expr.Compile().Invoke(arg1, arg2); } + /// + /// Compiles and invokes an expression with three arguments. + /// + /// The type of the first argument. + /// The type of the second argument. + /// The type of the third argument. + /// The return type of the expression. + /// The expression to compile and invoke. + /// The first argument. + /// The second argument. + /// The third argument. + /// The result of invoking the compiled expression. public static TResult Invoke( this Expression> expr, T1 arg1, T2 arg2, T3 arg3) { return expr.Compile().Invoke(arg1, arg2, arg3); } + /// + /// Compiles and invokes an expression with four arguments. + /// + /// The type of the first argument. + /// The type of the second argument. + /// The type of the third argument. + /// The type of the fourth argument. + /// The return type of the expression. + /// The expression to compile and invoke. + /// The first argument. + /// The second argument. + /// The third argument. + /// The fourth argument. + /// The result of invoking the compiled expression. public static TResult Invoke( this Expression> expr, T1 arg1, T2 arg2, T3 arg3, T4 arg4) { diff --git a/Src/RCommon.Core/Extensions/GuidExtensions.cs b/Src/RCommon.Core/Extensions/GuidExtensions.cs index 26970c78..522ecd7a 100644 --- a/Src/RCommon.Core/Extensions/GuidExtensions.cs +++ b/Src/RCommon.Core/Extensions/GuidExtensions.cs @@ -6,9 +6,17 @@ namespace RCommon { + /// + /// Provides extension methods for operations. + /// public static class GuidExtension { + /// + /// Determines whether the is equal to . + /// + /// The to check. + /// true if the GUID is empty; otherwise, false. [DebuggerStepThrough] public static bool IsEmpty(this Guid target) { diff --git a/Src/RCommon.Core/Extensions/IDataReaderExtensions.cs b/Src/RCommon.Core/Extensions/IDataReaderExtensions.cs index f84b5559..e4c3e87f 100644 --- a/Src/RCommon.Core/Extensions/IDataReaderExtensions.cs +++ b/Src/RCommon.Core/Extensions/IDataReaderExtensions.cs @@ -6,6 +6,10 @@ namespace RCommon { + /// + /// Provides extension methods for including safe value retrieval + /// and conversion to . + /// public static class IDataReaderExtensions { @@ -38,6 +42,13 @@ public static object GetValue(this IDataReader dr, int index, object defaultValu return rv; } + /// + /// Acquires the value from a row of an based on the column name. + /// + /// A populated . + /// The name of the column to retrieve the value from. + /// The default value to return if the column is not found or is . + /// The value of the column, or if not found or null. public static object GetValue(this IDataReader dr, string columnName, object defaultValue) { object rv = defaultValue; @@ -68,24 +79,27 @@ public static object GetValue(this IDataReader dr, string columnName, object def /// This method does not close the IDataReader. You will have to. public static DataTable ToDataTable(this IDataReader dr) { - DataTable dtSchema = dr.GetSchemaTable(); + DataTable? dtSchema = dr.GetSchemaTable(); DataTable dtData = new DataTable(); DataColumn dc; DataRow row; System.Collections.ArrayList al = new System.Collections.ArrayList(); + if (dtSchema == null) return dtData; + // Populate the Column Information for (int i = 0; i < dtSchema.Rows.Count; i++) { dc = new DataColumn(); - if (!dtData.Columns.Contains(dtSchema.Rows[i]["ColumnName"].ToString())) + var columnName = dtSchema.Rows[i]["ColumnName"]?.ToString(); + if (columnName != null && !dtData.Columns.Contains(columnName)) { - dc.ColumnName = dtSchema.Rows[i]["ColumnName"].ToString(); + dc.ColumnName = columnName; dc.Unique = Convert.ToBoolean(dtSchema.Rows[i]["IsUnique"]); dc.AllowDBNull = Convert.ToBoolean(dtSchema.Rows[i]["AllowDBNull"]); dc.ReadOnly = Convert.ToBoolean(dtSchema.Rows[i]["IsReadOnly"]); - dc.DataType = (Type)dtSchema.Rows[i]["DataType"]; + dc.DataType = (Type?)dtSchema.Rows[i]["DataType"] ?? typeof(object); al.Add(dc.ColumnName); dtData.Columns.Add(dc); @@ -99,7 +113,7 @@ public static DataTable ToDataTable(this IDataReader dr) for (int i = 0; i < al.Count; i++) { - row[((System.String)al[i])] = dr[(System.String)al[i]]; + row[((string)al[i]!)] = dr[(string)al[i]!]; } dtData.Rows.Add(row); @@ -119,20 +133,23 @@ public static DataTable ToDataTable(this IDataReader dr, bool destroyReader) { try { - DataTable dtSchema = dr.GetSchemaTable(); + DataTable? dtSchema = dr.GetSchemaTable(); DataTable dtData = new DataTable(); DataColumn dc; DataRow row; System.Collections.ArrayList al = new System.Collections.ArrayList(); + if (dtSchema == null) return dtData; + // Populate the Column Information for (int i = 0; i < dtSchema.Rows.Count; i++) { dc = new DataColumn(); - if (!dtData.Columns.Contains(dtSchema.Rows[i]["ColumnName"].ToString())) + var columnName2 = dtSchema.Rows[i]["ColumnName"]?.ToString(); + if (columnName2 != null && !dtData.Columns.Contains(columnName2)) { - dc.ColumnName = dtSchema.Rows[i]["ColumnName"].ToString(); + dc.ColumnName = columnName2; dc.Unique = Convert.ToBoolean(dtSchema.Rows[i]["IsUnique"]); dc.AllowDBNull = Convert.ToBoolean(dtSchema.Rows[i]["AllowDBNull"]); dc.ReadOnly = Convert.ToBoolean(dtSchema.Rows[i]["IsReadOnly"]); @@ -148,7 +165,7 @@ public static DataTable ToDataTable(this IDataReader dr, bool destroyReader) for (int i = 0; i < al.Count; i++) { - row[((System.String)al[i])] = dr[(System.String)al[i]]; + row[((string)al[i]!)] = dr[(string)al[i]!]; } dtData.Rows.Add(row); diff --git a/Src/RCommon.Core/Extensions/ILoggerExtensions.cs b/Src/RCommon.Core/Extensions/ILoggerExtensions.cs index 519bb0ae..cf52b288 100644 --- a/Src/RCommon.Core/Extensions/ILoggerExtensions.cs +++ b/Src/RCommon.Core/Extensions/ILoggerExtensions.cs @@ -6,12 +6,22 @@ namespace Microsoft.Extensions.Logging { + /// + /// Provides extension methods for that add optional console output alongside standard logging. + /// public static class ILoggerExtensions { + /// + /// Logs an informational message with optional console output. + /// + /// The logger instance. + /// The log message template. + /// The message format parameters. + /// If true, also writes the message to the console. public static void LogInformation(this ILogger logger, string? message, object[]? @params, bool outputToConsole = false) { - logger.LogInformation(message, @params); + logger.LogInformation(message, @params ?? System.Array.Empty()); if (outputToConsole) { @@ -19,9 +29,17 @@ public static void LogInformation(this ILogger logger, string? message, object[] } } + /// + /// Logs an informational message with an event ID and optional console output. + /// + /// The logger instance. + /// The event ID associated with the log entry. + /// The log message template. + /// The message format parameters. + /// If true, also writes the message to the console. public static void LogInformation(this ILogger logger, EventId eventId, string? message, object[]? @params, bool outputToConsole = false) { - logger.LogInformation(eventId, message, @params); + logger.LogInformation(eventId, message, @params ?? System.Array.Empty()); if (outputToConsole) { @@ -29,9 +47,17 @@ public static void LogInformation(this ILogger logger, EventId eventId, string? } } + /// + /// Logs an informational message with an exception and optional console output. + /// + /// The logger instance. + /// The exception associated with the log entry. + /// The log message template. + /// The message format parameters. + /// If true, also writes the message to the console. public static void LogInformation(this ILogger logger, Exception exception, string? message, object[]? @params, bool outputToConsole = false) { - logger.LogInformation(exception, message, @params); + logger.LogInformation(exception, message, @params ?? System.Array.Empty()); if (outputToConsole) { @@ -39,9 +65,18 @@ public static void LogInformation(this ILogger logger, Exception exception, stri } } + /// + /// Logs an informational message with an event ID, exception, and optional console output. + /// + /// The logger instance. + /// The event ID associated with the log entry. + /// The exception associated with the log entry. + /// The log message template. + /// The message format parameters. + /// If true, also writes the message to the console. public static void LogInformation(this ILogger logger, EventId eventId, Exception exception, string? message, object[]? @params, bool outputToConsole = false) { - logger.LogInformation(eventId, exception, message, @params); + logger.LogInformation(eventId, exception, message, @params ?? System.Array.Empty()); if (outputToConsole) { @@ -49,9 +84,14 @@ public static void LogInformation(this ILogger logger, EventId eventId, Exceptio } } + /// + /// Writes a formatted message to the console using . + /// + /// The message template. + /// The format parameters. private static void OutputToConsole(string? message, object[]? @params) { - System.Console.WriteLine(message, @params); + System.Console.WriteLine(message ?? string.Empty, @params ?? System.Array.Empty()); } } } diff --git a/Src/RCommon.Core/Extensions/IQueryableExtensions.cs b/Src/RCommon.Core/Extensions/IQueryableExtensions.cs index 280d1a86..de5a27c3 100644 --- a/Src/RCommon.Core/Extensions/IQueryableExtensions.cs +++ b/Src/RCommon.Core/Extensions/IQueryableExtensions.cs @@ -10,19 +10,38 @@ namespace RCommon { + /// + /// Provides extension methods for including dynamic ordering, + /// conditional filtering, LIKE-style queries, and pagination. + /// public static class IQueryableExtensions { + /// + /// Dynamically orders an by a property name specified as a string. + /// + /// The entity type. + /// The queryable source. + /// The name of the property to order by. + /// If true, orders descending; otherwise, ascending. + /// An ordered . + /// + /// Builds an expression tree at runtime to call OrderBy or OrderByDescending + /// on the query provider using the specified property name. + /// public static IQueryable OrderBy(this IQueryable source, string orderByProperty, bool desc) where TEntity : class { - + // Determine which Queryable method to call based on sort direction string command = desc ? "OrderByDescending" : "OrderBy"; var type = typeof(TEntity); - var property = type.GetProperty(orderByProperty); + var property = type.GetProperty(orderByProperty) + ?? throw new ArgumentException($"Property '{orderByProperty}' not found on type '{type.FullName}'.", nameof(orderByProperty)); var parameter = Expression.Parameter(type, "p"); + // Build a member access expression for the property (e.g., p.PropertyName) var propertyAccess = Expression.MakeMemberAccess(parameter, property); var orderByExpression = Expression.Lambda(propertyAccess, parameter); + // Create a method call expression to Queryable.OrderBy/OrderByDescending var resultExpression = Expression.Call(typeof(Queryable), command, new Type[] { type, property.PropertyType }, source.Expression, Expression.Quote(orderByExpression)); return source.Provider.CreateQuery(resultExpression); @@ -31,6 +50,15 @@ public static IQueryable OrderBy(this IQueryable sour + /// + /// Conditionally filters an based on a boolean condition. + /// If the condition is false, the source is returned unfiltered. + /// + /// The type of elements in the source. + /// The queryable source to filter. + /// If true, the predicate is applied; otherwise, the source is returned as-is. + /// The filter expression to apply when is true. + /// A filtered or unfiltered queryable depending on the condition. public static IQueryable WhereIf(this IQueryable source, bool condition, Expression> predicate) { if (condition) @@ -39,6 +67,15 @@ public static IQueryable WhereIf(this IQueryable sour return source; } + /// + /// Conditionally filters an based on a boolean condition, using an index-aware predicate. + /// If the condition is false, the source is returned unfiltered. + /// + /// The type of elements in the source. + /// The queryable source to filter. + /// If true, the predicate is applied; otherwise, the source is returned as-is. + /// The index-aware filter expression to apply when is true. + /// A filtered or unfiltered queryable depending on the condition. public static IQueryable WhereIf(this IQueryable source, bool condition, Expression> predicate) { if (condition) @@ -47,11 +84,29 @@ public static IQueryable WhereIf(this IQueryable sour return source; } + /// + /// Filters an using SQL LIKE-style matching with the % wildcard character. + /// + /// The type of elements in the source. + /// The queryable source to filter. + /// An expression selecting the string property to match against. + /// The pattern to match, using % as the wildcard character. + /// A filtered . public static IQueryable WhereLike(this IQueryable source, Expression> valueSelector, string value) { return source.Where(BuildLikeExpression(valueSelector, value, '%')); } + /// + /// Builds an expression tree that mimics SQL LIKE behavior by mapping wildcard positions + /// to , , or . + /// + /// The type of element in the source. + /// An expression selecting the string property to match against. + /// The pattern to match, with wildcards. + /// The wildcard character (typically %). + /// A predicate expression representing the LIKE comparison. + /// Thrown when is null. public static Expression> BuildLikeExpression(Expression> valueSelector, string value, char wildcard) { if (valueSelector == null) @@ -66,6 +121,13 @@ public static Expression> BuildLikeExpression(Exp return Expression.Lambda>(body, parameter); } + /// + /// Determines the appropriate method (Contains, StartsWith, or EndsWith) + /// based on the position of wildcard characters in the value. + /// + /// The pattern string containing wildcards. + /// The wildcard character to detect. + /// The for the chosen string comparison method. private static MethodInfo GetLikeMethod(string value, char wildcard) { var methodName = "Contains"; @@ -87,9 +149,19 @@ private static MethodInfo GetLikeMethod(string value, char wildcard) } var stringType = typeof(string); - return stringType.GetMethod(methodName, new Type[] { stringType }); + return stringType.GetMethod(methodName, new Type[] { stringType }) + ?? throw new InvalidOperationException($"Method '{methodName}' not found on type 'System.String'."); } + /// + /// Converts an to an with the specified page number and size. + /// + /// The type of elements in the queryable. + /// The queryable source to paginate. + /// The 1-based page number. Defaults to 1. + /// The number of items per page. Defaults to 10. Must be greater than zero. + /// A paginated list containing the items for the requested page. + /// public static IPaginatedList ToPaginatedList(this IQueryable source, int pageNumber = 1, int pageSize = 10) { Guard.IsNotNegativeOrZero(pageSize, "pageSize"); diff --git a/Src/RCommon.Core/Extensions/ObjectExtensions.cs b/Src/RCommon.Core/Extensions/ObjectExtensions.cs index 0758339d..e4d32409 100644 --- a/Src/RCommon.Core/Extensions/ObjectExtensions.cs +++ b/Src/RCommon.Core/Extensions/ObjectExtensions.cs @@ -9,6 +9,10 @@ namespace RCommon { + /// + /// Provides general-purpose extension methods for including casting, + /// type conversion, reflection-based property access, conditional execution, and object graph traversal. + /// public static class ObjectExtensions { /// @@ -24,8 +28,8 @@ public static bool BinaryEquals(this object binaryValue1, object binaryValue2) return true; } - byte[] array1 = binaryValue1 as byte[]; - byte[] array2 = binaryValue2 as byte[]; + byte[]? array1 = binaryValue1 as byte[]; + byte[]? array2 = binaryValue2 as byte[]; if (array1 != null && array2 != null) { @@ -55,14 +59,14 @@ public static bool BinaryEquals(this object binaryValue1, object binaryValue2) /// The source object from which the property is to be fetched /// The name of the property /// - public static T GetPropertyValueWithReflection(this object sourceObject, + public static T? GetPropertyValueWithReflection(this object sourceObject, string propertyName) { Guard.Against(sourceObject == null, "sourceObject cannot be null"); Guard.Against(string.IsNullOrEmpty(propertyName), "propertyName, cannot be null or empty"); BindingFlags eFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; - PropertyInfo propertyInfo = sourceObject.GetType().GetProperty(propertyName, eFlags); + PropertyInfo? propertyInfo = sourceObject!.GetType().GetProperty(propertyName, eFlags); if (propertyInfo == null) { @@ -70,7 +74,12 @@ public static T GetPropertyValueWithReflection(this object sourceObject, sourceObject.GetType().Name); } - return (T)propertyInfo.GetValue(sourceObject, null); + object? value = propertyInfo.GetValue(sourceObject, null); + if (value == null) + { + return default; + } + return (T)value; } /// @@ -81,13 +90,17 @@ public static T GetPropertyValueWithReflection(this object sourceObject, /// the name of the property /// the value of the property public static void SetPropertyValueWithReflection(this object anObject, - string propertyName, object propertyValue) + string propertyName, object? propertyValue) { Guard.Against(anObject == null, "anObject cannot be null"); Guard.Against(string.IsNullOrEmpty(propertyName), "propertyName, cannot be null or empty"); BindingFlags eFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; - PropertyInfo propertyInfo = anObject.GetType().GetProperty(propertyName, eFlags); + PropertyInfo? propertyInfo = anObject!.GetType().GetProperty(propertyName, eFlags); + + if (propertyInfo == null) + throw new ApplicationException("Cannot find property [" + propertyName + "] in object type: " + + anObject.GetType().FullName); Type dataType = propertyInfo.PropertyType; if (!(dataType.IsGenericType && (dataType.GetGenericTypeDefinition() == typeof(Nullable<>)))) @@ -97,9 +110,6 @@ public static void SetPropertyValueWithReflection(this object anObject, throw new ArgumentNullException("propertyValue"); } } - if (propertyInfo == null) - throw new ApplicationException("Cannot find property [" + propertyName + "] in object type: " + - anObject.GetType().FullName); propertyInfo.SetValue(anObject, propertyValue, null); } @@ -110,7 +120,7 @@ public static void SetPropertyValueWithReflection(this object anObject, ///The user supplied key, if any. ///The type for which the key is built. ///string. - public static string BuildFullKey(this object userKey) + public static string? BuildFullKey(this object userKey) { if (userKey == null) return typeof(T).FullName; @@ -140,7 +150,7 @@ public static T To(this object obj) { if (typeof(T) == typeof(Guid)) { - return (T)TypeDescriptor.GetConverter(typeof(T)).ConvertFromInvariantString(obj.ToString()); + return (T)TypeDescriptor.GetConverter(typeof(T)).ConvertFromInvariantString(obj.ToString()!)!; } return (T)Convert.ChangeType(obj, typeof(T), CultureInfo.InvariantCulture); @@ -213,6 +223,12 @@ public static T If(this T obj, bool condition, Action action) return obj; } + /// + /// Gets a human-readable generic type name for the object (e.g., "List<String>" instead of "List`1"). + /// + /// The object whose type name is retrieved. + /// A formatted generic type name string. + /// public static string GetGenericTypeName(this object @object) { return @object.GetType().GetGenericTypeName(); diff --git a/Src/RCommon.Core/Extensions/ServiceCollectionExtensions.cs b/Src/RCommon.Core/Extensions/ServiceCollectionExtensions.cs index 286b359b..7eea61d0 100644 --- a/Src/RCommon.Core/Extensions/ServiceCollectionExtensions.cs +++ b/Src/RCommon.Core/Extensions/ServiceCollectionExtensions.cs @@ -10,6 +10,10 @@ namespace RCommon { + /// + /// Provides extension methods for to bootstrap RCommon services, + /// register hosted services, and provide diagnostic output of service registrations. + /// public static class ServiceCollectionExtensions { /// @@ -24,6 +28,11 @@ public static IRCommonBuilder AddRCommon(this IServiceCollection services) return config; } + /// + /// Registers as an singleton if the type implements the interface. + /// + /// The service type to check and potentially register as a hosted service. + /// The service collection to register with. public static void AddHostedServiceIfSupported(this IServiceCollection services) where T : class { @@ -33,17 +42,28 @@ public static void AddHostedServiceIfSupported(this IServiceCollection servic } } + /// + /// Creates a snapshot collection of all instances currently registered in the service collection. + /// + /// The service collection to extract descriptors from. + /// A collection of instances. public static ICollection GenerateServiceDescriptors(this IServiceCollection services) { ICollection returnItems = new List(services); return returnItems; } + /// + /// Generates a formatted string listing all service descriptors in the service collection, ordered by service type name. + /// Useful for diagnostics and debugging IoC registrations. + /// + /// The service collection to describe. + /// A multi-line string describing each service descriptor. public static string GenerateServiceDescriptorsString(this IServiceCollection services) { StringBuilder sb = new StringBuilder(); IEnumerable sds = GenerateServiceDescriptors(services).AsEnumerable() - .OrderBy(o => o.ServiceType.FullName); + .OrderBy(o => o.ServiceType.FullName ?? string.Empty); foreach (ServiceDescriptor sd in sds) { sb.Append($"(ServiceDescriptor):"); @@ -57,6 +77,12 @@ public static string GenerateServiceDescriptorsString(this IServiceCollection se return returnValue; } + /// + /// Generates a formatted string listing duplicate service registrations in the service collection. + /// Identifies registrations where the same service type, lifetime, and implementation type appear more than once. + /// + /// The service collection to check for duplicates. + /// A multi-line string describing each duplicate registration, or empty if none found. public static string GeneratePossibleDuplicatesServiceDescriptorsString(this IServiceCollection services) { StringBuilder sb = new StringBuilder(); @@ -96,10 +122,17 @@ where grp.Count() > 1 return returnValue; } + /// + /// Logs all service descriptors and any duplicate registrations to both the logger and the console. + /// Duplicates are logged at warning level. + /// + /// The type used to create the logger category. + /// The service collection to log. + /// The logger factory used to create the logger. public static void LogServiceDescriptors(this IServiceCollection services, ILoggerFactory loggerFactory) { string iocDebugging = services.GenerateServiceDescriptorsString(); - Func logMsgStringFunc = (a, b) => iocDebugging; + Func logMsgStringFunc = (a, b) => iocDebugging; ILogger logger = loggerFactory.CreateLogger(); logger.Log( LogLevel.Information, @@ -112,7 +145,7 @@ public static void LogServiceDescriptors(this IServiceCollection services, IL string iocPossibleDuplicates = GeneratePossibleDuplicatesServiceDescriptorsString(services); if (!string.IsNullOrWhiteSpace(iocPossibleDuplicates)) { - Func logMsgStringDuplicatesFunc = (a, b) => iocPossibleDuplicates; + Func logMsgStringDuplicatesFunc = (a, b) => iocPossibleDuplicates; logger.Log( LogLevel.Warning, ushort.MaxValue, @@ -123,15 +156,22 @@ public static void LogServiceDescriptors(this IServiceCollection services, IL } } + /// + /// Internal data holder for tracking duplicate IoC registrations during diagnostic analysis. + /// [DebuggerDisplay("ServiceTypeFullName='{ServiceTypeFullName}', Lifetime='{Lifetime}', ImplementationTypeFullName='{ImplementationTypeFullName}', DuplicateCount='{DuplicateCount}'")] private sealed class DuplicateIocRegistrationHolder { - public string ServiceTypeFullName { get; set; } + /// Gets or sets the full name of the service type. + public string? ServiceTypeFullName { get; set; } + /// Gets or sets the service lifetime. public ServiceLifetime Lifetime { get; set; } - public string ImplementationTypeFullName { get; set; } + /// Gets or sets the full name of the implementation type. + public string? ImplementationTypeFullName { get; set; } + /// Gets or sets the number of duplicate registrations found. public int DuplicateCount { get; set; } } } diff --git a/Src/RCommon.Core/Extensions/StreamExtensions.cs b/Src/RCommon.Core/Extensions/StreamExtensions.cs index 5ab7369d..61284741 100644 --- a/Src/RCommon.Core/Extensions/StreamExtensions.cs +++ b/Src/RCommon.Core/Extensions/StreamExtensions.cs @@ -8,9 +8,18 @@ namespace RCommon { + /// + /// Provides extension methods for including reading all bytes and async copy operations. + /// public static class StreamExtensions { + /// + /// Reads all bytes from the stream into a byte array. + /// Resets the stream position to the beginning if the stream supports seeking. + /// + /// The stream to read from. + /// A byte array containing the entire contents of the stream. public static byte[] GetAllBytes(this Stream stream) { using (var memoryStream = new MemoryStream()) @@ -24,6 +33,13 @@ public static byte[] GetAllBytes(this Stream stream) } } + /// + /// Asynchronously reads all bytes from the stream into a byte array. + /// Resets the stream position to the beginning if the stream supports seeking. + /// + /// The stream to read from. + /// A token to monitor for cancellation requests. + /// A task that resolves to a byte array containing the entire contents of the stream. public static async Task GetAllBytesAsync(this Stream stream, CancellationToken cancellationToken = default) { using (var memoryStream = new MemoryStream()) @@ -37,6 +53,14 @@ public static async Task GetAllBytesAsync(this Stream stream, Cancellati } } + /// + /// Asynchronously copies the stream to a destination stream with cancellation support. + /// Resets the stream position to the beginning if the stream supports seeking. + /// + /// The source stream to copy from. + /// The destination stream to copy to. + /// A token to monitor for cancellation requests. + /// A task representing the asynchronous copy operation. public static Task CopyToAsync(this Stream stream, Stream destination, CancellationToken cancellationToken) { if (stream.CanSeek) diff --git a/Src/RCommon.Core/Extensions/StringExtensions.cs b/Src/RCommon.Core/Extensions/StringExtensions.cs index 36c6f95d..21cb6d58 100644 --- a/Src/RCommon.Core/Extensions/StringExtensions.cs +++ b/Src/RCommon.Core/Extensions/StringExtensions.cs @@ -11,6 +11,10 @@ namespace RCommon { + /// + /// Provides extension methods for operations including validation, formatting, + /// encoding, hashing, case conversion, and various string manipulation utilities. + /// public static class StringExtension { private static readonly Regex WebUrlExpression = new Regex(@"(http|https)://([\w-]+\.)+[\w-]+(/[\w- ./?%&=]*)?", RegexOptions.Singleline | RegexOptions.Compiled); @@ -19,24 +23,45 @@ public static class StringExtension private static readonly char[] IllegalUrlCharacters = new[] { ';', '/', '\\', '?', ':', '@', '&', '=', '+', '$', ',', '<', '>', '#', '%', '.', '!', '*', '\'', '"', '(', ')', '[', ']', '{', '}', '|', '^', '`', '~', '–', '‘', '’', '“', '”', '»', '«' }; + /// + /// Determines whether the string is a valid HTTP or HTTPS web URL. + /// + /// The string to test. + /// true if the string is a valid web URL; otherwise, false. [DebuggerStepThrough] public static bool IsWebUrl(this string target) { return !string.IsNullOrEmpty(target) && WebUrlExpression.IsMatch(target); } + /// + /// Determines whether the string is a valid email address. + /// + /// The string to test. + /// true if the string is a valid email address; otherwise, false. [DebuggerStepThrough] public static bool IsEmail(this string target) { return !string.IsNullOrEmpty(target) && EmailExpression.IsMatch(target); } + /// + /// Returns the string trimmed, or if the string is null. + /// + /// The string to make null-safe. + /// The trimmed string, or empty string if null. [DebuggerStepThrough] - public static string NullSafe(this string target) + public static string NullSafe(this string? target) { return (target ?? string.Empty).Trim(); } + /// + /// Formats the string using the current culture with the specified arguments. + /// + /// The format string template. + /// The arguments to substitute into the format string. + /// The formatted string. [DebuggerStepThrough] public static string FormatWith(this string target, params object[] args) { @@ -45,6 +70,11 @@ public static string FormatWith(this string target, params object[] args) return string.Format(Constants.CurrentCulture, target, args); } + /// + /// Computes an MD5 hash of the string and returns it as a Base64-encoded string. + /// + /// The string to hash. + /// A Base64-encoded MD5 hash of the string. [DebuggerStepThrough] public static string Hash(this string target) { @@ -59,6 +89,12 @@ public static string Hash(this string target) } } + /// + /// Truncates the string at the specified index and appends an ellipsis ("...") if the string exceeds that length. + /// + /// The string to truncate. + /// The maximum length including the ellipsis. + /// The original string if shorter than , or the truncated string with ellipsis. [DebuggerStepThrough] public static string WrapAt(this string target, int index) { @@ -70,12 +106,22 @@ public static string WrapAt(this string target, int index) return (target.Length <= index) ? target : string.Concat(target.Substring(0, index - DotCount), new string('.', DotCount)); } + /// + /// Removes all HTML tags from the string. + /// + /// The string to strip HTML from. + /// The string with all HTML tags removed. [DebuggerStepThrough] public static string StripHtml(this string target) { return StripHTMLExpression.Replace(target, string.Empty); } + /// + /// Converts a 22-character URL-safe Base64-encoded string back to a . + /// + /// The 22-character Base64-encoded GUID string (with - and _ as safe characters). + /// The decoded , or if the input is invalid. [DebuggerStepThrough] public static Guid ToGuid(this string target) { @@ -99,6 +145,13 @@ public static Guid ToGuid(this string target) return result; } + /// + /// Converts the string to an enum value, returning a default value if conversion fails. + /// + /// The enum type. + /// The string to convert. + /// The default value to return if parsing fails. + /// The parsed enum value, or if parsing fails. [DebuggerStepThrough] public static T ToEnum(this string target, T defaultValue) where T : IComparable, IFormattable { @@ -118,6 +171,11 @@ public static T ToEnum(this string target, T defaultValue) where T : ICompara return convertedValue; } + /// + /// Removes illegal URL characters from the string and replaces spaces with hyphens to produce a URL-safe string. + /// + /// The string to convert. + /// A URL-safe string, or the original string if it is null or empty. [DebuggerStepThrough] public static string ToLegalUrl(this string target) { @@ -146,36 +204,68 @@ public static string ToLegalUrl(this string target) return target; } + /// + /// URL-encodes the string using . + /// + /// The string to encode. + /// The URL-encoded string. [DebuggerStepThrough] - public static string UrlEncode(this string target) + public static string? UrlEncode(this string target) { return HttpUtility.UrlEncode(target); } + /// + /// URL-decodes the string using . + /// + /// The string to decode. + /// The URL-decoded string. [DebuggerStepThrough] - public static string UrlDecode(this string target) + public static string? UrlDecode(this string target) { return HttpUtility.UrlDecode(target); } + /// + /// HTML-attribute-encodes the string using . + /// + /// The string to encode. + /// The HTML-attribute-encoded string. [DebuggerStepThrough] public static string AttributeEncode(this string target) { return HttpUtility.HtmlAttributeEncode(target); } + /// + /// HTML-encodes the string using . + /// + /// The string to encode. + /// The HTML-encoded string. [DebuggerStepThrough] public static string HtmlEncode(this string target) { return HttpUtility.HtmlEncode(target); } + /// + /// HTML-decodes the string using . + /// + /// The string to decode. + /// The HTML-decoded string. [DebuggerStepThrough] public static string HtmlDecode(this string target) { return HttpUtility.HtmlDecode(target); } + /// + /// Replaces all occurrences of each string in with . + /// + /// The string to perform replacements on. + /// The collection of strings to replace. + /// The replacement string. + /// The modified string with all replacements applied. public static string Replace(this string target, ICollection oldValues, string newValue) { oldValues.ForEach(oldValue => target = target.Replace(oldValue, newValue)); @@ -187,7 +277,7 @@ public static string Replace(this string target, ICollection oldValues, /// /// the input string to be converted /// The converted string - public static string ToNullIfEmptyOrBlank(this string inputString) + public static string? ToNullIfEmptyOrBlank(this string? inputString) { if (inputString == null || (inputString = inputString.Trim()).Length == 0) return null; @@ -199,7 +289,7 @@ public static string ToNullIfEmptyOrBlank(this string inputString) /// /// /// - public static bool IsNullOrEmptyOrBlank(this string inputString) + public static bool IsNullOrEmptyOrBlank(this string? inputString) { return (inputString == null || inputString.Trim().Length == 0); } @@ -411,11 +501,24 @@ public static string Limit(this string str, int characterCount) else return str.Substring(0, characterCount).TrimEnd(' '); } + /// + /// Compares two strings for equality, ignoring case by default. + /// + /// The first string. + /// The string to compare against. + /// true if the strings are equal (case-insensitive); otherwise, false. public static bool IsTheSameAs(this string target, string stringToCompare) { return IsTheSameAs(target, stringToCompare, true); } + /// + /// Compares two strings for equality with optional case sensitivity. + /// + /// The first string. + /// The string to compare against. + /// If true, performs a case-insensitive comparison. + /// true if the strings are equal; otherwise, false. public static bool IsTheSameAs(this string target, string stringToCompare, bool ignoreCase) { return string.Compare(target, stringToCompare, ignoreCase) == 0; @@ -441,11 +544,23 @@ public static string ToSlug(this string name) return sb.ToString().Trim('-'); } + /// + /// Returns the plural form of the string using the . + /// + /// The word to pluralize. + /// The pluralized form of the word. + /// public static string Pluralize(this string target) { return Inflector.Pluralize(target); } + /// + /// Conditionally pluralizes the string based on the result of a boolean expression. + /// + /// The word to conditionally pluralize. + /// An expression that returns true if the word should be pluralized. + /// The pluralized form if the expression evaluates to true; otherwise, the original string. public static string PluralizeIf(this string target, Expression> expression) { if (expression.Invoke()) @@ -491,7 +606,7 @@ public static string EnsureStartsWith(this string str, char c, StringComparison /// /// Indicates whether this string is null or an System.String.Empty string. /// - public static bool IsNullOrEmpty(this string str) + public static bool IsNullOrEmpty(this string? str) { return string.IsNullOrEmpty(str); } @@ -499,7 +614,7 @@ public static bool IsNullOrEmpty(this string str) /// /// indicates whether this string is null, empty, or consists only of white-space characters. /// - public static bool IsNullOrWhiteSpace(this string str) + public static bool IsNullOrWhiteSpace(this string? str) { return string.IsNullOrWhiteSpace(str); } @@ -638,6 +753,14 @@ public static string RemovePreFix(this string str, StringComparison comparisonTy return str; } + /// + /// Replaces the first occurrence of a search string with a replacement string. + /// + /// The source string. + /// The string to find. + /// The replacement string. + /// The string comparison type to use for finding the search string. + /// The string with the first occurrence replaced, or the original string if not found. public static string ReplaceFirst(this string str, string search, string replace, StringComparison comparisonType = StringComparison.Ordinal) { Guard.IsNotNull(str, nameof(str)); @@ -860,6 +983,11 @@ public static T ToEnum(this string value, bool ignoreCase) return (T)Enum.Parse(typeof(T), value, ignoreCase); } + /// + /// Computes the MD5 hash of the string and returns it as an uppercase hexadecimal string. + /// + /// The string to hash. + /// The MD5 hash as an uppercase hexadecimal string. public static string ToMd5(this string str) { using (var md5 = MD5.Create()) @@ -902,7 +1030,7 @@ public static string ToPascalCase(this string str, bool useCurrentCulture = fals /// Gets a substring of a string from beginning of the string if it exceeds maximum length. /// /// Thrown if is null - public static string Truncate(this string str, int maxLength) + public static string? Truncate(this string? str, int maxLength) { if (str == null) { @@ -921,7 +1049,7 @@ public static string Truncate(this string str, int maxLength) /// Gets a substring of a string from Ending of the string if it exceeds maximum length. /// /// Thrown if is null - public static string TruncateFromBeginning(this string str, int maxLength) + public static string? TruncateFromBeginning(this string? str, int maxLength) { if (str == null) { @@ -942,7 +1070,7 @@ public static string TruncateFromBeginning(this string str, int maxLength) /// Returning string can not be longer than maxLength. /// /// Thrown if is null - public static string TruncateWithPostfix(this string str, int maxLength) + public static string? TruncateWithPostfix(this string? str, int maxLength) { return TruncateWithPostfix(str, maxLength, "..."); } @@ -953,7 +1081,7 @@ public static string TruncateWithPostfix(this string str, int maxLength) /// Returning string can not be longer than maxLength. /// /// Thrown if is null - public static string TruncateWithPostfix(this string str, int maxLength, string postfix) + public static string? TruncateWithPostfix(this string? str, int maxLength, string postfix) { if (str == null) { @@ -997,6 +1125,12 @@ public static byte[] GetBytes(this string str, Encoding encoding) return encoding.GetBytes(str); } + /// + /// Determines whether all letter characters in the input string are uppercase. + /// Non-letter characters are ignored. + /// + /// The string to check. + /// true if all letter characters are uppercase; otherwise, false. private static bool IsAllUpperCase(string input) { for (int i = 0; i < input.Length; i++) diff --git a/Src/RCommon.Core/Extensions/TypeExtensions.cs b/Src/RCommon.Core/Extensions/TypeExtensions.cs index 69ce9a11..3afb9dd5 100644 --- a/Src/RCommon.Core/Extensions/TypeExtensions.cs +++ b/Src/RCommon.Core/Extensions/TypeExtensions.cs @@ -32,8 +32,18 @@ namespace RCommon { + /// + /// Provides extension methods for including human-readable type name formatting, + /// cache key generation, constructor inspection, and assignability checks. + /// public static class TypeExtensions { + /// + /// Gets a human-readable generic type name (e.g., "List<String>" instead of "List`1"). + /// For non-generic types, returns . + /// + /// The type to get the name for. + /// A formatted type name string. public static string GetGenericTypeName(this Type type) { var typeName = string.Empty; @@ -51,8 +61,17 @@ public static string GetGenericTypeName(this Type type) return typeName; } + /// + /// Thread-safe cache for pretty-printed type names to avoid recomputation. + /// private static readonly ConcurrentDictionary PrettyPrintCache = new ConcurrentDictionary(); + /// + /// Returns a human-readable representation of the type, including nested generic arguments. + /// Results are cached for performance. + /// + /// The type to format. + /// A pretty-printed type name string. public static string PrettyPrint(this Type type) { return PrettyPrintCache.GetOrAdd( @@ -70,7 +89,17 @@ public static string PrettyPrint(this Type type) }); } + /// + /// Thread-safe cache for type cache key strings. + /// private static readonly ConcurrentDictionary TypeCacheKeys = new ConcurrentDictionary(); + + /// + /// Gets a unique cache key string for the type, combining its pretty-printed name with its hash code. + /// Results are cached for performance. + /// + /// The type to generate a cache key for. + /// A cache key string in the format "TypeName[hash: hashCode]". public static string GetCacheKey(this Type type) { return TypeCacheKeys.GetOrAdd( @@ -78,6 +107,13 @@ public static string GetCacheKey(this Type type) t => $"{t.PrettyPrint()}[hash: {t.GetHashCode()}]"); } + /// + /// Recursively builds a pretty-printed type name, expanding generic arguments up to a depth limit of 3 + /// to prevent infinite recursion with self-referencing generic types. + /// + /// The type to format. + /// The current recursion depth. + /// A formatted type name string. private static string PrettyPrintRecursive(Type type, int depth) { if (depth > 3) @@ -97,6 +133,12 @@ private static string PrettyPrintRecursive(Type type, int depth) : $"{nameParts[0]}<{string.Join(",", genericArguments.Select(t => PrettyPrintRecursive(t, depth + 1)))}>"; } + /// + /// Determines whether the type has any constructor with a parameter matching the given predicate. + /// + /// The type to inspect. + /// A predicate to test each constructor parameter type against. + /// true if any constructor parameter matches the predicate; otherwise, false. public static bool HasConstructorParameterOfType(this Type type, Predicate predicate) { return type.GetTypeInfo().GetConstructors() @@ -104,6 +146,12 @@ public static bool HasConstructorParameterOfType(this Type type, Predicate .Any(p => predicate(p.ParameterType))); } + /// + /// Determines whether the type is assignable to . + /// + /// The target type to check assignability against. + /// The type to check. + /// true if instances of can be assigned to ; otherwise, false. public static bool IsAssignableTo(this Type type) { return typeof(T).GetTypeInfo().IsAssignableFrom(type); diff --git a/Src/RCommon.Core/GeneralException.cs b/Src/RCommon.Core/GeneralException.cs index 4a8e836a..d03eec29 100644 --- a/Src/RCommon.Core/GeneralException.cs +++ b/Src/RCommon.Core/GeneralException.cs @@ -6,31 +6,54 @@ namespace RCommon { + /// + /// Defines severity levels for exceptions, used to classify the impact of an error. + /// public enum SeverityOptions : int { + /// Low severity; typically informational or easily recoverable. Low = 1, + /// Medium severity; may require attention but is not critical. Medium = 2, + /// High severity; default level indicating a significant error. High = 3, + /// Critical severity; indicates a fatal or system-level failure. Critical = 4 } + /// + /// A general-purpose exception that supports severity classification and parameterized message formatting. + /// Extends to include environment diagnostic information. + /// [Serializable] public class GeneralException : BaseApplicationException { private SeverityOptions _severity = SeverityOptions.High; private string _debugMessage = string.Empty; - private object[] _messageParameters = null; + private object[]? _messageParameters = null; + /// + /// Initializes a new instance of with default values. + /// public GeneralException():base() { } + /// + /// Initializes a new instance of with the specified message. + /// + /// The exception message or format string key. public GeneralException(string keyMessage) : base(keyMessage) { _debugMessage = keyMessage; } + /// + /// Initializes a new instance of with the specified severity and message. + /// + /// The level of the exception. + /// The exception message or format string key. public GeneralException(SeverityOptions severity, string keyMessage) : base(keyMessage) { @@ -38,6 +61,11 @@ public GeneralException(SeverityOptions severity, string keyMessage) _debugMessage = keyMessage; } + /// + /// Initializes a new instance of with a parameterized message. + /// + /// The message format string. + /// Parameters to format into the message string. public GeneralException(string keyMessage, params object[] messageParameters) : base(keyMessage) { @@ -45,6 +73,12 @@ public GeneralException(string keyMessage, params object[] messageParameters) _messageParameters = messageParameters; } + /// + /// Initializes a new instance of with severity and a parameterized message. + /// + /// The level of the exception. + /// The message format string. + /// Parameters to format into the message string. public GeneralException(SeverityOptions severity, string keyMessage, params object[] messageParameters) : base(keyMessage) { @@ -53,12 +87,23 @@ public GeneralException(SeverityOptions severity, string keyMessage, params obje _messageParameters = messageParameters; } + /// + /// Initializes a new instance of with a message and inner exception. + /// + /// The exception message or format string key. + /// The inner exception that caused this exception. public GeneralException(string keyMessage, System.Exception innerException) : base(keyMessage, innerException) { _debugMessage = keyMessage; } + /// + /// Initializes a new instance of with a parameterized message and inner exception. + /// + /// The message format string. + /// The inner exception that caused this exception. + /// Parameters to format into the message string. public GeneralException(string keyMessage, System.Exception innerException, params object[] messageParameters) : base(keyMessage, innerException) { @@ -66,6 +111,9 @@ public GeneralException(string keyMessage, System.Exception innerException, para _messageParameters = messageParameters; } + /// + /// Gets the formatted exception message, applying any message parameters if provided. + /// public override string Message { get @@ -74,6 +122,10 @@ public override string Message } } + /// + /// Gets the debug-friendly message with parameters applied via . + /// If no parameters were provided, returns the base message unmodified. + /// public string DebugMessage { get @@ -82,6 +134,10 @@ public string DebugMessage } } + /// + /// Gets or sets the severity level of this exception. + /// + /// public SeverityOptions Severity { get @@ -94,6 +150,10 @@ public SeverityOptions Severity } } + /// + /// Formats the exception message by delegating to . + /// + /// The formatted message string. private string FormatMessage() { return DebugMessage; diff --git a/Src/RCommon.Core/Guard.cs b/Src/RCommon.Core/Guard.cs index 7ee63c34..8e30f36a 100644 --- a/Src/RCommon.Core/Guard.cs +++ b/Src/RCommon.Core/Guard.cs @@ -10,6 +10,9 @@ namespace RCommon public class Guard { + /// + /// Initializes a new instance of the class. + /// public Guard() { @@ -26,21 +29,21 @@ public Guard() public static void Against(bool assertion, string message) where TException : Exception { if (assertion) - throw (TException)Activator.CreateInstance(typeof(TException), message); + throw (TException)Activator.CreateInstance(typeof(TException), message)!; } /// /// Throws an exception of type with the specified message - /// when the assertion + /// when the assertion function evaluates to true. /// - /// - /// - /// + /// The type of exception to throw. + /// A function that returns the assertion to evaluate. If true then the exception is thrown. + /// string. The exception message to throw. public static void Against(Func assertion, string message) where TException : Exception { //Execute the lambda and if it evaluates to true then throw the exception. if (assertion()) - throw (TException)Activator.CreateInstance(typeof(TException), message); + throw (TException)Activator.CreateInstance(typeof(TException), message)!; } /// @@ -116,15 +119,27 @@ public static void TypeOf(object instance, string message) public static void IsEqual(object compare, object instance, string message) where TException : Exception { if (compare != instance) - throw (TException)Activator.CreateInstance(typeof(TException), message); + throw (TException)Activator.CreateInstance(typeof(TException), message)!; } + /// + /// Throws an when the specified argument is . + /// + /// The value to check. + /// The name of the argument for the exception message. [DebuggerStepThrough] public static void IsNotEmpty(Guid argument, string argumentName) { IsNotEmpty(argument, argumentName, true); } + /// + /// Checks whether the specified argument is not . + /// + /// The value to check. + /// The name of the argument for the exception message. + /// If true, throws an ; otherwise returns false. + /// true if the argument is not empty; false if empty and is false. [DebuggerStepThrough] public static bool IsNotEmpty(Guid argument, string argumentName, bool throwException) { @@ -142,12 +157,24 @@ public static bool IsNotEmpty(Guid argument, string argumentName, bool throwExce } } + /// + /// Throws an when the specified string argument is null, empty, or whitespace. + /// + /// The string value to check. + /// The name of the argument for the exception message. [DebuggerStepThrough] public static void IsNotEmpty(string argument, string argumentName) { IsNotEmpty(argument, argumentName, true); } + /// + /// Checks whether the specified string argument is not null, empty, or whitespace. + /// + /// The string value to check. + /// The name of the argument for the exception message. + /// If true, throws an ; otherwise returns false. + /// true if the argument is not blank; false if blank and is false. [DebuggerStepThrough] public static bool IsNotEmpty(string argument, string argumentName, bool throwException) { @@ -165,6 +192,12 @@ public static bool IsNotEmpty(string argument, string argumentName, bool throwEx } } + /// + /// Throws an when the trimmed string exceeds the specified maximum length. + /// + /// The string value to check. + /// The maximum allowed length. + /// The name of the argument for the exception message. [DebuggerStepThrough] public static void IsNotOutOfLength(string argument, int length, string argumentName) { @@ -174,6 +207,11 @@ public static void IsNotOutOfLength(string argument, int length, string argument } } + /// + /// Throws an when the specified argument is null. + /// + /// The object to check for null. + /// The name of the argument for the exception message. [DebuggerStepThrough] public static void IsNotNull(object argument, string argumentName) { @@ -183,6 +221,11 @@ public static void IsNotNull(object argument, string argumentName) } } + /// + /// Throws an when the specified argument is negative. + /// + /// The integer value to check. + /// The name of the argument for the exception message. [DebuggerStepThrough] public static void IsNotNegative(int argument, string argumentName) { @@ -192,12 +235,24 @@ public static void IsNotNegative(int argument, string argumentName) } } + /// + /// Throws an when the specified argument is negative or zero. + /// + /// The integer value to check. + /// The name of the argument for the exception message. [DebuggerStepThrough] public static void IsNotNegativeOrZero(int argument, string argumentName) { IsNotNegativeOrZero(argument, argumentName, true); } + /// + /// Checks whether the specified argument is not negative or zero. + /// + /// The integer value to check. + /// The name of the argument for the exception message. + /// If true, throws an ; otherwise returns false. + /// true if the argument is positive; false if non-positive and is false. public static bool IsNotNegativeOrZero(int argument, string argumentName, bool throwException) { if (argument <= 0) @@ -214,6 +269,11 @@ public static bool IsNotNegativeOrZero(int argument, string argumentName, bool t } } + /// + /// Throws an when the specified argument is negative. + /// + /// The long value to check. + /// The name of the argument for the exception message. [DebuggerStepThrough] public static void IsNotNegative(long argument, string argumentName) { @@ -223,6 +283,11 @@ public static void IsNotNegative(long argument, string argumentName) } } + /// + /// Throws an when the specified argument is negative or zero. + /// + /// The long value to check. + /// The name of the argument for the exception message. [DebuggerStepThrough] public static void IsNotNegativeOrZero(long argument, string argumentName) { @@ -232,6 +297,11 @@ public static void IsNotNegativeOrZero(long argument, string argumentName) } } + /// + /// Throws an when the specified argument is negative. + /// + /// The float value to check. + /// The name of the argument for the exception message. [DebuggerStepThrough] public static void IsNotNegative(float argument, string argumentName) { @@ -241,6 +311,11 @@ public static void IsNotNegative(float argument, string argumentName) } } + /// + /// Throws an when the specified argument is negative or zero. + /// + /// The float value to check. + /// The name of the argument for the exception message. [DebuggerStepThrough] public static void IsNotNegativeOrZero(float argument, string argumentName) { @@ -250,6 +325,11 @@ public static void IsNotNegativeOrZero(float argument, string argumentName) } } + /// + /// Throws an when the specified argument is negative. + /// + /// The decimal value to check. + /// The name of the argument for the exception message. [DebuggerStepThrough] public static void IsNotNegative(decimal argument, string argumentName) { @@ -259,6 +339,11 @@ public static void IsNotNegative(decimal argument, string argumentName) } } + /// + /// Throws an when the specified argument is negative or zero. + /// + /// The decimal value to check. + /// The name of the argument for the exception message. [DebuggerStepThrough] public static void IsNotNegativeOrZero(decimal argument, string argumentName) { @@ -268,6 +353,11 @@ public static void IsNotNegativeOrZero(decimal argument, string argumentName) } } + /// + /// Throws an when the specified argument is not a valid date. + /// + /// The value to validate. + /// The name of the argument for the exception message. [DebuggerStepThrough] public static void IsNotInvalidDate(DateTime argument, string argumentName) { @@ -277,6 +367,11 @@ public static void IsNotInvalidDate(DateTime argument, string argumentName) } } + /// + /// Throws an when the specified argument is negative. + /// + /// The value to check. + /// The name of the argument for the exception message. [DebuggerStepThrough] public static void IsNotNegative(TimeSpan argument, string argumentName) { @@ -286,6 +381,11 @@ public static void IsNotNegative(TimeSpan argument, string argumentName) } } + /// + /// Throws an when the specified argument is negative or zero. + /// + /// The value to check. + /// The name of the argument for the exception message. [DebuggerStepThrough] public static void IsNotNegativeOrZero(TimeSpan argument, string argumentName) { @@ -295,6 +395,12 @@ public static void IsNotNegativeOrZero(TimeSpan argument, string argumentName) } } + /// + /// Throws an when the specified collection is null or contains no elements. + /// + /// The element type of the collection. + /// The collection to check. + /// The name of the argument for the exception message. [DebuggerStepThrough] public static void IsNotEmpty(ICollection argument, string argumentName) { @@ -306,6 +412,14 @@ public static void IsNotEmpty(ICollection argument, string argumentName) } } + /// + /// Throws an when the specified argument + /// falls outside the inclusive range of to . + /// + /// The integer value to check. + /// The minimum allowed value (inclusive). + /// The maximum allowed value (inclusive). + /// The name of the argument for the exception message. [DebuggerStepThrough] public static void IsNotOutOfRange(int argument, int min, int max, string argumentName) { @@ -315,6 +429,11 @@ public static void IsNotOutOfRange(int argument, int min, int max, string argume } } + /// + /// Throws an when the specified string is not a valid email address. + /// + /// The string to validate as an email address. + /// The name of the argument for the exception message. [DebuggerStepThrough] public static void IsNotInvalidEmail(string argument, string argumentName) { @@ -326,6 +445,11 @@ public static void IsNotInvalidEmail(string argument, string argumentName) } } + /// + /// Throws an when the specified string is not a valid web URL. + /// + /// The string to validate as a web URL. + /// The name of the argument for the exception message. [DebuggerStepThrough] public static void IsNotInvalidWebUrl(string argument, string argumentName) { diff --git a/Src/RCommon.Core/ICommonFactory.cs b/Src/RCommon.Core/ICommonFactory.cs index 122bebc7..f53d2be1 100644 --- a/Src/RCommon.Core/ICommonFactory.cs +++ b/Src/RCommon.Core/ICommonFactory.cs @@ -4,21 +4,79 @@ namespace RCommon { + /// + /// Defines a factory that creates instances of with optional customization. + /// + /// The type of object to create. + /// public interface ICommonFactory { + /// + /// Creates a new instance of . + /// + /// A new instance of . T Create(); + + /// + /// Creates a new instance of and applies the specified customization action. + /// + /// An action to customize the created instance before it is returned. + /// A customized instance of . T Create(Action customize); } + /// + /// Defines a factory that creates instances of using a single argument, + /// with optional customization. + /// + /// The type of the input argument. + /// The type of object to create. + /// public interface ICommonFactory { + /// + /// Creates a new instance of using the specified argument. + /// + /// The input argument used during creation. + /// A new instance of . TResult Create(T arg); + + /// + /// Creates a new instance of using the specified argument + /// and applies the customization action. + /// + /// The input argument used during creation. + /// An action to customize the created instance before it is returned. + /// A customized instance of . TResult Create(T arg, Action customize); } + /// + /// Defines a factory that creates instances of using two arguments, + /// with optional customization. + /// + /// The type of the first input argument. + /// The type of the second input argument. + /// The type of object to create. + /// public interface ICommonFactory { + /// + /// Creates a new instance of using the specified arguments. + /// + /// The first input argument. + /// The second input argument. + /// A new instance of . TResult Create(T arg, T2 arg2); + + /// + /// Creates a new instance of using the specified arguments + /// and applies the customization action. + /// + /// The first input argument. + /// The second input argument. + /// An action to customize the created instance before it is returned. + /// A customized instance of . TResult Create(T arg, T2 arg2, Action customize); } diff --git a/Src/RCommon.Core/IPagedSpecification.cs b/Src/RCommon.Core/IPagedSpecification.cs index 23364dc9..6b806234 100644 --- a/Src/RCommon.Core/IPagedSpecification.cs +++ b/Src/RCommon.Core/IPagedSpecification.cs @@ -7,13 +7,32 @@ namespace RCommon { + /// + /// Extends to add paging and ordering support for query specifications. + /// + /// The entity type that the specification applies to. + /// public interface IPagedSpecification : ISpecification { + /// + /// Gets the 1-based page number to retrieve. + /// public int PageNumber { get; } + + /// + /// Gets the number of items per page. + /// public int PageSize { get; } + /// + /// Gets the expression used to determine the sort order of results. + /// public Expression> OrderByExpression { get; } + /// + /// Gets or sets a value indicating whether results should be sorted in ascending order. + /// When false, results are sorted in descending order. + /// public bool OrderByAscending { get; set; } } } diff --git a/Src/RCommon.Core/IRCommonBuilder.cs b/Src/RCommon.Core/IRCommonBuilder.cs index d5f19710..99c13a09 100644 --- a/Src/RCommon.Core/IRCommonBuilder.cs +++ b/Src/RCommon.Core/IRCommonBuilder.cs @@ -3,14 +3,53 @@ namespace RCommon { + /// + /// Defines the fluent builder interface for configuring RCommon framework services including + /// GUID generation, date/time systems, and common factories. + /// + /// public interface IRCommonBuilder { + /// + /// Gets the used to register services. + /// IServiceCollection Services { get; } + /// + /// Finalizes the configuration and returns the populated . + /// + /// The configured . IServiceCollection Configure(); + + /// + /// Configures the date/time system using the specified . + /// + /// An action to configure . + /// The builder instance for method chaining. IRCommonBuilder WithDateTimeSystem(Action actions); + + /// + /// Configures the to use + /// with the specified options. + /// + /// An action to configure . + /// The builder instance for method chaining. IRCommonBuilder WithSequentialGuidGenerator(Action actions); + + /// + /// Configures the to use + /// which generates standard random GUIDs. + /// + /// The builder instance for method chaining. IRCommonBuilder WithSimpleGuidGenerator(); + + /// + /// Registers a service and its implementation along with a corresponding + /// for creating instances through dependency injection. + /// + /// The service interface type. + /// The concrete implementation type. + /// The builder instance for method chaining. IRCommonBuilder WithCommonFactory() where TService : class where TImplementation : class, TService; diff --git a/Src/RCommon.Core/ISystemTime.cs b/Src/RCommon.Core/ISystemTime.cs index ddc5bff2..bfa66c2b 100644 --- a/Src/RCommon.Core/ISystemTime.cs +++ b/Src/RCommon.Core/ISystemTime.cs @@ -4,6 +4,11 @@ namespace RCommon { + /// + /// Abstracts the system clock to enable testable time-dependent code and consistent time zone handling. + /// + /// + /// public interface ISystemTime { /// diff --git a/Src/RCommon.Core/ISystemTimeOptions.cs b/Src/RCommon.Core/ISystemTimeOptions.cs index 3c314b62..52085cd4 100644 --- a/Src/RCommon.Core/ISystemTimeOptions.cs +++ b/Src/RCommon.Core/ISystemTimeOptions.cs @@ -4,8 +4,16 @@ namespace RCommon { + /// + /// Defines configuration options for the abstraction. + /// + /// public interface ISystemTimeOptions { + /// + /// Gets or sets the that controls whether the system time + /// operates in UTC, Local, or Unspecified mode. + /// DateTimeKind Kind { get; set; } } } diff --git a/Src/RCommon.Core/InvalidArgumentException.cs b/Src/RCommon.Core/InvalidArgumentException.cs index c2bc28c0..9fe448c7 100644 --- a/Src/RCommon.Core/InvalidArgumentException.cs +++ b/Src/RCommon.Core/InvalidArgumentException.cs @@ -13,23 +13,40 @@ namespace RCommon [Serializable] public class InvalidArgumentException : GeneralException { + /// + /// Initializes a new instance of with severity. + /// public InvalidArgumentException() { base.Severity = SeverityOptions.Low; } + /// + /// Initializes a new instance of with the specified message. + /// + /// The exception message or format string key. public InvalidArgumentException(string keyMessage) : base(keyMessage) { base.Severity = SeverityOptions.Low; } + /// + /// Initializes a new instance of with a parameterized message. + /// + /// The message format string. + /// Parameters to format into the message string. public InvalidArgumentException(string keyMessage, params object[] messageParameters) : base(keyMessage, messageParameters) { base.Severity = SeverityOptions.Low; } + /// + /// Initializes a new instance of with a message and inner exception. + /// + /// The exception message or format string key. + /// The inner exception that caused this exception. public InvalidArgumentException(string keyMessage, System.Exception innerException) : base(keyMessage, innerException) { diff --git a/Src/RCommon.Core/Linq/Linq.cs b/Src/RCommon.Core/Linq/Linq.cs index cd70ff67..4d52a01e 100644 --- a/Src/RCommon.Core/Linq/Linq.cs +++ b/Src/RCommon.Core/Linq/Linq.cs @@ -12,13 +12,27 @@ namespace RCommon.Linq /// public static class Linq { - // Returns the given anonymous method as a lambda expression + /// + /// Returns the given anonymous method as a strongly-typed lambda expression. + /// Useful for building dynamic LINQ queries where the compiler cannot infer the expression type. + /// + /// The input type of the expression. + /// The return type of the expression. + /// The lambda expression to return. + /// The same expression, strongly typed. public static Expression> Expr (Expression> expr) { return expr; } - // Returns the given anonymous function as a Func delegate + /// + /// Returns the given anonymous function as a strongly-typed delegate. + /// Useful when the compiler cannot infer the delegate type from context. + /// + /// The input type of the function. + /// The return type of the function. + /// The function delegate to return. + /// The same function, strongly typed. public static Func Func (Func expr) { return expr; diff --git a/Src/RCommon.Core/Linq/PredicateBuilder.cs b/Src/RCommon.Core/Linq/PredicateBuilder.cs index b50af2c2..0c5b3b5d 100644 --- a/Src/RCommon.Core/Linq/PredicateBuilder.cs +++ b/Src/RCommon.Core/Linq/PredicateBuilder.cs @@ -8,22 +8,59 @@ namespace RCommon.Linq /// /// See http://www.albahari.com/expressions for information and examples. /// + /// + /// Provides methods to dynamically compose LINQ predicate expressions using logical AND/OR operators. + /// Start with or as an identity, then chain with + /// or . + /// + /// + /// See http://www.albahari.com/expressions for information and examples. + /// public static class PredicateBuilder { + /// + /// Creates a predicate expression that always evaluates to true. + /// Use as the starting point when building AND-chained predicates. + /// + /// The type the predicate operates on. + /// An expression that always returns true. public static Expression> True () { return f => true; } + + /// + /// Creates a predicate expression that always evaluates to false. + /// Use as the starting point when building OR-chained predicates. + /// + /// The type the predicate operates on. + /// An expression that always returns false. public static Expression> False () { return f => false; } + /// + /// Combines two predicate expressions using a logical OR (short-circuit) operation. + /// + /// The type the predicates operate on. + /// The first predicate expression. + /// The second predicate expression to OR with the first. + /// A combined predicate expression representing OR . public static Expression> Or (this Expression> expr1, Expression> expr2) { + // Invoke expr2 using expr1's parameters so both share the same parameter expression var invokedExpr = Expression.Invoke (expr2, expr1.Parameters.Cast ()); return Expression.Lambda> (Expression.OrElse (expr1.Body, invokedExpr), expr1.Parameters); } + /// + /// Combines two predicate expressions using a logical AND (short-circuit) operation. + /// + /// The type the predicates operate on. + /// The first predicate expression. + /// The second predicate expression to AND with the first. + /// A combined predicate expression representing AND . public static Expression> And (this Expression> expr1, Expression> expr2) { + // Invoke expr2 using expr1's parameters so both share the same parameter expression var invokedExpr = Expression.Invoke (expr2, expr1.Parameters.Cast ()); return Expression.Lambda> (Expression.AndAlso (expr1.Body, invokedExpr), expr1.Parameters); diff --git a/Src/RCommon.Core/PagedSpecification.cs b/Src/RCommon.Core/PagedSpecification.cs index 1d1a317c..4153ef85 100644 --- a/Src/RCommon.Core/PagedSpecification.cs +++ b/Src/RCommon.Core/PagedSpecification.cs @@ -7,9 +7,22 @@ namespace RCommon { + /// + /// Default implementation of that combines a filter predicate + /// with paging and ordering parameters for querying collections of . + /// + /// The entity type that the specification applies to. public class PagedSpecification : Specification, IPagedSpecification { - public PagedSpecification(Expression> predicate, Expression> orderByExpression, + /// + /// Initializes a new instance of with the specified filter, ordering, and paging parameters. + /// + /// The filter expression to select matching entities. + /// The expression used to determine sort order. + /// true for ascending order; false for descending. + /// The 1-based page number to retrieve. + /// The number of items per page. + public PagedSpecification(Expression> predicate, Expression> orderByExpression, bool orderByAscending, int pageNumber, int pageSize) : base(predicate) { OrderByExpression = orderByExpression; @@ -18,10 +31,16 @@ public PagedSpecification(Expression> predicate, Expression public Expression> OrderByExpression { get; } + + /// public int PageNumber { get; } + + /// public int PageSize { get; } + /// public bool OrderByAscending { get; set; } } } diff --git a/Src/RCommon.Core/RCommon.Core.csproj b/Src/RCommon.Core/RCommon.Core.csproj index 0b501504..41daed51 100644 --- a/Src/RCommon.Core/RCommon.Core.csproj +++ b/Src/RCommon.Core/RCommon.Core.csproj @@ -2,6 +2,7 @@ net8.0;net9.0;net10.0 + enable $(MSBuildProjectName.Replace(" ", "_").Replace(".Core", "")) True RCommon.Core diff --git a/Src/RCommon.Core/RCommonBuilder.cs b/Src/RCommon.Core/RCommonBuilder.cs index 46057d59..e8f85e18 100644 --- a/Src/RCommon.Core/RCommonBuilder.cs +++ b/Src/RCommon.Core/RCommonBuilder.cs @@ -12,29 +12,39 @@ namespace RCommon /// public class RCommonBuilder : IRCommonBuilder { + /// public IServiceCollection Services { get; } private bool _guidConfigured = false; private bool _dateTimeConfigured = false; + /// + /// Initializes a new instance of and registers core framework services + /// including caching options, the , the , + /// and the default . + /// + /// The to register services into. + /// Thrown when is null. public RCommonBuilder(IServiceCollection services) { Guard.Against(services == null, "IServiceCollection cannot be null"); - Services = services; + Services = services!; this.Services.Configure(x => { x.CachingEnabled = false; }); // Event Subscription Manager - tracks which events are routed to which producers - services.AddSingleton(new EventSubscriptionManager()); + Services.AddSingleton(new EventSubscriptionManager()); // Event Bus - services.AddSingleton(sp => + Services.AddSingleton(sp => { - return new InMemoryEventBus(sp, services); + return new InMemoryEventBus(sp, Services); }); Services.AddScoped(); } + /// + /// Thrown if a GUID generator has already been configured. public IRCommonBuilder WithSequentialGuidGenerator(Action actions) { Guard.Against(this._guidConfigured, @@ -45,6 +55,8 @@ public IRCommonBuilder WithSequentialGuidGenerator(Action + /// Thrown if a GUID generator has already been configured. public IRCommonBuilder WithSimpleGuidGenerator() { Guard.Against(this._guidConfigured, @@ -54,6 +66,8 @@ public IRCommonBuilder WithSimpleGuidGenerator() return this; } + /// + /// Thrown if the date/time system has already been configured. public IRCommonBuilder WithDateTimeSystem(Action actions) { Guard.Against(this._dateTimeConfigured, @@ -64,16 +78,18 @@ public IRCommonBuilder WithDateTimeSystem(Action actions) return this; } + /// public IRCommonBuilder WithCommonFactory() where TService : class where TImplementation : class, TService { this.Services.AddTransient(); - this.Services.AddScoped>(x => () => x.GetService()); + this.Services.AddScoped>(x => () => x.GetService()!); this.Services.AddScoped, CommonFactory>(); return this; } + /// public virtual IServiceCollection Configure() { return this.Services; diff --git a/Src/RCommon.Core/RCommonBuilderException.cs b/Src/RCommon.Core/RCommonBuilderException.cs index c597ecc1..2c6a514a 100644 --- a/Src/RCommon.Core/RCommonBuilderException.cs +++ b/Src/RCommon.Core/RCommonBuilderException.cs @@ -6,8 +6,18 @@ namespace RCommon { + /// + /// Exception thrown when the encounters a configuration error, + /// such as attempting to configure a service that has already been configured. + /// Always has severity. + /// public class RCommonBuilderException : GeneralException { + /// + /// Initializes a new instance of with the specified message + /// and severity. + /// + /// The error message describing the configuration failure. public RCommonBuilderException(string message) : base(SeverityOptions.Critical , message) { diff --git a/Src/RCommon.Core/README.md b/Src/RCommon.Core/README.md index e469bd29..ac5ab3a5 100644 --- a/Src/RCommon.Core/README.md +++ b/Src/RCommon.Core/README.md @@ -1,3 +1,73 @@ # RCommon.Core -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 +The foundation package for the RCommon framework, providing the fluent builder for dependency injection configuration, an in-memory event bus with transactional event routing, guard clauses, GUID generation, system time abstraction, and a rich set of extension methods. + +## Features + +- Fluent `AddRCommon()` builder pattern for configuring framework services via Microsoft DI +- In-memory event bus (`IEventBus`) with publish/subscribe support and polymorphic event dispatch +- Transactional event routing (`IEventRouter`) that stores events and dispatches them to the correct `IEventProducer` instances based on subscription configuration +- `EventSubscriptionManager` for isolating event subscriptions so each producer only receives its registered events +- `Guard` class with validation methods for nulls, empty strings, ranges, types, collections, email, and more +- Sequential and simple GUID generators (`IGuidGenerator`) optimized for database-friendly ordering +- `ISystemTime` abstraction for testable, time zone-aware date/time handling +- `ICommonFactory` for DI-aware factory pattern with customization support +- Extension methods for collections, strings, expressions, streams, dictionaries, reflection, and IQueryable +- Reflection utilities including `ObjectGraphWalker` for traversing object graphs and `ReflectionHelper` for generic type inspection and compiled method invocation + +## Installation + +```shell +dotnet add package RCommon.Core +``` + +## Usage + +```csharp +using RCommon; +using RCommon.EventHandling; + +// Bootstrap RCommon in your DI configuration +services.AddRCommon(builder => +{ + builder + .WithSequentialGuidGenerator(options => + options.DefaultSequentialGuidType = SequentialGuidType.SequentialAsString) + .WithDateTimeSystem(options => + options.Kind = DateTimeKind.Utc) + .WithEventHandling(eventHandling => + { + eventHandling.AddSubscriber(); + }); +}); +``` + +## Key Types + +| Type | Description | +|------|-------------| +| `IRCommonBuilder` | Fluent builder interface for configuring RCommon framework services | +| `IEventBus` | In-process event bus for publishing events and subscribing handlers | +| `ISubscriber` | Strongly-typed event subscriber that handles a specific event type | +| `IEventRouter` | Routes stored transactional events to the appropriate `IEventProducer` instances | +| `IEventProducer` | Dispatches serializable events to a destination (bus, broker, etc.) | +| `EventSubscriptionManager` | Tracks event-to-producer subscriptions for isolated event routing | +| `Guard` | Utility class with guard clause methods for parameter validation | +| `IGuidGenerator` | Abstraction for GUID generation (sequential or simple) | +| `ISystemTime` | Abstracts the system clock for testable time-dependent code | +| `ICommonFactory` | DI-aware factory pattern for creating service instances | +| `ObjectGraphWalker` | Recursively traverses an object graph searching for instances of a specified type | +| `ReflectionHelper` | Utilities for generic type inspection, attribute retrieval, and compiled method invocation | + +## Documentation + +For full documentation, visit [rcommon.com](https://rcommon.com). + +## Related Packages + +- [RCommon.Models](https://www.nuget.org/packages/RCommon.Models) - Shared models for CQRS commands, queries, events, pagination, and execution results +- [RCommon.Entities](https://www.nuget.org/packages/RCommon.Entities) - Domain entity base classes with auditing, soft delete, and transactional event tracking + +## License + +Licensed under the [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0). diff --git a/Src/RCommon.Core/Reflection/ObjectGraphWalker.cs b/Src/RCommon.Core/Reflection/ObjectGraphWalker.cs index c35c3dba..d279f235 100644 --- a/Src/RCommon.Core/Reflection/ObjectGraphWalker.cs +++ b/Src/RCommon.Core/Reflection/ObjectGraphWalker.cs @@ -8,8 +8,22 @@ namespace RCommon.Reflection { + /// + /// Recursively traverses an object graph, searching for instances of a specified type + /// across all public properties, fields, and enumerable collections. + /// + /// + /// The walker tracks visited objects to avoid infinite recursion from circular references. + /// public static class ObjectGraphWalker { + /// + /// Traverses an object graph starting from and collects all instances + /// of type found within the graph. + /// + /// The type to search for in the object graph. Must be a reference type. + /// The root object to begin traversal from. + /// An enumerable of all instances found in the graph. public static IEnumerable TraverseGraphFor(object root) where T : class { var results = new List(); @@ -18,7 +32,16 @@ public static IEnumerable TraverseGraphFor(object root) where T : class return results.ToArray(); } - private static void Walk(object source, IList results, IList visited) + /// + /// Recursively walks an object: checks if it matches , + /// enumerates it if it is a sequence, and inspects its public properties and fields. + /// Tracks visited objects to prevent infinite loops from circular references. + /// + /// The type to search for. + /// The current object being inspected. + /// The accumulator list for matching instances. + /// The list of already-visited objects to prevent cycles. + private static void Walk(object? source, IList results, IList visited) where T : class { if (source == null) return; @@ -31,18 +54,24 @@ private static void Walk(object source, IList results, IList visited) results.Add((T)source); } - // source is a sequence of objects + // source is a sequence of objects (includes Array, IDictionary, IList, IQueryable) if (source is IEnumerable) { - // includes Array, IDictionary, IList, IQueryable WalkSequence((IEnumerable)source, results, visited); } - // dive into the object's properties + // dive into the object's properties and fields WalkComplexObject(source, results, visited); } - private static void WalkSequence(IEnumerable source, + /// + /// Iterates over each element in a sequence and recursively walks it. + /// + /// The type to search for. + /// The enumerable sequence to iterate. + /// The accumulator list for matching instances. + /// The list of already-visited objects to prevent cycles. + private static void WalkSequence(IEnumerable? source, IList results, IList visited) where T : class { @@ -53,12 +82,21 @@ private static void WalkSequence(IEnumerable source, } } - private static void WalkComplexObject(object source, + /// + /// Inspects all public instance fields and readable non-indexed properties of the source object, + /// recursively walking each value to find instances of . + /// + /// The type to search for. + /// The complex object whose members are inspected. + /// The accumulator list for matching instances. + /// The list of already-visited objects to prevent cycles. + private static void WalkComplexObject(object? source, IList results, IList visited) where T : class { if (source == null) return; var type = source.GetType(); + // Only inspect readable, non-indexed properties to avoid exceptions var properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance) .Where(x => x.CanRead && !x.GetIndexParameters().Any()); var fields = type.GetFields(BindingFlags.Public | BindingFlags.Instance); diff --git a/Src/RCommon.Core/Reflection/ReflectionHelper.cs b/Src/RCommon.Core/Reflection/ReflectionHelper.cs index 0821ee50..e24c0295 100644 --- a/Src/RCommon.Core/Reflection/ReflectionHelper.cs +++ b/Src/RCommon.Core/Reflection/ReflectionHelper.cs @@ -6,7 +6,10 @@ namespace RCommon.Reflection { - //TODO: Consider to make internal + /// + /// Provides utility methods for reflection-based operations including generic type inspection, + /// attribute retrieval, property path navigation, constant discovery, and compiled method invocation. + /// public static class ReflectionHelper { //TODO: Ehhance summary @@ -40,7 +43,13 @@ public static bool IsAssignableToGenericType(Type givenType, Type genericType) return IsAssignableToGenericType(givenTypeInfo.BaseType, genericType); } - //TODO: Summary + /// + /// Gets all closed generic types that implements or inherits + /// matching the open generic type definition specified by . + /// + /// The type to inspect. + /// The open generic type definition to match against (e.g., typeof(IRepository<>)). + /// A list of closed generic types matching the specified definition. public static List GetImplementedGenericTypes(Type givenType, Type genericType) { var result = new List(); @@ -48,6 +57,14 @@ public static List GetImplementedGenericTypes(Type givenType, Type generic return result; } + /// + /// Recursively searches through a type's hierarchy (interfaces and base types) + /// to find all closed generic types matching the given open generic type definition, + /// adding each unique match to the result list. + /// + /// The accumulator list for matching types. + /// The current type being inspected. + /// The open generic type definition to match against. private static void AddImplementedGenericTypes(List result, Type givenType, Type genericType) { var givenTypeInfo = givenType.GetTypeInfo(); @@ -81,7 +98,7 @@ private static void AddImplementedGenericTypes(List result, Type givenType /// MemberInfo /// Default value (null as default) /// Inherit attribute from base classes - public static TAttribute GetSingleAttributeOrDefault(MemberInfo memberInfo, TAttribute defaultValue = default, bool inherit = true) + public static TAttribute? GetSingleAttributeOrDefault(MemberInfo memberInfo, TAttribute? defaultValue = default, bool inherit = true) where TAttribute : Attribute { //Get attribute on the member @@ -101,7 +118,7 @@ public static TAttribute GetSingleAttributeOrDefault(MemberInfo memb /// MemberInfo /// Default value (null as default) /// Inherit attribute from base classes - public static TAttribute GetSingleAttributeOfMemberOrDeclaringTypeOrDefault(MemberInfo memberInfo, TAttribute defaultValue = default, bool inherit = true) + public static TAttribute? GetSingleAttributeOfMemberOrDeclaringTypeOrDefault(MemberInfo memberInfo, TAttribute? defaultValue = default, bool inherit = true) where TAttribute : class { return memberInfo.GetCustomAttributes(true).OfType().FirstOrDefault() @@ -129,9 +146,9 @@ public static IEnumerable GetAttributesOfMemberOrDeclaringType /// Gets value of a property by it's full path from given object /// - public static object GetValueByPath(object obj, Type objectType, string propertyPath) + public static object? GetValueByPath(object obj, Type objectType, string propertyPath) { - var value = obj; + object? value = obj; var currentType = objectType; var objectPath = currentType.FullName; var absolutePropertyPath = propertyPath; @@ -167,10 +184,10 @@ public static object GetValueByPath(object obj, Type objectType, string property internal static void SetValueByPath(object obj, Type objectType, string propertyPath, object value) { var currentType = objectType; - PropertyInfo property; + PropertyInfo? property; var objectPath = currentType.FullName; var absolutePropertyPath = propertyPath; - if (absolutePropertyPath.StartsWith(objectPath)) + if (objectPath != null && absolutePropertyPath.StartsWith(objectPath)) { absolutePropertyPath = absolutePropertyPath.Replace(objectPath + ".", ""); } @@ -180,19 +197,20 @@ internal static void SetValueByPath(object obj, Type objectType, string property if (properties.Length == 1) { property = objectType.GetProperty(properties.First()); - property.SetValue(obj, value); + property?.SetValue(obj, value); return; } for (int i = 0; i < properties.Length - 1; i++) { property = currentType.GetProperty(properties[i]); - obj = property.GetValue(obj, null); + if (property == null) return; + obj = property.GetValue(obj, null)!; currentType = property.PropertyType; } property = currentType.GetProperty(properties.Last()); - property.SetValue(obj, value); + property?.SetValue(obj, value); } @@ -216,7 +234,7 @@ void Recursively(List constants, Type targetType, int currentDepth) constants.AddRange(targetType.GetFields(BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy) .Where(x => x.IsLiteral && !x.IsInitOnly) - .Select(x => x.GetValue(null).ToString())); + .Select(x => x.GetValue(null)?.ToString() ?? string.Empty)); var nestedTypes = targetType.GetNestedTypes(BindingFlags.Public); @@ -285,7 +303,7 @@ public static TResult CompileMethodInvocation(MethodInfo methodInfo) instanceArgument, }; - var type = methodInfo.DeclaringType; + var type = methodInfo.DeclaringType!; var instanceVariable = Expression.Variable(type); var blockVariables = new List { diff --git a/Src/RCommon.Core/SequentialGuidGenerator.cs b/Src/RCommon.Core/SequentialGuidGenerator.cs index d4e6d6d6..03d9ea3a 100644 --- a/Src/RCommon.Core/SequentialGuidGenerator.cs +++ b/Src/RCommon.Core/SequentialGuidGenerator.cs @@ -14,20 +14,42 @@ namespace RCommon /// public class SequentialGuidGenerator : IGuidGenerator { + /// + /// Gets the options that configure the default for GUID generation. + /// public SequentialGuidGeneratorOptions Options { get; } private static readonly RandomNumberGenerator RandomNumberGenerator = RandomNumberGenerator.Create(); + /// + /// Initializes a new instance of with the specified options. + /// + /// The options controlling the default sequential GUID type. public SequentialGuidGenerator(IOptions options) { Options = options.Value; } + /// + /// Creates a new sequential using the default + /// from . + /// + /// A new sequential . public Guid Create() { return Create(Options.GetDefaultSequentialGuidType()); } + /// + /// Creates a new sequential of the specified type. + /// + /// The controlling byte layout for database compatibility. + /// A new sequential with a timestamp-based sequential portion. + /// + /// The GUID is composed of 10 bytes of cryptographically random data and a 6-byte timestamp + /// derived from in millisecond resolution. The byte ordering + /// varies by to optimize for different database platforms. + /// public Guid Create(SequentialGuidType guidType) { // We start with 16 bytes of cryptographically strong random data. diff --git a/Src/RCommon.Core/SequentialGuidGeneratorOptions.cs b/Src/RCommon.Core/SequentialGuidGeneratorOptions.cs index 745d91d4..c49fc9cd 100644 --- a/Src/RCommon.Core/SequentialGuidGeneratorOptions.cs +++ b/Src/RCommon.Core/SequentialGuidGeneratorOptions.cs @@ -2,6 +2,10 @@ namespace RCommon { + /// + /// Configuration options for the , controlling the default + /// used when generating sequential GUIDs. + /// public class SequentialGuidGeneratorOptions { /// diff --git a/Src/RCommon.Core/SimpleGuidGenerator.cs b/Src/RCommon.Core/SimpleGuidGenerator.cs index 21388011..85df5776 100644 --- a/Src/RCommon.Core/SimpleGuidGenerator.cs +++ b/Src/RCommon.Core/SimpleGuidGenerator.cs @@ -9,6 +9,10 @@ public class SimpleGuidGenerator : IGuidGenerator { + /// + /// Creates a new random using . + /// + /// A new randomly generated . public virtual Guid Create() { return Guid.NewGuid(); diff --git a/Src/RCommon.Core/Specification.cs b/Src/RCommon.Core/Specification.cs index 94fee7d2..0d15bd65 100644 --- a/Src/RCommon.Core/Specification.cs +++ b/Src/RCommon.Core/Specification.cs @@ -42,8 +42,8 @@ public class Specification : ISpecification public Specification(Expression> predicate) { Guard.Against(predicate == null, "Expected a non null expression as a predicate for the specification."); - _predicate = predicate; - _predicateCompiled = predicate.Compile(); + _predicate = predicate!; + _predicateCompiled = predicate!.Compile(); } /// diff --git a/Src/RCommon.Core/SystemTime.cs b/Src/RCommon.Core/SystemTime.cs index cd8491db..1777a013 100644 --- a/Src/RCommon.Core/SystemTime.cs +++ b/Src/RCommon.Core/SystemTime.cs @@ -5,38 +5,63 @@ namespace RCommon { + /// + /// Default implementation of that provides the current date/time + /// based on the configured and normalizes values accordingly. + /// public class SystemTime : ISystemTime { + /// + /// Gets the configuration options for this system time instance. + /// protected SystemTimeOptions Options { get; } + /// + /// Initializes a new instance of with the specified options. + /// + /// The options controlling the behavior. public SystemTime(IOptions options) { Options = options.Value; } + /// public virtual DateTime Now => Options.Kind == DateTimeKind.Utc ? DateTime.UtcNow : DateTime.Now; + /// public virtual DateTimeKind Kind => Options.Kind; + /// public virtual bool SupportsMultipleTimezone => Options.Kind == DateTimeKind.Utc; + /// + /// + /// Conversion rules: if is or matches the + /// input's Kind, the value is returned unchanged. Otherwise UTC-to-Local or Local-to-UTC conversion + /// is performed. If the input Kind is , the Kind is simply + /// re-specified without conversion. + /// public virtual DateTime Normalize(DateTime dateTime) { + // No conversion needed when Kind is unspecified or matches the input if (Kind == DateTimeKind.Unspecified || Kind == dateTime.Kind) { return dateTime; } + // Convert UTC input to local time if (Kind == DateTimeKind.Local && dateTime.Kind == DateTimeKind.Utc) { return dateTime.ToLocalTime(); } + // Convert local input to UTC if (Kind == DateTimeKind.Utc && dateTime.Kind == DateTimeKind.Local) { return dateTime.ToUniversalTime(); } + // For unspecified input, just re-specify the Kind without actual conversion return DateTime.SpecifyKind(dateTime, Kind); } } diff --git a/Src/RCommon.Core/SystemTimeOptions.cs b/Src/RCommon.Core/SystemTimeOptions.cs index 3e0154cb..5856dd1e 100644 --- a/Src/RCommon.Core/SystemTimeOptions.cs +++ b/Src/RCommon.Core/SystemTimeOptions.cs @@ -4,13 +4,21 @@ namespace RCommon { + /// + /// Default implementation of providing configuration + /// for the service. + /// public class SystemTimeOptions : ISystemTimeOptions { /// - /// Default: + /// Gets or sets the that controls system time behavior. + /// Default: . /// public DateTimeKind Kind { get; set; } + /// + /// Initializes a new instance of with as the default kind. + /// public SystemTimeOptions() { Kind = DateTimeKind.Unspecified; diff --git a/Src/RCommon.Core/Threading/AsyncHelper.cs b/Src/RCommon.Core/Threading/AsyncHelper.cs index c568b8a9..c8c313b0 100644 --- a/Src/RCommon.Core/Threading/AsyncHelper.cs +++ b/Src/RCommon.Core/Threading/AsyncHelper.cs @@ -7,6 +7,10 @@ namespace RCommon.Core.Threading { + /// + /// Provides helper methods to run asynchronous methods synchronously using + /// from the Nito.AsyncEx library. This avoids deadlocks that can occur with Task.Result or Task.Wait(). + /// public static class AsyncHelper { diff --git a/Src/RCommon.Core/Util/ImageHelper.cs b/Src/RCommon.Core/Util/ImageHelper.cs index b2c978a9..ba3d1960 100644 --- a/Src/RCommon.Core/Util/ImageHelper.cs +++ b/Src/RCommon.Core/Util/ImageHelper.cs @@ -5,19 +5,40 @@ namespace RCommon.Util { + /// + /// Provides utility methods for working with image files, including format detection from raw byte data. + /// public class ImageHelper { + /// + /// Represents supported image file formats. + /// public enum ImageFormat { + /// Bitmap image format. Bmp, + /// JPEG image format. Jpeg, + /// GIF image format. Gif, + /// TIFF image format. Tiff, + /// PNG image format. Png, + /// Unrecognized image format. Unknown } + /// + /// Detects the image format by inspecting the file header (magic bytes) of the byte array. + /// + /// The raw byte data of the image file. + /// The detected , or if unrecognized. + /// + /// Supports BMP, GIF, PNG, TIFF (both byte orders), and JPEG (standard and Canon variants) + /// by comparing the leading bytes against known file signatures. + /// public static ImageFormat GetImageFormat(byte[] bytes) { // see http://www.mikekunz.com/image_file_header.html diff --git a/Src/RCommon.Core/Util/Inflector.cs b/Src/RCommon.Core/Util/Inflector.cs index 6d9b7830..59e4ccbb 100644 --- a/Src/RCommon.Core/Util/Inflector.cs +++ b/Src/RCommon.Core/Util/Inflector.cs @@ -15,8 +15,11 @@ namespace RCommon.Util /// public static class Inflector { + /// Collection of pluralization rules applied in reverse order. private static readonly List Plurals = new List(); + /// Collection of singularization rules applied in reverse order. private static readonly List Singulars = new List(); + /// Collection of words that are the same in both singular and plural forms. private static readonly List Uncountables = new List(); /// @@ -83,18 +86,31 @@ static Inflector() AddUncountable("sheep"); } + /// + /// Represents a regex-based inflection rule that transforms words via pattern matching and replacement. + /// private class Rule { private readonly Regex _regex; private readonly string _replacement; + /// + /// Initializes a new inflection rule with the specified regex pattern and replacement string. + /// + /// The regex pattern to match against words. + /// The replacement string (may include regex group references like $1). public Rule(string pattern, string replacement) { _regex = new Regex(pattern, RegexOptions.IgnoreCase); _replacement = replacement; } - public string Apply(string word) + /// + /// Applies this rule to a word. Returns the transformed word if the pattern matches, or null otherwise. + /// + /// The word to transform. + /// The transformed word, or null if the pattern does not match. + public string? Apply(string word) { if (!_regex.IsMatch(word)) { @@ -105,27 +121,54 @@ public string Apply(string word) } } + /// + /// Adds bidirectional rules for an irregular word (e.g., "person" / "people"), + /// preserving the first letter's case. + /// + /// The singular form of the irregular word. + /// The plural form of the irregular word. private static void AddIrregular(string singular, string plural) { AddPlural("(" + singular[0] + ")" + singular.Substring(1) + "$", "$1" + plural.Substring(1)); AddSingular("(" + plural[0] + ")" + plural.Substring(1) + "$", "$1" + singular.Substring(1)); } + /// + /// Registers a word as uncountable (same in singular and plural forms, e.g., "sheep"). + /// + /// The uncountable word to register. private static void AddUncountable(string word) { Uncountables.Add(word.ToLower()); } + /// + /// Adds a pluralization regex rule. + /// + /// The regex pattern for matching singular words. + /// The replacement pattern for creating the plural form. private static void AddPlural(string rule, string replacement) { Plurals.Add(new Rule(rule, replacement)); } + /// + /// Adds a singularization regex rule. + /// + /// The regex pattern for matching plural words. + /// The replacement pattern for creating the singular form. private static void AddSingular(string rule, string replacement) { Singulars.Add(new Rule(rule, replacement)); } + /// + /// Applies the given list of rules to a word in reverse order (last rule wins). + /// Words in the list are returned unchanged. + /// + /// The list of rules to apply. + /// The word to transform. + /// The transformed word, or the original word if no rule matches or the word is uncountable. private static string ApplyRules(List rules, string word) { string result = word; @@ -134,8 +177,10 @@ private static string ApplyRules(List rules, string word) { for (int i = rules.Count - 1; i >= 0; i--) { - if ((result = rules[i].Apply(word)) != null) + string? applied = rules[i].Apply(word); + if (applied != null) { + result = applied; break; } } diff --git a/Src/RCommon.Dapper/Crud/DapperRepository.cs b/Src/RCommon.Dapper/Crud/DapperRepository.cs index b9ce34fa..b7479cae 100644 --- a/Src/RCommon.Dapper/Crud/DapperRepository.cs +++ b/Src/RCommon.Dapper/Crud/DapperRepository.cs @@ -22,11 +22,28 @@ namespace RCommon.Persistence.Dapper.Crud { + /// + /// A concrete repository implementation using Dapper and the Dommel extension library for CRUD operations. + /// + /// The entity type managed by this repository. Must implement . + /// + /// Each operation acquires a from the configured , + /// ensures it is open before executing, and closes it in a finally block. This repository + /// uses Dommel's extension methods (e.g., InsertAsync, DeleteAsync, SelectAsync) + /// for SQL generation from entity mappings. + /// public class DapperRepository : SqlRepositoryBase where TEntity : class, IBusinessEntity { - public DapperRepository(IDataStoreFactory dataStoreFactory, + /// + /// Initializes a new instance of . + /// + /// Factory used to resolve the for the configured data store. + /// Factory for creating loggers scoped to this repository type. + /// Tracker used to register entities for domain event dispatching. + /// Options specifying which data store to use when none is explicitly set. + public DapperRepository(IDataStoreFactory dataStoreFactory, ILoggerFactory logger, IEntityEventTracker eventTracker, IOptions defaultDataStoreOptions) : base(dataStoreFactory, logger, eventTracker, defaultDataStoreOptions) @@ -34,6 +51,7 @@ public DapperRepository(IDataStoreFactory dataStoreFactory, Logger = logger.CreateLogger(GetType().Name); } + /// public override async Task AddAsync(TEntity entity, CancellationToken token = default) { @@ -66,6 +84,7 @@ public override async Task AddAsync(TEntity entity, CancellationToken token = de } + /// public override async Task DeleteAsync(TEntity entity, CancellationToken token = default) { await using (var db = DataStore.GetDbConnection()) @@ -96,6 +115,7 @@ public override async Task DeleteAsync(TEntity entity, CancellationToken token = } } + /// public async override Task DeleteManyAsync(Expression> expression, CancellationToken token = default) { await using (var db = DataStore.GetDbConnection()) @@ -107,7 +127,7 @@ public async override Task DeleteManyAsync(Expression> await db.OpenAsync(); } - return await db.DeleteMultipleAsync(expression, cancellationToken: token); + return await db.DeleteMultipleAsync(expression, cancellationToken: token); } catch (ApplicationException exception) { @@ -125,6 +145,7 @@ public async override Task DeleteManyAsync(Expression> } } + /// public async override Task DeleteManyAsync(ISpecification specification, CancellationToken token = default) { return await DeleteManyAsync(specification.Predicate, token); @@ -132,6 +153,7 @@ public async override Task DeleteManyAsync(ISpecification specific + /// public override async Task UpdateAsync(TEntity entity, CancellationToken token = default) { @@ -162,11 +184,13 @@ public override async Task UpdateAsync(TEntity entity, CancellationToken token = } } + /// public override async Task> FindAsync(ISpecification specification, CancellationToken token = default) { return await FindAsync(specification.Predicate, token); } + /// public override async Task> FindAsync(Expression> expression, CancellationToken token = default) { await using (var db = DataStore.GetDbConnection()) @@ -196,6 +220,7 @@ public override async Task> FindAsync(Expression public override async Task FindAsync(object primaryKey, CancellationToken token = default) { await using (var db = DataStore.GetDbConnection()) @@ -208,7 +233,7 @@ public override async Task FindAsync(object primaryKey, CancellationTok } var result = await db.GetAsync(primaryKey, cancellationToken: token); - return result; + return result!; } catch (ApplicationException exception) { @@ -225,6 +250,7 @@ public override async Task FindAsync(object primaryKey, CancellationTok } } + /// public override async Task GetCountAsync(ISpecification selectSpec, CancellationToken token = default) { await using (var db = DataStore.GetDbConnection()) @@ -254,6 +280,7 @@ public override async Task GetCountAsync(ISpecification selectSpe } } + /// public override async Task GetCountAsync(Expression> expression, CancellationToken token = default) { await using (var db = DataStore.GetDbConnection()) @@ -284,32 +311,34 @@ public override async Task GetCountAsync(Expression> e } /// - /// Gets the single returned value based on the expression passed in. + /// Gets the single returned value based on the expression passed in. /// /// Custom Expression /// Cancellation Token /// Value matching expression criteria. - /// Do not use this if querying using primary key. Use Do not use this if querying using primary key. Use instead /// due to issues related to https://github.com/henkmollema/Dommel/issues/282 public override async Task FindSingleOrDefaultAsync(Expression> expression, CancellationToken token = default) { + // Dommel lacks a native SingleOrDefault, so we retrieve all matches and apply SingleOrDefault in-memory var result = await FindAsync(expression, token); - return result.SingleOrDefault(); + return result.SingleOrDefault()!; } /// - /// Gets the single returned value based on the expression passed in. + /// Gets the single returned value based on the expression passed in. /// /// Custom Specification /// Cancellation Token /// Value matching specification expression criteria. - /// Do not use this if querying using primary key. Use Do not use this if querying using primary key. Use instead /// due to issues related to https://github.com/henkmollema/Dommel/issues/282 public override async Task FindSingleOrDefaultAsync(ISpecification specification, CancellationToken token = default) { return await FindSingleOrDefaultAsync(specification, token); } + /// public override async Task AnyAsync(Expression> expression, CancellationToken token = default) { await using (var db = DataStore.GetDbConnection()) @@ -339,6 +368,7 @@ public override async Task AnyAsync(Expression> expres } } + /// public override async Task AnyAsync(ISpecification specification, CancellationToken token = default) { return await AnyAsync(specification.Predicate, token); diff --git a/Src/RCommon.Dapper/DapperFluentMappingsException.cs b/Src/RCommon.Dapper/DapperFluentMappingsException.cs index 0661541c..a35dc567 100644 --- a/Src/RCommon.Dapper/DapperFluentMappingsException.cs +++ b/Src/RCommon.Dapper/DapperFluentMappingsException.cs @@ -6,8 +6,19 @@ namespace RCommon.Persistence.Dapper { - public class DapperFluentMappingsException : GeneralException + /// + /// Exception thrown when Dapper fluent mapping configuration encounters an error. + /// + /// + /// This exception is raised with severity, + /// indicating a fatal configuration problem that prevents Dapper from operating correctly. + /// + public class DapperFluentMappingsException : GeneralException { + /// + /// Initializes a new instance of with the specified error message. + /// + /// A message describing the fluent mapping error. public DapperFluentMappingsException(string message) :base(SeverityOptions.Critical, message) { diff --git a/Src/RCommon.Dapper/DapperPersistenceBuilder.cs b/Src/RCommon.Dapper/DapperPersistenceBuilder.cs index 0315f678..1ac016ee 100644 --- a/Src/RCommon.Dapper/DapperPersistenceBuilder.cs +++ b/Src/RCommon.Dapper/DapperPersistenceBuilder.cs @@ -13,12 +13,27 @@ namespace RCommon { + /// + /// Implementation of that configures Dapper-based + /// persistence services in the dependency injection container. + /// + /// + /// Upon construction, this builder registers as the default + /// implementation for , , + /// and . + /// public class DapperPersistenceBuilder : IDapperBuilder { private readonly IServiceCollection _services; private List _dbContextTypes = new List(); + /// + /// Initializes a new instance of and registers + /// Dapper repository services in the provided service collection. + /// + /// The to register services with. + /// Thrown when is null. public DapperPersistenceBuilder(IServiceCollection services) { _services = services ?? throw new ArgumentNullException(nameof(services)); @@ -27,27 +42,43 @@ public DapperPersistenceBuilder(IServiceCollection services) services.AddTransient(typeof(ISqlMapperRepository<>), typeof(DapperRepository<>)); services.AddTransient(typeof(IWriteOnlyRepository<>), typeof(DapperRepository<>)); services.AddTransient(typeof(IReadOnlyRepository<>), typeof(DapperRepository<>)); - + } + /// public IServiceCollection Services => _services; + /// + /// Registers a database connection type with the specified data store name and connection options. + /// + /// The type of the database connection. Must derive from . + /// A unique name identifying this data store for resolution via . + /// An action to configure the (e.g., connection string). + /// The builder instance for fluent chaining. + /// Thrown when is null or empty. + /// Thrown when is null. public IDapperBuilder AddDbConnection(string dataStoreName, Action options) where TDbConnection : RDbConnection { Guard.Against(dataStoreName.IsNullOrEmpty(), "You must set a name for the Data Store"); Guard.Against(options == null, "You must configure the options for the RDbConnection for it to be useful"); - var dbContext = typeof(TDbConnection).AssemblyQualifiedName; + // Resolve the assembly-qualified type name to register the concrete connection type + var dbContext = typeof(TDbConnection).AssemblyQualifiedName!; this._services.TryAddTransient(); - this._services.TryAddTransient(Type.GetType(dbContext)); - this._services.Configure(options => options.Register(dataStoreName)); - this._services.Configure(options); + this._services.TryAddTransient(Type.GetType(dbContext)!); + this._services.Configure(o => o.Register(dataStoreName)); + this._services.Configure(options!); return this; } + /// + /// Sets the default data store used when no explicit data store name is specified. + /// + /// An action to configure . + /// The builder instance for fluent chaining. public IPersistenceBuilder SetDefaultDataStore(Action options) { this._services.Configure(options); diff --git a/Src/RCommon.Dapper/IDapperBuilder.cs b/Src/RCommon.Dapper/IDapperBuilder.cs index d4a0161b..24ab1e2f 100644 --- a/Src/RCommon.Dapper/IDapperBuilder.cs +++ b/Src/RCommon.Dapper/IDapperBuilder.cs @@ -3,8 +3,22 @@ namespace RCommon { + /// + /// Defines the fluent builder interface for configuring Dapper-based persistence in RCommon. + /// + /// + /// Extends to add Dapper-specific configuration such as + /// registering -derived database connections with named data stores. + /// public interface IDapperBuilder : IPersistenceBuilder { + /// + /// Registers a database connection type with the specified data store name and connection options. + /// + /// The type of the database connection. Must derive from . + /// A unique name identifying this data store for resolution via . + /// An action to configure the (e.g., connection string). + /// The builder instance for fluent chaining. IDapperBuilder AddDbConnection(string dataStoreName, Action options) where TDbConnection : RDbConnection; } } diff --git a/Src/RCommon.Dapper/RCommon.Dapper.csproj b/Src/RCommon.Dapper/RCommon.Dapper.csproj index 0cf4b958..03fa255b 100644 --- a/Src/RCommon.Dapper/RCommon.Dapper.csproj +++ b/Src/RCommon.Dapper/RCommon.Dapper.csproj @@ -2,6 +2,7 @@ net8.0;net9.0;net10.0 + enable True RCommon.Dapper https://rcommon.com diff --git a/Src/RCommon.Dapper/README.md b/Src/RCommon.Dapper/README.md index 9bcf36b7..8a793813 100644 --- a/Src/RCommon.Dapper/README.md +++ b/Src/RCommon.Dapper/README.md @@ -1,3 +1,96 @@ - # RCommon.Dapper +# RCommon.Dapper -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 +Dapper implementation of the RCommon persistence abstractions. Provides a lightweight SQL mapper repository using Dapper and Dommel for CRUD operations with expression-based querying, while integrating with the RCommon data store factory and domain event tracking. + +## Features + +- `DapperRepository` implementing `ISqlMapperRepository`, `IReadOnlyRepository`, and `IWriteOnlyRepository` +- Expression-based querying via Dommel's `SelectAsync`, `CountAsync`, and `AnyAsync` extension methods +- CRUD operations mapped to SQL using Dommel's `InsertAsync`, `UpdateAsync`, and `DeleteAsync` +- Bulk delete support via `DeleteMultipleAsync` with expression predicates +- Find by primary key using Dommel's `GetAsync` +- Automatic connection lifecycle management (open/close per operation) +- Named data store support for multi-database scenarios through `IDataStoreFactory` and `RDbConnection` +- Fluent DI configuration to register database connections as named data stores +- Domain event tracking integrated into add, update, and delete operations +- Targets .NET 8, .NET 9, and .NET 10 + +## Installation + +```shell +dotnet add package RCommon.Dapper +``` + +## Usage + +```csharp +// Configure in Program.cs or Startup +builder.Services.AddRCommon() + .WithPersistence(dapper => + { + dapper.AddDbConnection("ApplicationDb", options => + { + options.DbFactory = SqlClientFactory.Instance; + options.ConnectionString = builder.Configuration.GetConnectionString("ApplicationDb"); + }); + + dapper.SetDefaultDataStore(defaults => + defaults.DefaultDataStoreName = "ApplicationDb"); + }); +``` + +Your database connection must inherit from `RDbConnection`: + +```csharp +public class ApplicationDbConnection : RDbConnection +{ + public ApplicationDbConnection(IOptions options) + : base(options) { } +} +``` + +Then inject and use the repository abstractions: + +```csharp +public class ProductService +{ + private readonly ISqlMapperRepository _productRepo; + + public ProductService(ISqlMapperRepository productRepo) + { + _productRepo = productRepo; + } + + public async Task> GetActiveProductsAsync() + { + return await _productRepo.FindAsync(p => p.IsActive); + } + + public async Task GetByIdAsync(int id) + { + return await _productRepo.FindAsync(id); + } +} +``` + +## Key Types + +| Type | Description | +|------|-------------| +| `DapperRepository` | Concrete repository using Dapper/Dommel with expression-based CRUD operations | +| `DapperPersistenceBuilder` | Fluent builder for registering Dapper database connections and repository services in DI | +| `IDapperBuilder` | Builder interface exposing `AddDbConnection()` for registering named database connections | + +## Documentation + +For full documentation, visit [rcommon.com](https://rcommon.com). + +## Related Packages + +- [RCommon.Persistence](https://www.nuget.org/packages/RCommon.Persistence) - Core persistence abstractions (required dependency) +- [RCommon.EFCore](https://www.nuget.org/packages/RCommon.EFCore) - Entity Framework Core implementation +- [RCommon.Linq2Db](https://www.nuget.org/packages/RCommon.Linq2Db) - Linq2Db implementation + +## License + +Licensed under the [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0). diff --git a/Src/RCommon.EfCore/Crud/EFCoreRepository.cs b/Src/RCommon.EfCore/Crud/EFCoreRepository.cs index 2acebf26..db6d9715 100644 --- a/Src/RCommon.EfCore/Crud/EFCoreRepository.cs +++ b/Src/RCommon.EfCore/Crud/EFCoreRepository.cs @@ -22,28 +22,34 @@ namespace RCommon.Persistence.EFCore.Crud { /// - /// A concrete implementation for Entity Framework Core. - /// currently exposes much of the functionality of EF with the exception of change tracking and peristance models. We expose IQueryable to layers down stream - /// so that complex joins can be utilized and then managed at the domain level. This implementation makes special considerations for managing the lifetime of the - /// specifically when it applies to the . + /// A concrete repository implementation for Entity Framework Core that supports CRUD operations, + /// LINQ queries, eager loading, and graph-based entity navigation. /// - /// + /// The entity type managed by this repository. Must implement . + /// + /// Exposes much of the EF Core functionality with the exception of direct change tracking and persistence models. + /// Exposes to downstream layers so that complex joins can be utilized and managed at the domain level. + /// This implementation makes special considerations for managing the lifetime of the + /// specifically when it applies to the . + /// public class EFCoreRepository : GraphRepositoryBase where TEntity : class, IBusinessEntity { - private IQueryable _repositoryQuery; + private IQueryable? _repositoryQuery; private bool _tracking; - private IIncludableQueryable _includableQueryable; + private IIncludableQueryable? _includableQueryable; private readonly IDataStoreFactory _dataStoreFactory; /// - /// The default constructor for the repository. + /// Initializes a new instance of . /// - /// The is injected with scoped lifetime so it will always return the same instance of the - /// througout the HTTP request or the scope of the thread. - /// Logger used throughout the application. + /// Factory used to resolve the for the configured data store. + /// Factory for creating loggers scoped to this repository type. + /// Tracker used to register entities for domain event dispatching. + /// Options specifying which data store to use when none is explicitly set. + /// Thrown when any parameter is null. public EFCoreRepository(IDataStoreFactory dataStoreFactory, ILoggerFactory logger, IEntityEventTracker eventTracker, IOptions defaultDataStoreOptions) @@ -69,6 +75,9 @@ public EFCoreRepository(IDataStoreFactory dataStoreFactory, _dataStoreFactory = dataStoreFactory ?? throw new ArgumentNullException(nameof(dataStoreFactory)); } + /// + /// Gets the from the current for direct entity set operations. + /// protected DbSet ObjectSet { get @@ -77,6 +86,9 @@ protected DbSet ObjectSet } } + /// + /// Gets or sets whether EF Core change tracking is enabled for queries executed through this repository. + /// public override bool Tracking { get => _tracking; @@ -87,8 +99,14 @@ public override bool Tracking } + /// + /// Adds an eager-loading include path for the specified navigation property. + /// + /// An expression selecting the navigation property to include. + /// This repository instance for fluent chaining of additional includes. public override IEagerLoadableQueryable Include(Expression> path) { + // On first call, start from the DbSet; on subsequent calls, chain from the existing includable query if (_includableQueryable == null) { _includableQueryable = ObjectContext.Set().Include(path); @@ -97,17 +115,28 @@ public override IEagerLoadableQueryable Include(Expression + /// Adds a subsequent eager-loading path for a nested navigation property after a prior call. + /// + /// The type of the previously included navigation property. + /// The type of the nested navigation property to include. + /// An expression selecting the nested navigation property to include. + /// This repository instance for fluent chaining. public override IEagerLoadableQueryable ThenInclude(Expression> path) { - // TODO: This is likely a bug. The receiver is incorrect. - _repositoryQuery = _includableQueryable.ThenInclude(path); + // TODO: This is likely a bug. The receiver is incorrect. + _repositoryQuery = _includableQueryable!.ThenInclude(path); return this; } + /// + /// Gets the base used for all query operations. + /// Applies eager-loading expressions if any have been configured via . + /// protected override IQueryable RepositoryQuery { get @@ -117,7 +146,7 @@ protected override IQueryable RepositoryQuery _repositoryQuery = ObjectSet.AsQueryable(); } - // Start Eagerloading + // Override the base query with the eager-loaded queryable if includes have been configured if (_includableQueryable != null) { _repositoryQuery = _includableQueryable; @@ -126,6 +155,7 @@ protected override IQueryable RepositoryQuery } } + /// public override async Task AddAsync(TEntity entity, CancellationToken token = default) { EventTracker.AddEntity(entity); @@ -134,6 +164,7 @@ public override async Task AddAsync(TEntity entity, CancellationToken token = de } + /// public async override Task DeleteAsync(TEntity entity, CancellationToken token = default) { EventTracker.AddEntity(entity); @@ -141,16 +172,19 @@ public async override Task DeleteAsync(TEntity entity, CancellationToken token = await SaveAsync(); } + /// public async override Task DeleteManyAsync(ISpecification specification, CancellationToken token = default) { return await this.DeleteManyAsync(specification.Predicate, token); } + /// public async override Task DeleteManyAsync(Expression> expression, CancellationToken token = default) { return await this.FindQuery(expression).ExecuteDeleteAsync(token); } + /// public async override Task UpdateAsync(TEntity entity, CancellationToken token = default) { EventTracker.AddEntity(entity); @@ -158,13 +192,20 @@ public async override Task UpdateAsync(TEntity entity, CancellationToken token = await SaveAsync(token); } + /// + /// Core query method that applies the given filter expression to the . + /// All find operations delegate to this method to build the filtered queryable. + /// + /// A predicate expression to filter entities. + /// An representing the filtered query. + /// Thrown when is null. private IQueryable FindCore(Expression> expression) { IQueryable queryable; try { Guard.Against(RepositoryQuery == null, "RepositoryQuery is null"); - queryable = RepositoryQuery.Where(expression); + queryable = RepositoryQuery!.Where(expression); } catch (ApplicationException exception) { @@ -174,41 +215,49 @@ private IQueryable FindCore(Expression> expression) return queryable; } + /// public async override Task GetCountAsync(ISpecification selectSpec, CancellationToken token = default) { return await FindCore(selectSpec.Predicate).CountAsync(token); } + /// public async override Task GetCountAsync(Expression> expression, CancellationToken token = default) { return await FindCore(expression).CountAsync(token); } + /// public override IQueryable FindQuery(ISpecification specification) { return FindCore(specification.Predicate); } + /// public override IQueryable FindQuery(Expression> expression) { return FindCore(expression); } + /// public override async Task FindAsync(object primaryKey, CancellationToken token = default) { - return await ObjectSet.FindAsync(new object[] { primaryKey }, token); + return (await ObjectSet.FindAsync(new object[] { primaryKey }, token))!; } + /// public async override Task> FindAsync(ISpecification specification, CancellationToken token = default) { return await FindCore(specification.Predicate).ToListAsync(token); } + /// public async override Task> FindAsync(Expression> expression, CancellationToken token = default) { return await FindCore(expression).ToListAsync(token); } + /// public async override Task> FindAsync(IPagedSpecification specification, CancellationToken token = default) { IQueryable query; @@ -223,6 +272,7 @@ public async override Task> FindAsync(IPagedSpecificatio return await Task.FromResult(query.ToPaginatedList(specification.PageNumber, specification.PageSize)); } + /// public async override Task> FindAsync(Expression> expression, Expression> orderByExpression, bool orderByAscending, int pageNumber = 1, int pageSize = 1, CancellationToken token = default) @@ -238,6 +288,8 @@ public async override Task> FindAsync(Expression public override IQueryable FindQuery(Expression> expression, Expression> orderByExpression, bool orderByAscending, int pageNumber = 1, int pageSize = 0) { @@ -253,6 +305,7 @@ public override IQueryable FindQuery(Expression> ex return query.Skip((pageNumber - 1) * pageSize).Take(pageSize); } + /// public override IQueryable FindQuery(Expression> expression, Expression> orderByExpression, bool orderByAscending) { @@ -268,32 +321,40 @@ public override IQueryable FindQuery(Expression> ex return query; } + /// public override IQueryable FindQuery(IPagedSpecification specification) { - return this.FindQuery(specification.Predicate, specification.OrderByExpression, + return this.FindQuery(specification.Predicate, specification.OrderByExpression, specification.OrderByAscending, specification.PageNumber, specification.PageSize); } + /// public override async Task FindSingleOrDefaultAsync(Expression> expression, CancellationToken token = default) { - return await FindCore(expression).SingleOrDefaultAsync(token); + return (await FindCore(expression).SingleOrDefaultAsync(token))!; } + /// public override async Task FindSingleOrDefaultAsync(ISpecification specification, CancellationToken token = default) { - return await FindCore(specification.Predicate).SingleOrDefaultAsync(token); + return (await FindCore(specification.Predicate).SingleOrDefaultAsync(token))!; } + /// public async override Task AnyAsync(Expression> expression, CancellationToken token = default) { return await FindCore(expression).AnyAsync(token); } + /// public async override Task AnyAsync(ISpecification specification, CancellationToken token = default) { return await FindCore(specification.Predicate).AnyAsync(token); } + /// + /// Gets the for the configured data store, resolved through the . + /// protected internal RCommonDbContext ObjectContext { get @@ -302,11 +363,18 @@ protected internal RCommonDbContext ObjectContext } } + /// + /// Persists all pending changes in the to the database. + /// + /// A cancellation token to observe. + /// The number of rows affected by the save operation. + /// Thrown when the underlying save operation fails. private async Task SaveAsync(CancellationToken token = default) { int affected = 0; try { + // acceptAllChangesOnSuccess is set to true so EF resets tracking after a successful save affected = await ObjectContext.SaveChangesAsync(true, token); } catch (ApplicationException ex) diff --git a/Src/RCommon.EfCore/EFCorePerisistenceBuilder.cs b/Src/RCommon.EfCore/EFCorePerisistenceBuilder.cs index 1dd3f630..fb09e9e8 100644 --- a/Src/RCommon.EfCore/EFCorePerisistenceBuilder.cs +++ b/Src/RCommon.EfCore/EFCorePerisistenceBuilder.cs @@ -15,12 +15,24 @@ namespace RCommon { /// - /// Implementation of for Entity Framework. + /// Implementation of that configures Entity Framework Core + /// persistence services in the dependency injection container. /// + /// + /// Upon construction, this builder registers as the default + /// implementation for , , + /// , and . + /// public class EFCorePerisistenceBuilder : IEFCorePersistenceBuilder { private readonly IServiceCollection _services; + /// + /// Initializes a new instance of and registers + /// EF Core repository services in the provided service collection. + /// + /// The to register services with. + /// Thrown when is null. public EFCorePerisistenceBuilder(IServiceCollection services) { _services = services ?? throw new ArgumentNullException(nameof(services)); @@ -32,13 +44,23 @@ public EFCorePerisistenceBuilder(IServiceCollection services) services.AddTransient(typeof(IGraphRepository<>), typeof(EFCoreRepository<>)); } + /// public IServiceCollection Services => _services; + /// + /// Registers a -derived DbContext with the specified data store name and options. + /// + /// The type of the DbContext to register. Must derive from . + /// A unique name identifying this data store for resolution via . + /// An optional action to configure the . + /// The builder instance for fluent chaining. + /// Thrown when is null or empty. public IEFCorePersistenceBuilder AddDbContext(string dataStoreName, Action? options = null) where TDbContext : RCommonDbContext { Guard.Against(dataStoreName.IsNullOrEmpty(), "You must set a name for the Data Store"); + // Register the factory, map the concrete DbContext type to the data store name, and add the DbContext with scoped lifetime _services.TryAddTransient(); _services.Configure(options => options.Register(dataStoreName)); _services.AddDbContext(options, ServiceLifetime.Scoped); @@ -46,6 +68,11 @@ public IEFCorePersistenceBuilder AddDbContext(string dataStoreName, return this; } + /// + /// Sets the default data store used when no explicit data store name is specified. + /// + /// An action to configure . + /// The builder instance for fluent chaining. public IPersistenceBuilder SetDefaultDataStore(Action options) { _services.Configure(options); diff --git a/Src/RCommon.EfCore/IEFCorePersistenceBuilder.cs b/Src/RCommon.EfCore/IEFCorePersistenceBuilder.cs index 25b78162..878a92a8 100644 --- a/Src/RCommon.EfCore/IEFCorePersistenceBuilder.cs +++ b/Src/RCommon.EfCore/IEFCorePersistenceBuilder.cs @@ -8,8 +8,22 @@ namespace RCommon { + /// + /// Defines the fluent builder interface for configuring Entity Framework Core persistence in RCommon. + /// + /// + /// Extends to add EF Core-specific configuration such as + /// registering instances with named data stores. + /// public interface IEFCorePersistenceBuilder : IPersistenceBuilder { + /// + /// Registers a -derived DbContext with the specified data store name and options. + /// + /// The type of the DbContext to register. Must derive from . + /// A unique name identifying this data store for resolution via . + /// An optional action to configure the for this context. + /// The builder instance for fluent chaining. IEFCorePersistenceBuilder AddDbContext(string dataStoreName, Action? options) where TDbContext : RCommonDbContext; } } diff --git a/Src/RCommon.EfCore/RCommonDbContext.cs b/Src/RCommon.EfCore/RCommonDbContext.cs index f2182897..a15fca1e 100644 --- a/Src/RCommon.EfCore/RCommonDbContext.cs +++ b/Src/RCommon.EfCore/RCommonDbContext.cs @@ -13,9 +13,22 @@ namespace RCommon.Persistence.EFCore { + /// + /// Abstract base class for all EF Core DbContexts used within the RCommon persistence layer. + /// + /// + /// Implements to provide a uniform abstraction over data stores, + /// allowing the to resolve named DbContext instances. + /// Derive from this class instead of directly when using RCommon. + /// public abstract class RCommonDbContext : DbContext, IDataStore { + /// + /// Initializes a new instance of with the specified options. + /// + /// The used to configure this context. + /// Thrown when is null. public RCommonDbContext(DbContextOptions options) : base(options) { @@ -28,6 +41,10 @@ public RCommonDbContext(DbContextOptions options) + /// + /// Gets the underlying for this context. + /// + /// The associated with the current database. public DbConnection GetDbConnection() { return base.Database.GetDbConnection(); diff --git a/Src/RCommon.EfCore/README.md b/Src/RCommon.EfCore/README.md index 87278aef..d07687df 100644 --- a/Src/RCommon.EfCore/README.md +++ b/Src/RCommon.EfCore/README.md @@ -1,3 +1,94 @@ - # RCommon.EFCore +# RCommon.EFCore -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 +Entity Framework Core implementation of the RCommon persistence abstractions. Provides a fully-featured repository with LINQ queries, eager loading, change tracking, and automatic domain event integration -- all backed by EF Core's `DbContext`. + +## Features + +- `EFCoreRepository` implementing `IGraphRepository`, `ILinqRepository`, `IReadOnlyRepository`, and `IWriteOnlyRepository` +- Full `IQueryable` support for composable LINQ queries at the domain layer +- Eager loading via `Include` / `ThenInclude` mapped to EF Core's `IIncludableQueryable` +- Configurable change tracking (enable/disable per repository via the `Tracking` property) +- Paginated query results with ordering support +- Bulk delete via `ExecuteDeleteAsync` for expression-based batch operations +- Named data store support for multi-database scenarios through `IDataStoreFactory` +- `RCommonDbContext` base class implementing `IDataStore` for seamless factory resolution +- Fluent DI configuration to register DbContexts as named data stores +- Automatic entity event tracking for domain event dispatching on add, update, and delete +- Targets .NET 8, .NET 9, and .NET 10 + +## Installation + +```shell +dotnet add package RCommon.EFCore +``` + +## Usage + +```csharp +// Configure in Program.cs or Startup +builder.Services.AddRCommon() + .WithPersistence(ef => + { + ef.AddDbContext("ApplicationDb", options => + options.UseSqlServer(builder.Configuration.GetConnectionString("ApplicationDb"))); + + ef.SetDefaultDataStore(defaults => + defaults.DefaultDataStoreName = "ApplicationDb"); + }); +``` + +Your `DbContext` must inherit from `RCommonDbContext`: + +```csharp +public class ApplicationDbContext : RCommonDbContext +{ + public ApplicationDbContext(DbContextOptions options) + : base(options) { } + + public DbSet Orders => Set(); + public DbSet Customers => Set(); +} +``` + +Then inject and use the repository abstractions: + +```csharp +public class OrderService +{ + private readonly IGraphRepository _orderRepo; + + public OrderService(IGraphRepository orderRepo) + { + _orderRepo = orderRepo; + } + + public async Task> GetCustomerOrdersAsync(int customerId) + { + _orderRepo.Include(o => o.LineItems); + return await _orderRepo.FindAsync(o => o.CustomerId == customerId); + } +} +``` + +## Key Types + +| Type | Description | +|------|-------------| +| `EFCoreRepository` | Concrete repository backed by EF Core with full CRUD, LINQ, eager loading, and change tracking | +| `RCommonDbContext` | Abstract `DbContext` base class implementing `IDataStore` for named data store resolution | +| `EFCorePerisistenceBuilder` | Fluent builder for registering EF Core DbContexts and repository services in DI | +| `IEFCorePersistenceBuilder` | Builder interface exposing `AddDbContext()` for registering named DbContexts | + +## Documentation + +For full documentation, visit [rcommon.com](https://rcommon.com). + +## Related Packages + +- [RCommon.Persistence](https://www.nuget.org/packages/RCommon.Persistence) - Core persistence abstractions (required dependency) +- [RCommon.Dapper](https://www.nuget.org/packages/RCommon.Dapper) - Dapper implementation +- [RCommon.Linq2Db](https://www.nuget.org/packages/RCommon.Linq2Db) - Linq2Db implementation + +## License + +Licensed under the [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0). diff --git a/Src/RCommon.Emailing/EmailEventArgs.cs b/Src/RCommon.Emailing/EmailEventArgs.cs index 127650d3..747bb0ca 100644 --- a/Src/RCommon.Emailing/EmailEventArgs.cs +++ b/Src/RCommon.Emailing/EmailEventArgs.cs @@ -7,13 +7,23 @@ namespace RCommon.Emailing { + /// + /// Event arguments for email-related events, carrying the that was sent. + /// public class EmailEventArgs : EventArgs { + /// + /// Initializes a new instance of the class. + /// + /// The mail message associated with this event. public EmailEventArgs(MailMessage mailMessage) { MailMessage=mailMessage; } + /// + /// Gets the associated with this event. + /// public MailMessage MailMessage { get; } } } diff --git a/Src/RCommon.Emailing/EmailingBuilderExtensions.cs b/Src/RCommon.Emailing/EmailingBuilderExtensions.cs index 261819ce..4a31f700 100644 --- a/Src/RCommon.Emailing/EmailingBuilderExtensions.cs +++ b/Src/RCommon.Emailing/EmailingBuilderExtensions.cs @@ -9,9 +9,18 @@ namespace RCommon { + /// + /// Extension methods for that register SMTP-based email services. + /// public static class EmailingBuilderExtensions { - + /// + /// Registers as the implementation + /// and configures the SMTP settings via the provided delegate. + /// + /// The RCommon builder to configure. + /// A delegate to configure . + /// The same instance for fluent chaining. public static IRCommonBuilder WithSmtpEmailServices(this IRCommonBuilder config, Action emailSettings) { config.Services.Configure(emailSettings); diff --git a/Src/RCommon.Emailing/IEmailService.cs b/Src/RCommon.Emailing/IEmailService.cs index 6fc111ce..3ffe059a 100644 --- a/Src/RCommon.Emailing/IEmailService.cs +++ b/Src/RCommon.Emailing/IEmailService.cs @@ -4,11 +4,27 @@ namespace RCommon.Emailing { + /// + /// Defines a service for sending email messages, with support for both synchronous and asynchronous delivery. + /// public interface IEmailService { + /// + /// Occurs after an email has been successfully sent. + /// event EventHandler EmailSent; + /// + /// Sends the specified synchronously. + /// + /// The to send. void SendEmail(MailMessage message); + + /// + /// Sends the specified asynchronously. + /// + /// The to send. + /// A representing the asynchronous send operation. Task SendEmailAsync(MailMessage message); } } diff --git a/Src/RCommon.Emailing/README.md b/Src/RCommon.Emailing/README.md index ac41ef07..612f4aee 100644 --- a/Src/RCommon.Emailing/README.md +++ b/Src/RCommon.Emailing/README.md @@ -1,3 +1,88 @@ - # RCommon.Emailing +# RCommon.Emailing -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 +An email service abstraction for .NET with a built-in SMTP implementation, designed to integrate with the RCommon dependency injection builder pattern. + +## Features + +- `IEmailService` abstraction for sending email via `System.Net.Mail.MailMessage` +- Synchronous and asynchronous send methods +- Built-in SMTP implementation (`SmtpEmailService`) using `System.Net.Mail.SmtpClient` +- Configurable SMTP settings including host, port, SSL, credentials, and default sender +- `EmailSent` event for post-send notifications +- Fluent DI registration through the `AddRCommon()` builder + +## Installation + +```shell +dotnet add package RCommon.Emailing +``` + +## Usage + +```csharp +using RCommon; + +services.AddRCommon() + .WithSmtpEmailServices(settings => + { + settings.Host = "smtp.example.com"; + settings.Port = 587; + settings.EnableSsl = true; + settings.UserName = "user@example.com"; + settings.Password = "password"; + settings.FromEmailDefault = "noreply@example.com"; + settings.FromNameDefault = "My Application"; + }); +``` + +Then inject and use `IEmailService`: + +```csharp +using RCommon.Emailing; +using System.Net.Mail; + +public class NotificationService +{ + private readonly IEmailService _emailService; + + public NotificationService(IEmailService emailService) + { + _emailService = emailService; + } + + public async Task SendWelcomeEmailAsync(string toAddress) + { + var message = new MailMessage("noreply@example.com", toAddress) + { + Subject = "Welcome!", + Body = "

Welcome to our app

", + IsBodyHtml = true + }; + + await _emailService.SendEmailAsync(message); + } +} +``` + +## Key Types + +| Type | Description | +|------|-------------| +| `IEmailService` | Abstraction for sending email with sync and async methods | +| `SmtpEmailService` | SMTP-based implementation using `System.Net.Mail.SmtpClient` | +| `SmtpEmailSettings` | Configuration for SMTP host, port, SSL, credentials, and default sender | +| `EmailEventArgs` | Event args carrying the `MailMessage` after a successful send | +| `EmailingBuilderExtensions` | Provides `WithSmtpEmailServices()` for the RCommon builder | + +## Documentation + +For full documentation, visit [rcommon.com](https://rcommon.com). + +## Related Packages + +- [RCommon.SendGrid](https://www.nuget.org/packages/RCommon.SendGrid) - SendGrid implementation of `IEmailService` +- [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.Emailing/Smtp/SmtpEmailService.cs b/Src/RCommon.Emailing/Smtp/SmtpEmailService.cs index b94b4f59..96fa8342 100644 --- a/Src/RCommon.Emailing/Smtp/SmtpEmailService.cs +++ b/Src/RCommon.Emailing/Smtp/SmtpEmailService.cs @@ -11,10 +11,21 @@ namespace RCommon.Emailing.Smtp { + /// + /// Implementation of that sends email using the built-in . + /// + /// + /// SMTP connection settings are provided via . + /// The is disposed after each send to release resources. + /// public class SmtpEmailService : IEmailService { private readonly SmtpEmailSettings _settings; + /// + /// Initializes a new instance of the class. + /// + /// The SMTP configuration options. public SmtpEmailService(IOptions settings) { _settings=settings.Value; @@ -35,7 +46,7 @@ public void SendEmail(MailMessage message) using (var smtp = new SmtpClient()) { smtp.Credentials = new NetworkCredential(this._settings.UserName, this._settings.Password); - smtp.Host = this._settings.Host; + smtp.Host = this._settings.Host!; smtp.Port = this._settings.Port; smtp.EnableSsl = this._settings.EnableSsl; smtp.Send(message); @@ -62,7 +73,13 @@ public async Task SendEmailAsync(MailMessage message) /// /// Occurs after an e-mail has been sent. The sender is the MailMessage object. /// - public event EventHandler EmailSent; + /// + public event EventHandler? EmailSent; + + /// + /// Raises the event if any subscribers are attached. + /// + /// The mail message that was sent. private void OnEmailSent(MailMessage message) { if (EmailSent != null) diff --git a/Src/RCommon.Emailing/Smtp/SmtpEmailSettings.cs b/Src/RCommon.Emailing/Smtp/SmtpEmailSettings.cs index ee114e3e..a1db43f4 100644 --- a/Src/RCommon.Emailing/Smtp/SmtpEmailSettings.cs +++ b/Src/RCommon.Emailing/Smtp/SmtpEmailSettings.cs @@ -4,20 +4,54 @@ namespace RCommon.Emailing.Smtp { + /// + /// Configuration settings for the . + /// Typically bound from an application configuration section (e.g., appsettings.json). + /// public class SmtpEmailSettings { + /// + /// Initializes a new instance of the class. + /// public SmtpEmailSettings() { } - public string UserName { get; set; } - public string Password { get; set; } + /// + /// Gets or sets the SMTP authentication user name. + /// + public string? UserName { get; set; } + + /// + /// Gets or sets the SMTP authentication password. + /// + public string? Password { get; set; } + + /// + /// Gets or sets a value indicating whether SSL is enabled for the SMTP connection. + /// public bool EnableSsl { get; set; } + + /// + /// Gets or sets the SMTP server port number. + /// public int Port { get; set; } - public string Host { get; set; } - public string FromEmailDefault { get; set; } - public string FromNameDefault { get; set; } + + /// + /// Gets or sets the SMTP server host name or IP address. + /// + public string? Host { get; set; } + + /// + /// Gets or sets the default sender email address used when no explicit "From" address is specified. + /// + public string? FromEmailDefault { get; set; } + + /// + /// Gets or sets the default sender display name used when no explicit "From" name is specified. + /// + public string? FromNameDefault { get; set; } } } diff --git a/Src/RCommon.Entities/AuditedEntity.cs b/Src/RCommon.Entities/AuditedEntity.cs index f5b9681e..3ef7b933 100644 --- a/Src/RCommon.Entities/AuditedEntity.cs +++ b/Src/RCommon.Entities/AuditedEntity.cs @@ -6,21 +6,52 @@ namespace RCommon.Entities { + /// + /// Abstract base class for entities that track creation and modification audit information. + /// Uses as the base (no explicit primary key type). + /// + /// The type representing the user who created the entity. + /// The type representing the user who last modified the entity. + /// + /// public abstract class AuditedEntity : BusinessEntity, IAuditedEntity { + /// public DateTime? DateCreated { get; set; } + + /// public TCreatedByUser? CreatedBy { get; set; } + + /// public DateTime? DateLastModified { get; set; } + + /// public TLastModifiedByUser? LastModifiedBy { get; set; } } + /// + /// Abstract base class for entities that track creation and modification audit information + /// with an explicit strongly-typed primary key. + /// + /// The type of the entity's primary key. Must implement . + /// The type representing the user who created the entity. + /// The type representing the user who last modified the entity. + /// + /// public abstract class AuditedEntity : BusinessEntity, IAuditedEntity, IAuditedEntity where TKey : IEquatable { + /// public DateTime? DateCreated { get; set; } + + /// public TCreatedByUser? CreatedBy { get; set; } + + /// public DateTime? DateLastModified { get; set; } + + /// public TLastModifiedByUser? LastModifiedBy { get; set; } } } diff --git a/Src/RCommon.Entities/BusinessEntity.cs b/Src/RCommon.Entities/BusinessEntity.cs index 866dae26..f5693b40 100644 --- a/Src/RCommon.Entities/BusinessEntity.cs +++ b/Src/RCommon.Entities/BusinessEntity.cs @@ -8,21 +8,41 @@ namespace RCommon.Entities { /// + /// + /// Provides a base implementation for domain entities with support for transactional (local) + /// event tracking. Local events are accumulated on the entity and can be emitted via an + /// during persistence operations. + /// [Serializable] public abstract class BusinessEntity : IBusinessEntity { private bool _allowEventTracking = true; - private List _localEvents; + private List _localEvents = new List(); + /// + /// Initializes a new instance of . + /// public BusinessEntity() { + // Ensure the local events list is initialized (null-coalesce guards against serialization scenarios) _localEvents = _localEvents ?? new List(); } - - public event EventHandler TransactionalEventAdded; - public event EventHandler TransactionalEventRemoved; - public event EventHandler TransactionalEventsCleared; + + /// + /// Occurs when a transactional event is added to this entity. + /// + public event EventHandler? TransactionalEventAdded; + + /// + /// Occurs when a transactional event is removed from this entity. + /// + public event EventHandler? TransactionalEventRemoved; + + /// + /// Occurs when all transactional events are cleared from this entity. + /// + public event EventHandler? TransactionalEventsCleared; /// @@ -31,58 +51,80 @@ public override string ToString() return $"[ENTITY: {GetType().Name}] Keys = {GetKeys().GetDelimitedString(',')}"; } + /// public abstract object[] GetKeys(); + /// public bool EntityEquals(IBusinessEntity other) { return this.BinaryEquals(other); } + /// [NotMapped] - public IReadOnlyCollection LocalEvents => _localEvents?.AsReadOnly(); + public IReadOnlyCollection LocalEvents => _localEvents.AsReadOnly(); + /// [NotMapped] public bool AllowEventTracking { get => _allowEventTracking; set => _allowEventTracking = value; } + /// public void AddLocalEvent(ISerializableEvent eventItem) { _localEvents.Add(eventItem); this.OnLocalEventsAdded(new TransactionalEventsChangedEventArgs(this, eventItem)); } + /// public void RemoveLocalEvent(ISerializableEvent eventItem) { _localEvents?.Remove(eventItem); this.OnLocalEventsRemoved(new TransactionalEventsChangedEventArgs(this, eventItem)); } + /// public void ClearLocalEvents() { _localEvents?.Clear(); this.OnLocalEventsCleared(new TransactionalEventsClearedEventArgs(this)); } + /// + /// Raises the event. + /// + /// The event arguments containing the entity and the added event. protected void OnLocalEventsAdded(TransactionalEventsChangedEventArgs args) { - EventHandler handler = TransactionalEventAdded; + // Capture delegate to a local variable for thread safety + EventHandler? handler = TransactionalEventAdded; if (handler != null) { handler(this, args); } } + /// + /// Raises the event. + /// + /// The event arguments containing the entity and the removed event. protected void OnLocalEventsRemoved(TransactionalEventsChangedEventArgs args) { - EventHandler handler = TransactionalEventRemoved; + // Capture delegate to a local variable for thread safety + EventHandler? handler = TransactionalEventRemoved; if (handler != null) { handler(this, args); } } + /// + /// Raises the event. + /// + /// The event arguments containing the entity whose events were cleared. protected void OnLocalEventsCleared(TransactionalEventsClearedEventArgs args) { - EventHandler handler = TransactionalEventsCleared; + // Capture delegate to a local variable for thread safety + EventHandler? handler = TransactionalEventsCleared; if (handler != null) { handler(this, args); @@ -96,18 +138,26 @@ public abstract class BusinessEntity : BusinessEntity, IBusinessEntity { /// - public virtual TKey Id { get; protected set; } + public virtual TKey Id { get; protected set; } = default!; + /// + /// Initializes a new instance of with a default key. + /// protected BusinessEntity() { } + /// + /// Initializes a new instance of with the specified key. + /// + /// The primary key value for this entity. protected BusinessEntity(TKey id) { Id = id; } + /// public override object[] GetKeys() { return new object[] { Id }; diff --git a/Src/RCommon.Entities/EntityNotFoundException.cs b/Src/RCommon.Entities/EntityNotFoundException.cs index 88e9082a..d7dcfa85 100644 --- a/Src/RCommon.Entities/EntityNotFoundException.cs +++ b/Src/RCommon.Entities/EntityNotFoundException.cs @@ -4,19 +4,20 @@ namespace RCommon.Entities { /// - /// This exception is thrown if an entity excepted to be found but not found. + /// Exception thrown when an entity expected to be found does not exist. /// + /// public class EntityNotFoundException : GeneralException { /// /// Type of the entity. /// - public Type EntityType { get; set; } + public Type? EntityType { get; set; } /// /// Id of the Entity. /// - public object Id { get; set; } + public object? Id { get; set; } /// /// Creates a new object. @@ -29,6 +30,7 @@ public EntityNotFoundException() /// /// Creates a new object. /// + /// The type of the entity that was not found. public EntityNotFoundException(Type entityType) : this(entityType, null, null) { @@ -38,21 +40,26 @@ public EntityNotFoundException(Type entityType) /// /// Creates a new object. /// - public EntityNotFoundException(Type entityType, object id) + /// The type of the entity that was not found. + /// The identifier of the entity that was not found. + public EntityNotFoundException(Type entityType, object? id) : this(entityType, id, null) { } /// - /// Creates a new object. + /// Creates a new object with an auto-generated message. /// - public EntityNotFoundException(Type entityType, object id, Exception innerException) + /// The type of the entity that was not found. + /// The identifier of the entity that was not found, or null if unknown. + /// The inner exception, or null if none. + public EntityNotFoundException(Type entityType, object? id, Exception? innerException) : base( id == null ? $"There is no such an entity given given id. Entity type: {entityType.FullName}" : $"There is no such an entity. Entity type: {entityType.FullName}, id: {id}", - innerException) + innerException!) { EntityType = entityType; Id = id; diff --git a/Src/RCommon.Entities/IAuditedEntity.cs b/Src/RCommon.Entities/IAuditedEntity.cs index 8b0f44e7..b466a1f1 100644 --- a/Src/RCommon.Entities/IAuditedEntity.cs +++ b/Src/RCommon.Entities/IAuditedEntity.cs @@ -2,18 +2,47 @@ namespace RCommon.Entities { - public interface IAuditedEntity + /// + /// Defines the contract for an entity that captures audit information including + /// who created and last modified it, and when. + /// + /// The type representing the user who created the entity. + /// The type representing the user who last modified the entity. + /// + public interface IAuditedEntity : IBusinessEntity { + /// + /// Gets or sets the user who created this entity. + /// TCreatedByUser? CreatedBy { get; set; } + + /// + /// Gets or sets the date and time when this entity was created. + /// DateTime? DateCreated { get; set; } + + /// + /// Gets or sets the date and time when this entity was last modified. + /// DateTime? DateLastModified { get; set; } + + /// + /// Gets or sets the user who last modified this entity. + /// TLastModifiedByUser? LastModifiedBy { get; set; } } - public interface IAuditedEntity + /// + /// Extends with a strongly-typed primary key. + /// + /// The type of the entity's primary key. + /// The type representing the user who created the entity. + /// The type representing the user who last modified the entity. + /// + public interface IAuditedEntity : IAuditedEntity, IBusinessEntity { - + } } diff --git a/Src/RCommon.Entities/IBusinessEntity.cs b/Src/RCommon.Entities/IBusinessEntity.cs index 75406e30..c87a9fb7 100644 --- a/Src/RCommon.Entities/IBusinessEntity.cs +++ b/Src/RCommon.Entities/IBusinessEntity.cs @@ -13,14 +13,36 @@ public interface IBusinessEntity : ITrackedEntity /// /// Returns an array of ordered keys for this entity. /// - /// + /// An object array containing the entity's key values in order. object[] GetKeys(); + /// + /// Gets the read-only collection of local (transactional) events accumulated on this entity. + /// IReadOnlyCollection LocalEvents { get; } + /// + /// Adds a transactional event to this entity's local event collection. + /// + /// The serializable event to add. void AddLocalEvent(ISerializableEvent eventItem); + + /// + /// Removes all transactional events from this entity's local event collection. + /// void ClearLocalEvents(); + + /// + /// Determines whether this entity is equal to another using binary comparison. + /// + /// The other entity to compare against. + /// true if the entities are equal; otherwise, false. bool EntityEquals(IBusinessEntity other); + + /// + /// Removes a specific transactional event from this entity's local event collection. + /// + /// The serializable event to remove. void RemoveLocalEvent(ISerializableEvent eventItem); } diff --git a/Src/RCommon.Entities/IEntityEventTracker.cs b/Src/RCommon.Entities/IEntityEventTracker.cs index 8f73e508..a8e822b8 100644 --- a/Src/RCommon.Entities/IEntityEventTracker.cs +++ b/Src/RCommon.Entities/IEntityEventTracker.cs @@ -4,6 +4,11 @@ namespace RCommon.Entities { + /// + /// Defines a mechanism for tracking entities and emitting their transactional (local) events + /// through the event routing infrastructure. + /// + /// public interface IEntityEventTracker { /// @@ -14,7 +19,7 @@ public interface IEntityEventTracker /// /// Adds an entity that can be tracked for any new events associated with it. /// - /// + /// The business entity to track for transactional events. void AddEntity(IBusinessEntity entity); /// diff --git a/Src/RCommon.Entities/ITrackedEntity.cs b/Src/RCommon.Entities/ITrackedEntity.cs index 1d90e3b4..817f8b84 100644 --- a/Src/RCommon.Entities/ITrackedEntity.cs +++ b/Src/RCommon.Entities/ITrackedEntity.cs @@ -6,8 +6,16 @@ namespace RCommon.Entities { + /// + /// Marks an entity as capable of participating in event tracking. + /// When is true, the entity's local events + /// can be collected and emitted by an . + /// public interface ITrackedEntity { + /// + /// Gets or sets a value indicating whether this entity should have its events tracked. + /// bool AllowEventTracking { get; set; } } } diff --git a/Src/RCommon.Entities/InMemoryEntityEventTracker.cs b/Src/RCommon.Entities/InMemoryEntityEventTracker.cs index fa640928..9cd1db0d 100644 --- a/Src/RCommon.Entities/InMemoryEntityEventTracker.cs +++ b/Src/RCommon.Entities/InMemoryEntityEventTracker.cs @@ -8,35 +8,60 @@ namespace RCommon.Entities { + /// + /// In-memory implementation of that collects entities and + /// emits their transactional events through an . + /// + /// + /// Entities are held in memory for the lifetime of this tracker instance. When + /// is called, the tracker traverses each entity's + /// object graph to collect all local events and routes them via the configured + /// . + /// public class InMemoryEntityEventTracker : IEntityEventTracker { private readonly ICollection _businessEntities = new List(); private readonly IEventRouter _eventRouter; + /// + /// Initializes a new instance of . + /// + /// The event router used to dispatch collected transactional events. + /// Thrown when is null. public InMemoryEntityEventTracker(IEventRouter eventRouter) { this._eventRouter = eventRouter ?? throw new ArgumentNullException(nameof(eventRouter)); } + /// public void AddEntity(IBusinessEntity entity) { - Guard.Against(entity == null, $"Entity of type {entity.GetGenericTypeName()} cannot be null"); + Guard.Against(entity == null, $"Entity of type {entity?.GetGenericTypeName()} cannot be null"); - if (entity.AllowEventTracking) + // Only track entities that have opted in to event tracking + if (entity!.AllowEventTracking) { _businessEntities.Add(entity); } - + } + /// public ICollection TrackedEntities { get => _businessEntities; } + /// + /// + /// Traverses the object graph of each tracked entity to discover nested + /// instances, collects their local events, and routes all events through the . + /// public async Task EmitTransactionalEventsAsync() { + // Walk each tracked root entity and traverse its object graph for nested IBusinessEntity instances foreach (var entity in this._businessEntities) { var entityGraph = entity.TraverseGraphFor(); + // Collect local events from every entity in the graph (root + children) foreach (var graphEntity in entityGraph) { _eventRouter.AddTransactionalEvents(graphEntity.LocalEvents); diff --git a/Src/RCommon.Entities/RCommon.Entities.csproj b/Src/RCommon.Entities/RCommon.Entities.csproj index d33ace5a..ae880720 100644 --- a/Src/RCommon.Entities/RCommon.Entities.csproj +++ b/Src/RCommon.Entities/RCommon.Entities.csproj @@ -2,6 +2,7 @@ net8.0;net9.0;net10.0 + enable True RCommon.Entities https://rcommon.com diff --git a/Src/RCommon.Entities/README.md b/Src/RCommon.Entities/README.md index 329fce1b..b246633c 100644 --- a/Src/RCommon.Entities/README.md +++ b/Src/RCommon.Entities/README.md @@ -1,3 +1,92 @@ # RCommon.Entities -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 +Domain entity base classes for the RCommon framework, providing a strongly-typed `BusinessEntity` base with built-in transactional event tracking, auditing support via `AuditedEntity`, and an `IEntityEventTracker` that collects and emits entity events through the event routing infrastructure. + +## Features + +- `BusinessEntity` and `BusinessEntity` abstract base classes with composite and single-key support +- Built-in transactional (local) event accumulation on entities via `AddLocalEvent`, `RemoveLocalEvent`, and `ClearLocalEvents` +- Entity-level event notifications (`TransactionalEventAdded`, `TransactionalEventRemoved`, `TransactionalEventsCleared`) for observing event changes +- `AuditedEntity` base classes that track `CreatedBy`, `DateCreated`, `LastModifiedBy`, and `DateLastModified` with flexible user types +- `ITrackedEntity` interface for opting entities into event tracking +- `IEntityEventTracker` and `InMemoryEntityEventTracker` for collecting entity events across object graphs and routing them through `IEventRouter` +- `EntityNotFoundException` for consistent "entity not found" error handling with type and ID context + +## Installation + +```shell +dotnet add package RCommon.Entities +``` + +## Usage + +```csharp +using RCommon.Entities; +using RCommon.Models.Events; + +// Define a domain entity with a GUID key +public class Order : BusinessEntity +{ + public string ProductName { get; set; } + public int Quantity { get; set; } + + public void Submit() + { + // Add a transactional event that will be emitted on persistence + AddLocalEvent(new OrderSubmittedEvent { OrderId = Id }); + } +} + +// Define an audited entity tracking who created/modified it +public class Invoice : AuditedEntity +{ + public decimal Amount { get; set; } + public string Currency { get; set; } +} + +// Emit entity events through the event router +public class OrderService +{ + private readonly IEntityEventTracker _eventTracker; + + public OrderService(IEntityEventTracker eventTracker) + { + _eventTracker = eventTracker; + } + + public async Task ProcessOrderAsync(Order order) + { + order.Submit(); + _eventTracker.AddEntity(order); + await _eventTracker.EmitTransactionalEventsAsync(); + } +} +``` + +## Key Types + +| Type | Description | +|------|-------------| +| `IBusinessEntity` | Base entity interface with composite key support and local event collection | +| `IBusinessEntity` | Entity interface with a single strongly-typed `Id` property | +| `BusinessEntity` | Abstract base class with transactional event tracking and entity equality | +| `BusinessEntity` | Generic base class adding a typed primary key to `BusinessEntity` | +| `IAuditedEntity` | Audit contract with created/modified user and timestamp properties | +| `AuditedEntity` | Base class combining `BusinessEntity` with full audit tracking | +| `ITrackedEntity` | Marks an entity as eligible for event tracking via `AllowEventTracking` | +| `IEntityEventTracker` | Collects tracked entities and emits their transactional events | +| `InMemoryEntityEventTracker` | In-memory implementation that traverses entity object graphs and routes events | +| `EntityNotFoundException` | Exception for when an expected entity does not exist, with type and ID context | + +## Documentation + +For full documentation, visit [rcommon.com](https://rcommon.com). + +## Related Packages + +- [RCommon.Core](https://www.nuget.org/packages/RCommon.Core) - Foundation package with event bus, builder pattern, guards, and extensions +- [RCommon.Models](https://www.nuget.org/packages/RCommon.Models) - Shared models for CQRS commands, queries, events, pagination, and execution results + +## License + +Licensed under the [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0). diff --git a/Src/RCommon.Entities/TransactionalEventsChangedEventArgs.cs b/Src/RCommon.Entities/TransactionalEventsChangedEventArgs.cs index 55c0ec44..7a948d4f 100644 --- a/Src/RCommon.Entities/TransactionalEventsChangedEventArgs.cs +++ b/Src/RCommon.Entities/TransactionalEventsChangedEventArgs.cs @@ -7,15 +7,32 @@ namespace RCommon.Entities { + /// + /// Provides data for events raised when a transactional (local) event is added to + /// or removed from a . + /// + /// public class TransactionalEventsChangedEventArgs : EventArgs { + /// + /// Initializes a new instance of . + /// + /// The entity on which the transactional event changed. + /// The serializable event that was added or removed. public TransactionalEventsChangedEventArgs(IBusinessEntity entity, ISerializableEvent eventData) { Entity=entity; EventData=eventData; } + /// + /// Gets the entity on which the transactional event changed. + /// public IBusinessEntity Entity { get; } + + /// + /// Gets the serializable event that was added or removed. + /// public ISerializableEvent EventData { get; } } } diff --git a/Src/RCommon.Entities/TransactionalEventsClearedEventArgs.cs b/Src/RCommon.Entities/TransactionalEventsClearedEventArgs.cs index ce52c837..61d01ca0 100644 --- a/Src/RCommon.Entities/TransactionalEventsClearedEventArgs.cs +++ b/Src/RCommon.Entities/TransactionalEventsClearedEventArgs.cs @@ -7,13 +7,25 @@ namespace RCommon.Entities { + /// + /// Provides data for the event raised when all transactional (local) events + /// are cleared from a . + /// + /// public class TransactionalEventsClearedEventArgs : EventArgs { + /// + /// Initializes a new instance of . + /// + /// The entity whose transactional events were cleared. public TransactionalEventsClearedEventArgs(IBusinessEntity entity) { Entity = entity; } + /// + /// Gets the entity whose transactional events were cleared. + /// public IBusinessEntity Entity { get; } } } diff --git a/Src/RCommon.FluentValidation/FluentValidationBuilder.cs b/Src/RCommon.FluentValidation/FluentValidationBuilder.cs index 9c79c6f6..c4ef86ee 100644 --- a/Src/RCommon.FluentValidation/FluentValidationBuilder.cs +++ b/Src/RCommon.FluentValidation/FluentValidationBuilder.cs @@ -9,19 +9,37 @@ namespace RCommon.FluentValidation { + /// + /// Default implementation of that registers + /// the as the + /// in the DI container. + /// + /// + /// public class FluentValidationBuilder : IFluentValidationBuilder { + /// + /// Initializes a new instance of and registers + /// FluentValidation services into the DI container. + /// + /// The RCommon builder providing access to the . public FluentValidationBuilder(IRCommonBuilder builder) { Services = builder.Services; this.RegisterServices(Services); } + /// + /// Registers the as the + /// implementation with a scoped lifetime. + /// + /// The service collection to register into. protected void RegisterServices(IServiceCollection services) { services.AddScoped(); } + /// public IServiceCollection Services { get; } } } diff --git a/Src/RCommon.FluentValidation/FluentValidationBuilderExtensions.cs b/Src/RCommon.FluentValidation/FluentValidationBuilderExtensions.cs index 4442a94b..d8a937ff 100644 --- a/Src/RCommon.FluentValidation/FluentValidationBuilderExtensions.cs +++ b/Src/RCommon.FluentValidation/FluentValidationBuilderExtensions.cs @@ -11,9 +11,19 @@ namespace RCommon.ApplicationServices { + /// + /// Provides extension methods on for registering + /// FluentValidation validators into the DI container. + /// public static class FluentValidationBuilderExtensions { + /// + /// Registers a specific FluentValidation validator for the given type with a scoped lifetime. + /// + /// The type being validated. + /// The implementation to register. + /// The FluentValidation builder instance. public static void AddValidator(this IFluentValidationBuilder builder) where TValidator : class, IValidator where T : class @@ -21,22 +31,47 @@ public static void AddValidator(this IFluentValidationBuilder bui builder.Services.AddScoped, TValidator>(); } - public static void AddValidatorsFromAssembly(this IFluentValidationBuilder builder, Assembly assembly, - ServiceLifetime lifetime = ServiceLifetime.Scoped, Func filter = null, + /// + /// Scans the specified assembly and registers all implementations found. + /// + /// The FluentValidation builder instance. + /// The assembly to scan for validator types. + /// The service lifetime for registered validators. Defaults to . + /// An optional filter to include or exclude specific scan results. + /// Whether to include internal (non-public) validator types. Defaults to . + public static void AddValidatorsFromAssembly(this IFluentValidationBuilder builder, Assembly assembly, + ServiceLifetime lifetime = ServiceLifetime.Scoped, Func? filter = null, bool includeInternalTypes = false) { builder.Services.AddValidatorsFromAssembly(assembly, lifetime, filter, includeInternalTypes); } + /// + /// Scans multiple assemblies and registers all implementations found. + /// + /// The FluentValidation builder instance. + /// The assemblies to scan for validator types. + /// The service lifetime for registered validators. Defaults to . + /// An optional filter to include or exclude specific scan results. + /// Whether to include internal (non-public) validator types. Defaults to . public static void AddValidatorsFromAssemblies(this IFluentValidationBuilder builder, IEnumerable assemblies, ServiceLifetime lifetime = ServiceLifetime.Scoped, - Func filter = null, bool includeInternalTypes = false) + Func? filter = null, bool includeInternalTypes = false) { builder.Services.AddValidatorsFromAssemblies(assemblies, lifetime, filter, includeInternalTypes); } + /// + /// Scans the assembly containing the specified and registers all + /// implementations found. + /// + /// The FluentValidation builder instance. + /// A type whose containing assembly will be scanned. + /// The service lifetime for registered validators. Defaults to . + /// An optional filter to include or exclude specific scan results. + /// Whether to include internal (non-public) validator types. Defaults to . public static void AddValidatorsFromAssemblyContaining(this IFluentValidationBuilder builder, Type type, - ServiceLifetime lifetime = ServiceLifetime.Scoped, Func filter = null, + ServiceLifetime lifetime = ServiceLifetime.Scoped, Func? filter = null, bool includeInternalTypes = false) { builder.Services.AddValidatorsFromAssemblyContaining(type, lifetime, filter, includeInternalTypes); diff --git a/Src/RCommon.FluentValidation/FluentValidationProvider.cs b/Src/RCommon.FluentValidation/FluentValidationProvider.cs index 212613d5..364820d2 100644 --- a/Src/RCommon.FluentValidation/FluentValidationProvider.cs +++ b/Src/RCommon.FluentValidation/FluentValidationProvider.cs @@ -12,17 +12,44 @@ namespace RCommon.FluentValidation { - public class FluentValidationProvider : IValidationProvider + /// + /// Implements using the FluentValidation library. + /// Resolves registered instances from the DI container + /// and executes them against the target object. + /// + /// + /// + public class FluentValidationProvider : IValidationProvider { private readonly ILogger _logger; private readonly IServiceProvider _serviceProvider; + /// + /// Initializes a new instance of . + /// + /// The logger for recording validation warnings and errors. + /// The service provider used to resolve validator instances. public FluentValidationProvider(ILogger logger, IServiceProvider serviceProvider) { _logger = logger; _serviceProvider = serviceProvider; } + /// + /// Validates the specified target object by resolving and executing all registered + /// instances for the target's type. + /// + /// The type of the object being validated. + /// The object to validate. + /// + /// When , a + /// is thrown if any validation failures are found. + /// + /// A token to cancel the async operation. + /// A containing any validation faults. + /// + /// Thrown when validation fails and is . + /// public async Task ValidateAsync(T target, bool throwOnFaults, CancellationToken cancellationToken = default) where T : class { @@ -30,13 +57,17 @@ public async Task ValidateAsync(T target, bool throwOnFaul using (var scope = _serviceProvider.CreateScope()) { + // Build the closed generic IValidator type using the runtime type of the target + // so that validators registered for derived types are also resolved var type = target.GetType(); var validatorType = typeof(IValidator<>).MakeGenericType(type); var untypedValidators = scope.ServiceProvider.GetServices(validatorType); - + Guard.IsNotNull(untypedValidators, nameof(untypedValidators)); - var validationResults = await ExecuteValidationAsync(target, untypedValidators, cancellationToken); // TODO: Need a better way than passing in object[] + var validationResults = await ExecuteValidationAsync(target, untypedValidators!, cancellationToken); // TODO: Need a better way than passing in object[] + + // Flatten all validation errors from all validators into a single list var failures = validationResults.SelectMany(r => r.Errors).Where(f => f != null).ToList(); if (failures.Count != 0) @@ -44,6 +75,7 @@ public async Task ValidateAsync(T target, bool throwOnFaul _logger.LogWarning("Validation errors - {CommandType} - Command: {@Command} - Errors: {@ValidationErrors}", target.GetGenericTypeName(), target, failures); string message = $"Validation Errors"; + // Map FluentValidation failures to RCommon ValidationFault instances var faults = new List(); foreach (var failure in failures) { @@ -62,17 +94,28 @@ public async Task ValidateAsync(T target, bool throwOnFaul return outcome; } + /// + /// Executes all provided validators against the target object concurrently. + /// + /// The type of the object being validated. + /// The object to validate. + /// The collection of untyped validator instances resolved from DI. + /// A token to cancel the async operation. + /// An array of from all executed validators. private async Task ExecuteValidationAsync(T target, IEnumerable validators, CancellationToken cancellationToken = default) where T : class { if (validators.Any()) - { + { var context = new ValidationContext(target); + + // Run all validators in parallel via Task.WhenAll, casting each to the non-generic IValidator interface var validationResults = await Task.WhenAll(validators.Select(v => ((IValidator)v).ValidateAsync(context, cancellationToken))); return validationResults; } else { + // No validators registered for this type; return an empty result set return new List().ToArray(); } } diff --git a/Src/RCommon.FluentValidation/IFluentValidationBuilder.cs b/Src/RCommon.FluentValidation/IFluentValidationBuilder.cs index ade2711b..9efa232d 100644 --- a/Src/RCommon.FluentValidation/IFluentValidationBuilder.cs +++ b/Src/RCommon.FluentValidation/IFluentValidationBuilder.cs @@ -3,8 +3,17 @@ namespace RCommon.FluentValidation { + /// + /// Builder interface for configuring validation using the FluentValidation library + /// within the RCommon framework. + /// + /// + /// public interface IFluentValidationBuilder : IValidationBuilder { - IServiceCollection Services { get; } + /// + /// Gets the used to register FluentValidation services and validators. + /// + new IServiceCollection Services { get; } } } diff --git a/Src/RCommon.FluentValidation/README.md b/Src/RCommon.FluentValidation/README.md index b06a181f..68ccf5c0 100644 --- a/Src/RCommon.FluentValidation/README.md +++ b/Src/RCommon.FluentValidation/README.md @@ -1,3 +1,104 @@ # RCommon.FluentValidation -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 +FluentValidation integration for RCommon's `IValidationProvider` abstraction. This package bridges the FluentValidation library into RCommon's validation pipeline, resolving registered `IValidator` instances from the DI container and executing them with support for automatic CQRS command/query validation. + +## Features + +- Implements `IValidationProvider` using the FluentValidation library +- Resolves and executes all registered `IValidator` instances for a given type from DI +- Runs multiple validators concurrently via `Task.WhenAll` +- Maps FluentValidation failures to RCommon's `ValidationOutcome` and `ValidationFault` types +- Supports optional automatic validation of CQRS commands and queries via `CqrsValidationOptions` +- Assembly scanning to auto-register all validators in one or more assemblies +- Configurable `throwOnFaults` behavior to throw `ValidationException` on failure +- Registered as a scoped service in the DI container + +## Installation + +```shell +dotnet add package RCommon.FluentValidation +``` + +## Usage + +Register FluentValidation through the RCommon builder and add your validators: + +```csharp +using RCommon; +using RCommon.FluentValidation; + +services.AddRCommon(builder => +{ + builder.WithValidation(validation => + { + validation.AddValidator(); + + // Or scan an assembly for all validators + validation.AddValidatorsFromAssembly(typeof(CreateOrderDtoValidator).Assembly); + }); +}); +``` + +To enable automatic validation in the CQRS pipeline: + +```csharp +services.AddRCommon(builder => +{ + builder.WithValidation(options => + { + options.ValidateCommands = true; + options.ValidateQueries = true; + }); +}); +``` + +Inject and use `IValidationProvider` directly when needed: + +```csharp +public class OrderService +{ + private readonly IValidationProvider _validator; + + public OrderService(IValidationProvider validator) + { + _validator = validator; + } + + public async Task CreateOrder(CreateOrderDto dto) + { + var outcome = await _validator.ValidateAsync(dto, throwOnFaults: true); + + // If throwOnFaults is false, inspect the outcome manually + if (!outcome.IsValid) + { + foreach (var fault in outcome.Errors) + { + Console.WriteLine($"{fault.PropertyName}: {fault.ErrorMessage}"); + } + } + } +} +``` + +## Key Types + +| Type | Description | +|------|-------------| +| `FluentValidationProvider` | `IValidationProvider` implementation that resolves and runs FluentValidation validators | +| `FluentValidationBuilder` | Registers `FluentValidationProvider` into the DI container | +| `IFluentValidationBuilder` | Builder interface exposing `IServiceCollection` for validator registration | +| `FluentValidationBuilderExtensions` | Provides `AddValidator()`, `AddValidatorsFromAssembly()`, and assembly scanning methods | +| `ValidationBuilderExtensions` | Provides `WithValidation()` on `IRCommonBuilder` for pipeline registration | + +## Documentation + +For full documentation, visit [rcommon.com](https://rcommon.com). + +## Related Packages + +- [RCommon.ApplicationServices](https://www.nuget.org/packages/RCommon.ApplicationServices) - Core validation abstractions (IValidationProvider, ValidationOutcome, ValidationFault) +- [RCommon.Core](https://www.nuget.org/packages/RCommon.Core) - RCommon framework core and DI builder + +## License + +Licensed under the [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0). diff --git a/Src/RCommon.FluentValidation/ValidationBuilderExtensions.cs b/Src/RCommon.FluentValidation/ValidationBuilderExtensions.cs index 2fffe14f..37cbc065 100644 --- a/Src/RCommon.FluentValidation/ValidationBuilderExtensions.cs +++ b/Src/RCommon.FluentValidation/ValidationBuilderExtensions.cs @@ -7,9 +7,21 @@ namespace RCommon.ApplicationServices { + /// + /// Provides extension methods on for registering validation + /// into the RCommon configuration pipeline. + /// public static class ValidationBuilderExtensions { + /// + /// Registers validation using the specified builder with default + /// . + /// + /// An implementation such as + /// FluentValidationBuilder. + /// The RCommon builder instance. + /// The for further chaining. public static IRCommonBuilder WithValidation(this IRCommonBuilder builder) where T : IValidationBuilder { @@ -17,12 +29,25 @@ public static IRCommonBuilder WithValidation(this IRCommonBuilder builder) return WithValidation(builder, x => { }); } + /// + /// Registers validation using the specified builder and configures + /// for CQRS pipeline validation behavior. + /// + /// An implementation. + /// The RCommon builder instance. + /// An action to configure . + /// The for further chaining. + /// + /// This method uses to instantiate the builder, + /// passing the as the constructor argument. The builder's constructor + /// is expected to register its validation services into the DI container. + /// public static IRCommonBuilder WithValidation(this IRCommonBuilder builder, Action actions) where T : IValidationBuilder { - // Event Handling Configurations - var cqrsBuilder = (T)Activator.CreateInstance(typeof(T), new object[] { builder }); + // Instantiate the validation builder via reflection; the constructor registers validation services into DI + var cqrsBuilder = (T)Activator.CreateInstance(typeof(T), new object[] { builder })!; builder.Services.Configure(actions); return builder; } diff --git a/Src/RCommon.Json/IJsonBuilder.cs b/Src/RCommon.Json/IJsonBuilder.cs index ff4ee589..a3875737 100644 --- a/Src/RCommon.Json/IJsonBuilder.cs +++ b/Src/RCommon.Json/IJsonBuilder.cs @@ -7,8 +7,17 @@ namespace RCommon.Json { + /// + /// Defines the contract for configuring JSON serialization within the RCommon framework. + /// Implementations register a specific JSON serialization library (e.g., Newtonsoft.Json or System.Text.Json) + /// into the dependency injection container. + /// + /// public interface IJsonBuilder { + /// + /// Gets the used to register JSON serialization services. + /// IServiceCollection Services { get; } } } diff --git a/Src/RCommon.Json/IJsonSerializer.cs b/Src/RCommon.Json/IJsonSerializer.cs index b53cc0bb..efb8189a 100644 --- a/Src/RCommon.Json/IJsonSerializer.cs +++ b/Src/RCommon.Json/IJsonSerializer.cs @@ -6,14 +6,47 @@ namespace RCommon.Json { + /// + /// Provides an abstraction for JSON serialization and deserialization operations. + /// Implementations wrap a specific JSON library (e.g., Newtonsoft.Json or System.Text.Json). + /// + /// + /// public interface IJsonSerializer { + /// + /// Serializes the specified object to a JSON string. + /// + /// The object to serialize. + /// Optional serialization options such as camel casing and indentation. + /// A JSON string representation of the object. public string Serialize(object obj, JsonSerializeOptions? options = null); + /// + /// Serializes the specified object to a JSON string using the provided type information. + /// + /// The object to serialize. + /// The to use during serialization, which may differ from the runtime type. + /// Optional serialization options such as camel casing and indentation. + /// A JSON string representation of the object. public string Serialize(object obj, Type type, JsonSerializeOptions? options = null); - public T Deserialize(string json, JsonDeserializeOptions? options = null); + /// + /// Deserializes a JSON string to an object of type . + /// + /// The target type to deserialize to. + /// The JSON string to deserialize. + /// Optional deserialization options such as camel casing. + /// The deserialized object of type , or null if the JSON represents a null value. + public T? Deserialize(string json, JsonDeserializeOptions? options = null); - public object Deserialize(string json, Type type, JsonDeserializeOptions? options = null); + /// + /// Deserializes a JSON string to an object of the specified . + /// + /// The JSON string to deserialize. + /// The to deserialize the JSON into. + /// Optional deserialization options such as camel casing. + /// The deserialized object, or null if the JSON represents a null value. + public object? Deserialize(string json, Type type, JsonDeserializeOptions? options = null); } } diff --git a/Src/RCommon.Json/JsonBuilderExtensions.cs b/Src/RCommon.Json/JsonBuilderExtensions.cs index 940b52bc..53077341 100644 --- a/Src/RCommon.Json/JsonBuilderExtensions.cs +++ b/Src/RCommon.Json/JsonBuilderExtensions.cs @@ -8,14 +8,37 @@ namespace RCommon.Json { + /// + /// Provides extension methods on for registering JSON serialization + /// into the RCommon configuration pipeline. + /// + /// + /// All overloads delegate to the primary overload that accepts serialize options, deserialize options, + /// and a builder configuration action. Overloads that omit parameters supply no-op defaults. + /// public static class JsonBuilderExtensions { + /// + /// Registers JSON serialization using the specified builder with default options. + /// + /// An implementation such as + /// JsonNetBuilder or TextJsonBuilder. + /// The RCommon builder instance. + /// The for further chaining. public static IRCommonBuilder WithJsonSerialization(this IRCommonBuilder builder) where T : IJsonBuilder { return WithJsonSerialization(builder, x => { }, x => { }, x => { }); } + /// + /// Registers JSON serialization with custom serialize and deserialize options. + /// + /// An implementation. + /// The RCommon builder instance. + /// An action to configure . + /// An action to configure . + /// The for further chaining. public static IRCommonBuilder WithJsonSerialization(this IRCommonBuilder builder, Action serializeOptions, Action deSerializeOptions) where T : IJsonBuilder @@ -23,12 +46,26 @@ public static IRCommonBuilder WithJsonSerialization(this IRCommonBuilder buil return WithJsonSerialization(builder, serializeOptions, deSerializeOptions, x => { }); } + /// + /// Registers JSON serialization with custom serialize options only. + /// + /// An implementation. + /// The RCommon builder instance. + /// An action to configure . + /// The for further chaining. public static IRCommonBuilder WithJsonSerialization(this IRCommonBuilder builder, Action serializeOptions) where T : IJsonBuilder { return WithJsonSerialization(builder, serializeOptions, x => { }, x => { }); } + /// + /// Registers JSON serialization with custom deserialize options only. + /// + /// An implementation. + /// The RCommon builder instance. + /// An action to configure . + /// The for further chaining. public static IRCommonBuilder WithJsonSerialization(this IRCommonBuilder builder, Action deSerializeOptions) where T : IJsonBuilder @@ -36,6 +73,13 @@ public static IRCommonBuilder WithJsonSerialization(this IRCommonBuilder buil return WithJsonSerialization(builder, x => { }, deSerializeOptions, x => { }); } + /// + /// Registers JSON serialization with a custom builder configuration action. + /// + /// An implementation. + /// The RCommon builder instance. + /// An action to further configure the builder instance. + /// The for further chaining. public static IRCommonBuilder WithJsonSerialization(this IRCommonBuilder builder, Action actions) where T : IJsonBuilder { @@ -43,7 +87,22 @@ public static IRCommonBuilder WithJsonSerialization(this IRCommonBuilder buil return WithJsonSerialization(builder, x => { }, x => { }, actions); } - public static IRCommonBuilder WithJsonSerialization(this IRCommonBuilder builder, Action serializeOptions, + /// + /// Primary overload that registers JSON serialization with full control over serialize options, + /// deserialize options, and builder-specific configuration. + /// + /// An implementation. + /// The RCommon builder instance. + /// An action to configure . + /// An action to configure . + /// An action to further configure the builder instance. + /// The for further chaining. + /// + /// This method uses to instantiate the builder, + /// passing the as the constructor argument. The builder's constructor + /// is expected to register its serialization services into the DI container. + /// + public static IRCommonBuilder WithJsonSerialization(this IRCommonBuilder builder, Action serializeOptions, Action deSerializeOptions, Action actions) where T : IJsonBuilder { @@ -51,7 +110,9 @@ public static IRCommonBuilder WithJsonSerialization(this IRCommonBuilder buil Guard.IsNotNull(deSerializeOptions, nameof(deSerializeOptions)); Guard.IsNotNull(actions, nameof(actions)); builder.Services.Configure(serializeOptions); - var jsonConfig = (T)Activator.CreateInstance(typeof(T), new object[] { builder }); + + // Instantiate the builder via reflection; the constructor registers serializer services into DI + var jsonConfig = (T)Activator.CreateInstance(typeof(T), new object[] { builder })!; actions(jsonConfig); return builder; } diff --git a/Src/RCommon.Json/JsonDeserializeOptions.cs b/Src/RCommon.Json/JsonDeserializeOptions.cs index 6cb9fc60..3568440f 100644 --- a/Src/RCommon.Json/JsonDeserializeOptions.cs +++ b/Src/RCommon.Json/JsonDeserializeOptions.cs @@ -6,13 +6,26 @@ namespace RCommon.Json { + /// + /// Configuration options applied during JSON deserialization. + /// + /// + /// public class JsonDeserializeOptions { + /// + /// Initializes a new instance of with default values. + /// defaults to . + /// public JsonDeserializeOptions() { - this.CamelCase = true; + this.CamelCase = true; } + /// + /// Gets or sets a value indicating whether property names should use camelCase naming policy + /// during deserialization. Defaults to . + /// public bool CamelCase { get; set; } } } diff --git a/Src/RCommon.Json/JsonSerializeOptions.cs b/Src/RCommon.Json/JsonSerializeOptions.cs index 2996532c..ef1f56ce 100644 --- a/Src/RCommon.Json/JsonSerializeOptions.cs +++ b/Src/RCommon.Json/JsonSerializeOptions.cs @@ -6,15 +6,33 @@ namespace RCommon.Json { + /// + /// Configuration options applied during JSON serialization. + /// + /// + /// public class JsonSerializeOptions { + /// + /// Initializes a new instance of with default values. + /// defaults to and defaults to . + /// public JsonSerializeOptions() { this.CamelCase = true; this.Indented = false; } + /// + /// Gets or sets a value indicating whether property names should use camelCase naming policy + /// during serialization. Defaults to . + /// public bool CamelCase { get; set; } + + /// + /// Gets or sets a value indicating whether the serialized JSON output should be indented + /// for readability. Defaults to . + /// public bool Indented { get; set; } } } diff --git a/Src/RCommon.Json/RCommon.Json.csproj b/Src/RCommon.Json/RCommon.Json.csproj index 604fa240..b015fae6 100644 --- a/Src/RCommon.Json/RCommon.Json.csproj +++ b/Src/RCommon.Json/RCommon.Json.csproj @@ -2,6 +2,7 @@ net8.0;net9.0;net10.0 + enable True RCommon.Json https://rcommon.com diff --git a/Src/RCommon.Json/README.md b/Src/RCommon.Json/README.md index b5d234cc..5eb99a1d 100644 --- a/Src/RCommon.Json/README.md +++ b/Src/RCommon.Json/README.md @@ -1,3 +1,84 @@ - # RCommon.Json +# RCommon.Json -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 JSON serialization abstraction layer for RCommon. This package defines the `IJsonSerializer` interface and configuration options, allowing your application to serialize and deserialize JSON without coupling to a specific library. + +## Features + +- `IJsonSerializer` interface with generic and non-generic serialize/deserialize methods +- `JsonSerializeOptions` for controlling camelCase naming and indented output +- `JsonDeserializeOptions` for controlling camelCase naming during deserialization +- `IJsonBuilder` interface for pluggable DI registration of JSON providers +- Fluent `WithJsonSerialization()` extension method on `IRCommonBuilder` for easy setup + +## Installation + +```shell +dotnet add package RCommon.Json +``` + +## Usage + +This package is typically not used directly. Instead, install a concrete implementation such as `RCommon.JsonNet` or `RCommon.SystemTextJson` and register it through the RCommon builder: + +```csharp +using RCommon; +using RCommon.Json; + +services.AddRCommon(builder => +{ + // Use one of the concrete implementations: + // builder.WithJsonSerialization(); + // builder.WithJsonSerialization(); +}); +``` + +Once registered, inject `IJsonSerializer` anywhere in your application: + +```csharp +public class MyService +{ + private readonly IJsonSerializer _serializer; + + public MyService(IJsonSerializer serializer) + { + _serializer = serializer; + } + + public string ToJson(Order order) + { + return _serializer.Serialize(order, new JsonSerializeOptions + { + CamelCase = true, + Indented = true + }); + } + + public Order FromJson(string json) + { + return _serializer.Deserialize(json); + } +} +``` + +## Key Types + +| Type | Description | +|------|-------------| +| `IJsonSerializer` | Abstraction for JSON serialization and deserialization operations | +| `JsonSerializeOptions` | Options for camelCase naming and indented formatting during serialization | +| `JsonDeserializeOptions` | Options for camelCase naming during deserialization | +| `IJsonBuilder` | Builder interface for registering a JSON provider into DI | +| `JsonBuilderExtensions` | Extension methods providing `WithJsonSerialization()` on `IRCommonBuilder` | + +## Documentation + +For full documentation, visit [rcommon.com](https://rcommon.com). + +## Related Packages + +- [RCommon.JsonNet](https://www.nuget.org/packages/RCommon.JsonNet) - Newtonsoft.Json implementation of IJsonSerializer +- [RCommon.SystemTextJson](https://www.nuget.org/packages/RCommon.SystemTextJson) - System.Text.Json implementation of IJsonSerializer + +## License + +Licensed under the [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0). diff --git a/Src/RCommon.JsonNet/IJsonNetBuilder.cs b/Src/RCommon.JsonNet/IJsonNetBuilder.cs index 4d62325d..89b5902c 100644 --- a/Src/RCommon.JsonNet/IJsonNetBuilder.cs +++ b/Src/RCommon.JsonNet/IJsonNetBuilder.cs @@ -7,6 +7,11 @@ namespace RCommon.JsonNet { + /// + /// Builder interface for configuring JSON serialization using the Newtonsoft.Json (Json.NET) library. + /// + /// + /// public interface IJsonNetBuilder : IJsonBuilder { } diff --git a/Src/RCommon.JsonNet/IJsonNetBuilderExtensions.cs b/Src/RCommon.JsonNet/IJsonNetBuilderExtensions.cs index 9c59c7dc..bf56d47a 100644 --- a/Src/RCommon.JsonNet/IJsonNetBuilderExtensions.cs +++ b/Src/RCommon.JsonNet/IJsonNetBuilderExtensions.cs @@ -9,8 +9,17 @@ namespace RCommon.JsonNet { + /// + /// Provides extension methods for to configure Newtonsoft.Json settings. + /// public static class IJsonNetBuilderExtensions { + /// + /// Configures the underlying used by the Newtonsoft.Json serializer. + /// + /// The Json.NET builder instance. + /// An action to configure . + /// The for further chaining. public static IJsonNetBuilder Configure(this IJsonNetBuilder builder, Action options) { builder.Services.Configure(options); diff --git a/Src/RCommon.JsonNet/JsonNetBuilder.cs b/Src/RCommon.JsonNet/JsonNetBuilder.cs index 77d3b785..c5d1b08b 100644 --- a/Src/RCommon.JsonNet/JsonNetBuilder.cs +++ b/Src/RCommon.JsonNet/JsonNetBuilder.cs @@ -8,19 +8,35 @@ namespace RCommon.JsonNet { + /// + /// Default implementation of that registers + /// the Newtonsoft.Json-based into the DI container. + /// + /// + /// public class JsonNetBuilder : IJsonNetBuilder { + /// + /// Initializes a new instance of and registers JSON serialization services. + /// + /// The RCommon builder providing access to the . public JsonNetBuilder(IRCommonBuilder builder) { Services = builder.Services; this.RegisterServices(Services); } + /// + /// Registers the as the implementation + /// with a transient lifetime. + /// + /// The service collection to register into. protected void RegisterServices(IServiceCollection services) { services.AddTransient(); } + /// public IServiceCollection Services { get; } } } diff --git a/Src/RCommon.JsonNet/JsonNetSerializer.cs b/Src/RCommon.JsonNet/JsonNetSerializer.cs index 4c551423..2dba230c 100644 --- a/Src/RCommon.JsonNet/JsonNetSerializer.cs +++ b/Src/RCommon.JsonNet/JsonNetSerializer.cs @@ -10,17 +10,34 @@ namespace RCommon.JsonNet { + /// + /// Implements using the Newtonsoft.Json (Json.NET) library. + /// Supports per-call overrides for camel-case naming and indented formatting through + /// and . + /// + /// + /// The underlying are injected via the options pattern + /// and may be mutated per-call when options are provided. This means per-call options + /// modify the shared settings instance. + /// public class JsonNetSerializer : IJsonSerializer { private readonly JsonSerializerSettings _settings; + /// + /// Initializes a new instance of with the configured + /// . + /// + /// The injected Newtonsoft.Json serializer settings. public JsonNetSerializer(IOptions options) { _settings = options.Value; } - public T Deserialize(string json, JsonDeserializeOptions? options = null) + /// + public T? Deserialize(string json, JsonDeserializeOptions? options = null) { + // Apply camelCase contract resolver when requested if (options != null && options.CamelCase) { _settings.ContractResolver = new CamelCasePropertyNamesContractResolver(); @@ -28,8 +45,10 @@ public T Deserialize(string json, JsonDeserializeOptions? options = null) return JsonConvert.DeserializeObject(json, _settings); } - public object Deserialize(string json, Type type, JsonDeserializeOptions? options = null) + /// + public object? Deserialize(string json, Type type, JsonDeserializeOptions? options = null) { + // Apply camelCase contract resolver when requested if (options != null && options.CamelCase) { _settings.ContractResolver = new CamelCasePropertyNamesContractResolver(); @@ -37,28 +56,34 @@ public object Deserialize(string json, Type type, JsonDeserializeOptions? option return JsonConvert.DeserializeObject(json, type, _settings); } + /// public string Serialize(object obj, JsonSerializeOptions? options = null) { + // Apply camelCase contract resolver when requested if (options != null && options.CamelCase) { _settings.ContractResolver = new CamelCasePropertyNamesContractResolver(); } + // Enable indented formatting when requested if (options != null && options.Indented) { _settings.Formatting = Formatting.Indented; } - + return JsonConvert.SerializeObject(obj, _settings); } + /// public string Serialize(object obj, Type type, JsonSerializeOptions? options = null) { + // Apply camelCase contract resolver when requested if (options != null && options.CamelCase) { _settings.ContractResolver = new CamelCasePropertyNamesContractResolver(); } + // Enable indented formatting when requested if (options != null && options.Indented) { _settings.Formatting = Formatting.Indented; diff --git a/Src/RCommon.JsonNet/RCommon.JsonNet.csproj b/Src/RCommon.JsonNet/RCommon.JsonNet.csproj index 5f6c190a..5d06a50e 100644 --- a/Src/RCommon.JsonNet/RCommon.JsonNet.csproj +++ b/Src/RCommon.JsonNet/RCommon.JsonNet.csproj @@ -2,6 +2,7 @@ net8.0;net9.0;net10.0 + enable True RCommon.JsonNet https://rcommon.com diff --git a/Src/RCommon.JsonNet/README.md b/Src/RCommon.JsonNet/README.md index 406fef59..eb2820a7 100644 --- a/Src/RCommon.JsonNet/README.md +++ b/Src/RCommon.JsonNet/README.md @@ -1,3 +1,79 @@ # RCommon.JsonNet -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 +Newtonsoft.Json (Json.NET) implementation of RCommon's `IJsonSerializer` abstraction. This package registers `JsonNetSerializer` into the dependency injection container and provides fluent configuration of `JsonSerializerSettings`. + +## Features + +- Implements `IJsonSerializer` using Newtonsoft.Json for serialization and deserialization +- Per-call options for camelCase property naming and indented formatting +- Fluent configuration of `JsonSerializerSettings` through the builder pattern +- Integrates with RCommon's `AddRCommon()` / `WithJsonSerialization()` pipeline +- Registered as a transient service in the DI container + +## Installation + +```shell +dotnet add package RCommon.JsonNet +``` + +## Usage + +Register the Newtonsoft.Json serializer through the RCommon builder: + +```csharp +using RCommon; +using RCommon.JsonNet; + +services.AddRCommon(builder => +{ + builder.WithJsonSerialization(serializer => + { + serializer.Configure(settings => + { + settings.NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore; + settings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore; + }); + }); +}); +``` + +Then inject and use `IJsonSerializer` in your services: + +```csharp +public class OrderService +{ + private readonly IJsonSerializer _serializer; + + public OrderService(IJsonSerializer serializer) + { + _serializer = serializer; + } + + public string SerializeOrder(Order order) + { + return _serializer.Serialize(order); + } +} +``` + +## Key Types + +| Type | Description | +|------|-------------| +| `JsonNetSerializer` | `IJsonSerializer` implementation backed by Newtonsoft.Json | +| `JsonNetBuilder` | Registers `JsonNetSerializer` into the DI container | +| `IJsonNetBuilder` | Builder interface for Newtonsoft.Json-specific configuration | +| `IJsonNetBuilderExtensions` | Provides `Configure(Action)` for customizing serializer settings | + +## Documentation + +For full documentation, visit [rcommon.com](https://rcommon.com). + +## Related Packages + +- [RCommon.Json](https://www.nuget.org/packages/RCommon.Json) - JSON serialization abstractions (IJsonSerializer, options) +- [RCommon.SystemTextJson](https://www.nuget.org/packages/RCommon.SystemTextJson) - Alternative implementation using System.Text.Json + +## License + +Licensed under the [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0). diff --git a/Src/RCommon.Linq2Db/Crud/Linq2DbRepository.cs b/Src/RCommon.Linq2Db/Crud/Linq2DbRepository.cs index 2246ee68..6faa1618 100644 --- a/Src/RCommon.Linq2Db/Crud/Linq2DbRepository.cs +++ b/Src/RCommon.Linq2Db/Crud/Linq2DbRepository.cs @@ -20,13 +20,30 @@ namespace RCommon.Persistence.Linq2Db.Crud { + /// + /// A concrete repository implementation using Linq2Db for CRUD operations and LINQ-based querying. + /// + /// The entity type managed by this repository. Must implement . + /// + /// Queries are built against from the underlying . + /// Supports eager loading via and + /// using Linq2Db's LoadWith/ThenLoad API. + /// public class Linq2DbRepository : LinqRepositoryBase where TEntity : class, IBusinessEntity { - private IQueryable _repositoryQuery; - private ILoadWithQueryable _includableQueryable; + private IQueryable? _repositoryQuery; + private ILoadWithQueryable? _includableQueryable; private readonly IDataStoreFactory _dataStoreFactory; + /// + /// Initializes a new instance of . + /// + /// Factory used to resolve the for the configured data store. + /// Factory for creating loggers scoped to this repository type. + /// Tracker used to register entities for domain event dispatching. + /// Options specifying which data store to use when none is explicitly set. + /// Thrown when any parameter is null. public Linq2DbRepository(IDataStoreFactory dataStoreFactory, ILoggerFactory logger, IEntityEventTracker eventTracker, IOptions defaultDataStoreOptions) @@ -54,6 +71,9 @@ public Linq2DbRepository(IDataStoreFactory dataStoreFactory, } + /// + /// Gets the for the configured data store, resolved through the . + /// protected internal RCommonDataConnection DataConnection { get @@ -62,6 +82,9 @@ protected internal RCommonDataConnection DataConnection } } + /// + /// Gets the Linq2Db from the current for direct table operations. + /// protected ITable ObjectSet { get @@ -70,19 +93,36 @@ protected ITable ObjectSet } } + /// + /// Adds an eager-loading path for the specified navigation property using Linq2Db's LoadWith API. + /// + /// An expression selecting the navigation property to include. + /// This repository instance for fluent chaining of additional includes. public override IEagerLoadableQueryable Include(Expression> path) { - _includableQueryable = RepositoryQuery.LoadWith(path); + _includableQueryable = RepositoryQuery.LoadWith(path!); return this; } + /// + /// Adds a subsequent eager-loading path for a nested navigation property after a prior call, + /// using Linq2Db's ThenLoad API. + /// + /// The type of the previously included navigation property. + /// The type of the nested navigation property to include. + /// An expression selecting the nested navigation property to include. + /// This repository instance for fluent chaining. public override IEagerLoadableQueryable ThenInclude(Expression> path) { - _repositoryQuery = _includableQueryable.ThenLoad(path); + _repositoryQuery = _includableQueryable!.ThenLoad(path!); return this; } + /// + /// Gets the base used for all query operations. + /// Applies eager-loading expressions if any have been configured via . + /// protected override IQueryable RepositoryQuery { get @@ -92,7 +132,7 @@ protected override IQueryable RepositoryQuery _repositoryQuery = ObjectSet.AsQueryable(); } - // Start Eagerloading + // Override the base query with the eager-loaded queryable if includes have been configured if (_includableQueryable != null) { _repositoryQuery = _includableQueryable; @@ -101,13 +141,20 @@ protected override IQueryable RepositoryQuery } } + /// + /// Core query method that applies the given filter expression to the . + /// All find operations delegate to this method to build the filtered queryable. + /// + /// A predicate expression to filter entities. + /// An representing the filtered query. + /// Thrown when is null. private IQueryable FindCore(Expression> expression) { IQueryable queryable; try { Guard.Against(RepositoryQuery == null, "RepositoryQuery is null"); - queryable = RepositoryQuery.Where(expression); + queryable = RepositoryQuery!.Where(expression); } catch (ApplicationException exception) { @@ -118,50 +165,58 @@ private IQueryable FindCore(Expression> expression) } + /// public async override Task AddAsync(TEntity entity, CancellationToken token = default) { EventTracker.AddEntity(entity); await DataConnection.InsertAsync(entity, token: token); } + /// public async override Task AnyAsync(Expression> expression, CancellationToken token = default) { return await RepositoryQuery.AnyAsync(expression, token: token); } + /// public async override Task AnyAsync(ISpecification specification, CancellationToken token = default) { return await AnyAsync(specification.Predicate, token: token); } + /// public async override Task DeleteAsync(TEntity entity, CancellationToken token = default) { EventTracker.AddEntity(entity); await DataConnection.DeleteAsync(entity); } + /// public async override Task DeleteManyAsync(Expression> expression, CancellationToken token = default) { return await FindQuery(expression).DeleteAsync(token); } + /// public async override Task DeleteManyAsync(ISpecification specification, CancellationToken token = default) { return await DeleteManyAsync(specification.Predicate, token); } + /// public override IQueryable FindQuery(ISpecification specification) { return FindCore(specification.Predicate); } + /// public override IQueryable FindQuery(Expression> expression) { return FindCore(expression); } /// - /// This is not yet implemented due to Linq2Db's inability to find primary key or array of primary key. + /// This is not yet implemented due to Linq2Db's inability to find primary key or array of primary key. /// /// Value of Primary Key /// Cancellation Token @@ -174,16 +229,19 @@ public override async Task FindAsync(object primaryKey, CancellationTok //DataExtensions.RetrieveIdentity(IEnumerable } + /// public async override Task> FindAsync(ISpecification specification, CancellationToken token = default) { return await FindCore(specification.Predicate).ToListAsync(token); } + /// public async override Task> FindAsync(Expression> expression, CancellationToken token = default) { return await FindCore(expression).ToListAsync(token); } + /// public async override Task> FindAsync(IPagedSpecification specification, CancellationToken token = default) { IQueryable query; @@ -198,6 +256,7 @@ public async override Task> FindAsync(IPagedSpecificatio return await Task.FromResult(query.ToPaginatedList(specification.PageNumber, specification.PageSize)); } + /// public async override Task> FindAsync(Expression> expression, Expression> orderByExpression, bool orderByAscending, int pageNumber = 1, int pageSize = 1, CancellationToken token = default) @@ -214,7 +273,8 @@ public async override Task> FindAsync(Expression FindQuery(Expression> expression, Expression> orderByExpression, + /// + public override IQueryable FindQuery(Expression> expression, Expression> orderByExpression, bool orderByAscending, int pageNumber = 1, int pageSize = 0) { IQueryable query; @@ -229,12 +289,14 @@ public override IQueryable FindQuery(Expression> ex return query.Skip((pageNumber - 1) * pageSize).Take(pageSize); } + /// public override IQueryable FindQuery(IPagedSpecification specification) { return this.FindQuery(specification.Predicate, specification.OrderByExpression, specification.OrderByAscending, specification.PageNumber, specification.PageSize); } + /// public override IQueryable FindQuery(Expression> expression, Expression> orderByExpression, bool orderByAscending) { @@ -250,26 +312,31 @@ public override IQueryable FindQuery(Expression> ex return query; } + /// public async override Task FindSingleOrDefaultAsync(Expression> expression, CancellationToken token = default) { - return await RepositoryQuery.SingleOrDefaultAsync(expression, token); + return (await RepositoryQuery.SingleOrDefaultAsync(expression, token))!; } + /// public async override Task FindSingleOrDefaultAsync(ISpecification specification, CancellationToken token = default) { return await FindSingleOrDefaultAsync(specification.Predicate, token); } + /// public async override Task GetCountAsync(ISpecification selectSpec, CancellationToken token = default) { return await GetCountAsync(selectSpec.Predicate, token); } + /// public async override Task GetCountAsync(Expression> expression, CancellationToken token = default) { return await RepositoryQuery.CountAsync(expression, token); } + /// public async override Task UpdateAsync(TEntity entity, CancellationToken token = default) { EventTracker.AddEntity(entity); diff --git a/Src/RCommon.Linq2Db/ILinq2DbPersistenceBuilder.cs b/Src/RCommon.Linq2Db/ILinq2DbPersistenceBuilder.cs index 8161b3a5..66d5dd17 100644 --- a/Src/RCommon.Linq2Db/ILinq2DbPersistenceBuilder.cs +++ b/Src/RCommon.Linq2Db/ILinq2DbPersistenceBuilder.cs @@ -3,8 +3,22 @@ namespace RCommon.Persistence.Linq2Db { + /// + /// Defines the fluent builder interface for configuring Linq2Db-based persistence in RCommon. + /// + /// + /// Extends to add Linq2Db-specific configuration such as + /// registering -derived data connections with named data stores. + /// public interface ILinq2DbPersistenceBuilder: IPersistenceBuilder { + /// + /// Registers a Linq2Db data connection type with the specified data store name and configuration options. + /// + /// The type of the data connection. Must derive from . + /// A unique name identifying this data store for resolution via . + /// A factory function that receives the and existing , returning configured . + /// The builder instance for fluent chaining. ILinq2DbPersistenceBuilder AddDataConnection(string dataStoreName, Func options) where TDataConnection : RCommonDataConnection; } } diff --git a/Src/RCommon.Linq2Db/Linq2DbPersistenceBuilder.cs b/Src/RCommon.Linq2Db/Linq2DbPersistenceBuilder.cs index 8e2fa169..661cd16c 100644 --- a/Src/RCommon.Linq2Db/Linq2DbPersistenceBuilder.cs +++ b/Src/RCommon.Linq2Db/Linq2DbPersistenceBuilder.cs @@ -16,11 +16,26 @@ namespace RCommon.Persistence.Linq2Db { + /// + /// Implementation of that configures Linq2Db-based + /// persistence services in the dependency injection container. + /// + /// + /// Upon construction, this builder registers as the default + /// implementation for , , + /// and . + /// public class Linq2DbPersistenceBuilder : ILinq2DbPersistenceBuilder { private readonly IServiceCollection _services; + /// + /// Initializes a new instance of and registers + /// Linq2Db repository services in the provided service collection. + /// + /// The to register services with. + /// Thrown when is null. public Linq2DbPersistenceBuilder(IServiceCollection services) { _services = services ?? throw new ArgumentNullException(nameof(services)); @@ -31,20 +46,35 @@ public Linq2DbPersistenceBuilder(IServiceCollection services) services.AddTransient(typeof(ILinqRepository<>), typeof(Linq2DbRepository<>)); } + /// public IServiceCollection Services => _services; + /// + /// Registers a Linq2Db data connection type with the specified data store name and configuration options. + /// + /// The type of the data connection. Must derive from . + /// A unique name identifying this data store for resolution via . + /// A factory function that receives the and existing , returning configured . + /// The builder instance for fluent chaining. + /// Thrown when is null or empty, or when is null. public ILinq2DbPersistenceBuilder AddDataConnection(string dataStoreName, Func options) where TDataConnection : RCommonDataConnection { Guard.Against(dataStoreName.IsNullOrEmpty(), "You must set a name for the Data Store"); Guard.Against(options == null, "You must set options to a value in order for them to be useful"); + // Register the factory, map the concrete DataConnection type to the data store name, and add the Linq2Db context this._services.TryAddTransient(); this._services.Configure(options => options.Register(dataStoreName)); - this._services.AddLinqToDBContext(options); + this._services.AddLinqToDBContext(options!); return this; } + /// + /// Sets the default data store used when no explicit data store name is specified. + /// + /// An action to configure . + /// The builder instance for fluent chaining. public IPersistenceBuilder SetDefaultDataStore(Action options) { this._services.Configure(options); diff --git a/Src/RCommon.Linq2Db/RCommonDataConnection.cs b/Src/RCommon.Linq2Db/RCommonDataConnection.cs index ee60f542..3cd9f683 100644 --- a/Src/RCommon.Linq2Db/RCommonDataConnection.cs +++ b/Src/RCommon.Linq2Db/RCommonDataConnection.cs @@ -14,17 +14,33 @@ namespace RCommon.Persistence.Linq2Db { + /// + /// Base data connection class for Linq2Db integration with the RCommon persistence layer. + /// + /// + /// Implements to provide a uniform abstraction over data stores, + /// allowing the to resolve named Linq2Db data connections. + /// Derive from this class instead of directly when using RCommon. + /// public class RCommonDataConnection : DataConnection, IDataStore { + /// + /// Initializes a new instance of with the specified Linq2Db data options. + /// + /// The used to configure the Linq2Db connection. public RCommonDataConnection(DataOptions linq2DbOptions) :base(linq2DbOptions) { - + } - + + /// + /// Gets the underlying for this data connection. + /// + /// The managed by the Linq2Db . public DbConnection GetDbConnection() { return this.Connection; diff --git a/Src/RCommon.Linq2Db/README.md b/Src/RCommon.Linq2Db/README.md index 1a1dda7a..9b841091 100644 --- a/Src/RCommon.Linq2Db/README.md +++ b/Src/RCommon.Linq2Db/README.md @@ -1,3 +1,101 @@ - # RCommon.Linq2Db +# RCommon.Linq2Db -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 +Linq2Db implementation of the RCommon persistence abstractions. Provides a LINQ-enabled repository backed by Linq2Db's `DataConnection`, supporting composable queries, eager loading, pagination, and integration with the RCommon data store factory and domain event tracking. + +## Features + +- `Linq2DbRepository` implementing `ILinqRepository`, `IReadOnlyRepository`, and `IWriteOnlyRepository` +- Full `IQueryable` support built on Linq2Db's `ITable` for composable LINQ queries +- Eager loading via `Include` / `ThenInclude` mapped to Linq2Db's `LoadWith` / `ThenLoad` API +- Paginated query results with ordering support via `IPaginatedList` +- Expression-based and specification-based querying with `FindQuery` returning `IQueryable` +- Bulk delete via Linq2Db's `DeleteAsync` on queryable expressions +- Named data store support for multi-database scenarios through `IDataStoreFactory` +- `RCommonDataConnection` base class implementing `IDataStore` for seamless factory resolution +- Fluent DI configuration using `AddLinqToDBContext` under the hood +- Domain event tracking integrated into add, update, and delete operations +- Targets .NET 8, .NET 9, and .NET 10 + +## Installation + +```shell +dotnet add package RCommon.Linq2Db +``` + +## Usage + +```csharp +// Configure in Program.cs or Startup +builder.Services.AddRCommon() + .WithPersistence(linq2Db => + { + linq2Db.AddDataConnection("ApplicationDb", + (serviceProvider, options) => + options.UseSqlServer(builder.Configuration.GetConnectionString("ApplicationDb"))); + + linq2Db.SetDefaultDataStore(defaults => + defaults.DefaultDataStoreName = "ApplicationDb"); + }); +``` + +Your data connection must inherit from `RCommonDataConnection`: + +```csharp +public class ApplicationDataConnection : RCommonDataConnection +{ + public ApplicationDataConnection(DataOptions options) + : base(options) { } +} +``` + +Then inject and use the repository abstractions: + +```csharp +public class CustomerService +{ + private readonly ILinqRepository _customerRepo; + + public CustomerService(ILinqRepository customerRepo) + { + _customerRepo = customerRepo; + } + + public async Task> GetActiveCustomersAsync() + { + return await _customerRepo.FindAsync(c => c.IsActive); + } + + public async Task> GetCustomersPagedAsync(int page, int pageSize) + { + return await _customerRepo.FindAsync( + c => c.IsActive, + c => c.LastName, + orderByAscending: true, + pageNumber: page, + pageSize: pageSize); + } +} +``` + +## Key Types + +| Type | Description | +|------|-------------| +| `Linq2DbRepository` | Concrete repository using Linq2Db with full LINQ, eager loading, and CRUD support | +| `RCommonDataConnection` | Base `DataConnection` class implementing `IDataStore` for named data store resolution | +| `Linq2DbPersistenceBuilder` | Fluent builder for registering Linq2Db data connections and repository services in DI | +| `ILinq2DbPersistenceBuilder` | Builder interface exposing `AddDataConnection()` for registering named data connections | + +## Documentation + +For full documentation, visit [rcommon.com](https://rcommon.com). + +## Related Packages + +- [RCommon.Persistence](https://www.nuget.org/packages/RCommon.Persistence) - Core persistence abstractions (required dependency) +- [RCommon.EFCore](https://www.nuget.org/packages/RCommon.EFCore) - Entity Framework Core implementation +- [RCommon.Dapper](https://www.nuget.org/packages/RCommon.Dapper) - Dapper implementation + +## License + +Licensed under the [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0). diff --git a/Src/RCommon.MassTransit/IMassTransitEventHandlingBuilder.cs b/Src/RCommon.MassTransit/IMassTransitEventHandlingBuilder.cs index 82e11f7f..5dc8077a 100644 --- a/Src/RCommon.MassTransit/IMassTransitEventHandlingBuilder.cs +++ b/Src/RCommon.MassTransit/IMassTransitEventHandlingBuilder.cs @@ -4,8 +4,13 @@ namespace RCommon.MassTransit { + /// + /// Builder interface for configuring MassTransit-based event handling within the RCommon framework. + /// Combines for RCommon event wiring with + /// for MassTransit bus configuration. + /// public interface IMassTransitEventHandlingBuilder : IEventHandlingBuilder, IBusRegistrationConfigurator { - + } } diff --git a/Src/RCommon.MassTransit/MassTransitEventHandlingBuilder.cs b/Src/RCommon.MassTransit/MassTransitEventHandlingBuilder.cs index fa046eaf..0d54ad39 100644 --- a/Src/RCommon.MassTransit/MassTransitEventHandlingBuilder.cs +++ b/Src/RCommon.MassTransit/MassTransitEventHandlingBuilder.cs @@ -11,16 +11,26 @@ namespace RCommon.MassTransit { + /// + /// Default implementation of that configures MassTransit + /// consumers and event handling services through the RCommon builder pipeline. + /// Inherits from to provide full MassTransit bus registration capabilities. + /// public class MassTransitEventHandlingBuilder : ServiceCollectionBusConfigurator, IMassTransitEventHandlingBuilder { + /// + /// Initializes a new instance of using the provided RCommon builder. + /// + /// The whose service collection is used for dependency registration. public MassTransitEventHandlingBuilder(IRCommonBuilder builder) :base(builder.Services) { Services = builder.Services; - + } + /// public IServiceCollection Services { get; } } } diff --git a/Src/RCommon.MassTransit/MassTransitEventHandlingBuilderExtensions.cs b/Src/RCommon.MassTransit/MassTransitEventHandlingBuilderExtensions.cs index ded9de01..bc005fae 100644 --- a/Src/RCommon.MassTransit/MassTransitEventHandlingBuilderExtensions.cs +++ b/Src/RCommon.MassTransit/MassTransitEventHandlingBuilderExtensions.cs @@ -21,14 +21,19 @@ namespace RCommon { + /// + /// Extension methods for configuring MassTransit event handling within the RCommon builder pipeline. + /// public static class MassTransitEventHandlingBuilderExtensions { /// - /// Adds MassTransit and its dependencies to the , and allows consumers, sagas, and activities to be configured + /// Adds MassTransit and its dependencies to the , and allows consumers, sagas, and activities to be configured. /// - /// - /// - private static IServiceCollection AddMassTransit(this IRCommonBuilder builder, Action configure = null) + /// The RCommon builder to register MassTransit services against. + /// Optional configuration action for . + /// The for further chaining. + /// Thrown if MassTransit has already been registered in this container. + private static IServiceCollection AddMassTransit(this IRCommonBuilder builder, Action? configure = null) { if (builder.Services.Any(d => d.ServiceType == typeof(IBus))) { @@ -46,6 +51,10 @@ private static IServiceCollection AddMassTransit(this IRCommonBuilder builder, A return builder.Services; } + /// + /// Registers the MassTransit hosted service, health checks, and host options required for bus lifetime management. + /// + /// The service collection to register services into. private static void AddHostedService(IServiceCollection collection) { collection.AddOptions(); @@ -57,6 +66,10 @@ private static void AddHostedService(IServiceCollection collection) collection.TryAddEnumerable(ServiceDescriptor.Singleton()); } + /// + /// Registers MassTransit instrumentation and monitoring options into the service collection. + /// + /// The service collection to register instrumentation services into. private static void AddInstrumentation(IServiceCollection collection) { collection.AddOptions(); @@ -64,12 +77,27 @@ private static void AddInstrumentation(IServiceCollection collection) } + /// + /// Configures MassTransit event handling with default settings. + /// + /// The implementation type. + /// The RCommon builder. + /// The for further chaining. public static IRCommonBuilder WithEventHandling(this IRCommonBuilder builder) where T : IMassTransitEventHandlingBuilder { return WithEventHandling(builder, x => { }); } + /// + /// Configures MassTransit event handling with custom builder actions. + /// Registers the generic as a scoped service + /// and wires up MassTransit via . + /// + /// The implementation type. + /// The RCommon builder. + /// Configuration delegate for MassTransit event handling. + /// The for further chaining. public static IRCommonBuilder WithEventHandling(this IRCommonBuilder builder, Action actions) where T : IMassTransitEventHandlingBuilder { @@ -82,6 +110,13 @@ public static IRCommonBuilder WithEventHandling(this IRCommonBuilder builder, return builder; } + /// + /// Registers a subscriber for a specific event type and adds the corresponding MassTransit consumer. + /// Also registers the event-to-producer subscription for correct event routing. + /// + /// The event type to subscribe to. Must implement . + /// The subscriber implementation that handles the event. + /// The MassTransit event handling builder. public static void AddSubscriber(this IMassTransitEventHandlingBuilder builder) where TEvent : class, ISerializableEvent where TEventHandler : class, ISubscriber diff --git a/Src/RCommon.MassTransit/Producers/PublishWithMassTransitEventProducer.cs b/Src/RCommon.MassTransit/Producers/PublishWithMassTransitEventProducer.cs index 1098e902..fea4de6e 100644 --- a/Src/RCommon.MassTransit/Producers/PublishWithMassTransitEventProducer.cs +++ b/Src/RCommon.MassTransit/Producers/PublishWithMassTransitEventProducer.cs @@ -11,6 +11,14 @@ namespace RCommon.MassTransit.Producers { + /// + /// An implementation that publishes events to all subscribed consumers + /// using MassTransit's method (fan-out pattern). + /// + /// + /// Use this producer when you want an event to be delivered to all consumers that are subscribed + /// to the event type. For point-to-point delivery, use instead. + /// public class PublishWithMassTransitEventProducer : IEventProducer { private readonly IBus _bus; @@ -18,6 +26,13 @@ public class PublishWithMassTransitEventProducer : IEventProducer private readonly IServiceProvider _serviceProvider; private readonly EventSubscriptionManager _subscriptionManager; + /// + /// Initializes a new instance of . + /// + /// The MassTransit bus used to publish events. + /// Logger for diagnostic output. + /// Service provider for creating scoped services during event production. + /// Manages event-to-producer subscriptions for routing decisions. public PublishWithMassTransitEventProducer(IBus bus, ILogger logger, IServiceProvider serviceProvider, EventSubscriptionManager subscriptionManager) { @@ -27,12 +42,14 @@ public PublishWithMassTransitEventProducer(IBus bus, ILogger public async Task ProduceEventAsync(T @event, CancellationToken cancellationToken = default) where T : ISerializableEvent { try { Guard.IsNotNull(@event, nameof(@event)); + // Check if this event type is subscribed to this producer; skip if not routed here if (!_subscriptionManager.ShouldProduceEvent(this.GetType(), typeof(T))) { _logger.LogDebug("{0} skipping event {1} - not subscribed to this producer", @@ -40,6 +57,7 @@ public async Task ProduceEventAsync(T @event, CancellationToken cancellationT return; } + // Create a scoped service context for the publish operation using (IServiceScope scope = _serviceProvider.CreateScope()) { if (_logger.IsEnabled(LogLevel.Information)) diff --git a/Src/RCommon.MassTransit/Producers/SendWithMassTransitEventProducer.cs b/Src/RCommon.MassTransit/Producers/SendWithMassTransitEventProducer.cs index a02b3437..1e75e23d 100644 --- a/Src/RCommon.MassTransit/Producers/SendWithMassTransitEventProducer.cs +++ b/Src/RCommon.MassTransit/Producers/SendWithMassTransitEventProducer.cs @@ -11,6 +11,14 @@ namespace RCommon.MassTransit.Producers { + /// + /// An implementation that sends events to a single consumer endpoint + /// using MassTransit's method (point-to-point pattern). + /// + /// + /// Use this producer for command-style messaging where only one consumer should process the event. + /// For fan-out delivery to all subscribers, use instead. + /// public class SendWithMassTransitEventProducer : IEventProducer { private readonly IBus _bus; @@ -18,6 +26,13 @@ public class SendWithMassTransitEventProducer : IEventProducer private readonly IServiceProvider _serviceProvider; private readonly EventSubscriptionManager _subscriptionManager; + /// + /// Initializes a new instance of . + /// + /// The MassTransit bus used to send events. + /// Logger for diagnostic output. + /// Service provider for creating scoped services during event production. + /// Manages event-to-producer subscriptions for routing decisions. public SendWithMassTransitEventProducer(IBus bus, ILogger logger, IServiceProvider serviceProvider, EventSubscriptionManager subscriptionManager) { @@ -27,18 +42,22 @@ public SendWithMassTransitEventProducer(IBus bus, ILogger public async Task ProduceEventAsync(T @event, CancellationToken cancellationToken = default) where T : ISerializableEvent { try { Guard.IsNotNull(@event, nameof(@event)); + // Check if this event type is subscribed to this producer; skip if not routed here if (!_subscriptionManager.ShouldProduceEvent(this.GetType(), typeof(T))) { _logger.LogDebug("{0} skipping event {1} - not subscribed to this producer", new object[] { this.GetGenericTypeName(), typeof(T).Name }); return; } + + // Create a scoped service context for the send operation using (IServiceScope scope = _serviceProvider.CreateScope()) { if (_logger.IsEnabled(LogLevel.Information)) diff --git a/Src/RCommon.MassTransit/README.md b/Src/RCommon.MassTransit/README.md index a5ef6c09..f5dd896f 100644 --- a/Src/RCommon.MassTransit/README.md +++ b/Src/RCommon.MassTransit/README.md @@ -1,3 +1,87 @@ - # RCommon.MassTransit +# RCommon.MassTransit -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 +Integrates [MassTransit](https://masstransit.io/) distributed messaging with RCommon's event handling system, allowing you to produce and consume events through MassTransit while programming against RCommon's `IEventProducer` and `ISubscriber` abstractions. + +## Features + +- Publish events to all subscribed consumers using MassTransit's fan-out (publish) semantics +- Send events to a single consumer endpoint using MassTransit's point-to-point (send) semantics +- Bridge MassTransit consumers to RCommon's `ISubscriber` abstraction for handler portability +- Event subscription routing ensures events are delivered only to their configured producers +- Full access to MassTransit's `IBusRegistrationConfigurator` for transport and consumer configuration +- Automatic hosted service, health check, and instrumentation registration + +## Installation + +```shell +dotnet add package RCommon.MassTransit +``` + +## Usage + +```csharp +using RCommon; +using RCommon.MassTransit; + +builder.Services.AddRCommon() + .WithEventHandling(eventHandling => + { + // Register subscribers that bridge MassTransit to RCommon + eventHandling.AddSubscriber(); + + // Configure MassTransit transports (full IBusRegistrationConfigurator access) + eventHandling.UsingRabbitMq((context, cfg) => + { + cfg.Host("localhost", "/", h => + { + h.Username("guest"); + h.Password("guest"); + }); + cfg.ConfigureEndpoints(context); + }); + }); +``` + +Produce events from application code: + +```csharp +public class OrderService +{ + private readonly IEventProducer _eventProducer; + + public OrderService(IEventProducer eventProducer) + { + _eventProducer = eventProducer; + } + + public async Task CreateOrderAsync(Order order) + { + // This publishes via MassTransit to all subscribed consumers + await _eventProducer.ProduceEventAsync(new OrderCreatedEvent(order)); + } +} +``` + +## Key Types + +| Type | Description | +|------|-------------| +| `PublishWithMassTransitEventProducer` | `IEventProducer` that publishes events to all consumers via `IBus.Publish` (fan-out) | +| `SendWithMassTransitEventProducer` | `IEventProducer` that sends events to a single endpoint via `IBus.Send` (point-to-point) | +| `MassTransitEventHandler` | MassTransit `IConsumer` that delegates to an RCommon `ISubscriber` | +| `IMassTransitEventHandlingBuilder` | Builder combining `IEventHandlingBuilder` with MassTransit's `IBusRegistrationConfigurator` | +| `MassTransitEventHandlingBuilder` | Default builder implementation for configuring MassTransit event handling | + +## Documentation + +For full documentation, visit [rcommon.com](https://rcommon.com). + +## Related Packages + +- [RCommon.Core](https://www.nuget.org/packages/RCommon.Core) - Core abstractions including `IEventProducer` and `ISubscriber` +- [RCommon.MediatR](https://www.nuget.org/packages/RCommon.MediatR) - MediatR integration for in-process event handling and mediator pattern +- [RCommon.Wolverine](https://www.nuget.org/packages/RCommon.Wolverine) - Wolverine integration for distributed messaging + +## License + +Licensed under the [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0). diff --git a/Src/RCommon.MassTransit/Subscribers/IMassTransitEventHandler.cs b/Src/RCommon.MassTransit/Subscribers/IMassTransitEventHandler.cs index a5193a50..72f41e3d 100644 --- a/Src/RCommon.MassTransit/Subscribers/IMassTransitEventHandler.cs +++ b/Src/RCommon.MassTransit/Subscribers/IMassTransitEventHandler.cs @@ -8,11 +8,18 @@ namespace RCommon.MassTransit.Subscribers { + /// + /// Non-generic marker interface for MassTransit event handlers within the RCommon framework. + /// public interface IMassTransitEventHandler { } - public interface IMassTransitEventHandler : IMassTransitEventHandler + /// + /// Generic marker interface for MassTransit event handlers that process a specific distributed event type. + /// + /// The distributed event type to handle. Must implement . + public interface IMassTransitEventHandler : IMassTransitEventHandler where TDistributedEvent : class, ISerializableEvent { } diff --git a/Src/RCommon.MassTransit/Subscribers/MassTransitEventHandler.cs b/Src/RCommon.MassTransit/Subscribers/MassTransitEventHandler.cs index 5bbbf891..92434105 100644 --- a/Src/RCommon.MassTransit/Subscribers/MassTransitEventHandler.cs +++ b/Src/RCommon.MassTransit/Subscribers/MassTransitEventHandler.cs @@ -10,18 +10,32 @@ namespace RCommon.MassTransit.Subscribers { + /// + /// MassTransit consumer that bridges MassTransit message consumption to the RCommon abstraction. + /// Implements both and MassTransit's . + /// + /// The event type to consume. Must implement . public class MassTransitEventHandler : IMassTransitEventHandler, IConsumer where TEvent : class, ISerializableEvent { private readonly ILogger> _logger; private readonly ISubscriber _subscriber; + /// + /// Initializes a new instance of . + /// + /// Logger for diagnostic output. + /// The RCommon subscriber that handles the event. public MassTransitEventHandler(ILogger> logger, ISubscriber subscriber) { _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _subscriber = subscriber ?? throw new ArgumentNullException(nameof(subscriber)); } + /// + /// Consumes a MassTransit message and delegates to the registered . + /// + /// The MassTransit consume context containing the event message. public async Task Consume(ConsumeContext context) { _logger.LogDebug("{0} handling event {1}", new object[] { this.GetGenericTypeName(), context.Message }); diff --git a/Src/RCommon.Mediator/IMediatorAdapter.cs b/Src/RCommon.Mediator/IMediatorAdapter.cs index 8911678c..e0055692 100644 --- a/Src/RCommon.Mediator/IMediatorAdapter.cs +++ b/Src/RCommon.Mediator/IMediatorAdapter.cs @@ -6,13 +6,41 @@ namespace RCommon.Mediator { + /// + /// Defines an adapter interface that abstracts the underlying mediator implementation (e.g., MediatR, Wolverine). + /// + /// + /// Concrete implementations of this interface bridge RCommon's mediator abstraction to a specific + /// mediator library. This enables swapping mediator implementations without changing application code. + /// public interface IMediatorAdapter { - + /// + /// Sends a request to a single handler with no return value. + /// + /// The type of the request to send. + /// The request object to dispatch. + /// Optional token to cancel the operation. + /// A representing the asynchronous operation. Task Send(TRequest request, CancellationToken cancellationToken = default); + /// + /// Sends a request to a single handler and returns a response. + /// + /// The type of the request to send. + /// The type of the response expected from the handler. + /// The request object to dispatch. + /// Optional token to cancel the operation. + /// A containing the handler's response. Task Send(TRequest request, CancellationToken cancellationToken = default); + /// + /// Publishes a notification to all registered handlers. + /// + /// The type of the notification to publish. + /// The notification object to broadcast. + /// Optional token to cancel the operation. + /// A representing the asynchronous operation. Task Publish(TNotification notification, CancellationToken cancellationToken = default); } } diff --git a/Src/RCommon.Mediator/IMediatorBuilder.cs b/Src/RCommon.Mediator/IMediatorBuilder.cs index 1ffd4090..51bb4d03 100644 --- a/Src/RCommon.Mediator/IMediatorBuilder.cs +++ b/Src/RCommon.Mediator/IMediatorBuilder.cs @@ -7,8 +7,19 @@ namespace RCommon.Mediator { + /// + /// Defines the contract for configuring a mediator implementation within the RCommon framework. + /// + /// + /// Implementations of this interface are responsible for registering mediator-specific services + /// (e.g., MediatR or Wolverine) into the dependency injection container. Used in conjunction + /// with . + /// public interface IMediatorBuilder { + /// + /// Gets the used to register mediator-related services. + /// IServiceCollection Services { get; } } } diff --git a/Src/RCommon.Mediator/IMediatorService.cs b/Src/RCommon.Mediator/IMediatorService.cs index cd4df058..124423df 100644 --- a/Src/RCommon.Mediator/IMediatorService.cs +++ b/Src/RCommon.Mediator/IMediatorService.cs @@ -2,12 +2,42 @@ namespace RCommon.Mediator { + /// + /// Provides a high-level mediator service for sending requests and publishing notifications. + /// + /// + /// This is the primary interface that application code should depend on for mediator operations. + /// It delegates to internally, decoupling consumers from the + /// underlying mediator implementation. + /// public interface IMediatorService { + /// + /// Publishes a notification to all registered handlers. + /// + /// The type of the notification to publish. + /// The notification object to broadcast to all subscribers. + /// Optional token to cancel the operation. + /// A representing the asynchronous operation. Task Publish(TNotification notification, CancellationToken cancellationToken = default); + /// + /// Sends a request to a single handler with no return value. + /// + /// The type of the request to send. + /// The request object to dispatch. + /// Optional token to cancel the operation. + /// A representing the asynchronous operation. Task Send(TRequest request, CancellationToken cancellationToken = default); + /// + /// Sends a request to a single handler and returns a response. + /// + /// The type of the request to send. + /// The type of the response expected from the handler. + /// The request object to dispatch. + /// Optional token to cancel the operation. + /// A containing the handler's response. Task Send(TRequest request, CancellationToken cancellationToken = default); } } \ No newline at end of file diff --git a/Src/RCommon.Mediator/MediatorBuilderExtensions.cs b/Src/RCommon.Mediator/MediatorBuilderExtensions.cs index 1f0979f8..fe70defd 100644 --- a/Src/RCommon.Mediator/MediatorBuilderExtensions.cs +++ b/Src/RCommon.Mediator/MediatorBuilderExtensions.cs @@ -11,22 +11,47 @@ namespace RCommon { + /// + /// Provides extension methods on for registering a mediator implementation. + /// public static class MediatorBuilderExtensions { + /// + /// Registers a mediator implementation using default configuration. + /// + /// + /// The implementation that configures the specific mediator library. + /// + /// The RCommon builder instance. + /// The for chaining additional configuration calls. public static IRCommonBuilder WithMediator(this IRCommonBuilder builder) where T : IMediatorBuilder { return WithMediator(builder, x => { }); } + /// + /// Registers a mediator implementation with custom configuration. + /// + /// + /// The implementation that configures the specific mediator library. + /// + /// The RCommon builder instance. + /// A delegate to configure the mediator builder of type . + /// The for chaining additional configuration calls. + /// + /// This method registers as the scoped implementation, + /// then creates the mediator builder via and invokes + /// the configuration delegate to allow library-specific setup. + /// public static IRCommonBuilder WithMediator(this IRCommonBuilder builder, Action actions) where T : IMediatorBuilder { builder.Services.AddScoped(); - // Event Handling Configurations - var mediatorConfig = (T)Activator.CreateInstance(typeof(T), new object[] { builder }); + // Create the mediator-specific builder by convention (expects a constructor accepting IRCommonBuilder) + var mediatorConfig = (T)Activator.CreateInstance(typeof(T), new object[] { builder })!; actions(mediatorConfig); return builder; } diff --git a/Src/RCommon.Mediator/MediatorService.cs b/Src/RCommon.Mediator/MediatorService.cs index 49d17872..8e2bc4c5 100644 --- a/Src/RCommon.Mediator/MediatorService.cs +++ b/Src/RCommon.Mediator/MediatorService.cs @@ -7,25 +7,40 @@ namespace RCommon.Mediator { + /// + /// Default implementation of that delegates all operations to an . + /// + /// + /// This service acts as a thin wrapper around , providing a consistent + /// application-facing API while the adapter handles library-specific dispatch logic. + /// Registered as a scoped service by . + /// public class MediatorService : IMediatorService { private readonly IMediatorAdapter _mediatorAdapter; + /// + /// Initializes a new instance of . + /// + /// The mediator adapter that handles the actual dispatch to handlers. public MediatorService(IMediatorAdapter mediatorAdapter) { _mediatorAdapter = mediatorAdapter; } + /// public Task Publish(TNotification notification, CancellationToken cancellationToken = default) { return _mediatorAdapter.Publish(notification, cancellationToken); } + /// public async Task Send(TRequest request, CancellationToken cancellationToken = default) { await _mediatorAdapter.Send(request, cancellationToken); } + /// public async Task Send(TRequest request, CancellationToken cancellationToken = default) { return await _mediatorAdapter.Send(request, cancellationToken); diff --git a/Src/RCommon.Mediator/README.md b/Src/RCommon.Mediator/README.md index 79f2bfd6..cba4a67d 100644 --- a/Src/RCommon.Mediator/README.md +++ b/Src/RCommon.Mediator/README.md @@ -1,3 +1,102 @@ - # RCommon.Mediator +# RCommon.Mediator -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 mediator abstraction layer for RCommon that decouples application code from specific mediator libraries, enabling request/response dispatch and notification publishing through a uniform API. + +## Features + +- **Library-agnostic mediator API** -- depend on `IMediatorService` instead of MediatR, Wolverine, or any other implementation +- **Request/response dispatch** -- send requests to a single handler with or without a return value +- **Notification publishing** -- broadcast notifications to all registered subscribers +- **Adapter pattern** -- swap mediator implementations by changing only the `IMediatorAdapter` registration +- **Subscriber contracts** -- `IAppRequest`, `IAppRequest`, and `IAppNotification` marker interfaces for message classification +- **Handler contracts** -- `IAppRequestHandler` and `IAppRequestHandler` for implementing handlers +- **Fluent builder API** -- integrates with the `AddRCommon()` builder pattern for clean DI configuration + +## Installation + +```shell +dotnet add package RCommon.Mediator +``` + +## Usage + +```csharp +using RCommon; +using RCommon.Mediator; +using RCommon.Mediator.Subscribers; + +// Configure the mediator in your DI setup using a concrete adapter (e.g., MediatR) +services.AddRCommon(config => +{ + config.WithMediator(mediator => + { + // Register handlers from your application assembly + mediator.AddHandlersFromAssemblyContainingType(); + }); +}); + +// Define a request and handler +public class CreateOrderRequest : IAppRequest +{ + public string ProductName { get; set; } + public int Quantity { get; set; } +} + +public class CreateOrderHandler : IAppRequestHandler +{ + public async Task HandleAsync(CreateOrderRequest request, + CancellationToken cancellationToken = default) + { + // Handle the request and return a result + return new OrderDto { Id = Guid.NewGuid(), ProductName = request.ProductName }; + } +} + +// Consume the mediator from your application layer +public class OrderController +{ + private readonly IMediatorService _mediator; + + public OrderController(IMediatorService mediator) + { + _mediator = mediator; + } + + public async Task CreateOrder(CreateOrderRequest request) + { + return await _mediator.Send(request); + } + + public async Task NotifyOrderCreated(OrderCreatedNotification notification) + { + await _mediator.Publish(notification); + } +} +``` + +## Key Types + +| Type | Description | +|------|-------------| +| `IMediatorService` | Primary application-facing interface for sending requests and publishing notifications | +| `MediatorService` | Default implementation that delegates to an `IMediatorAdapter` | +| `IMediatorAdapter` | Adapter interface that bridges to a specific mediator library (e.g., MediatR) | +| `IMediatorBuilder` | Builder contract for configuring a mediator implementation within `AddRCommon()` | +| `IAppRequest` | Marker interface for requests dispatched to a single handler with no return value | +| `IAppRequest` | Marker interface for requests that return a response of type `TResponse` | +| `IAppNotification` | Marker interface for notifications broadcast to all registered subscribers | +| `IAppRequestHandler` | Handler contract for requests with no return value | +| `IAppRequestHandler` | Handler contract for requests that produce a typed response | + +## 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.Mediatr](https://www.nuget.org/packages/RCommon.Mediatr) - MediatR adapter implementation for `IMediatorAdapter` + +## License + +Licensed under the [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0). diff --git a/Src/RCommon.Mediator/Subscribers/IAppNotification.cs b/Src/RCommon.Mediator/Subscribers/IAppNotification.cs index af53b9f0..c228af9a 100644 --- a/Src/RCommon.Mediator/Subscribers/IAppNotification.cs +++ b/Src/RCommon.Mediator/Subscribers/IAppNotification.cs @@ -6,6 +6,14 @@ namespace RCommon.Mediator.Subscribers { + /// + /// Marker interface for notification messages that are broadcast to multiple handlers. + /// + /// + /// Implement this interface on DTOs that represent events or notifications which should + /// be dispatched to all registered subscribers via . + /// Unlike , notifications can have zero or more handlers. + /// public interface IAppNotification { } diff --git a/Src/RCommon.Mediator/Subscribers/IAppRequest.cs b/Src/RCommon.Mediator/Subscribers/IAppRequest.cs index 70a658d5..518abc6b 100644 --- a/Src/RCommon.Mediator/Subscribers/IAppRequest.cs +++ b/Src/RCommon.Mediator/Subscribers/IAppRequest.cs @@ -6,11 +6,26 @@ namespace RCommon.Mediator.Subscribers { + /// + /// Marker interface for request messages that are dispatched to a single handler with no return value. + /// + /// + /// Implement this interface on command or request DTOs that should be handled by exactly one + /// and do not produce a response. + /// public interface IAppRequest { } + /// + /// Marker interface for request messages that are dispatched to a single handler and return a response. + /// + /// The type of the response produced by the handler. + /// + /// Implement this interface on query or request DTOs that should be handled by exactly one + /// and produce a result of type . + /// public interface IAppRequest { diff --git a/Src/RCommon.Mediator/Subscribers/IAppRequestHandler.cs b/Src/RCommon.Mediator/Subscribers/IAppRequestHandler.cs index 8601d600..5241d3cb 100644 --- a/Src/RCommon.Mediator/Subscribers/IAppRequestHandler.cs +++ b/Src/RCommon.Mediator/Subscribers/IAppRequestHandler.cs @@ -6,13 +6,42 @@ namespace RCommon.Mediator.Subscribers { + /// + /// Defines a handler for a request that does not return a value. + /// + /// The type of the request to handle. + /// + /// Implement this interface to handle requests of type dispatched + /// via . Each request type should have exactly one handler. + /// public interface IAppRequestHandler { + /// + /// Handles the specified request asynchronously. + /// + /// The request to handle. + /// Optional token to cancel the operation. + /// A representing the asynchronous operation. public Task HandleAsync(TRequest request, CancellationToken cancellationToken = default); } + /// + /// Defines a handler for a request that returns a response. + /// + /// The type of the request to handle. + /// The type of the response to return. + /// + /// Implement this interface to handle requests of type dispatched + /// via . Each request type should have exactly one handler. + /// public interface IAppRequestHandler { + /// + /// Handles the specified request asynchronously and returns a response. + /// + /// The request to handle. + /// Optional token to cancel the operation. + /// A containing the handler's response. public Task HandleAsync(TRequest request, CancellationToken cancellationToken = default); } } diff --git a/Src/RCommon.Mediatr/Behaviors/LoggingBehavior.cs b/Src/RCommon.Mediatr/Behaviors/LoggingBehavior.cs index 19d87c6a..ce12a325 100644 --- a/Src/RCommon.Mediatr/Behaviors/LoggingBehavior.cs +++ b/Src/RCommon.Mediatr/Behaviors/LoggingBehavior.cs @@ -10,13 +10,25 @@ namespace RCommon.Mediator.MediatR.Behaviors { + /// + /// MediatR pipeline behavior that logs the handling of fire-and-forget requests (no explicit response type). + /// Logs the command name and payload before and after the handler executes. + /// + /// The MediatR request type. Must implement . + /// The response type from the pipeline. public class LoggingRequestBehavior : IPipelineBehavior where TRequest : IRequest { private readonly ILogger> _logger; + + /// + /// Initializes a new instance of . + /// + /// Logger for diagnostic output. public LoggingRequestBehavior(ILogger> logger) => _logger = logger; + /// public async Task Handle(TRequest request, RequestHandlerDelegate next, CancellationToken cancellationToken) { _logger.LogInformation("----- Handling command {CommandName} ({@Command})", request.GetGenericTypeName(), request); @@ -27,13 +39,25 @@ public async Task Handle(TRequest request, RequestHandlerDelegate + /// MediatR pipeline behavior that logs the handling of requests that return a response. + /// Logs the command name and payload before and after the handler executes. + /// + /// The MediatR request type. Must implement . + /// The response type returned by the handler. public class LoggingRequestWithResponseBehavior : IPipelineBehavior where TRequest : IRequest { private readonly ILogger> _logger; + + /// + /// Initializes a new instance of . + /// + /// Logger for diagnostic output. public LoggingRequestWithResponseBehavior(ILogger> logger) => _logger = logger; + /// public async Task Handle(TRequest request, RequestHandlerDelegate next, CancellationToken cancellationToken) { _logger.LogInformation("----- Handling command {CommandName} ({@Command})", request.GetGenericTypeName(), request); diff --git a/Src/RCommon.Mediatr/Behaviors/UnitOfWorkBehavior.cs b/Src/RCommon.Mediatr/Behaviors/UnitOfWorkBehavior.cs index f025e026..962085bb 100644 --- a/Src/RCommon.Mediatr/Behaviors/UnitOfWorkBehavior.cs +++ b/Src/RCommon.Mediatr/Behaviors/UnitOfWorkBehavior.cs @@ -8,12 +8,23 @@ namespace RCommon.Mediator.MediatR.Behaviors { + /// + /// MediatR pipeline behavior that wraps fire-and-forget request handling in a transactional unit of work. + /// Creates a unit of work before the handler executes and commits it upon successful completion. + /// + /// The MediatR request type. Must implement . + /// The response type from the pipeline. public class UnitOfWorkRequestBehavior : IPipelineBehavior where TRequest : IRequest { private readonly ILogger> _logger; private readonly IUnitOfWorkFactory _unitOfWorkScopeFactory; + /// + /// Initializes a new instance of . + /// + /// Factory for creating unit of work instances. + /// Logger for diagnostic output. public UnitOfWorkRequestBehavior(IUnitOfWorkFactory unitOfWorkScopeFactory, ILogger> logger) { @@ -21,6 +32,7 @@ public UnitOfWorkRequestBehavior(IUnitOfWorkFactory unitOfWorkScopeFactory, _logger = logger ?? throw new ArgumentException(nameof(ILogger)); } + /// public async Task Handle(TRequest request, RequestHandlerDelegate next, CancellationToken cancellationToken) { var response = default(TResponse); @@ -28,6 +40,7 @@ public async Task Handle(TRequest request, RequestHandlerDelegate Handle(TRequest request, RequestHandlerDelegate + /// MediatR pipeline behavior that wraps request-with-response handling in a transactional unit of work. + /// Creates a unit of work before the handler executes and commits it upon successful completion. + /// + /// The MediatR request type. Must implement . + /// The response type returned by the handler. public class UnitOfWorkRequestWithResponseBehavior : IPipelineBehavior where TRequest : IRequest { private readonly ILogger> _logger; private readonly IUnitOfWorkFactory _unitOfWorkScopeFactory; + /// + /// Initializes a new instance of . + /// + /// Factory for creating unit of work instances. + /// Logger for diagnostic output. public UnitOfWorkRequestWithResponseBehavior(IUnitOfWorkFactory unitOfWorkScopeFactory, ILogger> logger) { @@ -65,6 +89,7 @@ public UnitOfWorkRequestWithResponseBehavior(IUnitOfWorkFactory unitOfWorkScopeF _logger = logger ?? throw new ArgumentException(nameof(ILogger)); } + /// public async Task Handle(TRequest request, RequestHandlerDelegate next, CancellationToken cancellationToken) { var response = default(TResponse); @@ -72,6 +97,7 @@ public async Task Handle(TRequest request, RequestHandlerDelegate + /// MediatR pipeline behavior that validates requests implementing + /// before they reach the handler. Uses to perform validation, + /// throwing on failure to prevent invalid requests from being processed. + /// + /// The MediatR request type. Must implement . + /// The response type returned by the handler. public class ValidatorBehaviorForMediatR : IPipelineBehavior where TRequest : class, IRequest { private readonly IValidationService _validationService; private readonly ILogger> _logger; + /// + /// Initializes a new instance of . + /// + /// The validation service used to validate the request. + /// Logger for diagnostic output. public ValidatorBehaviorForMediatR(IValidationService validationService, ILogger> logger) { _validationService = validationService; _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } + /// public async Task Handle(TRequest request, RequestHandlerDelegate next, CancellationToken cancellationToken) { var typeName = request.GetGenericTypeName(); _logger.LogInformation("----- Validating command {CommandType}", typeName); + // Validate the request and throw on failure (second param = throwOnFailure) await _validationService.ValidateAsync(request, true, cancellationToken); return await next(); } } + /// + /// MediatR pipeline behavior that validates requests implementing + /// before they reach the handler. Uses to perform validation, + /// throwing on failure to prevent invalid requests from being processed. + /// + /// The RCommon application request type. Must implement . + /// The response type returned by the handler. public class ValidatorBehavior : IPipelineBehavior where TRequest : class, IAppRequest { private readonly IValidationService _validationService; private readonly ILogger> _logger; + /// + /// Initializes a new instance of . + /// + /// The validation service used to validate the request. + /// Logger for diagnostic output. public ValidatorBehavior(IValidationService validationService, ILogger> logger) { _validationService = validationService; _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } + /// public async Task Handle(TRequest request, RequestHandlerDelegate next, CancellationToken cancellationToken) { var typeName = request.GetGenericTypeName(); _logger.LogInformation("----- Validating command {CommandType}", typeName); + // Validate the request and throw on failure (second param = throwOnFailure) await _validationService.ValidateAsync(request, true, cancellationToken); return await next(); } diff --git a/Src/RCommon.Mediatr/IMediatRBuilder.cs b/Src/RCommon.Mediatr/IMediatRBuilder.cs index 2fd4fb4e..f18a5bba 100644 --- a/Src/RCommon.Mediatr/IMediatRBuilder.cs +++ b/Src/RCommon.Mediatr/IMediatRBuilder.cs @@ -2,9 +2,24 @@ namespace RCommon.Mediator.MediatR { + /// + /// Builder interface for configuring the MediatR mediator implementation within RCommon. + /// Extends with MediatR-specific configuration methods. + /// public interface IMediatRBuilder : IMediatorBuilder { + /// + /// Configures MediatR services using a configuration action delegate. + /// + /// An action to configure . + /// The builder instance for fluent chaining. IMediatRBuilder Configure(Action options); + + /// + /// Configures MediatR services using a pre-built configuration instance. + /// + /// The to apply. + /// The builder instance for fluent chaining. IMediatRBuilder Configure(MediatRServiceConfiguration options); } } diff --git a/Src/RCommon.Mediatr/IMediatREventHandlingBuilder.cs b/Src/RCommon.Mediatr/IMediatREventHandlingBuilder.cs index 0ed64089..c1021962 100644 --- a/Src/RCommon.Mediatr/IMediatREventHandlingBuilder.cs +++ b/Src/RCommon.Mediatr/IMediatREventHandlingBuilder.cs @@ -3,8 +3,12 @@ namespace RCommon.MediatR { + /// + /// Builder interface for configuring MediatR-based event handling within the RCommon framework. + /// Extends to provide MediatR-specific event subscription capabilities. + /// public interface IMediatREventHandlingBuilder : IEventHandlingBuilder { - + } } diff --git a/Src/RCommon.Mediatr/MediatRAdapter.cs b/Src/RCommon.Mediatr/MediatRAdapter.cs index 82bb20ee..953d0be5 100644 --- a/Src/RCommon.Mediatr/MediatRAdapter.cs +++ b/Src/RCommon.Mediatr/MediatRAdapter.cs @@ -17,6 +17,10 @@ public class MediatRAdapter : IMediatorAdapter { private readonly IMediator _mediator; + /// + /// Initializes a new instance of . + /// + /// The MediatR instance to delegate operations to. public MediatRAdapter(IMediator mediator) { _mediator = mediator; @@ -36,11 +40,26 @@ public Task Publish(TNotification notification, CancellationToken return _mediator.Publish(new MediatRNotification(notification), cancellationToken); } + /// + /// Wraps the request in a and sends it via MediatR for single-handler processing. + /// + /// The type of request to send. + /// The request payload. + /// Optional cancellation token. public async Task Send(TRequest request, CancellationToken cancellationToken = default) { await _mediator.Send(new MediatRRequest(request), cancellationToken); } + /// + /// Wraps the request in a and sends it via MediatR, + /// returning the response from the single handler. + /// + /// The type of request to send. + /// The expected response type. + /// The request payload. + /// Optional cancellation token. + /// The response produced by the request handler. public async Task Send(TRequest request, CancellationToken cancellationToken = default) { return await _mediator.Send(new MediatRRequest(request), cancellationToken); diff --git a/Src/RCommon.Mediatr/MediatRBuilder.cs b/Src/RCommon.Mediatr/MediatRBuilder.cs index 183b82b4..75972c0b 100644 --- a/Src/RCommon.Mediatr/MediatRBuilder.cs +++ b/Src/RCommon.Mediatr/MediatRBuilder.cs @@ -13,9 +13,17 @@ namespace RCommon.Mediator.MediatR { + /// + /// Default implementation of that registers MediatR services + /// and the as the within the DI container. + /// public class MediatRBuilder : IMediatRBuilder { + /// + /// Initializes a new instance of and registers core MediatR services. + /// + /// The whose service collection is used for dependency registration. public MediatRBuilder(IRCommonBuilder builder) { @@ -25,6 +33,10 @@ public MediatRBuilder(IRCommonBuilder builder) } + /// + /// Registers the and MediatR services from this assembly. + /// + /// The service collection to register services into. protected void RegisterServices(IServiceCollection services) { services.AddScoped(); @@ -35,18 +47,21 @@ protected void RegisterServices(IServiceCollection services) }); } + /// public IMediatRBuilder Configure(Action options) { Services.AddMediatR(options); return this; } + /// public IMediatRBuilder Configure(MediatRServiceConfiguration options) { Services.AddMediatR(options); return this; } + /// public IServiceCollection Services { get; } } } diff --git a/Src/RCommon.Mediatr/MediatRBuilderExtensions.cs b/Src/RCommon.Mediatr/MediatRBuilderExtensions.cs index 8ed1ba2b..bbd1bfa6 100644 --- a/Src/RCommon.Mediatr/MediatRBuilderExtensions.cs +++ b/Src/RCommon.Mediatr/MediatRBuilderExtensions.cs @@ -16,10 +16,20 @@ namespace RCommon { + /// + /// Extension methods for configuring notifications, requests, and pipeline behaviors + /// on an instance. + /// public static class MediatRBuilderExtensions { - + /// + /// Registers a notification subscriber and its corresponding MediatR . + /// Notifications are delivered to all registered handlers (fan-out). + /// + /// The notification event type. Must implement . + /// The subscriber implementation that handles the notification. + /// The MediatR builder. public static void AddNotification(this IMediatRBuilder builder) where T : class, IAppNotification where TEventHandler : class, ISubscriber @@ -30,6 +40,13 @@ public static void AddNotification(this IMediatRBuilder builde builder.Services.AddScoped>, MediatRNotificationHandler>>(); } + /// + /// Registers a request handler for a fire-and-forget request (no response). + /// Requests are handled by a single handler via MediatR's Send method. + /// + /// The request type. Must implement . + /// The handler that processes the request. + /// The MediatR builder. public static void AddRequest(this IMediatRBuilder builder) where TRequest : class, IAppRequest where TEventHandler : class, IAppRequestHandler @@ -41,6 +58,14 @@ public static void AddRequest(this IMediatRBuilder buil MediatRRequestHandler>>(); } + /// + /// Registers a request handler for a request that returns a response. + /// Requests are handled by a single handler via MediatR's Send method. + /// + /// The request type. Must implement . + /// The response type returned by the handler. + /// The handler that processes the request and produces the response. + /// The MediatR builder. public static void AddRequest(this IMediatRBuilder builder) where TRequest : class, IAppRequest where TResponse : class @@ -53,12 +78,24 @@ public static void AddRequest(this IMediatRB MediatRRequestHandler, TResponse>>(); } + /// + /// Adds logging pipeline behaviors to the MediatR request pipeline. + /// Registers both and + /// . + /// + /// The MediatR builder. public static void AddLoggingToRequestPipeline(this IMediatRBuilder builder) { builder.Services.AddScoped(typeof(IPipelineBehavior<,>), typeof(LoggingRequestBehavior<,>)); builder.Services.AddScoped(typeof(IPipelineBehavior<,>), typeof(LoggingRequestWithResponseBehavior<,>)); } + /// + /// Adds validation pipeline behaviors to the MediatR request pipeline. + /// Registers both and + /// , along with the . + /// + /// The MediatR builder. public static void AddValidationToRequestPipeline(this IMediatRBuilder builder) { builder.Services.AddScoped(typeof(IPipelineBehavior<,>), typeof(ValidatorBehavior<,>)); @@ -66,6 +103,13 @@ public static void AddValidationToRequestPipeline(this IMediatRBuilder builder) builder.Services.AddScoped(); } + /// + /// Adds unit of work pipeline behaviors to the MediatR request pipeline. + /// Registers both and + /// so that + /// each request executes within a transactional unit of work. + /// + /// The MediatR builder. public static void AddUnitOfWorkToRequestPipeline(this IMediatRBuilder builder) { builder.Services.AddScoped(typeof(IPipelineBehavior<,>), typeof(UnitOfWorkRequestBehavior<,>)); diff --git a/Src/RCommon.Mediatr/MediatREventHandlingBuilder.cs b/Src/RCommon.Mediatr/MediatREventHandlingBuilder.cs index 9c813982..9d5ca600 100644 --- a/Src/RCommon.Mediatr/MediatREventHandlingBuilder.cs +++ b/Src/RCommon.Mediatr/MediatREventHandlingBuilder.cs @@ -13,19 +13,32 @@ namespace RCommon.MediatR { + /// + /// Default implementation of that registers MediatR event handling + /// services including the as the . + /// public class MediatREventHandlingBuilder : IMediatREventHandlingBuilder { + /// + /// Initializes a new instance of and registers core services. + /// + /// The whose service collection is used for dependency registration. public MediatREventHandlingBuilder(IRCommonBuilder builder) { this.RegisterServices(builder.Services); Services = builder.Services; } + /// + /// Registers the as the scoped implementation. + /// + /// The service collection to register services into. protected void RegisterServices(IServiceCollection services) - { + { services.AddScoped(); } + /// public IServiceCollection Services { get; } } } diff --git a/Src/RCommon.Mediatr/MediatREventHandlingBuilderExtensions.cs b/Src/RCommon.Mediatr/MediatREventHandlingBuilderExtensions.cs index 1ed91690..caf931c8 100644 --- a/Src/RCommon.Mediatr/MediatREventHandlingBuilderExtensions.cs +++ b/Src/RCommon.Mediatr/MediatREventHandlingBuilderExtensions.cs @@ -16,14 +16,30 @@ namespace RCommon.MediatR { + /// + /// Extension methods for configuring MediatR-based event handling within the RCommon builder pipeline. + /// public static class MediatREventHandlingBuilderExtensions { + /// + /// Configures MediatR event handling with default settings and no custom actions. + /// + /// The implementation type. + /// The RCommon builder. + /// The for further chaining. public static IRCommonBuilder WithEventHandling(this IRCommonBuilder builder) where T : IMediatREventHandlingBuilder { return WithEventHandling(builder, x => { }, x=> { }); } + /// + /// Configures MediatR event handling with custom builder actions and default MediatR assembly registration. + /// + /// The implementation type. + /// The RCommon builder. + /// Configuration delegate for MediatR event handling. + /// The for further chaining. public static IRCommonBuilder WithEventHandling(this IRCommonBuilder builder, Action actions) where T : IMediatREventHandlingBuilder { @@ -36,7 +52,16 @@ public static IRCommonBuilder WithEventHandling(this IRCommonBuilder builder, return builder; } - public static IRCommonBuilder WithEventHandling(this IRCommonBuilder builder, Action actions, + /// + /// Configures MediatR event handling with both custom event handling actions and custom MediatR service configuration. + /// Registers , wires up MediatR, and creates the event handling builder via reflection. + /// + /// The implementation type. + /// The RCommon builder. + /// Configuration delegate for the event handling builder. + /// Configuration delegate for . + /// The for further chaining. + public static IRCommonBuilder WithEventHandling(this IRCommonBuilder builder, Action actions, Action mediatRActions) where T : IMediatREventHandlingBuilder { @@ -46,12 +71,19 @@ public static IRCommonBuilder WithEventHandling(this IRCommonBuilder builder, builder.Services.AddMediatR(mediatRActions); // This will wire up common event handling - 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 event subscriber and its corresponding MediatR notification handler for the event handling pipeline. + /// Also registers the event-to-producer subscription for correct routing. + /// + /// The event type. Must implement . + /// The subscriber implementation that handles the event. + /// The MediatR event handling builder. public static void AddSubscriber(this IMediatREventHandlingBuilder builder) where TEvent : class, ISerializableEvent where TEventHandler : class, ISubscriber diff --git a/Src/RCommon.Mediatr/Producers/PublishWithMediatREventProducer.cs b/Src/RCommon.Mediatr/Producers/PublishWithMediatREventProducer.cs index 4c6a2098..06df8241 100644 --- a/Src/RCommon.Mediatr/Producers/PublishWithMediatREventProducer.cs +++ b/Src/RCommon.Mediatr/Producers/PublishWithMediatREventProducer.cs @@ -13,6 +13,14 @@ namespace RCommon.MediatR.Producers { + /// + /// An implementation that publishes events to all notification handlers + /// using MediatR's publish (fan-out) semantics via . + /// + /// + /// Use this producer when you want an event to be delivered to all registered notification handlers. + /// For point-to-point delivery, use instead. + /// public class PublishWithMediatREventProducer : IEventProducer { private readonly IMediatorService _mediatorService; @@ -20,6 +28,13 @@ public class PublishWithMediatREventProducer : IEventProducer private readonly IServiceProvider _serviceProvider; private readonly EventSubscriptionManager _subscriptionManager; + /// + /// Initializes a new instance of . + /// + /// The mediator service used to publish events. + /// Logger for diagnostic output. + /// Service provider for creating scoped services during event production. + /// Manages event-to-producer subscriptions for routing decisions. public PublishWithMediatREventProducer(IMediatorService mediatorService, ILogger logger, IServiceProvider serviceProvider, EventSubscriptionManager subscriptionManager) { @@ -29,6 +44,7 @@ public PublishWithMediatREventProducer(IMediatorService mediatorService, ILogger _subscriptionManager = subscriptionManager ?? throw new ArgumentNullException(nameof(subscriptionManager)); } + /// public async Task ProduceEventAsync(TEvent @event, CancellationToken cancellationToken = default) where TEvent : ISerializableEvent { @@ -36,12 +52,15 @@ public async Task ProduceEventAsync(TEvent @event, CancellationToken can { Guard.IsNotNull(@event, nameof(@event)); + // Check if this event type is subscribed to this producer; skip if not routed here if (!_subscriptionManager.ShouldProduceEvent(this.GetType(), typeof(TEvent))) { _logger.LogDebug("{0} skipping event {1} - not subscribed to this producer", new object[] { this.GetGenericTypeName(), typeof(TEvent).Name }); return; } + + // Create a scoped service context for the publish operation using (IServiceScope scope = _serviceProvider.CreateScope()) { if (_logger.IsEnabled(LogLevel.Information)) diff --git a/Src/RCommon.Mediatr/Producers/SendWithMediatREventProducer.cs b/Src/RCommon.Mediatr/Producers/SendWithMediatREventProducer.cs index 5bd707a2..877fe43b 100644 --- a/Src/RCommon.Mediatr/Producers/SendWithMediatREventProducer.cs +++ b/Src/RCommon.Mediatr/Producers/SendWithMediatREventProducer.cs @@ -13,6 +13,14 @@ namespace RCommon.MediatR.Producers { + /// + /// An implementation that sends events to a single request handler + /// using MediatR's send (point-to-point) semantics via . + /// + /// + /// Use this producer for command-style messaging where only one handler should process the event. + /// For fan-out delivery to all handlers, use instead. + /// public class SendWithMediatREventProducer : IEventProducer { private readonly IMediatorService _mediatorService; @@ -20,6 +28,13 @@ public class SendWithMediatREventProducer : IEventProducer private readonly IServiceProvider _serviceProvider; private readonly EventSubscriptionManager _subscriptionManager; + /// + /// Initializes a new instance of . + /// + /// The mediator service used to send events. + /// Logger for diagnostic output. + /// Service provider for creating scoped services during event production. + /// Manages event-to-producer subscriptions for routing decisions. public SendWithMediatREventProducer(IMediatorService mediatorService, ILogger logger, IServiceProvider serviceProvider, EventSubscriptionManager subscriptionManager) { @@ -29,6 +44,7 @@ public SendWithMediatREventProducer(IMediatorService mediatorService, ILogger public async Task ProduceEventAsync(TEvent @event, CancellationToken cancellationToken = default) where TEvent : ISerializableEvent { @@ -36,12 +52,15 @@ public async Task ProduceEventAsync(TEvent @event, CancellationToken can { Guard.IsNotNull(@event, nameof(@event)); + // Check if this event type is subscribed to this producer; skip if not routed here if (!_subscriptionManager.ShouldProduceEvent(this.GetType(), typeof(TEvent))) { _logger.LogDebug("{0} skipping event {1} - not subscribed to this producer", new object[] { this.GetGenericTypeName(), typeof(TEvent).Name }); return; } + + // Create a scoped service context for the send operation using (IServiceScope scope = _serviceProvider.CreateScope()) { if (_logger.IsEnabled(LogLevel.Information)) diff --git a/Src/RCommon.Mediatr/README.md b/Src/RCommon.Mediatr/README.md index 26afed5f..6fa0ff5b 100644 --- a/Src/RCommon.Mediatr/README.md +++ b/Src/RCommon.Mediatr/README.md @@ -1,3 +1,98 @@ - # RCommon.MediatR +# RCommon.MediatR -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 +Integrates [MediatR](https://github.com/jbogard/MediatR) with RCommon's event handling and mediator systems, providing in-process event production, notification/request handling, and pipeline behaviors while programming against RCommon's abstractions. + +## Features + +- Publish events to all notification handlers using MediatR's fan-out (publish) semantics +- Send events to a single request handler using MediatR's point-to-point (send) semantics +- Bridge MediatR notifications and requests to RCommon's `ISubscriber` and `IAppRequestHandler` abstractions +- Mediator adapter (`MediatRAdapter`) implementing `IMediatorAdapter` for request/response and notification patterns +- Built-in pipeline behaviors for logging, validation, and unit of work +- Support for both fire-and-forget requests and request/response patterns +- Event subscription routing ensures events are delivered only to their configured producers + +## Installation + +```shell +dotnet add package RCommon.MediatR +``` + +## Usage + +Configure MediatR for event handling: + +```csharp +using RCommon; +using RCommon.MediatR; + +builder.Services.AddRCommon() + .WithEventHandling(eventHandling => + { + // Register event subscribers + eventHandling.AddSubscriber(); + }); +``` + +Configure MediatR as the mediator with requests, notifications, and pipeline behaviors: + +```csharp +using RCommon; +using RCommon.Mediator.MediatR; + +builder.Services.AddRCommon() + .WithMediator(mediator => + { + // Register notifications (fan-out to all handlers) + mediator.AddNotification(); + + // Register requests (single handler, fire-and-forget) + mediator.AddRequest(); + + // Register requests with responses + mediator.AddRequest(); + + // Add pipeline behaviors + mediator.AddLoggingToRequestPipeline(); + mediator.AddValidationToRequestPipeline(); + mediator.AddUnitOfWorkToRequestPipeline(); + + // Custom MediatR configuration + mediator.Configure(cfg => + { + cfg.RegisterServicesFromAssemblyContaining(); + }); + }); +``` + +## Key Types + +| Type | Description | +|------|-------------| +| `PublishWithMediatREventProducer` | `IEventProducer` that publishes events to all handlers via MediatR publish (fan-out) | +| `SendWithMediatREventProducer` | `IEventProducer` that sends events to a single handler via MediatR send (point-to-point) | +| `MediatRAdapter` | `IMediatorAdapter` implementation that wraps MediatR's `IMediator` for send/publish operations | +| `MediatREventHandler` | Bridges MediatR `INotificationHandler` to RCommon's `ISubscriber` for event handling | +| `MediatRNotificationHandler` | Bridges MediatR notifications to RCommon's `ISubscriber` for app notifications | +| `MediatRRequestHandler` | Bridges MediatR requests to RCommon's `IAppRequestHandler` | +| `MediatRRequestHandler` | Bridges MediatR requests to RCommon's `IAppRequestHandler` for request/response | +| `LoggingRequestBehavior` | Pipeline behavior that logs command handling | +| `ValidatorBehavior` | Pipeline behavior that validates requests before handling | +| `UnitOfWorkRequestBehavior` | Pipeline behavior that wraps handlers in a transactional unit of work | +| `MediatREventHandlingBuilder` | Builder for configuring MediatR-based event handling | +| `MediatRBuilder` | Builder for configuring MediatR as the mediator implementation | + +## Documentation + +For full documentation, visit [rcommon.com](https://rcommon.com). + +## Related Packages + +- [RCommon.Core](https://www.nuget.org/packages/RCommon.Core) - Core abstractions including `IEventProducer` and `ISubscriber` +- [RCommon.Mediator](https://www.nuget.org/packages/RCommon.Mediator) - Mediator abstractions (`IMediatorService`, `IMediatorAdapter`) +- [RCommon.MassTransit](https://www.nuget.org/packages/RCommon.MassTransit) - MassTransit integration for distributed messaging +- [RCommon.Wolverine](https://www.nuget.org/packages/RCommon.Wolverine) - Wolverine integration for distributed messaging + +## License + +Licensed under the [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0). diff --git a/Src/RCommon.Mediatr/Subscribers/IMediatRNotification.cs b/Src/RCommon.Mediatr/Subscribers/IMediatRNotification.cs index 1bec7aa5..f37fb444 100644 --- a/Src/RCommon.Mediatr/Subscribers/IMediatRNotification.cs +++ b/Src/RCommon.Mediatr/Subscribers/IMediatRNotification.cs @@ -3,13 +3,24 @@ namespace RCommon.MediatR.Subscribers { + /// + /// Non-generic marker interface for MediatR notifications within the RCommon framework. + /// Extends to participate in the MediatR pipeline. + /// public interface IMediatRNotification: INotification { } + /// + /// Generic interface for MediatR notifications that wrap an underlying event payload. + /// + /// The type of the wrapped event payload. public interface IMediatRNotification : IMediatRNotification { + /// + /// Gets or sets the underlying notification event payload. + /// TEvent Notification { get; set; } } } diff --git a/Src/RCommon.Mediatr/Subscribers/IMediatRRequest.cs b/Src/RCommon.Mediatr/Subscribers/IMediatRRequest.cs index 964c9a48..d316a8bf 100644 --- a/Src/RCommon.Mediatr/Subscribers/IMediatRRequest.cs +++ b/Src/RCommon.Mediatr/Subscribers/IMediatRRequest.cs @@ -2,19 +2,35 @@ namespace RCommon.MediatR.Subscribers { - + /// + /// Non-generic marker interface for MediatR requests within the RCommon framework. + /// Extends for fire-and-forget request semantics. + /// public interface IMediatRRequest : IRequest { } + /// + /// Generic interface for MediatR requests that return a response. + /// Extends both and . + /// + /// The response type returned by the request handler. public interface IMediatRRequest : IRequest, IMediatRRequest { } + /// + /// Generic interface for MediatR requests that wrap an underlying request payload and return a response. + /// + /// The type of the wrapped request payload. + /// The response type returned by the request handler. public interface IMediatRRequest : IMediatRRequest { + /// + /// Gets the underlying request payload. + /// TRequest Request { get; } } } \ No newline at end of file diff --git a/Src/RCommon.Mediatr/Subscribers/MediatREventHandler.cs b/Src/RCommon.Mediatr/Subscribers/MediatREventHandler.cs index 6829a590..975fe30d 100644 --- a/Src/RCommon.Mediatr/Subscribers/MediatREventHandler.cs +++ b/Src/RCommon.Mediatr/Subscribers/MediatREventHandler.cs @@ -12,27 +12,49 @@ namespace RCommon.MediatR.Subscribers { + /// + /// MediatR notification handler that bridges notifications + /// to RCommon's abstraction for serializable event handling. + /// Resolves the subscriber from the DI container and delegates event handling. + /// + /// The serializable event type. Must implement . + /// The MediatR notification wrapper type. + /// + /// This differs from in that it handles + /// types used in the event handling pipeline, rather than + /// types used in the mediator pipeline. + /// public class MediatREventHandler : INotificationHandler where TEvent : class, ISerializableEvent where TNotification : MediatRNotification { private readonly IServiceProvider _serviceProvider; + /// + /// Initializes a new instance of . + /// + /// The service provider used to resolve at runtime. public MediatREventHandler(IServiceProvider serviceProvider) { _serviceProvider = serviceProvider; } + /// + /// Handles the MediatR notification by resolving the RCommon subscriber and delegating the event. + /// + /// The MediatR notification containing the wrapped serializable event. + /// Cancellation token. + /// Thrown when the subscriber cannot be resolved from the service provider. public async Task Handle(TNotification notification, CancellationToken cancellationToken) { // Resolve the actual event handler that we want to execute - var subscriber = (ISubscriber)_serviceProvider.GetService(typeof(ISubscriber)); + var subscriber = (ISubscriber?)_serviceProvider.GetService(typeof(ISubscriber)); Guard.Against(subscriber == null, "ISubscriber of type: " + typeof(TEvent).GetGenericTypeName() + " could not be resolved by IServiceProvider"); // Handle the event using the event handler we resolved - await subscriber.HandleAsync(notification.Notification); + await subscriber!.HandleAsync(notification.Notification); } } } diff --git a/Src/RCommon.Mediatr/Subscribers/MediatRNotification.cs b/Src/RCommon.Mediatr/Subscribers/MediatRNotification.cs index 7804065f..2bb99528 100644 --- a/Src/RCommon.Mediatr/Subscribers/MediatRNotification.cs +++ b/Src/RCommon.Mediatr/Subscribers/MediatRNotification.cs @@ -7,14 +7,24 @@ namespace RCommon.MediatR.Subscribers { + /// + /// Wrapper class that adapts an event of type into an + /// so it can be published through the MediatR notification pipeline. + /// + /// The type of the event being wrapped. public class MediatRNotification : IMediatRNotification { + /// + /// Initializes a new instance of with the specified event payload. + /// + /// The event payload to wrap. public MediatRNotification(TEvent notification) { Notification = notification; } + /// public TEvent Notification { get; set; } } } diff --git a/Src/RCommon.Mediatr/Subscribers/MediatRNotificationHandler.cs b/Src/RCommon.Mediatr/Subscribers/MediatRNotificationHandler.cs index a5fb4d8e..58460ebf 100644 --- a/Src/RCommon.Mediatr/Subscribers/MediatRNotificationHandler.cs +++ b/Src/RCommon.Mediatr/Subscribers/MediatRNotificationHandler.cs @@ -13,27 +13,44 @@ namespace RCommon.MediatR.Subscribers { + /// + /// MediatR notification handler that bridges notifications + /// to RCommon's abstraction for application notifications. + /// Resolves the subscriber from the DI container and delegates event handling. + /// + /// The application notification type. Must implement . + /// The MediatR notification wrapper type. public class MediatRNotificationHandler : INotificationHandler where T : class, IAppNotification where TNotification : MediatRNotification { private readonly IServiceProvider _serviceProvider; + /// + /// Initializes a new instance of . + /// + /// The service provider used to resolve at runtime. public MediatRNotificationHandler(IServiceProvider serviceProvider) { _serviceProvider = serviceProvider; } + /// + /// Handles the MediatR notification by resolving the RCommon subscriber and delegating the event. + /// + /// The MediatR notification containing the wrapped event. + /// Cancellation token. + /// Thrown when the subscriber cannot be resolved from the service provider. public async Task Handle(TNotification notification, CancellationToken cancellationToken) { // Resolve the actual event handler that we want to execute - var subscriber = (ISubscriber) _serviceProvider.GetService(typeof(ISubscriber)); - - Guard.Against(subscriber == null, + var subscriber = (ISubscriber?) _serviceProvider.GetService(typeof(ISubscriber)); + + Guard.Against(subscriber == null, "ISubscriber of type: " + typeof(T).GetGenericTypeName() + " could not be resolved by IServiceProvider"); - + // Handle the event using the event handler we resolved - await subscriber.HandleAsync(notification.Notification); + await subscriber!.HandleAsync(notification.Notification); } } } diff --git a/Src/RCommon.Mediatr/Subscribers/MediatRRequest.cs b/Src/RCommon.Mediatr/Subscribers/MediatRRequest.cs index 53b2143a..469c8a23 100644 --- a/Src/RCommon.Mediatr/Subscribers/MediatRRequest.cs +++ b/Src/RCommon.Mediatr/Subscribers/MediatRRequest.cs @@ -8,25 +8,46 @@ namespace RCommon.MediatR.Subscribers { + /// + /// Wrapper class that adapts a request of type into an + /// for fire-and-forget processing through the MediatR pipeline. + /// + /// The type of the wrapped request payload. public class MediatRRequest : IMediatRRequest { + /// + /// Initializes a new instance of with the specified request payload. + /// + /// The request payload to wrap. public MediatRRequest(TRequest request) { Request = request; } + /// public TRequest Request { get; } } + /// + /// Wrapper class that adapts a request of type into an + /// for request/response processing through the MediatR pipeline. + /// + /// The type of the wrapped request payload. + /// The response type returned by the handler. public class MediatRRequest : IMediatRRequest { + /// + /// Initializes a new instance of with the specified request payload. + /// + /// The request payload to wrap. public MediatRRequest(TRequest request) { Request = request; } + /// public TRequest Request { get; } } } diff --git a/Src/RCommon.Mediatr/Subscribers/MediatRRequestHandler.cs b/Src/RCommon.Mediatr/Subscribers/MediatRRequestHandler.cs index 7dbd3349..e95f0876 100644 --- a/Src/RCommon.Mediatr/Subscribers/MediatRRequestHandler.cs +++ b/Src/RCommon.Mediatr/Subscribers/MediatRRequestHandler.cs @@ -13,51 +13,87 @@ namespace RCommon.MediatR.Subscribers { + /// + /// MediatR request handler that bridges requests + /// to RCommon's abstraction for fire-and-forget processing. + /// Resolves the handler from the DI container and delegates request handling. + /// + /// The application request type. Must implement . + /// The MediatR request wrapper type. public class MediatRRequestHandler : IRequestHandler where T : class, IAppRequest where TRequest : MediatRRequest { private readonly IServiceProvider _serviceProvider; + /// + /// Initializes a new instance of . + /// + /// The service provider used to resolve at runtime. public MediatRRequestHandler(IServiceProvider serviceProvider) { _serviceProvider = serviceProvider; } + /// + /// Handles the MediatR request by resolving the RCommon request handler and delegating the request. + /// + /// The MediatR request containing the wrapped application request. + /// Cancellation token. + /// Thrown when the handler cannot be resolved from the service provider. public async Task Handle(TRequest request, CancellationToken cancellationToken) { // Resolve the actual event handler that we want to execute - var handler = (IAppRequestHandler)_serviceProvider.GetService(typeof(IAppRequestHandler)); + var handler = (IAppRequestHandler?)_serviceProvider.GetService(typeof(IAppRequestHandler)); Guard.Against(handler == null, "IAppRequestHandler of type: " + typeof(T).GetGenericTypeName() + " could not be resolved by IServiceProvider"); // Handle the event using the event handler we resolved - await handler.HandleAsync(request.Request); + await handler!.HandleAsync(request.Request); } } + /// + /// MediatR request handler that bridges requests + /// to RCommon's abstraction for request/response processing. + /// Resolves the handler from the DI container and delegates request handling. + /// + /// The application request type. Must implement . + /// The MediatR request wrapper type. + /// The response type returned by the handler. public class MediatRRequestHandler : IRequestHandler where T : class, IAppRequest where TRequest : MediatRRequest { private readonly IServiceProvider _serviceProvider; + /// + /// Initializes a new instance of . + /// + /// The service provider used to resolve at runtime. public MediatRRequestHandler(IServiceProvider serviceProvider) { _serviceProvider = serviceProvider; } + /// + /// Handles the MediatR request by resolving the RCommon request handler and delegating the request. + /// + /// The MediatR request containing the wrapped application request. + /// Cancellation token. + /// The response produced by the resolved application request handler. + /// Thrown when the handler cannot be resolved from the service provider. public async Task Handle(TRequest request, CancellationToken cancellationToken) { // Resolve the actual event handler that we want to execute - var handler = (IAppRequestHandler)_serviceProvider.GetService(typeof(IAppRequestHandler)); + var handler = (IAppRequestHandler?)_serviceProvider.GetService(typeof(IAppRequestHandler)); Guard.Against(handler == null, "IAppRequestHandler of type: " + typeof(T).GetGenericTypeName() + " could not be resolved by IServiceProvider"); // Handle the event using the event handler we resolved - return await handler.HandleAsync(request.Request); + return await handler!.HandleAsync(request.Request); } } } diff --git a/Src/RCommon.MemoryCache/DistributedMemoryCacheBuilder.cs b/Src/RCommon.MemoryCache/DistributedMemoryCacheBuilder.cs index d691c874..c438c011 100644 --- a/Src/RCommon.MemoryCache/DistributedMemoryCacheBuilder.cs +++ b/Src/RCommon.MemoryCache/DistributedMemoryCacheBuilder.cs @@ -7,19 +7,38 @@ namespace RCommon.MemoryCache { + /// + /// Builder for configuring distributed memory caching using the Microsoft + /// abstraction + /// backed by an in-memory store. + /// + /// + /// This is the concrete builder activated by + /// + /// when DistributedMemoryCacheBuilder is specified as the type parameter. + /// public class DistributedMemoryCacheBuilder : IDistributedMemoryCachingBuilder { + /// + /// Initializes a new instance of the class. + /// + /// The RCommon builder whose is used for service registration. public DistributedMemoryCacheBuilder(IRCommonBuilder builder) { Services = builder.Services; this.RegisterServices(Services); } + /// + /// Registers any default services required by the distributed memory cache builder. + /// + /// The service collection to register services into. protected void RegisterServices(IServiceCollection services) { } + /// public IServiceCollection Services { get; } } } diff --git a/Src/RCommon.MemoryCache/DistributedMemoryCacheService.cs b/Src/RCommon.MemoryCache/DistributedMemoryCacheService.cs index 3f5f2f68..376aac7a 100644 --- a/Src/RCommon.MemoryCache/DistributedMemoryCacheService.cs +++ b/Src/RCommon.MemoryCache/DistributedMemoryCacheService.cs @@ -10,47 +10,67 @@ namespace RCommon.MemoryCache { /// - /// Just a proxy for Distributed memory caching implemented through caching abstractions + /// A proxy for distributed memory caching implemented through the + /// abstraction. /// - /// This gives us a uniform way for getting/setting cache no matter the caching strategy + /// + /// This gives a uniform way for getting/setting cache no matter the caching strategy. + /// Data is serialized to JSON via before being stored + /// in the distributed cache, and deserialized on retrieval. + /// public class DistributedMemoryCacheService : ICacheService { private readonly IDistributedCache _distributedCache; private readonly IJsonSerializer _jsonSerializer; + /// + /// Initializes a new instance of the class. + /// + /// The underlying distributed cache implementation. + /// The JSON serializer used to serialize/deserialize cached values. public DistributedMemoryCacheService(IDistributedCache distributedCache, IJsonSerializer jsonSerializer) { _distributedCache = distributedCache; _jsonSerializer = jsonSerializer; } + /// public TData GetOrCreate(object key, Func data) { - var json = _distributedCache.GetString(key.ToString()); + var cacheKey = key.ToString()!; + var json = _distributedCache.GetString(cacheKey); if (json == null) { - _distributedCache.SetString(key.ToString(), _jsonSerializer.Serialize(data())); - return data(); + // Cache miss: invoke the factory, serialize and store the result, then return it + var result = data(); + _distributedCache.SetString(cacheKey, _jsonSerializer.Serialize(result!)); + return result; } else { - return _jsonSerializer.Deserialize(json); + // Cache hit: deserialize the stored JSON back into the requested type + return _jsonSerializer.Deserialize(json)!; } } + /// public async Task GetOrCreateAsync(object key, Func data) { - var json = await _distributedCache.GetStringAsync(key.ToString()).ConfigureAwait(false); + var cacheKey = key.ToString()!; + var json = await _distributedCache.GetStringAsync(cacheKey).ConfigureAwait(false); if (json == null) { - await _distributedCache.SetStringAsync(key.ToString(), _jsonSerializer.Serialize(data())).ConfigureAwait(false); - return data(); + // Cache miss: invoke the factory, serialize and store the result asynchronously + var result = data(); + await _distributedCache.SetStringAsync(cacheKey, _jsonSerializer.Serialize(result!)).ConfigureAwait(false); + return result; } else { - return _jsonSerializer.Deserialize(json); + // Cache hit: deserialize the stored JSON back into the requested type + return _jsonSerializer.Deserialize(json)!; } } } diff --git a/Src/RCommon.MemoryCache/IDistributedMemoryCachingBuilder.cs b/Src/RCommon.MemoryCache/IDistributedMemoryCachingBuilder.cs index a500c5a1..949b5f3e 100644 --- a/Src/RCommon.MemoryCache/IDistributedMemoryCachingBuilder.cs +++ b/Src/RCommon.MemoryCache/IDistributedMemoryCachingBuilder.cs @@ -7,6 +7,13 @@ namespace RCommon.MemoryCache { + /// + /// Marker interface for configuring distributed caching that is backed by an in-memory store. + /// + /// + /// Extends to allow memory-specific distributed + /// cache configuration via extension methods in . + /// public interface IDistributedMemoryCachingBuilder : IDistributedCachingBuilder { } diff --git a/Src/RCommon.MemoryCache/IDistributedMemoryCachingBuilderExtensions.cs b/Src/RCommon.MemoryCache/IDistributedMemoryCachingBuilderExtensions.cs index d4dcda72..6360daba 100644 --- a/Src/RCommon.MemoryCache/IDistributedMemoryCachingBuilderExtensions.cs +++ b/Src/RCommon.MemoryCache/IDistributedMemoryCachingBuilderExtensions.cs @@ -10,8 +10,18 @@ namespace RCommon.MemoryCache { + /// + /// Extension methods for that configure + /// distributed memory cache options and expression caching. + /// public static class IDistributedMemoryCachingBuilderExtensions { + /// + /// Configures the underlying for the distributed memory cache. + /// + /// The distributed memory caching builder. + /// A delegate to configure . + /// The same for chaining. public static IDistributedMemoryCachingBuilder Configure(this IDistributedMemoryCachingBuilder builder, Action actions) { builder.Services.AddDistributedMemoryCache(actions); @@ -19,7 +29,7 @@ public static IDistributedMemoryCachingBuilder Configure(this IDistributedMemory } /// - /// This greatly improves performance across various areas of RCommon which use generics and reflection heavily + /// This greatly improves performance across various areas of RCommon which use generics and reflection heavily /// to compile expressions and lambdas /// /// Builder @@ -34,22 +44,28 @@ public static IDistributedMemoryCachingBuilder CacheDynamicallyCompiledExpressio builder.Services.TryAddTransient, CommonFactory>(); ConfigureCachingOptions(builder); - // Add Caching Factory + // Add Caching Factory — resolves the correct ICacheService based on the ExpressionCachingStrategy builder.Services.TryAddTransient>(serviceProvider => strategy => { switch (strategy) { case ExpressionCachingStrategy.Default: - return serviceProvider.GetService(); + return serviceProvider.GetRequiredService(); default: - return serviceProvider.GetService(); + return serviceProvider.GetRequiredService(); } }); return builder; } - private static void ConfigureCachingOptions(IDistributedMemoryCachingBuilder builder, Action configure = null) + /// + /// Configures with default or custom settings, enabling caching + /// and expression caching flags. + /// + /// The distributed memory caching builder. + /// An optional delegate to customize . When null, defaults are applied. + private static void ConfigureCachingOptions(IDistributedMemoryCachingBuilder builder, Action? configure = null) { if (configure == null) diff --git a/Src/RCommon.MemoryCache/IInMemoryCachingBuilder.cs b/Src/RCommon.MemoryCache/IInMemoryCachingBuilder.cs index 50aea9fd..bd357858 100644 --- a/Src/RCommon.MemoryCache/IInMemoryCachingBuilder.cs +++ b/Src/RCommon.MemoryCache/IInMemoryCachingBuilder.cs @@ -7,6 +7,13 @@ namespace RCommon.MemoryCache { + /// + /// Marker interface for configuring in-memory caching backed by . + /// + /// + /// Extends to allow in-memory-specific cache + /// configuration via extension methods in . + /// public interface IInMemoryCachingBuilder : IMemoryCachingBuilder { } diff --git a/Src/RCommon.MemoryCache/IInMemoryCachingBuilderExtensions.cs b/Src/RCommon.MemoryCache/IInMemoryCachingBuilderExtensions.cs index f7ab9a40..f758328f 100644 --- a/Src/RCommon.MemoryCache/IInMemoryCachingBuilderExtensions.cs +++ b/Src/RCommon.MemoryCache/IInMemoryCachingBuilderExtensions.cs @@ -10,8 +10,18 @@ namespace RCommon.MemoryCache { + /// + /// Extension methods for that configure + /// in-memory cache options and expression caching. + /// public static class IInMemoryCachingBuilderExtensions { + /// + /// Configures the underlying for the in-memory cache. + /// + /// The in-memory caching builder. + /// A delegate to configure . + /// The same for chaining. public static IInMemoryCachingBuilder Configure(this IInMemoryCachingBuilder builder, Action actions) { builder.Services.AddMemoryCache(actions); @@ -19,7 +29,7 @@ public static IInMemoryCachingBuilder Configure(this IInMemoryCachingBuilder bui } /// - /// This greatly improves performance across various areas of RCommon which use generics and reflection heavily + /// This greatly improves performance across various areas of RCommon which use generics and reflection heavily /// to compile expressions and lambdas /// /// Builder @@ -34,22 +44,28 @@ public static IInMemoryCachingBuilder CacheDynamicallyCompiledExpressions(this I builder.Services.TryAddTransient, CommonFactory>(); ConfigureCachingOptions(builder); - // Add Caching Factory + // Add Caching Factory — resolves the correct ICacheService based on the ExpressionCachingStrategy builder.Services.TryAddTransient>(serviceProvider => strategy => { switch (strategy) { case ExpressionCachingStrategy.Default: - return serviceProvider.GetService(); + return serviceProvider.GetRequiredService(); default: - return serviceProvider.GetService(); + return serviceProvider.GetRequiredService(); } }); return builder; } - private static void ConfigureCachingOptions(IInMemoryCachingBuilder builder, Action configure = null) + /// + /// Configures with default or custom settings, enabling caching + /// and expression caching flags. + /// + /// The in-memory caching builder. + /// An optional delegate to customize . When null, defaults are applied. + private static void ConfigureCachingOptions(IInMemoryCachingBuilder builder, Action? configure = null) { if (configure == null) diff --git a/Src/RCommon.MemoryCache/InMemoryCacheService.cs b/Src/RCommon.MemoryCache/InMemoryCacheService.cs index cb0d19f7..88789d7c 100644 --- a/Src/RCommon.MemoryCache/InMemoryCacheService.cs +++ b/Src/RCommon.MemoryCache/InMemoryCacheService.cs @@ -9,32 +9,42 @@ namespace RCommon.MemoryCache { /// - /// Just a proxy for memory caching implemented through caching abstractions + /// A proxy for in-process memory caching implemented through the + /// abstraction. /// - /// This gives us a uniform way for getting/setting cache no matter the caching strategy + /// + /// This gives a uniform way for getting/setting cache no matter the caching strategy. + /// Delegates directly to and its async counterpart. + /// public class InMemoryCacheService : ICacheService { private readonly IMemoryCache _memoryCache; + /// + /// Initializes a new instance of the class. + /// + /// The underlying implementation. public InMemoryCacheService(IMemoryCache memoryCache) { _memoryCache = memoryCache; } + /// public TData GetOrCreate(object key, Func data) { return _memoryCache.GetOrCreate(key, cacheEntry => { return data(); - }); + })!; } + /// public async Task GetOrCreateAsync(object key, Func data) { - return await _memoryCache.GetOrCreateAsync(key, async cacheEntry => + return (await _memoryCache.GetOrCreateAsync(key, async cacheEntry => { return await Task.FromResult(data()); - }).ConfigureAwait(false); + }).ConfigureAwait(false))!; } } } diff --git a/Src/RCommon.MemoryCache/InMemoryCachingBuilder.cs b/Src/RCommon.MemoryCache/InMemoryCachingBuilder.cs index 9053b2d2..1fcc78a9 100644 --- a/Src/RCommon.MemoryCache/InMemoryCachingBuilder.cs +++ b/Src/RCommon.MemoryCache/InMemoryCachingBuilder.cs @@ -8,19 +8,37 @@ namespace RCommon.MemoryCache { + /// + /// Builder for configuring in-memory caching using the Microsoft + /// abstraction. + /// + /// + /// This is the concrete builder activated by + /// + /// when InMemoryCachingBuilder is specified as the type parameter. + /// public class InMemoryCachingBuilder : IInMemoryCachingBuilder { + /// + /// Initializes a new instance of the class. + /// + /// The RCommon builder whose is used for service registration. public InMemoryCachingBuilder(IRCommonBuilder builder) { Services = builder.Services; this.RegisterServices(Services); } + /// + /// Registers any default services required by the in-memory cache builder. + /// + /// The service collection to register services into. protected void RegisterServices(IServiceCollection services) { - + } + /// public IServiceCollection Services { get; } } } diff --git a/Src/RCommon.MemoryCache/RCommon.MemoryCache.csproj b/Src/RCommon.MemoryCache/RCommon.MemoryCache.csproj index b92495c5..d917779c 100644 --- a/Src/RCommon.MemoryCache/RCommon.MemoryCache.csproj +++ b/Src/RCommon.MemoryCache/RCommon.MemoryCache.csproj @@ -2,6 +2,7 @@ net8.0;net9.0;net10.0 + enable True RCommon.MemoryCache https://rcommon.com diff --git a/Src/RCommon.MemoryCache/README.md b/Src/RCommon.MemoryCache/README.md index d38fe775..358751e7 100644 --- a/Src/RCommon.MemoryCache/README.md +++ b/Src/RCommon.MemoryCache/README.md @@ -1,3 +1,66 @@ - # RCommon.MemoryCache +# RCommon.MemoryCache -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 two in-process memory caching implementations of `ICacheService`: one backed by `IMemoryCache` and another backed by `IDistributedCache` (in-memory distributed cache), with fluent builder extensions for DI configuration. + +## Features + +- `InMemoryCacheService` -- delegates to Microsoft's `IMemoryCache` for fast in-process caching with `GetOrCreate`/`GetOrCreateAsync` +- `DistributedMemoryCacheService` -- delegates to `IDistributedCache` (in-memory distributed store) with automatic JSON serialization via `IJsonSerializer` +- `InMemoryCachingBuilder` and `DistributedMemoryCacheBuilder` for plugging into the `AddRCommon()` builder pipeline +- `Configure()` extension to customize `MemoryCacheOptions` or `MemoryDistributedCacheOptions` +- `CacheDynamicallyCompiledExpressions()` extension to enable expression caching, which improves performance in areas of RCommon that compile expressions and lambdas at runtime + +## Installation + +```shell +dotnet add package RCommon.MemoryCache +``` + +## Usage + +```csharp +using RCommon; +using RCommon.MemoryCache; + +services.AddRCommon(builder => +{ + // Option 1: In-process IMemoryCache + builder.WithMemoryCaching(cache => + { + cache.Configure(options => options.SizeLimit = 1024); + cache.CacheDynamicallyCompiledExpressions(); + }); + + // Option 2: Distributed memory cache (IDistributedCache backed by memory) + builder.WithDistributedCaching(cache => + { + cache.Configure(options => options.SizeLimit = 2048); + cache.CacheDynamicallyCompiledExpressions(); + }); +}); +``` + +## Key Types + +| Type | Description | +|------|-------------| +| `InMemoryCacheService` | `ICacheService` implementation backed by `IMemoryCache` | +| `DistributedMemoryCacheService` | `ICacheService` implementation backed by `IDistributedCache` with JSON serialization | +| `InMemoryCachingBuilder` | Concrete builder for configuring in-process memory caching | +| `DistributedMemoryCacheBuilder` | Concrete builder for configuring distributed memory caching | +| `IInMemoryCachingBuilder` | Builder interface extending `IMemoryCachingBuilder` | +| `IDistributedMemoryCachingBuilder` | Builder interface extending `IDistributedCachingBuilder` | + +## Documentation + +For full documentation, visit [rcommon.com](https://rcommon.com). + +## Related Packages + +- [RCommon.Caching](https://www.nuget.org/packages/RCommon.Caching) - Core caching abstractions (`ICacheService`, `CacheKey`, builder contracts) +- [RCommon.RedisCache](https://www.nuget.org/packages/RCommon.RedisCache) - Redis-backed distributed cache implementation +- [RCommon.Persistence.Caching.MemoryCache](https://www.nuget.org/packages/RCommon.Persistence.Caching.MemoryCache) - Wires memory caching into the persistence caching repository decorators + +## License + +Licensed under the [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0). diff --git a/Src/RCommon.Models/Commands/CommandResult.cs b/Src/RCommon.Models/Commands/CommandResult.cs index 2a9aa856..72c70e03 100644 --- a/Src/RCommon.Models/Commands/CommandResult.cs +++ b/Src/RCommon.Models/Commands/CommandResult.cs @@ -8,13 +8,27 @@ namespace RCommon.Models.Commands { + /// + /// Default implementation of that wraps + /// an execution result returned from a command handler. + /// + /// The type of conveying the command outcome. + /// + /// This is a record type decorated with to support + /// serialization across service boundaries. + /// [DataContract] public record CommandResult : ICommandResult where TExecutionResult : IExecutionResult { + /// [DataMember] public TExecutionResult Result { get; } + /// + /// Initializes a new instance of the record. + /// + /// The execution result representing the outcome of the command. public CommandResult(TExecutionResult result) { Result = result; diff --git a/Src/RCommon.Models/Commands/ICommand.cs b/Src/RCommon.Models/Commands/ICommand.cs index fe14835e..d80283b1 100644 --- a/Src/RCommon.Models/Commands/ICommand.cs +++ b/Src/RCommon.Models/Commands/ICommand.cs @@ -7,10 +7,24 @@ namespace RCommon.Models.Commands { + /// + /// Marker interface representing a command in the CQRS pattern. + /// Commands encapsulate intent to change the system state and are typically + /// dispatched to a single handler for processing. + /// + /// public interface ICommand : IModel { } + /// + /// Generic command interface that specifies the expected execution result type. + /// Use this interface when a command handler should return a typed result indicating + /// the outcome of the operation. + /// + /// The type of returned by the command handler. + /// + /// public interface ICommand : ICommand where TResult : IExecutionResult { diff --git a/Src/RCommon.Models/Commands/ICommandResult.cs b/Src/RCommon.Models/Commands/ICommandResult.cs index c54bbf93..3d981a3f 100644 --- a/Src/RCommon.Models/Commands/ICommandResult.cs +++ b/Src/RCommon.Models/Commands/ICommandResult.cs @@ -2,9 +2,19 @@ namespace RCommon.Models.Commands { + /// + /// Represents the result of executing a command, wrapping an + /// to indicate whether the command succeeded or failed. + /// + /// The type of that conveys the execution outcome. + /// + /// public interface ICommandResult : IModel where TExecutionResult : IExecutionResult { + /// + /// Gets the execution result indicating the outcome of the command. + /// TExecutionResult Result { get; } } } diff --git a/Src/RCommon.Models/Events/IAsyncEvent.cs b/Src/RCommon.Models/Events/IAsyncEvent.cs index bf1c9d94..79ac9ecd 100644 --- a/Src/RCommon.Models/Events/IAsyncEvent.cs +++ b/Src/RCommon.Models/Events/IAsyncEvent.cs @@ -6,6 +6,16 @@ namespace RCommon.Models.Events { + /// + /// Marker interface for events that are dispatched and handled asynchronously. + /// Async events are typically published to a message bus or queue for eventual processing. + /// + /// + /// Extends because asynchronous delivery mechanisms + /// (e.g., message brokers) generally require events to be serializable. + /// + /// + /// public interface IAsyncEvent : ISerializableEvent { } diff --git a/Src/RCommon.Models/Events/ISerializableEvent.cs b/Src/RCommon.Models/Events/ISerializableEvent.cs index 998dc945..8408b44e 100644 --- a/Src/RCommon.Models/Events/ISerializableEvent.cs +++ b/Src/RCommon.Models/Events/ISerializableEvent.cs @@ -6,6 +6,12 @@ namespace RCommon.Models.Events { + /// + /// Marker interface indicating that an event can be serialized for transport + /// across process boundaries (e.g., message queues, event stores, distributed systems). + /// + /// + /// public interface ISerializableEvent { } diff --git a/Src/RCommon.Models/Events/ISyncEvent.cs b/Src/RCommon.Models/Events/ISyncEvent.cs index 782c7964..f94dff3b 100644 --- a/Src/RCommon.Models/Events/ISyncEvent.cs +++ b/Src/RCommon.Models/Events/ISyncEvent.cs @@ -6,6 +6,16 @@ namespace RCommon.Models.Events { + /// + /// Marker interface for events that are dispatched and handled synchronously + /// within the same process boundary. + /// + /// + /// Extends to allow synchronous events to + /// optionally participate in serialization scenarios (e.g., logging, auditing). + /// + /// + /// public interface ISyncEvent : ISerializableEvent { } diff --git a/Src/RCommon.Models/ExecutionResults/ExecutionResult.cs b/Src/RCommon.Models/ExecutionResults/ExecutionResult.cs index d3bfef80..aec9b6ec 100644 --- a/Src/RCommon.Models/ExecutionResults/ExecutionResult.cs +++ b/Src/RCommon.Models/ExecutionResults/ExecutionResult.cs @@ -27,20 +27,58 @@ namespace RCommon.Models.ExecutionResults { + /// + /// Abstract base record for execution results, providing factory methods + /// to create and instances. + /// + /// + /// Cached singleton instances are used for and the parameterless + /// to avoid unnecessary allocations for common result types. + /// + /// [DataContract] public abstract record ExecutionResult : IExecutionResult { + // Cached singleton for the common success case to avoid repeated allocations. private static readonly IExecutionResult SuccessResult = new SuccessExecutionResult(); + + // Cached singleton for a generic failure with no error messages. private static readonly IExecutionResult FailedResult = new FailedExecutionResult(Enumerable.Empty()); + /// + /// Returns a cached instance. + /// + /// An representing a successful operation. public static IExecutionResult Success() => SuccessResult; + + /// + /// Returns a cached instance with no error messages. + /// + /// An representing a failed operation. public static IExecutionResult Failed() => FailedResult; + + /// + /// Creates a new with the specified error messages. + /// + /// A collection of error messages describing the failure. + /// An representing a failed operation with error details. public static IExecutionResult Failed(IEnumerable errors) => new FailedExecutionResult(errors); + + /// + /// Creates a new with the specified error messages. + /// + /// One or more error messages describing the failure. + /// An representing a failed operation with error details. public static IExecutionResult Failed(params string[] errors) => new FailedExecutionResult(errors); + /// [DataMember] public abstract bool IsSuccess { get; } + /// + /// Returns a string representation of the execution result, including the success status. + /// + /// A string in the format "ExecutionResult - IsSuccess:{value}". public override string ToString() { return $"ExecutionResult - IsSuccess:{IsSuccess}"; diff --git a/Src/RCommon.Models/ExecutionResults/FailedExecutionResult.cs b/Src/RCommon.Models/ExecutionResults/FailedExecutionResult.cs index 106f4815..35d13db5 100644 --- a/Src/RCommon.Models/ExecutionResults/FailedExecutionResult.cs +++ b/Src/RCommon.Models/ExecutionResults/FailedExecutionResult.cs @@ -27,20 +27,41 @@ namespace RCommon.Models.ExecutionResults { + /// + /// Represents a failed execution result, optionally containing one or more error messages + /// that describe the reason(s) for failure. + /// + /// + /// [DataContract] public record FailedExecutionResult : ExecutionResult { + /// + /// Gets the collection of error messages associated with the failure. + /// public IReadOnlyCollection Errors { get; } + /// + /// Initializes a new instance of the record. + /// + /// + /// A collection of error messages describing the failure. If null, an empty collection is used. + /// public FailedExecutionResult( IEnumerable errors) { + // Materialize to a list and guard against null to ensure Errors is never null. Errors = (errors ?? Enumerable.Empty()).ToList(); } + /// [DataMember] public override bool IsSuccess { get; } = false; + /// + /// Returns a string describing the failure, including error messages if any are present. + /// + /// A human-readable description of the failure and its associated errors. public override string ToString() { return Errors.Any() diff --git a/Src/RCommon.Models/ExecutionResults/IExecutionResult.cs b/Src/RCommon.Models/ExecutionResults/IExecutionResult.cs index 19a1c482..86ca1036 100644 --- a/Src/RCommon.Models/ExecutionResults/IExecutionResult.cs +++ b/Src/RCommon.Models/ExecutionResults/IExecutionResult.cs @@ -23,8 +23,18 @@ namespace RCommon.Models.ExecutionResults { + /// + /// Represents the outcome of an operation, indicating success or failure. + /// This is the base contract for all execution results used by commands and handlers. + /// + /// + /// + /// public interface IExecutionResult : IModel { + /// + /// Gets a value indicating whether the operation completed successfully. + /// bool IsSuccess { get; } } } diff --git a/Src/RCommon.Models/ExecutionResults/SuccessExecutionResult.cs b/Src/RCommon.Models/ExecutionResults/SuccessExecutionResult.cs index 0a206cfb..bf5aee44 100644 --- a/Src/RCommon.Models/ExecutionResults/SuccessExecutionResult.cs +++ b/Src/RCommon.Models/ExecutionResults/SuccessExecutionResult.cs @@ -25,12 +25,26 @@ namespace RCommon.Models.ExecutionResults { + /// + /// Represents a successful execution result with set to true. + /// + /// + /// Typically obtained via rather than instantiated directly, + /// which returns a cached singleton to reduce allocations. + /// + /// + /// [DataContract] public record SuccessExecutionResult : ExecutionResult { + /// [DataMember] public override bool IsSuccess { get; } = true; + /// + /// Returns a human-readable string indicating successful execution. + /// + /// The string "Successful execution". public override string ToString() { return "Successful execution"; diff --git a/Src/RCommon.Models/IModel.cs b/Src/RCommon.Models/IModel.cs index b037d026..6e531b85 100644 --- a/Src/RCommon.Models/IModel.cs +++ b/Src/RCommon.Models/IModel.cs @@ -6,6 +6,11 @@ namespace RCommon.Models { + /// + /// Base marker interface for all models in the RCommon framework. + /// Implementing this interface identifies a type as an RCommon model, enabling + /// consistent type constraints across commands, queries, events, and execution results. + /// public interface IModel { } diff --git a/Src/RCommon.Models/IPaginatedListRequest.cs b/Src/RCommon.Models/IPaginatedListRequest.cs index fda9b160..99c3b127 100644 --- a/Src/RCommon.Models/IPaginatedListRequest.cs +++ b/Src/RCommon.Models/IPaginatedListRequest.cs @@ -1,10 +1,32 @@ namespace RCommon.Models { + /// + /// Defines the contract for a paginated list request, specifying paging and sorting parameters + /// used to retrieve a subset of data from a larger collection. + /// + /// + /// public interface IPaginatedListRequest : IModel { + /// + /// Gets or sets the one-based page number to retrieve. + /// int PageNumber { get; set; } + + /// + /// Gets or sets the number of items per page. + /// int PageSize { get; set; } - string SortBy { get; set; } + + /// + /// Gets or sets the name of the property to sort by. + /// + string? SortBy { get; set; } + + /// + /// Gets or sets the sort direction (ascending, descending, or none). + /// + /// SortDirectionEnum SortDirection { get; set; } } } diff --git a/Src/RCommon.Models/ISearchPaginatedListRequest.cs b/Src/RCommon.Models/ISearchPaginatedListRequest.cs index a3501cc6..0e3dd6a2 100644 --- a/Src/RCommon.Models/ISearchPaginatedListRequest.cs +++ b/Src/RCommon.Models/ISearchPaginatedListRequest.cs @@ -1,7 +1,16 @@ namespace RCommon.Models { + /// + /// Extends paginated list requests with a free-text search capability. + /// Implementations combine search filtering with pagination and sorting. + /// + /// + /// public interface ISearchPaginatedListRequest : IModel { - string SearchString { get; set; } + /// + /// Gets or sets the search string used to filter results. + /// + string? SearchString { get; set; } } } diff --git a/Src/RCommon.Models/PaginatedListModel.cs b/Src/RCommon.Models/PaginatedListModel.cs index fca80039..93b97f2f 100644 --- a/Src/RCommon.Models/PaginatedListModel.cs +++ b/Src/RCommon.Models/PaginatedListModel.cs @@ -11,17 +11,35 @@ namespace RCommon.Models /// Represents a Data Transfer Object (DTO) that is typically used to encapsulate a PaginatedList so that it can be /// delivered to the application layer. This should be an immutable object. /// + /// The source entity type from the data layer. + /// The output/projected type exposed to consumers (e.g., a view model or DTO). + /// + /// This two-type-parameter variant allows projecting source entities into a different output type + /// via the abstract method. + /// [DataContract] public abstract record PaginatedListModel : IModel where TSource : class where TOut : class { + /// + /// Initializes a new instance of + /// by paginating the provided queryable source. + /// + /// The queryable data source to paginate. + /// The request containing paging and sorting parameters. protected PaginatedListModel(IQueryable source, PaginatedListRequest paginatedListRequest) { PaginateQueryable(source, paginatedListRequest); } + /// + /// Applies pagination and sorting parameters to the source queryable and populates the model properties. + /// + /// The queryable data source to paginate. Must not be null. + /// The request containing paging and sorting parameters. Must not be null. + /// Thrown when or is null. protected void PaginateQueryable(IQueryable source, PaginatedListRequest paginatedListRequest) { if (source == null) @@ -34,41 +52,76 @@ protected void PaginateQueryable(IQueryable source, PaginatedListReques throw new ArgumentException("Request input cannot be null"); } + // Default sort field to "id" when none is specified. SortBy = paginatedListRequest.SortBy ?? "id"; SortDirection = paginatedListRequest.SortDirection; PageSize = paginatedListRequest.PageSize; PageNumber = paginatedListRequest.PageNumber; TotalCount = source.Count(); + + // Calculate total pages using ceiling division: add 1 if there is a remainder. TotalPages = TotalCount / PageSize + (TotalCount % PageSize > 0 ? 1 : 0); + // Skip to the requested page (one-based) and take only the page size number of items. var query = source.Skip(PageSize * (PageNumber - 1)).Take(PageSize); + // Project source items to the output type via the abstract CastItems method. Items = CastItems(query).ToList(); } + /// + /// When implemented in a derived class, projects source entities to the output type. + /// + /// The queryable containing the current page of source entities. + /// A queryable of projected output items. protected abstract IQueryable CastItems(IQueryable source); + /// + /// Gets or sets the list of items for the current page, projected to . + /// [DataMember] - public List Items { get; set; } + public List Items { get; set; } = new List(); + /// + /// Gets or sets the number of items per page. + /// [DataMember] public int PageSize { get; set; } + /// + /// Gets or sets the one-based page number. + /// [DataMember] public int PageNumber { get; set; } + /// + /// Gets or sets the total number of pages available. + /// [DataMember] public int TotalPages { get; set; } + /// + /// Gets or sets the total count of items across all pages. + /// [DataMember] public int TotalCount { get; set; } + /// + /// Gets or sets the name of the property used for sorting. + /// [DataMember] - public string SortBy { get; set; } + public string? SortBy { get; set; } + /// + /// Gets or sets the sort direction applied to the results. + /// + /// [DataMember] public SortDirectionEnum SortDirection { get; set; } + /// + /// Gets a value indicating whether there is a page before the current page. + /// [DataMember] public bool HasPreviousPage { @@ -78,6 +131,9 @@ public bool HasPreviousPage } } + /// + /// Gets a value indicating whether there is a page after the current page. + /// [DataMember] public bool HasNextPage { @@ -92,16 +148,34 @@ public bool HasNextPage /// Represents a Data Transfer Object (DTO) that is typically used to encapsulate a PaginatedList so that it can be /// delivered to the application layer. This should be an immutable object. /// + /// The entity type contained in the paginated list. + /// + /// This single-type-parameter variant returns items in their original source type + /// without projection. For source-to-output projection, use + /// instead. + /// [DataContract] public abstract record PaginatedListModel : IModel where TSource : class { + /// + /// Initializes a new instance of + /// by paginating the provided queryable source. + /// + /// The queryable data source to paginate. + /// The request containing paging and sorting parameters. protected PaginatedListModel(IQueryable source, PaginatedListRequest paginatedListRequest) { PaginateQueryable(source, paginatedListRequest); } + /// + /// Applies pagination and sorting parameters to the source queryable and populates the model properties. + /// + /// The queryable data source to paginate. Must not be null. + /// The request containing paging and sorting parameters. Must not be null. + /// Thrown when or is null. protected void PaginateQueryable(IQueryable source, PaginatedListRequest paginatedListRequest) { if (source == null) @@ -114,37 +188,66 @@ protected void PaginateQueryable(IQueryable source, PaginatedListReques throw new ArgumentException("Request input cannot be null"); } + // Default sort field to "id" when none is specified. SortBy = paginatedListRequest.SortBy ?? "id"; SortDirection = paginatedListRequest.SortDirection; PageSize = paginatedListRequest.PageSize; PageNumber = paginatedListRequest.PageNumber; TotalCount = source.Count(); + + // Calculate total pages using ceiling division: add 1 if there is a remainder. TotalPages = TotalCount / PageSize + (TotalCount % PageSize > 0 ? 1 : 0); + // Skip to the requested page (one-based) and take only the page size number of items. Items = source.Skip(PageSize * (PageNumber - 1)).Take(PageSize).ToList(); } + /// + /// Gets or sets the list of items for the current page. + /// [DataMember] - public List Items { get; set; } + public List Items { get; set; } = new List(); + /// + /// Gets or sets the number of items per page. + /// [DataMember] public int PageSize { get; set; } + /// + /// Gets or sets the one-based page number. + /// [DataMember] public int PageNumber { get; set; } + /// + /// Gets or sets the total number of pages available. + /// [DataMember] public int TotalPages { get; set; } + /// + /// Gets or sets the total count of items across all pages. + /// [DataMember] public int TotalCount { get; set; } + /// + /// Gets or sets the name of the property used for sorting. + /// [DataMember] - public string SortBy { get; set; } + public string? SortBy { get; set; } + /// + /// Gets or sets the sort direction applied to the results. + /// + /// [DataMember] public SortDirectionEnum SortDirection { get; set; } + /// + /// Gets a value indicating whether there is a page before the current page. + /// [DataMember] public bool HasPreviousPage { @@ -154,6 +257,9 @@ public bool HasPreviousPage } } + /// + /// Gets a value indicating whether there is a page after the current page. + /// [DataMember] public bool HasNextPage { diff --git a/Src/RCommon.Models/PaginatedListRequest.cs b/Src/RCommon.Models/PaginatedListRequest.cs index 663c9f00..aa5c8663 100644 --- a/Src/RCommon.Models/PaginatedListRequest.cs +++ b/Src/RCommon.Models/PaginatedListRequest.cs @@ -3,9 +3,22 @@ namespace RCommon.Models { + /// + /// Abstract base record providing default pagination and sorting parameters. + /// Derive from this class to create concrete paginated list request types. + /// + /// + /// Defaults: page 1, 20 items per page, sorted by "id" with no sort direction. + /// + /// + /// [DataContract] public abstract record PaginatedListRequest : IPaginatedListRequest { + /// + /// Initializes a new instance of with default values: + /// page 1, page size 20, sort by "id", and no sort direction. + /// public PaginatedListRequest() { PageNumber = 1; @@ -14,15 +27,19 @@ public PaginatedListRequest() SortDirection = SortDirectionEnum.None; } + /// [DataMember] public virtual int PageNumber { get; set; } + /// [DataMember] public virtual int PageSize { get; set; } + /// [DataMember] - public virtual string SortBy { get; set; } + public virtual string? SortBy { get; set; } + /// [DataMember] public virtual SortDirectionEnum SortDirection { get; set; } } diff --git a/Src/RCommon.Models/Queries/IQuery.cs b/Src/RCommon.Models/Queries/IQuery.cs index 75e34d01..aa555a7f 100644 --- a/Src/RCommon.Models/Queries/IQuery.cs +++ b/Src/RCommon.Models/Queries/IQuery.cs @@ -6,10 +6,20 @@ namespace RCommon.Models.Queries { + /// + /// Marker interface representing a query in the CQRS pattern. + /// Queries encapsulate the intent to read data without modifying system state. + /// public interface IQuery { } + /// + /// Generic query interface that specifies the expected result type. + /// Use this interface when a query handler should return a typed result. + /// + /// The type of result returned by the query handler. + /// public interface IQuery : IQuery { } diff --git a/Src/RCommon.Models/RCommon.Models.csproj b/Src/RCommon.Models/RCommon.Models.csproj index 200f3396..cd5100bc 100644 --- a/Src/RCommon.Models/RCommon.Models.csproj +++ b/Src/RCommon.Models/RCommon.Models.csproj @@ -2,6 +2,7 @@ net8.0;net9.0;net10.0 + enable True RCommon.Models https://rcommon.com diff --git a/Src/RCommon.Models/README.md b/Src/RCommon.Models/README.md index 681a2e58..0cd1e9da 100644 --- a/Src/RCommon.Models/README.md +++ b/Src/RCommon.Models/README.md @@ -1,3 +1,87 @@ # RCommon.Models -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 +Shared model interfaces and base types for the RCommon framework, providing CQRS command and query contracts, event marker interfaces for sync/async dispatch, pagination models, and execution result types for conveying operation outcomes. + +## Features + +- `ICommand` and `ICommand` marker interfaces for CQRS command segregation +- `IQuery` and `IQuery` marker interfaces for CQRS query segregation +- `ISerializableEvent`, `ISyncEvent`, and `IAsyncEvent` marker interfaces for controlling event dispatch behavior +- `CommandResult` record for wrapping command handler outcomes +- `ExecutionResult` with static factory methods for `Success()` and `Failed(errors)` using cached singletons +- `PaginatedListModel` and `PaginatedListModel` abstract records for building paginated DTOs with built-in page calculation +- `PaginatedListRequest` and `SearchPaginatedListRequest` for encapsulating paging, sorting, and search parameters +- `SortDirectionEnum` for specifying ascending, descending, or no-sort ordering +- All models are decorated with `DataContract`/`DataMember` attributes for serialization support + +## Installation + +```shell +dotnet add package RCommon.Models +``` + +## Usage + +```csharp +using RCommon.Models.Commands; +using RCommon.Models.Queries; +using RCommon.Models.Events; +using RCommon.Models.ExecutionResults; + +// Define a command +public class CreateOrderCommand : ICommand +{ + public string ProductName { get; set; } + public int Quantity { get; set; } +} + +// Define an event for async dispatch +public class OrderCreatedEvent : IAsyncEvent +{ + public Guid OrderId { get; set; } +} + +// Return execution results from handlers +IExecutionResult result = ExecutionResult.Success(); +IExecutionResult failure = ExecutionResult.Failed("Insufficient inventory", "Payment declined"); + +// Use paginated requests +var request = new SearchPaginatedListRequest +{ + PageNumber = 1, + PageSize = 25, + SortBy = "CreatedDate", + SortDirection = SortDirectionEnum.Descending, + SearchString = "widget" +}; +``` + +## Key Types + +| Type | Description | +|------|-------------| +| `IModel` | Base marker interface for all RCommon models | +| `ICommand` / `ICommand` | Marker interfaces for CQRS commands, optionally specifying a result type | +| `IQuery` / `IQuery` | Marker interfaces for CQRS queries, optionally specifying a result type | +| `ISerializableEvent` | Marker interface for events that can be serialized across process boundaries | +| `ISyncEvent` | Marker for events dispatched and handled synchronously within the same process | +| `IAsyncEvent` | Marker for events dispatched asynchronously via a message bus or queue | +| `IExecutionResult` | Represents the outcome of an operation with an `IsSuccess` flag | +| `ExecutionResult` | Abstract base with `Success()` and `Failed()` factory methods | +| `CommandResult` | Wraps an execution result returned from a command handler | +| `PaginatedListModel` | Abstract paginated DTO with page size, count, and navigation properties | +| `PaginatedListRequest` | Base request with page number, page size, sort field, and sort direction | +| `SearchPaginatedListRequest` | Extends `PaginatedListRequest` with a free-text `SearchString` property | + +## Documentation + +For full documentation, visit [rcommon.com](https://rcommon.com). + +## Related Packages + +- [RCommon.Core](https://www.nuget.org/packages/RCommon.Core) - Foundation package with event bus, builder pattern, guards, and extensions +- [RCommon.Entities](https://www.nuget.org/packages/RCommon.Entities) - Domain entity base classes with auditing and transactional event tracking + +## License + +Licensed under the [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0). diff --git a/Src/RCommon.Models/SearchPaginatedListRequest.cs b/Src/RCommon.Models/SearchPaginatedListRequest.cs index 3935b216..639e41d5 100644 --- a/Src/RCommon.Models/SearchPaginatedListRequest.cs +++ b/Src/RCommon.Models/SearchPaginatedListRequest.cs @@ -7,15 +7,27 @@ namespace RCommon.Models { + /// + /// A paginated list request that includes a free-text search filter. + /// Combines pagination/sorting from with + /// the search capability defined by . + /// + /// + /// [DataContract] public record SearchPaginatedListRequest : PaginatedListRequest, ISearchPaginatedListRequest { + /// + /// Initializes a new instance of + /// with default pagination values inherited from . + /// public SearchPaginatedListRequest() { } + /// [DataMember] - public string SearchString { get; set; } + public string? SearchString { get; set; } } } diff --git a/Src/RCommon.Models/SortDirectionEnum.cs b/Src/RCommon.Models/SortDirectionEnum.cs index 24a82cc2..713c4a88 100644 --- a/Src/RCommon.Models/SortDirectionEnum.cs +++ b/Src/RCommon.Models/SortDirectionEnum.cs @@ -7,12 +7,26 @@ namespace RCommon.Models { + /// + /// Specifies the direction in which a collection should be sorted. + /// [DataContract] public enum SortDirectionEnum : byte { + /// + /// Sort in ascending order (A-Z, 0-9). + /// Ascending = 1, + + /// + /// Sort in descending order (Z-A, 9-0). + /// Descending = 2, + + /// + /// No sorting is applied; the default order is preserved. + /// None = 3, - + } } diff --git a/Src/RCommon.Persistence.Caching.MemoryCache/IPersistenceBuilderExtensions.cs b/Src/RCommon.Persistence.Caching.MemoryCache/IPersistenceBuilderExtensions.cs index 5e5a7749..b5339ff5 100644 --- a/Src/RCommon.Persistence.Caching.MemoryCache/IPersistenceBuilderExtensions.cs +++ b/Src/RCommon.Persistence.Caching.MemoryCache/IPersistenceBuilderExtensions.cs @@ -12,8 +12,17 @@ namespace RCommon.Persistence.Caching.MemoryCache { + /// + /// Extension methods on for registering memory-based + /// persistence caching (both in-process and distributed memory). + /// public static class IPersistenceBuilderExtensions { + /// + /// Registers persistence caching backed by the in-process , + /// including all caching repository decorators and the strategy-based cache factory. + /// + /// The persistence builder. public static void AddInMemoryPersistenceCaching(this IPersistenceBuilder builder) { // Add Caching services @@ -22,20 +31,26 @@ public static void AddInMemoryPersistenceCaching(this IPersistenceBuilder builde builder.Services.TryAddTransient, CommonFactory>(); ConfigureCachingOptions(builder); - // Add Caching Factory + // Add Caching Factory — resolves the correct ICacheService based on the PersistenceCachingStrategy builder.Services.TryAddTransient>(serviceProvider => strategy => { switch (strategy) { case PersistenceCachingStrategy.Default: - return serviceProvider.GetService(); + return serviceProvider.GetRequiredService(); default: - return serviceProvider.GetService(); + return serviceProvider.GetRequiredService(); } }); - + } + /// + /// Registers persistence caching backed by the + /// (an in-memory distributed cache), including all caching repository decorators + /// and the strategy-based cache factory. + /// + /// The persistence builder. public static void AddDistributedMemoryPersistenceCaching(this IPersistenceBuilder builder) { // Add Caching services @@ -44,20 +59,26 @@ public static void AddDistributedMemoryPersistenceCaching(this IPersistenceBuild builder.Services.TryAddTransient, CommonFactory>(); ConfigureCachingOptions(builder); - // Add Caching Factory + // Add Caching Factory — resolves the correct ICacheService based on the PersistenceCachingStrategy builder.Services.TryAddTransient>(serviceProvider => strategy => { switch (strategy) { case PersistenceCachingStrategy.Default: - return serviceProvider.GetService(); + return serviceProvider.GetRequiredService(); default: - return serviceProvider.GetService(); + return serviceProvider.GetRequiredService(); } - }); + }); } - private static void ConfigureCachingOptions(IPersistenceBuilder builder, Action configure = null) + /// + /// Registers the open-generic caching repository decorators and configures + /// with default or custom settings. + /// + /// The persistence builder. + /// An optional delegate to customize . When null, defaults are applied. + private static void ConfigureCachingOptions(IPersistenceBuilder builder, Action? configure = null) { // Add Caching repositories builder.Services.TryAddTransient(typeof(ICachingGraphRepository<>), typeof(CachingGraphRepository<>)); diff --git a/Src/RCommon.Persistence.Caching.MemoryCache/README.md b/Src/RCommon.Persistence.Caching.MemoryCache/README.md index cfa43776..6b7b7e3b 100644 --- a/Src/RCommon.Persistence.Caching.MemoryCache/README.md +++ b/Src/RCommon.Persistence.Caching.MemoryCache/README.md @@ -1,3 +1,56 @@ # RCommon.Persistence.Caching.MemoryCache -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 +Wires memory-based caching into RCommon's persistence caching decorators, providing `AddInMemoryPersistenceCaching()` and `AddDistributedMemoryPersistenceCaching()` extension methods that register the appropriate `ICacheService` implementation and all caching repository decorators. + +## Features + +- `AddInMemoryPersistenceCaching()` -- registers `InMemoryCacheService` (backed by `IMemoryCache`) as the cache provider for persistence caching decorators +- `AddDistributedMemoryPersistenceCaching()` -- registers `DistributedMemoryCacheService` (backed by `IDistributedCache` in-memory store) as the cache provider +- Automatically registers all open-generic caching repository decorators (`CachingGraphRepository<>`, `CachingLinqRepository<>`, `CachingSqlMapperRepository<>`) +- Configures `CachingOptions` with caching enabled by default +- Strategy-based factory resolves the correct `ICacheService` via `PersistenceCachingStrategy` + +## Installation + +```shell +dotnet add package RCommon.Persistence.Caching.MemoryCache +``` + +## Usage + +```csharp +using RCommon; +using RCommon.Persistence.Caching.MemoryCache; + +services.AddRCommon(builder => +{ + builder.WithPersistence(persistence => + { + // Option 1: In-process IMemoryCache for persistence caching + persistence.AddInMemoryPersistenceCaching(); + + // Option 2: Distributed memory cache for persistence caching + persistence.AddDistributedMemoryPersistenceCaching(); + }); +}); +``` + +## Key Types + +| Type | Description | +|------|-------------| +| `IPersistenceBuilderExtensions` | Provides `AddInMemoryPersistenceCaching()` and `AddDistributedMemoryPersistenceCaching()` on `IPersistenceBuilder` | + +## Documentation + +For full documentation, visit [rcommon.com](https://rcommon.com). + +## Related Packages + +- [RCommon.Persistence.Caching](https://www.nuget.org/packages/RCommon.Persistence.Caching) - Core persistence caching decorators and abstractions +- [RCommon.MemoryCache](https://www.nuget.org/packages/RCommon.MemoryCache) - Underlying `InMemoryCacheService` and `DistributedMemoryCacheService` implementations +- [RCommon.Persistence.Caching.RedisCache](https://www.nuget.org/packages/RCommon.Persistence.Caching.RedisCache) - Redis-backed alternative for persistence caching + +## License + +Licensed under the [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0). diff --git a/Src/RCommon.Persistence.Caching.RedisCache/IPersistenceBuilderExtensions.cs b/Src/RCommon.Persistence.Caching.RedisCache/IPersistenceBuilderExtensions.cs index cefc079c..97375fdf 100644 --- a/Src/RCommon.Persistence.Caching.RedisCache/IPersistenceBuilderExtensions.cs +++ b/Src/RCommon.Persistence.Caching.RedisCache/IPersistenceBuilderExtensions.cs @@ -11,31 +11,46 @@ namespace RCommon.Persistence.Caching.RedisCache { + /// + /// Extension methods on for registering Redis-backed + /// persistence caching. + /// public static class IPersistenceBuilderExtensions { + /// + /// Registers persistence caching backed by the , + /// including all caching repository decorators and the strategy-based cache factory. + /// + /// The persistence builder. public static void AddRedisPersistenceCaching(this IPersistenceBuilder builder) { - // Add Caching repositories + // Add Caching services builder.Services.TryAddTransient(); builder.Services.TryAddTransient(); builder.Services.TryAddTransient, CommonFactory>(); ConfigureCachingOptions(builder); - // Add Caching services + // Add Caching Factory — resolves the correct ICacheService based on the PersistenceCachingStrategy builder.Services.TryAddTransient>(serviceProvider => strategy => { switch (strategy) { case PersistenceCachingStrategy.Default: - return serviceProvider.GetService(); + return serviceProvider.GetRequiredService(); default: - return serviceProvider.GetService(); + return serviceProvider.GetRequiredService(); } }); } - private static void ConfigureCachingOptions(IPersistenceBuilder builder, Action configure = null) + /// + /// Registers the open-generic caching repository decorators and configures + /// with default or custom settings. + /// + /// The persistence builder. + /// An optional delegate to customize . When null, defaults are applied. + private static void ConfigureCachingOptions(IPersistenceBuilder builder, Action? configure = null) { // Add Caching repositories builder.Services.TryAddTransient(typeof(ICachingGraphRepository<>), typeof(CachingGraphRepository<>)); diff --git a/Src/RCommon.Persistence.Caching.RedisCache/README.md b/Src/RCommon.Persistence.Caching.RedisCache/README.md index 0bf0ca7f..5cf28c22 100644 --- a/Src/RCommon.Persistence.Caching.RedisCache/README.md +++ b/Src/RCommon.Persistence.Caching.RedisCache/README.md @@ -1,3 +1,63 @@ # RCommon.Persistence.Caching.RedisCache -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 +Wires Redis-based caching into RCommon's persistence caching decorators, providing the `AddRedisPersistenceCaching()` extension method that registers `RedisCacheService` and all caching repository decorators. + +## Features + +- `AddRedisPersistenceCaching()` -- registers `RedisCacheService` (backed by StackExchange.Redis via `IDistributedCache`) as the cache provider for persistence caching decorators +- Automatically registers all open-generic caching repository decorators (`CachingGraphRepository<>`, `CachingLinqRepository<>`, `CachingSqlMapperRepository<>`) +- Configures `CachingOptions` with caching enabled by default +- Strategy-based factory resolves the correct `ICacheService` via `PersistenceCachingStrategy` + +## Installation + +```shell +dotnet add package RCommon.Persistence.Caching.RedisCache +``` + +## Usage + +```csharp +using RCommon; +using RCommon.RedisCache; +using RCommon.Persistence.Caching.RedisCache; + +services.AddRCommon(builder => +{ + // Configure Redis connection + builder.WithDistributedCaching(cache => + { + cache.Configure(options => + { + options.Configuration = "localhost:6379"; + options.InstanceName = "MyApp:"; + }); + }); + + builder.WithPersistence(persistence => + { + // Use Redis as the cache provider for persistence caching + persistence.AddRedisPersistenceCaching(); + }); +}); +``` + +## Key Types + +| Type | Description | +|------|-------------| +| `IPersistenceBuilderExtensions` | Provides `AddRedisPersistenceCaching()` on `IPersistenceBuilder` | + +## Documentation + +For full documentation, visit [rcommon.com](https://rcommon.com). + +## Related Packages + +- [RCommon.Persistence.Caching](https://www.nuget.org/packages/RCommon.Persistence.Caching) - Core persistence caching decorators and abstractions +- [RCommon.RedisCache](https://www.nuget.org/packages/RCommon.RedisCache) - Underlying `RedisCacheService` implementation +- [RCommon.Persistence.Caching.MemoryCache](https://www.nuget.org/packages/RCommon.Persistence.Caching.MemoryCache) - Memory-based alternative for persistence caching + +## License + +Licensed under the [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0). diff --git a/Src/RCommon.Persistence.Caching/Crud/CachingGraphRepository.cs b/Src/RCommon.Persistence.Caching/Crud/CachingGraphRepository.cs index 0badff41..942aad21 100644 --- a/Src/RCommon.Persistence.Caching/Crud/CachingGraphRepository.cs +++ b/Src/RCommon.Persistence.Caching/Crud/CachingGraphRepository.cs @@ -13,164 +13,212 @@ namespace RCommon.Persistence.Caching.Crud { + /// + /// Decorator around that adds cache-aware query overloads. + /// Non-cached operations are delegated directly to the underlying repository. + /// + /// The entity type managed by this repository. + /// + /// Cached overloads use to retrieve results + /// from cache or fall through to the inner repository and store the result. + /// The is resolved via + /// using . + /// public class CachingGraphRepository : ICachingGraphRepository where TEntity : class, IBusinessEntity { private readonly IGraphRepository _repository; private readonly ICacheService _cacheService; + /// + /// Initializes a new instance of the class. + /// + /// The inner graph repository to delegate operations to. + /// Factory used to resolve the for the default persistence caching strategy. public CachingGraphRepository(IGraphRepository repository, ICommonFactory cacheFactory) { _repository = repository; _cacheService = cacheFactory.Create(PersistenceCachingStrategy.Default); } + /// public bool Tracking { get => _repository.Tracking; set => _repository.Tracking = value; } + /// public Type ElementType => _repository.ElementType; + /// public Expression Expression => _repository.Expression; + /// public IQueryProvider Provider => _repository.Provider; + /// public string DataStoreName { get => _repository.DataStoreName; set => _repository.DataStoreName = value; } + /// public async Task AddAsync(TEntity entity, CancellationToken token = default) { await _repository.AddAsync(entity, token); } + /// public async Task AnyAsync(Expression> expression, CancellationToken token = default) { return await _repository.AnyAsync(expression, token); } + /// public async Task AnyAsync(ISpecification specification, CancellationToken token = default) { return await _repository.AnyAsync(specification, token); } + /// public async Task DeleteAsync(TEntity entity, CancellationToken token = default) { await _repository.DeleteAsync(entity, token); } + /// public async Task FindAsync(object primaryKey, CancellationToken token = default) { return await _repository.FindAsync(primaryKey, token); } + /// public IQueryable FindQuery(ISpecification specification) { return _repository.FindQuery(specification); } + /// public IQueryable FindQuery(Expression> expression) { return _repository.FindQuery(expression); } + /// public IQueryable FindQuery(Expression> expression, Expression> orderByExpression, bool orderByAscending, int pageNumber = 1, int pageSize = 0) { return _repository.FindQuery(expression, orderByExpression, orderByAscending, pageNumber, pageSize); } + /// public IQueryable FindQuery(Expression> expression, Expression> orderByExpression, bool orderByAscending) { return _repository.FindQuery(expression, orderByExpression, orderByAscending); } + /// public IQueryable FindQuery(IPagedSpecification specification) { return _repository.FindQuery(specification); } + /// public async Task FindSingleOrDefaultAsync(Expression> expression, CancellationToken token = default) { return await _repository.FindSingleOrDefaultAsync(expression, token); } + /// public async Task FindSingleOrDefaultAsync(ISpecification specification, CancellationToken token = default) { return await _repository.FindSingleOrDefaultAsync(specification, token); } + /// public async Task GetCountAsync(ISpecification selectSpec, CancellationToken token = default) { return await _repository.GetCountAsync(selectSpec, token); } + /// public async Task GetCountAsync(Expression> expression, CancellationToken token = default) { return await _repository.GetCountAsync(expression, token); } + /// public IEnumerator GetEnumerator() { return _repository.GetEnumerator(); } + /// public IEagerLoadableQueryable Include(Expression> path) { return _repository.Include(path); } + /// public IEagerLoadableQueryable ThenInclude(Expression> path) { return _repository.ThenInclude(path); } + /// public async Task UpdateAsync(TEntity entity, CancellationToken token = default) { await _repository.UpdateAsync(entity, token); } + /// IEnumerator IEnumerable.GetEnumerator() { return _repository.GetEnumerator(); } + /// public async Task> FindAsync(Expression> expression, Expression> orderByExpression, bool orderByAscending, int pageNumber = 1, int pageSize = 0, CancellationToken token = default) { return await _repository.FindAsync(expression, orderByExpression, orderByAscending, pageNumber, pageSize, token); } + /// public async Task> FindAsync(IPagedSpecification specification, CancellationToken token = default) { return await _repository.FindAsync(specification, token); } + /// public async Task> FindAsync(ISpecification specification, CancellationToken token = default) { return await _repository.FindAsync(specification, token); } + /// public async Task> FindAsync(Expression> expression, CancellationToken token = default) { return await _repository.FindAsync(expression, token); } + /// public async Task DeleteManyAsync(ISpecification specification, CancellationToken token = default) { return await _repository.DeleteManyAsync(specification, token); } + /// public async Task DeleteManyAsync(Expression> expression, CancellationToken token = default) { return await _repository.DeleteManyAsync(expression, token); } - // Cached items + // Cached items — these overloads check the cache first and fall through to the inner repository on a miss. + /// public async Task> FindAsync(object cacheKey, Expression> expression, Expression> orderByExpression, bool orderByAscending, int pageNumber = 1, int pageSize = 0, CancellationToken token = default) { - var data = await _cacheService.GetOrCreateAsync(cacheKey, + var data = await _cacheService.GetOrCreateAsync(cacheKey, async () => await _repository.FindAsync(expression, orderByExpression, orderByAscending, pageNumber, pageSize, token)); return await data; } + /// public async Task> FindAsync(object cacheKey, IPagedSpecification specification, CancellationToken token = default) { var data = await _cacheService.GetOrCreateAsync(cacheKey, @@ -178,6 +226,7 @@ public async Task> FindAsync(object cacheKey, IPagedSpec return await data; } + /// public async Task> FindAsync(object cacheKey, ISpecification specification, CancellationToken token = default) { var data = await _cacheService.GetOrCreateAsync(cacheKey, @@ -185,12 +234,14 @@ public async Task> FindAsync(object cacheKey, ISpecificatio return await data; } + /// public async Task> FindAsync(object cacheKey, Expression> expression, CancellationToken token = default) { var data = await _cacheService.GetOrCreateAsync(cacheKey, async () => await _repository.FindAsync(expression, token)); return await data; } + /// /// Adds a range of entities by delegating to the underlying repository. /// diff --git a/Src/RCommon.Persistence.Caching/Crud/CachingLinqRepository.cs b/Src/RCommon.Persistence.Caching/Crud/CachingLinqRepository.cs index bc67ea41..85298c32 100644 --- a/Src/RCommon.Persistence.Caching/Crud/CachingLinqRepository.cs +++ b/Src/RCommon.Persistence.Caching/Crud/CachingLinqRepository.cs @@ -13,154 +13,201 @@ namespace RCommon.Persistence.Caching.Crud { + /// + /// Decorator around (as an + /// implementation) that adds cache-aware query overloads. + /// Non-cached operations are delegated directly to the underlying repository. + /// + /// The entity type managed by this repository. + /// + /// Cached overloads use to retrieve results + /// from cache or fall through to the inner repository and store the result. + /// The is resolved via + /// using . + /// public class CachingLinqRepository : ICachingLinqRepository where TEntity : class, IBusinessEntity { private readonly IGraphRepository _repository; private readonly ICacheService _cacheService; + /// + /// Initializes a new instance of the class. + /// + /// The inner graph repository to delegate operations to. + /// Factory used to resolve the for the default persistence caching strategy. public CachingLinqRepository(IGraphRepository repository, ICommonFactory cacheFactory) { _repository = repository; _cacheService = cacheFactory.Create(PersistenceCachingStrategy.Default); } + /// public Type ElementType => _repository.ElementType; + /// public Expression Expression => _repository.Expression; + /// public IQueryProvider Provider => _repository.Provider; + /// public string DataStoreName { get => _repository.DataStoreName; set => _repository.DataStoreName = value; } + /// public async Task AddAsync(TEntity entity, CancellationToken token = default) { await _repository.AddAsync(entity, token); } + /// public async Task AnyAsync(Expression> expression, CancellationToken token = default) { return await _repository.AnyAsync(expression, token); } + /// public async Task AnyAsync(ISpecification specification, CancellationToken token = default) { return await _repository.AnyAsync(specification, token); } + /// public async Task DeleteAsync(TEntity entity, CancellationToken token = default) { await _repository.DeleteAsync(entity, token); } + /// public async Task FindAsync(object primaryKey, CancellationToken token = default) { return await _repository.FindAsync(primaryKey, token); } + /// public IQueryable FindQuery(ISpecification specification) { return _repository.FindQuery(specification); } + /// public IQueryable FindQuery(Expression> expression) { return _repository.FindQuery(expression); } + /// public IQueryable FindQuery(Expression> expression, Expression> orderByExpression, bool orderByAscending, int pageNumber = 1, int pageSize = 0) { return _repository.FindQuery(expression, orderByExpression, orderByAscending, pageNumber, pageSize); } + /// public IQueryable FindQuery(Expression> expression, Expression> orderByExpression, bool orderByAscending) { return _repository.FindQuery(expression, orderByExpression, orderByAscending); } + /// public IQueryable FindQuery(IPagedSpecification specification) { return _repository.FindQuery(specification); } + /// public async Task FindSingleOrDefaultAsync(Expression> expression, CancellationToken token = default) { return await _repository.FindSingleOrDefaultAsync(expression, token); } + /// public async Task FindSingleOrDefaultAsync(ISpecification specification, CancellationToken token = default) { return await _repository.FindSingleOrDefaultAsync(specification, token); } + /// public async Task GetCountAsync(ISpecification selectSpec, CancellationToken token = default) { return await _repository.GetCountAsync(selectSpec, token); } + /// public async Task GetCountAsync(Expression> expression, CancellationToken token = default) { return await _repository.GetCountAsync(expression, token); } + /// public IEnumerator GetEnumerator() { return _repository.GetEnumerator(); } + /// public IEagerLoadableQueryable Include(Expression> path) { return _repository.Include(path); } + /// public IEagerLoadableQueryable ThenInclude(Expression> path) { return _repository.ThenInclude(path); } + /// public async Task UpdateAsync(TEntity entity, CancellationToken token = default) { await _repository.UpdateAsync(entity, token); } + /// IEnumerator IEnumerable.GetEnumerator() { return _repository.GetEnumerator(); } + /// public async Task> FindAsync(Expression> expression, Expression> orderByExpression, bool orderByAscending, int pageNumber = 1, int pageSize = 0, CancellationToken token = default) { return await _repository.FindAsync(expression, orderByExpression, orderByAscending, pageNumber, pageSize, token); } + /// public async Task> FindAsync(IPagedSpecification specification, CancellationToken token = default) { return await _repository.FindAsync(specification, token); } + /// public async Task> FindAsync(ISpecification specification, CancellationToken token = default) { return await _repository.FindAsync(specification, token); } + /// public async Task> FindAsync(Expression> expression, CancellationToken token = default) { return await _repository.FindAsync(expression, token); } + /// public async Task DeleteManyAsync(ISpecification specification, CancellationToken token = default) { return await _repository.DeleteManyAsync(specification, token); } + /// public async Task DeleteManyAsync(Expression> expression, CancellationToken token = default) { return await _repository.DeleteManyAsync(expression, token); } - // Cached items + // Cached items — these overloads check the cache first and fall through to the inner repository on a miss. + /// public async Task> FindAsync(object cacheKey, Expression> expression, Expression> orderByExpression, bool orderByAscending, int pageNumber = 1, int pageSize = 0, CancellationToken token = default) { @@ -169,6 +216,7 @@ public async Task> FindAsync(object cacheKey, Expression return await data; } + /// public async Task> FindAsync(object cacheKey, IPagedSpecification specification, CancellationToken token = default) { var data = await _cacheService.GetOrCreateAsync(cacheKey, @@ -176,6 +224,7 @@ public async Task> FindAsync(object cacheKey, IPagedSpec return await data; } + /// public async Task> FindAsync(object cacheKey, ISpecification specification, CancellationToken token = default) { var data = await _cacheService.GetOrCreateAsync(cacheKey, @@ -183,12 +232,14 @@ public async Task> FindAsync(object cacheKey, ISpecificatio return await data; } + /// public async Task> FindAsync(object cacheKey, Expression> expression, CancellationToken token = default) { var data = await _cacheService.GetOrCreateAsync(cacheKey, async () => await _repository.FindAsync(expression, token)); return await data; } + /// /// Adds a range of entities by delegating to the underlying repository. /// diff --git a/Src/RCommon.Persistence.Caching/Crud/CachingSqlMapperRepository.cs b/Src/RCommon.Persistence.Caching/Crud/CachingSqlMapperRepository.cs index fb188a9d..7a9c5ff2 100644 --- a/Src/RCommon.Persistence.Caching/Crud/CachingSqlMapperRepository.cs +++ b/Src/RCommon.Persistence.Caching/Crud/CachingSqlMapperRepository.cs @@ -11,93 +11,127 @@ namespace RCommon.Persistence.Caching.Crud { + /// + /// Decorator around that adds cache-aware query overloads. + /// Non-cached operations are delegated directly to the underlying repository. + /// + /// The entity type managed by this repository. + /// + /// Cached overloads use to retrieve results + /// from cache or fall through to the inner repository and store the result. + /// The is resolved via + /// using . + /// public class CachingSqlMapperRepository : ICachingSqlMapperRepository where TEntity : class, IBusinessEntity { private readonly ISqlMapperRepository _repository; private readonly ICacheService _cacheService; + /// + /// Initializes a new instance of the class. + /// + /// The inner SQL mapper repository to delegate operations to. + /// Factory used to resolve the for the default persistence caching strategy. public CachingSqlMapperRepository(ISqlMapperRepository repository, ICommonFactory cacheFactory) { _repository = repository; _cacheService = cacheFactory.Create(PersistenceCachingStrategy.Default); } + /// public string TableName { get => _repository.TableName; set => _repository.TableName = value; } + + /// public string DataStoreName { get => _repository.DataStoreName; set => _repository.DataStoreName = value; } + /// public async Task AddAsync(TEntity entity, CancellationToken token = default) { await _repository.AddAsync(entity, token); } + /// public async Task AnyAsync(System.Linq.Expressions.Expression> expression, CancellationToken token = default) { return await _repository.AnyAsync(expression, token); } + /// public async Task AnyAsync(ISpecification specification, CancellationToken token = default) { return await _repository.AnyAsync(specification, token); } + /// public async Task DeleteAsync(TEntity entity, CancellationToken token = default) { await _repository.DeleteAsync(entity, token); } + /// public async Task> FindAsync(ISpecification specification, CancellationToken token = default) { return await _repository.FindAsync(specification, token); } + /// public async Task> FindAsync(System.Linq.Expressions.Expression> expression, CancellationToken token = default) { return await _repository.FindAsync(expression, token); } + /// public async Task FindAsync(object primaryKey, CancellationToken token = default) { return await _repository.FindAsync(primaryKey, token); } + /// public async Task FindSingleOrDefaultAsync(System.Linq.Expressions.Expression> expression, CancellationToken token = default) { return await _repository.FindSingleOrDefaultAsync(expression, token); } + /// public async Task FindSingleOrDefaultAsync(ISpecification specification, CancellationToken token = default) { return await _repository.FindSingleOrDefaultAsync(specification, token); } + /// public async Task GetCountAsync(ISpecification selectSpec, CancellationToken token = default) { return await _repository.GetCountAsync(selectSpec, token); } + /// public async Task GetCountAsync(System.Linq.Expressions.Expression> expression, CancellationToken token = default) { return await _repository.GetCountAsync(expression, token); } + /// public async Task UpdateAsync(TEntity entity, CancellationToken token = default) { await _repository.UpdateAsync(entity, token); } + /// public async Task DeleteManyAsync(ISpecification specification, CancellationToken token = default) { return await _repository.DeleteManyAsync(specification, token); } + /// public async Task DeleteManyAsync(Expression> expression, CancellationToken token = default) { return await _repository.DeleteManyAsync(expression, token); } - // Cached Items + // Cached Items — these overloads check the cache first and fall through to the inner repository on a miss. + /// public async Task> FindAsync(object cacheKey, ISpecification specification, CancellationToken token = default) { var data = await _cacheService.GetOrCreateAsync(cacheKey, @@ -105,12 +139,14 @@ public async Task> FindAsync(object cacheKey, ISpecificatio return await data; } + /// public async Task> FindAsync(object cacheKey, System.Linq.Expressions.Expression> expression, CancellationToken token = default) { var data = await _cacheService.GetOrCreateAsync(cacheKey, async () => await _repository.FindAsync(expression, token)); return await data; } + /// /// Adds a range of entities by delegating to the underlying repository. /// diff --git a/Src/RCommon.Persistence.Caching/Crud/ICachingGraphRepository.cs b/Src/RCommon.Persistence.Caching/Crud/ICachingGraphRepository.cs index d6d642ba..948a20bd 100644 --- a/Src/RCommon.Persistence.Caching/Crud/ICachingGraphRepository.cs +++ b/Src/RCommon.Persistence.Caching/Crud/ICachingGraphRepository.cs @@ -11,12 +11,55 @@ namespace RCommon.Persistence.Caching.Crud { + /// + /// Extends with cache-aware query overloads that accept a cache key. + /// + /// The entity type managed by this repository. + /// + /// Each overload mirrors a standard FindAsync method but adds an object cacheKey + /// parameter so results can be stored in and retrieved from a cache layer before hitting the data store. + /// public interface ICachingGraphRepository : IGraphRepository where TEntity : class, IBusinessEntity { + /// + /// Finds a paginated list of entities matching the expression, caching the result under the specified key. + /// + /// The key used to store/retrieve the cached result. + /// A filter expression for the query. + /// An expression selecting the property to order by. + /// true to sort ascending; false for descending. + /// The 1-based page number. + /// The number of items per page (0 for all). + /// A cancellation token. + /// A paginated list of matching entities. Task> FindAsync(object cacheKey, Expression> expression, Expression> orderByExpression, bool orderByAscending, int pageNumber = 1, int pageSize = 0, CancellationToken token = default); + + /// + /// Finds a paginated list of entities matching the paged specification, caching the result under the specified key. + /// + /// The key used to store/retrieve the cached result. + /// The paged specification defining filter, ordering, and paging. + /// A cancellation token. + /// A paginated list of matching entities. Task> FindAsync(object cacheKey, IPagedSpecification specification, CancellationToken token = default); + + /// + /// Finds a collection of entities matching the specification, caching the result under the specified key. + /// + /// The key used to store/retrieve the cached result. + /// The specification defining the query filter. + /// A cancellation token. + /// A collection of matching entities. Task> FindAsync(object cacheKey, ISpecification specification, CancellationToken token = default); + + /// + /// Finds a collection of entities matching the expression, caching the result under the specified key. + /// + /// The key used to store/retrieve the cached result. + /// A filter expression for the query. + /// A cancellation token. + /// A collection of matching entities. Task> FindAsync(object cacheKey, Expression> expression, CancellationToken token = default); } } diff --git a/Src/RCommon.Persistence.Caching/Crud/ICachingLinqRepository.cs b/Src/RCommon.Persistence.Caching/Crud/ICachingLinqRepository.cs index 000f09a2..f1d4c9a2 100644 --- a/Src/RCommon.Persistence.Caching/Crud/ICachingLinqRepository.cs +++ b/Src/RCommon.Persistence.Caching/Crud/ICachingLinqRepository.cs @@ -11,12 +11,55 @@ namespace RCommon.Persistence.Caching.Crud { + /// + /// Extends with cache-aware query overloads that accept a cache key. + /// + /// The entity type managed by this repository. + /// + /// Each overload mirrors a standard FindAsync method but adds an object cacheKey + /// parameter so results can be stored in and retrieved from a cache layer before hitting the data store. + /// public interface ICachingLinqRepository : ILinqRepository where TEntity : class, IBusinessEntity { + /// + /// Finds a paginated list of entities matching the expression, caching the result under the specified key. + /// + /// The key used to store/retrieve the cached result. + /// A filter expression for the query. + /// An expression selecting the property to order by. + /// true to sort ascending; false for descending. + /// The 1-based page number. + /// The number of items per page (0 for all). + /// A cancellation token. + /// A paginated list of matching entities. Task> FindAsync(object cacheKey, Expression> expression, Expression> orderByExpression, bool orderByAscending, int pageNumber = 1, int pageSize = 0, CancellationToken token = default); + + /// + /// Finds a paginated list of entities matching the paged specification, caching the result under the specified key. + /// + /// The key used to store/retrieve the cached result. + /// The paged specification defining filter, ordering, and paging. + /// A cancellation token. + /// A paginated list of matching entities. Task> FindAsync(object cacheKey, IPagedSpecification specification, CancellationToken token = default); + + /// + /// Finds a collection of entities matching the specification, caching the result under the specified key. + /// + /// The key used to store/retrieve the cached result. + /// The specification defining the query filter. + /// A cancellation token. + /// A collection of matching entities. Task> FindAsync(object cacheKey, ISpecification specification, CancellationToken token = default); + + /// + /// Finds a collection of entities matching the expression, caching the result under the specified key. + /// + /// The key used to store/retrieve the cached result. + /// A filter expression for the query. + /// A cancellation token. + /// A collection of matching entities. Task> FindAsync(object cacheKey, Expression> expression, CancellationToken token = default); } } diff --git a/Src/RCommon.Persistence.Caching/Crud/ICachingSqlMapperRepository.cs b/Src/RCommon.Persistence.Caching/Crud/ICachingSqlMapperRepository.cs index 8596b525..b4b02eec 100644 --- a/Src/RCommon.Persistence.Caching/Crud/ICachingSqlMapperRepository.cs +++ b/Src/RCommon.Persistence.Caching/Crud/ICachingSqlMapperRepository.cs @@ -10,10 +10,33 @@ namespace RCommon.Persistence.Caching.Crud { + /// + /// Extends with cache-aware query overloads that accept a cache key. + /// + /// The entity type managed by this repository. + /// + /// Each overload mirrors a standard FindAsync method but adds an object cacheKey + /// parameter so results can be stored in and retrieved from a cache layer before hitting the data store. + /// public interface ICachingSqlMapperRepository : ISqlMapperRepository where TEntity : class, IBusinessEntity { + /// + /// Finds a collection of entities matching the specification, caching the result under the specified key. + /// + /// The key used to store/retrieve the cached result. + /// The specification defining the query filter. + /// A cancellation token. + /// A collection of matching entities. Task> FindAsync(object cacheKey, ISpecification specification, CancellationToken token = default); + + /// + /// Finds a collection of entities matching the expression, caching the result under the specified key. + /// + /// The key used to store/retrieve the cached result. + /// A filter expression for the query. + /// A cancellation token. + /// A collection of matching entities. Task> FindAsync(object cacheKey, Expression> expression, CancellationToken token = default); } } diff --git a/Src/RCommon.Persistence.Caching/IPersistenceBuilderExtensions.cs b/Src/RCommon.Persistence.Caching/IPersistenceBuilderExtensions.cs index 06096e7d..f6d7b05a 100644 --- a/Src/RCommon.Persistence.Caching/IPersistenceBuilderExtensions.cs +++ b/Src/RCommon.Persistence.Caching/IPersistenceBuilderExtensions.cs @@ -10,8 +10,25 @@ namespace RCommon.Persistence.Caching { + /// + /// Extension methods on for registering persistence-level caching + /// with a custom factory. + /// public static class IPersistenceBuilderExtensions { + /// + /// Registers persistence caching infrastructure including the cache service factory, + /// all caching repository decorators, and default . + /// + /// The persistence builder. + /// + /// A factory that, given an , returns a delegate resolving + /// from a . + /// + /// + /// This overload lets callers supply their own cache factory delegate, which is useful when + /// the caching provider is not one of the built-in memory or Redis implementations. + /// public static void AddPersistenceCaching(this IPersistenceBuilder builder, Func> cacheFactory) { // Add caching services diff --git a/Src/RCommon.Persistence.Caching/PersistenceCachingStrategy.cs b/Src/RCommon.Persistence.Caching/PersistenceCachingStrategy.cs index 0897394d..97e6f23b 100644 --- a/Src/RCommon.Persistence.Caching/PersistenceCachingStrategy.cs +++ b/Src/RCommon.Persistence.Caching/PersistenceCachingStrategy.cs @@ -6,8 +6,18 @@ namespace RCommon.Persistence.Caching { + /// + /// Defines the strategy used when caching persistence (repository) query results. + /// + /// + /// Used as the key type for to resolve + /// the appropriate implementation for caching repositories at runtime. + /// public enum PersistenceCachingStrategy { + /// + /// The default persistence caching strategy. + /// Default } } diff --git a/Src/RCommon.Persistence.Caching/RCommon.Persistence.Caching.csproj b/Src/RCommon.Persistence.Caching/RCommon.Persistence.Caching.csproj index 77ae4197..388e2e6b 100644 --- a/Src/RCommon.Persistence.Caching/RCommon.Persistence.Caching.csproj +++ b/Src/RCommon.Persistence.Caching/RCommon.Persistence.Caching.csproj @@ -2,6 +2,7 @@ net8.0;net9.0;net10.0 + enable True RCommon.Persistence.Caching https://rcommon.com diff --git a/Src/RCommon.Persistence.Caching/README.md b/Src/RCommon.Persistence.Caching/README.md index 9f562963..c065ea3b 100644 --- a/Src/RCommon.Persistence.Caching/README.md +++ b/Src/RCommon.Persistence.Caching/README.md @@ -1,3 +1,83 @@ # RCommon.Persistence.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 \ No newline at end of file +Provides caching decorator repositories that wrap any RCommon persistence provider with a cache layer, enabling cache-aware query overloads on `IGraphRepository`, `ILinqRepository`, and `ISqlMapperRepository`. + +## Features + +- `CachingGraphRepository` -- decorator around `IGraphRepository` that adds cache-aware `FindAsync` overloads accepting a cache key +- `CachingLinqRepository` -- decorator around `ILinqRepository` (via `IGraphRepository`) with the same cached query pattern +- `CachingSqlMapperRepository` -- decorator around `ISqlMapperRepository` with cached `FindAsync` overloads +- Non-cached operations (Add, Update, Delete, etc.) are delegated directly to the inner repository +- `PersistenceCachingStrategy` enum for strategy-based resolution of the `ICacheService` used by repository decorators +- `AddPersistenceCaching()` extension on `IPersistenceBuilder` to register all caching repository decorators with a custom cache factory + +## Installation + +```shell +dotnet add package RCommon.Persistence.Caching +``` + +## Usage + +```csharp +using RCommon; +using RCommon.Persistence.Caching; +using RCommon.Persistence.Caching.Crud; + +// Register persistence caching with a custom cache factory +services.AddRCommon(builder => +{ + builder.WithPersistence(persistence => + { + persistence.AddPersistenceCaching(serviceProvider => strategy => + { + return serviceProvider.GetRequiredService(); + }); + }); +}); + +// Use the caching repository in your service +public class ProductService +{ + private readonly ICachingGraphRepository _repo; + + public ProductService(ICachingGraphRepository repo) + { + _repo = repo; + } + + public async Task> GetActiveProductsAsync() + { + // Cached query -- checks cache first, falls through to the database on a miss + return await _repo.FindAsync( + CacheKey.With("active-products"), + x => x.IsActive); + } +} +``` + +## Key Types + +| Type | Description | +|------|-------------| +| `CachingGraphRepository` | Decorator adding cached `FindAsync` overloads to `IGraphRepository` | +| `CachingLinqRepository` | Decorator adding cached `FindAsync` overloads to `ILinqRepository` | +| `CachingSqlMapperRepository` | Decorator adding cached `FindAsync` overloads to `ISqlMapperRepository` | +| `ICachingGraphRepository` | Interface extending `IGraphRepository` with cache-key-based query methods | +| `ICachingLinqRepository` | Interface extending `ILinqRepository` with cache-key-based query methods | +| `ICachingSqlMapperRepository` | Interface extending `ISqlMapperRepository` with cache-key-based query methods | +| `PersistenceCachingStrategy` | Strategy enum for resolving the `ICacheService` used by caching repositories | + +## Documentation + +For full documentation, visit [rcommon.com](https://rcommon.com). + +## Related Packages + +- [RCommon.Caching](https://www.nuget.org/packages/RCommon.Caching) - Core caching abstractions (`ICacheService`, `CacheKey`) +- [RCommon.Persistence.Caching.MemoryCache](https://www.nuget.org/packages/RCommon.Persistence.Caching.MemoryCache) - Wires in-memory caching into persistence caching decorators +- [RCommon.Persistence.Caching.RedisCache](https://www.nuget.org/packages/RCommon.Persistence.Caching.RedisCache) - Wires Redis caching into persistence caching decorators + +## License + +Licensed under the [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0). diff --git a/Src/RCommon.Persistence/Crud/GraphRepositoryBase.cs b/Src/RCommon.Persistence/Crud/GraphRepositoryBase.cs index 71616d0d..252d1564 100644 --- a/Src/RCommon.Persistence/Crud/GraphRepositoryBase.cs +++ b/Src/RCommon.Persistence/Crud/GraphRepositoryBase.cs @@ -15,20 +15,32 @@ namespace RCommon.Persistence.Crud { /// - /// A base class for implementors of . + /// A base class for implementors of that provides + /// graph-based (change-tracked) repository functionality on top of . /// - /// + ///The entity type, which must be a class implementing . + /// + /// Concrete implementations (e.g., EF Core repositories) should inherit from this class and provide + /// the actual data access logic including change tracking behavior via the property. + /// public abstract class GraphRepositoryBase : LinqRepositoryBase, IGraphRepository where TEntity : class, IBusinessEntity { + /// + /// Initializes a new instance of the class. + /// + /// The factory used to resolve named data stores. + /// The entity event tracker for publishing domain events. + /// Options specifying the default data store name. public GraphRepositoryBase(IDataStoreFactory dataStoreFactory, IEntityEventTracker eventTracker, IOptions defaultDataStoreOptions) :base(dataStoreFactory, eventTracker, defaultDataStoreOptions) { - + } + /// public abstract bool Tracking { get; set; } } } diff --git a/Src/RCommon.Persistence/Crud/IEagerLoadableQueryable.cs b/Src/RCommon.Persistence/Crud/IEagerLoadableQueryable.cs index c4305bd0..9391e23e 100644 --- a/Src/RCommon.Persistence/Crud/IEagerLoadableQueryable.cs +++ b/Src/RCommon.Persistence/Crud/IEagerLoadableQueryable.cs @@ -8,9 +8,24 @@ namespace RCommon.Persistence.Crud { + /// + /// Extends and with support + /// for chained eager loading of related navigation properties. + /// + /// The entity type, which must implement . + /// + /// This interface enables fluent Include(...).ThenInclude(...) chains similar to Entity Framework Core. + /// public interface IEagerLoadableQueryable : IQueryable, IReadOnlyRepository where TEntity : IBusinessEntity { + /// + /// Eagerly loads a nested navigation property following a prior Include call. + /// + /// The type of the previously included property. + /// The type of the nested property to include. + /// An expression specifying the nested navigation property to include. + /// An for further chaining. IEagerLoadableQueryable ThenInclude(Expression> path); } } diff --git a/Src/RCommon.Persistence/Crud/IGraphRepository.cs b/Src/RCommon.Persistence/Crud/IGraphRepository.cs index 2791f0e4..ebd9bde7 100644 --- a/Src/RCommon.Persistence/Crud/IGraphRepository.cs +++ b/Src/RCommon.Persistence/Crud/IGraphRepository.cs @@ -10,10 +10,23 @@ namespace RCommon.Persistence.Crud { + /// + /// A repository that supports graph-based (change-tracked) operations on entities of type , + /// extending with change tracking control. + /// + /// The entity type, which must be a class implementing . + /// + /// Typically used with ORM providers like Entity Framework Core that support automatic change tracking. + /// public interface IGraphRepository : ILinqRepository where TEntity : class, IBusinessEntity { - + /// + /// Gets or sets whether entity change tracking is enabled for this repository. + /// + /// + /// When set to false, queries return detached entities for better read performance. + /// public bool Tracking { get; set; } } } diff --git a/Src/RCommon.Persistence/Crud/ILinqRepository.cs b/Src/RCommon.Persistence/Crud/ILinqRepository.cs index 27471a09..5c95be21 100644 --- a/Src/RCommon.Persistence/Crud/ILinqRepository.cs +++ b/Src/RCommon.Persistence/Crud/ILinqRepository.cs @@ -11,24 +11,89 @@ namespace RCommon.Persistence.Crud { - public interface ILinqRepository: IQueryable, IReadOnlyRepository, IWriteOnlyRepository, + /// + /// A LINQ-enabled repository that combines read, write, queryable, and eager loading capabilities + /// for entities of type . + /// + /// The entity type, which must implement . + /// + /// This interface extends , allowing repositories to be used directly + /// in LINQ queries. For graph/change-tracked scenarios, see . + /// + public interface ILinqRepository: IQueryable, IReadOnlyRepository, IWriteOnlyRepository, IEagerLoadableQueryable where TEntity : IBusinessEntity { + /// + /// Returns a queryable filtered by the given specification. + /// + /// The specification to filter entities. + /// An representing the filtered query. IQueryable FindQuery(ISpecification specification); + + /// + /// Returns a queryable filtered by the given expression. + /// + /// A lambda expression to filter entities. + /// An representing the filtered query. IQueryable FindQuery(Expression> expression); + + /// + /// Returns a queryable filtered, ordered, and paged by the given parameters. + /// + /// A lambda expression to filter entities. + /// An expression specifying the ordering property. + /// true for ascending order; false for descending. + /// The 1-based page number (default is 1). + /// The page size; 0 means no paging (default is 0). + /// An representing the filtered, ordered, and paged query. IQueryable FindQuery(Expression> expression, Expression> orderByExpression, bool orderByAscending, int pageNumber = 1, int pageSize = 0); + + /// + /// Returns a queryable filtered and paged by the given paged specification. + /// + /// The paged specification containing filter, ordering, and paging criteria. + /// An representing the filtered and paged query. IQueryable FindQuery(IPagedSpecification specification); + /// + /// Returns a queryable filtered and ordered by the given parameters (without paging). + /// + /// A lambda expression to filter entities. + /// An expression specifying the ordering property. + /// true for ascending order; false for descending. + /// An representing the filtered and ordered query. IQueryable FindQuery(Expression> expression, Expression> orderByExpression, bool orderByAscending); + /// + /// Finds entities matching the expression with ordering and paging, returning a paginated list. + /// + /// A lambda expression to filter entities. + /// An expression specifying the ordering property. + /// true for ascending order; false for descending. + /// The 1-based page number (default is 1). + /// The page size; 0 means no paging (default is 0). + /// A cancellation token to observe. + /// A paginated list of matching entities. Task> FindAsync(Expression> expression, Expression> orderByExpression, bool orderByAscending, int pageNumber = 1, int pageSize = 0, CancellationToken token = default); + + /// + /// Finds entities matching the paged specification, returning a paginated list. + /// + /// The paged specification containing filter, ordering, and paging criteria. + /// A cancellation token to observe. + /// A paginated list of matching entities. Task> FindAsync(IPagedSpecification specification, CancellationToken token = default); + /// + /// Eagerly loads a related navigation property specified by the expression. + /// + /// An expression specifying the navigation property to include. + /// An for further chained includes via ThenInclude. IEagerLoadableQueryable Include(Expression> path); } } diff --git a/Src/RCommon.Persistence/Crud/IReadOnlyRepository.cs b/Src/RCommon.Persistence/Crud/IReadOnlyRepository.cs index c3f97648..ce9ee9e9 100644 --- a/Src/RCommon.Persistence/Crud/IReadOnlyRepository.cs +++ b/Src/RCommon.Persistence/Crud/IReadOnlyRepository.cs @@ -10,24 +10,86 @@ namespace RCommon.Persistence.Crud { + /// + /// Defines read-only repository operations for querying entities of type . + /// + /// The entity type to query. + /// + /// This interface provides async query methods using both and + /// lambda expressions. See for write operations. + /// public interface IReadOnlyRepository : INamedDataSource { - + /// + /// Finds all entities matching the given specification. + /// + /// The specification to filter entities. + /// A cancellation token to observe. + /// A collection of entities satisfying the specification. Task> FindAsync(ISpecification specification, CancellationToken token = default); + + /// + /// Finds all entities matching the given expression. + /// + /// A lambda expression to filter entities. + /// A cancellation token to observe. + /// A collection of entities satisfying the expression. Task> FindAsync(Expression> expression, CancellationToken token = default); + /// + /// Finds a single entity by its primary key. + /// + /// The primary key value of the entity to find. + /// A cancellation token to observe. + /// The entity if found; otherwise, the default value for . Task FindAsync(object primaryKey, CancellationToken token = default); + /// + /// Gets the count of entities matching the given specification. + /// + /// The specification to filter entities. + /// A cancellation token to observe. + /// The number of matching entities. Task GetCountAsync(ISpecification selectSpec, CancellationToken token = default); + /// + /// Gets the count of entities matching the given expression. + /// + /// A lambda expression to filter entities. + /// A cancellation token to observe. + /// The number of matching entities. Task GetCountAsync(Expression> expression, CancellationToken token = default); + /// + /// Finds a single entity matching the expression, or returns the default value if none is found. + /// + /// A lambda expression to filter entities. + /// A cancellation token to observe. + /// The matching entity or the default value for . Task FindSingleOrDefaultAsync(Expression> expression, CancellationToken token = default); + /// + /// Finds a single entity matching the specification, or returns the default value if none is found. + /// + /// The specification to filter entities. + /// A cancellation token to observe. + /// The matching entity or the default value for . Task FindSingleOrDefaultAsync(ISpecification specification, CancellationToken token = default); + /// + /// Determines whether any entity matches the given expression. + /// + /// A lambda expression to filter entities. + /// A cancellation token to observe. + /// true if at least one entity matches; otherwise, false. Task AnyAsync(Expression> expression, CancellationToken token = default); + /// + /// Determines whether any entity matches the given specification. + /// + /// The specification to filter entities. + /// A cancellation token to observe. + /// true if at least one entity matches; otherwise, false. Task AnyAsync(ISpecification specification, CancellationToken token = default); } } diff --git a/Src/RCommon.Persistence/Crud/ISqlMapperRepository.cs b/Src/RCommon.Persistence/Crud/ISqlMapperRepository.cs index a94ac4ca..cdb145d5 100644 --- a/Src/RCommon.Persistence/Crud/ISqlMapperRepository.cs +++ b/Src/RCommon.Persistence/Crud/ISqlMapperRepository.cs @@ -11,10 +11,22 @@ namespace RCommon.Persistence.Crud { + /// + /// A repository that provides SQL-mapped (micro-ORM) CRUD operations for entities of type . + /// + /// The entity type, which must implement . + /// + /// Intended for use with lightweight data mappers such as Dapper, where entities are mapped + /// directly to database tables via the property. + /// Uses for database connectivity. + /// public interface ISqlMapperRepository : IReadOnlyRepository, IWriteOnlyRepository where TEntity : IBusinessEntity { + /// + /// Gets or sets the database table name that this repository maps to. + /// public string TableName { get; set; } - + } } diff --git a/Src/RCommon.Persistence/Crud/IWriteOnlyRepository.cs b/Src/RCommon.Persistence/Crud/IWriteOnlyRepository.cs index 732c4407..371587fd 100644 --- a/Src/RCommon.Persistence/Crud/IWriteOnlyRepository.cs +++ b/Src/RCommon.Persistence/Crud/IWriteOnlyRepository.cs @@ -9,6 +9,13 @@ namespace RCommon.Persistence.Crud { + /// + /// Defines write-only repository operations for persisting entities of type . + /// + /// The entity type to persist. + /// + /// This interface provides async CRUD write methods. See for read operations. + /// public interface IWriteOnlyRepository : INamedDataSource { diff --git a/Src/RCommon.Persistence/Crud/LinqRepositoryBase.cs b/Src/RCommon.Persistence/Crud/LinqRepositoryBase.cs index 76cb9bae..a4c80c5d 100644 --- a/Src/RCommon.Persistence/Crud/LinqRepositoryBase.cs +++ b/Src/RCommon.Persistence/Crud/LinqRepositoryBase.cs @@ -16,13 +16,33 @@ namespace RCommon.Persistence.Crud { + /// + /// Abstract base class for LINQ-enabled repositories that provides common queryable infrastructure, + /// event tracking, and data store resolution for entities of type . + /// + /// The entity type, which must implement . + /// + /// This class implements by delegating to the abstract + /// property, which concrete implementations must provide. It also handles default data store name assignment + /// from . + /// public abstract class LinqRepositoryBase : DisposableResource, ILinqRepository where TEntity : IBusinessEntity { - private string _dataStoreName; + private string _dataStoreName = default!; private readonly IDataStoreFactory _dataStoreFactory; - public LinqRepositoryBase(IDataStoreFactory dataStoreFactory, + /// + /// Initializes a new instance of the class. + /// + /// The factory used to resolve named data stores. + /// The entity event tracker for publishing domain events. + /// Options specifying the default data store name. + /// + /// Thrown when , , or + /// is null. + /// + public LinqRepositoryBase(IDataStoreFactory dataStoreFactory, IEntityEventTracker eventTracker, IOptions defaultDataStoreOptions) { if (defaultDataStoreOptions is null) @@ -32,6 +52,7 @@ public LinqRepositoryBase(IDataStoreFactory dataStoreFactory, _dataStoreFactory = dataStoreFactory ?? throw new ArgumentNullException(nameof(dataStoreFactory)); EventTracker = eventTracker ?? throw new ArgumentNullException(nameof(eventTracker)); + // Apply default data store name if configured, so repositories work without explicit name assignment if (defaultDataStoreOptions != null && defaultDataStoreOptions.Value != null && !defaultDataStoreOptions.Value.DefaultDataStoreName.IsNullOrEmpty()) { @@ -121,40 +142,93 @@ public IEnumerable Query(ISpecification specification) return RepositoryQuery.Where(specification.Predicate).AsQueryable(); } + /// public abstract IQueryable FindQuery(ISpecification specification); + + /// public abstract IQueryable FindQuery(Expression> expression); + + /// public abstract IQueryable FindQuery(Expression> expression, Expression> orderByExpression, bool orderByAscending); + + /// public abstract Task AddAsync(TEntity entity, CancellationToken token = default); + + /// public abstract Task AddRangeAsync(IEnumerable entities, CancellationToken token = default); + + /// public abstract Task DeleteAsync(TEntity entity, CancellationToken token = default); + + /// public abstract Task DeleteManyAsync(Expression> expression, CancellationToken token = default); + + /// public abstract Task DeleteManyAsync(ISpecification specification, CancellationToken token = default); + + /// public abstract Task UpdateAsync(TEntity entity, CancellationToken token = default); + + /// public abstract Task> FindAsync(ISpecification specification, CancellationToken token = default); + + /// public abstract Task> FindAsync(Expression> expression, CancellationToken token = default); + + /// public abstract Task FindAsync(object primaryKey, CancellationToken token = default); + + /// public abstract Task GetCountAsync(ISpecification selectSpec, CancellationToken token = default); + + /// public abstract Task GetCountAsync(Expression> expression, CancellationToken token = default); + + /// public abstract Task FindSingleOrDefaultAsync(Expression> expression, CancellationToken token = default); + + /// public abstract Task FindSingleOrDefaultAsync(ISpecification specification, CancellationToken token = default); + + /// public abstract Task AnyAsync(Expression> expression, CancellationToken token = default); + + /// public abstract Task AnyAsync(ISpecification specification, CancellationToken token = default); + /// public abstract Task> FindAsync(Expression> expression, Expression> orderByExpression, bool orderByAscending, int pageNumber = 1, int pageSize = 0, CancellationToken token = default); + + /// public abstract Task> FindAsync(IPagedSpecification specification, CancellationToken token = default); + /// public abstract IQueryable FindQuery(Expression> expression, Expression> orderByExpression, bool orderByAscending, int pageNumber = 1, int pageSize = 0); + + /// public abstract IQueryable FindQuery(IPagedSpecification specification); + /// public abstract IEagerLoadableQueryable Include(Expression> path); + /// public abstract IEagerLoadableQueryable ThenInclude(Expression> path); - public ILogger Logger { get; set; } + + /// + /// Gets or sets the logger instance for this repository. + /// + public ILogger Logger { get; set; } = default!; + + /// + /// Gets the entity event tracker used to track and publish domain events raised by entities. + /// public IEntityEventTracker EventTracker { get; } + + /// public string DataStoreName { get => _dataStoreName; diff --git a/Src/RCommon.Persistence/Crud/RepositoryException.cs b/Src/RCommon.Persistence/Crud/RepositoryException.cs index 2aca9523..e0fcedda 100644 --- a/Src/RCommon.Persistence/Crud/RepositoryException.cs +++ b/Src/RCommon.Persistence/Crud/RepositoryException.cs @@ -4,8 +4,20 @@ namespace RCommon.Persistence.Crud { + /// + /// Exception thrown when a repository operation fails. + /// + /// + /// This wraps provider-specific exceptions (e.g., database constraint violations) at the repository layer. + /// See also for general persistence-level errors. + /// public class RepositoryException : ApplicationException { + /// + /// Initializes a new instance of the class. + /// + /// A message describing the repository operation failure. + /// The inner exception that caused this error. public RepositoryException(string message, Exception innerException) : base(message, innerException) { } diff --git a/Src/RCommon.Persistence/Crud/SqlRepositoryBase.cs b/Src/RCommon.Persistence/Crud/SqlRepositoryBase.cs index 4e6dc1fe..fe79638d 100644 --- a/Src/RCommon.Persistence/Crud/SqlRepositoryBase.cs +++ b/Src/RCommon.Persistence/Crud/SqlRepositoryBase.cs @@ -17,14 +17,33 @@ namespace RCommon.Persistence.Crud { + /// + /// Abstract base class for SQL-mapped (micro-ORM) repositories that provides common infrastructure + /// for data store resolution, event tracking, and CRUD operations for entities of type . + /// + /// The entity type, which must be a class implementing . + /// + /// Concrete implementations (e.g., Dapper repositories) should inherit from this class and provide + /// the actual SQL-based data access logic. Uses for database connectivity. + /// public abstract class SqlRepositoryBase : DisposableResource, ISqlMapperRepository where TEntity : class, IBusinessEntity { - private string _dataStoreName; + private string _dataStoreName = default!; private readonly IDataStoreFactory _dataStoreFactory; - public SqlRepositoryBase(IDataStoreFactory dataStoreFactory, - ILoggerFactory logger, IEntityEventTracker eventTracker, + /// + /// Initializes a new instance of the class. + /// + /// The factory used to resolve named data stores. + /// The logger factory for creating loggers. + /// The entity event tracker for publishing domain events. + /// Options specifying the default data store name. + /// + /// Thrown when any of the required parameters is null. + /// + public SqlRepositoryBase(IDataStoreFactory dataStoreFactory, + ILoggerFactory logger, IEntityEventTracker eventTracker, IOptions defaultDataStoreOptions) { if (logger is null) @@ -40,32 +59,66 @@ public SqlRepositoryBase(IDataStoreFactory dataStoreFactory, _dataStoreFactory = dataStoreFactory ?? throw new ArgumentNullException(nameof(dataStoreFactory)); EventTracker = eventTracker ?? throw new ArgumentNullException(nameof(eventTracker)); - if (defaultDataStoreOptions != null && defaultDataStoreOptions.Value != null + // Apply default data store name if configured, so repositories work without explicit name assignment + if (defaultDataStoreOptions != null && defaultDataStoreOptions.Value != null && !defaultDataStoreOptions.Value.DefaultDataStoreName.IsNullOrEmpty()) { this.DataStoreName = defaultDataStoreOptions.Value.DefaultDataStoreName; } } + /// + public string TableName { get; set; } = default!; - public string TableName { get; set; } - + /// public abstract Task AddAsync(TEntity entity, CancellationToken token = default); + + /// public abstract Task AddRangeAsync(IEnumerable entities, CancellationToken token = default); + + /// public abstract Task DeleteAsync(TEntity entity, CancellationToken token = default); + + /// public abstract Task DeleteManyAsync(Expression> expression, CancellationToken token = default); + + /// public abstract Task DeleteManyAsync(ISpecification specification, CancellationToken token = default); + + /// public abstract Task UpdateAsync(TEntity entity, CancellationToken token = default); + + /// public abstract Task> FindAsync(ISpecification specification, CancellationToken token = default); + + /// public abstract Task> FindAsync(Expression> expression, CancellationToken token = default); + + /// public abstract Task FindAsync(object primaryKey, CancellationToken token = default); + + /// public abstract Task GetCountAsync(ISpecification selectSpec, CancellationToken token = default); + + /// public abstract Task GetCountAsync(Expression> expression, CancellationToken token = default); + + /// public abstract Task FindSingleOrDefaultAsync(Expression> expression, CancellationToken token = default); + + /// public abstract Task FindSingleOrDefaultAsync(ISpecification specification, CancellationToken token = default); + + /// public abstract Task AnyAsync(Expression> expression, CancellationToken token = default); + + /// public abstract Task AnyAsync(ISpecification specification, CancellationToken token = default); + /// + /// Gets the resolved data store for this repository, + /// using the current to look up the connection from the factory. + /// protected internal RDbConnection DataStore { get @@ -74,6 +127,7 @@ protected internal RDbConnection DataStore } } + /// public string DataStoreName { get => _dataStoreName; @@ -83,7 +137,14 @@ public string DataStoreName } } - public ILogger Logger { get; set; } + /// + /// Gets or sets the logger instance for this repository. + /// + public ILogger Logger { get; set; } = default!; + + /// + /// Gets the entity event tracker used to track and publish domain events raised by entities. + /// public IEntityEventTracker EventTracker { get; } } diff --git a/Src/RCommon.Persistence/DataStoreFactory.cs b/Src/RCommon.Persistence/DataStoreFactory.cs index 74f970a4..71f844d2 100644 --- a/Src/RCommon.Persistence/DataStoreFactory.cs +++ b/Src/RCommon.Persistence/DataStoreFactory.cs @@ -10,22 +10,33 @@ namespace RCommon.Persistence { + /// + /// Default implementation of that resolves named data stores + /// from the DI container based on registered entries. + /// public class DataStoreFactory : IDataStoreFactory { private readonly IServiceProvider _provider; private readonly ConcurrentBag _values; + /// + /// Initializes a new instance of the class. + /// + /// The service provider used to resolve data store instances. + /// The configured factory options containing registered entries. public DataStoreFactory(IServiceProvider provider, IOptions options) { _provider = provider; _values = options.Value.Values; } + /// public C Resolve(string name) where B : IDataStore where C : B, IDataStore { - DataStoreValue value = new DataStoreValue(name, typeof(B), typeof(C)); + // Attempt to peek at the first value in the bag as a quick existence check + DataStoreValue? value = new DataStoreValue(name, typeof(B), typeof(C)); if (_values.TryPeek(out value)) { return (C)_provider.GetRequiredService(value.ConcreteType); @@ -34,9 +45,11 @@ public C Resolve(string name) throw new DataStoreNotFoundException($"DataStore with name of {name} not found"); } + /// public B Resolve(string name) where B : IDataStore { + // Search the registered values by both name and base type to find the matching concrete type if (_values.Any(x => x.Name == name && x.BaseType == typeof(B))) { return (B)_provider.GetRequiredService(_values.First(x => x.Name == name && x.BaseType == typeof(B)).ConcreteType); diff --git a/Src/RCommon.Persistence/DataStoreFactoryOptions.cs b/Src/RCommon.Persistence/DataStoreFactoryOptions.cs index df0b174b..49701521 100644 --- a/Src/RCommon.Persistence/DataStoreFactoryOptions.cs +++ b/Src/RCommon.Persistence/DataStoreFactoryOptions.cs @@ -8,14 +8,31 @@ namespace RCommon.Persistence { + /// + /// Configuration options for that holds the collection of registered + /// entries used to resolve data stores by name and type. + /// public class DataStoreFactoryOptions { + /// + /// Gets the thread-safe collection of registered entries. + /// public ConcurrentBag Values { get; } = new ConcurrentBag(); + /// + /// Registers a data store mapping with the specified name, base type, and concrete type. + /// + /// The base data store type (e.g., a provider-specific DbContext base). + /// The concrete data store type that implements . + /// A unique name identifying the data store registration. + /// + /// Thrown when a data store with the same and base type is already registered. + /// public void Register(string name) where B : IDataStore where C : IDataStore { + // Prevent duplicate registrations with the same name and base type if (!Values.Any(x => x.Name == name && x.BaseType == typeof(B))) { Values.Add(new DataStoreValue(name, typeof(B), typeof(C))); diff --git a/Src/RCommon.Persistence/DataStoreNotFoundException.cs b/Src/RCommon.Persistence/DataStoreNotFoundException.cs index 2efc76af..2104a890 100644 --- a/Src/RCommon.Persistence/DataStoreNotFoundException.cs +++ b/Src/RCommon.Persistence/DataStoreNotFoundException.cs @@ -6,8 +6,15 @@ namespace RCommon.Persistence { + /// + /// Exception thrown when a named data store cannot be found in the registry. + /// public class DataStoreNotFoundException : GeneralException { + /// + /// Initializes a new instance of the class with a specified error message. + /// + /// A message describing which data store could not be found. public DataStoreNotFoundException(string message) : base(message) { diff --git a/Src/RCommon.Persistence/DataStoreValue.cs b/Src/RCommon.Persistence/DataStoreValue.cs index 360c9943..c0133945 100644 --- a/Src/RCommon.Persistence/DataStoreValue.cs +++ b/Src/RCommon.Persistence/DataStoreValue.cs @@ -6,22 +6,47 @@ namespace RCommon.Persistence { + /// + /// Represents a named data store registration that maps a base type to a concrete type, + /// used by to resolve data stores from the DI container. + /// public class DataStoreValue { + /// + /// Initializes a new instance of the class. + /// + /// The unique name identifying this data store registration. + /// The base type (e.g., a provider-specific DbContext base class). + /// The concrete type that directly inherits from . + /// + /// Thrown when does not directly inherit from . + /// public DataStoreValue(string name, Type baseType, Type concreteType) { Name = name; BaseType = baseType; ConcreteType = concreteType; + // Validate that the concrete type directly inherits from the base type if (concreteType.BaseType != baseType) { throw new UnsupportedDataStoreException($"Concrete type must implement base type."); } } + /// + /// Gets the unique name identifying this data store registration. + /// public string Name { get; } + + /// + /// Gets the base type used for resolving this data store. + /// public Type BaseType { get; } + + /// + /// Gets the concrete type that will be resolved from the DI container. + /// public Type ConcreteType { get; } } } diff --git a/Src/RCommon.Persistence/DefaultDataStoreOptions.cs b/Src/RCommon.Persistence/DefaultDataStoreOptions.cs index ce67f76f..7c149999 100644 --- a/Src/RCommon.Persistence/DefaultDataStoreOptions.cs +++ b/Src/RCommon.Persistence/DefaultDataStoreOptions.cs @@ -6,13 +6,23 @@ namespace RCommon { + /// + /// Options for configuring the default data store name that repositories will use + /// when no explicit is specified. + /// public class DefaultDataStoreOptions { + /// + /// Initializes a new instance of the class. + /// public DefaultDataStoreOptions() { } - public string DefaultDataStoreName { get; set; } + /// + /// Gets or sets the name of the default data store to be used by repositories. + /// + public string DefaultDataStoreName { get; set; } = default!; } } diff --git a/Src/RCommon.Persistence/IDataStore.cs b/Src/RCommon.Persistence/IDataStore.cs index 17d945be..125a2b14 100644 --- a/Src/RCommon.Persistence/IDataStore.cs +++ b/Src/RCommon.Persistence/IDataStore.cs @@ -7,8 +7,19 @@ namespace RCommon.Persistence { + /// + /// Represents an abstraction over a data store (e.g., a database context or connection) that supports async disposal. + /// + /// + /// Implementations of this interface wrap provider-specific data access contexts such as EF Core DbContext + /// or ADO.NET connections. See for a concrete ADO.NET implementation. + /// public interface IDataStore : IAsyncDisposable - { + { + /// + /// Gets the underlying associated with this data store. + /// + /// A instance that can be used for direct database operations. DbConnection GetDbConnection(); } diff --git a/Src/RCommon.Persistence/INamedDataSource.cs b/Src/RCommon.Persistence/INamedDataSource.cs index ad4b5c59..bdfc0df7 100644 --- a/Src/RCommon.Persistence/INamedDataSource.cs +++ b/Src/RCommon.Persistence/INamedDataSource.cs @@ -6,8 +6,18 @@ namespace RCommon.Persistence { + /// + /// Indicates that a component (such as a repository) is associated with a named data source, + /// allowing it to be resolved from a by name. + /// public interface INamedDataSource { + /// + /// Gets or sets the name of the data store this component is associated with. + /// + /// + /// This name is used by to resolve the correct instance. + /// public string DataStoreName { get; set; } } } diff --git a/Src/RCommon.Persistence/IPersistenceBuilder.cs b/Src/RCommon.Persistence/IPersistenceBuilder.cs index 740a897f..a88c1793 100644 --- a/Src/RCommon.Persistence/IPersistenceBuilder.cs +++ b/Src/RCommon.Persistence/IPersistenceBuilder.cs @@ -6,9 +6,22 @@ namespace RCommon /// /// Base interface implemented by specific data configurators that configure RCommon data providers. /// + /// + /// Concrete implementations (e.g., EF Core, Dapper, MongoDB builders) register provider-specific services + /// and data stores into the DI container via the collection. + /// public interface IPersistenceBuilder { + /// + /// Sets the default data store that repositories will use when no explicit data store name is specified. + /// + /// An action to configure the . + /// The current instance for fluent chaining. IPersistenceBuilder SetDefaultDataStore(Action options); + + /// + /// Gets the used to register persistence-related services. + /// IServiceCollection Services { get; } } } diff --git a/Src/RCommon.Persistence/IReadModel.cs b/Src/RCommon.Persistence/IReadModel.cs index 3356dede..590457a1 100644 --- a/Src/RCommon.Persistence/IReadModel.cs +++ b/Src/RCommon.Persistence/IReadModel.cs @@ -6,6 +6,13 @@ namespace RCommon.Persistence { + /// + /// Marker interface for read model entities used in CQRS-style query projections. + /// + /// + /// Implementing this interface signals that the entity is intended for read-only query scenarios + /// and should not be used for write (command) operations. + /// public interface IReadModel { } diff --git a/Src/RCommon.Persistence/IScopedDataStore.cs b/Src/RCommon.Persistence/IScopedDataStore.cs index da8b4fd9..c5724355 100644 --- a/Src/RCommon.Persistence/IScopedDataStore.cs +++ b/Src/RCommon.Persistence/IScopedDataStore.cs @@ -3,8 +3,15 @@ namespace RCommon.Persistence { + /// + /// Represents a scoped registry of data stores, allowing multiple named data store types to be tracked + /// within a single scope (e.g., a request or unit of work). + /// public interface IScopedDataStore { + /// + /// Gets or sets the thread-safe dictionary that maps data store names to their corresponding entries. + /// ConcurrentDictionary DataStores { get; set; } } } \ No newline at end of file diff --git a/Src/RCommon.Persistence/PersistenceBuilderExtensions.cs b/Src/RCommon.Persistence/PersistenceBuilderExtensions.cs index 6b179962..11161cdc 100644 --- a/Src/RCommon.Persistence/PersistenceBuilderExtensions.cs +++ b/Src/RCommon.Persistence/PersistenceBuilderExtensions.cs @@ -13,27 +13,51 @@ namespace RCommon { + /// + /// Extension methods for that register persistence providers and unit of work services. + /// public static class PersistenceBuilderExtensions { + /// + /// Adds a persistence provider with default configuration. + /// + /// The implementation to configure (e.g., EF Core, Dapper). + /// The RCommon builder instance. + /// The for fluent chaining. public static IRCommonBuilder WithPersistence(this IRCommonBuilder builder) where TObjectAccess : IPersistenceBuilder { return WithPersistence(builder, x => { }); } + /// + /// Adds a persistence provider and applies the specified configuration actions. + /// + /// The implementation to configure. + /// The RCommon builder instance. + /// An action to configure the persistence provider (e.g., register data stores). + /// The for fluent chaining. public static IRCommonBuilder WithPersistence(this IRCommonBuilder builder, Action objectAccessActions) where TObjectAccess : IPersistenceBuilder { - var dataConfiguration = (TObjectAccess)Activator.CreateInstance(typeof(TObjectAccess), new object[] { builder.Services }); + // Create the persistence builder via Activator since the concrete type is not known at compile time + var dataConfiguration = (TObjectAccess)Activator.CreateInstance(typeof(TObjectAccess), new object[] { builder.Services })!; objectAccessActions(dataConfiguration); builder = WithEventTracking(builder); return builder; } + /// + /// Adds a unit of work implementation and applies the specified configuration actions. + /// + /// The implementation to configure. + /// The RCommon builder instance. + /// An action to configure the unit of work (e.g., set isolation level, auto-complete). + /// The for fluent chaining. public static IRCommonBuilder WithUnitOfWork(this IRCommonBuilder builder, Action unitOfWorkActions) where TUnitOfWork : IUnitOfWorkBuilder { - var unitOfWorkConfiguration = (TUnitOfWork)Activator.CreateInstance(typeof(TUnitOfWork), new object[] { builder.Services }); + var unitOfWorkConfiguration = (TUnitOfWork)Activator.CreateInstance(typeof(TUnitOfWork), new object[] { builder.Services })!; unitOfWorkActions(unitOfWorkConfiguration); return builder; } @@ -120,9 +144,9 @@ public static IRCommonBuilder WithPersistence(this I where TObjectAccess : IPersistenceBuilder where TUnitOfWork : IUnitOfWorkBuilder { - var dataConfiguration = (TObjectAccess)Activator.CreateInstance(typeof(TObjectAccess), new object[] { builder.Services }); + var dataConfiguration = (TObjectAccess)Activator.CreateInstance(typeof(TObjectAccess), new object[] { builder.Services })!; objectAccessActions(dataConfiguration); - var unitOfWorkConfiguration = (TUnitOfWork)Activator.CreateInstance(typeof(TUnitOfWork), new object[] { builder.Services }); + var unitOfWorkConfiguration = (TUnitOfWork)Activator.CreateInstance(typeof(TUnitOfWork), new object[] { builder.Services })!; unitOfWorkActions(unitOfWorkConfiguration); builder = WithEventTracking(builder); return builder; diff --git a/Src/RCommon.Persistence/PersistenceException.cs b/Src/RCommon.Persistence/PersistenceException.cs index 9b2ae7b8..872f0a2b 100644 --- a/Src/RCommon.Persistence/PersistenceException.cs +++ b/Src/RCommon.Persistence/PersistenceException.cs @@ -6,11 +6,23 @@ namespace RCommon.Persistence { + /// + /// Exception thrown when a general persistence operation fails. + /// + /// + /// This wraps provider-specific exceptions with a persistence-layer abstraction. + /// See also for repository-specific errors. + /// public class PersistenceException : GeneralException { + /// + /// Initializes a new instance of the class. + /// + /// A message describing the persistence failure. + /// The inner exception that caused this persistence error. public PersistenceException(string message, Exception exception) : base(message, exception) { - + } } } diff --git a/Src/RCommon.Persistence/RCommon.Persistence.csproj b/Src/RCommon.Persistence/RCommon.Persistence.csproj index d0b8dc24..585f9c08 100644 --- a/Src/RCommon.Persistence/RCommon.Persistence.csproj +++ b/Src/RCommon.Persistence/RCommon.Persistence.csproj @@ -2,6 +2,7 @@ net8.0;net9.0;net10.0 + enable True RCommon.Persistence https://rcommon.com diff --git a/Src/RCommon.Persistence/README.md b/Src/RCommon.Persistence/README.md index 29dd4641..57c0de6d 100644 --- a/Src/RCommon.Persistence/README.md +++ b/Src/RCommon.Persistence/README.md @@ -1,3 +1,99 @@ - # RCommon.Persistence +# RCommon.Persistence -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 +Persistence abstraction layer for RCommon providing the repository pattern, unit of work, specification pattern, and named data store management. This package defines the core interfaces and base classes that ORM-specific implementations (EF Core, Dapper, Linq2Db) build upon. + +## Features + +- Repository pattern with separate read-only and write-only interfaces for CQRS-friendly designs +- LINQ-enabled repositories exposing `IQueryable` for composable queries +- Graph repositories with change tracking support for ORMs like Entity Framework Core +- SQL mapper repositories for micro-ORMs like Dapper +- Specification pattern support for encapsulating query logic +- Paginated query results via `IPaginatedList` with built-in ordering +- Eager loading abstraction with `Include` / `ThenInclude` chaining +- Unit of work pattern with configurable transaction modes and isolation levels +- Named data store factory for managing multiple database connections +- Domain event tracking integrated into repository operations +- Fluent builder API for DI registration via `AddRCommon()` + +## Installation + +```shell +dotnet add package RCommon.Persistence +``` + +## Usage + +This package is typically used indirectly through a provider-specific package. However, you program against these abstractions in your application and domain layers: + +```csharp +// Inject repository abstractions into your services +public class OrderService +{ + private readonly IGraphRepository _orderRepo; + private readonly IUnitOfWorkFactory _unitOfWorkFactory; + + public OrderService(IGraphRepository orderRepo, IUnitOfWorkFactory unitOfWorkFactory) + { + _orderRepo = orderRepo; + _unitOfWorkFactory = unitOfWorkFactory; + } + + public async Task GetOrderAsync(int id) + { + return await _orderRepo.FindAsync(id); + } + + public async Task> GetPendingOrdersAsync() + { + return await _orderRepo.FindAsync(o => o.Status == OrderStatus.Pending); + } + + public async Task> GetOrdersPagedAsync(int page, int pageSize) + { + return await _orderRepo.FindAsync( + o => o.IsActive, + o => o.CreatedDate, + orderByAscending: false, + pageNumber: page, + pageSize: pageSize); + } + + public async Task PlaceOrderAsync(Order order) + { + using var unitOfWork = _unitOfWorkFactory.Create(TransactionMode.Default); + await _orderRepo.AddAsync(order); + unitOfWork.Commit(); + } +} +``` + +## Key Types + +| Type | Description | +|------|-------------| +| `IReadOnlyRepository` | Async read operations: find by key, expression, specification, count, and any | +| `IWriteOnlyRepository` | Async write operations: add, add range, update, delete, and delete many | +| `ILinqRepository` | Combines read/write with `IQueryable` support, pagination, and eager loading | +| `IGraphRepository` | Extends `ILinqRepository` with change tracking control for full ORMs | +| `ISqlMapperRepository` | Read/write repository for micro-ORMs with explicit table name mapping | +| `IUnitOfWork` | Transaction scope that commits or rolls back on dispose | +| `IUnitOfWorkFactory` | Creates `IUnitOfWork` instances with configurable transaction mode and isolation level | +| `IDataStoreFactory` | Resolves named data store instances (DbContext, DbConnection, etc.) | +| `IPersistenceBuilder` | Fluent builder interface for registering persistence providers in DI | +| `LinqRepositoryBase` | Abstract base class for LINQ-enabled repository implementations | +| `SqlRepositoryBase` | Abstract base class for SQL mapper repository implementations | + +## Documentation + +For full documentation, visit [rcommon.com](https://rcommon.com). + +## Related Packages + +- [RCommon.EFCore](https://www.nuget.org/packages/RCommon.EFCore) - Entity Framework Core implementation +- [RCommon.Dapper](https://www.nuget.org/packages/RCommon.Dapper) - Dapper implementation +- [RCommon.Linq2Db](https://www.nuget.org/packages/RCommon.Linq2Db) - Linq2Db implementation + +## License + +Licensed under the [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0). diff --git a/Src/RCommon.Persistence/Sql/IRDbConnection.cs b/Src/RCommon.Persistence/Sql/IRDbConnection.cs index 4e2861b9..2bba0f34 100644 --- a/Src/RCommon.Persistence/Sql/IRDbConnection.cs +++ b/Src/RCommon.Persistence/Sql/IRDbConnection.cs @@ -4,8 +4,14 @@ namespace RCommon.Persistence.Sql { + /// + /// Represents an ADO.NET-based data store that extends to provide + /// raw database connection access for SQL-mapper repositories. + /// + /// + /// public interface IRDbConnection : IDataStore { - + } } diff --git a/Src/RCommon.Persistence/Sql/RDbConnection.cs b/Src/RCommon.Persistence/Sql/RDbConnection.cs index a1667765..673b0232 100644 --- a/Src/RCommon.Persistence/Sql/RDbConnection.cs +++ b/Src/RCommon.Persistence/Sql/RDbConnection.cs @@ -11,26 +11,46 @@ namespace RCommon.Persistence.Sql { + /// + /// Default implementation of that creates ADO.NET + /// instances using a configured and connection string. + /// + /// + /// This class uses the options pattern to obtain connection configuration from . + /// Each call to creates a new connection instance. + /// public class RDbConnection : DisposableResource, IRDbConnection { private readonly IOptions _options; - private readonly IEntityEventTracker _entityEventTracker; + /// + /// Initializes a new instance of the class. + /// + /// The connection options containing the and connection string. + /// Thrown when is null. public RDbConnection(IOptions options) { _options = options ?? throw new ArgumentNullException(nameof(options)); } + /// + /// Creates and returns a new using the configured provider factory and connection string. + /// + /// A new with its connection string set. + /// + /// Thrown when options, the provider factory, or the connection string is not properly configured. + /// public DbConnection GetDbConnection() { + // Validate all configuration requirements before creating the connection Guard.Against(this._options == null, "No options configured for this RDbConnection"); - Guard.Against(this._options.Value == null, "No options configured for this RDbConnection"); - Guard.Against(this._options.Value.DbFactory == null, "You must configured a DbProviderFactory for this RDbConnection"); + Guard.Against(this._options!.Value == null, "No options configured for this RDbConnection"); + Guard.Against(this._options.Value!.DbFactory == null, "You must configured a DbProviderFactory for this RDbConnection"); Guard.Against(this._options.Value.ConnectionString.IsNullOrEmpty(), "You must configure a conneciton string for this RDbConnection"); - var connection = this._options.Value.DbFactory.CreateConnection(); - connection.ConnectionString = this._options.Value.ConnectionString; - + var connection = this._options.Value.DbFactory!.CreateConnection(); + connection!.ConnectionString = this._options.Value.ConnectionString; + return connection; } diff --git a/Src/RCommon.Persistence/Sql/RDbConnectionException.cs b/Src/RCommon.Persistence/Sql/RDbConnectionException.cs index 47c46831..1e212326 100644 --- a/Src/RCommon.Persistence/Sql/RDbConnectionException.cs +++ b/Src/RCommon.Persistence/Sql/RDbConnectionException.cs @@ -6,10 +6,16 @@ namespace RCommon.Persistence.Sql { + /// + /// Exception thrown when an encounters a configuration or connectivity error. + /// public class RDbConnectionException : GeneralException { - - public RDbConnectionException(string message) + /// + /// Initializes a new instance of the class with a specified error message. + /// + /// A message describing the connection error. + public RDbConnectionException(string message) : base(message) { diff --git a/Src/RCommon.Persistence/Sql/RDbConnectionOptions.cs b/Src/RCommon.Persistence/Sql/RDbConnectionOptions.cs index 6b3ba7ab..7d2658a3 100644 --- a/Src/RCommon.Persistence/Sql/RDbConnectionOptions.cs +++ b/Src/RCommon.Persistence/Sql/RDbConnectionOptions.cs @@ -7,14 +7,27 @@ namespace RCommon.Persistence.Sql { + /// + /// Configuration options for , specifying the provider factory and connection string. + /// public class RDbConnectionOptions { + /// + /// Initializes a new instance of the class. + /// public RDbConnectionOptions() { } - public DbProviderFactory DbFactory { get; set; } - public string ConnectionString { get; set; } + /// + /// Gets or sets the used to create instances. + /// + public DbProviderFactory DbFactory { get; set; } = default!; + + /// + /// Gets or sets the database connection string. + /// + public string ConnectionString { get; set; } = default!; } } diff --git a/Src/RCommon.Persistence/Transactions/DefaultUnitOfWorkBuilder.cs b/Src/RCommon.Persistence/Transactions/DefaultUnitOfWorkBuilder.cs index 04ca917c..8952010c 100644 --- a/Src/RCommon.Persistence/Transactions/DefaultUnitOfWorkBuilder.cs +++ b/Src/RCommon.Persistence/Transactions/DefaultUnitOfWorkBuilder.cs @@ -27,12 +27,18 @@ namespace RCommon.Persistence.Transactions { /// - /// Implementation of . + /// Default implementation of that registers + /// and into the DI container. /// public class DefaultUnitOfWorkBuilder : IUnitOfWorkBuilder { private readonly IServiceCollection _services; + /// + /// Initializes a new instance of the class and registers + /// unit of work services as transient in the DI container. + /// + /// The service collection to register services into. public DefaultUnitOfWorkBuilder(IServiceCollection services) { @@ -42,6 +48,7 @@ public DefaultUnitOfWorkBuilder(IServiceCollection services) _services = services; } + /// public IUnitOfWorkBuilder SetOptions(Action unitOfWorkOptions) { _services.Configure(unitOfWorkOptions); diff --git a/Src/RCommon.Persistence/Transactions/IUnitOfWork.cs b/Src/RCommon.Persistence/Transactions/IUnitOfWork.cs index d78b940a..93eb218f 100644 --- a/Src/RCommon.Persistence/Transactions/IUnitOfWork.cs +++ b/Src/RCommon.Persistence/Transactions/IUnitOfWork.cs @@ -4,14 +4,47 @@ namespace RCommon.Persistence.Transactions { + /// + /// Defines a unit of work that manages a transaction scope around one or more persistence operations. + /// + /// + /// Disposing a unit of work without calling will result in a rollback. + /// Use to create instances with specific transaction settings. + /// public interface IUnitOfWork : IDisposable { + /// + /// Gets a value indicating whether the unit of work will automatically commit on disposal + /// if no explicit commit or rollback was attempted. + /// bool AutoComplete { get; } + + /// + /// Gets or sets the for the underlying transaction. + /// IsolationLevel IsolationLevel { get; set; } + + /// + /// Gets the current of this unit of work. + /// UnitOfWorkState State { get; } + + /// + /// Gets the unique identifier for this unit of work transaction. + /// Guid TransactionId { get; } + + /// + /// Gets or sets the that determines how this unit of work + /// participates in ambient transactions. + /// TransactionMode TransactionMode { get; set; } + /// + /// Commits the unit of work, completing the underlying transaction scope. + /// + /// Thrown if the unit of work has already been disposed. + /// Thrown if the unit of work has already been completed. void Commit(); } } diff --git a/Src/RCommon.Persistence/Transactions/IUnitOfWorkBuilder.cs b/Src/RCommon.Persistence/Transactions/IUnitOfWorkBuilder.cs index 8afe5c92..64052ff8 100644 --- a/Src/RCommon.Persistence/Transactions/IUnitOfWorkBuilder.cs +++ b/Src/RCommon.Persistence/Transactions/IUnitOfWorkBuilder.cs @@ -13,8 +13,18 @@ namespace RCommon.Persistence.Transactions { + /// + /// Builder interface for configuring unit of work services and default settings during application startup. + /// + /// + /// public interface IUnitOfWorkBuilder { + /// + /// Configures the default such as isolation level and auto-complete behavior. + /// + /// An action to configure the . + /// The current instance for fluent chaining. IUnitOfWorkBuilder SetOptions(Action unitOfWorkOptions); } } \ No newline at end of file diff --git a/Src/RCommon.Persistence/Transactions/IUnitOfWorkFactory.cs b/Src/RCommon.Persistence/Transactions/IUnitOfWorkFactory.cs index 20a6a296..36eec98a 100644 --- a/Src/RCommon.Persistence/Transactions/IUnitOfWorkFactory.cs +++ b/Src/RCommon.Persistence/Transactions/IUnitOfWorkFactory.cs @@ -3,10 +3,31 @@ namespace RCommon.Persistence.Transactions { + /// + /// Factory for creating instances with configurable transaction settings. + /// + /// public interface IUnitOfWorkFactory { + /// + /// Creates a new with default transaction settings. + /// + /// A new instance. IUnitOfWork Create(); + + /// + /// Creates a new with the specified transaction mode. + /// + /// The to use. + /// A new instance. IUnitOfWork Create(TransactionMode transactionMode); + + /// + /// Creates a new with the specified transaction mode and isolation level. + /// + /// The to use. + /// The for the transaction. + /// A new instance. IUnitOfWork Create(TransactionMode transactionMode, IsolationLevel isolationLevel); } } diff --git a/Src/RCommon.Persistence/Transactions/TransactionScopeHelper.cs b/Src/RCommon.Persistence/Transactions/TransactionScopeHelper.cs index 1943ef54..063738a4 100644 --- a/Src/RCommon.Persistence/Transactions/TransactionScopeHelper.cs +++ b/Src/RCommon.Persistence/Transactions/TransactionScopeHelper.cs @@ -31,6 +31,22 @@ namespace RCommon.Persistence.Transactions public static class TransactionScopeHelper { + /// + /// Creates a based on the + /// of the specified unit of work. + /// + /// The logger for diagnostic output about the created scope type. + /// The unit of work whose and + /// determine the scope configuration. + /// + /// A configured as follows: + /// + /// : with the specified isolation level. + /// : (no transaction). + /// : (joins existing or creates new). + /// + /// All scopes are created with for async support. + /// public static TransactionScope CreateScope(ILogger logger, IUnitOfWork unitOfWork) { if (unitOfWork.TransactionMode == TransactionMode.New) @@ -43,6 +59,7 @@ public static TransactionScope CreateScope(ILogger logger, IUnitOfWo logger.LogDebug("Creating a new TransactionScope with TransactionScopeOption.Supress"); return new TransactionScope(TransactionScopeOption.Suppress, TransactionScopeAsyncFlowOption.Enabled); } + // Default mode: join existing ambient transaction or create a new one logger.LogDebug("Creating a new TransactionScope with TransactionScopeOption.Required"); return new TransactionScope(TransactionScopeOption.Required, TransactionScopeAsyncFlowOption.Enabled); } diff --git a/Src/RCommon.Persistence/Transactions/UnitOfWork.cs b/Src/RCommon.Persistence/Transactions/UnitOfWork.cs index 5110f202..18733b68 100644 --- a/Src/RCommon.Persistence/Transactions/UnitOfWork.cs +++ b/Src/RCommon.Persistence/Transactions/UnitOfWork.cs @@ -10,6 +10,15 @@ namespace RCommon.Persistence.Transactions { + /// + /// Default implementation of that wraps a + /// to provide transactional consistency across persistence operations. + /// + /// + /// The unit of work transitions through the lifecycle: + /// Created -> CommitAttempted -> Completed -> Disposed, or Created -> RolledBack -> Disposed. + /// Disposing without committing will trigger either auto-complete (if enabled) or rollback. + /// public class UnitOfWork : DisposableResource, IUnitOfWork { private readonly ILogger _logger; @@ -17,6 +26,12 @@ public class UnitOfWork : DisposableResource, IUnitOfWork private UnitOfWorkState _state; private TransactionScope _transactionScope; + /// + /// Initializes a new instance of the class using configured settings. + /// + /// The logger for diagnostic output. + /// The GUID generator for creating the transaction identifier. + /// The configured settings for isolation level and auto-complete behavior. public UnitOfWork(ILogger logger, IGuidGenerator guidGenerator, IOptions unitOfWorkSettings) { _logger = logger; @@ -29,6 +44,14 @@ public UnitOfWork(ILogger logger, IGuidGenerator guidGenerator, IOpt _state = UnitOfWorkState.Created; _transactionScope = TransactionScopeHelper.CreateScope(_logger, this); } + + /// + /// Initializes a new instance of the class with explicit transaction settings. + /// + /// The logger for diagnostic output. + /// The GUID generator for creating the transaction identifier. + /// The transaction mode for this unit of work. + /// The isolation level for the underlying transaction. public UnitOfWork(ILogger logger, IGuidGenerator guidGenerator, TransactionMode transactionMode, IsolationLevel isolationLevel) { _logger = logger; @@ -42,6 +65,7 @@ public UnitOfWork(ILogger logger, IGuidGenerator guidGenerator, Tran _transactionScope = TransactionScopeHelper.CreateScope(_logger, this); } + /// public void Commit() { Guard.Against(_state == UnitOfWorkState.Disposed, @@ -53,17 +77,27 @@ public void Commit() this.Complete(); } + /// + /// Marks the unit of work as rolled back, preventing the transaction from being committed. + /// private void Rollback() { _state = UnitOfWorkState.RolledBack; } + /// + /// Completes the underlying , signaling that all operations succeeded. + /// private void Complete() { _transactionScope.Complete(); _state = UnitOfWorkState.Completed; } + /// + /// Disposes the unit of work, handling auto-complete or rollback based on current state. + /// + /// true if called from ; false if from a finalizer. protected override void Dispose(bool disposing) { if (_state == UnitOfWorkState.Disposed) @@ -104,11 +138,19 @@ protected override void Dispose(bool disposing) } } + /// public Guid TransactionId { get; } + + /// public TransactionMode TransactionMode { get; set; } + + /// public IsolationLevel IsolationLevel { get; set; } + + /// public bool AutoComplete { get; } + /// public UnitOfWorkState State => _state; } } diff --git a/Src/RCommon.Persistence/Transactions/UnitOfWorkException.cs b/Src/RCommon.Persistence/Transactions/UnitOfWorkException.cs index e16df102..fd99dc57 100644 --- a/Src/RCommon.Persistence/Transactions/UnitOfWorkException.cs +++ b/Src/RCommon.Persistence/Transactions/UnitOfWorkException.cs @@ -6,8 +6,16 @@ namespace RCommon.Persistence.Transactions { + /// + /// Exception thrown when a operation fails, + /// such as attempting to commit an already completed or rolled-back scope. + /// public class UnitOfWorkException : GeneralException { + /// + /// Initializes a new instance of the class with a specified error message. + /// + /// A message describing the unit of work failure. public UnitOfWorkException(string message) : base(message) { diff --git a/Src/RCommon.Persistence/Transactions/UnitOfWorkFactory.cs b/Src/RCommon.Persistence/Transactions/UnitOfWorkFactory.cs index 9361bcfa..174d7136 100644 --- a/Src/RCommon.Persistence/Transactions/UnitOfWorkFactory.cs +++ b/Src/RCommon.Persistence/Transactions/UnitOfWorkFactory.cs @@ -8,34 +8,46 @@ namespace RCommon.Persistence.Transactions { + /// + /// Default implementation of that creates + /// instances from the DI container with optional transaction mode and isolation level overrides. + /// public class UnitOfWorkFactory : IUnitOfWorkFactory { private readonly IServiceProvider _serviceProvider; - private readonly IEventBus _eventBus; private readonly IGuidGenerator _guidGenerator; + /// + /// Initializes a new instance of the class. + /// + /// The service provider used to resolve instances. + /// The GUID generator (passed through to resolved unit of work instances). + /// Thrown when is null. public UnitOfWorkFactory(IServiceProvider serviceProvider, IGuidGenerator guidGenerator) { _serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); _guidGenerator = guidGenerator; } + /// public IUnitOfWork Create() { var unitOfWork = _serviceProvider.GetService(); - return unitOfWork; + return unitOfWork!; } + /// public IUnitOfWork Create(TransactionMode transactionMode) { - var unitOfWork = _serviceProvider.GetService(); + var unitOfWork = _serviceProvider.GetRequiredService(); unitOfWork.TransactionMode = transactionMode; return unitOfWork; } + /// public IUnitOfWork Create(TransactionMode transactionMode, IsolationLevel isolationLevel) { - var unitOfWork = _serviceProvider.GetService(); + var unitOfWork = _serviceProvider.GetRequiredService(); unitOfWork.TransactionMode = transactionMode; return unitOfWork; } diff --git a/Src/RCommon.Persistence/Transactions/UnitOfWorkSettings.cs b/Src/RCommon.Persistence/Transactions/UnitOfWorkSettings.cs index 90d03865..e027c9c1 100644 --- a/Src/RCommon.Persistence/Transactions/UnitOfWorkSettings.cs +++ b/Src/RCommon.Persistence/Transactions/UnitOfWorkSettings.cs @@ -19,10 +19,18 @@ namespace RCommon.Persistence.Transactions { /// - /// Contains settings for RCommon unit of work. + /// Contains default settings for instances, including isolation level and auto-complete behavior. /// + /// + /// These settings are applied when a is created via DI with the default constructor. + /// Configure these settings using . + /// public class UnitOfWorkSettings { + /// + /// Initializes a new instance of the class + /// with and auto-complete disabled. + /// public UnitOfWorkSettings() { DefaultIsolation = IsolationLevel.ReadCommitted; diff --git a/Src/RCommon.Persistence/Transactions/UnitOfWorkState.cs b/Src/RCommon.Persistence/Transactions/UnitOfWorkState.cs index 74616706..56543198 100644 --- a/Src/RCommon.Persistence/Transactions/UnitOfWorkState.cs +++ b/Src/RCommon.Persistence/Transactions/UnitOfWorkState.cs @@ -6,12 +6,34 @@ namespace RCommon.Persistence.Transactions { + /// + /// Represents the lifecycle state of a . + /// public enum UnitOfWorkState { + /// + /// The unit of work has been created but no commit or rollback has been attempted. + /// Created = 1, + + /// + /// A commit has been attempted on the unit of work. + /// CommitAttempted = 2, + + /// + /// The unit of work has been rolled back. + /// RolledBack = 3, + + /// + /// The unit of work has been successfully completed (committed). + /// Completed = 4, + + /// + /// The unit of work has been disposed and can no longer be used. + /// Disposed = 5 } } diff --git a/Src/RCommon.Persistence/UnsupportedDataStoreException.cs b/Src/RCommon.Persistence/UnsupportedDataStoreException.cs index bb59deeb..ce086e75 100644 --- a/Src/RCommon.Persistence/UnsupportedDataStoreException.cs +++ b/Src/RCommon.Persistence/UnsupportedDataStoreException.cs @@ -5,9 +5,19 @@ namespace RCommon.Persistence { + /// + /// Exception thrown when a data store registration or resolution is not supported, + /// such as registering a duplicate data store name or an invalid type hierarchy. + /// + /// + /// public class UnsupportedDataStoreException : GeneralException { + /// + /// Initializes a new instance of the class with a specified error message. + /// + /// A message describing the unsupported data store operation. public UnsupportedDataStoreException(string message) :base(message) { diff --git a/Src/RCommon.RedisCache/IRedisCachingBuilder.cs b/Src/RCommon.RedisCache/IRedisCachingBuilder.cs index 963e861a..5ce19898 100644 --- a/Src/RCommon.RedisCache/IRedisCachingBuilder.cs +++ b/Src/RCommon.RedisCache/IRedisCachingBuilder.cs @@ -7,6 +7,13 @@ namespace RCommon.RedisCache { + /// + /// Marker interface for configuring Redis-backed distributed caching. + /// + /// + /// Extends to allow Redis-specific cache + /// configuration via extension methods in . + /// public interface IRedisCachingBuilder : IDistributedCachingBuilder { } diff --git a/Src/RCommon.RedisCache/IRedisCachingBuilderExtensions.cs b/Src/RCommon.RedisCache/IRedisCachingBuilderExtensions.cs index 32427920..8d1f975e 100644 --- a/Src/RCommon.RedisCache/IRedisCachingBuilderExtensions.cs +++ b/Src/RCommon.RedisCache/IRedisCachingBuilderExtensions.cs @@ -10,8 +10,18 @@ namespace RCommon.RedisCache { + /// + /// Extension methods for that configure + /// Redis cache options and expression caching. + /// public static class IRedisCachingBuilderExtensions { + /// + /// Configures the underlying for the StackExchange Redis cache. + /// + /// The Redis caching builder. + /// A delegate to configure . + /// The same for chaining. public static IRedisCachingBuilder Configure(this IRedisCachingBuilder builder, Action actions) { builder.Services.AddStackExchangeRedisCache(actions); @@ -19,7 +29,7 @@ public static IRedisCachingBuilder Configure(this IRedisCachingBuilder builder, } /// - /// This greatly improves performance across various areas of RCommon which use generics and reflection heavily + /// This greatly improves performance across various areas of RCommon which use generics and reflection heavily /// to compile expressions and lambdas /// /// Builder @@ -35,17 +45,19 @@ public static IRedisCachingBuilder CacheDynamicallyCompiledExpressions(this IRed x.CachingEnabled = true; x.CacheDynamicallyCompiledExpressions = true; }); + + // Register factory that resolves the correct ICacheService based on the ExpressionCachingStrategy builder.Services.TryAddTransient>(serviceProvider => strategy => { switch (strategy) { case ExpressionCachingStrategy.Default: - return serviceProvider.GetService(); + return serviceProvider.GetRequiredService(); default: - return serviceProvider.GetService(); + return serviceProvider.GetRequiredService(); } }); - + return builder; } } diff --git a/Src/RCommon.RedisCache/RCommon.RedisCache.csproj b/Src/RCommon.RedisCache/RCommon.RedisCache.csproj index 8582968f..e39b880e 100644 --- a/Src/RCommon.RedisCache/RCommon.RedisCache.csproj +++ b/Src/RCommon.RedisCache/RCommon.RedisCache.csproj @@ -2,6 +2,7 @@ net8.0;net9.0;net10.0 + enable True RCommon.RedisCache https://rcommon.com diff --git a/Src/RCommon.RedisCache/README.md b/Src/RCommon.RedisCache/README.md index 30eef560..624aeee0 100644 --- a/Src/RCommon.RedisCache/README.md +++ b/Src/RCommon.RedisCache/README.md @@ -1,3 +1,59 @@ - # RCommon.RedisCache +# RCommon.RedisCache -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 Redis-backed distributed cache implementation of `ICacheService` using `IDistributedCache` from StackExchange.Redis, with fluent builder extensions for DI configuration. + +## Features + +- `RedisCacheService` -- implements `ICacheService` using `IDistributedCache` backed by StackExchange.Redis +- Automatic JSON serialization/deserialization of cached values via `IJsonSerializer` +- `RedisCachingBuilder` for plugging into the `AddRCommon()` builder pipeline via `WithDistributedCaching` +- `Configure()` extension to customize `RedisCacheOptions` (connection string, instance name, etc.) +- `CacheDynamicallyCompiledExpressions()` extension to enable expression caching for improved runtime performance + +## Installation + +```shell +dotnet add package RCommon.RedisCache +``` + +## Usage + +```csharp +using RCommon; +using RCommon.RedisCache; + +services.AddRCommon(builder => +{ + builder.WithDistributedCaching(cache => + { + cache.Configure(options => + { + options.Configuration = "localhost:6379"; + options.InstanceName = "MyApp:"; + }); + cache.CacheDynamicallyCompiledExpressions(); + }); +}); +``` + +## Key Types + +| Type | Description | +|------|-------------| +| `RedisCacheService` | `ICacheService` implementation backed by Redis via `IDistributedCache` with JSON serialization | +| `RedisCachingBuilder` | Concrete builder for configuring Redis distributed caching | +| `IRedisCachingBuilder` | Builder interface extending `IDistributedCachingBuilder` | + +## Documentation + +For full documentation, visit [rcommon.com](https://rcommon.com). + +## Related Packages + +- [RCommon.Caching](https://www.nuget.org/packages/RCommon.Caching) - Core caching abstractions (`ICacheService`, `CacheKey`, builder contracts) +- [RCommon.MemoryCache](https://www.nuget.org/packages/RCommon.MemoryCache) - In-process and distributed memory cache implementations +- [RCommon.Persistence.Caching.RedisCache](https://www.nuget.org/packages/RCommon.Persistence.Caching.RedisCache) - Wires Redis caching into the persistence caching repository decorators + +## License + +Licensed under the [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0). diff --git a/Src/RCommon.RedisCache/RedisCacheService.cs b/Src/RCommon.RedisCache/RedisCacheService.cs index 2e89bf59..b48505ec 100644 --- a/Src/RCommon.RedisCache/RedisCacheService.cs +++ b/Src/RCommon.RedisCache/RedisCacheService.cs @@ -10,47 +10,67 @@ namespace RCommon.RedisCache { /// - /// Just a wrapper for Redis data caching implemented through caching abstractions + /// A wrapper for Redis data caching implemented through the + /// abstraction (backed by StackExchange.Redis). /// - /// This gives us a uniform way for getting/setting cache no matter the caching strategy + /// + /// This gives a uniform way for getting/setting cache no matter the caching strategy. + /// Data is serialized to JSON via before being stored + /// in Redis, and deserialized on retrieval. + /// public class RedisCacheService : ICacheService { private readonly IDistributedCache _distributedCache; private readonly IJsonSerializer _jsonSerializer; + /// + /// Initializes a new instance of the class. + /// + /// The underlying distributed cache implementation (Redis). + /// The JSON serializer used to serialize/deserialize cached values. public RedisCacheService(IDistributedCache distributedCache, IJsonSerializer jsonSerializer) { _distributedCache = distributedCache; _jsonSerializer = jsonSerializer; } + /// public TData GetOrCreate(object key, Func data) { - var json = _distributedCache.GetString(key.ToString()); + var cacheKey = key.ToString()!; + var json = _distributedCache.GetString(cacheKey); if (json == null) { - _distributedCache.SetString(key.ToString(), _jsonSerializer.Serialize(data())); - return data(); + // Cache miss: invoke the factory, serialize and store the result, then return it + var result = data(); + _distributedCache.SetString(cacheKey, _jsonSerializer.Serialize(result!)); + return result; } else { - return _jsonSerializer.Deserialize(json); + // Cache hit: deserialize the stored JSON back into the requested type + return _jsonSerializer.Deserialize(json)!; } } + /// public async Task GetOrCreateAsync(object key, Func data) { - var json = await _distributedCache.GetStringAsync(key.ToString()).ConfigureAwait(false); + var cacheKey = key.ToString()!; + var json = await _distributedCache.GetStringAsync(cacheKey).ConfigureAwait(false); if (json == null) { - await _distributedCache.SetStringAsync(key.ToString(), _jsonSerializer.Serialize(data())).ConfigureAwait(false); - return data(); + // Cache miss: invoke the factory, serialize and store the result asynchronously + var result = data(); + await _distributedCache.SetStringAsync(cacheKey, _jsonSerializer.Serialize(result!)).ConfigureAwait(false); + return result; } else { - return _jsonSerializer.Deserialize(json); + // Cache hit: deserialize the stored JSON back into the requested type + return _jsonSerializer.Deserialize(json)!; } } } diff --git a/Src/RCommon.RedisCache/RedisCachingBuilder.cs b/Src/RCommon.RedisCache/RedisCachingBuilder.cs index a0adf641..1820ecc1 100644 --- a/Src/RCommon.RedisCache/RedisCachingBuilder.cs +++ b/Src/RCommon.RedisCache/RedisCachingBuilder.cs @@ -8,19 +8,36 @@ namespace RCommon.RedisCache { + /// + /// Builder for configuring Redis-backed distributed caching using the StackExchange.Redis provider. + /// + /// + /// This is the concrete builder activated by + /// + /// when RedisCachingBuilder is specified as the type parameter. + /// public class RedisCachingBuilder : IRedisCachingBuilder { + /// + /// Initializes a new instance of the class. + /// + /// The RCommon builder whose is used for service registration. public RedisCachingBuilder(IRCommonBuilder builder) { Services = builder.Services; this.RegisterServices(Services); } + /// + /// Registers any default services required by the Redis cache builder. + /// + /// The service collection to register services into. protected void RegisterServices(IServiceCollection services) { - + } + /// public IServiceCollection Services { get; } } } diff --git a/Src/RCommon.Security/Authorization/AuthorizationException.cs b/Src/RCommon.Security/Authorization/AuthorizationException.cs index 89e27d08..f729c1bb 100644 --- a/Src/RCommon.Security/Authorization/AuthorizationException.cs +++ b/Src/RCommon.Security/Authorization/AuthorizationException.cs @@ -23,7 +23,7 @@ public class AuthorizationException : ApplicationException /// /// Error code. /// - public string Code { get; } + public string? Code { get; } /// /// Creates a new object. @@ -60,13 +60,19 @@ public AuthorizationException(string message, Exception innerException) /// Exception message /// Exception code /// Inner exception - public AuthorizationException(string message = null, string code = null, Exception innerException = null) + public AuthorizationException(string? message = null, string? code = null, Exception? innerException = null) : base(message, innerException) { Code = code; LogLevel = LogLevel.Warning; } + /// + /// Adds a key/value pair to the exception's dictionary and returns the current instance for fluent chaining. + /// + /// The key to store in the data dictionary. + /// The value associated with the key. + /// The current instance. public AuthorizationException WithData(string name, object value) { Data[name] = value; diff --git a/Src/RCommon.Security/Claims/ClaimTypesConst.cs b/Src/RCommon.Security/Claims/ClaimTypesConst.cs index ae338f80..95a7c83b 100644 --- a/Src/RCommon.Security/Claims/ClaimTypesConst.cs +++ b/Src/RCommon.Security/Claims/ClaimTypesConst.cs @@ -7,6 +7,10 @@ namespace RCommon.Security.Claims { + /// + /// Provides configurable constants for standard claim type URIs used throughout the security subsystem. + /// Each property defaults to the corresponding value and can be overridden at startup. + /// public static class ClaimTypesConst { /// diff --git a/Src/RCommon.Security/Claims/CurrentPrincipalAccessorBase.cs b/Src/RCommon.Security/Claims/CurrentPrincipalAccessorBase.cs index d581c5bb..a75e3781 100644 --- a/Src/RCommon.Security/Claims/CurrentPrincipalAccessorBase.cs +++ b/Src/RCommon.Security/Claims/CurrentPrincipalAccessorBase.cs @@ -8,21 +8,45 @@ namespace RCommon.Security.Claims { + /// + /// Abstract base class for accessing and temporarily replacing the current . + /// Uses to maintain an override principal that flows across async contexts. + /// + /// + /// Derived classes must implement to provide the default principal + /// when no override has been set (e.g., from or an HTTP context). + /// public abstract class CurrentPrincipalAccessorBase : ICurrentPrincipalAccessor { - public ClaimsPrincipal Principal => _currentPrincipal.Value ?? GetClaimsPrincipal(); + /// + public ClaimsPrincipal? Principal => _currentPrincipal.Value ?? GetClaimsPrincipal(); - private readonly AsyncLocal _currentPrincipal = new AsyncLocal(); + /// + /// Async-local storage that holds the overridden principal for the current execution context. + /// + private readonly AsyncLocal _currentPrincipal = new AsyncLocal(); - protected abstract ClaimsPrincipal GetClaimsPrincipal(); + /// + /// When implemented in a derived class, returns the default for the current context. + /// + /// The current , or null if none is available. + protected abstract ClaimsPrincipal? GetClaimsPrincipal(); + /// public virtual IDisposable Change(ClaimsPrincipal principal) { return SetCurrent(principal); } + /// + /// Replaces the current principal with and returns an + /// that restores the previous principal when disposed. + /// + /// The new principal to set. + /// An that restores the previous principal on disposal. private IDisposable SetCurrent(ClaimsPrincipal principal) { + // Capture the current principal so it can be restored when the scope ends. var parent = Principal; _currentPrincipal.Value = principal; return new DisposeAction(() => diff --git a/Src/RCommon.Security/Claims/CurrentPrincipalAccessorExtensions.cs b/Src/RCommon.Security/Claims/CurrentPrincipalAccessorExtensions.cs index b3d7e9d2..4ec859ab 100644 --- a/Src/RCommon.Security/Claims/CurrentPrincipalAccessorExtensions.cs +++ b/Src/RCommon.Security/Claims/CurrentPrincipalAccessorExtensions.cs @@ -7,18 +7,40 @@ namespace RCommon.Security.Claims { + /// + /// Convenience extension methods for that allow changing the + /// current principal from a single , a collection of claims, or a . + /// public static class CurrentPrincipalAccessorExtensions { + /// + /// Temporarily replaces the current principal with one containing the specified . + /// + /// The principal accessor to change. + /// The claim to include in the new principal. + /// An that restores the previous principal on disposal. public static IDisposable Change(this ICurrentPrincipalAccessor currentPrincipalAccessor, Claim claim) { return currentPrincipalAccessor.Change(new[] { claim }); } + /// + /// Temporarily replaces the current principal with one built from the specified . + /// + /// The principal accessor to change. + /// The claims to include in the new principal's identity. + /// An that restores the previous principal on disposal. public static IDisposable Change(this ICurrentPrincipalAccessor currentPrincipalAccessor, IEnumerable claims) { return currentPrincipalAccessor.Change(new ClaimsIdentity(claims)); } + /// + /// Temporarily replaces the current principal with one wrapping the specified . + /// + /// The principal accessor to change. + /// The identity to wrap in a new . + /// An that restores the previous principal on disposal. public static IDisposable Change(this ICurrentPrincipalAccessor currentPrincipalAccessor, ClaimsIdentity claimsIdentity) { return currentPrincipalAccessor.Change(new ClaimsPrincipal(claimsIdentity)); diff --git a/Src/RCommon.Security/Claims/ICurrentPrincipalAccessor.cs b/Src/RCommon.Security/Claims/ICurrentPrincipalAccessor.cs index eba0356d..4fbb7f22 100644 --- a/Src/RCommon.Security/Claims/ICurrentPrincipalAccessor.cs +++ b/Src/RCommon.Security/Claims/ICurrentPrincipalAccessor.cs @@ -7,10 +7,21 @@ namespace RCommon.Security.Claims { + /// + /// Provides access to the current and allows temporarily replacing it within a scoped context. + /// public interface ICurrentPrincipalAccessor { - ClaimsPrincipal Principal { get; } + /// + /// Gets the current , or null if none is available. + /// + ClaimsPrincipal? Principal { get; } + /// + /// Temporarily replaces the current principal with the specified . + /// + /// The new principal to set as current. + /// An that restores the previous principal when disposed. IDisposable Change(ClaimsPrincipal principal); } } diff --git a/Src/RCommon.Security/Claims/ThreadCurrentPrincipalAccessor.cs b/Src/RCommon.Security/Claims/ThreadCurrentPrincipalAccessor.cs index 20a9756a..4168d429 100644 --- a/Src/RCommon.Security/Claims/ThreadCurrentPrincipalAccessor.cs +++ b/Src/RCommon.Security/Claims/ThreadCurrentPrincipalAccessor.cs @@ -8,9 +8,18 @@ namespace RCommon.Security.Claims { + /// + /// An implementation that retrieves the default principal + /// from . + /// + /// + /// This is the default accessor registered by . + /// In ASP.NET Core scenarios, consider using an HTTP-context-based accessor instead. + /// public class ThreadCurrentPrincipalAccessor : CurrentPrincipalAccessorBase { - protected override ClaimsPrincipal GetClaimsPrincipal() + /// + protected override ClaimsPrincipal? GetClaimsPrincipal() { return Thread.CurrentPrincipal as ClaimsPrincipal; } diff --git a/Src/RCommon.Security/ClaimsIdentityExtensions.cs b/Src/RCommon.Security/ClaimsIdentityExtensions.cs index 6cc5ba7a..25164990 100644 --- a/Src/RCommon.Security/ClaimsIdentityExtensions.cs +++ b/Src/RCommon.Security/ClaimsIdentityExtensions.cs @@ -9,8 +9,17 @@ namespace RCommon.Security { + /// + /// Extension methods for , , and + /// that simplify extracting well-known claim values and managing claims collections. + /// public static class ClaimsIdentityExtensions { + /// + /// Extracts the user identifier from the principal's claims as a . + /// + /// The claims principal to search. + /// The parsed user ID, or null if the claim is missing or not a valid GUID. public static Guid? FindUserId(this ClaimsPrincipal principal) { Guard.IsNotNull(principal, nameof(principal)); @@ -29,6 +38,11 @@ public static class ClaimsIdentityExtensions return null; } + /// + /// Extracts the user identifier from the identity's claims as a . + /// + /// The identity to search. Must be castable to . + /// The parsed user ID, or null if the claim is missing or not a valid GUID. public static Guid? FindUserId(this IIdentity identity) { Guard.IsNotNull(identity, nameof(identity)); @@ -49,6 +63,11 @@ public static class ClaimsIdentityExtensions return null; } + /// + /// Extracts the tenant identifier from the principal's claims as a . + /// + /// The claims principal to search. + /// The parsed tenant ID, or null if the claim is missing or not a valid GUID. public static Guid? FindTenantId(this ClaimsPrincipal principal) { Guard.IsNotNull(principal, nameof(principal)); @@ -67,6 +86,11 @@ public static class ClaimsIdentityExtensions return null; } + /// + /// Extracts the tenant identifier from the identity's claims as a . + /// + /// The identity to search. Must be castable to . + /// The parsed tenant ID, or null if the claim is missing or not a valid GUID. public static Guid? FindTenantId(this IIdentity identity) { Guard.IsNotNull(identity, nameof(identity)); @@ -87,7 +111,12 @@ public static class ClaimsIdentityExtensions return null; } - public static string FindClientId(this ClaimsPrincipal principal) + /// + /// Extracts the client identifier from the principal's claims. + /// + /// The claims principal to search. + /// The client ID string, or null if the claim is missing or empty. + public static string? FindClientId(this ClaimsPrincipal principal) { Guard.IsNotNull(principal, nameof(principal)); @@ -100,7 +129,12 @@ public static string FindClientId(this ClaimsPrincipal principal) return clientIdOrNull.Value; } - public static string FindClientId(this IIdentity identity) + /// + /// Extracts the client identifier from the identity's claims. + /// + /// The identity to search. Must be castable to . + /// The client ID string, or null if the claim is missing or empty. + public static string? FindClientId(this IIdentity identity) { Guard.IsNotNull(identity, nameof(identity)); @@ -115,8 +149,14 @@ public static string FindClientId(this IIdentity identity) return clientIdOrNull.Value; } - + + /// + /// Adds a claim to the identity only if no claim with the same type already exists (case-insensitive comparison). + /// + /// The identity to add the claim to. + /// The claim to add. + /// The same instance for fluent chaining. public static ClaimsIdentity AddIfNotContains(this ClaimsIdentity claimsIdentity, Claim claim) { Guard.IsNotNull(claimsIdentity, nameof(claimsIdentity)); @@ -129,10 +169,17 @@ public static ClaimsIdentity AddIfNotContains(this ClaimsIdentity claimsIdentity return claimsIdentity; } + /// + /// Removes all existing claims of the same type and adds the new . + /// + /// The identity to modify. + /// The claim to set, replacing any existing claims of the same type. + /// The same instance for fluent chaining. public static ClaimsIdentity AddOrReplace(this ClaimsIdentity claimsIdentity, Claim claim) { Guard.IsNotNull(claimsIdentity, nameof(claimsIdentity)); + // Remove all claims matching the type before adding the replacement. foreach (var x in claimsIdentity.FindAll(claim.Type).ToList()) { claimsIdentity.RemoveClaim(x); @@ -143,6 +190,13 @@ public static ClaimsIdentity AddOrReplace(this ClaimsIdentity claimsIdentity, Cl return claimsIdentity; } + /// + /// Adds a to the principal only if no identity with the same + /// already exists (case-insensitive comparison). + /// + /// The principal to add the identity to. + /// The identity to add. + /// The same instance for fluent chaining. public static ClaimsPrincipal AddIdentityIfNotContains(this ClaimsPrincipal principal, ClaimsIdentity identity) { Guard.IsNotNull(principal, nameof(principal)); diff --git a/Src/RCommon.Security/Clients/CurrentClient.cs b/Src/RCommon.Security/Clients/CurrentClient.cs index 457a6e3e..c10a6844 100644 --- a/Src/RCommon.Security/Clients/CurrentClient.cs +++ b/Src/RCommon.Security/Clients/CurrentClient.cs @@ -7,14 +7,24 @@ namespace RCommon.Security.Clients { + /// + /// Default implementation of that resolves the client identity + /// from the current via . + /// public class CurrentClient : ICurrentClient { - public virtual string Id => _principalAccessor.Principal?.FindClientId(); + /// + public virtual string? Id => _principalAccessor.Principal?.FindClientId(); + /// public virtual bool IsAuthenticated => Id != null; private readonly ICurrentPrincipalAccessor _principalAccessor; + /// + /// Initializes a new instance of the class. + /// + /// The accessor used to retrieve the current claims principal. public CurrentClient(ICurrentPrincipalAccessor principalAccessor) { _principalAccessor = principalAccessor; diff --git a/Src/RCommon.Security/Clients/ICurrentClient.cs b/Src/RCommon.Security/Clients/ICurrentClient.cs index 5d842fa6..ed88b297 100644 --- a/Src/RCommon.Security/Clients/ICurrentClient.cs +++ b/Src/RCommon.Security/Clients/ICurrentClient.cs @@ -1,8 +1,19 @@ namespace RCommon.Security.Clients { + /// + /// Represents the currently authenticated client (e.g., an OAuth client application) in a multi-client environment. + /// public interface ICurrentClient { - string Id { get; } + /// + /// Gets the unique identifier of the current client, derived from the claim. + /// Returns null if no client identity is present. + /// + string? Id { get; } + + /// + /// Gets a value indicating whether the current request has an authenticated client identity. + /// bool IsAuthenticated { get; } } } \ No newline at end of file diff --git a/Src/RCommon.Security/RCommon.Security.csproj b/Src/RCommon.Security/RCommon.Security.csproj index 35579496..5e21e874 100644 --- a/Src/RCommon.Security/RCommon.Security.csproj +++ b/Src/RCommon.Security/RCommon.Security.csproj @@ -2,6 +2,7 @@ net8.0;net9.0;net10.0 + enable True RCommon.Security https://rcommon.com diff --git a/Src/RCommon.Security/README.md b/Src/RCommon.Security/README.md index d7d3bd48..d9037b1b 100644 --- a/Src/RCommon.Security/README.md +++ b/Src/RCommon.Security/README.md @@ -1,3 +1,103 @@ # RCommon.Security -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 claims-based security abstractions for RCommon, including current user and current client identity access built on top of `ClaimsPrincipal`, with configurable claim type mappings and multi-tenancy support. + +## Features + +- **Current user abstraction** -- `ICurrentUser` exposes the authenticated user's ID, tenant ID, roles, and claims without depending on a specific auth framework +- **Current client abstraction** -- `ICurrentClient` identifies the calling OAuth/API client from the `client_id` claim +- **ClaimsPrincipal accessor** -- `ICurrentPrincipalAccessor` provides the current principal with support for temporary principal replacement via scoped `IDisposable` +- **AsyncLocal principal override** -- `CurrentPrincipalAccessorBase` uses `AsyncLocal` so overridden principals flow across async contexts +- **Configurable claim types** -- `ClaimTypesConst` allows customizing which claim URIs map to user ID, tenant ID, client ID, roles, etc. +- **ClaimsIdentity extensions** -- helper methods for finding user/tenant/client IDs and for safely adding or replacing claims +- **Authorization exception** -- `AuthorizationException` with configurable severity, error codes, and fluent data attachment +- **Fluent builder API** -- integrates with the `AddRCommon()` builder pattern for one-line DI registration + +## Installation + +```shell +dotnet add package RCommon.Security +``` + +## Usage + +```csharp +using RCommon; +using RCommon.Security.Users; +using RCommon.Security.Clients; + +// Register security services in your DI setup +services.AddRCommon(config => +{ + config.WithClaimsAndPrincipalAccessor(); +}); + +// Inject ICurrentUser or ICurrentClient in your services +public class TenantService +{ + private readonly ICurrentUser _currentUser; + private readonly ICurrentClient _currentClient; + + public TenantService(ICurrentUser currentUser, ICurrentClient currentClient) + { + _currentUser = currentUser; + _currentClient = currentClient; + } + + public Guid GetTenantId() + { + if (!_currentUser.IsAuthenticated) + throw new UnauthorizedAccessException("User is not authenticated."); + + return _currentUser.TenantId + ?? throw new InvalidOperationException("No tenant claim found."); + } + + public string GetClientId() + { + return _currentClient.Id + ?? throw new InvalidOperationException("No client identity found."); + } + + public bool IsInRole(string role) + { + return _currentUser.Roles.Contains(role); + } +} +``` + +### Customizing Claim Types + +```csharp +// Override the default claim type URIs at startup if your identity provider uses custom claims +ClaimTypesConst.UserId = "sub"; +ClaimTypesConst.TenantId = "tenant"; +ClaimTypesConst.ClientId = "azp"; +``` + +## Key Types + +| Type | Description | +|------|-------------| +| `ICurrentUser` | Provides the authenticated user's ID, tenant ID, roles, and claim lookups | +| `CurrentUser` | Default implementation that reads from the current `ClaimsPrincipal` | +| `ICurrentClient` | Provides the authenticated client application's ID and authentication status | +| `CurrentClient` | Default implementation that reads the `client_id` claim from the principal | +| `ICurrentPrincipalAccessor` | Accesses the current `ClaimsPrincipal` and supports scoped replacement | +| `ThreadCurrentPrincipalAccessor` | Default accessor that reads from `Thread.CurrentPrincipal` | +| `CurrentPrincipalAccessorBase` | Abstract base using `AsyncLocal` for async-safe principal overrides | +| `ClaimTypesConst` | Configurable constants for standard claim type URIs (user ID, role, tenant, etc.) | +| `AuthorizationException` | Exception for unauthorized requests with log level, error code, and fluent data API | +| `ClaimsIdentityExtensions` | Extension methods for extracting user/tenant/client IDs and managing claims | + +## 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 + +## License + +Licensed under the [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0). diff --git a/Src/RCommon.Security/SecurityConfigurationExtensions.cs b/Src/RCommon.Security/SecurityConfigurationExtensions.cs index ed251079..d047a2fc 100644 --- a/Src/RCommon.Security/SecurityConfigurationExtensions.cs +++ b/Src/RCommon.Security/SecurityConfigurationExtensions.cs @@ -10,9 +10,18 @@ namespace RCommon { + /// + /// Extension methods for that register security-related services + /// such as claims-based principal access, current user, and current client abstractions. + /// public static class SecurityConfigurationExtensions { - + /// + /// Registers the default claims and principal accessor services into the dependency injection container. + /// This includes , , and . + /// + /// The RCommon builder to configure. + /// The same instance for fluent chaining. public static IRCommonBuilder WithClaimsAndPrincipalAccessor(this IRCommonBuilder config) { config.Services.AddTransient(); diff --git a/Src/RCommon.Security/Users/CurrentUser.cs b/Src/RCommon.Security/Users/CurrentUser.cs index 8822ea21..cf4f2971 100644 --- a/Src/RCommon.Security/Users/CurrentUser.cs +++ b/Src/RCommon.Security/Users/CurrentUser.cs @@ -8,37 +8,53 @@ namespace RCommon.Security.Users { + /// + /// Default implementation of that resolves user information + /// from the current via . + /// public class CurrentUser : ICurrentUser { + /// + /// Shared empty array returned when the principal has no claims, avoiding repeated allocations. + /// private static readonly Claim[] EmptyClaimsArray = new Claim[0]; + private readonly ICurrentPrincipalAccessor _principalAccessor; + /// + /// Initializes a new instance of the class. + /// + /// The accessor used to retrieve the current claims principal. public CurrentUser(ICurrentPrincipalAccessor principalAccessor) { _principalAccessor = principalAccessor; } - - - + /// public virtual bool IsAuthenticated => Id.HasValue; + /// public virtual Guid? Id => _principalAccessor.Principal?.FindUserId(); + /// public virtual Guid? TenantId => _principalAccessor.Principal?.FindTenantId(); + /// public virtual string[] Roles => FindClaims(ClaimTypesConst.Role).Select(c => c.Value).Distinct().ToArray(); - public virtual Claim FindClaim(string claimType) + /// + public virtual Claim? FindClaim(string claimType) { return _principalAccessor.Principal?.Claims.FirstOrDefault(c => c.Type == claimType); } + /// public virtual Claim[] FindClaims(string claimType) { return _principalAccessor.Principal?.Claims.Where(c => c.Type == claimType).ToArray() ?? EmptyClaimsArray; } + /// public virtual Claim[] GetAllClaims() { return _principalAccessor.Principal?.Claims.ToArray() ?? EmptyClaimsArray; diff --git a/Src/RCommon.Security/Users/CurrentUserExtensions.cs b/Src/RCommon.Security/Users/CurrentUserExtensions.cs index 10df70d5..2a6cc9b5 100644 --- a/Src/RCommon.Security/Users/CurrentUserExtensions.cs +++ b/Src/RCommon.Security/Users/CurrentUserExtensions.cs @@ -9,13 +9,31 @@ namespace RCommon.Security.Users { + /// + /// Convenience extension methods for that simplify claim value retrieval + /// and provide strongly-typed access to user identity properties. + /// public static class CurrentUserExtensions { - public static string FindClaimValue(this ICurrentUser currentUser, string claimType) + /// + /// Finds a claim of the specified type and returns its string value. + /// + /// The current user instance. + /// The claim type URI to search for. + /// The claim value as a string, or null if the claim is not found. + public static string? FindClaimValue(this ICurrentUser currentUser, string claimType) { return currentUser.FindClaim(claimType)?.Value; } + /// + /// Finds a claim of the specified type and converts its value to . + /// + /// The target value type to convert the claim value to. + /// The current user instance. + /// The claim type URI to search for. + /// The converted claim value, or default if the claim is not found. + /// Uses with . public static T FindClaimValue(this ICurrentUser currentUser, string claimType) where T : struct { @@ -27,6 +45,12 @@ public static T FindClaimValue(this ICurrentUser currentUser, string claimTyp return (T)Convert.ChangeType(value, typeof(T), CultureInfo.InvariantCulture); } + /// + /// Gets the current user's ID, asserting that it is not null. + /// + /// The current user instance. + /// The user's identifier. + /// Thrown if is null. public static Guid GetId(this ICurrentUser currentUser) { Debug.Assert(currentUser.Id != null, "currentUser.Id != null"); diff --git a/Src/RCommon.Security/Users/ICurrentUser.cs b/Src/RCommon.Security/Users/ICurrentUser.cs index 2b1fb9d8..f5e464e8 100644 --- a/Src/RCommon.Security/Users/ICurrentUser.cs +++ b/Src/RCommon.Security/Users/ICurrentUser.cs @@ -3,15 +3,49 @@ namespace RCommon.Security.Users { + /// + /// Represents the currently authenticated user, providing access to identity properties and claims. + /// public interface ICurrentUser { + /// + /// Gets the unique identifier of the current user, or null if no user is authenticated. + /// Guid? Id { get; } + + /// + /// Gets a value indicating whether the current user is authenticated (i.e., is not null). + /// bool IsAuthenticated { get; } + + /// + /// Gets the distinct set of role names assigned to the current user. + /// string[] Roles { get; } + + /// + /// Gets the tenant identifier of the current user, or null if no tenant claim is present. + /// Guid? TenantId { get; } - Claim FindClaim(string claimType); + /// + /// Finds the first claim matching the specified . + /// + /// The claim type URI to search for. + /// The matching , or null if not found. + Claim? FindClaim(string claimType); + + /// + /// Finds all claims matching the specified . + /// + /// The claim type URI to search for. + /// An array of matching claims, or an empty array if none are found. Claim[] FindClaims(string claimType); + + /// + /// Gets all claims associated with the current user. + /// + /// An array of all claims, or an empty array if the user has no claims. Claim[] GetAllClaims(); } } diff --git a/Src/RCommon.SendGrid/README.md b/Src/RCommon.SendGrid/README.md index a0bdadc4..dde98a38 100644 --- a/Src/RCommon.SendGrid/README.md +++ b/Src/RCommon.SendGrid/README.md @@ -1,3 +1,84 @@ # RCommon.SendGrid -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 +A SendGrid implementation of the RCommon `IEmailService` abstraction, enabling email delivery through the SendGrid API using standard `System.Net.Mail.MailMessage` objects. + +## Features + +- Implements `IEmailService` using the SendGrid API client +- Accepts standard `MailMessage` objects and converts them to SendGrid messages automatically +- Supports HTML and plain text email bodies +- Streams file attachments to the SendGrid API +- Supports sending to multiple recipients in a single call +- `EmailSent` event for post-send notifications +- API key configuration via the options pattern +- Fluent DI registration through the `AddRCommon()` builder + +## Installation + +```shell +dotnet add package RCommon.SendGrid +``` + +## Usage + +```csharp +using RCommon; + +services.AddRCommon() + .WithSendGridEmailServices(settings => + { + settings.SendGridApiKey = "your-sendgrid-api-key"; + settings.FromEmailDefault = "noreply@example.com"; + settings.FromNameDefault = "My Application"; + }); +``` + +Then inject and use `IEmailService`: + +```csharp +using RCommon.Emailing; +using System.Net.Mail; + +public class NotificationService +{ + private readonly IEmailService _emailService; + + public NotificationService(IEmailService emailService) + { + _emailService = emailService; + } + + public async Task SendWelcomeEmailAsync(string toAddress) + { + var message = new MailMessage("noreply@example.com", toAddress) + { + Subject = "Welcome!", + Body = "

Welcome to our app

", + IsBodyHtml = true + }; + + await _emailService.SendEmailAsync(message); + } +} +``` + +## Key Types + +| Type | Description | +|------|-------------| +| `SendGridEmailService` | `IEmailService` implementation that sends email via the SendGrid API | +| `SendGridEmailSettings` | Configuration for the SendGrid API key and default sender details | +| `SendGridEmailingConfigurationExtensions` | Provides `WithSendGridEmailServices()` for the RCommon builder | + +## Documentation + +For full documentation, visit [rcommon.com](https://rcommon.com). + +## Related Packages + +- [RCommon.Emailing](https://www.nuget.org/packages/RCommon.Emailing) - Core email abstraction with SMTP implementation +- [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.SendGrid/SendGridEmailService.cs b/Src/RCommon.SendGrid/SendGridEmailService.cs index a67c0c83..0847445b 100644 --- a/Src/RCommon.SendGrid/SendGridEmailService.cs +++ b/Src/RCommon.SendGrid/SendGridEmailService.cs @@ -12,23 +12,43 @@ namespace RCommon.Emailing.SendGrid { + /// + /// Implementation of that sends email through the SendGrid API. + /// + /// + /// Converts standard objects to SendGrid messages. The API key is + /// configured via . + /// public class SendGridEmailService : IEmailService { + /// + public event EventHandler? EmailSent; - public event EventHandler EmailSent; - + /// + /// + /// Synchronous wrapper that delegates to using . + /// public void SendEmail(MailMessage message) { AsyncHelper.RunSync(() => SendEmailAsync(message)); } private readonly SendGridClient _client; - - public SendGridEmailService(IOptions settings, ILogger logger) - { - _client = new SendGridClient(settings.Value.SendGridApiKey); + + /// + /// Initializes a new instance of the class. + /// + /// The SendGrid configuration options containing the API key. + /// The logger instance for diagnostic output. + public SendGridEmailService(IOptions settings, ILogger logger) + { + _client = new SendGridClient(settings.Value.SendGridApiKey); } + /// + /// Raises the event if any subscribers are attached. + /// + /// The mail message that was sent. private void OnEmailSent(MailMessage message) { if (EmailSent != null) @@ -37,20 +57,28 @@ private void OnEmailSent(MailMessage message) } } + /// + /// + /// Converts the to a SendGrid , + /// streams any attachments, then sends via the SendGrid API client. + /// public async Task SendEmailAsync(MailMessage message) { + // No-op when there are no recipients. if (message.To.Count == 0) { await Task.CompletedTask; } - var sgMessage = MailHelper.CreateSingleEmailToMultipleRecipients(from: new EmailAddress(message.From.Address, message.From.DisplayName), + // Map the MailMessage to a SendGrid message, choosing plain text or HTML based on IsBodyHtml. + var sgMessage = MailHelper.CreateSingleEmailToMultipleRecipients(from: new EmailAddress(message.From!.Address, message.From.DisplayName), tos: message.To.Select(r => new EmailAddress(r.Address, r.DisplayName)).ToList(), subject: message.Subject, plainTextContent: message.IsBodyHtml ? null : message.Body, htmlContent: message.IsBodyHtml ? message.Body : null); + // Stream each attachment into the SendGrid message. if (message.Attachments != null) { foreach (var attachment in message.Attachments) diff --git a/Src/RCommon.SendGrid/SendGridEmailSettings.cs b/Src/RCommon.SendGrid/SendGridEmailSettings.cs index 373da35f..01fb6a7f 100644 --- a/Src/RCommon.SendGrid/SendGridEmailSettings.cs +++ b/Src/RCommon.SendGrid/SendGridEmailSettings.cs @@ -6,16 +6,33 @@ namespace RCommon.Emailing.SendGrid { + /// + /// Configuration settings for the . + /// Typically bound from an application configuration section (e.g., appsettings.json). + /// public class SendGridEmailSettings { - + /// + /// Initializes a new instance of the class. + /// public SendGridEmailSettings() { } + /// + /// Gets or sets the SendGrid API key used to authenticate with the SendGrid service. + /// public string? SendGridApiKey { get; set; } + + /// + /// Gets or sets the default sender email address used when no explicit "From" address is specified. + /// public string? FromEmailDefault { get; set; } + + /// + /// Gets or sets the default sender display name used when no explicit "From" name is specified. + /// public string? FromNameDefault { get; set; } } } diff --git a/Src/RCommon.SendGrid/SendGridEmailingConfigurationExtensions.cs b/Src/RCommon.SendGrid/SendGridEmailingConfigurationExtensions.cs index 0f787612..73431c90 100644 --- a/Src/RCommon.SendGrid/SendGridEmailingConfigurationExtensions.cs +++ b/Src/RCommon.SendGrid/SendGridEmailingConfigurationExtensions.cs @@ -10,9 +10,18 @@ namespace RCommon { + /// + /// Extension methods for that register SendGrid-based email services. + /// public static class SendGridEmailingConfigurationExtensions { - + /// + /// Registers as the implementation + /// and configures the SendGrid settings via the provided delegate. + /// + /// The RCommon builder to configure. + /// A delegate to configure . + /// The same instance for fluent chaining. public static IRCommonBuilder WithSendGridEmailServices(this IRCommonBuilder config, Action emailSettings) { config.Services.Configure(emailSettings); diff --git a/Src/RCommon.SystemTextJson/ITextJsonBuilder.cs b/Src/RCommon.SystemTextJson/ITextJsonBuilder.cs index 539c7b15..1c3f2911 100644 --- a/Src/RCommon.SystemTextJson/ITextJsonBuilder.cs +++ b/Src/RCommon.SystemTextJson/ITextJsonBuilder.cs @@ -7,6 +7,11 @@ namespace RCommon.SystemTextJson { + /// + /// Builder interface for configuring JSON serialization using the System.Text.Json library. + /// + /// + /// public interface ITextJsonBuilder : IJsonBuilder { } diff --git a/Src/RCommon.SystemTextJson/ITextJsonBuilderExtensions.cs b/Src/RCommon.SystemTextJson/ITextJsonBuilderExtensions.cs index 0e8f5cc8..f0162fc1 100644 --- a/Src/RCommon.SystemTextJson/ITextJsonBuilderExtensions.cs +++ b/Src/RCommon.SystemTextJson/ITextJsonBuilderExtensions.cs @@ -10,8 +10,17 @@ namespace RCommon.SystemTextJson { + /// + /// Provides extension methods for to configure System.Text.Json settings. + /// public static class ITextJsonBuilderExtensions { + /// + /// Configures the underlying used by the System.Text.Json serializer. + /// + /// The System.Text.Json builder instance. + /// An action to configure . + /// The for further chaining. public static ITextJsonBuilder Configure(this ITextJsonBuilder builder, Action options) { builder.Services.Configure(options); diff --git a/Src/RCommon.SystemTextJson/JsonByteEnumConverter.cs b/Src/RCommon.SystemTextJson/JsonByteEnumConverter.cs index af29da0f..cf3cdfcd 100644 --- a/Src/RCommon.SystemTextJson/JsonByteEnumConverter.cs +++ b/Src/RCommon.SystemTextJson/JsonByteEnumConverter.cs @@ -8,17 +8,42 @@ namespace RCommon.SystemTextJson { + /// + /// A custom that serializes enum values as their underlying + /// numeric representation and deserializes from string names. + /// + /// The enum type to convert. + /// + /// During reading, the converter expects the JSON token to be a string containing the enum member name. + /// During writing, the converter outputs the numeric value of the enum member. + /// + /// public class JsonByteEnumConverter : JsonConverter where T : Enum { + /// + /// Reads a JSON string token and parses it into the corresponding enum value. + /// + /// The UTF-8 JSON reader. + /// The target enum type. + /// The serializer options. + /// The parsed enum value of type . public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - T value = (T)(Enum.Parse(typeof(T), reader.GetString())); + // Parse the string token as the enum member name + T value = (T)(Enum.Parse(typeof(T), reader.GetString()!)); return value; } + /// + /// Writes the enum value as its numeric representation. + /// + /// The UTF-8 JSON writer. + /// The enum value to serialize. + /// The serializer options. public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) { + // Re-parse the enum to its base Enum type and convert to byte for numeric output Enum test = (Enum)Enum.Parse(typeof(T), value.ToString()); writer.WriteNumberValue(Convert.ToByte(test)); } diff --git a/Src/RCommon.SystemTextJson/JsonIntEnumConverter.cs b/Src/RCommon.SystemTextJson/JsonIntEnumConverter.cs index 0983829a..bcf51017 100644 --- a/Src/RCommon.SystemTextJson/JsonIntEnumConverter.cs +++ b/Src/RCommon.SystemTextJson/JsonIntEnumConverter.cs @@ -8,17 +8,42 @@ namespace RCommon.SystemTextJson { + /// + /// A custom that serializes enum values as their underlying + /// numeric representation and deserializes from string names. + /// + /// The enum type to convert. + /// + /// During reading, the converter expects the JSON token to be a string containing the enum member name. + /// During writing, the converter outputs the numeric value of the enum member. + /// + /// public class JsonIntEnumConverter : JsonConverter where T : Enum { + /// + /// Reads a JSON string token and parses it into the corresponding enum value. + /// + /// The UTF-8 JSON reader. + /// The target enum type. + /// The serializer options. + /// The parsed enum value of type . public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - T value = (T)(Enum.Parse(typeof(T), reader.GetString())); + // Parse the string token as the enum member name + T value = (T)(Enum.Parse(typeof(T), reader.GetString()!)); return value; } + /// + /// Writes the enum value as its numeric representation. + /// + /// The UTF-8 JSON writer. + /// The enum value to serialize. + /// The serializer options. public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) { + // Re-parse the enum to its base Enum type and convert to int for numeric output Enum test = (Enum)Enum.Parse(typeof(T), value.ToString()); writer.WriteNumberValue(Convert.ToInt32(test)); } diff --git a/Src/RCommon.SystemTextJson/RCommon.SystemTextJson.csproj b/Src/RCommon.SystemTextJson/RCommon.SystemTextJson.csproj index 4bcb3828..20567333 100644 --- a/Src/RCommon.SystemTextJson/RCommon.SystemTextJson.csproj +++ b/Src/RCommon.SystemTextJson/RCommon.SystemTextJson.csproj @@ -2,6 +2,7 @@ net8.0;net9.0;net10.0 + enable True RCommon.SystemTextJson https://rcommon.com @@ -16,18 +17,6 @@ README.md - - - - - - - - - - - - True diff --git a/Src/RCommon.SystemTextJson/README.md b/Src/RCommon.SystemTextJson/README.md index 2a2ac4aa..c11b706e 100644 --- a/Src/RCommon.SystemTextJson/README.md +++ b/Src/RCommon.SystemTextJson/README.md @@ -1,3 +1,83 @@ - # RCommon.SystemTextJson +# RCommon.SystemTextJson -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 +System.Text.Json implementation of RCommon's `IJsonSerializer` abstraction. This package registers `TextJsonSerializer` into the dependency injection container and provides fluent configuration of `JsonSerializerOptions`, along with custom enum converters. + +## Features + +- Implements `IJsonSerializer` using the built-in System.Text.Json library +- Per-call options for camelCase property naming and indented formatting +- Fluent configuration of `JsonSerializerOptions` through the builder pattern +- Custom `JsonByteEnumConverter` for serializing enums as byte values +- Custom `JsonIntEnumConverter` for serializing enums as int values +- Integrates with RCommon's `AddRCommon()` / `WithJsonSerialization()` pipeline +- Registered as a transient service in the DI container + +## Installation + +```shell +dotnet add package RCommon.SystemTextJson +``` + +## Usage + +Register the System.Text.Json serializer through the RCommon builder: + +```csharp +using RCommon; +using RCommon.SystemTextJson; + +services.AddRCommon(builder => +{ + builder.WithJsonSerialization(serializer => + { + serializer.Configure(options => + { + options.DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull; + options.Converters.Add(new JsonIntEnumConverter()); + }); + }); +}); +``` + +Then inject and use `IJsonSerializer` in your services: + +```csharp +public class OrderService +{ + private readonly IJsonSerializer _serializer; + + public OrderService(IJsonSerializer serializer) + { + _serializer = serializer; + } + + public string SerializeOrder(Order order) + { + return _serializer.Serialize(order); + } +} +``` + +## Key Types + +| Type | Description | +|------|-------------| +| `TextJsonSerializer` | `IJsonSerializer` implementation backed by System.Text.Json | +| `TextJsonBuilder` | Registers `TextJsonSerializer` into the DI container | +| `ITextJsonBuilder` | Builder interface for System.Text.Json-specific configuration | +| `ITextJsonBuilderExtensions` | Provides `Configure(Action)` for customizing serializer options | +| `JsonIntEnumConverter` | Custom converter that serializes enums as their int numeric value | +| `JsonByteEnumConverter` | Custom converter that serializes enums as their byte numeric value | + +## Documentation + +For full documentation, visit [rcommon.com](https://rcommon.com). + +## Related Packages + +- [RCommon.Json](https://www.nuget.org/packages/RCommon.Json) - JSON serialization abstractions (IJsonSerializer, options) +- [RCommon.JsonNet](https://www.nuget.org/packages/RCommon.JsonNet) - Alternative implementation using Newtonsoft.Json + +## License + +Licensed under the [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0). diff --git a/Src/RCommon.SystemTextJson/TextJsonBuilder.cs b/Src/RCommon.SystemTextJson/TextJsonBuilder.cs index 8774482c..747a1292 100644 --- a/Src/RCommon.SystemTextJson/TextJsonBuilder.cs +++ b/Src/RCommon.SystemTextJson/TextJsonBuilder.cs @@ -8,19 +8,35 @@ namespace RCommon.SystemTextJson { + /// + /// Default implementation of that registers + /// the System.Text.Json-based into the DI container. + /// + /// + /// public class TextJsonBuilder : ITextJsonBuilder { + /// + /// Initializes a new instance of and registers JSON serialization services. + /// + /// The RCommon builder providing access to the . public TextJsonBuilder(IRCommonBuilder builder) { Services = builder.Services; this.RegisterServices(Services); } + /// + /// Registers the as the implementation + /// with a transient lifetime. + /// + /// The service collection to register into. protected void RegisterServices(IServiceCollection services) { services.AddTransient(); } + /// public IServiceCollection Services { get; } } } diff --git a/Src/RCommon.SystemTextJson/TextJsonSerializer.cs b/Src/RCommon.SystemTextJson/TextJsonSerializer.cs index 75fd91c9..6384efff 100644 --- a/Src/RCommon.SystemTextJson/TextJsonSerializer.cs +++ b/Src/RCommon.SystemTextJson/TextJsonSerializer.cs @@ -9,62 +9,85 @@ namespace RCommon.SystemTextJson { + /// + /// Implements using the System.Text.Json library. + /// Supports per-call overrides for camel-case naming and indented formatting through + /// and . + /// + /// + /// The underlying are injected via the options pattern + /// and may be mutated per-call when options are provided. This means per-call options + /// modify the shared options instance. + /// public class TextJsonSerializer : IJsonSerializer { private JsonSerializerOptions _jsonOptions; + /// + /// Initializes a new instance of with the configured + /// . + /// + /// The injected System.Text.Json serializer options. public TextJsonSerializer(IOptions options) { _jsonOptions = options.Value; } - public T Deserialize(string json, JsonDeserializeOptions? options = null) + /// + public T? Deserialize(string json, JsonDeserializeOptions? options = null) { if (options != null) { + // Apply camelCase naming policy when requested if (options.CamelCase) { _jsonOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; } } - + return JsonSerializer.Deserialize(json, _jsonOptions); } - public object Deserialize(string json, Type type, JsonDeserializeOptions? options = null) + /// + public object? Deserialize(string json, Type type, JsonDeserializeOptions? options = null) { if (options != null) { + // Apply camelCase naming policy when requested if (options.CamelCase) { _jsonOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; } } - + return JsonSerializer.Deserialize(json, type, _jsonOptions); } + /// public string Serialize(object obj, JsonSerializeOptions? options = null) { if (options != null) { _jsonOptions.WriteIndented = options.Indented; + // Apply camelCase naming policy when requested if (options.CamelCase) { _jsonOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; } } - + return JsonSerializer.Serialize(obj, _jsonOptions); } + /// public string Serialize(object obj, Type type, JsonSerializeOptions? options = null) { if (options != null) { _jsonOptions.WriteIndented = options.Indented; + // Apply camelCase naming policy when requested if (options.CamelCase) { _jsonOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; diff --git a/Src/RCommon.Web/RCommon.Web.csproj b/Src/RCommon.Web/RCommon.Web.csproj index 821dd168..f61aef29 100644 --- a/Src/RCommon.Web/RCommon.Web.csproj +++ b/Src/RCommon.Web/RCommon.Web.csproj @@ -2,6 +2,7 @@ net8.0;net9.0;net10.0 + enable True RCommon.Web https://rcommon.com diff --git a/Src/RCommon.Web/README.md b/Src/RCommon.Web/README.md index 71afaf58..c0bda255 100644 --- a/Src/RCommon.Web/README.md +++ b/Src/RCommon.Web/README.md @@ -1,3 +1,36 @@ # RCommon.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 +A foundational package for ASP.NET Core web applications in the RCommon ecosystem. This package serves as a base for web-related utilities, attributes, and extensions that integrate with the RCommon infrastructure. + +## Features + +- Base package for RCommon ASP.NET Core web integration +- Targets .NET 8, .NET 9, and .NET 10 +- Designed to host shared web utilities such as custom attributes, middleware, and extensions + +## Installation + +```shell +dotnet add package RCommon.Web +``` + +## Usage + +Add the package reference to your ASP.NET Core project: + +```xml + +``` + +## Documentation + +For full documentation, visit [rcommon.com](https://rcommon.com). + +## Related Packages + +- [RCommon.Authorization.Web](https://www.nuget.org/packages/RCommon.Authorization.Web) - Swagger/OpenAPI authorization filters for ASP.NET Core +- [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.Wolverine/IWolverineEventHandlingBuilder.cs b/Src/RCommon.Wolverine/IWolverineEventHandlingBuilder.cs index b50ce1f7..5867b3bd 100644 --- a/Src/RCommon.Wolverine/IWolverineEventHandlingBuilder.cs +++ b/Src/RCommon.Wolverine/IWolverineEventHandlingBuilder.cs @@ -3,6 +3,10 @@ namespace RCommon.Wolverine { + /// + /// Builder interface for configuring Wolverine-based event handling within the RCommon framework. + /// Extends to provide Wolverine-specific event subscription capabilities. + /// public interface IWolverineEventHandlingBuilder : IEventHandlingBuilder { } diff --git a/Src/RCommon.Wolverine/Producers/PublishWithWolverineEventProducer.cs b/Src/RCommon.Wolverine/Producers/PublishWithWolverineEventProducer.cs index 314a1c12..661e7d09 100644 --- a/Src/RCommon.Wolverine/Producers/PublishWithWolverineEventProducer.cs +++ b/Src/RCommon.Wolverine/Producers/PublishWithWolverineEventProducer.cs @@ -12,6 +12,14 @@ namespace RCommon.Wolverine.Producers { + /// + /// An implementation that publishes events to all subscribed handlers + /// using Wolverine's method (fan-out pattern). + /// + /// + /// Use this producer when you want an event to be delivered to all Wolverine handlers subscribed + /// to the event type. For point-to-point delivery, use instead. + /// public class PublishWithWolverineEventProducer : IEventProducer { private readonly IMessageBus _messageBus; @@ -19,6 +27,13 @@ public class PublishWithWolverineEventProducer : IEventProducer private readonly IServiceProvider _serviceProvider; private readonly EventSubscriptionManager _subscriptionManager; + /// + /// Initializes a new instance of . + /// + /// The Wolverine message bus used to publish events. + /// Logger for diagnostic output. + /// Service provider for creating scoped services during event production. + /// Manages event-to-producer subscriptions for routing decisions. public PublishWithWolverineEventProducer(IMessageBus messageBus, ILogger logger, IServiceProvider serviceProvider, EventSubscriptionManager subscriptionManager) { @@ -28,18 +43,22 @@ public PublishWithWolverineEventProducer(IMessageBus messageBus, ILogger public async Task ProduceEventAsync(T @event, CancellationToken cancellationToken = default) where T : ISerializableEvent { try { Guard.IsNotNull(@event, nameof(@event)); + // Check if this event type is subscribed to this producer; skip if not routed here if (!_subscriptionManager.ShouldProduceEvent(this.GetType(), typeof(T))) { _logger.LogDebug("{0} skipping event {1} - not subscribed to this producer", new object[] { this.GetGenericTypeName(), typeof(T).Name }); return; } + + // Create a scoped service context for the publish operation using (IServiceScope scope = _serviceProvider.CreateScope()) { if (_logger.IsEnabled(LogLevel.Information)) diff --git a/Src/RCommon.Wolverine/Producers/SendWithWolverineEventProducer.cs b/Src/RCommon.Wolverine/Producers/SendWithWolverineEventProducer.cs index 7019433d..ee35b6bd 100644 --- a/Src/RCommon.Wolverine/Producers/SendWithWolverineEventProducer.cs +++ b/Src/RCommon.Wolverine/Producers/SendWithWolverineEventProducer.cs @@ -11,6 +11,14 @@ namespace RCommon.Wolverine.Producers { + /// + /// An implementation that sends events to a single handler endpoint + /// using Wolverine's method (point-to-point pattern). + /// + /// + /// Use this producer for command-style messaging where only one handler should process the event. + /// For fan-out delivery to all handlers, use instead. + /// public class SendWithWolverineEventProducer : IEventProducer { private readonly IMessageBus _messageBus; @@ -18,6 +26,13 @@ public class SendWithWolverineEventProducer : IEventProducer private readonly IServiceProvider _serviceProvider; private readonly EventSubscriptionManager _subscriptionManager; + /// + /// Initializes a new instance of . + /// + /// The Wolverine message bus used to send events. + /// Logger for diagnostic output. + /// Service provider for creating scoped services during event production. + /// Manages event-to-producer subscriptions for routing decisions. public SendWithWolverineEventProducer(IMessageBus messageBus, ILogger logger, IServiceProvider serviceProvider, EventSubscriptionManager subscriptionManager) { @@ -27,18 +42,22 @@ public SendWithWolverineEventProducer(IMessageBus messageBus, ILogger public async Task ProduceEventAsync(T @event, CancellationToken cancellationToken = default) where T : ISerializableEvent { try { Guard.IsNotNull(@event, nameof(@event)); + // Check if this event type is subscribed to this producer; skip if not routed here if (!_subscriptionManager.ShouldProduceEvent(this.GetType(), typeof(T))) { _logger.LogDebug("{0} skipping event {1} - not subscribed to this producer", new object[] { this.GetGenericTypeName(), typeof(T).Name }); return; } + + // Create a scoped service context for the send operation using (IServiceScope scope = _serviceProvider.CreateScope()) { if (_logger.IsEnabled(LogLevel.Information)) diff --git a/Src/RCommon.Wolverine/README.md b/Src/RCommon.Wolverine/README.md index 5f00b68d..9e934672 100644 --- a/Src/RCommon.Wolverine/README.md +++ b/Src/RCommon.Wolverine/README.md @@ -1,3 +1,86 @@ - # RCommon.Wolverine +# RCommon.Wolverine -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 +Integrates [Wolverine](https://wolverinefx.net/) messaging with RCommon's event handling system, allowing you to produce and consume events through Wolverine's `IMessageBus` while programming against RCommon's `IEventProducer` and `ISubscriber` abstractions. + +## Features + +- Publish events to all subscribed handlers using Wolverine's fan-out (publish) semantics +- Send events to a single handler endpoint using Wolverine's point-to-point (send) semantics +- Bridge Wolverine message handlers to RCommon's `ISubscriber` abstraction for handler portability +- Event subscription routing ensures events are delivered only to their configured producers +- Factory delegate support for subscriber registration + +## Installation + +```shell +dotnet add package RCommon.Wolverine +``` + +## Usage + +```csharp +using RCommon; +using RCommon.Wolverine; + +builder.Services.AddRCommon() + .WithEventHandling(eventHandling => + { + // Register subscribers that bridge Wolverine to RCommon + eventHandling.AddSubscriber(); + + // Or register with a factory delegate + eventHandling.AddSubscriber( + sp => new OrderShippedEventHandler(sp.GetRequiredService>())); + }); + +// Configure Wolverine transports separately via the host builder +builder.Host.UseWolverine(opts => +{ + opts.ListenToRabbitQueue("orders"); + opts.PublishMessage().ToRabbitExchange("orders"); +}); +``` + +Produce events from application code: + +```csharp +public class OrderService +{ + private readonly IEventProducer _eventProducer; + + public OrderService(IEventProducer eventProducer) + { + _eventProducer = eventProducer; + } + + public async Task CreateOrderAsync(Order order) + { + // This publishes via Wolverine's IMessageBus to all subscribed handlers + await _eventProducer.ProduceEventAsync(new OrderCreatedEvent(order)); + } +} +``` + +## Key Types + +| Type | Description | +|------|-------------| +| `PublishWithWolverineEventProducer` | `IEventProducer` that publishes events to all handlers via `IMessageBus.PublishAsync` (fan-out) | +| `SendWithWolverineEventProducer` | `IEventProducer` that sends events to a single endpoint via `IMessageBus.SendAsync` (point-to-point) | +| `WolverineEventHandler` | Wolverine `IWolverineHandler` that delegates to an RCommon `ISubscriber` | +| `IWolverineEventHandlingBuilder` | Builder interface for configuring Wolverine event handling within RCommon | +| `WolverineEventHandlingBuilder` | Default builder implementation for configuring Wolverine event handling | + +## Documentation + +For full documentation, visit [rcommon.com](https://rcommon.com). + +## Related Packages + +- [RCommon.Core](https://www.nuget.org/packages/RCommon.Core) - Core abstractions including `IEventProducer` and `ISubscriber` +- [RCommon.MassTransit](https://www.nuget.org/packages/RCommon.MassTransit) - MassTransit integration for distributed messaging +- [RCommon.MediatR](https://www.nuget.org/packages/RCommon.MediatR) - MediatR integration for in-process event handling and mediator pattern + +## License + +Licensed under the [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0). diff --git a/Src/RCommon.Wolverine/Subscribers/IWolverineEventHandler.cs b/Src/RCommon.Wolverine/Subscribers/IWolverineEventHandler.cs index 562f8182..95ae3a54 100644 --- a/Src/RCommon.Wolverine/Subscribers/IWolverineEventHandler.cs +++ b/Src/RCommon.Wolverine/Subscribers/IWolverineEventHandler.cs @@ -8,13 +8,26 @@ namespace RCommon.MassTransit.Subscribers { + /// + /// Non-generic marker interface for Wolverine event handlers within the RCommon framework. + /// public interface IWolverineEventHandler { } + /// + /// Generic interface for Wolverine event handlers that process a specific distributed event type. + /// + /// The distributed event type to handle. Must implement . public interface IWolverineEventHandler : IWolverineEventHandler where TDistributedEvent : class, ISerializableEvent { + /// + /// Handles the distributed event asynchronously. + /// + /// The event to handle. + /// Optional cancellation token. + /// A task representing the asynchronous operation. Task HandleAsync(TDistributedEvent distributedEvent, CancellationToken cancellationToken = default); } } diff --git a/Src/RCommon.Wolverine/Subscribers/WolverineEventHandler.cs b/Src/RCommon.Wolverine/Subscribers/WolverineEventHandler.cs index 48d55d02..1601a21e 100644 --- a/Src/RCommon.Wolverine/Subscribers/WolverineEventHandler.cs +++ b/Src/RCommon.Wolverine/Subscribers/WolverineEventHandler.cs @@ -10,18 +10,29 @@ namespace RCommon.MassTransit.Subscribers { + /// + /// Wolverine handler that bridges Wolverine message handling to the RCommon abstraction. + /// Implements both and Wolverine's . + /// + /// The event type to handle. Must implement . public class WolverineEventHandler : IWolverineEventHandler, IWolverineHandler where TEvent : class, ISerializableEvent { private readonly ISubscriber _subscriber; private readonly ILogger> _logger; + /// + /// Initializes a new instance of . + /// + /// The RCommon subscriber that handles the event. + /// Logger for diagnostic output. public WolverineEventHandler(ISubscriber subscriber, ILogger> logger) { _subscriber = subscriber ?? throw new ArgumentNullException(nameof(subscriber)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } + /// public async Task HandleAsync(TEvent @event, CancellationToken cancellationToken = default) { _logger.LogDebug("{0} handling event {1}", new object[] { this.GetGenericTypeName(), @event.GetGenericTypeName() }); diff --git a/Src/RCommon.Wolverine/WolverineEventHandlingBuilder.cs b/Src/RCommon.Wolverine/WolverineEventHandlingBuilder.cs index 470b1e37..104107b1 100644 --- a/Src/RCommon.Wolverine/WolverineEventHandlingBuilder.cs +++ b/Src/RCommon.Wolverine/WolverineEventHandlingBuilder.cs @@ -8,13 +8,22 @@ namespace RCommon.Wolverine { + /// + /// Default implementation of that configures + /// Wolverine event handling services through the RCommon builder pipeline. + /// public class WolverineEventHandlingBuilder : IWolverineEventHandlingBuilder { + /// + /// Initializes a new instance of using the provided RCommon builder. + /// + /// The whose service collection is used for dependency registration. public WolverineEventHandlingBuilder(IRCommonBuilder builder) { Services = builder.Services; } + /// public IServiceCollection Services { get; } } } diff --git a/Src/RCommon.Wolverine/WolverineEventHandlingBuilderExtensions.cs b/Src/RCommon.Wolverine/WolverineEventHandlingBuilderExtensions.cs index 2ade3d9e..b1b2721a 100644 --- a/Src/RCommon.Wolverine/WolverineEventHandlingBuilderExtensions.cs +++ b/Src/RCommon.Wolverine/WolverineEventHandlingBuilderExtensions.cs @@ -14,10 +14,18 @@ namespace RCommon { + /// + /// Extension methods for configuring Wolverine event handling within the RCommon builder pipeline. + /// public static class WolverineEventHandlingBuilderExtensions { - + /// + /// Registers a subscriber for a specific event type and records the event-to-producer subscription for routing. + /// + /// The event type to subscribe to. + /// The subscriber implementation that handles the event. + /// The Wolverine event handling builder. public static void AddSubscriber(this IWolverineEventHandlingBuilder builder) where TEvent : class where TEventHandler : class, ISubscriber @@ -29,6 +37,14 @@ public static void AddSubscriber(this IWolverineEventHand subscriptionManager?.AddSubscription(builder.GetType(), typeof(TEvent)); } + /// + /// Registers a subscriber for a specific event type using a factory delegate and records + /// the event-to-producer subscription for routing. + /// + /// The event type to subscribe to. + /// The subscriber implementation that handles the event. + /// The Wolverine event handling builder. + /// Factory delegate to create the subscriber from the service provider. public static void AddSubscriber(this IWolverineEventHandlingBuilder builder, Func getSubscriber) where TEvent : class where TEventHandler : class, ISubscriber diff --git a/Tests/RCommon.ApplicationServices.Tests/RCommon.ApplicationServices.Tests.csproj b/Tests/RCommon.ApplicationServices.Tests/RCommon.ApplicationServices.Tests.csproj index 5759578b..13fb3baa 100644 --- a/Tests/RCommon.ApplicationServices.Tests/RCommon.ApplicationServices.Tests.csproj +++ b/Tests/RCommon.ApplicationServices.Tests/RCommon.ApplicationServices.Tests.csproj @@ -10,22 +10,10 @@ - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - diff --git a/Tests/RCommon.Entities.Tests/EntityNotFoundExceptionTests.cs b/Tests/RCommon.Entities.Tests/EntityNotFoundExceptionTests.cs index b5c09d86..89b16495 100644 --- a/Tests/RCommon.Entities.Tests/EntityNotFoundExceptionTests.cs +++ b/Tests/RCommon.Entities.Tests/EntityNotFoundExceptionTests.cs @@ -298,7 +298,7 @@ public void Constructor_WithMessageAndNullInnerException_SetsOnlyMessage() var message = _faker.Lorem.Sentence(); // Act - var exception = new EntityNotFoundException(message, null); + var exception = new EntityNotFoundException(message, null!); // Assert exception.Message.Should().Be(message); diff --git a/Tests/RCommon.Persistence.Tests/DefaultDataStoreOptionsTests.cs b/Tests/RCommon.Persistence.Tests/DefaultDataStoreOptionsTests.cs index 2635977b..05de63e4 100644 --- a/Tests/RCommon.Persistence.Tests/DefaultDataStoreOptionsTests.cs +++ b/Tests/RCommon.Persistence.Tests/DefaultDataStoreOptionsTests.cs @@ -57,7 +57,7 @@ public void DefaultDataStoreName_CanBeSetToNull() options.DefaultDataStoreName = "SomeName"; // Act - options.DefaultDataStoreName = null; + options.DefaultDataStoreName = null!; // Assert options.DefaultDataStoreName.Should().BeNull(); diff --git a/Tests/RCommon.Persistence.Tests/RCommon.Persistence.Tests.csproj b/Tests/RCommon.Persistence.Tests/RCommon.Persistence.Tests.csproj index 1d8ef79a..7afb094b 100644 --- a/Tests/RCommon.Persistence.Tests/RCommon.Persistence.Tests.csproj +++ b/Tests/RCommon.Persistence.Tests/RCommon.Persistence.Tests.csproj @@ -10,23 +10,11 @@
- - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - diff --git a/Tests/RCommon.Persistence.Tests/RDbConnectionOptionsTests.cs b/Tests/RCommon.Persistence.Tests/RDbConnectionOptionsTests.cs index d098ef3e..fddcefe0 100644 --- a/Tests/RCommon.Persistence.Tests/RDbConnectionOptionsTests.cs +++ b/Tests/RCommon.Persistence.Tests/RDbConnectionOptionsTests.cs @@ -86,7 +86,7 @@ public void DbFactory_CanBeSetToNull() options.DbFactory = SqlClientFactory.Instance; // Act - options.DbFactory = null; + options.DbFactory = null!; // Assert options.DbFactory.Should().BeNull(); @@ -100,7 +100,7 @@ public void ConnectionString_CanBeSetToNull() options.ConnectionString = "Server=localhost;"; // Act - options.ConnectionString = null; + options.ConnectionString = null!; // Assert options.ConnectionString.Should().BeNull(); diff --git a/Tests/RCommon.Persistence.Tests/RDbConnectionTests.cs b/Tests/RCommon.Persistence.Tests/RDbConnectionTests.cs index 8e797dda..68881c6a 100644 --- a/Tests/RCommon.Persistence.Tests/RDbConnectionTests.cs +++ b/Tests/RCommon.Persistence.Tests/RDbConnectionTests.cs @@ -62,7 +62,7 @@ public void GetDbConnection_WhenOptionsNotConfigured_ThrowsRDbConnectionExceptio public void GetDbConnection_WhenDbFactoryNotConfigured_ThrowsRDbConnectionException() { // Arrange - _options.DbFactory = null; + _options.DbFactory = null!; _options.ConnectionString = "Server=localhost;Database=Test;"; var connection = new RDbConnection(_mockOptions.Object); @@ -80,7 +80,7 @@ public void GetDbConnection_WhenConnectionStringNotConfigured_ThrowsRDbConnectio { // Arrange _options.DbFactory = SqlClientFactory.Instance; - _options.ConnectionString = null; + _options.ConnectionString = null!; var connection = new RDbConnection(_mockOptions.Object); diff --git a/Tests/RCommon.TestBase.Data/RCommon.TestBase.Data.csproj b/Tests/RCommon.TestBase.Data/RCommon.TestBase.Data.csproj index 1db69f1e..8eae7763 100644 --- a/Tests/RCommon.TestBase.Data/RCommon.TestBase.Data.csproj +++ b/Tests/RCommon.TestBase.Data/RCommon.TestBase.Data.csproj @@ -7,7 +7,6 @@ - all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -20,10 +19,8 @@ - - diff --git a/Tests/RCommon.TestBase.Data/TestRepository.cs b/Tests/RCommon.TestBase.Data/TestRepository.cs index e56685b9..8141ea0d 100644 --- a/Tests/RCommon.TestBase.Data/TestRepository.cs +++ b/Tests/RCommon.TestBase.Data/TestRepository.cs @@ -18,7 +18,7 @@ public class TestRepository { readonly DbContext _context; readonly IList> _entityDeleteActions; - private IDataStoreFactory _dataStoreFactory; + private IDataStoreFactory _dataStoreFactory = default!; public TestRepository(DbContext context) { @@ -28,7 +28,7 @@ public TestRepository(DbContext context) public TestRepository(IServiceProvider serviceProvider) { - _dataStoreFactory = serviceProvider.GetService(); + _dataStoreFactory = serviceProvider.GetRequiredService(); _context = _dataStoreFactory.Resolve("TestDbContext"); _entityDeleteActions = new List>(); } diff --git a/Tests/RCommon.TestBase/Entities/Customer.cs b/Tests/RCommon.TestBase/Entities/Customer.cs index 0a69b242..6ce844c6 100644 --- a/Tests/RCommon.TestBase/Entities/Customer.cs +++ b/Tests/RCommon.TestBase/Entities/Customer.cs @@ -13,13 +13,13 @@ namespace RCommon.TestBase.Entities // Customers public partial class Customer : BusinessEntity { - public string StreetAddress1 { get; set; } // StreetAddress1 (length: 255) - public string StreetAddress2 { get; set; } // StreetAddress2 (length: 255) - public string City { get; set; } // City (length: 255) - public string State { get; set; } // State (length: 255) - public string ZipCode { get; set; } // ZipCode (length: 255) - public string FirstName { get; set; } // FirstName (length: 255) - public string LastName { get; set; } // LastName (length: 255) + public string StreetAddress1 { get; set; } = string.Empty; // StreetAddress1 (length: 255) + public string StreetAddress2 { get; set; } = string.Empty; // StreetAddress2 (length: 255) + public string City { get; set; } = string.Empty; // City (length: 255) + public string State { get; set; } = string.Empty; // State (length: 255) + public string ZipCode { get; set; } = string.Empty; // ZipCode (length: 255) + public string FirstName { get; set; } = string.Empty; // FirstName (length: 255) + public string LastName { get; set; } = string.Empty; // LastName (length: 255) // Reverse navigation diff --git a/Tests/RCommon.TestBase/Entities/Department.cs b/Tests/RCommon.TestBase/Entities/Department.cs index 42974c34..45d4068d 100644 --- a/Tests/RCommon.TestBase/Entities/Department.cs +++ b/Tests/RCommon.TestBase/Entities/Department.cs @@ -11,7 +11,7 @@ namespace RCommon.TestBase.Entities // Departments public partial class Department : BusinessEntity { - public string Name { get; set; } // Name (length: 255) + public string Name { get; set; } = string.Empty; // Name (length: 255) // Reverse navigation diff --git a/Tests/RCommon.TestBase/Entities/MonthlySalesSummary.cs b/Tests/RCommon.TestBase/Entities/MonthlySalesSummary.cs index 014c3841..50be91dc 100644 --- a/Tests/RCommon.TestBase/Entities/MonthlySalesSummary.cs +++ b/Tests/RCommon.TestBase/Entities/MonthlySalesSummary.cs @@ -15,9 +15,9 @@ public partial class MonthlySalesSummary : BusinessEntity public int Month { get; set; } // Month (Primary key) public int SalesPersonId { get; set; } // SalesPersonId (Primary key) public decimal? Amount { get; set; } // Amount - public string Currency { get; set; } // Currency (length: 255) - public string SalesPersonFirstName { get; set; } // SalesPersonFirstName (length: 255) - public string SalesPersonLastName { get; set; } // SalesPersonLastName (length: 255) + public string Currency { get; set; } = string.Empty; // Currency (length: 255) + public string SalesPersonFirstName { get; set; } = string.Empty; // SalesPersonFirstName (length: 255) + public string SalesPersonLastName { get; set; } = string.Empty; // SalesPersonLastName (length: 255) public MonthlySalesSummary() { diff --git a/Tests/RCommon.TestBase/Entities/Order.cs b/Tests/RCommon.TestBase/Entities/Order.cs index b30ab36c..978ff537 100644 --- a/Tests/RCommon.TestBase/Entities/Order.cs +++ b/Tests/RCommon.TestBase/Entities/Order.cs @@ -27,7 +27,7 @@ public partial class Order : BusinessEntity /// /// Parent Customer pointed by [Orders].([CustomerId]) (FK_Customer_Orders) /// - public virtual Customer Customer { get; set; } // FK_Customer_Orders + public virtual Customer? Customer { get; set; } // FK_Customer_Orders public Order() { diff --git a/Tests/RCommon.TestBase/Entities/OrderItem.cs b/Tests/RCommon.TestBase/Entities/OrderItem.cs index 7249cbdf..c8cd6ba2 100644 --- a/Tests/RCommon.TestBase/Entities/OrderItem.cs +++ b/Tests/RCommon.TestBase/Entities/OrderItem.cs @@ -14,7 +14,7 @@ public partial class OrderItem : BusinessEntity public int OrderItemId { get; set; } // OrderItemID (Primary key) public decimal? Price { get; set; } // Price public int? Quantity { get; set; } // Quantity - public string Store { get; set; } // Store (length: 255) + public string Store { get; set; } = string.Empty; // Store (length: 255) public int? ProductId { get; set; } // ProductId public int? OrderId { get; set; } // OrderId @@ -23,12 +23,12 @@ public partial class OrderItem : BusinessEntity /// /// Parent Order pointed by [OrderItems].([OrderId]) (FK_Orders_OrderItems) /// - public virtual Order Order { get; set; } // FK_Orders_OrderItems + public virtual Order? Order { get; set; } // FK_Orders_OrderItems /// /// Parent Product pointed by [OrderItems].([ProductId]) (FK_OrderItems_Product) /// - public virtual Product Product { get; set; } // FK_OrderItems_Product + public virtual Product? Product { get; set; } // FK_OrderItems_Product public OrderItem() { diff --git a/Tests/RCommon.TestBase/Entities/Product.cs b/Tests/RCommon.TestBase/Entities/Product.cs index 55ac0551..9eca14b0 100644 --- a/Tests/RCommon.TestBase/Entities/Product.cs +++ b/Tests/RCommon.TestBase/Entities/Product.cs @@ -12,8 +12,8 @@ namespace RCommon.TestBase.Entities public partial class Product : BusinessEntity { public int ProductId { get; set; } // ProductID (Primary key) - public string Name { get; set; } // Name (length: 255) - public string Description { get; set; } // Description (length: 255) + public string Name { get; set; } = string.Empty; // Name (length: 255) + public string Description { get; set; } = string.Empty; // Description (length: 255) // Reverse navigation diff --git a/Tests/RCommon.TestBase/Entities/SalesPerson.cs b/Tests/RCommon.TestBase/Entities/SalesPerson.cs index 652184a8..bb29dbd7 100644 --- a/Tests/RCommon.TestBase/Entities/SalesPerson.cs +++ b/Tests/RCommon.TestBase/Entities/SalesPerson.cs @@ -11,8 +11,8 @@ namespace RCommon.TestBase.Entities // SalesPerson public partial class SalesPerson : BusinessEntity { - public string FirstName { get; set; } // FirstName (length: 255) - public string LastName { get; set; } // LastName (length: 255) + public string FirstName { get; set; } = string.Empty; // FirstName (length: 255) + public string LastName { get; set; } = string.Empty; // LastName (length: 255) public float? SalesQuota { get; set; } // SalesQuota public decimal? SalesYtd { get; set; } // SalesYTD public int? DepartmentId { get; set; } // DepartmentId @@ -23,12 +23,12 @@ public partial class SalesPerson : BusinessEntity /// /// Parent Department pointed by [SalesPerson].([DepartmentId]) (FK74214A90E25FF6) /// - public virtual Department Department { get; set; } // FK74214A90E25FF6 + public virtual Department? Department { get; set; } // FK74214A90E25FF6 /// /// Parent SalesTerritory pointed by [SalesPerson].([TerritoryId]) (FK74214A90B23DB0A3) /// - public virtual SalesTerritory SalesTerritory { get; set; } // FK74214A90B23DB0A3 + public virtual SalesTerritory? SalesTerritory { get; set; } // FK74214A90B23DB0A3 public SalesPerson() { diff --git a/Tests/RCommon.TestBase/Entities/SalesTerritory.cs b/Tests/RCommon.TestBase/Entities/SalesTerritory.cs index 9759c19e..22669e97 100644 --- a/Tests/RCommon.TestBase/Entities/SalesTerritory.cs +++ b/Tests/RCommon.TestBase/Entities/SalesTerritory.cs @@ -11,8 +11,8 @@ namespace RCommon.TestBase.Entities // SalesTerritory public partial class SalesTerritory : BusinessEntity { - public string Name { get; set; } // Name (length: 255) - public string Description { get; set; } // Description (length: 255) + public string Name { get; set; } = string.Empty; // Name (length: 255) + public string Description { get; set; } = string.Empty; // Description (length: 255) // Reverse navigation diff --git a/Tests/RCommon.TestBase/RCommon.TestBase.csproj b/Tests/RCommon.TestBase/RCommon.TestBase.csproj index 3dffe30b..1aedafb0 100644 --- a/Tests/RCommon.TestBase/RCommon.TestBase.csproj +++ b/Tests/RCommon.TestBase/RCommon.TestBase.csproj @@ -7,16 +7,13 @@ - - - diff --git a/Tests/RCommon.TestBase/TestBootstrapper.cs b/Tests/RCommon.TestBase/TestBootstrapper.cs index ea2e85d0..53f5d0fb 100644 --- a/Tests/RCommon.TestBase/TestBootstrapper.cs +++ b/Tests/RCommon.TestBase/TestBootstrapper.cs @@ -18,8 +18,8 @@ namespace RCommon.TestBase { public abstract class TestBootstrapper { - private ServiceProvider _serviceProvider; - private Microsoft.Extensions.Logging.ILogger _logger; + private ServiceProvider _serviceProvider = default!; + private Microsoft.Extensions.Logging.ILogger _logger = default!; public TestBootstrapper() { @@ -67,7 +67,7 @@ protected Mock CreateMockHttpClient(HttpResponseMessage mock return mockFactory; } - public IConfigurationRoot Configuration { get; private set; } + public IConfigurationRoot Configuration { get; private set; } = default!; public ServiceProvider ServiceProvider { get => _serviceProvider; set => _serviceProvider = value; } public Microsoft.Extensions.Logging.ILogger Logger { get => _logger; set => _logger = value; } }