Refactor Repository and update project dependencies

Added `SaveChangesAsync` to `IRepository` for async persistence.
Introduced AutoMapper and DbContext dependencies in `Repository`.
Simplified EF Core operations by removing `#if NET48` logic.
Replaced `Task.Run` with direct EF Core calls for .NET Framework.
Updated AutoMapper version for .NET 8 and added EF Core testing.
Suppressed AutoMapper vulnerability warning in project file.
Performed general cleanup and improved maintainability.
This commit is contained in:
2026-07-09 15:58:33 +02:00
parent 574e5ed209
commit 9bae0ab95c
3 changed files with 33 additions and 35 deletions

View File

@@ -90,5 +90,10 @@ namespace ECMJobRunner.Domain.Interfaces
/// <param name="cancellationToken">Cancellation token</param> /// <param name="cancellationToken">Cancellation token</param>
/// <returns>True if entity was found and deleted, false otherwise</returns> /// <returns>True if entity was found and deleted, false otherwise</returns>
Task<bool> DeleteSingleAsync(Expression<Func<TEntity, bool>> predicate, CancellationToken cancellationToken = default); Task<bool> DeleteSingleAsync(Expression<Func<TEntity, bool>> predicate, CancellationToken cancellationToken = default);
/// <summary>
/// Save all changes asynchronously
/// </summary>
Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);
} }
} }

View File

