1 Commits

15 changed files with 177 additions and 750 deletions
-167
View File
@@ -1,167 +0,0 @@
# hrynco-ef Agent Rules
This file is the authoritative source for hrynco-ef workflow, delivery, and role rules.
## 1. Startup
- 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.
### Operational cadence
- TeamCity polling: do not poll more often than once every 30 seconds.
- If no relevant build is visible after 60 seconds, warn once and continue waiting normally.
- Play the completion sound after multi-step work.
- Play the question sound before or with any blocking user question.
### Startup report
Print one startup report line per check:
- `✅` success, `⚠️` failure
- include a service emoji on every line
- use bright colors when supported
- after all checks, print `✅` or `⚠️` for initialization
Required report items:
- Workflow rules loaded
- YouTrack auth + reachability
- Gitea auth + reachability
- Gitea resource access (`hrynco/hrynco-ef`)
- TeamCity auth + reachability
- Initialization
## 2. Workflow
- `AGENTS.md` is the source of truth; treat other docs as references only.
- Do not start a new task before the current one is complete.
- Default development branch: `development`.
- Before task work:
- be on local `development`
- local `development` must be clean
- local `development` must match remote
- if not, fix that first
- On task start:
- set YouTrack state to `In Progress`
- create a feature branch from `development`
- name the branch after the task
- Work only in the feature branch.
- Keep diffs small, targeted, and consistent with existing style.
- Do not merge or publish during development.
### Issue lifecycle
1. Product analyst creates/refines the YouTrack task.
2. Developer takes the task 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.
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.
10. Delivery and release manager handles commit, push, PRs, merge, and TeamCity publish validation.
### Task rules
- Create the YouTrack task 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.
## 3. Validation
- Build before finishing.
- Run relevant tests and checks.
- Verify no regressions.
- Do not guess about validation; use actual evidence.
## 4. Completion
After explicit user validation approval:
- commit staged changes
- 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
- switch back to `development`
- pull latest `development`
- ensure local `development` and `main` match their remotes
- 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.
### Command output
- Keep command summaries concise.
- Do not dump raw output unless needed for diagnosis or explicitly requested.
## 5. Communication
- 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 emojis intentionally for scanning, not mechanically.
- Commit messages must be Conventional Commit style, lowercase subject, short body, and final line `Ref: IT-<number>`.
## 6. Audio
- Play `C:\Sounds\AgentSounds\warcraft_2_jobs_done.mp3` after multi-step work completes.
- Play `C:\Sounds\AgentSounds\peasantdeath.mp3` before or with any blocking user question.
- Use `System.Windows.Media.MediaPlayer` for MP3 playback on Windows.
## 7. Engineering
- This repo publishes two NuGet packages: `HrynCo.DAL.Abstract` and `HrynCo.DAL.EF`.
- Package versions are injected by TeamCity at build time via `Directory.Build.props`.
- Packages are pushed to nuget.org by the `HrynCo / HrynCo.EF / publish` TC build.
- Do not hardcode versions in `.csproj` files.
- `Directory.Packages.props` centralizes all dependency versions.
- Keep `HrynCo.DAL.Abstract` free of EF Core dependencies — it must remain infrastructure-agnostic.
- `HrynCo.DAL.EF` may depend on EF Core and `HrynCo.DAL.Abstract`.
## 8. Role rules
### Developer
- Implement end to end.
- Do not leave partial work.
- Do not refactor unrelated code.
- Validate before finishing.
### Code reviewer
- Focus on correctness, regressions, security, and missing tests.
- Report only meaningful findings.
- Do not modify code.
### Product analyst
- Make tasks clear, scoped, and executable.
- Avoid implementation assumptions unless necessary.
### Tester
- Validate happy path, failure path, and adjacent regressions.
- Report exact steps, actual result, expected result, and severity.
### Delivery and release manager
- Handle PR flow and publish validation.
- Do not trigger the publish build manually — it runs automatically on merge to `main`.
- Mark delivered only after TC publish build succeeds.
+2 -46
View File
@@ -1,47 +1,9 @@
namespace HrynCo.DAL.Abstract.Entities; namespace HrynCo.DAL.Abstract.Entities;
public interface IEntity
{
DateTimeOffset Created { get; set; }
object Id { get; set; }
DateTimeOffset? Updated { get; set; }
}
public interface IEntity<TId> : IEntity where TId : struct
{
new TId Id { get; set; }
}
[Serializable] [Serializable]
public class Entity<TId> : IEntity<TId> where TId : struct public abstract class Entity<TId> : IEntity<TId> where TId : struct
{ {
protected Entity()
{
}
public Entity(TId id)
{
Id = id;
}
public TId Id { get; set; } public TId Id { get; set; }
object IEntity.Id
{
get => Id;
set
{
if (value is TId typedValue)
{
Id = typedValue;
}
else
{
throw new InvalidCastException($"Cannot cast value of type {value.GetType()} to {typeof(TId)}.");
}
}
}
public DateTimeOffset Created { get; set; } public DateTimeOffset Created { get; set; }
public DateTimeOffset? Updated { get; set; } public DateTimeOffset? Updated { get; set; }
} }
@@ -55,13 +17,7 @@ public abstract class Entity : Entity<Guid>
} }
protected Entity(Guid id) protected Entity(Guid id)
: base(id)
{ {
Id = id;
} }
} }
[Serializable]
public abstract class NamedEntity : Entity
{
public required string Name { get; set; }
}
+12
View File
@@ -0,0 +1,12 @@
namespace HrynCo.DAL.Abstract.Entities;
public interface IEntity
{
DateTimeOffset Created { get; set; }
DateTimeOffset? Updated { get; set; }
}
public interface IEntity<TId> : IEntity where TId : struct
{
TId Id { get; set; }
}
+1 -3
View File
@@ -1,10 +1,8 @@
namespace HrynCo.DAL.Abstract; namespace HrynCo.DAL.Abstract;
using System.Threading;
using System.Threading.Tasks;
public interface IUnitOfWork public interface IUnitOfWork
{ {
Task SaveChangesAsync(CancellationToken cancellationToken = default);
Task<ITransaction> BeginTransactionAsync(CancellationToken cancellationToken = default); Task<ITransaction> BeginTransactionAsync(CancellationToken cancellationToken = default);
ITransaction? GetCurrentTransaction(); ITransaction? GetCurrentTransaction();
-9
View File
@@ -1,9 +0,0 @@
namespace HrynCo.DAL.Abstract;
public sealed class PagedResult<T>
{
public required IReadOnlyList<T> Items { get; init; }
public required int Page { get; init; }
public required int PageSize { get; init; }
public required int TotalCount { get; init; }
}
@@ -0,0 +1,6 @@
namespace HrynCo.DAL.EF.Converters;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
internal class UtcValueConverter()
: ValueConverter<DateTime, DateTime>(v => v, v => DateTime.SpecifyKind(v, DateTimeKind.Utc));
+74
View File
@@ -0,0 +1,74 @@
namespace HrynCo.DAL.EF.Core;
using HrynCo.Common;
using HrynCo.DAL.Abstract.Entities;
using HrynCo.DAL.EF.Converters;
using HrynCo.DAL.EF.Exceptions;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using Microsoft.EntityFrameworkCore.Metadata;
public abstract class BaseDbContext : DbContext
{
private readonly IClock _clock;
protected BaseDbContext(DbContextOptions options, IClock clock)
: base(options)
{
_clock = clock;
}
public override int SaveChanges()
{
ApplyTimestamps();
return base.SaveChanges();
}
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
{
ApplyTimestamps();
return await base.SaveChangesAsync(cancellationToken);
}
private void ApplyTimestamps()
{
DateTimeOffset now = _clock.UtcNow;
foreach (EntityEntry<IEntity> entry in ChangeTracker.Entries<IEntity>())
{
switch (entry.State)
{
case EntityState.Added:
entry.Entity.Created = now;
break;
case EntityState.Modified:
entry.Entity.Updated = now;
break;
case EntityState.Detached:
case EntityState.Unchanged:
case EntityState.Deleted:
break;
default:
throw new UnexpectedEntityStateException(entry.State);
}
}
}
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
configurationBuilder.Properties<DateTime>().HaveConversion<UtcValueConverter>();
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
foreach (IMutableForeignKey relationship in modelBuilder.Model.GetEntityTypes()
.SelectMany(e => e.GetForeignKeys()))
{
relationship.DeleteBehavior = DeleteBehavior.Restrict;
}
modelBuilder.ApplyConfigurationsFromAssembly(GetType().Assembly);
base.OnModelCreating(modelBuilder);
}
}
-218
View File
@@ -1,218 +0,0 @@
namespace HrynCo.DAL.EF.Core;
using System.Diagnostics.CodeAnalysis;
using System.Linq.Expressions;
using HrynCo.DAL.Abstract.Entities;
using Microsoft.EntityFrameworkCore;
[SuppressMessage("Major Code Smell", "S2436:Reduce the number of generic parameters",
Justification = "Generic design is intentional and improves reusability")]
public abstract class BaseEfRepository<TDbContext, TEntity, TEntityId> :
IEfRepository<TEntity, TEntityId>
where TEntity : class, IEntity<TEntityId>
where TDbContext : DbContext
where TEntityId : struct
{
protected BaseEfRepository(TDbContext dbContext)
{
DbContext = dbContext;
DbSet = DbContext.Set<TEntity>();
}
public TDbContext DbContext { get; }
private DbSet<TEntity> DbSet { get; }
public TEntity Add(TEntity entity, bool save = true)
{
var entityEntry = DbSet.Add(entity);
TEntity addedEntity = entityEntry.Entity;
if (save)
{
DbContext.SaveChanges();
}
return addedEntity;
}
public void Add(TEntity[] entities, bool save = true)
{
foreach (TEntity entity in entities)
{
Add(entity, save);
}
}
public async Task<TEntity> AddAsync(TEntity entity, bool save = true)
{
var entityEntry = await DbSet.AddAsync(entity);
TEntity addedEntity = entityEntry.Entity;
if (save)
{
await DbContext.SaveChangesAsync();
}
return addedEntity;
}
public void Delete(IEnumerable<TEntityId> id)
{
foreach (TEntityId entityId in id)
{
Delete([GetById(entityId)!]);
}
}
public void Delete(TEntityId id)
{
Delete([id]);
}
public virtual void Delete(TEntity[] entities)
{
DoRemove(entities);
DbContext.SaveChanges();
}
public void Delete(TEntity entity)
{
DoRemove(entity);
DbContext.SaveChanges();
}
public async Task DeleteAsync(TEntityId id)
{
Delete(id);
await DbContext.SaveChangesAsync();
}
public async Task DeleteAsync(IEnumerable<TEntityId> id)
{
foreach (TEntityId entityId in id)
{
await DeleteAsync(entityId);
}
}
public async Task DeleteAsync(TEntity entity)
{
DoRemove(entity);
await DbContext.SaveChangesAsync();
}
public virtual async Task<TEntity?> GetByIdAsync(TEntityId id)
{
TEntity? entity = await DbSet.FindAsync(id);
return entity;
}
public async Task UpdateAsync(TEntity entity, bool save = true)
{
DbSet.Attach(entity);
DbContext.Entry(entity).State = EntityState.Modified;
if (save)
{
await DbContext.SaveChangesAsync();
}
}
public virtual void Update(TEntity entity, bool save = true)
{
DbSet.Attach(entity);
DbContext.Entry(entity).State = EntityState.Modified;
if (save)
{
DbContext.SaveChanges();
}
}
public async Task SaveChangesAsync()
{
await DbContext.SaveChangesAsync();
}
public IQueryable<TEntity> Get(
Expression<Func<TEntity, bool>>? filter = null,
Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>>? orderBy = null,
string includeProperties = "")
{
IQueryable<TEntity> query = DbContext.Set<TEntity>();
if (filter != null)
{
query = query.Where(filter);
}
if (!string.IsNullOrWhiteSpace(includeProperties))
{
foreach (string includeProperty in includeProperties.Split(new[]
{
','
},
StringSplitOptions.RemoveEmptyEntries))
{
query = query.Include(includeProperty);
}
}
if (orderBy != null)
{
return orderBy(query).AsQueryable();
}
return query.AsQueryable();
}
public virtual IQueryable<TEntity> GetAll()
{
return DbContext.Set<TEntity>().AsQueryable();
}
public void RemoveRange(IEnumerable<TEntity> entities)
{
DbContext.Set<TEntity>().RemoveRange(entities);
}
public void Remove(TEntity entity)
{
DbContext.Set<TEntity>().Remove(entity);
}
public async Task<List<TEntity>> GetAllAsync()
{
return await DbContext.Set<TEntity>().ToListAsync();
}
public async Task<bool> Exists(TEntityId id)
{
return await DbContext.Set<TEntity>().AnyAsync(e => e.Id.Equals(id));
}
public virtual TEntity? GetById(TEntityId id)
{
return DbContext.Set<TEntity>().Find(id);
}
public void ClearChangeTracker()
{
DbContext.ChangeTracker.Clear();
}
protected virtual void DoRemove(TEntity[] entities)
{
foreach (TEntity entity in entities)
{
DoRemove(entity);
}
}
protected virtual void DoRemove(TEntity entity)
{
DbSet.Remove(entity);
}
}
-25
View File
@@ -1,25 +0,0 @@
namespace HrynCo.DAL.EF.Core;
using System.Diagnostics.CodeAnalysis;
using HrynCo.DAL.Abstract.Entities;
using Microsoft.EntityFrameworkCore;
[SuppressMessage("Major Code Smell", "S2436:Reduce the number of generic parameters",
Justification = "Generic design is intentional and improves reusability")]
public abstract class BaseRepository<TEfRepository, TDbContext, TEntity, TEntityId>
where TEntity : class, IEntity<TEntityId>
where TDbContext : DbContext
where TEfRepository : BaseEfRepository<TDbContext, TEntity, TEntityId>
where TEntityId : struct
{
private readonly Lazy<TEfRepository> _lazyEfRepository;
protected BaseRepository()
{
_lazyEfRepository = new Lazy<TEfRepository>(CreateEfRepository);
}
protected TEfRepository EfRepository => _lazyEfRepository.Value;
protected abstract TEfRepository CreateEfRepository();
}
+46
View File
@@ -0,0 +1,46 @@
using System.Linq.Expressions;
using Microsoft.EntityFrameworkCore;
namespace HrynCo.DAL.EF.Core;
public abstract class EfRepository<TDbContext, TEntity>
where TDbContext : DbContext
where TEntity : class
{
protected TDbContext DbContext { get; }
protected DbSet<TEntity> DbSet { get; }
protected EfRepository(TDbContext dbContext)
{
DbContext = dbContext;
DbSet = dbContext.Set<TEntity>();
}
protected async Task AddAsync(TEntity entity, CancellationToken ct = default)
{
await DbSet.AddAsync(entity, ct);
}
protected async Task AddRangeAsync(IEnumerable<TEntity> entities, CancellationToken ct = default)
{
await DbSet.AddRangeAsync(entities, ct);
}
protected void Update(TEntity entity)
{
DbSet.Update(entity);
}
protected void Delete(TEntity entity)
{
DbSet.Remove(entity);
}
protected void DeleteRange(IEnumerable<TEntity> entities)
{
DbSet.RemoveRange(entities);
}
protected Task<bool> ExistsAsync(Expression<Func<TEntity, bool>> predicate, CancellationToken ct = default) =>
DbSet.AnyAsync(predicate, ct);
}
+10 -10
View File
@@ -1,29 +1,29 @@
namespace HrynCo.DAL.EF.Core;
using HrynCo.DAL.Abstract; using HrynCo.DAL.Abstract;
using Microsoft.EntityFrameworkCore.Storage; using Microsoft.EntityFrameworkCore.Storage;
public class EfTransactionAdapter : ITransaction namespace HrynCo.DAL.EF.Core;
{
private readonly IDbContextTransaction _efTransaction;
public EfTransactionAdapter(IDbContextTransaction efTransaction) internal sealed class EfTransactionAdapter : ITransaction
{ {
_efTransaction = efTransaction; private readonly IDbContextTransaction _transaction;
internal EfTransactionAdapter(IDbContextTransaction transaction)
{
_transaction = transaction;
} }
public Task CommitAsync(CancellationToken cancellationToken = default) public Task CommitAsync(CancellationToken cancellationToken = default)
{ {
return _efTransaction.CommitAsync(cancellationToken); return _transaction.CommitAsync(cancellationToken);
} }
public Task RollbackAsync(CancellationToken cancellationToken = default) public Task RollbackAsync(CancellationToken cancellationToken = default)
{ {
return _efTransaction.RollbackAsync(cancellationToken); return _transaction.RollbackAsync(cancellationToken);
} }
public ValueTask DisposeAsync() public ValueTask DisposeAsync()
{ {
return _efTransaction.DisposeAsync(); return _transaction.DisposeAsync();
} }
} }
+15 -12
View File
@@ -1,10 +1,10 @@
namespace HrynCo.DAL.EF.Core;
using HrynCo.DAL.Abstract; using HrynCo.DAL.Abstract;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Storage; using Microsoft.EntityFrameworkCore.Storage;
public class EfUnitOfWork<TDbContext> : IUnitOfWork namespace HrynCo.DAL.EF.Core;
public abstract class EfUnitOfWork<TDbContext> : IUnitOfWork
where TDbContext : DbContext where TDbContext : DbContext
{ {
private readonly TDbContext _context; private readonly TDbContext _context;
@@ -15,6 +15,11 @@ public class EfUnitOfWork<TDbContext> : IUnitOfWork
_context = context; _context = context;
} }
public Task SaveChangesAsync(CancellationToken cancellationToken = default)
{
return _context.SaveChangesAsync(cancellationToken);
}
public async Task<ITransaction> BeginTransactionAsync(CancellationToken cancellationToken = default) public async Task<ITransaction> BeginTransactionAsync(CancellationToken cancellationToken = default)
{ {
if (_currentTransaction != null) if (_currentTransaction != null)
@@ -22,8 +27,13 @@ public class EfUnitOfWork<TDbContext> : IUnitOfWork
return _currentTransaction; return _currentTransaction;
} }
IDbContextTransaction transaction = await _context.Database.BeginTransactionAsync(cancellationToken); IDbContextTransaction tx = await _context.Database.BeginTransactionAsync(cancellationToken);
_currentTransaction = new EfTransactionAdapter(transaction); _currentTransaction = new EfTransactionAdapter(tx);
return _currentTransaction;
}
public ITransaction? GetCurrentTransaction()
{
return _currentTransaction; return _currentTransaction;
} }
@@ -31,7 +41,6 @@ public class EfUnitOfWork<TDbContext> : IUnitOfWork
{ {
ITransaction? existing = GetCurrentTransaction(); ITransaction? existing = GetCurrentTransaction();
bool ownsTransaction = existing is null; bool ownsTransaction = existing is null;
ITransaction tx = existing ?? await BeginTransactionAsync(); ITransaction tx = existing ?? await BeginTransactionAsync();
try try
@@ -64,7 +73,6 @@ public class EfUnitOfWork<TDbContext> : IUnitOfWork
{ {
ITransaction? existing = GetCurrentTransaction(); ITransaction? existing = GetCurrentTransaction();
bool ownsTransaction = existing is null; bool ownsTransaction = existing is null;
ITransaction tx = existing ?? await BeginTransactionAsync(); ITransaction tx = existing ?? await BeginTransactionAsync();
try try
@@ -94,9 +102,4 @@ public class EfUnitOfWork<TDbContext> : IUnitOfWork
} }
} }
} }
public ITransaction? GetCurrentTransaction()
{
return _currentTransaction;
}
} }
-53
View File
@@ -1,53 +0,0 @@
namespace HrynCo.DAL.EF.Core;
using System.Linq.Expressions;
using HrynCo.DAL.Abstract.Entities;
public interface IEfRepository
{
void ClearChangeTracker();
}
public interface IEfRepository<TEntity, in TEntityId> : IEfRepository
where TEntity : IEntity<TEntityId> where TEntityId : struct
{
TEntity Add(TEntity entity, bool save = true);
void Add(TEntity[] entities, bool save = true);
Task<TEntity> AddAsync(TEntity entity, bool save = true);
void Delete(TEntity[] entities);
void Delete(TEntity entity);
void Delete(IEnumerable<TEntityId> id);
void Delete(TEntityId id);
Task DeleteAsync(TEntityId id);
Task DeleteAsync(IEnumerable<TEntityId> id);
Task DeleteAsync(TEntity entity);
IQueryable<TEntity> Get(
Expression<Func<TEntity, bool>>? filter = null,
Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>>? orderBy = null,
string includeProperties = "");
IQueryable<TEntity> GetAll();
void RemoveRange(IEnumerable<TEntity> entities);
void Remove(TEntity entity);
Task<List<TEntity>> GetAllAsync();
Task<bool> Exists(TEntityId id);
TEntity? GetById(TEntityId id);
Task<TEntity?> GetByIdAsync(TEntityId id);
Task UpdateAsync(TEntity entity, bool save = true);
void Update(TEntity entity, bool save = true);
Task SaveChangesAsync();
}
public interface IEfRepository<TEntity> : IEfRepository<TEntity, int>
where TEntity : IEntity<int>
{
}
@@ -0,0 +1,11 @@
namespace HrynCo.DAL.EF.Exceptions;
using Microsoft.EntityFrameworkCore;
public sealed class UnexpectedEntityStateException : Exception
{
public UnexpectedEntityStateException(EntityState state)
: base($"Unexpected entity state: {state}")
{
}
}
-207
View File
@@ -1,207 +0,0 @@
# hrynco-ef
Reusable Entity Framework Core base library for HrynCo applications.
## Solution
The solution (`hrynco-ef.slnx`) contains two 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. |
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
```mermaid
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
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).
```csharp
// 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>>();
```
## Related
- [IT-642](https://yt.grynco.com.ua/issue/IT-642) — Extract `PagedResult` builder as reusable `IQueryable<T>` extension