Files
hrynco-ef/README.md
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

7.8 KiB

hrynco-ef

Reusable Entity Framework Core base library for HrynCo applications.

Solution

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.

Versioning

Versions are managed entirely on the TeamCity side — do not set <Version> in .csproj files.

At publish time, the TC HrynCo / HrynCo.EF / publish build:

  1. Writes the current build number into Directory.Build.props as <Version>%build.number%</Version>.
  2. Builds and packs both projects.
  3. Pushes the resulting .nupkg files to nuget.org.

The build number follows the pattern 1.0.<counter> (e.g. 1.0.6, 1.0.7, …). The counter increments automatically on each successful publish run. To release a new version, merge to main — the publish build triggers automatically.

To bump the major or minor version, update the build number pattern in TC: HrynCo → HrynCo.EF → publish → Edit Configuration → General → Build number format.

Class diagram

classDiagram
    namespace HrynCo_DAL_Abstract {
        class IEntity {
            <<interface>>
            +object Id
            +DateTimeOffset Created
            +DateTimeOffset? Updated
        }
        class IEntityTId {
            <<interface>>
            +TId Id
        }
        class EntityTId {
            <<abstract>>
            +TId Id
            +DateTimeOffset Created
            +DateTimeOffset? Updated
        }
        class Entity {
            <<abstract>>
            +Guid Id
        }
        class NamedEntity {
            <<abstract>>
            +string Name
        }
        class IUnitOfWork {
            <<interface>>
            +BeginTransactionAsync() Task~ITransaction~
            +GetCurrentTransaction() ITransaction?
            +ExecuteInTransactionAsync(action) Task
            +ExecuteInTransactionAsync~TResult~(action) Task~TResult~
        }
        class ITransaction {
            <<interface>>
            +CommitAsync() Task
            +RollbackAsync() Task
            +DisposeAsync() ValueTask
        }
        class PagedResultT {
            <<sealed>>
            +IReadOnlyList~T~ Items
            +int Page
            +int PageSize
            +int TotalCount
        }
    }

    namespace HrynCo_DAL_EF {
        class IEfRepository {
            <<interface>>
            +ClearChangeTracker()
        }
        class IEfRepositoryTEntityTId {
            <<interface>>
            +Add(entity) TEntity
            +AddAsync(entity) Task~TEntity~
            +Delete(id)
            +DeleteAsync(id) Task
            +Get(filter, orderBy, includes) IQueryable~TEntity~
            +GetAll() IQueryable~TEntity~
            +GetAllAsync() Task~List~TEntity~~
            +GetById(id) TEntity?
            +GetByIdAsync(id) Task~TEntity?~
            +Exists(id) Task~bool~
            +UpdateAsync(entity) Task
            +SaveChangesAsync() Task
        }
        class IEfRepositoryTEntity {
            <<interface>>
        }
        class BaseEfRepositoryTDbContextTEntityTEntityId {
            <<abstract>>
            +TDbContext DbContext
            +Add() TEntity
            +AddAsync() Task~TEntity~
            +Delete()
            +DeleteAsync() Task
            +Get() IQueryable~TEntity~
            +GetAll() IQueryable~TEntity~
            +GetAllAsync() Task~List~TEntity~~
            +GetById() TEntity?
            +GetByIdAsync() Task~TEntity?~
            +Exists() Task~bool~
            +Update()
            +UpdateAsync() Task
            +SaveChangesAsync() Task
            #DoRemove()
        }
        class BaseRepositoryTEfRepositoryTDbContextTEntityTEntityId {
            <<abstract>>
            #EfRepository TEfRepository
            #CreateEfRepository()* TEfRepository
        }
        class EfUnitOfWorkTDbContext {
            +BeginTransactionAsync() Task~ITransaction~
            +GetCurrentTransaction() ITransaction?
            +ExecuteInTransactionAsync() Task
        }
        class EfTransactionAdapter {
            +CommitAsync() Task
            +RollbackAsync() Task
            +DisposeAsync() ValueTask
        }
    }

    IEntityTId --|> IEntity : extends
    EntityTId ..|> IEntityTId : implements
    Entity --|> EntityTId : extends (TId=Guid)
    NamedEntity --|> Entity : extends

    IEfRepositoryTEntityTId --|> IEfRepository : extends
    IEfRepositoryTEntity --|> IEfRepositoryTEntityTId : extends (TId=int)
    BaseEfRepositoryTDbContextTEntityTEntityId ..|> IEfRepositoryTEntityTId : implements
    BaseRepositoryTEfRepositoryTDbContextTEntityTEntityId --> BaseEfRepositoryTDbContextTEntityTEntityId : uses (lazy)

    EfUnitOfWorkTDbContext ..|> IUnitOfWork : implements
    EfTransactionAdapter ..|> ITransaction : implements
    EfUnitOfWorkTDbContext --> EfTransactionAdapter : creates

Packages

HrynCo.DAL.Abstract

Abstract DAL contracts — entities, repository interfaces, unit of work, transactions, and pagination.

Type Description
IEntity / IEntity<TId> Base entity contracts
Entity<TId> / Entity Base entity implementations with auto-generated Id
NamedEntity Entity with a Name property
IRepository<T> Generic async repository interface
IUnitOfWork Unit of work interface with transaction support
ITransaction Async transaction contract
PagedResult<T> Pagination result wrapper

HrynCo.DAL.EF

Entity Framework Core implementations of the abstract contracts.

Type Description
BaseRepository<T> Base repository with common CRUD operations
BaseEfRepository<T> EF Core repository with DbContext access
IEfRepository<T> EF-specific repository interface
EfUnitOfWork EF Core unit of work implementation
EfTransactionAdapter Adapts EF transactions to ITransaction

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).

// 1. Define your entity
public class Product : Entity
{
    public string Name { get; set; } = string.Empty;
}

// 2. Implement your repository
public class ProductRepository : BaseEfRepository<Product>
{
    public ProductRepository(YourDbContext context) : base(context) { }
}

// 3. Register in DI
services.AddScoped<IRepository<Product>, ProductRepository>();
services.AddScoped<IUnitOfWork, EfUnitOfWork<YourDbContext>>();