Compare commits

...

22 Commits

Author SHA1 Message Date
Kyle Ratti
6df46d3c2b
fix: specify generic
Some checks failed
ci / build (6.0.x) (push) Has been cancelled
ci / build (8.0.x) (push) Has been cancelled
2024-09-05 23:22:02 -04:00
github-actions[bot]
7eed044713 chore(release): 2.1.0 2024-09-06 03:06:16 +00:00
Kyle Ratti
ad62629a1d
feat: add F# tryQueryFirst 2024-09-05 23:03:43 -04:00
Kyle Ratti
4dbba5c2f3
feat: add FirstOrEmptyAsync 2024-09-05 22:53:48 -04:00
github-actions[bot]
e8e0ea8f60 chore(release): 2.0.0 2024-07-12 00:16:40 +00:00
Kyle
3d2c2524c1
fix: set new package to same version number 2024-07-11 20:14:51 -04:00
Kyle
0b4f638cb9
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.
2024-07-11 20:12:07 -04:00
github-actions[bot]
7482f08b4f chore(release): 1.12.1 2024-07-06 03:07:00 +00:00
Kyle
2fd363d4b4
deps: bump deps 2024-07-05 23:06:22 -04:00
github-actions[bot]
bf02139d64 chore(release): 1.12.0 2024-07-06 02:37:45 +00:00
Kyle
d745706082
feat: add FSharp database abstraction layer
I don't love having separate modules for ReadOnly vs. ReadWrite connections, but F# doesn't support covariance, so this is the best I could come up with for now.
2024-07-05 22:35:45 -04:00
github-actions[bot]
44b978309a chore(release): 1.11.0 2024-07-06 00:06:32 +00:00
Kyle
6cbccd3d7d
feat: add data access DI helper 2024-07-05 20:03:12 -04:00
github-actions[bot]
d079f12b16 chore(release): 1.10.0 2024-06-24 02:46:18 +00:00
Kyle Ratti
90c0740778
fix: add missing version node 2024-06-23 22:45:06 -04:00
Kyle Ratti
e5ddf60850
deps: upgrade deps 2024-06-23 22:41:02 -04:00
Kyle Ratti
df73060f99
ci: include data access in publish 2024-06-23 22:41:02 -04:00
Kyle Ratti
f169ca3287
fix: include license file 2024-06-23 22:29:43 -04:00
Kyle Ratti
1266932d4e
feat: make data access layer generic 2024-06-23 22:27:05 -04:00
Kyle Ratti
2242fe8733
feat: multi target .NET 6 and .NET 8 2024-06-23 18:27:45 -04:00
Kyle Ratti
2e2dc40cfb
test: DbConnectionFactory 2024-06-23 18:25:03 -04:00
Kyle Ratti
de8e30dbb4
feat: initial port of data access 2024-06-23 18:15:32 -04:00
33 changed files with 1481 additions and 10 deletions

View File

@ -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"
- 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
@ -45,3 +45,7 @@ jobs:
dotnet nuget push "FruityFoundation.Base.${{ 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.FsBase.${{ 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.Db.${{ 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.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

View File

@ -10,8 +10,8 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FakeItEasy" Version="8.2.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
<PackageReference Include="FakeItEasy" Version="8.3.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
<PackageReference Include="NUnit" Version="4.1.0" />
<PackageReference Include="NUnit.Analyzers" Version="4.2.0">
<PrivateAssets>all</PrivateAssets>

View File

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Threading.Tasks;
using FruityFoundation.Base.Structures;
using NUnit.Framework;
@ -51,6 +52,86 @@ public class MaybeExtensionTests
Assert.That(result.HasValue, Is.False);
}
[Test]
public async Task Enumerable_FirstOrEmptyAsync_WithEmptyEnumerable_ReturnsEmptyMaybe()
{
// Arrange
var data = ToAsyncEnumerable(Array.Empty<int>());
// Act
var result = await data.FirstOrEmptyAsync();
// Assert
Assert.That(result, Is.InstanceOf<Maybe<int>>());
Assert.That(result.HasValue, Is.False);
}
[Test]
public async Task Enumerable_FirstOrEmptyAsync_WithMatchingPredicate_ReturnsMaybeWithValue()
{
// Arrange
var data = ToAsyncEnumerable([1, 2, 3, 4]);
// Act
var result = await data.FirstOrEmptyAsync(x => x > 1);
// Assert
Assert.That(result, Is.InstanceOf<Maybe<int>>());
Assert.That(result.HasValue, Is.True);
Assert.That(result.Value, Is.EqualTo(2));
}
[Test]
public async Task Enumerable_FirstOrEmptyAsync_WithMatchingAsyncPredicate_ReturnsMaybeWithValue()
{
// Arrange
var data = ToAsyncEnumerable([1, 2, 3, 4]);
// Act
var result = await data.FirstOrEmptyAsync(async x =>
{
await Task.Yield();
return x > 1;
});
// Assert
Assert.That(result, Is.InstanceOf<Maybe<int>>());
Assert.That(result.HasValue, Is.True);
Assert.That(result.Value, Is.EqualTo(2));
}
[Test]
public async Task Enumerable_FirstOrEmptyAsync_WithNonMatchingPredicate_ReturnsEmptyMaybe()
{
// Arrange
var data = ToAsyncEnumerable([1, 2, 3, 4]);
// Act
var result = await data.FirstOrEmptyAsync(x => x > 100);
// Assert
Assert.That(result, Is.InstanceOf<Maybe<int>>());
Assert.That(result.HasValue, Is.False);
}
[Test]
public async Task Enumerable_FirstOrEmptyAsync_WithNonMatchingAsyncPredicate_ReturnsEmptyMaybe()
{
// Arrange
var data = ToAsyncEnumerable([1, 2, 3, 4]);
// Act
var result = await data.FirstOrEmptyAsync(async x =>
{
await Task.Yield();
return x > 100;
});
// Assert
Assert.That(result, Is.InstanceOf<Maybe<int>>());
Assert.That(result.HasValue, Is.False);
}
[Test]
public void MaybeNullableTests()
{
@ -125,4 +206,12 @@ public class MaybeExtensionTests
Assert.That(result, Is.InstanceOf<Maybe<int>>());
Assert.That(result.HasValue, Is.False);
}
private static async IAsyncEnumerable<T> ToAsyncEnumerable<T>(IEnumerable<T> enumerable)
{
foreach (var item in enumerable)
yield return item;
await Task.Yield();
}
}

