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<TConnectionType> 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.
This commit is contained in:
Kyle 2024-07-11 20:11:33 -04:00
parent 7482f08b4f
commit 0b4f638cb9
No known key found for this signature in database
GPG Key ID: 067C5D2D7C62E0F8
14 changed files with 197 additions and 49 deletions

View File

@ -36,7 +36,7 @@ jobs:
- run: dotnet restore - run: dotnet restore
- run: dotnet build --no-restore -c Release -p:Version=${{ steps.generate-version.outputs.RELEASE_VERSION_NUMBER }} - 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: 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: env:
GH_TOKEN: ${{ github.token }} GH_TOKEN: ${{ github.token }}
- name: Publish to NuGet - 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.${{ 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.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.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

5
Directory.Build.props Normal file
View File

@ -0,0 +1,5 @@
<Project>
<PropertyGroup>
<DisableTransitiveProjectReferences>true</DisableTransitiveProjectReferences>
</PropertyGroup>
</Project>

View File

@ -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<ReadWrite> CreateConnection()
{
var nonTxConnection = _serviceProvider.GetRequiredService<INonTransactionalDbConnection<ReadWrite>>();
return nonTxConnection;
}
public INonTransactionalDbConnection<ReadOnly> CreateReadOnlyConnection()
{
var nonTxConnection = _serviceProvider.GetRequiredService<INonTransactionalDbConnection<ReadOnly>>();
return nonTxConnection;
}
}

View File

@ -22,7 +22,6 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Dapper" Version="2.1.35" /> <PackageReference Include="Dapper" Version="2.1.35" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.1" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -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<IServiceProvider, INonTransactionalDbConnection<ReadWrite>> readWriteConnectionImplementationFactory,
Func<IServiceProvider, INonTransactionalDbConnection<ReadOnly>> readOnlyConnectionImplementationFactory
)
{
services.AddTransient<INonTransactionalDbConnection<ReadWrite>>(readWriteConnectionImplementationFactory);
services.AddTransient<INonTransactionalDbConnection<ReadOnly>>(readOnlyConnectionImplementationFactory);
services.AddSingleton<IDbConnectionFactory, DbConnectionFactory>();
}
}

View File

@ -0,0 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<Authors>Kyle Ratti</Authors>
<RepositoryUrl>https://github.com/kyleratti/FruityFoundation</RepositoryUrl>
<Version>1.12.2</Version>
<PackageRequireLicenseAcceptance>true</PackageRequireLicenseAcceptance>
<PackageLicenseFile>LICENSE</PackageLicenseFile>
<TargetFrameworks>net8.0;net6.0</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<None Include="..\LICENSE" Pack="true" PackagePath="" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\FruityFoundation.DataAccess.Abstractions\FruityFoundation.DataAccess.Abstractions.csproj" />
<ProjectReference Include="..\FruityFoundation.DataAccess.Core\FruityFoundation.DataAccess.Core.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Data.Sqlite" Version="8.0.6" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.1" />
</ItemGroup>
</Project>

View File

@ -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<IServiceProvider, string> getReadWriteConnectionString,
Func<IServiceProvider, string> getReadOnlyConnectionString
)
{
services.AddScoped<INonTransactionalDbConnection<ReadWrite>>(serviceProvider =>
{
var connectionFactory = serviceProvider.GetRequiredService<IDbConnectionFactory>();
return connectionFactory.CreateConnection();
});
services.AddScoped<INonTransactionalDbConnection<ReadOnly>>(serviceProvider =>
{
var connectionFactory = serviceProvider.GetRequiredService<IDbConnectionFactory>();
return connectionFactory.CreateReadOnlyConnection();
});
services.AddSingleton<IDbConnectionFactory, SqliteDbConnectionFactory>(serviceProvider =>
new SqliteDbConnectionFactory(
serviceProvider,
getReadOnlyConnectionString: getReadOnlyConnectionString,
getReadWriteConnectionString: getReadWriteConnectionString));
}
}

