mirror of
https://github.com/aaronpo97/the-biergarten-app.git
synced 2026-07-16 17:47:22 +00:00
Migrate Breweries to a vertical slice (Features.Breweries)
Moves brewery CRUD from the Service.Breweries/Infrastructure.Repository layered split into a single Features.Breweries project: MediatR commands/queries replace BreweryService, and the repository moves in alongside its own controller. Extracts Infrastructure.Sql out of Infrastructure.Repository (just the generic ADO.NET connection/base-repo plumbing) since Breweries no longer needs the rest of that project, while Auth and UserAccount repos still do until their own migration. Also fixes the API.Core and Infrastructure.Repository.Tests Dockerfiles, which were restoring against COPY destinations that didn't match the projects' actual relative paths (silently working only because of the COPY . . that followed); both now restore against Core.slnx with correctly nested paths, verified with a real docker build.
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
using Features.Breweries.Dtos;
|
||||
using MediatR;
|
||||
|
||||
namespace Features.Breweries.Commands.CreateBrewery;
|
||||
|
||||
/// <summary>
|
||||
/// Location data required to create a new brewery post, supplied as part of <see cref="CreateBreweryCommand"/>.
|
||||
/// </summary>
|
||||
public record CreateBreweryLocation(
|
||||
Guid CityId,
|
||||
string AddressLine1,
|
||||
string? AddressLine2,
|
||||
string PostalCode,
|
||||
byte[]? Coordinates
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new brewery post. Bound directly from the request body of <c>POST /api/brewery</c>.
|
||||
/// </summary>
|
||||
public record CreateBreweryCommand(
|
||||
Guid PostedById,
|
||||
string BreweryName,
|
||||
string Description,
|
||||
CreateBreweryLocation Location
|
||||
) : IRequest<BreweryDto>;
|
||||
@@ -0,0 +1,44 @@
|
||||
using Domain.Entities;
|
||||
using Features.Breweries.Dtos;
|
||||
using Features.Breweries.Repository;
|
||||
using MediatR;
|
||||
|
||||
namespace Features.Breweries.Commands.CreateBrewery;
|
||||
|
||||
/// <summary>
|
||||
/// Handles <see cref="CreateBreweryCommand"/> by persisting a new brewery post and its location.
|
||||
/// </summary>
|
||||
/// <param name="repository">Repository used to persist the new brewery post.</param>
|
||||
public class CreateBreweryHandler(IBreweryRepository repository)
|
||||
: IRequestHandler<CreateBreweryCommand, BreweryDto>
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new brewery post, generating new identifiers for the post and its location.
|
||||
/// </summary>
|
||||
/// <param name="request">The details of the brewery post to create.</param>
|
||||
/// <param name="cancellationToken">A token to observe for cancellation requests.</param>
|
||||
/// <returns>The newly created brewery post.</returns>
|
||||
public async Task<BreweryDto> Handle(CreateBreweryCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var entity = new BreweryPost
|
||||
{
|
||||
BreweryPostId = Guid.NewGuid(),
|
||||
PostedById = request.PostedById,
|
||||
BreweryName = request.BreweryName,
|
||||
Description = request.Description,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
Location = new BreweryPostLocation
|
||||
{
|
||||
BreweryPostLocationId = Guid.NewGuid(),
|
||||
CityId = request.Location.CityId,
|
||||
AddressLine1 = request.Location.AddressLine1,
|
||||
AddressLine2 = request.Location.AddressLine2,
|
||||
PostalCode = request.Location.PostalCode,
|
||||
Coordinates = request.Location.Coordinates,
|
||||
},
|
||||
};
|
||||
|
||||
await repository.CreateAsync(entity);
|
||||
return entity.ToDto();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
using FluentValidation;
|
||||
|
||||
namespace Features.Breweries.Commands.CreateBrewery;
|
||||
|
||||
/// <summary>
|
||||
/// Validates <see cref="CreateBreweryCommand"/> instances before they are processed.
|
||||
/// </summary>
|
||||
public class CreateBreweryValidator : AbstractValidator<CreateBreweryCommand>
|
||||
{
|
||||
/// <summary>
|
||||
/// Configures validation rules requiring <see cref="CreateBreweryCommand.PostedById"/>,
|
||||
/// <see cref="CreateBreweryCommand.BreweryName"/>, <see cref="CreateBreweryCommand.Description"/>, and
|
||||
/// <see cref="CreateBreweryCommand.Location"/> to be present, with length limits on the name, description,
|
||||
/// address line 1, and postal code fields.
|
||||
/// </summary>
|
||||
public CreateBreweryValidator()
|
||||
{
|
||||
RuleFor(x => x.PostedById)
|
||||
.NotEmpty()
|
||||
.WithMessage("PostedById is required.");
|
||||
|
||||
RuleFor(x => x.BreweryName)
|
||||
.NotEmpty()
|
||||
.WithMessage("Brewery name is required.")
|
||||
.MaximumLength(256)
|
||||
.WithMessage("Brewery name cannot exceed 256 characters.");
|
||||
|
||||
RuleFor(x => x.Description)
|
||||
.NotEmpty()
|
||||
.WithMessage("Description is required.")
|
||||
.MaximumLength(512)
|
||||
.WithMessage("Description cannot exceed 512 characters.");
|
||||
|
||||
RuleFor(x => x.Location)
|
||||
.NotNull()
|
||||
.WithMessage("Location is required.");
|
||||
|
||||
RuleFor(x => x.Location.CityId)
|
||||
.NotEmpty()
|
||||
.When(x => x.Location is not null)
|
||||
.WithMessage("CityId is required.");
|
||||
|
||||
RuleFor(x => x.Location.AddressLine1)
|
||||
.NotEmpty()
|
||||
.When(x => x.Location is not null)
|
||||
.WithMessage("Address line 1 is required.")
|
||||
.MaximumLength(256)
|
||||
.When(x => x.Location is not null)
|
||||
.WithMessage("Address line 1 cannot exceed 256 characters.");
|
||||
|
||||
RuleFor(x => x.Location.PostalCode)
|
||||
.NotEmpty()
|
||||
.When(x => x.Location is not null)
|
||||
.WithMessage("Postal code is required.")
|
||||
.MaximumLength(20)
|
||||
.When(x => x.Location is not null)
|
||||
.WithMessage("Postal code cannot exceed 20 characters.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using MediatR;
|
||||
|
||||
namespace Features.Breweries.Commands.DeleteBrewery;
|
||||
|
||||
/// <summary>
|
||||
/// Deletes a brewery post by its unique identifier.
|
||||
/// </summary>
|
||||
public record DeleteBreweryCommand(Guid BreweryPostId) : IRequest;
|
||||
@@ -0,0 +1,15 @@
|
||||
using Features.Breweries.Repository;
|
||||
using MediatR;
|
||||
|
||||
namespace Features.Breweries.Commands.DeleteBrewery;
|
||||
|
||||
/// <summary>
|
||||
/// Handles <see cref="DeleteBreweryCommand"/> by deleting the matching brewery post.
|
||||
/// </summary>
|
||||
/// <param name="repository">Repository used to delete the brewery post.</param>
|
||||
public class DeleteBreweryHandler(IBreweryRepository repository)
|
||||
: IRequestHandler<DeleteBreweryCommand>
|
||||
{
|
||||
public Task Handle(DeleteBreweryCommand request, CancellationToken cancellationToken) =>
|
||||
repository.DeleteAsync(request.BreweryPostId);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using Features.Breweries.Dtos;
|
||||
using MediatR;
|
||||
|
||||
namespace Features.Breweries.Commands.UpdateBrewery;
|
||||
|
||||
/// <summary>
|
||||
/// Location data for an existing brewery post, supplied as part of <see cref="UpdateBreweryCommand"/>.
|
||||
/// </summary>
|
||||
public record UpdateBreweryLocation(
|
||||
Guid BreweryPostLocationId,
|
||||
Guid CityId,
|
||||
string AddressLine1,
|
||||
string? AddressLine2,
|
||||
string PostalCode,
|
||||
byte[]? Coordinates
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Updates an existing brewery post. Bound directly from the request body of <c>PUT /api/brewery/{id}</c>.
|
||||
/// A <c>null</c> <see cref="Location"/> clears the brewery's location.
|
||||
/// </summary>
|
||||
public record UpdateBreweryCommand(
|
||||
Guid BreweryPostId,
|
||||
Guid PostedById,
|
||||
string BreweryName,
|
||||
string Description,
|
||||
UpdateBreweryLocation? Location
|
||||
) : IRequest<BreweryDto>;
|
||||
@@ -0,0 +1,46 @@
|
||||
using Domain.Entities;
|
||||
using Features.Breweries.Dtos;
|
||||
using Features.Breweries.Repository;
|
||||
using MediatR;
|
||||
|
||||
namespace Features.Breweries.Commands.UpdateBrewery;
|
||||
|
||||
/// <summary>
|
||||
/// Handles <see cref="UpdateBreweryCommand"/> by persisting changes to an existing brewery post.
|
||||
/// </summary>
|
||||
/// <param name="repository">Repository used to persist the updated brewery post.</param>
|
||||
public class UpdateBreweryHandler(IBreweryRepository repository)
|
||||
: IRequestHandler<UpdateBreweryCommand, BreweryDto>
|
||||
{
|
||||
/// <summary>
|
||||
/// Updates an existing brewery post. If <paramref name="request"/> has no <c>Location</c>,
|
||||
/// the brewery's location is cleared.
|
||||
/// </summary>
|
||||
/// <param name="request">The updated details of the brewery post.</param>
|
||||
/// <param name="cancellationToken">A token to observe for cancellation requests.</param>
|
||||
/// <returns>The updated brewery post.</returns>
|
||||
public async Task<BreweryDto> Handle(UpdateBreweryCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var entity = new BreweryPost
|
||||
{
|
||||
BreweryPostId = request.BreweryPostId,
|
||||
PostedById = request.PostedById,
|
||||
BreweryName = request.BreweryName,
|
||||
Description = request.Description,
|
||||
UpdatedAt = DateTime.UtcNow,
|
||||
Location = request.Location is null ? null : new BreweryPostLocation
|
||||
{
|
||||
BreweryPostLocationId = request.Location.BreweryPostLocationId,
|
||||
BreweryPostId = request.BreweryPostId,
|
||||
CityId = request.Location.CityId,
|
||||
AddressLine1 = request.Location.AddressLine1,
|
||||
AddressLine2 = request.Location.AddressLine2,
|
||||
PostalCode = request.Location.PostalCode,
|
||||
Coordinates = request.Location.Coordinates,
|
||||
},
|
||||
};
|
||||
|
||||
await repository.UpdateAsync(entity);
|
||||
return entity.ToDto();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user