feat: initial solution with HrynCo.DAL.Abstract and HrynCo.DAL.EF

- HrynCo.DAL.Abstract: IEntity<TId>, Entity base classes, ITransaction, IUnitOfWork
- HrynCo.DAL.EF: EfRepository<TDbContext,TEntity>, EfUnitOfWork<TDbContext>, 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>
This commit is contained in:
Anatolii Grynchuk
2026-05-05 18:52:18 +03:00
commit d254873172
14 changed files with 349 additions and 0 deletions
+7
View File
@@ -0,0 +1,7 @@
bin/
obj/
artifacts/
TestResults/
.idea/
*.DotSettings.user
Directory.Build.props
+13
View File
@@ -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
+38
View File
@@ -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
'<Project><PropertyGroup><Version>%build.number%</Version></PropertyGroup></Project>'"
- 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
+11
View File
@@ -0,0 +1,11 @@
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<!-- Entity Framework Core -->
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="9.0.5" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.5" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.5" />
</ItemGroup>
</Project>
+25
View File
@@ -0,0 +1,25 @@
namespace HrynCo.DAL.Abstract.Entities;
[Serializable]
public abstract class Entity<TId> : IEntity<TId> where TId : struct
{
public TId Id { get; set; }
public DateTimeOffset Created { get; set; }
public DateTimeOffset? Updated { get; set; }
}
[Serializable]
public abstract class Entity : Entity<Guid>
{
protected Entity()
{
Id = Guid.NewGuid();
Created = DateTimeOffset.UtcNow;
}
protected Entity(Guid id)
{
Id = id;
Created = DateTimeOffset.UtcNow;
}
}
+8
View File
@@ -0,0 +1,8 @@
namespace HrynCo.DAL.Abstract.Entities;
public interface IEntity<TId> where TId : struct
{
TId Id { get; set; }
DateTimeOffset Created { get; set; }
DateTimeOffset? Updated { get; set; }
}
@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>HrynCo.DAL.Abstract</RootNamespace>
<PackageId>HrynCo.DAL.Abstract</PackageId>
<Authors>HrynCo</Authors>
<Description>Abstract DAL contracts for HrynCo applications: entities, repository and unit-of-work interfaces.</Description>
<PackageTags>hrynco dal abstract entity repository unitofwork</PackageTags>
<RepositoryType>git</RepositoryType>
<RepositoryUrl>https://gitea.grynco.com.ua/hrynco/hrynco-ef.git</RepositoryUrl>
</PropertyGroup>
</Project>
+7
View File
@@ -0,0 +1,7 @@
namespace HrynCo.DAL.Abstract;
public interface ITransaction : IAsyncDisposable
{
Task CommitAsync(CancellationToken cancellationToken = default);
Task RollbackAsync(CancellationToken cancellationToken = default);
}
+11
View File
@@ -0,0 +1,11 @@
namespace HrynCo.DAL.Abstract;
public interface IUnitOfWork
{
Task SaveChangesAsync(CancellationToken cancellationToken = default);
Task<ITransaction> BeginTransactionAsync(CancellationToken cancellationToken = default);
ITransaction? GetCurrentTransaction();
Task ExecuteInTransactionAsync(Func<Task> action);
Task<TResult> ExecuteInTransactionAsync<TResult>(Func<Task<TResult>> action);
}
+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);
}
@@ -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();
}
}
+105
View File
@@ -0,0 +1,105 @@
using HrynCo.DAL.Abstract;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Storage;
namespace HrynCo.DAL.EF.Core;
public abstract class EfUnitOfWork<TDbContext> : 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<ITransaction> 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<Task> 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<TResult> ExecuteInTransactionAsync<TResult>(Func<Task<TResult>> 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();
}
}
}
}
+29
View File
@@ -0,0 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>HrynCo.DAL.EF</RootNamespace>
<PackageId>HrynCo.DAL.EF</PackageId>
<Authors>HrynCo</Authors>
<Description>Entity Framework Core base implementations for HrynCo applications: generic repository and unit-of-work.</Description>
<PackageTags>hrynco dal ef entityframework repository unitofwork</PackageTags>
<RepositoryType>git</RepositoryType>
<RepositoryUrl>https://gitea.grynco.com.ua/hrynco/hrynco-ef.git</RepositoryUrl>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\HrynCo.DAL.Abstract\HrynCo.DAL.Abstract.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" />
</ItemGroup>
</Project>
+4
View File
@@ -0,0 +1,4 @@
<Solution>
<Project Path="HrynCo.DAL.Abstract/HrynCo.DAL.Abstract.csproj" />
<Project Path="HrynCo.DAL.EF/HrynCo.DAL.EF.csproj" />
</Solution>