diff --git a/web/backend/API/API.Core/API.Core.csproj b/web/backend/API/API.Core/API.Core.csproj index aab193e..35541dc 100644 --- a/web/backend/API/API.Core/API.Core.csproj +++ b/web/backend/API/API.Core/API.Core.csproj @@ -17,6 +17,7 @@ Include="FluentValidation.AspNetCore" Version="11.3.0" /> + @@ -33,6 +34,8 @@ + + diff --git a/web/backend/API/API.Core/Authentication/JwtAuthenticationHandler.cs b/web/backend/API/API.Core/Authentication/JwtAuthenticationHandler.cs index 0072a84..90c5756 100644 --- a/web/backend/API/API.Core/Authentication/JwtAuthenticationHandler.cs +++ b/web/backend/API/API.Core/Authentication/JwtAuthenticationHandler.cs @@ -1,7 +1,7 @@ using System.Security.Claims; using System.Text.Encodings.Web; using System.Text.Json; -using API.Core.Contracts.Common; +using Shared.Contracts; using Infrastructure.Jwt; using Microsoft.AspNetCore.Authentication; using Microsoft.Extensions.Options; diff --git a/web/backend/API/API.Core/Contracts/Auth/Login.cs b/web/backend/API/API.Core/Contracts/Auth/Login.cs index 4d9a93f..ce64c9a 100644 --- a/web/backend/API/API.Core/Contracts/Auth/Login.cs +++ b/web/backend/API/API.Core/Contracts/Auth/Login.cs @@ -1,4 +1,4 @@ -using API.Core.Contracts.Common; +using Shared.Contracts; using FluentValidation; namespace API.Core.Contracts.Auth; diff --git a/web/backend/API/API.Core/Contracts/Auth/Register.cs b/web/backend/API/API.Core/Contracts/Auth/Register.cs index 26ea2d3..a1407a2 100644 --- a/web/backend/API/API.Core/Contracts/Auth/Register.cs +++ b/web/backend/API/API.Core/Contracts/Auth/Register.cs @@ -1,4 +1,4 @@ -using API.Core.Contracts.Common; +using Shared.Contracts; using FluentValidation; namespace API.Core.Contracts.Auth; diff --git a/web/backend/API/API.Core/Controllers/AuthController.cs b/web/backend/API/API.Core/Controllers/AuthController.cs index 245f60f..58d7fdb 100644 --- a/web/backend/API/API.Core/Controllers/AuthController.cs +++ b/web/backend/API/API.Core/Controllers/AuthController.cs @@ -1,5 +1,5 @@ using API.Core.Contracts.Auth; -using API.Core.Contracts.Common; +using Shared.Contracts; using Domain.Entities; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; diff --git a/web/backend/API/API.Core/Controllers/BreweryController.cs b/web/backend/API/API.Core/Controllers/BreweryController.cs index 6403027..d95074f 100644 --- a/web/backend/API/API.Core/Controllers/BreweryController.cs +++ b/web/backend/API/API.Core/Controllers/BreweryController.cs @@ -1,5 +1,5 @@ using API.Core.Contracts.Breweries; -using API.Core.Contracts.Common; +using Shared.Contracts; using Domain.Entities; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; diff --git a/web/backend/API/API.Core/Controllers/ProtectedController.cs b/web/backend/API/API.Core/Controllers/ProtectedController.cs index fb7e4f2..4f1be4f 100644 --- a/web/backend/API/API.Core/Controllers/ProtectedController.cs +++ b/web/backend/API/API.Core/Controllers/ProtectedController.cs @@ -1,5 +1,5 @@ using System.Security.Claims; -using API.Core.Contracts.Common; +using Shared.Contracts; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; diff --git a/web/backend/API/API.Core/GlobalException.cs b/web/backend/API/API.Core/GlobalException.cs index 71b01bc..4af77ad 100644 --- a/web/backend/API/API.Core/GlobalException.cs +++ b/web/backend/API/API.Core/GlobalException.cs @@ -1,6 +1,6 @@ // API.Core/Filters/GlobalExceptionFilter.cs -using API.Core.Contracts.Common; +using Shared.Contracts; using Domain.Exceptions; using FluentValidation; using Microsoft.Data.SqlClient; diff --git a/web/backend/API/API.Core/Program.cs b/web/backend/API/API.Core/Program.cs index 09587b7..0106454 100644 --- a/web/backend/API/API.Core/Program.cs +++ b/web/backend/API/API.Core/Program.cs @@ -14,6 +14,7 @@ using Service.Auth; using Service.Breweries; using Service.Emails; using Service.UserManagement.User; +using Shared.Application.Behaviors; var builder = WebApplication.CreateBuilder(args); @@ -31,6 +32,14 @@ builder.Services.AddOpenApi(); builder.Services.AddValidatorsFromAssemblyContaining(); builder.Services.AddFluentValidationAutoValidation(); +// Add MediatR. Each Features.* slice's assembly is registered here as it's introduced; +// ValidationBehavior runs FluentValidation validators in the MediatR pipeline for command/query handlers. +builder.Services.AddMediatR(cfg => +{ + cfg.RegisterServicesFromAssemblyContaining(); + cfg.AddOpenBehavior(typeof(ValidationBehavior<,>)); +}); + // Add health checks builder.Services.AddHealthChecks(); diff --git a/web/backend/Core.slnx b/web/backend/Core.slnx index 6add115..860f7f7 100644 --- a/web/backend/Core.slnx +++ b/web/backend/Core.slnx @@ -29,4 +29,8 @@ + + + + diff --git a/web/backend/Shared/Shared.Application/Behaviors/ValidationBehavior.cs b/web/backend/Shared/Shared.Application/Behaviors/ValidationBehavior.cs new file mode 100644 index 0000000..305d057 --- /dev/null +++ b/web/backend/Shared/Shared.Application/Behaviors/ValidationBehavior.cs @@ -0,0 +1,44 @@ +using FluentValidation; +using MediatR; + +namespace Shared.Application.Behaviors; + +/// +/// MediatR pipeline behavior that runs all registered FluentValidation validators for a +/// request before invoking its handler, short-circuiting with a +/// when any validator reports failures. +/// +public class ValidationBehavior(IEnumerable> validators) + : IPipelineBehavior + where TRequest : IRequest +{ + public async Task Handle( + TRequest request, + RequestHandlerDelegate next, + CancellationToken cancellationToken + ) + { + if (!validators.Any()) + { + return await next(); + } + + var context = new ValidationContext(request); + + var failures = ( + await Task.WhenAll( + validators.Select(v => v.ValidateAsync(context, cancellationToken)) + ) + ) + .SelectMany(result => result.Errors) + .Where(failure => failure != null) + .ToList(); + + if (failures.Count > 0) + { + throw new ValidationException(failures); + } + + return await next(); + } +} diff --git a/web/backend/Shared/Shared.Application/Emails/SendRegistrationEmailCommand.cs b/web/backend/Shared/Shared.Application/Emails/SendRegistrationEmailCommand.cs new file mode 100644 index 0000000..8a6c86a --- /dev/null +++ b/web/backend/Shared/Shared.Application/Emails/SendRegistrationEmailCommand.cs @@ -0,0 +1,13 @@ +using MediatR; + +namespace Shared.Application.Emails; + +/// +/// Cross-slice command sent by Features.Auth and handled by Features.Emails to dispatch a +/// registration confirmation email, without either slice taking a project reference on the other. +/// +public record SendRegistrationEmailCommand( + string FirstName, + string Email, + string ConfirmationToken +) : IRequest; diff --git a/web/backend/Shared/Shared.Application/Emails/SendResendConfirmationEmailCommand.cs b/web/backend/Shared/Shared.Application/Emails/SendResendConfirmationEmailCommand.cs new file mode 100644 index 0000000..e4d2ba4 --- /dev/null +++ b/web/backend/Shared/Shared.Application/Emails/SendResendConfirmationEmailCommand.cs @@ -0,0 +1,13 @@ +using MediatR; + +namespace Shared.Application.Emails; + +/// +/// Cross-slice command sent by Features.Auth and handled by Features.Emails to dispatch a +/// fresh confirmation-link email, without either slice taking a project reference on the other. +/// +public record SendResendConfirmationEmailCommand( + string FirstName, + string Email, + string ConfirmationToken +) : IRequest; diff --git a/web/backend/Shared/Shared.Application/Shared.Application.csproj b/web/backend/Shared/Shared.Application/Shared.Application.csproj new file mode 100644 index 0000000..e03dc4e --- /dev/null +++ b/web/backend/Shared/Shared.Application/Shared.Application.csproj @@ -0,0 +1,13 @@ + + + net10.0 + enable + enable + Shared.Application + + + + + + + diff --git a/web/backend/API/API.Core/Contracts/Common/ResponseBody.cs b/web/backend/Shared/Shared.Contracts/ResponseBody.cs similarity index 95% rename from web/backend/API/API.Core/Contracts/Common/ResponseBody.cs rename to web/backend/Shared/Shared.Contracts/ResponseBody.cs index e0c6935..080aca2 100644 --- a/web/backend/API/API.Core/Contracts/Common/ResponseBody.cs +++ b/web/backend/Shared/Shared.Contracts/ResponseBody.cs @@ -1,4 +1,4 @@ -namespace API.Core.Contracts.Common; +namespace Shared.Contracts; /// /// Generic envelope used to wrap API responses that carry a data payload alongside a human-readable message. diff --git a/web/backend/Shared/Shared.Contracts/Shared.Contracts.csproj b/web/backend/Shared/Shared.Contracts/Shared.Contracts.csproj new file mode 100644 index 0000000..7401600 --- /dev/null +++ b/web/backend/Shared/Shared.Contracts/Shared.Contracts.csproj @@ -0,0 +1,8 @@ + + + net10.0 + enable + enable + Shared.Contracts + +