code style cleanup

This commit is contained in:
Aaron Po
2026-06-20 13:55:17 -04:00
parent 0c3b0e99e8
commit 5b882ac51c
167 changed files with 3711 additions and 3522 deletions

View File

@@ -4,7 +4,7 @@ using MediatR;
namespace Features.Breweries.Commands.CreateBrewery;
/// <summary>
/// Location data required to create a new brewery post, supplied as part of <see cref="CreateBreweryCommand"/>.
/// Location data required to create a new brewery post, supplied as part of <see cref="CreateBreweryCommand" />.
/// </summary>
public record CreateBreweryLocation(
Guid CityId,
@@ -15,11 +15,11 @@ public record CreateBreweryLocation(
);
/// <summary>
/// Creates a new brewery post. Bound directly from the request body of <c>POST /api/brewery</c>.
/// 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>;
) : IRequest<BreweryDto>;

View File

@@ -6,21 +6,21 @@ using MediatR;
namespace Features.Breweries.Commands.CreateBrewery;
/// <summary>
/// Handles <see cref="CreateBreweryCommand"/> by persisting a new brewery post and its location.
/// 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.
/// 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
BreweryPost entity = new()
{
BreweryPostId = Guid.NewGuid(),
PostedById = request.PostedById,
@@ -34,11 +34,11 @@ public class CreateBreweryHandler(IBreweryRepository repository)
AddressLine1 = request.Location.AddressLine1,
AddressLine2 = request.Location.AddressLine2,
PostalCode = request.Location.PostalCode,
Coordinates = request.Location.Coordinates,
},
Coordinates = request.Location.Coordinates
}
};
await repository.CreateAsync(entity);
return entity.ToDto();
}
}
}

View File

@@ -3,15 +3,15 @@ using FluentValidation;
namespace Features.Breweries.Commands.CreateBrewery;
/// <summary>
/// Validates <see cref="CreateBreweryCommand"/> instances before they are processed.
/// 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.
/// 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()
{
@@ -56,4 +56,4 @@ public class CreateBreweryValidator : AbstractValidator<CreateBreweryCommand>
.When(x => x.Location is not null)
.WithMessage("Postal code cannot exceed 20 characters.");
}
}
}

View File

@@ -3,6 +3,6 @@ using MediatR;
namespace Features.Breweries.Commands.DeleteBrewery;
/// <summary>
/// Deletes a brewery post by its unique identifier.
/// Deletes a brewery post by its unique identifier.
/// </summary>
public record DeleteBreweryCommand(Guid BreweryPostId) : IRequest;
public record DeleteBreweryCommand(Guid BreweryPostId) : IRequest;

View File

@@ -4,12 +4,14 @@ using MediatR;
namespace Features.Breweries.Commands.DeleteBrewery;
/// <summary>
/// Handles <see cref="DeleteBreweryCommand"/> by deleting the matching brewery post.
/// 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);
}
public Task Handle(DeleteBreweryCommand request, CancellationToken cancellationToken)
{
return repository.DeleteAsync(request.BreweryPostId);
}
}

View File

@@ -4,7 +4,7 @@ using MediatR;
namespace Features.Breweries.Commands.UpdateBrewery;
/// <summary>
/// Location data for an existing brewery post, supplied as part of <see cref="UpdateBreweryCommand"/>.
/// Location data for an existing brewery post, supplied as part of <see cref="UpdateBreweryCommand" />.
/// </summary>
public record UpdateBreweryLocation(
Guid BreweryPostLocationId,
@@ -16,8 +16,8 @@ public record UpdateBreweryLocation(
);
/// <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.
/// 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,
@@ -25,4 +25,4 @@ public record UpdateBreweryCommand(
string BreweryName,
string Description,
UpdateBreweryLocation? Location
) : IRequest<BreweryDto>;
) : IRequest<BreweryDto>;

View File

@@ -6,41 +6,43 @@ using MediatR;
namespace Features.Breweries.Commands.UpdateBrewery;
/// <summary>
/// Handles <see cref="UpdateBreweryCommand"/> by persisting changes to an existing brewery post.
/// 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.
/// 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
BreweryPost entity = new()
{
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,
},
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();
}
}
}

View File

