From 30d8b8fd06b1ee08d709368dc0e2015aa4fa9063 Mon Sep 17 00:00:00 2001 From: pomian <13592821+pomianowski@users.noreply.github.com> Date: Mon, 9 Feb 2026 14:48:44 +0100 Subject: [PATCH 1/6] preapre docs --- .serena/.gitignore | 1 + .serena/memories/architecture_patterns.md | 108 ++++++ .../memories/code_style_and_conventions.md | 67 ++++ .serena/memories/project_purpose.md | 41 +++ .serena/memories/suggested_commands.md | 110 +++++++ .serena/memories/task_completion_checklist.md | 78 +++++ .serena/memories/tech_stack.md | 51 +++ .serena/project.yml | 112 +++++++ .vscode/settings.json | 5 + Directory.Build.props | 6 +- docs/architecture/README.md | 139 ++++++++ .../contracts/messaging/events.md | 205 ++++++++++++ .../cross-cutting/observability.md | 161 +++++++++ .../decisions/ADR-001-event-bus-pattern.md | 105 ++++++ .../ADR-002-multi-di-container-support.md | 134 ++++++++ .../decisions/ADR-003-async-first-design.md | 160 +++++++++ .../decisions/ADR-004-multi-targeting.md | 164 ++++++++++ docs/architecture/domain/overview.md | 189 +++++++++++ .../domain/ubiquitous-language.md | 139 ++++++++ docs/architecture/views/context.md | 140 ++++++++ .../views/logical-architecture.md | 307 ++++++++++++++++++ docs/board.md | 5 + .../ReflectionEventing.Autofac.csproj | 2 +- .../ReflectionEventing.Castle.Windsor.csproj | 2 +- .../ReflectionEventing.Demo.Wpf.csproj | 2 +- ...lectionEventing.DependencyInjection.csproj | 4 +- .../ReflectionEventing.Ninject.csproj | 2 +- .../ReflectionEventing.Unity.csproj | 2 +- .../ReflectionEventing.csproj | 2 +- ...eflectionEventing.Autofac.UnitTests.csproj | 10 +- ...onEventing.Castle.Windsor.UnitTests.csproj | 10 +- ...nting.DependencyInjection.UnitTests.csproj | 4 +- ...eflectionEventing.Ninject.UnitTests.csproj | 4 +- .../ReflectionEventing.UnitTests.csproj | 10 +- .../ReflectionEventing.Unity.UnitTests.csproj | 2 +- 35 files changed, 2447 insertions(+), 36 deletions(-) create mode 100644 .serena/.gitignore create mode 100644 .serena/memories/architecture_patterns.md create mode 100644 .serena/memories/code_style_and_conventions.md create mode 100644 .serena/memories/project_purpose.md create mode 100644 .serena/memories/suggested_commands.md create mode 100644 .serena/memories/task_completion_checklist.md create mode 100644 .serena/memories/tech_stack.md create mode 100644 .serena/project.yml create mode 100644 .vscode/settings.json create mode 100644 docs/architecture/README.md create mode 100644 docs/architecture/contracts/messaging/events.md create mode 100644 docs/architecture/cross-cutting/observability.md create mode 100644 docs/architecture/decisions/ADR-001-event-bus-pattern.md create mode 100644 docs/architecture/decisions/ADR-002-multi-di-container-support.md create mode 100644 docs/architecture/decisions/ADR-003-async-first-design.md create mode 100644 docs/architecture/decisions/ADR-004-multi-targeting.md create mode 100644 docs/architecture/domain/overview.md create mode 100644 docs/architecture/domain/ubiquitous-language.md create mode 100644 docs/architecture/views/context.md create mode 100644 docs/architecture/views/logical-architecture.md create mode 100644 docs/board.md 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..048edd5 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -6,8 +6,8 @@ - 4.1.0 - 4.0.0 + 5.0.0 + 5.0.0 @@ -38,7 +38,7 @@ true - 13.0 + 14.0 enable >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 Task ConsumeAsync(ITickedEvent payload, CancellationToken cancellationToken) + { + Console.WriteLine($"Received tick: {payload.TickCount}"); + return Task.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 Task ConsumeAsync(BackgroundTicked payload, CancellationToken cancellationToken) + { + // Handle the event + return Task.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..aae53cc --- /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 Task 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..8934827 --- /dev/null +++ b/docs/architecture/decisions/ADR-003-async-first-design.md @@ -0,0 +1,160 @@ +# ADR-003: Async-First Design for All Operations + +## Status + +**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/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..e84b20b --- /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 Task ConsumeAsync(BackgroundTicked payload, CancellationToken cancellationToken) + { + // Handle the event + return Task.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/board.md b/docs/board.md new file mode 100644 index 0000000..e746250 --- /dev/null +++ b/docs/board.md @@ -0,0 +1,5 @@ +# Tasks + +## Feature - ValueTask + +### 1. Implement ValueTask to all async methods in the codebase diff --git a/src/ReflectionEventing.Autofac/ReflectionEventing.Autofac.csproj b/src/ReflectionEventing.Autofac/ReflectionEventing.Autofac.csproj index e8f20d4..52bfae4 100644 --- a/src/ReflectionEventing.Autofac/ReflectionEventing.Autofac.csproj +++ b/src/ReflectionEventing.Autofac/ReflectionEventing.Autofac.csproj @@ -2,7 +2,7 @@ ReflectionEventing.Autofac - 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/src/ReflectionEventing.Castle.Windsor/ReflectionEventing.Castle.Windsor.csproj b/src/ReflectionEventing.Castle.Windsor/ReflectionEventing.Castle.Windsor.csproj index b180158..c9c1b26 100644 --- a/src/ReflectionEventing.Castle.Windsor/ReflectionEventing.Castle.Windsor.csproj +++ b/src/ReflectionEventing.Castle.Windsor/ReflectionEventing.Castle.Windsor.csproj @@ -2,7 +2,7 @@ ReflectionEventing.Castle.Windsor - 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/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.DependencyInjection/ReflectionEventing.DependencyInjection.csproj b/src/ReflectionEventing.DependencyInjection/ReflectionEventing.DependencyInjection.csproj index 8c278a2..0954e75 100644 --- a/src/ReflectionEventing.DependencyInjection/ReflectionEventing.DependencyInjection.csproj +++ b/src/ReflectionEventing.DependencyInjection/ReflectionEventing.DependencyInjection.csproj @@ -2,7 +2,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 @@ -51,4 +51,4 @@ - \ No newline at end of file + diff --git a/src/ReflectionEventing.Ninject/ReflectionEventing.Ninject.csproj b/src/ReflectionEventing.Ninject/ReflectionEventing.Ninject.csproj index 1ef33e6..f3a20c6 100644 --- a/src/ReflectionEventing.Ninject/ReflectionEventing.Ninject.csproj +++ b/src/ReflectionEventing.Ninject/ReflectionEventing.Ninject.csproj @@ -2,7 +2,7 @@ ReflectionEventing.Ninject - 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/src/ReflectionEventing.Unity/ReflectionEventing.Unity.csproj b/src/ReflectionEventing.Unity/ReflectionEventing.Unity.csproj index 7ff6966..0b4a679 100644 --- a/src/ReflectionEventing.Unity/ReflectionEventing.Unity.csproj +++ b/src/ReflectionEventing.Unity/ReflectionEventing.Unity.csproj @@ -2,7 +2,7 @@ ReflectionEventing.Unity - 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/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/ReflectionEventing.Autofac.UnitTests.csproj b/tests/ReflectionEventing.Autofac.UnitTests/ReflectionEventing.Autofac.UnitTests.csproj index 51c7a6e..c5832f9 100644 --- a/tests/ReflectionEventing.Autofac.UnitTests/ReflectionEventing.Autofac.UnitTests.csproj +++ b/tests/ReflectionEventing.Autofac.UnitTests/ReflectionEventing.Autofac.UnitTests.csproj @@ -1,15 +1,14 @@ - - net9.0 + net10.0 false true - - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive @@ -24,5 +23,4 @@ - diff --git a/tests/ReflectionEventing.Castle.Windsor.UnitTests/ReflectionEventing.Castle.Windsor.UnitTests.csproj b/tests/ReflectionEventing.Castle.Windsor.UnitTests/ReflectionEventing.Castle.Windsor.UnitTests.csproj index 554dc11..7592060 100644 --- a/tests/ReflectionEventing.Castle.Windsor.UnitTests/ReflectionEventing.Castle.Windsor.UnitTests.csproj +++ b/tests/ReflectionEventing.Castle.Windsor.UnitTests/ReflectionEventing.Castle.Windsor.UnitTests.csproj @@ -1,15 +1,14 @@ - - net9.0 + net10.0 false true - - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive @@ -24,5 +23,4 @@ - diff --git a/tests/ReflectionEventing.DependencyInjection.UnitTests/ReflectionEventing.DependencyInjection.UnitTests.csproj b/tests/ReflectionEventing.DependencyInjection.UnitTests/ReflectionEventing.DependencyInjection.UnitTests.csproj index b37d719..9cb9b4a 100644 --- a/tests/ReflectionEventing.DependencyInjection.UnitTests/ReflectionEventing.DependencyInjection.UnitTests.csproj +++ b/tests/ReflectionEventing.DependencyInjection.UnitTests/ReflectionEventing.DependencyInjection.UnitTests.csproj @@ -1,7 +1,6 @@ - - net9.0 + net10.0 false true @@ -26,5 +25,4 @@ - diff --git a/tests/ReflectionEventing.Ninject.UnitTests/ReflectionEventing.Ninject.UnitTests.csproj b/tests/ReflectionEventing.Ninject.UnitTests/ReflectionEventing.Ninject.UnitTests.csproj index 44c6815..2fab8b5 100644 --- a/tests/ReflectionEventing.Ninject.UnitTests/ReflectionEventing.Ninject.UnitTests.csproj +++ b/tests/ReflectionEventing.Ninject.UnitTests/ReflectionEventing.Ninject.UnitTests.csproj @@ -1,7 +1,6 @@ - - net9.0 + net10.0 false true @@ -24,5 +23,4 @@ - diff --git a/tests/ReflectionEventing.UnitTests/ReflectionEventing.UnitTests.csproj b/tests/ReflectionEventing.UnitTests/ReflectionEventing.UnitTests.csproj index 36672a0..90648d6 100644 --- a/tests/ReflectionEventing.UnitTests/ReflectionEventing.UnitTests.csproj +++ b/tests/ReflectionEventing.UnitTests/ReflectionEventing.UnitTests.csproj @@ -1,15 +1,14 @@ - - net9.0 + net10.0 false true - - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive @@ -24,5 +23,4 @@ - diff --git a/tests/ReflectionEventing.Unity.UnitTests/ReflectionEventing.Unity.UnitTests.csproj b/tests/ReflectionEventing.Unity.UnitTests/ReflectionEventing.Unity.UnitTests.csproj index 9d8d2de..bc08a7c 100644 --- a/tests/ReflectionEventing.Unity.UnitTests/ReflectionEventing.Unity.UnitTests.csproj +++ b/tests/ReflectionEventing.Unity.UnitTests/ReflectionEventing.Unity.UnitTests.csproj @@ -1,7 +1,7 @@  - net9.0 + net10.0 false true From f736e9a885b601edde31dc181f8389dd3d064fc0 Mon Sep 17 00:00:00 2001 From: pomian <13592821+pomianowski@users.noreply.github.com> Date: Mon, 9 Feb 2026 14:55:06 +0100 Subject: [PATCH 2/6] bump version --- Directory.Build.props | 9 +- Directory.Build.targets | 46 +++- docs/tasks/valuetask-implementation-plan.md | 200 ++++++++++++++++++ ...lectionEventing.DependencyInjection.csproj | 9 +- .../DependencyInjectionQueueProcessorTests.cs | 2 +- 5 files changed, 248 insertions(+), 18 deletions(-) create mode 100644 docs/tasks/valuetask-implementation-plan.md diff --git a/Directory.Build.props b/Directory.Build.props index 048edd5..1ce5140 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,5 +1,4 @@ - $(MSBuildThisFileDirectory) $(RepositoryDirectory)build\ @@ -53,14 +52,17 @@ $(MSBuildProjectName.Contains('Test')) False True - True + Or '$(TargetFramework)' == 'net7.0'" + >True @@ -112,5 +114,4 @@ - diff --git a/Directory.Build.targets b/Directory.Build.targets index 4876660..34ad327 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -1,5 +1,4 @@ - true @@ -13,7 +12,9 @@ true README.md - true + true true snupkg true @@ -21,15 +22,38 @@ - - - - + + + + - + <_Parameter1>CommitHash <_Parameter2>$(SourceRevisionId) @@ -37,7 +61,7 @@ - + true true true @@ -49,7 +73,11 @@ $(RepositoryDirectory)\src\lepo.snk - + - - - 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 c9c1b26..0000000 --- a/src/ReflectionEventing.Castle.Windsor/ReflectionEventing.Castle.Windsor.csproj +++ /dev/null @@ -1,44 +0,0 @@ - - - - ReflectionEventing.Castle.Windsor - net10.0;net9.0;net8.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/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/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 310ce9c..21d066f 100644 --- a/src/ReflectionEventing.DependencyInjection/DependencyInjectionEventBus.cs +++ b/src/ReflectionEventing.DependencyInjection/DependencyInjectionEventBus.cs @@ -9,11 +9,11 @@ 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 ValueTask PublishAsync( @@ -21,7 +21,7 @@ public override ValueTask PublishAsync( 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/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.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 f3a20c6..0000000 --- a/src/ReflectionEventing.Ninject/ReflectionEventing.Ninject.csproj +++ /dev/null @@ -1,43 +0,0 @@ - - - - ReflectionEventing.Ninject - net10.0;net9.0;net8.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 0b4a679..0000000 --- a/src/ReflectionEventing.Unity/ReflectionEventing.Unity.csproj +++ /dev/null @@ -1,44 +0,0 @@ - - - - ReflectionEventing.Unity - net10.0;net9.0;net8.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 0936928..6c3049f 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 @@ -74,14 +75,10 @@ public virtual ValueTask SendAsync( return ((IConsumer)consumers[0]).ConsumeAsync(eventItem, cancellationToken); } - // Multiple consumers - collect tasks and await all - List tasks = new(consumers.Count); - foreach (object consumer in consumers) - { - tasks.Add(((IConsumer)consumer).ConsumeAsync(eventItem, cancellationToken)); - } - - return WhenAll(tasks); + // Multiple consumers - execute based on configured mode + return options.ConsumerExecutionMode == ProcessingMode.Sequential + ? ExecuteSequentialAsync(consumers, eventItem, cancellationToken) + : ExecuteParallelAsync(consumers, eventItem, cancellationToken); } /// @@ -104,10 +101,63 @@ public virtual ValueTask PublishAsync( } /// - /// Waits for all ValueTasks to complete in parallel. + /// 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 list of ValueTasks to wait for (must contain 2 or more tasks). - /// A ValueTask that completes when all tasks have completed. - private static ValueTask WhenAll(List tasks) => - new(Task.WhenAll(tasks.Select(t => t.AsTask()))); + /// 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) + { + asyncTasks ??= new List(consumers.Count); + asyncTasks.Add(task); + } + } + + // Only allocate Task objects for truly async operations + return asyncTasks == null + ? default + : new ValueTask(Task.WhenAll(asyncTasks.Select(t => t.AsTask()))); + } } 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/tests/ReflectionEventing.Autofac.UnitTests/AutofacConsumerProviderTests.cs b/tests/ReflectionEventing.Autofac.UnitTests/AutofacConsumerProviderTests.cs deleted file mode 100644 index fd0c841..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 ValueTask ConsumeAsync(TestEvent payload, CancellationToken cancellationToken) => - ValueTask.CompletedTask; - } -} diff --git a/tests/ReflectionEventing.Autofac.UnitTests/ContainerBuilderExtensionsTests.cs b/tests/ReflectionEventing.Autofac.UnitTests/ContainerBuilderExtensionsTests.cs deleted file mode 100644 index 621da39..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 ValueTask ConsumeAsync(TestEvent payload, CancellationToken cancellationToken) => - ValueTask.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 c5832f9..0000000 --- a/tests/ReflectionEventing.Autofac.UnitTests/ReflectionEventing.Autofac.UnitTests.csproj +++ /dev/null @@ -1,26 +0,0 @@ - - - net10.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 872b6c9..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 ValueTask ConsumeAsync(TestEvent payload, CancellationToken cancellationToken) => - ValueTask.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 6adafe5..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 ValueTask ConsumeAsync(TestEvent payload, CancellationToken cancellationToken) - { - ReceivedEvents++; - - return ValueTask.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 7592060..0000000 --- a/tests/ReflectionEventing.Castle.Windsor.UnitTests/ReflectionEventing.Castle.Windsor.UnitTests.csproj +++ /dev/null @@ -1,26 +0,0 @@ - - - net10.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 89d6cf4..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()); diff --git a/tests/ReflectionEventing.DependencyInjection.UnitTests/DependencyInjectionQueueProcessorTests.cs b/tests/ReflectionEventing.DependencyInjection.UnitTests/DependencyInjectionQueueProcessorTests.cs index e9c45f7..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,7 +68,8 @@ 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 = async () => await bus.PublishAsync(new OtherEvent()); @@ -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()); 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 9cb9b4a..3a31734 100644 --- a/tests/ReflectionEventing.DependencyInjection.UnitTests/ReflectionEventing.DependencyInjection.UnitTests.csproj +++ b/tests/ReflectionEventing.DependencyInjection.UnitTests/ReflectionEventing.DependencyInjection.UnitTests.csproj @@ -1,8 +1,11 @@ net10.0 + false false true + $(NoWarn);SA1401;NU1903; + Exe @@ -11,7 +14,8 @@ - + + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/tests/ReflectionEventing.Ninject.UnitTests/EventBusModuleTests.cs b/tests/ReflectionEventing.Ninject.UnitTests/EventBusModuleTests.cs deleted file mode 100644 index 99f11ac..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 ValueTask ConsumeAsync(TestEvent payload, CancellationToken cancellationToken) => - ValueTask.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 2fab8b5..0000000 --- a/tests/ReflectionEventing.Ninject.UnitTests/ReflectionEventing.Ninject.UnitTests.csproj +++ /dev/null @@ -1,26 +0,0 @@ - - - net10.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 1e57976..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; 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/ReflectionEventing.UnitTests.csproj b/tests/ReflectionEventing.UnitTests/ReflectionEventing.UnitTests.csproj index 90648d6..6582d70 100644 --- a/tests/ReflectionEventing.UnitTests/ReflectionEventing.UnitTests.csproj +++ b/tests/ReflectionEventing.UnitTests/ReflectionEventing.UnitTests.csproj @@ -1,15 +1,19 @@ net10.0 + false false true + $(NoWarn);SA1401;NU1903; + Exe - + + runtime; build; native; contentfiles; analyzers; buildtransitive all 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 bc08a7c..0000000 --- a/tests/ReflectionEventing.Unity.UnitTests/ReflectionEventing.Unity.UnitTests.csproj +++ /dev/null @@ -1,30 +0,0 @@ - - - - net10.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 b3cca6a..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 ValueTask ConsumeAsync(TestEvent payload, CancellationToken cancellationToken) => - ValueTask.CompletedTask; - } - - public sealed record TestEvent; -} From 75733c0c6dc4f8b4e7862adac860db9c6c0201c9 Mon Sep 17 00:00:00 2001 From: pomian <13592821+pomianowski@users.noreply.github.com> Date: Mon, 9 Feb 2026 16:54:22 +0100 Subject: [PATCH 5/6] allocate dispatcher --- .../Services/BackgroundTickService.cs | 9 ++--- .../ViewModels/MainWindowViewModel.cs | 4 +- .../DependencyInjectionQueueProcessor.cs | 8 ++++ src/ReflectionEventing/EventBus.cs | 39 ++++++++++++++----- src/ReflectionEventing/Queues/EventsQueue.cs | 16 ++++++++ src/ReflectionEventing/Queues/IEventsQueue.cs | 7 +++- 6 files changed, 66 insertions(+), 17 deletions(-) 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 6f29d72..7cb0c01 100644 --- a/src/ReflectionEventing.Demo.Wpf/ViewModels/MainWindowViewModel.cs +++ b/src/ReflectionEventing.Demo.Wpf/ViewModels/MainWindowViewModel.cs @@ -43,9 +43,9 @@ public async ValueTask ConsumeAsync(OtherEvent payload, CancellationToken cancel } /// - public async ValueTask ConsumeAsync(AsyncQueuedEvent payload, CancellationToken cancellationToken) + public ValueTask ConsumeAsync(AsyncQueuedEvent payload, CancellationToken cancellationToken) { - await DispatchAsync( + return DispatchAsync( () => { QueueCount++; diff --git a/src/ReflectionEventing.DependencyInjection/Services/DependencyInjectionQueueProcessor.cs b/src/ReflectionEventing.DependencyInjection/Services/DependencyInjectionQueueProcessor.cs index 547a382..f949f9a 100644 --- a/src/ReflectionEventing.DependencyInjection/Services/DependencyInjectionQueueProcessor.cs +++ b/src/ReflectionEventing.DependencyInjection/Services/DependencyInjectionQueueProcessor.cs @@ -240,4 +240,12 @@ CancellationToken cancellationToken ); } } + + /// + public override void Dispose() + { + semaphore.Dispose(); + + base.Dispose(); + } } diff --git a/src/ReflectionEventing/EventBus.cs b/src/ReflectionEventing/EventBus.cs index 6c3049f..a6af737 100644 --- a/src/ReflectionEventing/EventBus.cs +++ b/src/ReflectionEventing/EventBus.cs @@ -49,14 +49,26 @@ public virtual ValueTask SendAsync( Type eventType = typeof(TEvent); IEnumerable consumerTypes = consumerTypesProvider.GetConsumerTypes(eventType); - // Collect all consumers first - List consumers = []; + // 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 not null) + if (consumer is null) + { + continue; + } + + if (singleConsumer is null) { + singleConsumer = consumer; + } + else + { + consumers ??= new List { singleConsumer }; consumers.Add(consumer); } } @@ -65,14 +77,14 @@ public virtual ValueTask SendAsync( SentCounter.Add(1, new KeyValuePair("message_type", eventType.Name)); // Execute based on consumer count - optimized paths - if (consumers.Count == 0) + if (singleConsumer is null) { return default; } - if (consumers.Count == 1) + if (consumers is null) { - return ((IConsumer)consumers[0]).ConsumeAsync(eventItem, cancellationToken); + return ((IConsumer)singleConsumer).ConsumeAsync(eventItem, cancellationToken); } // Multiple consumers - execute based on configured mode @@ -156,8 +168,17 @@ CancellationToken cancellationToken } // Only allocate Task objects for truly async operations - return asyncTasks == null - ? default - : new ValueTask(Task.WhenAll(asyncTasks.Select(t => t.AsTask()))); + 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/Queues/EventsQueue.cs b/src/ReflectionEventing/Queues/EventsQueue.cs index 1a7822c..caf464e 100644 --- a/src/ReflectionEventing/Queues/EventsQueue.cs +++ b/src/ReflectionEventing/Queues/EventsQueue.cs @@ -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 ae44d47..7835a1d 100644 --- a/src/ReflectionEventing/Queues/IEventsQueue.cs +++ b/src/ReflectionEventing/Queues/IEventsQueue.cs @@ -8,7 +8,7 @@ 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. @@ -32,6 +32,11 @@ ValueTask EnqueueAsync(TEvent @event, CancellationToken cancellationToke /// 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. /// From 0a70d8a6e64a5e0bdf24606d285d16fcc2d7c508 Mon Sep 17 00:00:00 2001 From: pomian <13592821+pomianowski@users.noreply.github.com> Date: Mon, 9 Feb 2026 17:13:55 +0100 Subject: [PATCH 6/6] update valuetask --- .../ViewModels/MainWindowViewModel.cs | 8 ++++---- src/ReflectionEventing/EventBus.cs | 7 ++++++- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/ReflectionEventing.Demo.Wpf/ViewModels/MainWindowViewModel.cs b/src/ReflectionEventing.Demo.Wpf/ViewModels/MainWindowViewModel.cs index 7cb0c01..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 ValueTask ConsumeAsync(ITickedEvent payload, CancellationToken cancellationToken) + public ValueTask ConsumeAsync(ITickedEvent payload, CancellationToken cancellationToken) { int tickValue = payload.Value; - await DispatchAsync( + return DispatchAsync( () => { CurrentTick = tickValue; @@ -35,11 +35,11 @@ await DispatchAsync( } /// - public async ValueTask ConsumeAsync(OtherEvent payload, CancellationToken cancellationToken) + public ValueTask ConsumeAsync(OtherEvent payload, CancellationToken cancellationToken) { logger.LogInformation("Received {Event} event.", nameof(OtherEvent)); - await ValueTask.CompletedTask; + return default; } /// diff --git a/src/ReflectionEventing/EventBus.cs b/src/ReflectionEventing/EventBus.cs index a6af737..e238b98 100644 --- a/src/ReflectionEventing/EventBus.cs +++ b/src/ReflectionEventing/EventBus.cs @@ -160,7 +160,12 @@ CancellationToken cancellationToken cancellationToken ); - if (!task.IsCompletedSuccessfully) + if (task.IsCompletedSuccessfully) + { + // Ensure pooled IValueTaskSource is properly returned + task.GetAwaiter().GetResult(); + } + else { asyncTasks ??= new List(consumers.Count); asyncTasks.Add(task);