Compare commits

..

2 Commits

Author SHA1 Message Date
Aaron Po
2b2df0053f updates 2026-08-19 16:27:55 -04:00
Aaron Po
ab265de54d - Updated column names from 'Timer' to 'RowVersion' in schema.sql for
- Introduced new BeerPost entity and updated BreweryPost and BreweryPostLocation entities to use 'RowVersion' for concurrency.
- Modified UserAccount, UserCredential, UserVerification, City, StateProvince, and Country entities to use 'RowVersion' instead of 'Timer'.
- Updated tests and command handlers to reflect changes in concurrency token from 'Timer' to 'RowVersion'.
- Adjusted BreweryDto and BreweryDtoMapper to include 'RowVersion' and updated mapping logic accordingly.
- Ensured all repository methods and interfaces are aligned with the new concurrency handling approach.
2026-08-19 16:15:34 -04:00
19 changed files with 398 additions and 236 deletions

View File

@@ -39,7 +39,7 @@ CREATE TABLE dbo.UserAccount
DateOfBirth DATE NOT NULL, DateOfBirth DATE NOT NULL,
Timer ROWVERSION, RowVersion ROWVERSION,
CONSTRAINT PK_UserAccount CONSTRAINT PK_UserAccount
PRIMARY KEY (UserAccountID), 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 UploadedAt DATETIME NOT NULL
CONSTRAINT DF_Photo_UploadedAt DEFAULT GETDATE(), CONSTRAINT DF_Photo_UploadedAt DEFAULT GETDATE(),
Timer ROWVERSION, RowVersion ROWVERSION,
CONSTRAINT PK_Photo CONSTRAINT PK_Photo
PRIMARY KEY (PhotoID), PRIMARY KEY (PhotoID),
@@ -94,7 +94,7 @@ CREATE TABLE UserAvatar -- delete avatar photo when user account is deleted
PhotoID UNIQUEIDENTIFIER NOT NULL, PhotoID UNIQUEIDENTIFIER NOT NULL,
Timer ROWVERSION, RowVersion ROWVERSION,
CONSTRAINT PK_UserAvatar PRIMARY KEY (UserAvatarID), 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 VerificationDateTime DATETIME NOT NULL
CONSTRAINT DF_VerificationDateTime DEFAULT GETDATE(), CONSTRAINT DF_VerificationDateTime DEFAULT GETDATE(),
Timer ROWVERSION, RowVersion ROWVERSION,
CONSTRAINT PK_UserVerification CONSTRAINT PK_UserVerification
PRIMARY KEY (UserVerificationID), PRIMARY KEY (UserVerificationID),
@@ -170,7 +170,7 @@ CREATE TABLE UserCredential -- delete credentials when user account is deleted
RevokedAt DATETIME NULL, RevokedAt DATETIME NULL,
Timer ROWVERSION, RowVersion ROWVERSION,
CONSTRAINT PK_UserCredential CONSTRAINT PK_UserCredential
PRIMARY KEY (UserCredentialID), PRIMARY KEY (UserCredentialID),
@@ -205,7 +205,7 @@ CREATE TABLE UserFollow
CreatedAt DATETIME NOT NULL CreatedAt DATETIME NOT NULL
CONSTRAINT DF_UserFollow_CreatedAt DEFAULT GETDATE(), CONSTRAINT DF_UserFollow_CreatedAt DEFAULT GETDATE(),
Timer ROWVERSION, RowVersion ROWVERSION,
CONSTRAINT PK_UserFollow CONSTRAINT PK_UserFollow
PRIMARY KEY (UserFollowID), PRIMARY KEY (UserFollowID),
@@ -245,7 +245,7 @@ CREATE TABLE Country
ISO3166_1 CHAR(2) NOT NULL, ISO3166_1 CHAR(2) NOT NULL,
Timer ROWVERSION, RowVersion ROWVERSION,
CONSTRAINT PK_Country CONSTRAINT PK_Country
PRIMARY KEY (CountryID), PRIMARY KEY (CountryID),
@@ -269,7 +269,7 @@ CREATE TABLE StateProvince
CountryID UNIQUEIDENTIFIER NOT NULL, CountryID UNIQUEIDENTIFIER NOT NULL,
Timer ROWVERSION, RowVersion ROWVERSION,
CONSTRAINT PK_StateProvince CONSTRAINT PK_StateProvince
PRIMARY KEY (StateProvinceID), PRIMARY KEY (StateProvinceID),
@@ -298,7 +298,7 @@ CREATE TABLE City
StateProvinceID UNIQUEIDENTIFIER NOT NULL, StateProvinceID UNIQUEIDENTIFIER NOT NULL,
Timer ROWVERSION, RowVersion ROWVERSION,
CONSTRAINT PK_City CONSTRAINT PK_City
PRIMARY KEY (CityID), PRIMARY KEY (CityID),
@@ -331,7 +331,7 @@ CREATE TABLE BreweryPost -- A user cannot be deleted if they have a post
UpdatedAt DATETIME NULL, UpdatedAt DATETIME NULL,
Timer ROWVERSION, RowVersion ROWVERSION,
CONSTRAINT PK_BreweryPost CONSTRAINT PK_BreweryPost
PRIMARY KEY (BreweryPostID), PRIMARY KEY (BreweryPostID),
@@ -366,7 +366,7 @@ CREATE TABLE BreweryPostLocation
Coordinates GEOGRAPHY NULL, Coordinates GEOGRAPHY NULL,
Timer ROWVERSION, RowVersion ROWVERSION,
CONSTRAINT PK_BreweryPostLocation CONSTRAINT PK_BreweryPostLocation
PRIMARY KEY (BreweryPostLocationID), PRIMARY KEY (BreweryPostLocationID),
@@ -419,7 +419,7 @@ CREATE TABLE BreweryPostPhoto -- All photos linked to a post are deleted if the
LinkedAt DATETIME NOT NULL LinkedAt DATETIME NOT NULL
CONSTRAINT DF_BreweryPostPhoto_LinkedAt DEFAULT GETDATE(), CONSTRAINT DF_BreweryPostPhoto_LinkedAt DEFAULT GETDATE(),
Timer ROWVERSION, RowVersion ROWVERSION,
CONSTRAINT PK_BreweryPostPhoto CONSTRAINT PK_BreweryPostPhoto
PRIMARY KEY (BreweryPostPhotoID), PRIMARY KEY (BreweryPostPhotoID),
@@ -455,7 +455,7 @@ CREATE TABLE BeerStyle
Description NVARCHAR(MAX), Description NVARCHAR(MAX),
Timer ROWVERSION, RowVersion ROWVERSION,
CONSTRAINT PK_BeerStyle CONSTRAINT PK_BeerStyle
PRIMARY KEY (BeerStyleID), PRIMARY KEY (BeerStyleID),
@@ -493,7 +493,7 @@ CREATE TABLE BeerPost
UpdatedAt DATETIME, UpdatedAt DATETIME,
Timer ROWVERSION, RowVersion ROWVERSION,
CONSTRAINT PK_BeerPost CONSTRAINT PK_BeerPost
PRIMARY KEY (BeerPostID), 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 LinkedAt DATETIME NOT NULL
CONSTRAINT DF_BeerPostPhoto_LinkedAt DEFAULT GETDATE(), CONSTRAINT DF_BeerPostPhoto_LinkedAt DEFAULT GETDATE(),
Timer ROWVERSION, RowVersion ROWVERSION,
CONSTRAINT PK_BeerPostPhoto CONSTRAINT PK_BeerPostPhoto
PRIMARY KEY (BeerPostPhotoID), PRIMARY KEY (BeerPostPhotoID),
@@ -590,7 +590,7 @@ CREATE TABLE BeerPostComment
UpdatedAt DATETIME NULL, UpdatedAt DATETIME NULL,
Timer ROWVERSION, RowVersion ROWVERSION,
CONSTRAINT PK_BeerPostComment CONSTRAINT PK_BeerPostComment
PRIMARY KEY (BeerPostCommentID), PRIMARY KEY (BeerPostCommentID),

