diff --git a/web/backend/Features/Features.Breweries.Tests/Commands/CreateBreweryHandlerTests.cs b/web/backend/Features/Features.Breweries.Tests/Commands/CreateBreweryHandlerTests.cs
index de0dc48a..485a6e13 100644
--- a/web/backend/Features/Features.Breweries.Tests/Commands/CreateBreweryHandlerTests.cs
+++ b/web/backend/Features/Features.Breweries.Tests/Commands/CreateBreweryHandlerTests.cs
@@ -17,10 +17,8 @@ public class CreateBreweryHandlerTests
_handler = new CreateBreweryHandler(_repoMock.Object);
}
- private static CreateBreweryLocation ValidLocation()
- {
- return new CreateBreweryLocation(Guid.NewGuid(), "123 Main St", null, "12345", null);
- }
+ private static CreateBreweryLocation ValidLocation() =>
+ new(Guid.NewGuid(), "123 Main St", null, "12345", null);
[Fact]
public async Task Handle_PersistsEntity_WithNewIdsAndCreatedAt()
diff --git a/web/backend/Features/Features.Breweries.Tests/Features.Breweries.Tests.csproj b/web/backend/Features/Features.Breweries.Tests/Features.Breweries.Tests.csproj
index 4fbc5a99..5179a631 100644
--- a/web/backend/Features/Features.Breweries.Tests/Features.Breweries.Tests.csproj
+++ b/web/backend/Features/Features.Breweries.Tests/Features.Breweries.Tests.csproj
@@ -8,18 +8,18 @@
-
-
-
-
-
+
+
+
+
+
-
+
-
+
diff --git a/web/backend/Features/Features.Breweries.Tests/Queries/GetAllBreweryLocationsHandlerTests.cs b/web/backend/Features/Features.Breweries.Tests/Queries/GetAllBreweryLocationsHandlerTests.cs
index 93fe73c6..8a6e1453 100644
--- a/web/backend/Features/Features.Breweries.Tests/Queries/GetAllBreweryLocationsHandlerTests.cs
+++ b/web/backend/Features/Features.Breweries.Tests/Queries/GetAllBreweryLocationsHandlerTests.cs
@@ -36,8 +36,10 @@ public class GetAllBreweryLocationsHandlerTests
{
BreweryPost[] breweries =
[
- new BreweryPost { BreweryPostId = Guid.NewGuid(), BreweryName = "A" },
- new BreweryPost { BreweryPostId = Guid.NewGuid(), BreweryName = "B" },
+ new()
+ { BreweryPostId = Guid.NewGuid(), BreweryName = "A" },
+ new()
+ { BreweryPostId = Guid.NewGuid(), BreweryName = "B" },
];
_repoMock.Setup(r => r.GetAllLocations()).ReturnsAsync(breweries);
diff --git a/web/backend/Features/Features.Breweries.Tests/Queries/GetBreweryLocationsWithinRangeHandlerTests.cs b/web/backend/Features/Features.Breweries.Tests/Queries/GetBreweryLocationsWithinRangeHandlerTests.cs
index 8af18141..c707a4cf 100644
--- a/web/backend/Features/Features.Breweries.Tests/Queries/GetBreweryLocationsWithinRangeHandlerTests.cs
+++ b/web/backend/Features/Features.Breweries.Tests/Queries/GetBreweryLocationsWithinRangeHandlerTests.cs
@@ -50,8 +50,10 @@ public class GetBreweryLocationsWithinRangeHandlerTests
{
BreweryPost[] breweries =
[
- new BreweryPost { BreweryPostId = Guid.NewGuid(), BreweryName = "Nearby A" },
- new BreweryPost { BreweryPostId = Guid.NewGuid(), BreweryName = "Nearby B" },
+ new()
+ { BreweryPostId = Guid.NewGuid(), BreweryName = "Nearby A" },
+ new()
+ { BreweryPostId = Guid.NewGuid(), BreweryName = "Nearby B" },
];
_repoMock
.Setup(r => r.GetAllLocationsWithinRange(It.IsAny(), It.IsAny()))
diff --git a/web/backend/Features/Features.Breweries/Commands/CreateBrewery/CreateBreweryCommand.cs b/web/backend/Features/Features.Breweries/Commands/CreateBrewery/CreateBreweryCommand.cs
index 2f0d3fd2..3ed8635c 100644
--- a/web/backend/Features/Features.Breweries/Commands/CreateBrewery/CreateBreweryCommand.cs
+++ b/web/backend/Features/Features.Breweries/Commands/CreateBrewery/CreateBreweryCommand.cs
@@ -4,8 +4,12 @@ using MediatR;
namespace Features.Breweries.Commands.CreateBrewery;
-/// Location details for a brewery being created.
-/// Raw binary representation of the brewery's geographic coordinates.
+///
+/// Describes the location supplied when creating a brewery.
+///
+///
+/// Serialized geographic coordinates for the brewery.
+///
public record CreateBreweryLocation(
Guid CityId,
string AddressLine1,
@@ -15,7 +19,7 @@ public record CreateBreweryLocation(
);
///
-/// Creates a new brewery post. Bound directly from the request body of POST /api/brewery.
+/// Represents the application request used to create a brewery post.
///
public record CreateBreweryCommand(
Guid PostedById,
@@ -24,9 +28,11 @@ public record CreateBreweryCommand(
CreateBreweryLocation Location
) : IRequest;
-/// Request body for POST /api/brewery. PostedById comes from the caller's JWT.
+///
+/// Defines the client-supplied body for creating a brewery post.
+///
public record CreateBreweryRequest(
string BreweryName,
string Description,
CreateBreweryLocation Location
-);
\ No newline at end of file
+);
diff --git a/web/backend/Features/Features.Breweries/Commands/CreateBrewery/CreateBreweryHandler.cs b/web/backend/Features/Features.Breweries/Commands/CreateBrewery/CreateBreweryHandler.cs
index 3df6dbf8..2a9c239f 100644
--- a/web/backend/Features/Features.Breweries/Commands/CreateBrewery/CreateBreweryHandler.cs
+++ b/web/backend/Features/Features.Breweries/Commands/CreateBrewery/CreateBreweryHandler.cs
@@ -5,13 +5,17 @@ using MediatR;
namespace Features.Breweries.Commands.CreateBrewery;
-/// Handles by persisting a new brewery post.
+///
+/// Persists brewery posts created through .
+///
public class CreateBreweryHandler(IBreweryRepository repository)
: IRequestHandler
{
- /// Creates a new brewery post, generating new identifiers for the post and its location.
+ ///
+ /// Creates the post and its associated location with new identifiers.
+ ///
///
- /// Thrown when 's PostedById or Location.CityId does not exist.
+ /// Thrown if the posting user or the selected city cannot be found.
///
public async Task Handle(
CreateBreweryCommand request,
@@ -38,10 +42,6 @@ public class CreateBreweryHandler(IBreweryRepository repository)
await repository.CreateAsync(entity);
- // RowVersion is a DB-computed column, unset on the in-memory entity; re-fetch so callers
- // get back the value needed for a subsequent optimistic-concurrency update. Uses
- // CancellationToken.None since the write already committed: cancelling the caller's request
- // at this point must not turn a successful creation into a thrown exception.
BreweryPost created =
await repository.GetByIdAsync(entity.BreweryPostId, CancellationToken.None)
?? throw new InvalidOperationException(
diff --git a/web/backend/Features/Features.Breweries/Commands/CreateBrewery/CreateBreweryValidator.cs b/web/backend/Features/Features.Breweries/Commands/CreateBrewery/CreateBreweryValidator.cs
index e2f08885..c9bdeba0 100644
--- a/web/backend/Features/Features.Breweries/Commands/CreateBrewery/CreateBreweryValidator.cs
+++ b/web/backend/Features/Features.Breweries/Commands/CreateBrewery/CreateBreweryValidator.cs
@@ -2,7 +2,9 @@ using FluentValidation;
namespace Features.Breweries.Commands.CreateBrewery;
-/// Validates .
+///
+/// Applies creation rules to .
+///
public class CreateBreweryValidator : AbstractValidator
{
public CreateBreweryValidator()
@@ -27,7 +29,6 @@ public class CreateBreweryValidator : AbstractValidator
RuleFor(x => x.Location.AddressLine1)
.NotEmpty()
- .When(x => x.Location is not null)
.WithMessage("Address line 1 is required.")
.MaximumLength(256)
.WithMessage("Address line 1 cannot exceed 256 characters.");
diff --git a/web/backend/Features/Features.Breweries/Commands/DeleteBrewery/DeleteBreweryCommand.cs b/web/backend/Features/Features.Breweries/Commands/DeleteBrewery/DeleteBreweryCommand.cs
index e0c391a6..8a26df9f 100644
--- a/web/backend/Features/Features.Breweries/Commands/DeleteBrewery/DeleteBreweryCommand.cs
+++ b/web/backend/Features/Features.Breweries/Commands/DeleteBrewery/DeleteBreweryCommand.cs
@@ -2,5 +2,7 @@ using MediatR;
namespace Features.Breweries.Commands.DeleteBrewery;
-/// Deletes a brewery post. is bound from the route of DELETE /api/brewery/{id}.
+///
+/// Represents a request to delete a brewery post on behalf of a caller.
+///
public record DeleteBreweryCommand(Guid BreweryPostId, Guid RequestingUserId) : IRequest;
diff --git a/web/backend/Features/Features.Breweries/Commands/DeleteBrewery/DeleteBreweryHandler.cs b/web/backend/Features/Features.Breweries/Commands/DeleteBrewery/DeleteBreweryHandler.cs
index 557a6326..fffafa17 100644
--- a/web/backend/Features/Features.Breweries/Commands/DeleteBrewery/DeleteBreweryHandler.cs
+++ b/web/backend/Features/Features.Breweries/Commands/DeleteBrewery/DeleteBreweryHandler.cs
@@ -3,15 +3,20 @@ using MediatR;
namespace Features.Breweries.Commands.DeleteBrewery;
-/// Handles by deleting the corresponding brewery post.
+///
+/// Processes requests.
+///
public class DeleteBreweryHandler(IBreweryRepository repository)
: IRequestHandler
{
+ ///
+ /// Confirms that the caller owns the post before deleting it.
+ ///
///
- /// Thrown when no brewery exists with 's BreweryPostId.
+ /// Thrown if the brewery post does not exist.
///
///
- /// Thrown when 's RequestingUserId did not post the brewery.
+ /// Thrown if the caller did not create the brewery post.
///
public async Task Handle(DeleteBreweryCommand request, CancellationToken cancellationToken)
{
diff --git a/web/backend/Features/Features.Breweries/Commands/UpdateBrewery/UpdateBreweryCommand.cs b/web/backend/Features/Features.Breweries/Commands/UpdateBrewery/UpdateBreweryCommand.cs
index 41e669fc..d51a3556 100644
--- a/web/backend/Features/Features.Breweries/Commands/UpdateBrewery/UpdateBreweryCommand.cs
+++ b/web/backend/Features/Features.Breweries/Commands/UpdateBrewery/UpdateBreweryCommand.cs
@@ -4,11 +4,15 @@ using MediatR;
namespace Features.Breweries.Commands.UpdateBrewery;
-/// Location details for a brewery being updated.
+///
+/// Describes the location supplied when updating a brewery.
+///
///
-/// Identifier of the existing location row, if any; a new row is created if none exists yet.
+/// Identifies the existing location; a new location is created when needed.
+///
+///
+/// Serialized geographic coordinates for the brewery.
///
-/// Raw binary representation of the brewery's geographic coordinates.
public record UpdateBreweryLocation(
Guid BreweryPostLocationId,
Guid CityId,
@@ -19,9 +23,8 @@ public record UpdateBreweryLocation(
);
///
-/// Updates an existing brewery post. A null clears the brewery's location.
-/// must be the row-version last read for this brewery (e.g. from a prior
-/// GET); the update is rejected with a 409 Conflict if the brewery was modified since then.
+/// Represents a concurrency-protected change to an existing brewery post.
+/// A removes the existing location.
///
public record UpdateBreweryCommand(
Guid BreweryPostId,
@@ -32,7 +35,9 @@ public record UpdateBreweryCommand(
UpdateBreweryLocation? Location
) : IRequest;
-/// Request body for PUT /api/brewery/{id}.
+///
+/// Defines the client-supplied body for updating a brewery post.
+///
public record UpdateBreweryRequest(
Guid BreweryPostId,
byte[] RowVersion,
@@ -40,4 +45,3 @@ public record UpdateBreweryRequest(
string Description,
UpdateBreweryLocation? Location
);
-
diff --git a/web/backend/Features/Features.Breweries/Commands/UpdateBrewery/UpdateBreweryHandler.cs b/web/backend/Features/Features.Breweries/Commands/UpdateBrewery/UpdateBreweryHandler.cs
index 5c237148..bdbdbbc3 100644
--- a/web/backend/Features/Features.Breweries/Commands/UpdateBrewery/UpdateBreweryHandler.cs
+++ b/web/backend/Features/Features.Breweries/Commands/UpdateBrewery/UpdateBreweryHandler.cs
@@ -5,21 +5,20 @@ using MediatR;
namespace Features.Breweries.Commands.UpdateBrewery;
-/// Handles by persisting changes to an existing brewery post.
+///
+/// Persists changes requested through .
+///
public class UpdateBreweryHandler(IBreweryRepository repository)
: IRequestHandler
{
///
- /// Updates an existing brewery post. If has no Location,
- /// the brewery's location is cleared.
+ /// Updates the post after confirming that the caller owns it.
///
///
- /// Thrown when no brewery exists with 's BreweryPostId, or its
- /// location's CityId does not exist.
+ /// Thrown if the brewery post or its requested city cannot be found.
///
///
- /// Thrown when the brewery was modified by another request since .
- /// RowVersion was read.
+ /// Thrown if the post has changed since its supplied row version was read.
///
public async Task Handle(
UpdateBreweryCommand request,
diff --git a/web/backend/Features/Features.Breweries/Commands/UpdateBrewery/UpdateBreweryValidator.cs b/web/backend/Features/Features.Breweries/Commands/UpdateBrewery/UpdateBreweryValidator.cs
index a8757fcb..c0a39bd0 100644
--- a/web/backend/Features/Features.Breweries/Commands/UpdateBrewery/UpdateBreweryValidator.cs
+++ b/web/backend/Features/Features.Breweries/Commands/UpdateBrewery/UpdateBreweryValidator.cs
@@ -2,7 +2,9 @@ using FluentValidation;
namespace Features.Breweries.Commands.UpdateBrewery;
-/// Validates .
+///
+/// Applies update rules to .
+///
public class UpdateBreweryValidator : AbstractValidator
{
public UpdateBreweryValidator()
diff --git a/web/backend/Features/Features.Breweries/Controllers/BreweryController.cs b/web/backend/Features/Features.Breweries/Controllers/BreweryController.cs
index b95e0d06..2d95743b 100644
--- a/web/backend/Features/Features.Breweries/Controllers/BreweryController.cs
+++ b/web/backend/Features/Features.Breweries/Controllers/BreweryController.cs
@@ -13,19 +13,25 @@ using Shared.Contracts;
namespace Features.Breweries.Controllers;
-/// Exposes CRUD operations for brewery posts.
+///
+/// Provides HTTP endpoints for reading and managing brewery posts.
+///
///
-/// Requires JWT authentication by default; and opt out via
-/// [AllowAnonymous].
+/// Requests require JWT authentication unless the endpoint allows anonymous access.
///
+///
[ApiController]
[Route("api/[controller]")]
[Authorize(AuthenticationSchemes = "JWT")]
public class BreweryController(IMediator mediator) : ControllerBase
{
- /// Retrieves a single brewery post by ID.
- /// Allows anonymous access.
- /// 200 OK with the brewery if found; otherwise 404 Not Found.
+ ///
+ /// Returns the brewery post identified by .
+ ///
+ /// This endpoint permits anonymous access.
+ ///
+ /// A successful response containing the post, or 404 Not Found when it is absent.
+ ///
[AllowAnonymous]
[HttpGet("{id:guid}")]
public async Task>> GetById(Guid id)
@@ -43,10 +49,16 @@ public class BreweryController(IMediator mediator) : ControllerBase
);
}
- /// Retrieves brewery posts, newest first.
- /// Maximum number of breweries to return. Unbounded if .
- /// Number of breweries to skip. Treated as zero if .
- /// Allows anonymous access.
+ ///
+ /// Returns brewery posts in reverse chronological order.
+ ///
+ ///
+ /// Limits the number of returned posts; leaves it unlimited.
+ ///
+ ///
+ /// Skips this many posts; means none.
+ ///
+ /// This endpoint permits anonymous access.
[AllowAnonymous]
[HttpGet]
public async Task>>> GetAll(
@@ -66,11 +78,15 @@ public class BreweryController(IMediator mediator) : ControllerBase
);
}
- /// Retrieves all brewery posts that have a set location.
- /// Allows anonymous access.
+ ///
+ /// Returns all brewery posts with an associated location.
+ ///
+ /// This endpoint permits anonymous access.
[AllowAnonymous]
[HttpGet("locations")]
- public async Task>>> GetAllLocations()
+ public async Task<
+ ActionResult>>
+ > GetAllLocations()
{
IEnumerable breweries = await mediator.Send(
new GetAllBreweryLocationsQuery()
@@ -84,14 +100,24 @@ public class BreweryController(IMediator mediator) : ControllerBase
);
}
- /// Retrieves brewery posts with a set location within range of the given coordinates, nearest first.
- /// The origin latitude, in decimal degrees.
- /// The origin longitude, in decimal degrees.
- /// The maximum distance, in metres, from the origin coordinates.
- /// Allows anonymous access.
+ ///
+ /// Returns located brewery posts within the requested radius, ordered by proximity.
+ ///
+ ///
+ /// Identifies the origin latitude in decimal degrees.
+ ///
+ ///
+ /// Identifies the origin longitude in decimal degrees.
+ ///
+ ///
+ /// Sets the maximum distance from the origin in metres.
+ ///
+ /// This endpoint permits anonymous access.
[AllowAnonymous]
[HttpGet("locations/nearby")]
- public async Task>>> GetLocationsWithinRange(
+ public async Task<
+ ActionResult>>
+ > GetLocationsWithinRange(
[FromQuery] double latitude,
[FromQuery] double longitude,
[FromQuery] double rangeInMetres
@@ -109,8 +135,10 @@ public class BreweryController(IMediator mediator) : ControllerBase
);
}
- /// Creates a new brewery post.
- /// 201 Created with the newly created brewery.
+ ///
+ /// Creates a brewery post for the authenticated user.
+ ///
+ /// A 201 Created response containing the new post.
[HttpPost]
public async Task>> Create(
[FromBody] CreateBreweryRequest request
@@ -134,12 +162,15 @@ public class BreweryController(IMediator mediator) : ControllerBase
);
}
- /// Updates an existing brewery post.
- /// Must match 's BreweryPostId.
+ ///
+ /// Updates a brewery post owned by the authenticated user.
+ ///
+ ///
+ /// Must equal the brewery post identifier in .
+ ///
///
- /// 200 OK with the updated brewery; 400 Bad Request if the route ID does not match the
- /// payload ID; 404 Not Found if the brewery or its CityId does not exist; or
- /// 409 Conflict if the brewery was modified since request.RowVersion was read.
+ /// A successful response containing the updated post, or an error for an invalid ID,
+ /// missing resource, or stale row version.
///
[HttpPut("{id:guid}")]
public async Task>> Update(
@@ -170,12 +201,16 @@ public class BreweryController(IMediator mediator) : ControllerBase
);
}
- /// Deletes a brewery post.
- /// 200 OK confirming the deletion; 404 Not Found if no brewery exists with .
+ ///
+ /// Deletes a brewery post owned by the authenticated user.
+ ///
+ ///
+ /// A confirmation response, or 404 Not Found if the post is absent.
+ ///
[HttpDelete("{id:guid}")]
public async Task> Delete(Guid id)
{
await mediator.Send(new DeleteBreweryCommand(id, User.GetAuthenticatedUserId()));
return Ok(new ResponseBody { Message = "Brewery deleted successfully." });
}
-}
\ No newline at end of file
+}
diff --git a/web/backend/Features/Features.Breweries/DependencyInjection/FeaturesBreweriesServiceCollectionExtensions.cs b/web/backend/Features/Features.Breweries/DependencyInjection/FeaturesBreweriesServiceCollectionExtensions.cs
index 79770f37..a5f27abd 100644
--- a/web/backend/Features/Features.Breweries/DependencyInjection/FeaturesBreweriesServiceCollectionExtensions.cs
+++ b/web/backend/Features/Features.Breweries/DependencyInjection/FeaturesBreweriesServiceCollectionExtensions.cs
@@ -3,11 +3,15 @@ using Microsoft.Extensions.DependencyInjection;
namespace Features.Breweries.DependencyInjection;
-/// Registers the services required by the Breweries feature.
+///
+/// Adds Breweries feature services to the dependency-injection container.
+///
public static class FeaturesBreweriesServiceCollectionExtensions
{
- /// Registers and its implementation.
- /// , for chaining.
+ ///
+ /// Registers the repository services used by the Breweries feature.
+ ///
+ /// The supplied service collection.
public static IServiceCollection AddFeaturesBreweries(this IServiceCollection services)
{
services.AddScoped();
diff --git a/web/backend/Features/Features.Breweries/Dtos/BreweryDto.cs b/web/backend/Features/Features.Breweries/Dtos/BreweryDto.cs
index a63ea99f..7402ed66 100644
--- a/web/backend/Features/Features.Breweries/Dtos/BreweryDto.cs
+++ b/web/backend/Features/Features.Breweries/Dtos/BreweryDto.cs
@@ -2,8 +2,12 @@ using Domain.Entities;
namespace Features.Breweries.Dtos;
-/// Location details of a brewery, as returned by the API.
-/// The brewery's geographic coordinates, in a raw binary representation.
+///
+/// Represents location data returned for a brewery post.
+///
+///
+/// Contains the serialized geographic coordinates, when available.
+///
public record BreweryLocationDto(
Guid BreweryPostLocationId,
Guid CityId,
@@ -18,10 +22,18 @@ public record BreweryLocationDto(
CoordinateData? Coordinates
);
-/// A brewery post, as returned by the API.
-/// The date and time of the last edit, or if never edited.
-/// The row-version/concurrency token used to detect conflicting concurrent updates.
-/// The brewery's location, or if none has been set.
+///
+/// Represents a brewery post returned by the API.
+///
+///
+/// Records the most recent update time, if the post has been updated.
+///
+///
+/// Provides the concurrency token required for updates.
+///
+///
+/// Contains the post's location, when one is available.
+///
public record BreweryDto(
Guid BreweryPostId,
Guid PostedById,
@@ -32,9 +44,12 @@ public record BreweryDto(
byte[]? RowVersion,
BreweryLocationDto? Location
);
+
+///
+/// Represents the location-focused result returned by brewery searches.
+///
///
-/// The distance, in metres, from a query's origin point, or for queries with no
-/// origin point (e.g. GET /api/brewery/locations).
+/// Contains the distance from the search origin in metres, when applicable.
///
public record BreweryWithLocationDto(
Guid BreweryPostId,
diff --git a/web/backend/Features/Features.Breweries/Dtos/BreweryDtoMapper.cs b/web/backend/Features/Features.Breweries/Dtos/BreweryDtoMapper.cs
index 4cbebeed..7afb7309 100644
--- a/web/backend/Features/Features.Breweries/Dtos/BreweryDtoMapper.cs
+++ b/web/backend/Features/Features.Breweries/Dtos/BreweryDtoMapper.cs
@@ -2,74 +2,62 @@ using Domain.Entities;
namespace Features.Breweries.Dtos;
-/// Maps domain entities to .
+///
+/// Converts brewery domain entities into API response models.
+///
public static class BreweryDtoMapper
{
///
- /// Converts a to its DTO representation, including its
- /// if the entity has one.
+ /// Converts a brewery post into its complete API representation.
///
- public static BreweryDto ToDto(this BreweryPost brewery)
- {
- return new BreweryDto(
- BreweryPostId: brewery.BreweryPostId,
- PostedById: brewery.PostedById,
- BreweryName: brewery.BreweryName,
- Description: brewery.Description,
- CreatedAt: brewery.CreatedAt,
- UpdatedAt: brewery.UpdatedAt,
- RowVersion: brewery.RowVersion,
- Location: brewery.Location is null
+ public static BreweryDto ToDto(this BreweryPost brewery) =>
+ new(
+ brewery.BreweryPostId,
+ brewery.PostedById,
+ brewery.BreweryName,
+ brewery.Description,
+ brewery.CreatedAt,
+ brewery.UpdatedAt,
+ brewery.RowVersion,
+ brewery.Location is null
? null
: new BreweryLocationDto(
- BreweryPostLocationId: brewery.Location.BreweryPostLocationId,
- CityId: brewery.Location.CityId,
- CityName: brewery.Location.City?.CityName ?? string.Empty,
- StateProvinceName: brewery.Location.City?.StateProvince?.StateProvinceName
- ?? string.Empty,
- StateProvinceCode: brewery.Location.City?.StateProvince?.Iso31662
- ?? string.Empty,
- CountryName: brewery.Location.City?.StateProvince?.Country?.CountryName
- ?? string.Empty,
- CountryCode: brewery.Location.City?.StateProvince?.Country?.Iso31661
- ?? string.Empty,
- AddressLine1: brewery.Location.AddressLine1,
- AddressLine2: brewery.Location.AddressLine2,
- PostalCode: brewery.Location.PostalCode,
- Coordinates: brewery.Location.Coordinates
+ brewery.Location.BreweryPostLocationId,
+ brewery.Location.CityId,
+ brewery.Location.City?.CityName ?? string.Empty,
+ brewery.Location.City?.StateProvince?.StateProvinceName ?? string.Empty,
+ brewery.Location.City?.StateProvince?.Iso31662 ?? string.Empty,
+ brewery.Location.City?.StateProvince?.Country?.CountryName ?? string.Empty,
+ brewery.Location.City?.StateProvince?.Country?.Iso31661 ?? string.Empty,
+ brewery.Location.AddressLine1,
+ brewery.Location.AddressLine2,
+ brewery.Location.PostalCode,
+ brewery.Location.Coordinates
)
);
- }
///
- /// Converts a to its simplified DTO representation, used for
- /// location-based queries that only need identifying and location details.
+ /// Converts a brewery post into its location-search representation.
///
- public static BreweryWithLocationDto ToSimplifiedDto(this BreweryPost brewery)
- {
- return new BreweryWithLocationDto(
- BreweryPostId: brewery.BreweryPostId,
- BreweryName: brewery.BreweryName,
- Location: brewery.Location is null
+ public static BreweryWithLocationDto ToSimplifiedDto(this BreweryPost brewery) =>
+ new(
+ brewery.BreweryPostId,
+ brewery.BreweryName,
+ brewery.Location is null
? null
: new BreweryLocationDto(
- BreweryPostLocationId: brewery.Location.BreweryPostLocationId,
- CityId: brewery.Location.CityId,
- CityName: brewery.Location.City?.CityName ?? string.Empty,
- StateProvinceName: brewery.Location.City?.StateProvince?.StateProvinceName
- ?? string.Empty,
- StateProvinceCode: brewery.Location.City?.StateProvince?.Iso31662
- ?? string.Empty,
- CountryName: brewery.Location.City?.StateProvince?.Country?.CountryName
- ?? string.Empty,
- CountryCode: brewery.Location.City?.StateProvince?.Country?.Iso31661
- ?? string.Empty,
- AddressLine1: brewery.Location.AddressLine1,
- AddressLine2: brewery.Location.AddressLine2,
- PostalCode: brewery.Location.PostalCode,
- Coordinates: brewery.Location.Coordinates
+ brewery.Location.BreweryPostLocationId,
+ brewery.Location.CityId,
+ brewery.Location.City?.CityName ?? string.Empty,
+ brewery.Location.City?.StateProvince?.StateProvinceName ?? string.Empty,
+ brewery.Location.City?.StateProvince?.Iso31662 ?? string.Empty,
+ brewery.Location.City?.StateProvince?.Country?.CountryName ?? string.Empty,
+ brewery.Location.City?.StateProvince?.Country?.Iso31661 ?? string.Empty,
+ brewery.Location.AddressLine1,
+ brewery.Location.AddressLine2,
+ brewery.Location.PostalCode,
+ brewery.Location.Coordinates
),
- DistanceMetres: brewery.Distance?.DistanceMetres
+ brewery.Distance?.DistanceMetres
);
- }
}
diff --git a/web/backend/Features/Features.Breweries/Queries/GetAllBreweries/GetAllBreweriesHandler.cs b/web/backend/Features/Features.Breweries/Queries/GetAllBreweries/GetAllBreweriesHandler.cs
index 2b0308c7..15a6da7e 100644
--- a/web/backend/Features/Features.Breweries/Queries/GetAllBreweries/GetAllBreweriesHandler.cs
+++ b/web/backend/Features/Features.Breweries/Queries/GetAllBreweries/GetAllBreweriesHandler.cs
@@ -5,11 +5,15 @@ using MediatR;
namespace Features.Breweries.Queries.GetAllBreweries;
-/// Handles by retrieving brewery posts, newest first.
+///
+/// Handles ordered, paged brewery-post queries.
+///
public class GetAllBreweriesHandler(IBreweryRepository repository)
: IRequestHandler>
{
- /// Retrieves brewery posts, newest first, applying 's paging.
+ ///
+ /// Retrieves and maps the requested page of brewery posts.
+ ///
public async Task> Handle(
GetAllBreweriesQuery request,
CancellationToken cancellationToken
diff --git a/web/backend/Features/Features.Breweries/Queries/GetAllBreweries/GetAllBreweriesQuery.cs b/web/backend/Features/Features.Breweries/Queries/GetAllBreweries/GetAllBreweriesQuery.cs
index 96267d81..bade0212 100644
--- a/web/backend/Features/Features.Breweries/Queries/GetAllBreweries/GetAllBreweriesQuery.cs
+++ b/web/backend/Features/Features.Breweries/Queries/GetAllBreweries/GetAllBreweriesQuery.cs
@@ -3,7 +3,13 @@ using MediatR;
namespace Features.Breweries.Queries.GetAllBreweries;
-/// Retrieves brewery posts, newest first. Bound directly from the query string of GET /api/brewery.
-/// Maximum number of breweries to return. Unbounded if .
-/// Number of breweries to skip. Treated as zero if .
+///
+/// Requests brewery posts ordered from newest to oldest.
+///
+///
+/// Caps the result count; disables the cap.
+///
+///
+/// Specifies the number of results to skip; means zero.
+///
public record GetAllBreweriesQuery(int? Limit, int? Offset) : IRequest>;
diff --git a/web/backend/Features/Features.Breweries/Queries/GetAllBreweryLocations/GetAllBreweryLocationsHandler.cs b/web/backend/Features/Features.Breweries/Queries/GetAllBreweryLocations/GetAllBreweryLocationsHandler.cs
index 42e1c2c0..45f07fe3 100644
--- a/web/backend/Features/Features.Breweries/Queries/GetAllBreweryLocations/GetAllBreweryLocationsHandler.cs
+++ b/web/backend/Features/Features.Breweries/Queries/GetAllBreweryLocations/GetAllBreweryLocationsHandler.cs
@@ -5,11 +5,15 @@ using MediatR;
namespace Features.Breweries.Queries.GetAllBreweryLocations;
-/// Handles by retrieving all located brewery posts.
+///
+/// Handles queries for brewery posts with locations.
+///
public class GetAllBreweryLocationsHandler(IBreweryRepository repository)
: IRequestHandler>
{
- /// Retrieves every brewery post that has a set location.
+ ///
+ /// Retrieves and maps every brewery post that has a location.
+ ///
public async Task> Handle(
GetAllBreweryLocationsQuery request,
CancellationToken cancellationToken
diff --git a/web/backend/Features/Features.Breweries/Queries/GetAllBreweryLocations/GetAllBreweryLocationsQuery.cs b/web/backend/Features/Features.Breweries/Queries/GetAllBreweryLocations/GetAllBreweryLocationsQuery.cs
index b6ea4ea3..d9b3b7fb 100644
--- a/web/backend/Features/Features.Breweries/Queries/GetAllBreweryLocations/GetAllBreweryLocationsQuery.cs
+++ b/web/backend/Features/Features.Breweries/Queries/GetAllBreweryLocations/GetAllBreweryLocationsQuery.cs
@@ -4,7 +4,6 @@ using MediatR;
namespace Features.Breweries.Queries.GetAllBreweryLocations;
///
-/// Retrieves all brewery posts that have a set location. Bound directly from
-/// GET /api/brewery/locations.
+/// Requests brewery posts that include a location.
///
public record GetAllBreweryLocationsQuery : IRequest>;
diff --git a/web/backend/Features/Features.Breweries/Queries/GetBreweryById/GetBreweryByIdHandler.cs b/web/backend/Features/Features.Breweries/Queries/GetBreweryById/GetBreweryByIdHandler.cs
index 1a27170b..e5678796 100644
--- a/web/backend/Features/Features.Breweries/Queries/GetBreweryById/GetBreweryByIdHandler.cs
+++ b/web/backend/Features/Features.Breweries/Queries/GetBreweryById/GetBreweryByIdHandler.cs
@@ -5,17 +5,24 @@ using MediatR;
namespace Features.Breweries.Queries.GetBreweryById;
-/// Handles by retrieving the matching brewery post.
+///
+/// Handles lookup requests for individual brewery posts.
+///
public class GetBreweryByIdHandler(IBreweryRepository repository)
: IRequestHandler
{
- /// Retrieves the brewery post, or if none exists with the given ID.
+ ///
+ /// Retrieves and maps the post, if it exists.
+ ///
public async Task Handle(
GetBreweryByIdQuery request,
CancellationToken cancellationToken
)
{
- BreweryPost? brewery = await repository.GetByIdAsync(request.BreweryPostId, cancellationToken);
+ BreweryPost? brewery = await repository.GetByIdAsync(
+ request.BreweryPostId,
+ cancellationToken
+ );
return brewery?.ToDto();
}
}
diff --git a/web/backend/Features/Features.Breweries/Queries/GetBreweryById/GetBreweryByIdQuery.cs b/web/backend/Features/Features.Breweries/Queries/GetBreweryById/GetBreweryByIdQuery.cs
index 775edc92..8d3c46e2 100644
--- a/web/backend/Features/Features.Breweries/Queries/GetBreweryById/GetBreweryByIdQuery.cs
+++ b/web/backend/Features/Features.Breweries/Queries/GetBreweryById/GetBreweryByIdQuery.cs
@@ -3,6 +3,10 @@ using MediatR;
namespace Features.Breweries.Queries.GetBreweryById;
-/// Retrieves a brewery post by ID. Bound directly from the route of GET /api/brewery/{id}.
-/// Yields if no brewery exists with the given BreweryPostId.
+///
+/// Requests a brewery post by its identifier.
+///
+///
+/// The handler returns when the post is absent.
+///
public record GetBreweryByIdQuery(Guid BreweryPostId) : IRequest;
diff --git a/web/backend/Features/Features.Breweries/Queries/GetBreweryLocationsWithinRange/GetBreweryLocationsWithinRangeHandler.cs b/web/backend/Features/Features.Breweries/Queries/GetBreweryLocationsWithinRange/GetBreweryLocationsWithinRangeHandler.cs
index 00b7a7ce..d21110d5 100644
--- a/web/backend/Features/Features.Breweries/Queries/GetBreweryLocationsWithinRange/GetBreweryLocationsWithinRangeHandler.cs
+++ b/web/backend/Features/Features.Breweries/Queries/GetBreweryLocationsWithinRange/GetBreweryLocationsWithinRangeHandler.cs
@@ -6,12 +6,14 @@ using MediatR;
namespace Features.Breweries.Queries.GetBreweryLocationsWithinRange;
///
-/// Handles by retrieving nearby located brewery posts.
+/// Handles proximity searches for brewery locations.
///
public class GetBreweryLocationsWithinRangeHandler(IBreweryRepository repository)
: IRequestHandler>
{
- /// Retrieves brewery posts within range of the requested coordinates, nearest first.
+ ///
+ /// Retrieves and maps nearby brewery locations.
+ ///
public async Task> Handle(
GetBreweryLocationsWithinRangeQuery request,
CancellationToken cancellationToken
diff --git a/web/backend/Features/Features.Breweries/Queries/GetBreweryLocationsWithinRange/GetBreweryLocationsWithinRangeQuery.cs b/web/backend/Features/Features.Breweries/Queries/GetBreweryLocationsWithinRange/GetBreweryLocationsWithinRangeQuery.cs
index f7322485..4adeadf9 100644
--- a/web/backend/Features/Features.Breweries/Queries/GetBreweryLocationsWithinRange/GetBreweryLocationsWithinRangeQuery.cs
+++ b/web/backend/Features/Features.Breweries/Queries/GetBreweryLocationsWithinRange/GetBreweryLocationsWithinRangeQuery.cs
@@ -4,12 +4,19 @@ using MediatR;
namespace Features.Breweries.Queries.GetBreweryLocationsWithinRange;
///
-/// Retrieves brewery posts with a set location within of the given
-/// coordinates, nearest first. Bound directly from the query string of
-/// GET /api/brewery/locations/nearby.
+/// Requests located brewery posts near a geographic point, nearest first.
///
-/// The origin latitude, in decimal degrees.
-/// The origin longitude, in decimal degrees.
-/// The maximum distance, in metres, from the origin coordinates.
-public record GetBreweryLocationsWithinRangeQuery(double Latitude, double Longitude, double RangeInMetres)
- : IRequest>;
+///
+/// Sets the origin latitude in decimal degrees.
+///
+///
+/// Sets the origin longitude in decimal degrees.
+///
+///
+/// Sets the inclusive search radius in metres.
+///
+public record GetBreweryLocationsWithinRangeQuery(
+ double Latitude,
+ double Longitude,
+ double RangeInMetres
+) : IRequest>;
diff --git a/web/backend/Features/Features.Breweries/Queries/GetBreweryLocationsWithinRange/GetBreweryLocationsWithinRangeValidator.cs b/web/backend/Features/Features.Breweries/Queries/GetBreweryLocationsWithinRange/GetBreweryLocationsWithinRangeValidator.cs
index 8b2e3fda..cdaad6b5 100644
--- a/web/backend/Features/Features.Breweries/Queries/GetBreweryLocationsWithinRange/GetBreweryLocationsWithinRangeValidator.cs
+++ b/web/backend/Features/Features.Breweries/Queries/GetBreweryLocationsWithinRange/GetBreweryLocationsWithinRangeValidator.cs
@@ -2,7 +2,9 @@ using FluentValidation;
namespace Features.Breweries.Queries.GetBreweryLocationsWithinRange;
-/// Validates .
+///
+/// Validates coordinates and radius values for proximity searches.
+///
public class GetBreweryLocationsWithinRangeValidator
: AbstractValidator
{
diff --git a/web/backend/Features/Features.Breweries/Repository/BreweryOwnershipExtensions.cs b/web/backend/Features/Features.Breweries/Repository/BreweryOwnershipExtensions.cs
index 83169d6f..c3731a4f 100644
--- a/web/backend/Features/Features.Breweries/Repository/BreweryOwnershipExtensions.cs
+++ b/web/backend/Features/Features.Breweries/Repository/BreweryOwnershipExtensions.cs
@@ -2,11 +2,20 @@ using Domain.Exceptions;
namespace Features.Breweries.Repository;
-/// Shared ownership check for commands that mutate a specific brewery post.
+///
+/// Provides authorization checks for brewery-post mutations.
+///
public static class BreweryOwnershipExtensions
{
- /// Thrown when no brewery exists with the given ID.
- /// Thrown when did not post the brewery.
+ ///
+ /// Verifies that the specified user created the brewery post.
+ ///
+ ///
+ /// Thrown if the brewery post cannot be found.
+ ///
+ ///
+ /// Thrown if the specified user does not own the brewery post.
+ ///
public static async Task EnsureCallerOwnsBreweryAsync(
this IBreweryRepository repository,
Guid breweryPostId,
diff --git a/web/backend/Features/Features.Breweries/Repository/BreweryRepository.cs b/web/backend/Features/Features.Breweries/Repository/BreweryRepository.cs
index 8c8cac7a..e078a28c 100644
--- a/web/backend/Features/Features.Breweries/Repository/BreweryRepository.cs
+++ b/web/backend/Features/Features.Breweries/Repository/BreweryRepository.cs
@@ -7,14 +7,11 @@ using Domain.Exceptions;
namespace Features.Breweries.Repository;
///
-/// Dapper-based implementation of .
+/// Implements brewery persistence with Dapper.
///
///
-/// BreweryPostLocation.Coordinates is a SQL Server GEOGRAPHY column. Dapper's generic
-/// Query<T> deserializer reads columns via IDataRecord.GetValue, which for a UDT
-/// column returns a CLR UDT instance rather than raw bytes and cannot be coerced to byte[].
-/// The read queries below therefore select CONVERT(varbinary(max), bpl.Coordinates), which
-/// yields the UDT's serialized bytes as a plain varbinary value that Dapper can bind directly.
+/// Coordinate queries convert SQL Server GEOGRAPHY values to varbinary(max)
+/// so Dapper can deserialize them as binary coordinate data.
///
public class BreweryRepository(ISqlConnectionFactory connectionFactory)
: DapperRepository(connectionFactory),
@@ -27,8 +24,11 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory)
SqlMapper.AddTypeHandler(new CoordinateDataTypeHandler());
}
- ///
- public async Task GetByIdAsync(Guid id, CancellationToken cancellationToken = default)
+ /// Gets a brewery post with its location data.
+ public async Task GetByIdAsync(
+ Guid id,
+ CancellationToken cancellationToken = default
+ )
{
await using DbConnection connection = await CreateConnection();
IEnumerable results = await connection.QueryAsync<
@@ -73,14 +73,17 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory)
cancellationToken: cancellationToken
),
MapBreweryRow,
- splitOn: "BreweryPostLocationID,CityID,StateProvinceID,CountryID"
+ "BreweryPostLocationID,CityID,StateProvinceID,CountryID"
);
return results.SingleOrDefault();
}
- ///
- public async Task GetPostedByIdAsync(Guid id, CancellationToken cancellationToken = default)
+ /// Gets the identifier of the user who created a brewery post.
+ public async Task GetPostedByIdAsync(
+ Guid id,
+ CancellationToken cancellationToken = default
+ )
{
await using DbConnection connection = await CreateConnection();
return await connection.ExecuteScalarAsync(
@@ -92,7 +95,7 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory)
);
}
- ///
+ /// Gets brewery posts in reverse chronological order.
public async Task> GetAllAsync(int? limit, int? offset)
{
await using DbConnection connection = await CreateConnection();
@@ -140,125 +143,141 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory)
);
}
- ///
- public async Task> GetAllLocationsWithinRange(CoordinateData coords, double rangeInMetres)
+ /// Gets brewery posts within the requested distance from an origin.
+ public async Task> GetAllLocationsWithinRange(
+ CoordinateData coords,
+ double rangeInMetres
+ )
{
await using DbConnection connection = await CreateConnection();
return await connection.QueryAsync<
- BreweryPost,
- BreweryPostLocation,
- City,
- StateProvince,
- Country,
- DistanceRow,
- BreweryPost
- > (
- """
- DECLARE @Origin geography = geography::Point(@Latitude, @Longitude, 4326);
+ BreweryPost,
+ BreweryPostLocation,
+ City,
+ StateProvince,
+ Country,
+ DistanceRow,
+ BreweryPost
+ >(
+ """
+ DECLARE @Origin geography = geography::Point(@Latitude, @Longitude, 4326);
- SELECT
- bp.BreweryPostID,
- bp.BreweryName,
- bpl.BreweryPostLocationID,
- bpl.AddressLine1,
- bpl.AddressLine2,
- bpl.PostalCode,
- CONVERT(varbinary(max), bpl.Coordinates) AS Coordinates,
- c.CityID,
- c.CityName,
- sp.StateProvinceID,
- sp.StateProvinceName,
- sp.ISO3166_2 AS Iso31662,
- co.CountryID,
- co.CountryName,
- co.ISO3166_1 AS Iso31661,
- bpl.Coordinates.STDistance(@Origin) AS DistanceMetres
- FROM Brewery.BreweryPost bp
- INNER JOIN Brewery.BreweryPostLocation bpl ON bp.BreweryPostID = bpl.BreweryPostID
- LEFT JOIN Geolocation.City c ON bpl.CityID = c.CityID
- LEFT JOIN Geolocation.StateProvince sp ON c.StateProvinceID = sp.StateProvinceID
- LEFT JOIN Geolocation.Country co ON sp.CountryID = co.CountryID
- WHERE bpl.Coordinates IS NOT NULL
- AND bpl.Coordinates.STDistance(@Origin) <= @RangeInMetres
- ORDER BY bpl.Coordinates.STDistance(@Origin) ASC
- """,
- (post, location, city, stateProvince, country, distance) =>
- MapBreweryRowWithDistance(post, location, city, stateProvince, country, distance, coords),
- param: new
- {
- coords.Latitude,
- coords.Longitude,
- RangeInMetres = rangeInMetres
- },
- splitOn: "BreweryPostLocationID,CityID,StateProvinceID,CountryID,DistanceMetres"
- );
+ SELECT
+ bp.BreweryPostID,
+ bp.BreweryName,
+ bpl.BreweryPostLocationID,
+ bpl.AddressLine1,
+ bpl.AddressLine2,
+ bpl.PostalCode,
+ CONVERT(varbinary(max), bpl.Coordinates) AS Coordinates,
+ c.CityID,
+ c.CityName,
+ sp.StateProvinceID,
+ sp.StateProvinceName,
+ sp.ISO3166_2 AS Iso31662,
+ co.CountryID,
+ co.CountryName,
+ co.ISO3166_1 AS Iso31661,
+ bpl.Coordinates.STDistance(@Origin) AS DistanceMetres
+ FROM Brewery.BreweryPost bp
+ INNER JOIN Brewery.BreweryPostLocation bpl ON bp.BreweryPostID = bpl.BreweryPostID
+ LEFT JOIN Geolocation.City c ON bpl.CityID = c.CityID
+ LEFT JOIN Geolocation.StateProvince sp ON c.StateProvinceID = sp.StateProvinceID
+ LEFT JOIN Geolocation.Country co ON sp.CountryID = co.CountryID
+ WHERE bpl.Coordinates IS NOT NULL
+ AND bpl.Coordinates.STDistance(@Origin) <= @RangeInMetres
+ ORDER BY bpl.Coordinates.STDistance(@Origin) ASC
+ """,
+ (post, location, city, stateProvince, country, distance) =>
+ MapBreweryRowWithDistance(
+ post,
+ location,
+ city,
+ stateProvince,
+ country,
+ distance,
+ coords
+ ),
+ new
+ {
+ coords.Latitude,
+ coords.Longitude,
+ RangeInMetres = rangeInMetres,
+ },
+ splitOn: "BreweryPostLocationID,CityID,StateProvinceID,CountryID,DistanceMetres"
+ );
}
- ///
+ /// Gets brewery posts that have location data.
public async Task> GetAllLocations()
{
await using DbConnection connection = await CreateConnection();
return await connection.QueryAsync<
- BreweryPost,
- BreweryPostLocation,
- City,
- StateProvince,
- Country,
- BreweryPost
- > (
- """
- SELECT
- bp.BreweryPostID,
- bp.BreweryName,
- bpl.BreweryPostLocationID,
- bpl.AddressLine1,
- bpl.AddressLine2,
- bpl.PostalCode,
- CONVERT(varbinary(max), bpl.Coordinates) AS Coordinates,
- c.CityID,
- c.CityName,
- sp.StateProvinceID,
- sp.StateProvinceName,
- sp.ISO3166_2 AS Iso31662,
- co.CountryID,
- co.CountryName,
- co.ISO3166_1 AS Iso31661
- FROM Brewery.BreweryPost bp
- INNER JOIN Brewery.BreweryPostLocation bpl ON bp.BreweryPostID = bpl.BreweryPostID
- LEFT JOIN Geolocation.City c ON bpl.CityID = c.CityID
- LEFT JOIN Geolocation.StateProvince sp ON c.StateProvinceID = sp.StateProvinceID
- LEFT JOIN Geolocation.Country co ON sp.CountryID = co.CountryID
- WHERE bpl.Coordinates IS NOT NULL
- ORDER BY bp.BreweryPostID ASC
- """,
- MapBreweryRow,
- splitOn: "BreweryPostLocationID,CityID,StateProvinceID,CountryID"
- );
+ BreweryPost,
+ BreweryPostLocation,
+ City,
+ StateProvince,
+ Country,
+ BreweryPost
+ >(
+ """
+ SELECT
+ bp.BreweryPostID,
+ bp.BreweryName,
+ bpl.BreweryPostLocationID,
+ bpl.AddressLine1,
+ bpl.AddressLine2,
+ bpl.PostalCode,
+ CONVERT(varbinary(max), bpl.Coordinates) AS Coordinates,
+ c.CityID,
+ c.CityName,
+ sp.StateProvinceID,
+ sp.StateProvinceName,
+ sp.ISO3166_2 AS Iso31662,
+ co.CountryID,
+ co.CountryName,
+ co.ISO3166_1 AS Iso31661
+ FROM Brewery.BreweryPost bp
+ INNER JOIN Brewery.BreweryPostLocation bpl ON bp.BreweryPostID = bpl.BreweryPostID
+ LEFT JOIN Geolocation.City c ON bpl.CityID = c.CityID
+ LEFT JOIN Geolocation.StateProvince sp ON c.StateProvinceID = sp.StateProvinceID
+ LEFT JOIN Geolocation.Country co ON sp.CountryID = co.CountryID
+ WHERE bpl.Coordinates IS NOT NULL
+ ORDER BY bp.BreweryPostID ASC
+ """,
+ MapBreweryRow,
+ splitOn: "BreweryPostLocationID,CityID,StateProvinceID,CountryID"
+ );
}
- ///
- public async Task UpdateAsync(BreweryPost brewery, CancellationToken cancellationToken = default)
+ /// Updates a brewery post and its location in a transaction.
+ public async Task UpdateAsync(
+ BreweryPost brewery,
+ CancellationToken cancellationToken = default
+ )
{
await using DbConnection connection = await CreateConnection();
- await using DbTransaction transaction = await connection.BeginTransactionAsync(cancellationToken);
+ await using DbTransaction transaction = await connection.BeginTransactionAsync(
+ cancellationToken
+ );
try
{
bool breweryExists =
await connection.ExecuteScalarAsync(
- new CommandDefinition(
- """
- SELECT 1 FROM
- Brewery.BreweryPost
- WHERE
- BreweryPostID = @BreweryPostId
- """,
- new { brewery.BreweryPostId },
- transaction,
- cancellationToken: cancellationToken
- )
+ new CommandDefinition(
+ """
+ SELECT 1 FROM
+ Brewery.BreweryPost
+ WHERE
+ BreweryPostID = @BreweryPostId
+ """,
+ new { brewery.BreweryPostId },
+ transaction,
+ cancellationToken: cancellationToken
)
- is not null;
+ )
+ is not null;
if (!breweryExists)
throw new NotFoundException("Brewery not found.");
@@ -267,14 +286,14 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory)
{
bool cityExists =
await connection.ExecuteScalarAsync(
- new CommandDefinition(
- "SELECT 1 FROM Geolocation.City WHERE CityID = @CityId",
- new { brewery.Location.CityId },
- transaction,
- cancellationToken: cancellationToken
- )
+ new CommandDefinition(
+ "SELECT 1 FROM Geolocation.City WHERE CityID = @CityId",
+ new { brewery.Location.CityId },
+ transaction,
+ cancellationToken: cancellationToken
)
- is not null;
+ )
+ is not null;
if (!cityExists)
throw new NotFoundException("City not found.");
@@ -328,18 +347,18 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory)
{
bool locationExists =
await connection.ExecuteScalarAsync(
- new CommandDefinition(
- """
- SELECT 1
- FROM Brewery.BreweryPostLocation
- WHERE BreweryPostID = @BreweryPostId
- """,
- new { brewery.BreweryPostId },
- transaction,
- cancellationToken: cancellationToken
- )
+ new CommandDefinition(
+ """
+ SELECT 1
+ FROM Brewery.BreweryPostLocation
+ WHERE BreweryPostID = @BreweryPostId
+ """,
+ new { brewery.BreweryPostId },
+ transaction,
+ cancellationToken: cancellationToken
)
- is not null;
+ )
+ is not null;
if (locationExists)
await connection.ExecuteAsync(
@@ -363,8 +382,8 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory)
brewery.Location.AddressLine1,
brewery.Location.AddressLine2,
brewery.Location.PostalCode,
- Latitude = brewery.Location.Coordinates?.Latitude,
- Longitude = brewery.Location.Coordinates?.Longitude,
+ brewery.Location.Coordinates?.Latitude,
+ brewery.Location.Coordinates?.Longitude,
Srid = GeographySrid,
},
transaction,
@@ -403,8 +422,8 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory)
brewery.Location.AddressLine1,
brewery.Location.AddressLine2,
brewery.Location.PostalCode,
- Latitude = brewery.Location.Coordinates?.Latitude,
- Longitude = brewery.Location.Coordinates?.Longitude,
+ brewery.Location.Coordinates?.Latitude,
+ brewery.Location.Coordinates?.Longitude,
Srid = GeographySrid,
},
transaction,
@@ -422,12 +441,12 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory)
}
return await GetByIdAsync(brewery.BreweryPostId, cancellationToken)
- ?? throw new InvalidOperationException(
- $"Brewery '{brewery.BreweryPostId}' was not found after a successful update."
- );
+ ?? throw new InvalidOperationException(
+ $"Brewery '{brewery.BreweryPostId}' was not found after a successful update."
+ );
}
- ///
+ /// Deletes the brewery post identified by .
public async Task DeleteAsync(Guid id)
{
await using DbConnection connection = await CreateConnection();
@@ -445,7 +464,7 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory)
throw new NotFoundException("Brewery not found.");
}
- ///
+ /// Creates a brewery post and its location in a transaction.
public async Task CreateAsync(BreweryPost brewery)
{
if (brewery.Location is null)
@@ -458,38 +477,38 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory)
{
bool userExists =
await connection.ExecuteScalarAsync(
- new CommandDefinition(
- """
- SELECT 1
- FROM
- Auth.UserAccount
- WHERE
- UserAccountID = @PostedById
- """,
- new { brewery.PostedById },
- transaction
- )
+ new CommandDefinition(
+ """
+ SELECT 1
+ FROM
+ Auth.UserAccount
+ WHERE
+ UserAccountID = @PostedById
+ """,
+ new { brewery.PostedById },
+ transaction
)
- is not null;
+ )
+ is not null;
if (!userExists)
throw new NotFoundException("User not found.");
bool cityExists =
await connection.ExecuteScalarAsync(
- new CommandDefinition(
- """
- SELECT 1
- FROM
- Geolocation.City
- WHERE
- CityID = @CityId
- """,
- new { brewery.Location.CityId },
- transaction
- )
+ new CommandDefinition(
+ """
+ SELECT 1
+ FROM
+ Geolocation.City
+ WHERE
+ CityID = @CityId
+ """,
+ new { brewery.Location.CityId },
+ transaction
)
- is not null;
+ )
+ is not null;
if (!cityExists)
throw new NotFoundException("City not found.");
@@ -543,8 +562,8 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory)
brewery.Location.AddressLine1,
brewery.Location.AddressLine2,
brewery.Location.PostalCode,
- Latitude = brewery.Location.Coordinates?.Latitude,
- Longitude = brewery.Location.Coordinates?.Longitude,
+ brewery.Location.Coordinates?.Latitude,
+ brewery.Location.Coordinates?.Longitude,
Srid = GeographySrid,
},
transaction
@@ -560,15 +579,8 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory)
}
}
-
///
- /// Composes a joined row's , ,
- /// , and fragments into a
- /// single populated . When the row has no location (a left-joined
- /// brewery with no address on file), Dapper passes for
- /// rather than a blank instance, since its identity column
- /// (BreweryPostLocationID) is NULL; the schema guarantees that whenever a location
- /// row was joined in, its City/StateProvince/Country chain was too.
+ /// Reconstructs a brewery post and its optional location hierarchy from a joined row.
///
private static BreweryPost MapBreweryRow(
BreweryPost post,
@@ -596,15 +608,8 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory)
return post;
}
- /// Dapper multi-mapping row for the DistanceMetres column computed by a range query.
- private sealed class DistanceRow
- {
- public double DistanceMetres { get; set; }
- }
-
///
- /// Composes a joined row via , then attaches the DB-computed distance
- /// from as the result's .
+ /// Reconstructs a brewery row and attaches its database-calculated distance.
///
private static BreweryPost MapBreweryRowWithDistance(
BreweryPost post,
@@ -620,4 +625,12 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory)
mapped.Distance = new DistanceInformation(origin, distance.DistanceMetres);
return mapped;
}
-}
\ No newline at end of file
+
+ ///
+ /// Holds the distance calculated by a proximity query.
+ ///
+ private sealed class DistanceRow
+ {
+ public double DistanceMetres { get; set; }
+ }
+}
diff --git a/web/backend/Features/Features.Breweries/Repository/CoordinateDataTypeHandler.cs b/web/backend/Features/Features.Breweries/Repository/CoordinateDataTypeHandler.cs
index 2068aceb..322272d1 100644
--- a/web/backend/Features/Features.Breweries/Repository/CoordinateDataTypeHandler.cs
+++ b/web/backend/Features/Features.Breweries/Repository/CoordinateDataTypeHandler.cs
@@ -7,9 +7,7 @@ using NetTopologySuite.IO;
namespace Features.Breweries.Repository;
///
-/// Dapper that deserializes a SQL Server GEOGRAPHY
-/// column (read as varbinary(max), see ) into a
-/// .
+/// Converts SQL Server geography values read as binary data into .
///
public sealed class CoordinateDataTypeHandler : SqlMapper.TypeHandler
{
diff --git a/web/backend/Features/Features.Breweries/Repository/IBreweryRepository.cs b/web/backend/Features/Features.Breweries/Repository/IBreweryRepository.cs
index c429616a..5821b54e 100644
--- a/web/backend/Features/Features.Breweries/Repository/IBreweryRepository.cs
+++ b/web/backend/Features/Features.Breweries/Repository/IBreweryRepository.cs
@@ -3,69 +3,80 @@ using Domain.Entities;
namespace Features.Breweries.Repository;
///
-/// Repository for CRUD operations on brewery post records.
+/// Defines persistence operations for brewery posts.
///
public interface IBreweryRepository
{
///
- /// Retrieves a brewery post by ID, joined to its location. Returns null if no brewery post
- /// exists with the given ID, or if it has no associated location.
+ /// Gets a brewery post and its location data by identifier.
///
Task GetByIdAsync(Guid id, CancellationToken cancellationToken = default);
///
- /// Retrieves the PostedById of the brewery post with the given ID, without joining its
- /// location. Returns null if no brewery post exists with the given ID.
+ /// Gets the identifier of the user who created a brewery post.
///
Task GetPostedByIdAsync(Guid id, CancellationToken cancellationToken = default);
///
- /// Retrieves all brewery posts, optionally paginated, ordered by creation date descending. Posts
- /// without a location are included, with left null.
+ /// Gets brewery posts ordered by creation time, with optional paging.
///
- /// Maximum number of rows to return. Unbounded if .
- /// Number of rows to skip. Treated as zero if .
+ ///
+ /// Limits returned posts; applies no limit.
+ ///
+ ///
+ /// Skips returned posts; skips none.
+ ///
Task> GetAllAsync(int? limit, int? offset);
///
- /// Retrieves brewery posts with a set location within of
- /// , nearest first. Posts without coordinates are excluded.
+ /// Gets located brewery posts within a radius, ordered by distance.
///
- /// The origin point to measure distance from.
- /// The maximum distance, in metres, from .
- Task> GetAllLocationsWithinRange(CoordinateData coords, double rangeInMetres);
+ ///
+ /// Sets the point from which distance is measured.
+ ///
+ ///
+ /// Sets the maximum search distance in metres.
+ ///
+ Task> GetAllLocationsWithinRange(
+ CoordinateData coords,
+ double rangeInMetres
+ );
///
- /// Retrieves all brewery posts that have a set location, ordered by BreweryPostId. Posts
- /// without coordinates are excluded.
+ /// Gets brewery posts with location data.
///
Task> GetAllLocations();
///
- /// Updates a brewery post's name and description, and upserts or clears its location, enforcing
- /// optimistic concurrency via 's RowVersion: the update is rejected
- /// if the row was modified since RowVersion was read. When 's
- /// Location is null, any existing location for the brewery is removed.
- /// Returns the freshly persisted brewery, including its new RowVersion.
+ /// Saves a brewery post and its location using optimistic concurrency.
///
///
- /// Thrown when no brewery exists with the given ID, or the given location's CityId does not
- /// exist.
+ /// Thrown if the brewery post or its requested city cannot be found.
///
///
- /// Thrown when 's RowVersion no longer matches the stored row (it was
- /// modified by another request since it was last read).
+ /// Thrown if the supplied row version is no longer current.
///
- Task UpdateAsync(BreweryPost brewery, CancellationToken cancellationToken = default);
+ Task UpdateAsync(
+ BreweryPost brewery,
+ CancellationToken cancellationToken = default
+ );
- /// Deletes a brewery post by ID. Its location and photos are removed via cascading foreign keys.
- /// Thrown when no brewery exists with the given .
+ ///
+ /// Deletes a brewery post and its dependent records.
+ ///
+ ///
+ /// Thrown if the brewery post cannot be found.
+ ///
Task DeleteAsync(Guid id);
- /// Creates a new brewery post, including its location details.
- /// Thrown when has no Location.
+ ///
+ /// Creates a brewery post and its required location.
+ ///
+ ///
+ /// Thrown if has no location.
+ ///
///
- /// Thrown when 's PostedById or Location.CityId does not exist.
+ /// Thrown if the posting user or location city cannot be found.
///
Task CreateAsync(BreweryPost brewery);
-}
\ No newline at end of file
+}