using System.Data;
using System.Data.Common;
using Domain.Entities;
using Infrastructure.Sql;
namespace Features.Breweries.Repository;
///
/// ADO.NET-based implementation of backed by SQL Server stored
/// procedures.
///
/// The factory used to create database connections.
public class BreweryRepository(ISqlConnectionFactory connectionFactory)
: Repository(connectionFactory), IBreweryRepository
{
///
/// Retrieves a brewery post by ID using the USP_GetBreweryById stored procedure.
///
/// The unique identifier of the brewery post.
/// The matching , or null if not found.
/// Thrown when the database command fails.
public async Task GetByIdAsync(Guid id)
{
await using DbConnection connection = await CreateConnection();
await using DbCommand command = connection.CreateCommand();
command.CommandType = CommandType.StoredProcedure;
command.CommandText = "USP_GetBreweryById";
AddParameter(command, "@BreweryPostID", id);
await using DbDataReader reader = await command.ExecuteReaderAsync();
if (await reader.ReadAsync()) return MapToEntity(reader);
return null;
}
///
/// Retrieves all brewery posts, optionally paginated, using the USP_GetAllBreweries
/// stored procedure. The @Limit and @Offset parameters are only added when their
/// corresponding argument has a value.
///
/// The maximum number of records to return, or null for no limit.
/// The number of records to skip, or null for no offset.
/// The collection of matching records.
/// Thrown when the database command fails.
public async Task> GetAllAsync(int? limit, int? offset)
{
await using DbConnection connection = await CreateConnection();
await using DbCommand command = connection.CreateCommand();
command.CommandText = "USP_GetAllBreweries";
command.CommandType = CommandType.StoredProcedure;
if (limit.HasValue)
AddParameter(command, "@Limit", limit.Value);
if (offset.HasValue)
AddParameter(command, "@Offset", offset.Value);
await using DbDataReader reader = await command.ExecuteReaderAsync();
List breweries = new();
while (await reader.ReadAsync()) breweries.Add(MapToEntity(reader));
return breweries;
}
///
/// Updates a brewery post's name and description, and upserts or clears its location, using the
/// USP_UpdateBrewery stored procedure. When .Location is
/// null, any existing location for the brewery is removed.
///
/// The brewery post containing updated values.
/// Thrown when the database command fails.
public async Task UpdateAsync(BreweryPost brewery)
{
await using DbConnection connection = await CreateConnection();
await using DbCommand command = connection.CreateCommand();
command.CommandText = "USP_UpdateBrewery";
command.CommandType = CommandType.StoredProcedure;
AddParameter(command, "@BreweryPostID", brewery.BreweryPostId);
AddParameter(command, "@BreweryName", brewery.BreweryName);
AddParameter(command, "@Description", brewery.Description);
AddParameter(command, "@BreweryPostLocationID", brewery.Location?.BreweryPostLocationId);
AddParameter(command, "@CityID", brewery.Location?.CityId);
AddParameter(command, "@AddressLine1", brewery.Location?.AddressLine1);
AddParameter(command, "@AddressLine2", brewery.Location?.AddressLine2);
AddParameter(command, "@PostalCode", brewery.Location?.PostalCode);
AddParameter(command, "@Coordinates", brewery.Location?.Coordinates);
await command.ExecuteNonQueryAsync();
}
///
/// Deletes a brewery post by ID using the USP_DeleteBrewery stored procedure. Its location
/// and photos are removed via cascading foreign keys.
///
/// The unique identifier of the brewery post to delete.
/// Thrown when the database command fails.
public async Task DeleteAsync(Guid id)
{
await using DbConnection connection = await CreateConnection();
await using DbCommand command = connection.CreateCommand();
command.CommandText = "USP_DeleteBrewery";
command.CommandType = CommandType.StoredProcedure;
AddParameter(command, "@BreweryPostID", id);
await command.ExecuteNonQueryAsync();
}
///
/// Creates a new brewery post and its location using the USP_CreateBrewery stored procedure.
///
/// The brewery post to create. Must have a non-null Location.
/// Thrown when .Location is null.
/// Thrown when the database command fails.
public async Task CreateAsync(BreweryPost brewery)
{
await using DbConnection connection = await CreateConnection();
await using DbCommand command = connection.CreateCommand();
command.CommandText = "USP_CreateBrewery";
command.CommandType = CommandType.StoredProcedure;
if (brewery.Location is null) throw new ArgumentException("Location must be provided when creating a brewery.");
AddParameter(command, "@BreweryName", brewery.BreweryName);
AddParameter(command, "@Description", brewery.Description);
AddParameter(command, "@PostedByID", brewery.PostedById);
AddParameter(command, "@CityID", brewery.Location?.CityId);
AddParameter(command, "@AddressLine1", brewery.Location?.AddressLine1);
AddParameter(command, "@AddressLine2", brewery.Location?.AddressLine2);
AddParameter(command, "@PostalCode", brewery.Location?.PostalCode);
AddParameter(command, "@Coordinates", brewery.Location?.Coordinates);
await command.ExecuteNonQueryAsync();
}
///
/// Maps the current row of a data reader to a entity, including its
/// rowversion Timer field and, if location columns are present in the result set, its
/// associated .
///
/// The data reader positioned on the row to map.
/// The mapped instance.
protected override BreweryPost MapToEntity(DbDataReader reader)
{
BreweryPost brewery = new();
int ordBreweryPostId = reader.GetOrdinal("BreweryPostId");
int ordPostedById = reader.GetOrdinal("PostedById");
int ordBreweryName = reader.GetOrdinal("BreweryName");
int ordDescription = reader.GetOrdinal("Description");
int ordCreatedAt = reader.GetOrdinal("CreatedAt");
int ordUpdatedAt = reader.GetOrdinal("UpdatedAt");
int ordTimer = reader.GetOrdinal("Timer");
brewery.BreweryPostId = reader.GetGuid(ordBreweryPostId);
brewery.PostedById = reader.GetGuid(ordPostedById);
brewery.BreweryName = reader.GetString(ordBreweryName);
brewery.Description = reader.GetString(ordDescription);
brewery.CreatedAt = reader.GetDateTime(ordCreatedAt);
brewery.UpdatedAt = reader.IsDBNull(ordUpdatedAt) ? null : reader.GetDateTime(ordUpdatedAt);
// Read timer (varbinary/rowversion) robustly
if (reader.IsDBNull(ordTimer))
brewery.Timer = null;
else
try
{
brewery.Timer = reader.GetFieldValue(ordTimer);
}
catch
{
long length = reader.GetBytes(ordTimer, 0, null, 0, 0);
byte[] buffer = new byte[length];
reader.GetBytes(ordTimer, 0, buffer, 0, (int)length);
brewery.Timer = buffer;
}
// Map BreweryPostLocation if columns are present
try
{
int ordLocationId = reader.GetOrdinal("BreweryPostLocationId");
if (!reader.IsDBNull(ordLocationId))
{
BreweryPostLocation location = new()
{
BreweryPostLocationId = reader.GetGuid(ordLocationId),
BreweryPostId = reader.GetGuid(reader.GetOrdinal("BreweryPostId")),
CityId = reader.GetGuid(reader.GetOrdinal("CityId")),
AddressLine1 = reader.GetString(reader.GetOrdinal("AddressLine1")),
AddressLine2 = reader.IsDBNull(reader.GetOrdinal("AddressLine2"))
? null
: reader.GetString(reader.GetOrdinal("AddressLine2")),
PostalCode = reader.GetString(reader.GetOrdinal("PostalCode")),
Coordinates = reader.IsDBNull(reader.GetOrdinal("Coordinates"))
? null
: reader.GetFieldValue(reader.GetOrdinal("Coordinates"))
};
brewery.Location = location;
}
}
catch (IndexOutOfRangeException)
{
// Location columns not present, skip mapping location
}
return brewery;
}
///
/// Helper method to add a parameter to a database command, converting null values to
/// .
///
/// The command to add the parameter to.
/// The parameter name (including any prefix, e.g. "@BreweryName").
/// The parameter value, or null to bind .
private static void AddParameter(
DbCommand command,
string name,
object? value
)
{
DbParameter p = command.CreateParameter();
p.ParameterName = name;
p.Value = value ?? DBNull.Value;
command.Parameters.Add(p);
}
}