Update database handling code (#247)

This commit is contained in:
2026-08-25 13:42:51 -04:00
committed by GitHub
parent 03ca966708
commit 8a047e8d9f
37 changed files with 386 additions and 405 deletions

View File

@@ -0,0 +1,10 @@
using Microsoft.Extensions.Configuration;
namespace Database.Connection;
public static class ConfigurationHelpers
{
public static string GetKeyOrThrow(IConfiguration configuration, string key) =>
configuration[key]
?? throw new InvalidOperationException($"The {key} environment variable is not set.");
}

View File

@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>Database.Connection</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Dapper" Version="2.1.79" />
<PackageReference Include="Microsoft.Data.SqlClient" Version="7.0.2" />
<PackageReference Include="Microsoft.SqlServer.Types" Version="160.1000.6" />
<PackageReference
Include="Microsoft.Extensions.Configuration.Abstractions"
Version="9.0.0"
/>
<PackageReference
Include="Microsoft.Extensions.DependencyInjection.Abstractions"
Version="10.0.0"
/>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,46 @@
using System.Data.Common;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Configuration;
namespace Database.Connection;
/// <summary>
/// Default <see cref="DefaultSqlConnectionFactory" /> implementation that creates SQL Server connections,
/// resolving the connection string from environment variables or application configuration.
/// </summary>
public class DefaultSqlConnectionFactory(IConfiguration configuration) : ISqlConnectionFactory
{
/// <summary>
/// The resolved SQL Server connection string, preferring (in order): the <c>DB_CONNECTION_STRING</c>
/// configuration value, and a connection string built from individual <c>DB_*</c> configuration values.
/// </summary>
/// <exception cref="InvalidOperationException">Thrown when no connection string can be resolved from any source.</exception>
public string ConnectionString
{
get
{
string? fullConnectionString = configuration["DB_CONNECTION_STRING"];
if (!string.IsNullOrEmpty(fullConnectionString))
return fullConnectionString;
// Try to build from individual configuration values (preferred method for Docker)
if (
SqlConnectionStringHelper.TryGetStandardDbConnectionString(
configuration,
out var connectionString
)
)
return connectionString;
throw new InvalidOperationException(
"Database connection string not configured. Set DB_CONNECTION_STRING or DB_SERVER, DB_NAME, DB_USER, DB_PASSWORD env vars."
);
}
}
/// <inheritdoc />
public SqlConnection CreateConnection()
{
return new SqlConnection(ConnectionString);
}
}

View File

@@ -0,0 +1,15 @@
using Microsoft.Extensions.DependencyInjection;
namespace Database.Connection.DependencyInjection;
/// <summary>Extension methods for registering database connection services with dependency injection.</summary>
public static class DatabaseConnectionServiceCollectionExtensions
{
/// <summary>Registers <see cref="DefaultSqlConnectionFactory" /> as a singleton <see cref="ISqlConnectionFactory" />.</summary>
/// <returns>The same <paramref name="services" /> instance, for chaining.</returns>
public static IServiceCollection AddDatabaseConnection(this IServiceCollection services)
{
services.AddSingleton<ISqlConnectionFactory, DefaultSqlConnectionFactory>();
return services;
}
}

View File

@@ -0,0 +1,10 @@
using Microsoft.Data.SqlClient;
namespace Database.Connection;
public interface ISqlConnectionFactory
{
public string ConnectionString { get; }
public SqlConnection CreateConnection();
}

View File

@@ -0,0 +1,21 @@
using System.Data.Common;
namespace Database.Connection;
/// <summary>
/// Base class for Dapper-based repositories, providing shared connection creation for derived
/// repository implementations.
/// </summary>
public abstract class DapperRepository(ISqlConnectionFactory connectionFactory)
{
/// <summary>
/// Creates and opens a new database connection using the configured <see cref="ISqlConnectionFactory" />.
/// </summary>
/// <exception cref="DbException">Thrown when the connection cannot be opened.</exception>
protected async Task<DbConnection> CreateConnection()
{
DbConnection connection = connectionFactory.CreateConnection();
await connection.OpenAsync();
return connection;
}
}

View File

@@ -0,0 +1,102 @@
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Configuration;
namespace Database.Connection;
/// <summary>
/// Helper for building SQL Server connection strings from configuration
/// (backed by the <c>DB_*</c> environment variables).
/// </summary>
public static class SqlConnectionStringHelper
{
private static SqlConnectionStringBuilder MakeConfiguredConnectionBuilder(
IConfiguration configuration
) =>
new()
{
DataSource = ConfigurationHelpers.GetKeyOrThrow(configuration, "DB_SERVER"),
UserID = ConfigurationHelpers.GetKeyOrThrow(configuration, "DB_USER"),
Password = ConfigurationHelpers.GetKeyOrThrow(configuration, "DB_PASSWORD"),
TrustServerCertificate = ResolveTrustServerCertificate(configuration),
Encrypt = true,
};
/// <summary>
/// Builds a SQL Server connection string from configuration.
/// </summary>
/// <remarks>
/// Returns <c>false</c> if DB_SERVER, DB_NAME, DB_USER, or DB_PASSWORD is unset.
/// </remarks>
/// <param name="configuration">The configuration to read the <c>DB_*</c> values from.</param>
/// <param name="connectionString">The resolved connection string, if successful.</param>
public static bool TryGetStandardDbConnectionString(
IConfiguration configuration,
out string connectionString
)
{
connectionString = string.Empty;
try
{
SqlConnectionStringBuilder builder = MakeConfiguredConnectionBuilder(configuration);
builder.InitialCatalog = ConfigurationHelpers.GetKeyOrThrow(configuration, "DB_NAME");
connectionString = builder.ConnectionString;
return true;
}
catch
{
return false;
}
}
public static bool TryGetMasterConnectionString(
IConfiguration configuration,
out string connectionString
)
{
connectionString = string.Empty;
try
{
var builder = MakeConfiguredConnectionBuilder(configuration);
builder.InitialCatalog = "master";
connectionString = builder.ConnectionString;
return true;
}
catch
{
return false;
}
}
public static bool TryGetDatabaseNameFromConnectionString(
string connectionString,
out string databaseName
)
{
databaseName = string.Empty;
try
{
var builder = new SqlConnectionStringBuilder(connectionString);
databaseName = builder.InitialCatalog;
return true;
}
catch
{
return false;
}
}
/// <summary>
/// Resolves the <c>DB_TRUST_SERVER_CERTIFICATE</c> configuration value, defaulting to
/// <c>true</c> when it is unset or not a valid boolean.
/// </summary>
private static bool ResolveTrustServerCertificate(IConfiguration configuration)
{
return !bool.TryParse(
configuration["DB_TRUST_SERVER_CERTIFICATE"],
out bool trustServerCertificate
) || trustServerCertificate;
}
}