View File

@ -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<IServiceProvider, string> _getReadWriteConnectionString;
private readonly Func<IServiceProvider, string> _getReadOnlyConnectionString;
public SqliteDbConnectionFactory(
IServiceProvider serviceProvider,
Func<IServiceProvider, string> getReadWriteConnectionString,
Func<IServiceProvider, string> getReadOnlyConnectionString)
{
_serviceProvider = serviceProvider;
_getReadWriteConnectionString = getReadWriteConnectionString;
_getReadOnlyConnectionString = getReadOnlyConnectionString;
}
/// <inheritdoc />
public INonTransactionalDbConnection<ReadWrite> CreateConnection()
{
var connectionString = _getReadWriteConnectionString(_serviceProvider);
var connection = new SqliteConnection(connectionString);
return new NonTransactionalDbConnection<ReadWrite>(connection);
}
/// <inheritdoc />
public INonTransactionalDbConnection<ReadOnly> CreateReadOnlyConnection()
{
var connectionString = _getReadOnlyConnectionString(_serviceProvider);
var connection = new SqliteConnection(connectionString);
return new NonTransactionalDbConnection<ReadOnly>(connection);
}
}

View File

@ -26,6 +26,7 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\FruityFoundation.DataAccess.Abstractions.FSharp\FruityFoundation.DataAccess.Abstractions.FSharp.fsproj" /> <ProjectReference Include="..\FruityFoundation.DataAccess.Abstractions.FSharp\FruityFoundation.DataAccess.Abstractions.FSharp.fsproj" />
<ProjectReference Include="..\FruityFoundation.DataAccess.Abstractions\FruityFoundation.DataAccess.Abstractions.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -0,0 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.0"/>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0"/>
<PackageReference Include="NUnit" Version="3.14.0"/>
<PackageReference Include="NUnit.Analyzers" Version="3.9.0"/>
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0"/>
</ItemGroup>
<ItemGroup>
<Using Include="NUnit.Framework"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\FruityFoundation.DataAccess.Abstractions\FruityFoundation.DataAccess.Abstractions.csproj" />
<ProjectReference Include="..\FruityFoundation.DataAccess.Sqlite\FruityFoundation.DataAccess.Sqlite.csproj" />
</ItemGroup>
</Project>

View File

@ -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<INonTransactionalDbConnection<ReadWrite>>());
}
[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<INonTransactionalDbConnection<ReadOnly>>());
}
}

View File

@ -30,6 +30,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Base\Base.csproj" />
<ProjectReference Include="..\FsBase\FsBase.fsproj" /> <ProjectReference Include="..\FsBase\FsBase.fsproj" />
</ItemGroup> </ItemGroup>

View File

@ -28,6 +28,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Base\Base.csproj" />
<ProjectReference Include="..\FsBase\FsBase.fsproj" /> <ProjectReference Include="..\FsBase\FsBase.fsproj" />
</ItemGroup> </ItemGroup>

View File

@ -24,6 +24,10 @@ Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "FruityFoundation.DataAccess
EndProject 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}" 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 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 Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU 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}.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.ActiveCfg = Release|Any CPU
{27F4FB64-7A51-4315-BDAA-6EE07736C976}.Release|Any CPU.Build.0 = 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 EndGlobalSection
GlobalSection(NestedProjects) = preSolution GlobalSection(NestedProjects) = preSolution
{50A75644-A1C3-4495-9DEB-DBB12D9334B5} = {B44178DF-5B81-4029-90FA-2BF8E2A1EDBF} {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} {B65527CC-218A-4EA3-93DC-985713B5DFF4} = {5C3A014A-7931-4A36-95F0-5EFE15AB06A3}
{B2E0156B-B631-4C80-A129-59472D2D0A77} = {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} {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 EndGlobalSection
EndGlobal EndGlobal