mirror of
https://github.com/aaronpo97/the-biergarten-app.git
synced 2026-10-15 01:54:43 +00:00
- 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.
This commit is contained in:
@@ -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),
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
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;
|
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; } = [];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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; }
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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; }
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
)),
|
||||||
|
]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,29 +22,28 @@ 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.RowVersion,
|
||||||
bp.Timer,
|
bpl.BreweryPostLocationID,
|
||||||
bpl.BreweryPostLocationID,
|
bpl.CityID,
|
||||||
bpl.CityID,
|
bpl.AddressLine1,
|
||||||
bpl.AddressLine1,
|
bpl.AddressLine2,
|
||||||
bpl.AddressLine2,
|
bpl.PostalCode,
|
||||||
bpl.PostalCode,
|
bpl.Coordinates,
|
||||||
bpl.Coordinates,
|
c.CityName,
|
||||||
c.CityName,
|
c.StateProvinceID,
|
||||||
c.StateProvinceID,
|
sp.StateProvinceName,
|
||||||
sp.StateProvinceName,
|
sp.ISO3166_2,
|
||||||
sp.ISO3166_2,
|
sp.CountryID,
|
||||||
sp.CountryID,
|
co.CountryName,
|
||||||
co.CountryName,
|
co.ISO3166_1
|
||||||
co.ISO3166_1
|
""";
|
||||||
""";
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Joins from <c>dbo.BreweryPost</c> down to <c>dbo.Country</c> so a row carries everything needed
|
/// Joins from <c>dbo.BreweryPost</c> down to <c>dbo.Country</c> so a row carries everything needed
|
||||||
@@ -52,22 +51,20 @@ 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
|
LEFT JOIN dbo.StateProvince sp ON c.StateProvinceID = sp.StateProvinceID
|
||||||
LEFT JOIN dbo.StateProvince sp ON c.StateProvinceID = sp.StateProvinceID
|
LEFT JOIN dbo.Country co ON sp.CountryID = co.CountryID
|
||||||
LEFT JOIN dbo.Country co ON sp.CountryID = co.CountryID
|
""";
|
||||||
""";
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public async Task<BreweryPost?> GetByIdAsync(Guid id)
|
public async Task<BreweryPost?> GetByIdAsync(Guid id)
|
||||||
{
|
{
|
||||||
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
|
||||||
@@ -83,8 +80,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}
|
||||||
ORDER BY bp.CreatedAt DESC
|
ORDER BY bp.CreatedAt DESC
|
||||||
@@ -117,7 +113,8 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory)
|
|||||||
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 +128,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.");
|
||||||
@@ -142,14 +140,14 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory)
|
|||||||
"""
|
"""
|
||||||
UPDATE dbo.BreweryPost
|
UPDATE dbo.BreweryPost
|
||||||
SET BreweryName = @BreweryName, Description = @Description, UpdatedAt = GETDATE()
|
SET BreweryName = @BreweryName, Description = @Description, UpdatedAt = GETDATE()
|
||||||
WHERE BreweryPostID = @BreweryPostId AND Timer = @Timer
|
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
|
||||||
)
|
)
|
||||||
@@ -180,7 +178,8 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory)
|
|||||||
new { brewery.BreweryPostId },
|
new { brewery.BreweryPostId },
|
||||||
transaction
|
transaction
|
||||||
)
|
)
|
||||||
) is not null;
|
)
|
||||||
|
is not null;
|
||||||
|
|
||||||
if (locationExists)
|
if (locationExists)
|
||||||
await connection.ExecuteAsync(
|
await connection.ExecuteAsync(
|
||||||
@@ -208,8 +207,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
|
||||||
{
|
{
|
||||||
@@ -271,7 +283,8 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory)
|
|||||||
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.");
|
||||||
@@ -283,14 +296,26 @@ 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.");
|
||||||
|
|
||||||
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,
|
||||||
@@ -344,7 +369,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()
|
||||||
{
|
{
|
||||||
@@ -354,7 +379,9 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory)
|
|||||||
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) ? null : reader.GetDateTime(ordUpdatedAt),
|
||||||
Timer = reader.IsDBNull(ordTimer) ? null : reader.GetFieldValue<byte[]>(ordTimer),
|
RowVersion = reader.IsDBNull(ordRowVersion)
|
||||||
|
? null
|
||||||
|
: reader.GetFieldValue<byte[]>(ordRowVersion),
|
||||||
};
|
};
|
||||||
|
|
||||||
int ordLocationId = reader.GetOrdinal("BreweryPostLocationId");
|
int ordLocationId = reader.GetOrdinal("BreweryPostLocationId");
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user