using Connected.Entities.Storage; using Connected.Middleware; using Connected.ServiceModel; using Connected.Threading; using System.Collections.Concurrent; using System.Collections.Immutable; using System.Data; using System.Data.Common; namespace Connected.Data.Storage; public abstract class DatabaseConnection : MiddlewareComponent, IStorageConnection { private readonly AsyncLocker _lock = new(); private IDbConnection _connection; private ConcurrentDictionary _commands = null; protected DatabaseConnection(ICancellationContext context) { Context = context; Commands = new(); } protected ICancellationContext Context { get; } private IDbTransaction Transaction { get; set; } public StorageConnectionMode Behavior { get; set; } public string ConnectionString { get; private set; } public async Task Initialize(StorageConnectionArgs args) { ConnectionString = args.ConnectionString; Behavior = args.Behavior; await OnInitialize(); } protected virtual async Task OnInitialize() { await Task.CompletedTask; } protected abstract Task OnCreateConnection(); private async Task GetConnection() { if (_connection is null) { await _lock.LockAsync(1, async () => { _connection ??= await OnCreateConnection(); }); } return _connection; } private ConcurrentDictionary Commands { get; } public async Task Commit() { if (Transaction is null || Transaction.Connection is null) return; await _lock.LockAsync(2, async () => { if (Transaction is null || Transaction.Connection is null) return; if (Transaction is DbTransaction db) await db.CommitAsync(Context is null ? CancellationToken.None : Context.CancellationToken); else Transaction.Commit(); Transaction.Dispose(); Transaction = null; }); await Task.CompletedTask; } public async Task Rollback() { if (Transaction is null || Transaction.Connection is null) return; await _lock.LockAsync(3, async () => { if (Transaction is null || Transaction.Connection is null) return; try { if (Transaction is DbTransaction db) await db.RollbackAsync(Context is null ? CancellationToken.None : Context.CancellationToken); else Transaction.Rollback(); } catch { } }); await Task.CompletedTask; } private async Task Open() { var connection = await GetConnection(); if (connection.State == ConnectionState.Open) return; await _lock.LockAsync(4, async () => { if (connection.State != ConnectionState.Closed) return; if (connection is DbConnection db) await db.OpenAsync(Context is null ? CancellationToken.None : Context.CancellationToken); else connection.Open(); }); await Task.CompletedTask; } public async Task Close() { if (_connection is null) return; if (_connection is not null && _connection.State == ConnectionState.Open) { await _lock.LockAsync(5, async () => { if (_connection is not null && _connection.State == ConnectionState.Open) { if (Transaction is not null && Transaction.Connection is not null) { try { if (Transaction is DbTransaction db) await db.RollbackAsync(Context is null ? CancellationToken.None : Context.CancellationToken); else Transaction.Rollback(); } catch { } } if (_connection is DbConnection dbc) await dbc.CloseAsync(); else _connection.Close(); } }); } await Task.CompletedTask; } public async Task Execute(IStorageCommand command) { await EnsureOpen(true); var com = await ResolveCommand(command); SetupParameters(command, com); if (command.Operation.Parameters is not null) { foreach (var i in command.Operation.Parameters) SetParameterValue(com, i.Name, i.Value); } var recordsAffected = await OnExecute(command, com); if (command.Operation.Parameters is not null) { foreach (var i in command.Operation.Parameters) { if (i.Direction == ParameterDirection.Output) i.Value = GetParameterValue(com, i.Name); } } return recordsAffected; } protected virtual void SetParameterValue(IDbCommand command, string parameterName, object value) { } protected virtual object? GetParameterValue(IDbCommand command, string parameterName) { return default; } protected virtual void SetupParameters(IStorageCommand command, IDbCommand cmd) { } protected virtual async Task OnExecute(IStorageCommand command, IDbCommand cmd) { if (cmd is DbCommand dbCommand) return await dbCommand.ExecuteNonQueryAsync(Context is null ? CancellationToken.None : Context.CancellationToken); else return cmd.ExecuteNonQuery(); } public virtual async Task> Query(IStorageCommand command) { await EnsureOpen(false); var com = await ResolveCommand(command); IDataReader rdr = null; try { SetupParameters(command, com); if (command.Operation.Parameters is not null) { foreach (var i in command.Operation.Parameters) SetParameterValue(com, i.Name, i.Value); } rdr = com is DbCommand db ? await db.ExecuteReaderAsync(Context is null ? CancellationToken.None : Context.CancellationToken) : com.ExecuteReader(); var result = new List(); var mappings = new FieldMappings(rdr); while (rdr.Read()) result.Add(mappings.CreateInstance(rdr)); return result.ToImmutableList(); } finally { if (rdr != null && !rdr.IsClosed) { if (rdr is DbDataReader db) await db.CloseAsync(); else rdr.Close(); } } } public virtual async Task Select(IStorageCommand command) { await EnsureOpen(false); var com = await ResolveCommand(command); IDataReader rdr = null; try { SetupParameters(command, com); if (command.Operation.Parameters is not null) { foreach (var i in command.Operation.Parameters) SetParameterValue(com, i.Name, i.Value); } rdr = com.ExecuteReader(CommandBehavior.SingleRow); var mappings = new FieldMappings(rdr); if (rdr.Read()) return mappings.CreateInstance(rdr); return default; } finally { if (rdr != null && !rdr.IsClosed) { if (rdr is DbDataReader db) await db.CloseAsync(); else rdr.Close(); } } } public virtual async Task OpenReader(IStorageCommand command) { await EnsureOpen(false); var com = await ResolveCommand(command); SetupParameters(command, com); if (command.Operation.Parameters is not null) { foreach (var i in command.Operation.Parameters) SetParameterValue(com, i.Name, i.Value); } return com.ExecuteReader(); } protected virtual async Task ResolveCommand(IStorageCommand command) { if (Commands.TryGetValue(command, out IDbCommand? existing)) return existing; if (Commands.TryGetValue(command, out IDbCommand? existing2)) return existing2; return await _lock.LockAsync(6, async () => { var connection = await GetConnection(); var r = connection.CreateCommand(); r.CommandText = command.Operation.CommandText; r.CommandType = command.Operation.CommandType; r.CommandTimeout = command.Operation.CommandTimeout; if (Transaction is not null) r.Transaction = Transaction; Commands.TryAdd(command, r); return r; }); } private async Task EnsureOpen(bool createTransaction) { var connection = await GetConnection(); if (connection is null || connection.State == ConnectionState.Open) return; await _lock.LockAsync(7, async () => { await Open(); if (createTransaction && Transaction is null) { Transaction = connection is DbConnection dbc ? await dbc.BeginTransactionAsync(Context is null ? CancellationToken.None : Context.CancellationToken) : connection.BeginTransaction(IsolationLevel.ReadCommitted); } }); } public void Dispose() { OnDispose(true); GC.SuppressFinalize(this); } protected virtual void OnDispose(bool disposing) { AsyncUtils.RunSync(() => Close()); if (_commands is not null) { foreach (var command in _commands) command.Value.Dispose(); _commands = null; } if (Transaction is not null) { try { AsyncUtils.RunSync(Rollback); Transaction.Dispose(); } catch { } Transaction = null; } if (_connection is not null) { _connection.Dispose(); _connection = null; } } public async ValueTask DisposeAsync() { await OnDisposeAsyncCore().ConfigureAwait(false); OnDispose(false); GC.SuppressFinalize(this); } protected virtual async ValueTask OnDisposeAsyncCore() { await Close().ConfigureAwait(false); if (_commands is not null) { foreach (var command in _commands) { if (command.Value is DbCommand db) await db.DisposeAsync().ConfigureAwait(false); else command.Value.Dispose(); } _commands = null; } if (Transaction is not null) { try { //No way to check if possible await Rollback().ConfigureAwait(false); if (Transaction is DbTransaction dbt) await dbt.DisposeAsync().ConfigureAwait(false); else Transaction.Dispose(); } catch { } Transaction = null; } if (_connection is not null) { if (_connection is DbConnection dbc) await dbc.DisposeAsync().ConfigureAwait(false); else _connection.Dispose(); _connection = null; } } }