@@ -12,11 +12,11 @@ using Shared.Contracts;
namespace Features.Breweries.Controllers;
/// <summary>
/// Provides CRUD endpoints for managing brewery posts.
/// Provides CRUD endpoints for managing brewery posts.
/// </summary>
/// <remarks>
/// The controller is decorated with <c>[Authorize(AuthenticationSchemes = "JWT")]</c> by default; read endpoints
/// (<see cref="GetById"/> and <see cref="GetAll"/>) opt out via <c>[AllowAnonymous]</c>.
/// The controller is decorated with <c>[Authorize(AuthenticationSchemes = "JWT")]</c> by default; read endpoints
/// (<see cref="GetById" /> and <see cref="GetAll" />) opt out via <c>[AllowAnonymous]</c>.
/// </remarks>
/// <param name="mediator">Used to dispatch brewery commands and queries to their handlers.</param>
[ApiController]
@@ -25,75 +25,84 @@ namespace Features.Breweries.Controllers;
public class BreweryController(IMediator mediator) : ControllerBase
{
/// <summary>
/// Retrieves a single brewery post by its unique identifier.
/// Retrieves a single brewery post by its unique identifier.
/// </summary>
/// <remarks>Allows anonymous access.</remarks>
/// <param name="id">The unique identifier of the brewery post to retrieve.</param>
/// <returns>
/// A <c>200 OK</c> result wrapping a <see cref="ResponseBody{T}"/> of <see cref="BreweryDto"/> if found;
/// otherwise a <c>404 Not Found</c> result wrapping a <see cref="ResponseBody"/> error message.
/// A <c>200 OK</c> result wrapping a <see cref="ResponseBody{T}" /> of <see cref="BreweryDto" /> if found;
/// otherwise a <c>404 Not Found</c> result wrapping a <see cref="ResponseBody" /> error message.
/// </returns>
[AllowAnonymous]
[HttpGet("{id:guid}")]
public async Task<ActionResult<ResponseBody<BreweryDto>>> GetById(Guid id)
{
var brewery = await mediator.Send(new GetBreweryByIdQuery(id));
BreweryDto? brewery = await mediator.Send(new GetBreweryByIdQuery(id));
if (brewery is null)
return NotFound(new ResponseBody { Message = $"Brewery with ID {id} not found." });
return Ok(new ResponseBody<BreweryDto>
{
Message = "Brewery retrieved successfully.",
Payload = brewery,
Payload = brewery
});
}
/// <summary>
/// Retrieves a paginated list of brewery posts.
/// Retrieves a paginated list of brewery posts.
/// </summary>
/// <remarks>Allows anonymous access.</remarks>
/// <param name="limit">The maximum number of brewery posts to return, or <c>null</c> for no limit.</param>
/// <param name="offset">The number of brewery posts to skip before returning results, or <c>null</c> for no offset.</param>
/// <returns>A <c>200 OK</c> result wrapping a <see cref="ResponseBody{T}"/> of a collection of <see cref="BreweryDto"/>.</returns>
/// <returns>A <c>200 OK</c> result wrapping a <see cref="ResponseBody{T}" /> of a collection of <see cref="BreweryDto" />.</returns>
[AllowAnonymous]
[HttpGet]
public async Task<ActionResult<ResponseBody<IEnumerable<BreweryDto>>>> GetAll(
[FromQuery] int? limit,
[FromQuery] int? offset)
{
var breweries = await mediator.Send(new GetAllBreweriesQuery(limit, offset));
IEnumerable<BreweryDto> breweries = await mediator.Send(new GetAllBreweriesQuery(limit, offset));
return Ok(new ResponseBody<IEnumerable<BreweryDto>>
{
Message = "Breweries retrieved successfully.",
Payload = breweries,
Payload = breweries
});
}
/// <summary>
/// Creates a new brewery post.
/// Creates a new brewery post.
/// </summary>
/// <param name="command">The brewery details to create, including the posting user, name, description, and location.</param>
/// <returns>A <c>201 Created</c> result wrapping a <see cref="ResponseBody{T}"/> of the newly created <see cref="BreweryDto"/>.</returns>
/// <returns>
/// A <c>201 Created</c> result wrapping a <see cref="ResponseBody{T}" /> of the newly created
/// <see cref="BreweryDto" />.
/// </returns>
[HttpPost]
public async Task<ActionResult<ResponseBody<BreweryDto>>> Create([FromBody] CreateBreweryCommand command)
{
var brewery = await mediator.Send(command);
BreweryDto brewery = await mediator.Send(command);
return Created($"/api/brewery/{brewery.BreweryPostId}", new ResponseBody<BreweryDto>
{
Message = "Brewery created successfully.",
Payload = brewery,
Payload = brewery
});
}
/// <summary>
/// Updates an existing brewery post.
/// Updates an existing brewery post.
/// </summary>
/// <param name="id">The unique identifier of the brewery post from the route, which must match <paramref name="command"/>'s ID.</param>
/// <param name="command">The updated brewery details, including the posting user, name, description, and optional location.</param>
/// <param name="id">
/// The unique identifier of the brewery post from the route, which must match <paramref name="command" />
/// 's ID.
/// </param>
/// <param name="command">
/// The updated brewery details, including the posting user, name, description, and optional
/// location.
/// </param>
/// <returns>
/// A <c>200 OK</c> result wrapping a <see cref="ResponseBody{T}"/> of the updated <see cref="BreweryDto"/> if the
/// update succeeds; otherwise a <c>400 Bad Request</c> result wrapping a <see cref="ResponseBody"/> error message
/// when the route ID does not match the payload ID.
/// A <c>200 OK</c> result wrapping a <see cref="ResponseBody{T}" /> of the updated <see cref="BreweryDto" /> if the
/// update succeeds; otherwise a <c>400 Bad Request</c> result wrapping a <see cref="ResponseBody" /> error message
/// when the route ID does not match the payload ID.
/// </returns>
[HttpPut("{id:guid}")]
public async Task<ActionResult<ResponseBody<BreweryDto>>> Update(Guid id, [FromBody] UpdateBreweryCommand command)
@@ -101,23 +110,23 @@ public class BreweryController(IMediator mediator) : ControllerBase
if (command.BreweryPostId != id)
return BadRequest(new ResponseBody { Message = "Route ID does not match payload ID." });
var brewery = await mediator.Send(command);
BreweryDto brewery = await mediator.Send(command);
return Ok(new ResponseBody<BreweryDto>
{
Message = "Brewery updated successfully.",
Payload = brewery,
Payload = brewery
});
}
/// <summary>
/// Deletes a brewery post.
/// Deletes a brewery post.
/// </summary>
/// <param name="id">The unique identifier of the brewery post to delete.</param>
/// <returns>A <c>200 OK</c> result wrapping a <see cref="ResponseBody"/> confirming the deletion.</returns>
/// <returns>A <c>200 OK</c> result wrapping a <see cref="ResponseBody" /> confirming the deletion.</returns>
[HttpDelete("{id:guid}")]
public async Task<ActionResult<ResponseBody>> Delete(Guid id)
{
await mediator.Send(new DeleteBreweryCommand(id));
return Ok(new ResponseBody { Message = "Brewery deleted successfully." });
}
}
}

