diff --git a/.editorconfig b/.editorconfig index 82d0bab..3d61970 100644 --- a/.editorconfig +++ b/.editorconfig @@ -15,17 +15,41 @@ indent_size = 4 indent_style = space # New line preferences -end_of_line = unset +end_of_line = crlf insert_final_newline = false +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:warning +dotnet_style_prefer_auto_properties = true:silent +dotnet_style_object_initializer = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_prefer_simplified_boolean_expressions = true:suggestion +dotnet_style_prefer_conditional_expression_over_assignment = true:silent +dotnet_style_prefer_conditional_expression_over_return = true:silent +dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_prefer_inferred_tuple_names = true:suggestion +dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion +dotnet_style_prefer_compound_assignment = true:suggestion +dotnet_style_prefer_simplified_interpolation = true:suggestion +dotnet_style_prefer_collection_expression = when_types_loosely_match:suggestion +dotnet_style_namespace_match_folder = true:suggestion +dotnet_style_operator_placement_when_wrapping = beginning_of_line +dotnet_diagnostic.CA1304.severity = warning +dotnet_diagnostic.CA1707.severity = none +dotnet_diagnostic.CA1829.severity = warning #### Build files #### # Solution files -[*.{sln,slnx}] +[*.{sln}] tab_width = 4 indent_size = 4 indent_style = tab +# Modern solution files +[*.{slnx}] +indent_size = 2 + # Configuration files [*.{json,xml,yml,config,runsettings}] indent_size = 2 @@ -123,7 +147,7 @@ csharp_prefer_braces = true:suggestion csharp_using_directive_placement = outside_namespace:warning csharp_style_namespace_declarations = file_scoped:warning csharp_style_unused_value_assignment_preference = discard_variable:warning -csharp_style_unused_value_expression_statement_preference = discard_variable:warning +csharp_style_unused_value_expression_statement_preference = discard_variable:silent csharp_style_allow_blank_lines_between_consecutive_braces_experimental = false:warning # Expression-level preferences @@ -225,9 +249,9 @@ dotnet_naming_style.prefix_interface_interface_with_i.required_prefix # Naming Rules # Async -dotnet_naming_rule.async_methods_end_in_async.severity = silent +dotnet_naming_rule.async_methods_end_in_async.severity = silent dotnet_naming_rule.async_methods_end_in_async.symbols = any_async_methods -dotnet_naming_rule.async_methods_end_in_async.style = end_in_async +dotnet_naming_rule.async_methods_end_in_async.style = end_in_async dotnet_naming_symbols.any_async_methods.applicable_kinds = method dotnet_naming_symbols.any_async_methods.applicable_accessibilities = * @@ -237,45 +261,45 @@ dotnet_naming_style.end_in_async.required_suffix dotnet_naming_style.end_in_async.capitalization = pascal_case # Constant fields must be PascalCase -dotnet_naming_rule.constant_fields_must_be_pascal_case.severity = silent +dotnet_naming_rule.constant_fields_must_be_pascal_case.severity = silent dotnet_naming_rule.constant_fields_must_be_pascal_case.symbols = constant_fields -dotnet_naming_rule.constant_fields_must_be_pascal_case.style = pascal_case +dotnet_naming_rule.constant_fields_must_be_pascal_case.style = pascal_case # Public, internal and protected readonly fields must be PascalCase -dotnet_naming_rule.non_private_readonly_fields_must_be_pascal_case.severity = silent +dotnet_naming_rule.non_private_readonly_fields_must_be_pascal_case.severity = silent dotnet_naming_rule.non_private_readonly_fields_must_be_pascal_case.symbols = non_private_readonly_fields -dotnet_naming_rule.non_private_readonly_fields_must_be_pascal_case.style = pascal_case +dotnet_naming_rule.non_private_readonly_fields_must_be_pascal_case.style = pascal_case # Static readonly fields must be PascalCase -dotnet_naming_rule.static_readonly_fields_must_be_pascal_case.severity = silent +dotnet_naming_rule.static_readonly_fields_must_be_pascal_case.severity = silent dotnet_naming_rule.static_readonly_fields_must_be_pascal_case.symbols = static_readonly_fields -dotnet_naming_rule.static_readonly_fields_must_be_pascal_case.style = pascal_case +dotnet_naming_rule.static_readonly_fields_must_be_pascal_case.style = pascal_case # Private readonly fields must be camelCase -dotnet_naming_rule.private_readonly_fields_must_be_camel_case.severity = silent +dotnet_naming_rule.private_readonly_fields_must_be_camel_case.severity = silent dotnet_naming_rule.private_readonly_fields_must_be_camel_case.symbols = private_readonly_fields -dotnet_naming_rule.private_readonly_fields_must_be_camel_case.style = camel_case +dotnet_naming_rule.private_readonly_fields_must_be_camel_case.style = camel_case # Public and internal fields must be PascalCase -dotnet_naming_rule.public_internal_protected_fields_must_be_pascal_case.severity = silent +dotnet_naming_rule.public_internal_protected_fields_must_be_pascal_case.severity = silent dotnet_naming_rule.public_internal_protected_fields_must_be_pascal_case.symbols = public_internal_protected_fields -dotnet_naming_rule.public_internal_protected_fields_must_be_pascal_case.style = pascal_case +dotnet_naming_rule.public_internal_protected_fields_must_be_pascal_case.style = pascal_case # Private and protected fields must be camelCase -dotnet_naming_rule.private_fields_must_be_camel_case.severity = silent +dotnet_naming_rule.private_fields_must_be_camel_case.severity = silent dotnet_naming_rule.private_fields_must_be_camel_case.symbols = private_protected_fields -dotnet_naming_rule.private_fields_must_be_camel_case.style = prefix_private_field_with_underscore +dotnet_naming_rule.private_fields_must_be_camel_case.style = prefix_private_field_with_underscore # Public members must be capitalized -dotnet_naming_rule.public_members_must_be_capitalized.severity = silent +dotnet_naming_rule.public_members_must_be_capitalized.severity = silent dotnet_naming_rule.public_members_must_be_capitalized.symbols = public_symbols -dotnet_naming_rule.public_members_must_be_capitalized.style = first_upper +dotnet_naming_rule.public_members_must_be_capitalized.style = first_upper # Parameters must be camelCase -dotnet_naming_rule.parameters_must_be_camel_case.severity = silent +dotnet_naming_rule.parameters_must_be_camel_case.severity = silent dotnet_naming_rule.parameters_must_be_camel_case.symbols = parameters -dotnet_naming_rule.parameters_must_be_camel_case.style = camel_case +dotnet_naming_rule.parameters_must_be_camel_case.style = camel_case # Class, struct, enum and delegates must be PascalCase -dotnet_naming_rule.non_interface_types_must_be_pascal_case.severity = silent +dotnet_naming_rule.non_interface_types_must_be_pascal_case.severity = silent dotnet_naming_rule.non_interface_types_must_be_pascal_case.symbols = non_interface_types -dotnet_naming_rule.non_interface_types_must_be_pascal_case.style = pascal_case +dotnet_naming_rule.non_interface_types_must_be_pascal_case.style = pascal_case # Interfaces must be PascalCase and start with an 'I' -dotnet_naming_rule.interface_types_must_be_prefixed_with_i.severity = silent +dotnet_naming_rule.interface_types_must_be_prefixed_with_i.severity = silent dotnet_naming_rule.interface_types_must_be_prefixed_with_i.symbols = interface_types -dotnet_naming_rule.interface_types_must_be_prefixed_with_i.style = prefix_interface_interface_with_i +dotnet_naming_rule.interface_types_must_be_prefixed_with_i.style = prefix_interface_interface_with_i # prefix_private_field_with_underscore - Private fields must be prefixed with _ dotnet_naming_style.prefix_private_field_with_underscore.capitalization = camel_case dotnet_naming_style.prefix_private_field_with_underscore.required_prefix = _ @@ -344,18 +368,36 @@ dotnet_diagnostic.CA2238.severity = warning dotnet_diagnostic.CA2240.severity = warning dotnet_diagnostic.CA2241.severity = warning dotnet_diagnostic.CA2242.severity = warning +dotnet_diagnostic.CS9107.severity = suggestion +dotnet_diagnostic.CS0612.severity = suggestion + +# Async Fixer + +dotnet_diagnostic.AsyncFixer01.severity = silent + +# ErrorProne + +dotnet_diagnostic.EPC12.severity = suggestion +dotnet_diagnostic.EPS05.severity = suggestion + +# IDispossable + +dotnet_diagnostic.IDISP001.severity = warning +dotnet_diagnostic.IDISP017.severity = none +dotnet_diagnostic.IDISP013.severity = suggestion +dotnet_diagnostic.IDISP004.severity = warning +dotnet_diagnostic.IDISP001.severity = warning +dotnet_diagnostic.IDISP026.severity = suggestion # Require file header OR A source file contains a header that does not match the required text dotnet_diagnostic.IDE0073.severity = error # StyleCop Code Analysis -# Closing parenthesis should be spaced correctly: "foo()!" -dotnet_diagnostic.SA1009.severity = none - -# Hide warnings when using the new() expression from C# 9. -dotnet_diagnostic.SA1000.severity = none - +dotnet_diagnostic.SA1005.severity = suggestion # Single line comments should begin with a space +dotnet_diagnostic.SA0001.severity = none +dotnet_diagnostic.SA1009.severity = none # Closing parenthesis should be spaced correctly: "foo()!" +dotnet_diagnostic.SA1000.severity = none # Hide warnings when using the new() expression from C# 9. dotnet_diagnostic.SA1011.severity = none dotnet_diagnostic.SA1101.severity = none @@ -382,7 +424,7 @@ dotnet_diagnostic.SA1602.severity = none dotnet_diagnostic.SA1611.severity = none # DocumentationTextMustEndWithAPeriod: Let's enable this rule back when we shift to WinUI3 (v8.x). If we do it now, it would mean more than 400 file changes. -dotnet_diagnostic.SA1629.severity = none +dotnet_diagnostic.SA1629.severity = warning dotnet_diagnostic.SA1633.severity = none dotnet_diagnostic.SA1634.severity = none @@ -409,7 +451,33 @@ dotnet_diagnostic.WPF0070.severity = none # Add default field to converter # Suppress some IDE warnings dotnet_diagnostic.IDE0290.severity = none # Use primary constructor dotnet_diagnostic.CS1591.severity = none # Missing XML comment for publicly visible type or member - # 15000+ warnings in the solution + dotnet_diagnostic.CA1510.severity = none # Use ArgumentNullException throw helper - # doesn't work with older versions of .NET - \ No newline at end of file +csharp_prefer_simple_using_statement = true:suggestion +csharp_style_prefer_method_group_conversion = true:silent +csharp_style_prefer_top_level_statements = true:silent +csharp_style_prefer_primary_constructors = true:suggestion +csharp_style_expression_bodied_local_functions = false:silent + +dotnet_diagnostic.ASPIREPROXYENDPOINTS001.severity = none +csharp_prefer_system_threading_lock = true:suggestion +csharp_style_prefer_null_check_over_type_check = true:suggestion +csharp_style_prefer_local_over_anonymous_function = true:suggestion +dotnet_diagnostic.CA1311.severity = warning + +# unreachable code +dotnet_diagnostic.CS0162.severity = error + +# non-nullable property uninitialized +dotnet_diagnostic.CS8618.severity = error + +# nullability of type argument doesn't match 'notnull' constraint +dotnet_diagnostic.CS8714.severity = error + +# nullability mismatch in return type of lambda +dotnet_diagnostic.CS8621.severity = error +dotnet_diagnostic.CS8766.severity = error + +# switch expression not exhaustive +dotnet_diagnostic.CS8524.severity = warning +dotnet_diagnostic.CS8509.severity = error diff --git a/.github/workflows/reflection-events-cd-nuget.yaml b/.github/workflows/reflection-events-cd-nuget.yaml index ac0efd1..826d165 100644 --- a/.github/workflows/reflection-events-cd-nuget.yaml +++ b/.github/workflows/reflection-events-cd-nuget.yaml @@ -37,7 +37,7 @@ jobs: run: dotnet restore - name: Build - run: dotnet build ReflectionEventing.sln --configuration Release --no-restore -p:SourceLinkEnabled=true + run: dotnet build ReflectionEventing.slnx --configuration Release --no-restore -p:SourceLinkEnabled=true - name: Publish the package to NuGet.org run: nuget push **\*.nupkg -NonInteractive -SkipDuplicate -Source 'https://api.nuget.org/v3/index.json' diff --git a/.github/workflows/reflection-events-pr-validator.yaml b/.github/workflows/reflection-events-pr-validator.yaml index ea4a87b..4bf4254 100644 --- a/.github/workflows/reflection-events-pr-validator.yaml +++ b/.github/workflows/reflection-events-pr-validator.yaml @@ -27,7 +27,7 @@ jobs: run: dotnet restore - name: Build - run: dotnet build ReflectionEventing.sln --configuration Release --no-restore + run: dotnet build ReflectionEventing.slnx --configuration Release --no-restore - name: Run tests - run: dotnet test ReflectionEventing.sln --configuration Release --no-restore --no-build --verbosity quiet + run: dotnet test ReflectionEventing.slnx --configuration Release --no-restore --no-build --verbosity quiet diff --git a/.gitignore b/.gitignore index 3ac8829..441bb07 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,8 @@ src/lepo.snk # DocFX docs/api/ +docs/board.md + # User-specific files *.rsuser *.suo diff --git a/.serena/.gitignore b/.serena/.gitignore new file mode 100644 index 0000000..14d86ad --- /dev/null +++ b/.serena/.gitignore @@ -0,0 +1 @@ +/cache diff --git a/.serena/memories/architecture_patterns.md b/.serena/memories/architecture_patterns.md new file mode 100644 index 0000000..8483e00 --- /dev/null +++ b/.serena/memories/architecture_patterns.md @@ -0,0 +1,108 @@ +# Architecture and Design Patterns + +## Core Architecture + +ReflectionEventing follows a modular architecture with clear separation of concerns: + +### Core Components + +1. **EventBus** (`src/ReflectionEventing/EventBus.cs`) + - Central event dispatching mechanism + - Manages consumer registration and event publication + - Supports multiple processing modes + +2. **IConsumer<T>** Interface + - Event consumers implement this interface + - Provides `ConsumeAsync(T payload, CancellationToken)` method + - Allows multiple consumers per event type + +3. **Consumer Providers** + - Abstract consumer resolution from specific DI containers + - Each DI integration has its own provider implementation + - Supports dependency injection for consumers + +4. **Consumer Types Providers** + - Hashed lookup for efficient consumer discovery + - Supports polymorphic event handling + - Uses reflection to find matching consumers + +5. **Event Queues** (`src/ReflectionEventing/Queues/`) + - Channel-based event queue implementation + - Background processing support + - Failed event handling + +## Design Patterns + +### Dependency Injection +- Core abstraction independent of specific DI container +- Adapter pattern for each DI framework +- Consumers resolved from DI container at runtime + +### Builder Pattern +- `EventBusBuilder` for configuring the event bus +- Fluent API for adding consumers and options +- Extension methods for each DI integration + +### Observer Pattern +- Event bus as subject +- Consumers as observers +- Decoupled event publishers and subscribers + +### Strategy Pattern +- Different processing modes (sequential, parallel, queued) +- Pluggable consumer type resolution strategies +- Flexible error handling strategies + +### Factory Pattern +- Consumer provider factories for each DI container +- EventBus instantiation through builders + +## Processing Modes + +1. **Sequential** - Events processed one at a time in order +2. **Parallel** - Multiple events processed concurrently +3. **Queued** - Events placed in queue for background processing + +## Key Design Principles + +### Inversion of Control (IoC) +- Event publishers don't know about consumers +- Consumers registered via DI container +- Loose coupling between components + +### Single Responsibility +- EventBus: Event routing +- Consumer Providers: DI integration +- Consumer Types Providers: Type resolution +- Event Queues: Background processing + +### Open/Closed Principle +- Core library closed for modification +- Open for extension via DI integrations +- Custom consumer types providers can be implemented + +### Interface Segregation +- Small, focused interfaces (`IEventBus`, `IConsumer`) +- Separate concerns (provider vs types provider) + +### Dependency Inversion +- Depend on abstractions (`IEventBus`, `IConsumerProvider`) +- DI integrations depend on core abstractions + +## Threading and Async +- Async/await throughout +- CancellationToken support +- Channel-based queuing for thread-safe event processing +- Task-based parallelism for concurrent event handling + +## Error Handling +- EventBusException for bus-specific errors +- QueueException for queue-specific errors +- Failed events captured and can be reprocessed +- Graceful degradation on consumer failures + +## Performance Considerations +- Hashed lookup for consumer types (O(1) average case) +- Channel-based queuing for efficient producer-consumer pattern +- AOT compilation support for .NET 8.0+ +- Trimming support to reduce app size diff --git a/.serena/memories/code_style_and_conventions.md b/.serena/memories/code_style_and_conventions.md new file mode 100644 index 0000000..b0e72cc --- /dev/null +++ b/.serena/memories/code_style_and_conventions.md @@ -0,0 +1,67 @@ +# Code Style and Conventions + +## File Header +All C# source files must include the following header: +``` +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, You can obtain one at https://opensource.org/licenses/MIT. +// Copyright (C) Leszek Pomianowski and ReflectionEventing Contributors. +// All Rights Reserved. +``` + +## Encoding and Indentation +- UTF-8 encoding +- 4 spaces for indentation (not tabs) +- Tab width: 4 +- End of line: unset (cross-platform) + +## Naming Conventions +- **Classes, structs, enums, delegates**: PascalCase +- **Interfaces**: PascalCase with 'I' prefix (e.g., `IEventBus`) +- **Methods, properties, events**: PascalCase +- **Public/internal fields**: PascalCase +- **Private/protected fields**: camelCase with underscore prefix (e.g., `_fieldName`) +- **Parameters**: camelCase +- **Local variables**: camelCase +- **Const fields**: PascalCase +- **Static readonly fields**: PascalCase +- **Private readonly fields**: camelCase +- **Async methods**: Should end with 'Async' suffix + +## C# Style Preferences +- **var**: Avoid using `var` (`false:warning` for non-apparent types) +- **this.**: Don't use `this.` qualifier unless necessary +- **Language keywords**: Prefer language keywords over BCL types (e.g., `int` not `Int32`) +- **Expression bodies**: Prefer block bodies for methods, constructors, accessors +- **Braces**: Always use braces for control flow statements +- **Namespaces**: Use file-scoped namespaces +- **using directives**: Place outside namespace +- **Nullable**: Enable nullable reference types +- **Pattern matching**: Prefer pattern matching over `as` with null check +- **Target-typed new**: Use target-typed `new()` expressions + +## Code Analysis +- EnforceCodeStyleInBuild: true +- StyleCop analyzers enabled (with some rules suppressed) +- .NET code analysis warnings enabled +- File header template enforced (IDE0073: error) + +## Documentation +- XML documentation required for public APIs in core projects +- Use `///` for XML doc comments +- Tests and demo projects don't require documentation + +## Unsafe Code +- Allowed when necessary (`AllowUnsafeBlocks: true`) +- CS8500 warnings suppressed in unsafe contexts + +## GlobalUsings +Each project should have a `GlobalUsings.cs` file with common imports: +- System namespaces +- Testing frameworks (xUnit, NSubstitute, FluentAssertions for tests) +- ReflectionEventing namespaces + +## Async/Await +- Always use async/await for asynchronous operations +- Pass CancellationToken parameters where appropriate +- Return Task/Task<T> for async methods diff --git a/.serena/memories/project_purpose.md b/.serena/memories/project_purpose.md new file mode 100644 index 0000000..ce6c3b1 --- /dev/null +++ b/.serena/memories/project_purpose.md @@ -0,0 +1,41 @@ +# Project Purpose + +## ReflectionEventing + +ReflectionEventing is a powerful tool for developers looking to create decoupled designs in WPF, WinForms, or CLI applications. By leveraging the power of Dependency Injection (DI) and eventing, ReflectionEventing promotes better Inversion of Control (IoC), reducing coupling and enhancing the modularity and flexibility of your applications. + +## Key Features + +- Event bus implementation with reflection-based consumer registration +- Support for multiple DI containers: + - Microsoft.Extensions.DependencyInjection + - Autofac + - Castle Windsor + - Ninject + - Unity +- Async event consumption +- Multiple processing modes (sequential, parallel, queued) +- Multi-targeting support (net9.0, net8.0, net6.0, netstandard2.0, net462, net472) +- AOT compilation support for .NET 8.0+ + +## Project Structure + +- `src/ReflectionEventing` - Core library with EventBus implementation +- `src/ReflectionEventing.DependencyInjection` - Microsoft DI integration +- `src/ReflectionEventing.Autofac` - Autofac integration +- `src/ReflectionEventing.Castle.Windsor` - Castle Windsor integration +- `src/ReflectionEventing.Ninject` - Ninject integration +- `src/ReflectionEventing.Unity` - Unity integration +- `src/ReflectionEventing.Demo.Wpf` - WPF demo application +- `tests/` - Unit tests for all packages +- `docs/` - Documentation and DocFX templates + +## Distribution + +All packages are distributed via NuGet.org with package names: +- ReflectionEventing +- ReflectionEventing.DependencyInjection +- ReflectionEventing.Autofac +- ReflectionEventing.Castle.Windsor +- ReflectionEventing.Ninject +- ReflectionEventing.Unity diff --git a/.serena/memories/suggested_commands.md b/.serena/memories/suggested_commands.md new file mode 100644 index 0000000..f5cbdbf --- /dev/null +++ b/.serena/memories/suggested_commands.md @@ -0,0 +1,110 @@ +# Suggested Commands + +## Windows System Commands +Since this project is developed on Windows, use PowerShell commands: +- `Get-ChildItem` (or `dir`/`ls` alias) - List directory contents +- `Set-Location` (or `cd` alias) - Change directory +- `Select-String` - Search for text patterns (similar to grep) +- `Copy-Item` - Copy files +- `Remove-Item` - Delete files +- `New-Item` - Create files/directories + +## .NET CLI Commands + +### Restore Dependencies +```powershell +dotnet restore +``` + +### Build +```powershell +# Build entire solution +dotnet build ReflectionEventing.sln --configuration Release + +# Build specific project +dotnet build src\ReflectionEventing\ReflectionEventing.csproj --configuration Release + +# Build without restore +dotnet build ReflectionEventing.sln --configuration Release --no-restore +``` + +### Run Tests +```powershell +# Run all tests +dotnet test ReflectionEventing.sln --configuration Release + +# Run tests without build +dotnet test ReflectionEventing.sln --configuration Release --no-restore --no-build + +# Run tests with quiet verbosity (less output) +dotnet test ReflectionEventing.sln --configuration Release --verbosity quiet + +# Run specific test project +dotnet test tests\ReflectionEventing.UnitTests\ReflectionEventing.UnitTests.csproj +``` + +### Create NuGet Packages +```powershell +# Packages are generated automatically on build if GeneratePackageOnBuild is true +dotnet build --configuration Release + +# Or manually pack +dotnet pack src\ReflectionEventing\ReflectionEventing.csproj --configuration Release +``` + +### Clean Build Artifacts +```powershell +dotnet clean ReflectionEventing.sln --configuration Release +``` + +## Git Commands +```powershell +# Check status +git status + +# View changes (without pager) +git --no-pager diff + +# View specific file diff +git --no-pager diff -- src\ReflectionEventing\EventBus.cs + +# Commit changes +git add . +git commit -m "Your message" + +# Push changes +git push origin main +``` + +## NuGet Commands +```powershell +# Restore NuGet packages +nuget restore ReflectionEventing.sln + +# Push package to NuGet.org +nuget push **\*.nupkg -Source 'https://api.nuget.org/v3/index.json' +``` + +## MSBuild Commands +```powershell +# Build with MSBuild +msbuild ReflectionEventing.sln /p:Configuration=Release + +# Clean and rebuild +msbuild ReflectionEventing.sln /t:Clean,Build /p:Configuration=Release +``` + +## DocFX Commands +For building documentation: +```powershell +# Navigate to docs folder +cd docs + +# Build documentation +docfx docfx.json +``` + +## Run Demo Application +```powershell +dotnet run --project src\ReflectionEventing.Demo.Wpf\ReflectionEventing.Demo.Wpf.csproj +``` diff --git a/.serena/memories/task_completion_checklist.md b/.serena/memories/task_completion_checklist.md new file mode 100644 index 0000000..5da7cc2 --- /dev/null +++ b/.serena/memories/task_completion_checklist.md @@ -0,0 +1,78 @@ +# Task Completion Checklist + +When completing a development task in ReflectionEventing, follow these steps: + +## 1. Code Changes +- [ ] Make the necessary code changes +- [ ] Ensure file headers are present and correct in all modified files +- [ ] Use proper naming conventions (see code_style_and_conventions.md) +- [ ] Follow C# coding standards defined in .editorconfig +- [ ] Add XML documentation comments for public APIs +- [ ] Use file-scoped namespaces +- [ ] Place using directives outside namespaces + +## 2. Build +```powershell +dotnet build ReflectionEventing.sln --configuration Release --no-restore +``` +- [ ] Verify build succeeds without errors +- [ ] Check for any new analyzer warnings +- [ ] Ensure no StyleCop violations + +## 3. Run Tests +```powershell +dotnet test ReflectionEventing.sln --configuration Release --no-restore --no-build +``` +- [ ] Verify all existing tests pass +- [ ] Add new tests for new functionality +- [ ] Use xUnit, NSubstitute for mocks, AwesomeAssertions for assertions +- [ ] Ensure test coverage for edge cases + +## 4. Code Quality +- [ ] Review code for potential null reference issues +- [ ] Ensure async methods have 'Async' suffix +- [ ] Verify CancellationToken usage in async methods +- [ ] Check for proper disposal of IDisposable resources +- [ ] Ensure thread-safety where necessary + +## 5. Multi-Targeting Considerations +- [ ] If changes affect compatibility, test on multiple frameworks: + - net9.0, net8.0, net6.0, netstandard2.0, net462, net472 +- [ ] Use conditional compilation if needed (#if NET8_0_OR_GREATER) +- [ ] Check PolySharp polyfills work for older frameworks + +## 6. Documentation +- [ ] Update README.md if public API changes +- [ ] Update docs/documentation/*.md if behavior changes +- [ ] Ensure XML documentation is complete for public members + +## 7. Git Workflow +```powershell +# Check what changed +git status +git --no-pager diff + +# Stage changes +git add . + +# Commit with descriptive message +git commit -m "Description of changes" + +# Push to branch (not directly to main) +git push origin your-branch-name +``` + +## 8. Before Creating PR +- [ ] Rebase on latest main if needed +- [ ] Run full build and test suite one more time +- [ ] Check that no unnecessary files are included +- [ ] Write descriptive PR title and description +- [ ] Reference any related issues + +## Notes +- **Do not push directly to main** - use pull requests +- **Do not commit secrets** or credentials +- PR validator will run automatically and check: + - Build success + - All tests passing + - No StyleCop violations diff --git a/.serena/memories/tech_stack.md b/.serena/memories/tech_stack.md new file mode 100644 index 0000000..2f92bd3 --- /dev/null +++ b/.serena/memories/tech_stack.md @@ -0,0 +1,51 @@ +# Tech Stack + +## Programming Language +- C# 13.0 +- Nullable reference types enabled + +## Target Frameworks +- .NET 9.0 +- .NET 8.0 +- .NET 6.0 +- .NET Standard 2.0 +- .NET Framework 4.6.2 +- .NET Framework 4.7.2 + +## Build System +- MSBuild +- .NET SDK 9.x +- Central Package Management (CPM) via Directory.Packages.props + +## Testing +- xUnit 2.6.2 +- NSubstitute 5.1.0 (mocking) +- AwesomeAssertions 8.0.1 (fluent assertions) +- coverlet.collector for code coverage + +## Dependencies +- System.Threading.Channels (for queued event processing) +- System.Diagnostics.DiagnosticSource (for diagnostics) +- PolySharp (for polyfills on older frameworks) +- Microsoft.SourceLink.GitHub (for source linking) + +## DI Frameworks Supported +- Microsoft.Extensions.DependencyInjection 8.0.0 +- Autofac 4.0.0 +- Castle.Windsor 6.0.0 +- Ninject 3.0.1.10 +- Unity 5.11.10 + +## Documentation +- DocFX for API documentation +- Custom templates with TypeScript/SCSS + +## CI/CD +- GitHub Actions +- Windows runners +- Automated NuGet publishing +- Code signing with strong name keys + +## Other Tools +- EditorConfig for code style enforcement +- StyleCop and .NET analyzers diff --git a/.serena/project.yml b/.serena/project.yml new file mode 100644 index 0000000..05f028e --- /dev/null +++ b/.serena/project.yml @@ -0,0 +1,112 @@ +# the name by which the project can be referenced within Serena +project_name: "reflectioneventing" + + +# list of languages for which language servers are started; choose from: +# al bash clojure cpp csharp +# csharp_omnisharp dart elixir elm erlang +# fortran fsharp go groovy haskell +# java julia kotlin lua markdown +# matlab nix pascal perl php +# powershell python python_jedi r rego +# ruby ruby_solargraph rust scala swift +# terraform toml typescript typescript_vts vue +# yaml zig +# (This list may be outdated. For the current list, see values of Language enum here: +# https://github.com/oraios/serena/blob/main/src/solidlsp/ls_config.py +# For some languages, there are alternative language servers, e.g. csharp_omnisharp, ruby_solargraph.) +# Note: +# - For C, use cpp +# - For JavaScript, use typescript +# - For Free Pascal/Lazarus, use pascal +# Special requirements: +# Some languages require additional setup/installations. +# See here for details: https://oraios.github.io/serena/01-about/020_programming-languages.html#language-servers +# When using multiple languages, the first language server that supports a given file will be used for that file. +# The first language is the default language and the respective language server will be used as a fallback. +# Note that when using the JetBrains backend, language servers are not used and this list is correspondingly ignored. +languages: +- csharp + +# the encoding used by text files in the project +# For a list of possible encodings, see https://docs.python.org/3.11/library/codecs.html#standard-encodings +encoding: "utf-8" + +# whether to use project's .gitignore files to ignore files +ignore_all_files_in_gitignore: true + +# list of additional paths to ignore in all projects +# same syntax as gitignore, so you can use * and ** +ignored_paths: [] + +# whether the project is in read-only mode +# If set to true, all editing tools will be disabled and attempts to use them will result in an error +# Added on 2025-04-18 +read_only: false + +# list of tool names to exclude. We recommend not excluding any tools, see the readme for more details. +# Below is the complete list of tools for convenience. +# To make sure you have the latest list of tools, and to view their descriptions, +# execute `uv run scripts/print_tool_overview.py`. +# +# * `activate_project`: Activates a project by name. +# * `check_onboarding_performed`: Checks whether project onboarding was already performed. +# * `create_text_file`: Creates/overwrites a file in the project directory. +# * `delete_lines`: Deletes a range of lines within a file. +# * `delete_memory`: Deletes a memory from Serena's project-specific memory store. +# * `execute_shell_command`: Executes a shell command. +# * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced. +# * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type). +# * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type). +# * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes. +# * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file. +# * `initial_instructions`: Gets the initial instructions for the current project. +# Should only be used in settings where the system prompt cannot be set, +# e.g. in clients you have no control over, like Claude Desktop. +# * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol. +# * `insert_at_line`: Inserts content at a given line in a file. +# * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol. +# * `list_dir`: Lists files and directories in the given directory (optionally with recursion). +# * `list_memories`: Lists memories in Serena's project-specific memory store. +# * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building). +# * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context). +# * `read_file`: Reads a file within the project directory. +# * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store. +# * `remove_project`: Removes a project from the Serena configuration. +# * `replace_lines`: Replaces a range of lines within a file with new content. +# * `replace_symbol_body`: Replaces the full definition of a symbol. +# * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen. +# * `search_for_pattern`: Performs a search for a pattern in the project. +# * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase. +# * `switch_modes`: Activates modes by providing a list of their names +# * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information. +# * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task. +# * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed. +# * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store. +excluded_tools: [] + +# list of tools to include that would otherwise be disabled (particularly optional tools that are disabled by default) +included_optional_tools: [] + +# fixed set of tools to use as the base tool set (if non-empty), replacing Serena's default set of tools. +# This cannot be combined with non-empty excluded_tools or included_optional_tools. +fixed_tools: [] + +# list of mode names to that are always to be included in the set of active modes +# The full set of modes to be activated is base_modes + default_modes. +# If the setting is undefined, the base_modes from the global configuration (serena_config.yml) apply. +# Otherwise, this setting overrides the global configuration. +# Set this to [] to disable base modes for this project. +# Set this to a list of mode names to always include the respective modes for this project. +base_modes: + +# list of mode names that are to be activated by default. +# The full set of modes to be activated is base_modes + default_modes. +# If the setting is undefined, the default_modes from the global configuration (serena_config.yml) apply. +# Otherwise, this overrides the setting from the global configuration (serena_config.yml). +# This setting can, in turn, be overridden by CLI parameters (--mode). +default_modes: + +# initial prompt for the project. It will always be given to the LLM upon activating the project +# (contrary to the memories, which are loaded on demand). +initial_prompt: "" diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..30cdd7a --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "workbench.colorCustomizations": { + "editorWhitespace.foreground": "#5a5a5a" + } +} diff --git a/Directory.Build.props b/Directory.Build.props index 7af207f..1ce5140 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,13 +1,12 @@ - $(MSBuildThisFileDirectory) $(RepositoryDirectory)build\ - 4.1.0 - 4.0.0 + 5.0.0 + 5.0.0 @@ -38,7 +37,7 @@ true - 13.0 + 14.0 enable - + true true true @@ -49,7 +73,11 @@ $(RepositoryDirectory)\src\lepo.snk - + >Bus: [MainWindowViewModel] + + Bus->>Provider: GetConsumers(typeof(MainWindowViewModel)) + Provider-->>Bus: [viewModelInstance] + + Bus->>VM: ConsumeAsync(BackgroundTicked) + VM->>VM: Update state + VM-->>Bus: Task.CompletedTask + + Bus-->>Service: Event processed +``` + +## Polymorphic Events + +ReflectionEventing supports polymorphic event handling via `HashedPolymorphicConsumerTypesProvider`. This allows consumers to subscribe to base types or interfaces. + +```mermaid +classDiagram + class ITickedEvent { + <> + +TickCount: int + } + + class SystemTicked { + +TickCount: int + } + + class BackgroundTicked { + +TickCount: int + } + + class PolymorphicConsumer { + +ConsumeAsync(ITickedEvent) + } + + ITickedEvent <|.. SystemTicked + ITickedEvent <|.. BackgroundTicked + PolymorphicConsumer ..> ITickedEvent : consumes +``` + +### Example + +```csharp +// Consumer for any ITickedEvent +public class AnyTickEventConsumer : IConsumer +{ + public ValueTask ConsumeAsync(ITickedEvent payload, CancellationToken cancellationToken) + { + Console.WriteLine($"Received tick: {payload.TickCount}"); + return ValueTask.CompletedTask; + } +} + +// This consumer will receive both SystemTicked and BackgroundTicked +``` + +## Event Lifecycle + +### 1. Creation + +Events are created by the publisher: + +```csharp +var event = new BackgroundTicked(42); +``` + +### 2. Publishing + +Events are sent through the event bus: + +```csharp +// Immediate processing (waits for completion) +await eventBus.SendAsync(event); + +// Queued processing (returns immediately) +await eventBus.PublishAsync(event); +``` + +### 3. Routing + +The event bus finds all consumers for the event type: + +```csharp +// Internal process +IEnumerable consumerTypes = typesProvider.GetConsumerTypes(typeof(BackgroundTicked)); +``` + +### 4. Consumption + +Each consumer processes the event: + +```csharp +public class MyConsumer : IConsumer +{ + public ValueTask ConsumeAsync(BackgroundTicked payload, CancellationToken cancellationToken) + { + // Handle the event + return ValueTask.CompletedTask; + } +} +``` + +### 5. Completion + +For `SendAsync`: Returns when all consumers complete. +For `PublishAsync`: Returns immediately; processing happens in background. + +## Failed Events + +When event processing fails, the failure is captured: + +```csharp +public class FailedEvent +{ + public object Event { get; } + public Exception Exception { get; } + public DateTime FailedAt { get; } +} +``` + +## Best Practices + +### Do + +- ✅ Keep events small and focused +- ✅ Use immutable types (records) +- ✅ Include correlation IDs for tracing +- ✅ Use meaningful names in past tense +- ✅ Consider interface hierarchies for related events + +### Don't + +- ❌ Include behavior in events (events are data only) +- ❌ Include entire entities (only relevant data) +- ❌ Use events for request/response (use direct calls) +- ❌ Rely on event ordering (consumers may run in parallel) + +## See Also + +- [Domain Overview](../domain/overview.md) +- [Ubiquitous Language](../domain/ubiquitous-language.md) +- [ADR-001](../decisions/ADR-001-event-bus-pattern.md) + +--- + +*Last updated: 2026-02-09* diff --git a/docs/architecture/cross-cutting/observability.md b/docs/architecture/cross-cutting/observability.md new file mode 100644 index 0000000..0f7e51d --- /dev/null +++ b/docs/architecture/cross-cutting/observability.md @@ -0,0 +1,161 @@ +# Cross-Cutting Concerns: Observability + +## Overview + +ReflectionEventing includes built-in observability support using OpenTelemetry-compatible instrumentation for tracing and metrics. + +## Tracing + +The `EventBus` creates traces for event operations using `System.Diagnostics.ActivitySource`. + +### Activity Source + +```csharp +private static readonly ActivitySource ActivitySource = new("ReflectionEventing.EventBus"); +``` + +### Trace Spans + +| Operation | Activity Name | Tags | +|-----------|--------------|------| +| `SendAsync` | Producer activity | `co.lepo.reflection.eventing.message` = event type name | +| `PublishAsync` | Producer activity | `co.lepo.reflection.eventing.message` = event type name | + +### Example Trace + +``` +TraceId: abc123... +├─ ReflectionEventing.EventBus (Producer) +│ └─ Tags: co.lepo.reflection.eventing.message = "BackgroundTicked" +``` + +### Integration with OpenTelemetry + +```csharp +services.AddOpenTelemetry() + .WithTracing(tracing => + { + tracing.AddSource("ReflectionEventing.EventBus"); + // ... other sources + }); +``` + +## Metrics + +The `EventBus` emits metrics using `System.Diagnostics.Metrics.Meter`. + +### Meter + +```csharp +private static readonly Meter Meter = new("ReflectionEventing.EventBus"); +``` + +### Counters + +| Counter | Name | Tags | Description | +|---------|------|------|-------------| +| Sent | `bus.sent` | `message_type` | Count of events sent via `SendAsync` | +| Published | `bus.published` | `message_type` | Count of events published via `PublishAsync` | + +### Example Metrics Output + +``` +bus.sent{message_type="BackgroundTicked"} = 42 +bus.sent{message_type="OrderCreated"} = 15 +bus.published{message_type="AsyncEvent"} = 100 +``` + +### Integration with OpenTelemetry + +```csharp +services.AddOpenTelemetry() + .WithMetrics(metrics => + { + metrics.AddMeter("ReflectionEventing.EventBus"); + // ... other meters + }); +``` + +## Logging + +The library doesn't include its own logging, but consumers can add logging: + +```csharp +public class LoggingConsumer : IConsumer +{ + private readonly ILogger> _logger; + + public async ValueTask ConsumeAsync(TEvent payload, CancellationToken cancellationToken) + { + _logger.LogInformation("Processing event: {EventType}", typeof(TEvent).Name); + // ... process event + } +} +``` + +## Distributed Tracing + +For distributed systems, correlation can be maintained through events: + +```csharp +public record OrderCreated +{ + public Guid OrderId { get; init; } + public string TraceId { get; init; } // For correlation + public string SpanId { get; init; } +} + +// Publishing +var activity = Activity.Current; +await eventBus.SendAsync(new OrderCreated +{ + OrderId = Guid.NewGuid(), + TraceId = activity?.TraceId.ToString(), + SpanId = activity?.SpanId.ToString() +}); +``` + +## Monitoring Dashboard Example + +### Key Metrics to Monitor + +| Metric | Query | Alert Threshold | +|--------|-------|-----------------| +| Events sent/min | `rate(bus_sent_total[1m])` | Varies by app | +| Events published/min | `rate(bus_published_total[1m])` | Varies by app | +| Error rate | `rate(consumer_errors_total[1m])` | > 1% | + +### Grafana Dashboard + +```mermaid +flowchart LR + subgraph Metrics["Metrics Collection"] + EventBus[EventBus] + OTel[OpenTelemetry Collector] + end + + subgraph Storage["Storage"] + Prometheus[Prometheus] + Jaeger[Jaeger] + end + + subgraph Visualization["Visualization"] + Grafana[Grafana] + end + + EventBus -->|Metrics| OTel + EventBus -->|Traces| OTel + OTel -->|Metrics| Prometheus + OTel -->|Traces| Jaeger + Prometheus --> Grafana + Jaeger --> Grafana +``` + +## See Also + +- [Logical Architecture](../views/logical-architecture.md) - Component overview +- [EventBus Implementation](../../src/ReflectionEventing/EventBus.cs) - Source code + +--- + +*Last updated: 2026-02-09* diff --git a/docs/architecture/decisions/ADR-001-event-bus-pattern.md b/docs/architecture/decisions/ADR-001-event-bus-pattern.md new file mode 100644 index 0000000..57945fb --- /dev/null +++ b/docs/architecture/decisions/ADR-001-event-bus-pattern.md @@ -0,0 +1,105 @@ +# ADR-001: Event Bus Pattern for Decoupled Communication + +## Status + +**Accepted** + +## Date + +2026-02-09 + +## Context + +When building applications (WPF, WinForms, Console, or ASP.NET Core), components often need to communicate with each other. Traditional approaches include: + +1. **Direct method calls** - Creates tight coupling between components +2. **Events/Delegates** - Better, but still requires references between components +3. **Mediator pattern** - Centralizes communication but can become complex + +The project needs a communication mechanism that: +- Allows complete decoupling between publishers and subscribers +- Integrates seamlessly with dependency injection +- Supports asynchronous operations +- Is easy to test and maintain + +## Decision + +Implement an **Event Bus pattern** with the following characteristics: + +1. **Central event bus (`IEventBus`)** that acts as a message broker +2. **Generic consumers (`IConsumer`)** for type-safe event handling +3. **Reflection-based discovery** of consumer registrations +4. **DI integration** for consumer resolution + +### Key Design Elements + +```csharp +public interface IEventBus +{ + Task SendAsync(TEvent eventItem, CancellationToken cancellationToken = default) + where TEvent : class; + + Task PublishAsync(TEvent eventItem, CancellationToken cancellationToken = default) + where TEvent : class; +} + +public interface IConsumer +{ + Task ConsumeAsync(TEvent payload, CancellationToken cancellationToken); +} +``` + +## Consequences + +### Positive + +- ✅ **Complete decoupling** - Publishers don't know about consumers +- ✅ **Testability** - Easy to mock the event bus in tests +- ✅ **Flexibility** - Multiple consumers can handle the same event +- ✅ **Type safety** - Generic consumers ensure compile-time type checking +- ✅ **Async-first** - Native support for async/await patterns +- ✅ **DI integration** - Consumers are resolved from the container + +### Negative + +- ⚠️ **Indirection** - Harder to trace event flow through the codebase +- ⚠️ **Runtime errors** - Consumer registration issues surface at runtime +- ⚠️ **Debugging complexity** - Stack traces span across the event bus + +### Mitigations + +- Use OpenTelemetry tracing for event flow visibility +- Add compile-time checks where possible +- Provide good error messages for misconfiguration + +## Alternatives Considered + +### 1. MediatR + +**Pros:** Popular, well-documented, supports pipelines +**Cons:** Different design philosophy (request/response), external dependency + +**Why rejected:** ReflectionEventing focuses on event-driven (fire-and-forget or parallel) scenarios rather than request/response. + +### 2. .NET Events/Delegates + +**Pros:** Built-in, simple, fast +**Cons:** Tight coupling, no DI integration, synchronous by default + +**Why rejected:** Doesn't meet decoupling requirements. + +### 3. Message Queue (RabbitMQ, Azure Service Bus) + +**Pros:** Distributed, persistent, scalable +**Cons:** Infrastructure overhead, complexity for simple scenarios + +**Why rejected:** Over-engineered for in-process communication; could be used alongside for distributed scenarios. + +## Related Decisions + +- [ADR-002](./ADR-002-multi-di-container-support.md) - Multi DI Container Support +- [ADR-003](./ADR-003-async-first-design.md) - Async-First Design + +--- + +*Last updated: 2026-02-09* diff --git a/docs/architecture/decisions/ADR-002-multi-di-container-support.md b/docs/architecture/decisions/ADR-002-multi-di-container-support.md new file mode 100644 index 0000000..6e87ea3 --- /dev/null +++ b/docs/architecture/decisions/ADR-002-multi-di-container-support.md @@ -0,0 +1,134 @@ +# ADR-002: Multi DI Container Support via Adapter Pattern + +## Status + +**Accepted** + +## Date + +2026-02-09 + +## Context + +.NET ecosystem has multiple popular dependency injection containers: + +- **Microsoft.Extensions.DependencyInjection** - Default for ASP.NET Core, modern .NET +- **Autofac** - Popular for advanced scenarios, WPF +- **Castle Windsor** - Enterprise, legacy systems +- **Ninject** - Lightweight, convention-based +- **Unity** - Microsoft patterns & practices + +Different projects use different containers based on: +- Existing infrastructure +- Advanced features (decorators, modules, etc.) +- Team familiarity +- Legacy code constraints + +ReflectionEventing needs to work with any of these containers without forcing users to switch. + +## Decision + +Implement the **Adapter Pattern** to support multiple DI containers: + +1. **Core library is DI-agnostic** - No dependencies on any specific container +2. **Abstract interfaces** define contracts for DI operations +3. **Separate packages** for each container integration +4. **Consistent API** across all integrations + +### Architecture + +```mermaid +flowchart TB + subgraph Core["ReflectionEventing (Core)"] + IConsumerProvider["IConsumerProvider
«interface»"] + IEventBus["IEventBus
«interface»"] + end + + subgraph Adapters["DI Adapter Packages"] + MSDI["DependencyInjectionConsumerProvider"] + Autofac["AutofacConsumerProvider"] + Castle["WindsorConsumerProvider"] + Ninject["NinjectConsumerProvider"] + Unity["UnityConsumerProvider"] + end + + MSDI -->|implements| IConsumerProvider + Autofac -->|implements| IConsumerProvider + Castle -->|implements| IConsumerProvider + Ninject -->|implements| IConsumerProvider + Unity -->|implements| IConsumerProvider +``` + +### Package Structure + +| Package | Container | Extension Point | +|---------|-----------|-----------------| +| ReflectionEventing.DependencyInjection | Microsoft.Extensions.DI | `IServiceCollection.AddEventBus()` | +| ReflectionEventing.Autofac | Autofac | `ContainerBuilder.AddEventBus()` | +| ReflectionEventing.Castle.Windsor | Castle Windsor | `IWindsorContainer.Install()` | +| ReflectionEventing.Ninject | Ninject | `IKernel.Load()` | +| ReflectionEventing.Unity | Unity | `IUnityContainer.AddEventBus()` | + +## Consequences + +### Positive + +- ✅ **Flexibility** - Works with any supported container +- ✅ **No lock-in** - Users can switch containers if needed +- ✅ **Minimal core** - Core library has no external DI dependencies +- ✅ **Familiar patterns** - Each adapter follows container conventions +- ✅ **Independent versioning** - Adapters can update separately + +### Negative + +- ⚠️ **Maintenance burden** - Multiple packages to maintain +- ⚠️ **Feature parity** - Must ensure consistent behavior across adapters +- ⚠️ **Testing overhead** - Need tests for each container integration +- ⚠️ **Documentation** - Need examples for each container + +### Mitigations + +- Comprehensive test suite for each adapter +- Shared test cases to verify behavior consistency +- Clear documentation with examples for each container + +## Alternatives Considered + +### 1. Support Only Microsoft.Extensions.DI + +**Pros:** Simpler, one package, standard in modern .NET +**Cons:** Alienates users of other containers, especially in legacy/WPF + +**Why rejected:** Significant user base uses alternative containers. + +### 2. Use Abstractions Package Only + +**Pros:** Single integration point +**Cons:** Not all containers implement Microsoft's abstractions + +**Why rejected:** Autofac, Castle, Ninject have their own patterns. + +### 3. Service Locator Pattern + +**Pros:** Simpler integration +**Cons:** Anti-pattern, testability issues + +**Why rejected:** Goes against modern DI best practices. + +## Implementation Notes + +Each adapter must implement: + +1. **Consumer Provider** - Resolves consumers from the container +2. **Event Bus Builder** - Container-specific configuration +3. **Extension methods** - Idiomatic registration for the container +4. **Lifetime management** - Proper scoping of consumers + +## Related Decisions + +- [ADR-001](./ADR-001-event-bus-pattern.md) - Event Bus Pattern +- [ADR-004](./ADR-004-multi-targeting.md) - Multi-targeting + +--- + +*Last updated: 2026-02-09* diff --git a/docs/architecture/decisions/ADR-003-async-first-design.md b/docs/architecture/decisions/ADR-003-async-first-design.md new file mode 100644 index 0000000..c1e5be1 --- /dev/null +++ b/docs/architecture/decisions/ADR-003-async-first-design.md @@ -0,0 +1,162 @@ +# ADR-003: Async-First Design for All Operations + +## Status + +**Superseded by [ADR-005](ADR-005-valuetask-adoption.md)** - Return types changed from `Task` to `ValueTask` in v5.0.0 + +**Accepted** + +## Date + +2026-02-09 + +## Context + +Modern .NET applications heavily rely on asynchronous programming for: + +- I/O-bound operations (database, HTTP, file system) +- UI responsiveness (WPF, WinForms) +- Scalability (ASP.NET Core request handling) +- Long-running operations + +Event consumers often perform these async operations, making async support essential. + +## Decision + +Design all public APIs as **async-first**: + +1. **All event bus methods return `Task`** +2. **Consumer interface uses `ConsumeAsync`** +3. **CancellationToken support throughout** +4. **No synchronous alternatives provided** + +### API Design + +```csharp +public interface IEventBus +{ + Task SendAsync(TEvent eventItem, CancellationToken cancellationToken = default) + where TEvent : class; + + Task PublishAsync(TEvent eventItem, CancellationToken cancellationToken = default) + where TEvent : class; +} + +public interface IConsumer +{ + Task ConsumeAsync(TEvent payload, CancellationToken cancellationToken); +} +``` + +### CancellationToken Flow + +```mermaid +sequenceDiagram + participant App as Application + participant Bus as IEventBus + participant Consumer as IConsumer + participant DB as Database + + App->>Bus: SendAsync(event, cts.Token) + Bus->>Consumer: ConsumeAsync(event, cts.Token) + Consumer->>DB: SaveAsync(data, cts.Token) + + Note over App: User cancels + App->>cts: Cancel() + + DB-->>Consumer: OperationCanceledException + Consumer-->>Bus: OperationCanceledException + Bus-->>App: OperationCanceledException +``` + +## Consequences + +### Positive + +- ✅ **Scalability** - Async operations don't block threads +- ✅ **UI responsiveness** - Non-blocking for WPF/WinForms +- ✅ **Cancellation support** - Operations can be cancelled gracefully +- ✅ **Modern .NET alignment** - Follows current best practices +- ✅ **Composability** - Easy to chain async operations + +### Negative + +- ⚠️ **Complexity** - Async code requires understanding of Task/async/await +- ⚠️ **Debugging** - Async stack traces can be harder to follow +- ⚠️ **Overhead** - Small overhead for truly synchronous consumers + +### Mitigations + +- Provide clear documentation on async patterns +- Use `ConfigureAwait(false)` where appropriate +- Consider `ValueTask` for hot paths in future versions + +## Implementation Details + +### EventBus Implementation + +```csharp +public virtual async Task SendAsync( + TEvent eventItem, + CancellationToken cancellationToken = default) + where TEvent : class +{ + // Create tasks for all consumers + List tasks = []; + + foreach (Type consumerType in consumerTypesProvider.GetConsumerTypes(typeof(TEvent))) + { + foreach (var consumer in consumerProvider.GetConsumers(consumerType)) + { + tasks.Add(((IConsumer)consumer).ConsumeAsync(eventItem, cancellationToken)); + } + } + + // Execute all consumers in parallel + await Task.WhenAll(tasks).ConfigureAwait(false); +} +``` + +### Queue-Based Processing + +```csharp +public virtual async Task PublishAsync( + TEvent eventItem, + CancellationToken cancellationToken = default) + where TEvent : class +{ + // Non-blocking enqueue + await queue.EnqueueAsync(eventItem, cancellationToken); +} +``` + +## Alternatives Considered + +### 1. Dual API (Sync + Async) + +**Pros:** Flexibility for simple scenarios +**Cons:** Maintenance overhead, sync-over-async anti-pattern + +**Why rejected:** Encourages anti-patterns, doubles API surface. + +### 2. Sync-First with Async Extensions + +**Pros:** Simpler for sync-only consumers +**Cons:** Blocks threads, poor scalability + +**Why rejected:** Doesn't align with modern .NET practices. + +### 3. ValueTask Instead of Task + +**Pros:** Less allocation for sync completion +**Cons:** More complex, easier to misuse + +**Why rejected:** Premature optimization; can be added later if needed. + +## Related Decisions + +- [ADR-001](./ADR-001-event-bus-pattern.md) - Event Bus Pattern + +--- + +*Last updated: 2026-02-09* diff --git a/docs/architecture/decisions/ADR-004-multi-targeting.md b/docs/architecture/decisions/ADR-004-multi-targeting.md new file mode 100644 index 0000000..7256e03 --- /dev/null +++ b/docs/architecture/decisions/ADR-004-multi-targeting.md @@ -0,0 +1,164 @@ +# ADR-004: Multi-targeting for Broad Framework Support + +## Status + +**Accepted** + +## Date + +2026-02-09 + +## Context + +.NET ecosystem has evolved significantly: + +- **.NET Framework 4.6.2+** - Still used in enterprise WPF/WinForms +- **.NET Standard 2.0** - Bridge between old and new +- **.NET 6.0** - LTS, widely adopted +- **.NET 8.0** - Current LTS, AOT support +- **.NET 9.0** - Latest, cutting edge features + +ReflectionEventing targets: +- WPF applications (often .NET Framework or .NET 6+) +- WinForms applications (legacy .NET Framework) +- Console/CLI tools (modern .NET) +- ASP.NET Core services (.NET 6+) + +To maximize adoption, the library must support multiple target frameworks. + +## Decision + +Implement **multi-targeting** with the following targets: + +| Target Framework | Reason | +|------------------|--------| +| `net9.0` | Latest features, best performance, AOT | +| `net8.0` | Current LTS, AOT support | +| `net6.0` | Previous LTS, wide adoption | +| `netstandard2.0` | .NET Framework compatibility | +| `net462` | Legacy WPF/WinForms support | +| `net472` | Legacy with some modern APIs | + +### Project File Configuration + +```xml + + net9.0;net8.0;net6.0;netstandard2.0;net462;net472 + 13.0 + enable + + + + true + true + +``` + +### Polyfill Strategy + +Use **PolySharp** to provide missing APIs on older frameworks: + +```xml + + + all + build; analyzers + + +``` + +Polyfilled types include: +- `CallerArgumentExpressionAttribute` +- `IsExternalInit` (for records) +- `RequiredMemberAttribute` +- Nullable analysis attributes + +## Consequences + +### Positive + +- ✅ **Broad adoption** - Works in legacy and modern projects +- ✅ **Future-proof** - Ready for .NET 9 and beyond +- ✅ **AOT support** - Native compilation for .NET 8+ +- ✅ **Trimming** - Smaller deployments on modern frameworks +- ✅ **Single codebase** - Minimal conditional compilation + +### Negative + +- ⚠️ **Complexity** - Must test on all targets +- ⚠️ **Feature restrictions** - Can't use APIs not available everywhere +- ⚠️ **Package size** - Multiple TFMs increase NuGet package size +- ⚠️ **Polyfill dependencies** - Additional build-time dependency + +### Mitigations + +- Automated CI builds on all targets +- Conditional dependencies where needed +- Clear documentation of framework-specific behavior + +## Framework-Specific Dependencies + +```xml + + + + +``` + +## Conditional Compilation + +When framework-specific code is needed: + +```csharp +#if NET8_0_OR_GREATER + // Use modern APIs + ReadOnlySpan span = value.AsSpan(); +#else + // Fallback for older frameworks + string span = value; +#endif +``` + +## Alternatives Considered + +### 1. .NET Standard 2.0 Only + +**Pros:** Single target, simpler +**Cons:** No AOT, no trimming, misses modern optimizations + +**Why rejected:** Leaves significant value on the table for modern apps. + +### 2. Modern .NET Only (.NET 6+) + +**Pros:** Modern APIs, simpler testing +**Cons:** Excludes legacy WPF/WinForms apps + +**Why rejected:** Significant user base on .NET Framework. + +### 3. Separate Packages per Target + +**Pros:** Cleaner separation +**Cons:** Maintenance overhead, confusing for users + +**Why rejected:** Multi-targeting in single package is standard practice. + +## Build Matrix + +| Framework | Windows | Linux | macOS | AOT | +|-----------|---------|-------|-------|-----| +| net9.0 | ✅ | ✅ | ✅ | ✅ | +| net8.0 | ✅ | ✅ | ✅ | ✅ | +| net6.0 | ✅ | ✅ | ✅ | ❌ | +| netstandard2.0 | ✅ | ✅ | ✅ | ❌ | +| net462 | ✅ | ❌ | ❌ | ❌ | +| net472 | ✅ | ❌ | ❌ | ❌ | + +## Related Decisions + +- [ADR-002](./ADR-002-multi-di-container-support.md) - Multi DI Container Support + +--- + +*Last updated: 2026-02-09* diff --git a/docs/architecture/decisions/ADR-005-valuetask-adoption.md b/docs/architecture/decisions/ADR-005-valuetask-adoption.md new file mode 100644 index 0000000..7a0dd03 --- /dev/null +++ b/docs/architecture/decisions/ADR-005-valuetask-adoption.md @@ -0,0 +1,201 @@ +# ADR-005: ValueTask Adoption for Performance Optimization + +## Status + +**Accepted** + +## Date + +2026-02-09 + +## Context + +The current async API uses `Task` and `Task` return types throughout the library. While `Task` provides excellent async/await support, it has allocation overhead: + +- Each `Task` instance allocates ~80 bytes on the heap +- Hot path operations (event sending) may complete synchronously +- Multiple allocations per event when no async work is needed + +### Performance Characteristics + +**When synchronous completion occurs:** +- Empty consumer list: Allocates Task for no work +- Single sync consumer: Consumer completes immediately +- Multiple sync consumers: Allocates Task for coordination + +**ValueTask Benefits:** +- Zero allocation when completing synchronously +- Same API surface as Task +- Backward compatible at call sites (await works the same) + +### When ValueTask Should Be Used (Microsoft Guidance) + +✅ Method frequently completes synchronously +✅ Called in hot paths with high frequency +✅ Allocation overhead is measurable +✅ Performance is a key requirement + +### ValueTask Caveats + +⚠️ Can only be awaited **once** +⚠️ Cannot use with `Task.WhenAll` directly (need `.AsTask()`) +⚠️ Slightly larger state machines for async methods +⚠️ More complex to reason about lifetime + +## Decision + +Replace `Task` with `ValueTask` in all async APIs to reduce allocations in hot paths: + +### API Changes + +```csharp +// Before +public interface IEventBus +{ + Task SendAsync(TEvent eventItem, CancellationToken cancellationToken = default); + Task PublishAsync(TEvent eventItem, CancellationToken cancellationToken = default); +} + +public interface IConsumer +{ + Task ConsumeAsync(TEvent payload, CancellationToken cancellationToken); +} + +public interface IEventsQueue +{ + Task EnqueueAsync(TEvent @event, CancellationToken cancellationToken = default); +} + +// After +public interface IEventBus +{ + ValueTask SendAsync(TEvent eventItem, CancellationToken cancellationToken = default); + ValueTask PublishAsync(TEvent eventItem, CancellationToken cancellationToken = default); +} + +public interface IConsumer +{ + ValueTask ConsumeAsync(TEvent payload, CancellationToken cancellationToken); +} + +public interface IEventsQueue +{ + ValueTask EnqueueAsync(TEvent @event, CancellationToken cancellationToken = default); +} +``` + +### Parallel Execution Strategy + +For `SendAsync` with multiple consumers, use a custom helper to maintain parallel execution without excessive allocations: + +```csharp +// EventBus.SendAsync implementation +List tasks = []; +foreach (Type consumerType in consumerTypes) +{ + foreach (object? consumer in consumerProviders.GetConsumers(consumerType)) + { + tasks.Add(((IConsumer)consumer).ConsumeAsync(eventItem, cancellationToken)); + } +} + +// Use helper method for parallel execution +await WhenAll(tasks).ConfigureAwait(false); + +// Helper method +private static async ValueTask WhenAll(List tasks) +{ + if (tasks.Count == 0) + return; + + if (tasks.Count == 1) + { + await tasks[0].ConfigureAwait(false); + return; + } + + // Only allocate Task[] when truly needed + await Task.WhenAll(tasks.Select(t => t.AsTask())).ConfigureAwait(false); +} +``` + +### Version and Breaking Change + +This is a **BREAKING CHANGE**: +- All consumer implementations must change signatures +- Version bump: 4.x → **5.0.0** (major version) +- Migration guide required + +## Consequences + +### Positive + +✅ **Reduced allocations** - No Task allocation when completing synchronously +✅ **Better performance** - Hot path operations (send/publish) allocate less +✅ **Modern .NET pattern** - Aligns with .NET 5+ best practices +✅ **Scalability** - Less GC pressure under high load +✅ **API surface unchanged** - await still works the same way + +### Negative + +❌ **Breaking change** - All consumers must update signatures +❌ **Migration required** - Users must update to v5.0.0 +❌ **Complexity** - Slightly more complex to implement correctly +❌ **Tooling** - Some older analyzers may not understand ValueTask +❌ **State machine size** - Slightly larger IL for async methods + +### Neutral + +↔️ **Compatibility** - `netstandard2.0` supported via `System.Threading.Tasks.Extensions` +↔️ **Learning curve** - Developers must understand ValueTask semantics +↔️ **Testing** - No impact on test structure + +## Implementation Notes + +### Target Frameworks + +All supported frameworks have ValueTask: +- `net8.0` - Native support +- `netstandard2.0` - Via `System.Threading.Tasks.Extensions` (already referenced) + +### Migration Path + +1. Update all interfaces to return `ValueTask` +2. Update all implementations +3. Update all consumers in tests and demos +4. Update documentation and examples +5. Bump version to 5.0.0 +6. Add migration guide + +### Performance Testing + +Benchmark scenarios: +- Empty consumer list (sync completion) +- Single sync consumer +- Single async consumer +- Multiple mixed consumers + +Expected improvements primarily in sync completion paths. + +## Alternatives Considered + +### Alternative 1: Keep Task +**Rejected** - Does not address allocation overhead in hot paths + +### Alternative 2: Provide both Task and ValueTask +**Rejected** - Doubles API surface, confusing for users + +### Alternative 3: Sequential execution in SendAsync +**Revisited in v5.1.0** - Originally rejected to maintain backward compatibility. Later implemented as a configurable option via `EventBusBuilderOptions.ConsumerExecutionMode` to provide flexibility for use cases where execution order matters or where parallel execution causes issues. + +## References + +- [Understanding the Whys, Whats, and Whens of ValueTask (Microsoft)](https://devblogs.microsoft.com/dotnet/understanding-the-whys-whats-and-whens-of-valuetask/) +- [Task vs ValueTask Performance](https://source.dot.net/#System.Private.CoreLib/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/ValueTask.cs) +- ADR-003: Async-First Design for All Operations + +## Related + +- Supersedes: None +- Relates to: ADR-003 (Async-First Design) +- Breaking change: Requires major version bump (4.x → 5.0.0) diff --git a/docs/architecture/domain/overview.md b/docs/architecture/domain/overview.md new file mode 100644 index 0000000..1021432 --- /dev/null +++ b/docs/architecture/domain/overview.md @@ -0,0 +1,189 @@ +# Domain Overview + +## Introduction + +ReflectionEventing is a library, not a business application, so its "domain" consists of the patterns and concepts related to event-driven architecture and dependency injection. + +## Core Domain Concepts + +### Event Bus + +The central component that enables decoupled communication between parts of an application through events. + +```mermaid +classDiagram + class IEventBus { + <> + +SendAsync~TEvent~(event, cancellationToken) Task + +PublishAsync~TEvent~(event, cancellationToken) Task + } + + class EventBus { + -IConsumerProvider consumerProvider + -IConsumerTypesProvider typesProvider + -IEventsQueue queue + +SendAsync~TEvent~(event, cancellationToken) Task + +PublishAsync~TEvent~(event, cancellationToken) Task + } + + IEventBus <|.. EventBus +``` + +### Consumer + +A component that handles a specific type of event. + +```mermaid +classDiagram + class IConsumer~TEvent~ { + <> + +ConsumeAsync(payload, cancellationToken) Task + } + + note for IConsumer~TEvent~ "Implemented by application code\nto handle specific event types" +``` + +### Consumer Provider + +Abstracts the dependency injection container for resolving consumer instances. + +```mermaid +classDiagram + class IConsumerProvider { + <> + +GetConsumers(consumerType) IEnumerable~object~ + } + + class DependencyInjectionConsumerProvider + class AutofacConsumerProvider + class WindsorConsumerProvider + class NinjectConsumerProvider + class UnityConsumerProvider + + IConsumerProvider <|.. DependencyInjectionConsumerProvider + IConsumerProvider <|.. AutofacConsumerProvider + IConsumerProvider <|.. WindsorConsumerProvider + IConsumerProvider <|.. NinjectConsumerProvider + IConsumerProvider <|.. UnityConsumerProvider +``` + +### Consumer Types Provider + +Maps event types to their registered consumer types. + +```mermaid +classDiagram + class IConsumerTypesProvider { + <> + +GetConsumerTypes(eventType) IEnumerable~Type~ + } + + class HashedConsumerTypesProvider { + -Dictionary~Type,List~Type~~ consumerMap + +GetConsumerTypes(eventType) IEnumerable~Type~ + } + + class HashedPolymorphicConsumerTypesProvider { + -Dictionary~Type,List~Type~~ consumerMap + +GetConsumerTypes(eventType) IEnumerable~Type~ + } + + IConsumerTypesProvider <|.. HashedConsumerTypesProvider + IConsumerTypesProvider <|.. HashedPolymorphicConsumerTypesProvider + + note for HashedPolymorphicConsumerTypesProvider "Supports consuming events\nthrough base types/interfaces" +``` + +### Events Queue + +A queue for asynchronous event processing. + +```mermaid +classDiagram + class IEventsQueue { + <> + +EnqueueAsync(event, cancellationToken) Task + +DequeueAsync(cancellationToken) Task~object~ + +MarkAsCompletedAsync(event, cancellationToken) Task + +MarkAsFailedAsync(event, exception, cancellationToken) Task + } + + class EventsQueue { + -Channel~object~ channel + +EnqueueAsync(event, cancellationToken) Task + +DequeueAsync(cancellationToken) Task~object~ + } + + class FailedEvent { + +object Event + +Exception Exception + +DateTime FailedAt + } + + IEventsQueue <|.. EventsQueue + EventsQueue --> FailedEvent : tracks +``` + +## Domain Relationships + +```mermaid +flowchart TD + subgraph Publishing["Event Publishing"] + Publisher[Publisher Component] + Event[Event Object] + end + + subgraph Bus["Event Bus"] + IEventBus[IEventBus] + EventBus[EventBus] + end + + subgraph Routing["Consumer Routing"] + TypesProvider[IConsumerTypesProvider] + ConsumerProvider[IConsumerProvider] + end + + subgraph Processing["Event Processing"] + Queue[IEventsQueue] + Consumer[IConsumer] + end + + Publisher -->|creates| Event + Publisher -->|sends via| IEventBus + EventBus -->|queries| TypesProvider + EventBus -->|resolves from| ConsumerProvider + EventBus -->|may enqueue to| Queue + ConsumerProvider -->|provides| Consumer + Consumer -->|processes| Event +``` + +## Processing Modes + +The library supports different processing modes for events: + +| Mode | Method | Behavior | +|------|--------|----------| +| **Immediate** | `SendAsync` | Event is processed immediately in the current scope, blocking until all consumers complete | +| **Queued** | `PublishAsync` | Event is added to a queue for background processing, returns immediately | + +## Key Design Decisions + +1. **Interface-based design**: All core components are defined as interfaces, allowing for multiple implementations and easy testing. + +2. **Generic events**: Events are typed using generics (`TEvent`), allowing compile-time type safety. + +3. **Async-first**: All operations are asynchronous, supporting modern async/await patterns. + +4. **DI-agnostic core**: The core library has no dependency on any specific DI container. + +5. **Parallel processing**: Multiple consumers can process the same event in parallel. + +## See Also + +- [Ubiquitous Language](./ubiquitous-language.md) - Terminology definitions +- [Logical Architecture](../views/logical-architecture.md) - Component details +- [ADR-001](../decisions/ADR-001-event-bus-pattern.md) - Event Bus Pattern decision + +--- + +*Last updated: 2026-02-09* diff --git a/docs/architecture/domain/ubiquitous-language.md b/docs/architecture/domain/ubiquitous-language.md new file mode 100644 index 0000000..706f96d --- /dev/null +++ b/docs/architecture/domain/ubiquitous-language.md @@ -0,0 +1,139 @@ +# Ubiquitous Language + +This document defines the key terms and concepts used throughout the ReflectionEventing library. + +## Core Terms + +### Event +A message that represents something that has happened in the application. Events are immutable objects that carry data about the occurrence. + +**Example:** +```csharp +public record BackgroundTicked(int Value); +``` + +### Event Bus +The central message broker that receives events from publishers and routes them to the appropriate consumers. It decouples the publisher from the consumers. + +**In code:** `IEventBus`, `EventBus` + +### Publisher +Any component that sends events to the event bus. Publishers don't know about consumers. + +**Action:** Calls `IEventBus.SendAsync()` or `IEventBus.PublishAsync()` + +### Consumer +A component that handles a specific type of event. Consumers implement `IConsumer` and process events asynchronously. + +**In code:** `IConsumer` + +**Example:** +```csharp +public class MyConsumer : IConsumer +{ + public ValueTask ConsumeAsync(BackgroundTicked payload, CancellationToken cancellationToken) + { + // Handle the event + return ValueTask.CompletedTask; + } +} +``` + +### Consumer Provider +An abstraction that resolves consumer instances from the dependency injection container. Each DI container has its own implementation. + +**In code:** `IConsumerProvider` + +### Consumer Types Provider +A component that maps event types to their registered consumer types. Uses hashing for O(1) lookup performance. + +**In code:** `IConsumerTypesProvider`, `HashedConsumerTypesProvider` + +### Events Queue +A channel-based queue for asynchronous event processing. Events published via `PublishAsync` are added to this queue for background processing. + +**In code:** `IEventsQueue`, `EventsQueue` + +## Operations + +### Send +Immediately dispatches an event to all registered consumers within the current scope. Blocks until all consumers have completed processing. + +**Method:** `IEventBus.SendAsync()` + +**Characteristics:** +- Synchronous (waits for completion) +- Same scope as caller +- Exceptions propagate to caller + +### Publish +Adds an event to the queue for background processing. Returns immediately without waiting for consumers. + +**Method:** `IEventBus.PublishAsync()` + +**Characteristics:** +- Asynchronous (fire-and-forget) +- Different scope (background processing) +- Exceptions are captured as `FailedEvent` + +### Consume +The act of handling an event by a consumer. Each consumer processes the event in its `ConsumeAsync` method. + +**Method:** `IConsumer.ConsumeAsync()` + +## Patterns + +### Observer Pattern +The underlying design pattern where the event bus acts as the subject and consumers are observers. + +### Builder Pattern +Used for configuring the event bus. The `EventBusBuilder` provides a fluent API for registration. + +**Example:** +```csharp +services.AddEventBus(builder => +{ + builder.AddConsumer(); +}); +``` + +### Adapter Pattern +Each DI container integration is an adapter that implements the common `IConsumerProvider` interface. + +## Components + +| Term | Definition | +|------|------------| +| **EventBusBuilder** | Fluent builder for configuring event bus options and registering consumers | +| **EventBusBuilderOptions** | Configuration options for the event bus | +| **ProcessingMode** | Enum defining how events are processed (Sequential, Parallel, Queued) | +| **FailedEvent** | Wrapper for events that failed during processing, includes exception details | +| **EventBusException** | Exception thrown when event bus operations fail | +| **QueueException** | Exception thrown when queue operations fail | + +## Naming Conventions + +| Pattern | Convention | Example | +|---------|------------|---------| +| Event classes | Past tense or noun phrase | `BackgroundTicked`, `OrderCreated` | +| Consumer classes | `{Domain}{Action}Consumer` | `OrderCreatedConsumer` | +| Provider interfaces | `I{What}Provider` | `IConsumerProvider` | +| Builder classes | `{What}Builder` | `EventBusBuilder` | +| Extension classes | `{Type}Extensions` | `ServiceCollectionExtensions` | + +## Acronyms + +| Acronym | Meaning | +|---------|---------| +| **DI** | Dependency Injection | +| **IoC** | Inversion of Control | +| **AOT** | Ahead-of-Time (compilation) | + +## See Also + +- [Domain Overview](./overview.md) - Core concepts +- [Logical Architecture](../views/logical-architecture.md) - Component details + +--- + +*Last updated: 2026-02-09* diff --git a/docs/architecture/views/context.md b/docs/architecture/views/context.md new file mode 100644 index 0000000..0288e29 --- /dev/null +++ b/docs/architecture/views/context.md @@ -0,0 +1,140 @@ +# System Context View + +> C4 Model - Level 1: System Context Diagram + +## Overview + +This view shows the ReflectionEventing library in the context of its environment, including the applications that use it and the DI containers it integrates with. + +## Context Diagram + +```mermaid +C4Context + title System Context Diagram for ReflectionEventing + + Person(developer, "Developer", "Application developer using the library") + + System_Boundary(app, "Client Application") { + System(clientApp, "Client Application", "WPF, WinForms, Console, or ASP.NET Core application") + } + + System(reflectionEventing, "ReflectionEventing", "Event bus library for decoupled communication via DI and reflection") + + System_Ext(msDI, "Microsoft.Extensions.DependencyInjection", "Microsoft DI container") + System_Ext(autofac, "Autofac", "Autofac IoC container") + System_Ext(castle, "Castle Windsor", "Castle Windsor IoC container") + System_Ext(ninject, "Ninject", "Ninject IoC container") + System_Ext(unity, "Unity", "Unity IoC container") + System_Ext(nuget, "NuGet.org", "Package distribution") + + Rel(developer, clientApp, "Builds") + Rel(clientApp, reflectionEventing, "Uses") + Rel(reflectionEventing, msDI, "Integrates with") + Rel(reflectionEventing, autofac, "Integrates with") + Rel(reflectionEventing, castle, "Integrates with") + Rel(reflectionEventing, ninject, "Integrates with") + Rel(reflectionEventing, unity, "Integrates with") + Rel(nuget, reflectionEventing, "Distributes") +``` + +## System Description + +| Element | Type | Description | +|---------|------|-------------| +| ReflectionEventing | Library | Core event bus library enabling decoupled pub/sub communication | +| Client Application | System | Any .NET application consuming the library | +| DI Containers | External Systems | IoC containers for dependency resolution | +| NuGet.org | Distribution | Package hosting and distribution | + +## Users and Actors + +| Actor | Description | Interactions | +|-------|-------------|--------------| +| Developer | Application developer | Integrates library, registers consumers, publishes events | +| Consumer | Event handler component | Receives and processes events | +| Publisher | Event source component | Creates and publishes events | + +## Integration Points + +### Supported DI Containers + +| Container | Package | Minimum Version | +|-----------|---------|-----------------| +| Microsoft.Extensions.DependencyInjection | ReflectionEventing.DependencyInjection | 3.1.0+ | +| Autofac | ReflectionEventing.Autofac | 4.0.0+ | +| Castle Windsor | ReflectionEventing.Castle.Windsor | 6.0.0+ | +| Ninject | ReflectionEventing.Ninject | 3.0.1+ | +| Unity | ReflectionEventing.Unity | 5.11.0+ | + +### Target Frameworks + +| Framework | Support Level | +|-----------|--------------| +| .NET 9.0 | Full (AOT enabled) | +| .NET 8.0 | Full (AOT enabled) | +| .NET 6.0 | Full | +| .NET Standard 2.0 | Full | +| .NET Framework 4.6.2 | Full | +| .NET Framework 4.7.2 | Full | + +## System Boundaries + +### What's Inside the Boundary + +- Event bus core implementation +- Consumer discovery and registration +- Event routing and dispatching +- Queue-based async event processing +- DI container adapters +- Observability (OpenTelemetry metrics and traces) + +### What's Outside the Boundary + +- DI container implementations (provided by external libraries) +- Application-specific event types +- Consumer implementations +- Application hosting and lifecycle + +## Usage Pattern + +```mermaid +sequenceDiagram + participant App as Application + participant DI as DI Container + participant Bus as IEventBus + participant Provider as ConsumerProvider + participant Consumer as IConsumer + + App->>DI: Register services & consumers + App->>DI: AddEventBus() + + Note over App,Consumer: At runtime + + App->>Bus: SendAsync(event) + Bus->>Provider: GetConsumerTypes(eventType) + Provider->>DI: Resolve consumers + DI-->>Provider: Consumer instances + Bus->>Consumer: ConsumeAsync(event) + Consumer-->>Bus: Task completed + Bus-->>App: Task completed +``` + +## Quality Attributes + +| Attribute | Requirement | How Addressed | +|-----------|-------------|---------------| +| Availability | High (library shouldn't crash host) | Exception handling, CancellationToken support | +| Scalability | Handle many events efficiently | Parallel processing, Channel-based queuing | +| Performance | Low latency event dispatch | Hashed consumer lookup (O(1)), async/await | +| Extensibility | Support multiple DI containers | Adapter pattern, interface-based design | +| Testability | Easy to mock and test | Interface-based design, NSubstitute support | + +## See Also + +- [Logical Architecture](./logical-architecture.md) - Component details +- [Domain Overview](../domain/overview.md) - Core concepts +- [ADR-001](../decisions/ADR-001-event-bus-pattern.md) - Event Bus Pattern decision + +--- + +*Last updated: 2026-02-09* diff --git a/docs/architecture/views/logical-architecture.md b/docs/architecture/views/logical-architecture.md new file mode 100644 index 0000000..66cca23 --- /dev/null +++ b/docs/architecture/views/logical-architecture.md @@ -0,0 +1,307 @@ +# Logical Architecture View + +> C4 Model - Level 2: Container Diagram & Level 3: Component Diagram + +## Overview + +This view presents the logical architecture of ReflectionEventing, showing the main modules (packages) and their components. + +## Container Diagram (C4 Level 2) + +```mermaid +C4Container + title Container Diagram for ReflectionEventing + + Container_Boundary(core, "Core Library") { + Container(eventBus, "ReflectionEventing", ".NET Library", "Core event bus implementation with interfaces and abstractions") + } + + Container_Boundary(integrations, "DI Integrations") { + Container(msDI, "ReflectionEventing.DependencyInjection", ".NET Library", "Microsoft.Extensions.DI integration") + Container(autofac, "ReflectionEventing.Autofac", ".NET Library", "Autofac integration") + Container(castle, "ReflectionEventing.Castle.Windsor", ".NET Library", "Castle Windsor integration") + Container(ninject, "ReflectionEventing.Ninject", ".NET Library", "Ninject integration") + Container(unity, "ReflectionEventing.Unity", ".NET Library", "Unity integration") + } + + Rel(msDI, eventBus, "References") + Rel(autofac, eventBus, "References") + Rel(castle, eventBus, "References") + Rel(ninject, eventBus, "References") + Rel(unity, eventBus, "References") +``` + +## Module Overview + +| Module | Technology | Purpose | Responsibility | +|--------|------------|---------|----------------| +| ReflectionEventing | .NET Standard 2.0+ | Core library | Event bus abstraction, interfaces, base implementation | +| ReflectionEventing.DependencyInjection | .NET Standard 2.0+ | MS DI adapter | Integration with Microsoft.Extensions.DependencyInjection | +| ReflectionEventing.Autofac | .NET Standard 2.0+ | Autofac adapter | Integration with Autofac IoC container | +| ReflectionEventing.Castle.Windsor | .NET Standard 2.0+ | Castle adapter | Integration with Castle Windsor container | +| ReflectionEventing.Ninject | .NET Standard 2.0+ | Ninject adapter | Integration with Ninject container | +| ReflectionEventing.Unity | .NET Standard 2.0+ | Unity adapter | Integration with Unity container | + +## Core Component Diagram (C4 Level 3) + +```mermaid +flowchart TB + subgraph ReflectionEventing["ReflectionEventing (Core Library)"] + IEventBus["IEventBus
«interface»"] + EventBus["EventBus
«class»"] + + IConsumer["IConsumer<TEvent>
«interface»"] + + IConsumerProvider["IConsumerProvider
«interface»"] + IConsumerTypesProvider["IConsumerTypesProvider
«interface»"] + + HashedProvider["HashedConsumerTypesProvider
«class»"] + PolymorphicProvider["HashedPolymorphicConsumerTypesProvider
«class»"] + + EventBusBuilder["EventBusBuilder
«class»"] + + subgraph Queues["Queues"] + IEventsQueue["IEventsQueue
«interface»"] + EventsQueue["EventsQueue
«class»"] + FailedEvent["FailedEvent
«class»"] + end + + EventBus -->|implements| IEventBus + EventBus -->|uses| IConsumerProvider + EventBus -->|uses| IConsumerTypesProvider + EventBus -->|uses| IEventsQueue + + HashedProvider -->|implements| IConsumerTypesProvider + PolymorphicProvider -->|implements| IConsumerTypesProvider + + EventsQueue -->|implements| IEventsQueue + EventsQueue -->|stores| FailedEvent + + EventBusBuilder -->|creates| EventBus + end +``` + +## Component Responsibilities + +| Component | Responsibility | Dependencies | +|-----------|----------------|--------------| +| `IEventBus` | Contract for event publishing | None | +| `EventBus` | Routes events to consumers | IConsumerProvider, IConsumerTypesProvider, IEventsQueue | +| `IConsumer` | Contract for event consumption | None | +| `IConsumerProvider` | Resolves consumer instances from DI | None | +| `IConsumerTypesProvider` | Maps event types to consumer types | None | +| `HashedConsumerTypesProvider` | O(1) lookup for consumer types | None | +| `HashedPolymorphicConsumerTypesProvider` | Supports polymorphic event handling | None | +| `EventBusBuilder` | Fluent API for configuration | All above | +| `IEventsQueue` | Contract for event queuing | None | +| `EventsQueue` | Channel-based event queue | System.Threading.Channels | +| `FailedEvent` | Tracks failed event processing | None | + +## DI Integration Pattern + +Each DI integration package follows the same pattern: + +```mermaid +flowchart LR + subgraph DIPackage["DI Integration Package"] + ContainerAdapter["ConsumerProvider
«class»"] + BuilderExt["EventBusBuilder
«class»"] + Extensions["ContainerExtensions
«static class»"] + end + + subgraph Core["ReflectionEventing Core"] + IConsumerProvider2["IConsumerProvider"] + EventBusBuilder2["EventBusBuilder"] + end + + subgraph DIContainer["DI Container"] + Container["IContainer/IServiceProvider"] + end + + ContainerAdapter -->|implements| IConsumerProvider2 + BuilderExt -->|extends| EventBusBuilder2 + ContainerAdapter -->|uses| Container + Extensions -->|configures| Container +``` + +## Module Structure + +``` +ReflectionEventing/ +├── src/ +│ ├── ReflectionEventing/ +│ │ ├── IEventBus.cs +│ │ ├── EventBus.cs +│ │ ├── IConsumer.cs +│ │ ├── IConsumerProvider.cs +│ │ ├── IConsumerTypesProvider.cs +│ │ ├── HashedConsumerTypesProvider.cs +│ │ ├── HashedPolymorphicConsumerTypesProvider.cs +│ │ ├── EventBusBuilder.cs +│ │ ├── EventBusBuilderOptions.cs +│ │ ├── EventBusException.cs +│ │ ├── ProcessingMode.cs +│ │ └── Queues/ +│ │ ├── IEventsQueue.cs +│ │ ├── EventsQueue.cs +│ │ ├── FailedEvent.cs +│ │ └── QueueException.cs +│ │ +│ ├── ReflectionEventing.DependencyInjection/ +│ │ ├── DependencyInjectionConsumerProvider.cs +│ │ ├── DependencyInjectionEventBus.cs +│ │ ├── DependencyInjectionEventBusBuilder.cs +│ │ ├── ServiceCollectionExtensions.cs +│ │ ├── EventBusBuilderExtensions.cs +│ │ └── Configuration/ +│ │ └── QueueProcessorOptionsProvider.cs +│ │ +│ ├── ReflectionEventing.Autofac/ +│ │ ├── AutofacConsumerProvider.cs +│ │ ├── AutofacEventBusBuilder.cs +│ │ └── ContainerBuilderExtensions.cs +│ │ +│ ├── ReflectionEventing.Castle.Windsor/ +│ │ ├── WindsorConsumerProvider.cs +│ │ ├── WindsorEventBusBuilder.cs +│ │ └── EventBusInstaller.cs +│ │ +│ ├── ReflectionEventing.Ninject/ +│ │ ├── NinjectConsumerProvider.cs +│ │ ├── NinjectEventBusBuilder.cs +│ │ └── EventBusModule.cs +│ │ +│ └── ReflectionEventing.Unity/ +│ ├── UnityConsumerProvider.cs +│ ├── UnityEventBusBuilder.cs +│ └── UnityContainerExtensions.cs +│ +└── tests/ + ├── ReflectionEventing.UnitTests/ + ├── ReflectionEventing.DependencyInjection.UnitTests/ + ├── ReflectionEventing.Autofac.UnitTests/ + ├── ReflectionEventing.Castle.Windsor.UnitTests/ + ├── ReflectionEventing.Ninject.UnitTests/ + └── ReflectionEventing.Unity.UnitTests/ +``` + +## Module Dependencies + +```mermaid +flowchart TD + Core[ReflectionEventing] + + MSDI[ReflectionEventing.DependencyInjection] + Autofac[ReflectionEventing.Autofac] + Castle[ReflectionEventing.Castle.Windsor] + Ninject[ReflectionEventing.Ninject] + Unity[ReflectionEventing.Unity] + + MSDI --> Core + Autofac --> Core + Castle --> Core + Ninject --> Core + Unity --> Core + + subgraph External["External Dependencies"] + MSExtDI["Microsoft.Extensions.DI"] + AutofacLib["Autofac"] + CastleLib["Castle.Windsor"] + NinjectLib["Ninject"] + UnityLib["Unity"] + end + + MSDI --> MSExtDI + Autofac --> AutofacLib + Castle --> CastleLib + Ninject --> NinjectLib + Unity --> UnityLib +``` + +## Communication Patterns + +### Event Flow - SendAsync (Synchronous) + +```mermaid +sequenceDiagram + participant Publisher + participant EventBus + participant TypesProvider as ConsumerTypesProvider + participant ConsumerProvider + participant Consumer1 as Consumer 1 + participant Consumer2 as Consumer 2 + + Publisher->>EventBus: SendAsync(event) + EventBus->>TypesProvider: GetConsumerTypes(eventType) + TypesProvider-->>EventBus: [Consumer1Type, Consumer2Type] + + EventBus->>ConsumerProvider: GetConsumers(Consumer1Type) + ConsumerProvider-->>EventBus: [consumer1Instance] + + EventBus->>ConsumerProvider: GetConsumers(Consumer2Type) + ConsumerProvider-->>EventBus: [consumer2Instance] + + par Parallel Execution + EventBus->>Consumer1: ConsumeAsync(event) + EventBus->>Consumer2: ConsumeAsync(event) + end + + Consumer1-->>EventBus: Task completed + Consumer2-->>EventBus: Task completed + + EventBus-->>Publisher: Task completed +``` + +### Event Flow - PublishAsync (Queued) + +```mermaid +sequenceDiagram + participant Publisher + participant EventBus + participant Queue as EventsQueue + participant Processor as QueueProcessor + participant Consumer + + Publisher->>EventBus: PublishAsync(event) + EventBus->>Queue: EnqueueAsync(event) + Queue-->>EventBus: Task completed + EventBus-->>Publisher: Task completed (event queued) + + Note over Processor: Background processing + + Processor->>Queue: DequeueAsync() + Queue-->>Processor: event + Processor->>Consumer: ConsumeAsync(event) + Consumer-->>Processor: Task completed +``` + +## Technology Stack + +### Runtime + +| Component | Technology | Version | +|-----------|------------|---------| +| Runtime | .NET 9/8/6/Standard 2.0/FX 4.6.2+ | Multiple | +| Language | C# | 13.0 | +| Async | System.Threading.Channels | 9.0.0 | +| Observability | System.Diagnostics.DiagnosticSource | 6.0.0 | + +### Development + +| Tool | Purpose | +|------|---------| +| xUnit | Unit testing | +| NSubstitute | Mocking | +| AwesomeAssertions | Fluent assertions | +| StyleCop | Code style enforcement | +| PolySharp | Polyfills for older frameworks | + +## See Also + +- [Context View](./context.md) - System context +- [Domain Model](../domain/overview.md) - Business domain +- [ADR-002](../decisions/ADR-002-multi-di-container-support.md) - Multi DI Container decision + +--- + +*Last updated: 2026-02-09* diff --git a/docs/documentation/getting-started.md b/docs/documentation/getting-started.md index 8a9529e..91fe325 100644 --- a/docs/documentation/getting-started.md +++ b/docs/documentation/getting-started.md @@ -28,10 +28,10 @@ public record TestEvent; public class TestConsumer : IConsumer { - public Task ConsumeAsync(TestEvent payload, CancellationToken cancellationToken) + public ValueTask ConsumeAsync(TestEvent payload, CancellationToken cancellationToken) { // Handle the event - return Task.CompletedTask; + return ValueTask.CompletedTask; } } ``` diff --git a/docs/index.md b/docs/index.md index 4a9b5db..09fbf5e 100644 --- a/docs/index.md +++ b/docs/index.md @@ -87,11 +87,11 @@ public partial class MainWindowViewModel : ObservableObject, IConsumer -/// Provides event consumers for Autofac. -/// -public class AutofacConsumerProvider(ILifetimeScope lifetimeScope) : IConsumerProvider -{ - /// - public IEnumerable GetConsumers(Type consumerType) - { - if (consumerType is null) - { - throw new ArgumentNullException(nameof(consumerType)); - } - - return new List { lifetimeScope.Resolve(consumerType) }; - } -} diff --git a/src/ReflectionEventing.Autofac/AutofacEventBusBuilder.cs b/src/ReflectionEventing.Autofac/AutofacEventBusBuilder.cs deleted file mode 100644 index b50ccc2..0000000 --- a/src/ReflectionEventing.Autofac/AutofacEventBusBuilder.cs +++ /dev/null @@ -1,13 +0,0 @@ -// This Source Code Form is subject to the terms of the MIT License. -// If a copy of the MIT was not distributed with this file, You can obtain one at https://opensource.org/licenses/MIT. -// Copyright (C) Leszek Pomianowski and ReflectionEventing Contributors. -// All Rights Reserved. - -using Autofac; - -namespace ReflectionEventing.Autofac; - -/// -/// Represents a builder for configuring the event bus with Autofac. -/// -public class AutofacEventBusBuilder(ContainerBuilder builder) : EventBusBuilder; diff --git a/src/ReflectionEventing.Autofac/ContainerBuilderExtensions.cs b/src/ReflectionEventing.Autofac/ContainerBuilderExtensions.cs deleted file mode 100644 index ae296e7..0000000 --- a/src/ReflectionEventing.Autofac/ContainerBuilderExtensions.cs +++ /dev/null @@ -1,51 +0,0 @@ -// This Source Code Form is subject to the terms of the MIT License. -// If a copy of the MIT was not distributed with this file, You can obtain one at https://opensource.org/licenses/MIT. -// Copyright (C) Leszek Pomianowski and ReflectionEventing Contributors. -// All Rights Reserved. - -using Autofac; -using ReflectionEventing.Queues; - -namespace ReflectionEventing.Autofac; - -/// -/// Provides extension methods for the class. -/// -public static class ContainerBuilderExtensions -{ - /// - /// Adds the event bus and its related services to the specified Autofac container builder. - /// - /// The to add the event bus to. - /// A delegate that configures the . - /// The same container builder so that multiple calls can be chained. - /// - /// This method adds a singleton service of type that uses a with the consumers from the event bus builder. - /// It also adds a scoped service of type that uses the class. - /// - public static ContainerBuilder AddEventBus( - this ContainerBuilder builder, - Action configure - ) - { - AutofacEventBusBuilder autofacBuilder = new(builder); - - configure(autofacBuilder); - - _ = builder - .RegisterInstance(autofacBuilder.BuildTypesProvider()) - .As() - .SingleInstance(); - - _ = builder.RegisterType().As().SingleInstance(); - - _ = builder - .RegisterType() - .As() - .InstancePerLifetimeScope(); - - _ = builder.RegisterType().As().InstancePerLifetimeScope(); - - return builder; - } -} diff --git a/src/ReflectionEventing.Autofac/GlobalUsings.cs b/src/ReflectionEventing.Autofac/GlobalUsings.cs deleted file mode 100644 index 0012f8a..0000000 --- a/src/ReflectionEventing.Autofac/GlobalUsings.cs +++ /dev/null @@ -1,7 +0,0 @@ -// This Source Code Form is subject to the terms of the MIT License. -// If a copy of the MIT was not distributed with this file, You can obtain one at https://opensource.org/licenses/MIT. -// Copyright (C) Leszek Pomianowski and ReflectionEventing Contributors. -// All Rights Reserved. - -global using System; -global using System.Collections.Generic; diff --git a/src/ReflectionEventing.Autofac/ReflectionEventing.Autofac.csproj b/src/ReflectionEventing.Autofac/ReflectionEventing.Autofac.csproj deleted file mode 100644 index e8f20d4..0000000 --- a/src/ReflectionEventing.Autofac/ReflectionEventing.Autofac.csproj +++ /dev/null @@ -1,44 +0,0 @@ - - - - ReflectionEventing.Autofac - net9.0;net8.0;net6.0;netstandard2.0;net462;net472 - true - true - true - $(CommonTags);autofac;module - Autofac Container module with ReflectionEventing, which promotes better Inversion of Control (IoC), reducing coupling and enhancing the modularity and flexibility of your applications. - - - - true - true - Speed - - - - - - System.Diagnostics.CodeAnalysis.DoesNotReturnAttribute; - System.Diagnostics.CodeAnalysis.MaybeNullWhenAttribute; - System.Diagnostics.CodeAnalysis.MemberNotNullAttribute; - System.Diagnostics.CodeAnalysis.NotNullAttribute; - System.Diagnostics.CodeAnalysis.NotNullIfNotNullAttribute; - System.Diagnostics.CodeAnalysis.NotNullWhenAttribute; - System.Diagnostics.CodeAnalysis.RequiresUnreferencedCodeAttribute; - System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessageAttribute; - System.Runtime.CompilerServices.CallerArgumentExpressionAttribute; - System.Runtime.CompilerServices.IsExternalInit; - System.Runtime.CompilerServices.SkipLocalsInitAttribute; - - - - - - - - - - - - diff --git a/src/ReflectionEventing.Castle.Windsor/EventBusInstaller.cs b/src/ReflectionEventing.Castle.Windsor/EventBusInstaller.cs deleted file mode 100644 index d68adb6..0000000 --- a/src/ReflectionEventing.Castle.Windsor/EventBusInstaller.cs +++ /dev/null @@ -1,47 +0,0 @@ -// This Source Code Form is subject to the terms of the MIT License. -// If a copy of the MIT was not distributed with this file, You can obtain one at https://opensource.org/licenses/MIT. -// Copyright (C) Leszek Pomianowski and ReflectionEventing Contributors. -// All Rights Reserved. - -using Castle.MicroKernel.Registration; -using Castle.MicroKernel.SubSystems.Configuration; -using Castle.Windsor; -using ReflectionEventing.Queues; - -namespace ReflectionEventing.Castle.Windsor; - -/// -/// Represents a Castle Windsor installer for configuring the event bus and its related services. -/// -public class EventBusInstaller(Action configure) : IWindsorInstaller -{ - /// - /// Adds the event bus and its related services to the specified Windsor container. - /// - /// The to add the event bus to. - /// The for the container. - public void Install(IWindsorContainer container, IConfigurationStore store) - { - WindsorEventBusBuilder builder = new(container); - - if (!container.Kernel.HasComponent(typeof(IWindsorContainer))) - { - _ = container.Register(Component.For().Instance(container)); - } - - configure(builder); - - _ = container.Register( - Component - .For() - .Instance(builder.BuildTypesProvider()) - .LifestyleScoped(), - Component.For().ImplementedBy().LifestyleSingleton(), - Component - .For() - .ImplementedBy() - .LifestyleScoped(), - Component.For().ImplementedBy().LifestyleTransient() - ); - } -} diff --git a/src/ReflectionEventing.Castle.Windsor/GlobalUsings.cs b/src/ReflectionEventing.Castle.Windsor/GlobalUsings.cs deleted file mode 100644 index 0012f8a..0000000 --- a/src/ReflectionEventing.Castle.Windsor/GlobalUsings.cs +++ /dev/null @@ -1,7 +0,0 @@ -// This Source Code Form is subject to the terms of the MIT License. -// If a copy of the MIT was not distributed with this file, You can obtain one at https://opensource.org/licenses/MIT. -// Copyright (C) Leszek Pomianowski and ReflectionEventing Contributors. -// All Rights Reserved. - -global using System; -global using System.Collections.Generic; diff --git a/src/ReflectionEventing.Castle.Windsor/ReflectionEventing.Castle.Windsor.csproj b/src/ReflectionEventing.Castle.Windsor/ReflectionEventing.Castle.Windsor.csproj deleted file mode 100644 index b180158..0000000 --- a/src/ReflectionEventing.Castle.Windsor/ReflectionEventing.Castle.Windsor.csproj +++ /dev/null @@ -1,44 +0,0 @@ - - - - ReflectionEventing.Castle.Windsor - net9.0;net8.0;net6.0;netstandard2.0;net462;net472 - true - true - true - $(CommonTags);castle;windsor;installer - Castle Windsor installer with ReflectionEventing, which promotes better Inversion of Control (IoC), reducing coupling and enhancing the modularity and flexibility of your applications. - - - - true - true - Speed - - - - - - System.Diagnostics.CodeAnalysis.DoesNotReturnAttribute; - System.Diagnostics.CodeAnalysis.MaybeNullWhenAttribute; - System.Diagnostics.CodeAnalysis.MemberNotNullAttribute; - System.Diagnostics.CodeAnalysis.NotNullAttribute; - System.Diagnostics.CodeAnalysis.NotNullIfNotNullAttribute; - System.Diagnostics.CodeAnalysis.NotNullWhenAttribute; - System.Diagnostics.CodeAnalysis.RequiresUnreferencedCodeAttribute; - System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessageAttribute; - System.Runtime.CompilerServices.CallerArgumentExpressionAttribute; - System.Runtime.CompilerServices.IsExternalInit; - System.Runtime.CompilerServices.SkipLocalsInitAttribute; - - - - - - - - - - - - diff --git a/src/ReflectionEventing.Castle.Windsor/WindsorConsumerProvider.cs b/src/ReflectionEventing.Castle.Windsor/WindsorConsumerProvider.cs deleted file mode 100644 index b8b3ed7..0000000 --- a/src/ReflectionEventing.Castle.Windsor/WindsorConsumerProvider.cs +++ /dev/null @@ -1,25 +0,0 @@ -// This Source Code Form is subject to the terms of the MIT License. -// If a copy of the MIT was not distributed with this file, You can obtain one at https://opensource.org/licenses/MIT. -// Copyright (C) Leszek Pomianowski and ReflectionEventing Contributors. -// All Rights Reserved. - -using Castle.Windsor; - -namespace ReflectionEventing.Castle.Windsor; - -/// -/// Represents a provider for retrieving event consumers from Castle Windsor's IoC container. -/// -public class WindsorConsumerProvider(IWindsorContainer container) : IConsumerProvider -{ - /// - public IEnumerable GetConsumers(Type consumerType) - { - if (consumerType is null) - { - throw new ArgumentNullException(nameof(consumerType)); - } - - return container.ResolveAll(consumerType) as object[] ?? []; - } -} diff --git a/src/ReflectionEventing.Castle.Windsor/WindsorEventBusBuilder.cs b/src/ReflectionEventing.Castle.Windsor/WindsorEventBusBuilder.cs deleted file mode 100644 index 8f8a23f..0000000 --- a/src/ReflectionEventing.Castle.Windsor/WindsorEventBusBuilder.cs +++ /dev/null @@ -1,33 +0,0 @@ -// This Source Code Form is subject to the terms of the MIT License. -// If a copy of the MIT was not distributed with this file, You can obtain one at https://opensource.org/licenses/MIT. -// Copyright (C) Leszek Pomianowski and ReflectionEventing Contributors. -// All Rights Reserved. - -using System.Diagnostics.CodeAnalysis; -using Castle.Windsor; - -namespace ReflectionEventing.Castle.Windsor; - -/// -/// Represents a builder for configuring the event bus with Castle Windsor's IoC container. -/// -public class WindsorEventBusBuilder(IWindsorContainer container) : EventBusBuilder -{ - /// - public override EventBusBuilder AddConsumer( -#if NET5_0_OR_GREATER - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] -#endif - Type consumerType - ) - { - if (!container.Kernel.HasComponent(consumerType)) - { - throw new InvalidOperationException( - "Event consumer must be registered in the container." - ); - } - - return base.AddConsumer(consumerType); - } -} diff --git a/src/ReflectionEventing.Demo.Wpf/App.xaml.cs b/src/ReflectionEventing.Demo.Wpf/App.xaml.cs index bab575d..de757a3 100644 --- a/src/ReflectionEventing.Demo.Wpf/App.xaml.cs +++ b/src/ReflectionEventing.Demo.Wpf/App.xaml.cs @@ -35,6 +35,7 @@ public partial class App : Application { e.Options.UseEventPolymorphism = true; e.Options.UseEventsQueue = true; + e.Options.ConsumerExecutionMode = ProcessingMode.Sequential; e.Options.QueueMode = ProcessingMode.Parallel; e.UseBackgroundService(); diff --git a/src/ReflectionEventing.Demo.Wpf/ReflectionEventing.Demo.Wpf.csproj b/src/ReflectionEventing.Demo.Wpf/ReflectionEventing.Demo.Wpf.csproj index dc935ba..88daf5f 100644 --- a/src/ReflectionEventing.Demo.Wpf/ReflectionEventing.Demo.Wpf.csproj +++ b/src/ReflectionEventing.Demo.Wpf/ReflectionEventing.Demo.Wpf.csproj @@ -2,7 +2,7 @@ WinExe - net9.0-windows + net10.0-windows true true false diff --git a/src/ReflectionEventing.Demo.Wpf/Services/ApplicationHostService.cs b/src/ReflectionEventing.Demo.Wpf/Services/ApplicationHostService.cs index 5fcd808..87c81bf 100644 --- a/src/ReflectionEventing.Demo.Wpf/Services/ApplicationHostService.cs +++ b/src/ReflectionEventing.Demo.Wpf/Services/ApplicationHostService.cs @@ -11,9 +11,9 @@ internal sealed class ApplicationHostService(IServiceProvider serviceProvider) : /// Triggered when the application host is ready to start the service. /// /// Indicates that the start process has been aborted. - public async Task StartAsync(CancellationToken cancellationToken) + public Task StartAsync(CancellationToken cancellationToken) { - await HandleActivationAsync(); + return HandleActivationAsync(); } /// @@ -28,7 +28,7 @@ public Task StopAsync(CancellationToken cancellationToken) /// /// Creates main window during activation. /// - private async Task HandleActivationAsync() + private Task HandleActivationAsync() { if (!Application.Current.Windows.OfType().Any()) { @@ -36,6 +36,6 @@ private async Task HandleActivationAsync() mainWindow!.Show(); } - await Task.CompletedTask; + return Task.CompletedTask; } } diff --git a/src/ReflectionEventing.Demo.Wpf/Services/BackgroundTickService.cs b/src/ReflectionEventing.Demo.Wpf/Services/BackgroundTickService.cs index f7c6218..6dd6403 100644 --- a/src/ReflectionEventing.Demo.Wpf/Services/BackgroundTickService.cs +++ b/src/ReflectionEventing.Demo.Wpf/Services/BackgroundTickService.cs @@ -34,12 +34,11 @@ private async Task TickInBackground(CancellationToken cancellationToken) while (!cancellationToken.IsCancellationRequested) { - await eventBus.SendAsync( - new BackgroundTicked(random.Next(10, 1001)), - cancellationToken - ); + await Task.Delay(TickRateInMilliseconds, cancellationToken).ConfigureAwait(false); - await Task.Delay(TickRateInMilliseconds, cancellationToken); + await eventBus + .SendAsync(new BackgroundTicked(random.Next(10, 1001)), cancellationToken) + .ConfigureAwait(false); } } } diff --git a/src/ReflectionEventing.Demo.Wpf/ViewModels/MainWindowViewModel.cs b/src/ReflectionEventing.Demo.Wpf/ViewModels/MainWindowViewModel.cs index 30420a2..c65eeec 100644 --- a/src/ReflectionEventing.Demo.Wpf/ViewModels/MainWindowViewModel.cs +++ b/src/ReflectionEventing.Demo.Wpf/ViewModels/MainWindowViewModel.cs @@ -21,11 +21,11 @@ public partial class MainWindowViewModel(IEventBus eventBus, ILogger - public async Task ConsumeAsync(ITickedEvent payload, CancellationToken cancellationToken) + public ValueTask ConsumeAsync(ITickedEvent payload, CancellationToken cancellationToken) { int tickValue = payload.Value; - await DispatchAsync( + return DispatchAsync( () => { CurrentTick = tickValue; @@ -35,17 +35,17 @@ await DispatchAsync( } /// - public async Task ConsumeAsync(OtherEvent payload, CancellationToken cancellationToken) + public ValueTask ConsumeAsync(OtherEvent payload, CancellationToken cancellationToken) { logger.LogInformation("Received {Event} event.", nameof(OtherEvent)); - await Task.CompletedTask; + return default; } /// - public async Task ConsumeAsync(AsyncQueuedEvent payload, CancellationToken cancellationToken) + public ValueTask ConsumeAsync(AsyncQueuedEvent payload, CancellationToken cancellationToken) { - await DispatchAsync( + return DispatchAsync( () => { QueueCount++; diff --git a/src/ReflectionEventing.Demo.Wpf/ViewModels/ViewModel.cs b/src/ReflectionEventing.Demo.Wpf/ViewModels/ViewModel.cs index 11bda4f..2ab9e30 100644 --- a/src/ReflectionEventing.Demo.Wpf/ViewModels/ViewModel.cs +++ b/src/ReflectionEventing.Demo.Wpf/ViewModels/ViewModel.cs @@ -3,6 +3,8 @@ // Copyright (C) Leszek Pomianowski and ReflectionEventing Contributors. // All Rights Reserved. +using System.Windows.Threading; + namespace ReflectionEventing.Demo.Wpf.ViewModels; /// @@ -14,23 +16,44 @@ namespace ReflectionEventing.Demo.Wpf.ViewModels; public abstract class ViewModel : ObservableObject { /// - /// Dispatches the specified action on the UI thread. + /// Invokes the specified action on the UI thread asynchronously. + /// If already on the UI thread, executes synchronously. /// - /// The action to be dispatched. - /// A cancellation token that can be used to cancel the operation. - /// A task that represents the asynchronous operation. - protected static async Task DispatchAsync(Action action, CancellationToken cancellationToken) + /// The action to execute on the UI thread. + /// A token to cancel the operation. + /// The priority at which to invoke the action. + /// A ValueTask representing the asynchronous operation. + /// Thrown when is null. + /// Thrown when no WPF Dispatcher is available. + protected static ValueTask DispatchAsync( + Action action, + CancellationToken cancellationToken = default, + DispatcherPriority priority = DispatcherPriority.Normal + ) { + ArgumentNullException.ThrowIfNull(action); + + if (Application.Current?.Dispatcher is not { } dispatcher) + { + throw new InvalidOperationException( + "No WPF Dispatcher available. Ensure Application.Current is initialized." + ); + } + if (cancellationToken.IsCancellationRequested) { - return; + return ValueTask.FromCanceled(cancellationToken); } - if (Application.Current is null) + // Fast path: already on UI thread + if (dispatcher.CheckAccess()) { - return; + action(); + + return ValueTask.CompletedTask; } - await Application.Current.Dispatcher.InvokeAsync(action); + // Slow path: dispatch to UI thread + return new ValueTask(dispatcher.InvokeAsync(action, priority, cancellationToken).Task); } } diff --git a/src/ReflectionEventing.DependencyInjection/DependencyInjectionEventBus.cs b/src/ReflectionEventing.DependencyInjection/DependencyInjectionEventBus.cs index 93aef26..21d066f 100644 --- a/src/ReflectionEventing.DependencyInjection/DependencyInjectionEventBus.cs +++ b/src/ReflectionEventing.DependencyInjection/DependencyInjectionEventBus.cs @@ -9,19 +9,19 @@ namespace ReflectionEventing.DependencyInjection; public class DependencyInjectionEventBus( - QueueProcessorOptionsProvider options, + EventBusBuilderOptions options, IConsumerProvider consumerProviders, IConsumerTypesProvider consumerTypesProvider, IEventsQueue queue -) : EventBus(consumerProviders, consumerTypesProvider, queue) +) : EventBus(options, consumerProviders, consumerTypesProvider, queue) { /// - public override Task PublishAsync( + public override ValueTask PublishAsync( TEvent eventItem, CancellationToken cancellationToken = default ) { - if (!options.Value.UseEventsQueue) + if (!options.UseEventsQueue) { throw new QueueException("The background queue processor is disabled."); } diff --git a/src/ReflectionEventing.DependencyInjection/ReflectionEventing.DependencyInjection.csproj b/src/ReflectionEventing.DependencyInjection/ReflectionEventing.DependencyInjection.csproj index 8c278a2..7513ac3 100644 --- a/src/ReflectionEventing.DependencyInjection/ReflectionEventing.DependencyInjection.csproj +++ b/src/ReflectionEventing.DependencyInjection/ReflectionEventing.DependencyInjection.csproj @@ -1,8 +1,7 @@  - ReflectionEventing.DependencyInjection - net9.0;net8.0;net6.0;netstandard2.0;net462;net472 + net10.0;net9.0;net8.0;netstandard2.0;net462;net472 true true true @@ -42,13 +41,15 @@ - - + + - - \ No newline at end of file + diff --git a/src/ReflectionEventing.DependencyInjection/ServiceCollectionExtensions.cs b/src/ReflectionEventing.DependencyInjection/ServiceCollectionExtensions.cs index 9bb8859..3ba185e 100644 --- a/src/ReflectionEventing.DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/ReflectionEventing.DependencyInjection/ServiceCollectionExtensions.cs @@ -23,7 +23,7 @@ public static class ServiceCollectionExtensions /// The same service collection so that multiple calls can be chained. /// /// This method adds a singleton service of type that uses a with the consumers from the event bus builder. - /// It also adds a scoped service of type that uses the class. + /// It also adds a transient service of type that uses the class. /// public static IServiceCollection AddEventBus( this IServiceCollection services, @@ -40,8 +40,9 @@ public static IServiceCollection AddEventBus( _ = services.AddKeyedScoped( serviceKey ); - _ = services.AddKeyedScoped(serviceKey); + _ = services.AddKeyedTransient(serviceKey); + _ = services.AddSingleton(builder.Options); _ = services.AddSingleton(new QueueProcessorOptionsProvider(builder.Options, serviceKey)); if (builder.Options.UseEventsQueue) @@ -61,7 +62,7 @@ public static IServiceCollection AddEventBus( /// The same service collection so that multiple calls can be chained. /// /// This method adds a singleton service of type that uses a with the consumers from the event bus builder. - /// It also adds a scoped service of type that uses the class. + /// It also adds a transient service of type that uses the class. /// public static IServiceCollection AddEventBus( this IServiceCollection services, @@ -75,8 +76,9 @@ Action configure _ = services.AddSingleton(builder.BuildTypesProvider()); _ = services.AddSingleton(); _ = services.AddScoped(); - _ = services.AddScoped(); + _ = services.AddTransient(); + _ = services.AddSingleton(builder.Options); _ = services.AddSingleton(new QueueProcessorOptionsProvider(builder.Options)); if (builder.Options.UseEventsQueue) diff --git a/src/ReflectionEventing.DependencyInjection/Services/DependencyInjectionQueueProcessor.cs b/src/ReflectionEventing.DependencyInjection/Services/DependencyInjectionQueueProcessor.cs index 8b8ef3b..f949f9a 100644 --- a/src/ReflectionEventing.DependencyInjection/Services/DependencyInjectionQueueProcessor.cs +++ b/src/ReflectionEventing.DependencyInjection/Services/DependencyInjectionQueueProcessor.cs @@ -187,7 +187,17 @@ CancellationToken cancellationToken { try { - await (Task)consumeMethod.Invoke(consumer, [@event, cancellationToken])!; + object? result = consumeMethod.Invoke(consumer, [@event, cancellationToken]); + + // Handle ValueTask return type + if (result is ValueTask valueTask) + { + await valueTask.ConfigureAwait(false); + } + else if (result is Task task) + { + await task.ConfigureAwait(false); + } } catch (Exception e) { @@ -230,4 +240,12 @@ CancellationToken cancellationToken ); } } + + /// + public override void Dispose() + { + semaphore.Dispose(); + + base.Dispose(); + } } diff --git a/src/ReflectionEventing.Ninject/EventBusModule.cs b/src/ReflectionEventing.Ninject/EventBusModule.cs deleted file mode 100644 index cc61595..0000000 --- a/src/ReflectionEventing.Ninject/EventBusModule.cs +++ /dev/null @@ -1,40 +0,0 @@ -// This Source Code Form is subject to the terms of the MIT License. -// If a copy of the MIT was not distributed with this file, You can obtain one at https://opensource.org/licenses/MIT. -// Copyright (C) Leszek Pomianowski and ReflectionEventing Contributors. -// All Rights Reserved. - -using Ninject.Modules; -using ReflectionEventing.Queues; - -namespace ReflectionEventing.Ninject; - -/// -/// Represents a Ninject module for configuring the event bus. -/// -public class EventBusModule(Action configure) : NinjectModule -{ - /// - /// Loads the module into the kernel. - /// - public override void Load() - { - if (Kernel is null) - { - throw new InvalidOperationException("The kernel is not set."); - } - - NinjectEventBusBuilder builder = new(Kernel); - - configure(builder); - - _ = Bind() - .ToConstant(builder.BuildTypesProvider()) - .InSingletonScope(); - - _ = Bind().To().InSingletonScope(); - - _ = Bind().To().InTransientScope(); - - _ = Bind().To().InTransientScope(); - } -} diff --git a/src/ReflectionEventing.Ninject/GlobalUsings.cs b/src/ReflectionEventing.Ninject/GlobalUsings.cs deleted file mode 100644 index 65109da..0000000 --- a/src/ReflectionEventing.Ninject/GlobalUsings.cs +++ /dev/null @@ -1,8 +0,0 @@ -// This Source Code Form is subject to the terms of the MIT License. -// If a copy of the MIT was not distributed with this file, You can obtain one at https://opensource.org/licenses/MIT. -// Copyright (C) Leszek Pomianowski and ReflectionEventing Contributors. -// All Rights Reserved. - -global using System; -global using System.Collections.Generic; -global using System.Linq; diff --git a/src/ReflectionEventing.Ninject/NinjectConsumerProvider.cs b/src/ReflectionEventing.Ninject/NinjectConsumerProvider.cs deleted file mode 100644 index 44b2349..0000000 --- a/src/ReflectionEventing.Ninject/NinjectConsumerProvider.cs +++ /dev/null @@ -1,25 +0,0 @@ -// This Source Code Form is subject to the terms of the MIT License. -// If a copy of the MIT was not distributed with this file, You can obtain one at https://opensource.org/licenses/MIT. -// Copyright (C) Leszek Pomianowski and ReflectionEventing Contributors. -// All Rights Reserved. - -using Ninject; - -namespace ReflectionEventing.Ninject; - -/// -/// Provides event consumers for Ninject. -/// -public class NinjectConsumerProvider(IKernel kernel) : IConsumerProvider -{ - /// - public IEnumerable GetConsumers(Type consumerType) - { - if (consumerType is null) - { - throw new ArgumentNullException(nameof(consumerType)); - } - - return kernel.GetAll(consumerType); - } -} diff --git a/src/ReflectionEventing.Ninject/NinjectEventBusBuilder.cs b/src/ReflectionEventing.Ninject/NinjectEventBusBuilder.cs deleted file mode 100644 index b369317..0000000 --- a/src/ReflectionEventing.Ninject/NinjectEventBusBuilder.cs +++ /dev/null @@ -1,25 +0,0 @@ -// This Source Code Form is subject to the terms of the MIT License. -// If a copy of the MIT was not distributed with this file, You can obtain one at https://opensource.org/licenses/MIT. -// Copyright (C) Leszek Pomianowski and ReflectionEventing Contributors. -// All Rights Reserved. - -using Ninject; - -namespace ReflectionEventing.Ninject; - -/// -/// Represents a builder for configuring the event bus with Ninject. -/// -public class NinjectEventBusBuilder(IKernel kernel) : EventBusBuilder -{ - /// - public override EventBusBuilder AddConsumer(Type consumerType) - { - if (!kernel.GetBindings(consumerType).Any()) - { - throw new InvalidOperationException("Event consumer must be registered in the kernel."); - } - - return base.AddConsumer(consumerType); - } -} diff --git a/src/ReflectionEventing.Ninject/ReflectionEventing.Ninject.csproj b/src/ReflectionEventing.Ninject/ReflectionEventing.Ninject.csproj deleted file mode 100644 index 1ef33e6..0000000 --- a/src/ReflectionEventing.Ninject/ReflectionEventing.Ninject.csproj +++ /dev/null @@ -1,43 +0,0 @@ - - - - ReflectionEventing.Ninject - net9.0;net8.0;net6.0;netstandard2.0;net462;net472 - true - true - true - Ninject module with ReflectionEventing, which promotes better Inversion of Control (IoC), reducing coupling and enhancing the modularity and flexibility of your applications. - - - - true - true - Speed - - - - - - System.Diagnostics.CodeAnalysis.DoesNotReturnAttribute; - System.Diagnostics.CodeAnalysis.MaybeNullWhenAttribute; - System.Diagnostics.CodeAnalysis.MemberNotNullAttribute; - System.Diagnostics.CodeAnalysis.NotNullAttribute; - System.Diagnostics.CodeAnalysis.NotNullIfNotNullAttribute; - System.Diagnostics.CodeAnalysis.NotNullWhenAttribute; - System.Diagnostics.CodeAnalysis.RequiresUnreferencedCodeAttribute; - System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessageAttribute; - System.Runtime.CompilerServices.CallerArgumentExpressionAttribute; - System.Runtime.CompilerServices.IsExternalInit; - System.Runtime.CompilerServices.SkipLocalsInitAttribute; - - - - - - - - - - - - diff --git a/src/ReflectionEventing.Unity/GlobalUsings.cs b/src/ReflectionEventing.Unity/GlobalUsings.cs deleted file mode 100644 index 65109da..0000000 --- a/src/ReflectionEventing.Unity/GlobalUsings.cs +++ /dev/null @@ -1,8 +0,0 @@ -// This Source Code Form is subject to the terms of the MIT License. -// If a copy of the MIT was not distributed with this file, You can obtain one at https://opensource.org/licenses/MIT. -// Copyright (C) Leszek Pomianowski and ReflectionEventing Contributors. -// All Rights Reserved. - -global using System; -global using System.Collections.Generic; -global using System.Linq; diff --git a/src/ReflectionEventing.Unity/ReflectionEventing.Unity.csproj b/src/ReflectionEventing.Unity/ReflectionEventing.Unity.csproj deleted file mode 100644 index 7ff6966..0000000 --- a/src/ReflectionEventing.Unity/ReflectionEventing.Unity.csproj +++ /dev/null @@ -1,44 +0,0 @@ - - - - ReflectionEventing.Unity - net9.0;net8.0;net6.0;netstandard2.0;net462;net472 - true - true - true - $(CommonTags);unity;container - Unity container extensions with ReflectionEventing, which promotes better Inversion of Control (IoC), reducing coupling and enhancing the modularity and flexibility of your applications. - - - - true - true - Speed - - - - - - System.Diagnostics.CodeAnalysis.DoesNotReturnAttribute; - System.Diagnostics.CodeAnalysis.MaybeNullWhenAttribute; - System.Diagnostics.CodeAnalysis.MemberNotNullAttribute; - System.Diagnostics.CodeAnalysis.NotNullAttribute; - System.Diagnostics.CodeAnalysis.NotNullIfNotNullAttribute; - System.Diagnostics.CodeAnalysis.NotNullWhenAttribute; - System.Diagnostics.CodeAnalysis.RequiresUnreferencedCodeAttribute; - System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessageAttribute; - System.Runtime.CompilerServices.CallerArgumentExpressionAttribute; - System.Runtime.CompilerServices.IsExternalInit; - System.Runtime.CompilerServices.SkipLocalsInitAttribute; - - - - - - - - - - - - diff --git a/src/ReflectionEventing.Unity/UnityConsumerProvider.cs b/src/ReflectionEventing.Unity/UnityConsumerProvider.cs deleted file mode 100644 index 67b450b..0000000 --- a/src/ReflectionEventing.Unity/UnityConsumerProvider.cs +++ /dev/null @@ -1,25 +0,0 @@ -// This Source Code Form is subject to the terms of the MIT License. -// If a copy of the MIT was not distributed with this file, You can obtain one at https://opensource.org/licenses/MIT. -// Copyright (C) Leszek Pomianowski and ReflectionEventing Contributors. -// All Rights Reserved. - -using Unity; - -namespace ReflectionEventing.Unity; - -/// -/// Provides event consumers for Unity. -/// -public class UnityConsumerProvider(IUnityContainer container) : IConsumerProvider -{ - /// - public IEnumerable GetConsumers(Type consumerType) - { - if (consumerType is null) - { - throw new ArgumentNullException(nameof(consumerType)); - } - - return container.ResolveAll(consumerType); - } -} diff --git a/src/ReflectionEventing.Unity/UnityContainerExtensions.cs b/src/ReflectionEventing.Unity/UnityContainerExtensions.cs deleted file mode 100644 index 3ca58e0..0000000 --- a/src/ReflectionEventing.Unity/UnityContainerExtensions.cs +++ /dev/null @@ -1,53 +0,0 @@ -// This Source Code Form is subject to the terms of the MIT License. -// If a copy of the MIT was not distributed with this file, You can obtain one at https://opensource.org/licenses/MIT. -// Copyright (C) Leszek Pomianowski and ReflectionEventing Contributors. -// All Rights Reserved. - -using ReflectionEventing.Queues; -using Unity; -using Unity.Lifetime; - -namespace ReflectionEventing.Unity; - -/// -/// Provides extension methods for the interface. -/// -public static class UnityContainerExtensions -{ - /// - /// Adds the event bus and its related services to the specified Unity container. - /// - /// The to add the event bus to. - /// A delegate that configures the . - /// The same Unity container so that multiple calls can be chained. - /// - /// This method adds a singleton service of type that uses a with the consumers from the event bus builder. - /// It also adds a scoped service of type that uses the class. - /// - public static IUnityContainer AddEventBus( - this IUnityContainer container, - Action configure - ) - { - UnityEventBusBuilder builder = new(container); - - configure(builder); - - _ = container.RegisterInstance( - builder.BuildTypesProvider(), - new ContainerControlledLifetimeManager() - ); - - _ = container.RegisterType( - new ContainerControlledLifetimeManager() - ); - - _ = container.RegisterType( - new HierarchicalLifetimeManager() - ); - - _ = container.RegisterType(new HierarchicalLifetimeManager()); - - return container; - } -} diff --git a/src/ReflectionEventing.Unity/UnityEventBusBuilder.cs b/src/ReflectionEventing.Unity/UnityEventBusBuilder.cs deleted file mode 100644 index e52347b..0000000 --- a/src/ReflectionEventing.Unity/UnityEventBusBuilder.cs +++ /dev/null @@ -1,13 +0,0 @@ -// This Source Code Form is subject to the terms of the MIT License. -// If a copy of the MIT was not distributed with this file, You can obtain one at https://opensource.org/licenses/MIT. -// Copyright (C) Leszek Pomianowski and ReflectionEventing Contributors. -// All Rights Reserved. - -using Unity; - -namespace ReflectionEventing.Unity; - -/// -/// Represents a builder for configuring the event bus with Unity. -/// -public class UnityEventBusBuilder(IUnityContainer container) : EventBusBuilder; diff --git a/src/ReflectionEventing/EventBus.cs b/src/ReflectionEventing/EventBus.cs index edbdc58..e238b98 100644 --- a/src/ReflectionEventing/EventBus.cs +++ b/src/ReflectionEventing/EventBus.cs @@ -14,6 +14,7 @@ namespace ReflectionEventing; /// This class uses a service provider to get required services and a consumer provider to get consumers for a specific event type. /// public class EventBus( + EventBusBuilderOptions options, IConsumerProvider consumerProviders, IConsumerTypesProvider consumerTypesProvider, IEventsQueue queue @@ -30,7 +31,7 @@ IEventsQueue queue ); /// - public virtual async Task SendAsync( + public virtual ValueTask SendAsync( TEvent eventItem, CancellationToken cancellationToken = default ) @@ -43,37 +44,57 @@ public virtual async Task SendAsync( using Activity? activity = ActivitySource.StartActivity(ActivityKind.Producer); - activity?.AddTag("co.lepo.reflection.eventing.message", typeof(TEvent).Name); - - if (eventItem is null) - { - throw new EventBusException(nameof(eventItem)); - } + _ = activity?.AddTag("co.lepo.reflection.eventing.message", typeof(TEvent).Name); Type eventType = typeof(TEvent); - List tasks = []; IEnumerable consumerTypes = consumerTypesProvider.GetConsumerTypes(eventType); + // Defer List allocation: track first consumer in a local variable + object? singleConsumer = null; + List? consumers = null; + foreach (Type consumerType in consumerTypes) { foreach (object? consumer in consumerProviders.GetConsumers(consumerType)) { if (consumer is null) { - return; + continue; } - tasks.Add(((IConsumer)consumer).ConsumeAsync(eventItem, cancellationToken)); + if (singleConsumer is null) + { + singleConsumer = consumer; + } + else + { + consumers ??= new List { singleConsumer }; + consumers.Add(consumer); + } } } - await Task.WhenAll(tasks).ConfigureAwait(false); - SentCounter.Add(1, new KeyValuePair("message_type", eventType.Name)); + + // Execute based on consumer count - optimized paths + if (singleConsumer is null) + { + return default; + } + + if (consumers is null) + { + return ((IConsumer)singleConsumer).ConsumeAsync(eventItem, cancellationToken); + } + + // Multiple consumers - execute based on configured mode + return options.ConsumerExecutionMode == ProcessingMode.Sequential + ? ExecuteSequentialAsync(consumers, eventItem, cancellationToken) + : ExecuteParallelAsync(consumers, eventItem, cancellationToken); } /// - public virtual async Task PublishAsync( + public virtual ValueTask PublishAsync( TEvent eventItem, CancellationToken cancellationToken = default ) @@ -81,13 +102,88 @@ public virtual async Task PublishAsync( { using Activity? activity = ActivitySource.StartActivity(ActivityKind.Producer); - activity?.AddTag("co.lepo.reflection.eventing.message", typeof(TEvent).Name); - - await queue.EnqueueAsync(eventItem, cancellationToken); + _ = activity?.AddTag("co.lepo.reflection.eventing.message", typeof(TEvent).Name); PublishedCounter.Add( 1, new KeyValuePair("message_type", typeof(TEvent).Name) ); + + return queue.EnqueueAsync(eventItem, cancellationToken); + } + + /// + /// Executes consumers sequentially, one at a time. + /// + /// The type of the event. + /// The list of consumers to execute. + /// The event to consume. + /// The cancellation token. + /// A ValueTask that completes when all consumers have completed. + private static async ValueTask ExecuteSequentialAsync( + List consumers, + TEvent eventItem, + CancellationToken cancellationToken + ) + where TEvent : class + { + foreach (object consumer in consumers) + { + await ((IConsumer)consumer) + .ConsumeAsync(eventItem, cancellationToken) + .ConfigureAwait(false); + } + } + + /// + /// Executes consumers in parallel using Task.WhenAll. + /// + /// The type of the event. + /// The list of consumers to execute. + /// The event to consume. + /// The cancellation token. + /// A ValueTask that completes when all consumers have completed. + private static ValueTask ExecuteParallelAsync( + List consumers, + TEvent eventItem, + CancellationToken cancellationToken + ) + where TEvent : class + { + List? asyncTasks = null; + + // First pass: execute all synchronous completions + foreach (object consumer in consumers) + { + ValueTask task = ((IConsumer)consumer).ConsumeAsync( + eventItem, + cancellationToken + ); + + if (task.IsCompletedSuccessfully) + { + // Ensure pooled IValueTaskSource is properly returned + task.GetAwaiter().GetResult(); + } + else + { + asyncTasks ??= new List(consumers.Count); + asyncTasks.Add(task); + } + } + + // Only allocate Task objects for truly async operations + if (asyncTasks is null) + { + return default; + } + + Task[] tasks = new Task[asyncTasks.Count]; + for (int i = 0; i < asyncTasks.Count; i++) + { + tasks[i] = asyncTasks[i].AsTask(); + } + + return new ValueTask(Task.WhenAll(tasks)); } } diff --git a/src/ReflectionEventing/EventBusBuilderOptions.cs b/src/ReflectionEventing/EventBusBuilderOptions.cs index 48ac876..b762fbc 100644 --- a/src/ReflectionEventing/EventBusBuilderOptions.cs +++ b/src/ReflectionEventing/EventBusBuilderOptions.cs @@ -43,6 +43,14 @@ public class EventBusBuilderOptions /// public ProcessingMode QueueMode { get; set; } = ProcessingMode.Sequential; + /// + /// Gets or sets the mode in which consumers are executed when sending events via . + /// If set to , consumers are executed one at a time in sequence. + /// If set to , consumers are executed concurrently using Task.WhenAll. + /// The default value is . + /// + public ProcessingMode ConsumerExecutionMode { get; set; } = ProcessingMode.Parallel; + /// /// Gets or sets the maximum number of concurrent tasks that can be processed. /// This value is used to limit the number of tasks running in parallel when is set to . diff --git a/src/ReflectionEventing/IConsumer.cs b/src/ReflectionEventing/IConsumer.cs index e554bbe..0d07a8c 100644 --- a/src/ReflectionEventing/IConsumer.cs +++ b/src/ReflectionEventing/IConsumer.cs @@ -19,6 +19,6 @@ public interface IConsumer /// /// The event to consume. /// A cancellation token that can be used to cancel the operation. - /// A task that represents the asynchronous operation. - Task ConsumeAsync(TEvent payload, CancellationToken cancellationToken); + /// A value task that represents the asynchronous operation. + ValueTask ConsumeAsync(TEvent payload, CancellationToken cancellationToken); } diff --git a/src/ReflectionEventing/IEventBus.cs b/src/ReflectionEventing/IEventBus.cs index 5b55841..23cdb8a 100644 --- a/src/ReflectionEventing/IEventBus.cs +++ b/src/ReflectionEventing/IEventBus.cs @@ -16,12 +16,12 @@ public interface IEventBus /// The type of the event to send. /// The event to send. /// A cancellation token that can be used to cancel the operation. - /// A task that represents the asynchronous operation. + /// A value task that represents the asynchronous operation. /// /// This method gets the consumers for the specified event type from the consumer provider and then uses the service provider to get the required service for each consumer. /// Each consumer is then used to consume the event asynchronously. /// - Task SendAsync(TEvent eventItem, CancellationToken cancellationToken = default) + ValueTask SendAsync(TEvent eventItem, CancellationToken cancellationToken = default) where TEvent : class; /// @@ -30,10 +30,10 @@ Task SendAsync(TEvent eventItem, CancellationToken cancellationToken = d /// The type of the event to publish. /// The event to publish. /// A cancellation token that can be used to cancel the operation. - /// A task that represents the asynchronous operation. + /// A value task that represents the asynchronous operation. /// /// The method only adds the event to the execution queue, it does not wait for its successful execution. /// - Task PublishAsync(TEvent eventItem, CancellationToken cancellationToken = default) + ValueTask PublishAsync(TEvent eventItem, CancellationToken cancellationToken = default) where TEvent : class; } diff --git a/src/ReflectionEventing/Queues/EventsQueue.cs b/src/ReflectionEventing/Queues/EventsQueue.cs index d6ff1ca..caf464e 100644 --- a/src/ReflectionEventing/Queues/EventsQueue.cs +++ b/src/ReflectionEventing/Queues/EventsQueue.cs @@ -12,13 +12,13 @@ public class EventsQueue : IEventsQueue private readonly ConcurrentQueue errorQueue = new(); /// - public virtual async Task EnqueueAsync( + public virtual ValueTask EnqueueAsync( TEvent @event, CancellationToken cancellationToken = default ) where TEvent : class { - await events.Writer.WriteAsync(@event, cancellationToken); + return events.Writer.WriteAsync(@event, cancellationToken); } /// @@ -27,6 +27,12 @@ public IAsyncEnumerable ReadEventsAsync(CancellationToken cancellationTo return events.Reader.ReadAllAsync(cancellationToken); } + /// + public void Complete() + { + events.Writer.TryComplete(); + } + /// public void EnqueueError(FailedEvent fail) { @@ -38,4 +44,14 @@ public IEnumerable GetErrors() { return errorQueue; } + + /// + public ValueTask DisposeAsync() + { + Complete(); + + GC.SuppressFinalize(this); + + return default; + } } diff --git a/src/ReflectionEventing/Queues/IEventsQueue.cs b/src/ReflectionEventing/Queues/IEventsQueue.cs index 470513a..7835a1d 100644 --- a/src/ReflectionEventing/Queues/IEventsQueue.cs +++ b/src/ReflectionEventing/Queues/IEventsQueue.cs @@ -8,15 +8,15 @@ namespace ReflectionEventing.Queues; /// /// Defines a contract for an event queue that supports asynchronous operations for appending and retrieving events. /// -public interface IEventsQueue +public interface IEventsQueue : IAsyncDisposable { /// /// Appends an event to the queue asynchronously. /// /// The event to append to the queue. /// A token to monitor for cancellation requests. - /// A task that represents the asynchronous append operation. - Task EnqueueAsync(TEvent @event, CancellationToken cancellationToken = default) + /// A value task that represents the asynchronous append operation. + ValueTask EnqueueAsync(TEvent @event, CancellationToken cancellationToken = default) where TEvent : class; /// @@ -32,6 +32,11 @@ Task EnqueueAsync(TEvent @event, CancellationToken cancellationToken = d /// An of events from the queue. IAsyncEnumerable ReadEventsAsync(CancellationToken cancellationToken); + /// + /// Signals that no more events will be written to the queue, allowing readers to drain remaining events and complete. + /// + void Complete(); + /// /// Gets the events that failed processing from the error queue. /// diff --git a/src/ReflectionEventing/ReflectionEventing.csproj b/src/ReflectionEventing/ReflectionEventing.csproj index 8efcfe7..00b1af6 100644 --- a/src/ReflectionEventing/ReflectionEventing.csproj +++ b/src/ReflectionEventing/ReflectionEventing.csproj @@ -2,7 +2,7 @@ ReflectionEventing - net9.0;net8.0;net6.0;netstandard2.0;net462;net472 + net10.0;net9.0;net8.0;netstandard2.0;net462;net472 true true true diff --git a/tests/ReflectionEventing.Autofac.UnitTests/AutofacConsumerProviderTests.cs b/tests/ReflectionEventing.Autofac.UnitTests/AutofacConsumerProviderTests.cs deleted file mode 100644 index 2c338bc..0000000 --- a/tests/ReflectionEventing.Autofac.UnitTests/AutofacConsumerProviderTests.cs +++ /dev/null @@ -1,49 +0,0 @@ -// This Source Code Form is subject to the terms of the MIT License. -// If a copy of the MIT was not distributed with this file, You can obtain one at https://opensource.org/licenses/MIT. -// Copyright (C) Leszek Pomianowski and ReflectionEventing Contributors. -// All Rights Reserved. - -using Autofac; - -namespace ReflectionEventing.Autofac.UnitTests; - -public sealed class AutofacConsumerProviderTests -{ - [Fact] - public void GetConsumerTypes_ShouldThrowExceptionWhenConsumerTypeIsNull() - { - ILifetimeScope lifetimeScope = Substitute.For(); - AutofacConsumerProvider consumerProvider = new AutofacConsumerProvider(lifetimeScope); - - Action act = () => consumerProvider.GetConsumers(null!); - - _ = act.Should() - .Throw() - .WithMessage("Value cannot be null. (Parameter 'consumerType')"); - } - - [Fact] - public void GetConsumerTypes_ShouldReturnResolvedConsumerType() - { - TestConsumer testInstance = new(); - - ContainerBuilder builder = new ContainerBuilder(); - _ = builder.RegisterInstance(testInstance).As().SingleInstance(); - IContainer container = builder.Build(); - ILifetimeScope scope = container.BeginLifetimeScope(); - - AutofacConsumerProvider consumerProvider = new AutofacConsumerProvider(scope); - - IEnumerable actualConsumers = consumerProvider.GetConsumers(typeof(TestConsumer)); - - _ = actualConsumers.First().Should().Be(testInstance); - } - - public sealed record TestEvent; - - public sealed class TestConsumer : IConsumer - { - public Task ConsumeAsync(TestEvent payload, CancellationToken cancellationToken) => - Task.CompletedTask; - } -} diff --git a/tests/ReflectionEventing.Autofac.UnitTests/ContainerBuilderExtensionsTests.cs b/tests/ReflectionEventing.Autofac.UnitTests/ContainerBuilderExtensionsTests.cs deleted file mode 100644 index 04b408b..0000000 --- a/tests/ReflectionEventing.Autofac.UnitTests/ContainerBuilderExtensionsTests.cs +++ /dev/null @@ -1,48 +0,0 @@ -// This Source Code Form is subject to the terms of the MIT License. -// If a copy of the MIT was not distributed with this file, You can obtain one at https://opensource.org/licenses/MIT. -// Copyright (C) Leszek Pomianowski and ReflectionEventing Contributors. -// All Rights Reserved. - -using Autofac; - -namespace ReflectionEventing.Autofac.UnitTests; - -public sealed class ContainerBuilderExtensionsTests -{ - [Fact] - public void AddEventBus_RegistersServicesAndAddsConsumer() - { - ContainerBuilder builder = new ContainerBuilder(); - - _ = builder.RegisterType().AsSelf(); - _ = builder.AddEventBus(eventBusBuilder => - { - _ = eventBusBuilder.AddConsumer(); - }); - - IContainer container = builder.Build(); - - IConsumerTypesProvider consumerTypesProvider = container.Resolve(); - _ = consumerTypesProvider.Should().NotBeNull(); - _ = consumerTypesProvider.Should().BeOfType(); - - IConsumerProvider consumerProvider = container.Resolve(); - _ = consumerProvider.Should().NotBeNull(); - _ = consumerProvider.Should().BeOfType(); - - IEventBus eventBus = container.Resolve(); - _ = eventBus.Should().NotBeNull(); - _ = eventBus.Should().BeOfType(); - - IEnumerable consumers = consumerTypesProvider.GetConsumerTypes(); - _ = consumers.Should().ContainSingle().Which.Should().Be(typeof(TestConsumer)); - } - - public class TestConsumer : IConsumer - { - public Task ConsumeAsync(TestEvent payload, CancellationToken cancellationToken) => - Task.CompletedTask; - } - - public sealed record TestEvent; -} diff --git a/tests/ReflectionEventing.Autofac.UnitTests/GlobalUsings.cs b/tests/ReflectionEventing.Autofac.UnitTests/GlobalUsings.cs deleted file mode 100644 index e49f51b..0000000 --- a/tests/ReflectionEventing.Autofac.UnitTests/GlobalUsings.cs +++ /dev/null @@ -1,13 +0,0 @@ -// This Source Code Form is subject to the terms of the MIT License. -// If a copy of the MIT was not distributed with this file, You can obtain one at https://opensource.org/licenses/MIT. -// Copyright (C) Leszek Pomianowski and ReflectionEventing Contributors. -// All Rights Reserved. - -global using System; -global using System.Collections.Generic; -global using System.Linq; -global using System.Threading; -global using System.Threading.Tasks; -global using FluentAssertions; -global using NSubstitute; -global using Xunit; diff --git a/tests/ReflectionEventing.Autofac.UnitTests/ReflectionEventing.Autofac.UnitTests.csproj b/tests/ReflectionEventing.Autofac.UnitTests/ReflectionEventing.Autofac.UnitTests.csproj deleted file mode 100644 index 51c7a6e..0000000 --- a/tests/ReflectionEventing.Autofac.UnitTests/ReflectionEventing.Autofac.UnitTests.csproj +++ /dev/null @@ -1,28 +0,0 @@ - - - - net9.0 - false - true - - - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - - - - diff --git a/tests/ReflectionEventing.Castle.Windsor.UnitTests/EventBusInstallerTests.cs b/tests/ReflectionEventing.Castle.Windsor.UnitTests/EventBusInstallerTests.cs deleted file mode 100644 index b0dfb74..0000000 --- a/tests/ReflectionEventing.Castle.Windsor.UnitTests/EventBusInstallerTests.cs +++ /dev/null @@ -1,54 +0,0 @@ -// This Source Code Form is subject to the terms of the MIT License. -// If a copy of the MIT was not distributed with this file, You can obtain one at https://opensource.org/licenses/MIT. -// Copyright (C) Leszek Pomianowski and ReflectionEventing Contributors. -// All Rights Reserved. - -using Castle.MicroKernel.Lifestyle; -using Castle.MicroKernel.Registration; -using Castle.Windsor; - -namespace ReflectionEventing.Castle.Windsor.UnitTests; - -public sealed class EventBusInstallerTests -{ - [Fact] - public void Install_RegistersServicesAndAddsConsumer() - { - IWindsorContainer container = new WindsorContainer(); - - _ = container.Register(Component.For().LifestyleScoped()); - - EventBusInstaller installer = new(builder => - { - _ = builder.AddConsumer(); - }); - - installer.Install(container, null!); - - using IDisposable scope = container.BeginScope(); - - IConsumerTypesProvider consumerTypesProvider = container.Resolve(); - _ = consumerTypesProvider.Should().NotBeNull(); - _ = consumerTypesProvider.Should().BeOfType(); - - IConsumerProvider consumerProvider = container.Resolve(); - _ = consumerProvider.Should().NotBeNull(); - _ = consumerProvider.Should().BeOfType(); - - IEventBus eventBus = container.Resolve(); - _ = eventBus.Should().NotBeNull(); - _ = eventBus.Should().BeOfType(); - - IEnumerable consumers = consumerTypesProvider.GetConsumerTypes(); - - _ = consumers.First().Should().Be(typeof(TestConsumer)); - } - - public class TestConsumer : IConsumer - { - public Task ConsumeAsync(TestEvent payload, CancellationToken cancellationToken) => - Task.CompletedTask; - } - - public sealed record TestEvent; -} diff --git a/tests/ReflectionEventing.Castle.Windsor.UnitTests/EventBusTests.cs b/tests/ReflectionEventing.Castle.Windsor.UnitTests/EventBusTests.cs deleted file mode 100644 index 089d3d5..0000000 --- a/tests/ReflectionEventing.Castle.Windsor.UnitTests/EventBusTests.cs +++ /dev/null @@ -1,58 +0,0 @@ -// This Source Code Form is subject to the terms of the MIT License. -// If a copy of the MIT was not distributed with this file, You can obtain one at https://opensource.org/licenses/MIT. -// Copyright (C) Leszek Pomianowski and ReflectionEventing Contributors. -// All Rights Reserved. - -using Castle.MicroKernel.Lifestyle; -using Castle.MicroKernel.Registration; -using Castle.Windsor; - -namespace ReflectionEventing.Castle.Windsor.UnitTests; - -public sealed class EventBusTests : IDisposable -{ - private readonly IWindsorContainer _container; - - public EventBusTests() - { - _container = new WindsorContainer(); - _ = _container.Register(Component.For().LifestyleScoped()); - - EventBusInstaller installer = new(builder => - { - _ = builder.AddConsumer(); - }); - - installer.Install(_container, null!); - } - - [Fact] - public async Task SendAsync_ShouldCallConsumeAsyncOnAllConsumers() - { - using IDisposable scope = _container.BeginScope(); - IEventBus eventBus = _container.Resolve(); - - await eventBus.SendAsync(new TestEvent(), CancellationToken.None); - - _ = _container.Resolve().ReceivedEvents.Should().Be(1); - } - - public void Dispose() - { - _container.Dispose(); - } - - public class TestConsumer : IConsumer - { - public int ReceivedEvents { get; private set; } = 0; - - public Task ConsumeAsync(TestEvent payload, CancellationToken cancellationToken) - { - ReceivedEvents++; - - return Task.CompletedTask; - } - } - - public sealed record TestEvent; -} diff --git a/tests/ReflectionEventing.Castle.Windsor.UnitTests/GlobalUsings.cs b/tests/ReflectionEventing.Castle.Windsor.UnitTests/GlobalUsings.cs deleted file mode 100644 index 47b5797..0000000 --- a/tests/ReflectionEventing.Castle.Windsor.UnitTests/GlobalUsings.cs +++ /dev/null @@ -1,12 +0,0 @@ -// This Source Code Form is subject to the terms of the MIT License. -// If a copy of the MIT was not distributed with this file, You can obtain one at https://opensource.org/licenses/MIT. -// Copyright (C) Leszek Pomianowski and ReflectionEventing Contributors. -// All Rights Reserved. - -global using System; -global using System.Collections.Generic; -global using System.Linq; -global using System.Threading; -global using System.Threading.Tasks; -global using FluentAssertions; -global using Xunit; diff --git a/tests/ReflectionEventing.Castle.Windsor.UnitTests/ReflectionEventing.Castle.Windsor.UnitTests.csproj b/tests/ReflectionEventing.Castle.Windsor.UnitTests/ReflectionEventing.Castle.Windsor.UnitTests.csproj deleted file mode 100644 index 554dc11..0000000 --- a/tests/ReflectionEventing.Castle.Windsor.UnitTests/ReflectionEventing.Castle.Windsor.UnitTests.csproj +++ /dev/null @@ -1,28 +0,0 @@ - - - - net9.0 - false - true - - - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - - - - diff --git a/tests/ReflectionEventing.DependencyInjection.UnitTests/DependencyInjectionConsumerProviderTests.cs b/tests/ReflectionEventing.DependencyInjection.UnitTests/DependencyInjectionConsumerProviderTests.cs index 13d2112..c4a0dbc 100644 --- a/tests/ReflectionEventing.DependencyInjection.UnitTests/DependencyInjectionConsumerProviderTests.cs +++ b/tests/ReflectionEventing.DependencyInjection.UnitTests/DependencyInjectionConsumerProviderTests.cs @@ -23,7 +23,7 @@ public void GetConsumerTypes_ShouldThrowExceptionWhenConsumerTypeIsNull() [Fact] public void GetConsumerTypes_ShouldReturnServicesOfConsumerType() { - IServiceCollection services = new ServiceCollection(); + ServiceCollection services = []; _ = services.AddSingleton(); _ = services.AddSingleton(); DependencyInjectionConsumerProvider consumerProvider = new(services.BuildServiceProvider()); @@ -37,7 +37,7 @@ public record TestEvent; public class TestConsumer : IConsumer { - public Task ConsumeAsync(TestEvent payload, CancellationToken cancellationToken) => - Task.CompletedTask; + public ValueTask ConsumeAsync(TestEvent payload, CancellationToken cancellationToken) => + ValueTask.CompletedTask; } } diff --git a/tests/ReflectionEventing.DependencyInjection.UnitTests/DependencyInjectionEventBusBuilderTests.cs b/tests/ReflectionEventing.DependencyInjection.UnitTests/DependencyInjectionEventBusBuilderTests.cs index 2ce0fc7..842b1fb 100644 --- a/tests/ReflectionEventing.DependencyInjection.UnitTests/DependencyInjectionEventBusBuilderTests.cs +++ b/tests/ReflectionEventing.DependencyInjection.UnitTests/DependencyInjectionEventBusBuilderTests.cs @@ -42,7 +42,7 @@ public record TestEvent : ITestEvent; public class TestConsumer : IConsumer { - public Task ConsumeAsync(TestEvent payload, CancellationToken cancellationToken) => - Task.CompletedTask; + public ValueTask ConsumeAsync(TestEvent payload, CancellationToken cancellationToken) => + ValueTask.CompletedTask; } } diff --git a/tests/ReflectionEventing.DependencyInjection.UnitTests/DependencyInjectionQueueProcessorTests.cs b/tests/ReflectionEventing.DependencyInjection.UnitTests/DependencyInjectionQueueProcessorTests.cs index 597df05..1b56be3 100644 --- a/tests/ReflectionEventing.DependencyInjection.UnitTests/DependencyInjectionQueueProcessorTests.cs +++ b/tests/ReflectionEventing.DependencyInjection.UnitTests/DependencyInjectionQueueProcessorTests.cs @@ -30,7 +30,8 @@ public async Task PublishAsync_ShouldNotSendEventsToWrongCustomers() await host.StartAsync(); - IEventBus bus = host.Services.GetRequiredService(); + await using AsyncServiceScope scope = host.Services.CreateAsyncScope(); + IEventBus bus = scope.ServiceProvider.GetRequiredService(); await bus.PublishAsync(new VoidEvent()); @@ -67,9 +68,10 @@ public async Task PublishAsync_ThrowsExceptionWhenQueueIsDisabled() await host.StartAsync(); - IEventBus bus = host.Services.GetRequiredService(); + await using AsyncServiceScope scope = host.Services.CreateAsyncScope(); + IEventBus bus = scope.ServiceProvider.GetRequiredService(); - Func action = () => bus.PublishAsync(new OtherEvent()); + Func action = async () => await bus.PublishAsync(new OtherEvent()); await action .Should() @@ -98,7 +100,8 @@ public async Task PublishAsync_ShouldSendOnlyOneEvent() await host.StartAsync(); - IEventBus bus = host.Services.GetRequiredService(); + await using AsyncServiceScope scope = host.Services.CreateAsyncScope(); + IEventBus bus = scope.ServiceProvider.GetRequiredService(); await bus.PublishAsync(new OtherEvent()); @@ -134,7 +137,8 @@ public async Task PublishAsync_ShouldProperlyAddEventsToQueue() await host.StartAsync(); - IEventBus bus = host.Services.GetRequiredService(); + await using AsyncServiceScope scope = host.Services.CreateAsyncScope(); + IEventBus bus = scope.ServiceProvider.GetRequiredService(); await bus.PublishAsync(new TestEvent()); await bus.PublishAsync(new OtherEvent()); @@ -172,7 +176,8 @@ public async Task PublishAsync_SwallowsConsumerException() await host.StartAsync(); - IEventBus bus = host.Services.GetRequiredService(); + await using AsyncServiceScope scope = host.Services.CreateAsyncScope(); + IEventBus bus = scope.ServiceProvider.GetRequiredService(); await bus.PublishAsync(new TestEvent()); @@ -208,7 +213,8 @@ public async Task PublishAsync_MovesToErrorQueue() await host.StartAsync(); - IEventBus bus = host.Services.GetRequiredService(); + await using AsyncServiceScope scope = host.Services.CreateAsyncScope(); + IEventBus bus = scope.ServiceProvider.GetRequiredService(); await bus.PublishAsync(new TestEvent()); @@ -240,28 +246,28 @@ public class TestConsumer public bool OtherEventConsumed { get; private set; } public bool AsyncQueuedEventConsumed { get; private set; } - public Task ConsumeAsync(TestEvent payload, CancellationToken cancellationToken) + public ValueTask ConsumeAsync(TestEvent payload, CancellationToken cancellationToken) { TestEventConsumed = true; - return Task.CompletedTask; + return ValueTask.CompletedTask; } - public Task ConsumeAsync(OtherEvent payload, CancellationToken cancellationToken) + public ValueTask ConsumeAsync(OtherEvent payload, CancellationToken cancellationToken) { OtherEventConsumed = true; - return Task.CompletedTask; + return ValueTask.CompletedTask; } - public Task ConsumeAsync(AsyncQueuedEvent payload, CancellationToken cancellationToken) + public ValueTask ConsumeAsync(AsyncQueuedEvent payload, CancellationToken cancellationToken) { AsyncQueuedEventConsumed = true; - return Task.CompletedTask; + return ValueTask.CompletedTask; } } public class FailingConsumer : IConsumer { - public Task ConsumeAsync(TestEvent payload, CancellationToken cancellationToken) + public ValueTask ConsumeAsync(TestEvent payload, CancellationToken cancellationToken) { throw new Exception("Consumer failed."); } diff --git a/tests/ReflectionEventing.DependencyInjection.UnitTests/EventBusBuilderExtensionsTests.cs b/tests/ReflectionEventing.DependencyInjection.UnitTests/EventBusBuilderExtensionsTests.cs index fb41614..5f3fdc9 100644 --- a/tests/ReflectionEventing.DependencyInjection.UnitTests/EventBusBuilderExtensionsTests.cs +++ b/tests/ReflectionEventing.DependencyInjection.UnitTests/EventBusBuilderExtensionsTests.cs @@ -109,7 +109,7 @@ public record TestEvent : ITestEvent; public class TestConsumer : IConsumer { - public Task ConsumeAsync(TestEvent payload, CancellationToken cancellationToken) => - Task.CompletedTask; + public ValueTask ConsumeAsync(TestEvent payload, CancellationToken cancellationToken) => + ValueTask.CompletedTask; } } diff --git a/tests/ReflectionEventing.DependencyInjection.UnitTests/GlobalUsings.cs b/tests/ReflectionEventing.DependencyInjection.UnitTests/GlobalUsings.cs index fc3403c..e77dd30 100644 --- a/tests/ReflectionEventing.DependencyInjection.UnitTests/GlobalUsings.cs +++ b/tests/ReflectionEventing.DependencyInjection.UnitTests/GlobalUsings.cs @@ -8,7 +8,7 @@ global using System.Linq; global using System.Threading; global using System.Threading.Tasks; -global using FluentAssertions; +global using AwesomeAssertions; global using Microsoft.Extensions.DependencyInjection; global using Microsoft.Extensions.Hosting; global using NSubstitute; diff --git a/tests/ReflectionEventing.DependencyInjection.UnitTests/ReflectionEventing.DependencyInjection.UnitTests.csproj b/tests/ReflectionEventing.DependencyInjection.UnitTests/ReflectionEventing.DependencyInjection.UnitTests.csproj index b37d719..3a31734 100644 --- a/tests/ReflectionEventing.DependencyInjection.UnitTests/ReflectionEventing.DependencyInjection.UnitTests.csproj +++ b/tests/ReflectionEventing.DependencyInjection.UnitTests/ReflectionEventing.DependencyInjection.UnitTests.csproj @@ -1,9 +1,11 @@ - - net9.0 + net10.0 + false false true + $(NoWarn);SA1401;NU1903; + Exe @@ -12,7 +14,8 @@ - + + runtime; build; native; contentfiles; analyzers; buildtransitive all @@ -26,5 +29,4 @@ - diff --git a/tests/ReflectionEventing.DependencyInjection.UnitTests/ServiceCollectionExtensionsTests.cs b/tests/ReflectionEventing.DependencyInjection.UnitTests/ServiceCollectionExtensionsTests.cs index f2ea6a5..f6a98ab 100644 --- a/tests/ReflectionEventing.DependencyInjection.UnitTests/ServiceCollectionExtensionsTests.cs +++ b/tests/ReflectionEventing.DependencyInjection.UnitTests/ServiceCollectionExtensionsTests.cs @@ -39,8 +39,8 @@ public void AddEventBus_RegistersServicesAndAddsConsumer() public class TestConsumer : IConsumer { - public Task ConsumeAsync(TestEvent payload, CancellationToken cancellationToken) => - Task.CompletedTask; + public ValueTask ConsumeAsync(TestEvent payload, CancellationToken cancellationToken) => + ValueTask.CompletedTask; } public sealed record TestEvent; diff --git a/tests/ReflectionEventing.Ninject.UnitTests/EventBusModuleTests.cs b/tests/ReflectionEventing.Ninject.UnitTests/EventBusModuleTests.cs deleted file mode 100644 index 1ceaf52..0000000 --- a/tests/ReflectionEventing.Ninject.UnitTests/EventBusModuleTests.cs +++ /dev/null @@ -1,49 +0,0 @@ -// This Source Code Form is subject to the terms of the MIT License. -// If a copy of the MIT was not distributed with this file, You can obtain one at https://opensource.org/licenses/MIT. -// Copyright (C) Leszek Pomianowski and ReflectionEventing Contributors. -// All Rights Reserved. - -using Ninject; - -namespace ReflectionEventing.Ninject.UnitTests; - -public sealed class EventBusModuleTests -{ - [Fact] - public void Load_RegistersServicesAndAddsConsumer() - { - IKernel kernel = new StandardKernel(); - - _ = kernel.Bind().ToSelf().InTransientScope(); - - kernel.Load( - new EventBusModule(builder => - { - _ = builder.AddConsumer(); - }) - ); - - IConsumerTypesProvider? consumerTypesProvider = kernel.Get(); - _ = consumerTypesProvider.Should().NotBeNull(); - _ = consumerTypesProvider.Should().BeOfType(); - - IConsumerProvider? consumerProvider = kernel.Get(); - _ = consumerProvider.Should().NotBeNull(); - _ = consumerProvider.Should().BeOfType(); - - IEventBus? eventBus = kernel.Get(); - _ = eventBus.Should().NotBeNull(); - _ = eventBus.Should().BeOfType(); - - IEnumerable consumers = consumerTypesProvider!.GetConsumerTypes(); - _ = consumers.First().Should().Be(typeof(TestConsumer)); - } - - public class TestConsumer : IConsumer - { - public Task ConsumeAsync(TestEvent payload, CancellationToken cancellationToken) => - Task.CompletedTask; - } - - public sealed record TestEvent; -} diff --git a/tests/ReflectionEventing.Ninject.UnitTests/GlobalUsings.cs b/tests/ReflectionEventing.Ninject.UnitTests/GlobalUsings.cs deleted file mode 100644 index 47b5797..0000000 --- a/tests/ReflectionEventing.Ninject.UnitTests/GlobalUsings.cs +++ /dev/null @@ -1,12 +0,0 @@ -// This Source Code Form is subject to the terms of the MIT License. -// If a copy of the MIT was not distributed with this file, You can obtain one at https://opensource.org/licenses/MIT. -// Copyright (C) Leszek Pomianowski and ReflectionEventing Contributors. -// All Rights Reserved. - -global using System; -global using System.Collections.Generic; -global using System.Linq; -global using System.Threading; -global using System.Threading.Tasks; -global using FluentAssertions; -global using Xunit; diff --git a/tests/ReflectionEventing.Ninject.UnitTests/ReflectionEventing.Ninject.UnitTests.csproj b/tests/ReflectionEventing.Ninject.UnitTests/ReflectionEventing.Ninject.UnitTests.csproj deleted file mode 100644 index 44c6815..0000000 --- a/tests/ReflectionEventing.Ninject.UnitTests/ReflectionEventing.Ninject.UnitTests.csproj +++ /dev/null @@ -1,28 +0,0 @@ - - - - net9.0 - false - true - - - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - - - - diff --git a/tests/ReflectionEventing.UnitTests/EventBusBuilderTests.cs b/tests/ReflectionEventing.UnitTests/EventBusBuilderTests.cs index 45342bd..6b5572b 100644 --- a/tests/ReflectionEventing.UnitTests/EventBusBuilderTests.cs +++ b/tests/ReflectionEventing.UnitTests/EventBusBuilderTests.cs @@ -3,7 +3,7 @@ // Copyright (C) Leszek Pomianowski and ReflectionEventing Contributors. // All Rights Reserved. -using FluentAssertions.Collections; +using AwesomeAssertions.Collections; namespace ReflectionEventing.UnitTests; @@ -83,24 +83,24 @@ public sealed record SecondaryEvent : IBaseEvent; public sealed record MySampleConsumer : IConsumer, IConsumer { - public Task ConsumeAsync(ITestEvent payload, CancellationToken cancellationToken) + public ValueTask ConsumeAsync(ITestEvent payload, CancellationToken cancellationToken) { - return Task.CompletedTask; + return ValueTask.CompletedTask; } /// - public Task ConsumeAsync(IBaseEvent payload, CancellationToken cancellationToken) + public ValueTask ConsumeAsync(IBaseEvent payload, CancellationToken cancellationToken) { - return Task.CompletedTask; + return ValueTask.CompletedTask; } } public sealed record MySecondarySampleConsumer : IConsumer { /// - public Task ConsumeAsync(IBaseEvent payload, CancellationToken cancellationToken) + public ValueTask ConsumeAsync(IBaseEvent payload, CancellationToken cancellationToken) { - return Task.CompletedTask; + return ValueTask.CompletedTask; } } } diff --git a/tests/ReflectionEventing.UnitTests/EventBusTests.cs b/tests/ReflectionEventing.UnitTests/EventBusTests.cs index 6158a1c..fd4f2d4 100644 --- a/tests/ReflectionEventing.UnitTests/EventBusTests.cs +++ b/tests/ReflectionEventing.UnitTests/EventBusTests.cs @@ -12,19 +12,20 @@ public sealed class EventBusTests private readonly IConsumerProvider _consumerProvider; private readonly IConsumerTypesProvider _consumerTypesProvider; private readonly IEventsQueue _eventsQueue; - private readonly EventBus _eventBus; public EventBusTests() { _consumerProvider = Substitute.For(); _consumerTypesProvider = Substitute.For(); _eventsQueue = Substitute.For(); - _eventBus = new EventBus(_consumerProvider, _consumerTypesProvider, _eventsQueue); } [Fact] public async Task SendAsync_ShouldCallConsumeAsyncOnAllConsumers() { + EventBusBuilderOptions options = new(); + EventBus eventBus = new(options, _consumerProvider, _consumerTypesProvider, _eventsQueue); + TestEvent testEvent = new(); Type consumerType = typeof(IConsumer); IConsumer consumer = Substitute.For>(); @@ -32,7 +33,7 @@ public async Task SendAsync_ShouldCallConsumeAsyncOnAllConsumers() _ = _consumerTypesProvider.GetConsumerTypes().Returns([consumerType]); _ = _consumerProvider.GetConsumers(consumerType).Returns([consumer]); - await _eventBus.SendAsync(testEvent, CancellationToken.None); + await eventBus.SendAsync(testEvent, CancellationToken.None); await consumer.Received().ConsumeAsync(testEvent, Arg.Any()); } @@ -41,6 +42,9 @@ public async Task SendAsync_ShouldCallConsumeAsyncOnAllConsumers() #pragma warning disable CS0618 // Type or member is obsolete public async Task Send_ShouldCallSendAsync() { + EventBusBuilderOptions options = new(); + EventBus eventBus = new(options, _consumerProvider, _consumerTypesProvider, _eventsQueue); + TestEvent testEvent = new(); Type consumerType = typeof(IConsumer); IConsumer consumer = Substitute.For>(); @@ -48,11 +52,105 @@ public async Task Send_ShouldCallSendAsync() _ = _consumerTypesProvider.GetConsumerTypes().Returns([consumerType]); _ = _consumerProvider.GetConsumers(consumerType).Returns([consumer]); - _eventBus.Send(testEvent); + eventBus.Send(testEvent); await consumer.Received().ConsumeAsync(testEvent, Arg.Any()); } #pragma warning restore CS0618 // Type or member is obsolete + [Fact] + public async Task SendAsync_WithParallelMode_ShouldExecuteConsumersConcurrently() + { + // Arrange + EventBusBuilderOptions options = new() { ConsumerExecutionMode = ProcessingMode.Parallel }; + EventBus eventBus = new(options, _consumerProvider, _consumerTypesProvider, _eventsQueue); + + TestEvent testEvent = new(); + Type consumerType = typeof(IConsumer); + + IConsumer consumer1 = Substitute.For>(); + IConsumer consumer2 = Substitute.For>(); + IConsumer consumer3 = Substitute.For>(); + + _ = _consumerTypesProvider.GetConsumerTypes().Returns([consumerType]); + _ = _consumerProvider + .GetConsumers(consumerType) + .Returns([consumer1, consumer2, consumer3]); + + // Act + await eventBus.SendAsync(testEvent, CancellationToken.None); + + // Assert + await consumer1.Received(1).ConsumeAsync(testEvent, Arg.Any()); + await consumer2.Received(1).ConsumeAsync(testEvent, Arg.Any()); + await consumer3.Received(1).ConsumeAsync(testEvent, Arg.Any()); + } + + [Fact] + public async Task SendAsync_WithSequentialMode_ShouldExecuteConsumersSequentially() + { + // Arrange + EventBusBuilderOptions options = + new() { ConsumerExecutionMode = ProcessingMode.Sequential }; + EventBus eventBus = new(options, _consumerProvider, _consumerTypesProvider, _eventsQueue); + + TestEvent testEvent = new(); + Type consumerType = typeof(IConsumer); + + List executionOrder = []; + + IConsumer consumer1 = Substitute.For>(); + _ = consumer1 + .ConsumeAsync(testEvent, Arg.Any()) + .Returns(callInfo => + { + executionOrder.Add(1); + return ValueTask.CompletedTask; + }); + + IConsumer consumer2 = Substitute.For>(); + _ = consumer2 + .ConsumeAsync(testEvent, Arg.Any()) + .Returns(callInfo => + { + executionOrder.Add(2); + return ValueTask.CompletedTask; + }); + + IConsumer consumer3 = Substitute.For>(); + _ = consumer3 + .ConsumeAsync(testEvent, Arg.Any()) + .Returns(callInfo => + { + executionOrder.Add(3); + return ValueTask.CompletedTask; + }); + + _ = _consumerTypesProvider.GetConsumerTypes().Returns([consumerType]); + _ = _consumerProvider + .GetConsumers(consumerType) + .Returns([consumer1, consumer2, consumer3]); + + // Act + await eventBus.SendAsync(testEvent, CancellationToken.None); + + // Assert + await consumer1.Received(1).ConsumeAsync(testEvent, Arg.Any()); + await consumer2.Received(1).ConsumeAsync(testEvent, Arg.Any()); + await consumer3.Received(1).ConsumeAsync(testEvent, Arg.Any()); + + Assert.Equal([1, 2, 3], executionOrder); + } + + [Fact] + public void EventBusBuilderOptions_ShouldHaveParallelAsDefaultConsumerExecutionMode() + { + // Arrange & Act + EventBusBuilderOptions options = new(); + + // Assert + Assert.Equal(ProcessingMode.Parallel, options.ConsumerExecutionMode); + } + public sealed record TestEvent; } diff --git a/tests/ReflectionEventing.UnitTests/GlobalUsings.cs b/tests/ReflectionEventing.UnitTests/GlobalUsings.cs index 1ea6c8b..069ebc7 100644 --- a/tests/ReflectionEventing.UnitTests/GlobalUsings.cs +++ b/tests/ReflectionEventing.UnitTests/GlobalUsings.cs @@ -7,6 +7,6 @@ global using System.Collections.Generic; global using System.Threading; global using System.Threading.Tasks; -global using FluentAssertions; +global using AwesomeAssertions; global using NSubstitute; global using Xunit; diff --git a/tests/ReflectionEventing.UnitTests/HashedConsumerTypesProviderTests.cs b/tests/ReflectionEventing.UnitTests/HashedConsumerTypesProviderTests.cs index 528bb3a..80c1f5f 100644 --- a/tests/ReflectionEventing.UnitTests/HashedConsumerTypesProviderTests.cs +++ b/tests/ReflectionEventing.UnitTests/HashedConsumerTypesProviderTests.cs @@ -36,19 +36,19 @@ private sealed record SecondaryTestEvent : ITestEvent; private sealed record PrimarySampleConsumer : IConsumer { - public Task ConsumeAsync(PrimaryTestEvent payload, CancellationToken cancellationToken) => - Task.CompletedTask; + public ValueTask ConsumeAsync(PrimaryTestEvent payload, CancellationToken cancellationToken) => + ValueTask.CompletedTask; } private sealed record SecondarySampleConsumer : IConsumer { - public Task ConsumeAsync(PrimaryTestEvent payload, CancellationToken cancellationToken) => - Task.CompletedTask; + public ValueTask ConsumeAsync(PrimaryTestEvent payload, CancellationToken cancellationToken) => + ValueTask.CompletedTask; } private sealed record TertiarySampleConsumer : IConsumer { - public Task ConsumeAsync(SecondaryTestEvent payload, CancellationToken cancellationToken) => - Task.CompletedTask; + public ValueTask ConsumeAsync(SecondaryTestEvent payload, CancellationToken cancellationToken) => + ValueTask.CompletedTask; } } diff --git a/tests/ReflectionEventing.UnitTests/ReflectionEventing.UnitTests.csproj b/tests/ReflectionEventing.UnitTests/ReflectionEventing.UnitTests.csproj index 36672a0..6582d70 100644 --- a/tests/ReflectionEventing.UnitTests/ReflectionEventing.UnitTests.csproj +++ b/tests/ReflectionEventing.UnitTests/ReflectionEventing.UnitTests.csproj @@ -1,16 +1,19 @@ - - net9.0 + net10.0 + false false true + $(NoWarn);SA1401;NU1903; + Exe - - - - + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all @@ -24,5 +27,4 @@ - diff --git a/tests/ReflectionEventing.Unity.UnitTests/GlobalUsings.cs b/tests/ReflectionEventing.Unity.UnitTests/GlobalUsings.cs deleted file mode 100644 index 25c353c..0000000 --- a/tests/ReflectionEventing.Unity.UnitTests/GlobalUsings.cs +++ /dev/null @@ -1,11 +0,0 @@ -// This Source Code Form is subject to the terms of the MIT License. -// If a copy of the MIT was not distributed with this file, You can obtain one at https://opensource.org/licenses/MIT. -// Copyright (C) Leszek Pomianowski and ReflectionEventing Contributors. -// All Rights Reserved. - -global using System.Collections.Generic; -global using System.Linq; -global using System.Threading; -global using System.Threading.Tasks; -global using FluentAssertions; -global using Xunit; diff --git a/tests/ReflectionEventing.Unity.UnitTests/ReflectionEventing.Unity.UnitTests.csproj b/tests/ReflectionEventing.Unity.UnitTests/ReflectionEventing.Unity.UnitTests.csproj deleted file mode 100644 index 9d8d2de..0000000 --- a/tests/ReflectionEventing.Unity.UnitTests/ReflectionEventing.Unity.UnitTests.csproj +++ /dev/null @@ -1,30 +0,0 @@ - - - - net9.0 - false - true - - - - - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - - - - diff --git a/tests/ReflectionEventing.Unity.UnitTests/UnityContainerExtensionsTests.cs b/tests/ReflectionEventing.Unity.UnitTests/UnityContainerExtensionsTests.cs deleted file mode 100644 index 67afaa0..0000000 --- a/tests/ReflectionEventing.Unity.UnitTests/UnityContainerExtensionsTests.cs +++ /dev/null @@ -1,48 +0,0 @@ -// This Source Code Form is subject to the terms of the MIT License. -// If a copy of the MIT was not distributed with this file, You can obtain one at https://opensource.org/licenses/MIT. -// Copyright (C) Leszek Pomianowski and ReflectionEventing Contributors. -// All Rights Reserved. - -using System; -using Unity; -using Unity.Lifetime; - -namespace ReflectionEventing.Unity.UnitTests; - -public sealed class UnityContainerExtensionsTests -{ - [Fact] - public void AddEventBus_RegistersServicesAndAddsConsumer() - { - UnityContainer container = new UnityContainer(); - - _ = container.RegisterType(new HierarchicalLifetimeManager()); - _ = container.AddEventBus(builder => - { - _ = builder.AddConsumer(); - }); - - IConsumerTypesProvider? consumerTypesProvider = container.Resolve(); - _ = consumerTypesProvider.Should().NotBeNull(); - _ = consumerTypesProvider.Should().BeOfType(); - - IConsumerProvider? consumerProvider = container.Resolve(); - _ = consumerProvider.Should().NotBeNull(); - _ = consumerProvider.Should().BeOfType(); - - IEventBus? eventBus = container.Resolve(); - _ = eventBus.Should().NotBeNull(); - _ = eventBus.Should().BeOfType(); - - IEnumerable consumers = consumerTypesProvider!.GetConsumerTypes(); - _ = consumers.First().Should().Be(typeof(TestConsumer)); - } - - public class TestConsumer : IConsumer - { - public Task ConsumeAsync(TestEvent payload, CancellationToken cancellationToken) => - Task.CompletedTask; - } - - public sealed record TestEvent; -}