From 0b4f638cb98568dcbb92baa1a4d76d5f91f6316b Mon Sep 17 00:00:00 2001 From: Kyle Date: Thu, 11 Jul 2024 20:11:33 -0400 Subject: [PATCH] feat: factor connection factory into Sqlite package BREAKING CHANGE: The previous abstraction was confusing/misleading and was forcefully registering all connections as transient. These changes invert the relationship a bit and change the injectable INonTransactionalDbConnection into scoped. It also allows us to provide an explicit connection factory for Sqlite and an extension method that's easier to use while preserving the very slim nature of the core data access package. --- .github/workflows/publish-release.yml | 3 +- Directory.Build.props | 5 +++ .../DbConnectionFactory.cs | 27 ------------- .../FruityFoundation.DataAccess.Core.csproj | 1 - .../ServiceCollectionExtensions.cs | 20 ---------- .../FruityFoundation.DataAccess.Sqlite.csproj | 29 ++++++++++++++ .../ServiceCollectionExtensions.cs | 36 +++++++++++++++++ .../SqliteDbConnectionFactory.cs | 40 +++++++++++++++++++ ...ests.DataAccess.Abstractions.FSharp.fsproj | 1 + ...yFoundation.Tests.DataAccess.Sqlite.csproj | 29 ++++++++++++++ .../SqliteDbConnectionFactoryTests.cs | 39 ++++++++++++++++++ .../FruityFoundation.Tests.FsBase.fsproj | 1 + ...ruityFoundation.Tests.FsBaseInterop.csproj | 1 + FruityFoundation.sln | 14 +++++++ 14 files changed, 197 insertions(+), 49 deletions(-) create mode 100644 Directory.Build.props delete mode 100644 FruityFoundation.DataAccess.Core/DbConnectionFactory.cs delete mode 100644 FruityFoundation.DataAccess.Core/ServiceCollectionExtensions.cs create mode 100644 FruityFoundation.DataAccess.Sqlite/FruityFoundation.DataAccess.Sqlite.csproj create mode 100644 FruityFoundation.DataAccess.Sqlite/ServiceCollectionExtensions.cs create mode 100644 FruityFoundation.DataAccess.Sqlite/SqliteDbConnectionFactory.cs create mode 100644 FruityFoundation.Tests.DataAccess.Sqlite/FruityFoundation.Tests.DataAccess.Sqlite.csproj create mode 100644 FruityFoundation.Tests.DataAccess.Sqlite/SqliteDbConnectionFactoryTests.cs diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index b82f24f..0b0dcab 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -36,7 +36,7 @@ jobs: - run: dotnet restore - run: dotnet build --no-restore -c Release -p:Version=${{ steps.generate-version.outputs.RELEASE_VERSION_NUMBER }} - run: dotnet pack --no-build --no-restore --nologo --output=dist -c Release - - run: gh release create "v${{ steps.generate-version.outputs.RELEASE_VERSION_NUMBER }}" --notes-file CHANGELOG.md "dist/FruityFoundation.Base.${{ steps.generate-version.outputs.RELEASE_VERSION_NUMBER }}.nupkg" "dist/FruityFoundation.Db.${{ steps.generate-version.outputs.RELEASE_VERSION_NUMBER }}.nupkg" "dist/FruityFoundation.FsBase.${{ steps.generate-version.outputs.RELEASE_VERSION_NUMBER }}.nupkg" "dist/FruityFoundation.DataAccess.Abstractions.${{ steps.generate-version.outputs.RELEASE_VERSION_NUMBER }}.nupkg" "dist/FruityFoundation.DataAccess.Core.${{ steps.generate-version.outputs.RELEASE_VERSION_NUMBER }}.nupkg" + - run: gh release create "v${{ steps.generate-version.outputs.RELEASE_VERSION_NUMBER }}" --notes-file CHANGELOG.md "dist/FruityFoundation.Base.${{ steps.generate-version.outputs.RELEASE_VERSION_NUMBER }}.nupkg" "dist/FruityFoundation.Db.${{ steps.generate-version.outputs.RELEASE_VERSION_NUMBER }}.nupkg" "dist/FruityFoundation.FsBase.${{ steps.generate-version.outputs.RELEASE_VERSION_NUMBER }}.nupkg" "dist/FruityFoundation.DataAccess.Abstractions.${{ steps.generate-version.outputs.RELEASE_VERSION_NUMBER }}.nupkg" "dist/FruityFoundation.DataAccess.Core.${{ steps.generate-version.outputs.RELEASE_VERSION_NUMBER }}.nupkg" "dist/FruityFoundation.DataAccess.Sqlite.${{ steps.generate-version.outputs.RELEASE_VERSION_NUMBER }}.nupkg" env: GH_TOKEN: ${{ github.token }} - name: Publish to NuGet @@ -48,3 +48,4 @@ jobs: dotnet nuget push "FruityFoundation.DataAccess.Abstractions.${{ steps.generate-version.outputs.RELEASE_VERSION_NUMBER }}.nupkg" --api-key=${{ secrets.NUGET_API_KEY }} --source=https://api.nuget.org/v3/index.json --skip-duplicate dotnet nuget push "FruityFoundation.DataAccess.Abstractions.FSharp.${{ steps.generate-version.outputs.RELEASE_VERSION_NUMBER }}.nupkg" --api-key=${{ secrets.NUGET_API_KEY }} --source=https://api.nuget.org/v3/index.json --skip-duplicate dotnet nuget push "FruityFoundation.DataAccess.Core.${{ steps.generate-version.outputs.RELEASE_VERSION_NUMBER }}.nupkg" --api-key=${{ secrets.NUGET_API_KEY }} --source=https://api.nuget.org/v3/index.json --skip-duplicate + dotnet nuget push "FruityFoundation.DataAccess.Sqlite.${{ steps.generate-version.outputs.RELEASE_VERSION_NUMBER }}.nupkg" --api-key=${{ secrets.NUGET_API_KEY }} --source=https://api.nuget.org/v3/index.json --skip-duplicate diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000..32969e5 --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,5 @@ + + + true + + diff --git a/FruityFoundation.DataAccess.Core/DbConnectionFactory.cs b/FruityFoundation.DataAccess.Core/DbConnectionFactory.cs deleted file mode 100644 index 7a9c3fc..0000000 --- a/FruityFoundation.DataAccess.Core/DbConnectionFactory.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System.Data.Common; -using FruityFoundation.DataAccess.Abstractions; -using Microsoft.Extensions.DependencyInjection; - -namespace FruityFoundation.DataAccess.Core; - -public class DbConnectionFactory : IDbConnectionFactory -{ - private readonly IServiceProvider _serviceProvider; - - public DbConnectionFactory(IServiceProvider serviceProvider) - { - _serviceProvider = serviceProvider; - } - - public INonTransactionalDbConnection CreateConnection() - { - var nonTxConnection = _serviceProvider.GetRequiredService>(); - return nonTxConnection; - } - - public INonTransactionalDbConnection CreateReadOnlyConnection() - { - var nonTxConnection = _serviceProvider.GetRequiredService>(); - return nonTxConnection; - } -} diff --git a/FruityFoundation.DataAccess.Core/FruityFoundation.DataAccess.Core.csproj b/FruityFoundation.DataAccess.Core/FruityFoundation.DataAccess.Core.csproj index 30a0b16..8902fb2 100644 --- a/FruityFoundation.DataAccess.Core/FruityFoundation.DataAccess.Core.csproj +++ b/FruityFoundation.DataAccess.Core/FruityFoundation.DataAccess.Core.csproj @@ -22,7 +22,6 @@ - diff --git a/FruityFoundation.DataAccess.Core/ServiceCollectionExtensions.cs b/FruityFoundation.DataAccess.Core/ServiceCollectionExtensions.cs deleted file mode 100644 index dfa19c1..0000000 --- a/FruityFoundation.DataAccess.Core/ServiceCollectionExtensions.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using FruityFoundation.DataAccess.Abstractions; -using Microsoft.Extensions.DependencyInjection; - -namespace FruityFoundation.DataAccess.Core; - -[ExcludeFromCodeCoverage(Justification = "Dependency injection helpers")] -public static class ServiceCollectionExtensions -{ - public static void AddDataAccessCore( - this IServiceCollection services, - Func> readWriteConnectionImplementationFactory, - Func> readOnlyConnectionImplementationFactory - ) - { - services.AddTransient>(readWriteConnectionImplementationFactory); - services.AddTransient>(readOnlyConnectionImplementationFactory); - services.AddSingleton(); - } -} diff --git a/FruityFoundation.DataAccess.Sqlite/FruityFoundation.DataAccess.Sqlite.csproj b/FruityFoundation.DataAccess.Sqlite/FruityFoundation.DataAccess.Sqlite.csproj new file mode 100644 index 0000000..4c61bb6 --- /dev/null +++ b/FruityFoundation.DataAccess.Sqlite/FruityFoundation.DataAccess.Sqlite.csproj @@ -0,0 +1,29 @@ + + + + enable + enable + true + Kyle Ratti + https://github.com/kyleratti/FruityFoundation + 1.12.2 + true + LICENSE + net8.0;net6.0 + + + + + + + + + + + + + + + + + diff --git a/FruityFoundation.DataAccess.Sqlite/ServiceCollectionExtensions.cs b/FruityFoundation.DataAccess.Sqlite/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..8bc3ca3 --- /dev/null +++ b/FruityFoundation.DataAccess.Sqlite/ServiceCollectionExtensions.cs @@ -0,0 +1,36 @@ +using System.Diagnostics.CodeAnalysis; +using FruityFoundation.DataAccess.Abstractions; +using Microsoft.Extensions.DependencyInjection; + +namespace FruityFoundation.DataAccess.Sqlite; + +[ExcludeFromCodeCoverage(Justification = "Dependency injection helpers")] +public static class ServiceCollectionExtensions +{ + public static void AddSqliteDataAccess( + this IServiceCollection services, + Func getReadWriteConnectionString, + Func getReadOnlyConnectionString + ) + { + services.AddScoped>(serviceProvider => + { + var connectionFactory = serviceProvider.GetRequiredService(); + + return connectionFactory.CreateConnection(); + }); + + services.AddScoped>(serviceProvider => + { + var connectionFactory = serviceProvider.GetRequiredService(); + + return connectionFactory.CreateReadOnlyConnection(); + }); + + services.AddSingleton(serviceProvider => + new SqliteDbConnectionFactory( + serviceProvider, + getReadOnlyConnectionString: getReadOnlyConnectionString, + getReadWriteConnectionString: getReadWriteConnectionString)); + } +} diff --git a/FruityFoundation.DataAccess.Sqlite/SqliteDbConnectionFactory.cs b/FruityFoundation.DataAccess.Sqlite/SqliteDbConnectionFactory.cs new file mode 100644 index 0000000..9cb7150 --- /dev/null +++ b/FruityFoundation.DataAccess.Sqlite/SqliteDbConnectionFactory.cs @@ -0,0 +1,40 @@ +using FruityFoundation.DataAccess.Abstractions; +using FruityFoundation.DataAccess.Core; +using Microsoft.Data.Sqlite; + +namespace FruityFoundation.DataAccess.Sqlite; + +public class SqliteDbConnectionFactory : IDbConnectionFactory +{ + private readonly IServiceProvider _serviceProvider; + private readonly Func _getReadWriteConnectionString; + private readonly Func _getReadOnlyConnectionString; + + public SqliteDbConnectionFactory( + IServiceProvider serviceProvider, + Func getReadWriteConnectionString, + Func getReadOnlyConnectionString) + { + _serviceProvider = serviceProvider; + _getReadWriteConnectionString = getReadWriteConnectionString; + _getReadOnlyConnectionString = getReadOnlyConnectionString; + } + + /// + public INonTransactionalDbConnection CreateConnection() + { + var connectionString = _getReadWriteConnectionString(_serviceProvider); + var connection = new SqliteConnection(connectionString); + + return new NonTransactionalDbConnection(connection); + } + + /// + public INonTransactionalDbConnection CreateReadOnlyConnection() + { + var connectionString = _getReadOnlyConnectionString(_serviceProvider); + var connection = new SqliteConnection(connectionString); + + return new NonTransactionalDbConnection(connection); + } +} diff --git a/FruityFoundation.Tests.DataAccess.Abstractions.FSharp/FruityFoundation.Tests.DataAccess.Abstractions.FSharp.fsproj b/FruityFoundation.Tests.DataAccess.Abstractions.FSharp/FruityFoundation.Tests.DataAccess.Abstractions.FSharp.fsproj index 7c3288d..7a91cfc 100644 --- a/FruityFoundation.Tests.DataAccess.Abstractions.FSharp/FruityFoundation.Tests.DataAccess.Abstractions.FSharp.fsproj +++ b/FruityFoundation.Tests.DataAccess.Abstractions.FSharp/FruityFoundation.Tests.DataAccess.Abstractions.FSharp.fsproj @@ -26,6 +26,7 @@ + diff --git a/FruityFoundation.Tests.DataAccess.Sqlite/FruityFoundation.Tests.DataAccess.Sqlite.csproj b/FruityFoundation.Tests.DataAccess.Sqlite/FruityFoundation.Tests.DataAccess.Sqlite.csproj new file mode 100644 index 0000000..4054c9f --- /dev/null +++ b/FruityFoundation.Tests.DataAccess.Sqlite/FruityFoundation.Tests.DataAccess.Sqlite.csproj @@ -0,0 +1,29 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + + + + diff --git a/FruityFoundation.Tests.DataAccess.Sqlite/SqliteDbConnectionFactoryTests.cs b/FruityFoundation.Tests.DataAccess.Sqlite/SqliteDbConnectionFactoryTests.cs new file mode 100644 index 0000000..ead2e49 --- /dev/null +++ b/FruityFoundation.Tests.DataAccess.Sqlite/SqliteDbConnectionFactoryTests.cs @@ -0,0 +1,39 @@ +using FruityFoundation.DataAccess.Abstractions; +using FruityFoundation.DataAccess.Sqlite; + +namespace FruityFoundation.Tests.DataAccess.Sqlite; + +public class SqliteDbConnectionFactoryTests +{ + [Test] + public void CreateConnection_WithValidConnectionString_ReturnsNonTransactionalDbConnection_ReadWrite() + { + // Arrange + var connectionFactory = new SqliteDbConnectionFactory( + serviceProvider: null!, + getReadWriteConnectionString: _ => "Data Source=:memory:", + getReadOnlyConnectionString: null!); + + // Act + var result = connectionFactory.CreateConnection(); + + // Assert + Assert.That(result, Is.InstanceOf>()); + } + + [Test] + public void CreateReadOnlyConnection_WithValidConnectionString_ReturnsNonTransactionalDbConnection_ReadOnly() + { + // Arrange + var connectionFactory = new SqliteDbConnectionFactory( + serviceProvider: null!, + getReadWriteConnectionString: null!, + getReadOnlyConnectionString: _ => "Data Source=:memory:"); + + // Act + var result = connectionFactory.CreateReadOnlyConnection(); + + // Assert + Assert.That(result, Is.InstanceOf>()); + } +} diff --git a/FruityFoundation.Tests.FsBase/FruityFoundation.Tests.FsBase.fsproj b/FruityFoundation.Tests.FsBase/FruityFoundation.Tests.FsBase.fsproj index 1e2d1e4..d5fc0b6 100644 --- a/FruityFoundation.Tests.FsBase/FruityFoundation.Tests.FsBase.fsproj +++ b/FruityFoundation.Tests.FsBase/FruityFoundation.Tests.FsBase.fsproj @@ -30,6 +30,7 @@ + diff --git a/FruityFoundation.Tests.FsBaseInterop/FruityFoundation.Tests.FsBaseInterop.csproj b/FruityFoundation.Tests.FsBaseInterop/FruityFoundation.Tests.FsBaseInterop.csproj index fe56c82..f77053d 100644 --- a/FruityFoundation.Tests.FsBaseInterop/FruityFoundation.Tests.FsBaseInterop.csproj +++ b/FruityFoundation.Tests.FsBaseInterop/FruityFoundation.Tests.FsBaseInterop.csproj @@ -28,6 +28,7 @@ + diff --git a/FruityFoundation.sln b/FruityFoundation.sln index 3130355..ac16973 100644 --- a/FruityFoundation.sln +++ b/FruityFoundation.sln @@ -24,6 +24,10 @@ Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "FruityFoundation.DataAccess EndProject Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "FruityFoundation.Tests.DataAccess.Abstractions.FSharp", "FruityFoundation.Tests.DataAccess.Abstractions.FSharp\FruityFoundation.Tests.DataAccess.Abstractions.FSharp.fsproj", "{27F4FB64-7A51-4315-BDAA-6EE07736C976}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FruityFoundation.DataAccess.Sqlite", "FruityFoundation.DataAccess.Sqlite\FruityFoundation.DataAccess.Sqlite.csproj", "{2DB2E605-68E8-47E3-A183-985C1A52B56A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FruityFoundation.Tests.DataAccess.Sqlite", "FruityFoundation.Tests.DataAccess.Sqlite\FruityFoundation.Tests.DataAccess.Sqlite.csproj", "{A1AB7658-7310-490A-82BE-DAABCC204EA4}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -70,6 +74,14 @@ Global {27F4FB64-7A51-4315-BDAA-6EE07736C976}.Debug|Any CPU.Build.0 = Debug|Any CPU {27F4FB64-7A51-4315-BDAA-6EE07736C976}.Release|Any CPU.ActiveCfg = Release|Any CPU {27F4FB64-7A51-4315-BDAA-6EE07736C976}.Release|Any CPU.Build.0 = Release|Any CPU + {2DB2E605-68E8-47E3-A183-985C1A52B56A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2DB2E605-68E8-47E3-A183-985C1A52B56A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2DB2E605-68E8-47E3-A183-985C1A52B56A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2DB2E605-68E8-47E3-A183-985C1A52B56A}.Release|Any CPU.Build.0 = Release|Any CPU + {A1AB7658-7310-490A-82BE-DAABCC204EA4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A1AB7658-7310-490A-82BE-DAABCC204EA4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A1AB7658-7310-490A-82BE-DAABCC204EA4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A1AB7658-7310-490A-82BE-DAABCC204EA4}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {50A75644-A1C3-4495-9DEB-DBB12D9334B5} = {B44178DF-5B81-4029-90FA-2BF8E2A1EDBF} @@ -79,5 +91,7 @@ Global {B65527CC-218A-4EA3-93DC-985713B5DFF4} = {5C3A014A-7931-4A36-95F0-5EFE15AB06A3} {B2E0156B-B631-4C80-A129-59472D2D0A77} = {5C3A014A-7931-4A36-95F0-5EFE15AB06A3} {27F4FB64-7A51-4315-BDAA-6EE07736C976} = {B44178DF-5B81-4029-90FA-2BF8E2A1EDBF} + {2DB2E605-68E8-47E3-A183-985C1A52B56A} = {5C3A014A-7931-4A36-95F0-5EFE15AB06A3} + {A1AB7658-7310-490A-82BE-DAABCC204EA4} = {B44178DF-5B81-4029-90FA-2BF8E2A1EDBF} EndGlobalSection EndGlobal