View File

@@ -4,7 +4,7 @@ using Microsoft.Extensions.DependencyInjection;
namespace Features.Breweries.DependencyInjection;
/// <summary>
/// Registers the services owned by the Breweries feature slice.
/// Registers the services owned by the Breweries feature slice.
/// </summary>
public static class FeaturesBreweriesServiceCollectionExtensions
{
@@ -13,4 +13,4 @@ public static class FeaturesBreweriesServiceCollectionExtensions
services.AddScoped<IBreweryRepository, BreweryRepository>();
return services;
}
}
}

View File

@@ -1,89 +1,89 @@
namespace Features.Breweries.Dtos;
/// <summary>
/// Represents the location details of an existing brewery, as returned in <see cref="BreweryDto"/>.
/// Represents the location details of an existing brewery, as returned in <see cref="BreweryDto" />.
/// </summary>
public class BreweryLocationDto
{
/// <summary>
/// The unique identifier of the brewery's location record.
/// The unique identifier of the brewery's location record.
/// </summary>
public Guid BreweryPostLocationId { get; set; }
/// <summary>
/// The unique identifier of the brewery post that this location belongs to.
/// The unique identifier of the brewery post that this location belongs to.
/// </summary>
public Guid BreweryPostId { get; set; }
/// <summary>
/// The unique identifier of the city in which the brewery is located.
/// The unique identifier of the city in which the brewery is located.
/// </summary>
public Guid CityId { get; set; }
/// <summary>
/// The primary street address line of the brewery.
/// The primary street address line of the brewery.
/// </summary>
public string AddressLine1 { get; set; } = string.Empty;
/// <summary>
/// An optional secondary address line (e.g. suite or unit number).
/// An optional secondary address line (e.g. suite or unit number).
/// </summary>
public string? AddressLine2 { get; set; }
/// <summary>
/// The postal/ZIP code of the brewery's address.
/// The postal/ZIP code of the brewery's address.
/// </summary>
public string PostalCode { get; set; } = string.Empty;
/// <summary>
/// The optional geographic coordinates of the brewery, in a raw binary representation.
/// The optional geographic coordinates of the brewery, in a raw binary representation.
/// </summary>
public byte[]? Coordinates { get; set; }
}
/// <summary>
/// Represents a brewery post as returned by the brewery endpoints, including its metadata and
/// optional location.
/// Represents a brewery post as returned by the brewery endpoints, including its metadata and
/// optional location.
/// </summary>
public class BreweryDto
{
/// <summary>
/// The unique identifier of the brewery post.
/// The unique identifier of the brewery post.
/// </summary>
public Guid BreweryPostId { get; set; }
/// <summary>
/// The unique identifier of the user account that created the brewery post.
/// The unique identifier of the user account that created the brewery post.
/// </summary>
public Guid PostedById { get; set; }
/// <summary>
/// The name of the brewery.
/// The name of the brewery.
/// </summary>
public string BreweryName { get; set; } = string.Empty;
/// <summary>
/// A description of the brewery.
/// A description of the brewery.
/// </summary>
public string Description { get; set; } = string.Empty;
/// <summary>
/// The date and time at which the brewery post was created.
/// The date and time at which the brewery post was created.
/// </summary>
public DateTime CreatedAt { get; set; }
/// <summary>
/// The date and time at which the brewery post was last updated, or <c>null</c> if it has never been updated.
/// The date and time at which the brewery post was last updated, or <c>null</c> if it has never been updated.
/// </summary>
public DateTime? UpdatedAt { get; set; }
/// <summary>
/// A row-version/concurrency token used to detect conflicting concurrent updates to the brewery post.
/// A row-version/concurrency token used to detect conflicting concurrent updates to the brewery post.
/// </summary>
public byte[]? Timer { get; set; }
/// <summary>
/// The location details of the brewery, or <c>null</c> if no location is associated with it.
/// The location details of the brewery, or <c>null</c> if no location is associated with it.
/// </summary>
public BreweryLocationDto? Location { get; set; }
}
}

View File

@@ -3,34 +3,39 @@ using Domain.Entities;
namespace Features.Breweries.Dtos;
/// <summary>
/// Maps <see cref="BreweryPost"/> domain entities to their <see cref="BreweryDto"/> wire representation.
/// Maps <see cref="BreweryPost" /> domain entities to their <see cref="BreweryDto" /> wire representation.
/// </summary>
public static class BreweryDtoMapper
{
/// <summary>
/// Maps a <see cref="BreweryPost"/> domain entity to its <see cref="BreweryDto"/> representation,
/// including its location, if present.
/// Maps a <see cref="BreweryPost" /> domain entity to its <see cref="BreweryDto" /> representation,
/// including its location, if present.
/// </summary>
/// <param name="brewery">The brewery post entity to map.</param>
/// <returns>The mapped <see cref="BreweryDto"/>.</returns>
public static BreweryDto ToDto(this BreweryPost brewery) => new()
/// <returns>The mapped <see cref="BreweryDto" />.</returns>
public static BreweryDto ToDto(this BreweryPost brewery)
{
BreweryPostId = brewery.BreweryPostId,
PostedById = brewery.PostedById,
BreweryName = brewery.BreweryName,
Description = brewery.Description,
CreatedAt = brewery.CreatedAt,
UpdatedAt = brewery.UpdatedAt,
Timer = brewery.Timer,
Location = brewery.Location is null ? null : new BreweryLocationDto
return new BreweryDto
{
BreweryPostLocationId = brewery.Location.BreweryPostLocationId,
BreweryPostId = brewery.Location.BreweryPostId,
CityId = brewery.Location.CityId,
AddressLine1 = brewery.Location.AddressLine1,
AddressLine2 = brewery.Location.AddressLine2,
PostalCode = brewery.Location.PostalCode,
Coordinates = brewery.Location.Coordinates,
},
};
}
BreweryPostId = brewery.BreweryPostId,
PostedById = brewery.PostedById,
BreweryName = brewery.BreweryName,
Description = brewery.Description,
CreatedAt = brewery.CreatedAt,
UpdatedAt = brewery.UpdatedAt,
Timer = brewery.Timer,
Location = brewery.Location is null
? null
: new BreweryLocationDto
{
BreweryPostLocationId = brewery.Location.BreweryPostLocationId,
BreweryPostId = brewery.Location.BreweryPostId,
CityId = brewery.Location.CityId,
AddressLine1 = brewery.Location.AddressLine1,
AddressLine2 = brewery.Location.AddressLine2,
PostalCode = brewery.Location.PostalCode,
Coordinates = brewery.Location.Coordinates
}
};
}
}

View File

@@ -1,19 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>Features.Breweries</RootNamespace>
</PropertyGroup>
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>Features.Breweries</RootNamespace>
</PropertyGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Domain\Domain.Entities\Domain.Entities.csproj" />
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Sql\Infrastructure.Sql.csproj" />
<ProjectReference Include="..\..\Shared\Shared.Contracts\Shared.Contracts.csproj" />
<ProjectReference Include="..\..\Shared\Shared.Application\Shared.Application.csproj" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Domain\Domain.Entities\Domain.Entities.csproj"/>
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Sql\Infrastructure.Sql.csproj"/>
<ProjectReference Include="..\..\Shared\Shared.Contracts\Shared.Contracts.csproj"/>
<ProjectReference Include="..\..\Shared\Shared.Application\Shared.Application.csproj"/>
</ItemGroup>
</Project>

