diff --git a/FruityFoundation.DataAccess.Abstractions/ConnectionType.cs b/FruityFoundation.DataAccess.Abstractions/ConnectionType.cs
new file mode 100644
index 0000000..fd0ccdd
--- /dev/null
+++ b/FruityFoundation.DataAccess.Abstractions/ConnectionType.cs
@@ -0,0 +1,7 @@
+namespace FruityFoundation.DataAccess.Abstractions;
+
+public abstract class ConnectionType;
+
+public abstract class ReadOnly : ConnectionType;
+
+public abstract class ReadWrite : ReadOnly;
diff --git a/FruityFoundation.DataAccess.Abstractions/FruityFoundation.DataAccess.Abstractions.csproj b/FruityFoundation.DataAccess.Abstractions/FruityFoundation.DataAccess.Abstractions.csproj
new file mode 100644
index 0000000..3a63532
--- /dev/null
+++ b/FruityFoundation.DataAccess.Abstractions/FruityFoundation.DataAccess.Abstractions.csproj
@@ -0,0 +1,9 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
diff --git a/FruityFoundation.DataAccess.Abstractions/IDatabaseConnection.cs b/FruityFoundation.DataAccess.Abstractions/IDatabaseConnection.cs
new file mode 100644
index 0000000..e60597a
--- /dev/null
+++ b/FruityFoundation.DataAccess.Abstractions/IDatabaseConnection.cs
@@ -0,0 +1,14 @@
+using System.Data.Common;
+
+namespace FruityFoundation.DataAccess.Abstractions;
+
+// ReSharper disable once UnusedTypeParameter
+public interface IDatabaseConnection where TConnectionType : ConnectionType
+{
+ public Task> Query(string sql, object? param = null, CancellationToken cancellationToken = default);
+ public IAsyncEnumerable QueryUnbuffered(string sql, object? param = null, CancellationToken cancellationToken = default);
+ public Task QuerySingle(string sql, object? param = null, CancellationToken cancellationToken = default);
+ public Task Execute(string sql, object? param = null, CancellationToken cancellationToken = default);
+ public Task ExecuteScalar(string sql, object? param = null, CancellationToken cancellationToken = default);
+ public Task ExecuteReader(string sql, object? param = null, CancellationToken cancellationToken = default);
+}
diff --git a/FruityFoundation.DataAccess.Abstractions/IDatabaseTransactionConnection.cs b/FruityFoundation.DataAccess.Abstractions/IDatabaseTransactionConnection.cs
new file mode 100644
index 0000000..074e308
--- /dev/null
+++ b/FruityFoundation.DataAccess.Abstractions/IDatabaseTransactionConnection.cs
@@ -0,0 +1,8 @@
+namespace FruityFoundation.DataAccess.Abstractions;
+
+public interface IDatabaseTransactionConnection : IDatabaseConnection, IDisposable, IAsyncDisposable
+ where TConnectionType : ConnectionType
+{
+ public Task Commit(CancellationToken cancellationToken);
+ public Task Rollback(CancellationToken cancellationToken);
+}
diff --git a/FruityFoundation.DataAccess.Abstractions/IDbConnectionFactory.cs b/FruityFoundation.DataAccess.Abstractions/IDbConnectionFactory.cs
new file mode 100644
index 0000000..18735bb
--- /dev/null
+++ b/FruityFoundation.DataAccess.Abstractions/IDbConnectionFactory.cs
@@ -0,0 +1,7 @@
+namespace FruityFoundation.DataAccess.Abstractions;
+
+public interface IDbConnectionFactory
+{
+ public INonTransactionalDbConnection CreateConnection();
+ public INonTransactionalDbConnection CreateReadOnlyConnection();
+}
diff --git a/FruityFoundation.DataAccess.Abstractions/INonTransactionalDbConnection.cs b/FruityFoundation.DataAccess.Abstractions/INonTransactionalDbConnection.cs
new file mode 100644
index 0000000..9dcef19
--- /dev/null
+++ b/FruityFoundation.DataAccess.Abstractions/INonTransactionalDbConnection.cs
@@ -0,0 +1,11 @@
+using System.Data;
+
+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);
+}
diff --git a/FruityFoundation.DataAccess.Sqlite/DbConnectionFactory.cs b/FruityFoundation.DataAccess.Sqlite/DbConnectionFactory.cs
new file mode 100644
index 0000000..7760c4c
--- /dev/null
+++ b/FruityFoundation.DataAccess.Sqlite/DbConnectionFactory.cs
@@ -0,0 +1,33 @@
+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 was not found or empty.");
+
+ var connection = new NonTransactionalDbConnection(_readWriteConnectionString);
+ return connection;
+ }
+
+ public INonTransactionalDbConnection CreateReadOnlyConnection()
+ {
+ if (string.IsNullOrWhiteSpace(_readOnlyConnectionString))
+ throw new ApplicationException("ReadOnly connection string was not found or empty.");
+
+ var connection = new NonTransactionalDbConnection(_readOnlyConnectionString);
+ return connection;
+ }
+}
diff --git a/FruityFoundation.DataAccess.Sqlite/DbTransaction.cs b/FruityFoundation.DataAccess.Sqlite/DbTransaction.cs
new file mode 100644
index 0000000..609e4af
--- /dev/null
+++ b/FruityFoundation.DataAccess.Sqlite/DbTransaction.cs
@@ -0,0 +1,125 @@
+using System.Data.Common;
+using System.Runtime.CompilerServices;
+using Dapper;
+using FruityFoundation.DataAccess.Abstractions;
+using Microsoft.Data.Sqlite;
+
+namespace FruityFoundation.DataAccess.Sqlite;
+
+public class DbTransaction : IDatabaseTransactionConnection
+ where TConnectionType : ConnectionType
+{
+ private readonly SqliteTransaction _transaction;
+
+ internal DbTransaction(SqliteTransaction transaction)
+ {
+ _transaction = transaction;
+ }
+
+ ///
+ ///
+ public async Task> Query(
+ string sql,
+ object? param = null,
+ CancellationToken cancellationToken = default
+ )
+ {
+ if (_transaction.Connection is not { } conn)
+ throw new InvalidOperationException("Transaction connection cannot be null");
+
+ var command = new CommandDefinition(sql, param, transaction: _transaction, cancellationToken: cancellationToken);
+
+ return await conn.QueryAsync(command);
+ }
+
+ ///
+ public async IAsyncEnumerable QueryUnbuffered(
+ string sql,
+ object? param = null,
+ [EnumeratorCancellation] CancellationToken cancellationToken = default
+ )
+ {
+ if (_transaction.Connection is not { } conn)
+ throw new InvalidOperationException("Transaction connection cannot be null");
+
+ var query = conn.QueryUnbufferedAsync(sql, param, transaction: _transaction)
+ .WithCancellation(cancellationToken);
+
+ await foreach (var item in query)
+ yield return item;
+ }
+
+ ///
+ public async Task QuerySingle(string sql, object? param = null, CancellationToken cancellationToken = default)
+ {
+ if (_transaction.Connection is not { } conn)
+ throw new InvalidOperationException("Transaction connection cannot be null");
+
+ var command = new CommandDefinition(sql, param, transaction: _transaction, cancellationToken: cancellationToken);
+
+ return await conn.QuerySingleAsync(command);
+ }
+
+ ///
+ public async Task Execute(string sql, object? param = null, CancellationToken cancellationToken = default)
+ {
+ if (_transaction.Connection is not { } conn)
+ throw new InvalidOperationException("Transaction connection cannot be null");
+
+ var command = new CommandDefinition(sql, param, transaction: _transaction, cancellationToken: cancellationToken);
+
+ await conn.ExecuteAsync(command);
+ }
+
+ ///
+ public async Task ExecuteScalar(string sql, object? param = null, CancellationToken cancellationToken = default)
+ {
+ if (_transaction.Connection is not { } conn)
+ throw new InvalidOperationException("Transaction connection cannot be null");
+
+ var command = new CommandDefinition(sql, param, transaction: _transaction, cancellationToken: cancellationToken);
+
+ return await conn.ExecuteScalarAsync(command);
+ }
+
+ ///
+ public async Task ExecuteReader(
+ string sql,
+ object? param = null,
+ CancellationToken cancellationToken = default
+ )
+ {
+ if (_transaction.Connection is not { } conn)
+ throw new InvalidOperationException("Transaction connection cannot be null");
+
+ var command = new CommandDefinition(sql, param, transaction: _transaction, cancellationToken: cancellationToken);
+
+ return await conn.ExecuteReaderAsync(command);
+ }
+
+ ///
+ public async Task Commit(CancellationToken cancellationToken)
+ {
+ await _transaction.CommitAsync(cancellationToken);
+ }
+
+ ///
+ public async Task Rollback(CancellationToken cancellationToken)
+ {
+ await _transaction.RollbackAsync(cancellationToken);
+ }
+
+ ///
+ public void Dispose()
+ {
+ _transaction.Dispose();
+ GC.SuppressFinalize(this);
+ }
+
+ ///
+ public async ValueTask DisposeAsync()
+ {
+ await _transaction.DisposeAsync();
+ GC.SuppressFinalize(this);
+ }
+}
diff --git a/FruityFoundation.DataAccess.Sqlite/FruityFoundation.DataAccess.Sqlite.csproj b/FruityFoundation.DataAccess.Sqlite/FruityFoundation.DataAccess.Sqlite.csproj
new file mode 100644
index 0000000..4850e21
--- /dev/null
+++ b/FruityFoundation.DataAccess.Sqlite/FruityFoundation.DataAccess.Sqlite.csproj
@@ -0,0 +1,19 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/FruityFoundation.DataAccess.Sqlite/NonTransactionalDbConnection.cs b/FruityFoundation.DataAccess.Sqlite/NonTransactionalDbConnection.cs
new file mode 100644
index 0000000..2caf299
--- /dev/null
+++ b/FruityFoundation.DataAccess.Sqlite/NonTransactionalDbConnection.cs
@@ -0,0 +1,102 @@
+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
new file mode 100644
index 0000000..adc5e38
--- /dev/null
+++ b/FruityFoundation.DataAccess.Sqlite/ServiceCollectionExtensions.cs
@@ -0,0 +1,13 @@
+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
new file mode 100644
index 0000000..c6bb5b3
--- /dev/null
+++ b/FruityFoundation.DataAccess.Sqlite/SqliteConnectionExtensions.cs
@@ -0,0 +1,13 @@
+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.sln b/FruityFoundation.sln
index db789da..9083a66 100644
--- a/FruityFoundation.sln
+++ b/FruityFoundation.sln
@@ -14,6 +14,12 @@ Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "FruityFoundation.Tests.FsBa
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FruityFoundation.Tests.FsBaseInterop", "FruityFoundation.Tests.FsBaseInterop\FruityFoundation.Tests.FsBaseInterop.csproj", "{A64E73D3-EF87-4938-B01E-F9CC0B59F9DE}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "DataAccess", "DataAccess", "{5C3A014A-7931-4A36-95F0-5EFE15AB06A3}"
+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
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU