update features docs

This commit is contained in:
Aaron Po
2026-08-04 06:18:30 -04:00
parent 633550f808
commit 19dfad5ab5
46 changed files with 65 additions and 671 deletions

View File

@@ -3,9 +3,6 @@ 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,

View File

@@ -5,19 +5,10 @@ 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>
/// <summary>Creates a new brewery post, generating new identifiers for the post and its location.</summary>
public async Task<BreweryDto> Handle(
CreateBreweryCommand request,
CancellationToken cancellationToken

View File

@@ -2,17 +2,8 @@ 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.");

View File

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

View File

@@ -3,10 +3,6 @@ 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>
{

View File

@@ -3,9 +3,6 @@ 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,

View File

@@ -5,10 +5,6 @@ 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>
{
@@ -16,9 +12,6 @@ public class UpdateBreweryHandler(IBreweryRepository repository)
/// 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

View File

@@ -11,28 +11,17 @@ using Shared.Contracts;
namespace Features.Breweries.Controllers;
/// <summary>
/// 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>.
/// Requires JWT authentication by default; <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]
[Route("api/[controller]")]
[Authorize(AuthenticationSchemes = "JWT")]
public class BreweryController(IMediator mediator) : ControllerBase
{
/// <summary>
/// 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.
/// </returns>
/// <returns><c>200 OK</c> with the brewery if found; otherwise <c>404 Not Found</c>.</returns>
[AllowAnonymous]
[HttpGet("{id:guid}")]
public async Task<ActionResult<ResponseBody<BreweryDto>>> GetById(Guid id)
@@ -50,13 +39,7 @@ public class BreweryController(IMediator mediator) : ControllerBase
);
}
/// <summary>
/// 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>
[AllowAnonymous]
[HttpGet]
public async Task<ActionResult<ResponseBody<IEnumerable<BreweryDto>>>> GetAll(
@@ -76,14 +59,7 @@ public class BreweryController(IMediator mediator) : ControllerBase
);
}
/// <summary>
/// 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><c>201 Created</c> with the newly created brewery.</returns>
[HttpPost]
public async Task<ActionResult<ResponseBody<BreweryDto>>> Create(
[FromBody] CreateBreweryCommand command
@@ -100,22 +76,8 @@ public class BreweryController(IMediator mediator) : ControllerBase
);
}
/// <summary>
/// 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>
/// <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.
/// </returns>
/// <param name="id">Must match <paramref name="command" />'s <c>BreweryPostId</c>.</param>
/// <returns><c>200 OK</c> with the updated brewery; or <c>400 Bad Request</c> if the route ID does not match the payload ID.</returns>
[HttpPut("{id:guid}")]
public async Task<ActionResult<ResponseBody<BreweryDto>>> Update(
Guid id,
@@ -135,11 +97,7 @@ public class BreweryController(IMediator mediator) : ControllerBase
);
}
/// <summary>
/// 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><c>200 OK</c> confirming the deletion.</returns>
[HttpDelete("{id:guid}")]
public async Task<ActionResult<ResponseBody>> Delete(Guid id)
{

View File

@@ -1,89 +1,39 @@
namespace Features.Breweries.Dtos;
/// <summary>
/// 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.
/// </summary>
public Guid BreweryPostLocationId { get; set; }
/// <summary>
/// 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.
/// </summary>
public Guid CityId { get; set; }
/// <summary>
/// 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).
/// </summary>
public string? AddressLine2 { get; set; }
/// <summary>
/// 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.
/// </summary>
/// <summary>Gets or sets the brewery's geographic coordinates, 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.
/// </summary>
public class BreweryDto
{
/// <summary>
/// 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.
/// </summary>
public Guid PostedById { get; set; }
/// <summary>
/// The name of the brewery.
/// </summary>
public string BreweryName { get; set; } = string.Empty;
/// <summary>
/// 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.
/// </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.
/// </summary>
public DateTime? UpdatedAt { get; set; }
/// <summary>
/// A row-version/concurrency token used to detect conflicting concurrent updates to the brewery post.
/// </summary>
/// <summary>Gets or sets the row-version/concurrency token used to detect conflicting concurrent updates.</summary>
public byte[]? Timer { get; set; }
/// <summary>
/// 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

@@ -2,17 +2,8 @@ using Domain.Entities;
namespace Features.Breweries.Dtos;
/// <summary>
/// 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.
/// </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)
{
return new BreweryDto

View File

@@ -5,10 +5,6 @@ using MediatR;
namespace Features.Breweries.Queries.GetAllBreweries;
/// <summary>
/// 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)
: IRequestHandler<GetAllBreweriesQuery, IEnumerable<BreweryDto>>
{

View File

@@ -5,10 +5,6 @@ using MediatR;
namespace Features.Breweries.Queries.GetBreweryById;
/// <summary>
/// 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)
: IRequestHandler<GetBreweryByIdQuery, BreweryDto?>
{

View File

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

View File

@@ -9,17 +9,11 @@ namespace Features.Breweries.Repository;
/// 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.
/// </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>
/// <exception cref="Microsoft.Data.SqlClient.SqlException">Thrown when the database command fails.</exception>
/// <summary>Retrieves a brewery post by ID using the <c>USP_GetBreweryById</c> stored procedure.</summary>
public async Task<BreweryPost?> GetByIdAsync(Guid id)
{
await using DbConnection connection = await CreateConnection();
@@ -40,10 +34,6 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory)
/// 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>
/// <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 DbConnection connection = await CreateConnection();
@@ -71,8 +61,6 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory)
/// <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 DbConnection connection = await CreateConnection();
@@ -97,8 +85,6 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory)
/// 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 DbConnection connection = await CreateConnection();
@@ -110,12 +96,8 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory)
await command.ExecuteNonQueryAsync();
}
/// <summary>
/// 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>
/// <summary>Creates a new brewery post and its location using the <c>USP_CreateBrewery</c> stored procedure.</summary>
/// <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 DbConnection connection = await CreateConnection();
@@ -139,12 +121,9 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory)
}
/// <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 <paramref name="reader" /> to a <see cref="BreweryPost" />, including its
/// associated <see cref="BreweryPostLocation" /> if location columns are present in the result set.
/// </summary>
/// <param name="reader">The data reader positioned on the row to map.</param>
/// <returns>The mapped <see cref="BreweryPost" /> instance.</returns>
protected override BreweryPost MapToEntity(DbDataReader reader)
{
BreweryPost brewery = new();
@@ -212,13 +191,7 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory)
return brewery;
}
/// <summary>
/// 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>
/// <summary>Adds a parameter to <paramref name="command" />, converting <c>null</c> to <see cref="DBNull.Value" />.</summary>
private static void AddParameter(DbCommand command, string name, object? value)
{
DbParameter p = command.CreateParameter();

View File

@@ -7,37 +7,16 @@ namespace Features.Breweries.Repository;
/// </summary>
public interface IBreweryRepository
{
/// <summary>
/// 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>
/// <summary>Retrieves a brewery post by ID, or <c>null</c> if not found.</summary>
Task<BreweryPost?> GetByIdAsync(Guid id);
/// <summary>
/// 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>
Task<IEnumerable<BreweryPost>> GetAllAsync(int? limit, int? offset);
/// <summary>
/// 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.
/// </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.
/// </summary>
/// <param name="brewery">The brewery post to create. Must have a non-null <c>Location</c>.</param>
/// <summary>Creates a new brewery post, including its location details.</summary>
/// <exception cref="ArgumentException">Thrown when <paramref name="brewery" /> has no <c>Location</c>.</exception>
Task CreateAsync(BreweryPost brewery);
}