View File

@@ -1,3 +1,4 @@
using Domain.Entities;
using Features.Breweries.Dtos;
using Features.Breweries.Repository;
using MediatR;
@@ -5,7 +6,7 @@ using MediatR;
namespace Features.Breweries.Queries.GetAllBreweries;
/// <summary>
/// Handles <see cref="GetAllBreweriesQuery"/> by retrieving a paginated list of brewery posts.
/// Handles <see cref="GetAllBreweriesQuery" /> by retrieving a paginated list of brewery posts.
/// </summary>
/// <param name="repository">Repository used to query brewery post data.</param>
public class GetAllBreweriesHandler(IBreweryRepository repository)
@@ -13,7 +14,7 @@ public class GetAllBreweriesHandler(IBreweryRepository repository)
{
public async Task<IEnumerable<BreweryDto>> Handle(GetAllBreweriesQuery request, CancellationToken cancellationToken)
{
var breweries = await repository.GetAllAsync(request.Limit, request.Offset);
IEnumerable<BreweryPost> breweries = await repository.GetAllAsync(request.Limit, request.Offset);
return breweries.Select(b => b.ToDto());
}
}
}

View File

@@ -4,6 +4,6 @@ using MediatR;
namespace Features.Breweries.Queries.GetAllBreweries;
/// <summary>
/// Retrieves a paginated list of brewery posts.
/// Retrieves a paginated list of brewery posts.
/// </summary>
public record GetAllBreweriesQuery(int? Limit, int? Offset) : IRequest<IEnumerable<BreweryDto>>;
public record GetAllBreweriesQuery(int? Limit, int? Offset) : IRequest<IEnumerable<BreweryDto>>;

View File

@@ -1,3 +1,4 @@
using Domain.Entities;
using Features.Breweries.Dtos;
using Features.Breweries.Repository;
using MediatR;
@@ -5,7 +6,7 @@ using MediatR;
namespace Features.Breweries.Queries.GetBreweryById;
/// <summary>
/// Handles <see cref="GetBreweryByIdQuery"/> by looking up the matching brewery post.
/// Handles <see cref="GetBreweryByIdQuery" /> by looking up the matching brewery post.
/// </summary>
/// <param name="repository">Repository used to query brewery post data.</param>
public class GetBreweryByIdHandler(IBreweryRepository repository)
@@ -13,7 +14,7 @@ public class GetBreweryByIdHandler(IBreweryRepository repository)
{
public async Task<BreweryDto?> Handle(GetBreweryByIdQuery request, CancellationToken cancellationToken)
{
var brewery = await repository.GetByIdAsync(request.BreweryPostId);
BreweryPost? brewery = await repository.GetByIdAsync(request.BreweryPostId);
return brewery?.ToDto();
}
}
}

View File

@@ -4,6 +4,6 @@ using MediatR;
namespace Features.Breweries.Queries.GetBreweryById;
/// <summary>
/// Retrieves a single brewery post by its unique identifier.
/// Retrieves a single brewery post by its unique identifier.
/// </summary>
public record GetBreweryByIdQuery(Guid BreweryPostId) : IRequest<BreweryDto?>;
public record GetBreweryByIdQuery(Guid BreweryPostId) : IRequest<BreweryDto?>;

View File

