15 Commits

Author SHA1 Message Date
agrynco 2142e41d80 docs(nuget): include package readmes
Add package-specific NuGet readmes for both HrynCo DAL packages.

Ref: hrynco/hrynco-ef#18
2026-10-15 14:37:24 +03:00
agrynco dbd569b3d6 Merge pull request 'fix(transaction): retain both transaction failures (IT-1588)' (#16) from fix/IT-1588-preserve-transaction-errors into development 2026-10-15 14:27:57 +03:00
agrynco 5dd8342bf7 fix(transaction): retain both transaction failures
Keep the original action or commit failure visible alongside a failed rollback and cover owned transaction reuse.

Ref: IT-1588
2026-10-15 14:13:56 +03:00
Anatolii Grynchuk 1da2dd8579 fix: update RepositoryUrl in project files to correct domain 2026-05-06 22:41:21 +03:00
agrynco 3f1371e33a Merge pull request 'fix: use GetByIdAsync in DeleteAsync(TEntityId)' (#11) from IT-631-fix-delete-async-getbyid into development 2026-05-06 12:50:04 +03:00
Anatolii Grynchuk 9c1da388d8 fix: use GetByIdAsync in DeleteAsync(TEntityId)
Replaced sync GetById with await GetByIdAsync to avoid blocking the thread inside an async method.

Ref: #IT-631
2026-05-06 12:46:35 +03:00
agrynco 1edcafdebd Merge pull request 'fix: eliminate multi-save in batch Add and DeleteAsync(TEntityId)' (#10) from IT-631-fix-delete into development 2026-05-06 12:45:09 +03:00
Anatolii Grynchuk 93461fd35e fix: eliminate multi-save in batch Add and DeleteAsync(TEntityId)
- Add(TEntity[]) now passes save:false in the loop and calls SaveChanges once at the end
- DeleteAsync(TEntityId) now calls DoRemove directly instead of Delete(TEntityId) to avoid the double-save from the sync overload chain

Ref: #IT-631
2026-05-06 12:43:27 +03:00
Anatolii Grynchuk 4020981eec chore: upgrade ef core 8.0.15 -> 10.0.7
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-06 02:20:44 +03:00
Anatolii Grynchuk 2b02374b19 chore: downgrade ef core 9.0.5 -> 8.0.15
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-06 01:55:07 +03:00
agrynco c630db2582 Merge pull request 'docs: switch task management and wiki to gitea, merge to development only for non-release changes' (#7) from docs/gitea-workflow-agents into development 2026-05-06 01:29:15 +03:00
Anatolii Grynchuk a58b4d9279 docs: switch task management and wiki to gitea, merge to development only for non-release changes
- remove YouTrack references from AGENTS.md
- use Gitea issues for task tracking
- merge to main only when a NuGet release is explicitly requested
- remove IT-642 link from README.md

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-06 01:29:01 +03:00
agrynco 23297f76f3 Merge pull request 'feat: rebuild base repository hierarchy, add readme and agents' (#5) from fix/base-db-context-utcnow into development 2026-05-06 01:16:19 +03:00
agrynco 69e3ab6079 Merge pull request 'feat: expose UtcNow property on BaseDbContext' (#3) from fix/base-db-context-utcnow into development 2026-05-05 22:17:06 +03:00
agrynco 521ae31dce Merge pull request 'feat: add BaseDbContext, non-generic IEntity, fix Entity constructor' (#1) from fix/entity-base-improvements into development 2026-05-05 22:03:21 +03:00
14 changed files with 386 additions and 38 deletions
+12 -19
View File
@@ -6,7 +6,6 @@ This file is the authoritative source for hrynco-ef workflow, delivery, and role
- Command timeout: if a command runs longer than 10 minutes, report timeout and ask the user.
- Validate required services before task work:
- YouTrack authenticated access
- Gitea authenticated access and the `hrynco/hrynco-ef` repo
- TeamCity authenticated access
- If any startup check fails, stop and ask the user.
@@ -30,7 +29,6 @@ Print one startup report line per check:
Required report items:
- Workflow rules loaded
- YouTrack auth + reachability
- Gitea auth + reachability
- Gitea resource access (`hrynco/hrynco-ef`)
- TeamCity auth + reachability
@@ -47,7 +45,7 @@ Required report items:
- local `development` must match remote
- if not, fix that first
- On task start:
- set YouTrack state to `In Progress`
- create a Gitea issue in `hrynco/hrynco-ef` if one does not exist
- create a feature branch from `development`
- name the branch after the task
- Work only in the feature branch.
@@ -56,12 +54,12 @@ Required report items:
### Issue lifecycle
1. Product analyst creates/refines the YouTrack task.
2. Developer takes the task and starts implementation.
1. Product analyst creates/refines the Gitea issue.
2. Developer takes the issue and starts implementation.
3. Developer finishes implementation and hands off to code review.
4. Code reviewer reviews and either passes or returns to developer.
5. Repeat developer/reviewer cycles as needed.
6. Maximum five review rounds total for the same unresolved task state.
6. Maximum five review rounds total for the same unresolved issue state.
7. After review round five, if still unresolved, return to product analysis.
8. Tester validates the implementation against acceptance criteria.
9. Developer waits for user validation approval.
@@ -69,7 +67,7 @@ Required report items:
### Task rules
- Create the YouTrack task before development starts.
- Create the Gitea issue before development starts.
- Default assignee is AI unless the task flow requires another assignee.
- Define scope and acceptance criteria.
- Tasks must be independently deliverable and testable.
@@ -89,22 +87,17 @@ After explicit user validation approval:
- push the feature branch
- create PR from feature branch to `development`
- merge that PR
- create PR from `development` to `main`
- merge that PR
- wait for TC `HrynCo / HrynCo.EF / publish` to finish successfully
- set the YouTrack task to `Done`
- calculate exact Spent Time from YouTrack timestamps only
- set the YouTrack Spent Time field
- **do not merge to `main` unless a new NuGet package release is explicitly requested**
- when a release is requested: create PR from `development` to `main`, merge, wait for TC `HrynCo / HrynCo.EF / publish` to finish successfully
- close the Gitea issue
- switch back to `development`
- pull latest `development`
- ensure local `development` and `main` match their remotes
- ensure local `development` matches remote
- leave the repository in a clean end state
### Spent Time
- Do not estimate or infer Spent Time.
- Use exact YouTrack creation and Done timestamps only.
- If exact timestamps are unavailable, stop and ask the user.
- Do not track spent time — this repo uses Gitea, not YouTrack.
### Command output
@@ -115,9 +108,9 @@ After explicit user validation approval:
- Keep command output summaries concise.
- Do not dump raw command output unless needed.
- Use direct links in YouTrack and implementation notes when stable links exist.
- Use direct links to Gitea issues and wiki pages in implementation notes when stable links exist.
- Use emojis intentionally for scanning, not mechanically.
- Commit messages must be Conventional Commit style, lowercase subject, short body, and final line `Ref: IT-<number>`.
- Commit messages must be Conventional Commit style, lowercase subject, short body.
## 6. Audio
+9 -3
View File
@@ -6,8 +6,14 @@
<!-- HrynCo shared packages -->
<PackageVersion Include="HrynCo.Common" Version="1.0.0" />
<!-- Entity Framework Core -->
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="9.0.5" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.5" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.5" />
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="10.0.7" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.7" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.7" />
<!-- Tests -->
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.7" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
<PackageVersion Include="SQLitePCLRaw.bundle_e_sqlite3" Version="3.0.5" />
<PackageVersion Include="xunit" Version="2.9.3" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />
</ItemGroup>
</Project>
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
@@ -6,11 +6,17 @@
<Nullable>enable</Nullable>
<RootNamespace>HrynCo.DAL.Abstract</RootNamespace>
<PackageId>HrynCo.DAL.Abstract</PackageId>
<PackageReadmeFile>README.md</PackageReadmeFile>
<Authors>HrynCo</Authors>
<Description>Abstract DAL contracts for HrynCo applications: entities, repository and unit-of-work interfaces.</Description>
<PackageTags>hrynco dal abstract entity repository unitofwork</PackageTags>
<RepositoryType>git</RepositoryType>
<RepositoryUrl>https://gitea.grynco.com.ua/hrynco/hrynco-ef.git</RepositoryUrl>
<RepositoryUrl>https://gitea.hrynco.com/hrynco/hrynco-ef.git</RepositoryUrl>
</PropertyGroup>
<ItemGroup>
<None Include="README.md" Pack="true" PackagePath="/" />
</ItemGroup>
</Project>
+5
View File
@@ -0,0 +1,5 @@
# HrynCo.DAL.Abstract
Infrastructure-independent contracts for entities, repositories, unit of work, transactions, and pagination. Reference this package from application layers that should not depend on Entity Framework Core.
Source and usage context: [hrynco-ef](https://gitea.hrynco.com/hrynco/hrynco-ef).
+213
View File
@@ -0,0 +1,213 @@
namespace HrynCo.DAL.EF.Tests;
using HrynCo.DAL.Abstract;
using Microsoft.EntityFrameworkCore;
using Xunit;
public sealed class EfUnitOfWorkTests
{
[Theory]
[InlineData(false)]
[InlineData(true)]
public async Task ActionFailure_ReportsBothExceptionsWhenRollbackFails(bool returnsResult)
{
var original = new InvalidOperationException("Action failed.");
var rollbackFailure = new InvalidOperationException("Transaction completed.");
var interceptor = new TransactionFailureInterceptor
{
RollbackException = rollbackFailure
};
await using DbContext context = await CreateContextAsync(interceptor);
var unitOfWork = new TestUnitOfWork(context);
AggregateException actual = await Assert.ThrowsAsync<AggregateException>(() =>
ExecuteAsync(unitOfWork, returnsResult, () => Task.FromException(original)));
Assert.Collection(actual.InnerExceptions,
error => Assert.Same(original, error),
error => Assert.Same(rollbackFailure, error));
Assert.Equal(1, interceptor.RollbackAttempts);
Assert.Null(unitOfWork.GetCurrentTransaction());
Assert.Null(context.Database.CurrentTransaction);
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public async Task CommitFailure_ReportsBothExceptionsWhenRollbackFails(bool returnsResult)
{
var original = new InvalidOperationException("Commit failed.");
var rollbackFailure = new InvalidOperationException("Transaction completed.");
var interceptor = new TransactionFailureInterceptor
{
CommitException = original,
RollbackException = rollbackFailure
};
await using DbContext context = await CreateContextAsync(interceptor);
var unitOfWork = new TestUnitOfWork(context);
AggregateException actual = await Assert.ThrowsAsync<AggregateException>(() =>
ExecuteAsync(unitOfWork, returnsResult, () => Task.CompletedTask));
Assert.Collection(actual.InnerExceptions,
error => Assert.Same(original, error),
error => Assert.Same(rollbackFailure, error));
Assert.Equal(1, interceptor.CommitAttempts);
Assert.Equal(1, interceptor.RollbackAttempts);
Assert.Null(unitOfWork.GetCurrentTransaction());
Assert.Null(context.Database.CurrentTransaction);
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public async Task CompletedTransaction_ReportsActionAndRollbackExceptions(bool returnsResult)
{
await using DbContext context = await CreateContextAsync();
var unitOfWork = new TestUnitOfWork(context);
var original = new InvalidOperationException("Action failed after the provider completed the transaction.");
AggregateException actual = await Assert.ThrowsAsync<AggregateException>(() =>
ExecuteAsync(unitOfWork, returnsResult, async () =>
{
await context.Database.CurrentTransaction!.CommitAsync();
throw original;
}));
Assert.Same(original, actual.InnerExceptions[0]);
Assert.IsType<InvalidOperationException>(actual.InnerExceptions[1]);
Assert.Null(unitOfWork.GetCurrentTransaction());
Assert.Null(context.Database.CurrentTransaction);
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public async Task ActionFailure_RollsBackWritesAndAllowsNextTransaction(bool returnsResult)
{
await using DbContext context = await CreateContextAsync();
var unitOfWork = new TestUnitOfWork(context);
var original = new InvalidOperationException("Action failed.");
Exception actual = await Assert.ThrowsAsync<InvalidOperationException>(() =>
ExecuteAsync(unitOfWork, returnsResult, async () =>
{
await context.Database.ExecuteSqlRawAsync("INSERT INTO records VALUES (1)");
throw original;
}));
Assert.Same(original, actual);
Assert.Equal(0, await CountRecordsAsync(context));
Assert.Null(unitOfWork.GetCurrentTransaction());
await ExecuteAsync(unitOfWork, returnsResult,
() => context.Database.ExecuteSqlRawAsync("INSERT INTO records VALUES (2)"));
Assert.Equal(1, await CountRecordsAsync(context));
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public async Task Success_CommitsWritesAndCreatesFreshTransactionForNextCall(bool returnsResult)
{
var interceptor = new TransactionFailureInterceptor();
await using DbContext context = await CreateContextAsync(interceptor);
var unitOfWork = new TestUnitOfWork(context);
ITransaction? first = null;
await ExecuteAsync(unitOfWork, returnsResult, async () =>
{
first = unitOfWork.GetCurrentTransaction();
await context.Database.ExecuteSqlRawAsync("INSERT INTO records VALUES (1)");
});
Assert.Null(unitOfWork.GetCurrentTransaction());
Assert.Null(context.Database.CurrentTransaction);
await ExecuteAsync(unitOfWork, returnsResult, async () =>
{
Assert.NotSame(first, unitOfWork.GetCurrentTransaction());
await context.Database.ExecuteSqlRawAsync("INSERT INTO records VALUES (2)");
});
Assert.Equal(2, await CountRecordsAsync(context));
Assert.Equal(2, interceptor.CommitAttempts);
Assert.Equal(0, interceptor.RollbackAttempts);
Assert.Null(unitOfWork.GetCurrentTransaction());
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public async Task CallerOwnedTransaction_SuccessLeavesLifecycleToCaller(bool returnsResult)
{
var interceptor = new TransactionFailureInterceptor();
await using DbContext context = await CreateContextAsync(interceptor);
var unitOfWork = new TestUnitOfWork(context);
await using ITransaction transaction = await unitOfWork.BeginTransactionAsync();
await ExecuteAsync(unitOfWork, returnsResult,
() => context.Database.ExecuteSqlRawAsync("INSERT INTO records VALUES (1)"));
Assert.Same(transaction, unitOfWork.GetCurrentTransaction());
Assert.NotNull(context.Database.CurrentTransaction);
Assert.Equal(0, interceptor.CommitAttempts);
Assert.Equal(0, interceptor.RollbackAttempts);
await transaction.RollbackAsync();
Assert.Equal(0, await CountRecordsAsync(context));
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public async Task CallerOwnedTransaction_FailureLeavesLifecycleToCaller(bool returnsResult)
{
var interceptor = new TransactionFailureInterceptor();
await using DbContext context = await CreateContextAsync(interceptor);
var unitOfWork = new TestUnitOfWork(context);
await using ITransaction transaction = await unitOfWork.BeginTransactionAsync();
var original = new InvalidOperationException("Action failed.");
Exception actual = await Assert.ThrowsAsync<InvalidOperationException>(() =>
ExecuteAsync(unitOfWork, returnsResult, () => Task.FromException(original)));
Assert.Same(original, actual);
Assert.Same(transaction, unitOfWork.GetCurrentTransaction());
Assert.NotNull(context.Database.CurrentTransaction);
Assert.Equal(0, interceptor.CommitAttempts);
Assert.Equal(0, interceptor.RollbackAttempts);
await transaction.RollbackAsync();
}
private static async Task ExecuteAsync(TestUnitOfWork unitOfWork, bool returnsResult, Func<Task> action)
{
if (returnsResult)
{
int result = await unitOfWork.ExecuteInTransactionAsync(async () =>
{
await action();
return 42;
});
Assert.Equal(42, result);
return;
}
await unitOfWork.ExecuteInTransactionAsync(action);
}
private static async Task<DbContext> CreateContextAsync(TransactionFailureInterceptor? interceptor = null)
{
var options = new DbContextOptionsBuilder().UseSqlite("Data Source=:memory:");
if (interceptor is not null)
{
options.AddInterceptors(interceptor);
}
var context = new DbContext(options.Options);
await context.Database.OpenConnectionAsync();
await context.Database.ExecuteSqlRawAsync("CREATE TABLE records (id INTEGER NOT NULL)");
return context;
}
private static Task<int> CountRecordsAsync(DbContext context)
{
return context.Database.SqlQueryRaw<int>("SELECT COUNT(*) AS Value FROM records").SingleAsync();
}
}
@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" />
<PackageReference Include="xunit" />
<PackageReference Include="xunit.runner.visualstudio">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<ProjectReference Include="..\HrynCo.DAL.EF\HrynCo.DAL.EF.csproj" />
</ItemGroup>
</Project>
+6
View File
@@ -0,0 +1,6 @@
namespace HrynCo.DAL.EF.Tests;
using HrynCo.DAL.EF.Core;
using Microsoft.EntityFrameworkCore;
internal sealed class TestUnitOfWork(DbContext context) : EfUnitOfWork<DbContext>(context);
@@ -0,0 +1,42 @@
namespace HrynCo.DAL.EF.Tests;
using System.Data.Common;
using Microsoft.EntityFrameworkCore.Diagnostics;
internal sealed class TransactionFailureInterceptor : DbTransactionInterceptor
{
public Exception? CommitException { get; init; }
public Exception? RollbackException { get; init; }
public int CommitAttempts { get; private set; }
public int RollbackAttempts { get; private set; }
public override ValueTask<InterceptionResult> TransactionCommittingAsync(
DbTransaction transaction,
TransactionEventData eventData,
InterceptionResult result,
CancellationToken cancellationToken = default)
{
CommitAttempts++;
if (CommitException is not null)
{
throw CommitException;
}
return ValueTask.FromResult(result);
}
public override ValueTask<InterceptionResult> TransactionRollingBackAsync(
DbTransaction transaction,
TransactionEventData eventData,
InterceptionResult result,
CancellationToken cancellationToken = default)
{
RollbackAttempts++;
if (RollbackException is not null)
{
throw RollbackException;
}
return ValueTask.FromResult(result);
}
}
+11 -2
View File
@@ -40,7 +40,12 @@ public abstract class BaseEfRepository<TDbContext, TEntity, TEntityId> :
{
foreach (TEntity entity in entities)
{
Add(entity, save);
Add(entity, save: false);
}
if (save)
{
DbContext.SaveChanges();
}
}
@@ -84,7 +89,11 @@ public abstract class BaseEfRepository<TDbContext, TEntity, TEntityId> :
public async Task DeleteAsync(TEntityId id)
{
Delete(id);
TEntity? entity = await GetByIdAsync(id);
if (entity != null)
{
DoRemove(entity);
}
await DbContext.SaveChangesAsync();
}
+32 -6
View File
@@ -42,11 +42,11 @@ public class EfUnitOfWork<TDbContext> : IUnitOfWork
await tx.CommitAsync();
}
}
catch
catch (Exception original)
{
if (ownsTransaction)
{
await tx.RollbackAsync();
await RollbackPreservingFailureAsync(tx, original);
}
throw;
@@ -55,7 +55,14 @@ public class EfUnitOfWork<TDbContext> : IUnitOfWork
{
if (ownsTransaction)
{
await tx.DisposeAsync();
try
{
await tx.DisposeAsync();
}
finally
{
_currentTransaction = null;
}
}
}
}
@@ -77,11 +84,11 @@ public class EfUnitOfWork<TDbContext> : IUnitOfWork
return result;
}
catch
catch (Exception original)
{
if (ownsTransaction)
{
await tx.RollbackAsync();
await RollbackPreservingFailureAsync(tx, original);
}
throw;
@@ -90,11 +97,30 @@ public class EfUnitOfWork<TDbContext> : IUnitOfWork
{
if (ownsTransaction)
{
await tx.DisposeAsync();
try
{
await tx.DisposeAsync();
}
finally
{
_currentTransaction = null;
}
}
}
}
private static async Task RollbackPreservingFailureAsync(ITransaction transaction, Exception original)
{
try
{
await transaction.RollbackAsync();
}
catch (Exception rollbackFailure)
{
throw new AggregateException("The transaction and its rollback both failed.", original, rollbackFailure);
}
}
public ITransaction? GetCurrentTransaction()
{
return _currentTransaction;
+8 -2
View File
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
@@ -6,13 +6,18 @@
<Nullable>enable</Nullable>
<RootNamespace>HrynCo.DAL.EF</RootNamespace>
<PackageId>HrynCo.DAL.EF</PackageId>
<PackageReadmeFile>README.md</PackageReadmeFile>
<Authors>HrynCo</Authors>
<Description>Entity Framework Core base implementations for HrynCo applications: generic repository and unit-of-work.</Description>
<PackageTags>hrynco dal ef entityframework repository unitofwork</PackageTags>
<RepositoryType>git</RepositoryType>
<RepositoryUrl>https://gitea.grynco.com.ua/hrynco/hrynco-ef.git</RepositoryUrl>
<RepositoryUrl>https://gitea.hrynco.com/hrynco/hrynco-ef.git</RepositoryUrl>
</PropertyGroup>
<ItemGroup>
<None Include="README.md" Pack="true" PackagePath="/" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\HrynCo.DAL.Abstract\HrynCo.DAL.Abstract.csproj" />
</ItemGroup>
@@ -28,3 +33,4 @@
</ItemGroup>
</Project>
+5
View File
@@ -0,0 +1,5 @@
# HrynCo.DAL.EF
Entity Framework Core implementations of the HrynCo.DAL.Abstract repository and unit-of-work contracts. Reference this package from the data-access layer alongside your EF Core database provider.
Source and usage context: [hrynco-ef](https://gitea.hrynco.com/hrynco/hrynco-ef).
+14 -4
View File
@@ -4,12 +4,13 @@ Reusable Entity Framework Core base library for HrynCo applications.
## Solution
The solution (`hrynco-ef.slnx`) contains two projects:
The solution (`hrynco-ef.slnx`) contains the following projects:
| Project | Description |
|---|---|
| `HrynCo.DAL.Abstract` | Infrastructure-agnostic contracts: entities, repository interfaces, unit of work, transactions, pagination. No EF Core dependency. |
| `HrynCo.DAL.EF` | Entity Framework Core implementations of the abstract contracts. Depends on `HrynCo.DAL.Abstract` and EF Core. |
| `HrynCo.DAL.EF.Tests` | Transaction lifecycle regression tests using SQLite in memory. |
The split allows consuming projects to reference only `HrynCo.DAL.Abstract` in domain/application layers, keeping those layers free of EF Core.
@@ -181,6 +182,18 @@ Entity Framework Core implementations of the abstract contracts.
## Usage
### Transaction ownership and failures
Both `ExecuteInTransactionAsync` overloads create, commit, and dispose a transaction when no transaction is already cached by the unit of work. After disposal, the cached reference is cleared, so a later call starts a fresh transaction.
If the action or commit fails, the owned transaction is rolled back. If rollback also fails (including when the provider reports a completed transaction), an `AggregateException` contains the original failure first and the rollback failure second. Disposal still runs.
When `BeginTransactionAsync` has already supplied a transaction to the caller, `ExecuteInTransactionAsync` runs the action inside that transaction and leaves commit, rollback, and disposal to the caller. Its cached reference is retained as before; the automatic cleanup above applies only to transactions owned by `ExecuteInTransactionAsync`.
Run the regression suite with `dotnet test hrynco-ef.slnx`.
### Package references
Reference `HrynCo.DAL.Abstract` for contracts only (e.g. in domain/application layers).
Reference `HrynCo.DAL.EF` for the full EF Core implementation (infrastructure layer).
@@ -202,6 +215,3 @@ services.AddScoped<IRepository<Product>, ProductRepository>();
services.AddScoped<IUnitOfWork, EfUnitOfWork<YourDbContext>>();
```
## Related
- [IT-642](https://yt.grynco.com.ua/issue/IT-642) — Extract `PagedResult` builder as reusable `IQueryable<T>` extension
+1
View File
@@ -1,4 +1,5 @@
<Solution>
<Project Path="HrynCo.DAL.Abstract/HrynCo.DAL.Abstract.csproj" />
<Project Path="HrynCo.DAL.EF/HrynCo.DAL.EF.csproj" />
<Project Path="HrynCo.DAL.EF.Tests/HrynCo.DAL.EF.Tests.csproj" />
</Solution>