View File

@@ -86,7 +86,6 @@ static async Task<int> RunAsync()
"Loading user data into target database...", "Loading user data into target database...",
async ctx => async ctx =>
{ {
Thread.Sleep(3000); // Simulate some work being done
postedByIds = await LoadUsersIntoDatabaseAsync( postedByIds = await LoadUsersIntoDatabaseAsync(
authRepository, authRepository,
passwordInfrastructure, passwordInfrastructure,

View 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; }
}

View File

@@ -1,13 +1,23 @@
namespace Domain.Entities; namespace Domain.Entities;
public class BreweryPost public sealed class BreweryPost
{ {
// PK
public Guid BreweryPostId { get; set; } public Guid BreweryPostId { get; set; }
// FK References
public Guid PostedById { get; set; } public Guid PostedById { get; set; }
// Attributes
public string BreweryName { get; set; } = string.Empty; public string BreweryName { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty; public string Description { get; set; } = string.Empty;
// Audit Fields
public DateTime CreatedAt { get; set; } public DateTime CreatedAt { get; set; }
public DateTime? UpdatedAt { 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 BreweryPostLocation? Location { get; set; }
public List<BeerPost> BeerPosts { get; set; } = [];
} }

View File

@@ -1,14 +1,23 @@
namespace Domain.Entities; namespace Domain.Entities;
public class BreweryPostLocation public sealed class BreweryPostLocation
{ {
// PK
public Guid BreweryPostLocationId { get; set; } public Guid BreweryPostLocationId { get; set; }
// FK References
public Guid BreweryPostId { get; set; } public Guid BreweryPostId { get; set; }
public Guid CityId { get; set; }
// Attributes
public string AddressLine1 { get; set; } = string.Empty; public string AddressLine1 { get; set; } = string.Empty;
public string? AddressLine2 { get; set; } public string? AddressLine2 { get; set; }
public string PostalCode { get; set; } = string.Empty; public string PostalCode { get; set; } = string.Empty;
public Guid CityId { get; set; }
public City? City { get; set; }
public byte[]? Coordinates { 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; }
} }

View File

@@ -1,38 +1,56 @@
namespace Domain.Entities; namespace Domain.Entities;
public class City public sealed class City
{ {
// PK
public Guid CityId { get; set; } public Guid CityId { get; set; }
public string CityName { get; set; } = string.Empty;
// FK References
public Guid StateProvinceId { get; set; } 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; } public Guid StateProvinceId { get; set; }
// FK References
public Guid CountryId { get; set; }
// Attributes
public string StateProvinceName { get; set; } = string.Empty; 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. // 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. // and is rendered like US-CA for California, CA-ON for Ontario, etc.
public string ISO3166_2 { get; set; } = string.Empty; 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 Country? Country { get; set; }
public byte[]? Timer { get; set; }
} }
public class Country public sealed class Country
{ {
// PK
public Guid CountryId { get; set; } public Guid CountryId { get; set; }
// Attributes
public string CountryName { get; set; } = string.Empty; 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 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; }
} }

View File

@@ -1,14 +1,19 @@
namespace Domain.Entities; namespace Domain.Entities;
public class UserAccount public sealed class UserAccount
{ {
// PK
public Guid UserAccountId { get; set; } public Guid UserAccountId { get; set; }
// Attributes
public string Username { get; set; } = string.Empty; public string Username { get; set; } = string.Empty;
public string FirstName { get; set; } = string.Empty; public string FirstName { get; set; } = string.Empty;
public string LastName { get; set; } = string.Empty; public string LastName { get; set; } = string.Empty;
public string Email { 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 CreatedAt { get; set; }
public DateTime? UpdatedAt { get; set; } public DateTime? UpdatedAt { get; set; }
public DateTime DateOfBirth { get; set; } public byte[]? RowVersion { get; set; }
public byte[]? Timer { get; set; }
} }

View File

@@ -1,11 +1,20 @@
namespace Domain.Entities; namespace Domain.Entities;
public class UserCredential public sealed class UserCredential
{ {
// PK
public Guid UserCredentialId { get; set; } public Guid UserCredentialId { get; set; }
// FK References
public Guid UserAccountId { get; set; } public Guid UserAccountId { get; set; }
public DateTime CreatedAt { get; set; }
// Attributes
public DateTime Expiry { get; set; } public DateTime Expiry { get; set; }
public string Hash { get; set; } = string.Empty; 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; }
} }

View File

@@ -1,9 +1,16 @@
namespace Domain.Entities; namespace Domain.Entities;
public class UserVerification public sealed class UserVerification
{ {
// PK
public Guid UserVerificationId { get; set; } public Guid UserVerificationId { get; set; }
// FK References
public Guid UserAccountId { get; set; } public Guid UserAccountId { get; set; }
// Attributes
public DateTime VerificationDateTime { get; set; } public DateTime VerificationDateTime { get; set; }
public byte[]? Timer { get; set; }
// Audit Fields
public byte[]? RowVersion { get; set; }
} }

View File

@@ -43,7 +43,7 @@ public class UpdateBreweryHandlerTests
persisted.PostedById.Should().Be(command.PostedById); persisted.PostedById.Should().Be(command.PostedById);
persisted.BreweryName.Should().Be("Renamed"); persisted.BreweryName.Should().Be("Renamed");
persisted.Description.Should().Be("New description"); persisted.Description.Should().Be("New description");
persisted.Timer.Should().Equal(command.Timer); persisted.RowVersion.Should().Equal(command.RowVersion);
persisted.UpdatedAt.Should().NotBeNull(); persisted.UpdatedAt.Should().NotBeNull();
persisted.UpdatedAt!.Value.Should().BeOnOrAfter(before).And.BeOnOrBefore(after); persisted.UpdatedAt!.Value.Should().BeOnOrAfter(before).And.BeOnOrBefore(after);
} }

