Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b09777252b |
@@ -1,160 +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:
|
|
||||||
- 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
|
|
||||||
- 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:
|
|
||||||
- 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.
|
|
||||||
- Keep diffs small, targeted, and consistent with existing style.
|
|
||||||
- Do not merge or publish during development.
|
|
||||||
|
|
||||||
### Issue lifecycle
|
|
||||||
|
|
||||||
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 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.
|
|
||||||
10. Delivery and release manager handles commit, push, PRs, merge, and TeamCity publish validation.
|
|
||||||
|
|
||||||
### Task rules
|
|
||||||
|
|
||||||
- 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.
|
|
||||||
|
|
||||||
## 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
|
|
||||||
- **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` matches remote
|
|
||||||
- leave the repository in a clean end state
|
|
||||||
|
|
||||||
### Spent Time
|
|
||||||
|
|
||||||
- Do not track spent time — this repo uses Gitea, not YouTrack.
|
|
||||||
|
|
||||||
### 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 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.
|
|
||||||
|
|
||||||
## 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.
|
|
||||||
@@ -6,8 +6,8 @@
|
|||||||
<!-- HrynCo shared packages -->
|
<!-- HrynCo shared packages -->
|
||||||
<PackageVersion Include="HrynCo.Common" Version="1.0.0" />
|
<PackageVersion Include="HrynCo.Common" Version="1.0.0" />
|
||||||
<!-- Entity Framework Core -->
|
<!-- Entity Framework Core -->
|
||||||
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="10.0.7" />
|
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="9.0.5" />
|
||||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.7" />
|
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.5" />
|
||||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.7" />
|
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.5" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -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; }
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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,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();
|
||||||
|
|
||||||
|
|||||||
@@ -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));
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,227 +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: false);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (save)
|
|
||||||
{
|
|
||||||
DbContext.SaveChanges();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
{
|
|
||||||
TEntity? entity = GetById(id);
|
|
||||||
if (entity != null)
|
|
||||||
{
|
|
||||||
DoRemove(entity);
|
|
||||||
}
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}")
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,204 +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>>();
|
|
||||||
```
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user