feat(infrastructure): implement ISQLExecutor with EF6/EF Core support

- Move ISQLExecutor interface from Application to Domain layer
  - Follow Clean Architecture: Infrastructure references Domain, not Application
  - Namespace: ECMJobRunner.Domain.Interfaces
- Implement SQLExecutor service with conditional compilation
  - EF6 (net480): Database.SqlQuery<T>()
  - EF Core (net8.0): Database.SqlQueryRaw<T>()
- Register ISQLExecutor in DI (AddInfrastructure, AddInfrastructureInMemory)
  - Scoped lifetime (aligns with DbContext)
This commit is contained in:
2026-07-11 16:43:57 +02:00
parent 4767bcfbe1
commit 26ea1db09f
3 changed files with 87 additions and 0 deletions

View File

@@ -0,0 +1,20 @@
using System.Threading;
using System.Threading.Tasks;
namespace ECMJobRunner.Domain.Interfaces
{
/// <summary>
/// Interface for executing SQL queries and mapping results to DTOs
/// </summary>
public interface ISQLExecutor
{
/// <summary>
/// Executes a SQL query and maps the result to the specified type
/// </summary>
/// <typeparam name="TResult">The type to map the query result to</typeparam>
/// <param name="sql">The SQL query to execute</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>The mapped result or null if no result</returns>
Task<TResult?> ExecuteQueryAsync<TResult>(string sql, CancellationToken cancellationToken = default);
}
}

View File

@@ -8,6 +8,7 @@ using Microsoft.Extensions.DependencyInjection;
using ECMJobRunner.Domain.Interfaces; using ECMJobRunner.Domain.Interfaces;
using ECMJobRunner.Infrastructure.Data; using ECMJobRunner.Infrastructure.Data;
using ECMJobRunner.Infrastructure.Repositories; using ECMJobRunner.Infrastructure.Repositories;
using ECMJobRunner.Infrastructure.Services;
namespace ECMJobRunner.Infrastructure namespace ECMJobRunner.Infrastructure
{ {
@@ -43,6 +44,9 @@ namespace ECMJobRunner.Infrastructure
services.AddScoped<IProfileSqlJobRepository, ProfileSqlJobRepository>(); services.AddScoped<IProfileSqlJobRepository, ProfileSqlJobRepository>();
services.AddScoped<IProfileHistoryRepository, ProfileHistoryRepository>(); services.AddScoped<IProfileHistoryRepository, ProfileHistoryRepository>();
// Register SQL executor as scoped
services.AddScoped<ISQLExecutor, SQLExecutor>();
return services; return services;
} }
@@ -70,6 +74,9 @@ namespace ECMJobRunner.Infrastructure
services.AddScoped<IProfileSqlJobRepository, ProfileSqlJobRepository>(); services.AddScoped<IProfileSqlJobRepository, ProfileSqlJobRepository>();
services.AddScoped<IProfileHistoryRepository, ProfileHistoryRepository>(); services.AddScoped<IProfileHistoryRepository, ProfileHistoryRepository>();
// Register SQL executor as scoped
services.AddScoped<ISQLExecutor, SQLExecutor>();
return services; return services;
} }
#else #else
@@ -96,6 +103,9 @@ namespace ECMJobRunner.Infrastructure
services.AddScoped<IProfileSqlJobRepository, ProfileSqlJobRepository>(); services.AddScoped<IProfileSqlJobRepository, ProfileSqlJobRepository>();
services.AddScoped<IProfileHistoryRepository, ProfileHistoryRepository>(); services.AddScoped<IProfileHistoryRepository, ProfileHistoryRepository>();
// Register SQL executor as scoped
services.AddScoped<ISQLExecutor, SQLExecutor>();
return services; return services;
} }
#endif #endif

View File

@@ -0,0 +1,57 @@
#if NET48
using System.Data.Entity;
using System.Linq;
#else
using Microsoft.EntityFrameworkCore;
#endif
using ECMJobRunner.Domain.Interfaces;
using ECMJobRunner.Infrastructure.Data;
using System.Threading;
using System.Threading.Tasks;
namespace ECMJobRunner.Infrastructure.Services
{
/// <summary>
/// Entity Framework implementation of ISQLExecutor
/// Executes raw SQL queries and maps results to DTOs
/// </summary>
public class SQLExecutor : ISQLExecutor
{
private readonly JobRunnerDbContext _context;
/// <summary>
/// Initializes a new instance of SQLExecutor
/// </summary>
/// <param name="context">The database context</param>
public SQLExecutor(JobRunnerDbContext context)
{
_context = context;
}
/// <summary>
/// Executes a SQL query and maps the result to the specified type
/// </summary>
/// <typeparam name="TResult">The type to map the query result to</typeparam>
/// <param name="sql">The SQL query to execute</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>The mapped result or null if no result</returns>
public async Task<TResult?> ExecuteQueryAsync<TResult>(string sql, CancellationToken cancellationToken = default)
{
#if NET48
// Entity Framework 6 implementation
var result = await _context.Database
.SqlQuery<TResult>(sql)
.FirstOrDefaultAsync(cancellationToken);
return result;
#else
// Entity Framework Core implementation
var result = await _context.Database
.SqlQueryRaw<TResult>(sql)
.FirstOrDefaultAsync(cancellationToken);
return result;
#endif
}
}
}