Modify brewery location dto to include denormalized data

This commit is contained in:
Aaron Po
2026-08-19 03:30:30 -04:00
parent 693f74bb5b
commit 81a76c9baf
17 changed files with 261 additions and 133 deletions

View File

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

View File

@@ -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,

View File

@@ -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();