diff --git a/FruityFoundation.DataAccess.Abstractions/FruityFoundation.DataAccess.Abstractions.csproj b/FruityFoundation.DataAccess.Abstractions/FruityFoundation.DataAccess.Abstractions.csproj index c5fa411..e87adc6 100644 --- a/FruityFoundation.DataAccess.Abstractions/FruityFoundation.DataAccess.Abstractions.csproj +++ b/FruityFoundation.DataAccess.Abstractions/FruityFoundation.DataAccess.Abstractions.csproj @@ -4,6 +4,14 @@ enable enable net8.0;net6.0 + true + Kyle Ratti + https://github.com/kyleratti/FruityFoundation + true + + + + diff --git a/FruityFoundation.DataAccess.Abstractions/INonTransactionalDbConnection.cs b/FruityFoundation.DataAccess.Abstractions/INonTransactionalDbConnection.cs index 9dcef19..2a8789c 100644 --- a/FruityFoundation.DataAccess.Abstractions/INonTransactionalDbConnection.cs +++ b/FruityFoundation.DataAccess.Abstractions/INonTransactionalDbConnection.cs @@ -5,7 +5,6 @@ namespace FruityFoundation.DataAccess.Abstractions; public interface INonTransactionalDbConnection : IDatabaseConnection, IDisposable, IAsyncDisposable where TConnectionType : ConnectionType { - public Task> CreateTransaction(); - public Task> CreateTransaction(IsolationLevel isolationLevel); - public Task> CreateTransaction(IsolationLevel isolationLevel, bool deferred); + public Task> CreateTransaction(CancellationToken cancellationToken); + public Task> CreateTransaction(IsolationLevel isolationLevel, CancellationToken cancellationToken); } diff --git a/FruityFoundation.DataAccess.Core/DbConnectionFactory.cs b/FruityFoundation.DataAccess.Core/DbConnectionFactory.cs new file mode 100644 index 0000000..84dd0ca --- /dev/null +++ b/FruityFoundation.DataAccess.Core/DbConnectionFactory.cs @@ -0,0 +1,30 @@ +using System.Data.Common; +using FruityFoundation.DataAccess.Abstractions; + +namespace FruityFoundation.DataAccess.Core; + +public class DbConnectionFactory : IDbConnectionFactory +{ + private readonly Func _readWriteConnectionFactory; + private readonly Func _readOnlyConnectionFactory; + + public DbConnectionFactory(Func readWriteConnectionFactory, Func readOnlyConnectionFactory) + { + _readWriteConnectionFactory = readWriteConnectionFactory; + _readOnlyConnectionFactory = readOnlyConnectionFactory; + } + + public INonTransactionalDbConnection CreateConnection() + { + var connection = _readWriteConnectionFactory(); + var nonTxConnection = new NonTransactionalDbConnection(connection); + return nonTxConnection; + } + + public INonTransactionalDbConnection CreateReadOnlyConnection() + { + var connection = _readOnlyConnectionFactory(); + var nonTxConnection = new NonTransactionalDbConnection(connection); + return nonTxConnection; + } +} diff --git a/FruityFoundation.DataAccess.Sqlite/DbTransaction.cs b/FruityFoundation.DataAccess.Core/DbTransaction.cs similarity index 86% rename from FruityFoundation.DataAccess.Sqlite/DbTransaction.cs rename to FruityFoundation.DataAccess.Core/DbTransaction.cs index 609e4af..1420da7 100644 --- a/FruityFoundation.DataAccess.Sqlite/DbTransaction.cs +++ b/FruityFoundation.DataAccess.Core/DbTransaction.cs @@ -2,21 +2,20 @@ using System.Runtime.CompilerServices; using Dapper; using FruityFoundation.DataAccess.Abstractions; -using Microsoft.Data.Sqlite; -namespace FruityFoundation.DataAccess.Sqlite; +namespace FruityFoundation.DataAccess.Core; +// ReSharper disable once ClassWithVirtualMembersNeverInherited.Global public class DbTransaction : IDatabaseTransactionConnection where TConnectionType : ConnectionType { - private readonly SqliteTransaction _transaction; + private readonly DbTransaction _transaction; - internal DbTransaction(SqliteTransaction transaction) + internal DbTransaction(DbTransaction transaction) { _transaction = transaction; } - /// /// public async Task> Query( string sql, @@ -109,17 +108,34 @@ public class DbTransaction : IDatabaseTransactionConnection public void Dispose() { - _transaction.Dispose(); + Dispose(true); GC.SuppressFinalize(this); } + protected virtual async ValueTask DisposeAsyncCore() + { +#pragma warning disable IDISP007 + await _transaction.DisposeAsync(); +#pragma warning restore IDISP007 + } + /// public async ValueTask DisposeAsync() { - await _transaction.DisposeAsync(); + await DisposeAsyncCore(); GC.SuppressFinalize(this); } } diff --git a/FruityFoundation.DataAccess.Sqlite/FruityFoundation.DataAccess.Sqlite.csproj b/FruityFoundation.DataAccess.Core/FruityFoundation.DataAccess.Core.csproj similarity index 57% rename from FruityFoundation.DataAccess.Sqlite/FruityFoundation.DataAccess.Sqlite.csproj rename to FruityFoundation.DataAccess.Core/FruityFoundation.DataAccess.Core.csproj index 3fdb130..6d43d4b 100644 --- a/FruityFoundation.DataAccess.Sqlite/FruityFoundation.DataAccess.Sqlite.csproj +++ b/FruityFoundation.DataAccess.Core/FruityFoundation.DataAccess.Core.csproj @@ -3,17 +3,23 @@ enable enable + true + Kyle Ratti + https://github.com/kyleratti/FruityFoundation + true net8.0;net6.0 + + + + - - diff --git a/FruityFoundation.DataAccess.Core/NonTransactionalDbConnection.cs b/FruityFoundation.DataAccess.Core/NonTransactionalDbConnection.cs new file mode 100644 index 0000000..2c898e9 --- /dev/null +++ b/FruityFoundation.DataAccess.Core/NonTransactionalDbConnection.cs @@ -0,0 +1,124 @@ +using System.Data; +using System.Data.Common; +using System.Runtime.CompilerServices; +using Dapper; +using FruityFoundation.DataAccess.Abstractions; + +namespace FruityFoundation.DataAccess.Core; + +// ReSharper disable once ClassWithVirtualMembersNeverInherited.Global +public class NonTransactionalDbConnection : INonTransactionalDbConnection + where TConnectionType : ConnectionType +{ + private readonly DbConnection _connection; + + /// + /// C'tor + /// + public NonTransactionalDbConnection(DbConnection connection) + { + _connection = connection; + } + + /// + public async Task> Query( + string sql, + object? param = null, + CancellationToken cancellationToken = default + ) => await _connection.QueryAsync(new CommandDefinition(sql, param, cancellationToken: cancellationToken)); + + /// + public async IAsyncEnumerable QueryUnbuffered( + string sql, + object? param = null, + [EnumeratorCancellation] CancellationToken cancellationToken = default + ) + { + var query = _connection.QueryUnbufferedAsync(sql, param, transaction: null) + .WithCancellation(cancellationToken); + + await foreach (var item in query) + yield return item; + } + + /// + public async Task QuerySingle( + string sql, + object? param = null, + CancellationToken cancellationToken = default + ) => await _connection.QuerySingleAsync(new CommandDefinition(sql, param, cancellationToken: cancellationToken)); + + /// + public async Task Execute( + string sql, + object? param = null, + CancellationToken cancellationToken = default + ) => await _connection.ExecuteAsync(new CommandDefinition(sql, param, cancellationToken: cancellationToken)); + + /// + public async Task ExecuteScalar( + string sql, + object? param = null, + CancellationToken cancellationToken = default + ) => await _connection.ExecuteScalarAsync(new CommandDefinition(sql, param, cancellationToken: cancellationToken)); + + /// + public async Task ExecuteReader( + string sql, + object? param = null, + CancellationToken cancellationToken = default + ) => await _connection.ExecuteReaderAsync(new CommandDefinition(sql, param, cancellationToken: cancellationToken)); + + /// + public async Task> CreateTransaction(CancellationToken cancellationToken) + { + if (!_connection.State.HasFlag(ConnectionState.Open)) + await _connection.OpenAsync(cancellationToken); + + var tx = await _connection.BeginTransactionAsync(cancellationToken); + + return new DbTransaction(tx); + } + + /// + public async Task> CreateTransaction(IsolationLevel isolationLevel, CancellationToken cancellationToken) + { + if (!_connection.State.HasFlag(ConnectionState.Open)) + await _connection.OpenAsync(cancellationToken); + + var tx = await _connection.BeginTransactionAsync(isolationLevel, cancellationToken); + + return new DbTransaction(tx); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { +#pragma warning disable IDISP007 + _connection.Dispose(); +#pragma warning restore IDISP007 + } + } + + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual async ValueTask DisposeAsyncCore() + { +#pragma warning disable IDISP007 + await _connection.DisposeAsync(); +#pragma warning restore IDISP007 + } + + /// + public async ValueTask DisposeAsync() + { + await DisposeAsyncCore(); + GC.SuppressFinalize(this); + } +} diff --git a/FruityFoundation.DataAccess.Sqlite/DbConnectionFactory.cs b/FruityFoundation.DataAccess.Sqlite/DbConnectionFactory.cs deleted file mode 100644 index a946050..0000000 --- a/FruityFoundation.DataAccess.Sqlite/DbConnectionFactory.cs +++ /dev/null @@ -1,33 +0,0 @@ -using FruityFoundation.DataAccess.Abstractions; - -namespace FruityFoundation.DataAccess.Sqlite; - -public class DbConnectionFactory : IDbConnectionFactory -{ - private readonly string _readWriteConnectionString; - private readonly string _readOnlyConnectionString; - - public DbConnectionFactory(string readWriteConnectionString, string readOnlyConnectionString) - { - _readWriteConnectionString = readWriteConnectionString; - _readOnlyConnectionString = readOnlyConnectionString; - } - - public INonTransactionalDbConnection CreateConnection() - { - if (string.IsNullOrWhiteSpace(_readWriteConnectionString)) - throw new ApplicationException("ReadWrite connection string cannot be null or empty."); - - var connection = new NonTransactionalDbConnection(_readWriteConnectionString); - return connection; - } - - public INonTransactionalDbConnection CreateReadOnlyConnection() - { - if (string.IsNullOrWhiteSpace(_readOnlyConnectionString)) - throw new ApplicationException("ReadOnly connection string cannot be null or empty."); - - var connection = new NonTransactionalDbConnection(_readOnlyConnectionString); - return connection; - } -} diff --git a/FruityFoundation.DataAccess.Sqlite/NonTransactionalDbConnection.cs b/FruityFoundation.DataAccess.Sqlite/NonTransactionalDbConnection.cs deleted file mode 100644 index 2caf299..0000000 --- a/FruityFoundation.DataAccess.Sqlite/NonTransactionalDbConnection.cs +++ /dev/null @@ -1,102 +0,0 @@ -using System.Data; -using System.Data.Common; -using System.Runtime.CompilerServices; -using Dapper; -using FruityFoundation.DataAccess.Abstractions; -using Microsoft.Data.Sqlite; - -namespace FruityFoundation.DataAccess.Sqlite; - -// ReSharper disable once UnusedTypeParameter -public class NonTransactionalDbConnection : SqliteConnection, INonTransactionalDbConnection - where TConnectionType : ConnectionType -{ - /// - /// C'tor - /// - public NonTransactionalDbConnection(string connectionString) : base(connectionString) - { - } - - /// - public async Task> Query( - string sql, - object? param = null, - CancellationToken cancellationToken = default - ) => await this.QueryAsync(new CommandDefinition(sql, param, cancellationToken: cancellationToken)); - - /// - public async IAsyncEnumerable QueryUnbuffered( - string sql, - object? param = null, - [EnumeratorCancellation] CancellationToken cancellationToken = default - ) - { - var query = this.QueryUnbufferedAsync(sql, param, transaction: null) - .WithCancellation(cancellationToken); - - await foreach (var item in query) - yield return item; - } - - /// - public async Task QuerySingle( - string sql, - object? param = null, - CancellationToken cancellationToken = default - ) => await this.QuerySingleAsync(new CommandDefinition(sql, param, cancellationToken: cancellationToken)); - - /// - public async Task Execute( - string sql, - object? param = null, - CancellationToken cancellationToken = default - ) => await this.ExecuteAsync(new CommandDefinition(sql, param, cancellationToken: cancellationToken)); - - /// - public async Task ExecuteScalar( - string sql, - object? param = null, - CancellationToken cancellationToken = default - ) => await this.ExecuteScalarAsync(new CommandDefinition(sql, param, cancellationToken: cancellationToken)); - - /// - public async Task ExecuteReader( - string sql, - object? param = null, - CancellationToken cancellationToken = default - ) => await this.ExecuteReaderAsync(new CommandDefinition(sql, param, cancellationToken: cancellationToken)); - - /// - public async Task> CreateTransaction() - { - if (!State.HasFlag(ConnectionState.Open)) - await OpenAsync(); - - var tx = BeginTransaction(); - - return new DbTransaction(tx); - } - - /// - public async Task> CreateTransaction(IsolationLevel isolationLevel) - { - if (!State.HasFlag(ConnectionState.Open)) - await OpenAsync(); - - var tx = BeginTransaction(isolationLevel); - - return new DbTransaction(tx); - } - - /// - public async Task> CreateTransaction(IsolationLevel isolationLevel, bool deferred) - { - if (!State.HasFlag(ConnectionState.Open)) - await OpenAsync(); - - var tx = BeginTransaction(isolationLevel, deferred); - - return new DbTransaction(tx); - } -} diff --git a/FruityFoundation.DataAccess.Sqlite/ServiceCollectionExtensions.cs b/FruityFoundation.DataAccess.Sqlite/ServiceCollectionExtensions.cs deleted file mode 100644 index adc5e38..0000000 --- a/FruityFoundation.DataAccess.Sqlite/ServiceCollectionExtensions.cs +++ /dev/null @@ -1,13 +0,0 @@ -using FruityFoundation.DataAccess.Abstractions; -using Microsoft.Extensions.DependencyInjection; - -namespace FruityFoundation.DataAccess.Sqlite; - -public static class ServiceCollectionExtensions -{ - public static IServiceCollection AddSqliteConnectionFactory(this IServiceCollection services) - { - services.AddSingleton(); - return services; - } -} diff --git a/FruityFoundation.DataAccess.Sqlite/SqliteConnectionExtensions.cs b/FruityFoundation.DataAccess.Sqlite/SqliteConnectionExtensions.cs deleted file mode 100644 index c6bb5b3..0000000 --- a/FruityFoundation.DataAccess.Sqlite/SqliteConnectionExtensions.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System.Data; -using Microsoft.Data.Sqlite; - -namespace FruityFoundation.DataAccess.Sqlite; - -public static class SqliteConnectionExtensions -{ - public static async Task CreateTransaction(this SqliteConnection connection, IsolationLevel isolationLevel) - { - await connection.OpenAsync(); - return connection.BeginTransaction(isolationLevel); - } -} diff --git a/FruityFoundation.Tests.DataAccess.Sqlite/DbConnectionFactoryTests.cs b/FruityFoundation.Tests.DataAccess.Sqlite/DbConnectionFactoryTests.cs deleted file mode 100644 index 56a709d..0000000 --- a/FruityFoundation.Tests.DataAccess.Sqlite/DbConnectionFactoryTests.cs +++ /dev/null @@ -1,67 +0,0 @@ -using FruityFoundation.DataAccess.Abstractions; -using FruityFoundation.DataAccess.Sqlite; - -namespace FruityFoundation.Tests.DataAccess.Sqlite; - -public class DbConnectionFactoryTests -{ - [TestCase("")] - [TestCase(" ")] - [TestCase(null)] - public void CreateConnection_ThrowsException_WhenConnectionStringIsNullOrEmpty(string? connectionString) - { - // Arrange - var dbConnectionFactory = new DbConnectionFactory(connectionString!, readOnlyConnectionString: "ReadOnlyConnectionString"); - - // Act - var exception = Assert.Throws(() => dbConnectionFactory.CreateConnection()); - - // Assert - Assert.That(exception, Is.Not.Null); - Assert.That(exception.Message, Is.EqualTo("ReadWrite connection string cannot be null or empty.")); - } - - [TestCase("")] - [TestCase(" ")] - [TestCase(null)] - public void CreateReadOnlyConnection_ThrowsException_WhenConnectionStringIsNullOrEmpty(string? connectionString) - { - // Arrange - var dbConnectionFactory = new DbConnectionFactory(readWriteConnectionString: "connectionString", readOnlyConnectionString: null!); - - // Act - var exception = Assert.Throws(() => dbConnectionFactory.CreateReadOnlyConnection()); - - // Assert - Assert.That(exception, Is.Not.Null); - Assert.That(exception.Message, Is.EqualTo("ReadOnly connection string cannot be null or empty.")); - } - - [Test] - public void CreateConnection_ReturnsNonTransactionalDbConnection_WhenConnectionStringIsValid() - { - // Arrange - var dbConnectionFactory = new DbConnectionFactory(readWriteConnectionString: "Data Source=:memory:", readOnlyConnectionString: null!); - - // Act - var connection = dbConnectionFactory.CreateConnection(); - - // Assert - Assert.That(connection, Is.Not.Null); - Assert.That(connection, Is.InstanceOf>()); - } - - [Test] - public void CreateReadOnlyConnection_ReturnsNonTransactionalDbConnection_WhenConnectionStringIsValid() - { - // Arrange - var dbConnectionFactory = new DbConnectionFactory(readWriteConnectionString: null!, readOnlyConnectionString: "Data Source=:memory:;Mode=ReadOnly"); - - // Act - var connection = dbConnectionFactory.CreateReadOnlyConnection(); - - // Assert - Assert.That(connection, Is.Not.Null); - Assert.That(connection, Is.InstanceOf>()); - } -} diff --git a/FruityFoundation.Tests.DataAccess.Sqlite/FruityFoundation.Tests.DataAccess.Sqlite.csproj b/FruityFoundation.Tests.DataAccess.Sqlite/FruityFoundation.Tests.DataAccess.Sqlite.csproj deleted file mode 100644 index 77c87ec..0000000 --- a/FruityFoundation.Tests.DataAccess.Sqlite/FruityFoundation.Tests.DataAccess.Sqlite.csproj +++ /dev/null @@ -1,28 +0,0 @@ - - - - enable - enable - - false - true - net8.0;net6.0 - - - - - - - - - - - - - - - - - - - diff --git a/FruityFoundation.sln b/FruityFoundation.sln index f32a9e6..7b46a1c 100644 --- a/FruityFoundation.sln +++ b/FruityFoundation.sln @@ -18,9 +18,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "DataAccess", "DataAccess", EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FruityFoundation.DataAccess.Abstractions", "FruityFoundation.DataAccess.Abstractions\FruityFoundation.DataAccess.Abstractions.csproj", "{C003E247-C62E-4830-94E4-F274D8466A5C}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FruityFoundation.DataAccess.Sqlite", "FruityFoundation.DataAccess.Sqlite\FruityFoundation.DataAccess.Sqlite.csproj", "{BB25E92F-5D51-487A-8937-27E28EF5E20F}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FruityFoundation.Tests.DataAccess.Sqlite", "FruityFoundation.Tests.DataAccess.Sqlite\FruityFoundation.Tests.DataAccess.Sqlite.csproj", "{A2E62C7C-62A1-43C0-BD60-752B0C84E518}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FruityFoundation.DataAccess.Core", "FruityFoundation.DataAccess.Core\FruityFoundation.DataAccess.Core.csproj", "{B65527CC-218A-4EA3-93DC-985713B5DFF4}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -56,21 +54,16 @@ Global {C003E247-C62E-4830-94E4-F274D8466A5C}.Debug|Any CPU.Build.0 = Debug|Any CPU {C003E247-C62E-4830-94E4-F274D8466A5C}.Release|Any CPU.ActiveCfg = Release|Any CPU {C003E247-C62E-4830-94E4-F274D8466A5C}.Release|Any CPU.Build.0 = Release|Any CPU - {BB25E92F-5D51-487A-8937-27E28EF5E20F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {BB25E92F-5D51-487A-8937-27E28EF5E20F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {BB25E92F-5D51-487A-8937-27E28EF5E20F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {BB25E92F-5D51-487A-8937-27E28EF5E20F}.Release|Any CPU.Build.0 = Release|Any CPU - {A2E62C7C-62A1-43C0-BD60-752B0C84E518}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A2E62C7C-62A1-43C0-BD60-752B0C84E518}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A2E62C7C-62A1-43C0-BD60-752B0C84E518}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A2E62C7C-62A1-43C0-BD60-752B0C84E518}.Release|Any CPU.Build.0 = Release|Any CPU + {B65527CC-218A-4EA3-93DC-985713B5DFF4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B65527CC-218A-4EA3-93DC-985713B5DFF4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B65527CC-218A-4EA3-93DC-985713B5DFF4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B65527CC-218A-4EA3-93DC-985713B5DFF4}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {50A75644-A1C3-4495-9DEB-DBB12D9334B5} = {B44178DF-5B81-4029-90FA-2BF8E2A1EDBF} {EBDC3640-4E47-43FE-BF0D-4BFFD07FE2EF} = {B44178DF-5B81-4029-90FA-2BF8E2A1EDBF} {A64E73D3-EF87-4938-B01E-F9CC0B59F9DE} = {B44178DF-5B81-4029-90FA-2BF8E2A1EDBF} {C003E247-C62E-4830-94E4-F274D8466A5C} = {5C3A014A-7931-4A36-95F0-5EFE15AB06A3} - {BB25E92F-5D51-487A-8937-27E28EF5E20F} = {5C3A014A-7931-4A36-95F0-5EFE15AB06A3} - {A2E62C7C-62A1-43C0-BD60-752B0C84E518} = {B44178DF-5B81-4029-90FA-2BF8E2A1EDBF} + {B65527CC-218A-4EA3-93DC-985713B5DFF4} = {5C3A014A-7931-4A36-95F0-5EFE15AB06A3} EndGlobalSection EndGlobal