mirror of
https://github.com/aaronpo97/the-biergarten-app.git
synced 2026-10-14 17:54:42 +00:00
Update doc comments in Features.Breweries source (#279)
This commit is contained in:
@@ -17,10 +17,8 @@ public class CreateBreweryHandlerTests
|
|||||||
_handler = new CreateBreweryHandler(_repoMock.Object);
|
_handler = new CreateBreweryHandler(_repoMock.Object);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static CreateBreweryLocation ValidLocation()
|
private static CreateBreweryLocation ValidLocation() =>
|
||||||
{
|
new(Guid.NewGuid(), "123 Main St", null, "12345", null);
|
||||||
return new CreateBreweryLocation(Guid.NewGuid(), "123 Main St", null, "12345", null);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Handle_PersistsEntity_WithNewIdsAndCreatedAt()
|
public async Task Handle_PersistsEntity_WithNewIdsAndCreatedAt()
|
||||||
|
|||||||
@@ -8,18 +8,18 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1"/>
|
||||||
<PackageReference Include="xunit" Version="2.9.2" />
|
<PackageReference Include="xunit" Version="2.9.2"/>
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2"/>
|
||||||
<PackageReference Include="Moq" Version="4.20.72" />
|
<PackageReference Include="Moq" Version="4.20.72"/>
|
||||||
<PackageReference Include="FluentAssertions" Version="7.2.0" />
|
<PackageReference Include="FluentAssertions" Version="7.2.0"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Using Include="Xunit" />
|
<Using Include="Xunit"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\Features.Breweries\Features.Breweries.csproj" />
|
<ProjectReference Include="..\Features.Breweries\Features.Breweries.csproj"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -36,8 +36,10 @@ public class GetAllBreweryLocationsHandlerTests
|
|||||||
{
|
{
|
||||||
BreweryPost[] breweries =
|
BreweryPost[] breweries =
|
||||||
[
|
[
|
||||||
new BreweryPost { BreweryPostId = Guid.NewGuid(), BreweryName = "A" },
|
new()
|
||||||
new BreweryPost { BreweryPostId = Guid.NewGuid(), BreweryName = "B" },
|
{ BreweryPostId = Guid.NewGuid(), BreweryName = "A" },
|
||||||
|
new()
|
||||||
|
{ BreweryPostId = Guid.NewGuid(), BreweryName = "B" },
|
||||||
];
|
];
|
||||||
_repoMock.Setup(r => r.GetAllLocations()).ReturnsAsync(breweries);
|
_repoMock.Setup(r => r.GetAllLocations()).ReturnsAsync(breweries);
|
||||||
|
|
||||||
|
|||||||
@@ -50,8 +50,10 @@ public class GetBreweryLocationsWithinRangeHandlerTests
|
|||||||
{
|
{
|
||||||
BreweryPost[] breweries =
|
BreweryPost[] breweries =
|
||||||
[
|
[
|
||||||
new BreweryPost { BreweryPostId = Guid.NewGuid(), BreweryName = "Nearby A" },
|
new()
|
||||||
new BreweryPost { BreweryPostId = Guid.NewGuid(), BreweryName = "Nearby B" },
|
{ BreweryPostId = Guid.NewGuid(), BreweryName = "Nearby A" },
|
||||||
|
new()
|
||||||
|
{ BreweryPostId = Guid.NewGuid(), BreweryName = "Nearby B" },
|
||||||
];
|
];
|
||||||
_repoMock
|
_repoMock
|
||||||
.Setup(r => r.GetAllLocationsWithinRange(It.IsAny<CoordinateData>(), It.IsAny<double>()))
|
.Setup(r => r.GetAllLocationsWithinRange(It.IsAny<CoordinateData>(), It.IsAny<double>()))
|
||||||
|
|||||||
@@ -4,8 +4,12 @@ using MediatR;
|
|||||||
|
|
||||||
namespace Features.Breweries.Commands.CreateBrewery;
|
namespace Features.Breweries.Commands.CreateBrewery;
|
||||||
|
|
||||||
/// <summary>Location details for a brewery being created.</summary>
|
/// <summary>
|
||||||
/// <param name="Coordinates">Raw binary representation of the brewery's geographic coordinates.</param>
|
/// Describes the location supplied when creating a brewery.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="Coordinates">
|
||||||
|
/// Serialized geographic coordinates for the brewery.
|
||||||
|
/// </param>
|
||||||
public record CreateBreweryLocation(
|
public record CreateBreweryLocation(
|
||||||
Guid CityId,
|
Guid CityId,
|
||||||
string AddressLine1,
|
string AddressLine1,
|
||||||
@@ -15,7 +19,7 @@ public record CreateBreweryLocation(
|
|||||||
);
|
);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a new brewery post. Bound directly from the request body of <c>POST /api/brewery</c>.
|
/// Represents the application request used to create a brewery post.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public record CreateBreweryCommand(
|
public record CreateBreweryCommand(
|
||||||
Guid PostedById,
|
Guid PostedById,
|
||||||
@@ -24,7 +28,9 @@ public record CreateBreweryCommand(
|
|||||||
CreateBreweryLocation Location
|
CreateBreweryLocation Location
|
||||||
) : IRequest<BreweryDto>;
|
) : IRequest<BreweryDto>;
|
||||||
|
|
||||||
/// <summary>Request body for <c>POST /api/brewery</c>. <c>PostedById</c> comes from the caller's JWT.</summary>
|
/// <summary>
|
||||||
|
/// Defines the client-supplied body for creating a brewery post.
|
||||||
|
/// </summary>
|
||||||
public record CreateBreweryRequest(
|
public record CreateBreweryRequest(
|
||||||
string BreweryName,
|
string BreweryName,
|
||||||
string Description,
|
string Description,
|
||||||
|
|||||||
@@ -5,13 +5,17 @@ using MediatR;
|
|||||||
|
|
||||||
namespace Features.Breweries.Commands.CreateBrewery;
|
namespace Features.Breweries.Commands.CreateBrewery;
|
||||||
|
|
||||||
/// <summary>Handles <see cref="CreateBreweryCommand" /> by persisting a new brewery post.</summary>
|
/// <summary>
|
||||||
|
/// Persists brewery posts created through <see cref="CreateBreweryCommand" />.
|
||||||
|
/// </summary>
|
||||||
public class CreateBreweryHandler(IBreweryRepository repository)
|
public class CreateBreweryHandler(IBreweryRepository repository)
|
||||||
: IRequestHandler<CreateBreweryCommand, BreweryDto>
|
: IRequestHandler<CreateBreweryCommand, BreweryDto>
|
||||||
{
|
{
|
||||||
/// <summary>Creates a new brewery post, generating new identifiers for the post and its location.</summary>
|
/// <summary>
|
||||||
|
/// Creates the post and its associated location with new identifiers.
|
||||||
|
/// </summary>
|
||||||
/// <exception cref="Domain.Exceptions.NotFoundException">
|
/// <exception cref="Domain.Exceptions.NotFoundException">
|
||||||
/// Thrown when <paramref name="request" />'s <c>PostedById</c> or <c>Location.CityId</c> does not exist.
|
/// Thrown if the posting user or the selected city cannot be found.
|
||||||
/// </exception>
|
/// </exception>
|
||||||
public async Task<BreweryDto> Handle(
|
public async Task<BreweryDto> Handle(
|
||||||
CreateBreweryCommand request,
|
CreateBreweryCommand request,
|
||||||
@@ -38,10 +42,6 @@ public class CreateBreweryHandler(IBreweryRepository repository)
|
|||||||
|
|
||||||
await repository.CreateAsync(entity);
|
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 =
|
BreweryPost created =
|
||||||
await repository.GetByIdAsync(entity.BreweryPostId, CancellationToken.None)
|
await repository.GetByIdAsync(entity.BreweryPostId, CancellationToken.None)
|
||||||
?? throw new InvalidOperationException(
|
?? throw new InvalidOperationException(
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ using FluentValidation;
|
|||||||
|
|
||||||
namespace Features.Breweries.Commands.CreateBrewery;
|
namespace Features.Breweries.Commands.CreateBrewery;
|
||||||
|
|
||||||
/// <summary>Validates <see cref="CreateBreweryCommand" />.</summary>
|
/// <summary>
|
||||||
|
/// Applies creation rules to <see cref="CreateBreweryCommand" />.
|
||||||
|
/// </summary>
|
||||||
public class CreateBreweryValidator : AbstractValidator<CreateBreweryCommand>
|
public class CreateBreweryValidator : AbstractValidator<CreateBreweryCommand>
|
||||||
{
|
{
|
||||||
public CreateBreweryValidator()
|
public CreateBreweryValidator()
|
||||||
@@ -27,7 +29,6 @@ public class CreateBreweryValidator : AbstractValidator<CreateBreweryCommand>
|
|||||||
|
|
||||||
RuleFor(x => x.Location.AddressLine1)
|
RuleFor(x => x.Location.AddressLine1)
|
||||||
.NotEmpty()
|
.NotEmpty()
|
||||||
.When(x => x.Location is not null)
|
|
||||||
.WithMessage("Address line 1 is required.")
|
.WithMessage("Address line 1 is required.")
|
||||||
.MaximumLength(256)
|
.MaximumLength(256)
|
||||||
.WithMessage("Address line 1 cannot exceed 256 characters.");
|
.WithMessage("Address line 1 cannot exceed 256 characters.");
|
||||||
|
|||||||
@@ -2,5 +2,7 @@ using MediatR;
|
|||||||
|
|
||||||
namespace Features.Breweries.Commands.DeleteBrewery;
|
namespace Features.Breweries.Commands.DeleteBrewery;
|
||||||
|
|
||||||
/// <summary>Deletes a brewery post. <see cref="BreweryPostId" /> is bound from the route of <c>DELETE /api/brewery/{id}</c>.</summary>
|
/// <summary>
|
||||||
|
/// Represents a request to delete a brewery post on behalf of a caller.
|
||||||
|
/// </summary>
|
||||||
public record DeleteBreweryCommand(Guid BreweryPostId, Guid RequestingUserId) : IRequest;
|
public record DeleteBreweryCommand(Guid BreweryPostId, Guid RequestingUserId) : IRequest;
|
||||||
|
|||||||
@@ -3,15 +3,20 @@ using MediatR;
|
|||||||
|
|
||||||
namespace Features.Breweries.Commands.DeleteBrewery;
|
namespace Features.Breweries.Commands.DeleteBrewery;
|
||||||
|
|
||||||
/// <summary>Handles <see cref="DeleteBreweryCommand" /> by deleting the corresponding brewery post.</summary>
|
/// <summary>
|
||||||
|
/// Processes <see cref="DeleteBreweryCommand" /> requests.
|
||||||
|
/// </summary>
|
||||||
public class DeleteBreweryHandler(IBreweryRepository repository)
|
public class DeleteBreweryHandler(IBreweryRepository repository)
|
||||||
: IRequestHandler<DeleteBreweryCommand>
|
: IRequestHandler<DeleteBreweryCommand>
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Confirms that the caller owns the post before deleting it.
|
||||||
|
/// </summary>
|
||||||
/// <exception cref="Domain.Exceptions.NotFoundException">
|
/// <exception cref="Domain.Exceptions.NotFoundException">
|
||||||
/// Thrown when no brewery exists with <paramref name="request" />'s <c>BreweryPostId</c>.
|
/// Thrown if the brewery post does not exist.
|
||||||
/// </exception>
|
/// </exception>
|
||||||
/// <exception cref="Domain.Exceptions.ForbiddenException">
|
/// <exception cref="Domain.Exceptions.ForbiddenException">
|
||||||
/// Thrown when <paramref name="request" />'s <c>RequestingUserId</c> did not post the brewery.
|
/// Thrown if the caller did not create the brewery post.
|
||||||
/// </exception>
|
/// </exception>
|
||||||
public async Task Handle(DeleteBreweryCommand request, CancellationToken cancellationToken)
|
public async Task Handle(DeleteBreweryCommand request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -4,11 +4,15 @@ using MediatR;
|
|||||||
|
|
||||||
namespace Features.Breweries.Commands.UpdateBrewery;
|
namespace Features.Breweries.Commands.UpdateBrewery;
|
||||||
|
|
||||||
/// <summary>Location details for a brewery being updated.</summary>
|
/// <summary>
|
||||||
|
/// Describes the location supplied when updating a brewery.
|
||||||
|
/// </summary>
|
||||||
/// <param name="BreweryPostLocationId">
|
/// <param name="BreweryPostLocationId">
|
||||||
/// 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.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="Coordinates">
|
||||||
|
/// Serialized geographic coordinates for the brewery.
|
||||||
/// </param>
|
/// </param>
|
||||||
/// <param name="Coordinates">Raw binary representation of the brewery's geographic coordinates.</param>
|
|
||||||
public record UpdateBreweryLocation(
|
public record UpdateBreweryLocation(
|
||||||
Guid BreweryPostLocationId,
|
Guid BreweryPostLocationId,
|
||||||
Guid CityId,
|
Guid CityId,
|
||||||
@@ -19,9 +23,8 @@ public record UpdateBreweryLocation(
|
|||||||
);
|
);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Updates an existing brewery post. A <c>null</c> <see cref="Location" /> clears the brewery's location.
|
/// Represents a concurrency-protected change to an existing brewery post.
|
||||||
/// <see cref="RowVersion" /> must be the row-version last read for this brewery (e.g. from a prior
|
/// A <see langword="null" /> <see cref="Location" /> removes the existing location.
|
||||||
/// <c>GET</c>); the update is rejected with a <c>409 Conflict</c> if the brewery was modified since then.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public record UpdateBreweryCommand(
|
public record UpdateBreweryCommand(
|
||||||
Guid BreweryPostId,
|
Guid BreweryPostId,
|
||||||
@@ -32,7 +35,9 @@ public record UpdateBreweryCommand(
|
|||||||
UpdateBreweryLocation? Location
|
UpdateBreweryLocation? Location
|
||||||
) : IRequest<BreweryDto>;
|
) : IRequest<BreweryDto>;
|
||||||
|
|
||||||
/// <summary>Request body for <c>PUT /api/brewery/{id}</c>.</summary>
|
/// <summary>
|
||||||
|
/// Defines the client-supplied body for updating a brewery post.
|
||||||
|
/// </summary>
|
||||||
public record UpdateBreweryRequest(
|
public record UpdateBreweryRequest(
|
||||||
Guid BreweryPostId,
|
Guid BreweryPostId,
|
||||||
byte[] RowVersion,
|
byte[] RowVersion,
|
||||||
@@ -40,4 +45,3 @@ public record UpdateBreweryRequest(
|
|||||||
string Description,
|
string Description,
|
||||||
UpdateBreweryLocation? Location
|
UpdateBreweryLocation? Location
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -5,21 +5,20 @@ using MediatR;
|
|||||||
|
|
||||||
namespace Features.Breweries.Commands.UpdateBrewery;
|
namespace Features.Breweries.Commands.UpdateBrewery;
|
||||||
|
|
||||||
/// <summary>Handles <see cref="UpdateBreweryCommand" /> by persisting changes to an existing brewery post.</summary>
|
/// <summary>
|
||||||
|
/// Persists changes requested through <see cref="UpdateBreweryCommand" />.
|
||||||
|
/// </summary>
|
||||||
public class UpdateBreweryHandler(IBreweryRepository repository)
|
public class UpdateBreweryHandler(IBreweryRepository repository)
|
||||||
: IRequestHandler<UpdateBreweryCommand, BreweryDto>
|
: IRequestHandler<UpdateBreweryCommand, BreweryDto>
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Updates an existing brewery post. If <paramref name="request" /> has no <c>Location</c>,
|
/// Updates the post after confirming that the caller owns it.
|
||||||
/// the brewery's location is cleared.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <exception cref="Domain.Exceptions.NotFoundException">
|
/// <exception cref="Domain.Exceptions.NotFoundException">
|
||||||
/// Thrown when no brewery exists with <paramref name="request" />'s <c>BreweryPostId</c>, or its
|
/// Thrown if the brewery post or its requested city cannot be found.
|
||||||
/// location's <c>CityId</c> does not exist.
|
|
||||||
/// </exception>
|
/// </exception>
|
||||||
/// <exception cref="Domain.Exceptions.ConflictException">
|
/// <exception cref="Domain.Exceptions.ConflictException">
|
||||||
/// Thrown when the brewery was modified by another request since <paramref name="request" />.
|
/// Thrown if the post has changed since its supplied row version was read.
|
||||||
/// <c>RowVersion</c> was read.
|
|
||||||
/// </exception>
|
/// </exception>
|
||||||
public async Task<BreweryDto> Handle(
|
public async Task<BreweryDto> Handle(
|
||||||
UpdateBreweryCommand request,
|
UpdateBreweryCommand request,
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ using FluentValidation;
|
|||||||
|
|
||||||
namespace Features.Breweries.Commands.UpdateBrewery;
|
namespace Features.Breweries.Commands.UpdateBrewery;
|
||||||
|
|
||||||
/// <summary>Validates <see cref="UpdateBreweryCommand" />.</summary>
|
/// <summary>
|
||||||
|
/// Applies update rules to <see cref="UpdateBreweryCommand" />.
|
||||||
|
/// </summary>
|
||||||
public class UpdateBreweryValidator : AbstractValidator<UpdateBreweryCommand>
|
public class UpdateBreweryValidator : AbstractValidator<UpdateBreweryCommand>
|
||||||
{
|
{
|
||||||
public UpdateBreweryValidator()
|
public UpdateBreweryValidator()
|
||||||
|
|||||||
@@ -13,19 +13,25 @@ using Shared.Contracts;
|
|||||||
|
|
||||||
namespace Features.Breweries.Controllers;
|
namespace Features.Breweries.Controllers;
|
||||||
|
|
||||||
/// <summary>Exposes CRUD operations for brewery posts.</summary>
|
/// <summary>
|
||||||
|
/// Provides HTTP endpoints for reading and managing brewery posts.
|
||||||
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// Requires JWT authentication by default; <see cref="GetById" /> and <see cref="GetAll" /> opt out via
|
/// Requests require JWT authentication unless the endpoint allows anonymous access.
|
||||||
/// <c>[AllowAnonymous]</c>.
|
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
|
///
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("api/[controller]")]
|
[Route("api/[controller]")]
|
||||||
[Authorize(AuthenticationSchemes = "JWT")]
|
[Authorize(AuthenticationSchemes = "JWT")]
|
||||||
public class BreweryController(IMediator mediator) : ControllerBase
|
public class BreweryController(IMediator mediator) : ControllerBase
|
||||||
{
|
{
|
||||||
/// <summary>Retrieves a single brewery post by ID.</summary>
|
/// <summary>
|
||||||
/// <remarks>Allows anonymous access.</remarks>
|
/// Returns the brewery post identified by <paramref name="id" />.
|
||||||
/// <returns><c>200 OK</c> with the brewery if found; otherwise <c>404 Not Found</c>.</returns>
|
/// </summary>
|
||||||
|
/// <remarks>This endpoint permits anonymous access.</remarks>
|
||||||
|
/// <returns>
|
||||||
|
/// A successful response containing the post, or <c>404 Not Found</c> when it is absent.
|
||||||
|
/// </returns>
|
||||||
[AllowAnonymous]
|
[AllowAnonymous]
|
||||||
[HttpGet("{id:guid}")]
|
[HttpGet("{id:guid}")]
|
||||||
public async Task<ActionResult<ResponseBody<BreweryDto>>> GetById(Guid id)
|
public async Task<ActionResult<ResponseBody<BreweryDto>>> GetById(Guid id)
|
||||||
@@ -43,10 +49,16 @@ public class BreweryController(IMediator mediator) : ControllerBase
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Retrieves brewery posts, newest first.</summary>
|
/// <summary>
|
||||||
/// <param name="limit">Maximum number of breweries to return. Unbounded if <see langword="null"/>.</param>
|
/// Returns brewery posts in reverse chronological order.
|
||||||
/// <param name="offset">Number of breweries to skip. Treated as zero if <see langword="null"/>.</param>
|
/// </summary>
|
||||||
/// <remarks>Allows anonymous access.</remarks>
|
/// <param name="limit">
|
||||||
|
/// Limits the number of returned posts; <see langword="null" /> leaves it unlimited.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="offset">
|
||||||
|
/// Skips this many posts; <see langword="null" /> means none.
|
||||||
|
/// </param>
|
||||||
|
/// <remarks>This endpoint permits anonymous access.</remarks>
|
||||||
[AllowAnonymous]
|
[AllowAnonymous]
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
public async Task<ActionResult<ResponseBody<IEnumerable<BreweryDto>>>> GetAll(
|
public async Task<ActionResult<ResponseBody<IEnumerable<BreweryDto>>>> GetAll(
|
||||||
@@ -66,11 +78,15 @@ public class BreweryController(IMediator mediator) : ControllerBase
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Retrieves all brewery posts that have a set location.</summary>
|
/// <summary>
|
||||||
/// <remarks>Allows anonymous access.</remarks>
|
/// Returns all brewery posts with an associated location.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>This endpoint permits anonymous access.</remarks>
|
||||||
[AllowAnonymous]
|
[AllowAnonymous]
|
||||||
[HttpGet("locations")]
|
[HttpGet("locations")]
|
||||||
public async Task<ActionResult<ResponseBody<IEnumerable<BreweryWithLocationDto>>>> GetAllLocations()
|
public async Task<
|
||||||
|
ActionResult<ResponseBody<IEnumerable<BreweryWithLocationDto>>>
|
||||||
|
> GetAllLocations()
|
||||||
{
|
{
|
||||||
IEnumerable<BreweryWithLocationDto> breweries = await mediator.Send(
|
IEnumerable<BreweryWithLocationDto> breweries = await mediator.Send(
|
||||||
new GetAllBreweryLocationsQuery()
|
new GetAllBreweryLocationsQuery()
|
||||||
@@ -84,14 +100,24 @@ public class BreweryController(IMediator mediator) : ControllerBase
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Retrieves brewery posts with a set location within range of the given coordinates, nearest first.</summary>
|
/// <summary>
|
||||||
/// <param name="latitude">The origin latitude, in decimal degrees.</param>
|
/// Returns located brewery posts within the requested radius, ordered by proximity.
|
||||||
/// <param name="longitude">The origin longitude, in decimal degrees.</param>
|
/// </summary>
|
||||||
/// <param name="rangeInMetres">The maximum distance, in metres, from the origin coordinates.</param>
|
/// <param name="latitude">
|
||||||
/// <remarks>Allows anonymous access.</remarks>
|
/// Identifies the origin latitude in decimal degrees.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="longitude">
|
||||||
|
/// Identifies the origin longitude in decimal degrees.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="rangeInMetres">
|
||||||
|
/// Sets the maximum distance from the origin in metres.
|
||||||
|
/// </param>
|
||||||
|
/// <remarks>This endpoint permits anonymous access.</remarks>
|
||||||
[AllowAnonymous]
|
[AllowAnonymous]
|
||||||
[HttpGet("locations/nearby")]
|
[HttpGet("locations/nearby")]
|
||||||
public async Task<ActionResult<ResponseBody<IEnumerable<BreweryWithLocationDto>>>> GetLocationsWithinRange(
|
public async Task<
|
||||||
|
ActionResult<ResponseBody<IEnumerable<BreweryWithLocationDto>>>
|
||||||
|
> GetLocationsWithinRange(
|
||||||
[FromQuery] double latitude,
|
[FromQuery] double latitude,
|
||||||
[FromQuery] double longitude,
|
[FromQuery] double longitude,
|
||||||
[FromQuery] double rangeInMetres
|
[FromQuery] double rangeInMetres
|
||||||
@@ -109,8 +135,10 @@ public class BreweryController(IMediator mediator) : ControllerBase
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Creates a new brewery post.</summary>
|
/// <summary>
|
||||||
/// <returns><c>201 Created</c> with the newly created brewery.</returns>
|
/// Creates a brewery post for the authenticated user.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>A <c>201 Created</c> response containing the new post.</returns>
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
public async Task<ActionResult<ResponseBody<BreweryDto>>> Create(
|
public async Task<ActionResult<ResponseBody<BreweryDto>>> Create(
|
||||||
[FromBody] CreateBreweryRequest request
|
[FromBody] CreateBreweryRequest request
|
||||||
@@ -134,12 +162,15 @@ public class BreweryController(IMediator mediator) : ControllerBase
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Updates an existing brewery post.</summary>
|
/// <summary>
|
||||||
/// <param name="id">Must match <paramref name="request" />'s <c>BreweryPostId</c>.</param>
|
/// Updates a brewery post owned by the authenticated user.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">
|
||||||
|
/// Must equal the brewery post identifier in <paramref name="request" />.
|
||||||
|
/// </param>
|
||||||
/// <returns>
|
/// <returns>
|
||||||
/// <c>200 OK</c> with the updated brewery; <c>400 Bad Request</c> if the route ID does not match the
|
/// A successful response containing the updated post, or an error for an invalid ID,
|
||||||
/// payload ID; <c>404 Not Found</c> if the brewery or its <c>CityId</c> does not exist; or
|
/// missing resource, or stale row version.
|
||||||
/// <c>409 Conflict</c> if the brewery was modified since <c>request.RowVersion</c> was read.
|
|
||||||
/// </returns>
|
/// </returns>
|
||||||
[HttpPut("{id:guid}")]
|
[HttpPut("{id:guid}")]
|
||||||
public async Task<ActionResult<ResponseBody<BreweryDto>>> Update(
|
public async Task<ActionResult<ResponseBody<BreweryDto>>> Update(
|
||||||
@@ -170,8 +201,12 @@ public class BreweryController(IMediator mediator) : ControllerBase
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Deletes a brewery post.</summary>
|
/// <summary>
|
||||||
/// <returns><c>200 OK</c> confirming the deletion; <c>404 Not Found</c> if no brewery exists with <paramref name="id" />.</returns>
|
/// Deletes a brewery post owned by the authenticated user.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>
|
||||||
|
/// A confirmation response, or <c>404 Not Found</c> if the post is absent.
|
||||||
|
/// </returns>
|
||||||
[HttpDelete("{id:guid}")]
|
[HttpDelete("{id:guid}")]
|
||||||
public async Task<ActionResult<ResponseBody>> Delete(Guid id)
|
public async Task<ActionResult<ResponseBody>> Delete(Guid id)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -3,11 +3,15 @@ using Microsoft.Extensions.DependencyInjection;
|
|||||||
|
|
||||||
namespace Features.Breweries.DependencyInjection;
|
namespace Features.Breweries.DependencyInjection;
|
||||||
|
|
||||||
/// <summary>Registers the services required by the Breweries feature.</summary>
|
/// <summary>
|
||||||
|
/// Adds Breweries feature services to the dependency-injection container.
|
||||||
|
/// </summary>
|
||||||
public static class FeaturesBreweriesServiceCollectionExtensions
|
public static class FeaturesBreweriesServiceCollectionExtensions
|
||||||
{
|
{
|
||||||
/// <summary>Registers <see cref="IBreweryRepository" /> and its implementation.</summary>
|
/// <summary>
|
||||||
/// <returns><paramref name="services" />, for chaining.</returns>
|
/// Registers the repository services used by the Breweries feature.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The supplied service collection.</returns>
|
||||||
public static IServiceCollection AddFeaturesBreweries(this IServiceCollection services)
|
public static IServiceCollection AddFeaturesBreweries(this IServiceCollection services)
|
||||||
{
|
{
|
||||||
services.AddScoped<IBreweryRepository, BreweryRepository>();
|
services.AddScoped<IBreweryRepository, BreweryRepository>();
|
||||||
|
|||||||
@@ -2,8 +2,12 @@ using Domain.Entities;
|
|||||||
|
|
||||||
namespace Features.Breweries.Dtos;
|
namespace Features.Breweries.Dtos;
|
||||||
|
|
||||||
/// <summary>Location details of a brewery, as returned by the API.</summary>
|
/// <summary>
|
||||||
/// <param name="Coordinates">The brewery's geographic coordinates, in a raw binary representation.</param>
|
/// Represents location data returned for a brewery post.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="Coordinates">
|
||||||
|
/// Contains the serialized geographic coordinates, when available.
|
||||||
|
/// </param>
|
||||||
public record BreweryLocationDto(
|
public record BreweryLocationDto(
|
||||||
Guid BreweryPostLocationId,
|
Guid BreweryPostLocationId,
|
||||||
Guid CityId,
|
Guid CityId,
|
||||||
@@ -18,10 +22,18 @@ public record BreweryLocationDto(
|
|||||||
CoordinateData? Coordinates
|
CoordinateData? Coordinates
|
||||||
);
|
);
|
||||||
|
|
||||||
/// <summary>A brewery post, as returned by the API.</summary>
|
/// <summary>
|
||||||
/// <param name="UpdatedAt">The date and time of the last edit, or <see langword="null"/> if never edited.</param>
|
/// Represents a brewery post returned by the API.
|
||||||
/// <param name="RowVersion">The row-version/concurrency token used to detect conflicting concurrent updates.</param>
|
/// </summary>
|
||||||
/// <param name="Location">The brewery's location, or <see langword="null"/> if none has been set.</param>
|
/// <param name="UpdatedAt">
|
||||||
|
/// Records the most recent update time, if the post has been updated.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="RowVersion">
|
||||||
|
/// Provides the concurrency token required for updates.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="Location">
|
||||||
|
/// Contains the post's location, when one is available.
|
||||||
|
/// </param>
|
||||||
public record BreweryDto(
|
public record BreweryDto(
|
||||||
Guid BreweryPostId,
|
Guid BreweryPostId,
|
||||||
Guid PostedById,
|
Guid PostedById,
|
||||||
@@ -32,9 +44,12 @@ public record BreweryDto(
|
|||||||
byte[]? RowVersion,
|
byte[]? RowVersion,
|
||||||
BreweryLocationDto? Location
|
BreweryLocationDto? Location
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the location-focused result returned by brewery searches.
|
||||||
|
/// </summary>
|
||||||
/// <param name="DistanceMetres">
|
/// <param name="DistanceMetres">
|
||||||
/// The distance, in metres, from a query's origin point, or <see langword="null"/> for queries with no
|
/// Contains the distance from the search origin in metres, when applicable.
|
||||||
/// origin point (e.g. <c>GET /api/brewery/locations</c>).
|
|
||||||
/// </param>
|
/// </param>
|
||||||
public record BreweryWithLocationDto(
|
public record BreweryWithLocationDto(
|
||||||
Guid BreweryPostId,
|
Guid BreweryPostId,
|
||||||
|
|||||||
@@ -2,74 +2,62 @@ using Domain.Entities;
|
|||||||
|
|
||||||
namespace Features.Breweries.Dtos;
|
namespace Features.Breweries.Dtos;
|
||||||
|
|
||||||
/// <summary>Maps <see cref="BreweryPost" /> domain entities to <see cref="BreweryDto" />.</summary>
|
/// <summary>
|
||||||
|
/// Converts brewery domain entities into API response models.
|
||||||
|
/// </summary>
|
||||||
public static class BreweryDtoMapper
|
public static class BreweryDtoMapper
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Converts a <see cref="BreweryPost" /> to its DTO representation, including its
|
/// Converts a brewery post into its complete API representation.
|
||||||
/// <see cref="BreweryDto.Location" /> if the entity has one.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static BreweryDto ToDto(this BreweryPost brewery)
|
public static BreweryDto ToDto(this BreweryPost brewery) =>
|
||||||
{
|
new(
|
||||||
return new BreweryDto(
|
brewery.BreweryPostId,
|
||||||
BreweryPostId: brewery.BreweryPostId,
|
brewery.PostedById,
|
||||||
PostedById: brewery.PostedById,
|
brewery.BreweryName,
|
||||||
BreweryName: brewery.BreweryName,
|
brewery.Description,
|
||||||
Description: brewery.Description,
|
brewery.CreatedAt,
|
||||||
CreatedAt: brewery.CreatedAt,
|
brewery.UpdatedAt,
|
||||||
UpdatedAt: brewery.UpdatedAt,
|
brewery.RowVersion,
|
||||||
RowVersion: brewery.RowVersion,
|
brewery.Location is null
|
||||||
Location: brewery.Location is null
|
|
||||||
? null
|
? null
|
||||||
: new BreweryLocationDto(
|
: new BreweryLocationDto(
|
||||||
BreweryPostLocationId: brewery.Location.BreweryPostLocationId,
|
brewery.Location.BreweryPostLocationId,
|
||||||
CityId: brewery.Location.CityId,
|
brewery.Location.CityId,
|
||||||
CityName: brewery.Location.City?.CityName ?? string.Empty,
|
brewery.Location.City?.CityName ?? string.Empty,
|
||||||
StateProvinceName: brewery.Location.City?.StateProvince?.StateProvinceName
|
brewery.Location.City?.StateProvince?.StateProvinceName ?? string.Empty,
|
||||||
?? string.Empty,
|
brewery.Location.City?.StateProvince?.Iso31662 ?? string.Empty,
|
||||||
StateProvinceCode: brewery.Location.City?.StateProvince?.Iso31662
|
brewery.Location.City?.StateProvince?.Country?.CountryName ?? string.Empty,
|
||||||
?? string.Empty,
|
brewery.Location.City?.StateProvince?.Country?.Iso31661 ?? string.Empty,
|
||||||
CountryName: brewery.Location.City?.StateProvince?.Country?.CountryName
|
brewery.Location.AddressLine1,
|
||||||
?? string.Empty,
|
brewery.Location.AddressLine2,
|
||||||
CountryCode: brewery.Location.City?.StateProvince?.Country?.Iso31661
|
brewery.Location.PostalCode,
|
||||||
?? string.Empty,
|
brewery.Location.Coordinates
|
||||||
AddressLine1: brewery.Location.AddressLine1,
|
|
||||||
AddressLine2: brewery.Location.AddressLine2,
|
|
||||||
PostalCode: brewery.Location.PostalCode,
|
|
||||||
Coordinates: brewery.Location.Coordinates
|
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Converts a <see cref="BreweryPost" /> to its simplified DTO representation, used for
|
/// Converts a brewery post into its location-search representation.
|
||||||
/// location-based queries that only need identifying and location details.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static BreweryWithLocationDto ToSimplifiedDto(this BreweryPost brewery)
|
public static BreweryWithLocationDto ToSimplifiedDto(this BreweryPost brewery) =>
|
||||||
{
|
new(
|
||||||
return new BreweryWithLocationDto(
|
brewery.BreweryPostId,
|
||||||
BreweryPostId: brewery.BreweryPostId,
|
brewery.BreweryName,
|
||||||
BreweryName: brewery.BreweryName,
|
brewery.Location is null
|
||||||
Location: brewery.Location is null
|
|
||||||
? null
|
? null
|
||||||
: new BreweryLocationDto(
|
: new BreweryLocationDto(
|
||||||
BreweryPostLocationId: brewery.Location.BreweryPostLocationId,
|
brewery.Location.BreweryPostLocationId,
|
||||||
CityId: brewery.Location.CityId,
|
brewery.Location.CityId,
|
||||||
CityName: brewery.Location.City?.CityName ?? string.Empty,
|
brewery.Location.City?.CityName ?? string.Empty,
|
||||||
StateProvinceName: brewery.Location.City?.StateProvince?.StateProvinceName
|
brewery.Location.City?.StateProvince?.StateProvinceName ?? string.Empty,
|
||||||
?? string.Empty,
|
brewery.Location.City?.StateProvince?.Iso31662 ?? string.Empty,
|
||||||
StateProvinceCode: brewery.Location.City?.StateProvince?.Iso31662
|
brewery.Location.City?.StateProvince?.Country?.CountryName ?? string.Empty,
|
||||||
?? string.Empty,
|
brewery.Location.City?.StateProvince?.Country?.Iso31661 ?? string.Empty,
|
||||||
CountryName: brewery.Location.City?.StateProvince?.Country?.CountryName
|
brewery.Location.AddressLine1,
|
||||||
?? string.Empty,
|
brewery.Location.AddressLine2,
|
||||||
CountryCode: brewery.Location.City?.StateProvince?.Country?.Iso31661
|
brewery.Location.PostalCode,
|
||||||
?? string.Empty,
|
brewery.Location.Coordinates
|
||||||
AddressLine1: brewery.Location.AddressLine1,
|
|
||||||
AddressLine2: brewery.Location.AddressLine2,
|
|
||||||
PostalCode: brewery.Location.PostalCode,
|
|
||||||
Coordinates: brewery.Location.Coordinates
|
|
||||||
),
|
),
|
||||||
DistanceMetres: brewery.Distance?.DistanceMetres
|
brewery.Distance?.DistanceMetres
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,11 +5,15 @@ using MediatR;
|
|||||||
|
|
||||||
namespace Features.Breweries.Queries.GetAllBreweries;
|
namespace Features.Breweries.Queries.GetAllBreweries;
|
||||||
|
|
||||||
/// <summary>Handles <see cref="GetAllBreweriesQuery" /> by retrieving brewery posts, newest first.</summary>
|
/// <summary>
|
||||||
|
/// Handles ordered, paged brewery-post queries.
|
||||||
|
/// </summary>
|
||||||
public class GetAllBreweriesHandler(IBreweryRepository repository)
|
public class GetAllBreweriesHandler(IBreweryRepository repository)
|
||||||
: IRequestHandler<GetAllBreweriesQuery, IEnumerable<BreweryDto>>
|
: IRequestHandler<GetAllBreweriesQuery, IEnumerable<BreweryDto>>
|
||||||
{
|
{
|
||||||
/// <summary>Retrieves brewery posts, newest first, applying <see cref="GetAllBreweriesQuery" />'s paging.</summary>
|
/// <summary>
|
||||||
|
/// Retrieves and maps the requested page of brewery posts.
|
||||||
|
/// </summary>
|
||||||
public async Task<IEnumerable<BreweryDto>> Handle(
|
public async Task<IEnumerable<BreweryDto>> Handle(
|
||||||
GetAllBreweriesQuery request,
|
GetAllBreweriesQuery request,
|
||||||
CancellationToken cancellationToken
|
CancellationToken cancellationToken
|
||||||
|
|||||||
@@ -3,7 +3,13 @@ using MediatR;
|
|||||||
|
|
||||||
namespace Features.Breweries.Queries.GetAllBreweries;
|
namespace Features.Breweries.Queries.GetAllBreweries;
|
||||||
|
|
||||||
/// <summary>Retrieves brewery posts, newest first. Bound directly from the query string of <c>GET /api/brewery</c>.</summary>
|
/// <summary>
|
||||||
/// <param name="Limit">Maximum number of breweries to return. Unbounded if <see langword="null"/>.</param>
|
/// Requests brewery posts ordered from newest to oldest.
|
||||||
/// <param name="Offset">Number of breweries to skip. Treated as zero if <see langword="null"/>.</param>
|
/// </summary>
|
||||||
|
/// <param name="Limit">
|
||||||
|
/// Caps the result count; <see langword="null" /> disables the cap.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="Offset">
|
||||||
|
/// Specifies the number of results to skip; <see langword="null" /> means zero.
|
||||||
|
/// </param>
|
||||||
public record GetAllBreweriesQuery(int? Limit, int? Offset) : IRequest<IEnumerable<BreweryDto>>;
|
public record GetAllBreweriesQuery(int? Limit, int? Offset) : IRequest<IEnumerable<BreweryDto>>;
|
||||||
|
|||||||
@@ -5,11 +5,15 @@ using MediatR;
|
|||||||
|
|
||||||
namespace Features.Breweries.Queries.GetAllBreweryLocations;
|
namespace Features.Breweries.Queries.GetAllBreweryLocations;
|
||||||
|
|
||||||
/// <summary>Handles <see cref="GetAllBreweryLocationsQuery" /> by retrieving all located brewery posts.</summary>
|
/// <summary>
|
||||||
|
/// Handles queries for brewery posts with locations.
|
||||||
|
/// </summary>
|
||||||
public class GetAllBreweryLocationsHandler(IBreweryRepository repository)
|
public class GetAllBreweryLocationsHandler(IBreweryRepository repository)
|
||||||
: IRequestHandler<GetAllBreweryLocationsQuery, IEnumerable<BreweryWithLocationDto>>
|
: IRequestHandler<GetAllBreweryLocationsQuery, IEnumerable<BreweryWithLocationDto>>
|
||||||
{
|
{
|
||||||
/// <summary>Retrieves every brewery post that has a set location.</summary>
|
/// <summary>
|
||||||
|
/// Retrieves and maps every brewery post that has a location.
|
||||||
|
/// </summary>
|
||||||
public async Task<IEnumerable<BreweryWithLocationDto>> Handle(
|
public async Task<IEnumerable<BreweryWithLocationDto>> Handle(
|
||||||
GetAllBreweryLocationsQuery request,
|
GetAllBreweryLocationsQuery request,
|
||||||
CancellationToken cancellationToken
|
CancellationToken cancellationToken
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ using MediatR;
|
|||||||
namespace Features.Breweries.Queries.GetAllBreweryLocations;
|
namespace Features.Breweries.Queries.GetAllBreweryLocations;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Retrieves all brewery posts that have a set location. Bound directly from
|
/// Requests brewery posts that include a location.
|
||||||
/// <c>GET /api/brewery/locations</c>.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public record GetAllBreweryLocationsQuery : IRequest<IEnumerable<BreweryWithLocationDto>>;
|
public record GetAllBreweryLocationsQuery : IRequest<IEnumerable<BreweryWithLocationDto>>;
|
||||||
|
|||||||
@@ -5,17 +5,24 @@ using MediatR;
|
|||||||
|
|
||||||
namespace Features.Breweries.Queries.GetBreweryById;
|
namespace Features.Breweries.Queries.GetBreweryById;
|
||||||
|
|
||||||
/// <summary>Handles <see cref="GetBreweryByIdQuery" /> by retrieving the matching brewery post.</summary>
|
/// <summary>
|
||||||
|
/// Handles lookup requests for individual brewery posts.
|
||||||
|
/// </summary>
|
||||||
public class GetBreweryByIdHandler(IBreweryRepository repository)
|
public class GetBreweryByIdHandler(IBreweryRepository repository)
|
||||||
: IRequestHandler<GetBreweryByIdQuery, BreweryDto?>
|
: IRequestHandler<GetBreweryByIdQuery, BreweryDto?>
|
||||||
{
|
{
|
||||||
/// <summary>Retrieves the brewery post, or <see langword="null"/> if none exists with the given ID.</summary>
|
/// <summary>
|
||||||
|
/// Retrieves and maps the post, if it exists.
|
||||||
|
/// </summary>
|
||||||
public async Task<BreweryDto?> Handle(
|
public async Task<BreweryDto?> Handle(
|
||||||
GetBreweryByIdQuery request,
|
GetBreweryByIdQuery request,
|
||||||
CancellationToken cancellationToken
|
CancellationToken cancellationToken
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
BreweryPost? brewery = await repository.GetByIdAsync(request.BreweryPostId, cancellationToken);
|
BreweryPost? brewery = await repository.GetByIdAsync(
|
||||||
|
request.BreweryPostId,
|
||||||
|
cancellationToken
|
||||||
|
);
|
||||||
return brewery?.ToDto();
|
return brewery?.ToDto();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,10 @@ using MediatR;
|
|||||||
|
|
||||||
namespace Features.Breweries.Queries.GetBreweryById;
|
namespace Features.Breweries.Queries.GetBreweryById;
|
||||||
|
|
||||||
/// <summary>Retrieves a brewery post by ID. Bound directly from the route of <c>GET /api/brewery/{id}</c>.</summary>
|
/// <summary>
|
||||||
/// <remarks>Yields <see langword="null"/> if no brewery exists with the given <c>BreweryPostId</c>.</remarks>
|
/// Requests a brewery post by its identifier.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// The handler returns <see langword="null" /> when the post is absent.
|
||||||
|
/// </remarks>
|
||||||
public record GetBreweryByIdQuery(Guid BreweryPostId) : IRequest<BreweryDto?>;
|
public record GetBreweryByIdQuery(Guid BreweryPostId) : IRequest<BreweryDto?>;
|
||||||
|
|||||||
@@ -6,12 +6,14 @@ using MediatR;
|
|||||||
namespace Features.Breweries.Queries.GetBreweryLocationsWithinRange;
|
namespace Features.Breweries.Queries.GetBreweryLocationsWithinRange;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handles <see cref="GetBreweryLocationsWithinRangeQuery" /> by retrieving nearby located brewery posts.
|
/// Handles proximity searches for brewery locations.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class GetBreweryLocationsWithinRangeHandler(IBreweryRepository repository)
|
public class GetBreweryLocationsWithinRangeHandler(IBreweryRepository repository)
|
||||||
: IRequestHandler<GetBreweryLocationsWithinRangeQuery, IEnumerable<BreweryWithLocationDto>>
|
: IRequestHandler<GetBreweryLocationsWithinRangeQuery, IEnumerable<BreweryWithLocationDto>>
|
||||||
{
|
{
|
||||||
/// <summary>Retrieves brewery posts within range of the requested coordinates, nearest first.</summary>
|
/// <summary>
|
||||||
|
/// Retrieves and maps nearby brewery locations.
|
||||||
|
/// </summary>
|
||||||
public async Task<IEnumerable<BreweryWithLocationDto>> Handle(
|
public async Task<IEnumerable<BreweryWithLocationDto>> Handle(
|
||||||
GetBreweryLocationsWithinRangeQuery request,
|
GetBreweryLocationsWithinRangeQuery request,
|
||||||
CancellationToken cancellationToken
|
CancellationToken cancellationToken
|
||||||
|
|||||||
@@ -4,12 +4,19 @@ using MediatR;
|
|||||||
namespace Features.Breweries.Queries.GetBreweryLocationsWithinRange;
|
namespace Features.Breweries.Queries.GetBreweryLocationsWithinRange;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Retrieves brewery posts with a set location within <paramref name="RangeInMetres" /> of the given
|
/// Requests located brewery posts near a geographic point, nearest first.
|
||||||
/// coordinates, nearest first. Bound directly from the query string of
|
|
||||||
/// <c>GET /api/brewery/locations/nearby</c>.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="Latitude">The origin latitude, in decimal degrees.</param>
|
/// <param name="Latitude">
|
||||||
/// <param name="Longitude">The origin longitude, in decimal degrees.</param>
|
/// Sets the origin latitude in decimal degrees.
|
||||||
/// <param name="RangeInMetres">The maximum distance, in metres, from the origin coordinates.</param>
|
/// </param>
|
||||||
public record GetBreweryLocationsWithinRangeQuery(double Latitude, double Longitude, double RangeInMetres)
|
/// <param name="Longitude">
|
||||||
: IRequest<IEnumerable<BreweryWithLocationDto>>;
|
/// Sets the origin longitude in decimal degrees.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="RangeInMetres">
|
||||||
|
/// Sets the inclusive search radius in metres.
|
||||||
|
/// </param>
|
||||||
|
public record GetBreweryLocationsWithinRangeQuery(
|
||||||
|
double Latitude,
|
||||||
|
double Longitude,
|
||||||
|
double RangeInMetres
|
||||||
|
) : IRequest<IEnumerable<BreweryWithLocationDto>>;
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ using FluentValidation;
|
|||||||
|
|
||||||
namespace Features.Breweries.Queries.GetBreweryLocationsWithinRange;
|
namespace Features.Breweries.Queries.GetBreweryLocationsWithinRange;
|
||||||
|
|
||||||
/// <summary>Validates <see cref="GetBreweryLocationsWithinRangeQuery" />.</summary>
|
/// <summary>
|
||||||
|
/// Validates coordinates and radius values for proximity searches.
|
||||||
|
/// </summary>
|
||||||
public class GetBreweryLocationsWithinRangeValidator
|
public class GetBreweryLocationsWithinRangeValidator
|
||||||
: AbstractValidator<GetBreweryLocationsWithinRangeQuery>
|
: AbstractValidator<GetBreweryLocationsWithinRangeQuery>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,11 +2,20 @@ using Domain.Exceptions;
|
|||||||
|
|
||||||
namespace Features.Breweries.Repository;
|
namespace Features.Breweries.Repository;
|
||||||
|
|
||||||
/// <summary>Shared ownership check for commands that mutate a specific brewery post.</summary>
|
/// <summary>
|
||||||
|
/// Provides authorization checks for brewery-post mutations.
|
||||||
|
/// </summary>
|
||||||
public static class BreweryOwnershipExtensions
|
public static class BreweryOwnershipExtensions
|
||||||
{
|
{
|
||||||
/// <exception cref="Domain.Exceptions.NotFoundException">Thrown when no brewery exists with the given ID.</exception>
|
/// <summary>
|
||||||
/// <exception cref="Domain.Exceptions.ForbiddenException">Thrown when <paramref name="requestingUserId" /> did not post the brewery.</exception>
|
/// Verifies that the specified user created the brewery post.
|
||||||
|
/// </summary>
|
||||||
|
/// <exception cref="Domain.Exceptions.NotFoundException">
|
||||||
|
/// Thrown if the brewery post cannot be found.
|
||||||
|
/// </exception>
|
||||||
|
/// <exception cref="Domain.Exceptions.ForbiddenException">
|
||||||
|
/// Thrown if the specified user does not own the brewery post.
|
||||||
|
/// </exception>
|
||||||
public static async Task EnsureCallerOwnsBreweryAsync(
|
public static async Task EnsureCallerOwnsBreweryAsync(
|
||||||
this IBreweryRepository repository,
|
this IBreweryRepository repository,
|
||||||
Guid breweryPostId,
|
Guid breweryPostId,
|
||||||
|
|||||||
@@ -7,14 +7,11 @@ using Domain.Exceptions;
|
|||||||
namespace Features.Breweries.Repository;
|
namespace Features.Breweries.Repository;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Dapper-based implementation of <see cref="IBreweryRepository" />.
|
/// Implements brewery persistence with Dapper.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// <c>BreweryPostLocation.Coordinates</c> is a SQL Server <c>GEOGRAPHY</c> column. Dapper's generic
|
/// Coordinate queries convert SQL Server <c>GEOGRAPHY</c> values to <c>varbinary(max)</c>
|
||||||
/// <c>Query<T></c> deserializer reads columns via <c>IDataRecord.GetValue</c>, which for a UDT
|
/// so Dapper can deserialize them as binary coordinate data.
|
||||||
/// column returns a CLR UDT instance rather than raw bytes and cannot be coerced to <c>byte[]</c>.
|
|
||||||
/// The read queries below therefore select <c>CONVERT(varbinary(max), bpl.Coordinates)</c>, which
|
|
||||||
/// yields the UDT's serialized bytes as a plain <c>varbinary</c> value that Dapper can bind directly.
|
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public class BreweryRepository(ISqlConnectionFactory connectionFactory)
|
public class BreweryRepository(ISqlConnectionFactory connectionFactory)
|
||||||
: DapperRepository(connectionFactory),
|
: DapperRepository(connectionFactory),
|
||||||
@@ -27,8 +24,11 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory)
|
|||||||
SqlMapper.AddTypeHandler(new CoordinateDataTypeHandler());
|
SqlMapper.AddTypeHandler(new CoordinateDataTypeHandler());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <summary>Gets a brewery post with its location data.</summary>
|
||||||
public async Task<BreweryPost?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default)
|
public async Task<BreweryPost?> GetByIdAsync(
|
||||||
|
Guid id,
|
||||||
|
CancellationToken cancellationToken = default
|
||||||
|
)
|
||||||
{
|
{
|
||||||
await using DbConnection connection = await CreateConnection();
|
await using DbConnection connection = await CreateConnection();
|
||||||
IEnumerable<BreweryPost> results = await connection.QueryAsync<
|
IEnumerable<BreweryPost> results = await connection.QueryAsync<
|
||||||
@@ -73,14 +73,17 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory)
|
|||||||
cancellationToken: cancellationToken
|
cancellationToken: cancellationToken
|
||||||
),
|
),
|
||||||
MapBreweryRow,
|
MapBreweryRow,
|
||||||
splitOn: "BreweryPostLocationID,CityID,StateProvinceID,CountryID"
|
"BreweryPostLocationID,CityID,StateProvinceID,CountryID"
|
||||||
);
|
);
|
||||||
|
|
||||||
return results.SingleOrDefault();
|
return results.SingleOrDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <summary>Gets the identifier of the user who created a brewery post.</summary>
|
||||||
public async Task<Guid?> GetPostedByIdAsync(Guid id, CancellationToken cancellationToken = default)
|
public async Task<Guid?> GetPostedByIdAsync(
|
||||||
|
Guid id,
|
||||||
|
CancellationToken cancellationToken = default
|
||||||
|
)
|
||||||
{
|
{
|
||||||
await using DbConnection connection = await CreateConnection();
|
await using DbConnection connection = await CreateConnection();
|
||||||
return await connection.ExecuteScalarAsync<Guid?>(
|
return await connection.ExecuteScalarAsync<Guid?>(
|
||||||
@@ -92,7 +95,7 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory)
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <summary>Gets brewery posts in reverse chronological order.</summary>
|
||||||
public async Task<IEnumerable<BreweryPost>> GetAllAsync(int? limit, int? offset)
|
public async Task<IEnumerable<BreweryPost>> GetAllAsync(int? limit, int? offset)
|
||||||
{
|
{
|
||||||
await using DbConnection connection = await CreateConnection();
|
await using DbConnection connection = await CreateConnection();
|
||||||
@@ -140,8 +143,11 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory)
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <summary>Gets brewery posts within the requested distance from an origin.</summary>
|
||||||
public async Task<IEnumerable<BreweryPost>> GetAllLocationsWithinRange(CoordinateData coords, double rangeInMetres)
|
public async Task<IEnumerable<BreweryPost>> GetAllLocationsWithinRange(
|
||||||
|
CoordinateData coords,
|
||||||
|
double rangeInMetres
|
||||||
|
)
|
||||||
{
|
{
|
||||||
await using DbConnection connection = await CreateConnection();
|
await using DbConnection connection = await CreateConnection();
|
||||||
return await connection.QueryAsync<
|
return await connection.QueryAsync<
|
||||||
@@ -152,7 +158,7 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory)
|
|||||||
Country,
|
Country,
|
||||||
DistanceRow,
|
DistanceRow,
|
||||||
BreweryPost
|
BreweryPost
|
||||||
> (
|
>(
|
||||||
"""
|
"""
|
||||||
DECLARE @Origin geography = geography::Point(@Latitude, @Longitude, 4326);
|
DECLARE @Origin geography = geography::Point(@Latitude, @Longitude, 4326);
|
||||||
|
|
||||||
@@ -183,18 +189,26 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory)
|
|||||||
ORDER BY bpl.Coordinates.STDistance(@Origin) ASC
|
ORDER BY bpl.Coordinates.STDistance(@Origin) ASC
|
||||||
""",
|
""",
|
||||||
(post, location, city, stateProvince, country, distance) =>
|
(post, location, city, stateProvince, country, distance) =>
|
||||||
MapBreweryRowWithDistance(post, location, city, stateProvince, country, distance, coords),
|
MapBreweryRowWithDistance(
|
||||||
param: new
|
post,
|
||||||
|
location,
|
||||||
|
city,
|
||||||
|
stateProvince,
|
||||||
|
country,
|
||||||
|
distance,
|
||||||
|
coords
|
||||||
|
),
|
||||||
|
new
|
||||||
{
|
{
|
||||||
coords.Latitude,
|
coords.Latitude,
|
||||||
coords.Longitude,
|
coords.Longitude,
|
||||||
RangeInMetres = rangeInMetres
|
RangeInMetres = rangeInMetres,
|
||||||
},
|
},
|
||||||
splitOn: "BreweryPostLocationID,CityID,StateProvinceID,CountryID,DistanceMetres"
|
splitOn: "BreweryPostLocationID,CityID,StateProvinceID,CountryID,DistanceMetres"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <summary>Gets brewery posts that have location data.</summary>
|
||||||
public async Task<IEnumerable<BreweryPost>> GetAllLocations()
|
public async Task<IEnumerable<BreweryPost>> GetAllLocations()
|
||||||
{
|
{
|
||||||
await using DbConnection connection = await CreateConnection();
|
await using DbConnection connection = await CreateConnection();
|
||||||
@@ -205,7 +219,7 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory)
|
|||||||
StateProvince,
|
StateProvince,
|
||||||
Country,
|
Country,
|
||||||
BreweryPost
|
BreweryPost
|
||||||
> (
|
>(
|
||||||
"""
|
"""
|
||||||
SELECT
|
SELECT
|
||||||
bp.BreweryPostID,
|
bp.BreweryPostID,
|
||||||
@@ -236,11 +250,16 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory)
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <summary>Updates a brewery post and its location in a transaction.</summary>
|
||||||
public async Task<BreweryPost> UpdateAsync(BreweryPost brewery, CancellationToken cancellationToken = default)
|
public async Task<BreweryPost> UpdateAsync(
|
||||||
|
BreweryPost brewery,
|
||||||
|
CancellationToken cancellationToken = default
|
||||||
|
)
|
||||||
{
|
{
|
||||||
await using DbConnection connection = await CreateConnection();
|
await using DbConnection connection = await CreateConnection();
|
||||||
await using DbTransaction transaction = await connection.BeginTransactionAsync(cancellationToken);
|
await using DbTransaction transaction = await connection.BeginTransactionAsync(
|
||||||
|
cancellationToken
|
||||||
|
);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -363,8 +382,8 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory)
|
|||||||
brewery.Location.AddressLine1,
|
brewery.Location.AddressLine1,
|
||||||
brewery.Location.AddressLine2,
|
brewery.Location.AddressLine2,
|
||||||
brewery.Location.PostalCode,
|
brewery.Location.PostalCode,
|
||||||
Latitude = brewery.Location.Coordinates?.Latitude,
|
brewery.Location.Coordinates?.Latitude,
|
||||||
Longitude = brewery.Location.Coordinates?.Longitude,
|
brewery.Location.Coordinates?.Longitude,
|
||||||
Srid = GeographySrid,
|
Srid = GeographySrid,
|
||||||
},
|
},
|
||||||
transaction,
|
transaction,
|
||||||
@@ -403,8 +422,8 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory)
|
|||||||
brewery.Location.AddressLine1,
|
brewery.Location.AddressLine1,
|
||||||
brewery.Location.AddressLine2,
|
brewery.Location.AddressLine2,
|
||||||
brewery.Location.PostalCode,
|
brewery.Location.PostalCode,
|
||||||
Latitude = brewery.Location.Coordinates?.Latitude,
|
brewery.Location.Coordinates?.Latitude,
|
||||||
Longitude = brewery.Location.Coordinates?.Longitude,
|
brewery.Location.Coordinates?.Longitude,
|
||||||
Srid = GeographySrid,
|
Srid = GeographySrid,
|
||||||
},
|
},
|
||||||
transaction,
|
transaction,
|
||||||
@@ -427,7 +446,7 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory)
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <summary>Deletes the brewery post identified by <paramref name="id" />.</summary>
|
||||||
public async Task DeleteAsync(Guid id)
|
public async Task DeleteAsync(Guid id)
|
||||||
{
|
{
|
||||||
await using DbConnection connection = await CreateConnection();
|
await using DbConnection connection = await CreateConnection();
|
||||||
@@ -445,7 +464,7 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory)
|
|||||||
throw new NotFoundException("Brewery not found.");
|
throw new NotFoundException("Brewery not found.");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <summary>Creates a brewery post and its location in a transaction.</summary>
|
||||||
public async Task CreateAsync(BreweryPost brewery)
|
public async Task CreateAsync(BreweryPost brewery)
|
||||||
{
|
{
|
||||||
if (brewery.Location is null)
|
if (brewery.Location is null)
|
||||||
@@ -543,8 +562,8 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory)
|
|||||||
brewery.Location.AddressLine1,
|
brewery.Location.AddressLine1,
|
||||||
brewery.Location.AddressLine2,
|
brewery.Location.AddressLine2,
|
||||||
brewery.Location.PostalCode,
|
brewery.Location.PostalCode,
|
||||||
Latitude = brewery.Location.Coordinates?.Latitude,
|
brewery.Location.Coordinates?.Latitude,
|
||||||
Longitude = brewery.Location.Coordinates?.Longitude,
|
brewery.Location.Coordinates?.Longitude,
|
||||||
Srid = GeographySrid,
|
Srid = GeographySrid,
|
||||||
},
|
},
|
||||||
transaction
|
transaction
|
||||||
@@ -560,15 +579,8 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Composes a joined row's <see cref="BreweryPost" />, <see cref="BreweryPostLocation" />,
|
/// Reconstructs a brewery post and its optional location hierarchy from a joined row.
|
||||||
/// <see cref="City" />, <see cref="StateProvince" /> and <see cref="Country" /> fragments into a
|
|
||||||
/// single populated <see cref="BreweryPost" />. When the row has no location (a left-joined
|
|
||||||
/// brewery with no address on file), Dapper passes <see langword="null" /> for
|
|
||||||
/// <paramref name="location" /> rather than a blank instance, since its identity column
|
|
||||||
/// (<c>BreweryPostLocationID</c>) is <c>NULL</c>; the schema guarantees that whenever a location
|
|
||||||
/// row was joined in, its City/StateProvince/Country chain was too.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static BreweryPost MapBreweryRow(
|
private static BreweryPost MapBreweryRow(
|
||||||
BreweryPost post,
|
BreweryPost post,
|
||||||
@@ -596,15 +608,8 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory)
|
|||||||
return post;
|
return post;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Dapper multi-mapping row for the <c>DistanceMetres</c> column computed by a range query.</summary>
|
|
||||||
private sealed class DistanceRow
|
|
||||||
{
|
|
||||||
public double DistanceMetres { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Composes a joined row via <see cref="MapBreweryRow" />, then attaches the DB-computed distance
|
/// Reconstructs a brewery row and attaches its database-calculated distance.
|
||||||
/// from <paramref name="origin" /> as the result's <see cref="BreweryPost.Distance" />.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static BreweryPost MapBreweryRowWithDistance(
|
private static BreweryPost MapBreweryRowWithDistance(
|
||||||
BreweryPost post,
|
BreweryPost post,
|
||||||
@@ -620,4 +625,12 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory)
|
|||||||
mapped.Distance = new DistanceInformation(origin, distance.DistanceMetres);
|
mapped.Distance = new DistanceInformation(origin, distance.DistanceMetres);
|
||||||
return mapped;
|
return mapped;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Holds the distance calculated by a proximity query.
|
||||||
|
/// </summary>
|
||||||
|
private sealed class DistanceRow
|
||||||
|
{
|
||||||
|
public double DistanceMetres { get; set; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -7,9 +7,7 @@ using NetTopologySuite.IO;
|
|||||||
namespace Features.Breweries.Repository;
|
namespace Features.Breweries.Repository;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Dapper <see cref="SqlMapper.TypeHandler{T}" /> that deserializes a SQL Server <c>GEOGRAPHY</c>
|
/// Converts SQL Server geography values read as binary data into <see cref="CoordinateData" />.
|
||||||
/// column (read as <c>varbinary(max)</c>, see <see cref="BreweryRepository" />) into a
|
|
||||||
/// <see cref="CoordinateData" />.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class CoordinateDataTypeHandler : SqlMapper.TypeHandler<CoordinateData>
|
public sealed class CoordinateDataTypeHandler : SqlMapper.TypeHandler<CoordinateData>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -3,69 +3,80 @@ using Domain.Entities;
|
|||||||
namespace Features.Breweries.Repository;
|
namespace Features.Breweries.Repository;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Repository for CRUD operations on brewery post records.
|
/// Defines persistence operations for brewery posts.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface IBreweryRepository
|
public interface IBreweryRepository
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Retrieves a brewery post by ID, joined to its location. Returns <c>null</c> if no brewery post
|
/// Gets a brewery post and its location data by identifier.
|
||||||
/// exists with the given ID, or if it has no associated location.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Task<BreweryPost?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default);
|
Task<BreweryPost?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Retrieves the <c>PostedById</c> of the brewery post with the given ID, without joining its
|
/// Gets the identifier of the user who created a brewery post.
|
||||||
/// location. Returns <c>null</c> if no brewery post exists with the given ID.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Task<Guid?> GetPostedByIdAsync(Guid id, CancellationToken cancellationToken = default);
|
Task<Guid?> GetPostedByIdAsync(Guid id, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Retrieves all brewery posts, optionally paginated, ordered by creation date descending. Posts
|
/// Gets brewery posts ordered by creation time, with optional paging.
|
||||||
/// without a location are included, with <see cref="BreweryPost.Location" /> left <c>null</c>.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="limit">Maximum number of rows to return. Unbounded if <see langword="null" />.</param>
|
/// <param name="limit">
|
||||||
/// <param name="offset">Number of rows to skip. Treated as zero if <see langword="null" />.</param>
|
/// Limits returned posts; <see langword="null" /> applies no limit.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="offset">
|
||||||
|
/// Skips returned posts; <see langword="null" /> skips none.
|
||||||
|
/// </param>
|
||||||
Task<IEnumerable<BreweryPost>> GetAllAsync(int? limit, int? offset);
|
Task<IEnumerable<BreweryPost>> GetAllAsync(int? limit, int? offset);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Retrieves brewery posts with a set location within <paramref name="rangeInMetres" /> of
|
/// Gets located brewery posts within a radius, ordered by distance.
|
||||||
/// <paramref name="coords" />, nearest first. Posts without coordinates are excluded.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="coords">The origin point to measure distance from.</param>
|
/// <param name="coords">
|
||||||
/// <param name="rangeInMetres">The maximum distance, in metres, from <paramref name="coords" />.</param>
|
/// Sets the point from which distance is measured.
|
||||||
Task<IEnumerable<BreweryPost>> GetAllLocationsWithinRange(CoordinateData coords, double rangeInMetres);
|
/// </param>
|
||||||
|
/// <param name="rangeInMetres">
|
||||||
|
/// Sets the maximum search distance in metres.
|
||||||
|
/// </param>
|
||||||
|
Task<IEnumerable<BreweryPost>> GetAllLocationsWithinRange(
|
||||||
|
CoordinateData coords,
|
||||||
|
double rangeInMetres
|
||||||
|
);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Retrieves all brewery posts that have a set location, ordered by <c>BreweryPostId</c>. Posts
|
/// Gets brewery posts with location data.
|
||||||
/// without coordinates are excluded.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Task<IEnumerable<BreweryPost>> GetAllLocations();
|
Task<IEnumerable<BreweryPost>> GetAllLocations();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Updates a brewery post's name and description, and upserts or clears its location, enforcing
|
/// Saves a brewery post and its location using optimistic concurrency.
|
||||||
/// optimistic concurrency via <paramref name="brewery" />'s <c>RowVersion</c>: the update is rejected
|
|
||||||
/// if the row was modified since <c>RowVersion</c> was read. When <paramref name="brewery" />'s
|
|
||||||
/// <c>Location</c> is <c>null</c>, any existing location for the brewery is removed.
|
|
||||||
/// Returns the freshly persisted brewery, including its new <c>RowVersion</c>.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <exception cref="Domain.Exceptions.NotFoundException">
|
/// <exception cref="Domain.Exceptions.NotFoundException">
|
||||||
/// Thrown when no brewery exists with the given ID, or the given location's <c>CityId</c> does not
|
/// Thrown if the brewery post or its requested city cannot be found.
|
||||||
/// exist.
|
|
||||||
/// </exception>
|
/// </exception>
|
||||||
/// <exception cref="Domain.Exceptions.ConflictException">
|
/// <exception cref="Domain.Exceptions.ConflictException">
|
||||||
/// Thrown when <paramref name="brewery" />'s <c>RowVersion</c> no longer matches the stored row (it was
|
/// Thrown if the supplied row version is no longer current.
|
||||||
/// modified by another request since it was last read).
|
|
||||||
/// </exception>
|
/// </exception>
|
||||||
Task<BreweryPost> UpdateAsync(BreweryPost brewery, CancellationToken cancellationToken = default);
|
Task<BreweryPost> UpdateAsync(
|
||||||
|
BreweryPost brewery,
|
||||||
|
CancellationToken cancellationToken = default
|
||||||
|
);
|
||||||
|
|
||||||
/// <summary>Deletes a brewery post by ID. Its location and photos are removed via cascading foreign keys.</summary>
|
/// <summary>
|
||||||
/// <exception cref="Domain.Exceptions.NotFoundException">Thrown when no brewery exists with the given <paramref name="id" />.</exception>
|
/// Deletes a brewery post and its dependent records.
|
||||||
|
/// </summary>
|
||||||
|
/// <exception cref="Domain.Exceptions.NotFoundException">
|
||||||
|
/// Thrown if the brewery post cannot be found.
|
||||||
|
/// </exception>
|
||||||
Task DeleteAsync(Guid id);
|
Task DeleteAsync(Guid id);
|
||||||
|
|
||||||
/// <summary>Creates a new brewery post, including its location details.</summary>
|
/// <summary>
|
||||||
/// <exception cref="ArgumentException">Thrown when <paramref name="brewery" /> has no <c>Location</c>.</exception>
|
/// Creates a brewery post and its required location.
|
||||||
|
/// </summary>
|
||||||
|
/// <exception cref="ArgumentException">
|
||||||
|
/// Thrown if <paramref name="brewery" /> has no location.
|
||||||
|
/// </exception>
|
||||||
/// <exception cref="Domain.Exceptions.NotFoundException">
|
/// <exception cref="Domain.Exceptions.NotFoundException">
|
||||||
/// Thrown when <paramref name="brewery" />'s <c>PostedById</c> or <c>Location.CityId</c> does not exist.
|
/// Thrown if the posting user or location city cannot be found.
|
||||||
/// </exception>
|
/// </exception>
|
||||||
Task CreateAsync(BreweryPost brewery);
|
Task CreateAsync(BreweryPost brewery);
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user