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
+
+