2025-09-28 15:24:13 +03:30
|
|
|
namespace BackOffice.BFF.WebApi.Common.Services;
|
2025-12-08 21:10:21 +03:30
|
|
|
|
|
|
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
|
2025-09-28 15:24:13 +03:30
|
|
|
public interface IDispatchRequestToCQRS
|
|
|
|
|
{
|
|
|
|
|
Task<TResponse> Handle<TRequest, TCommand, TResponse>(TRequest request,
|
|
|
|
|
ServerCallContext context);
|
|
|
|
|
Task<Empty> Handle<TRequest, TCommand>(TRequest request,
|
|
|
|
|
ServerCallContext context);
|
|
|
|
|
Task<TResponse> Handle<TCommand, TResponse>(ServerCallContext context);
|
|
|
|
|
}
|
|
|
|
|
public class DispatchRequestToCQRS : IDispatchRequestToCQRS
|
|
|
|
|
{
|
|
|
|
|
private readonly ISender _sender;
|
2025-12-08 21:10:21 +03:30
|
|
|
private readonly ILogger<DispatchRequestToCQRS> _logger;
|
2025-09-28 15:24:13 +03:30
|
|
|
|
2025-12-08 21:10:21 +03:30
|
|
|
public DispatchRequestToCQRS(ISender sender, ILogger<DispatchRequestToCQRS> logger)
|
2025-09-28 15:24:13 +03:30
|
|
|
{
|
|
|
|
|
_sender = sender;
|
2025-12-08 21:10:21 +03:30
|
|
|
_logger = logger;
|
2025-09-28 15:24:13 +03:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async Task<TResponse> Handle<TRequest, TCommand, TResponse>(TRequest request,
|
|
|
|
|
ServerCallContext context)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
if (request is null)
|
|
|
|
|
{
|
|
|
|
|
throw new ArgumentNullException(nameof(request));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var cqrsInput = request.Adapt<TCommand>();
|
|
|
|
|
|
|
|
|
|
if (cqrsInput is null)
|
|
|
|
|
{
|
|
|
|
|
throw new ArgumentNullException(nameof(cqrsInput));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var output = await _sender.Send(cqrsInput, context.CancellationToken);
|
2025-12-08 21:10:21 +03:30
|
|
|
|
|
|
|
|
if (output is null)
|
|
|
|
|
{
|
|
|
|
|
_logger.LogError("Command/Query {CommandType} returned null output", typeof(TCommand).Name);
|
|
|
|
|
throw new InvalidOperationException($"Command/Query {typeof(TCommand).Name} returned null output");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
_logger.LogInformation("Mapping from {SourceType} to {TargetType}", output.GetType().Name, typeof(TResponse).Name);
|
|
|
|
|
|
|
|
|
|
// Try to detect problematic properties before mapping
|
|
|
|
|
LogSourceObjectStructure(output);
|
|
|
|
|
|
|
|
|
|
// Use TypeAdapter.Adapt instead of extension method to ensure MapWith() is used
|
|
|
|
|
var result = TypeAdapter.Adapt(output, output.GetType(), typeof(TResponse));
|
|
|
|
|
|
|
|
|
|
_logger.LogInformation("Mapping successful: {SourceType} -> {TargetType}", output.GetType().Name, typeof(TResponse).Name);
|
|
|
|
|
return (TResponse)result;
|
|
|
|
|
}
|
|
|
|
|
catch (Exception mappingEx)
|
|
|
|
|
{
|
|
|
|
|
_logger.LogError(mappingEx, "Mapping failed: {SourceType} -> {TargetType}. Full error: {ErrorMessage}",
|
|
|
|
|
output.GetType().Name, typeof(TResponse).Name, mappingEx.ToString());
|
|
|
|
|
|
|
|
|
|
// Log detailed property information
|
|
|
|
|
LogDetailedPropertyInfo(output, typeof(TResponse));
|
|
|
|
|
|
|
|
|
|
throw new InvalidOperationException(
|
|
|
|
|
$"Failed to map {output.GetType().Name} to {typeof(TResponse).Name}. Check ProductsProfile configuration.",
|
|
|
|
|
mappingEx);
|
|
|
|
|
}
|
2025-09-28 15:24:13 +03:30
|
|
|
}
|
2025-12-08 21:10:21 +03:30
|
|
|
catch (Exception ex)
|
2025-09-28 15:24:13 +03:30
|
|
|
{
|
2025-12-08 21:10:21 +03:30
|
|
|
_logger.LogError(ex, "Error in DispatchRequestToCQRS.Handle<{Request},{Command},{Response}>",
|
|
|
|
|
typeof(TRequest).Name, typeof(TCommand).Name, typeof(TResponse).Name);
|
2025-09-28 15:24:13 +03:30
|
|
|
throw;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
public async Task<TResponse> Handle<TCommand, TResponse>(ServerCallContext context)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var cqrsInput = Activator.CreateInstance<TCommand>();
|
|
|
|
|
if (cqrsInput is null)
|
|
|
|
|
{
|
|
|
|
|
throw new ArgumentNullException(nameof(cqrsInput));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var output = await _sender.Send(cqrsInput, context.CancellationToken);
|
2025-12-08 21:10:21 +03:30
|
|
|
|
|
|
|
|
if (output is null)
|
|
|
|
|
{
|
|
|
|
|
_logger.LogError("Command/Query {CommandType} returned null output", typeof(TCommand).Name);
|
|
|
|
|
throw new InvalidOperationException($"Command/Query {typeof(TCommand).Name} returned null output");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
_logger.LogInformation("Mapping from {SourceType} to {TargetType}", output.GetType().Name, typeof(TResponse).Name);
|
|
|
|
|
|
|
|
|
|
// Use TypeAdapter.Adapt instead of extension method to ensure MapWith() is used
|
|
|
|
|
var result = TypeAdapter.Adapt(output, output.GetType(), typeof(TResponse));
|
|
|
|
|
|
|
|
|
|
_logger.LogInformation("Mapping successful: {SourceType} -> {TargetType}", output.GetType().Name, typeof(TResponse).Name);
|
|
|
|
|
return (TResponse)result;
|
|
|
|
|
}
|
|
|
|
|
catch (Exception mappingEx)
|
|
|
|
|
{
|
|
|
|
|
_logger.LogError(mappingEx, "Mapping failed: {SourceType} -> {TargetType}. Source properties: {@SourceData}",
|
|
|
|
|
output.GetType().Name, typeof(TResponse).Name, output);
|
|
|
|
|
throw new InvalidOperationException(
|
|
|
|
|
$"Failed to map {output.GetType().Name} to {typeof(TResponse).Name}. Check mapping Profile configuration.",
|
|
|
|
|
mappingEx);
|
|
|
|
|
}
|
2025-09-28 15:24:13 +03:30
|
|
|
}
|
2025-12-08 21:10:21 +03:30
|
|
|
catch (Exception ex)
|
2025-09-28 15:24:13 +03:30
|
|
|
{
|
2025-12-08 21:10:21 +03:30
|
|
|
_logger.LogError(ex, "Error in DispatchRequestToCQRS.Handle<{Command},{Response}>",
|
|
|
|
|
typeof(TCommand).Name, typeof(TResponse).Name);
|
2025-09-28 15:24:13 +03:30
|
|
|
throw;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
public async Task<Empty> Handle<TRequest, TCommand>(TRequest request,
|
|
|
|
|
ServerCallContext context)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
if (request is null)
|
|
|
|
|
{
|
|
|
|
|
throw new ArgumentNullException(nameof(request));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var cqrsInput = request.Adapt<TCommand>();
|
|
|
|
|
|
|
|
|
|
if (cqrsInput is null)
|
|
|
|
|
{
|
|
|
|
|
throw new ArgumentNullException(nameof(cqrsInput));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await _sender.Send(cqrsInput, context.CancellationToken);
|
|
|
|
|
return new Empty();
|
|
|
|
|
}
|
|
|
|
|
catch (Exception)
|
|
|
|
|
{
|
|
|
|
|
throw;
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-12-08 21:10:21 +03:30
|
|
|
|
|
|
|
|
private void LogSourceObjectStructure(object source)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var properties = source.GetType().GetProperties();
|
|
|
|
|
_logger.LogInformation("Source object has {PropertyCount} properties:", properties.Length);
|
|
|
|
|
|
|
|
|
|
foreach (var prop in properties)
|
|
|
|
|
{
|
|
|
|
|
var value = prop.GetValue(source);
|
|
|
|
|
var valueType = value?.GetType().Name ?? "null";
|
|
|
|
|
var isCollection = value != null && value.GetType().IsGenericType &&
|
|
|
|
|
value.GetType().GetGenericTypeDefinition() == typeof(List<>);
|
|
|
|
|
|
|
|
|
|
if (isCollection)
|
|
|
|
|
{
|
|
|
|
|
var count = ((System.Collections.ICollection)value).Count;
|
|
|
|
|
_logger.LogInformation(" - {PropertyName}: {PropertyType} (Collection with {Count} items)",
|
|
|
|
|
prop.Name, prop.PropertyType.Name, count);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
_logger.LogInformation(" - {PropertyName}: {PropertyType} = {Value}",
|
|
|
|
|
prop.Name, prop.PropertyType.Name, value);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
_logger.LogWarning(ex, "Could not log source object structure");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void LogDetailedPropertyInfo(object source, System.Type targetType)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var sourceProps = source.GetType().GetProperties();
|
|
|
|
|
var targetProps = targetType.GetProperties();
|
|
|
|
|
|
|
|
|
|
_logger.LogError("=== DETAILED MAPPING ANALYSIS ===");
|
|
|
|
|
_logger.LogError("Source type: {SourceType} ({SourcePropsCount} properties)",
|
|
|
|
|
source.GetType().FullName, sourceProps.Length);
|
|
|
|
|
_logger.LogError("Target type: {TargetType} ({TargetPropsCount} properties)",
|
|
|
|
|
targetType.FullName, targetProps.Length);
|
|
|
|
|
|
|
|
|
|
foreach (var sourceProp in sourceProps)
|
|
|
|
|
{
|
|
|
|
|
var targetProp = targetProps.FirstOrDefault(p =>
|
|
|
|
|
p.Name.Equals(sourceProp.Name, StringComparison.OrdinalIgnoreCase));
|
|
|
|
|
|
|
|
|
|
if (targetProp != null)
|
|
|
|
|
{
|
|
|
|
|
var sourceValue = sourceProp.GetValue(source);
|
|
|
|
|
var sourceType = sourceProp.PropertyType;
|
|
|
|
|
var targetPropType = targetProp.PropertyType;
|
|
|
|
|
|
|
|
|
|
var isImmutable = targetPropType.FullName?.Contains("Google.Protobuf") == true ||
|
|
|
|
|
targetPropType.Name.Contains("RepeatedField");
|
|
|
|
|
|
|
|
|
|
_logger.LogError(" Property '{PropertyName}':", sourceProp.Name);
|
|
|
|
|
_logger.LogError(" Source: {SourceType} (value: {Value})",
|
|
|
|
|
sourceType.Name, sourceValue);
|
|
|
|
|
_logger.LogError(" Target: {TargetType} {ImmutableWarning}",
|
|
|
|
|
targetPropType.Name,
|
|
|
|
|
isImmutable ? "⚠️ IMMUTABLE - needs MapWith()" : "");
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
_logger.LogWarning(" Property '{PropertyName}' exists in source but NOT in target",
|
|
|
|
|
sourceProp.Name);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
_logger.LogWarning(ex, "Could not log detailed property info");
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-09-28 15:24:13 +03:30
|
|
|
}
|