View File

@@ -34,7 +34,7 @@ public class BreweryRepositoryTests
("Description", typeof(string)), ("Description", typeof(string)),
("CreatedAt", typeof(DateTime)), ("CreatedAt", typeof(DateTime)),
("UpdatedAt", typeof(DateTime?)), ("UpdatedAt", typeof(DateTime?)),
("Timer", typeof(byte[])), ("RowVersion", typeof(byte[])),
("BreweryPostLocationId", typeof(Guid)), ("BreweryPostLocationId", typeof(Guid)),
("CityId", typeof(Guid)), ("CityId", typeof(Guid)),
("AddressLine1", typeof(string)), ("AddressLine1", typeof(string)),
@@ -230,7 +230,7 @@ public class BreweryRepositoryTests
public async Task UpdateAsync_ReturnsFreshlyPersistedBrewery_WhenSuccessful() public async Task UpdateAsync_ReturnsFreshlyPersistedBrewery_WhenSuccessful()
{ {
Guid breweryId = Guid.NewGuid(); Guid breweryId = Guid.NewGuid();
byte[] newTimer = [0x00, 0x02]; byte[] newRowVersion = [0x00, 0x02];
MockDbConnection conn = new(); MockDbConnection conn = new();
conn.Mocks.When(cmd => conn.Mocks.When(cmd =>
@@ -239,7 +239,7 @@ public class BreweryRepositoryTests
.ReturnsScalar(1); .ReturnsScalar(1);
conn.Mocks.When(cmd => cmd.CommandText.Contains("SELECT 1 FROM dbo.City")).ReturnsScalar(1); conn.Mocks.When(cmd => cmd.CommandText.Contains("SELECT 1 FROM dbo.City")).ReturnsScalar(1);
conn.Mocks.When(cmd => conn.Mocks.When(cmd =>
cmd.CommandText.Contains("WHERE BreweryPostID = @BreweryPostId AND Timer = @Timer") cmd.CommandText.Contains("WHERE BreweryPostID = @BreweryPostId AND RowVersion = @RowVersion")
) )
.ReturnsScalar(1); .ReturnsScalar(1);
conn.Mocks.When(cmd => cmd.CommandText.Contains("SELECT 1 FROM dbo.BreweryPostLocation")) 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")) conn.Mocks.When(cmd => cmd.CommandText.Contains("UPDATE dbo.BreweryPostLocation"))
.ReturnsScalar(1); .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")) conn.Mocks.When(cmd => cmd.CommandText.Contains("LEFT JOIN dbo.BreweryPostLocation"))
.ReturnsTable( .ReturnsTable(
MockTable MockTable
@@ -258,7 +258,7 @@ public class BreweryRepositoryTests
("Description", typeof(string)), ("Description", typeof(string)),
("CreatedAt", typeof(DateTime)), ("CreatedAt", typeof(DateTime)),
("UpdatedAt", typeof(DateTime?)), ("UpdatedAt", typeof(DateTime?)),
("Timer", typeof(byte[])), ("RowVersion", typeof(byte[])),
("BreweryPostLocationId", typeof(Guid)), ("BreweryPostLocationId", typeof(Guid)),
("CityId", typeof(Guid)), ("CityId", typeof(Guid)),
("AddressLine1", typeof(string)), ("AddressLine1", typeof(string)),
@@ -280,7 +280,7 @@ public class BreweryRepositoryTests
"Updated description", "Updated description",
DateTime.UtcNow, DateTime.UtcNow,
DateTime.UtcNow, DateTime.UtcNow,
newTimer, newRowVersion,
Guid.NewGuid(), Guid.NewGuid(),
Guid.NewGuid(), Guid.NewGuid(),
"123 Main St", "123 Main St",
@@ -304,7 +304,7 @@ public class BreweryRepositoryTests
PostedById = Guid.NewGuid(), PostedById = Guid.NewGuid(),
BreweryName = "Renamed Brewery", BreweryName = "Renamed Brewery",
Description = "Updated description", Description = "Updated description",
Timer = [0x00, 0x01], RowVersion = [0x00, 0x01],
Location = new BreweryPostLocation Location = new BreweryPostLocation
{ {
CityId = Guid.NewGuid(), CityId = Guid.NewGuid(),
@@ -316,7 +316,7 @@ public class BreweryRepositoryTests
BreweryPost result = await repo.UpdateAsync(brewery); BreweryPost result = await repo.UpdateAsync(brewery);
result.BreweryPostId.Should().Be(breweryId); result.BreweryPostId.Should().Be(breweryId);
result.Timer.Should().Equal(newTimer); result.RowVersion.Should().Equal(newRowVersion);
} }
[Fact] [Fact]
@@ -329,14 +329,14 @@ public class BreweryRepositoryTests
.ReturnsScalar((int?)null); .ReturnsScalar((int?)null);
BreweryRepository repo = CreateRepo(conn); 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); Func<Task> act = async () => await repo.UpdateAsync(brewery);
await act.Should().ThrowAsync<NotFoundException>(); await act.Should().ThrowAsync<NotFoundException>();
} }
[Fact] [Fact]
public async Task UpdateAsync_ThrowsConflict_WhenTimerStale() public async Task UpdateAsync_ThrowsConflict_WhenRowVersionStale()
{ {
MockDbConnection conn = new(); MockDbConnection conn = new();
conn.Mocks.When(cmd => conn.Mocks.When(cmd =>
@@ -344,10 +344,10 @@ public class BreweryRepositoryTests
) )
.ReturnsScalar(1); .ReturnsScalar(1);
// Brewery exists but the conditional UPDATE (WHERE ... AND Timer = @Timer) matches no rows, // Brewery exists but the conditional UPDATE (WHERE ... AND RowVersion = @RowVersion) matches no rows,
// since the caller's Timer no longer matches the stored row. // since the caller's RowVersion no longer matches the stored row.
conn.Mocks.When(cmd => conn.Mocks.When(cmd =>
cmd.CommandText.Contains("WHERE BreweryPostID = @BreweryPostId AND Timer = @Timer") cmd.CommandText.Contains("WHERE BreweryPostID = @BreweryPostId AND RowVersion = @RowVersion")
) )
.ReturnsScalar(0); .ReturnsScalar(0);
@@ -357,7 +357,7 @@ public class BreweryRepositoryTests
BreweryPostId = Guid.NewGuid(), BreweryPostId = Guid.NewGuid(),
BreweryName = "Renamed Brewery", BreweryName = "Renamed Brewery",
Description = "Updated description", Description = "Updated description",
Timer = [0x00, 0x01], RowVersion = [0x00, 0x01],
}; };
Func<Task> act = async () => await repo.UpdateAsync(brewery); Func<Task> act = async () => await repo.UpdateAsync(brewery);

View File

@@ -19,13 +19,13 @@ public record UpdateBreweryLocation(
/// <summary> /// <summary>
/// Updates an existing brewery post. Bound directly from the request body of <c>PUT /api/brewery/{id}</c>. /// 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 /// 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. /// <c>409 Conflict</c> if the brewery was modified since then.
/// </summary> /// </summary>
public record UpdateBreweryCommand( public record UpdateBreweryCommand(
Guid BreweryPostId, Guid BreweryPostId,
byte[] Timer, byte[] RowVersion,
Guid PostedById, Guid PostedById,
string BreweryName, string BreweryName,
string Description, string Description,

View File

@@ -19,7 +19,7 @@ public class UpdateBreweryHandler(IBreweryRepository repository)
/// </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 when the brewery was modified by another request since <paramref name="request" />.
/// <c>Timer</c> was read. /// <c>RowVersion</c> was read.
/// </exception> /// </exception>
public async Task<BreweryDto> Handle( public async Task<BreweryDto> Handle(
UpdateBreweryCommand request, UpdateBreweryCommand request,
@@ -32,7 +32,7 @@ public class UpdateBreweryHandler(IBreweryRepository repository)
PostedById = request.PostedById, PostedById = request.PostedById,
BreweryName = request.BreweryName, BreweryName = request.BreweryName,
Description = request.Description, Description = request.Description,
Timer = request.Timer, RowVersion = request.RowVersion,
UpdatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow,
Location = request.Location is null Location = request.Location is null
? null ? null

View File

@@ -7,8 +7,8 @@ public class UpdateBreweryValidator : AbstractValidator<UpdateBreweryCommand>
{ {
public UpdateBreweryValidator() public UpdateBreweryValidator()
{ {
RuleFor(x => x.Timer) RuleFor(x => x.RowVersion)
.NotEmpty() .NotEmpty()
.WithMessage("Timer is required to detect conflicting concurrent updates."); .WithMessage("RowVersion is required to detect conflicting concurrent updates.");
} }
} }

View File

@@ -87,7 +87,7 @@ public class BreweryController(IMediator mediator) : ControllerBase
/// <returns> /// <returns>
/// <c>200 OK</c> with the updated brewery; <c>400 Bad Request</c> if the route ID does not match the /// <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 /// 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> /// </returns>
[HttpPut("{id:guid}")] [HttpPut("{id:guid}")]
public async Task<ActionResult<ResponseBody<BreweryDto>>> Update( public async Task<ActionResult<ResponseBody<BreweryDto>>> Update(

View File

@@ -1,51 +1,35 @@
namespace Features.Breweries.Dtos; namespace Features.Breweries.Dtos;
/// <summary>Location details of a brewery, as returned by the API.</summary> /// <summary>Location details of a brewery, as returned by the API.</summary>
public class BreweryLocationDto /// <param name="Coordinates">The brewery's geographic coordinates, in a raw binary representation.</param>
{ public record BreweryLocationDto(
public Guid BreweryPostLocationId { get; set; } Guid BreweryPostLocationId,
public Guid CityId { get; set; } Guid CityId,
public string CityName { get; set; } = string.Empty; 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 record BeerPreviewDto(Guid BeerPostId, string Name, decimal ABV, int IBU);
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; }
}
/// <summary>A brewery post, as returned by the API.</summary> /// <summary>A brewery post, as returned by the API.</summary>
public class BreweryDto /// <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>
public Guid BreweryPostId { get; set; } /// <param name="Location">The brewery's location, or <see langword="null"/> if none has been set.</param>
public record BreweryDto(
public Guid PostedById { get; set; } Guid BreweryPostId,
Guid PostedById,
public string BreweryName { get; set; } = string.Empty; string BreweryName,
string Description,
public string Description { get; set; } = string.Empty; DateTime CreatedAt,
DateTime? UpdatedAt,
public DateTime CreatedAt { get; set; } byte[]? RowVersion,
BreweryLocationDto? Location,
/// <summary>Gets or sets the date and time of the last edit, or <see langword="null"/> if never edited.</summary> List<BeerPreviewDto> BeerPosts
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; }
}

View File

@@ -11,31 +11,42 @@ public static class BreweryDtoMapper
/// </summary> /// </summary>
public static BreweryDto ToDto(this BreweryPost brewery) public static BreweryDto ToDto(this BreweryPost brewery)
{ {
return new BreweryDto return new BreweryDto(
{ BreweryPostId: brewery.BreweryPostId,
BreweryPostId = brewery.BreweryPostId, PostedById: brewery.PostedById,
PostedById = brewery.PostedById, BreweryName: brewery.BreweryName,
BreweryName = brewery.BreweryName, Description: brewery.Description,
Description = brewery.Description, CreatedAt: brewery.CreatedAt,
CreatedAt = brewery.CreatedAt, UpdatedAt: brewery.UpdatedAt,
UpdatedAt = brewery.UpdatedAt, RowVersion: brewery.RowVersion,
Timer = brewery.Timer, Location: brewery.Location is null
Location = brewery.Location is null
? null ? null
: new BreweryLocationDto : new BreweryLocationDto(
{ BreweryPostLocationId: brewery.Location.BreweryPostLocationId,
BreweryPostLocationId = brewery.Location.BreweryPostLocationId, CityId: brewery.Location.CityId,
CityId = brewery.Location.CityId, CityName: brewery.Location.City?.CityName ?? string.Empty,
CityName = brewery.Location.City?.CityName ?? string.Empty, StateProvinceName: brewery.Location.City?.StateProvince?.StateProvinceName
StateProvinceName = brewery.Location.City?.StateProvince?.StateProvinceName ?? string.Empty, ?? string.Empty,
StateProvinceCode = brewery.Location.City?.StateProvince?.ISO3166_2 ?? string.Empty, StateProvinceCode: brewery.Location.City?.StateProvince?.ISO3166_2
CountryName = brewery.Location.City?.StateProvince?.Country?.CountryName ?? string.Empty, ?? string.Empty,
CountryCode = brewery.Location.City?.StateProvince?.Country?.ISO3166_1 ?? string.Empty, CountryName: brewery.Location.City?.StateProvince?.Country?.CountryName
AddressLine1 = brewery.Location.AddressLine1, ?? string.Empty,
AddressLine2 = brewery.Location.AddressLine2, CountryCode: brewery.Location.City?.StateProvince?.Country?.ISO3166_1
PostalCode = brewery.Location.PostalCode, ?? string.Empty,
Coordinates = brewery.Location.Coordinates, 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
)),
]
);
} }
} }

View File

@@ -22,15 +22,14 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory)
: Repository<BreweryPost>(connectionFactory), : Repository<BreweryPost>(connectionFactory),
IBreweryRepository IBreweryRepository
{ {
private const string SelectColumns = private const string SelectColumns = """
"""
bp.BreweryPostID, bp.BreweryPostID,
bp.PostedByID, bp.PostedByID,
bp.BreweryName, bp.BreweryName,
bp.Description, bp.Description,
bp.CreatedAt, bp.CreatedAt,
bp.UpdatedAt, bp.UpdatedAt,
bp.Timer, bp.RowVersion,
bpl.BreweryPostLocationID, bpl.BreweryPostLocationID,
bpl.CityID, bpl.CityID,
bpl.AddressLine1, bpl.AddressLine1,
@@ -52,8 +51,7 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory)
/// <c>Country</c> chain. All joins are <c>LEFT JOIN</c> (matching the previous stored procedure's /// <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. /// behavior), since a brewery post may have no location.
/// </summary> /// </summary>
private const string FromJoins = private const string FromJoins = """
"""
dbo.BreweryPost bp dbo.BreweryPost bp
LEFT JOIN dbo.BreweryPostLocation bpl ON bp.BreweryPostID = bpl.BreweryPostID LEFT JOIN dbo.BreweryPostLocation bpl ON bp.BreweryPostID = bpl.BreweryPostID
LEFT JOIN dbo.City c ON bpl.CityID = c.CityID LEFT JOIN dbo.City c ON bpl.CityID = c.CityID
@@ -66,8 +64,7 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory)
{ {
await using DbConnection connection = await CreateConnection(); await using DbConnection connection = await CreateConnection();
await using DbCommand command = connection.CreateCommand(); await using DbCommand command = connection.CreateCommand();
command.CommandText = command.CommandText = $"""
$"""
SELECT {SelectColumns} SELECT {SelectColumns}
FROM {FromJoins} FROM {FromJoins}
WHERE bp.BreweryPostID = @BreweryPostId WHERE bp.BreweryPostID = @BreweryPostId
@@ -79,12 +76,12 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory)
} }
/// <inheritdoc/> /// <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 DbConnection connection = await CreateConnection();
await using DbCommand command = connection.CreateCommand(); await using DbCommand command = connection.CreateCommand();
command.CommandText = command.CommandText = $"""
$"""
SELECT {SelectColumns} SELECT {SelectColumns}
FROM {FromJoins} FROM {FromJoins}
ORDER BY bp.CreatedAt DESC ORDER BY bp.CreatedAt DESC
@@ -106,18 +103,25 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory)
public async Task<BreweryPost> UpdateAsync(BreweryPost brewery) public async Task<BreweryPost> UpdateAsync(BreweryPost brewery)
{ {
await using DbConnection connection = await CreateConnection(); await using DbConnection connection = await CreateConnection();
await using DbTransaction transaction = await connection.BeginTransactionAsync(); await using DbTransaction transaction =
await connection.BeginTransactionAsync();
try try
{ {
bool breweryExists = bool breweryExists =
await connection.ExecuteScalarAsync<int?>( await connection.ExecuteScalarAsync<int?>(
new CommandDefinition( new CommandDefinition(
"SELECT 1 FROM dbo.BreweryPost WHERE BreweryPostID = @BreweryPostId", """
SELECT 1 FROM
dbo.BreweryPost
WHERE
BreweryPostID = @BreweryPostId
""",
new { brewery.BreweryPostId }, new { brewery.BreweryPostId },
transaction transaction
) )
) is not null; )
is not null;
if (!breweryExists) if (!breweryExists)
throw new NotFoundException("Brewery not found."); throw new NotFoundException("Brewery not found.");
@@ -131,7 +135,8 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory)
new { brewery.Location.CityId }, new { brewery.Location.CityId },
transaction transaction
) )
) is not null; )
is not null;
if (!cityExists) if (!cityExists)
throw new NotFoundException("City not found."); throw new NotFoundException("City not found.");
@@ -141,15 +146,19 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory)
new CommandDefinition( new CommandDefinition(
""" """
UPDATE dbo.BreweryPost UPDATE dbo.BreweryPost
SET BreweryName = @BreweryName, Description = @Description, UpdatedAt = GETDATE() SET BreweryName = @BreweryName,
WHERE BreweryPostID = @BreweryPostId AND Timer = @Timer Description = @Description,
UpdatedAt = GETDATE()
WHERE
BreweryPostID = @BreweryPostId
AND RowVersion = @RowVersion
""", """,
new new
{ {
brewery.BreweryPostId, brewery.BreweryPostId,
brewery.BreweryName, brewery.BreweryName,
brewery.Description, brewery.Description,
brewery.Timer, brewery.RowVersion,
}, },
transaction transaction
) )
@@ -165,7 +174,11 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory)
// No location supplied: clear any existing location for this brewery. // No location supplied: clear any existing location for this brewery.
await connection.ExecuteAsync( await connection.ExecuteAsync(
new CommandDefinition( new CommandDefinition(
"DELETE FROM dbo.BreweryPostLocation WHERE BreweryPostID = @BreweryPostId", """
DELETE FROM
dbo.BreweryPostLocation
WHERE BreweryPostID = @BreweryPostId
""",
new { brewery.BreweryPostId }, new { brewery.BreweryPostId },
transaction transaction
) )
@@ -176,19 +189,27 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory)
bool locationExists = bool locationExists =
await connection.ExecuteScalarAsync<int?>( await connection.ExecuteScalarAsync<int?>(
new CommandDefinition( new CommandDefinition(
"SELECT 1 FROM dbo.BreweryPostLocation WHERE BreweryPostID = @BreweryPostId", """
SELECT 1
FROM dbo.BreweryPostLocation
WHERE BreweryPostID = @BreweryPostId
""",
new { brewery.BreweryPostId }, new { brewery.BreweryPostId },
transaction transaction
) )
) is not null; )
is not null;
if (locationExists) if (locationExists)
await connection.ExecuteAsync( await connection.ExecuteAsync(
new CommandDefinition( new CommandDefinition(
""" """
UPDATE dbo.BreweryPostLocation UPDATE dbo.BreweryPostLocation
SET CityID = @CityId, AddressLine1 = @AddressLine1, AddressLine2 = @AddressLine2, SET CityID = @CityId,
PostalCode = @PostalCode, Coordinates = @Coordinates AddressLine1 = @AddressLine1,
AddressLine2 = @AddressLine2,
PostalCode = @PostalCode,
Coordinates = @Coordinates
WHERE BreweryPostID = @BreweryPostId WHERE BreweryPostID = @BreweryPostId
""", """,
new new
@@ -208,8 +229,21 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory)
new CommandDefinition( new CommandDefinition(
""" """
INSERT INTO dbo.BreweryPostLocation INSERT INTO dbo.BreweryPostLocation
(BreweryPostLocationID, BreweryPostID, CityID, AddressLine1, AddressLine2, PostalCode, Coordinates) (BreweryPostLocationID,
VALUES (@BreweryPostLocationId, @BreweryPostId, @CityId, @AddressLine1, @AddressLine2, @PostalCode, @Coordinates) BreweryPostID,
CityID,
AddressLine1,
AddressLine2,
PostalCode,
Coordinates)
VALUES
(@BreweryPostLocationId,
@BreweryPostId,
@CityId,
@AddressLine1,
@AddressLine2,
@PostalCode,
@Coordinates)
""", """,
new new
{ {
@@ -245,7 +279,12 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory)
{ {
await using DbConnection connection = await CreateConnection(); await using DbConnection connection = await CreateConnection();
int rows = await connection.ExecuteAsync( int rows = await connection.ExecuteAsync(
"DELETE FROM dbo.BreweryPost WHERE BreweryPostID = @BreweryPostId", """
DELETE FROM
dbo.BreweryPost
WHERE
BreweryPostID = @BreweryPostId
""",
new { BreweryPostId = id } new { BreweryPostId = id }
); );
@@ -257,21 +296,30 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory)
public async Task CreateAsync(BreweryPost brewery) public async Task CreateAsync(BreweryPost brewery)
{ {
if (brewery.Location is null) 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 DbConnection connection = await CreateConnection();
await using DbTransaction transaction = await connection.BeginTransactionAsync(); await using DbTransaction transaction =
await connection.BeginTransactionAsync();
try try
{ {
bool userExists = bool userExists =
await connection.ExecuteScalarAsync<int?>( await connection.ExecuteScalarAsync<int?>(
new CommandDefinition( new CommandDefinition(
"SELECT 1 FROM dbo.UserAccount WHERE UserAccountID = @PostedById", """
SELECT 1
FROM
dbo.UserAccount
WHERE
UserAccountID = @PostedById
""",
new { brewery.PostedById }, new { brewery.PostedById },
transaction transaction
) )
) is not null; )
is not null;
if (!userExists) if (!userExists)
throw new NotFoundException("User not found."); throw new NotFoundException("User not found.");
@@ -279,18 +327,30 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory)
bool cityExists = bool cityExists =
await connection.ExecuteScalarAsync<int?>( await connection.ExecuteScalarAsync<int?>(
new CommandDefinition( new CommandDefinition(
"SELECT 1 FROM dbo.City WHERE CityID = @CityId", """
SELECT 1
FROM
dbo.City
WHERE
CityID = @CityId
""",
new { brewery.Location.CityId }, new { brewery.Location.CityId },
transaction transaction
) )
) is not null; )
is not null;
if (!cityExists) if (!cityExists)
throw new NotFoundException("City not found."); throw new NotFoundException("City not found.");
await connection.ExecuteAsync( await connection.ExecuteAsync(
new CommandDefinition( 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 new
{ {
brewery.BreweryPostId, brewery.BreweryPostId,
@@ -306,8 +366,20 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory)
new CommandDefinition( new CommandDefinition(
""" """
INSERT INTO dbo.BreweryPostLocation INSERT INTO dbo.BreweryPostLocation
(BreweryPostLocationID, BreweryPostID, CityID, AddressLine1, AddressLine2, PostalCode, Coordinates) (BreweryPostLocationID,
VALUES (@BreweryPostLocationId, @BreweryPostId, @CityId, @AddressLine1, @AddressLine2, @PostalCode, @Coordinates) BreweryPostID,
CityID,
AddressLine1,
AddressLine2,
PostalCode,
Coordinates)
VALUES (@BreweryPostLocationId,
@BreweryPostId,
@CityId,
@AddressLine1,
@AddressLine2,
@PostalCode,
@Coordinates)
""", """,
new new
{ {
@@ -344,7 +416,7 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory)
int ordDescription = reader.GetOrdinal("Description"); int ordDescription = reader.GetOrdinal("Description");
int ordCreatedAt = reader.GetOrdinal("CreatedAt"); int ordCreatedAt = reader.GetOrdinal("CreatedAt");
int ordUpdatedAt = reader.GetOrdinal("UpdatedAt"); int ordUpdatedAt = reader.GetOrdinal("UpdatedAt");
int ordTimer = reader.GetOrdinal("Timer"); int ordRowVersion = reader.GetOrdinal("RowVersion");
BreweryPost brewery = new() BreweryPost brewery = new()
{ {
@@ -353,8 +425,12 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory)
BreweryName = reader.GetString(ordBreweryName), BreweryName = reader.GetString(ordBreweryName),
Description = reader.GetString(ordDescription), Description = reader.GetString(ordDescription),
CreatedAt = reader.GetDateTime(ordCreatedAt), CreatedAt = reader.GetDateTime(ordCreatedAt),
UpdatedAt = reader.IsDBNull(ordUpdatedAt) ? null : reader.GetDateTime(ordUpdatedAt), UpdatedAt = reader.IsDBNull(ordUpdatedAt)
Timer = reader.IsDBNull(ordTimer) ? null : reader.GetFieldValue<byte[]>(ordTimer), ? null
: reader.GetDateTime(ordUpdatedAt),
RowVersion = reader.IsDBNull(ordRowVersion)
? null
: reader.GetFieldValue<byte[]>(ordRowVersion),
}; };
int ordLocationId = reader.GetOrdinal("BreweryPostLocationId"); int ordLocationId = reader.GetOrdinal("BreweryPostLocationId");
@@ -369,7 +445,8 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory)
BreweryPostId = brewery.BreweryPostId, BreweryPostId = brewery.BreweryPostId,
CityId = reader.GetGuid(reader.GetOrdinal("CityId")), CityId = reader.GetGuid(reader.GetOrdinal("CityId")),
City = MapCity(reader), City = MapCity(reader),
AddressLine1 = reader.GetString(reader.GetOrdinal("AddressLine1")), AddressLine1 =
reader.GetString(reader.GetOrdinal("AddressLine1")),
AddressLine2 = reader.IsDBNull(ordAddressLine2) AddressLine2 = reader.IsDBNull(ordAddressLine2)
? null ? null
: reader.GetString(ordAddressLine2), : reader.GetString(ordAddressLine2),
@@ -394,24 +471,30 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory)
{ {
CityId = reader.GetGuid(reader.GetOrdinal("CityId")), CityId = reader.GetGuid(reader.GetOrdinal("CityId")),
CityName = reader.GetString(reader.GetOrdinal("CityName")), CityName = reader.GetString(reader.GetOrdinal("CityName")),
StateProvinceId = reader.GetGuid(reader.GetOrdinal("StateProvinceId")), StateProvinceId =
reader.GetGuid(reader.GetOrdinal("StateProvinceId")),
StateProvince = new StateProvince StateProvince = new StateProvince
{ {
StateProvinceId = reader.GetGuid(reader.GetOrdinal("StateProvinceId")), StateProvinceId =
StateProvinceName = reader.GetString(reader.GetOrdinal("StateProvinceName")), reader.GetGuid(reader.GetOrdinal("StateProvinceId")),
StateProvinceName =
reader.GetString(reader.GetOrdinal("StateProvinceName")),
ISO3166_2 = reader.GetString(reader.GetOrdinal("ISO3166_2")), ISO3166_2 = reader.GetString(reader.GetOrdinal("ISO3166_2")),
CountryId = reader.GetGuid(reader.GetOrdinal("CountryId")), CountryId = reader.GetGuid(reader.GetOrdinal("CountryId")),
Country = new Country Country = new Country
{ {
CountryId = reader.GetGuid(reader.GetOrdinal("CountryId")), CountryId = reader.GetGuid(reader.GetOrdinal("CountryId")),
CountryName = reader.GetString(reader.GetOrdinal("CountryName")), CountryName =
ISO3166_1 = reader.GetString(reader.GetOrdinal("ISO3166_1")), 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(); DbParameter p = command.CreateParameter();
p.ParameterName = name; p.ParameterName = name;

View File

@@ -23,17 +23,17 @@ public interface IBreweryRepository
/// <summary> /// <summary>
/// Updates a brewery post's name and description, and upserts or clears its location, enforcing /// 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 /// optimistic concurrency via <paramref name="brewery" />'s <c>RowVersion</c>: the update is rejected
/// if the row was modified since <c>Timer</c> was read. When <paramref name="brewery" />'s /// 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. /// <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> /// </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 when no brewery exists with the given ID, or the given location's <c>CityId</c> does not
/// exist. /// exist.
/// </exception> /// </exception>
/// <exception cref="Domain.Exceptions.ConflictException"> /// <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). /// modified by another request since it was last read).
/// </exception> /// </exception>
Task<BreweryPost> UpdateAsync(BreweryPost brewery); Task<BreweryPost> UpdateAsync(BreweryPost brewery);