mirror of
https://github.com/aaronpo97/the-biergarten-app.git
synced 2026-10-15 10:04:44 +00:00
Modify brewery location dto to include denormalized data
This commit is contained in:
@@ -20,8 +20,11 @@ public class BreweryRepositoryTests
|
||||
MockDbConnection conn = new();
|
||||
|
||||
Guid locationId = Guid.NewGuid();
|
||||
Guid cityId = Guid.NewGuid();
|
||||
Guid stateProvinceId = Guid.NewGuid();
|
||||
Guid countryId = Guid.NewGuid();
|
||||
|
||||
conn.Mocks.When(cmd => cmd.CommandText.Contains("INNER JOIN dbo.BreweryPostLocation"))
|
||||
conn.Mocks.When(cmd => cmd.CommandText.Contains("LEFT JOIN dbo.BreweryPostLocation"))
|
||||
.ReturnsTable(
|
||||
MockTable
|
||||
.WithColumns(
|
||||
@@ -37,7 +40,14 @@ public class BreweryRepositoryTests
|
||||
("AddressLine1", typeof(string)),
|
||||
("AddressLine2", typeof(string)),
|
||||
("PostalCode", typeof(string)),
|
||||
("Coordinates", typeof(byte[]))
|
||||
("Coordinates", typeof(byte[])),
|
||||
("CityName", typeof(string)),
|
||||
("StateProvinceId", typeof(Guid)),
|
||||
("StateProvinceName", typeof(string)),
|
||||
("ISO3166_2", typeof(string)),
|
||||
("CountryId", typeof(Guid)),
|
||||
("CountryName", typeof(string)),
|
||||
("ISO3166_1", typeof(string))
|
||||
)
|
||||
.AddRow(
|
||||
breweryId,
|
||||
@@ -48,11 +58,18 @@ public class BreweryRepositoryTests
|
||||
null,
|
||||
null,
|
||||
locationId,
|
||||
Guid.NewGuid(),
|
||||
cityId,
|
||||
"123 Main St",
|
||||
null,
|
||||
"12345",
|
||||
null
|
||||
null,
|
||||
"Portland",
|
||||
stateProvinceId,
|
||||
"Oregon",
|
||||
"US-OR",
|
||||
countryId,
|
||||
"United States",
|
||||
"US"
|
||||
)
|
||||
);
|
||||
|
||||
@@ -62,13 +79,19 @@ public class BreweryRepositoryTests
|
||||
result!.BreweryPostId.Should().Be(breweryId);
|
||||
result.Location.Should().NotBeNull();
|
||||
result.Location!.BreweryPostLocationId.Should().Be(locationId);
|
||||
result.Location.City.Should().NotBeNull();
|
||||
result.Location.City.CityName.Should().Be("Portland");
|
||||
result.Location.City.StateProvince.Should().NotBeNull();
|
||||
result.Location.City.StateProvince.StateProvinceName.Should().Be("Oregon");
|
||||
result.Location.City.StateProvince.Country.Should().NotBeNull();
|
||||
result.Location.City.StateProvince.Country.CountryName.Should().Be("United States");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetByIdAsync_ReturnsNull_WhenNotExists()
|
||||
{
|
||||
MockDbConnection conn = new();
|
||||
conn.Mocks.When(cmd => cmd.CommandText.Contains("INNER JOIN dbo.BreweryPostLocation"))
|
||||
conn.Mocks.When(cmd => cmd.CommandText.Contains("LEFT JOIN dbo.BreweryPostLocation"))
|
||||
.ReturnsTable(MockTable.Empty());
|
||||
BreweryRepository repo = CreateRepo(conn);
|
||||
BreweryPost? result = await repo.GetByIdAsync(Guid.NewGuid());
|
||||
@@ -225,7 +248,7 @@ public class BreweryRepositoryTests
|
||||
.ReturnsScalar(1);
|
||||
|
||||
// Repository re-fetches the row after a successful update to return the new Timer.
|
||||
conn.Mocks.When(cmd => cmd.CommandText.Contains("INNER JOIN dbo.BreweryPostLocation"))
|
||||
conn.Mocks.When(cmd => cmd.CommandText.Contains("LEFT JOIN dbo.BreweryPostLocation"))
|
||||
.ReturnsTable(
|
||||
MockTable
|
||||
.WithColumns(
|
||||
@@ -241,7 +264,14 @@ public class BreweryRepositoryTests
|
||||
("AddressLine1", typeof(string)),
|
||||
("AddressLine2", typeof(string)),
|
||||
("PostalCode", typeof(string)),
|
||||
("Coordinates", typeof(byte[]))
|
||||
("Coordinates", typeof(byte[])),
|
||||
("CityName", typeof(string)),
|
||||
("StateProvinceId", typeof(Guid)),
|
||||
("StateProvinceName", typeof(string)),
|
||||
("ISO3166_2", typeof(string)),
|
||||
("CountryId", typeof(Guid)),
|
||||
("CountryName", typeof(string)),
|
||||
("ISO3166_1", typeof(string))
|
||||
)
|
||||
.AddRow(
|
||||
breweryId,
|
||||
@@ -256,7 +286,14 @@ public class BreweryRepositoryTests
|
||||
"123 Main St",
|
||||
null,
|
||||
"12345",
|
||||
null
|
||||
null,
|
||||
"Portland",
|
||||
Guid.NewGuid(),
|
||||
"Oregon",
|
||||
"US-OR",
|
||||
Guid.NewGuid(),
|
||||
"United States",
|
||||
"US"
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
@@ -4,10 +4,16 @@ namespace Features.Breweries.Dtos;
|
||||
public class BreweryLocationDto
|
||||
{
|
||||
public Guid BreweryPostLocationId { get; set; }
|
||||
|
||||
public Guid BreweryPostId { get; set; }
|
||||
|
||||
public Guid CityId { get; set; }
|
||||
public string CityName { get; set; } = string.Empty;
|
||||
|
||||
public string StateProvinceName { get; set; } = string.Empty;
|
||||
|
||||
public string StateProvinceCode { get; set; } = string.Empty;
|
||||
|
||||
public string CountryName { get; set; } = string.Empty;
|
||||
|
||||
public string CountryCode { get; set; } = string.Empty;
|
||||
|
||||
public string AddressLine1 { get; set; } = string.Empty;
|
||||
|
||||
@@ -41,3 +47,5 @@ public class BreweryDto
|
||||
/// <summary>Gets or sets the brewery's location, or <see langword="null"/> if none has been set.</summary>
|
||||
public BreweryLocationDto? Location { get; set; }
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -25,8 +25,12 @@ public static class BreweryDtoMapper
|
||||
: new BreweryLocationDto
|
||||
{
|
||||
BreweryPostLocationId = brewery.Location.BreweryPostLocationId,
|
||||
BreweryPostId = brewery.Location.BreweryPostId,
|
||||
CityId = brewery.Location.CityId,
|
||||
CityName = brewery.Location.City?.CityName ?? string.Empty,
|
||||
StateProvinceName = brewery.Location.City?.StateProvince?.StateProvinceName ?? string.Empty,
|
||||
StateProvinceCode = brewery.Location.City?.StateProvince?.ISO3166_2 ?? string.Empty,
|
||||
CountryName = brewery.Location.City?.StateProvince?.Country?.CountryName ?? string.Empty,
|
||||
CountryCode = brewery.Location.City?.StateProvince?.Country?.ISO3166_1 ?? string.Empty,
|
||||
AddressLine1 = brewery.Location.AddressLine1,
|
||||
AddressLine2 = brewery.Location.AddressLine2,
|
||||
PostalCode = brewery.Location.PostalCode,
|
||||
|
||||
@@ -23,14 +23,45 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory)
|
||||
IBreweryRepository
|
||||
{
|
||||
private const string SelectColumns =
|
||||
"bp.BreweryPostID, bp.PostedByID, bp.BreweryName, bp.Description, bp.CreatedAt, bp.UpdatedAt, bp.Timer, "
|
||||
+ "bpl.BreweryPostLocationID, bpl.CityID, bpl.AddressLine1, bpl.AddressLine2, bpl.PostalCode, bpl.Coordinates";
|
||||
"""
|
||||
bp.BreweryPostID,
|
||||
bp.PostedByID,
|
||||
bp.BreweryName,
|
||||
bp.Description,
|
||||
bp.CreatedAt,
|
||||
bp.UpdatedAt,
|
||||
bp.Timer,
|
||||
bpl.BreweryPostLocationID,
|
||||
bpl.CityID,
|
||||
bpl.AddressLine1,
|
||||
bpl.AddressLine2,
|
||||
bpl.PostalCode,
|
||||
bpl.Coordinates,
|
||||
c.CityName,
|
||||
c.StateProvinceID,
|
||||
sp.StateProvinceName,
|
||||
sp.ISO3166_2,
|
||||
sp.CountryID,
|
||||
co.CountryName,
|
||||
co.ISO3166_1
|
||||
""";
|
||||
|
||||
/// <summary>
|
||||
/// Joins from <c>dbo.BreweryPost</c> down to <c>dbo.Country</c> so a row carries everything needed
|
||||
/// to populate <see cref="BreweryPost.Location" />'s full <c>City</c> → <c>StateProvince</c> →
|
||||
/// <c>Country</c> chain. All joins are <c>LEFT JOIN</c> (matching the previous stored procedure's
|
||||
/// behavior), since a brewery post may have no location.
|
||||
/// </summary>
|
||||
private const string FromJoins =
|
||||
"""
|
||||
dbo.BreweryPost bp
|
||||
LEFT JOIN dbo.BreweryPostLocation bpl ON bp.BreweryPostID = bpl.BreweryPostID
|
||||
LEFT JOIN dbo.City c ON bpl.CityID = c.CityID
|
||||
LEFT JOIN dbo.StateProvince sp ON c.StateProvinceID = sp.StateProvinceID
|
||||
LEFT JOIN dbo.Country co ON sp.CountryID = co.CountryID
|
||||
""";
|
||||
|
||||
/// <inheritdoc/>
|
||||
/// <remarks>
|
||||
/// Joins to the location table with an <c>INNER JOIN</c> (matching the previous stored procedure's
|
||||
/// behavior), which is why a brewery with no location row is treated as not found.
|
||||
/// </remarks>
|
||||
public async Task<BreweryPost?> GetByIdAsync(Guid id)
|
||||
{
|
||||
await using DbConnection connection = await CreateConnection();
|
||||
@@ -38,8 +69,7 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory)
|
||||
command.CommandText =
|
||||
$"""
|
||||
SELECT {SelectColumns}
|
||||
FROM dbo.BreweryPost bp
|
||||
INNER JOIN dbo.BreweryPostLocation bpl ON bp.BreweryPostID = bpl.BreweryPostID
|
||||
FROM {FromJoins}
|
||||
WHERE bp.BreweryPostID = @BreweryPostId
|
||||
""";
|
||||
AddParameter(command, "@BreweryPostId", id);
|
||||
@@ -56,8 +86,7 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory)
|
||||
command.CommandText =
|
||||
$"""
|
||||
SELECT {SelectColumns}
|
||||
FROM dbo.BreweryPost bp
|
||||
LEFT JOIN dbo.BreweryPostLocation bpl ON bp.BreweryPostID = bpl.BreweryPostID
|
||||
FROM {FromJoins}
|
||||
ORDER BY bp.CreatedAt DESC
|
||||
OFFSET @Offset ROWS FETCH NEXT @Limit ROWS ONLY
|
||||
""";
|
||||
@@ -339,6 +368,7 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory)
|
||||
BreweryPostLocationId = reader.GetGuid(ordLocationId),
|
||||
BreweryPostId = brewery.BreweryPostId,
|
||||
CityId = reader.GetGuid(reader.GetOrdinal("CityId")),
|
||||
City = MapCity(reader),
|
||||
AddressLine1 = reader.GetString(reader.GetOrdinal("AddressLine1")),
|
||||
AddressLine2 = reader.IsDBNull(ordAddressLine2)
|
||||
? null
|
||||
@@ -353,6 +383,34 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory)
|
||||
return brewery;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maps the current row's <c>City</c> → <c>StateProvince</c> → <c>Country</c> columns. The schema
|
||||
/// enforces <c>NOT NULL</c> foreign keys down this whole chain, so whenever a location row was
|
||||
/// joined in (the caller only calls this when it was), these columns are always populated too.
|
||||
/// </summary>
|
||||
private static City MapCity(DbDataReader reader)
|
||||
{
|
||||
return new City
|
||||
{
|
||||
CityId = reader.GetGuid(reader.GetOrdinal("CityId")),
|
||||
CityName = reader.GetString(reader.GetOrdinal("CityName")),
|
||||
StateProvinceId = reader.GetGuid(reader.GetOrdinal("StateProvinceId")),
|
||||
StateProvince = new StateProvince
|
||||
{
|
||||
StateProvinceId = reader.GetGuid(reader.GetOrdinal("StateProvinceId")),
|
||||
StateProvinceName = reader.GetString(reader.GetOrdinal("StateProvinceName")),
|
||||
ISO3166_2 = reader.GetString(reader.GetOrdinal("ISO3166_2")),
|
||||
CountryId = reader.GetGuid(reader.GetOrdinal("CountryId")),
|
||||
Country = new Country
|
||||
{
|
||||
CountryId = reader.GetGuid(reader.GetOrdinal("CountryId")),
|
||||
CountryName = reader.GetString(reader.GetOrdinal("CountryName")),
|
||||
ISO3166_1 = reader.GetString(reader.GetOrdinal("ISO3166_1")),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private static void AddParameter(DbCommand command, string name, object? value)
|
||||
{
|
||||
DbParameter p = command.CreateParameter();
|
||||
|
||||
Reference in New Issue
Block a user