Refactor SQL execution and enhance envelope creation

- Updated `ISQLExecutor<TEntity>` to inherit from new `ISQLExecutor` interface for improved SQL execution flexibility.
- Added package references for `Dapper` and `DigitalData.Core` in project files.
- Modified `CreateEnvelopeCommand` to include `[BindNever]` on `UserId` for better model binding control.
- Refactored `CreateEnvelopeCommandHandler` to use `DynamicParameters` for SQL parameter handling.
- Updated `CreateEnvelopeSQL` to select only the top record for performance.
- Introduced `GetIdOrDefault` method in `ControllerExtensions` for user ID retrieval with fallback.
- Added `CreateAsync` method in `EnvelopeController` for envelope creation using `IMediator`.
- Ensured infrastructure project has necessary package references.
- Refactored `SQLExecutor` to implement new interface and simplified constructor.
- Introduced `SQLExecutorBaseEntity` for entity-specific SQL command execution.
This commit is contained in:
Developer 02
2025-05-05 10:15:36 +02:00
parent 5166f41941
commit a757749767
11 changed files with 131 additions and 29 deletions

View File

@@ -5,7 +5,7 @@
/// Provides abstraction for raw SQL execution as well as mapping the results to <typeparamref name="TEntity"/> objects. /// Provides abstraction for raw SQL execution as well as mapping the results to <typeparamref name="TEntity"/> objects.
/// </summary> /// </summary>
/// <typeparam name="TEntity">The entity type to which the SQL query results will be mapped.</typeparam> /// <typeparam name="TEntity">The entity type to which the SQL query results will be mapped.</typeparam>
public interface ISQLExecutor<TEntity> public interface ISQLExecutor<TEntity>: ISQLExecutor
{ {
/// <summary> /// <summary>
/// Executes a raw SQL query and returns an <see cref="IQuery{TEntity}"/> for further querying operations on the result. /// Executes a raw SQL query and returns an <see cref="IQuery{TEntity}"/> for further querying operations on the result.

View File

@@ -0,0 +1,29 @@
using Dapper;
namespace EnvelopeGenerator.Application.Contracts.SQLExecutor;
/// <summary>
///
/// </summary>
public interface ISQLExecutor
{
/// <summary>
/// Executes a raw SQL query and returns an <see cref="IQuery{TEntity}"/> for further querying operations on the result.
/// </summary>
/// <typeparam name="TEntity">The entity type to which the SQL query results will be mapped.</typeparam>
/// <param name="sql">The raw SQL query to execute.</param>
/// <param name="parameters">Parameters for the SQL query.</param>
/// <param name="cancellation">Optional cancellation token for the operation.</param>
/// <returns>An <see cref="IQuery{TEntity}"/> instance for further query operations on the result.</returns>
Task<IEnumerable<TEntity>> Execute<TEntity>(string sql, DynamicParameters parameters, CancellationToken cancellation = default);
/// <summary>
/// Executes a custom SQL query defined by a class that implements <see cref="ISQL{TEntity}"/> and returns an <see cref="IQuery{TEntity}"/> for further querying operations on the result.
/// </summary>
/// <typeparam name="TEntity">The entity type to which the SQL query results will be mapped.</typeparam>
/// <typeparam name="TSQL">The type of the custom SQL query class implementing <see cref="ISQL{TEntity}"/>.</typeparam>
/// <param name="parameters">Parameters for the SQL query.</param>
/// <param name="cancellation">Optional cancellation token for the operation.</param>
/// <returns>An <see cref="IQuery{TEntity}"/> instance for further query operations on the result.</returns>
Task<IEnumerable<TEntity>> Execute<TEntity, TSQL>(DynamicParameters parameters, CancellationToken cancellation = default) where TSQL : ISQL;
}

View File

@@ -13,6 +13,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Dapper" Version="2.1.66" />
<PackageReference Include="DigitalData.Core.Abstractions" Version="3.6.0" /> <PackageReference Include="DigitalData.Core.Abstractions" Version="3.6.0" />
<PackageReference Include="DigitalData.Core.Application" Version="3.2.1" /> <PackageReference Include="DigitalData.Core.Application" Version="3.2.1" />
<PackageReference Include="DigitalData.Core.Client" Version="2.0.3" /> <PackageReference Include="DigitalData.Core.Client" Version="2.0.3" />

View File

@@ -1,4 +1,5 @@
using MediatR; using MediatR;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
@@ -20,5 +21,6 @@ public record CreateEnvelopeCommand(
/// Id of receiver /// Id of receiver
/// </summary> /// </summary>
[JsonIgnore] [JsonIgnore]
[BindNever]
public int? UserId { get; set; } public int? UserId { get; set; }
}; };

View File

@@ -1,4 +1,5 @@
using AutoMapper; using AutoMapper;
using Dapper;
using EnvelopeGenerator.Application.Contracts.SQLExecutor; using EnvelopeGenerator.Application.Contracts.SQLExecutor;
using EnvelopeGenerator.Domain.Entities; using EnvelopeGenerator.Domain.Entities;
using MediatR; using MediatR;
@@ -34,16 +35,14 @@ public class CreateEnvelopeCommandHandler : IRequestHandler<CreateEnvelopeComman
/// <returns></returns> /// <returns></returns>
public async Task<CreateEnvelopeResponse?> Handle(CreateEnvelopeCommand request, CancellationToken cancellationToken) public async Task<CreateEnvelopeResponse?> Handle(CreateEnvelopeCommand request, CancellationToken cancellationToken)
{ {
object[] parameters = new object[] var parameters = new DynamicParameters();
{ parameters.Add("@UserId", request.UserId);
new SqlParameter("@UserId", request.UserId), parameters.Add("@Title", request.Title);
new SqlParameter("@Title", request.Title), parameters.Add("@TfaEnabled", request.TFAEnabled ? 1 : 0);
new SqlParameter("@TfaEnabled", request.TFAEnabled ? 1 : 0), parameters.Add("@Message", request.Message);
new SqlParameter("@Message", request.Message)
};
var envelope = await _sqlExecutor.Execute<CreateEnvelopeSQL>(cancellationToken, parameters).FirstOrDefaultAsync(); var envelopes = await _sqlExecutor.Execute<Envelope, CreateEnvelopeSQL>(parameters, cancellationToken);
return _mapper.Map<CreateEnvelopeResponse>(envelope); return _mapper.Map<CreateEnvelopeResponse>(envelopes.FirstOrDefault());
} }
} }

