This commit is contained in:
Курнат Андрей
2026-03-19 23:31:41 +03:00
parent ce3a3f02d2
commit a47a7a5a3b
104 changed files with 21982 additions and 0 deletions

View File

@@ -0,0 +1,73 @@
using Microsoft.Extensions.Configuration;
using System;
using System.Threading;
namespace XLAB2.Infrastructure;
internal static class DatabaseConfiguration
{
private static readonly Lazy<DatabaseOptions> Options = new(LoadOptions, LazyThreadSafetyMode.ExecutionAndPublication);
public static DatabaseOptions Current => Options.Value;
private static DatabaseOptions LoadOptions()
{
var configuration = new ConfigurationBuilder()
.SetBasePath(AppContext.BaseDirectory)
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.AddEnvironmentVariables(prefix: "XLAB2_")
.Build();
var options = configuration.GetSection("Database").Get<DatabaseOptions>() ?? new DatabaseOptions();
Validate(options);
return options;
}
private static void Validate(DatabaseOptions options)
{
if (string.IsNullOrWhiteSpace(options.Server))
{
throw new InvalidOperationException("Database:Server is required.");
}
if (string.IsNullOrWhiteSpace(options.Database))
{
throw new InvalidOperationException("Database:Database is required.");
}
if (options.ConnectTimeoutSeconds <= 0)
{
throw new InvalidOperationException("Database:ConnectTimeoutSeconds must be greater than zero.");
}
if (options.CommandTimeoutSeconds <= 0)
{
throw new InvalidOperationException("Database:CommandTimeoutSeconds must be greater than zero.");
}
if (options.MinPoolSize < 0)
{
throw new InvalidOperationException("Database:MinPoolSize cannot be negative.");
}
if (options.MaxPoolSize <= 0)
{
throw new InvalidOperationException("Database:MaxPoolSize must be greater than zero.");
}
if (options.MinPoolSize > options.MaxPoolSize)
{
throw new InvalidOperationException("Database:MinPoolSize cannot be greater than Database:MaxPoolSize.");
}
if (options.ConnectRetryCount < 0)
{
throw new InvalidOperationException("Database:ConnectRetryCount cannot be negative.");
}
if (options.ConnectRetryIntervalSeconds < 1)
{
throw new InvalidOperationException("Database:ConnectRetryIntervalSeconds must be greater than zero.");
}
}
}

View File

@@ -0,0 +1,32 @@
namespace XLAB2.Infrastructure;
internal sealed class DatabaseOptions
{
public string ApplicationName { get; set; } = "XLAB2";
public int CommandTimeoutSeconds { get; set; } = 60;
public int ConnectRetryCount { get; set; } = 3;
public int ConnectRetryIntervalSeconds { get; set; } = 10;
public int ConnectTimeoutSeconds { get; set; } = 15;
public string Database { get; set; } = "ASUMS";
public bool Encrypt { get; set; } = false;
public bool IntegratedSecurity { get; set; } = true;
public bool MultipleActiveResultSets { get; set; } = true;
public bool Pooling { get; set; } = true;
public int MaxPoolSize { get; set; } = 100;
public int MinPoolSize { get; set; } = 0;
public string Server { get; set; } = @"SEVENHILL\SQLEXPRESS";
public bool TrustServerCertificate { get; set; } = true;
}

View File

@@ -0,0 +1,16 @@
using Microsoft.Data.SqlClient;
using System.Threading;
using System.Threading.Tasks;
namespace XLAB2.Infrastructure;
internal interface IDatabaseConnectionFactory
{
string ConnectionString { get; }
DatabaseOptions Options { get; }
SqlConnection CreateConnection();
Task<SqlConnection> OpenConnectionAsync(CancellationToken cancellationToken = default);
}

View File

