sumatube/SumaTube.Crosscutting/Mapper/GenericMapper.cs
2025-04-20 23:33:46 -03:00

255 lines
11 KiB
C#

namespace SumaTube.Crosscutting.Mappers
{
using System;
using System.Linq;
using System.Reflection;
public static class GenericMapper
{
public static TDestination MapTo<TSource, TDestination>(this TSource source)
where TDestination : class
{
if (source == null)
return default;
var destType = typeof(TDestination);
// Verifica se o tipo de destino tem um construtor público
var constructors = destType.GetConstructors(BindingFlags.Public | BindingFlags.Instance)
.OrderByDescending(c => c.GetParameters().Length)
.ToList();
// Se o tipo de destino tem um construtor com parâmetros, tenta mapear para ele
if (constructors.Any() && constructors[0].GetParameters().Length > 0)
{
return MapToImmutableObject<TSource, TDestination>(source, constructors);
}
// Caso contrário, usa a abordagem de mapeamento de propriedades para objetos mutáveis
else
{
return MapToMutableObject<TSource, TDestination>(source);
}
}
private static TDestination MapToImmutableObject<TSource, TDestination>(TSource source, List<ConstructorInfo> constructors)
where TDestination : class
{
var sourceType = typeof(TSource);
var sourceProps = sourceType.GetProperties(BindingFlags.Public | BindingFlags.Instance);
// Tenta cada construtor, começando pelo que tem mais parâmetros
foreach (var constructor in constructors)
{
var parameters = constructor.GetParameters();
if (parameters.Length == 0)
{
// Construtor sem parâmetros, cria instância diretamente
var instance = constructor.Invoke(null);
return (TDestination)instance;
}
var parameterValues = new object[parameters.Length];
bool canUseConstructor = true;
// Tenta mapear os parâmetros do construtor
for (int i = 0; i < parameters.Length; i++)
{
var param = parameters[i];
// Procura propriedade com o mesmo nome (case insensitive)
var matchingProp = sourceProps.FirstOrDefault(p =>
string.Equals(p.Name, param.Name, StringComparison.OrdinalIgnoreCase));
if (matchingProp != null)
{
var value = matchingProp.GetValue(source);
// Se os tipos são compatíveis, usa o valor diretamente
if (param.ParameterType.IsAssignableFrom(matchingProp.PropertyType))
{
parameterValues[i] = value;
}
// Verifica se existe uma conversão implícita
else if (value != null && TryImplicitConversion(value, param.ParameterType, out var convertedValue))
{
parameterValues[i] = convertedValue;
}
// Se o valor é um tipo complexo, tenta mapear recursivamente
else if (value != null && !matchingProp.PropertyType.IsPrimitive &&
!matchingProp.PropertyType.Namespace.StartsWith("System"))
{
try
{
var method = typeof(GenericMapper).GetMethod(nameof(MapTo));
var genericMethod = method.MakeGenericMethod(matchingProp.PropertyType, param.ParameterType);
parameterValues[i] = genericMethod.Invoke(null, new[] { value });
}
catch
{
canUseConstructor = false;
break;
}
}
else
{
canUseConstructor = false;
break;
}
}
else
{
// Se não encontrou uma propriedade correspondente, verifica se o parâmetro é opcional
if (param.IsOptional)
{
parameterValues[i] = param.DefaultValue;
}
else
{
canUseConstructor = false;
break;
}
}
}
if (canUseConstructor)
{
try
{
var instance = constructor.Invoke(parameterValues);
return (TDestination)instance;
}
catch
{
// Se falhou ao criar a instância, tenta o próximo construtor
continue;
}
}
}
// Se não conseguiu usar nenhum construtor, lança exceção
throw new InvalidOperationException(
$"Não foi possível mapear {sourceType.Name} para {typeof(TDestination).Name} " +
$"usando os construtores disponíveis. Verifique se os nomes das propriedades " +
$"correspondem aos nomes dos parâmetros do construtor (case insensitive).");
}
private static TDestination MapToMutableObject<TSource, TDestination>(TSource source)
where TDestination : class
{
var destType = typeof(TDestination);
// Tenta criar uma instância usando o construtor sem parâmetros
TDestination destination;
try
{
destination = Activator.CreateInstance<TDestination>();
}
catch (Exception ex)
{
throw new InvalidOperationException(
$"Não foi possível criar uma instância de {destType.Name}. " +
$"Certifique-se de que a classe tenha um construtor sem parâmetros acessível.", ex);
}
var sourceType = typeof(TSource);
var sourceProps = sourceType.GetProperties(BindingFlags.Public | BindingFlags.Instance);
var destProps = destType.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.SetProperty);
foreach (var destProp in destProps)
{
if (!destProp.CanWrite)
continue;
// Procura propriedade com o mesmo nome (case insensitive)
var sourceProp = sourceProps.FirstOrDefault(p =>
string.Equals(p.Name, destProp.Name, StringComparison.OrdinalIgnoreCase));
if (sourceProp != null)
{
var value = sourceProp.GetValue(source);
if (value != null)
{
// Se os tipos são compatíveis, atribui diretamente
if (destProp.PropertyType.IsAssignableFrom(sourceProp.PropertyType))
{
destProp.SetValue(destination, value);
}
// Verifica se existe uma conversão implícita
else if (TryImplicitConversion(value, destProp.PropertyType, out var convertedValue))
{
destProp.SetValue(destination, convertedValue);
}
// Se o valor é um tipo complexo, tenta mapear recursivamente
else if (!sourceProp.PropertyType.IsPrimitive &&
!sourceProp.PropertyType.Namespace.StartsWith("System"))
{
try
{
var method = typeof(GenericMapper).GetMethod(nameof(MapTo));
var genericMethod = method.MakeGenericMethod(sourceProp.PropertyType, destProp.PropertyType);
var mappedValue = genericMethod.Invoke(null, new[] { value });
destProp.SetValue(destination, mappedValue);
}
catch
{
// Ignora se não conseguir mapear
}
}
}
}
}
return destination;
}
private static bool TryImplicitConversion(object source, Type destinationType, out object result)
{
result = null;
if (source == null) return false;
var sourceType = source.GetType();
// Verifica operador de conversão implícita no tipo de origem
var methodSource = sourceType.GetMethods(BindingFlags.Public | BindingFlags.Static)
.FirstOrDefault(m =>
m.Name == "op_Implicit" &&
m.ReturnType == destinationType &&
m.GetParameters().Length == 1 &&
m.GetParameters()[0].ParameterType == sourceType);
if (methodSource != null)
{
result = methodSource.Invoke(null, new[] { source });
return true;
}
// Verifica operador de conversão implícita no tipo de destino
var methodDest = destinationType.GetMethods(BindingFlags.Public | BindingFlags.Static)
.FirstOrDefault(m =>
m.Name == "op_Implicit" &&
m.ReturnType == destinationType &&
m.GetParameters().Length == 1 &&
m.GetParameters()[0].ParameterType == sourceType);
if (methodDest != null)
{
result = methodDest.Invoke(null, new[] { source });
return true;
}
// Tenta converter usando Convert.ChangeType
try
{
if (destinationType.IsValueType || destinationType == typeof(string))
{
result = Convert.ChangeType(source, destinationType);
return true;
}
}
catch { }
return false;
}
}
}