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