@@ -0,0 +1,172 @@
using Microsoft.Data.SqlClient;
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace XLAB2.Infrastructure;
internal static class SqlAsync
{
public static async Task<List<T>> QueryAsync<T>(
this SqlConnection connection,
string commandText,
Func<SqlDataReader, T> map,
Action<SqlCommand> configureCommand = null,
CancellationToken cancellationToken = default)
{
if (connection == null)
{
throw new ArgumentNullException(nameof(connection));
}
if (map == null)
{
throw new ArgumentNullException(nameof(map));
}
using var command = connection.CreateCommand();
command.CommandText = commandText;
configureCommand?.Invoke(command);
using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
var items = new List<T>();
while (await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
{
items.Add(map(reader));
}
return items;
}
public static async Task<List<T>> QueryAsync<T>(
this SqlConnection connection,
SqlTransaction transaction,
string commandText,
Func<SqlDataReader, T> map,
Action<SqlCommand> configureCommand = null,
CancellationToken cancellationToken = default)
{
if (connection == null)
{
throw new ArgumentNullException(nameof(connection));
}
if (map == null)
{
throw new ArgumentNullException(nameof(map));
}
using var command = new SqlCommand(commandText, connection, transaction);
configureCommand?.Invoke(command);
using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
var items = new List<T>();
while (await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
{
items.Add(map(reader));
}
return items;
}
public static async Task<int> ExecuteNonQueryAsync(
this SqlConnection connection,
string commandText,
Action<SqlCommand> configureCommand = null,
CancellationToken cancellationToken = default)
{
if (connection == null)
{
throw new ArgumentNullException(nameof(connection));
}
using var command = connection.CreateCommand();
command.CommandText = commandText;
configureCommand?.Invoke(command);
return await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
}
public static async Task<int> ExecuteNonQueryAsync(
this SqlConnection connection,
SqlTransaction transaction,
string commandText,
Action<SqlCommand> configureCommand = null,
CancellationToken cancellationToken = default)
{
if (connection == null)
{
throw new ArgumentNullException(nameof(connection));
}
using var command = new SqlCommand(commandText, connection, transaction);
configureCommand?.Invoke(command);
return await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
}
public static async Task<T> ExecuteScalarAsync<T>(
this SqlConnection connection,
string commandText,
Action<SqlCommand> configureCommand = null,
CancellationToken cancellationToken = default)
{
if (connection == null)
{
throw new ArgumentNullException(nameof(connection));
}
using var command = connection.CreateCommand();
command.CommandText = commandText;
configureCommand?.Invoke(command);
var result = await command.ExecuteScalarAsync(cancellationToken).ConfigureAwait(false);
if (result == null || result is DBNull)
{
return default!;
}
return (T)Convert.ChangeType(result, typeof(T));
}
public static async Task<T> ExecuteScalarAsync<T>(
this SqlConnection connection,
SqlTransaction transaction,
string commandText,
Action<SqlCommand> configureCommand = null,
CancellationToken cancellationToken = default)
{
if (connection == null)
{
throw new ArgumentNullException(nameof(connection));
}
using var command = new SqlCommand(commandText, connection, transaction);
configureCommand?.Invoke(command);
var result = await command.ExecuteScalarAsync(cancellationToken).ConfigureAwait(false);
if (result == null || result is DBNull)
{
return default!;
}
return (T)Convert.ChangeType(result, typeof(T));
}
public static async Task<List<T>> QueryOpenConnectionAsync<T>(
this IDatabaseConnectionFactory connectionFactory,
string commandText,
Func<SqlDataReader, T> map,
Action<SqlCommand> configureCommand = null,
CancellationToken cancellationToken = default)
{
if (connectionFactory == null)
{
throw new ArgumentNullException(nameof(connectionFactory));
}
await using var connection = await connectionFactory.OpenConnectionAsync(cancellationToken).ConfigureAwait(false);
return await connection.QueryAsync(commandText, map, configureCommand, cancellationToken).ConfigureAwait(false);
}
}

View File

@@ -0,0 +1,68 @@
using Microsoft.Data.SqlClient;
using System;
using System.Threading;
using System.Threading.Tasks;
namespace XLAB2.Infrastructure;
internal sealed class SqlServerConnectionFactory : IDatabaseConnectionFactory
{
private static readonly Lazy<IDatabaseConnectionFactory> CurrentFactory = new(() => new SqlServerConnectionFactory(DatabaseConfiguration.Current), LazyThreadSafetyMode.ExecutionAndPublication);
private readonly DatabaseOptions _options;
public SqlServerConnectionFactory(DatabaseOptions options)
{
_options = options ?? throw new ArgumentNullException(nameof(options));
ConnectionString = BuildConnectionString(options);
}
public static IDatabaseConnectionFactory Current => CurrentFactory.Value;
public string ConnectionString { get; }
public DatabaseOptions Options => _options;
public SqlConnection CreateConnection()
{
return new SqlConnection(ConnectionString);
}
public async Task<SqlConnection> OpenConnectionAsync(CancellationToken cancellationToken = default)
{
var connection = CreateConnection();
try
{
await connection.OpenAsync(cancellationToken).ConfigureAwait(false);
return connection;
}
catch
{
await connection.DisposeAsync().ConfigureAwait(false);
throw;
}
}
internal static string BuildConnectionString(DatabaseOptions options)
{
var builder = new SqlConnectionStringBuilder
{
ApplicationName = string.IsNullOrWhiteSpace(options.ApplicationName) ? "XLAB2" : options.ApplicationName.Trim(),
ConnectRetryCount = Math.Max(0, options.ConnectRetryCount),
ConnectRetryInterval = Math.Max(1, options.ConnectRetryIntervalSeconds),
ConnectTimeout = Math.Max(1, options.ConnectTimeoutSeconds),
DataSource = options.Server.Trim(),
Encrypt = options.Encrypt,
InitialCatalog = options.Database.Trim(),
IntegratedSecurity = options.IntegratedSecurity,
MaxPoolSize = Math.Max(1, options.MaxPoolSize),
MinPoolSize = Math.Max(0, options.MinPoolSize),
MultipleActiveResultSets = options.MultipleActiveResultSets,
Pooling = options.Pooling,
TrustServerCertificate = options.TrustServerCertificate
};
return builder.ConnectionString;
}
}