From 4f0718a4f5588e8a55607a1a13d7eeec85a6d6c0 Mon Sep 17 00:00:00 2001 From: TekH Date: Fri, 9 Oct 2026 23:06:00 +0200 Subject: [PATCH] feat(application): add CQRS pipeline infrastructure and repository contracts --- .../Common/Behaviors/ValidationBehavior.cs | 48 +++++++++++++++++++ .../Common/Interfaces/IDocumentRepository.cs | 17 +++++++ .../Common/Interfaces/IRepository.cs | 8 ++++ .../Common/Interfaces/IWorkflowRepository.cs | 34 +++++++++++++ .../Mappings/ApplicationMappingProfile.cs | 16 +++++++ .../DependencyInjection.cs | 2 + 6 files changed, 125 insertions(+) create mode 100644 src/core/DigitalData.TaskFlow.Application/Common/Behaviors/ValidationBehavior.cs create mode 100644 src/core/DigitalData.TaskFlow.Application/Common/Interfaces/IDocumentRepository.cs create mode 100644 src/core/DigitalData.TaskFlow.Application/Common/Interfaces/IRepository.cs create mode 100644 src/core/DigitalData.TaskFlow.Application/Common/Interfaces/IWorkflowRepository.cs create mode 100644 src/core/DigitalData.TaskFlow.Application/Common/Mappings/ApplicationMappingProfile.cs diff --git a/src/core/DigitalData.TaskFlow.Application/Common/Behaviors/ValidationBehavior.cs b/src/core/DigitalData.TaskFlow.Application/Common/Behaviors/ValidationBehavior.cs new file mode 100644 index 0000000..6af7d8d --- /dev/null +++ b/src/core/DigitalData.TaskFlow.Application/Common/Behaviors/ValidationBehavior.cs @@ -0,0 +1,48 @@ +using FluentValidation; +using MediatR; + +namespace DigitalData.TaskFlow.Application.Common.Behaviors; + +/// +/// Runs FluentValidation validators before a MediatR request reaches its handler. +/// +/// Request type. +/// Response type. +public sealed class ValidationBehavior(IEnumerable> validators) + : IPipelineBehavior + where TRequest : notnull +{ + /// + /// Validates the request and continues the pipeline when validation succeeds. + /// + /// Current request instance. + /// Next pipeline delegate. + /// Cancellation token. + /// Handler response when validation passes. + /// Thrown when one or more validation errors are found. + public async Task Handle( + TRequest request, + RequestHandlerDelegate next, + CancellationToken cancellationToken) + { + if (!validators.Any()) + { + return await next(cancellationToken); + } + + var context = new ValidationContext(request); + var validationResults = await Task.WhenAll(validators.Select(v => v.ValidateAsync(context, cancellationToken))); + + var failures = validationResults + .SelectMany(r => r.Errors) + .Where(f => f is not null) + .ToList(); + + if (failures.Count != 0) + { + throw new ValidationException(failures); + } + + return await next(cancellationToken); + } +} diff --git a/src/core/DigitalData.TaskFlow.Application/Common/Interfaces/IDocumentRepository.cs b/src/core/DigitalData.TaskFlow.Application/Common/Interfaces/IDocumentRepository.cs new file mode 100644 index 0000000..058c4fd --- /dev/null +++ b/src/core/DigitalData.TaskFlow.Application/Common/Interfaces/IDocumentRepository.cs @@ -0,0 +1,17 @@ +using DigitalData.TaskFlow.Domain.Documents; + +namespace DigitalData.TaskFlow.Application.Common.Interfaces; + +/// +/// Defines document-specific data access operations. +/// +public interface IDocumentRepository : IRepository +{ + /// + /// Gets document binary content for the given document identifier. + /// + /// Document identifier. + /// Cancellation token. + /// Binary document payload. + Task GetAsync(string id, CancellationToken cancellationToken); +} diff --git a/src/core/DigitalData.TaskFlow.Application/Common/Interfaces/IRepository.cs b/src/core/DigitalData.TaskFlow.Application/Common/Interfaces/IRepository.cs new file mode 100644 index 0000000..589ec36 --- /dev/null +++ b/src/core/DigitalData.TaskFlow.Application/Common/Interfaces/IRepository.cs @@ -0,0 +1,8 @@ +namespace DigitalData.TaskFlow.Application.Common.Interfaces; + +/// +/// Defines a marker contract for repository abstractions in the application layer. +/// +public interface IRepository +{ +} diff --git a/src/core/DigitalData.TaskFlow.Application/Common/Interfaces/IWorkflowRepository.cs b/src/core/DigitalData.TaskFlow.Application/Common/Interfaces/IWorkflowRepository.cs new file mode 100644 index 0000000..87fb6ec --- /dev/null +++ b/src/core/DigitalData.TaskFlow.Application/Common/Interfaces/IWorkflowRepository.cs @@ -0,0 +1,34 @@ +using DigitalData.TaskFlow.Domain.Closing; +using DigitalData.TaskFlow.Domain.Workflows; + +namespace DigitalData.TaskFlow.Application.Common.Interfaces; + +/// +/// Defines workflow-specific data access operations for reading and closing workflow items. +/// +public interface IWorkflowRepository : IRepository +{ + /// + /// Gets grouped workflow data for the given WISAG user identifier. + /// + /// WISAG user identifier. + /// Cancellation token. + /// Workflow data grouped by source. + Task GetAsync(int wisagId, CancellationToken cancellationToken); + + /// + /// Applies a closing decision to a workflow process. + /// + /// WISAG user identifier. + /// Source system name. + /// Process identifier. + /// Closing decision payload. + /// Cancellation token. + /// Closing operation result. + Task CloseAsync( + int wisagId, + string source, + int processId, + CloseWorkflowDecision decision, + CancellationToken cancellationToken); +} diff --git a/src/core/DigitalData.TaskFlow.Application/Common/Mappings/ApplicationMappingProfile.cs b/src/core/DigitalData.TaskFlow.Application/Common/Mappings/ApplicationMappingProfile.cs new file mode 100644 index 0000000..8797642 --- /dev/null +++ b/src/core/DigitalData.TaskFlow.Application/Common/Mappings/ApplicationMappingProfile.cs @@ -0,0 +1,16 @@ +using AutoMapper; + +namespace DigitalData.TaskFlow.Application.Common.Mappings; + +/// +/// Defines AutoMapper configuration for application-level mappings. +/// +public sealed class ApplicationMappingProfile : Profile +{ + /// + /// Initializes mapping configuration. + /// + public ApplicationMappingProfile() + { + } +} diff --git a/src/core/DigitalData.TaskFlow.Application/DependencyInjection.cs b/src/core/DigitalData.TaskFlow.Application/DependencyInjection.cs index 840ce5e..59c36d5 100644 --- a/src/core/DigitalData.TaskFlow.Application/DependencyInjection.cs +++ b/src/core/DigitalData.TaskFlow.Application/DependencyInjection.cs @@ -4,6 +4,7 @@ using MediatR; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using System.Reflection; +using DigitalData.TaskFlow.Application.Common.Behaviors; namespace DigitalData.TaskFlow.Application; @@ -30,6 +31,7 @@ public static class DependencyInjection }); services.AddValidatorsFromAssembly(typeof(DependencyInjection).Assembly); + services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>)); services.AddAutoMapper(_ => { }, Assembly.GetExecutingAssembly()); return services;