Skip to content

Commit c99ed28

Browse files
committed
Fixed issue with nested collection mapping and improved test assertions
1 parent 9d47bf5 commit c99ed28

File tree

8 files changed

+782
-200
lines changed

8 files changed

+782
-200
lines changed

src/MiniExcel.Core/Helpers/CollectionAccessor.cs

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
namespace MiniExcelLib.Core.Helpers;
1+
using System.Linq.Expressions;
2+
3+
namespace MiniExcelLib.Core.Helpers;
24

35
/// <summary>
46
/// Optimized collection access utilities to reduce code duplication across mapping components.
@@ -56,10 +58,27 @@ public static object FinalizeCollection(IList list, Type targetType, Type itemTy
5658
/// </summary>
5759
/// <param name="itemType">The type to create instances of</param>
5860
/// <returns>A factory function that creates new instances</returns>
59-
public static Func<object?> CreateItemFactory(Type itemType)
60-
{
61-
return () => itemType.IsValueType ? Activator.CreateInstance(itemType) : null;
62-
}
61+
public static Func<object?> CreateItemFactory(Type itemType)
62+
{
63+
// Value types can always be created via Activator.CreateInstance
64+
if (itemType.IsValueType)
65+
{
66+
return () => Activator.CreateInstance(itemType);
67+
}
68+
69+
// For reference types, prefer a compiled parameterless constructor if available
70+
var ctor = itemType.GetConstructor(Type.EmptyTypes);
71+
if (ctor is null)
72+
{
73+
// No default constructor - unable to materialize items automatically
74+
return () => null;
75+
}
76+
77+
var newExpression = Expression.New(ctor);
78+
var lambda = Expression.Lambda<Func<object?>>(Expression.Convert(newExpression, typeof(object)));
79+
var factory = lambda.Compile();
80+
return factory;
81+
}
6382

6483
/// <summary>
6584
/// Determines the item type from a collection type.
Lines changed: 193 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -1,106 +1,194 @@
1-
namespace MiniExcelLib.Core.Helpers;
2-
3-
/// <summary>
4-
/// Helper class for extracting mapping metadata using reflection.
5-
/// Consolidates reflection-based property extraction logic to reduce duplication and improve performance.
6-
/// </summary>
7-
internal static class MappingMetadataExtractor
8-
{
9-
/// <summary>
10-
/// Extracts nested mapping information from a compiled mapping object.
11-
/// This method minimizes reflection by extracting properties once at compile time.
12-
/// </summary>
13-
/// <param name="nestedMapping">The nested mapping object to extract information from</param>
14-
/// <param name="itemType">The type of items in the nested mapping</param>
15-
/// <returns>Nested mapping information or null if extraction fails</returns>
16-
public static NestedMappingInfo? ExtractNestedMappingInfo(object nestedMapping, Type itemType)
17-
{
18-
// Use reflection minimally to extract properties from the nested mapping
19-
// This is done once at compile time, not at runtime
20-
var nestedMappingType = nestedMapping.GetType();
21-
var propsProperty = nestedMappingType.GetProperty("Properties");
22-
23-
if (propsProperty?.GetValue(nestedMapping) is not IEnumerable properties)
24-
return null;
25-
26-
var nestedInfo = new NestedMappingInfo
27-
{
28-
ItemType = itemType,
29-
ItemFactory = CollectionAccessor.CreateItemFactory(itemType)
30-
};
31-
32-
var propertyList = ExtractPropertyList(properties);
33-
nestedInfo.Properties = propertyList;
34-
35-
return nestedInfo;
36-
}
37-
38-
/// <summary>
39-
/// Extracts a list of property information from a collection of property mapping objects.
40-
/// </summary>
41-
/// <param name="properties">The collection of property mappings</param>
42-
/// <returns>A list of nested property information</returns>
43-
private static List<NestedPropertyInfo> ExtractPropertyList(IEnumerable properties)
44-
{
45-
var propertyList = new List<NestedPropertyInfo>();
46-
47-
foreach (var prop in properties)
48-
{
49-
var propType = prop.GetType();
50-
var nameProperty = propType.GetProperty("PropertyName");
51-
var columnProperty = propType.GetProperty("CellColumn");
52-
var getterProperty = propType.GetProperty("Getter");
53-
var setterProperty = propType.GetProperty("Setter");
54-
var typeProperty = propType.GetProperty("PropertyType");
55-
56-
if (nameProperty is null || columnProperty is null || getterProperty is null)
57-
continue;
58-
59-
var name = nameProperty.GetValue(prop) as string;
60-
var column = (int)columnProperty.GetValue(prop)!;
61-
var getter = getterProperty.GetValue(prop) as Func<object, object?>;
62-
var setter = setterProperty?.GetValue(prop) as Action<object, object?>;
63-
var propTypeValue = typeProperty?.GetValue(prop) as Type;
64-
65-
if (name is not null && getter is not null)
66-
{
67-
propertyList.Add(new NestedPropertyInfo
68-
{
69-
PropertyName = name,
70-
ColumnIndex = column,
71-
Getter = getter,
72-
Setter = setter ?? ((_, _) => { }),
73-
PropertyType = propTypeValue ?? typeof(object)
74-
});
75-
}
76-
}
77-
78-
return propertyList;
79-
}
80-
81-
/// <summary>
82-
/// Gets a specific property by name from a type.
83-
/// </summary>
84-
/// <param name="type">The type to search</param>
85-
/// <param name="propertyName">The name of the property</param>
86-
/// <returns>PropertyInfo if found, otherwise null</returns>
87-
public static PropertyInfo? GetPropertyByName(Type type, string propertyName)
88-
{
89-
return type.GetProperty(propertyName, BindingFlags.Public | BindingFlags.Instance);
90-
}
91-
92-
private static bool IsSimpleType(Type type)
93-
{
94-
return type == typeof(string) || type.IsValueType || type.IsPrimitive;
95-
}
96-
97-
/// <summary>
98-
/// Determines if a type is a complex type that likely has nested properties.
99-
/// </summary>
100-
/// <param name="type">The type to check</param>
101-
/// <returns>True if the type is considered complex</returns>
102-
public static bool IsComplexType(Type type)
103-
{
104-
return !IsSimpleType(type) && type != typeof(object);
105-
}
1+
namespace MiniExcelLib.Core.Helpers;
2+
3+
/// <summary>
4+
/// Helper class for extracting mapping metadata using reflection.
5+
/// Consolidates reflection-based property extraction logic to reduce duplication and improve performance.
6+
/// </summary>
7+
internal static class MappingMetadataExtractor
8+
{
9+
/// <summary>
10+
/// Extracts nested mapping information from a compiled mapping object.
11+
/// This method minimizes reflection by extracting properties once at compile time.
12+
/// </summary>
13+
/// <param name="nestedMapping">The nested mapping object to extract information from</param>
14+
/// <param name="itemType">The type of items in the nested mapping</param>
15+
/// <returns>Nested mapping information or null if extraction fails</returns>
16+
public static NestedMappingInfo? ExtractNestedMappingInfo(object nestedMapping, Type itemType)
17+
{
18+
// Use reflection minimally to extract properties from the nested mapping
19+
// This is done once at compile time, not at runtime
20+
var nestedMappingType = nestedMapping.GetType();
21+
var propsProperty = nestedMappingType.GetProperty("Properties");
22+
23+
if (propsProperty?.GetValue(nestedMapping) is not IEnumerable properties)
24+
return null;
25+
26+
var nestedInfo = new NestedMappingInfo
27+
{
28+
ItemType = itemType,
29+
ItemFactory = CollectionAccessor.CreateItemFactory(itemType)
30+
};
31+
32+
var propertyList = ExtractPropertyList(properties, itemType);
33+
nestedInfo.Properties = propertyList;
34+
35+
var collectionsProperty = nestedMappingType.GetProperty("Collections");
36+
if (collectionsProperty?.GetValue(nestedMapping) is IEnumerable collectionMappings)
37+
{
38+
var nestedCollections = new Dictionary<string, NestedCollectionInfo>(StringComparer.Ordinal);
39+
40+
foreach (var collection in collectionMappings)
41+
{
42+
if (collection is not CompiledCollectionMapping compiledCollection)
43+
continue;
44+
45+
var nestedItemType = compiledCollection.ItemType ?? typeof(object);
46+
var collectionInfo = new NestedCollectionInfo
47+
{
48+
PropertyName = compiledCollection.PropertyName,
49+
StartColumn = compiledCollection.StartCellColumn,
50+
StartRow = compiledCollection.StartCellRow,
51+
Layout = compiledCollection.Layout,
52+
RowSpacing = compiledCollection.RowSpacing,
53+
ItemType = nestedItemType,
54+
Getter = compiledCollection.Getter,
55+
Setter = compiledCollection.Setter,
56+
ListFactory = () => CollectionAccessor.CreateTypedList(nestedItemType),
57+
ItemFactory = CollectionAccessor.CreateItemFactory(nestedItemType)
58+
};
59+
60+
if (compiledCollection.Registry is not null && nestedItemType != typeof(object))
61+
{
62+
var childMapping = compiledCollection.Registry.GetCompiledMapping(nestedItemType);
63+
if (childMapping is not null)
64+
{
65+
collectionInfo.NestedMapping = ExtractNestedMappingInfo(childMapping, nestedItemType);
66+
}
67+
}
68+
69+
nestedCollections[collectionInfo.PropertyName] = collectionInfo;
70+
}
71+
72+
if (nestedCollections.Count > 0)
73+
{
74+
nestedInfo.Collections = nestedCollections;
75+
}
76+
}
77+
78+
return nestedInfo;
79+
}
80+
81+
/// <summary>
82+
/// Extracts a list of property information from a collection of property mapping objects.
83+
/// </summary>
84+
/// <param name="properties">The collection of property mappings</param>
85+
/// <returns>A list of nested property information</returns>
86+
private static readonly MethodInfo? CreateTypedSetterMethod = typeof(ConversionHelper)
87+
.GetMethods(BindingFlags.Public | BindingFlags.Static)
88+
.FirstOrDefault(m => m.Name == nameof(ConversionHelper.CreateTypedPropertySetter) && m.IsGenericMethodDefinition);
89+
90+
private static List<NestedPropertyInfo> ExtractPropertyList(IEnumerable properties, Type itemType)
91+
{
92+
var propertyList = new List<NestedPropertyInfo>();
93+
94+
foreach (var prop in properties)
95+
{
96+
var propType = prop.GetType();
97+
var nameProperty = propType.GetProperty("PropertyName");
98+
var columnProperty = propType.GetProperty("CellColumn");
99+
var getterProperty = propType.GetProperty("Getter");
100+
var setterProperty = propType.GetProperty("Setter");
101+
var typeProperty = propType.GetProperty("PropertyType");
102+
103+
if (nameProperty is null || columnProperty is null || getterProperty is null)
104+
continue;
105+
106+
var name = nameProperty.GetValue(prop) as string;
107+
var column = (int)columnProperty.GetValue(prop)!;
108+
var getter = getterProperty.GetValue(prop) as Func<object, object?>;
109+
var setter = setterProperty?.GetValue(prop) as Action<object, object?>;
110+
var propTypeValue = typeProperty?.GetValue(prop) as Type;
111+
112+
if (setter is null && name is not null)
113+
{
114+
var propertyInfo = itemType.GetProperty(name, BindingFlags.Public | BindingFlags.Instance);
115+
if (propertyInfo?.CanWrite == true)
116+
{
117+
setter = CreateSetterWithConversion(itemType, propertyInfo)
118+
?? CreateFallbackSetter(propertyInfo);
119+
}
120+
}
121+
122+
setter ??= (_, _) => { };
123+
124+
if (name is not null && getter is not null)
125+
{
126+
propertyList.Add(new NestedPropertyInfo
127+
{
128+
PropertyName = name,
129+
ColumnIndex = column,
130+
Getter = getter,
131+
Setter = setter,
132+
PropertyType = propTypeValue ?? typeof(object)
133+
});
134+
}
135+
}
136+
137+
return propertyList;
138+
}
139+
140+
private static Action<object, object?>? CreateSetterWithConversion(Type itemType, PropertyInfo propertyInfo)
141+
{
142+
if (CreateTypedSetterMethod is null)
143+
return null;
144+
145+
try
146+
{
147+
var generic = CreateTypedSetterMethod.MakeGenericMethod(itemType);
148+
return generic.Invoke(null, new object[] { propertyInfo }) as Action<object, object?>;
149+
}
150+
catch
151+
{
152+
return null;
153+
}
154+
}
155+
156+
private static Action<object, object?>? CreateFallbackSetter(PropertyInfo propertyInfo)
157+
{
158+
try
159+
{
160+
var memberSetter = new MemberSetter(propertyInfo);
161+
return memberSetter.Invoke;
162+
}
163+
catch
164+
{
165+
return null;
166+
}
167+
}
168+
169+
/// <summary>
170+
/// Gets a specific property by name from a type.
171+
/// </summary>
172+
/// <param name="type">The type to search</param>
173+
/// <param name="propertyName">The name of the property</param>
174+
/// <returns>PropertyInfo if found, otherwise null</returns>
175+
public static PropertyInfo? GetPropertyByName(Type type, string propertyName)
176+
{
177+
return type.GetProperty(propertyName, BindingFlags.Public | BindingFlags.Instance);
178+
}
179+
180+
private static bool IsSimpleType(Type type)
181+
{
182+
return type == typeof(string) || type.IsValueType || type.IsPrimitive;
183+
}
184+
185+
/// <summary>
186+
/// Determines if a type is a complex type that likely has nested properties.
187+
/// </summary>
188+
/// <param name="type">The type to check</param>
189+
/// <returns>True if the type is considered complex</returns>
190+
public static bool IsComplexType(Type type)
191+
{
192+
return !IsSimpleType(type) && type != typeof(object);
193+
}
106194
}

0 commit comments

Comments
 (0)