diff --git a/.gitignore b/.gitignore index 09c67d33f..89137d61d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -## Ignore Visual Studio temporary files, build results, and +## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. ## ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore @@ -358,4 +358,5 @@ MigrationBackup/ .vscode **/Files/ +.worktrees/ src/Server.UI/appsettings.development.json diff --git a/README.md b/README.md index d87660dbf..b44a201bb 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ Experience the application in action: | Layer | Technologies | |-------|-------------| | **Frontend** | Blazor Server, MudBlazor, SignalR | -| **Backend** | .NET 10, ASP.NET Core, MediatR, FluentValidation | +| **Backend** | .NET 10, ASP.NET Core, Mediator, FluentValidation | | **Database** | Entity Framework Core, MSSQL/PostgreSQL/SQLite | | **Authentication** | ASP.NET Core Identity, OAuth 2.0, JWT | | **Caching** | FusionCache, Redis | @@ -193,7 +193,7 @@ OpenSpec enables spec-driven, reviewable changes with clear proposals, deltas, a - Use the patterns in `openspec/project.md`: - For data access in handlers use `IApplicationDbContextFactory` and per-operation context lifetime: - `await using var db = await _dbContextFactory.CreateAsync(cancellationToken);` - - Follow MediatR pipeline behaviors, caching tags, and specification patterns. + - Follow mediator pipeline behaviors, caching tags, and specification patterns. - Mirror the Contacts module for a new entity's DTOs, commands, queries, specs, security, and UI pages/components. 5) Archive after deployment diff --git a/docs/superpowers/plans/2026-03-29-migrate-mediatr-to-mediator.md b/docs/superpowers/plans/2026-03-29-migrate-mediatr-to-mediator.md new file mode 100644 index 000000000..8f124b169 --- /dev/null +++ b/docs/superpowers/plans/2026-03-29-migrate-mediatr-to-mediator.md @@ -0,0 +1,173 @@ +# Migrate MediatR To Mediator Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development. Execute one task at a time with spec review first, then code-quality review. + +**Goal:** Remove `MediatR` references from the repository, move shared contracts to `Mediator.Abstractions`, and then reconnect runtime registration to `Mediator.SourceGenerator` in the outer layer. + +**Architecture:** This plan no longer preserves a repo-owned `MediatR` facade. Shared projects should reference `Mediator.Abstractions` directly. Temporary compatibility files created during earlier migration attempts must be deleted. Runtime registration belongs in infrastructure or another outer executable-facing layer, not in the shared contract layer. + +**Tech Stack:** .NET 10, Blazor Server, NUnit, FluentAssertions, `Mediator.Abstractions`, `Mediator.SourceGenerator`, Microsoft DI + +--- + +## Task 1: Add Migration Safety-Net Tests + +Status: completed on branch `migrate-mediatr-to-mediator-main-impl`. + +Keep the existing safety-net tests. They intentionally prove that low-level `Mediator` contracts are not fully wired yet. + +## Task 2: Replace MediatR Contracts With Direct Mediator.Abstractions Usage + +**Intent:** Delete the temporary MediatR compatibility layer and move the shared contract surface to `Mediator.Abstractions` without yet completing the runtime registration rewrite. + +**Files:** +- Delete: `src/Domain/Common/MediatorCompatibility/MediatR/Contracts.cs` +- Delete: `src/Domain/Common/MediatorCompatibility/MediatR/Handlers.cs` +- Delete: `src/Domain/Common/MediatorCompatibility/MediatR/PipelineContracts.cs` +- Delete: `src/Domain/Common/MediatorCompatibility/MediatR/PreProcessorContracts.cs` +- Delete: `src/Domain/Common/MediatorCompatibility/MediatR/ExceptionContracts.cs` +- Delete: `src/Domain/Common/MediatorCompatibility/MediatR/NotificationPublishing.cs` +- Delete: `src/Domain/Common/MediatorCompatibility/MediatR/ServiceCollectionContracts.cs` +- Modify: `src/Domain/Domain.csproj` +- Modify: `src/Application/_Imports.cs` +- Modify: `src/Domain/Common/DomainEvent.cs` +- Modify: `tests/Application.UnitTests/Common/MediatorCompatibility/RequestExceptionHandlerStateTests.cs` + +- [ ] **Step 1: Write the failing direct-abstractions smoke test** + +Rewrite `tests/Application.UnitTests/Common/MediatorCompatibility/RequestExceptionHandlerStateTests.cs` into a direct `Mediator.Abstractions` smoke test. It should prove the project compiles against `Mediator.INotification`, `Mediator.IRequest`, and `Mediator.IPipelineBehavior` rather than the deleted MediatR compatibility state type. + +Suggested shape: + +```csharp +using Mediator; +using NUnit.Framework; + +namespace CleanArchitecture.Blazor.Application.UnitTests.Common.MediatorCompatibility; + +public class MediatorAbstractionsSmokeTests +{ + [Test] + public void Contracts_ShouldCompileAgainstMediatorAbstractions() + { + _ = new SmokeNotification(); + _ = new SmokeRequest(); + _ = typeof(IPipelineBehavior); + } + + private sealed record SmokeNotification : INotification; + private sealed record SmokeRequest : IRequest; +} +``` + +- [ ] **Step 2: Run the smoke test to confirm it fails** + +Run: `dotnet test tests/Application.UnitTests/Application.UnitTests.csproj --filter "FullyQualifiedName~MediatorAbstractionsSmokeTests" -v minimal` + +Expected: FAIL because `Mediator.Abstractions` is not referenced yet. + +- [ ] **Step 3: Add direct Mediator.Abstractions package support** + +Update `src/Domain/Domain.csproj` to reference `Mediator.Abstractions`. + +Keep the dependency in the lowest shared layer that exposes `DomainEvent` and shared mediator contracts. + +- [ ] **Step 4: Remove the temporary MediatR compatibility files** + +Delete all files under `src/Domain/Common/MediatorCompatibility/MediatR/`. + +Do not replace them with new repo-owned MediatR shims. + +- [ ] **Step 5: Update shared imports and domain event contracts** + +Apply the smallest source edits needed so the shared code targets `Mediator` directly: + +- `src/Application/_Imports.cs`: replace `global using MediatR;` and `global using MediatR.Pipeline;` with direct `Mediator` imports. +- `src/Domain/Common/DomainEvent.cs`: replace `global::MediatR` usage with `global::Mediator`. + +- [ ] **Step 6: Add the temporary AddMediatR compile shim** + +Because the runtime registration rewrite has not happened yet, keep `src/Application/DependencyInjection.cs` compiling by adding the smallest temporary `AddMediatR(...)` shim needed for this stage. + +Constraints: + +- The shim is transitional only. +- It must not recreate the deleted MediatR contract surface. +- It should exist only to let Task 2 land while Task 3 still owns the real DI rewrite. + +- [ ] **Step 7: Re-run the smoke test** + +Run: `dotnet test tests/Application.UnitTests/Application.UnitTests.csproj --filter "FullyQualifiedName~MediatorAbstractionsSmokeTests" -v minimal` + +Expected: PASS, or at worst fail for a known unrelated baseline issue that already existed before this migration work. + +- [ ] **Step 8: Run the scoped mediator safety-net test** + +Run: `dotnet test tests/Application.UnitTests/Application.UnitTests.csproj --filter "FullyQualifiedName~ScopedMediatorTests" -v minimal` + +Expected: The test should move closer to green by compiling against real `Mediator` abstractions. If it still fails, the remaining failure should be due to runtime wiring, not missing MediatR contracts. + +- [ ] **Step 9: Commit** + +```bash +git add src/Domain/Domain.csproj src/Application/_Imports.cs src/Domain/Common/DomainEvent.cs tests/Application.UnitTests/Common/MediatorCompatibility/RequestExceptionHandlerStateTests.cs src/Application/DependencyInjection.cs +git rm -r src/Domain/Common/MediatorCompatibility/MediatR +git commit -m "refactor: move mediator contracts to abstractions" +``` + +## Task 3: Replace DI Registration With Mediator.SourceGenerator Runtime Wiring + +**Intent:** Remove the temporary `AddMediatR(...)` bridge, register the real `Mediator` runtime in the outer layer, and reconnect handlers, notifications, behaviors, and scoped mediator execution. + +**Files:** +- Modify: `src/Application/DependencyInjection.cs` +- Modify: `src/Infrastructure/DependencyInjection.cs` +- Modify: `src/Infrastructure/Infrastructure.csproj` +- Modify: `src/Infrastructure/Services/MediatorWrapper/ScopedMediator.cs` +- Modify: `tests/Application.IntegrationTests/Testing.cs` +- Modify: `tests/Application.IntegrationTests/Common/MediatorCompatibility/MediatorCompatibilityTests.cs` +- Modify: `tests/Application.UnitTests/Common/MediatorCompatibility/ParallelNoWaitPublisherTests.cs` +- Modify: `tests/Application.UnitTests/Infrastructure/MediatorCompatibility/ScopedMediatorTests.cs` + +Notes: + +- Add `Mediator.SourceGenerator` in the outer layer only. +- Delete the temporary `AddMediatR(...)` shim as part of this task. +- Reconnect `ParallelNoWaitPublisher`, scoped mediator resolution, and at least one existing query path through DI. +- Decide explicitly how current exception-handler behavior maps to `Mediator`. Do not assume a drop-in equivalent exists. + +## Task 4: Update UI/Test Imports And Resolve Remaining Runtime Gaps + +**Files:** +- Modify: `src/Server.UI/_Imports.razor` +- Modify: `src/Server.UI/Services/DialogServiceHelper.cs` +- Modify: `tests/Application.IntegrationTests/Testing.cs` +- Modify: any source or test files still importing `MediatR` + +Notes: + +- Remove leftover `MediatR` namespace imports after runtime wiring is stable. +- Keep `IScopedMediator` intact as a repository abstraction if it still provides value. + +## Task 5: Remove Leftover MediatR References And Update Documentation + +**Files:** +- Modify: `README.md` +- Modify: `src/Server.UI/Pages/Public/Index.razor` +- Modify: `src/Server.UI/Pages/AI/Chatbot.razor` +- Modify: any `.csproj` files still referencing `MediatR` + +Verification: + +- `rg -n "MediatR" src tests README.md` +- Update user-facing copy so the app no longer claims it uses MediatR. + +## Final Verification + +Before claiming the migration complete, run: + +- `dotnet test tests/Application.UnitTests/Application.UnitTests.csproj -v minimal` +- `dotnet test tests/Application.IntegrationTests/Application.IntegrationTests.csproj --filter "FullyQualifiedName~MediatorCompatibilityTests" -v minimal` +- `rg -n "MediatR" src tests README.md` + +If any command fails, report the exact remaining blocker instead of declaring success. diff --git a/docs/superpowers/specs/2026-03-29-migrate-mediatr-to-mediator-design.md b/docs/superpowers/specs/2026-03-29-migrate-mediatr-to-mediator-design.md new file mode 100644 index 000000000..b316c42cb --- /dev/null +++ b/docs/superpowers/specs/2026-03-29-migrate-mediatr-to-mediator-design.md @@ -0,0 +1,127 @@ +# Migrate MediatR To Mediator Design + +## Status + +Approved for planning and implementation. + +## Summary + +Migrate the repository from `MediatR` to `martinothamar/Mediator` by deleting `MediatR` package and namespace dependencies rather than preserving a repository-owned MediatR-compatible facade. + +This migration is still conservative about behavior, but it is no longer conservative about API shape: + +- Application, domain, infrastructure, UI, and tests should move to `Mediator` / `Mediator.Abstractions` namespaces directly. +- `Mediator.Abstractions` should be referenced by projects that only need contracts. +- `Mediator.SourceGenerator` should be referenced only by the outer runtime layer that owns DI wiring and generated handler dispatch. +- Existing behaviors such as request handling, notification publishing, domain event dispatch, and scoped mediator usage should remain intact or be reconnected with equivalent semantics. + +## Goals + +- Remove direct `MediatR` references from source, tests, and project files. +- Move shared contracts to `Mediator.Abstractions`. +- Preserve current application behavior for request sending, notification publishing, and scoped mediator usage. +- Prepare the repository for a later runtime registration step that adds `Mediator.SourceGenerator` in the correct outer layer. + +## Non-Goals + +- Do not keep or expand the temporary repo-owned `MediatR` compatibility layer. +- Do not redesign the command/query architecture. +- Do not change feature behavior unless required by the mediator migration. +- Do not pull `Mediator.SourceGenerator` into low-level shared projects that only need abstractions. + +## Current State + +The repository currently contains three conflicting mediator shapes: + +- Legacy source files still import `MediatR`. +- Temporary compatibility contracts exist under `src/Domain/Common/MediatorCompatibility/MediatR/*`. +- Migration safety-net tests already target low-level `Mediator` runtime concepts in a few places. + +That mixed state is unstable. The next implementation step must simplify the contract layer instead of adding more compatibility code. + +## Approved Architecture + +Use direct `Mediator` abstractions throughout the codebase. + +### Contract Layer + +- `Domain` references `Mediator.Abstractions`. +- `Application` imports `Mediator` namespaces directly for requests, notifications, handlers, behaviors, preprocessors, and exception handlers. +- `DomainEvent` implements `Mediator.INotification` directly. +- UI and integration tests may resolve `Mediator.IMediator` directly once runtime wiring is in place. + +### Runtime Layer + +- `Infrastructure` will eventually own `Mediator.SourceGenerator` and registration. +- `Application/DependencyInjection.cs` must stop depending on `AddMediatR(...)`. +- `IScopedMediator` remains as a repository abstraction, but its implementation should resolve the real `Mediator.IMediator` service from a created scope. + +### Temporary Transition Rule + +Until the runtime registration task lands: + +- Contract-only work may add `Mediator.Abstractions`. +- Temporary `MediatR` compatibility files should be removed instead of expanded. +- A minimal compile shim for `AddMediatR(...)` is allowed only as a short-lived bridge while the old DI registration still exists, but it must not become part of the final architecture. + +## Behavioral Requirements + +The migration must preserve or re-establish these behaviors: + +- Existing request handlers still execute for current commands and queries. +- Existing notification handlers still execute for current notifications and domain events. +- `ParallelNoWaitPublisher` remains available and keeps its fire-and-forget behavior if the runtime layer still needs that strategy. +- `DispatchDomainEventsInterceptor` continues to publish domain events through `IScopedMediator`. +- `ScopedMediator` continues to resolve a fresh scoped `IMediator`. + +## File Organization + +Expected migration touchpoints: + +- `src/Domain/Domain.csproj` +- `src/Application/_Imports.cs` +- `src/Application/DependencyInjection.cs` +- `src/Domain/Common/DomainEvent.cs` +- `src/Infrastructure/Services/MediatorWrapper/ScopedMediator.cs` +- `src/Server.UI/_Imports.razor` +- `src/Server.UI/Services/DialogServiceHelper.cs` +- `tests/Application.IntegrationTests/Testing.cs` +- `tests/Application.UnitTests/Common/MediatorCompatibility/*` +- `tests/Application.IntegrationTests/Common/MediatorCompatibility/*` + +Files under `src/Domain/Common/MediatorCompatibility/MediatR/*` are temporary and should be deleted rather than treated as a stable design boundary. + +## Migration Sequence + +1. Keep the safety-net tests from Task 1. +2. Remove the temporary MediatR compatibility contracts. +3. Add `Mediator.Abstractions` where compile-time contracts are required. +4. Update source imports and contract usage to direct `Mediator` namespaces. +5. Keep DI compiling with the smallest temporary bridge necessary until runtime registration is replaced. +6. In a later task, add the real `Mediator.SourceGenerator` runtime registration in the outer layer and reconnect behaviors. +7. Remove any temporary DI shims before completion. + +## Verification Scope + +At minimum, verify: + +- Contract-layer projects compile against `Mediator.Abstractions`. +- Safety-net tests that intentionally exercise `Mediator` low-level contracts compile in the expected direction. +- `IScopedMediator` still resolves `IMediator` from a created scope after runtime wiring is updated. +- There are no leftover `MediatR` package references or stable source dependencies outside temporary transitional code that is explicitly scheduled for deletion. + +## Risks + +- `Mediator` does not provide a drop-in equivalent for every MediatR interface, so request exception handling and DI registration need explicit redesign instead of namespace swapping. +- The temporary compatibility files can hide architectural drift if they are left in place. +- `AddMediatR(...)` currently anchors application startup, so contract-layer migration and runtime registration need to be staged carefully. + +## Success Criteria + +The migration is complete when: + +- The repository no longer depends on `MediatR`. +- Shared code compiles against `Mediator.Abstractions` instead of `MediatR`. +- Runtime registration is owned by `Mediator.SourceGenerator` in the correct outer layer. +- Scoped mediator, notifications, and domain events still work. +- Documentation accurately describes the repository as using `Mediator`, not `MediatR`. diff --git a/src/Application/Application.csproj b/src/Application/Application.csproj index 6036bf5c8..843b83c1a 100644 --- a/src/Application/Application.csproj +++ b/src/Application/Application.csproj @@ -8,6 +8,7 @@ default + @@ -23,10 +24,10 @@ - - - - + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Application/Common/ExceptionHandlers/DbExceptionHandler.cs b/src/Application/Common/ExceptionHandlers/DbExceptionHandler.cs index 9036105e2..8245264be 100644 --- a/src/Application/Common/ExceptionHandlers/DbExceptionHandler.cs +++ b/src/Application/Common/ExceptionHandlers/DbExceptionHandler.cs @@ -2,10 +2,9 @@ namespace CleanArchitecture.Blazor.Application.Common.ExceptionHandlers; -public class DbExceptionHandler : IRequestExceptionHandler +public class DbExceptionHandler : MessageExceptionHandler where TRequest : IRequest> where TResponse : Result - where TException : DbUpdateException { private readonly ILogger _logger; private readonly ILoggerFactory _loggerFactory; @@ -14,14 +13,18 @@ public DbExceptionHandler(ILoggerFactory loggerFactory) { _loggerFactory = loggerFactory; - _logger = _loggerFactory.CreateLogger(nameof(DbExceptionHandler)); + _logger = _loggerFactory.CreateLogger(nameof(DbExceptionHandler)); } - public Task Handle(TRequest request, TException exception, RequestExceptionHandlerState state, + protected override ValueTask> Handle(TRequest request, Exception exception, CancellationToken cancellationToken) { - state.SetHandled((TResponse)Result.Failure(GetErrors(exception))); - return Task.CompletedTask; + if (exception is not DbUpdateException dbUpdateException) + { + return NotHandled; + } + + return Handled((TResponse)Result.Failure(GetErrors(dbUpdateException))); } private string[] GetErrors(DbUpdateException exception) @@ -82,4 +85,4 @@ private string[] GetReferenceConstraintExceptionErrors(ReferenceConstraintExcep }; } -} \ No newline at end of file +} diff --git a/src/Application/Common/ExceptionHandlers/GlobalExceptionHandler.cs b/src/Application/Common/ExceptionHandlers/GlobalExceptionHandler.cs index 1b78c80c0..b19a3d6d5 100644 --- a/src/Application/Common/ExceptionHandlers/GlobalExceptionHandler.cs +++ b/src/Application/Common/ExceptionHandlers/GlobalExceptionHandler.cs @@ -1,17 +1,16 @@ namespace CleanArchitecture.Blazor.Application.Common.ExceptionHandlers; -public class GlobalExceptionHandler : IRequestExceptionHandler +public class GlobalExceptionHandler : MessageExceptionHandler where TRequest : IRequest where TResponse : IResult - where TException : Exception { - private readonly ILogger> _logger; + private readonly ILogger> _logger; /// /// Initializes a new instance of the class. /// /// The logger. - public GlobalExceptionHandler(ILogger> logger) + public GlobalExceptionHandler(ILogger> logger) { _logger = logger; } @@ -24,7 +23,7 @@ public GlobalExceptionHandler(ILoggerThe request exception handler state. /// The cancellation token. /// A task representing the asynchronous operation. - public Task Handle(TRequest request, TException exception, RequestExceptionHandlerState state, + protected override ValueTask> Handle(TRequest request, Exception exception, CancellationToken cancellationToken) { TResponse failureResult; @@ -48,8 +47,7 @@ public Task Handle(TRequest request, TException exception, RequestExceptionHandl } // Set the handled response - state.SetHandled(failureResult!); _logger.LogError(exception, exception.Message); - return Task.CompletedTask; + return Handled(failureResult!); } } diff --git a/src/Application/Common/ExceptionHandlers/ServerExceptionHandler.cs b/src/Application/Common/ExceptionHandlers/ServerExceptionHandler.cs index ec32e50cc..142efc092 100644 --- a/src/Application/Common/ExceptionHandlers/ServerExceptionHandler.cs +++ b/src/Application/Common/ExceptionHandlers/ServerExceptionHandler.cs @@ -1,23 +1,26 @@ namespace CleanArchitecture.Blazor.Application.Common.ExceptionHandlers; public class - ServerExceptionHandler : IRequestExceptionHandler + ServerExceptionHandler : MessageExceptionHandler where TRequest : IRequest> where TResponse : Result - where TException : ServerException { - private readonly ILogger> _logger; + private readonly ILogger> _logger; - public ServerExceptionHandler(ILogger> logger) + public ServerExceptionHandler(ILogger> logger) { _logger = logger; } - public Task Handle(TRequest request, TException exception, RequestExceptionHandlerState state, + protected override ValueTask> Handle(TRequest request, Exception exception, CancellationToken cancellationToken) { - state.SetHandled((TResponse)Result.Failure(exception.Message)); - _logger.LogError(exception, exception.Message); - return Task.CompletedTask; + if (exception is not ServerException serverException) + { + return NotHandled; + } + + _logger.LogError(serverException, serverException.Message); + return Handled((TResponse)Result.Failure(serverException.Message)); } -} \ No newline at end of file +} diff --git a/src/Application/Common/ExceptionHandlers/ValidationExceptionHandler.cs b/src/Application/Common/ExceptionHandlers/ValidationExceptionHandler.cs index 15cec28f7..06b726d55 100644 --- a/src/Application/Common/ExceptionHandlers/ValidationExceptionHandler.cs +++ b/src/Application/Common/ExceptionHandlers/ValidationExceptionHandler.cs @@ -1,24 +1,26 @@ namespace CleanArchitecture.Blazor.Application.Common.ExceptionHandlers; public class - ValidationExceptionHandler : IRequestExceptionHandler + ValidationExceptionHandler : MessageExceptionHandler where TRequest : IRequest> where TResponse : Result - where TException : ValidationException { - private readonly ILogger> _logger; + private readonly ILogger> _logger; - public ValidationExceptionHandler(ILogger> logger) + public ValidationExceptionHandler(ILogger> logger) { _logger = logger; } - public Task Handle(TRequest request, TException exception, RequestExceptionHandlerState state, + protected override ValueTask> Handle(TRequest request, Exception exception, CancellationToken cancellationToken) { - state.SetHandled( - (TResponse)Result.Failure(exception.Errors.Select(x => x.ErrorMessage).Distinct().ToArray())); - return Task.CompletedTask; + if (exception is not ValidationException validationException) + { + return NotHandled; + } + + return Handled( + (TResponse)Result.Failure(validationException.Errors.Select(x => x.ErrorMessage).Distinct().ToArray())); } -} \ No newline at end of file +} diff --git a/src/Application/Common/Interfaces/IApplicationHubWrapper.cs b/src/Application/Common/Interfaces/IApplicationHubWrapper.cs index bbfff5d7a..c9d283c3e 100644 --- a/src/Application/Common/Interfaces/IApplicationHubWrapper.cs +++ b/src/Application/Common/Interfaces/IApplicationHubWrapper.cs @@ -1,8 +1,8 @@ namespace CleanArchitecture.Blazor.Application.Common.Interfaces; -// TODO: can be improved or removed using MediatR? +// TODO: can be improved or removed as the mediator pipeline evolves? public interface IApplicationHubWrapper { Task JobStarted(int id,string message); Task JobCompleted(int id,string message); -} \ No newline at end of file +} diff --git a/src/Application/Common/PublishStrategies/ParallelNoWaitPublisher.cs b/src/Application/Common/PublishStrategies/ParallelNoWaitPublisher.cs index ed8286af7..91ea444c6 100644 --- a/src/Application/Common/PublishStrategies/ParallelNoWaitPublisher.cs +++ b/src/Application/Common/PublishStrategies/ParallelNoWaitPublisher.cs @@ -2,12 +2,13 @@ public class ParallelNoWaitPublisher : INotificationPublisher { - public Task Publish(IEnumerable handlerExecutors, INotification notification, + public ValueTask Publish(NotificationHandlers handlers, TNotification notification, CancellationToken cancellationToken) + where TNotification : INotification { - foreach (var handler in handlerExecutors) - Task.Run(() => handler.HandlerCallback(notification, cancellationToken)); + foreach (var handler in handlers) + _ = Task.Run(async () => await handler.Handle(notification, cancellationToken)); - return Task.CompletedTask; + return ValueTask.CompletedTask; } -} \ No newline at end of file +} diff --git a/src/Application/DependencyInjection.cs b/src/Application/DependencyInjection.cs index 08f43232d..dfa85fd4e 100644 --- a/src/Application/DependencyInjection.cs +++ b/src/Application/DependencyInjection.cs @@ -1,9 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using CleanArchitecture.Blazor.Application.Common.PublishStrategies; -using CleanArchitecture.Blazor.Application.Pipeline; -using CleanArchitecture.Blazor.Application.Pipeline.PreProcessors; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -14,17 +11,6 @@ public static class DependencyInjection public static IServiceCollection AddApplication(this IServiceCollection services) { services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly()); - services.AddTransient(typeof(IRequestExceptionHandler<,,>), typeof(DbExceptionHandler<,,>)); - services.AddMediatR(config => - { - config.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly()); - config.NotificationPublisher = new ParallelNoWaitPublisher(); - config.AddRequestPreProcessor(typeof(IRequestPreProcessor<>), typeof(ValidationPreProcessor<>)); - config.AddOpenBehavior(typeof(PerformanceBehaviour<,>)); - config.AddOpenBehavior(typeof(FusionCacheBehaviour<,>)); - config.AddOpenBehavior(typeof(CacheInvalidationBehaviour<,>)); - - }); services.AddScoped(); return services; } @@ -32,4 +18,4 @@ public static void InitializeCacheFactory(this IHost host) { FusionCacheFactory.Configure(host.Services); } -} \ No newline at end of file +} diff --git a/src/Application/Features/AuditTrails/Queries/Export/ExportAuditTrailsQuery.cs b/src/Application/Features/AuditTrails/Queries/Export/ExportAuditTrailsQuery.cs index fbe9496cf..a0c0e9885 100644 --- a/src/Application/Features/AuditTrails/Queries/Export/ExportAuditTrailsQuery.cs +++ b/src/Application/Features/AuditTrails/Queries/Export/ExportAuditTrailsQuery.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using CleanArchitecture.Blazor.Application.Features.AuditTrails.DTOs; @@ -31,7 +31,7 @@ IStringLocalizer localizer _localizer = localizer; } - public async Task Handle(ExportAuditTrailsQuery request, CancellationToken cancellationToken) + public async ValueTask Handle(ExportAuditTrailsQuery request, CancellationToken cancellationToken) { var data = await _context.AuditTrails .Where(x => x.TableName!.Contains(request.Keyword)) diff --git a/src/Application/Features/AuditTrails/Queries/PaginationQuery/AuditTrailsWithPaginationQuery.cs b/src/Application/Features/AuditTrails/Queries/PaginationQuery/AuditTrailsWithPaginationQuery.cs index c3abcd8d4..67a61bab5 100644 --- a/src/Application/Features/AuditTrails/Queries/PaginationQuery/AuditTrailsWithPaginationQuery.cs +++ b/src/Application/Features/AuditTrails/Queries/PaginationQuery/AuditTrailsWithPaginationQuery.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using CleanArchitecture.Blazor.Application.Features.AuditTrails.Caching; @@ -32,7 +32,7 @@ IApplicationDbContext context _context = context; } - public async Task> Handle(AuditTrailsWithPaginationQuery request, + public async ValueTask> Handle(AuditTrailsWithPaginationQuery request, CancellationToken cancellationToken) { var data = await _context.AuditTrails.OrderBy($"{request.OrderBy} {request.SortDirection}") diff --git a/src/Application/Features/Contacts/Commands/AddEdit/AddEditContactCommand.cs b/src/Application/Features/Contacts/Commands/AddEdit/AddEditContactCommand.cs index e6699365d..4c3a78497 100644 --- a/src/Application/Features/Contacts/Commands/AddEdit/AddEditContactCommand.cs +++ b/src/Application/Features/Contacts/Commands/AddEdit/AddEditContactCommand.cs @@ -1,4 +1,4 @@ -//------------------------------------------------------------------------------ +//------------------------------------------------------------------------------ // // This file is part of the CleanArchitecture.Blazor project. // Licensed to the .NET Foundation under one or more agreements. @@ -56,7 +56,7 @@ public AddEditContactCommandHandler( { _context = context; } - public async Task> Handle(AddEditContactCommand request, CancellationToken cancellationToken) + public async ValueTask> Handle(AddEditContactCommand request, CancellationToken cancellationToken) { if (request.Id > 0) { diff --git a/src/Application/Features/Contacts/Commands/Create/CreateContactCommand.cs b/src/Application/Features/Contacts/Commands/Create/CreateContactCommand.cs index 848a7828c..a07c58034 100644 --- a/src/Application/Features/Contacts/Commands/Create/CreateContactCommand.cs +++ b/src/Application/Features/Contacts/Commands/Create/CreateContactCommand.cs @@ -1,4 +1,4 @@ -//------------------------------------------------------------------------------ +//------------------------------------------------------------------------------ // // This file is part of the CleanArchitecture.Blazor project. // Licensed to the .NET Foundation under one or more agreements. @@ -57,7 +57,7 @@ public CreateContactCommandHandler( { _context = context; } - public async Task> Handle(CreateContactCommand request, CancellationToken cancellationToken) + public async ValueTask> Handle(CreateContactCommand request, CancellationToken cancellationToken) { var item = ContactMapper.FromCreateCommand(request); // raise a create domain event diff --git a/src/Application/Features/Contacts/Commands/Delete/DeleteContactCommand.cs b/src/Application/Features/Contacts/Commands/Delete/DeleteContactCommand.cs index 4fda2fd3a..733e4bcd1 100644 --- a/src/Application/Features/Contacts/Commands/Delete/DeleteContactCommand.cs +++ b/src/Application/Features/Contacts/Commands/Delete/DeleteContactCommand.cs @@ -1,4 +1,4 @@ -//------------------------------------------------------------------------------ +//------------------------------------------------------------------------------ // // This file is part of the CleanArchitecture.Blazor project. // Licensed to the .NET Foundation under one or more agreements. @@ -52,7 +52,7 @@ public DeleteContactCommandHandler( { _context = context; } - public async Task> Handle(DeleteContactCommand request, CancellationToken cancellationToken) + public async ValueTask> Handle(DeleteContactCommand request, CancellationToken cancellationToken) { var items = await _context.Contacts.Where(x=>request.Id.Contains(x.Id)).ToListAsync(cancellationToken); foreach (var item in items) diff --git a/src/Application/Features/Contacts/Commands/Import/ImportContactsCommand.cs b/src/Application/Features/Contacts/Commands/Import/ImportContactsCommand.cs index 468e53327..55caa081c 100644 --- a/src/Application/Features/Contacts/Commands/Import/ImportContactsCommand.cs +++ b/src/Application/Features/Contacts/Commands/Import/ImportContactsCommand.cs @@ -1,4 +1,4 @@ -//------------------------------------------------------------------------------ +//------------------------------------------------------------------------------ // // This file is part of the CleanArchitecture.Blazor project. // Licensed to the .NET Foundation under one or more agreements. @@ -68,7 +68,7 @@ public ImportContactsCommandHandler( _excelService = excelService; } #nullable disable warnings - public async Task> Handle(ImportContactsCommand request, CancellationToken cancellationToken) + public async ValueTask> Handle(ImportContactsCommand request, CancellationToken cancellationToken) { var result = await _excelService.ImportAsync(request.Data, mappers: new Dictionary> @@ -101,7 +101,7 @@ public async Task> Handle(ImportContactsCommand request, Cancellatio return await Result.FailureAsync(result.Errors); } } - public async Task> Handle(CreateContactsTemplateCommand request, CancellationToken cancellationToken) + public async ValueTask> Handle(CreateContactsTemplateCommand request, CancellationToken cancellationToken) { // TODO: Implement ImportContactsCommandHandler method var fields = new string[] { diff --git a/src/Application/Features/Contacts/Commands/Update/UpdateContactCommand.cs b/src/Application/Features/Contacts/Commands/Update/UpdateContactCommand.cs index 17492f55c..9f901a4f1 100644 --- a/src/Application/Features/Contacts/Commands/Update/UpdateContactCommand.cs +++ b/src/Application/Features/Contacts/Commands/Update/UpdateContactCommand.cs @@ -1,4 +1,4 @@ -//------------------------------------------------------------------------------ +//------------------------------------------------------------------------------ // // This file is part of the CleanArchitecture.Blazor project. // Licensed to the .NET Foundation under one or more agreements. @@ -58,7 +58,7 @@ public UpdateContactCommandHandler( { _context = context; } - public async Task> Handle(UpdateContactCommand request, CancellationToken cancellationToken) + public async ValueTask> Handle(UpdateContactCommand request, CancellationToken cancellationToken) { var item = await _context.Contacts.FindAsync(request.Id, cancellationToken); diff --git a/src/Application/Features/Contacts/EventHandlers/ContactCreatedEventHandler.cs b/src/Application/Features/Contacts/EventHandlers/ContactCreatedEventHandler.cs index 2646e9d28..e58d4519a 100644 --- a/src/Application/Features/Contacts/EventHandlers/ContactCreatedEventHandler.cs +++ b/src/Application/Features/Contacts/EventHandlers/ContactCreatedEventHandler.cs @@ -1,4 +1,4 @@ -//------------------------------------------------------------------------------ +//------------------------------------------------------------------------------ // // This file is part of the CleanArchitecture.Blazor project. // Licensed to the .NET Foundation under one or more agreements. @@ -27,9 +27,9 @@ ILogger logger { _logger = logger; } - public Task Handle(ContactCreatedEvent notification, CancellationToken cancellationToken) + public ValueTask Handle(ContactCreatedEvent notification, CancellationToken cancellationToken) { _logger.LogInformation("Handled domain event '{EventType}' with notification: {@Notification} ", notification.GetType().Name, notification); - return Task.CompletedTask; + return ValueTask.CompletedTask; } } diff --git a/src/Application/Features/Contacts/EventHandlers/ContactDeletedEventHandler.cs b/src/Application/Features/Contacts/EventHandlers/ContactDeletedEventHandler.cs index 3293d114b..85a06d40a 100644 --- a/src/Application/Features/Contacts/EventHandlers/ContactDeletedEventHandler.cs +++ b/src/Application/Features/Contacts/EventHandlers/ContactDeletedEventHandler.cs @@ -1,4 +1,4 @@ -//------------------------------------------------------------------------------ +//------------------------------------------------------------------------------ // // This file is part of the CleanArchitecture.Blazor project. // Licensed to the .NET Foundation under one or more agreements. @@ -27,9 +27,9 @@ ILogger logger { _logger = logger; } - public Task Handle(ContactDeletedEvent notification, CancellationToken cancellationToken) + public ValueTask Handle(ContactDeletedEvent notification, CancellationToken cancellationToken) { _logger.LogInformation("Handled domain event '{EventType}' with notification: {@Notification} ", notification.GetType().Name, notification); - return Task.CompletedTask; + return ValueTask.CompletedTask; } } diff --git a/src/Application/Features/Contacts/EventHandlers/ContactUpdatedEventHandler.cs b/src/Application/Features/Contacts/EventHandlers/ContactUpdatedEventHandler.cs index 3fd1a930b..4e1e10146 100644 --- a/src/Application/Features/Contacts/EventHandlers/ContactUpdatedEventHandler.cs +++ b/src/Application/Features/Contacts/EventHandlers/ContactUpdatedEventHandler.cs @@ -1,4 +1,4 @@ -//------------------------------------------------------------------------------ +//------------------------------------------------------------------------------ // // This file is part of the CleanArchitecture.Blazor project. // Licensed to the .NET Foundation under one or more agreements. @@ -27,9 +27,9 @@ ILogger logger { _logger = logger; } - public Task Handle(ContactUpdatedEvent notification, CancellationToken cancellationToken) + public ValueTask Handle(ContactUpdatedEvent notification, CancellationToken cancellationToken) { _logger.LogInformation("Handled domain event '{EventType}' with notification: {@Notification} ", notification.GetType().Name, notification); - return Task.CompletedTask; + return ValueTask.CompletedTask; } } diff --git a/src/Application/Features/Contacts/Queries/Export/ExportContactsQuery.cs b/src/Application/Features/Contacts/Queries/Export/ExportContactsQuery.cs index 399e6be87..b8c0f9f26 100644 --- a/src/Application/Features/Contacts/Queries/Export/ExportContactsQuery.cs +++ b/src/Application/Features/Contacts/Queries/Export/ExportContactsQuery.cs @@ -1,4 +1,4 @@ -//------------------------------------------------------------------------------ +//------------------------------------------------------------------------------ // // This file is part of the CleanArchitecture.Blazor project. // Licensed to the .NET Foundation under the MIT license. @@ -50,7 +50,7 @@ IStringLocalizer localizer _localizer = localizer; } #nullable disable warnings - public async Task> Handle(ExportContactsQuery request, CancellationToken cancellationToken) + public async ValueTask> Handle(ExportContactsQuery request, CancellationToken cancellationToken) { var data = await _context.Contacts.ApplySpecification(request.Specification) .OrderBy($"{request.OrderBy} {request.SortDirection}") diff --git a/src/Application/Features/Contacts/Queries/GetAll/GetAllContactsQuery.cs b/src/Application/Features/Contacts/Queries/GetAll/GetAllContactsQuery.cs index a2b393188..3faebbd3b 100644 --- a/src/Application/Features/Contacts/Queries/GetAll/GetAllContactsQuery.cs +++ b/src/Application/Features/Contacts/Queries/GetAll/GetAllContactsQuery.cs @@ -1,4 +1,4 @@ -//------------------------------------------------------------------------------ +//------------------------------------------------------------------------------ // // This file is part of the CleanArchitecture.Blazor project. // Licensed to the .NET Foundation under the MIT license. @@ -37,7 +37,7 @@ public GetAllContactsQueryHandler( _context = context; } - public async Task> Handle(GetAllContactsQuery request, CancellationToken cancellationToken) + public async ValueTask> Handle(GetAllContactsQuery request, CancellationToken cancellationToken) { var data = await _context.Contacts.ProjectTo() .AsNoTracking() diff --git a/src/Application/Features/Contacts/Queries/GetById/GetContactByIdQuery.cs b/src/Application/Features/Contacts/Queries/GetById/GetContactByIdQuery.cs index f482a939e..023a8f310 100644 --- a/src/Application/Features/Contacts/Queries/GetById/GetContactByIdQuery.cs +++ b/src/Application/Features/Contacts/Queries/GetById/GetContactByIdQuery.cs @@ -1,4 +1,4 @@ -//------------------------------------------------------------------------------ +//------------------------------------------------------------------------------ // // This file is part of the CleanArchitecture.Blazor project. // Licensed to the .NET Foundation under the MIT license. @@ -38,7 +38,7 @@ public GetContactByIdQueryHandler( _context = context; } - public async Task> Handle(GetContactByIdQuery request, CancellationToken cancellationToken) + public async ValueTask> Handle(GetContactByIdQuery request, CancellationToken cancellationToken) { var data = await _context.Contacts.ApplySpecification(new ContactByIdSpecification(request.Id)) .ProjectTo() diff --git a/src/Application/Features/Contacts/Queries/Pagination/ContactsPaginationQuery.cs b/src/Application/Features/Contacts/Queries/Pagination/ContactsPaginationQuery.cs index a06611554..11a0230c8 100644 --- a/src/Application/Features/Contacts/Queries/Pagination/ContactsPaginationQuery.cs +++ b/src/Application/Features/Contacts/Queries/Pagination/ContactsPaginationQuery.cs @@ -1,4 +1,4 @@ -//------------------------------------------------------------------------------ +//------------------------------------------------------------------------------ // // This file is part of the CleanArchitecture.Blazor project. // Licensed to the .NET Foundation under the MIT license. @@ -42,7 +42,7 @@ public ContactsWithPaginationQueryHandler( _context = context; } - public async Task> Handle(ContactsWithPaginationQuery request, CancellationToken cancellationToken) + public async ValueTask> Handle(ContactsWithPaginationQuery request, CancellationToken cancellationToken) { var data = await _context.Contacts.OrderBy($"{request.OrderBy} {request.SortDirection}") .ProjectToPaginatedDataAsync(request.Specification, diff --git a/src/Application/Features/Documents/Commands/AddEdit/AddEditDocumentCommand.cs b/src/Application/Features/Documents/Commands/AddEdit/AddEditDocumentCommand.cs index 365584afb..4811d6003 100644 --- a/src/Application/Features/Documents/Commands/AddEdit/AddEditDocumentCommand.cs +++ b/src/Application/Features/Documents/Commands/AddEdit/AddEditDocumentCommand.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using CleanArchitecture.Blazor.Application.Features.Documents.Caching; @@ -37,7 +37,7 @@ IUploadService uploadService _uploadService = uploadService; } - public async Task> Handle(AddEditDocumentCommand request, CancellationToken cancellationToken) + public async ValueTask> Handle(AddEditDocumentCommand request, CancellationToken cancellationToken) { if (request.Id > 0) { diff --git a/src/Application/Features/Documents/Commands/Delete/DeleteDocumentCommand.cs b/src/Application/Features/Documents/Commands/Delete/DeleteDocumentCommand.cs index 775824066..0c9ad59f6 100644 --- a/src/Application/Features/Documents/Commands/Delete/DeleteDocumentCommand.cs +++ b/src/Application/Features/Documents/Commands/Delete/DeleteDocumentCommand.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using CleanArchitecture.Blazor.Application.Features.Documents.Caching; @@ -28,7 +28,7 @@ IApplicationDbContext context _context = context; } - public async Task> Handle(DeleteDocumentCommand request, CancellationToken cancellationToken) + public async ValueTask> Handle(DeleteDocumentCommand request, CancellationToken cancellationToken) { var items = await _context.Documents.Where(x => request.Id.Contains(x.Id)).ToListAsync(cancellationToken); foreach (var item in items) diff --git a/src/Application/Features/Documents/Commands/Upload/UploadDocumentCommand.cs b/src/Application/Features/Documents/Commands/Upload/UploadDocumentCommand.cs index 40b6624f6..8a2005bfd 100644 --- a/src/Application/Features/Documents/Commands/Upload/UploadDocumentCommand.cs +++ b/src/Application/Features/Documents/Commands/Upload/UploadDocumentCommand.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using CleanArchitecture.Blazor.Application.Features.Documents.Caching; @@ -29,7 +29,7 @@ IUploadService uploadService _uploadService = uploadService; } - public async Task> Handle(UploadDocumentCommand request, CancellationToken cancellationToken) + public async ValueTask> Handle(UploadDocumentCommand request, CancellationToken cancellationToken) { var list = new List(); foreach (var uploadRequest in request.UploadRequests) diff --git a/src/Application/Features/Documents/EventHandlers/DocumentCreatedEventHandler.cs b/src/Application/Features/Documents/EventHandlers/DocumentCreatedEventHandler.cs index 7f76b1043..d4f37f611 100644 --- a/src/Application/Features/Documents/EventHandlers/DocumentCreatedEventHandler.cs +++ b/src/Application/Features/Documents/EventHandlers/DocumentCreatedEventHandler.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. @@ -22,7 +22,7 @@ ILogger logger _logger = logger; } - public Task Handle(CreatedEvent notification, CancellationToken cancellationToken) + public ValueTask Handle(CreatedEvent notification, CancellationToken cancellationToken) { _logger.LogInformation( "Document upload successful. Beginning OCR recognition process for Document Id: {DocumentId}", @@ -34,6 +34,6 @@ public Task Handle(CreatedEvent notification, CancellationToken cancel BackgroundJob.Enqueue(() => ocrJob.Do(notification.Entity.Id)); } - return Task.CompletedTask; + return ValueTask.CompletedTask; } } \ No newline at end of file diff --git a/src/Application/Features/Documents/EventHandlers/DocumentDeletedEventHandler.cs b/src/Application/Features/Documents/EventHandlers/DocumentDeletedEventHandler.cs index 934cfb389..8e3c3860c 100644 --- a/src/Application/Features/Documents/EventHandlers/DocumentDeletedEventHandler.cs +++ b/src/Application/Features/Documents/EventHandlers/DocumentDeletedEventHandler.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. namespace CleanArchitecture.Blazor.Application.Features.Documents.EventHandlers; @@ -12,12 +12,12 @@ public DocumentDeletedEventHandler(ILogger logger) _logger = logger; } - public Task Handle(DeletedEvent notification, CancellationToken cancellationToken) + public ValueTask Handle(DeletedEvent notification, CancellationToken cancellationToken) { if (string.IsNullOrEmpty(notification.Entity.URL)) { _logger.LogWarning("The document URL is null or empty, skipping file deletion."); - return Task.CompletedTask; + return ValueTask.CompletedTask; } var folder = UploadType.Document.GetDescription(); @@ -41,6 +41,6 @@ public Task Handle(DeletedEvent notification, CancellationToken cancel _logger.LogWarning("File not found for deletion: {FilePath}", deleteFilePath); } - return Task.CompletedTask; + return ValueTask.CompletedTask; } } \ No newline at end of file diff --git a/src/Application/Features/Documents/Queries/GetFileStream/GetFileStreamQuery.cs b/src/Application/Features/Documents/Queries/GetFileStream/GetFileStreamQuery.cs index 42fe20c4e..dc6fbfc91 100644 --- a/src/Application/Features/Documents/Queries/GetFileStream/GetFileStreamQuery.cs +++ b/src/Application/Features/Documents/Queries/GetFileStream/GetFileStreamQuery.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using CleanArchitecture.Blazor.Application.Features.Documents.Caching; @@ -28,7 +28,7 @@ IApplicationDbContext context _context = context; } - public async Task<(string, byte[])> Handle(GetFileStreamQuery request, CancellationToken cancellationToken) + public async ValueTask<(string, byte[])> Handle(GetFileStreamQuery request, CancellationToken cancellationToken) { var item = await _context.Documents.FindAsync(new object?[] { request.Id }, cancellationToken); if (item is null) throw new Exception($"not found document entry by Id:{request.Id}."); diff --git a/src/Application/Features/Documents/Queries/PaginationQuery/DocumentsWithPaginationQuery.cs b/src/Application/Features/Documents/Queries/PaginationQuery/DocumentsWithPaginationQuery.cs index 22aaf828d..3f79e0cac 100644 --- a/src/Application/Features/Documents/Queries/PaginationQuery/DocumentsWithPaginationQuery.cs +++ b/src/Application/Features/Documents/Queries/PaginationQuery/DocumentsWithPaginationQuery.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using CleanArchitecture.Blazor.Application.Features.Documents.Caching; @@ -33,7 +33,7 @@ IApplicationDbContext context _context = context; } - public async Task> Handle(DocumentsWithPaginationQuery request, + public async ValueTask> Handle(DocumentsWithPaginationQuery request, CancellationToken cancellationToken) { var data = await _context.Documents.OrderBy($"{request.OrderBy} {request.SortDirection}") diff --git a/src/Application/Features/Identity/Notifications/ResetPassword/ResetPasswordCommand.cs b/src/Application/Features/Identity/Notifications/ResetPassword/ResetPasswordCommand.cs index 6c9151c9a..062d980ad 100644 --- a/src/Application/Features/Identity/Notifications/ResetPassword/ResetPasswordCommand.cs +++ b/src/Application/Features/Identity/Notifications/ResetPassword/ResetPasswordCommand.cs @@ -1,4 +1,4 @@ -namespace CleanArchitecture.Blazor.Application.Features.Identity.Notifications.ResetPassword; +namespace CleanArchitecture.Blazor.Application.Features.Identity.Notifications.ResetPassword; public record ResetPasswordNotification(string RequestUrl, string Email, string UserName) : INotification; @@ -22,7 +22,7 @@ public ResetPasswordNotificationHandler( } - public async Task Handle(ResetPasswordNotification notification, CancellationToken cancellationToken) + public async ValueTask Handle(ResetPasswordNotification notification, CancellationToken cancellationToken) { var sendMailResult = await _mailService.SendAsync( notification.Email, diff --git a/src/Application/Features/Identity/Notifications/SendFactorCode/SendFactorCodeCommand.cs b/src/Application/Features/Identity/Notifications/SendFactorCode/SendFactorCodeCommand.cs index 8ca7709f5..95c96dfed 100644 --- a/src/Application/Features/Identity/Notifications/SendFactorCode/SendFactorCodeCommand.cs +++ b/src/Application/Features/Identity/Notifications/SendFactorCode/SendFactorCodeCommand.cs @@ -1,4 +1,4 @@ -namespace CleanArchitecture.Blazor.Application.Features.Identity.Notifications.SendFactorCode; +namespace CleanArchitecture.Blazor.Application.Features.Identity.Notifications.SendFactorCode; public record SendFactorCodeNotification(string Email, string UserName, string AuthenticatorCode) : INotification; @@ -23,7 +23,7 @@ public SendFactorCodeNotificationHandler( } - public async Task Handle(SendFactorCodeNotification notification, CancellationToken cancellationToken) + public async ValueTask Handle(SendFactorCodeNotification notification, CancellationToken cancellationToken) { var subject = _localizer["Your Verification Code"]; var sendMailResult = await _mailService.SendAsync( diff --git a/src/Application/Features/Identity/Notifications/SendWelcome/SendWelcomeCommand.cs b/src/Application/Features/Identity/Notifications/SendWelcome/SendWelcomeCommand.cs index 906e1e579..bc253576a 100644 --- a/src/Application/Features/Identity/Notifications/SendWelcome/SendWelcomeCommand.cs +++ b/src/Application/Features/Identity/Notifications/SendWelcome/SendWelcomeCommand.cs @@ -1,4 +1,4 @@ -namespace CleanArchitecture.Blazor.Application.Features.Identity.Notifications.SendWelcome; +namespace CleanArchitecture.Blazor.Application.Features.Identity.Notifications.SendWelcome; public record SendWelcomeNotification(string LoginUrl, string Email, string UserName) : INotification; @@ -23,7 +23,7 @@ public SendWelcomeNotificationHandler( } - public async Task Handle(SendWelcomeNotification notification, CancellationToken cancellationToken) + public async ValueTask Handle(SendWelcomeNotification notification, CancellationToken cancellationToken) { var subject = string.Format(_localizer["Welcome to {0}"], _settings.AppName); var sendMailResult = await _mailService.SendAsync( diff --git a/src/Application/Features/Identity/Notifications/UserActivation/UserActivationCommand.cs b/src/Application/Features/Identity/Notifications/UserActivation/UserActivationCommand.cs index dd8dba680..7f854c64f 100644 --- a/src/Application/Features/Identity/Notifications/UserActivation/UserActivationCommand.cs +++ b/src/Application/Features/Identity/Notifications/UserActivation/UserActivationCommand.cs @@ -1,4 +1,4 @@ -namespace CleanArchitecture.Blazor.Application.Features.Identity.Notifications.UserActivation; +namespace CleanArchitecture.Blazor.Application.Features.Identity.Notifications.UserActivation; public record UserActivationNotification(string ActivationUrl, string Email, string UserId, string UserName) : INotification; @@ -23,7 +23,7 @@ public UserActivationNotificationHandler( } - public async Task Handle(UserActivationNotification notification, CancellationToken cancellationToken) + public async ValueTask Handle(UserActivationNotification notification, CancellationToken cancellationToken) { var sendMailResult = await _mailService.SendAsync( notification.Email, diff --git a/src/Application/Features/Loggers/Commands/Clear/ClearLogsCommand.cs b/src/Application/Features/Loggers/Commands/Clear/ClearLogsCommand.cs index 6ef8134d1..3edf5288d 100644 --- a/src/Application/Features/Loggers/Commands/Clear/ClearLogsCommand.cs +++ b/src/Application/Features/Loggers/Commands/Clear/ClearLogsCommand.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. @@ -27,7 +27,7 @@ ILogger logger _logger = logger; } - public async Task Handle(ClearLogsCommand request, CancellationToken cancellationToken) + public async ValueTask Handle(ClearLogsCommand request, CancellationToken cancellationToken) { await _context.Loggers.ExecuteDeleteAsync(); _logger.LogInformation("Logs have been erased"); diff --git a/src/Application/Features/Loggers/Queries/ChatData/LogsChatDataQuery.cs b/src/Application/Features/Loggers/Queries/ChatData/LogsChatDataQuery.cs index ea045e1a8..1a3928a95 100644 --- a/src/Application/Features/Loggers/Queries/ChatData/LogsChatDataQuery.cs +++ b/src/Application/Features/Loggers/Queries/ChatData/LogsChatDataQuery.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using CleanArchitecture.Blazor.Application.Features.Loggers.Caching; @@ -28,7 +28,7 @@ IStringLocalizer localizer _localizer = localizer; } - public async Task> Handle(LogsTimeLineChatDataQuery request, + public async ValueTask> Handle(LogsTimeLineChatDataQuery request, CancellationToken cancellationToken) { var data = await _context.Loggers.Where(x => x.TimeStamp >= request.LastDateTime) diff --git a/src/Application/Features/Loggers/Queries/Export/ExportLogsQuery.cs b/src/Application/Features/Loggers/Queries/Export/ExportLogsQuery.cs index f5d921311..164105cbe 100644 --- a/src/Application/Features/Loggers/Queries/Export/ExportLogsQuery.cs +++ b/src/Application/Features/Loggers/Queries/Export/ExportLogsQuery.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using CleanArchitecture.Blazor.Application.Features.Loggers.DTOs; @@ -30,7 +30,7 @@ IStringLocalizer localizer _localizer = localizer; } - public async Task Handle(ExportLogsQuery request, CancellationToken cancellationToken) + public async ValueTask Handle(ExportLogsQuery request, CancellationToken cancellationToken) { var data = await _context.Loggers .Where(x => x.Message!.Contains(request.Keyword) || x.Exception!.Contains(request.Keyword)) diff --git a/src/Application/Features/Loggers/Queries/PaginationQuery/LogsWithPaginationQuery.cs b/src/Application/Features/Loggers/Queries/PaginationQuery/LogsWithPaginationQuery.cs index 81972b17b..9c9ae5353 100644 --- a/src/Application/Features/Loggers/Queries/PaginationQuery/LogsWithPaginationQuery.cs +++ b/src/Application/Features/Loggers/Queries/PaginationQuery/LogsWithPaginationQuery.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using CleanArchitecture.Blazor.Application.Features.Loggers.Caching; @@ -33,7 +33,7 @@ IApplicationDbContext context _context = context; } - public async Task> Handle(LogsWithPaginationQuery request, + public async ValueTask> Handle(LogsWithPaginationQuery request, CancellationToken cancellationToken) { var data = await _context.Loggers.OrderBy($"{request.OrderBy} {request.SortDirection}") diff --git a/src/Application/Features/PicklistSets/Commands/AddEdit/AddEditPicklistSetCommand.cs b/src/Application/Features/PicklistSets/Commands/AddEdit/AddEditPicklistSetCommand.cs index c735e5939..5729a3137 100644 --- a/src/Application/Features/PicklistSets/Commands/AddEdit/AddEditPicklistSetCommand.cs +++ b/src/Application/Features/PicklistSets/Commands/AddEdit/AddEditPicklistSetCommand.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using CleanArchitecture.Blazor.Application.Features.PicklistSets.Caching; @@ -28,7 +28,7 @@ public AddEditPicklistSetCommandHandler( _context = context; } - public async Task> Handle(AddEditPicklistSetCommand request, CancellationToken cancellationToken) + public async ValueTask> Handle(AddEditPicklistSetCommand request, CancellationToken cancellationToken) { if (request.Id > 0) { diff --git a/src/Application/Features/PicklistSets/Commands/Delete/DeletePicklistSetCommand.cs b/src/Application/Features/PicklistSets/Commands/Delete/DeletePicklistSetCommand.cs index 245afe7fe..9faf3e050 100644 --- a/src/Application/Features/PicklistSets/Commands/Delete/DeletePicklistSetCommand.cs +++ b/src/Application/Features/PicklistSets/Commands/Delete/DeletePicklistSetCommand.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using CleanArchitecture.Blazor.Application.Features.PicklistSets.Caching; @@ -29,7 +29,7 @@ IApplicationDbContext context _context = context; } - public async Task> Handle(DeletePicklistSetCommand request, CancellationToken cancellationToken) + public async ValueTask> Handle(DeletePicklistSetCommand request, CancellationToken cancellationToken) { var items = await _context.PicklistSets.Where(x => request.Id.Contains(x.Id)).ToListAsync(cancellationToken); foreach (var item in items) diff --git a/src/Application/Features/PicklistSets/Commands/Import/ImportPicklistSetsCommand.cs b/src/Application/Features/PicklistSets/Commands/Import/ImportPicklistSetsCommand.cs index 7ead843d7..f508398de 100644 --- a/src/Application/Features/PicklistSets/Commands/Import/ImportPicklistSetsCommand.cs +++ b/src/Application/Features/PicklistSets/Commands/Import/ImportPicklistSetsCommand.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using CleanArchitecture.Blazor.Application.Features.PicklistSets.Caching; @@ -43,7 +43,7 @@ IValidator addValidator #nullable disable warnings - public async Task Handle(ImportPicklistSetsCommand request, CancellationToken cancellationToken) + public async ValueTask Handle(ImportPicklistSetsCommand request, CancellationToken cancellationToken) { var result = await _excelService.ImportAsync(request.Data, new Dictionary> diff --git a/src/Application/Features/PicklistSets/EventHandlers/PicklistSetChangedEventHandler.cs b/src/Application/Features/PicklistSets/EventHandlers/PicklistSetChangedEventHandler.cs index 0cf677464..286e3094d 100644 --- a/src/Application/Features/PicklistSets/EventHandlers/PicklistSetChangedEventHandler.cs +++ b/src/Application/Features/PicklistSets/EventHandlers/PicklistSetChangedEventHandler.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. namespace CleanArchitecture.Blazor.Application.Features.PicklistSets.EventHandlers; @@ -17,12 +17,12 @@ ILogger logger _logger = logger; } - public Task Handle(UpdatedEvent notification, CancellationToken cancellationToken) + public ValueTask Handle(UpdatedEvent notification, CancellationToken cancellationToken) { _logger.LogInformation("Handled domain event '{EventType}' with notification: {@Notification} ", notification.GetType().Name, notification); _picklistService.Refresh(); - return Task.CompletedTask; + return ValueTask.CompletedTask; } } \ No newline at end of file diff --git a/src/Application/Features/PicklistSets/Queries/ByName/PicklistSetsQueryByName.cs b/src/Application/Features/PicklistSets/Queries/ByName/PicklistSetsQueryByName.cs index e6ba3024c..ef62c99f3 100644 --- a/src/Application/Features/PicklistSets/Queries/ByName/PicklistSetsQueryByName.cs +++ b/src/Application/Features/PicklistSets/Queries/ByName/PicklistSetsQueryByName.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using CleanArchitecture.Blazor.Application.Features.PicklistSets.Caching; @@ -27,7 +27,7 @@ public PicklistSetsQueryByNameHandler( _context = context; } - public async Task> Handle(PicklistSetsQueryByName request, + public async ValueTask> Handle(PicklistSetsQueryByName request, CancellationToken cancellationToken) { var data = await _context.PicklistSets.Where(x => x.Name == request.Name) diff --git a/src/Application/Features/PicklistSets/Queries/Export/ExportPicklistSetsQuery.cs b/src/Application/Features/PicklistSets/Queries/Export/ExportPicklistSetsQuery.cs index 29330c1ec..a6b5b6dce 100644 --- a/src/Application/Features/PicklistSets/Queries/Export/ExportPicklistSetsQuery.cs +++ b/src/Application/Features/PicklistSets/Queries/Export/ExportPicklistSetsQuery.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using CleanArchitecture.Blazor.Application.Features.PicklistSets.DTOs; @@ -32,7 +32,7 @@ IStringLocalizer localizer } #pragma warning disable CS8602 #pragma warning disable CS8604 - public async Task Handle(ExportPicklistSetsQuery request, CancellationToken cancellationToken) + public async ValueTask Handle(ExportPicklistSetsQuery request, CancellationToken cancellationToken) { var data = await _context.PicklistSets.Where(x => x.Description.Contains(request.Keyword) || x.Value.Contains(request.Keyword) || diff --git a/src/Application/Features/PicklistSets/Queries/GetAll/GetAllPicklistSetsQuery.cs b/src/Application/Features/PicklistSets/Queries/GetAll/GetAllPicklistSetsQuery.cs index c99b5ca9d..ec640c232 100644 --- a/src/Application/Features/PicklistSets/Queries/GetAll/GetAllPicklistSetsQuery.cs +++ b/src/Application/Features/PicklistSets/Queries/GetAll/GetAllPicklistSetsQuery.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using CleanArchitecture.Blazor.Application.Features.PicklistSets.Caching; @@ -23,7 +23,7 @@ public GetAllPicklistSetsQueryHandler( _context = context; } - public async Task> Handle(GetAllPicklistSetsQuery request, + public async ValueTask> Handle(GetAllPicklistSetsQuery request, CancellationToken cancellationToken) { var data = await _context.PicklistSets.OrderBy(x => x.Name).ThenBy(x => x.Value) diff --git a/src/Application/Features/PicklistSets/Queries/PaginationQuery/PicklistSetsWithPaginationQuery.cs b/src/Application/Features/PicklistSets/Queries/PaginationQuery/PicklistSetsWithPaginationQuery.cs index c2ead2fe1..cdfa1ecec 100644 --- a/src/Application/Features/PicklistSets/Queries/PaginationQuery/PicklistSetsWithPaginationQuery.cs +++ b/src/Application/Features/PicklistSets/Queries/PaginationQuery/PicklistSetsWithPaginationQuery.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using CleanArchitecture.Blazor.Application.Features.PicklistSets.Caching; @@ -30,7 +30,7 @@ public PicklistSetsQueryHandler( _context = context; } - public async Task> Handle(PicklistSetsWithPaginationQuery request, + public async ValueTask> Handle(PicklistSetsWithPaginationQuery request, CancellationToken cancellationToken) { var data = await _context.PicklistSets.OrderBy($"{request.OrderBy} {request.SortDirection}") diff --git a/src/Application/Features/Products/Commands/AddEdit/AddEditProductCommand.cs b/src/Application/Features/Products/Commands/AddEdit/AddEditProductCommand.cs index 0bef48a9e..ed8daf71e 100644 --- a/src/Application/Features/Products/Commands/AddEdit/AddEditProductCommand.cs +++ b/src/Application/Features/Products/Commands/AddEdit/AddEditProductCommand.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. @@ -34,7 +34,7 @@ IApplicationDbContext context _context = context; } - public async Task> Handle(AddEditProductCommand request, CancellationToken cancellationToken) + public async ValueTask> Handle(AddEditProductCommand request, CancellationToken cancellationToken) { if (request.Id > 0) { diff --git a/src/Application/Features/Products/Commands/Delete/DeleteProductCommand.cs b/src/Application/Features/Products/Commands/Delete/DeleteProductCommand.cs index 72e5f769d..ad0efd82c 100644 --- a/src/Application/Features/Products/Commands/Delete/DeleteProductCommand.cs +++ b/src/Application/Features/Products/Commands/Delete/DeleteProductCommand.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. @@ -30,7 +30,7 @@ IApplicationDbContext context _context = context; } - public async Task> Handle(DeleteProductCommand request, CancellationToken cancellationToken) + public async ValueTask> Handle(DeleteProductCommand request, CancellationToken cancellationToken) { var items = await _context.Products.Where(x => request.Id.Contains(x.Id)).ToListAsync(cancellationToken); foreach (var item in items) diff --git a/src/Application/Features/Products/Commands/Import/ImportProductsCommand.cs b/src/Application/Features/Products/Commands/Import/ImportProductsCommand.cs index c0a66f431..0b587eb67 100644 --- a/src/Application/Features/Products/Commands/Import/ImportProductsCommand.cs +++ b/src/Application/Features/Products/Commands/Import/ImportProductsCommand.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. @@ -49,7 +49,7 @@ IStringLocalizer localizer _serializer = serializer; } - public async Task> Handle(CreateProductsTemplateCommand request, CancellationToken cancellationToken) + public async ValueTask> Handle(CreateProductsTemplateCommand request, CancellationToken cancellationToken) { var fields = new string[] { @@ -64,7 +64,7 @@ public async Task> Handle(CreateProductsTemplateCommand request, return await Result.SuccessAsync(result); } #nullable disable warnings - public async Task> Handle(ImportProductsCommand request, CancellationToken cancellationToken) + public async ValueTask> Handle(ImportProductsCommand request, CancellationToken cancellationToken) { var result = await _excelService.ImportAsync(request.Data, new Dictionary> diff --git a/src/Application/Features/Products/EventHandlers/ProductCreatedEventHandler.cs b/src/Application/Features/Products/EventHandlers/ProductCreatedEventHandler.cs index 1b45960eb..ac62589d8 100644 --- a/src/Application/Features/Products/EventHandlers/ProductCreatedEventHandler.cs +++ b/src/Application/Features/Products/EventHandlers/ProductCreatedEventHandler.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. @@ -19,7 +19,7 @@ ILogger logger _timer = new Stopwatch(); } - public async Task Handle(CreatedEvent notification, CancellationToken cancellationToken) + public async ValueTask Handle(CreatedEvent notification, CancellationToken cancellationToken) { _timer.Start(); await Task.Delay(3000, cancellationToken); diff --git a/src/Application/Features/Products/EventHandlers/ProductDeletedEventHandler.cs b/src/Application/Features/Products/EventHandlers/ProductDeletedEventHandler.cs index 493ca0391..3f101b2a1 100644 --- a/src/Application/Features/Products/EventHandlers/ProductDeletedEventHandler.cs +++ b/src/Application/Features/Products/EventHandlers/ProductDeletedEventHandler.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. namespace CleanArchitecture.Blazor.Application.Features.Products.EventHandlers; @@ -14,9 +14,9 @@ ILogger logger _logger = logger; } - public Task Handle(DeletedEvent notification, CancellationToken cancellationToken) + public ValueTask Handle(DeletedEvent notification, CancellationToken cancellationToken) { _logger.LogInformation("Handled domain event '{EventType}' with notification: {@Notification} ", notification.GetType().Name, notification); - return Task.CompletedTask; + return ValueTask.CompletedTask; } } \ No newline at end of file diff --git a/src/Application/Features/Products/EventHandlers/ProductUpdatedEventHandler.cs b/src/Application/Features/Products/EventHandlers/ProductUpdatedEventHandler.cs index 22450d643..8507d0945 100644 --- a/src/Application/Features/Products/EventHandlers/ProductUpdatedEventHandler.cs +++ b/src/Application/Features/Products/EventHandlers/ProductUpdatedEventHandler.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. @@ -15,10 +15,10 @@ ILogger logger _logger = logger; } - public Task Handle(UpdatedEvent notification, CancellationToken cancellationToken) + public ValueTask Handle(UpdatedEvent notification, CancellationToken cancellationToken) { _logger.LogInformation("Handled domain event '{EventType}' with notification: {@Notification} ", notification.GetType().Name, notification); - return Task.CompletedTask; + return ValueTask.CompletedTask; } } \ No newline at end of file diff --git a/src/Application/Features/Products/Queries/Export/ExportProductsQuery.cs b/src/Application/Features/Products/Queries/Export/ExportProductsQuery.cs index 0aed2d86a..422f2d793 100644 --- a/src/Application/Features/Products/Queries/Export/ExportProductsQuery.cs +++ b/src/Application/Features/Products/Queries/Export/ExportProductsQuery.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. @@ -39,7 +39,7 @@ IStringLocalizer localizer _localizer = localizer; } #nullable disable warnings - public async Task> Handle(ExportProductsQuery request, CancellationToken cancellationToken) + public async ValueTask> Handle(ExportProductsQuery request, CancellationToken cancellationToken) { var data = await _context.Products.ApplySpecification(request.Specification) .AsNoTracking() diff --git a/src/Application/Features/Products/Queries/GetAll/GetAllProductsQuery.cs b/src/Application/Features/Products/Queries/GetAll/GetAllProductsQuery.cs index 63d7e9810..d290c8f12 100644 --- a/src/Application/Features/Products/Queries/GetAll/GetAllProductsQuery.cs +++ b/src/Application/Features/Products/Queries/GetAll/GetAllProductsQuery.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using CleanArchitecture.Blazor.Application.Features.Products.Caching; @@ -35,7 +35,7 @@ IApplicationDbContext context _context = context; } - public async Task> Handle(GetAllProductsQuery request, CancellationToken cancellationToken) + public async ValueTask> Handle(GetAllProductsQuery request, CancellationToken cancellationToken) { var data = await _context.Products .ProjectTo() @@ -43,7 +43,7 @@ public async Task> Handle(GetAllProductsQuery request, C return data; } - public async Task Handle(GetProductQuery request, CancellationToken cancellationToken) + public async ValueTask Handle(GetProductQuery request, CancellationToken cancellationToken) { var data = await _context.Products.Where(x => x.Id == request.Id) .ProjectTo() diff --git a/src/Application/Features/Products/Queries/Pagination/ProductsPaginationQuery.cs b/src/Application/Features/Products/Queries/Pagination/ProductsPaginationQuery.cs index 8cda3e670..be0cab13a 100644 --- a/src/Application/Features/Products/Queries/Pagination/ProductsPaginationQuery.cs +++ b/src/Application/Features/Products/Queries/Pagination/ProductsPaginationQuery.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. @@ -35,7 +35,7 @@ IApplicationDbContext context _context = context; } - public async Task> Handle(ProductsWithPaginationQuery request, + public async ValueTask> Handle(ProductsWithPaginationQuery request, CancellationToken cancellationToken) { var data = await _context.Products.OrderBy($"{request.OrderBy} {request.SortDirection}") diff --git a/src/Application/Features/Tenants/Commands/AddEdit/AddEditTenantCommand.cs b/src/Application/Features/Tenants/Commands/AddEdit/AddEditTenantCommand.cs index b2c05be2f..c384599f3 100644 --- a/src/Application/Features/Tenants/Commands/AddEdit/AddEditTenantCommand.cs +++ b/src/Application/Features/Tenants/Commands/AddEdit/AddEditTenantCommand.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. @@ -31,7 +31,7 @@ IApplicationDbContext context _context = context; } - public async Task> Handle(AddEditTenantCommand request, CancellationToken cancellationToken) + public async ValueTask> Handle(AddEditTenantCommand request, CancellationToken cancellationToken) { var item = await _context.Tenants.FindAsync(new object[] { request.Id }, cancellationToken); if (item is null) diff --git a/src/Application/Features/Tenants/Commands/Delete/DeleteTenantCommand.cs b/src/Application/Features/Tenants/Commands/Delete/DeleteTenantCommand.cs index d24862c2d..0aa0ebe4c 100644 --- a/src/Application/Features/Tenants/Commands/Delete/DeleteTenantCommand.cs +++ b/src/Application/Features/Tenants/Commands/Delete/DeleteTenantCommand.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using CleanArchitecture.Blazor.Application.Common.Interfaces.MultiTenant; @@ -33,7 +33,7 @@ IApplicationDbContext context _context = context; } - public async Task> Handle(DeleteTenantCommand request, CancellationToken cancellationToken) + public async ValueTask> Handle(DeleteTenantCommand request, CancellationToken cancellationToken) { var items = await _context.Tenants.Where(x => request.Id.Contains(x.Id)).ToListAsync(cancellationToken); foreach (var item in items) _context.Tenants.Remove(item); diff --git a/src/Application/Features/Tenants/Queries/GetAll/GetAllTenantsQuery.cs b/src/Application/Features/Tenants/Queries/GetAll/GetAllTenantsQuery.cs index 253b55eb5..b08ae32e7 100644 --- a/src/Application/Features/Tenants/Queries/GetAll/GetAllTenantsQuery.cs +++ b/src/Application/Features/Tenants/Queries/GetAll/GetAllTenantsQuery.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using CleanArchitecture.Blazor.Application.Features.Tenants.Caching; @@ -24,7 +24,7 @@ IApplicationDbContext context _context = context; } - public async Task> Handle(GetAllTenantsQuery request, CancellationToken cancellationToken) + public async ValueTask> Handle(GetAllTenantsQuery request, CancellationToken cancellationToken) { var data = await _context.Tenants.OrderBy(x => x.Name) .ProjectTo() diff --git a/src/Application/Features/Tenants/Queries/Pagination/TenantsPaginationQuery.cs b/src/Application/Features/Tenants/Queries/Pagination/TenantsPaginationQuery.cs index 2b175db8f..506a41017 100644 --- a/src/Application/Features/Tenants/Queries/Pagination/TenantsPaginationQuery.cs +++ b/src/Application/Features/Tenants/Queries/Pagination/TenantsPaginationQuery.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using CleanArchitecture.Blazor.Application.Features.Tenants.Caching; @@ -31,7 +31,7 @@ IApplicationDbContext context _context = context; } - public async Task> Handle(TenantsWithPaginationQuery request, + public async ValueTask> Handle(TenantsWithPaginationQuery request, CancellationToken cancellationToken) { var data = await _context.Tenants.OrderBy($"{request.OrderBy} {request.SortDirection}") diff --git a/src/Application/Pipeline/AuthorizationBehaviour.cs b/src/Application/Pipeline/AuthorizationBehaviour.cs index c199b9c79..619fbf54b 100644 --- a/src/Application/Pipeline/AuthorizationBehaviour.cs +++ b/src/Application/Pipeline/AuthorizationBehaviour.cs @@ -19,7 +19,7 @@ public AuthorizationBehaviour( _identityService = identityService; } - public async Task Handle(TRequest request, RequestHandlerDelegate next, + public async ValueTask Handle(TRequest request, MessageHandlerDelegate next, CancellationToken cancellationToken) { var authorizeAttributes = request.GetType().GetCustomAttributes(); @@ -63,6 +63,6 @@ public async Task Handle(TRequest request, RequestHandlerDelegate> logger _logger = logger; } - public async Task Handle(TRequest request, RequestHandlerDelegate next, + public async ValueTask Handle(TRequest request, MessageHandlerDelegate next, CancellationToken cancellationToken) { _logger.LogTrace("Handling request of type {RequestType} with details {@Request}", nameof(request), request); - var response = await next().ConfigureAwait(false); + var response = await next(request, cancellationToken).ConfigureAwait(false); if (!string.IsNullOrEmpty(request.CacheKey)) { _cache.Remove(request.CacheKey); @@ -40,4 +40,4 @@ public async Task Handle(TRequest request, RequestHandlerDelegate> logger _logger = logger; } - public async Task Handle(TRequest request, RequestHandlerDelegate next, + public async ValueTask Handle(TRequest request, MessageHandlerDelegate next, CancellationToken cancellationToken) { _logger.LogTrace("Handling request of type {RequestType} with cache key {CacheKey}", nameof(request), request.CacheKey); - var response = await _fusionCache.GetOrSetAsync( + var response = await _fusionCache.GetOrSetAsync( request.CacheKey, - _ => next(), + (_, ct) => next(request, ct).AsTask(), tags:request.Tags ).ConfigureAwait(false); return response; } -} \ No newline at end of file +} diff --git a/src/Application/Pipeline/GlobalExceptionBehaviour.cs b/src/Application/Pipeline/GlobalExceptionBehaviour.cs index 8ac222ab7..d8bde866c 100644 --- a/src/Application/Pipeline/GlobalExceptionBehaviour.cs +++ b/src/Application/Pipeline/GlobalExceptionBehaviour.cs @@ -17,12 +17,12 @@ public GlobalExceptionBehaviour(ILogger logger, ICurrentUserAccessor c _currentUserAccessor = currentUserAccessor; } - public async Task Handle(TRequest request, RequestHandlerDelegate next, + public async ValueTask Handle(TRequest request, MessageHandlerDelegate next, CancellationToken cancellationToken) { try { - return await next().ConfigureAwait(false); + return await next(request, cancellationToken).ConfigureAwait(false); } catch (Exception ex) { @@ -37,4 +37,4 @@ public async Task Handle(TRequest request, RequestHandlerDelegate -/// This class is a behavior pipeline in MediatR. It is used to monitor performance +/// This class is a mediator pipeline behavior. It is used to monitor performance /// and log warnings if a request takes longer to execute than a specified threshold. /// /// Type of the Request @@ -30,7 +30,7 @@ public PerformanceBehaviour( /// The delegate for the next action in the pipeline process. /// Cancellation token /// Response from the next delegate - public async Task Handle(TRequest request, RequestHandlerDelegate next, + public async ValueTask Handle(TRequest request, MessageHandlerDelegate next, CancellationToken cancellationToken) { Stopwatch? timer = null; @@ -39,7 +39,7 @@ public async Task Handle(TRequest request, RequestHandlerDelegate 3) timer = Stopwatch.StartNew(); - var response = await next().ConfigureAwait(false); + var response = await next(request, cancellationToken).ConfigureAwait(false); timer?.Stop(); var elapsedMilliseconds = timer?.ElapsedMilliseconds; @@ -66,4 +66,4 @@ public async Task Handle(TRequest request, RequestHandlerDelegate : IRequestPreProcessor where TRequest : notnull +public class LoggingPreProcessor : MessagePreProcessor + where TRequest : notnull, IMessage { private readonly ICurrentUserAccessor _currentUserAccessor; private readonly ILogger _logger; @@ -17,12 +18,12 @@ public LoggingPreProcessor(ILogger logger, ICurrentUserAccessor curren _currentUserAccessor = currentUserAccessor; } - public Task Process(TRequest request, CancellationToken cancellationToken) + protected override ValueTask Handle(TRequest request, CancellationToken cancellationToken) { var requestName = nameof(TRequest); var userName = _currentUserAccessor.SessionInfo?.UserName; _logger.LogTrace("Processing request of type {RequestName} with details {@Request} by user {UserName}", requestName, request, userName); - return Task.CompletedTask; + return ValueTask.CompletedTask; } -} \ No newline at end of file +} diff --git a/src/Application/Pipeline/PreProcessors/ValidationPreProcessor.cs b/src/Application/Pipeline/PreProcessors/ValidationPreProcessor.cs index 0c49ed1f6..60df9393f 100644 --- a/src/Application/Pipeline/PreProcessors/ValidationPreProcessor.cs +++ b/src/Application/Pipeline/PreProcessors/ValidationPreProcessor.cs @@ -1,6 +1,7 @@ namespace CleanArchitecture.Blazor.Application.Pipeline.PreProcessors; -public sealed class ValidationPreProcessor : IRequestPreProcessor where TRequest : notnull +public sealed class ValidationPreProcessor : MessagePreProcessor + where TRequest : notnull, IMessage { private readonly IReadOnlyCollection> _validators; @@ -9,14 +10,16 @@ public ValidationPreProcessor(IEnumerable> validators) _validators = validators.ToList() ?? throw new ArgumentNullException(nameof(validators)); } - public async Task Process(TRequest request, CancellationToken cancellationToken) + protected override async ValueTask Handle(TRequest request, CancellationToken cancellationToken) { - if (!_validators.Any()) return; + if (!_validators.Any()) + return; var validationContext = new ValidationContext(request); var failures = await _validators.ValidateAsync(validationContext, cancellationToken); - if (failures.Any()) throw new ValidationException(failures); + if (failures.Any()) + throw new ValidationException(failures); } -} \ No newline at end of file +} diff --git a/src/Application/_Imports.cs b/src/Application/_Imports.cs index 57e2a6fe4..694be1872 100644 --- a/src/Application/_Imports.cs +++ b/src/Application/_Imports.cs @@ -19,8 +19,7 @@ global using CleanArchitecture.Blazor.Domain.Events; global using CleanArchitecture.Blazor.Domain.Entities; global using FluentValidation; -global using MediatR; -global using MediatR.Pipeline; +global using Mediator; global using Microsoft.EntityFrameworkCore; global using Microsoft.Extensions.Localization; -global using Microsoft.Extensions.Logging; \ No newline at end of file +global using Microsoft.Extensions.Logging; diff --git a/src/Domain/Common/DomainEvent.cs b/src/Domain/Common/DomainEvent.cs index d063faf77..911bf5e4a 100644 --- a/src/Domain/Common/DomainEvent.cs +++ b/src/Domain/Common/DomainEvent.cs @@ -1,7 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using MediatR; +using global::Mediator; namespace CleanArchitecture.Blazor.Domain.Common; @@ -14,4 +14,4 @@ protected DomainEvent() public bool IsPublished { get; set; } public DateTimeOffset DateOccurred { get; protected set; } -} \ No newline at end of file +} diff --git a/src/Domain/Domain.csproj b/src/Domain/Domain.csproj index 7eb7c68bc..6d4f1c92b 100644 --- a/src/Domain/Domain.csproj +++ b/src/Domain/Domain.csproj @@ -10,12 +10,12 @@ - + - - - + + + diff --git a/src/Infrastructure/DependencyInjection.cs b/src/Infrastructure/DependencyInjection.cs index 13d0d8a73..6a336bf83 100644 --- a/src/Infrastructure/DependencyInjection.cs +++ b/src/Infrastructure/DependencyInjection.cs @@ -3,10 +3,14 @@ using System.Reflection; using ActualLab.Fusion; +using CleanArchitecture.Blazor.Application.Common.PublishStrategies; using CleanArchitecture.Blazor.Application.Common.Interfaces.MediatorWrapper; using CleanArchitecture.Blazor.Application.Common.Interfaces.MultiTenant; using CleanArchitecture.Blazor.Application.Common.Interfaces.Serialization; +using CleanArchitecture.Blazor.Application.Common.ExceptionHandlers; using CleanArchitecture.Blazor.Application.Features.Fusion; +using CleanArchitecture.Blazor.Application.Pipeline; +using CleanArchitecture.Blazor.Application.Pipeline.PreProcessors; using CleanArchitecture.Blazor.Domain.Identity; using CleanArchitecture.Blazor.Infrastructure.Configurations; using CleanArchitecture.Blazor.Infrastructure.Constants.ClaimTypes; @@ -24,6 +28,7 @@ using Microsoft.AspNetCore.DataProtection; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.Extensions.Configuration; +using Mediator; using ZiggyCreatures.Caching.Fusion; namespace CleanArchitecture.Blazor.Infrastructure; @@ -54,6 +59,27 @@ public static IServiceCollection AddInfrastructure(this IServiceCollection servi .AddServices() .AddMessageServices(configuration); + services.AddMediator(options => + { + options.ServiceLifetime = ServiceLifetime.Scoped; + options.NotificationPublisherType = typeof(ParallelNoWaitPublisher); + options.Assemblies = + [ + typeof(CleanArchitecture.Blazor.Application.DependencyInjection), + ]; + options.PipelineBehaviors = + [ + typeof(DbExceptionHandler<,>), + typeof(GlobalExceptionHandler<,>), + typeof(ServerExceptionHandler<,>), + typeof(ValidationExceptionHandler<,>), + typeof(ValidationPreProcessor<,>), + typeof(PerformanceBehaviour<,>), + typeof(FusionCacheBehaviour<,>), + typeof(CacheInvalidationBehaviour<,>), + ]; + }); + services .AddAuthenticationService(configuration) .AddFusionCacheService() diff --git a/src/Infrastructure/Infrastructure.csproj b/src/Infrastructure/Infrastructure.csproj index 03e7f52fd..c52d46001 100644 --- a/src/Infrastructure/Infrastructure.csproj +++ b/src/Infrastructure/Infrastructure.csproj @@ -8,12 +8,16 @@ default + + all + runtime; build; native; contentfiles; analyzers + - + @@ -25,4 +29,4 @@ - \ No newline at end of file + diff --git a/src/Infrastructure/Services/MediatorWrapper/ScopedMediator.cs b/src/Infrastructure/Services/MediatorWrapper/ScopedMediator.cs index 51ce7fd06..6bab6c879 100644 --- a/src/Infrastructure/Services/MediatorWrapper/ScopedMediator.cs +++ b/src/Infrastructure/Services/MediatorWrapper/ScopedMediator.cs @@ -1,6 +1,6 @@ -using System.Runtime.CompilerServices; +using System.Runtime.CompilerServices; using CleanArchitecture.Blazor.Application.Common.Interfaces.MediatorWrapper; -using MediatR; +using Mediator; namespace CleanArchitecture.Blazor.Infrastructure.Services.MediatorWrapper; @@ -21,112 +21,111 @@ public ScopedMediator(IServiceScopeFactory scopeFactory) } /// - public async Task Send( + public async ValueTask Send( IRequest request, CancellationToken cancellationToken = default) { - AsyncServiceScope scope = _scopeFactory.CreateAsyncScope(); - await using (scope.ConfigureAwait(false)) - { - IMediator mediator = scope.ServiceProvider.GetRequiredService(); - - TResponse response = await mediator.Send(request, cancellationToken); - - return response; - } + await using AsyncServiceScope scope = _scopeFactory.CreateAsyncScope(); + IMediator mediator = scope.ServiceProvider.GetRequiredService(); + return await mediator.Send(request, cancellationToken).ConfigureAwait(false); } /// - public async Task Send(TRequest request, CancellationToken cancellationToken = default) - where TRequest : IRequest + public async ValueTask Send( + ICommand command, + CancellationToken cancellationToken = default) { - AsyncServiceScope scope = _scopeFactory.CreateAsyncScope(); - await using (scope.ConfigureAwait(false)) - { - IMediator mediator = scope.ServiceProvider.GetRequiredService(); - - await mediator.Send(request, cancellationToken); - } + await using AsyncServiceScope scope = _scopeFactory.CreateAsyncScope(); + IMediator mediator = scope.ServiceProvider.GetRequiredService(); + return await mediator.Send(command, cancellationToken).ConfigureAwait(false); } /// - public async Task Send(object request, CancellationToken cancellationToken = default) + public async ValueTask Send( + IQuery query, + CancellationToken cancellationToken = default) { - AsyncServiceScope scope = _scopeFactory.CreateAsyncScope(); - await using (scope.ConfigureAwait(false)) - { - IMediator mediator = scope.ServiceProvider.GetRequiredService(); - - object? response = await mediator.Send(request, cancellationToken); + await using AsyncServiceScope scope = _scopeFactory.CreateAsyncScope(); + IMediator mediator = scope.ServiceProvider.GetRequiredService(); + return await mediator.Send(query, cancellationToken).ConfigureAwait(false); + } - return response; - } + /// + public async ValueTask Send(object request, CancellationToken cancellationToken = default) + { + await using AsyncServiceScope scope = _scopeFactory.CreateAsyncScope(); + IMediator mediator = scope.ServiceProvider.GetRequiredService(); + return await mediator.Send(request, cancellationToken).ConfigureAwait(false); } /// - public async Task Publish( + public async ValueTask Publish( TNotification notification, CancellationToken cancellationToken = default) where TNotification : INotification { - AsyncServiceScope scope = _scopeFactory.CreateAsyncScope(); - await using (scope.ConfigureAwait(false)) - { - IMediator mediator = scope.ServiceProvider.GetRequiredService(); + await using AsyncServiceScope scope = _scopeFactory.CreateAsyncScope(); + IMediator mediator = scope.ServiceProvider.GetRequiredService(); + await mediator.Publish(notification, cancellationToken).ConfigureAwait(false); + } - await mediator.Publish(notification, cancellationToken); - } + /// + public async ValueTask Publish(object notification, CancellationToken cancellationToken = default) + { + await using AsyncServiceScope scope = _scopeFactory.CreateAsyncScope(); + IMediator mediator = scope.ServiceProvider.GetRequiredService(); + await mediator.Publish(notification, cancellationToken).ConfigureAwait(false); } /// - public async Task Publish(object notification, CancellationToken cancellationToken = default) + public async IAsyncEnumerable CreateStream( + IStreamRequest request, + [EnumeratorCancellation] CancellationToken cancellationToken = default) { - AsyncServiceScope scope = _scopeFactory.CreateAsyncScope(); - await using (scope.ConfigureAwait(false)) + await using AsyncServiceScope scope = _scopeFactory.CreateAsyncScope(); + IMediator mediator = scope.ServiceProvider.GetRequiredService(); + await foreach (TResponse item in mediator.CreateStream(request, cancellationToken).ConfigureAwait(false)) { - IMediator mediator = scope.ServiceProvider.GetRequiredService(); - - await mediator.Publish(notification, cancellationToken); + yield return item; } } /// public async IAsyncEnumerable CreateStream( - IStreamRequest request, - [EnumeratorCancellation] - CancellationToken cancellationToken = default) + IStreamCommand command, + [EnumeratorCancellation] CancellationToken cancellationToken = default) { - AsyncServiceScope scope = _scopeFactory.CreateAsyncScope(); - await using (scope.ConfigureAwait(false)) + await using AsyncServiceScope scope = _scopeFactory.CreateAsyncScope(); + IMediator mediator = scope.ServiceProvider.GetRequiredService(); + await foreach (TResponse item in mediator.CreateStream(command, cancellationToken).ConfigureAwait(false)) { - IMediator mediator = scope.ServiceProvider.GetRequiredService(); - - IAsyncEnumerable items = mediator.CreateStream(request, cancellationToken); + yield return item; + } + } - await foreach (TResponse item in items) - { - yield return item; - } + /// + public async IAsyncEnumerable CreateStream( + IStreamQuery query, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + await using AsyncServiceScope scope = _scopeFactory.CreateAsyncScope(); + IMediator mediator = scope.ServiceProvider.GetRequiredService(); + await foreach (TResponse item in mediator.CreateStream(query, cancellationToken).ConfigureAwait(false)) + { + yield return item; } } /// public async IAsyncEnumerable CreateStream( object request, - [EnumeratorCancellation] - CancellationToken cancellationToken = default) + [EnumeratorCancellation] CancellationToken cancellationToken = default) { - AsyncServiceScope scope = _scopeFactory.CreateAsyncScope(); - await using (scope.ConfigureAwait(false)) + await using AsyncServiceScope scope = _scopeFactory.CreateAsyncScope(); + IMediator mediator = scope.ServiceProvider.GetRequiredService(); + await foreach (object? item in mediator.CreateStream(request, cancellationToken).ConfigureAwait(false)) { - IMediator mediator = scope.ServiceProvider.GetRequiredService(); - - IAsyncEnumerable items = mediator.CreateStream(request, cancellationToken); - - await foreach (object? item in items) - { - yield return item; - } + yield return item; } } } diff --git a/src/Server.UI/Pages/AI/Chatbot.razor b/src/Server.UI/Pages/AI/Chatbot.razor index 683cad9b9..35c6fd14b 100644 --- a/src/Server.UI/Pages/AI/Chatbot.razor +++ b/src/Server.UI/Pages/AI/Chatbot.razor @@ -348,7 +348,7 @@ return """ CleanArchitecture.Blazor is an enterprise-ready Blazor Server template built on .NET 10 and follows Clean Architecture principles. - The solution is organized into Domain, Application, Infrastructure, and Server.UI layers, with business workflows structured using MediatR patterns. + The solution is organized into Domain, Application, Infrastructure, and Server.UI layers, with business workflows structured using mediator-driven request and notification patterns. The frontend uses MudBlazor and includes real-time communication through SignalR. The platform includes common enterprise capabilities: authentication and authorization (including MFA/RBAC/OAuth), multi-tenancy, auditing, background jobs (Hangfire), and business modules such as documents, products, and contacts, with support for AI-assisted development workflows and code generation. diff --git a/src/Server.UI/Pages/Public/Index.razor b/src/Server.UI/Pages/Public/Index.razor index 9df23b704..2ddd03cd6 100644 --- a/src/Server.UI/Pages/Public/Index.razor +++ b/src/Server.UI/Pages/Public/Index.razor @@ -419,7 +419,7 @@
Blazor Server
EF Core 10
MudBlazor 8
-
MediatR
+
Mediator
FluentValidation
PostgreSQL 17
Redis
@@ -506,4 +506,4 @@ @code { // Expose the Dark Mode state to the view to toggle CSS variables public bool IsDarkMode => LayoutService.IsDarkMode; -} \ No newline at end of file +} diff --git a/src/Server.UI/Services/DialogServiceHelper.cs b/src/Server.UI/Services/DialogServiceHelper.cs index 526ee762c..2fe276e5d 100644 --- a/src/Server.UI/Services/DialogServiceHelper.cs +++ b/src/Server.UI/Services/DialogServiceHelper.cs @@ -1,5 +1,5 @@ using CleanArchitecture.Blazor.Server.UI.Components.Dialogs; -using MediatR; +using Mediator; namespace CleanArchitecture.Blazor.Server.UI.Services; diff --git a/src/Server.UI/_Imports.razor b/src/Server.UI/_Imports.razor index ecfe7e1ba..bf1e08eeb 100644 --- a/src/Server.UI/_Imports.razor +++ b/src/Server.UI/_Imports.razor @@ -18,7 +18,7 @@ @using Microsoft.AspNetCore.Identity @using Microsoft.JSInterop @using MudBlazor -@using MediatR +@using Mediator @using FluentValidation; @using CleanArchitecture.Blazor.Domain @using CleanArchitecture.Blazor.Application.Common.ExceptionHandlers @@ -60,4 +60,4 @@ @inject IJSRuntime JS @inject IMediator Mediator @inject NavigationManager Navigation -@inject DialogServiceHelper DialogServiceHelper \ No newline at end of file +@inject DialogServiceHelper DialogServiceHelper diff --git a/tests/Application.IntegrationTests/Application.IntegrationTests.csproj b/tests/Application.IntegrationTests/Application.IntegrationTests.csproj index c124f7ecc..297e4882f 100644 --- a/tests/Application.IntegrationTests/Application.IntegrationTests.csproj +++ b/tests/Application.IntegrationTests/Application.IntegrationTests.csproj @@ -23,11 +23,11 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/tests/Application.IntegrationTests/Common/MediatorCompatibility/MediatorCompatibilityTests.cs b/tests/Application.IntegrationTests/Common/MediatorCompatibility/MediatorCompatibilityTests.cs new file mode 100644 index 000000000..bc1803a51 --- /dev/null +++ b/tests/Application.IntegrationTests/Common/MediatorCompatibility/MediatorCompatibilityTests.cs @@ -0,0 +1,42 @@ +using System.Threading.Tasks; +using CleanArchitecture.Blazor.Application.Features.Products.Commands.Delete; +using CleanArchitecture.Blazor.Domain.Entities; +using Microsoft.Extensions.DependencyInjection; +using NUnit.Framework; + +namespace CleanArchitecture.Blazor.Application.IntegrationTests.Common.MediatorCompatibility; + +using static Testing; + +public class MediatorCompatibilityTests : TestBase +{ + [Test] + public async Task Should_resolve_mediator_from_di_and_send_command() + { + var product = new Product { Name = "Compatibility product" }; + await AddAsync(product); + + using IServiceScope scope = CreateScope(); + + Mediator.IMediator mediator = scope.ServiceProvider.GetRequiredService(); + var result = await mediator.Send(new DeleteProductCommand([product.Id])); + + Assert.That(result.Succeeded, Is.True); + Assert.That(await CountAsync(), Is.EqualTo(0)); + } + + [Test] + public async Task Should_resolve_scoped_mediator_from_di_and_send_command() + { + var product = new Product { Name = "Scoped compatibility product" }; + await AddAsync(product); + + using IServiceScope scope = CreateScope(); + + var scopedMediator = scope.ServiceProvider.GetRequiredService(); + var result = await scopedMediator.Send(new DeleteProductCommand([product.Id])); + + Assert.That(result.Succeeded, Is.True); + Assert.That(await CountAsync(), Is.EqualTo(0)); + } +} diff --git a/tests/Application.IntegrationTests/Testing.cs b/tests/Application.IntegrationTests/Testing.cs index b1158c703..f50c41308 100644 --- a/tests/Application.IntegrationTests/Testing.cs +++ b/tests/Application.IntegrationTests/Testing.cs @@ -8,7 +8,7 @@ using CleanArchitecture.Blazor.Infrastructure; using CleanArchitecture.Blazor.Application.Common.Extensions; using CleanArchitecture.Blazor.Infrastructure.Persistence; -using MediatR; +using Mediator; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; @@ -107,6 +107,7 @@ private static void EnsureDatabase() { using var scope = _scopeFactory.CreateScope(); var context = scope.ServiceProvider.GetService(); + context.Database.EnsureDeleted(); context.Database.Migrate(); } @@ -117,6 +118,8 @@ public static async Task SendAsync(IRequest req return await mediator.Send(request); } + public static IServiceScope CreateScope() => _scopeFactory.CreateScope(); + public static async Task RunAsDefaultUserAsync() { return await RunAsUserAsync("Demo", "Password123!", new string[] { }); diff --git a/tests/Application.IntegrationTests/appsettings.json b/tests/Application.IntegrationTests/appsettings.json index 63c44599f..780dc912c 100644 --- a/tests/Application.IntegrationTests/appsettings.json +++ b/tests/Application.IntegrationTests/appsettings.json @@ -39,6 +39,15 @@ "MailPickupDirectory": "", "SocketOptions": null }, + "IdentitySettings": { + "RequireDigit": false, + "RequiredLength": 6, + "MaxLength": 16, + "RequireNonAlphanumeric": false, + "RequireUpperCase": false, + "RequireLowerCase": false, + "DefaultLockoutTimeSpan": 30 + }, "PrivacySettings": { "LogClientIpAddresses": true, "LogClientAgents": true diff --git a/tests/Application.UnitTests/Application.UnitTests.csproj b/tests/Application.UnitTests/Application.UnitTests.csproj index 4eaf45797..82d86aa1e 100644 --- a/tests/Application.UnitTests/Application.UnitTests.csproj +++ b/tests/Application.UnitTests/Application.UnitTests.csproj @@ -13,11 +13,11 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/tests/Application.UnitTests/Common/Behaviours/RequestLoggerTests.cs b/tests/Application.UnitTests/Common/Behaviours/RequestLoggerTests.cs index a2509e276..6964a8d91 100644 --- a/tests/Application.UnitTests/Common/Behaviours/RequestLoggerTests.cs +++ b/tests/Application.UnitTests/Common/Behaviours/RequestLoggerTests.cs @@ -1,9 +1,10 @@ -using System.Threading; +using System.Threading; using System.Threading.Tasks; using CleanArchitecture.Blazor.Application.Common.Interfaces; using CleanArchitecture.Blazor.Application.Common.Interfaces.Identity; using CleanArchitecture.Blazor.Application.Features.Products.Commands.AddEdit; using CleanArchitecture.Blazor.Application.Pipeline.PreProcessors; +using Mediator; using Microsoft.Extensions.Logging; using Moq; using NUnit.Framework; @@ -26,21 +27,23 @@ public RequestLoggerTests() [Test] public async Task ShouldCallGetUserNameAsyncOnceIfAuthenticated() { - _currentUserAccessor.Setup(x => x.SessionInfo).Returns(new SessionInfo("Administrator", "Administrator", "","","","", UserPresence.Available)); - var requestLogger = new LoggingPreProcessor(_logger.Object, _currentUserAccessor.Object); - await requestLogger.Process( + _currentUserAccessor.Setup(x => x.SessionInfo).Returns(new SessionInfo("Administrator", "Administrator", "", "", "", "", UserPresence.Available)); + var requestLogger = new LoggingPreProcessor(_logger.Object, _currentUserAccessor.Object); + await requestLogger.Handle( new AddEditProductCommand { Brand = "Brand", Name = "Brand", Price = 1.0m, Unit = "EA" }, - new CancellationToken()); + (_, _) => new ValueTask("ok"), + CancellationToken.None); _currentUserAccessor.Verify(i => i.SessionInfo, Times.Once); } [Test] public async Task ShouldNotCallGetUserNameAsyncOnceIfUnauthenticated() { - var requestLogger = new LoggingPreProcessor(_logger.Object, _currentUserAccessor.Object); - await requestLogger.Process( + var requestLogger = new LoggingPreProcessor(_logger.Object, _currentUserAccessor.Object); + await requestLogger.Handle( new AddEditProductCommand { Brand = "Brand", Name = "Brand", Price = 1.0m, Unit = "EA" }, - new CancellationToken()); + (_, _) => new ValueTask("ok"), + CancellationToken.None); _identityService.Verify(i => i.GetUserNameAsync(It.IsAny(), CancellationToken.None), Times.Never); } -} \ No newline at end of file +} diff --git a/tests/Application.UnitTests/Common/MediatorCompatibility/ParallelNoWaitPublisherTests.cs b/tests/Application.UnitTests/Common/MediatorCompatibility/ParallelNoWaitPublisherTests.cs new file mode 100644 index 000000000..d06013911 --- /dev/null +++ b/tests/Application.UnitTests/Common/MediatorCompatibility/ParallelNoWaitPublisherTests.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using CleanArchitecture.Blazor.Application.Common.PublishStrategies; +using Mediator; +using NUnit.Framework; + +namespace CleanArchitecture.Blazor.Application.UnitTests.Common.MediatorCompatibility; + +public class ParallelNoWaitPublisherTests +{ + [Test] + public async Task Publish_returns_before_handler_completion() + { + var handlerStarted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var releaseHandler = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var publisher = new ParallelNoWaitPublisher(); + var notification = new TestNotification(); + var handler = new TestHandler(handlerStarted, releaseHandler); + var handlers = new NotificationHandlers(new[] { handler }, true); + + ValueTask publishTask = publisher.Publish(handlers, notification, CancellationToken.None); + + Assert.That(publishTask.IsCompleted, Is.True); + + await handlerStarted.Task.WaitAsync(TimeSpan.FromSeconds(5)); + Assert.That(handlerStarted.Task.IsCompleted, Is.True); + Assert.That(releaseHandler.Task.IsCompleted, Is.False); + + releaseHandler.SetResult(true); + await publishTask; + } + + private sealed class TestHandler : INotificationHandler + { + private readonly TaskCompletionSource _handlerStarted; + private readonly TaskCompletionSource _releaseHandler; + + public TestHandler(TaskCompletionSource handlerStarted, TaskCompletionSource releaseHandler) + { + _handlerStarted = handlerStarted; + _releaseHandler = releaseHandler; + } + + public async ValueTask Handle(TestNotification notification, CancellationToken cancellationToken) + { + _handlerStarted.TrySetResult(true); + await _releaseHandler.Task.WaitAsync(cancellationToken); + } + } + + private sealed record TestNotification : INotification; +} diff --git a/tests/Application.UnitTests/Common/MediatorCompatibility/RequestExceptionHandlerStateTests.cs b/tests/Application.UnitTests/Common/MediatorCompatibility/RequestExceptionHandlerStateTests.cs new file mode 100644 index 000000000..6d07373e3 --- /dev/null +++ b/tests/Application.UnitTests/Common/MediatorCompatibility/RequestExceptionHandlerStateTests.cs @@ -0,0 +1,36 @@ +using System.Linq; +using Mediator; +using Microsoft.Extensions.DependencyInjection; +using NUnit.Framework; + +namespace CleanArchitecture.Blazor.Application.UnitTests.Common.MediatorCompatibility; + +public class MediatorAbstractionsSmokeTests +{ + [Test] + public void Contracts_ShouldCompileAgainstMediatorAbstractions() + { + _ = new SmokeNotification(); + _ = new SmokeRequest(); + _ = typeof(IPipelineBehavior); + } + + [Test] + public void AddApplication_ShouldNotRegisterMediatorPipelineBehaviorsDirectly() + { + var services = new ServiceCollection(); + + services.AddApplication(); + + var pipelineImplementations = services + .Where(d => d.ServiceType == typeof(IPipelineBehavior<,>)) + .Select(d => d.ImplementationType) + .ToList(); + + Assert.That(pipelineImplementations, Is.Empty); + } + + private sealed record SmokeNotification : INotification; + + private sealed record SmokeRequest : IRequest; +} diff --git a/tests/Application.UnitTests/Constants/ConstantStringTests.cs b/tests/Application.UnitTests/Constants/ConstantStringTests.cs index fe0f83dd6..15af03169 100644 --- a/tests/Application.UnitTests/Constants/ConstantStringTests.cs +++ b/tests/Application.UnitTests/Constants/ConstantStringTests.cs @@ -8,37 +8,37 @@ public class ConstantStringTests [Test] public void Test() { - Assert.Equals("Refresh", ConstantString.Refresh); - Assert.Equals("Edit", ConstantString.Edit); - Assert.Equals("Delete", ConstantString.Delete); - Assert.Equals("Add", ConstantString.Add); - Assert.Equals("New", ConstantString.New); - Assert.Equals("Export to Excel", ConstantString.Export); - Assert.Equals("Import from Excel", ConstantString.Import); - Assert.Equals("Actions", ConstantString.Actions); - Assert.Equals("Save", ConstantString.Save); - Assert.Equals("Save Changes", ConstantString.SaveChanges); - Assert.Equals("Cancel", ConstantString.Cancel); - Assert.Equals("Close", ConstantString.Close); - Assert.Equals("Search", ConstantString.Search); - Assert.Equals("Clear", ConstantString.Clear); - Assert.Equals("Reset", ConstantString.Reset); - Assert.Equals("OK", ConstantString.Ok); - Assert.Equals("Confirm", ConstantString.Confirm); - Assert.Equals("Yes", ConstantString.Yes); - Assert.Equals("No", ConstantString.No); - Assert.Equals("Next", ConstantString.Next); - Assert.Equals("Previous", ConstantString.Previous); - Assert.Equals("Upload", ConstantString.Upload); - Assert.Equals("Download", ConstantString.Download); - Assert.Equals("Uploading...", ConstantString.Uploading); - Assert.Equals("Downloading...", ConstantString.Downloading); - Assert.Equals("No Allowed", ConstantString.NoAllowed); - Assert.Equals("Sign in with {0}", ConstantString.SigninWith); - Assert.Equals("Logout", ConstantString.Logout); - Assert.Equals("Sign In", ConstantString.Signin); - Assert.Equals("Microsoft", ConstantString.Microsoft); - Assert.Equals("Facebook", ConstantString.Facebook); - Assert.Equals("Google", ConstantString.Google); + Assert.AreEqual(ConstantString.Localize("Refresh"), ConstantString.Refresh); + Assert.AreEqual(ConstantString.Localize("Edit"), ConstantString.Edit); + Assert.AreEqual(ConstantString.Localize("Delete"), ConstantString.Delete); + Assert.AreEqual(ConstantString.Localize("Add"), ConstantString.Add); + Assert.AreEqual(ConstantString.Localize("New"), ConstantString.New); + Assert.AreEqual(ConstantString.Localize("Export to Excel"), ConstantString.Export); + Assert.AreEqual(ConstantString.Localize("Import from Excel"), ConstantString.Import); + Assert.AreEqual(ConstantString.Localize("Actions"), ConstantString.Actions); + Assert.AreEqual(ConstantString.Localize("Save"), ConstantString.Save); + Assert.AreEqual(ConstantString.Localize("Save Changes"), ConstantString.SaveChanges); + Assert.AreEqual(ConstantString.Localize("Cancel"), ConstantString.Cancel); + Assert.AreEqual(ConstantString.Localize("Close"), ConstantString.Close); + Assert.AreEqual(ConstantString.Localize("Search"), ConstantString.Search); + Assert.AreEqual(ConstantString.Localize("Clear"), ConstantString.Clear); + Assert.AreEqual(ConstantString.Localize("Reset"), ConstantString.Reset); + Assert.AreEqual(ConstantString.Localize("OK"), ConstantString.Ok); + Assert.AreEqual(ConstantString.Localize("Confirm"), ConstantString.Confirm); + Assert.AreEqual(ConstantString.Localize("Yes"), ConstantString.Yes); + Assert.AreEqual(ConstantString.Localize("No"), ConstantString.No); + Assert.AreEqual(ConstantString.Localize("Next"), ConstantString.Next); + Assert.AreEqual(ConstantString.Localize("Previous"), ConstantString.Previous); + Assert.AreEqual(ConstantString.Localize("Upload"), ConstantString.Upload); + Assert.AreEqual(ConstantString.Localize("Download"), ConstantString.Download); + Assert.AreEqual(ConstantString.Localize("Uploading..."), ConstantString.Uploading); + Assert.AreEqual(ConstantString.Localize("Downloading..."), ConstantString.Downloading); + Assert.AreEqual(ConstantString.Localize("No Allowed"), ConstantString.NoAllowed); + Assert.AreEqual(ConstantString.Localize("Sign in with {0}"), ConstantString.SigninWith); + Assert.AreEqual(ConstantString.Localize("Logout"), ConstantString.Logout); + Assert.AreEqual(ConstantString.Localize("Sign In"), ConstantString.Signin); + Assert.AreEqual(ConstantString.Localize("Microsoft"), ConstantString.Microsoft); + Assert.AreEqual(ConstantString.Localize("Facebook"), ConstantString.Facebook); + Assert.AreEqual(ConstantString.Localize("Google"), ConstantString.Google); } -} \ No newline at end of file +} diff --git a/tests/Application.UnitTests/Infrastructure/MediatorCompatibility/MediatorRuntimeRegistrationTests.cs b/tests/Application.UnitTests/Infrastructure/MediatorCompatibility/MediatorRuntimeRegistrationTests.cs new file mode 100644 index 000000000..559fed800 --- /dev/null +++ b/tests/Application.UnitTests/Infrastructure/MediatorCompatibility/MediatorRuntimeRegistrationTests.cs @@ -0,0 +1,92 @@ +#nullable enable + +using System.Collections.Generic; +using System.Linq; +using CleanArchitecture.Blazor.Application; +using CleanArchitecture.Blazor.Application.Common.PublishStrategies; +using CleanArchitecture.Blazor.Infrastructure; +using Mediator; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using NUnit.Framework; + +namespace CleanArchitecture.Blazor.Application.UnitTests.Infrastructure.MediatorCompatibility; + +public class MediatorRuntimeRegistrationTests +{ + [Test] + public void AddInfrastructure_And_AddApplication_ShouldResolveMediatorRuntime() + { + using ServiceProvider provider = CreateServices().BuildServiceProvider(new ServiceProviderOptions { ValidateScopes = true }); + using IServiceScope scope = provider.CreateScope(); + IMediator mediator = scope.ServiceProvider.GetRequiredService(); + + Assert.That(mediator, Is.Not.Null); + } + + [Test] + public void AddInfrastructure_And_AddApplication_ShouldUseParallelNoWaitPublisher() + { + using ServiceProvider provider = CreateServices().BuildServiceProvider(new ServiceProviderOptions { ValidateScopes = true }); + using IServiceScope scope = provider.CreateScope(); + INotificationPublisher publisher = scope.ServiceProvider.GetRequiredService(); + + Assert.That(publisher, Is.TypeOf()); + } + + [Test] + public void AddInfrastructure_And_AddApplication_ShouldRegisterEachPipelineBehaviorOnce() + { + var services = CreateServices(); + + var duplicatePipelineRegistrations = services + .Where(d => d.ServiceType == typeof(IPipelineBehavior<,>)) + .GroupBy(d => d.ImplementationType) + .Where(group => group.Key is not null && group.Count() > 1) + .Select(group => group.Key!.Name) + .ToList(); + + Assert.That(duplicatePipelineRegistrations, Is.Empty); + } + + private static IServiceCollection CreateServices() + { + var settings = new Dictionary + { + ["UseInMemoryDatabase"] = bool.TrueString, + ["DatabaseSettings:DBProvider"] = "sqlite", + ["DatabaseSettings:ConnectionString"] = "Data Source=:memory:", + ["AppConfigurationSettings:Secret"] = "unit-test-secret", + ["AppConfigurationSettings:BehindSSLProxy"] = bool.FalseString, + ["AppConfigurationSettings:ProxyIP"] = string.Empty, + ["AppConfigurationSettings:ApplicationUrl"] = "https://localhost", + ["AppConfigurationSettings:Resilience"] = bool.FalseString, + ["SmtpClientOptions:Server"] = string.Empty, + ["SmtpClientOptions:Port"] = "25", + ["SmtpClientOptions:User"] = string.Empty, + ["SmtpClientOptions:Password"] = string.Empty, + ["SmtpClientOptions:UseSsl"] = bool.FalseString, + ["SmtpClientOptions:RequiresAuthentication"] = bool.FalseString, + ["SmtpClientOptions:PreferredEncoding"] = string.Empty, + ["SmtpClientOptions:UsePickupDirectory"] = bool.FalseString, + ["SmtpClientOptions:MailPickupDirectory"] = string.Empty, + ["SmtpClientOptions:DefaultFromEmail"] = "noreply@test.local", + ["IdentitySettings:RequireDigit"] = bool.FalseString, + ["IdentitySettings:RequiredLength"] = "6", + ["IdentitySettings:MaxLength"] = "16", + ["IdentitySettings:RequireNonAlphanumeric"] = bool.FalseString, + ["IdentitySettings:RequireUpperCase"] = bool.FalseString, + ["IdentitySettings:RequireLowerCase"] = bool.FalseString, + ["IdentitySettings:DefaultLockoutTimeSpan"] = "30", + }; + + IConfiguration configuration = new ConfigurationBuilder() + .AddInMemoryCollection(settings) + .Build(); + + var services = new ServiceCollection(); + services.AddInfrastructure(configuration); + services.AddApplication(); + return services; + } +} diff --git a/tests/Application.UnitTests/Infrastructure/MediatorCompatibility/ScopedMediatorTests.cs b/tests/Application.UnitTests/Infrastructure/MediatorCompatibility/ScopedMediatorTests.cs new file mode 100644 index 000000000..b99e23f13 --- /dev/null +++ b/tests/Application.UnitTests/Infrastructure/MediatorCompatibility/ScopedMediatorTests.cs @@ -0,0 +1,174 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using CleanArchitecture.Blazor.Infrastructure.Services.MediatorWrapper; +using Mediator; +using Microsoft.Extensions.DependencyInjection; +using NUnit.Framework; + +namespace CleanArchitecture.Blazor.Application.UnitTests.Infrastructure.MediatorCompatibility; + +public class ScopedMediatorTests +{ + [SetUp] + public void SetUp() + { + ScopeProbe.Reset(); + FakeMediator.Reset(); + } + + [Test] + public async Task Send_creates_and_disposes_a_child_scope() + { + var services = new ServiceCollection(); + services.AddScoped(); + services.AddScoped(); + + using ServiceProvider provider = services.BuildServiceProvider(new ServiceProviderOptions + { + ValidateScopes = true, + ValidateOnBuild = true, + }); + var sut = new ScopedMediator(provider.GetRequiredService()); + + string firstResult = await sut.Send(new PingQuery()); + string secondResult = await sut.Send(new PingQuery()); + + Assert.That(firstResult, Is.EqualTo("pong")); + Assert.That(secondResult, Is.EqualTo("pong")); + Assert.That(FakeMediator.SendCallCount, Is.EqualTo(2)); + Assert.That(ScopeProbe.CreatedCount, Is.EqualTo(2)); + Assert.That(ScopeProbe.DisposedCount, Is.EqualTo(2)); + Assert.That(ScopeProbe.CreatedIds[0], Is.Not.EqualTo(ScopeProbe.CreatedIds[1])); + Assert.That(ScopeProbe.DisposedIds, Is.EquivalentTo(ScopeProbe.CreatedIds)); + Assert.That(FakeMediator.ResolvedProbeIds, Is.EquivalentTo(ScopeProbe.CreatedIds)); + } + + private sealed record PingQuery : IRequest; + + private sealed class ScopeProbe : IDisposable + { + public static int CreatedCount { get; private set; } + public static int DisposedCount { get; private set; } + + public static List CreatedIds { get; } = new(); + public static List DisposedIds { get; } = new(); + + public Guid Id { get; } = Guid.NewGuid(); + + public ScopeProbe() + { + CreatedCount++; + CreatedIds.Add(Id); + } + + public void Dispose() + { + DisposedCount++; + DisposedIds.Add(Id); + } + + public static void Reset() + { + CreatedCount = 0; + DisposedCount = 0; + CreatedIds.Clear(); + DisposedIds.Clear(); + } + } + + private sealed class FakeMediator : IMediator + { + public static int SendCallCount { get; private set; } + + public static List ResolvedProbeIds { get; } = new(); + + private readonly ScopeProbe _scopeProbe; + + public FakeMediator(ScopeProbe scopeProbe) + { + _scopeProbe = scopeProbe; + } + + public static void Reset() + { + SendCallCount = 0; + ResolvedProbeIds.Clear(); + } + + public ValueTask Send(IRequest request, CancellationToken cancellationToken = default) + { + SendCallCount++; + ResolvedProbeIds.Add(_scopeProbe.Id); + + if (request is PingQuery) + { + return new ValueTask((TResponse)(object)"pong"); + } + + throw new NotSupportedException($"Unexpected request type: {request.GetType().Name}"); + } + + public ValueTask Send(ICommand command, CancellationToken cancellationToken = default) + { + throw new NotSupportedException(); + } + + public ValueTask Send(IQuery query, CancellationToken cancellationToken = default) + { + throw new NotSupportedException(); + } + + public ValueTask Send(object request, CancellationToken cancellationToken = default) + { + throw new NotSupportedException(); + } + + public ValueTask Publish(TNotification notification, CancellationToken cancellationToken = default) + where TNotification : INotification + { + throw new NotSupportedException(); + } + + public ValueTask Publish(object notification, CancellationToken cancellationToken = default) + { + throw new NotSupportedException(); + } + + public async IAsyncEnumerable CreateStream( + IStreamRequest request, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + await Task.CompletedTask; + yield break; + } + + public async IAsyncEnumerable CreateStream( + IStreamCommand command, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + await Task.CompletedTask; + yield break; + } + + public async IAsyncEnumerable CreateStream( + IStreamQuery query, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + await Task.CompletedTask; + yield break; + } + + public async IAsyncEnumerable CreateStream( + object request, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + await Task.CompletedTask; + yield break; + } + } +} diff --git a/tests/Domain.UnitTests/Domain.UnitTests.csproj b/tests/Domain.UnitTests/Domain.UnitTests.csproj index c22c21f31..28af5ea88 100644 --- a/tests/Domain.UnitTests/Domain.UnitTests.csproj +++ b/tests/Domain.UnitTests/Domain.UnitTests.csproj @@ -14,11 +14,11 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - +