mirror of
https://github.com/aaronpo97/the-biergarten-app.git
synced 2026-10-15 10:04:44 +00:00
Compare commits
2 Commits
81a76c9baf
...
2b2df0053f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2b2df0053f | ||
|
|
ab265de54d |
@@ -39,7 +39,7 @@ CREATE TABLE dbo.UserAccount
|
||||
|
||||
DateOfBirth DATE NOT NULL,
|
||||
|
||||
Timer ROWVERSION,
|
||||
RowVersion ROWVERSION,
|
||||
|
||||
CONSTRAINT PK_UserAccount
|
||||
PRIMARY KEY (UserAccountID),
|
||||
@@ -67,7 +67,7 @@ CREATE TABLE Photo -- All photos must be linked to a user account, you cannot de
|
||||
UploadedAt DATETIME NOT NULL
|
||||
CONSTRAINT DF_Photo_UploadedAt DEFAULT GETDATE(),
|
||||
|
||||
Timer ROWVERSION,
|
||||
RowVersion ROWVERSION,
|
||||
|
||||
CONSTRAINT PK_Photo
|
||||
PRIMARY KEY (PhotoID),
|
||||
@@ -94,7 +94,7 @@ CREATE TABLE UserAvatar -- delete avatar photo when user account is deleted
|
||||
|
||||
PhotoID UNIQUEIDENTIFIER NOT NULL,
|
||||
|
||||
Timer ROWVERSION,
|
||||
RowVersion ROWVERSION,
|
||||
|
||||
CONSTRAINT PK_UserAvatar PRIMARY KEY (UserAvatarID),
|
||||
|
||||
@@ -128,7 +128,7 @@ CREATE TABLE UserVerification -- delete verification data when user account is d
|
||||
VerificationDateTime DATETIME NOT NULL
|
||||
CONSTRAINT DF_VerificationDateTime DEFAULT GETDATE(),
|
||||
|
||||
Timer ROWVERSION,
|
||||
RowVersion ROWVERSION,
|
||||
|
||||
CONSTRAINT PK_UserVerification
|
||||
PRIMARY KEY (UserVerificationID),
|
||||
@@ -170,7 +170,7 @@ CREATE TABLE UserCredential -- delete credentials when user account is deleted
|
||||
|
||||
RevokedAt DATETIME NULL,
|
||||
|
||||
Timer ROWVERSION,
|
||||
RowVersion ROWVERSION,
|
||||
|
||||
CONSTRAINT PK_UserCredential
|
||||
PRIMARY KEY (UserCredentialID),
|
||||
@@ -205,7 +205,7 @@ CREATE TABLE UserFollow
|
||||
CreatedAt DATETIME NOT NULL
|
||||
CONSTRAINT DF_UserFollow_CreatedAt DEFAULT GETDATE(),
|
||||
|
||||
Timer ROWVERSION,
|
||||
RowVersion ROWVERSION,
|
||||
|
||||
CONSTRAINT PK_UserFollow
|
||||
PRIMARY KEY (UserFollowID),
|
||||
@@ -245,7 +245,7 @@ CREATE TABLE Country
|
||||
|
||||
ISO3166_1 CHAR(2) NOT NULL,
|
||||
|
||||
Timer ROWVERSION,
|
||||
RowVersion ROWVERSION,
|
||||
|
||||
CONSTRAINT PK_Country
|
||||
PRIMARY KEY (CountryID),
|
||||
@@ -269,7 +269,7 @@ CREATE TABLE StateProvince
|
||||
|
||||
CountryID UNIQUEIDENTIFIER NOT NULL,
|
||||
|
||||
Timer ROWVERSION,
|
||||
RowVersion ROWVERSION,
|
||||
|
||||
CONSTRAINT PK_StateProvince
|
||||
PRIMARY KEY (StateProvinceID),
|
||||
@@ -298,7 +298,7 @@ CREATE TABLE City
|
||||
|
||||
StateProvinceID UNIQUEIDENTIFIER NOT NULL,
|
||||
|
||||
Timer ROWVERSION,
|
||||
RowVersion ROWVERSION,
|
||||
|
||||
CONSTRAINT PK_City
|
||||
PRIMARY KEY (CityID),
|
||||
@@ -331,7 +331,7 @@ CREATE TABLE BreweryPost -- A user cannot be deleted if they have a post
|
||||
|
||||
UpdatedAt DATETIME NULL,
|
||||
|
||||
Timer ROWVERSION,
|
||||
RowVersion ROWVERSION,
|
||||
|
||||
CONSTRAINT PK_BreweryPost
|
||||
PRIMARY KEY (BreweryPostID),
|
||||
@@ -366,7 +366,7 @@ CREATE TABLE BreweryPostLocation
|
||||
|
||||
Coordinates GEOGRAPHY NULL,
|
||||
|
||||
Timer ROWVERSION,
|
||||
RowVersion ROWVERSION,
|
||||
|
||||
CONSTRAINT PK_BreweryPostLocation
|
||||
PRIMARY KEY (BreweryPostLocationID),
|
||||
@@ -419,7 +419,7 @@ CREATE TABLE BreweryPostPhoto -- All photos linked to a post are deleted if the
|
||||
LinkedAt DATETIME NOT NULL
|
||||
CONSTRAINT DF_BreweryPostPhoto_LinkedAt DEFAULT GETDATE(),
|
||||
|
||||
Timer ROWVERSION,
|
||||
RowVersion ROWVERSION,
|
||||
|
||||
CONSTRAINT PK_BreweryPostPhoto
|
||||
PRIMARY KEY (BreweryPostPhotoID),
|
||||
@@ -455,7 +455,7 @@ CREATE TABLE BeerStyle
|
||||
|
||||
Description NVARCHAR(MAX),
|
||||
|
||||
Timer ROWVERSION,
|
||||
RowVersion ROWVERSION,
|
||||
|
||||
CONSTRAINT PK_BeerStyle
|
||||
PRIMARY KEY (BeerStyleID),
|
||||
@@ -493,7 +493,7 @@ CREATE TABLE BeerPost
|
||||
|
||||
UpdatedAt DATETIME,
|
||||
|
||||
Timer ROWVERSION,
|
||||
RowVersion ROWVERSION,
|
||||
|
||||
CONSTRAINT PK_BeerPost
|
||||
PRIMARY KEY (BeerPostID),
|
||||
@@ -545,7 +545,7 @@ CREATE TABLE BeerPostPhoto -- All photos linked to a beer post are deleted if th
|
||||
LinkedAt DATETIME NOT NULL
|
||||
CONSTRAINT DF_BeerPostPhoto_LinkedAt DEFAULT GETDATE(),
|
||||
|
||||
Timer ROWVERSION,
|
||||
RowVersion ROWVERSION,
|
||||
|
||||
CONSTRAINT PK_BeerPostPhoto
|
||||
PRIMARY KEY (BeerPostPhotoID),
|
||||
@@ -590,7 +590,7 @@ CREATE TABLE BeerPostComment
|
||||
|
||||
UpdatedAt DATETIME NULL,
|
||||
|
||||
Timer ROWVERSION,
|
||||
RowVersion ROWVERSION,
|
||||
|
||||
CONSTRAINT PK_BeerPostComment
|
||||
PRIMARY KEY (BeerPostCommentID),
|
||||
|
||||
@@ -86,7 +86,6 @@ static async Task<int> RunAsync()
|
||||
"Loading user data into target database...",
|
||||
async ctx =>
|
||||
{
|
||||
Thread.Sleep(3000); // Simulate some work being done
|
||||
postedByIds = await LoadUsersIntoDatabaseAsync(
|
||||
authRepository,
|
||||
passwordInfrastructure,
|
||||
|
||||
27
web/backend/Domain/Domain.Entities/Entities/BeerPost.cs
Normal file
27
web/backend/Domain/Domain.Entities/Entities/BeerPost.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
namespace Domain.Entities;
|
||||
|
||||
public sealed class BeerPost
|
||||
{
|
||||
// PK
|
||||
public Guid BeerPostId { get; set; } = Guid.Empty;
|
||||
|
||||
// FK References
|
||||
public Guid PostedById { get; set; } = Guid.Empty;
|
||||
public Guid BeerStyleId { get; set; } = Guid.Empty;
|
||||
public Guid BrewedById { get; set; } = Guid.Empty;
|
||||
|
||||
// Attributes
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Description { get; set; } = string.Empty;
|
||||
public decimal ABV { get; set; }
|
||||
public int IBU { get; set; }
|
||||
|
||||
// Audit Fields
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
|
||||
public byte[]? RowVersion { get; set; }
|
||||
|
||||
// Navigation Properties
|
||||
public UserAccount? PostedBy { get; set; }
|
||||
public BreweryPost? BrewedBy { get; set; }
|
||||
}
|
||||
@@ -1,13 +1,23 @@
|
||||
namespace Domain.Entities;
|
||||
|
||||
public class BreweryPost
|
||||
public sealed class BreweryPost
|
||||
{
|
||||
// PK
|
||||
public Guid BreweryPostId { get; set; }
|
||||
|
||||
// FK References
|
||||
public Guid PostedById { get; set; }
|
||||
|
||||
// Attributes
|
||||
public string BreweryName { get; set; } = string.Empty;
|
||||
public string Description { get; set; } = string.Empty;
|
||||
|
||||
// Audit Fields
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public DateTime? UpdatedAt { get; set; }
|
||||
public byte[]? Timer { get; set; }
|
||||
public byte[]? RowVersion { get; set; }
|
||||
|
||||
// Navigation Properties
|
||||
public BreweryPostLocation? Location { get; set; }
|
||||
public List<BeerPost> BeerPosts { get; set; } = [];
|
||||
}
|
||||
|
||||
@@ -1,14 +1,23 @@
|
||||
namespace Domain.Entities;
|
||||
|
||||
public class BreweryPostLocation
|
||||
public sealed class BreweryPostLocation
|
||||
{
|
||||
// PK
|
||||
public Guid BreweryPostLocationId { get; set; }
|
||||
|
||||
// FK References
|
||||
public Guid BreweryPostId { get; set; }
|
||||
public Guid CityId { get; set; }
|
||||
|
||||
// Attributes
|
||||
public string AddressLine1 { get; set; } = string.Empty;
|
||||
public string? AddressLine2 { get; set; }
|
||||
public string PostalCode { get; set; } = string.Empty;
|
||||
public Guid CityId { get; set; }
|
||||
public City? City { get; set; }
|
||||
public byte[]? Coordinates { get; set; }
|
||||
public byte[]? Timer { get; set; }
|
||||
|
||||
// Audit Fields
|
||||
public byte[]? RowVersion { get; set; }
|
||||
|
||||
// Navigation Properties
|
||||
public City? City { get; set; }
|
||||
}
|
||||
|
||||
@@ -1,38 +1,56 @@
|
||||
namespace Domain.Entities;
|
||||
|
||||
public class City
|
||||
public sealed class City
|
||||
{
|
||||
// PK
|
||||
public Guid CityId { get; set; }
|
||||
public string CityName { get; set; } = string.Empty;
|
||||
|
||||
// FK References
|
||||
public Guid StateProvinceId { get; set; }
|
||||
|
||||
public StateProvince? StateProvince { get; set; }
|
||||
// Attributes
|
||||
public string CityName { get; set; } = string.Empty;
|
||||
|
||||
public byte[]? Timer { get; set; }
|
||||
// Audit Fields
|
||||
public byte[]? RowVersion { get; set; }
|
||||
|
||||
// Navigation Properties
|
||||
public StateProvince? StateProvince { get; set; }
|
||||
}
|
||||
|
||||
|
||||
public class StateProvince
|
||||
public sealed class StateProvince
|
||||
{
|
||||
// PK
|
||||
public Guid StateProvinceId { get; set; }
|
||||
|
||||
// FK References
|
||||
public Guid CountryId { get; set; }
|
||||
|
||||
// Attributes
|
||||
public string StateProvinceName { get; set; } = string.Empty;
|
||||
|
||||
// this is the iso 3166-2 code for the state/province, e.g. "CA" for California, "ON" for Ontario, etc.
|
||||
// and is rendered like US-CA for California, CA-ON for Ontario, etc.
|
||||
public string ISO3166_2 { get; set; } = string.Empty;
|
||||
public Guid CountryId { get; set; }
|
||||
|
||||
// Audit Fields
|
||||
public byte[]? RowVersion { get; set; }
|
||||
|
||||
// Navigation Properties
|
||||
public Country? Country { get; set; }
|
||||
|
||||
public byte[]? Timer { get; set; }
|
||||
}
|
||||
|
||||
public class Country
|
||||
public sealed class Country
|
||||
{
|
||||
// PK
|
||||
public Guid CountryId { get; set; }
|
||||
|
||||
// Attributes
|
||||
public string CountryName { get; set; } = string.Empty;
|
||||
|
||||
public string ISO3166_1 { get; set; } = string.Empty; // this is the iso 3166-1 alpha-2 code for the country, e.g. "US" for United States, "CA" for Canada, etc.
|
||||
|
||||
public byte[]? Timer { get; set; }
|
||||
// Audit Fields
|
||||
public byte[]? RowVersion { get; set; }
|
||||
}
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
namespace Domain.Entities;
|
||||
|
||||
public class UserAccount
|
||||
public sealed class UserAccount
|
||||
{
|
||||
// PK
|
||||
public Guid UserAccountId { get; set; }
|
||||
|
||||
// Attributes
|
||||
public string Username { get; set; } = string.Empty;
|
||||
public string FirstName { get; set; } = string.Empty;
|
||||
public string LastName { get; set; } = string.Empty;
|
||||
public string Email { get; set; } = string.Empty;
|
||||
public DateTime DateOfBirth { get; set; }
|
||||
|
||||
// Audit Fields
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public DateTime? UpdatedAt { get; set; }
|
||||
public DateTime DateOfBirth { get; set; }
|
||||
public byte[]? Timer { get; set; }
|
||||
public byte[]? RowVersion { get; set; }
|
||||
}
|
||||
|
||||
@@ -1,11 +1,20 @@
|
||||
namespace Domain.Entities;
|
||||
|
||||
public class UserCredential
|
||||
public sealed class UserCredential
|
||||
{
|
||||
// PK
|
||||
public Guid UserCredentialId { get; set; }
|
||||
|
||||
// FK References
|
||||
public Guid UserAccountId { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
|
||||
// Attributes
|
||||
public DateTime Expiry { get; set; }
|
||||
public string Hash { get; set; } = string.Empty;
|
||||
public byte[]? Timer { get; set; }
|
||||
public bool IsRevoked { get; set; }
|
||||
public DateTime? RevokedAt { get; set; }
|
||||
|
||||
// Audit Fields
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public byte[]? RowVersion { get; set; }
|
||||
}
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
namespace Domain.Entities;
|
||||
|
||||
public class UserVerification
|
||||
public sealed class UserVerification
|
||||
{
|
||||
// PK
|
||||
public Guid UserVerificationId { get; set; }
|
||||
|
||||
// FK References
|
||||
public Guid UserAccountId { get; set; }
|
||||
|
||||
// Attributes
|
||||
public DateTime VerificationDateTime { get; set; }
|
||||
public byte[]? Timer { get; set; }
|
||||
|
||||
// Audit Fields
|
||||
public byte[]? RowVersion { get; set; }
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ public class UpdateBreweryHandlerTests
|
||||
persisted.PostedById.Should().Be(command.PostedById);
|
||||
persisted.BreweryName.Should().Be("Renamed");
|
||||
persisted.Description.Should().Be("New description");
|
||||
persisted.Timer.Should().Equal(command.Timer);
|
||||
persisted.RowVersion.Should().Equal(command.RowVersion);
|
||||
persisted.UpdatedAt.Should().NotBeNull();
|
||||
persisted.UpdatedAt!.Value.Should().BeOnOrAfter(before).And.BeOnOrBefore(after);
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ public class BreweryRepositoryTests
|
||||
("Description", typeof(string)),
|
||||
("CreatedAt", typeof(DateTime)),
|
||||
("UpdatedAt", typeof(DateTime?)),
|
||||
("Timer", typeof(byte[])),
|
||||
("RowVersion", typeof(byte[])),
|
||||
("BreweryPostLocationId", typeof(Guid)),
|
||||
("CityId", typeof(Guid)),
|
||||
("AddressLine1", typeof(string)),
|
||||
@@ -230,7 +230,7 @@ public class BreweryRepositoryTests
|
||||
public async Task UpdateAsync_ReturnsFreshlyPersistedBrewery_WhenSuccessful()
|
||||
{
|
||||
Guid breweryId = Guid.NewGuid();
|
||||
byte[] newTimer = [0x00, 0x02];
|
||||
byte[] newRowVersion = [0x00, 0x02];
|
||||
MockDbConnection conn = new();
|
||||
|
||||
conn.Mocks.When(cmd =>
|
||||
@@ -239,7 +239,7 @@ public class BreweryRepositoryTests
|
||||
.ReturnsScalar(1);
|
||||
conn.Mocks.When(cmd => cmd.CommandText.Contains("SELECT 1 FROM dbo.City")).ReturnsScalar(1);
|
||||
conn.Mocks.When(cmd =>
|
||||
cmd.CommandText.Contains("WHERE BreweryPostID = @BreweryPostId AND Timer = @Timer")
|
||||
cmd.CommandText.Contains("WHERE BreweryPostID = @BreweryPostId AND RowVersion = @RowVersion")
|
||||
)
|
||||
.ReturnsScalar(1);
|
||||
conn.Mocks.When(cmd => cmd.CommandText.Contains("SELECT 1 FROM dbo.BreweryPostLocation"))
|
||||
@@ -247,7 +247,7 @@ public class BreweryRepositoryTests
|
||||
conn.Mocks.When(cmd => cmd.CommandText.Contains("UPDATE dbo.BreweryPostLocation"))
|
||||
.ReturnsScalar(1);
|
||||
|
||||
// Repository re-fetches the row after a successful update to return the new Timer.
|
||||
// Repository re-fetches the row after a successful update to return the new RowVersion.
|
||||
conn.Mocks.When(cmd => cmd.CommandText.Contains("LEFT JOIN dbo.BreweryPostLocation"))
|
||||
.ReturnsTable(
|
||||
MockTable
|
||||
@@ -258,7 +258,7 @@ public class BreweryRepositoryTests
|
||||
("Description", typeof(string)),
|
||||
("CreatedAt", typeof(DateTime)),
|
||||
("UpdatedAt", typeof(DateTime?)),
|
||||
("Timer", typeof(byte[])),
|
||||
("RowVersion", typeof(byte[])),
|
||||
("BreweryPostLocationId", typeof(Guid)),
|
||||
("CityId", typeof(Guid)),
|
||||
("AddressLine1", typeof(string)),
|
||||
@@ -280,7 +280,7 @@ public class BreweryRepositoryTests
|
||||
"Updated description",
|
||||
DateTime.UtcNow,
|
||||
DateTime.UtcNow,
|
||||
newTimer,
|
||||
newRowVersion,
|
||||
Guid.NewGuid(),
|
||||
Guid.NewGuid(),
|
||||
"123 Main St",
|
||||
@@ -304,7 +304,7 @@ public class BreweryRepositoryTests
|
||||
PostedById = Guid.NewGuid(),
|
||||
BreweryName = "Renamed Brewery",
|
||||
Description = "Updated description",
|
||||
Timer = [0x00, 0x01],
|
||||
RowVersion = [0x00, 0x01],
|
||||
Location = new BreweryPostLocation
|
||||
{
|
||||
CityId = Guid.NewGuid(),
|
||||
@@ -316,7 +316,7 @@ public class BreweryRepositoryTests
|
||||
BreweryPost result = await repo.UpdateAsync(brewery);
|
||||
|
||||
result.BreweryPostId.Should().Be(breweryId);
|
||||
result.Timer.Should().Equal(newTimer);
|
||||
result.RowVersion.Should().Equal(newRowVersion);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -329,14 +329,14 @@ public class BreweryRepositoryTests
|
||||
.ReturnsScalar((int?)null);
|
||||
|
||||
BreweryRepository repo = CreateRepo(conn);
|
||||
BreweryPost brewery = new() { BreweryPostId = Guid.NewGuid(), Timer = [0x00, 0x01] };
|
||||
BreweryPost brewery = new() { BreweryPostId = Guid.NewGuid(), RowVersion = [0x00, 0x01] };
|
||||
|
||||
Func<Task> act = async () => await repo.UpdateAsync(brewery);
|
||||
await act.Should().ThrowAsync<NotFoundException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpdateAsync_ThrowsConflict_WhenTimerStale()
|
||||
public async Task UpdateAsync_ThrowsConflict_WhenRowVersionStale()
|
||||
{
|
||||
MockDbConnection conn = new();
|
||||
conn.Mocks.When(cmd =>
|
||||
@@ -344,10 +344,10 @@ public class BreweryRepositoryTests
|
||||
)
|
||||
.ReturnsScalar(1);
|
||||
|
||||
// Brewery exists but the conditional UPDATE (WHERE ... AND Timer = @Timer) matches no rows,
|
||||
// since the caller's Timer no longer matches the stored row.
|
||||
// Brewery exists but the conditional UPDATE (WHERE ... AND RowVersion = @RowVersion) matches no rows,
|
||||
// since the caller's RowVersion no longer matches the stored row.
|
||||
conn.Mocks.When(cmd =>
|
||||
cmd.CommandText.Contains("WHERE BreweryPostID = @BreweryPostId AND Timer = @Timer")
|
||||
cmd.CommandText.Contains("WHERE BreweryPostID = @BreweryPostId AND RowVersion = @RowVersion")
|
||||
)
|
||||
.ReturnsScalar(0);
|
||||
|
||||
@@ -357,7 +357,7 @@ public class BreweryRepositoryTests
|
||||
BreweryPostId = Guid.NewGuid(),
|
||||
BreweryName = "Renamed Brewery",
|
||||
Description = "Updated description",
|
||||
Timer = [0x00, 0x01],
|
||||
RowVersion = [0x00, 0x01],
|
||||
};
|
||||
|
||||
Func<Task> act = async () => await repo.UpdateAsync(brewery);
|
||||
|
||||
@@ -19,13 +19,13 @@ public record UpdateBreweryLocation(
|
||||
|
||||
/// <summary>
|
||||
/// Updates an existing brewery post. Bound directly from the request body of <c>PUT /api/brewery/{id}</c>.
|
||||
/// A <c>null</c> <see cref="Location" /> clears the brewery's location. <see cref="Timer" /> must be the
|
||||
/// A <c>null</c> <see cref="Location" /> clears the brewery's location. <see cref="RowVersion" /> must be the
|
||||
/// row-version last read for this brewery (e.g. from a prior <c>GET</c>); the update is rejected with a
|
||||
/// <c>409 Conflict</c> if the brewery was modified since then.
|
||||
/// </summary>
|
||||
public record UpdateBreweryCommand(
|
||||
Guid BreweryPostId,
|
||||
byte[] Timer,
|
||||
byte[] RowVersion,
|
||||
Guid PostedById,
|
||||
string BreweryName,
|
||||
string Description,
|
||||
|
||||
@@ -19,7 +19,7 @@ public class UpdateBreweryHandler(IBreweryRepository repository)
|
||||
/// </exception>
|
||||
/// <exception cref="Domain.Exceptions.ConflictException">
|
||||
/// Thrown when the brewery was modified by another request since <paramref name="request" />.
|
||||
/// <c>Timer</c> was read.
|
||||
/// <c>RowVersion</c> was read.
|
||||
/// </exception>
|
||||
public async Task<BreweryDto> Handle(
|
||||
UpdateBreweryCommand request,
|
||||
@@ -32,7 +32,7 @@ public class UpdateBreweryHandler(IBreweryRepository repository)
|
||||
PostedById = request.PostedById,
|
||||
BreweryName = request.BreweryName,
|
||||
Description = request.Description,
|
||||
Timer = request.Timer,
|
||||
RowVersion = request.RowVersion,
|
||||
UpdatedAt = DateTime.UtcNow,
|
||||
Location = request.Location is null
|
||||
? null
|
||||
|
||||
@@ -7,8 +7,8 @@ public class UpdateBreweryValidator : AbstractValidator<UpdateBreweryCommand>
|
||||
{
|
||||
public UpdateBreweryValidator()
|
||||
{
|
||||
RuleFor(x => x.Timer)
|
||||
RuleFor(x => x.RowVersion)
|
||||
.NotEmpty()
|
||||
.WithMessage("Timer is required to detect conflicting concurrent updates.");
|
||||
.WithMessage("RowVersion is required to detect conflicting concurrent updates.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,7 +87,7 @@ public class BreweryController(IMediator mediator) : ControllerBase
|
||||
/// <returns>
|
||||
/// <c>200 OK</c> with the updated brewery; <c>400 Bad Request</c> if the route ID does not match the
|
||||
/// payload ID; <c>404 Not Found</c> if the brewery or its <c>CityId</c> does not exist; or
|
||||
/// <c>409 Conflict</c> if the brewery was modified since <c>command.Timer</c> was read.
|
||||
/// <c>409 Conflict</c> if the brewery was modified since <c>command.RowVersion</c> was read.
|
||||
/// </returns>
|
||||
[HttpPut("{id:guid}")]
|
||||
public async Task<ActionResult<ResponseBody<BreweryDto>>> Update(
|
||||
|
||||
@@ -1,51 +1,35 @@
|
||||
namespace Features.Breweries.Dtos;
|
||||
|
||||
/// <summary>Location details of a brewery, as returned by the API.</summary>
|
||||
public class BreweryLocationDto
|
||||
{
|
||||
public Guid BreweryPostLocationId { get; set; }
|
||||
public Guid CityId { get; set; }
|
||||
public string CityName { get; set; } = string.Empty;
|
||||
/// <param name="Coordinates">The brewery's geographic coordinates, in a raw binary representation.</param>
|
||||
public record BreweryLocationDto(
|
||||
Guid BreweryPostLocationId,
|
||||
Guid CityId,
|
||||
string CityName,
|
||||
string StateProvinceName,
|
||||
string StateProvinceCode,
|
||||
string CountryName,
|
||||
string CountryCode,
|
||||
string AddressLine1,
|
||||
string? AddressLine2,
|
||||
string PostalCode,
|
||||
byte[]? Coordinates
|
||||
);
|
||||
|
||||
public string StateProvinceName { get; set; } = string.Empty;
|
||||
|
||||
public string StateProvinceCode { get; set; } = string.Empty;
|
||||
|
||||
public string CountryName { get; set; } = string.Empty;
|
||||
|
||||
public string CountryCode { get; set; } = string.Empty;
|
||||
|
||||
public string AddressLine1 { get; set; } = string.Empty;
|
||||
|
||||
public string? AddressLine2 { get; set; }
|
||||
|
||||
public string PostalCode { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Gets or sets the brewery's geographic coordinates, in a raw binary representation.</summary>
|
||||
public byte[]? Coordinates { get; set; }
|
||||
}
|
||||
public record BeerPreviewDto(Guid BeerPostId, string Name, decimal ABV, int IBU);
|
||||
|
||||
/// <summary>A brewery post, as returned by the API.</summary>
|
||||
public class BreweryDto
|
||||
{
|
||||
public Guid BreweryPostId { get; set; }
|
||||
|
||||
public Guid PostedById { get; set; }
|
||||
|
||||
public string BreweryName { get; set; } = string.Empty;
|
||||
|
||||
public string Description { get; set; } = string.Empty;
|
||||
|
||||
public DateTime CreatedAt { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the date and time of the last edit, or <see langword="null"/> if never edited.</summary>
|
||||
public DateTime? UpdatedAt { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the row-version/concurrency token used to detect conflicting concurrent updates.</summary>
|
||||
public byte[]? Timer { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the brewery's location, or <see langword="null"/> if none has been set.</summary>
|
||||
public BreweryLocationDto? Location { get; set; }
|
||||
}
|
||||
|
||||
|
||||
/// <param name="UpdatedAt">The date and time of the last edit, or <see langword="null"/> if never edited.</param>
|
||||
/// <param name="RowVersion">The row-version/concurrency token used to detect conflicting concurrent updates.</param>
|
||||
/// <param name="Location">The brewery's location, or <see langword="null"/> if none has been set.</param>
|
||||
public record BreweryDto(
|
||||
Guid BreweryPostId,
|
||||
Guid PostedById,
|
||||
string BreweryName,
|
||||
string Description,
|
||||
DateTime CreatedAt,
|
||||
DateTime? UpdatedAt,
|
||||
byte[]? RowVersion,
|
||||
BreweryLocationDto? Location,
|
||||
List<BeerPreviewDto> BeerPosts
|
||||
);
|
||||
|
||||
@@ -11,31 +11,42 @@ public static class BreweryDtoMapper
|
||||
/// </summary>
|
||||
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,
|
||||
Timer = brewery.Timer,
|
||||
Location = brewery.Location is null
|
||||
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
|
||||
? 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?.ISO3166_2 ?? string.Empty,
|
||||
CountryName = brewery.Location.City?.StateProvince?.Country?.CountryName ?? string.Empty,
|
||||
CountryCode = brewery.Location.City?.StateProvince?.Country?.ISO3166_1 ?? string.Empty,
|
||||
AddressLine1 = brewery.Location.AddressLine1,
|
||||
AddressLine2 = brewery.Location.AddressLine2,
|
||||
PostalCode = brewery.Location.PostalCode,
|
||||
Coordinates = brewery.Location.Coordinates,
|
||||
},
|
||||
};
|
||||
: 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?.ISO3166_2
|
||||
?? string.Empty,
|
||||
CountryName: brewery.Location.City?.StateProvince?.Country?.CountryName
|
||||
?? string.Empty,
|
||||
CountryCode: brewery.Location.City?.StateProvince?.Country?.ISO3166_1
|
||||
?? string.Empty,
|
||||
AddressLine1: brewery.Location.AddressLine1,
|
||||
AddressLine2: brewery.Location.AddressLine2,
|
||||
PostalCode: brewery.Location.PostalCode,
|
||||
Coordinates: brewery.Location.Coordinates
|
||||
),
|
||||
BeerPosts:
|
||||
[
|
||||
.. brewery.BeerPosts.Select(beer => new BeerPreviewDto(
|
||||
BeerPostId: beer.BeerPostId,
|
||||
Name: beer.Name,
|
||||
ABV: beer.ABV,
|
||||
IBU: beer.IBU
|
||||
)),
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,29 +22,28 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory)
|
||||
: Repository<BreweryPost>(connectionFactory),
|
||||
IBreweryRepository
|
||||
{
|
||||
private const string SelectColumns =
|
||||
"""
|
||||
bp.BreweryPostID,
|
||||
bp.PostedByID,
|
||||
bp.BreweryName,
|
||||
bp.Description,
|
||||
bp.CreatedAt,
|
||||
bp.UpdatedAt,
|
||||
bp.Timer,
|
||||
bpl.BreweryPostLocationID,
|
||||
bpl.CityID,
|
||||
bpl.AddressLine1,
|
||||
bpl.AddressLine2,
|
||||
bpl.PostalCode,
|
||||
bpl.Coordinates,
|
||||
c.CityName,
|
||||
c.StateProvinceID,
|
||||
sp.StateProvinceName,
|
||||
sp.ISO3166_2,
|
||||
sp.CountryID,
|
||||
co.CountryName,
|
||||
co.ISO3166_1
|
||||
""";
|
||||
private const string SelectColumns = """
|
||||
bp.BreweryPostID,
|
||||
bp.PostedByID,
|
||||
bp.BreweryName,
|
||||
bp.Description,
|
||||
bp.CreatedAt,
|
||||
bp.UpdatedAt,
|
||||
bp.RowVersion,
|
||||
bpl.BreweryPostLocationID,
|
||||
bpl.CityID,
|
||||
bpl.AddressLine1,
|
||||
bpl.AddressLine2,
|
||||
bpl.PostalCode,
|
||||
bpl.Coordinates,
|
||||
c.CityName,
|
||||
c.StateProvinceID,
|
||||
sp.StateProvinceName,
|
||||
sp.ISO3166_2,
|
||||
sp.CountryID,
|
||||
co.CountryName,
|
||||
co.ISO3166_1
|
||||
""";
|
||||
|
||||
/// <summary>
|
||||
/// Joins from <c>dbo.BreweryPost</c> down to <c>dbo.Country</c> so a row carries everything needed
|
||||
@@ -52,26 +51,24 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory)
|
||||
/// <c>Country</c> chain. All joins are <c>LEFT JOIN</c> (matching the previous stored procedure's
|
||||
/// behavior), since a brewery post may have no location.
|
||||
/// </summary>
|
||||
private const string FromJoins =
|
||||
"""
|
||||
dbo.BreweryPost bp
|
||||
LEFT JOIN dbo.BreweryPostLocation bpl ON bp.BreweryPostID = bpl.BreweryPostID
|
||||
LEFT JOIN dbo.City c ON bpl.CityID = c.CityID
|
||||
LEFT JOIN dbo.StateProvince sp ON c.StateProvinceID = sp.StateProvinceID
|
||||
LEFT JOIN dbo.Country co ON sp.CountryID = co.CountryID
|
||||
""";
|
||||
private const string FromJoins = """
|
||||
dbo.BreweryPost bp
|
||||
LEFT JOIN dbo.BreweryPostLocation bpl ON bp.BreweryPostID = bpl.BreweryPostID
|
||||
LEFT JOIN dbo.City c ON bpl.CityID = c.CityID
|
||||
LEFT JOIN dbo.StateProvince sp ON c.StateProvinceID = sp.StateProvinceID
|
||||
LEFT JOIN dbo.Country co ON sp.CountryID = co.CountryID
|
||||
""";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<BreweryPost?> GetByIdAsync(Guid id)
|
||||
{
|
||||
await using DbConnection connection = await CreateConnection();
|
||||
await using DbCommand command = connection.CreateCommand();
|
||||
command.CommandText =
|
||||
$"""
|
||||
SELECT {SelectColumns}
|
||||
FROM {FromJoins}
|
||||
WHERE bp.BreweryPostID = @BreweryPostId
|
||||
""";
|
||||
command.CommandText = $"""
|
||||
SELECT {SelectColumns}
|
||||
FROM {FromJoins}
|
||||
WHERE bp.BreweryPostID = @BreweryPostId
|
||||
""";
|
||||
AddParameter(command, "@BreweryPostId", id);
|
||||
|
||||
await using DbDataReader reader = await command.ExecuteReaderAsync();
|
||||
@@ -79,17 +76,17 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory)
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
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 DbCommand command = connection.CreateCommand();
|
||||
command.CommandText =
|
||||
$"""
|
||||
SELECT {SelectColumns}
|
||||
FROM {FromJoins}
|
||||
ORDER BY bp.CreatedAt DESC
|
||||
OFFSET @Offset ROWS FETCH NEXT @Limit ROWS ONLY
|
||||
""";
|
||||
command.CommandText = $"""
|
||||
SELECT {SelectColumns}
|
||||
FROM {FromJoins}
|
||||
ORDER BY bp.CreatedAt DESC
|
||||
OFFSET @Offset ROWS FETCH NEXT @Limit ROWS ONLY
|
||||
""";
|
||||
AddParameter(command, "@Offset", offset ?? 0);
|
||||
AddParameter(command, "@Limit", limit ?? int.MaxValue);
|
||||
|
||||
@@ -106,18 +103,25 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory)
|
||||
public async Task<BreweryPost> UpdateAsync(BreweryPost brewery)
|
||||
{
|
||||
await using DbConnection connection = await CreateConnection();
|
||||
await using DbTransaction transaction = await connection.BeginTransactionAsync();
|
||||
await using DbTransaction transaction =
|
||||
await connection.BeginTransactionAsync();
|
||||
|
||||
try
|
||||
{
|
||||
bool breweryExists =
|
||||
await connection.ExecuteScalarAsync<int?>(
|
||||
new CommandDefinition(
|
||||
"SELECT 1 FROM dbo.BreweryPost WHERE BreweryPostID = @BreweryPostId",
|
||||
new { brewery.BreweryPostId },
|
||||
transaction
|
||||
new CommandDefinition(
|
||||
"""
|
||||
SELECT 1 FROM
|
||||
dbo.BreweryPost
|
||||
WHERE
|
||||
BreweryPostID = @BreweryPostId
|
||||
""",
|
||||
new { brewery.BreweryPostId },
|
||||
transaction
|
||||
)
|
||||
)
|
||||
) is not null;
|
||||
is not null;
|
||||
|
||||
if (!breweryExists)
|
||||
throw new NotFoundException("Brewery not found.");
|
||||
@@ -126,12 +130,13 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory)
|
||||
{
|
||||
bool cityExists =
|
||||
await connection.ExecuteScalarAsync<int?>(
|
||||
new CommandDefinition(
|
||||
"SELECT 1 FROM dbo.City WHERE CityID = @CityId",
|
||||
new { brewery.Location.CityId },
|
||||
transaction
|
||||
new CommandDefinition(
|
||||
"SELECT 1 FROM dbo.City WHERE CityID = @CityId",
|
||||
new { brewery.Location.CityId },
|
||||
transaction
|
||||
)
|
||||
)
|
||||
) is not null;
|
||||
is not null;
|
||||
|
||||
if (!cityExists)
|
||||
throw new NotFoundException("City not found.");
|
||||
@@ -141,15 +146,19 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory)
|
||||
new CommandDefinition(
|
||||
"""
|
||||
UPDATE dbo.BreweryPost
|
||||
SET BreweryName = @BreweryName, Description = @Description, UpdatedAt = GETDATE()
|
||||
WHERE BreweryPostID = @BreweryPostId AND Timer = @Timer
|
||||
SET BreweryName = @BreweryName,
|
||||
Description = @Description,
|
||||
UpdatedAt = GETDATE()
|
||||
WHERE
|
||||
BreweryPostID = @BreweryPostId
|
||||
AND RowVersion = @RowVersion
|
||||
""",
|
||||
new
|
||||
{
|
||||
brewery.BreweryPostId,
|
||||
brewery.BreweryName,
|
||||
brewery.Description,
|
||||
brewery.Timer,
|
||||
brewery.RowVersion,
|
||||
},
|
||||
transaction
|
||||
)
|
||||
@@ -165,7 +174,11 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory)
|
||||
// No location supplied: clear any existing location for this brewery.
|
||||
await connection.ExecuteAsync(
|
||||
new CommandDefinition(
|
||||
"DELETE FROM dbo.BreweryPostLocation WHERE BreweryPostID = @BreweryPostId",
|
||||
"""
|
||||
DELETE FROM
|
||||
dbo.BreweryPostLocation
|
||||
WHERE BreweryPostID = @BreweryPostId
|
||||
""",
|
||||
new { brewery.BreweryPostId },
|
||||
transaction
|
||||
)
|
||||
@@ -175,20 +188,28 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory)
|
||||
{
|
||||
bool locationExists =
|
||||
await connection.ExecuteScalarAsync<int?>(
|
||||
new CommandDefinition(
|
||||
"SELECT 1 FROM dbo.BreweryPostLocation WHERE BreweryPostID = @BreweryPostId",
|
||||
new { brewery.BreweryPostId },
|
||||
transaction
|
||||
new CommandDefinition(
|
||||
"""
|
||||
SELECT 1
|
||||
FROM dbo.BreweryPostLocation
|
||||
WHERE BreweryPostID = @BreweryPostId
|
||||
""",
|
||||
new { brewery.BreweryPostId },
|
||||
transaction
|
||||
)
|
||||
)
|
||||
) is not null;
|
||||
is not null;
|
||||
|
||||
if (locationExists)
|
||||
await connection.ExecuteAsync(
|
||||
new CommandDefinition(
|
||||
"""
|
||||
UPDATE dbo.BreweryPostLocation
|
||||
SET CityID = @CityId, AddressLine1 = @AddressLine1, AddressLine2 = @AddressLine2,
|
||||
PostalCode = @PostalCode, Coordinates = @Coordinates
|
||||
SET CityID = @CityId,
|
||||
AddressLine1 = @AddressLine1,
|
||||
AddressLine2 = @AddressLine2,
|
||||
PostalCode = @PostalCode,
|
||||
Coordinates = @Coordinates
|
||||
WHERE BreweryPostID = @BreweryPostId
|
||||
""",
|
||||
new
|
||||
@@ -208,8 +229,21 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory)
|
||||
new CommandDefinition(
|
||||
"""
|
||||
INSERT INTO dbo.BreweryPostLocation
|
||||
(BreweryPostLocationID, BreweryPostID, CityID, AddressLine1, AddressLine2, PostalCode, Coordinates)
|
||||
VALUES (@BreweryPostLocationId, @BreweryPostId, @CityId, @AddressLine1, @AddressLine2, @PostalCode, @Coordinates)
|
||||
(BreweryPostLocationID,
|
||||
BreweryPostID,
|
||||
CityID,
|
||||
AddressLine1,
|
||||
AddressLine2,
|
||||
PostalCode,
|
||||
Coordinates)
|
||||
VALUES
|
||||
(@BreweryPostLocationId,
|
||||
@BreweryPostId,
|
||||
@CityId,
|
||||
@AddressLine1,
|
||||
@AddressLine2,
|
||||
@PostalCode,
|
||||
@Coordinates)
|
||||
""",
|
||||
new
|
||||
{
|
||||
@@ -235,9 +269,9 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory)
|
||||
}
|
||||
|
||||
return await GetByIdAsync(brewery.BreweryPostId)
|
||||
?? 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."
|
||||
);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -245,7 +279,12 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory)
|
||||
{
|
||||
await using DbConnection connection = await CreateConnection();
|
||||
int rows = await connection.ExecuteAsync(
|
||||
"DELETE FROM dbo.BreweryPost WHERE BreweryPostID = @BreweryPostId",
|
||||
"""
|
||||
DELETE FROM
|
||||
dbo.BreweryPost
|
||||
WHERE
|
||||
BreweryPostID = @BreweryPostId
|
||||
""",
|
||||
new { BreweryPostId = id }
|
||||
);
|
||||
|
||||
@@ -257,40 +296,61 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory)
|
||||
public async Task CreateAsync(BreweryPost brewery)
|
||||
{
|
||||
if (brewery.Location is null)
|
||||
throw new ArgumentException("Location must be provided when creating a brewery.");
|
||||
throw new ArgumentException(
|
||||
"Location must be provided when creating a brewery.");
|
||||
|
||||
await using DbConnection connection = await CreateConnection();
|
||||
await using DbTransaction transaction = await connection.BeginTransactionAsync();
|
||||
await using DbTransaction transaction =
|
||||
await connection.BeginTransactionAsync();
|
||||
|
||||
try
|
||||
{
|
||||
bool userExists =
|
||||
await connection.ExecuteScalarAsync<int?>(
|
||||
new CommandDefinition(
|
||||
"SELECT 1 FROM dbo.UserAccount WHERE UserAccountID = @PostedById",
|
||||
new { brewery.PostedById },
|
||||
transaction
|
||||
new CommandDefinition(
|
||||
"""
|
||||
SELECT 1
|
||||
FROM
|
||||
dbo.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<int?>(
|
||||
new CommandDefinition(
|
||||
"SELECT 1 FROM dbo.City WHERE CityID = @CityId",
|
||||
new { brewery.Location.CityId },
|
||||
transaction
|
||||
new CommandDefinition(
|
||||
"""
|
||||
SELECT 1
|
||||
FROM
|
||||
dbo.City
|
||||
WHERE
|
||||
CityID = @CityId
|
||||
""",
|
||||
new { brewery.Location.CityId },
|
||||
transaction
|
||||
)
|
||||
)
|
||||
) is not null;
|
||||
is not null;
|
||||
|
||||
if (!cityExists)
|
||||
throw new NotFoundException("City not found.");
|
||||
|
||||
await connection.ExecuteAsync(
|
||||
new CommandDefinition(
|
||||
"INSERT INTO dbo.BreweryPost (BreweryPostID, BreweryName, Description, PostedByID) VALUES (@BreweryPostId, @BreweryName, @Description, @PostedById)",
|
||||
"""
|
||||
INSERT INTO dbo.BreweryPost
|
||||
(BreweryPostID, BreweryName, Description, PostedByID)
|
||||
VALUES
|
||||
(@BreweryPostId, @BreweryName, @Description, @PostedById)
|
||||
""",
|
||||
new
|
||||
{
|
||||
brewery.BreweryPostId,
|
||||
@@ -306,8 +366,20 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory)
|
||||
new CommandDefinition(
|
||||
"""
|
||||
INSERT INTO dbo.BreweryPostLocation
|
||||
(BreweryPostLocationID, BreweryPostID, CityID, AddressLine1, AddressLine2, PostalCode, Coordinates)
|
||||
VALUES (@BreweryPostLocationId, @BreweryPostId, @CityId, @AddressLine1, @AddressLine2, @PostalCode, @Coordinates)
|
||||
(BreweryPostLocationID,
|
||||
BreweryPostID,
|
||||
CityID,
|
||||
AddressLine1,
|
||||
AddressLine2,
|
||||
PostalCode,
|
||||
Coordinates)
|
||||
VALUES (@BreweryPostLocationId,
|
||||
@BreweryPostId,
|
||||
@CityId,
|
||||
@AddressLine1,
|
||||
@AddressLine2,
|
||||
@PostalCode,
|
||||
@Coordinates)
|
||||
""",
|
||||
new
|
||||
{
|
||||
@@ -344,7 +416,7 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory)
|
||||
int ordDescription = reader.GetOrdinal("Description");
|
||||
int ordCreatedAt = reader.GetOrdinal("CreatedAt");
|
||||
int ordUpdatedAt = reader.GetOrdinal("UpdatedAt");
|
||||
int ordTimer = reader.GetOrdinal("Timer");
|
||||
int ordRowVersion = reader.GetOrdinal("RowVersion");
|
||||
|
||||
BreweryPost brewery = new()
|
||||
{
|
||||
@@ -353,8 +425,12 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory)
|
||||
BreweryName = reader.GetString(ordBreweryName),
|
||||
Description = reader.GetString(ordDescription),
|
||||
CreatedAt = reader.GetDateTime(ordCreatedAt),
|
||||
UpdatedAt = reader.IsDBNull(ordUpdatedAt) ? null : reader.GetDateTime(ordUpdatedAt),
|
||||
Timer = reader.IsDBNull(ordTimer) ? null : reader.GetFieldValue<byte[]>(ordTimer),
|
||||
UpdatedAt = reader.IsDBNull(ordUpdatedAt)
|
||||
? null
|
||||
: reader.GetDateTime(ordUpdatedAt),
|
||||
RowVersion = reader.IsDBNull(ordRowVersion)
|
||||
? null
|
||||
: reader.GetFieldValue<byte[]>(ordRowVersion),
|
||||
};
|
||||
|
||||
int ordLocationId = reader.GetOrdinal("BreweryPostLocationId");
|
||||
@@ -369,7 +445,8 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory)
|
||||
BreweryPostId = brewery.BreweryPostId,
|
||||
CityId = reader.GetGuid(reader.GetOrdinal("CityId")),
|
||||
City = MapCity(reader),
|
||||
AddressLine1 = reader.GetString(reader.GetOrdinal("AddressLine1")),
|
||||
AddressLine1 =
|
||||
reader.GetString(reader.GetOrdinal("AddressLine1")),
|
||||
AddressLine2 = reader.IsDBNull(ordAddressLine2)
|
||||
? null
|
||||
: reader.GetString(ordAddressLine2),
|
||||
@@ -394,24 +471,30 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory)
|
||||
{
|
||||
CityId = reader.GetGuid(reader.GetOrdinal("CityId")),
|
||||
CityName = reader.GetString(reader.GetOrdinal("CityName")),
|
||||
StateProvinceId = reader.GetGuid(reader.GetOrdinal("StateProvinceId")),
|
||||
StateProvinceId =
|
||||
reader.GetGuid(reader.GetOrdinal("StateProvinceId")),
|
||||
StateProvince = new StateProvince
|
||||
{
|
||||
StateProvinceId = reader.GetGuid(reader.GetOrdinal("StateProvinceId")),
|
||||
StateProvinceName = reader.GetString(reader.GetOrdinal("StateProvinceName")),
|
||||
StateProvinceId =
|
||||
reader.GetGuid(reader.GetOrdinal("StateProvinceId")),
|
||||
StateProvinceName =
|
||||
reader.GetString(reader.GetOrdinal("StateProvinceName")),
|
||||
ISO3166_2 = reader.GetString(reader.GetOrdinal("ISO3166_2")),
|
||||
CountryId = reader.GetGuid(reader.GetOrdinal("CountryId")),
|
||||
Country = new Country
|
||||
{
|
||||
CountryId = reader.GetGuid(reader.GetOrdinal("CountryId")),
|
||||
CountryName = reader.GetString(reader.GetOrdinal("CountryName")),
|
||||
ISO3166_1 = reader.GetString(reader.GetOrdinal("ISO3166_1")),
|
||||
CountryName =
|
||||
reader.GetString(reader.GetOrdinal("CountryName")),
|
||||
ISO3166_1 =
|
||||
reader.GetString(reader.GetOrdinal("ISO3166_1")),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private static void AddParameter(DbCommand command, string name, object? value)
|
||||
private static void AddParameter(DbCommand command, string name,
|
||||
object? value)
|
||||
{
|
||||
DbParameter p = command.CreateParameter();
|
||||
p.ParameterName = name;
|
||||
|
||||
@@ -23,17 +23,17 @@ public interface IBreweryRepository
|
||||
|
||||
/// <summary>
|
||||
/// Updates a brewery post's name and description, and upserts or clears its location, enforcing
|
||||
/// optimistic concurrency via <paramref name="brewery" />'s <c>Timer</c>: the update is rejected
|
||||
/// if the row was modified since <c>Timer</c> was read. When <paramref name="brewery" />'s
|
||||
/// 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>Timer</c>.
|
||||
/// Returns the freshly persisted brewery, including its new <c>RowVersion</c>.
|
||||
/// </summary>
|
||||
/// <exception cref="Domain.Exceptions.NotFoundException">
|
||||
/// Thrown when no brewery exists with the given ID, or the given location's <c>CityId</c> does not
|
||||
/// exist.
|
||||
/// </exception>
|
||||
/// <exception cref="Domain.Exceptions.ConflictException">
|
||||
/// Thrown when <paramref name="brewery" />'s <c>Timer</c> no longer matches the stored row (it was
|
||||
/// Thrown when <paramref name="brewery" />'s <c>RowVersion</c> no longer matches the stored row (it was
|
||||
/// modified by another request since it was last read).
|
||||
/// </exception>
|
||||
Task<BreweryPost> UpdateAsync(BreweryPost brewery);
|
||||
|
||||
Reference in New Issue
Block a user