Scaffold vertical-slice migration: Shared.Contracts, Shared.Application, MediatR

Begins the move to vertical-slice architecture. ResponseBody moves out of
API.Core into Shared.Contracts so slices won't have to depend on the API
host project for it. Shared.Application adds the MediatR pipeline's
ValidationBehavior plus the cross-slice email commands that Features.Auth
will send and Features.Emails will handle, keeping slices decoupled from
each other.
This commit is contained in:
Aaron Po
2026-06-19 23:23:10 -04:00
parent 4554fff534
commit 60a7b790d4
16 changed files with 115 additions and 8 deletions

View File

@@ -17,6 +17,7 @@
Include="FluentValidation.AspNetCore"
Version="11.3.0"
/>
<PackageReference Include="MediatR" Version="12.4.1" />
</ItemGroup>
<ItemGroup>
@@ -33,6 +34,8 @@
<ProjectReference Include="..\..\Service\Service.Auth\Service.Auth.csproj" />
<ProjectReference Include="..\..\Service\Service.Breweries\Service.Breweries.csproj" />
<ProjectReference Include="..\..\Service\Service.UserManagement\Service.UserManagement.csproj" />
<ProjectReference Include="..\..\Shared\Shared.Contracts\Shared.Contracts.csproj" />
<ProjectReference Include="..\..\Shared\Shared.Application\Shared.Application.csproj" />
</ItemGroup>
<ItemGroup>

View File

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

View File

@@ -1,4 +1,4 @@
using API.Core.Contracts.Common;
using Shared.Contracts;
using FluentValidation;
namespace API.Core.Contracts.Auth;

View File

@@ -1,4 +1,4 @@
using API.Core.Contracts.Common;
using Shared.Contracts;
using FluentValidation;
namespace API.Core.Contracts.Auth;

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
using System.Security.Claims;
using API.Core.Contracts.Common;
using Shared.Contracts;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

View File

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

View File

@@ -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<Program>();
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<Program>();
cfg.AddOpenBehavior(typeof(ValidationBehavior<,>));
});
// Add health checks
builder.Services.AddHealthChecks();

View File

@@ -29,4 +29,8 @@
<Project Path="Service/Service.Auth/Service.Auth.csproj" />
<Project Path="Service/Service.Breweries/Service.Breweries.csproj" />
</Folder>
<Folder Name="/Shared/">
<Project Path="Shared/Shared.Contracts/Shared.Contracts.csproj" />
<Project Path="Shared/Shared.Application/Shared.Application.csproj" />
</Folder>
</Solution>

View File

@@ -0,0 +1,44 @@
using FluentValidation;
using MediatR;
namespace Shared.Application.Behaviors;
/// <summary>
/// MediatR pipeline behavior that runs all registered FluentValidation validators for a
/// request before invoking its handler, short-circuiting with a <see cref="ValidationException"/>
/// when any validator reports failures.
/// </summary>
public class ValidationBehavior<TRequest, TResponse>(IEnumerable<IValidator<TRequest>> validators)
: IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
public async Task<TResponse> Handle(
TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken cancellationToken
)
{
if (!validators.Any())
{
return await next();
}
var context = new ValidationContext<TRequest>(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();
}
}

View File

@@ -0,0 +1,13 @@
using MediatR;
namespace Shared.Application.Emails;
/// <summary>
/// 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.
/// </summary>
public record SendRegistrationEmailCommand(
string FirstName,
string Email,
string ConfirmationToken
) : IRequest;

View File

@@ -0,0 +1,13 @@
using MediatR;
namespace Shared.Application.Emails;
/// <summary>
/// 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.
/// </summary>
public record SendResendConfirmationEmailCommand(
string FirstName,
string Email,
string ConfirmationToken
) : IRequest;

View File

@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>Shared.Application</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MediatR" Version="12.4.1" />
<PackageReference Include="FluentValidation" Version="11.3.0" />
</ItemGroup>
</Project>

View File

@@ -1,4 +1,4 @@
namespace API.Core.Contracts.Common;
namespace Shared.Contracts;
/// <summary>
/// Generic envelope used to wrap API responses that carry a data payload alongside a human-readable message.

View File

@@ -0,0 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>Shared.Contracts</RootNamespace>
</PropertyGroup>
</Project>