refactor(application): consolidate query architecture with AutoMapper integration

- Add AutoMapper profile for CfgProfile and ProfileSqlJob entity-to-DTO mappings
- Consolidate GetProfileByIdQuery and GetAllActiveProfilesQuery into unified GetProfileQuery
- Implement flexible filtering with nullable query options (Id, Active, TypeId, ProfileName)
- Add IncludeSqlJobs option for optimized SQL job loading
- Move CfgProfileDto to Common/Dtos for better architecture alignment
- Add comprehensive unit tests for GetProfileQuery with multiple filter scenarios
- Move ISQLExecutor interface from Domain to Application layer
- Add GetByIdWithSqlJobsAsync and GetAllActiveWithSqlJobsAsync to repository
This commit is contained in:
2026-07-11 19:14:51 +02:00
parent 3f924b75b0
commit eaf24e05ee
15 changed files with 629 additions and 16 deletions

View File

@@ -1,9 +1,9 @@
using ECMJobRunner.Application.Common.Constants;
using ECMJobRunner.Application.Common.Dtos;
using ECMJobRunner.Application.Common.Exceptions;
using ECMJobRunner.Domain.Interfaces;
using ECMJobRunner.Application.Common.Interfaces;
using ECMJobRunner.Application.Common.Options;
using ECMJobRunner.Application.DEXJob;
using ECMJobRunner.Application.DEXJob.Commands;
using MediatR;
using Microsoft.Extensions.Options;
using System;

View File

@@ -1,9 +1,9 @@
using ECMJobRunner.Application.Common.Constants;
using ECMJobRunner.Application.Common.Dtos;
using ECMJobRunner.Application.Common.Exceptions;
using ECMJobRunner.Domain.Interfaces;
using ECMJobRunner.Application.Common.Interfaces;
using ECMJobRunner.Application.Common.Options;
using ECMJobRunner.Application.DEXJob;
using ECMJobRunner.Application.DEXJob.Commands;
using MediatR;
using Microsoft.Extensions.Options;
using System;

View File

@@ -1,7 +1,7 @@
using ECMJobRunner.Application.Common.Constants;
using ECMJobRunner.Application.Common.Exceptions;
using ECMJobRunner.Application.Common.Options;
using ECMJobRunner.Application.DEXJob;
using ECMJobRunner.Application.DEXJob.Commands;
using MediatR;
using Microsoft.Extensions.Options;
using ReC.Client;

View File

@@ -0,0 +1,118 @@
using System;
using System.Collections.Generic;
namespace ECMJobRunner.Application.Common.Dtos
{
/// <summary>
/// Data Transfer Object for CfgProfile entity
/// Used for querying and returning profile data
/// </summary>
public class CfgProfileDto
{
/// <summary>
/// Profile ID
/// </summary>
public long Id { get; set; }
/// <summary>
/// Active / Inactive switch
/// </summary>
public bool Active { get; set; }
/// <summary>
/// Profile name
/// </summary>
public string ProfileName { get; set; } = null!;
/// <summary>
/// Profile type: 0 = ADSync; 1 = GraphQL; 2 = SQL-Job; 3 = SQL and REST-Job
/// </summary>
public byte TypeId { get; set; }
/// <summary>
/// Schedule in Cron format
/// </summary>
public string Schedule { get; set; } = null!;
/// <summary>
/// Optional description
/// </summary>
public string? Comment { get; set; }
/// <summary>
/// Created by
/// </summary>
public string AddedWho { get; set; } = null!;
/// <summary>
/// Created at
/// </summary>
public DateTime AddedWhen { get; set; }
/// <summary>
/// Modified by
/// </summary>
public string? ChangedWho { get; set; }
/// <summary>
/// Modified at
/// </summary>
public DateTime? ChangedWhen { get; set; }
/// <summary>
/// SQL Jobs associated with this profile
/// </summary>
public List<ProfileSqlJobDto>? SqlJobs { get; set; }
}
/// <summary>
/// Data Transfer Object for ProfileSqlJob entity
/// </summary>
public class ProfileSqlJobDto
{
/// <summary>
/// SQL Job ID
/// </summary>
public long Id { get; set; }
/// <summary>
/// Profile ID (foreign key)
/// </summary>
public long ProfileId { get; set; }
/// <summary>
/// Active / Inactive switch
/// </summary>
public bool Active { get; set; }
/// <summary>
/// Execution sequence order
/// </summary>
public short Sequence { get; set; }
/// <summary>
/// Job name
/// </summary>
public string? Name { get; set; }
/// <summary>
/// SQL query for pre-check validation
/// </summary>
public string? SqlCheckQuery { get; set; }
/// <summary>
/// Main SQL query to execute
/// </summary>
public string? SqlMainQuery { get; set; }
/// <summary>
/// API command to execute
/// </summary>
public string? ApiCommand { get; set; }
/// <summary>
/// Optional description
/// </summary>
public string? Comment { get; set; }
}
}