@@ -1,3 +1,4 @@
using System.Data;
using System.Data.Common;
using Domain.Entities;
using Infrastructure.Sql;
@@ -5,51 +6,48 @@ using Infrastructure.Sql;
namespace Features.Breweries.Repository;
/// <summary>
/// ADO.NET-based implementation of <see cref="IBreweryRepository"/> backed by SQL Server stored
/// procedures.
/// ADO.NET-based implementation of <see cref="IBreweryRepository" /> backed by SQL Server stored
/// procedures.
/// </summary>
/// <param name="connectionFactory">The factory used to create database connections.</param>
public class BreweryRepository(ISqlConnectionFactory connectionFactory)
: Repository<BreweryPost>(connectionFactory), IBreweryRepository
{
/// <summary>
/// Retrieves a brewery post by ID using the <c>USP_GetBreweryById</c> stored procedure.
/// Retrieves a brewery post by ID using the <c>USP_GetBreweryById</c> stored procedure.
/// </summary>
/// <param name="id">The unique identifier of the brewery post.</param>
/// <returns>The matching <see cref="BreweryPost"/>, or <c>null</c> if not found.</returns>
/// <returns>The matching <see cref="BreweryPost" />, or <c>null</c> if not found.</returns>
/// <exception cref="Microsoft.Data.SqlClient.SqlException">Thrown when the database command fails.</exception>
public async Task<BreweryPost?> GetByIdAsync(Guid id)
{
await using var connection = await CreateConnection();
await using var command = connection.CreateCommand();
command.CommandType = System.Data.CommandType.StoredProcedure;
await using DbConnection connection = await CreateConnection();
await using DbCommand command = connection.CreateCommand();
command.CommandType = CommandType.StoredProcedure;
command.CommandText = "USP_GetBreweryById";
AddParameter(command, "@BreweryPostID", id);
await using var reader = await command.ExecuteReaderAsync();
if (await reader.ReadAsync())
{
return MapToEntity(reader);
}
await using DbDataReader reader = await command.ExecuteReaderAsync();
if (await reader.ReadAsync()) return MapToEntity(reader);
return null;
}
/// <summary>
/// Retrieves all brewery posts, optionally paginated, using the <c>USP_GetAllBreweries</c>
/// stored procedure. The <c>@Limit</c> and <c>@Offset</c> parameters are only added when their
/// corresponding argument has a value.
/// Retrieves all brewery posts, optionally paginated, using the <c>USP_GetAllBreweries</c>
/// stored procedure. The <c>@Limit</c> and <c>@Offset</c> parameters are only added when their
/// corresponding argument has a value.
/// </summary>
/// <param name="limit">The maximum number of records to return, or <c>null</c> for no limit.</param>
/// <param name="offset">The number of records to skip, or <c>null</c> for no offset.</param>
/// <returns>The collection of matching <see cref="BreweryPost"/> records.</returns>
/// <returns>The collection of matching <see cref="BreweryPost" /> records.</returns>
/// <exception cref="Microsoft.Data.SqlClient.SqlException">Thrown when the database command fails.</exception>
public async Task<IEnumerable<BreweryPost>> GetAllAsync(int? limit, int? offset)
{
await using var connection = await CreateConnection();
await using var command = connection.CreateCommand();
await using DbConnection connection = await CreateConnection();
await using DbCommand command = connection.CreateCommand();
command.CommandText = "USP_GetAllBreweries";
command.CommandType = System.Data.CommandType.StoredProcedure;
command.CommandType = CommandType.StoredProcedure;
if (limit.HasValue)
AddParameter(command, "@Limit", limit.Value);
@@ -57,30 +55,27 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory)
if (offset.HasValue)
AddParameter(command, "@Offset", offset.Value);
await using var reader = await command.ExecuteReaderAsync();
var breweries = new List<BreweryPost>();
await using DbDataReader reader = await command.ExecuteReaderAsync();
List<BreweryPost> breweries = new();
while (await reader.ReadAsync())
{
breweries.Add(MapToEntity(reader));
}
while (await reader.ReadAsync()) breweries.Add(MapToEntity(reader));
return breweries;
}
/// <summary>
/// Updates a brewery post's name and description, and upserts or clears its location, using the
/// <c>USP_UpdateBrewery</c> stored procedure. When <paramref name="brewery"/>.<c>Location</c> is
/// <c>null</c>, any existing location for the brewery is removed.
/// Updates a brewery post's name and description, and upserts or clears its location, using the
/// <c>USP_UpdateBrewery</c> stored procedure. When <paramref name="brewery" />.<c>Location</c> is
/// <c>null</c>, any existing location for the brewery is removed.
/// </summary>
/// <param name="brewery">The brewery post containing updated values.</param>
/// <exception cref="Microsoft.Data.SqlClient.SqlException">Thrown when the database command fails.</exception>
public async Task UpdateAsync(BreweryPost brewery)
{
await using var connection = await CreateConnection();
await using var command = connection.CreateCommand();
await using DbConnection connection = await CreateConnection();
await using DbCommand command = connection.CreateCommand();
command.CommandText = "USP_UpdateBrewery";
command.CommandType = System.Data.CommandType.StoredProcedure;
command.CommandType = CommandType.StoredProcedure;
AddParameter(command, "@BreweryPostID", brewery.BreweryPostId);
AddParameter(command, "@BreweryName", brewery.BreweryName);
@@ -96,40 +91,37 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory)
}
/// <summary>
/// Deletes a brewery post by ID using the <c>USP_DeleteBrewery</c> stored procedure. Its location
/// and photos are removed via cascading foreign keys.
/// Deletes a brewery post by ID using the <c>USP_DeleteBrewery</c> stored procedure. Its location
/// and photos are removed via cascading foreign keys.
/// </summary>
/// <param name="id">The unique identifier of the brewery post to delete.</param>
/// <exception cref="Microsoft.Data.SqlClient.SqlException">Thrown when the database command fails.</exception>
public async Task DeleteAsync(Guid id)
{
await using var connection = await CreateConnection();
await using var command = connection.CreateCommand();
await using DbConnection connection = await CreateConnection();
await using DbCommand command = connection.CreateCommand();
command.CommandText = "USP_DeleteBrewery";
command.CommandType = System.Data.CommandType.StoredProcedure;
command.CommandType = CommandType.StoredProcedure;
AddParameter(command, "@BreweryPostID", id);
await command.ExecuteNonQueryAsync();
}
/// <summary>
/// Creates a new brewery post and its location using the <c>USP_CreateBrewery</c> stored procedure.
/// Creates a new brewery post and its location using the <c>USP_CreateBrewery</c> stored procedure.
/// </summary>
/// <param name="brewery">The brewery post to create. Must have a non-null <c>Location</c>.</param>
/// <exception cref="ArgumentException">Thrown when <paramref name="brewery"/>.<c>Location</c> is <c>null</c>.</exception>
/// <exception cref="ArgumentException">Thrown when <paramref name="brewery" />.<c>Location</c> is <c>null</c>.</exception>
/// <exception cref="Microsoft.Data.SqlClient.SqlException">Thrown when the database command fails.</exception>
public async Task CreateAsync(BreweryPost brewery)
{
await using var connection = await CreateConnection();
await using var command = connection.CreateCommand();
await using DbConnection connection = await CreateConnection();
await using DbCommand command = connection.CreateCommand();
command.CommandText = "USP_CreateBrewery";
command.CommandType = System.Data.CommandType.StoredProcedure;
command.CommandType = CommandType.StoredProcedure;
if (brewery.Location is null)
{
throw new ArgumentException("Location must be provided when creating a brewery.");
}
if (brewery.Location is null) throw new ArgumentException("Location must be provided when creating a brewery.");
AddParameter(command, "@BreweryName", brewery.BreweryName);
AddParameter(command, "@Description", brewery.Description);
@@ -140,27 +132,26 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory)
AddParameter(command, "@PostalCode", brewery.Location?.PostalCode);
AddParameter(command, "@Coordinates", brewery.Location?.Coordinates);
await command.ExecuteNonQueryAsync();
}
/// <summary>
/// Maps the current row of a data reader to a <see cref="BreweryPost"/> entity, including its
/// rowversion <c>Timer</c> field and, if location columns are present in the result set, its
/// associated <see cref="BreweryPostLocation"/>.
/// Maps the current row of a data reader to a <see cref="BreweryPost" /> entity, including its
/// rowversion <c>Timer</c> field and, if location columns are present in the result set, its
/// associated <see cref="BreweryPostLocation" />.
/// </summary>
/// <param name="reader">The data reader positioned on the row to map.</param>
/// <returns>The mapped <see cref="BreweryPost"/> instance.</returns>
/// <returns>The mapped <see cref="BreweryPost" /> instance.</returns>
protected override BreweryPost MapToEntity(DbDataReader reader)
{
var brewery = new BreweryPost();
BreweryPost brewery = new();
var ordBreweryPostId = reader.GetOrdinal("BreweryPostId");
var ordPostedById = reader.GetOrdinal("PostedById");
var ordBreweryName = reader.GetOrdinal("BreweryName");
var ordDescription = reader.GetOrdinal("Description");
var ordCreatedAt = reader.GetOrdinal("CreatedAt");
var ordUpdatedAt = reader.GetOrdinal("UpdatedAt");
var ordTimer = reader.GetOrdinal("Timer");
int ordBreweryPostId = reader.GetOrdinal("BreweryPostId");
int ordPostedById = reader.GetOrdinal("PostedById");
int ordBreweryName = reader.GetOrdinal("BreweryName");
int ordDescription = reader.GetOrdinal("Description");
int ordCreatedAt = reader.GetOrdinal("CreatedAt");
int ordUpdatedAt = reader.GetOrdinal("UpdatedAt");
int ordTimer = reader.GetOrdinal("Timer");
brewery.BreweryPostId = reader.GetGuid(ordBreweryPostId);
brewery.PostedById = reader.GetGuid(ordPostedById);
@@ -172,39 +163,39 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory)
// Read timer (varbinary/rowversion) robustly
if (reader.IsDBNull(ordTimer))
{
brewery.Timer = null;
}
else
{
try
{
brewery.Timer = reader.GetFieldValue<byte[]>(ordTimer);
}
catch
{
var length = reader.GetBytes(ordTimer, 0, null, 0, 0);
var buffer = new byte[length];
long length = reader.GetBytes(ordTimer, 0, null, 0, 0);
byte[] buffer = new byte[length];
reader.GetBytes(ordTimer, 0, buffer, 0, (int)length);
brewery.Timer = buffer;
}
}
// Map BreweryPostLocation if columns are present
try
{
var ordLocationId = reader.GetOrdinal("BreweryPostLocationId");
int ordLocationId = reader.GetOrdinal("BreweryPostLocationId");
if (!reader.IsDBNull(ordLocationId))
{
var location = new BreweryPostLocation
BreweryPostLocation location = new()
{
BreweryPostLocationId = reader.GetGuid(ordLocationId),
BreweryPostId = reader.GetGuid(reader.GetOrdinal("BreweryPostId")),
CityId = reader.GetGuid(reader.GetOrdinal("CityId")),
AddressLine1 = reader.GetString(reader.GetOrdinal("AddressLine1")),
AddressLine2 = reader.IsDBNull(reader.GetOrdinal("AddressLine2")) ? null : reader.GetString(reader.GetOrdinal("AddressLine2")),
AddressLine2 = reader.IsDBNull(reader.GetOrdinal("AddressLine2"))
? null
: reader.GetString(reader.GetOrdinal("AddressLine2")),
PostalCode = reader.GetString(reader.GetOrdinal("PostalCode")),
Coordinates = reader.IsDBNull(reader.GetOrdinal("Coordinates")) ? null : reader.GetFieldValue<byte[]>(reader.GetOrdinal("Coordinates"))
Coordinates = reader.IsDBNull(reader.GetOrdinal("Coordinates"))
? null
: reader.GetFieldValue<byte[]>(reader.GetOrdinal("Coordinates"))
};
brewery.Location = location;
}
@@ -218,21 +209,21 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory)
}
/// <summary>
/// Helper method to add a parameter to a database command, converting <c>null</c> values to
/// <see cref="DBNull.Value"/>.
/// Helper method to add a parameter to a database command, converting <c>null</c> values to
/// <see cref="DBNull.Value" />.
/// </summary>
/// <param name="command">The command to add the parameter to.</param>
/// <param name="name">The parameter name (including any prefix, e.g. "@BreweryName").</param>
/// <param name="value">The parameter value, or <c>null</c> to bind <see cref="DBNull.Value"/>.</param>
/// <param name="value">The parameter value, or <c>null</c> to bind <see cref="DBNull.Value" />.</param>
private static void AddParameter(
DbCommand command,
string name,
object? value
)
{
var p = command.CreateParameter();
DbParameter p = command.CreateParameter();
p.ParameterName = name;
p.Value = value ?? DBNull.Value;
command.Parameters.Add(p);
}
}
}

