From 479febe94d2b8144d55a304f01f41c6b3c431d45 Mon Sep 17 00:00:00 2001 From: MarvelTiter Date: Fri, 23 Jan 2026 17:16:52 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=E5=A4=87=E4=BB=BD=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/AutoGenMapperGenerator/GMapper.cs | 15 +++++---------- .../ReflectMapper/ExpressionMapper.cs | 10 ++++++++++ 2 files changed, 15 insertions(+), 10 deletions(-) create mode 100644 src/AutoGenMapperGenerator/ReflectMapper/ExpressionMapper.cs diff --git a/src/AutoGenMapperGenerator/GMapper.cs b/src/AutoGenMapperGenerator/GMapper.cs index 6a19609..b84d6e3 100644 --- a/src/AutoGenMapperGenerator/GMapper.cs +++ b/src/AutoGenMapperGenerator/GMapper.cs @@ -1,4 +1,8 @@ -namespace AutoGenMapperGenerator; +using AutoGenMapperGenerator.ReflectMapper; +using System.Collections; +using System.Collections.Generic; + +namespace AutoGenMapperGenerator; /// /// G -> Generator @@ -22,12 +26,3 @@ public static TTarget Map(TSource source) return ExpressionMapper.Map(source); } } - -internal static class ExpressionMapper -{ - public static TTarget Map(TSource source) - { - // TODO - throw new System.NotImplementedException(); - } -} diff --git a/src/AutoGenMapperGenerator/ReflectMapper/ExpressionMapper.cs b/src/AutoGenMapperGenerator/ReflectMapper/ExpressionMapper.cs new file mode 100644 index 0000000..68f036b --- /dev/null +++ b/src/AutoGenMapperGenerator/ReflectMapper/ExpressionMapper.cs @@ -0,0 +1,10 @@ +namespace AutoGenMapperGenerator.ReflectMapper; + +internal static class ExpressionMapper +{ + public static TTarget Map(TSource source) + { + // TODO + throw new System.NotImplementedException(); + } +} From d22e9f82c1ced5ef1c850fc42e0ba81fe5064214 Mon Sep 17 00:00:00 2001 From: MarvelTiter Date: Thu, 5 Feb 2026 14:14:36 +0800 Subject: [PATCH 2/2] =?UTF-8?q?=E8=A1=A8=E8=BE=BE=E5=BC=8F=E6=98=A0?= =?UTF-8?q?=E5=B0=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AutoGenMapperGenerator.csproj | 6 +- src/AutoGenMapperGenerator/GMapper.cs | 51 ++- .../ReflectMapper/DictionaryExtensions.cs | 94 ++++++ .../ReflectMapper/ExpressionHelper.cs | 245 ++++++++++++++ .../ReflectMapper/ExpressionMapper.Convert.cs | 17 + .../ExpressionMapper.Dictionary.cs | 141 ++++++++ .../ReflectMapper/ExpressionMapper.cs | 309 +++++++++++++++++- .../ReflectMapper/IMapperService.cs | 88 +++++ .../ReflectMapper/IoCExtensions.cs | 26 ++ .../ReflectMapper/MapperOptions.cs | 169 ++++++++++ src/AutoGenMapperGenerator/Versions.props | 2 +- .../Blazor.Test.Client/Models/User.cs | 8 + src/Blazor.Test/Blazor.Test/Program.cs | 9 + src/TestProject1/MapperGenTest.cs | 95 +++++- src/TestProject1/Models/User.cs | 32 ++ 15 files changed, 1283 insertions(+), 9 deletions(-) create mode 100644 src/AutoGenMapperGenerator/ReflectMapper/DictionaryExtensions.cs create mode 100644 src/AutoGenMapperGenerator/ReflectMapper/ExpressionHelper.cs create mode 100644 src/AutoGenMapperGenerator/ReflectMapper/ExpressionMapper.Convert.cs create mode 100644 src/AutoGenMapperGenerator/ReflectMapper/ExpressionMapper.Dictionary.cs create mode 100644 src/AutoGenMapperGenerator/ReflectMapper/IMapperService.cs create mode 100644 src/AutoGenMapperGenerator/ReflectMapper/IoCExtensions.cs create mode 100644 src/AutoGenMapperGenerator/ReflectMapper/MapperOptions.cs diff --git a/src/AutoGenMapperGenerator/AutoGenMapperGenerator.csproj b/src/AutoGenMapperGenerator/AutoGenMapperGenerator.csproj index f8a1212..fd930b1 100644 --- a/src/AutoGenMapperGenerator/AutoGenMapperGenerator.csproj +++ b/src/AutoGenMapperGenerator/AutoGenMapperGenerator.csproj @@ -1,7 +1,7 @@  - netstandard2.0;net6.0;net8.0;net10.0 + net6.0;net8.0;net10.0 latest enable true @@ -25,4 +25,8 @@ + + + + diff --git a/src/AutoGenMapperGenerator/GMapper.cs b/src/AutoGenMapperGenerator/GMapper.cs index b84d6e3..b70cf6f 100644 --- a/src/AutoGenMapperGenerator/GMapper.cs +++ b/src/AutoGenMapperGenerator/GMapper.cs @@ -1,6 +1,8 @@ using AutoGenMapperGenerator.ReflectMapper; +using System; using System.Collections; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; namespace AutoGenMapperGenerator; @@ -17,12 +19,59 @@ public static class GMapper /// /// /// - public static TTarget Map(TSource source) + public static TTarget Map< +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] +#endif + TSource, +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] +#endif + TTarget>(this TSource source) { + if (source is null) + { + return default!; + } if (source is IAutoMap m) { return m.MapTo(); } + var (IsComplex, IsDictionary, IsEnumerable) = ExpressionHelper.IsComplexType(typeof(TSource)); + if (IsComplex && IsDictionary) + { + throw new InvalidOperationException("请使用TEntity ToEntity(this IDictionary dict)"); + } + if (IsComplex && IsEnumerable) + { + throw new InvalidOperationException("不支持集合类型的映射,请使用LINQ的Select方法进行映射"); + } return ExpressionMapper.Map(source); } + + /// + /// + /// + /// + /// + /// + public static IDictionary ToDictionary(this TEntity entity) + { + if (entity is null) + { + return new Dictionary(); + } + return ExpressionMapper.MapToDictionary(entity); + } + + /// + /// + /// + /// + /// + /// + public static TEntity ToEntity(this IDictionary dict) + { + return ExpressionMapper.MapFromDictionary(dict); + } } diff --git a/src/AutoGenMapperGenerator/ReflectMapper/DictionaryExtensions.cs b/src/AutoGenMapperGenerator/ReflectMapper/DictionaryExtensions.cs new file mode 100644 index 0000000..aae838b --- /dev/null +++ b/src/AutoGenMapperGenerator/ReflectMapper/DictionaryExtensions.cs @@ -0,0 +1,94 @@ +using System; +using System.Collections.Generic; +using System.Globalization; + +namespace AutoGenMapperGenerator.ReflectMapper; + +internal static class DictionaryExtensions +{ + private static readonly Dictionary conversionCache = []; + + private static T TryParse(string str, CultureInfo culture) + { + var targetType = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T); + if (conversionCache.TryGetValue(targetType, out var del)) + { + var parser = (Func)del; + return parser(str, culture); + } + Func parserFunc = targetType switch + { + Type t when t == typeof(int) => (s, c) => (T)(object)int.Parse(s, NumberStyles.Any, c), + Type t when t == typeof(long) => (s, c) => (T)(object)long.Parse(s, NumberStyles.Any, c), + Type t when t == typeof(float) => (s, c) => (T)(object)float.Parse(s, NumberStyles.Any, c), + Type t when t == typeof(double) => (s, c) => (T)(object)double.Parse(s, NumberStyles.Any, c), + Type t when t == typeof(decimal) => (s, c) => (T)(object)decimal.Parse(s, NumberStyles.Any, c), + Type t when t == typeof(DateTime) => (s, c) => (T)(object)DateTime.Parse(s, c), + Type t when t == typeof(bool) => (s, c) => (T)(object)bool.Parse(s), + _ => throw new NotSupportedException($"Type {targetType.FullName} is not supported for parsing.") + }; + conversionCache[targetType] = parserFunc; + return parserFunc(str, culture); + } + + + public static bool TryGetValue(this IDictionary dict, string key, out object value) + { + value = default!; + if (!dict.TryGetValue(key, out var dictValue)) + { + return default!; + } + if (dictValue is null) + { + return false; + } + if (typeof(TValue) == typeof(object)) + { + value = (TValue)dictValue; + return true; + } + if (typeof(TValue) == typeof(string)) + { + value = (TValue)(object)dictValue.ToString()!; + return true; + } + if (dictValue is TValue tValue) + { + value = tValue; + return true; + } + // 处理可空类型 + var underlyingType = Nullable.GetUnderlyingType(typeof(TValue)); + var conversionTargetType = underlyingType ?? typeof(TValue); + if (conversionTargetType.IsEnum) + { + value = (TValue)Enum.Parse(conversionTargetType, dictValue.ToString() ?? string.Empty, true); + return true; + } + if (dictValue is string str) + { + // string转基础类型 + try + { + value = TryParse(str, CultureInfo.CurrentCulture); + return true; + } + catch + { + return false; + } + } + // 使用 Convert.ChangeType 兜底 + try + { + var convertedValue = Convert.ChangeType(dictValue, conversionTargetType, CultureInfo.CurrentCulture); + value = (TValue)convertedValue!; + return true; + } + catch + { + return false; + } + } +} diff --git a/src/AutoGenMapperGenerator/ReflectMapper/ExpressionHelper.cs b/src/AutoGenMapperGenerator/ReflectMapper/ExpressionHelper.cs new file mode 100644 index 0000000..3edf320 --- /dev/null +++ b/src/AutoGenMapperGenerator/ReflectMapper/ExpressionHelper.cs @@ -0,0 +1,245 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq.Expressions; +using System.Reflection; + +namespace AutoGenMapperGenerator.ReflectMapper; + +internal static class ExpressionHelper +{ + public static (bool IsComplex, bool IsDictionary, bool IsEnumerable) IsComplexType(Type type) + { + if (type.IsPrimitive || + type == typeof(string) || + type == typeof(decimal) || + type == typeof(DateTime) || + type == typeof(Guid) || + type == typeof(TimeSpan)) + { + return (false, false, false); + } + + if (typeof(IDictionary).IsAssignableFrom(type)) + { + return (true, true, false); + } + var isEnumerable = IsCollectionType(type); + return (type.IsClass && type != typeof(string), false, isEnumerable); + + static bool IsCollectionType(Type type) + { + // 数组 + if (type.IsArray) + { + return true; + } + + // 泛型接口 IEnumerable + if (type.IsGenericType && typeof(IEnumerable<>).MakeGenericType(type.GetGenericArguments()) == type) + { + return true; + } + + // 具体集合类 (List, ICollection 等) + foreach (var iface in type.GetInterfaces()) + { + if (iface.IsGenericType && iface.GetGenericTypeDefinition() == typeof(IEnumerable<>)) + { + return true; + } + } + + return false; + } + } + public static MemberInfo? GetMemberInfoFromLambda(LambdaExpression lambda) + { + // 去除 Convert 节点 (例如 Expression> 会产生 Convert(node)) + var body = lambda.Body is UnaryExpression unary && unary.NodeType == ExpressionType.Convert + ? unary.Operand + : lambda.Body; + + if (body is MemberExpression memberExpression) + { + return memberExpression.Member; + } + return null; + } + + public static (Type, string) GetMemberTypeAndNameFromLambda(LambdaExpression lambda) + { + // 去除 Convert 节点 (例如 Expression> 会产生 Convert(node)) + var body = lambda.Body is UnaryExpression unary && unary.NodeType == ExpressionType.Convert + ? unary.Operand + : lambda.Body; + if (body is MemberExpression memberExpression) + { + return (memberExpression.Type, memberExpression.Member.Name); + } + throw new InvalidOperationException("Lambda expression does not refer to a member."); + } + + + private static readonly MethodInfo CustomStringParseToBoolean = typeof(ExpressionMapper<,>).GetMethod(nameof(CustomStringToBoolean), BindingFlags.Public | BindingFlags.Static)!; + private static readonly MethodInfo enumParseMethod = typeof(Enum).GetMethod("Parse", [typeof(Type), typeof(string), typeof(bool)])!; + public static string CustomStringToBoolean(string valueString) + { + return ",是,1,Y,YES,TRUE,".Contains(valueString.ToUpper()) ? "True" : "False"; + } + public static Expression GetConversionExpression(Type SourceType, Expression SourceExpression, Type TargetType, CultureInfo Culture) + { + Expression TargetExpression; + if (TargetType == SourceType) + { + TargetExpression = SourceExpression; + } + else if (SourceType == typeof(string)) + { + TargetExpression = GetParseExpression(SourceExpression, TargetType, Culture); + } + else if (TargetType == typeof(string)) + { + TargetExpression = Expression.Call(SourceExpression, SourceType.GetMethod("ToString", Type.EmptyTypes)!); + } + else if (TargetType == typeof(bool)) + { + MethodInfo ToBooleanMethod = typeof(Convert).GetMethod("ToBoolean", [SourceType])!; + TargetExpression = Expression.Call(ToBooleanMethod, SourceExpression); + } + else if (SourceType == typeof(byte[])) + { + TargetExpression = GetArrayHandlerExpression(SourceExpression, TargetType); + } + else + { + TargetExpression = ConvertTypeExpression(SourceExpression, SourceType, TargetType); + //TargetExpression = Expression.Convert(SourceExpression, TargetType); + } + return TargetExpression; + } + + private static Expression GetArrayHandlerExpression(Expression sourceExpression, Type targetType) + { + Expression TargetExpression; + if (targetType == typeof(byte[])) + { + TargetExpression = sourceExpression; + } + else if (targetType == typeof(MemoryStream)) + { + ConstructorInfo ConstructorInfo = targetType.GetConstructor([typeof(byte[])])!; + TargetExpression = Expression.New(ConstructorInfo, sourceExpression); + } + else + { + throw new Exception("Cannot convert a byte array to " + targetType.Name); + } + return TargetExpression; + } + private static Expression GetParseExpression(Expression SourceExpression, Type TargetType, CultureInfo Culture) + { + Type UnderlyingType = GetUnderlyingType(TargetType); + if (UnderlyingType.IsEnum) + { + MethodCallExpression ParsedEnumExpression = GetEnumParseExpression(SourceExpression, UnderlyingType); + //Enum.Parse returns an object that needs to be unboxed + return Expression.Convert(ParsedEnumExpression, TargetType); + } + else + { + Expression ParseExpression; + switch (UnderlyingType.FullName) + { + case "System.Byte": + case "System.UInt16": + case "System.UInt32": + case "System.UInt64": + case "System.SByte": + case "System.Int16": + case "System.Int32": + case "System.Int64": + case "System.Double": + case "System.Decimal": + ParseExpression = GetNumberParseExpression(SourceExpression, UnderlyingType, Culture); + break; + case "System.DateTime": + ParseExpression = GetDateTimeParseExpression(SourceExpression, UnderlyingType, Culture); + break; + case "System.Boolean": + ParseExpression = TryParseStringToBoolean(SourceExpression, UnderlyingType); + break; + case "System.Char": + ParseExpression = GetGenericParseExpression(SourceExpression, UnderlyingType); + break; + default: + throw new Exception(string.Format("Conversion from {0} to {1} is not supported", "String", TargetType)); + } + if (Nullable.GetUnderlyingType(TargetType) == null) + { + return ParseExpression; + } + else + { + //Convert to nullable if necessary + return Expression.Convert(ParseExpression, TargetType); + } + } + Expression GetGenericParseExpression(Expression sourceExpression, Type type) + { + MethodInfo ParseMetod = type.GetMethod("Parse", [typeof(string)])!; + MethodCallExpression CallExpression = Expression.Call(ParseMetod, [sourceExpression]); + return CallExpression; + } + Expression GetDateTimeParseExpression(Expression sourceExpression, Type type, CultureInfo culture) + { + MethodInfo ParseMetod = type.GetMethod("Parse", [typeof(string), typeof(DateTimeFormatInfo)])!; + ConstantExpression ProviderExpression = Expression.Constant(culture.DateTimeFormat, typeof(DateTimeFormatInfo)); + MethodCallExpression CallExpression = Expression.Call(ParseMetod, [sourceExpression, ProviderExpression]); + return CallExpression; + } + + MethodCallExpression GetEnumParseExpression(Expression sourceExpression, Type type) + { + //Get the MethodInfo for parsing an Enum + //MethodInfo EnumParseMethod = typeof(Enum).GetMethod("Parse", [typeof(Type), typeof(string), typeof(bool)])!; + ConstantExpression TargetMemberTypeExpression = Expression.Constant(type); + ConstantExpression IgnoreCase = Expression.Constant(true, typeof(bool)); + //Create an expression the calls the Parse method + MethodCallExpression CallExpression = Expression.Call(enumParseMethod, [TargetMemberTypeExpression, sourceExpression, IgnoreCase]); + return CallExpression; + } + + MethodCallExpression GetNumberParseExpression(Expression sourceExpression, Type type, CultureInfo culture) + { + MethodInfo ParseMetod = type.GetMethod("Parse", [typeof(string), typeof(NumberFormatInfo)])!; + ConstantExpression ProviderExpression = Expression.Constant(culture.NumberFormat, typeof(NumberFormatInfo)); + MethodCallExpression CallExpression = Expression.Call(ParseMetod, [sourceExpression, ProviderExpression]); + return CallExpression; + } + Expression TryParseStringToBoolean(Expression sourceExpression, Type type) + { + var valueExpression = Expression.Call(CustomStringParseToBoolean, [sourceExpression]); + return GetGenericParseExpression(valueExpression, type); + } + } + private static Type GetUnderlyingType(Type targetType) + { + return Nullable.GetUnderlyingType(targetType) ?? targetType; + } + + static MethodInfo changeType = typeof(Convert).GetMethod("ChangeType", [typeof(object), typeof(Type)])!; + static MethodInfo isNullOrEmpty = typeof(string).GetMethod(nameof(string.IsNullOrEmpty))!; + private static ConditionalExpression ConvertTypeExpression(Expression source, Type sourceType, Type targetType) + { + var underType = Nullable.GetUnderlyingType(targetType) ?? targetType; + var isNull = Expression.Equal(source, Expression.Constant(null)); + var stringValue = Expression.Call(source, sourceType.GetMethod("ToString", Type.EmptyTypes)!); + var isNullOrEmptyExpression = Expression.Call(isNullOrEmpty, stringValue); + var canConvert = Expression.AndAlso(Expression.IsFalse(isNull), Expression.IsFalse(isNullOrEmptyExpression)); + var finalValue = Expression.Convert(Expression.Call(changeType, source, Expression.Constant(underType)), targetType); + return Expression.Condition(canConvert, finalValue, Expression.Default(targetType)); + + } +} diff --git a/src/AutoGenMapperGenerator/ReflectMapper/ExpressionMapper.Convert.cs b/src/AutoGenMapperGenerator/ReflectMapper/ExpressionMapper.Convert.cs new file mode 100644 index 0000000..271803a --- /dev/null +++ b/src/AutoGenMapperGenerator/ReflectMapper/ExpressionMapper.Convert.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; + +namespace AutoGenMapperGenerator.ReflectMapper; + +internal static partial class ExpressionMapper +{ + +} diff --git a/src/AutoGenMapperGenerator/ReflectMapper/ExpressionMapper.Dictionary.cs b/src/AutoGenMapperGenerator/ReflectMapper/ExpressionMapper.Dictionary.cs new file mode 100644 index 0000000..0600139 --- /dev/null +++ b/src/AutoGenMapperGenerator/ReflectMapper/ExpressionMapper.Dictionary.cs @@ -0,0 +1,141 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; + +namespace AutoGenMapperGenerator.ReflectMapper; +internal static class ExpressionMapper +{ + private static readonly Func, TEntity> mapFromDictionaryFunc; + private static readonly Func> mapToDictionaryFunc; + public static TEntity MapFromDictionary(IDictionary dict) => mapFromDictionaryFunc(dict); + public static IDictionary MapToDictionary(TEntity entity) => mapToDictionaryFunc(entity); + static ExpressionMapper() + { + mapFromDictionaryFunc = CreateMapFromDelegate(); + mapToDictionaryFunc = CreateMapToDelegate(); + } + + private static Func> CreateMapToDelegate() + { + // 参数: TEntity entity + var entityParam = Expression.Parameter(typeof(TEntity), "entity"); + + // 1. 创建新字典: new Dictionary() + var dictionaryType = typeof(Dictionary); + var newDict = Expression.New(dictionaryType); + + // 2. 获取 TEntity 的所有属性 + var properties = typeof(TEntity) + .GetProperties(BindingFlags.Public | BindingFlags.Instance) + .Where(p => p.CanRead); + + // 3. 构建初始化器列表 + var initializers = new List(); + + foreach (var prop in properties) + { + // 字典的 Add 方法 + var addMethod = dictionaryType.GetMethod("Add")!; + + // Key: 属性名 + var keyExpr = Expression.Constant(prop.Name); + + // Value: entity.Property (需要转换为 object) + var valueExpr = Expression.MakeMemberAccess(entityParam, prop); + var convertedValue = Expression.Convert(valueExpr, typeof(object)); + + // 构建 Add 调用: .Add("PropertyName", (object)entity.Property) + var elementInit = Expression.ElementInit(addMethod, keyExpr, convertedValue); + initializers.Add(elementInit); + } + + // 4. 构建集合初始化表达式 + var initExpr = Expression.ListInit(newDict, initializers); + + // 5. 构建 Lambda + var lambda = Expression.Lambda>>(initExpr, entityParam); + + return lambda.Compile(); + } + + private static readonly MethodInfo tryGetValueMethod = typeof(DictionaryExtensions).GetMethod("TryGetValue")!; + private static Func, TEntity> CreateMapFromDelegate() + { + var dictParam = Expression.Parameter(typeof(IDictionary), "dict"); + + // 创建新实体 + var newExpr = Expression.New(typeof(TEntity)); + var properties = typeof(TEntity) + .GetProperties(BindingFlags.Public | BindingFlags.Instance) + .Where(p => p.CanWrite); + var bindings = new List(); + // 1. 声明一个局部变量用于接收值: out object resultValue + ParameterExpression? outVariable = Expression.Variable(typeof(object), "value"); + var assign = Expression.Assign(outVariable, Expression.Constant(null, typeof(object))); + foreach (var prop in properties) + { + var propType = prop.PropertyType; + //var (IsComplex, IsDictionary, IsEnumerable) = ExpressionHelper.IsComplexType(propType); + + var keyConst = Expression.Constant(prop.Name); + // 2. 调用 TryGetValue 方法 + var genericTryGetValueMethod = tryGetValueMethod.MakeGenericMethod(prop.PropertyType); + var tryGetValueCall = Expression.Call(genericTryGetValueMethod, dictParam, keyConst, outVariable); + //var tryGetValueCall = Expression.Call(dictParam, tryGetValueMethod, keyConst, outVariable); + + // 3. 构建转换逻辑:如果 TryGetValue 返回 true,则转换 outVariable;否则使用默认值 + //var conversionExpr = ExpressionHelper.GetConversionExpression(typeof(object), outVariable, propType, CultureInfo.CurrentCulture); + var conversionExpr = BuildConversionFromDictionaryValue(outVariable, propType); + + // 4. 组合条件表达式: dict.TryGetValue(key, out value) ? Convert(value) : default(TProp) + var conditionExpr = Expression.Condition( + tryGetValueCall, // 条件: TryGetValue 的结果 + conversionExpr, // 成功: 转换值 + Expression.Default(propType) // 失败: 默认值 + ); + + //var block = Expression.Block([outVariable], assign, conditionExpr); + var bind = Expression.Bind(prop, conditionExpr); + bindings.Add(bind); + } + + var memberInit = Expression.MemberInit(newExpr, bindings); + var block = Expression.Block([outVariable], assign, memberInit); + var lambda = Expression.Lambda, TEntity>>(block, dictParam); + return lambda.Compile(); + + static Expression BuildConversionFromDictionaryValue(Expression sourceExpr, Type targetType) + { + // 如果目标类型是 object 或 dynamic,直接返回 + if (targetType == typeof(object)) + { + return sourceExpr; + } + + // 处理可空类型 + var underlyingType = Nullable.GetUnderlyingType(targetType); + var conversionTargetType = underlyingType ?? targetType; + + // 如果是枚举 + if (conversionTargetType.IsEnum) + { + // 调用 Enum.Parse + var parseMethod = typeof(Enum).GetMethod("Parse", new[] { typeof(Type), typeof(string), typeof(bool) })!; + var toStringCall = Expression.Call(sourceExpr, typeof(object).GetMethod("ToString")!); + var callExpr = Expression.Call( + parseMethod, + Expression.Constant(conversionTargetType), + toStringCall, + Expression.Constant(true) + ); + return Expression.Convert(callExpr, targetType); + } + return Expression.Convert(sourceExpr, targetType); + } + } +} diff --git a/src/AutoGenMapperGenerator/ReflectMapper/ExpressionMapper.cs b/src/AutoGenMapperGenerator/ReflectMapper/ExpressionMapper.cs index 68f036b..e654c5b 100644 --- a/src/AutoGenMapperGenerator/ReflectMapper/ExpressionMapper.cs +++ b/src/AutoGenMapperGenerator/ReflectMapper/ExpressionMapper.cs @@ -1,10 +1,309 @@ -namespace AutoGenMapperGenerator.ReflectMapper; +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using static AutoGenMapperGenerator.ReflectMapper.ExpressionHelper; +namespace AutoGenMapperGenerator.ReflectMapper; -internal static class ExpressionMapper +internal static partial class ExpressionMapper< +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] +#endif +TSource, +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] +#endif +TTarget> { - public static TTarget Map(TSource source) + private static readonly Func mapFunc; + + public static TTarget Map(TSource source) => mapFunc(source); + static ExpressionMapper() + { + var sourceParam = Expression.Parameter(typeof(TSource), "source"); + + // 获取目标类型的属性信息 + var targetPropsDict = typeof(TTarget) + .GetProperties(BindingFlags.Public | BindingFlags.Instance) + .Where(p => p.CanWrite) + .ToDictionary(p => p.Name); + + // 获取源类型的属性信息 (用于自动映射) + var sourcePropsDict = typeof(TSource) + .GetProperties(BindingFlags.Public | BindingFlags.Instance) + .Where(p => p.CanRead) + .ToDictionary(p => p.Name); + + NewExpression? targetNew; + var bindings = new List(); + // --- 阶段 1: 处理 Profile (如果存在) --- + if (MapperOptions.Instance.TryGetProfile(out var profile)) + { + // 获取 Profile 中定义的所有配置 + var configurations = profile.GetConfigurations(); + var ctorParams = profile.GetConstructorParameters(); + var ignores = profile.GetIgnoreMembers(); + foreach (var item in ignores) + { + sourcePropsDict.Remove(item); + } + if (ctorParams.Count > 0) + { + targetNew = CreateNewWithParameters(sourceParam, ctorParams, name => + { + // 优化: 从自动映射字典中移除已处理的属性,防止后面重复处理 + sourcePropsDict.Remove(name); + }); + } + else + { + targetNew = Expression.New(typeof(TTarget)); + } + foreach (var config in configurations) + { + if (config.SourceExpression == null || config.DestinationExpression == null) continue; + + // 1. 解析目标表达式 (例如: t => t.TargetName) + var targetMember = GetMemberInfoFromLambda(config.DestinationExpression); + if (targetMember == null) continue; + + // 2. 转换源表达式:将 Profile 中定义的表达式转换为基于当前 sourceParam 的表达式 + // 例如:将 (s) => s.A + s.B 转换为基于当前上下文的表达式 + var sourceExpressionBody = config.SourceExpression.Body; + + // 关键步骤:替换表达式中的参数 + var modifiedSourceBody = ReplaceParameter(config.SourceExpression.Parameters[0], sourceParam, sourceExpressionBody); + + // 4. 创建绑定 + var bind = Expression.Bind(targetMember, modifiedSourceBody); + bindings.Add(bind); + + // 优化: 从自动映射字典中移除已处理的属性,防止后面重复处理 + targetPropsDict.Remove(targetMember.Name); + } + } + else + { + targetNew = Expression.New(typeof(TTarget)); + } + + // 剩下的属性按默认规则处理 (名称匹配) + foreach (var targetProp in targetPropsDict.Values) + { + // 检查源对象中是否存在同名属性 + if (!sourcePropsDict.TryGetValue(targetProp.Name, out var sourceProp)) + { + continue; + } + var sourceType = sourceProp.PropertyType; + var targetType = targetProp.PropertyType; + var sp = IsComplexType(sourceType); + var tp = IsComplexType(targetType); + Expression convertedSource; + if ((sp.IsEnumerable || tp.IsEnumerable) && !(sp.IsDictionary || tp.IsDictionary)) // 排除字典 + { + convertedSource = HandleCollectionConversion( + Expression.MakeMemberAccess(sourceParam, sourceProp), + sourceType, + targetType); + } + else if (sp.IsComplex || tp.IsComplex) + { + var sourceAccess = Expression.MakeMemberAccess(sourceParam, sourceProp); + if (sp.IsDictionary && tp.IsDictionary) + { + throw new InvalidOperationException("目标类型和数据源类型都是字典"); + } + if (sp.IsDictionary) + { + var method = typeof(ExpressionMapper<>).MakeGenericType(targetType).GetMethod("MapFromDictionary", [typeof(IDictionary)])!; + convertedSource = Expression.Call(method, sourceAccess); + } + else if (tp.IsDictionary) + { + var method = typeof(ExpressionMapper<>).MakeGenericType(sourceType).GetMethod("MapToDictionary", [sourceType])!; + convertedSource = Expression.Call(method, sourceAccess); + } + else + { + var method = typeof(ExpressionMapper<,>).MakeGenericType(sourceType, targetType).GetMethod("Map", [sourceType])!; + convertedSource = Expression.Call(method, sourceAccess); + } + convertedSource = Expression.Condition(Expression.Equal(sourceAccess, Expression.Default(sourceProp.PropertyType)), + Expression.Default(targetProp.PropertyType), + convertedSource); + } + else + { + var sourceAccess = Expression.MakeMemberAccess(sourceParam, sourceProp); + convertedSource = GetConversionExpression( + sourceProp.PropertyType, + sourceAccess, + targetProp.PropertyType, System.Globalization.CultureInfo.CurrentCulture); + } + + var bind = Expression.Bind(targetProp, convertedSource); + bindings.Add(bind); + } + + // --- 构建表达式树 --- + var memberInit = Expression.MemberInit(targetNew, bindings); + var lambda = Expression.Lambda>(memberInit, sourceParam); + mapFunc = lambda.Compile(); + } + + private static NewExpression CreateNewWithParameters(ParameterExpression source + , IReadOnlyList<(Type, string)> parameters, Action each) + { + var ctorInfo = typeof(TTarget).GetConstructors().FirstOrDefault(FindConstructor) ?? throw new InvalidOperationException("No matching constructor found for the specified parameters."); + +#pragma warning disable IL2026 + List cps = []; + foreach (var item in parameters) + { + cps.Add(Expression.Property(source, item.Item2)); + each.Invoke(item.Item2); + } + return Expression.New(ctorInfo, cps); +#pragma warning restore IL2026 + + bool FindConstructor(ConstructorInfo ctor) + { + var pp = ctor.GetParameters(); + if (pp.Length != parameters.Count) return false; + for (int i = 0; i < pp.Length; i++) + { + if (pp[i].ParameterType != parameters[i].Item1) return false; + } + return true; + } + } + + private static Expression HandleCollectionConversion(Expression sourceExpression, Type sourceType, Type targetType) + { + // 获取元素类型 + // 例如:List -> elementTye = Source + var sourceElementType = GetEnumerableElementType(sourceType); + var targetElementType = GetEnumerableElementType(targetType); + + if (sourceElementType == null || targetElementType == null) + { + // 如果无法获取元素类型,尝试直接转换(可能是 object[] 转 List 等简单情况) + return Expression.Convert(sourceExpression, targetType); + } + + // --- 构建 Select 表达式 --- + // 1. 创建参数: TSourceElement item + var itemParam = Expression.Parameter(sourceElementType, "item"); + + // 2. 构建转换逻辑: 将 item 转换为目标元素类型 + Expression conversionExpr; + + // 如果元素是简单类型或不需要特殊处理,直接使用 Convert + // 如果元素是复杂类型,使用 ExpressionMapper.Map(item) + var elementSp = IsComplexType(sourceElementType); + var elementTp = IsComplexType(targetElementType); + if (elementSp.IsComplex || elementTp.IsComplex) + { + var mapperType = typeof(ExpressionMapper<,>).MakeGenericType(sourceElementType, targetElementType); + var mapMethod = mapperType.GetMethod("Map", BindingFlags.Public | BindingFlags.Static); + conversionExpr = Expression.Call(mapMethod, itemParam); + } + else if (elementSp.IsDictionary) + { + var method = typeof(ExpressionMapper<>).MakeGenericType(targetType).GetMethod("MapFromDictionary", [typeof(IDictionary)])!; + conversionExpr = Expression.Call(method, itemParam); + } + else if (elementTp.IsDictionary) + { + var method = typeof(ExpressionMapper<>).MakeGenericType(sourceType).GetMethod("MapToDictionary", [sourceType])!; + conversionExpr = Expression.Call(method, itemParam); + } + else + { + conversionExpr = Expression.Convert(itemParam, targetElementType); + } + + // 3. 构建 Lambda: item => conversionExpr + var selector = Expression.Lambda(conversionExpr, itemParam); + + // --- 构建 LINQ 查询 --- + // 这里使用 Enumerable.Select 和 Enumerable.ToList + Expression result; + try + { + // 1. 调用 Select: sourceExpression.AsQueryable().Select(selector) + // 注意:对于 IEnumerable,直接使用 Enumerable.Select + var selectMethod = typeof(Enumerable).GetMethods() + .First(m => m.Name == "Select" && m.GetParameters().Length == 2) + .MakeGenericMethod(sourceElementType, targetElementType); + + var selectCall = Expression.Call(selectMethod, sourceExpression, selector); + + // 2. 调用 ToList: Select(...).ToList() + // 这里需要根据目标类型决定是 ToList 还是 ToArray + if (targetType.IsArray) + { + // 如果目标是数组,调用 ToArray() + var toArrayMethod = typeof(Enumerable).GetMethod("ToArray").MakeGenericMethod(targetElementType); + result = Expression.Call(toArrayMethod, selectCall); + } + else + { + // 如果目标是 List 或 IEnumerable,调用 ToList() + targetType = typeof(List<>).MakeGenericType(targetElementType); + var toListMethod = typeof(Enumerable).GetMethod("ToList").MakeGenericMethod(targetElementType); + result = Expression.Call(toListMethod, selectCall); + } + } + catch (Exception ex) + { + // 如果 Linq 方法调用失败(例如方法未找到),回退到直接转换 + result = Expression.Convert(sourceExpression, targetType); + } + return Expression.Condition(Expression.Equal(sourceExpression, Expression.Default(sourceType)) + , Expression.Default(targetType) + , result); + + + static Type? GetEnumerableElementType(Type type) + { + if (type.IsArray) + { + return type.GetElementType(); + } + + foreach (var iface in type.GetInterfaces().Concat([type])) + { + if (iface.IsGenericType) + { + var def = iface.GetGenericTypeDefinition(); + if (def == typeof(IEnumerable<>) || def == typeof(ICollection<>) || def == typeof(IList<>)) + { + return iface.GetGenericArguments()[0]; + } + } + } + + return null; + } + } + + + private static Expression ReplaceParameter(ParameterExpression oldParam, ParameterExpression newParam, Expression expression) + { + + var visitor = new ParameterReplaceVisitor(oldParam, newParam); + return visitor.Visit(expression); + } + class ParameterReplaceVisitor(ParameterExpression oldParameter, ParameterExpression newParameter) : ExpressionVisitor { - // TODO - throw new System.NotImplementedException(); + protected override Expression VisitParameter(ParameterExpression node) + { + // 如果节点是我们要替换的旧参数,返回新参数 + return node == oldParameter ? newParameter : base.VisitParameter(node); + } } } diff --git a/src/AutoGenMapperGenerator/ReflectMapper/IMapperService.cs b/src/AutoGenMapperGenerator/ReflectMapper/IMapperService.cs new file mode 100644 index 0000000..5d71cec --- /dev/null +++ b/src/AutoGenMapperGenerator/ReflectMapper/IMapperService.cs @@ -0,0 +1,88 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AutoGenMapperGenerator.ReflectMapper; + +/// +/// +/// +public interface IMapperService +{ + /// + /// 首先检查是否,如果是,则直接调用接口 + /// 否侧,回退到表达式动态创建映射 + /// + /// + /// + /// + /// + TTarget Map< +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] +#endif + TSource, +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] +#endif + TTarget>(TSource source); + + /// + /// + /// + /// + /// + /// + IDictionary ToDictionary(TEntity entity); + + /// + /// + /// + /// + /// + /// + TEntity ToEntity(IDictionary dict); +} + + +internal sealed class MapperService : IMapperService +{ + public TTarget Map<[DynamicallyAccessedMembers((DynamicallyAccessedMemberTypes)(-1))] TSource, [DynamicallyAccessedMembers((DynamicallyAccessedMemberTypes)(-1))] TTarget>(TSource source) + { + if (source is null) + { + return default!; + } + if (source is IAutoMap m) + { + return m.MapTo(); + } + var (IsComplex, IsDictionary, IsEnumerable) = ExpressionHelper.IsComplexType(typeof(TSource)); + if (IsComplex && IsDictionary) + { + throw new InvalidOperationException("请使用TEntity ToEntity(this IDictionary dict)"); + } + if (IsComplex && IsEnumerable) + { + throw new InvalidOperationException("不支持集合类型的映射,请使用LINQ的Select方法进行映射"); + } + return ExpressionMapper.Map(source); + } + + public IDictionary ToDictionary(TEntity entity) + { + if (entity is null) + { + return new Dictionary(); + } + return ExpressionMapper.MapToDictionary(entity); + } + + public TEntity ToEntity(IDictionary dict) + { + return ExpressionMapper.MapFromDictionary(dict); + } +} \ No newline at end of file diff --git a/src/AutoGenMapperGenerator/ReflectMapper/IoCExtensions.cs b/src/AutoGenMapperGenerator/ReflectMapper/IoCExtensions.cs new file mode 100644 index 0000000..aa176b3 --- /dev/null +++ b/src/AutoGenMapperGenerator/ReflectMapper/IoCExtensions.cs @@ -0,0 +1,26 @@ +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Collections.Generic; +using System.Text; + +namespace AutoGenMapperGenerator.ReflectMapper; + +/// +/// +/// +public static class IoCExtensions +{ + /// + /// + /// + /// + /// + /// + public static IServiceCollection AddMapperService(this IServiceCollection services, Action? optionsAction = null) + { + optionsAction?.Invoke(MapperOptions.Instance); + services.AddSingleton(MapperOptions.Instance); + services.AddSingleton(); + return services; + } +} diff --git a/src/AutoGenMapperGenerator/ReflectMapper/MapperOptions.cs b/src/AutoGenMapperGenerator/ReflectMapper/MapperOptions.cs new file mode 100644 index 0000000..e4104dc --- /dev/null +++ b/src/AutoGenMapperGenerator/ReflectMapper/MapperOptions.cs @@ -0,0 +1,169 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Linq.Expressions; +using System.Text; +using System.Threading.Tasks; + +namespace AutoGenMapperGenerator.ReflectMapper +{ + /// + /// + /// + public sealed class MapperOptions + { + private MapperOptions() + { + + } + private static readonly Lazy lazyInstance = new(new MapperOptions()); + /// + /// + /// + public static MapperOptions Instance => lazyInstance.Value; + + private readonly ConcurrentDictionary<(Type Source, Type Target), object> mapperProfiles = new(); + /// + /// + /// + /// + /// + /// + public void AddProfile(MapperProfile profile) + { + mapperProfiles.TryAdd((typeof(TSource), typeof(TTarget)), profile); + } + + public void AddProfile() + where TProfile : new() + { + var profileType = typeof(TProfile); + var genericArgs = profileType.BaseType?.GetGenericArguments(); + if (genericArgs?.Length == 2) + { + var key = (genericArgs[0], genericArgs[1]); + mapperProfiles.TryAdd(key, new TProfile()); + } + else + { + throw new ArgumentException("The provided profile does not inherit from MapperProfile."); + } + } + + /// + /// + /// + /// + /// + /// + public void ConfigProfile(Action> configAction) + { + var key = (typeof(TSource), typeof(TTarget)); + if (mapperProfiles.TryGetValue(key, out var existingProfileObj) && existingProfileObj is MapperProfile existingProfile) + { + configAction(existingProfile); + } + else + { + var newProfile = new MapperProfile(); + configAction(newProfile); + mapperProfiles.TryAdd(key, newProfile); + } + } + + internal bool TryGetProfile([NotNullWhen(true)] out MapperProfile? profile) + { + var key = (typeof(TSource), typeof(TTarget)); + if (mapperProfiles.TryGetValue(key, out var existingProfileObj) && existingProfileObj is MapperProfile existingProfile) + { + profile = existingProfile; + return true; + } + profile = null; + return false; + } + } + + /// + /// + /// + /// + /// + public class MapperProfile + { + private readonly List configurations = []; + private readonly List<(Type, string)> constructorParameters = []; + private readonly List ignoreMembers = []; + /// + /// + /// + /// + /// + /// + /// + public void ForMember( + Expression> destinationMemberExpression + , Expression> sourceMemberExpression + ) + { + configurations.Add(new MappingConfiguration + { + SourceExpression = sourceMemberExpression, + DestinationExpression = destinationMemberExpression + }); + } + + /// + /// 配置构造函数参数映射 + /// + /// + public void ForConstructor(params Expression>[] parameters) + { + constructorParameters.Clear(); + foreach (var item in parameters) + { + var member = ExpressionHelper.GetMemberTypeAndNameFromLambda(item); + constructorParameters.Add(member); + } + } + + /// + /// + /// + /// + public void ForIgnores(params Expression>[] parameters) + { + foreach (var item in parameters) + { + var member = ExpressionHelper.GetMemberInfoFromLambda(item); + if (member is not null) + { + ignoreMembers.Add(member.Name); + } + } + } + + internal IReadOnlyList GetConfigurations() + { + return configurations.AsReadOnly(); + } + + internal IReadOnlyList<(Type, string)> GetConstructorParameters() + { + return constructorParameters.AsReadOnly(); + } + + internal IReadOnlyList GetIgnoreMembers() + { + return ignoreMembers.AsReadOnly(); + } + + internal class MappingConfiguration + { + public LambdaExpression? SourceExpression { get; set; } + public LambdaExpression? DestinationExpression { get; set; } + } + } +} diff --git a/src/AutoGenMapperGenerator/Versions.props b/src/AutoGenMapperGenerator/Versions.props index 068b9a4..b08c4be 100644 --- a/src/AutoGenMapperGenerator/Versions.props +++ b/src/AutoGenMapperGenerator/Versions.props @@ -1,6 +1,6 @@ - 0.1.0-pre + 0.1.0 $(BuildVersion) $(BuildVersion) diff --git a/src/Blazor.Test/Blazor.Test.Client/Models/User.cs b/src/Blazor.Test/Blazor.Test.Client/Models/User.cs index 7c27ac9..9796833 100644 --- a/src/Blazor.Test/Blazor.Test.Client/Models/User.cs +++ b/src/Blazor.Test/Blazor.Test.Client/Models/User.cs @@ -29,3 +29,11 @@ public partial class UserDto public string Display { get; set; } } + +public class UserProfile : AutoGenMapperGenerator.ReflectMapper.MapperProfile +{ + public UserProfile() + { + //ForMember(); + } +} \ No newline at end of file diff --git a/src/Blazor.Test/Blazor.Test/Program.cs b/src/Blazor.Test/Blazor.Test/Program.cs index a4cdba3..43fbc39 100644 --- a/src/Blazor.Test/Blazor.Test/Program.cs +++ b/src/Blazor.Test/Blazor.Test/Program.cs @@ -5,6 +5,8 @@ using Blazor.Test.Components; using Microsoft.AspNetCore.Authentication.Cookies; using AutoPageStateContainerGenerator; +using AutoGenMapperGenerator.ReflectMapper; +using Blazor.Test.Client.Models; [assembly: AutoWasmApiGenerator.WebControllerAssembly] var builder = WebApplication.CreateBuilder(args); @@ -24,6 +26,13 @@ builder.Services.InjectHybrid(); builder.Services.AddHttpClient(); builder.Services.AddStateContainers(); +builder.Services.AddMapperService(o => +{ + o.ConfigProfile(profile => + { + + }); +}); //builder.Services.(); var app = builder.Build(); diff --git a/src/TestProject1/MapperGenTest.cs b/src/TestProject1/MapperGenTest.cs index 92fdf74..b12aa6e 100644 --- a/src/TestProject1/MapperGenTest.cs +++ b/src/TestProject1/MapperGenTest.cs @@ -1,9 +1,12 @@ using AutoGenMapperGenerator; +using AutoGenMapperGenerator.ReflectMapper; +using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; +using TestProject1.AopGeneratorTest; using TestProject1.Models; namespace TestProject1 @@ -27,9 +30,99 @@ public void AutoMode() } [TestMethod] - public void MapFromTest() + public void ExpressionMapEntity() { + MapperOptions.Instance.ConfigProfile(profile => + { + profile.ForMember(dest => dest.Date, opt => opt.ProductDate); + profile.ForMember(dest => dest.Name, opt => $"{opt.Name} - {opt.Category}"); + profile.ForConstructor(p => p.Id); + }); + var p = new Product2() + { + Id = 5, + Name = "Product A", + Category = "Category 1", + ProductDate = DateTime.Now, + SubProduct = new() + { + Id = 6, + Name = "Product B", + }, + Products = new List() + { + new Product2() + { + Id =7, + Name="Product C" + }, + new Product2() + { + Id=8, + Name="Product D" + } + } + }; + + var dto = p.Map(); + + } + class TTT + { + public int Id { get; set; } + public string? Name { get; set; } + public string? Category { get; set; } + public DateTime? ProductDate { get; set; } + } + [TestMethod] + public void TestFromDictionary() + { + var dateString = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"); + var date = DateTime.Now; + var dict = new Dictionary() + { + { "Id", 1 }, + { "Name", "Test Product" }, + { "Category", "Category A" }, + { "ProductDate", date }, + }; + var product = dict.ToEntity(); + Assert.IsNotNull(product); + Assert.AreEqual(1, product.Id); + Assert.AreEqual("Test Product", product.Name); + Assert.AreEqual("Category A", product.Category); + } + + [TestMethod] + public void TestToDictionary() + { + var p = new Product2() + { + Id = 5, + Name = "Product A", + Category = "Category 1", + ProductDate = DateTime.Now, + SubProduct = new() + { + Id = 6, + Name = "Product B", + }, + Products = new List() + { + new Product2() + { + Id =7, + Name="Product C" + }, + new Product2() + { + Id=8, + Name="Product D" + } + } + }; + var dict = p.ToDictionary(); } } } diff --git a/src/TestProject1/Models/User.cs b/src/TestProject1/Models/User.cs index 70bcb9e..6df8d09 100644 --- a/src/TestProject1/Models/User.cs +++ b/src/TestProject1/Models/User.cs @@ -95,4 +95,36 @@ public ProductDto(int id) public ProductDto? SubProduct { get; set; } public IEnumerable Products { get; set; } = []; } + + + internal partial class Product2 + { + public int Id { get; set; } + public string? Name { get; set; } + public string? Category { get; set; } + public DateTime? ProductDate { get; set; } + public Product2? SubProduct { get; set; } + public IEnumerable Products { get; set; } = []; + public string? SplitValue { get; set; } + + } + + internal class Product2Dto + { + public Product2Dto() + { + + } + public Product2Dto(int id) + { + Id = id; + } + public int Id { get; set; } + + public string? Name { get; set; } + //[MapFrom(Source = typeof(Product), Name = nameof(Product.ProductDate))] + public DateTime? Date { get; set; } + public Product2Dto? SubProduct { get; set; } + public IEnumerable Products { get; set; } = []; + } }