feat: rebuild base repository hierarchy, add readme and agents

- replace EfRepository/BaseDbContext/UtcValueConverter with BaseEfRepository and BaseRepository
- add IEfRepository interface hierarchy
- consolidate IEntity into Entity.cs, remove standalone IEntity.cs
- add PagedResult
- adjust all namespaces to HrynCo.DAL.Abstract / HrynCo.DAL.EF
- add README.md with solution overview, versioning rules, class diagram
- add AGENTS.md

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Anatolii Grynchuk
2026-05-06 01:15:59 +03:00
parent 4fac3ddba9
commit 15c58522ef
15 changed files with 750 additions and 179 deletions
-76
View File
@@ -1,76 +0,0 @@
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 DateTimeOffset UtcNow => _clock.UtcNow;
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
@@ -0,0 +1,218 @@
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
@@ -0,0 +1,25 @@
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
@@ -1,46 +0,0 @@
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);
}
+9 -9
View File
@@ -1,29 +1,29 @@
namespace HrynCo.DAL.EF.Core;
using HrynCo.DAL.Abstract;
using Microsoft.EntityFrameworkCore.Storage;
namespace HrynCo.DAL.EF.Core;
internal sealed class EfTransactionAdapter : ITransaction
public class EfTransactionAdapter : ITransaction
{
private readonly IDbContextTransaction _transaction;
private readonly IDbContextTransaction _efTransaction;
internal EfTransactionAdapter(IDbContextTransaction transaction)
public EfTransactionAdapter(IDbContextTransaction efTransaction)
{
_transaction = transaction;
_efTransaction = efTransaction;
}
public Task CommitAsync(CancellationToken cancellationToken = default)
{
return _transaction.CommitAsync(cancellationToken);
return _efTransaction.CommitAsync(cancellationToken);
}
public Task RollbackAsync(CancellationToken cancellationToken = default)
{
return _transaction.RollbackAsync(cancellationToken);
return _efTransaction.RollbackAsync(cancellationToken);
}
public ValueTask DisposeAsync()
{
return _transaction.DisposeAsync();
return _efTransaction.DisposeAsync();
}
}
+12 -15
View File
@@ -1,10 +1,10 @@
namespace HrynCo.DAL.EF.Core;
using HrynCo.DAL.Abstract;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Storage;
namespace HrynCo.DAL.EF.Core;
public abstract class EfUnitOfWork<TDbContext> : IUnitOfWork
public class EfUnitOfWork<TDbContext> : IUnitOfWork
where TDbContext : DbContext
{
private readonly TDbContext _context;
@@ -15,11 +15,6 @@ public abstract class EfUnitOfWork<TDbContext> : IUnitOfWork
_context = context;
}
public Task SaveChangesAsync(CancellationToken cancellationToken = default)
{
return _context.SaveChangesAsync(cancellationToken);
}
public async Task<ITransaction> BeginTransactionAsync(CancellationToken cancellationToken = default)
{
if (_currentTransaction != null)
@@ -27,13 +22,8 @@ public abstract class EfUnitOfWork<TDbContext> : IUnitOfWork
return _currentTransaction;
}
IDbContextTransaction tx = await _context.Database.BeginTransactionAsync(cancellationToken);
_currentTransaction = new EfTransactionAdapter(tx);
return _currentTransaction;
}
public ITransaction? GetCurrentTransaction()
{
IDbContextTransaction transaction = await _context.Database.BeginTransactionAsync(cancellationToken);
_currentTransaction = new EfTransactionAdapter(transaction);
return _currentTransaction;
}
@@ -41,6 +31,7 @@ public abstract class EfUnitOfWork<TDbContext> : IUnitOfWork
{
ITransaction? existing = GetCurrentTransaction();
bool ownsTransaction = existing is null;
ITransaction tx = existing ?? await BeginTransactionAsync();
try
@@ -73,6 +64,7 @@ public abstract class EfUnitOfWork<TDbContext> : IUnitOfWork
{
ITransaction? existing = GetCurrentTransaction();
bool ownsTransaction = existing is null;
ITransaction tx = existing ?? await BeginTransactionAsync();
try
@@ -102,4 +94,9 @@ public abstract class EfUnitOfWork<TDbContext> : IUnitOfWork
}
}
}
public ITransaction? GetCurrentTransaction()
{
return _currentTransaction;
}
}
+53
View File
@@ -0,0 +1,53 @@
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>
{
}