View File

@@ -22,7 +22,7 @@ public class CreateEnvelopeSQL : ISQL<Envelope>
@MESSAGE = @Message, @MESSAGE = @Message,
@OUT_UID = @OUT_UID OUTPUT; @OUT_UID = @OUT_UID OUTPUT;
SELECT * SELECT TOP(1) *
FROM [dbo].[TBSIG_ENVELOPE] FROM [dbo].[TBSIG_ENVELOPE]
WHERE [ENVELOPE_UUID] = @OUT_UID; WHERE [ENVELOPE_UUID] = @OUT_UID;
"; ";

View File

@@ -5,10 +5,14 @@ namespace EnvelopeGenerator.GeneratorAPI.Controllers
{ {
public static class ControllerExtensions public static class ControllerExtensions
{ {
public static int? GetId(this ClaimsPrincipal user) public static int? GetIdOrDefault(this ClaimsPrincipal user)
=> int.TryParse(user.FindFirst(ClaimTypes.NameIdentifier)?.Value, out int result) => int.TryParse(user.FindFirstValue(ClaimTypes.NameIdentifier) ?? user.FindFirstValue("sub"), out int result)
? result : null; ? result : null;
public static int GetId(this ClaimsPrincipal user)
=> user.GetIdOrDefault()
?? throw new InvalidOperationException("User ID claim is missing or invalid. This may indicate a misconfigured or forged JWT token.");
public static string? GetUsername(this ClaimsPrincipal user) public static string? GetUsername(this ClaimsPrincipal user)
=> user.FindFirst(ClaimTypes.Name)?.Value; => user.FindFirst(ClaimTypes.Name)?.Value;

View File

@@ -1,8 +1,11 @@
using DigitalData.Core.DTO; using DigitalData.Core.DTO;
using EnvelopeGenerator.Application.Contracts.Services; using EnvelopeGenerator.Application.Contracts.Services;
using EnvelopeGenerator.Application.Envelopes.Commands;
using EnvelopeGenerator.Application.Envelopes.Queries.Read; using EnvelopeGenerator.Application.Envelopes.Queries.Read;
using MediatR;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
namespace EnvelopeGenerator.GeneratorAPI.Controllers; namespace EnvelopeGenerator.GeneratorAPI.Controllers;
@@ -27,16 +30,19 @@ public class EnvelopeController : ControllerBase
{ {
private readonly ILogger<EnvelopeController> _logger; private readonly ILogger<EnvelopeController> _logger;
private readonly IEnvelopeService _envelopeService; private readonly IEnvelopeService _envelopeService;
private readonly IMediator _mediator;
/// <summary> /// <summary>
/// Erstellt eine neue Instanz des EnvelopeControllers. /// Erstellt eine neue Instanz des EnvelopeControllers.
/// </summary> /// </summary>
/// <param name="logger">Der Logger, der für das Protokollieren von Informationen verwendet wird.</param> /// <param name="logger">Der Logger, der für das Protokollieren von Informationen verwendet wird.</param>
/// <param name="envelopeService">Der Dienst, der für die Verarbeitung von Umschlägen zuständig ist.</param> /// <param name="envelopeService">Der Dienst, der für die Verarbeitung von Umschlägen zuständig ist.</param>
public EnvelopeController(ILogger<EnvelopeController> logger, IEnvelopeService envelopeService) /// <param name="mediator"></param>
public EnvelopeController(ILogger<EnvelopeController> logger, IEnvelopeService envelopeService, IMediator mediator)
{ {
_logger = logger; _logger = logger;
_envelopeService = envelopeService; _envelopeService = envelopeService;
_mediator = mediator;
} }
/// <summary> /// <summary>
@@ -75,4 +81,33 @@ public class EnvelopeController : ControllerBase
return StatusCode(StatusCodes.Status500InternalServerError); return StatusCode(StatusCodes.Status500InternalServerError);
} }
} }
/// <summary>
///
/// </summary>
/// <param name="envelope"></param>
/// <returns></returns>
[Authorize]
[HttpPost]
public async Task<IActionResult> CreateAsync([FromQuery] CreateEnvelopeCommand envelope)
{
try
{
envelope.UserId = User.GetId();
var res = await _mediator.Send(envelope);
if (res is null)
{
_logger.LogError("Failed to create envelope. Envelope details: {EnvelopeDetails}", JsonConvert.SerializeObject(envelope));
return StatusCode(StatusCodes.Status500InternalServerError);
}
else
return Ok(res);
}
catch (Exception ex)
{
_logger.LogError(ex, "{Message}", ex.Message);
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
} }

View File

@@ -7,6 +7,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Dapper" Version="2.1.66" />
<PackageReference Include="DigitalData.Core.Abstractions" Version="3.6.0" /> <PackageReference Include="DigitalData.Core.Abstractions" Version="3.6.0" />
<PackageReference Include="DigitalData.Core.Infrastructure" Version="2.0.4" /> <PackageReference Include="DigitalData.Core.Infrastructure" Version="2.0.4" />
<PackageReference Include="DigitalData.Core.Infrastructure.AutoMapper" Version="1.0.2" /> <PackageReference Include="DigitalData.Core.Infrastructure.AutoMapper" Version="1.0.2" />

View File

@@ -1,30 +1,31 @@
using EnvelopeGenerator.Application.Contracts.SQLExecutor; using Dapper;
using Microsoft.EntityFrameworkCore; using EnvelopeGenerator.Application.Contracts.SQLExecutor;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
namespace EnvelopeGenerator.Infrastructure; namespace EnvelopeGenerator.Infrastructure;
public sealed class SQLExecutor<T> : ISQLExecutor<T> where T : class public class SQLExecutor : ISQLExecutor
{ {
private readonly EGDbContext _context; private readonly string _cnnStr = "Server=SDD-VMP04-SQL17\\DD_DEVELOP01;Database=DD_ECM;User Id=sa;Password=dd;Encrypt=false;TrustServerCertificate=True;";
private readonly IServiceProvider _provider; private readonly IServiceProvider _provider;
public SQLExecutor(EGDbContext context, IServiceProvider provider) public SQLExecutor(IServiceProvider provider)
{ {
_context = context;
_provider = provider; _provider = provider;
} }
public IQuery<T> Execute(string sql, CancellationToken cancellation = default, params object[] parameters) public async Task<IEnumerable<TEntity>> Execute<TEntity>(string sql, DynamicParameters parameters, CancellationToken cancellation = default)
=> _context {
.Set<T>() using var connection = new SqlConnection(_cnnStr);
.FromSqlRaw(sql, parameters) await connection.OpenAsync(cancellation);
.ToQuery(); return await connection.QueryAsync<TEntity>(sql, parameters);
}
public IQuery<T> Execute<TSQL>(CancellationToken cancellation = default, params object[] parameters) where TSQL : ISQL<T> public Task<IEnumerable<TEntity>> Execute<TEntity, TSQL>(DynamicParameters parameters, CancellationToken cancellation = default) where TSQL : ISQL
{ {
var sql = _provider.GetRequiredService<TSQL>(); var sql = _provider.GetRequiredService<TSQL>();
return Execute(sql.Raw); return Execute<TEntity>(sql.Raw, parameters, cancellation);
} }
} }

View File

@@ -0,0 +1,30 @@
using EnvelopeGenerator.Application.Contracts.SQLExecutor;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
namespace EnvelopeGenerator.Infrastructure;
public sealed class SQLExecutor<T> : SQLExecutor, ISQLExecutor<T> where T : class
{
private readonly EGDbContext _context;
private readonly IServiceProvider _provider;
public SQLExecutor(EGDbContext context, IServiceProvider provider) : base(provider)
{
_context = context;
_provider = provider;
}
public IQuery<T> Execute(string sql, CancellationToken cancellation = default, params object[] parameters)
=> _context
.Set<T>()
.FromSqlRaw(sql, parameters)
.ToQuery();
public IQuery<T> Execute<TSQL>(CancellationToken cancellation = default, params object[] parameters) where TSQL : ISQL<T>
{
var sql = _provider.GetRequiredService<TSQL>();
return Execute(sql.Raw);
}
}