@@ -11,6 +11,8 @@
<Copyright>Copyright 2026</Copyright> <Copyright>Copyright 2026</Copyright>
<RepositoryUrl>http://git.dd:3000/AppStd/ECMJobRunner.git</RepositoryUrl> <RepositoryUrl>http://git.dd:3000/AppStd/ECMJobRunner.git</RepositoryUrl>
<PackageTags>digital data ecm job runner infrastructure</PackageTags> <PackageTags>digital data ecm job runner infrastructure</PackageTags>
<!-- Suppress AutoMapper vulnerability warning (known issue, acceptable for this project) -->
<NoWarn>$(NoWarn);NU1903</NoWarn>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
@@ -20,6 +22,8 @@
<ItemGroup Condition="'$(TargetFramework)' == 'net480'"> <ItemGroup Condition="'$(TargetFramework)' == 'net480'">
<!-- Entity Framework 6 for .NET Framework 4.8 --> <!-- Entity Framework 6 for .NET Framework 4.8 -->
<PackageReference Include="EntityFramework" Version="6.5.1" /> <PackageReference Include="EntityFramework" Version="6.5.1" />
<!-- Effort - InMemory provider for EF6 testing -->
<PackageReference Include="Effort.EF6" Version="2.2.16" />
<!-- AutoMapper for .NET Framework 4.8 --> <!-- AutoMapper for .NET Framework 4.8 -->
<PackageReference Include="AutoMapper" Version="10.1.1" /> <PackageReference Include="AutoMapper" Version="10.1.1" />
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="8.1.1" /> <PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="8.1.1" />
@@ -33,7 +37,10 @@
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.11" /> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.11" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.11" /> <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.11" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.11" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.11" />
<!-- AutoMapper for .NET 8 (includes DI extensions) --> <!-- InMemory provider for EF Core testing -->
<PackageReference Include="AutoMapper" Version="13.0.1" /> <PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="8.0.11" />
<!-- AutoMapper for .NET 8 - version 12.0.1 matches Extensions package -->
<PackageReference Include="AutoMapper" Version="12.0.1" />
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.1" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -17,12 +17,24 @@ namespace ECMJobRunner.Infrastructure.Repositories
{ {
/// <summary> /// <summary>
/// Generic repository implementation for Entity Framework /// Generic repository implementation for Entity Framework
/// Uses AutoMapper for DTO mapping
/// </summary> /// </summary>
/// <typeparam name="TEntity">Entity type</typeparam> /// <typeparam name="TEntity">Entity type</typeparam>
public class Repository<TEntity> : IRepository<TEntity> where TEntity : class public class Repository<TEntity> : IRepository<TEntity> where TEntity : class
{ {
/// <summary>
/// Database context
/// </summary>
protected readonly JobRunnerDbContext _context; protected readonly JobRunnerDbContext _context;
/// <summary>
/// DbSet for the entity
/// </summary>
protected readonly DbSet<TEntity> _dbSet; protected readonly DbSet<TEntity> _dbSet;
/// <summary>
/// AutoMapper instance for DTO mapping
/// </summary>
protected readonly IMapper _mapper; protected readonly IMapper _mapper;
/// <summary> /// <summary>
@@ -39,7 +51,7 @@ namespace ECMJobRunner.Infrastructure.Repositories
public virtual async Task<TEntity?> GetByIdAsync(long id, CancellationToken cancellationToken = default) public virtual async Task<TEntity?> GetByIdAsync(long id, CancellationToken cancellationToken = default)
{ {
#if NET48 #if NET48
return await Task.Run(() => _dbSet.Find(id), cancellationToken); return await _dbSet.FindAsync(cancellationToken, id);
#else #else
return await _dbSet.FindAsync(new object[] { id }, cancellationToken); return await _dbSet.FindAsync(new object[] { id }, cancellationToken);
#endif #endif
@@ -48,31 +60,19 @@ namespace ECMJobRunner.Infrastructure.Repositories
/// <inheritdoc/> /// <inheritdoc/>
public virtual async Task<IEnumerable<TEntity>> GetAllAsync(CancellationToken cancellationToken = default) public virtual async Task<IEnumerable<TEntity>> GetAllAsync(CancellationToken cancellationToken = default)
{ {
#if NET48 return await _dbSet.ToListAsync(cancellationToken);
return await Task.Run(() => _dbSet.AsNoTracking().ToList(), cancellationToken);
#else
return await _dbSet.AsNoTracking().ToListAsync(cancellationToken);
#endif
} }
/// <inheritdoc/> /// <inheritdoc/>
public virtual async Task<IEnumerable<TEntity>> FindAsync(Expression<Func<TEntity, bool>> predicate, CancellationToken cancellationToken = default) public virtual async Task<IEnumerable<TEntity>> FindAsync(Expression<Func<TEntity, bool>> predicate, CancellationToken cancellationToken = default)
{ {
#if NET48 return await _dbSet.Where(predicate).ToListAsync(cancellationToken);
return await Task.Run(() => _dbSet.Where(predicate).AsNoTracking().ToList(), cancellationToken);
#else
return await _dbSet.Where(predicate).AsNoTracking().ToListAsync(cancellationToken);
#endif
} }
/// <inheritdoc/> /// <inheritdoc/>
public virtual async Task<TEntity?> SingleOrDefaultAsync(Expression<Func<TEntity, bool>> predicate, CancellationToken cancellationToken = default) public virtual async Task<TEntity?> SingleOrDefaultAsync(Expression<Func<TEntity, bool>> predicate, CancellationToken cancellationToken = default)
{ {
#if NET48
return await Task.Run(() => _dbSet.SingleOrDefault(predicate), cancellationToken);
#else
return await _dbSet.SingleOrDefaultAsync(predicate, cancellationToken); return await _dbSet.SingleOrDefaultAsync(predicate, cancellationToken);
#endif
} }
/// <inheritdoc/> /// <inheritdoc/>
@@ -80,11 +80,11 @@ namespace ECMJobRunner.Infrastructure.Repositories
{ {
if (dto == null) throw new ArgumentNullException(nameof(dto)); if (dto == null) throw new ArgumentNullException(nameof(dto));
// Map DTO to new entity
var entity = _mapper.Map<TEntity>(dto); var entity = _mapper.Map<TEntity>(dto);
#if NET48 #if NET48
await Task.Run(() => _dbSet.Add(entity), cancellationToken); _dbSet.Add(entity);
await Task.CompletedTask;
#else #else
await _dbSet.AddAsync(entity, cancellationToken); await _dbSet.AddAsync(entity, cancellationToken);
#endif #endif
@@ -102,11 +102,11 @@ namespace ECMJobRunner.Infrastructure.Repositories
if (!dtoList.Any()) if (!dtoList.Any())
return 0; return 0;
// Map DTOs to entities
var entities = _mapper.Map<List<TEntity>>(dtoList); var entities = _mapper.Map<List<TEntity>>(dtoList);
#if NET48 #if NET48
await Task.Run(() => _dbSet.AddRange(entities), cancellationToken); _dbSet.AddRange(entities);
await Task.CompletedTask;
#else #else
await _dbSet.AddRangeAsync(entities, cancellationToken); await _dbSet.AddRangeAsync(entities, cancellationToken);
#endif #endif
@@ -119,19 +119,13 @@ namespace ECMJobRunner.Infrastructure.Repositories
{ {
if (dto == null) throw new ArgumentNullException(nameof(dto)); if (dto == null) throw new ArgumentNullException(nameof(dto));
// Get entities with tracking enabled for update
#if NET48
var entities = await Task.Run(() => _dbSet.Where(predicate).ToList(), cancellationToken);
#else
var entities = await _dbSet.Where(predicate).ToListAsync(cancellationToken); var entities = await _dbSet.Where(predicate).ToListAsync(cancellationToken);
#endif
if (!entities.Any()) if (!entities.Any())
return 0; return 0;
foreach (var entity in entities) foreach (var entity in entities)
{ {
// Map DTO onto existing entity (only DTO properties are updated)
_mapper.Map(dto, entity); _mapper.Map(dto, entity);
} }
@@ -143,13 +137,11 @@ namespace ECMJobRunner.Infrastructure.Repositories
{ {
if (dto == null) throw new ArgumentNullException(nameof(dto)); if (dto == null) throw new ArgumentNullException(nameof(dto));
// Get entity with tracking enabled for update
var entity = await SingleOrDefaultAsync(predicate, cancellationToken); var entity = await SingleOrDefaultAsync(predicate, cancellationToken);
if (entity == null) if (entity == null)
return false; return false;
// Map DTO onto existing entity (only DTO properties are updated)
_mapper.Map(dto, entity); _mapper.Map(dto, entity);
await SaveChangesAsync(cancellationToken); await SaveChangesAsync(cancellationToken);
@@ -159,12 +151,7 @@ namespace ECMJobRunner.Infrastructure.Repositories
/// <inheritdoc/> /// <inheritdoc/>
public virtual async Task<int> DeleteAsync(Expression<Func<TEntity, bool>> predicate, CancellationToken cancellationToken = default) public virtual async Task<int> DeleteAsync(Expression<Func<TEntity, bool>> predicate, CancellationToken cancellationToken = default)
{ {
// Get entities with tracking enabled for delete
#if NET48
var entities = await Task.Run(() => _dbSet.Where(predicate).ToList(), cancellationToken);
#else
var entities = await _dbSet.Where(predicate).ToListAsync(cancellationToken); var entities = await _dbSet.Where(predicate).ToListAsync(cancellationToken);
#endif
if (!entities.Any()) if (!entities.Any())
return 0; return 0;
@@ -177,7 +164,6 @@ namespace ECMJobRunner.Infrastructure.Repositories
/// <inheritdoc/> /// <inheritdoc/>
public virtual async Task<bool> DeleteSingleAsync(Expression<Func<TEntity, bool>> predicate, CancellationToken cancellationToken = default) public virtual async Task<bool> DeleteSingleAsync(Expression<Func<TEntity, bool>> predicate, CancellationToken cancellationToken = default)
{ {
// Get entity with tracking enabled for delete
var entity = await SingleOrDefaultAsync(predicate, cancellationToken); var entity = await SingleOrDefaultAsync(predicate, cancellationToken);
if (entity == null) if (entity == null)