diff --git a/src/SmartSql.Test.Unit/Deserializer/EntityDeserializerTest.cs b/src/SmartSql.Test.Unit/Deserializer/EntityDeserializerTest.cs index cc223763..589cce0e 100644 --- a/src/SmartSql.Test.Unit/Deserializer/EntityDeserializerTest.cs +++ b/src/SmartSql.Test.Unit/Deserializer/EntityDeserializerTest.cs @@ -1,6 +1,7 @@ using SmartSql.Test.Entities; using System; using System.Collections.Generic; +using System.Linq; using System.Text; using System.Threading.Tasks; using Xunit; @@ -75,5 +76,37 @@ public async Task QueryAsync() }); Assert.NotNull(list); } + // Unit test for nested property mapping functionality + // 嵌套属性映射功能的单元测试 + [Fact] + public void NestedPropertyMappingTest() + { + // Execute query with nested property mapping + // 执行包含嵌套属性映射的查询 + var list = SqlMapper.Query(new RequestContext + { + Scope = nameof(AllPrimitive), + SqlId = "QueryNestedPropertyResult", // SQL statement ID (SQL语句ID) + Request = new { Taken = 10000 } // Query parameters (查询参数) + }); + + // Verify result validity + // 验证结果有效性 + Assert.NotNull(list); // Result list should not be null (结果列表不应为空) + + // Validate nested properties existence + // 验证嵌套属性存在性 + Assert.NotNull(list.First().NestedProp1); // Level 1 nesting (第一层嵌套) + Assert.NotNull(list.First().NestedProp1.NestedProp2); // Level 2 nesting (第二层嵌套) + Assert.NotNull(list.First().NestedProp1.NestedProp2.NestedProp3); // Level 3 nesting (第三层嵌套) + + // Test Purpose: + // Verifies ORM's ability to handle multi-level nested object mapping + // 验证ORM处理多级嵌套对象映射的能力 + // Key Validations: + // 1. Correct parsing of nested property paths (正确解析嵌套属性路径) + // 2. Proper object initialization at each level (各级对象正确初始化) + // 3. Maintains data integrity through mapping (通过映射保持数据完整性) + } } } \ No newline at end of file diff --git a/src/SmartSql.Test.Unit/Maps/AllPrimitive.xml b/src/SmartSql.Test.Unit/Maps/AllPrimitive.xml index e4d59ca1..116f03ac 100644 --- a/src/SmartSql.Test.Unit/Maps/AllPrimitive.xml +++ b/src/SmartSql.Test.Unit/Maps/AllPrimitive.xml @@ -8,6 +8,12 @@ //*******************************--> + + + + + + @@ -513,6 +519,21 @@ + + + SELECT + T.* From T_AllPrimitive T + + + + T.Id Desc + + + + ?Taken + + + diff --git a/src/SmartSql.Test/Entities/NestedEntity.cs b/src/SmartSql.Test/Entities/NestedEntity.cs new file mode 100644 index 00000000..5fd69938 --- /dev/null +++ b/src/SmartSql.Test/Entities/NestedEntity.cs @@ -0,0 +1,17 @@ + +namespace SmartSql.Test.Entities +{ + public class NestedEntity + { + public virtual long Id { get; set; } + public virtual NestedProperty1 NestedProp1 { get; set; } + } + public class NestedProperty1 + { + public virtual NestedProperty2 NestedProp2 { get; set; } + } + public class NestedProperty2 + { + public virtual string NestedProp3 { get; set; } + } +} diff --git a/src/SmartSql/Deserializer/EntityDeserializer.cs b/src/SmartSql/Deserializer/EntityDeserializer.cs index f06b3407..1fcdcddf 100644 --- a/src/SmartSql/Deserializer/EntityDeserializer.cs +++ b/src/SmartSql/Deserializer/EntityDeserializer.cs @@ -197,11 +197,70 @@ private Delegate CreateDeserialize(ExecutionContext executionContext) ilGen.Call(DataType.Method.IsDBNull); ilGen.IfTrueS(isDbNullLabel); } + // Handle property chain access logic (处理属性链访问逻辑) + if (propertyHolder.IsChain) + { + // Load root object instance (加载根对象实例) + ilGen.LoadLocalVar(0); // Stack: [currentObj] + + // Traverse all properties except the last one in the chain (遍历属性链中除最后属性外的所有属性) + foreach (var prop in propertyHolder.PropertyChain.Take(propertyHolder.PropertyChain.Count - 1)) + { + // Define null-check label (定义空值检查标签) + var notNullLabel = ilGen.DefineLabel(); + + // ==== Start null-check logic ==== (开始空值检查逻辑) + + // 1. Preserve current instance reference (保留当前实例引用) + ilGen.Dup(); // Stack: [currentObj, currentObj] + + // 2. Get child property value (获取子属性值) + ilGen.Call(prop.GetMethod); // Stack: [childObj, currentObj] + + // 3. Check if child object is null (检查子对象是否为空) + ilGen.IfTrueS(notNullLabel); // Stack: [currentObj] (consumes childObj) + + // ==== Null branch ==== (空值分支) + + // 1. Preserve parent instance again (再次保留父实例) + ilGen.Dup(); // Stack: [currentObj, currentObj] + + // 2. Create new child instance (创建新的子实例) + ilGen.New(prop.PropertyType.GetConstructor(Type.EmptyTypes)); // Stack: [newChildObj, currentObj, currentObj] + + // 3. Assign new instance to parent property (将新实例赋值给父属性) + ilGen.Call(prop.SetMethod); // Stack: [currentObj] (consumes currentObj and newChildObj) + + // ==== Non-null branch ==== (非空值分支) + ilGen.MarkLabel(notNullLabel); // Stack: [currentObj] + + // Get child object (either existing or newly created) (获取子对象:已存在的或新创建的) + ilGen.Call(prop.GetMethod); // Stack: [childObj] + + // Now childObj becomes the new currentObj for next iteration (此时childObj成为下一轮迭代的currentObj) + } + + // ==== Set final property value ==== (设置最终属性值) + + // 1. Load property value onto stack (加载属性值到栈顶) + LoadPropertyValue(ilGen, executionContext, propertyType, columnDescriptor.FieldType, propertyHolder.TypeHandler); + + // 2. Call final set method (调用最终set方法) + ilGen.Call(propertyHolder.SetMethod); // Stack: [] (consumes childObj and value) + } + // Handle single property access (处理单属性访问) + else + { + // 1. Load root instance (加载根实例) + ilGen.LoadLocalVar(0); // Stack: [currentObj] + + // 2. Load property value (加载属性值) + LoadPropertyValue(ilGen, executionContext, propertyType, columnDescriptor.FieldType, propertyHolder.TypeHandler); + + // 3. Directly set property (直接设置属性) + ilGen.Call(propertyHolder.SetMethod); // Stack: [] (consumes currentObj and value) + } - ilGen.LoadLocalVar(0); - LoadPropertyValue(ilGen, executionContext, propertyType, columnDescriptor.FieldType, - propertyHolder.TypeHandler); - ilGen.Call(propertyHolder.SetMethod); if (ignoreDbNull) { ilGen.MarkLabel(isDbNullLabel); @@ -278,21 +337,46 @@ public static void ThrowDeserializeException(Exception ex, Object result, int co private static bool ResolveProperty(ResultMap resultMap, Type resultType, ColumnDescriptor columnDescriptor - , out PropertyHolder propertyHolder) + , out IPropertyHolder propertyHolder) { propertyHolder = null; if (resultMap?.Properties != null) { if (resultMap.Properties.TryGetValue(columnDescriptor.ColumnName, out var resultProperty)) { - var property = resultType.GetProperty(resultProperty.Name) ?? - throw new SmartSqlException($"ResultMap:[{resultMap.Id}], can not find property:[{resultProperty.Name}] in class:[{resultType.Name}]"); - propertyHolder = new PropertyHolder + // Handle nested property path (处理嵌套属性路径) + if (resultProperty.Name.Contains('.')) { - Property = property, - TypeHandler = resultProperty.TypeHandler - }; - return true; + // Parse property chain (e.g. "User.Address.City") and create chain holder + // 解析属性链(例如"User.Address.City")并创建链式属性容器 + propertyHolder = new PropertyChainHolder( + ParsePropertyChain( + resultMapId: resultMap.Id, // 当前ResultMap ID + rootType: resultType, // 根对象类型 + propertyPath: resultProperty.Name // 属性路径字符串 + ), + resultProperty.TypeHandler // 类型处理器 + ); + return true; // Successfully resolved property chain (成功解析属性链) + } + // Handle single property (处理单属性) + else + { + // Get property info from result type (从结果类型获取属性信息) + var property = resultType.GetProperty(resultProperty.Name) + ?? throw new SmartSqlException( + $"ResultMap:[{resultMap.Id}], can not find property:[{resultProperty.Name}] in class:[{resultType.Name}]" + // 错误格式:结果映射:[ID], 在类[类名]中找不到属性[属性名] + ); + + // Create standard property holder (创建标准属性容器) + propertyHolder = new PropertyHolder + { + Property = property, // 目标属性 + TypeHandler = resultProperty.TypeHandler // 类型处理器 + }; + return true; // Successfully resolved single property (成功解析单属性) + } } } @@ -321,6 +405,27 @@ ColumnDescriptor columnDescriptor return false; } + private static List ParsePropertyChain(string resultMapId, Type rootType, string propertyPath) + { + var propertyNames = propertyPath.Split('.'); + var chain = new List(); + Type currentType = rootType; + + foreach (var name in propertyNames) + { + var property = currentType.GetProperty(name); + if (property == null) + { + throw new SmartSqlException($"ResultMap:[{resultMapId}], Cannot find property:[{name}] in type:[{currentType.Name}] for path:[{propertyPath}]"); + } + + chain.Add(property); + currentType = property.PropertyType; + } + + return chain; + } + private void LoadPropertyValue(ILGenerator ilGen, ExecutionContext executionContext, Type propertyType, Type fieldType, String typeHandler) { diff --git a/src/SmartSql/Reflection/IPropertyHolder.cs b/src/SmartSql/Reflection/IPropertyHolder.cs new file mode 100644 index 00000000..31ca778d --- /dev/null +++ b/src/SmartSql/Reflection/IPropertyHolder.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Reflection; + +namespace SmartSql.Reflection +{ + internal interface IPropertyHolder + { + PropertyInfo Property { get; set; } + String TypeHandler { get; set; } + + Type PropertyType { get; } + + bool CanWrite { get; } + + MethodInfo SetMethod { get; } + + /// + /// 是否为属性链(如 User.Address.City) + /// + bool IsChain { get; } + + /// + /// 属性链中的中间属性(仅当 IsChain = true 时有效) + /// + IReadOnlyList PropertyChain { get; } + } +} \ No newline at end of file diff --git a/src/SmartSql/Reflection/PropertyChainHolder.cs b/src/SmartSql/Reflection/PropertyChainHolder.cs new file mode 100644 index 00000000..3bb42044 --- /dev/null +++ b/src/SmartSql/Reflection/PropertyChainHolder.cs @@ -0,0 +1,82 @@ + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace SmartSql.Reflection +{ + /// + /// Represents a chain of nested properties (e.g. User.Address.City) + /// ʾǶ User.Address.City + /// + public class PropertyChainHolder : IPropertyHolder + { + private readonly PropertyInfo property; + + /// + /// Gets the final property in the chain (ȡһ) + /// Note: Setter is explicitly disabled for immutability (Setter ʽԱ֤ɱ) + /// + public PropertyInfo Property { get => property; set => throw new NotSupportedException(); } + + /// + /// Type handler for property value conversion (ֵת) + /// + public string TypeHandler { get; set; } + + /// + /// Property type of the final property (Ե) + /// + public Type PropertyType => Property.PropertyType; + + /// + /// Indicates if all properties in the chain are writable (ʶǷд) + /// Pre-calculated during initialization for performance (ʼʱԤŻ) + /// + public bool CanWrite { get; } + + /// + /// Setter method of the final property (Ե÷) + /// Essential for reflection-based value assignment (ڷ丳ֵĹؼ) + /// + public MethodInfo SetMethod => Property.SetMethod; + + /// + /// Explicit marker for chain property type (ȷʶʽ) + /// Always returns true for this implementation (ڱʵк㷵 true) + /// + public bool IsChain => true; + + /// + /// Immutable list of property chain elements (ɱԪؼ) + /// Stored as read-only collection for thread safety (洢ΪֻԱ֤̰߳ȫ) + /// + public IReadOnlyList PropertyChain { get; } + + /// + /// Constructs a property chain holder (캯) + /// + /// + /// Ordered list of properties in the chain (б) + /// Must contain at least one element (һԪ) + /// + /// + /// Type conversion handler for the property (ת) + /// + public PropertyChainHolder(List propertyChain, string typeHandler) + { + // Capture final property () + property = propertyChain.Last(); + + // Create defensive copy as read-only (ԸΪֻ) + PropertyChain = propertyChain.AsReadOnly(); + + // Pre-calculate writability status (Ԥд״̬) + CanWrite = PropertyChain.All(property => property.CanWrite); + + // Store type handler (洢ʹ) + TypeHandler = typeHandler; + } + } +} \ No newline at end of file diff --git a/src/SmartSql/Reflection/PropertyHolder.cs b/src/SmartSql/Reflection/PropertyHolder.cs index 70aeb769..461a49f0 100644 --- a/src/SmartSql/Reflection/PropertyHolder.cs +++ b/src/SmartSql/Reflection/PropertyHolder.cs @@ -1,9 +1,10 @@ using System; +using System.Collections.Generic; using System.Reflection; namespace SmartSql.Reflection { - public class PropertyHolder + public class PropertyHolder : IPropertyHolder { public PropertyInfo Property { get; set; } public String TypeHandler { get; set; } @@ -13,5 +14,10 @@ public class PropertyHolder public bool CanWrite => Property.CanWrite; public MethodInfo SetMethod => Property.SetMethod; + + public bool IsChain => false; + + public IReadOnlyList PropertyChain => throw new NotSupportedException(); + } } \ No newline at end of file