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:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
{
|
||||
}
|
||||
Reference in New Issue
Block a user