From d254873172da226e7876dfa6c987d5fe1da0ccb1 Mon Sep 17 00:00:00 2001 From: Anatolii Grynchuk Date: Tue, 5 May 2026 18:52:18 +0300 Subject: [PATCH] feat: initial solution with HrynCo.DAL.Abstract and HrynCo.DAL.EF - HrynCo.DAL.Abstract: IEntity, Entity base classes, ITransaction, IUnitOfWork - HrynCo.DAL.EF: EfRepository, EfUnitOfWork, EfTransactionAdapter - Directory.Packages.props with centralized EF Core 9.0.5 versions - TeamCity pipeline YAMLs for general-checks and publish Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .gitignore | 7 ++ .teamcity/pipelines/general-checks.yaml | 13 +++ .teamcity/pipelines/publish.yaml | 38 +++++++ Directory.Packages.props | 11 ++ HrynCo.DAL.Abstract/Entities/Entity.cs | 25 +++++ HrynCo.DAL.Abstract/Entities/IEntity.cs | 8 ++ .../HrynCo.DAL.Abstract.csproj | 16 +++ HrynCo.DAL.Abstract/ITransaction.cs | 7 ++ HrynCo.DAL.Abstract/IUnitOfWork.cs | 11 ++ HrynCo.DAL.EF/Core/EfRepository.cs | 46 ++++++++ HrynCo.DAL.EF/Core/EfTransactionAdapter.cs | 29 +++++ HrynCo.DAL.EF/Core/EfUnitOfWork.cs | 105 ++++++++++++++++++ HrynCo.DAL.EF/HrynCo.DAL.EF.csproj | 29 +++++ hrynco-ef.slnx | 4 + 14 files changed, 349 insertions(+) create mode 100644 .gitignore create mode 100644 .teamcity/pipelines/general-checks.yaml create mode 100644 .teamcity/pipelines/publish.yaml create mode 100644 Directory.Packages.props create mode 100644 HrynCo.DAL.Abstract/Entities/Entity.cs create mode 100644 HrynCo.DAL.Abstract/Entities/IEntity.cs create mode 100644 HrynCo.DAL.Abstract/HrynCo.DAL.Abstract.csproj create mode 100644 HrynCo.DAL.Abstract/ITransaction.cs create mode 100644 HrynCo.DAL.Abstract/IUnitOfWork.cs create mode 100644 HrynCo.DAL.EF/Core/EfRepository.cs create mode 100644 HrynCo.DAL.EF/Core/EfTransactionAdapter.cs create mode 100644 HrynCo.DAL.EF/Core/EfUnitOfWork.cs create mode 100644 HrynCo.DAL.EF/HrynCo.DAL.EF.csproj create mode 100644 hrynco-ef.slnx diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f077eae --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +bin/ +obj/ +artifacts/ +TestResults/ +.idea/ +*.DotSettings.user +Directory.Build.props diff --git a/.teamcity/pipelines/general-checks.yaml b/.teamcity/pipelines/general-checks.yaml new file mode 100644 index 0000000..5d712a2 --- /dev/null +++ b/.teamcity/pipelines/general-checks.yaml @@ -0,0 +1,13 @@ +jobs: + general-checks: + name: general-checks + runs-on: + self-hosted: + - teamcity.agent.name: ItemTrackerDotNet10 + steps: + - type: script + script-content: dotnet restore hrynco-ef.slnx + - type: script + script-content: dotnet build hrynco-ef.slnx -c Release --no-restore + - type: script + script-content: dotnet test hrynco-ef.slnx --no-build -c Release diff --git a/.teamcity/pipelines/publish.yaml b/.teamcity/pipelines/publish.yaml new file mode 100644 index 0000000..f697d1d --- /dev/null +++ b/.teamcity/pipelines/publish.yaml @@ -0,0 +1,38 @@ +# Build number pattern should be set to: 1.0.%build.counter% +# Trigger: on push to main branch + +jobs: + publish: + name: publish + runs-on: + self-hosted: + - teamcity.agent.name: ItemTrackerDotNet10 + steps: + - type: script + script-content: >- + pwsh -Command "Set-Content -Path 'Directory.Build.props' -Encoding UTF8 -Value + '%build.number%'" + + - type: script + script-content: dotnet restore hrynco-ef.slnx + + - type: script + script-content: dotnet build hrynco-ef.slnx -c Release --no-restore + + - type: script + script-content: dotnet pack hrynco-ef.slnx -c Release --no-build -o artifacts + + - type: script + script-content: >- + dotnet nuget push "artifacts/HrynCo.DAL.Abstract.*.nupkg" + --api-key %nuget-api-key% + --source https://api.nuget.org/v3/index.json + + - type: script + script-content: >- + dotnet nuget push "artifacts/HrynCo.DAL.EF.*.nupkg" + --api-key %nuget-api-key% + --source https://api.nuget.org/v3/index.json + +secrets: + nuget-api-key: credentialsJSON:a414ca02-733e-4588-9a5d-e0f0f5653d48 diff --git a/Directory.Packages.props b/Directory.Packages.props new file mode 100644 index 0000000..175b4c7 --- /dev/null +++ b/Directory.Packages.props @@ -0,0 +1,11 @@ + + + true + + + + + + + + diff --git a/HrynCo.DAL.Abstract/Entities/Entity.cs b/HrynCo.DAL.Abstract/Entities/Entity.cs new file mode 100644 index 0000000..84f8738 --- /dev/null +++ b/HrynCo.DAL.Abstract/Entities/Entity.cs @@ -0,0 +1,25 @@ +namespace HrynCo.DAL.Abstract.Entities; + +[Serializable] +public abstract class Entity : IEntity where TId : struct +{ + public TId Id { get; set; } + public DateTimeOffset Created { get; set; } + public DateTimeOffset? Updated { get; set; } +} + +[Serializable] +public abstract class Entity : Entity +{ + protected Entity() + { + Id = Guid.NewGuid(); + Created = DateTimeOffset.UtcNow; + } + + protected Entity(Guid id) + { + Id = id; + Created = DateTimeOffset.UtcNow; + } +} diff --git a/HrynCo.DAL.Abstract/Entities/IEntity.cs b/HrynCo.DAL.Abstract/Entities/IEntity.cs new file mode 100644 index 0000000..86c256a --- /dev/null +++ b/HrynCo.DAL.Abstract/Entities/IEntity.cs @@ -0,0 +1,8 @@ +namespace HrynCo.DAL.Abstract.Entities; + +public interface IEntity where TId : struct +{ + TId Id { get; set; } + DateTimeOffset Created { get; set; } + DateTimeOffset? Updated { get; set; } +} diff --git a/HrynCo.DAL.Abstract/HrynCo.DAL.Abstract.csproj b/HrynCo.DAL.Abstract/HrynCo.DAL.Abstract.csproj new file mode 100644 index 0000000..4df4fb5 --- /dev/null +++ b/HrynCo.DAL.Abstract/HrynCo.DAL.Abstract.csproj @@ -0,0 +1,16 @@ + + + + net10.0 + enable + enable + HrynCo.DAL.Abstract + HrynCo.DAL.Abstract + HrynCo + Abstract DAL contracts for HrynCo applications: entities, repository and unit-of-work interfaces. + hrynco dal abstract entity repository unitofwork + git + https://gitea.grynco.com.ua/hrynco/hrynco-ef.git + + + diff --git a/HrynCo.DAL.Abstract/ITransaction.cs b/HrynCo.DAL.Abstract/ITransaction.cs new file mode 100644 index 0000000..c60d63d --- /dev/null +++ b/HrynCo.DAL.Abstract/ITransaction.cs @@ -0,0 +1,7 @@ +namespace HrynCo.DAL.Abstract; + +public interface ITransaction : IAsyncDisposable +{ + Task CommitAsync(CancellationToken cancellationToken = default); + Task RollbackAsync(CancellationToken cancellationToken = default); +} diff --git a/HrynCo.DAL.Abstract/IUnitOfWork.cs b/HrynCo.DAL.Abstract/IUnitOfWork.cs new file mode 100644 index 0000000..aebb603 --- /dev/null +++ b/HrynCo.DAL.Abstract/IUnitOfWork.cs @@ -0,0 +1,11 @@ +namespace HrynCo.DAL.Abstract; + +public interface IUnitOfWork +{ + Task SaveChangesAsync(CancellationToken cancellationToken = default); + Task BeginTransactionAsync(CancellationToken cancellationToken = default); + ITransaction? GetCurrentTransaction(); + + Task ExecuteInTransactionAsync(Func action); + Task ExecuteInTransactionAsync(Func> action); +} diff --git a/HrynCo.DAL.EF/Core/EfRepository.cs b/HrynCo.DAL.EF/Core/EfRepository.cs new file mode 100644 index 0000000..8d4db72 --- /dev/null +++ b/HrynCo.DAL.EF/Core/EfRepository.cs @@ -0,0 +1,46 @@ +using System.Linq.Expressions; +using Microsoft.EntityFrameworkCore; + +namespace HrynCo.DAL.EF.Core; + +public abstract class EfRepository + where TDbContext : DbContext + where TEntity : class +{ + protected TDbContext DbContext { get; } + protected DbSet DbSet { get; } + + protected EfRepository(TDbContext dbContext) + { + DbContext = dbContext; + DbSet = dbContext.Set(); + } + + protected async Task AddAsync(TEntity entity, CancellationToken ct = default) + { + await DbSet.AddAsync(entity, ct); + } + + protected async Task AddRangeAsync(IEnumerable 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 entities) + { + DbSet.RemoveRange(entities); + } + + protected Task ExistsAsync(Expression> predicate, CancellationToken ct = default) => + DbSet.AnyAsync(predicate, ct); +} diff --git a/HrynCo.DAL.EF/Core/EfTransactionAdapter.cs b/HrynCo.DAL.EF/Core/EfTransactionAdapter.cs new file mode 100644 index 0000000..6408ab6 --- /dev/null +++ b/HrynCo.DAL.EF/Core/EfTransactionAdapter.cs @@ -0,0 +1,29 @@ +using HrynCo.DAL.Abstract; +using Microsoft.EntityFrameworkCore.Storage; + +namespace HrynCo.DAL.EF.Core; + +internal sealed class EfTransactionAdapter : ITransaction +{ + private readonly IDbContextTransaction _transaction; + + internal EfTransactionAdapter(IDbContextTransaction transaction) + { + _transaction = transaction; + } + + public Task CommitAsync(CancellationToken cancellationToken = default) + { + return _transaction.CommitAsync(cancellationToken); + } + + public Task RollbackAsync(CancellationToken cancellationToken = default) + { + return _transaction.RollbackAsync(cancellationToken); + } + + public ValueTask DisposeAsync() + { + return _transaction.DisposeAsync(); + } +} diff --git a/HrynCo.DAL.EF/Core/EfUnitOfWork.cs b/HrynCo.DAL.EF/Core/EfUnitOfWork.cs new file mode 100644 index 0000000..b5413ab --- /dev/null +++ b/HrynCo.DAL.EF/Core/EfUnitOfWork.cs @@ -0,0 +1,105 @@ +using HrynCo.DAL.Abstract; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Storage; + +namespace HrynCo.DAL.EF.Core; + +public abstract class EfUnitOfWork : IUnitOfWork + where TDbContext : DbContext +{ + private readonly TDbContext _context; + private EfTransactionAdapter? _currentTransaction; + + protected EfUnitOfWork(TDbContext context) + { + _context = context; + } + + public Task SaveChangesAsync(CancellationToken cancellationToken = default) + { + return _context.SaveChangesAsync(cancellationToken); + } + + public async Task BeginTransactionAsync(CancellationToken cancellationToken = default) + { + if (_currentTransaction != null) + { + return _currentTransaction; + } + + IDbContextTransaction tx = await _context.Database.BeginTransactionAsync(cancellationToken); + _currentTransaction = new EfTransactionAdapter(tx); + return _currentTransaction; + } + + public ITransaction? GetCurrentTransaction() + { + return _currentTransaction; + } + + public async Task ExecuteInTransactionAsync(Func action) + { + ITransaction? existing = GetCurrentTransaction(); + bool ownsTransaction = existing is null; + ITransaction tx = existing ?? await BeginTransactionAsync(); + + try + { + await action(); + if (ownsTransaction) + { + await tx.CommitAsync(); + } + } + catch + { + if (ownsTransaction) + { + await tx.RollbackAsync(); + } + + throw; + } + finally + { + if (ownsTransaction) + { + await tx.DisposeAsync(); + } + } + } + + public async Task ExecuteInTransactionAsync(Func> action) + { + ITransaction? existing = GetCurrentTransaction(); + bool ownsTransaction = existing is null; + ITransaction tx = existing ?? await BeginTransactionAsync(); + + try + { + TResult result = await action(); + if (ownsTransaction) + { + await tx.CommitAsync(); + } + + return result; + } + catch + { + if (ownsTransaction) + { + await tx.RollbackAsync(); + } + + throw; + } + finally + { + if (ownsTransaction) + { + await tx.DisposeAsync(); + } + } + } +} diff --git a/HrynCo.DAL.EF/HrynCo.DAL.EF.csproj b/HrynCo.DAL.EF/HrynCo.DAL.EF.csproj new file mode 100644 index 0000000..83eeae1 --- /dev/null +++ b/HrynCo.DAL.EF/HrynCo.DAL.EF.csproj @@ -0,0 +1,29 @@ + + + + net10.0 + enable + enable + HrynCo.DAL.EF + HrynCo.DAL.EF + HrynCo + Entity Framework Core base implementations for HrynCo applications: generic repository and unit-of-work. + hrynco dal ef entityframework repository unitofwork + git + https://gitea.grynco.com.ua/hrynco/hrynco-ef.git + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + diff --git a/hrynco-ef.slnx b/hrynco-ef.slnx new file mode 100644 index 0000000..ef03f8d --- /dev/null +++ b/hrynco-ef.slnx @@ -0,0 +1,4 @@ + + + +