View File

@@ -3,41 +3,41 @@ using Domain.Entities;
namespace Features.Breweries.Repository;
/// <summary>
/// Repository for CRUD operations on brewery post records.
/// Repository for CRUD operations on brewery post records.
/// </summary>
public interface IBreweryRepository
{
/// <summary>
/// Retrieves a brewery post by its unique identifier.
/// Retrieves a brewery post by its unique identifier.
/// </summary>
/// <param name="id">The unique identifier of the brewery post.</param>
/// <returns>The matching <see cref="BreweryPost"/>, or <c>null</c> if not found.</returns>
/// <returns>The matching <see cref="BreweryPost" />, or <c>null</c> if not found.</returns>
Task<BreweryPost?> GetByIdAsync(Guid id);
/// <summary>
/// Retrieves all brewery posts, optionally paginated.
/// Retrieves all brewery posts, optionally paginated.
/// </summary>
/// <param name="limit">The maximum number of records to return, or <c>null</c> for no limit.</param>
/// <param name="offset">The number of records to skip, or <c>null</c> for no offset.</param>
/// <returns>The collection of matching <see cref="BreweryPost"/> records.</returns>
/// <returns>The collection of matching <see cref="BreweryPost" /> records.</returns>
Task<IEnumerable<BreweryPost>> GetAllAsync(int? limit, int? offset);
/// <summary>
/// Updates an existing brewery post.
/// Updates an existing brewery post.
/// </summary>
/// <param name="brewery">The brewery post containing updated values.</param>
Task UpdateAsync(BreweryPost brewery);
/// <summary>
/// Deletes a brewery post by its unique identifier.
/// Deletes a brewery post by its unique identifier.
/// </summary>
/// <param name="id">The unique identifier of the brewery post to delete.</param>
Task DeleteAsync(Guid id);
/// <summary>
/// Creates a new brewery post, including its location details.
/// Creates a new brewery post, including its location details.
/// </summary>
/// <param name="brewery">The brewery post to create. Must have a non-null <c>Location</c>.</param>
/// <exception cref="ArgumentException">Thrown when <paramref name="brewery"/> has no <c>Location</c>.</exception>
/// <exception cref="ArgumentException">Thrown when <paramref name="brewery" /> has no <c>Location</c>.</exception>
Task CreateAsync(BreweryPost brewery);
}
}