feat(application): add CQRS pipeline infrastructure and repository contracts

This commit is contained in:
2026-10-09 23:06:00 +02:00
parent 986d0aa256
commit 4f0718a4f5
6 changed files with 125 additions and 0 deletions

View File

@@ -0,0 +1,48 @@
using FluentValidation;
using MediatR;
namespace DigitalData.TaskFlow.Application.Common.Behaviors;
/// <summary>
/// Runs FluentValidation validators before a MediatR request reaches its handler.
/// </summary>
/// <typeparam name="TRequest">Request type.</typeparam>
/// <typeparam name="TResponse">Response type.</typeparam>
public sealed class ValidationBehavior<TRequest, TResponse>(IEnumerable<IValidator<TRequest>> validators)
: IPipelineBehavior<TRequest, TResponse>
where TRequest : notnull
{
/// <summary>
/// Validates the request and continues the pipeline when validation succeeds.
/// </summary>
/// <param name="request">Current request instance.</param>
/// <param name="next">Next pipeline delegate.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Handler response when validation passes.</returns>
/// <exception cref="ValidationException">Thrown when one or more validation errors are found.</exception>
public async Task<TResponse> Handle(
TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken cancellationToken)
{
if (!validators.Any())
{
return await next(cancellationToken);
}
var context = new ValidationContext<TRequest>(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);
}
}

View File

@@ -0,0 +1,17 @@
using DigitalData.TaskFlow.Domain.Documents;
namespace DigitalData.TaskFlow.Application.Common.Interfaces;
/// <summary>
/// Defines document-specific data access operations.
/// </summary>
public interface IDocumentRepository : IRepository
{
/// <summary>
/// Gets document binary content for the given document identifier.
/// </summary>
/// <param name="id">Document identifier.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Binary document payload.</returns>
Task<DocumentBinaryResult> GetAsync(string id, CancellationToken cancellationToken);
}

View File

@@ -0,0 +1,8 @@
namespace DigitalData.TaskFlow.Application.Common.Interfaces;
/// <summary>
/// Defines a marker contract for repository abstractions in the application layer.
/// </summary>
public interface IRepository
{
}

View File

@@ -0,0 +1,34 @@
using DigitalData.TaskFlow.Domain.Closing;
using DigitalData.TaskFlow.Domain.Workflows;
namespace DigitalData.TaskFlow.Application.Common.Interfaces;
/// <summary>
/// Defines workflow-specific data access operations for reading and closing workflow items.
/// </summary>
public interface IWorkflowRepository : IRepository
{
/// <summary>
/// Gets grouped workflow data for the given WISAG user identifier.
/// </summary>
/// <param name="wisagId">WISAG user identifier.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Workflow data grouped by source.</returns>
Task<WorkflowSourcesResponse> GetAsync(int wisagId, CancellationToken cancellationToken);
/// <summary>
/// Applies a closing decision to a workflow process.
/// </summary>
/// <param name="wisagId">WISAG user identifier.</param>
/// <param name="source">Source system name.</param>
/// <param name="processId">Process identifier.</param>
/// <param name="decision">Closing decision payload.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Closing operation result.</returns>
Task<CloseWorkflowResult> CloseAsync(
int wisagId,
string source,
int processId,
CloseWorkflowDecision decision,
CancellationToken cancellationToken);
}

View File

@@ -0,0 +1,16 @@
using AutoMapper;
namespace DigitalData.TaskFlow.Application.Common.Mappings;
/// <summary>
/// Defines AutoMapper configuration for application-level mappings.
/// </summary>
public sealed class ApplicationMappingProfile : Profile
{
/// <summary>
/// Initializes mapping configuration.
/// </summary>
public ApplicationMappingProfile()
{
}
}

View File

@@ -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;