View File

@ -12,7 +12,7 @@
<Company />
<Product>FruityFoundation.Base</Product>
<RepositoryUrl>https://github.com/kyleratti/FruityFoundation</RepositoryUrl>
<Version>1.9.0</Version>
<Version>2.1.0</Version>
<PackageRequireLicenseAcceptance>true</PackageRequireLicenseAcceptance>
<PackageLicenseFile>LICENSE</PackageLicenseFile>
<AssemblyName>FruityFoundation.Base</AssemblyName>

View File

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace FruityFoundation.Base.Structures;
@ -22,6 +23,64 @@ public static class MaybeExtensions
return Maybe.Empty<T>();
}
public static async ValueTask<Maybe<T>> FirstOrEmptyAsync<T>(this IAsyncEnumerable<T> source, CancellationToken cancellationToken = default)
{
if (source is IList<T> { Count: > 0 } list)
return Maybe.Create(list[0]);
await using var e = source
.ConfigureAwait(false)
.WithCancellation(cancellationToken)
.GetAsyncEnumerator();
if (await e.MoveNextAsync())
return e.Current;
return Maybe.Empty<T>();
}
public static async ValueTask<Maybe<T>> FirstOrEmptyAsync<T>(this IAsyncEnumerable<T> source, Func<T, bool> predicate, CancellationToken cancellationToken = default)
{
if (source is IList<T> { Count: > 0 } list)
return list[0];
await using var e = source
.ConfigureAwait(false)
.WithCancellation(cancellationToken)
.GetAsyncEnumerator();
while (await e.MoveNextAsync())
{
var value = e.Current;
if (predicate(value))
return value;
}
return Maybe.Empty<T>();
}
public static async ValueTask<Maybe<T>> FirstOrEmptyAsync<T>(this IAsyncEnumerable<T> source, Func<T, ValueTask<bool>> asyncPredicate, CancellationToken cancellationToken = default)
{
if (source is IList<T> { Count: > 0 } list)
return list[0];
await using var e = source
.ConfigureAwait(false)
.WithCancellation(cancellationToken)
.GetAsyncEnumerator();
while (await e.MoveNextAsync())
{
var value = e.Current;
if (await asyncPredicate(value).ConfigureAwait(false))
return value;
}
return Maybe.Empty<T>();
}
public static Maybe<TValue> TryGetValue<TKey, TValue>(this IDictionary<TKey, TValue> dict, TKey key) =>
dict.TryGetValue(key, out var value) ? Maybe.Create(value) : Maybe.Empty<TValue>();

View File