View File

@@ -0,0 +1,20 @@
using System.Threading;
using System.Threading.Tasks;
namespace ECMJobRunner.Application.Common.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

@@ -0,0 +1,23 @@
using AutoMapper;
using ECMJobRunner.Application.Common.Dtos;
using ECMJobRunner.Domain.Entities;
namespace ECMJobRunner.Application.Common.Mapping
{
/// <summary>
/// AutoMapper profile for CfgProfile and ProfileSqlJob mappings
/// Maps domain entities to DTOs
/// </summary>
public class ProfileMappingProfile : Profile
{
public ProfileMappingProfile()
{
// CfgProfile -> CfgProfileDto
CreateMap<CfgProfile, CfgProfileDto>()
.ForMember(dest => dest.SqlJobs, opt => opt.MapFrom(src => src.SqlJobs));
// ProfileSqlJob -> ProfileSqlJobDto
CreateMap<ProfileSqlJob, ProfileSqlJobDto>();
}
}
}

View File

@@ -0,0 +1,124 @@
using AutoMapper;
using ECMJobRunner.Application.Common.Dtos;
using ECMJobRunner.Domain.Interfaces;
using MediatR;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace ECMJobRunner.Application.DEXJob.Queries
{
/// <summary>
/// Query to retrieve profiles with flexible filtering options
/// All query options are nullable - when no filters are specified, returns all profiles
/// </summary>
public class GetProfileQuery : IRequest<List<CfgProfileDto>>
{
/// <summary>
/// Profile ID to retrieve (optional)
/// When specified, returns only the profile with this ID
/// </summary>
public long? Id { get; set; }
/// <summary>
/// Filter by active status (optional)
/// When null, returns both active and inactive profiles
/// When true, returns only active profiles
/// When false, returns only inactive profiles
/// </summary>
public bool? Active { get; set; }
/// <summary>
/// Filter by profile type (optional)
/// When specified, returns only profiles with this type
/// Type: 0 = ADSync; 1 = GraphQL; 2 = SQL-Job; 3 = SQL and REST-Job
/// </summary>
public byte? TypeId { get; set; }
/// <summary>
/// Filter by profile name (optional)
/// When specified, returns profiles with matching name (case-insensitive contains)
/// </summary>
public string? ProfileName { get; set; }
/// <summary>
/// Include associated SQL jobs in the result (optional)
/// Default: true
/// </summary>
public bool IncludeSqlJobs { get; set; } = true;
}
/// <summary>
/// Handler for GetProfileQuery
/// Retrieves profiles with optional filtering and uses AutoMapper for DTO mapping
/// </summary>
public class GetProfileQueryHandler : IRequestHandler<GetProfileQuery, List<CfgProfileDto>>
{
private readonly ICfgProfileRepository _profileRepository;
private readonly IMapper _mapper;
public GetProfileQueryHandler(ICfgProfileRepository profileRepository, IMapper mapper)
{
_profileRepository = profileRepository;
_mapper = mapper;
}
public async Task<List<CfgProfileDto>> Handle(GetProfileQuery request, CancellationToken cancellationToken)
{
IEnumerable<Domain.Entities.CfgProfile> profiles;
// If ID is specified, get single profile by ID
if (request.Id.HasValue)
{
var profile = request.IncludeSqlJobs
? await _profileRepository.GetByIdWithSqlJobsAsync(request.Id.Value, cancellationToken)
: await _profileRepository.GetByIdAsync(request.Id.Value, cancellationToken);
profiles = profile != null ? new[] { profile } : [];
}
// Otherwise, get profiles with filters
else
{
// Get all profiles with SQL jobs if requested
if (request.IncludeSqlJobs)
{
// If Active filter is specified and true, use optimized method
if (request.Active.HasValue && request.Active.Value)
{
profiles = await _profileRepository.GetAllActiveWithSqlJobsAsync(cancellationToken);
}
else
{
// Generic query with filters
profiles = await _profileRepository.FindAsync(p => true, cancellationToken);
}
}
else
{
profiles = await _profileRepository.GetAllAsync(cancellationToken);
}
// Apply filters
if (request.Active.HasValue)
{
profiles = profiles.Where(p => p.Active == request.Active.Value);
}
if (request.TypeId.HasValue)
{
profiles = profiles.Where(p => p.TypeId == request.TypeId.Value);
}
if (!string.IsNullOrWhiteSpace(request.ProfileName))
{
var searchName = request.ProfileName.ToLowerInvariant();
profiles = profiles.Where(p => p.ProfileName.ToLowerInvariant().Contains(searchName));
}
}
// Use AutoMapper to map entities to DTOs
return _mapper.Map<List<CfgProfileDto>>(profiles.ToList());
}
}
}