@ -1,4 +1,58 @@
Changelog
<a name="2.1.0"></a>
## [2.1.0](https://www.github.com/kyleratti/FruityFoundation/releases/tag/v2.1.0) (2024-09-06)
### Features
* add F# tryQueryFirst ([ad62629](https://www.github.com/kyleratti/FruityFoundation/commit/ad62629a1d92248ce87b125b7b960dca215ce44b))
* add FirstOrEmptyAsync ([4dbba5c](https://www.github.com/kyleratti/FruityFoundation/commit/4dbba5c2f3d90ad1709d91aee13d32b9cb672835))
<a name="2.0.0"></a>
## [2.0.0](https://www.github.com/kyleratti/FruityFoundation/releases/tag/v2.0.0) (2024-07-12)
### Features
* factor connection factory into Sqlite package ([0b4f638](https://www.github.com/kyleratti/FruityFoundation/commit/0b4f638cb98568dcbb92baa1a4d76d5f91f6316b))
### Bug Fixes
* set new package to same version number ([3d2c252](https://www.github.com/kyleratti/FruityFoundation/commit/3d2c2524c1c61546e7ae51ce6a6893cebf91e1ee))
### Breaking Changes
* factor connection factory into Sqlite package ([0b4f638](https://www.github.com/kyleratti/FruityFoundation/commit/0b4f638cb98568dcbb92baa1a4d76d5f91f6316b))
<a name="1.12.1"></a>
## [1.12.1](https://www.github.com/kyleratti/FruityFoundation/releases/tag/v1.12.1) (2024-07-06)
<a name="1.12.0"></a>
## [1.12.0](https://www.github.com/kyleratti/FruityFoundation/releases/tag/v1.12.0) (2024-07-06)
### Features
* add FSharp database abstraction layer ([d745706](https://www.github.com/kyleratti/FruityFoundation/commit/d7457060829e897c726f5d7522f56bfac11b8e9a))
<a name="1.11.0"></a>
## [1.11.0](https://www.github.com/kyleratti/FruityFoundation/releases/tag/v1.11.0) (2024-07-06)
### Features
* add data access DI helper ([6cbccd3](https://www.github.com/kyleratti/FruityFoundation/commit/6cbccd3d7dd8308bd6e65fd28362f36c61dedd5e))
<a name="1.10.0"></a>
## [1.10.0](https://www.github.com/kyleratti/FruityFoundation/releases/tag/v1.10.0) (2024-06-24)
### Features
* initial port of data access ([de8e30d](https://www.github.com/kyleratti/FruityFoundation/commit/de8e30dbb41d8d70b685324847b07f7800e31488))
* make data access layer generic ([1266932](https://www.github.com/kyleratti/FruityFoundation/commit/1266932d4e5f76adb6a181234e644a74da273fa0))
* multi target .NET 6 and .NET 8 ([2242fe8](https://www.github.com/kyleratti/FruityFoundation/commit/2242fe8733f808fb6055baa14cf28123a31fa9dc))
### Bug Fixes
* add missing version node ([90c0740](https://www.github.com/kyleratti/FruityFoundation/commit/90c074077806f0e470081463480a716bcf552b0c))
* include license file ([f169ca3](https://www.github.com/kyleratti/FruityFoundation/commit/f169ca3287116ee17c5e32485af8a9b9107603b8))
<a name="1.9.0"></a>
## [1.9.0](https://www.github.com/kyleratti/FruityFoundation/releases/tag/v1.9.0) (2024-05-03)

View File

@ -10,7 +10,7 @@
<Company />
<Product>FruityFoudnation.Db</Product>
<RepositoryUrl>https://github.com/kyleratti/FruityFoundation</RepositoryUrl>
<Version>1.9.0</Version>
<Version>2.1.0</Version>
<PackageRequireLicenseAcceptance>true</PackageRequireLicenseAcceptance>
<PackageLicenseFile>LICENSE</PackageLicenseFile>
<TargetFrameworks>net6.0;net8.0</TargetFrameworks>
@ -24,4 +24,8 @@
<Compile Include="Db.fs" />
</ItemGroup>
<ItemGroup>
<PackageReference Update="FSharp.Core" Version="8.0.300" />
</ItemGroup>
</Project>

5
Directory.Build.props Normal file
View File

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

View File

@ -0,0 +1,34 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<Version>2.1.0</Version>
<Authors>Kyle Ratti</Authors>
<RepositoryUrl>https://github.com/kyleratti/FruityFoundation</RepositoryUrl>
<PackageRequireLicenseAcceptance>true</PackageRequireLicenseAcceptance>
<PackageLicenseFile>LICENSE</PackageLicenseFile>
</PropertyGroup>
<ItemGroup>
<None Include="..\LICENSE" Pack="true" PackagePath="" />
</ItemGroup>
<ItemGroup>
<Compile Include="ReadOnlyDb.fs" />
<Compile Include="ReadWriteDb.fs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Base\Base.csproj" />
<ProjectReference Include="..\FruityFoundation.DataAccess.Abstractions\FruityFoundation.DataAccess.Abstractions.csproj" />
<ProjectReference Include="..\FsBase\FsBase.fsproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="FSharp.Control.TaskSeq" Version="0.4.0" />
<PackageReference Update="FSharp.Core" Version="8.0.300" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,41 @@
[<RequireQualifiedAccess>]
module FruityFoundation.DataAccess.Abstractions.FSharp.ReadOnlyDb
open System.Collections.Generic
open System.Threading
open FSharp.Control
open FruityFoundation.DataAccess.Abstractions
open FruityFoundation.FsBase
let private toKeyValuePair (parms : (string * obj) seq) =
parms
|> Seq.map (fun (name, value) -> KeyValuePair(name, value))
let query<'a> (connection : IDatabaseConnection<ReadOnly>) (cancellationToken : CancellationToken) (sql : string) (parms : (string * obj) seq) = task {
return! connection.Query<'a>(sql, parms |> toKeyValuePair, cancellationToken)
}
let queryUnbuffered<'a> (connection : IDatabaseConnection<ReadOnly>) (cancellationToken : CancellationToken) (sql : string) (parms : (string * obj) seq) = taskSeq {
yield! connection.QueryUnbuffered<'a>(sql, parms |> toKeyValuePair, cancellationToken)
}
let querySingle<'a> (connection : IDatabaseConnection<ReadOnly>) (cancellationToken : CancellationToken) (sql : string) (parms : (string * obj) seq) = task {
return! connection.QuerySingle<'a>(sql, parms |> toKeyValuePair, cancellationToken)
}
let tryQueryFirst<'a> (connection : ReadOnly IDatabaseConnection) (cancellationToken : CancellationToken) (sql : string) (parms : (string * obj) seq) = task {
let! result = connection.TryQueryFirst<'a>(sql, parms |> toKeyValuePair, cancellationToken)
return result |> Option.fromMaybe
}
let execute (connection : IDatabaseConnection<ReadOnly>) (cancellationToken : CancellationToken) (sql : string) (parms : (string * obj) seq) = task {
return! connection.Execute(sql, parms |> toKeyValuePair, cancellationToken)
}
let executeScalar<'a> (connection : IDatabaseConnection<ReadOnly>) (cancellationToken : CancellationToken) (sql : string) (parms : (string * obj) seq) = task {
return! connection.ExecuteScalar<'a>(sql, parms |> toKeyValuePair, cancellationToken)
}
let executeReader (connection : IDatabaseConnection<ReadOnly>) (cancellationToken : CancellationToken) (sql : string) (parms : (string * obj) seq) = task {
return! connection.ExecuteReader(sql, parms |> toKeyValuePair, cancellationToken)
}

View File

@ -0,0 +1,41 @@
[<RequireQualifiedAccess>]
module FruityFoundation.DataAccess.Abstractions.FSharp.ReadWriteDb
open System.Collections.Generic
open System.Threading
open FSharp.Control
open FruityFoundation.DataAccess.Abstractions
open FruityFoundation.FsBase
let private toKeyValuePair (parms : (string * obj) seq) =
parms
|> Seq.map (fun (name, value) -> KeyValuePair(name, value))
let query<'a> (connection : IDatabaseConnection<ReadWrite>) (cancellationToken : CancellationToken) (sql : string) (parms : (string * obj) seq) = task {
return! connection.Query<'a>(sql, parms |> toKeyValuePair, cancellationToken)
}
let queryUnbuffered<'a> (connection : IDatabaseConnection<ReadWrite>) (cancellationToken : CancellationToken) (sql : string) (parms : (string * obj) seq) = taskSeq {
yield! connection.QueryUnbuffered<'a>(sql, parms |> toKeyValuePair, cancellationToken)
}
let querySingle<'a> (connection : IDatabaseConnection<ReadWrite>) (cancellationToken : CancellationToken) (sql : string) (parms : (string * obj) seq) = task {
return! connection.QuerySingle<'a>(sql, parms |> toKeyValuePair, cancellationToken)
}
let tryQueryFirst<'a> (connection : ReadOnly IDatabaseConnection) (cancellationToken : CancellationToken) (sql : string) (parms : (string * obj) seq) = task {
let! result = connection.TryQueryFirst<'a>(sql, parms |> toKeyValuePair, cancellationToken)
return result |> Option.fromMaybe
}
let execute (connection : IDatabaseConnection<ReadWrite>) (cancellationToken : CancellationToken) (sql : string) (parms : (string * obj) seq) = task {
return! connection.Execute(sql, parms |> toKeyValuePair, cancellationToken)
}
let executeScalar<'a> (connection : IDatabaseConnection<ReadWrite>) (cancellationToken : CancellationToken) (sql : string) (parms : (string * obj) seq) = task {
return! connection.ExecuteScalar<'a>(sql, parms |> toKeyValuePair, cancellationToken)
}
let executeReader (connection : IDatabaseConnection<ReadWrite>) (cancellationToken : CancellationToken) (sql : string) (parms : (string * obj) seq) = task {
return! connection.ExecuteReader(sql, parms |> toKeyValuePair, cancellationToken)
}

View File

@ -0,0 +1,7 @@
namespace FruityFoundation.DataAccess.Abstractions;
public abstract class ConnectionType { }
public abstract class ReadOnly : ConnectionType { }
public abstract class ReadWrite : ReadOnly { }

View File

@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<TargetFrameworks>net8.0;net6.0</TargetFrameworks>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<Authors>Kyle Ratti</Authors>
<RepositoryUrl>https://github.com/kyleratti/FruityFoundation</RepositoryUrl>
<Version>2.1.0</Version>
<PackageRequireLicenseAcceptance>true</PackageRequireLicenseAcceptance>
<PackageLicenseFile>LICENSE</PackageLicenseFile>
</PropertyGroup>
<ItemGroup>
<None Include="..\LICENSE" Pack="true" PackagePath="" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Base\Base.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,16 @@
using System.Data.Common;
using FruityFoundation.Base.Structures;
namespace FruityFoundation.DataAccess.Abstractions;
// ReSharper disable once UnusedTypeParameter
public interface IDatabaseConnection<out TConnectionType> where TConnectionType : ConnectionType
{
public Task<IEnumerable<T>> Query<T>(string sql, object? param = null, CancellationToken cancellationToken = default);
public IAsyncEnumerable<T> QueryUnbuffered<T>(string sql, object? param = null, CancellationToken cancellationToken = default);
public Task<T> QuerySingle<T>(string sql, object? param = null, CancellationToken cancellationToken = default);
public Task<Maybe<T>> TryQueryFirst<T>(string sql, object? param = null, CancellationToken cancellationToken = default);
public Task Execute(string sql, object? param = null, CancellationToken cancellationToken = default);
public Task<T?> ExecuteScalar<T>(string sql, object? param = null, CancellationToken cancellationToken = default);
public Task<DbDataReader> ExecuteReader(string sql, object? param = null, CancellationToken cancellationToken = default);
}

View File

@ -0,0 +1,8 @@
namespace FruityFoundation.DataAccess.Abstractions;
public interface IDatabaseTransactionConnection<out TConnectionType> : IDatabaseConnection<TConnectionType>, IDisposable, IAsyncDisposable
where TConnectionType : ConnectionType
{
public Task Commit(CancellationToken cancellationToken);
public Task Rollback(CancellationToken cancellationToken);
}

View File

@ -0,0 +1,7 @@
namespace FruityFoundation.DataAccess.Abstractions;
public interface IDbConnectionFactory
{
public INonTransactionalDbConnection<ReadWrite> CreateConnection();
public INonTransactionalDbConnection<ReadOnly> CreateReadOnlyConnection();
}

View File

@ -0,0 +1,11 @@
using System.Data;
using FruityFoundation.Base.Structures;
namespace FruityFoundation.DataAccess.Abstractions;
public interface INonTransactionalDbConnection<TConnectionType> : IDatabaseConnection<TConnectionType>, IDisposable, IAsyncDisposable
where TConnectionType : ConnectionType
{
public Task<IDatabaseTransactionConnection<TConnectionType>> CreateTransaction(CancellationToken cancellationToken);
public Task<IDatabaseTransactionConnection<TConnectionType>> CreateTransaction(IsolationLevel isolationLevel, CancellationToken cancellationToken);
}

View File

@ -0,0 +1,152 @@
using System.Data.Common;
using System.Runtime.CompilerServices;
using Dapper;
using FruityFoundation.Base.Structures;
using FruityFoundation.DataAccess.Abstractions;
namespace FruityFoundation.DataAccess.Core;
// ReSharper disable once ClassWithVirtualMembersNeverInherited.Global
public class DbTransaction<TConnectionType> : IDatabaseTransactionConnection<TConnectionType>
where TConnectionType : ConnectionType
{
private readonly DbTransaction _transaction;
internal DbTransaction(DbTransaction transaction)
{
_transaction = transaction;
}
/// <inheritdoc />
public async Task<IEnumerable<T>> Query<T>(
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<T>(command);
}
/// <inheritdoc />
public async IAsyncEnumerable<T> QueryUnbuffered<T>(
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<T>(sql, param, transaction: _transaction)
.WithCancellation(cancellationToken);
await foreach (var item in query)
yield return item;
}
/// <inheritdoc />
public async Task<T> QuerySingle<T>(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<T>(command);
}
/// <inheritdoc />
public async Task<Maybe<T>> TryQueryFirst<T>(string sql, object? param = null, CancellationToken cancellationToken = default)
{
if (_transaction.Connection is not { } conn)
throw new InvalidOperationException("Transaction connection cannot be null");
return await conn.QueryUnbufferedAsync<T>(sql, param, transaction: _transaction)
.FirstOrEmptyAsync(cancellationToken);
}
/// <inheritdoc />
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);
}
/// <inheritdoc />
public async Task<T?> ExecuteScalar<T>(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<T>(command);
}
/// <inheritdoc />
public async Task<DbDataReader> 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);
}
/// <inheritdoc />
public async Task Commit(CancellationToken cancellationToken)
{
await _transaction.CommitAsync(cancellationToken);
}
/// <inheritdoc />
public async Task Rollback(CancellationToken cancellationToken)
{
await _transaction.RollbackAsync(cancellationToken);
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
#pragma warning disable IDISP007
_transaction.Dispose();
#pragma warning restore IDISP007
}
}
/// <inheritdoc />
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual async ValueTask DisposeAsyncCore()
{
#pragma warning disable IDISP007
await _transaction.DisposeAsync();
#pragma warning restore IDISP007
}
/// <inheritdoc />
public async ValueTask DisposeAsync()
{
await DisposeAsyncCore();
GC.SuppressFinalize(this);
}
}

View File

@ -0,0 +1,28 @@
<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>2.1.0</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="..\Base\Base.csproj" />
<ProjectReference Include="..\FruityFoundation.DataAccess.Abstractions\FruityFoundation.DataAccess.Abstractions.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Dapper" Version="2.1.35" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,134 @@
using System.Data;
using System.Data.Common;
using System.Runtime.CompilerServices;
using Dapper;
using FruityFoundation.Base.Structures;
using FruityFoundation.DataAccess.Abstractions;
namespace FruityFoundation.DataAccess.Core;
// ReSharper disable once ClassWithVirtualMembersNeverInherited.Global
public class NonTransactionalDbConnection<TConnectionType> : INonTransactionalDbConnection<TConnectionType>
where TConnectionType : ConnectionType
{
private readonly DbConnection _connection;
/// <summary>
/// C'tor
/// </summary>
public NonTransactionalDbConnection(DbConnection connection)
{
_connection = connection;
}
/// <inheritdoc />
public async Task<IEnumerable<T>> Query<T>(
string sql,
object? param = null,
CancellationToken cancellationToken = default
) => await _connection.QueryAsync<T>(new CommandDefinition(sql, param, cancellationToken: cancellationToken));
/// <inheritdoc />
public async IAsyncEnumerable<T> QueryUnbuffered<T>(
string sql,
object? param = null,
[EnumeratorCancellation] CancellationToken cancellationToken = default
)
{
var query = _connection.QueryUnbufferedAsync<T>(sql, param, transaction: null)
.WithCancellation(cancellationToken);
await foreach (var item in query)
yield return item;
}
/// <inheritdoc />
public async Task<T> QuerySingle<T>(
string sql,
object? param = null,
CancellationToken cancellationToken = default
) => await _connection.QuerySingleAsync<T>(new CommandDefinition(sql, param, cancellationToken: cancellationToken));
/// <inheritdoc />
public async Task<Maybe<T>> TryQueryFirst<T>(
string sql,
object? param = null,
CancellationToken cancellationToken = default
) =>
await _connection.QueryUnbufferedAsync<T>(sql, param, transaction: null)
.FirstOrEmptyAsync(cancellationToken);
/// <inheritdoc />
public async Task Execute(
string sql,
object? param = null,
CancellationToken cancellationToken = default
) => await _connection.ExecuteAsync(new CommandDefinition(sql, param, cancellationToken: cancellationToken));
/// <inheritdoc />
public async Task<T?> ExecuteScalar<T>(
string sql,
object? param = null,
CancellationToken cancellationToken = default
) => await _connection.ExecuteScalarAsync<T>(new CommandDefinition(sql, param, cancellationToken: cancellationToken));
/// <inheritdoc />
public async Task<DbDataReader> ExecuteReader(
string sql,
object? param = null,
CancellationToken cancellationToken = default
) => await _connection.ExecuteReaderAsync(new CommandDefinition(sql, param, cancellationToken: cancellationToken));
/// <inheritdoc />
public async Task<IDatabaseTransactionConnection<TConnectionType>> CreateTransaction(CancellationToken cancellationToken)
{
if (!_connection.State.HasFlag(ConnectionState.Open))
await _connection.OpenAsync(cancellationToken);
var tx = await _connection.BeginTransactionAsync(cancellationToken);
return new DbTransaction<TConnectionType>(tx);
}
/// <inheritdoc />
public async Task<IDatabaseTransactionConnection<TConnectionType>> 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<TConnectionType>(tx);
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
#pragma warning disable IDISP007
_connection.Dispose();
#pragma warning restore IDISP007
}
}
/// <inheritdoc />
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual async ValueTask DisposeAsyncCore()
{
#pragma warning disable IDISP007
await _connection.DisposeAsync();
#pragma warning restore IDISP007
}
/// <inheritdoc />
public async ValueTask DisposeAsync()
{
await DisposeAsyncCore();
GC.SuppressFinalize(this);
}
}

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>2.1.0</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

@ -0,0 +1,32 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<IsPackable>false</IsPackable>
<GenerateProgramFile>false</GenerateProgramFile>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<Compile Include="ReadWriteDbTests.fs" />
<Compile Include="ReadOnlyDbTests.fs" />
<Compile Include="Program.fs"/>
</ItemGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.0"/>
<PackageReference Include="FakeItEasy" Version="8.3.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"/>
<PackageReference Update="FSharp.Core" Version="8.0.300" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\FruityFoundation.DataAccess.Abstractions.FSharp\FruityFoundation.DataAccess.Abstractions.FSharp.fsproj" />
<ProjectReference Include="..\FruityFoundation.DataAccess.Abstractions\FruityFoundation.DataAccess.Abstractions.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,4 @@
module Program =
[<EntryPoint>]
let main _ = 0

View File

@ -0,0 +1,249 @@
module FruityFoundation.Tests.DataAccess.Abstractions.FSharp.ReadOnlyDbTests
open System.Collections.Generic
open System.Threading
open FSharp.Control
open FakeItEasy
open FruityFoundation.DataAccess.Abstractions
open FruityFoundation.DataAccess.Abstractions.FSharp
open NUnit.Framework
let fakeDbConnection : IDatabaseConnection<ReadOnly> = A.Fake<IDatabaseConnection<ReadOnly>> ()
[<Test>]
let Db_Query_Calls_IDatabaseConnection_Query_NoParms () = task {
// Arrange
let sql = "SELECT 1 FROM table"
let parms = Array.empty
// Act
let! _ = task {
return! (sql, parms) ||> ReadOnlyDb.query fakeDbConnection CancellationToken.None
}
// Assert
A.CallTo(fun () -> fakeDbConnection.Query(
"SELECT 1 FROM table",
A<IEnumerable<KeyValuePair<string, obj>>>.That.Matches(Seq.isEmpty),
CancellationToken.None)).MustHaveHappenedOnceExactly ()
|> ignore
}
[<Test>]
let Db_Query_Calls_IDatabaseConnection_Query_WithParms () = task {
// Arrange
let sql = "SELECT 1 FROM table"
let parms = [| ("@id", box 1) |]
// Act
let! result = task {
return! (sql, parms) ||> ReadOnlyDb.query fakeDbConnection CancellationToken.None
}
// Assert
A.CallTo(fun () -> fakeDbConnection.Query(
"SELECT 1 FROM table",
A<IEnumerable<KeyValuePair<string, obj>>>.That.Matches(fun x ->
Seq.length x = 1 && Seq.head x = KeyValuePair("@id", box 1)),
CancellationToken.None)).MustHaveHappenedOnceExactly ()
|> ignore
}
[<Test>]
let Db_QueryUnbuffered_Calls_IDatabaseConnection_Query_NoParms () = task {
// Arrange
let sql = "SELECT 1 FROM table"
let parms = Array.empty
// Act
let! _ = task {
return! (sql, parms)
||> ReadOnlyDb.queryUnbuffered fakeDbConnection CancellationToken.None
|> TaskSeq.toArrayAsync
}
// Assert
A.CallTo(fun () -> fakeDbConnection.QueryUnbuffered(
"SELECT 1 FROM table",
A<IEnumerable<KeyValuePair<string, obj>>>.That.Matches(Seq.isEmpty),
CancellationToken.None)).MustHaveHappenedOnceExactly ()
|> ignore
}
[<Test>]
let Db_QueryUnbuffered_Calls_IDatabaseConnection_Query_WithParms () = task {
// Arrange
let sql = "SELECT 1 FROM table"
let parms = [| ("@id", box 1) |]
// Act
let! result = task {
return! (sql, parms)
||> ReadOnlyDb.queryUnbuffered fakeDbConnection CancellationToken.None
|> TaskSeq.toArrayAsync
}
// Assert
A.CallTo(fun () -> fakeDbConnection.QueryUnbuffered(
"SELECT 1 FROM table",
A<IEnumerable<KeyValuePair<string, obj>>>.That.Matches(fun x ->
Seq.length x = 1 && Seq.head x = KeyValuePair("@id", box 1)),
CancellationToken.None)).MustHaveHappenedOnceExactly ()
|> ignore
}
[<Test>]
let Db_QuerySingle_Calls_IDatabaseConnection_QuerySingle_NoParms () = task {
// Arrange
let sql = "SELECT 1 FROM table"
let parms = Array.empty
// Act
let! _ = task {
return! (sql, parms) ||> ReadOnlyDb.querySingle fakeDbConnection CancellationToken.None
}
// Assert
A.CallTo(fun () -> fakeDbConnection.QuerySingle(
"SELECT 1 FROM table",
A<IEnumerable<KeyValuePair<string, obj>>>.That.Matches(Seq.isEmpty),
CancellationToken.None)).MustHaveHappenedOnceExactly ()
|> ignore
}
[<Test>]
let Db_QuerySingle_Calls_IDatabaseConnection_QuerySingle_WithParms () = task {
// Arrange
let sql = "SELECT 1 FROM table"
let parms = [| ("@id", box 1) |]
// Act
let! result = task {
return! (sql, parms) ||> ReadOnlyDb.querySingle fakeDbConnection CancellationToken.None
}
// Assert
A.CallTo(fun () -> fakeDbConnection.QuerySingle(
"SELECT 1 FROM table",
A<IEnumerable<KeyValuePair<string, obj>>>.That.Matches(fun x ->
Seq.length x = 1 && Seq.head x = KeyValuePair("@id", box 1)),
CancellationToken.None)).MustHaveHappenedOnceExactly ()
|> ignore
}
[<Test>]
let Db_Execute_Calls_IDatabaseConnection_Execute_NoParms () = task {
// Arrange
let sql = "SELECT 1 FROM table"
let parms = Array.empty
// Act
let! _ = task {
return! (sql, parms) ||> ReadOnlyDb.execute fakeDbConnection CancellationToken.None
}
// Assert
A.CallTo(fun () -> fakeDbConnection.Execute(
"SELECT 1 FROM table",
A<IEnumerable<KeyValuePair<string, obj>>>.That.Matches(Seq.isEmpty),
CancellationToken.None)).MustHaveHappenedOnceExactly ()
|> ignore
}
[<Test>]
let Db_Execute_Calls_IDatabaseConnection_Execute_WithParms () = task {
// Arrange
let sql = "SELECT 1 FROM table"
let parms = [| ("@id", box 1) |]
// Act
let! result = task {
return! (sql, parms) ||> ReadOnlyDb.execute fakeDbConnection CancellationToken.None
}
// Assert
A.CallTo(fun () -> fakeDbConnection.Execute(
"SELECT 1 FROM table",
A<IEnumerable<KeyValuePair<string, obj>>>.That.Matches(fun x ->
Seq.length x = 1 && Seq.head x = KeyValuePair("@id", box 1)),
CancellationToken.None)).MustHaveHappenedOnceExactly ()
|> ignore
}
[<Test>]
let Db_ExecuteScalar_Calls_IDatabaseConnection_ExecuteScalar_NoParms () = task {
// Arrange
let sql = "SELECT 1 FROM table"
let parms = Array.empty
// Act
let! _ = task {
return! (sql, parms) ||> ReadOnlyDb.executeScalar fakeDbConnection CancellationToken.None
}
// Assert
A.CallTo(fun () -> fakeDbConnection.ExecuteScalar(
"SELECT 1 FROM table",
A<IEnumerable<KeyValuePair<string, obj>>>.That.Matches(Seq.isEmpty),
CancellationToken.None)).MustHaveHappenedOnceExactly ()
|> ignore
}
[<Test>]
let Db_ExecuteScalar_Calls_IDatabaseConnection_ExecuteScalar_WithParms () = task {
// Arrange
let sql = "SELECT 1 FROM table"
let parms = [| ("@id", box 1) |]
// Act
let! result = task {
return! (sql, parms) ||> ReadOnlyDb.executeScalar fakeDbConnection CancellationToken.None
}
// Assert
A.CallTo(fun () -> fakeDbConnection.ExecuteScalar(
"SELECT 1 FROM table",
A<IEnumerable<KeyValuePair<string, obj>>>.That.Matches(fun x ->
Seq.length x = 1 && Seq.head x = KeyValuePair("@id", box 1)),
CancellationToken.None)).MustHaveHappenedOnceExactly ()
|> ignore
}
[<Test>]
let Db_ExecuteReader_Calls_IDatabaseConnection_ExecuteReader_NoParms () = task {
// Arrange
let sql = "SELECT 1 FROM table"
let parms = Array.empty
// Act
let! _ = task {
return! (sql, parms) ||> ReadOnlyDb.executeReader fakeDbConnection CancellationToken.None
}
// Assert
A.CallTo(fun () -> fakeDbConnection.ExecuteReader(
"SELECT 1 FROM table",
A<IEnumerable<KeyValuePair<string, obj>>>.That.Matches(Seq.isEmpty),
CancellationToken.None)).MustHaveHappenedOnceExactly ()
|> ignore
}
[<Test>]
let Db_ExecuteReader_Calls_IDatabaseConnection_ExecuteReader_WithParms () = task {
// Arrange
let sql = "SELECT 1 FROM table"
let parms = [| ("@id", box 1) |]
// Act
let! result = task {
return! (sql, parms) ||> ReadOnlyDb.executeReader fakeDbConnection CancellationToken.None
}
// Assert
A.CallTo(fun () -> fakeDbConnection.ExecuteReader(
"SELECT 1 FROM table",
A<IEnumerable<KeyValuePair<string, obj>>>.That.Matches(fun x ->
Seq.length x = 1 && Seq.head x = KeyValuePair("@id", box 1)),
CancellationToken.None)).MustHaveHappenedOnceExactly ()
|> ignore
}

View File

@ -0,0 +1,249 @@
module FruityFoundation.Tests.DataAccess.Abstractions.FSharp.ReadWriteDbTests
open System.Collections.Generic
open System.Threading
open FSharp.Control
open FakeItEasy
open FruityFoundation.DataAccess.Abstractions
open FruityFoundation.DataAccess.Abstractions.FSharp
open NUnit.Framework
let fakeDbConnection : IDatabaseConnection<ReadWrite> = A.Fake<IDatabaseConnection<ReadWrite>> ()
[<Test>]
let Db_Query_Calls_IDatabaseConnection_Query_NoParms () = task {
// Arrange
let sql = "SELECT 1 FROM table"
let parms = Array.empty
// Act
let! _ = task {
return! (sql, parms) ||> ReadWriteDb.query fakeDbConnection CancellationToken.None
}
// Assert
A.CallTo(fun () -> fakeDbConnection.Query(
"SELECT 1 FROM table",
A<IEnumerable<KeyValuePair<string, obj>>>.That.Matches(Seq.isEmpty),
CancellationToken.None)).MustHaveHappenedOnceExactly ()
|> ignore
}
[<Test>]
let Db_Query_Calls_IDatabaseConnection_Query_WithParms () = task {
// Arrange
let sql = "SELECT 1 FROM table"
let parms = [| ("@id", box 1) |]
// Act
let! result = task {
return! (sql, parms) ||> ReadWriteDb.query fakeDbConnection CancellationToken.None
}
// Assert
A.CallTo(fun () -> fakeDbConnection.Query(
"SELECT 1 FROM table",
A<IEnumerable<KeyValuePair<string, obj>>>.That.Matches(fun x ->
Seq.length x = 1 && Seq.head x = KeyValuePair("@id", box 1)),
CancellationToken.None)).MustHaveHappenedOnceExactly ()
|> ignore
}
[<Test>]
let Db_QueryUnbuffered_Calls_IDatabaseConnection_Query_NoParms () = task {
// Arrange
let sql = "SELECT 1 FROM table"
let parms = Array.empty
// Act
let! _ = task {
return! (sql, parms)
||> ReadWriteDb.queryUnbuffered fakeDbConnection CancellationToken.None
|> TaskSeq.toArrayAsync
}
// Assert
A.CallTo(fun () -> fakeDbConnection.QueryUnbuffered(
"SELECT 1 FROM table",
A<IEnumerable<KeyValuePair<string, obj>>>.That.Matches(Seq.isEmpty),
CancellationToken.None)).MustHaveHappenedOnceExactly ()
|> ignore
}
[<Test>]
let Db_QueryUnbuffered_Calls_IDatabaseConnection_Query_WithParms () = task {
// Arrange
let sql = "SELECT 1 FROM table"
let parms = [| ("@id", box 1) |]
// Act
let! result = task {
return! (sql, parms)
||> ReadWriteDb.queryUnbuffered fakeDbConnection CancellationToken.None
|> TaskSeq.toArrayAsync
}
// Assert
A.CallTo(fun () -> fakeDbConnection.QueryUnbuffered(
"SELECT 1 FROM table",
A<IEnumerable<KeyValuePair<string, obj>>>.That.Matches(fun x ->
Seq.length x = 1 && Seq.head x = KeyValuePair("@id", box 1)),
CancellationToken.None)).MustHaveHappenedOnceExactly ()
|> ignore
}
[<Test>]
let Db_QuerySingle_Calls_IDatabaseConnection_QuerySingle_NoParms () = task {
// Arrange
let sql = "SELECT 1 FROM table"
let parms = Array.empty
// Act
let! _ = task {
return! (sql, parms) ||> ReadWriteDb.querySingle fakeDbConnection CancellationToken.None
}
// Assert
A.CallTo(fun () -> fakeDbConnection.QuerySingle(
"SELECT 1 FROM table",
A<IEnumerable<KeyValuePair<string, obj>>>.That.Matches(Seq.isEmpty),
CancellationToken.None)).MustHaveHappenedOnceExactly ()
|> ignore
}
[<Test>]
let Db_QuerySingle_Calls_IDatabaseConnection_QuerySingle_WithParms () = task {
// Arrange
let sql = "SELECT 1 FROM table"
let parms = [| ("@id", box 1) |]
// Act
let! result = task {
return! (sql, parms) ||> ReadWriteDb.querySingle fakeDbConnection CancellationToken.None
}
// Assert
A.CallTo(fun () -> fakeDbConnection.QuerySingle(
"SELECT 1 FROM table",
A<IEnumerable<KeyValuePair<string, obj>>>.That.Matches(fun x ->
Seq.length x = 1 && Seq.head x = KeyValuePair("@id", box 1)),
CancellationToken.None)).MustHaveHappenedOnceExactly ()
|> ignore
}
[<Test>]
let Db_Execute_Calls_IDatabaseConnection_Execute_NoParms () = task {
// Arrange
let sql = "SELECT 1 FROM table"
let parms = Array.empty
// Act
let! _ = task {
return! (sql, parms) ||> ReadWriteDb.execute fakeDbConnection CancellationToken.None
}
// Assert
A.CallTo(fun () -> fakeDbConnection.Execute(
"SELECT 1 FROM table",
A<IEnumerable<KeyValuePair<string, obj>>>.That.Matches(Seq.isEmpty),
CancellationToken.None)).MustHaveHappenedOnceExactly ()
|> ignore
}
[<Test>]
let Db_Execute_Calls_IDatabaseConnection_Execute_WithParms () = task {
// Arrange
let sql = "SELECT 1 FROM table"
let parms = [| ("@id", box 1) |]
// Act
let! result = task {
return! (sql, parms) ||> ReadWriteDb.execute fakeDbConnection CancellationToken.None
}
// Assert
A.CallTo(fun () -> fakeDbConnection.Execute(
"SELECT 1 FROM table",
A<IEnumerable<KeyValuePair<string, obj>>>.That.Matches(fun x ->
Seq.length x = 1 && Seq.head x = KeyValuePair("@id", box 1)),
CancellationToken.None)).MustHaveHappenedOnceExactly ()
|> ignore
}
[<Test>]
let Db_ExecuteScalar_Calls_IDatabaseConnection_ExecuteScalar_NoParms () = task {
// Arrange
let sql = "SELECT 1 FROM table"
let parms = Array.empty
// Act
let! _ = task {
return! (sql, parms) ||> ReadWriteDb.executeScalar fakeDbConnection CancellationToken.None
}
// Assert
A.CallTo(fun () -> fakeDbConnection.ExecuteScalar(
"SELECT 1 FROM table",
A<IEnumerable<KeyValuePair<string, obj>>>.That.Matches(Seq.isEmpty),
CancellationToken.None)).MustHaveHappenedOnceExactly ()
|> ignore
}
[<Test>]
let Db_ExecuteScalar_Calls_IDatabaseConnection_ExecuteScalar_WithParms () = task {
// Arrange
let sql = "SELECT 1 FROM table"
let parms = [| ("@id", box 1) |]
// Act
let! result = task {
return! (sql, parms) ||> ReadWriteDb.executeScalar fakeDbConnection CancellationToken.None
}
// Assert
A.CallTo(fun () -> fakeDbConnection.ExecuteScalar(
"SELECT 1 FROM table",
A<IEnumerable<KeyValuePair<string, obj>>>.That.Matches(fun x ->
Seq.length x = 1 && Seq.head x = KeyValuePair("@id", box 1)),
CancellationToken.None)).MustHaveHappenedOnceExactly ()
|> ignore
}
[<Test>]
let Db_ExecuteReader_Calls_IDatabaseConnection_ExecuteReader_NoParms () = task {
// Arrange
let sql = "SELECT 1 FROM table"
let parms = Array.empty
// Act
let! _ = task {
return! (sql, parms) ||> ReadWriteDb.executeReader fakeDbConnection CancellationToken.None
}
// Assert
A.CallTo(fun () -> fakeDbConnection.ExecuteReader(
"SELECT 1 FROM table",
A<IEnumerable<KeyValuePair<string, obj>>>.That.Matches(Seq.isEmpty),
CancellationToken.None)).MustHaveHappenedOnceExactly ()
|> ignore
}
[<Test>]
let Db_ExecuteReader_Calls_IDatabaseConnection_ExecuteReader_WithParms () = task {
// Arrange
let sql = "SELECT 1 FROM table"
let parms = [| ("@id", box 1) |]
// Act
let! result = task {
return! (sql, parms) ||> ReadWriteDb.executeReader fakeDbConnection CancellationToken.None
}
// Assert
A.CallTo(fun () -> fakeDbConnection.ExecuteReader(
"SELECT 1 FROM table",
A<IEnumerable<KeyValuePair<string, obj>>>.That.Matches(fun x ->
Seq.length x = 1 && Seq.head x = KeyValuePair("@id", box 1)),
CancellationToken.None)).MustHaveHappenedOnceExactly ()
|> ignore
}

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

@ -19,16 +19,18 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="FsUnit" Version="6.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
<PackageReference Include="NUnit" Version="4.1.0" />
<PackageReference Include="NUnit.Analyzers" Version="4.2.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0"/>
<PackageReference Update="FSharp.Core" Version="8.0.300" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Base\Base.csproj" />
<ProjectReference Include="..\FsBase\FsBase.fsproj" />
</ItemGroup>

View File

@ -14,7 +14,7 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
<PackageReference Include="NUnit" Version="4.1.0" />
<PackageReference Include="NUnit.Analyzers" Version="4.2.0">
<PrivateAssets>all</PrivateAssets>
@ -28,6 +28,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Base\Base.csproj" />
<ProjectReference Include="..\FsBase\FsBase.fsproj" />
</ItemGroup>

View File

@ -14,6 +14,20 @@ 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.Core", "FruityFoundation.DataAccess.Core\FruityFoundation.DataAccess.Core.csproj", "{B65527CC-218A-4EA3-93DC-985713B5DFF4}"
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "FruityFoundation.DataAccess.Abstractions.FSharp", "FruityFoundation.DataAccess.Abstractions.FSharp\FruityFoundation.DataAccess.Abstractions.FSharp.fsproj", "{B2E0156B-B631-4C80-A129-59472D2D0A77}"
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
@ -44,10 +58,40 @@ Global
{A64E73D3-EF87-4938-B01E-F9CC0B59F9DE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A64E73D3-EF87-4938-B01E-F9CC0B59F9DE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A64E73D3-EF87-4938-B01E-F9CC0B59F9DE}.Release|Any CPU.Build.0 = Release|Any CPU
{C003E247-C62E-4830-94E4-F274D8466A5C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{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
{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
{B2E0156B-B631-4C80-A129-59472D2D0A77}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B2E0156B-B631-4C80-A129-59472D2D0A77}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B2E0156B-B631-4C80-A129-59472D2D0A77}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B2E0156B-B631-4C80-A129-59472D2D0A77}.Release|Any CPU.Build.0 = Release|Any CPU
{27F4FB64-7A51-4315-BDAA-6EE07736C976}.Debug|Any CPU.ActiveCfg = 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.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}
{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}
{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

View File

@ -7,7 +7,7 @@
<PackageId>FruityFoundation.FsBase</PackageId>
<Authors>Kyle Ratti</Authors>
<PackageRequireLicenseAcceptance>true</PackageRequireLicenseAcceptance>
<Version>1.9.0</Version>
<Version>2.1.0</Version>
<PackageLicenseFile>LICENSE</PackageLicenseFile>
<TargetFrameworks>net6.0;net8.0</TargetFrameworks>
<LangVersion>8.0</LangVersion>
@ -30,7 +30,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Update="FSharp.Core" Version="8.0.200" />
<PackageReference Update="FSharp.Core" Version="8.0.300" />
</ItemGroup>
<ItemGroup>