diff --git a/README-V2.md b/README-V2.md index 4ff8c7eb..d580431f 100644 --- a/README-V2.md +++ b/README-V2.md @@ -259,6 +259,30 @@ using var stream = File.OpenRead(path); var rows = importer.Query(stream); ``` +Only public properties get mapped by default, but public fields can also be mapped if decorated with `MiniExcelColumnAttribute` or any of the other MiniExcel attributes: +```csharp +public class UserAccount +{ + [MiniExcelColumn] + public Guid ID; + + public string Name { get; set; } + + [MiniExcelFormat("dd/MM/yyyy")] + public DateTime BoD; + + public int Age { get; set; } + + [MiniExcelColumnIndex(2)] + public bool VIP; + + public decimal Points { get; set; } +} + +var importer = MiniExcel.Importers.GetOpenXmlImporter(); +var rows = importer.Query(path); +``` + #### 2. Execute a query and map it to a list of dynamic objects By default no header will be used and the dynamic keys will be `.A`, `.B`, `.C`, etc..: diff --git a/src/MiniExcel.Core/Abstractions/IMiniExcelWriteAdapter.cs b/src/MiniExcel.Core/Abstractions/IMiniExcelWriteAdapter.cs index a57675ed..7c0784ef 100644 --- a/src/MiniExcel.Core/Abstractions/IMiniExcelWriteAdapter.cs +++ b/src/MiniExcel.Core/Abstractions/IMiniExcelWriteAdapter.cs @@ -3,13 +3,13 @@ public interface IMiniExcelWriteAdapter { bool TryGetKnownCount(out int count); - List? GetColumns(); - IEnumerable> GetRows(List props, CancellationToken cancellationToken = default); + List? GetColumns(); + IEnumerable> GetRows(List props, CancellationToken cancellationToken = default); } -public readonly struct CellWriteInfo(object? value, int cellIndex, MiniExcelColumnInfo prop) +public readonly struct CellWriteInfo(object? value, int cellIndex, MiniExcelColumnMapping prop) { public object? Value { get; } = value; public int CellIndex { get; } = cellIndex; - public MiniExcelColumnInfo Prop { get; } = prop; + public MiniExcelColumnMapping Prop { get; } = prop; } \ No newline at end of file diff --git a/src/MiniExcel.Core/Abstractions/IMiniExcelWriteAdapterAsync.cs b/src/MiniExcel.Core/Abstractions/IMiniExcelWriteAdapterAsync.cs index 41908a66..2e88ec26 100644 --- a/src/MiniExcel.Core/Abstractions/IMiniExcelWriteAdapterAsync.cs +++ b/src/MiniExcel.Core/Abstractions/IMiniExcelWriteAdapterAsync.cs @@ -2,6 +2,6 @@ public interface IMiniExcelWriteAdapterAsync { - Task?> GetColumnsAsync(); - IAsyncEnumerable GetRowsAsync(List props, CancellationToken cancellationToken); + Task?> GetColumnsAsync(); + IAsyncEnumerable GetRowsAsync(List props, CancellationToken cancellationToken); } \ No newline at end of file diff --git a/src/MiniExcel.Core/Attributes/MiniExcelAttributeBase.cs b/src/MiniExcel.Core/Attributes/MiniExcelAttributeBase.cs new file mode 100644 index 00000000..f656a991 --- /dev/null +++ b/src/MiniExcel.Core/Attributes/MiniExcelAttributeBase.cs @@ -0,0 +1,4 @@ +namespace MiniExcelLib.Core.Attributes; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +public abstract class MiniExcelAttributeBase : Attribute; diff --git a/src/MiniExcel.Core/Attributes/MiniExcelColumnAttribute.cs b/src/MiniExcel.Core/Attributes/MiniExcelColumnAttribute.cs index 1316e990..fe055d6b 100644 --- a/src/MiniExcel.Core/Attributes/MiniExcelColumnAttribute.cs +++ b/src/MiniExcel.Core/Attributes/MiniExcelColumnAttribute.cs @@ -1,16 +1,15 @@ namespace MiniExcelLib.Core.Attributes; -[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] -public class MiniExcelColumnAttribute : Attribute +public class MiniExcelColumnAttribute : MiniExcelAttributeBase { private int _index = -1; private string? _xName; public string? Name { get; set; } - public string[]? Aliases { get; set; } = []; - public string? Format { get; set; } - public bool Ignore { get; set; } - + public string[]? Aliases { get; set; } = []; + public string? Format { get; set; } + public bool Ignore { get; set; } + internal int FormatId { get; private set; } = -1; public double Width { get; set; } = 8.42857143; public ColumnType Type { get; set; } = ColumnType.Value; @@ -35,19 +34,19 @@ private void Init(int index, string? columnName = null) _index = index; _xName ??= columnName ?? CellReferenceConverter.GetAlphabeticalIndex(index); } - - public void SetFormatId(int formatId) => FormatId = formatId; -} -public enum ColumnType { Value, Formula } + public void SetFormatId(int formatId) => FormatId = formatId; +} public class DynamicExcelColumn : MiniExcelColumnAttribute { public string Key { get; set; } - public Func? CustomFormatter { get; set; } + public Func? CustomFormatter { get; set; } public DynamicExcelColumn(string key) { Key = key; } } + +public enum ColumnType { Value, Formula } diff --git a/src/MiniExcel.Core/Attributes/MiniExcelColumnIndexAttribute.cs b/src/MiniExcel.Core/Attributes/MiniExcelColumnIndexAttribute.cs index 7c0137ae..66fe0aa6 100644 --- a/src/MiniExcel.Core/Attributes/MiniExcelColumnIndexAttribute.cs +++ b/src/MiniExcel.Core/Attributes/MiniExcelColumnIndexAttribute.cs @@ -1,7 +1,6 @@ namespace MiniExcelLib.Core.Attributes; -[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] -public class MiniExcelColumnIndexAttribute : Attribute +public class MiniExcelColumnIndexAttribute : MiniExcelAttributeBase { public int ExcelColumnIndex { get; set; } internal string? ExcelXName { get; set; } diff --git a/src/MiniExcel.Core/Attributes/MiniExcelColumnNameAttribute.cs b/src/MiniExcel.Core/Attributes/MiniExcelColumnNameAttribute.cs index c1a3b657..a323220b 100644 --- a/src/MiniExcel.Core/Attributes/MiniExcelColumnNameAttribute.cs +++ b/src/MiniExcel.Core/Attributes/MiniExcelColumnNameAttribute.cs @@ -1,7 +1,6 @@ namespace MiniExcelLib.Core.Attributes; -[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] -public class MiniExcelColumnNameAttribute(string columnName, string[]? aliases = null) : Attribute +public class MiniExcelColumnNameAttribute(string columnName, string[]? aliases = null) : MiniExcelAttributeBase { public string ExcelColumnName { get; set; } = columnName; public string[] Aliases { get; set; } = aliases ?? []; diff --git a/src/MiniExcel.Core/Attributes/MiniExcelColumnWidthAttribute.cs b/src/MiniExcel.Core/Attributes/MiniExcelColumnWidthAttribute.cs index 38c1a9a2..00e6fe02 100644 --- a/src/MiniExcel.Core/Attributes/MiniExcelColumnWidthAttribute.cs +++ b/src/MiniExcel.Core/Attributes/MiniExcelColumnWidthAttribute.cs @@ -1,7 +1,6 @@ namespace MiniExcelLib.Core.Attributes; -[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] -public class MiniExcelColumnWidthAttribute(double columnWidth) : Attribute +public class MiniExcelColumnWidthAttribute(double columnWidth) : MiniExcelAttributeBase { public double ExcelColumnWidth { get; set; } = columnWidth; } \ No newline at end of file diff --git a/src/MiniExcel.Core/Attributes/MiniExcelFormatAttribute.cs b/src/MiniExcel.Core/Attributes/MiniExcelFormatAttribute.cs index 6df4f1ae..6bcc3f39 100644 --- a/src/MiniExcel.Core/Attributes/MiniExcelFormatAttribute.cs +++ b/src/MiniExcel.Core/Attributes/MiniExcelFormatAttribute.cs @@ -1,7 +1,6 @@ namespace MiniExcelLib.Core.Attributes; -[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] -public class MiniExcelFormatAttribute(string format) : Attribute +public class MiniExcelFormatAttribute(string format) : MiniExcelAttributeBase { public string Format { get; set; } = format; } \ No newline at end of file diff --git a/src/MiniExcel.Core/Attributes/MiniExcelIgnoreAttribute.cs b/src/MiniExcel.Core/Attributes/MiniExcelIgnoreAttribute.cs index 20aceb7e..c0d8b60c 100644 --- a/src/MiniExcel.Core/Attributes/MiniExcelIgnoreAttribute.cs +++ b/src/MiniExcel.Core/Attributes/MiniExcelIgnoreAttribute.cs @@ -1,7 +1,6 @@ namespace MiniExcelLib.Core.Attributes; -[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] -public class MiniExcelIgnoreAttribute(bool ignore = true) : Attribute +public class MiniExcelIgnoreAttribute(bool ignore = true) : MiniExcelAttributeBase { public bool Ignore { get; set; } = ignore; } \ No newline at end of file diff --git a/src/MiniExcel.Core/Exceptions/MiniExcelColumnNotFoundException.cs b/src/MiniExcel.Core/Exceptions/ColumnNotFoundException.cs similarity index 90% rename from src/MiniExcel.Core/Exceptions/MiniExcelColumnNotFoundException.cs rename to src/MiniExcel.Core/Exceptions/ColumnNotFoundException.cs index 621bfafb..36031948 100644 --- a/src/MiniExcel.Core/Exceptions/MiniExcelColumnNotFoundException.cs +++ b/src/MiniExcel.Core/Exceptions/ColumnNotFoundException.cs @@ -1,6 +1,6 @@ namespace MiniExcelLib.Core.Exceptions; -public class MiniExcelColumnNotFoundException( +public class ColumnNotFoundException( string? columnIndex, string? columnName, string[] columnAliases, diff --git a/src/MiniExcel.Core/Exceptions/InvalidMappingException.cs b/src/MiniExcel.Core/Exceptions/InvalidMappingException.cs new file mode 100644 index 00000000..4b1f73fe --- /dev/null +++ b/src/MiniExcel.Core/Exceptions/InvalidMappingException.cs @@ -0,0 +1,8 @@ +namespace MiniExcelLib.Core.Exceptions; + +public class InvalidMappingException(string message, Type? type, MemberInfo? member = null) + : InvalidOperationException(message) +{ + public Type? InvalidType { get; } = type; + public MemberInfo? InvalidProperty { get; } = member; +} \ No newline at end of file diff --git a/src/MiniExcel.Core/Exceptions/MiniExcelNotSerializableException.cs b/src/MiniExcel.Core/Exceptions/MemberNotSerializableException.cs similarity index 59% rename from src/MiniExcel.Core/Exceptions/MiniExcelNotSerializableException.cs rename to src/MiniExcel.Core/Exceptions/MemberNotSerializableException.cs index 7440bb13..81868d97 100644 --- a/src/MiniExcel.Core/Exceptions/MiniExcelNotSerializableException.cs +++ b/src/MiniExcel.Core/Exceptions/MemberNotSerializableException.cs @@ -1,6 +1,6 @@ namespace MiniExcelLib.Core.Exceptions; -public class MiniExcelNotSerializableException(string message, MemberInfo member) +public class MemberNotSerializableException(string message, MemberInfo member) : InvalidOperationException(message) { public MemberInfo Member { get; } = member; diff --git a/src/MiniExcel.Core/Exceptions/MiniExcelInvalidCastException.cs b/src/MiniExcel.Core/Exceptions/ValueNotAssignableException.cs similarity index 53% rename from src/MiniExcel.Core/Exceptions/MiniExcelInvalidCastException.cs rename to src/MiniExcel.Core/Exceptions/ValueNotAssignableException.cs index 2c883e0f..8a70834d 100644 --- a/src/MiniExcel.Core/Exceptions/MiniExcelInvalidCastException.cs +++ b/src/MiniExcel.Core/Exceptions/ValueNotAssignableException.cs @@ -1,10 +1,10 @@ namespace MiniExcelLib.Core.Exceptions; -public class MiniExcelInvalidCastException(string columnName, int row, object value, Type invalidCastType, string message) +public class ValueNotAssignableException(string columnName, int row, object value, Type columnType, string message) : InvalidCastException(message) { public string ColumnName { get; set; } = columnName; public int Row { get; set; } = row; public object Value { get; set; } = value; - public Type InvalidCastType { get; set; } = invalidCastType; + public Type ColumnType { get; set; } = columnType; } \ No newline at end of file diff --git a/src/MiniExcel.Core/Helpers/AsyncEnumerableExtensions.cs b/src/MiniExcel.Core/Helpers/AsyncEnumerableExtensions.cs index f91bede2..929cb29a 100644 --- a/src/MiniExcel.Core/Helpers/AsyncEnumerableExtensions.cs +++ b/src/MiniExcel.Core/Helpers/AsyncEnumerableExtensions.cs @@ -15,4 +15,13 @@ public static async Task> CreateListAsync(this IAsyncEnumerable en // needed by the SyncGenerator public static List CreateList(this IEnumerable enumerable) => [..enumerable]; + + public static async IAsyncEnumerable> CastToDictionary(this IAsyncEnumerable enumerable, [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + await foreach (var item in enumerable.WithCancellation(cancellationToken).ConfigureAwait(false)) + { + if (item is IDictionary dict) + yield return dict; + } + } } diff --git a/src/MiniExcel.Core/Helpers/ExpandoHelper.cs b/src/MiniExcel.Core/Helpers/ExpandoHelper.cs new file mode 100644 index 00000000..5d5572ef --- /dev/null +++ b/src/MiniExcel.Core/Helpers/ExpandoHelper.cs @@ -0,0 +1,39 @@ +using System.Dynamic; + +namespace MiniExcelLib.Core.Helpers; + +public static class ExpandoHelper +{ + public static IDictionary CreateEmptyByIndices(int maxColumnIndex, int startCellIndex) + { + IDictionary cell = new ExpandoObject(); + for (int i = startCellIndex; i <= maxColumnIndex; i++) + { + var key = CellReferenceConverter.GetAlphabeticalIndex(i); +#if NETCOREAPP2_0_OR_GREATER + cell.TryAdd(key, null); +#else + if (!cell.ContainsKey(key)) + cell.Add(key, null); +#endif + } + + return cell; + } + + public static IDictionary CreateEmptyByHeaders(Dictionary headers) + { + IDictionary cell = new ExpandoObject(); + foreach (var hr in headers) + { +#if NETCOREAPP2_0_OR_GREATER + cell.TryAdd(hr.Value, null); +#else + if (!cell.ContainsKey(hr.Value)) + cell.Add(hr.Value, null); +#endif + } + + return cell; + } +} diff --git a/src/MiniExcel.Core/Helpers/ListHelper.cs b/src/MiniExcel.Core/Helpers/ListExtensions.cs similarity index 85% rename from src/MiniExcel.Core/Helpers/ListHelper.cs rename to src/MiniExcel.Core/Helpers/ListExtensions.cs index f2f2182d..4dd803f6 100644 --- a/src/MiniExcel.Core/Helpers/ListHelper.cs +++ b/src/MiniExcel.Core/Helpers/ListExtensions.cs @@ -1,6 +1,6 @@ namespace MiniExcelLib.Core.Helpers; -internal static class ListHelper +internal static class ListExtensions { internal static bool StartsWith(this IList span, IList value) where T : IEquatable { @@ -12,4 +12,4 @@ internal static bool StartsWith(this IList span, IList value) where T : return span.Take(value.Count).SequenceEqual(value); } -} \ No newline at end of file +} diff --git a/src/MiniExcel.Core/Reflection/ColumnMappingsProvider.cs b/src/MiniExcel.Core/Reflection/ColumnMappingsProvider.cs new file mode 100644 index 00000000..acc2b0b0 --- /dev/null +++ b/src/MiniExcel.Core/Reflection/ColumnMappingsProvider.cs @@ -0,0 +1,311 @@ +using System.ComponentModel; +using MiniExcelLib.Core.Attributes; +using MiniExcelLib.Core.Exceptions; + +namespace MiniExcelLib.Core.Reflection; + +internal static class ColumnMappingsProvider +{ + private const BindingFlags ExportMembersFlags = BindingFlags.Public | BindingFlags.Instance; + private const BindingFlags ImportMembersFlags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.SetProperty; + + + internal static List GetMappingsForImport(Type type, string[] keys, MiniExcelBaseConfiguration configuration) + { + List mappings = GetColumnMappings(type, ImportMembersFlags, configuration) + .Where(col => col?.MemberAccessor.CanWrite is true && + !col.MemberAccessor.MemberInfo.GetAttributeValue((MiniExcelIgnoreAttribute x) => x.Ignore) && + !col.MemberAccessor.MemberInfo.GetAttributeValue((MiniExcelColumnAttribute x) => x.Ignore)) + .ToList()!; + + if (mappings.Count == 0) + throw new InvalidMappingException($"{type.Name} must contain at least one mappable property or field.", type); + + var firstDuplicateIndexGroup = mappings + .Where(m => m?.ExcelColumnIndex > -1) + .GroupBy(m => m?.ExcelColumnIndex) + .FirstOrDefault(g => g.Count() > 1); + + if (firstDuplicateIndexGroup?.FirstOrDefault() is { } duplicate) + throw new InvalidOperationException($"Duplicate column index in type {type.Name}: {duplicate.ExcelColumnIndex}"); + + var maxKey = keys.Last(); + var maxIndex = CellReferenceConverter.GetNumericalIndex(maxKey); + foreach (var p in mappings) + { + if (p?.ExcelColumnIndex is null) + continue; + + if (p.ExcelColumnIndex > maxIndex) + throw new InvalidMappingException($"The defined MiniExcelColumnIndex({p.ExcelColumnIndex}) exceeds the worksheets size({maxIndex})", type, p.MemberAccessor.MemberInfo); + + if (p.ExcelColumnName is null) + throw new InvalidMappingException($"The defined MiniExcelColumnIndex({p.ExcelColumnIndex}) for type {type.Name}.{p.MemberAccessor.Name} does not match the defined MiniExcelColumnName({p.ExcelColumnName})", type, p.MemberAccessor.MemberInfo); + } + + return mappings; + } + + private static List GetMappingsForExport(this Type type, MiniExcelBaseConfiguration configuration) + { + var props = GetColumnMappings(type, ExportMembersFlags, configuration) + .Where(prop => prop?.MemberAccessor.CanRead is true) + .ToList(); + + if (props.Count == 0) + throw new InvalidMappingException($"{type.Name} must contain at least one mappable property or field.", type); + + return SortMappings(props); + } + + private static List SortMappings(List mappings) + { + //TODO: need optimize performance + + var explicitIndexMappings = mappings + .Where(w => w?.ExcelColumnIndex > -1) + .ToList(); + + var firstDuplicateIndexGroup = mappings + .Where(m => m?.ExcelColumnIndex > -1) + .GroupBy(m => m?.ExcelColumnIndex) + .FirstOrDefault(g => g.Count() > 1); + + if (firstDuplicateIndexGroup?.FirstOrDefault() is { } duplicate) + { + var type = duplicate.MemberAccessor.MemberInfo.DeclaringType; + throw new InvalidMappingException($"Duplicate column index in type {type?.Name}: {duplicate.ExcelColumnIndex}", type, duplicate.MemberAccessor.MemberInfo); + } + + var maxColumnIndex = mappings.Count - 1; + if (explicitIndexMappings.Count != 0) + maxColumnIndex = Math.Max(explicitIndexMappings.Max(w => w?.ExcelColumnIndex ?? 0), maxColumnIndex); + + var withoutCustomIndexProps = mappings + .Where(w => w?.ExcelColumnIndex is null or -1) + .ToList(); + + var index = 0; + var newProps = new List(); + for (int i = 0; i <= maxColumnIndex; i++) + { + if (explicitIndexMappings.SingleOrDefault(s => s?.ExcelColumnIndex == i) is { } p1) + { + newProps.Add(p1); + } + else + { + var p2 = withoutCustomIndexProps.ElementAtOrDefault(index); + + p2?.ExcelColumnIndex = i; + newProps.Add(p2); + + index++; + } + } + return newProps; + } + + private static IEnumerable GetColumnMappings(Type type, BindingFlags bindingFlags, MiniExcelBaseConfiguration configuration) + { + var properties = type.GetProperties(bindingFlags); + var fields = type.GetFields(bindingFlags).Where(x => x.GetCustomAttributes().Any()); + var members = properties.Cast().Concat(fields); + + var columnInfos = members.Select(m => + { + var excelColumn = m.GetAttribute(); + if (configuration.DynamicColumns?.SingleOrDefault(dc => dc.Key == m.Name) is { } dynamicColumn) + excelColumn = dynamicColumn; + + var excelColumnName = m.GetAttribute(); + var excelFormat = m.GetAttribute()?.Format; + + var ignoreMember = + m.GetAttributeValue((MiniExcelIgnoreAttribute x) => x.Ignore) || + m.GetAttributeValue((MiniExcelColumnAttribute x) => x.Ignore) || + excelColumn?.Ignore is true; + + if (ignoreMember) + return null; + + int? excelColumnIndex = excelColumn?.Index > -1 ? excelColumn.Index : null; + var accessor = new MiniExcelMemberAccessor(m); + + //TODO: or dynamic configuration + return new MiniExcelColumnMapping + { + MemberAccessor = new MiniExcelMemberAccessor(m), + ExcludeNullableType = accessor.Type, + Nullable = accessor.IsNullable, + ExcelColumnAliases = excelColumnName?.Aliases ?? excelColumn?.Aliases ?? [], + ExcelColumnName = excelColumnName?.ExcelColumnName ?? m.GetAttribute()?.DisplayName ?? excelColumn?.Name ?? m.Name, + ExcelColumnIndex = m.GetAttribute()?.ExcelColumnIndex ?? excelColumnIndex, + ExcelIndexName = m.GetAttribute()?.ExcelXName ?? excelColumn?.IndexName, + ExcelColumnWidth = m.GetAttribute()?.ExcelColumnWidth ?? excelColumn?.Width, + ExcelFormat = excelFormat ?? excelColumn?.Format, + ExcelFormatId = excelColumn?.FormatId ?? -1, + ExcelColumnType = excelColumn?.Type ?? ColumnType.Value, + CustomFormatter = (excelColumn as DynamicExcelColumn)?.CustomFormatter + }; + }); + + return columnInfos.Where(x => x is not null); + } + + private static List GetDictionaryColumnInfo(IDictionary? dicString, IDictionary? dic, MiniExcelBaseConfiguration configuration) + { + var props = new List(); + + var keys = dicString?.Keys.ToList() + ?? dic?.Keys + ?? throw new InvalidOperationException(); + + foreach (var key in keys) + { + SetDictionaryColumnInfo(props, key, configuration); + } + + return SortMappings(props); + } + + private static void SetDictionaryColumnInfo(List props, object key, MiniExcelBaseConfiguration configuration) + { + var mapping = new MiniExcelColumnMapping + { + Key = key, + ExcelColumnName = key?.ToString() + }; + + // TODO:Dictionary value type is not fixed + var isIgnore = false; + if (configuration.DynamicColumns is { Length: > 0 }) + { + var dynamicColumn = configuration.DynamicColumns.SingleOrDefault(x => x.Key == key?.ToString()); + if (dynamicColumn is not null) + { + mapping.Nullable = true; + + if (dynamicColumn is { Format: { } fmt, FormatId: var fmtId }) + { + mapping.ExcelFormat = fmt; + mapping.ExcelFormatId = fmtId; + } + + if (dynamicColumn.Aliases is { } aliases) + mapping.ExcelColumnAliases = aliases; + + if (dynamicColumn.IndexName is { } idxName) + mapping.ExcelIndexName = idxName; + + if (dynamicColumn.Name is { } colName) + mapping.ExcelColumnName = colName; + + + mapping.ExcelColumnIndex = dynamicColumn.Index; + mapping.ExcelColumnWidth = dynamicColumn.Width; + mapping.ExcelColumnType = dynamicColumn.Type; + mapping.CustomFormatter = dynamicColumn.CustomFormatter; + + isIgnore = dynamicColumn.Ignore; + } + } + + if (!isIgnore) + props.Add(mapping); + } + + internal static bool TryGetColumnMappings(Type? type, MiniExcelBaseConfiguration configuration, out List props) + { + props = []; + + // Unknown type + if (type is null) + return false; + + if (type.IsValueType || type == typeof(string)) + throw new NotSupportedException($"MiniExcel does not support the use of {type.FullName} as a generic type"); + + if (ValueIsNeededToDetermineProperties(type)) + return false; + + props = type.GetMappingsForExport(configuration); + return true; + } + + internal static List GetColumnMappingFromValue(object value, MiniExcelBaseConfiguration configuration) => value switch + { + IDictionary genericDictionary => GetDictionaryColumnInfo(genericDictionary, null, configuration), + IDictionary dictionary => GetDictionaryColumnInfo(null, dictionary, configuration), + _ => value.GetType().GetMappingsForExport(configuration) + }; + + private static bool ValueIsNeededToDetermineProperties(Type type) => + typeof(object) == type || + typeof(IDictionary).IsAssignableFrom(type) || + typeof(IDictionary).IsAssignableFrom(type); + + internal static MiniExcelColumnMapping GetColumnMappingFromDynamicConfiguration(string columnName, MiniExcelBaseConfiguration configuration) + { + var member = new MiniExcelColumnMapping + { + ExcelColumnName = columnName, + Key = columnName + }; + + if (configuration.DynamicColumns is null or []) + return member; + + var dynamicColumn = configuration.DynamicColumns + .SingleOrDefault(col => string.Equals(col.Key, columnName, StringComparison.OrdinalIgnoreCase)); + + if (dynamicColumn is null) + return member; + + member.Nullable = true; + member.ExcelIgnore = dynamicColumn.Ignore; + member.ExcelColumnType = dynamicColumn.Type; + member.ExcelColumnWidth = dynamicColumn.Width; + member.CustomFormatter = dynamicColumn.CustomFormatter; + + if (dynamicColumn is { Format: { } fmt, FormatId: var fmtId }) + { + member.ExcelFormat = fmt; + member.ExcelFormatId = fmtId; + } + + if (dynamicColumn.Index > -1) + member.ExcelColumnIndex = dynamicColumn.Index; + + if (dynamicColumn.Aliases is { } aliases) + member.ExcelColumnAliases = aliases; + + if (dynamicColumn.IndexName is { } idxName) + member.ExcelIndexName = idxName; + + if (dynamicColumn.Name is { } colName) + member.ExcelColumnName = colName; + + return member; + } + + internal static Dictionary GetHeaders(IDictionary item, bool trimNames = false) + { + return DictToNameWithIndex(item) + .GroupBy(x => x.Name) + .SelectMany(GroupToNameWithIndex) + .ToDictionary(kv => trimNames ? kv.Name.Trim() : kv.Name, kv => kv.Index); + + static IEnumerable DictToNameWithIndex(IDictionary dict) + => dict.Values.Select((obj, idx) => new NameIndexPair(idx, obj?.ToString() ?? "")); + + static IEnumerable GroupToNameWithIndex(IGrouping group) + => group.Select((grp, idx) => new NameIndexPair(grp.Index, idx == 0 ? grp.Name : $"{grp.Name}_____{idx + 1}")); + } + + private class NameIndexPair(int index, string name) + { + public int Index { get; } = index; + public string Name { get; } = name; + } +} diff --git a/src/MiniExcel.Core/Reflection/CustomPropertyHelper.cs b/src/MiniExcel.Core/Reflection/CustomPropertyHelper.cs deleted file mode 100644 index 416c0065..00000000 --- a/src/MiniExcel.Core/Reflection/CustomPropertyHelper.cs +++ /dev/null @@ -1,344 +0,0 @@ -using System.ComponentModel; -using MiniExcelLib.Core.Attributes; - -namespace MiniExcelLib.Core.Reflection; - -public static class CustomPropertyHelper -{ - public static IDictionary GetEmptyExpandoObject(int maxColumnIndex, int startCellIndex) - { - var cell = new Dictionary(); - for (int i = startCellIndex; i <= maxColumnIndex; i++) - { - var key = CellReferenceConverter.GetAlphabeticalIndex(i); -#if NETCOREAPP2_0_OR_GREATER - cell.TryAdd(key, null); -#else - if (!cell.ContainsKey(key)) - cell.Add(key, null); -#endif - } - return cell; - } - - public static IDictionary GetEmptyExpandoObject(Dictionary hearrows) - { - var cell = new Dictionary(); - foreach (var hr in hearrows) - { -#if NETCOREAPP2_0_OR_GREATER - cell.TryAdd(hr.Value, null); -#else - if (!cell.ContainsKey(hr.Value)) - cell.Add(hr.Value, null); -#endif - } - - return cell; - } - - private static List GetSaveAsProperties(this Type type, MiniExcelBaseConfiguration configuration) - { - var props = GetExcelPropertyInfo(type, BindingFlags.Public | BindingFlags.Instance, configuration) - .Where(prop => prop.Property.CanRead) - .ToList() /*ignore without set*/; - - if (props.Count == 0) - throw new InvalidOperationException($"{type.Name} un-ignore properties count can't be 0"); - - return SortCustomProps(props); - } - - private static List SortCustomProps(List props) - { - // https://github.com/mini-software/MiniExcel/issues/142 - //TODO: need optimize performance - - var withCustomIndexProps = props.Where(w => w.ExcelColumnIndex is > -1).ToList(); - if (withCustomIndexProps.GroupBy(g => g.ExcelColumnIndex).Any(x => x.Count() > 1)) - throw new InvalidOperationException("Duplicate column name"); - - var maxColumnIndex = props.Count - 1; - if (withCustomIndexProps.Count != 0) - maxColumnIndex = Math.Max((int)withCustomIndexProps.Max(w => w.ExcelColumnIndex), maxColumnIndex); - - var withoutCustomIndexProps = props.Where(w => w.ExcelColumnIndex is null or -1).ToList(); - - var index = 0; - var newProps = new List(); - for (int i = 0; i <= maxColumnIndex; i++) - { - var p1 = withCustomIndexProps.SingleOrDefault(s => s.ExcelColumnIndex == i); - if (p1 is not null) - { - newProps.Add(p1); - } - else - { - var p2 = withoutCustomIndexProps.ElementAtOrDefault(index); - if (p2 is null) - { - newProps.Add(null); - } - else - { - p2.ExcelColumnIndex = i; - newProps.Add(p2); - } - index++; - } - } - return newProps; - } - - internal static List GetExcelCustomPropertyInfos(Type type, string[] keys, MiniExcelBaseConfiguration configuration) - { - const BindingFlags flags = BindingFlags.SetProperty | BindingFlags.Public | BindingFlags.Instance; - var props = GetExcelPropertyInfo(type, flags, configuration) - .Where(prop => prop?.Property.Info.GetSetMethod() is not null // why not .Property.CanWrite? because it will use private setter - && !prop.Property.Info.GetAttributeValue((MiniExcelIgnoreAttribute x) => x.Ignore) - && !prop.Property.Info.GetAttributeValue((MiniExcelColumnAttribute x) => x.Ignore)) - .ToList() /*ignore without set*/; - - if (props.Count == 0) - throw new InvalidOperationException($"{type.Name} un-ignore properties count can't be 0"); - - var withCustomIndexProps = props.Where(w => w?.ExcelColumnIndex is > -1); - if (withCustomIndexProps.GroupBy(g => g?.ExcelColumnIndex).Any(x => x.Count() > 1)) - throw new InvalidOperationException("Duplicate column name"); - - var maxkey = keys.Last(); - var maxIndex = CellReferenceConverter.GetNumericalIndex(maxkey); - foreach (var p in props) - { - if (p?.ExcelColumnIndex is null) - continue; - if (p.ExcelColumnIndex > maxIndex) - throw new ArgumentException($"ExcelColumnIndex {p.ExcelColumnIndex} over haeder max index {maxkey}"); - if (p.ExcelColumnName is null) - throw new InvalidOperationException($"{p.Property.Info.DeclaringType?.Name} {p.Property.Name}'s ExcelColumnIndex {p.ExcelColumnIndex} can't find excel column name"); - } - - return props; - } - - public static string? GetDescriptionAttribute(Type type, object? source) - { - var name = source?.ToString(); - return type.GetField(name) //For some database dirty data, there may be no way to change to the correct enumeration, will return NULL - ?.GetCustomAttribute(false)?.Description - ?? name; - } - - private static IEnumerable GetExcelPropertyInfo(Type type, BindingFlags bindingFlags, MiniExcelBaseConfiguration configuration) - { - var props = type.GetProperties(bindingFlags); - var columnInfos = props.Select(p => - { - var gt = Nullable.GetUnderlyingType(p.PropertyType); - var excelColumnName = p.GetAttribute(); - var excludeNullableType = gt ?? p.PropertyType; - var excelFormat = p.GetAttribute()?.Format; - var excelColumn = p.GetAttribute(); - var dynamicColumn = configuration?.DynamicColumns?.SingleOrDefault(dc => dc.Key == p.Name); - if (dynamicColumn is not null) - excelColumn = dynamicColumn; - - var ignore = p.GetAttributeValue((MiniExcelIgnoreAttribute x) => x.Ignore) || - p.GetAttributeValue((MiniExcelColumnAttribute x) => x.Ignore) || - (excelColumn?.Ignore ?? false); - if (ignore) - return null; - - //TODO:or configulation Dynamic - int? excelColumnIndex = excelColumn?.Index > -1 ? excelColumn.Index : null; - return new MiniExcelColumnInfo - { - Property = new MiniExcelProperty(p), - ExcludeNullableType = excludeNullableType, - Nullable = gt is not null, - ExcelColumnAliases = excelColumnName?.Aliases ?? excelColumn?.Aliases ?? [], - ExcelColumnName = excelColumnName?.ExcelColumnName ?? p.GetAttribute()?.DisplayName ?? excelColumn?.Name ?? p.Name, - ExcelColumnIndex = p.GetAttribute()?.ExcelColumnIndex ?? excelColumnIndex, - ExcelIndexName = p.GetAttribute()?.ExcelXName ?? excelColumn?.IndexName, - ExcelColumnWidth = p.GetAttribute()?.ExcelColumnWidth ?? excelColumn?.Width, - ExcelFormat = excelFormat ?? excelColumn?.Format, - ExcelFormatId = excelColumn?.FormatId ?? -1, - ExcelColumnType = excelColumn?.Type ?? ColumnType.Value, - CustomFormatter = dynamicColumn?.CustomFormatter - }; - }); - - return columnInfos.Where(x => x is not null); - } - - private static List GetDictionaryColumnInfo(IDictionary? dicString, IDictionary? dic, MiniExcelBaseConfiguration configuration) - { - var props = new List(); - - var keys = dicString?.Keys.ToList() - ?? dic?.Keys - ?? throw new NotSupportedException(); - - foreach (var key in keys) - { - SetDictionaryColumnInfo(props, key, configuration); - } - - return SortCustomProps(props); - } - - private static void SetDictionaryColumnInfo(List props, object key, MiniExcelBaseConfiguration configuration) - { - var p = new MiniExcelColumnInfo - { - Key = key, - ExcelColumnName = key?.ToString() - }; - - // TODO:Dictionary value type is not fixed - var isIgnore = false; - if (configuration.DynamicColumns is { Length: > 0 }) - { - var dynamicColumn = configuration.DynamicColumns.SingleOrDefault(x => x.Key == key?.ToString()); - if (dynamicColumn is not null) - { - p.Nullable = true; - - if (dynamicColumn.Format is not null) - { - p.ExcelFormat = dynamicColumn.Format; - p.ExcelFormatId = dynamicColumn.FormatId; - } - - if (dynamicColumn.Aliases is not null) - p.ExcelColumnAliases = dynamicColumn.Aliases; - - if (dynamicColumn.IndexName is not null) - p.ExcelIndexName = dynamicColumn.IndexName; - - if (dynamicColumn.Name is not null) - p.ExcelColumnName = dynamicColumn.Name; - - p.ExcelColumnIndex = dynamicColumn.Index; - p.ExcelColumnWidth = dynamicColumn.Width; - p.ExcelColumnType = dynamicColumn.Type; - p.CustomFormatter = dynamicColumn.CustomFormatter; - - isIgnore = dynamicColumn.Ignore; - } - } - - if (!isIgnore) - props.Add(p); - } - - internal static bool TryGetTypeColumnInfo(Type? type, MiniExcelBaseConfiguration configuration, out List? props) - { - // Unknown type - if (type is null) - { - props = null; - return false; - } - - if (type.IsValueType || type == typeof(string)) - throw new NotSupportedException($"MiniExcel does not support the use of {type.FullName} as a generic type"); - - if (ValueIsNeededToDetermineProperties(type)) - { - props = null; - return false; - } - - props = type.GetSaveAsProperties(configuration); - return true; - } - - internal static List GetColumnInfoFromValue(object value, MiniExcelBaseConfiguration configuration) => value switch - { - IDictionary genericDictionary => GetDictionaryColumnInfo(genericDictionary, null, configuration), - IDictionary dictionary => GetDictionaryColumnInfo(null, dictionary, configuration), - _ => value.GetType().GetSaveAsProperties(configuration) - }; - - private static bool ValueIsNeededToDetermineProperties(Type type) => - typeof(object) == type || - typeof(IDictionary).IsAssignableFrom(type) || - typeof(IDictionary).IsAssignableFrom(type); - - internal static MiniExcelColumnInfo GetColumnInfosFromDynamicConfiguration(string columnName, MiniExcelBaseConfiguration configuration) - { - var prop = new MiniExcelColumnInfo - { - ExcelColumnName = columnName, - Key = columnName - }; - - if (configuration.DynamicColumns is null or []) - return prop; - - var dynamicColumn = configuration.DynamicColumns - .SingleOrDefault(col => string.Equals(col.Key, columnName, StringComparison.OrdinalIgnoreCase)); - - if (dynamicColumn is null) - return prop; - - prop.Nullable = true; - prop.ExcelIgnore = dynamicColumn.Ignore; - prop.ExcelColumnType = dynamicColumn.Type; - prop.ExcelColumnWidth = dynamicColumn.Width; - prop.CustomFormatter = dynamicColumn.CustomFormatter; - - if (dynamicColumn.Index > -1) - { - prop.ExcelColumnIndex = dynamicColumn.Index; - } - - if (dynamicColumn.Format is not null) - { - prop.ExcelFormat = dynamicColumn.Format; - prop.ExcelFormatId = dynamicColumn.FormatId; - } - - if (dynamicColumn.Aliases is not null) - { - prop.ExcelColumnAliases = dynamicColumn.Aliases; - } - - if (dynamicColumn.IndexName is not null) - { - prop.ExcelIndexName = dynamicColumn.IndexName; - } - - if (dynamicColumn.Name is not null) - { - prop.ExcelColumnName = dynamicColumn.Name; - } - - return prop; - } - - internal static Dictionary GetHeaders(IDictionary item, bool trimNames = false) - { - return DictToNameWithIndex(item) - .GroupBy(x => x.Name) - .SelectMany(GroupToNameWithIndex) - .ToDictionary(kv => trimNames ? kv.Name.Trim() : kv.Name, kv => kv.Index); - - static IEnumerable DictToNameWithIndex(IDictionary dict) - => dict.Values.Select((obj, idx) => - new NameIndexPair(idx, obj?.ToString() ?? "")); - - static IEnumerable GroupToNameWithIndex(IGrouping group) - => group.Select((grp, idx) => - new NameIndexPair(grp.Index, idx == 0 ? grp.Name : $"{grp.Name}_____{idx + 1}") - ); - } - - private class NameIndexPair(int index, string name) - { - public int Index { get; } = index; - public string Name { get; } = name; - } -} \ No newline at end of file diff --git a/src/MiniExcel.Core/Reflection/MemberGetter.cs b/src/MiniExcel.Core/Reflection/MemberGetter.cs index 233fde62..74cfa640 100644 --- a/src/MiniExcel.Core/Reflection/MemberGetter.cs +++ b/src/MiniExcel.Core/Reflection/MemberGetter.cs @@ -2,22 +2,21 @@ namespace MiniExcelLib.Core.Reflection; -public class MemberGetter(PropertyInfo property) +public class MemberGetter(MemberInfo member) { - private readonly Func _mGetFunc = CreateGetterDelegate(property); + private readonly Func _mGetFunc = CreateGetterDelegate(member); public object? Invoke(object instance) - { - return _mGetFunc.Invoke(instance); - } + => _mGetFunc.Invoke(instance); - private static Func CreateGetterDelegate(PropertyInfo property) + private static Func CreateGetterDelegate(MemberInfo member) { var paramInstance = Expression.Parameter(typeof(object)); - var bodyInstance = Expression.Convert(paramInstance, property.DeclaringType!); - var bodyProperty = Expression.Property(bodyInstance, property); + var bodyInstance = Expression.Convert(paramInstance, member.DeclaringType!); + + var bodyProperty = Expression.MakeMemberAccess(bodyInstance, member); var bodyReturn = Expression.Convert(bodyProperty, typeof(object)); return Expression.Lambda>(bodyReturn, paramInstance).Compile(); } -} \ No newline at end of file +} diff --git a/src/MiniExcel.Core/Reflection/MemberSetter.cs b/src/MiniExcel.Core/Reflection/MemberSetter.cs index 1e81c84c..af763293 100644 --- a/src/MiniExcel.Core/Reflection/MemberSetter.cs +++ b/src/MiniExcel.Core/Reflection/MemberSetter.cs @@ -6,7 +6,7 @@ public class MemberSetter { private readonly Action _setFunc; - public MemberSetter(PropertyInfo property) + public MemberSetter(MemberInfo property) { if (property is null) throw new ArgumentNullException(nameof(property)); @@ -19,15 +19,17 @@ public void Invoke(object instance, object? value) _setFunc.Invoke(instance, value); } - private static Action CreateSetterDelegate(PropertyInfo property) + private static Action CreateSetterDelegate(MemberInfo member) { var paramInstance = Expression.Parameter(typeof(object)); var paramValue = Expression.Parameter(typeof(object)); - var bodyInstance = Expression.Convert(paramInstance, property.DeclaringType!); - var bodyValue = Expression.Convert(paramValue, property.PropertyType); - var bodyCall = Expression.Call(bodyInstance, property.GetSetMethod(true)!, bodyValue); + var bodyInstance = Expression.Convert(paramInstance, member.DeclaringType!); + + var memberAccess = Expression.MakeMemberAccess(bodyInstance, member); + var bodyValue = Expression.Convert(paramValue, memberAccess.Type); + var assignExp = Expression.Assign(memberAccess, bodyValue); - return Expression.Lambda>(bodyCall, paramInstance, paramValue).Compile(); + return Expression.Lambda>(assignExp, paramInstance, paramValue).Compile(); } -} \ No newline at end of file +} diff --git a/src/MiniExcel.Core/Reflection/MiniExcelColumnInfo.cs b/src/MiniExcel.Core/Reflection/MiniExcelColumnMapping.cs similarity index 86% rename from src/MiniExcel.Core/Reflection/MiniExcelColumnInfo.cs rename to src/MiniExcel.Core/Reflection/MiniExcelColumnMapping.cs index 6b4a2d93..c406bad9 100644 --- a/src/MiniExcel.Core/Reflection/MiniExcelColumnInfo.cs +++ b/src/MiniExcel.Core/Reflection/MiniExcelColumnMapping.cs @@ -2,15 +2,15 @@ namespace MiniExcelLib.Core.Reflection; -public class MiniExcelColumnInfo +public class MiniExcelColumnMapping { public object Key { get; set; } + public MiniExcelMemberAccessor MemberAccessor { get; set; } + public Type ExcludeNullableType { get; set; } + public bool Nullable { get; internal set; } public int? ExcelColumnIndex { get; set; } public string? ExcelColumnName { get; set; } public string[]? ExcelColumnAliases { get; set; } = []; - public MiniExcelProperty Property { get; set; } - public Type ExcludeNullableType { get; set; } - public bool Nullable { get; internal set; } public string? ExcelFormat { get; internal set; } public double? ExcelColumnWidth { get; internal set; } public string? ExcelIndexName { get; internal set; } diff --git a/src/MiniExcel.Core/Reflection/MiniExcelMapper.cs b/src/MiniExcel.Core/Reflection/MiniExcelMapper.cs index f7a028cf..257509f5 100644 --- a/src/MiniExcel.Core/Reflection/MiniExcelMapper.cs +++ b/src/MiniExcel.Core/Reflection/MiniExcelMapper.cs @@ -14,7 +14,7 @@ public static partial class MiniExcelMapper var type = typeof(T); //TODO:need to optimize - List props = []; + List mappings = []; Dictionary headersDic = []; string[] keys = []; var first = true; @@ -25,10 +25,8 @@ public static partial class MiniExcelMapper if (first) { keys = item.Keys.ToArray(); - headersDic = CustomPropertyHelper.GetHeaders(item, trimColumnNames); - - //TODO: alert don't duplicate column name - props = CustomPropertyHelper.GetExcelCustomPropertyInfos(type, keys, configuration); + headersDic = ColumnMappingsProvider.GetHeaders(item, trimColumnNames); + mappings = ColumnMappingsProvider.GetMappingsForImport(type, keys, configuration); first = false; // if we treat the header as data we move forwards with the mapping otherwise we jump to the next iteration @@ -37,20 +35,20 @@ public static partial class MiniExcelMapper } var v = new T(); - foreach (var pInfo in props) + foreach (var map in mappings) { - if (pInfo.ExcelColumnAliases is not null) + if (map.ExcelColumnAliases is not null) { - foreach (var alias in pInfo.ExcelColumnAliases) + foreach (var alias in map.ExcelColumnAliases) { - if (headersDic?.TryGetValue(alias, out var columnId) ?? false) + if (headersDic?.TryGetValue(alias, out var columnId) is true) { var columnName = keys[columnId]; item.TryGetValue(columnName, out var aliasItemValue); if (aliasItemValue is not null) { - var newAliasValue = MapValue(v, pInfo, aliasItemValue, rowIndex + rowOffset, configuration, stringDecoderFunc); + var newAliasValue = MapValue(v, map, aliasItemValue, rowIndex + rowOffset, configuration, stringDecoderFunc); } } } @@ -58,11 +56,11 @@ public static partial class MiniExcelMapper //Q: Why need to check every time? A: it needs to check everytime, because it's dictionary object? itemValue = null; - if (pInfo.ExcelIndexName is not null && (keys?.Contains(pInfo.ExcelIndexName) ?? false)) + if (map.ExcelIndexName is not null && (keys?.Contains(map.ExcelIndexName) is true)) { - item.TryGetValue(pInfo.ExcelIndexName, out itemValue); + item.TryGetValue(map.ExcelIndexName, out itemValue); } - else if (pInfo.ExcelColumnName is not null && (headersDic?.TryGetValue(pInfo.ExcelColumnName, out var columnId) ?? false)) + else if (map.ExcelColumnName is not null && (headersDic?.TryGetValue(map.ExcelColumnName, out var columnId) is true)) { var columnName = keys[columnId]; item.TryGetValue(columnName, out itemValue); @@ -70,7 +68,7 @@ public static partial class MiniExcelMapper if (itemValue is not null) { - var newValue = MapValue(v, pInfo, itemValue, rowIndex + rowOffset, configuration, stringDecoderFunc); + var newValue = MapValue(v, map, itemValue, rowIndex + rowOffset, configuration, stringDecoderFunc); } } @@ -79,28 +77,33 @@ public static partial class MiniExcelMapper } } - public static object? MapValue(T v, MiniExcelColumnInfo pInfo, object itemValue, int rowIndex, MiniExcelBaseConfiguration config, Func? stringDecoderFunc = null) where T : class, new() + public static object? MapValue(T v, MiniExcelColumnMapping map, object itemValue, int rowIndex, MiniExcelBaseConfiguration config, Func? stringDecoderFunc = null) where T : class, new() { try { object? newValue = null; - if (pInfo.Nullable && string.IsNullOrWhiteSpace(itemValue?.ToString())) + if (map.Nullable && string.IsNullOrWhiteSpace(itemValue?.ToString())) { // value is null, no transformation required } - else if (pInfo.ExcludeNullableType == typeof(Guid)) + else if (map.ExcludeNullableType == typeof(Guid)) { - newValue = Guid.Parse(itemValue?.ToString() ?? Guid.Empty.ToString()); + newValue = itemValue switch + { + Guid g => g, + string str => Guid.Parse(str), + _ => Guid.Empty.ToString() + }; } - else if (pInfo.ExcludeNullableType == typeof(DateTimeOffset)) + else if (map.ExcludeNullableType == typeof(DateTimeOffset)) { var vs = itemValue?.ToString(); - if (pInfo.ExcelFormat is not null) + if (map.ExcelFormat is not null) { - if (DateTimeOffset.TryParseExact(vs, pInfo.ExcelFormat, CultureInfo.InvariantCulture, DateTimeStyles.None, out var value)) + if (DateTimeOffset.TryParseExact(vs, map.ExcelFormat, CultureInfo.InvariantCulture, DateTimeStyles.None, out var value)) { newValue = value; } @@ -115,24 +118,20 @@ public static partial class MiniExcelMapper } } - else if (pInfo.ExcludeNullableType == typeof(DateTime)) + else if (map.ExcludeNullableType == typeof(DateTime)) { // fix issue 257 https://github.com/mini-software/MiniExcel/issues/257 if (itemValue is DateTime) { newValue = itemValue; - pInfo.Property.SetValue(v, newValue); + map.MemberAccessor.SetValue(v, newValue); return newValue; } var vs = itemValue?.ToString(); - if (pInfo.ExcelFormat is not null) + if (map.ExcelFormat is not null) { - if (pInfo.Property.Info.PropertyType == typeof(DateTimeOffset) && DateTimeOffset.TryParseExact(vs, pInfo.ExcelFormat, CultureInfo.InvariantCulture, DateTimeStyles.None, out var offsetValue)) - { - newValue = offsetValue; - } - else if (DateTime.TryParseExact(vs, pInfo.ExcelFormat, CultureInfo.InvariantCulture, DateTimeStyles.None, out var value)) + if (DateTime.TryParseExact(vs, map.ExcelFormat, CultureInfo.InvariantCulture, DateTimeStyles.None, out var value)) { newValue = value; } @@ -148,12 +147,12 @@ public static partial class MiniExcelMapper } #if NET6_0_OR_GREATER - else if (pInfo.ExcludeNullableType == typeof(DateOnly)) + else if (map.ExcludeNullableType == typeof(DateOnly)) { if (itemValue is DateOnly) { newValue = itemValue; - pInfo.Property.SetValue(v, newValue); + map.MemberAccessor.SetValue(v, newValue); return newValue; } @@ -163,14 +162,14 @@ public static partial class MiniExcelMapper throw new InvalidCastException($"Could not convert cell of type DateTime to DateOnly, because DateTime was not at midnight, but at {dateTimeValue:HH:mm:ss}."); newValue = DateOnly.FromDateTime(dateTimeValue); - pInfo.Property.SetValue(v, newValue); + map.MemberAccessor.SetValue(v, newValue); return newValue; } var vs = itemValue?.ToString(); - if (pInfo.ExcelFormat is not null) + if (map.ExcelFormat is not null) { - if (DateOnly.TryParseExact(vs, pInfo.ExcelFormat, CultureInfo.InvariantCulture, DateTimeStyles.None, out var dateOnlyCustom)) + if (DateOnly.TryParseExact(vs, map.ExcelFormat, CultureInfo.InvariantCulture, DateTimeStyles.None, out var dateOnlyCustom)) { newValue = dateOnlyCustom; } @@ -186,19 +185,19 @@ public static partial class MiniExcelMapper } #endif - else if (pInfo.ExcludeNullableType == typeof(TimeSpan)) + else if (map.ExcludeNullableType == typeof(TimeSpan)) { if (itemValue is TimeSpan) { newValue = itemValue; - pInfo.Property.SetValue(v, newValue); + map.MemberAccessor.SetValue(v, newValue); return newValue; } var vs = itemValue?.ToString(); - if (pInfo.ExcelFormat is not null) + if (map.ExcelFormat is not null) { - if (TimeSpan.TryParseExact(vs, pInfo.ExcelFormat, CultureInfo.InvariantCulture, out var value)) + if (TimeSpan.TryParseExact(vs, map.ExcelFormat, CultureInfo.InvariantCulture, out var value)) { newValue = value; } @@ -213,7 +212,7 @@ public static partial class MiniExcelMapper throw new InvalidCastException($"{vs} cannot be cast to TimeSpan"); } - else if (pInfo.ExcludeNullableType == typeof(double)) + else if (map.ExcludeNullableType == typeof(double)) { if (double.TryParse(Convert.ToString(itemValue, config.Culture), NumberStyles.Any, config.Culture, out var doubleValue)) { @@ -228,7 +227,7 @@ public static partial class MiniExcelMapper } } - else if (pInfo.ExcludeNullableType == typeof(bool)) + else if (map.ExcludeNullableType == typeof(bool)) { var vs = itemValue?.ToString(); newValue = vs switch @@ -239,20 +238,20 @@ public static partial class MiniExcelMapper }; } - else if (pInfo.Property.Info.PropertyType == typeof(string)) + else if (map.ExcludeNullableType == typeof(string)) { var strValue = itemValue?.ToString(); newValue = stringDecoderFunc?.Invoke(strValue) ?? strValue; } - else if (pInfo.ExcludeNullableType.IsEnum) + else if (map.ExcludeNullableType.IsEnum) { - var fieldInfo = pInfo.ExcludeNullableType.GetFields().FirstOrDefault(e => e.GetCustomAttribute(false)?.Description == itemValue?.ToString()); + var fieldInfo = map.ExcludeNullableType.GetFields().FirstOrDefault(e => e.GetCustomAttribute(false)?.Description == itemValue?.ToString()); var value = fieldInfo?.Name ?? itemValue?.ToString() ?? ""; - newValue = Enum.Parse(pInfo.ExcludeNullableType, value, true); + newValue = Enum.Parse(map.ExcludeNullableType, value, true); } - else if (pInfo.ExcludeNullableType == typeof(Uri)) + else if (map.ExcludeNullableType == typeof(Uri)) { var rawValue = itemValue?.ToString(); if (!Uri.TryCreate(rawValue, UriKind.RelativeOrAbsolute, out var uri)) @@ -262,20 +261,25 @@ public static partial class MiniExcelMapper else { - // Use pInfo.ExcludeNullableType to resolve : https://github.com/mini-software/MiniExcel/issues/138 - newValue = Convert.ChangeType(itemValue, pInfo.ExcludeNullableType, config.Culture); + // Use map.ExcludeNullableType to resolve : https://github.com/mini-software/MiniExcel/issues/138 + newValue = Convert.ChangeType(itemValue, map.ExcludeNullableType, config.Culture); } - pInfo.Property.SetValue(v, newValue); + map.MemberAccessor.SetValue(v, newValue); return newValue; } catch (Exception ex) when (ex is InvalidCastException or FormatException) { - var columnName = pInfo.ExcelColumnName ?? pInfo.Property.Name; + var columnName = map.ExcelColumnName ?? map.MemberAccessor.Name; var errorRow = rowIndex + 1; - var msg = $"ColumnName: {columnName}, CellRow: {errorRow}, Value: {itemValue}. The value cannot be cast to type {pInfo.Property.Info.PropertyType.Name}."; - throw new MiniExcelInvalidCastException(columnName, errorRow, itemValue, pInfo.Property.Info.PropertyType, msg); + throw new ValueNotAssignableException( + columnName: columnName, + row: errorRow, + value: itemValue, + columnType: map.ExcludeNullableType, + message: $"The value {itemValue} cannot be assigned to type {map.ExcludeNullableType.Name}." + ); } } -} \ No newline at end of file +} diff --git a/src/MiniExcel.Core/Reflection/MiniExcelMemberAccessor.cs b/src/MiniExcel.Core/Reflection/MiniExcelMemberAccessor.cs new file mode 100644 index 00000000..2dd95890 --- /dev/null +++ b/src/MiniExcel.Core/Reflection/MiniExcelMemberAccessor.cs @@ -0,0 +1,63 @@ +using MiniExcelLib.Core.Exceptions; + +namespace MiniExcelLib.Core.Reflection; + +public class MiniExcelMemberAccessor +{ + private readonly MemberGetter? _getter; + private readonly MemberSetter? _setter; + + public MiniExcelMemberAccessor(MemberInfo member) + { + Name = member.Name; + MemberInfo = member; + + var type = member switch + { + PropertyInfo p => p.PropertyType, + FieldInfo f => f.FieldType, + _ => throw new InvalidMappingException("Only properties and fields can be mapped", member.DeclaringType!, member) // unreachable exception + }; + + var nullableType = Nullable.GetUnderlyingType(type); + IsNullable = nullableType is not null; + Type = nullableType ?? type; + + if (member is PropertyInfo property && property.GetIndexParameters().Length != 0) + { + const string msg = "Types containing indexers cannot be serialized. Please remove them or decorate them with MiniExcelIgnoreAttribute."; + throw new MemberNotSerializableException(msg, member); + } + + if (member is FieldInfo or PropertyInfo { CanRead: true }) + { + CanRead = true; + _getter = new MemberGetter(member); + } + + if (member is FieldInfo { IsInitOnly: false } || (member is PropertyInfo prop && prop.GetSetMethod() is not null)) + { + CanWrite = true; + _setter = new MemberSetter(member); + } + } + + public string Name { get; private set; } + public Type Type { get; private set; } + public MemberInfo MemberInfo { get; private set; } + public bool IsNullable { get; private set; } + public bool CanRead { get; private set; } + public bool CanWrite { get; private set; } + + public object? GetValue(object instance) => _getter is not null + ? _getter.Invoke(instance) + : throw new InvalidOperationException($"The value of member \"{Name}\" cannot be retrieved"); + + public void SetValue(object instance, object? value) + { + if (_setter is null) + throw new InvalidOperationException($"The value of member \"{Name}\" cannot be set"); + + _setter.Invoke(instance, value); + } +} diff --git a/src/MiniExcel.Core/Reflection/MiniExcelProperty.cs b/src/MiniExcel.Core/Reflection/MiniExcelProperty.cs deleted file mode 100644 index cd1c5fa5..00000000 --- a/src/MiniExcel.Core/Reflection/MiniExcelProperty.cs +++ /dev/null @@ -1,52 +0,0 @@ -using MiniExcelLib.Core.Exceptions; - -namespace MiniExcelLib.Core.Reflection; - -public abstract class Member; - -public class MiniExcelProperty : Member -{ - private readonly MemberGetter? _getter; - private readonly MemberSetter? _setter; - - public MiniExcelProperty(PropertyInfo property) - { - Name = property.Name; - Info = property; - - if (property.GetIndexParameters().Length != 0) - { - const string msg = "Types containing indexers cannot be serialized. Please remove them or decorate them with MiniExcelIgnoreAttribute."; - throw new MiniExcelNotSerializableException(msg, property); - } - - if (property.CanRead) - { - CanRead = true; - _getter = new MemberGetter(property); - } - - if (property.CanWrite) - { - CanWrite = true; - _setter = new MemberSetter(property); - } - } - - public string Name { get; protected set; } - public bool CanRead { get; private set; } - public bool CanWrite { get; private set; } - public PropertyInfo Info { get; private set; } - - public object? GetValue(object instance) => _getter is not null - ? _getter.Invoke(instance) - : throw new NotSupportedException(); - - public void SetValue(object instance, object? value) - { - if (_setter is null) - throw new NotSupportedException($"{Name} can't set value"); - - _setter.Invoke(instance, value); - } -} \ No newline at end of file diff --git a/src/MiniExcel.Core/WriteAdapters/AsyncEnumerableWriteAdapter.cs b/src/MiniExcel.Core/WriteAdapters/AsyncEnumerableWriteAdapter.cs index 671abd5f..d9665b09 100644 --- a/src/MiniExcel.Core/WriteAdapters/AsyncEnumerableWriteAdapter.cs +++ b/src/MiniExcel.Core/WriteAdapters/AsyncEnumerableWriteAdapter.cs @@ -10,11 +10,11 @@ internal sealed class AsyncEnumerableWriteAdapter(IAsyncEnumerable values, private bool _disposed = false; - public async Task?> GetColumnsAsync() + public async Task?> GetColumnsAsync() { - if (CustomPropertyHelper.TryGetTypeColumnInfo(typeof(T), _configuration, out var props)) + if (ColumnMappingsProvider.TryGetColumnMappings(typeof(T), _configuration, out var mappings)) { - return props; + return mappings; } _enumerator = _values.GetAsyncEnumerator(); @@ -24,10 +24,10 @@ internal sealed class AsyncEnumerableWriteAdapter(IAsyncEnumerable values, return null; } - return CustomPropertyHelper.GetColumnInfoFromValue(_enumerator.Current, _configuration); + return ColumnMappingsProvider.GetColumnMappingFromValue(_enumerator.Current, _configuration); } - public async IAsyncEnumerable GetRowsAsync(List props, [EnumeratorCancellation] CancellationToken cancellationToken) + public async IAsyncEnumerable GetRowsAsync(List props, [EnumeratorCancellation] CancellationToken cancellationToken) { if (_empty) yield break; @@ -49,7 +49,7 @@ public async IAsyncEnumerable GetRowsAsync(List props) + private static CellWriteInfo[] GetRowValues(T currentValue, List props) { var column = 0; var result = new List(); @@ -65,7 +65,7 @@ private static CellWriteInfo[] GetRowValues(T currentValue, List genericDictionary => new CellWriteInfo(genericDictionary[prop.Key.ToString()], column, prop), IDictionary dictionary => new CellWriteInfo(dictionary[prop.Key], column, prop), - _ => new CellWriteInfo(prop.Property.GetValue(currentValue), column, prop) + _ => new CellWriteInfo(prop.MemberAccessor.GetValue(currentValue), column, prop) }; result.Add(info); } diff --git a/src/MiniExcel.Core/WriteAdapters/DataReaderWriteAdapter.cs b/src/MiniExcel.Core/WriteAdapters/DataReaderWriteAdapter.cs index a85a9273..6e99c376 100644 --- a/src/MiniExcel.Core/WriteAdapters/DataReaderWriteAdapter.cs +++ b/src/MiniExcel.Core/WriteAdapters/DataReaderWriteAdapter.cs @@ -11,23 +11,23 @@ public bool TryGetKnownCount(out int count) return false; } - public List GetColumns() + public List GetColumns() { - var props = new List(); + var props = new List(); for (var i = 0; i < _reader.FieldCount; i++) { var columnName = _reader.GetName(i); if (!_configuration.DynamicColumnFirst || _configuration.DynamicColumns.Any(d => string.Equals(d.Key, columnName, StringComparison.OrdinalIgnoreCase))) { - var prop = CustomPropertyHelper.GetColumnInfosFromDynamicConfiguration(columnName, _configuration); + var prop = ColumnMappingsProvider.GetColumnMappingFromDynamicConfiguration(columnName, _configuration); props.Add(prop); } } return props; } - public IEnumerable> GetRows(List props, CancellationToken cancellationToken = default) + public IEnumerable> GetRows(List props, CancellationToken cancellationToken = default) { while (_reader.Read()) { @@ -36,7 +36,7 @@ public IEnumerable> GetRows(List } } - private IEnumerable GetRowValues(List props) + private IEnumerable GetRowValues(List props) { var column = 1; for (int i = 0; i < _reader.FieldCount; i++) diff --git a/src/MiniExcel.Core/WriteAdapters/DataTableWriteAdapter.cs b/src/MiniExcel.Core/WriteAdapters/DataTableWriteAdapter.cs index fb0bd33a..273d36bd 100644 --- a/src/MiniExcel.Core/WriteAdapters/DataTableWriteAdapter.cs +++ b/src/MiniExcel.Core/WriteAdapters/DataTableWriteAdapter.cs @@ -11,19 +11,19 @@ public bool TryGetKnownCount(out int count) return true; } - public List GetColumns() + public List GetColumns() { - var props = new List(); + var props = new List(); for (var i = 0; i < _dataTable.Columns.Count; i++) { var columnName = _dataTable.Columns[i].Caption ?? _dataTable.Columns[i].ColumnName; - var prop = CustomPropertyHelper.GetColumnInfosFromDynamicConfiguration(columnName, _configuration); + var prop = ColumnMappingsProvider.GetColumnMappingFromDynamicConfiguration(columnName, _configuration); props.Add(prop); } return props; } - public IEnumerable> GetRows(List props, CancellationToken cancellationToken = default) + public IEnumerable> GetRows(List props, CancellationToken cancellationToken = default) { for (int row = 0; row < _dataTable.Rows.Count; row++) { @@ -32,7 +32,7 @@ public IEnumerable> GetRows(List } } - private IEnumerable GetRowValues(int row, List props) + private IEnumerable GetRowValues(int row, List props) { for (int i = 0, column = 1; i < _dataTable.Columns.Count; i++, column++) { diff --git a/src/MiniExcel.Core/WriteAdapters/EnumerableWriteAdapter.cs b/src/MiniExcel.Core/WriteAdapters/EnumerableWriteAdapter.cs index 47a7c1ef..19c12270 100644 --- a/src/MiniExcel.Core/WriteAdapters/EnumerableWriteAdapter.cs +++ b/src/MiniExcel.Core/WriteAdapters/EnumerableWriteAdapter.cs @@ -21,14 +21,14 @@ public bool TryGetKnownCount(out int count) return false; } - public List? GetColumns() + public List? GetColumns() { - if (CustomPropertyHelper.TryGetTypeColumnInfo(_genericType, _configuration, out var props)) + if (ColumnMappingsProvider.TryGetColumnMappings(_genericType, _configuration, out var props)) return props; _enumerator = _values.GetEnumerator(); if (_enumerator.MoveNext()) - return CustomPropertyHelper.GetColumnInfoFromValue(_enumerator.Current, _configuration); + return ColumnMappingsProvider.GetColumnMappingFromValue(_enumerator.Current, _configuration); try { @@ -42,7 +42,7 @@ public bool TryGetKnownCount(out int count) } } - public IEnumerable> GetRows(List props, CancellationToken cancellationToken = default) + public IEnumerable> GetRows(List props, CancellationToken cancellationToken = default) { if (_empty) yield break; @@ -70,7 +70,7 @@ public IEnumerable> GetRows(List } } - public static IEnumerable GetRowValues(object currentValue, List props) + public static IEnumerable GetRowValues(object currentValue, List props) { var column = 1; foreach (var prop in props) @@ -90,7 +90,7 @@ public static IEnumerable GetRowValues(object currentValue, List< } else { - cellValue = prop.Property.GetValue(currentValue); + cellValue = prop.MemberAccessor.GetValue(currentValue); } yield return new CellWriteInfo(cellValue, column, prop); diff --git a/src/MiniExcel.Csv/Api/CsvImporter.cs b/src/MiniExcel.Csv/Api/CsvImporter.cs index 306c29ed..d54c86b9 100644 --- a/src/MiniExcel.Csv/Api/CsvImporter.cs +++ b/src/MiniExcel.Csv/Api/CsvImporter.cs @@ -49,7 +49,8 @@ public async IAsyncEnumerable QueryAsync(Stream stream, bool useHeaderR { using var excelReader = new CsvReader(stream, configuration); await foreach (var item in excelReader.QueryAsync(useHeaderRow, null, "A1", cancellationToken).ConfigureAwait(false)) - yield return item.Aggregate(seed: GetNewExpandoObject(), func: AddPairToDict); + yield return item; + //yield return item.ToDynamicObject(); } #endregion @@ -187,7 +188,7 @@ public async Task GetAsyncDataReader(string path, bool useH var stream = FileHelper.OpenSharedRead(path); var values = QueryAsync(stream, useHeaderRow, configuration, cancellationToken); - return await MiniExcelDataReader.CreateAsync(stream, CastAsync(values, cancellationToken)).ConfigureAwait(false); + return await MiniExcelDataReader.CreateAsync(stream, values.CastToDictionary(cancellationToken)).ConfigureAwait(false); } /// @@ -198,24 +199,8 @@ public async Task GetAsyncDataReader(Stream stream, bool us CsvConfiguration? configuration = null, CancellationToken cancellationToken = default) { var values = QueryAsync(stream, useHeaderRow, configuration, cancellationToken); - return await MiniExcelDataReader.CreateAsync(stream, CastAsync(values, cancellationToken)).ConfigureAwait(false); + return await MiniExcelDataReader.CreateAsync(stream, values.CastToDictionary(cancellationToken)).ConfigureAwait(false); } #endregion - - private static IDictionary GetNewExpandoObject() => new ExpandoObject(); - private static IDictionary AddPairToDict(IDictionary dict, KeyValuePair pair) - { - dict.Add(pair); - return dict; - } - - private static async IAsyncEnumerable> CastAsync(IAsyncEnumerable enumerable, CancellationToken cancellationToken = default) - { - await foreach (var item in enumerable.WithCancellation(cancellationToken).ConfigureAwait(false)) - { - if (item is IDictionary dict) - yield return dict; - } - } } diff --git a/src/MiniExcel.Csv/CsvReader.cs b/src/MiniExcel.Csv/CsvReader.cs index 96c22754..0b01092d 100644 --- a/src/MiniExcel.Csv/CsvReader.cs +++ b/src/MiniExcel.Csv/CsvReader.cs @@ -71,7 +71,7 @@ internal CsvReader(Stream stream, IMiniExcelConfiguration? configuration) .Select((x, i) => new KeyValuePair(headRows[i], x)) .ToDictionary(x => x.Key, x => x.Value); - throw new MiniExcelColumnNotFoundException(columnIndex: null, headRows[colIndex], [], rowIndex, headers, rowValues, $"Csv read error: Column {colIndex} not found in Row {rowIndex}"); + throw new ColumnNotFoundException(columnIndex: null, headRows[colIndex], [], rowIndex, headers, rowValues, $"Csv read error: Column {colIndex} not found in Row {rowIndex}"); } //header @@ -85,7 +85,7 @@ internal CsvReader(Stream stream, IMiniExcelConfiguration? configuration) continue; } - var headCell = CustomPropertyHelper.GetEmptyExpandoObject(headRows); + var headCell = ExpandoHelper.CreateEmptyByHeaders(headRows); for (int i = 0; i <= read.Length - 1; i++) headCell[headRows[i]] = read[i]; @@ -102,7 +102,7 @@ internal CsvReader(Stream stream, IMiniExcelConfiguration? configuration) } // todo: can we find a way to remove the redundant cell conversions for CSV? - var cell = CustomPropertyHelper.GetEmptyExpandoObject(read.Length - 1, 0); + var cell = ExpandoHelper.CreateEmptyByIndices(read.Length - 1, 0); if (_config.ReadEmptyStringAsNull) { for (int i = 0; i <= read.Length - 1; i++) @@ -152,20 +152,6 @@ internal CsvReader(Stream stream, IMiniExcelConfiguration? configuration) return MiniExcelMapper.MapQueryAsync(dynamicRecords, 0, treatHeaderAsData, false, _config, null, cancellationToken); } - private static string ConvertXyToCell(int x, int y) - { - int dividend = x; - string columnName = string.Empty; - - while (dividend > 0) - { - var modulo = (dividend - 1) % 26; - columnName = Convert.ToChar(65 + modulo) + columnName; - dividend = (dividend - modulo) / 26; - } - return $"{columnName}{y}"; - } - private string[] Split(string row) { if (_config.SplitFn is not null) diff --git a/src/MiniExcel.Csv/CsvWriter.cs b/src/MiniExcel.Csv/CsvWriter.cs index 2f615494..37c11ddf 100644 --- a/src/MiniExcel.Csv/CsvWriter.cs +++ b/src/MiniExcel.Csv/CsvWriter.cs @@ -200,7 +200,7 @@ public async Task InsertAsync(bool overwriteSheet = false, IProgress? return rowsWritten.FirstOrDefault(); } - public string ToCsvString(object? value, MiniExcelColumnInfo? p) + public string ToCsvString(object? value, MiniExcelColumnMapping? p) { if (value is null) return ""; @@ -221,7 +221,7 @@ public string ToCsvString(object? value, MiniExcelColumnInfo? p) return Convert.ToString(value, _configuration.Culture) ?? ""; } - private string GetHeader(List props) => string.Join( + private string GetHeader(List props) => string.Join( _configuration.Seperator.ToString(), props.Select(s => CsvSanitizer.SanitizeCsvField(s?.ExcelColumnName, _configuration))); diff --git a/src/MiniExcel.OpenXml/Api/OpenXmlImporter.cs b/src/MiniExcel.OpenXml/Api/OpenXmlImporter.cs index 15788e19..1164aad2 100644 --- a/src/MiniExcel.OpenXml/Api/OpenXmlImporter.cs +++ b/src/MiniExcel.OpenXml/Api/OpenXmlImporter.cs @@ -52,7 +52,7 @@ public async IAsyncEnumerable QueryAsync(Stream stream, bool useHeaderR { using var excelReader = await OpenXmlReader.CreateAsync(stream, configuration, cancellationToken).ConfigureAwait(false); await foreach (var item in excelReader.QueryAsync(useHeaderRow, sheetName, startCell, cancellationToken).ConfigureAwait(false)) - yield return item.Aggregate(seed: GetNewExpandoObject(), func: AddPairToDict); + yield return item; } #endregion @@ -84,7 +84,7 @@ public async IAsyncEnumerable QueryRangeAsync(Stream stream, bool useHe { using var excelReader = await OpenXmlReader.CreateAsync(stream, configuration, cancellationToken).ConfigureAwait(false); await foreach (var item in excelReader.QueryRangeAsync(useHeaderRow, sheetName, startCell, endCell, cancellationToken).ConfigureAwait(false)) - yield return item.Aggregate(seed: GetNewExpandoObject(), func: AddPairToDict); + yield return item; } [CreateSyncVersion] @@ -106,7 +106,7 @@ public async IAsyncEnumerable QueryRangeAsync(Stream stream, bool useHe { using var excelReader = await OpenXmlReader.CreateAsync(stream, configuration, cancellationToken).ConfigureAwait(false); await foreach (var item in excelReader.QueryRangeAsync(useHeaderRow, sheetName, startRowIndex, startColumnIndex, endRowIndex, endColumnIndex, cancellationToken).ConfigureAwait(false)) - yield return item.Aggregate(seed: GetNewExpandoObject(), func: AddPairToDict); + yield return item; } #endregion @@ -307,7 +307,7 @@ public async Task GetAsyncDataReader(string path, bool useH var stream = FileHelper.OpenSharedRead(path); var values = QueryAsync(stream, useHeaderRow, sheetName, startCell, configuration, cancellationToken); - return await MiniExcelDataReader.CreateAsync(stream, CastAsync(values, cancellationToken)).ConfigureAwait(false); + return await MiniExcelDataReader.CreateAsync(stream, values.CastToDictionary(cancellationToken)).ConfigureAwait(false); } /// @@ -319,24 +319,8 @@ public async Task GetAsyncDataReader(Stream stream, bool us CancellationToken cancellationToken = default) { var values = QueryAsync(stream, useHeaderRow, sheetName, startCell, configuration, cancellationToken); - return await MiniExcelDataReader.CreateAsync(stream, CastAsync(values, cancellationToken)).ConfigureAwait(false); + return await MiniExcelDataReader.CreateAsync(stream, values.CastToDictionary(cancellationToken)).ConfigureAwait(false); } #endregion - - private static IDictionary GetNewExpandoObject() => new ExpandoObject(); - private static IDictionary AddPairToDict(IDictionary dict, KeyValuePair pair) - { - dict.Add(pair); - return dict; - } - - private static async IAsyncEnumerable> CastAsync(IAsyncEnumerable enumerable, CancellationToken cancellationToken = default) - { - await foreach (var item in enumerable.WithCancellation(cancellationToken).ConfigureAwait(false)) - { - if (item is IDictionary dict) - yield return dict; - } - } } diff --git a/src/MiniExcel.OpenXml/FluentMapping/MappingCellStreamAdapter.cs b/src/MiniExcel.OpenXml/FluentMapping/MappingCellStreamAdapter.cs index e0579644..5859d926 100644 --- a/src/MiniExcel.OpenXml/FluentMapping/MappingCellStreamAdapter.cs +++ b/src/MiniExcel.OpenXml/FluentMapping/MappingCellStreamAdapter.cs @@ -16,13 +16,13 @@ public bool TryGetKnownCount(out int count) return false; } - public List GetColumns() + public List GetColumns() { - var props = new List(); + var props = new List(); for (int i = 0; i < _columnLetters.Length; i++) { - props.Add(new MiniExcelColumnInfo + props.Add(new MiniExcelColumnMapping { Key = _columnLetters[i], ExcelColumnName = _columnLetters[i], @@ -33,7 +33,7 @@ public List GetColumns() return props; } - public IEnumerable> GetRows(List props, CancellationToken cancellationToken = default) + public IEnumerable> GetRows(List props, CancellationToken cancellationToken = default) { var currentRow = new Dictionary(); var currentRowIndex = 0; @@ -67,7 +67,7 @@ public IEnumerable> GetRows(List } } - private static IEnumerable ConvertRowToCellWriteInfos(Dictionary row, List props) + private static IEnumerable ConvertRowToCellWriteInfos(Dictionary row, List props) { var columnIndex = 1; foreach (var prop in props) diff --git a/src/MiniExcel.OpenXml/Models/ExcelColumnWidth.cs b/src/MiniExcel.OpenXml/Models/ExcelColumnWidth.cs index d6786d14..ad5930f7 100644 --- a/src/MiniExcel.OpenXml/Models/ExcelColumnWidth.cs +++ b/src/MiniExcel.OpenXml/Models/ExcelColumnWidth.cs @@ -30,7 +30,7 @@ private ExcelColumnWidthCollection(ICollection columnWidths, d _columnWidths = columnWidths.ToDictionary(x => x.Index); } - internal static ExcelColumnWidthCollection GetFromMappings(ICollection mappings, double? minWidth = null, double maxWidth = 200) + internal static ExcelColumnWidthCollection GetFromMappings(ICollection mappings, double? minWidth = null, double maxWidth = 200) { var i = 1; List columnWidths = []; diff --git a/src/MiniExcel.OpenXml/OpenXmlReader.cs b/src/MiniExcel.OpenXml/OpenXmlReader.cs index 6e5587ea..d9c23796 100644 --- a/src/MiniExcel.OpenXml/OpenXmlReader.cs +++ b/src/MiniExcel.OpenXml/OpenXmlReader.cs @@ -386,8 +386,8 @@ private ZipArchiveEntry GetSheetEntry(string? sheetName) private static IDictionary GetCell(bool useHeaderRow, int maxColumnIndex, Dictionary headRows, int startColumnIndex) { return useHeaderRow - ? CustomPropertyHelper.GetEmptyExpandoObject(headRows) - : CustomPropertyHelper.GetEmptyExpandoObject(maxColumnIndex, startColumnIndex); + ? ExpandoHelper.CreateEmptyByHeaders(headRows) + : ExpandoHelper.CreateEmptyByIndices(maxColumnIndex, startColumnIndex); } private static void SetCellsValueAndHeaders(object? cellValue, bool useHeaderRow, Dictionary headRows, bool isFirstRow, IDictionary cell, int columnIndex) diff --git a/src/MiniExcel.OpenXml/OpenXmlWriter.DefaultOpenXml.cs b/src/MiniExcel.OpenXml/OpenXmlWriter.DefaultOpenXml.cs index e893b741..dcd2e3c9 100644 --- a/src/MiniExcel.OpenXml/OpenXmlWriter.DefaultOpenXml.cs +++ b/src/MiniExcel.OpenXml/OpenXmlWriter.DefaultOpenXml.cs @@ -1,4 +1,5 @@ using MiniExcelLib.OpenXml.Constants; +using System.ComponentModel; using static MiniExcelLib.Core.Helpers.ImageHelper; namespace MiniExcelLib.OpenXml; @@ -152,7 +153,7 @@ private string GetPanes() return sb.ToString(); } - private Tuple GetCellValue(int rowIndex, int cellIndex, object value, MiniExcelColumnInfo? columnInfo, bool valueIsNull) + private Tuple GetCellValue(int rowIndex, int cellIndex, object value, MiniExcelColumnMapping? columnInfo, bool valueIsNull) { if (valueIsNull) return Tuple.Create("2", "str", string.Empty); @@ -177,8 +178,12 @@ private Tuple GetCellValue(int rowIndex, int cellIndex, #endif if (type.IsEnum) { - var description = CustomPropertyHelper.GetDescriptionAttribute(type, value); - return Tuple.Create("2", "str", description ?? value.ToString()); + var name = Enum.GetName(type, value) ?? ""; + var description = type.GetField(name) + ?.GetCustomAttribute() + ?.Description ?? name; + + return Tuple.Create("2", "str", description); } if (TypeHelper.IsNumericType(type)) @@ -209,7 +214,7 @@ private Tuple GetCellValue(int rowIndex, int cellIndex, return Tuple.Create("2", "str", XmlHelper.EncodeXml(value.ToString())); } - private static Type? GetValueType(object value, MiniExcelColumnInfo? columnInfo) + private static Type? GetValueType(object value, MiniExcelColumnMapping? columnInfo) { Type type; if (columnInfo is not { Key: null }) @@ -293,7 +298,7 @@ private string GetFileValue(int rowIndex, int cellIndex, object value) return base64; } - private Tuple GetDateTimeValue(DateTime value, MiniExcelColumnInfo columnInfo) + private Tuple GetDateTimeValue(DateTime value, MiniExcelColumnMapping columnInfo) { string? cellValue; if (!ReferenceEquals(_configuration.Culture, CultureInfo.InvariantCulture)) diff --git a/src/MiniExcel.OpenXml/OpenXmlWriter.cs b/src/MiniExcel.OpenXml/OpenXmlWriter.cs index 5f0fbf80..c846c435 100644 --- a/src/MiniExcel.OpenXml/OpenXmlWriter.cs +++ b/src/MiniExcel.OpenXml/OpenXmlWriter.cs @@ -250,7 +250,7 @@ private async Task WriteValuesAsync(EnhancedStreamWriter writer, object val #else var props = writeAdapter is not null ? writeAdapter.GetColumns() - : await (asyncWriteAdapter?.GetColumnsAsync() ?? Task.FromResult?>(null)).ConfigureAwait(false); + : await (asyncWriteAdapter?.GetColumnsAsync() ?? Task.FromResult?>(null)).ConfigureAwait(false); #endif if (props is null) @@ -422,7 +422,7 @@ private static async Task WriteColumnsWidthsAsync(EnhancedStreamWriter writer, I } [CreateSyncVersion] - private async Task PrintHeaderAsync(EnhancedStreamWriter writer, List props, CancellationToken cancellationToken = default) + private async Task PrintHeaderAsync(EnhancedStreamWriter writer, List props, CancellationToken cancellationToken = default) { const int yIndex = 1; await writer.WriteAsync(WorksheetXml.StartRow(yIndex), cancellationToken).ConfigureAwait(false); @@ -452,7 +452,7 @@ private async Task WriteCellAsync(EnhancedStreamWriter writer, string cellRefere } [CreateSyncVersion] - private async Task WriteCellAsync(EnhancedStreamWriter writer, int rowIndex, int cellIndex, object? value, MiniExcelColumnInfo columnInfo, ExcelColumnWidthCollection? widthCollection, CancellationToken cancellationToken = default) + private async Task WriteCellAsync(EnhancedStreamWriter writer, int rowIndex, int cellIndex, object? value, MiniExcelColumnMapping columnInfo, ExcelColumnWidthCollection? widthCollection, CancellationToken cancellationToken = default) { if (columnInfo?.CustomFormatter is not null) { diff --git a/src/MiniExcel.OpenXml/Templates/OpenXmlTemplate.Impl.cs b/src/MiniExcel.OpenXml/Templates/OpenXmlTemplate.Impl.cs index 3301b699..532a693a 100644 --- a/src/MiniExcel.OpenXml/Templates/OpenXmlTemplate.Impl.cs +++ b/src/MiniExcel.OpenXml/Templates/OpenXmlTemplate.Impl.cs @@ -1,5 +1,6 @@ using MiniExcelLib.Core.Attributes; using MiniExcelLib.OpenXml.Constants; +using System.ComponentModel; namespace MiniExcelLib.OpenXml.Templates; @@ -815,7 +816,11 @@ private async Task GenerateCellValuesAsync( } else if (type?.IsEnum is true) { - var description = CustomPropertyHelper.GetDescriptionAttribute(type, cellValue); + var stringValue = Enum.GetName(type, cellValue) ?? ""; + + var attr = type.GetField(stringValue)?.GetCustomAttribute(); + var description = attr?.Description ?? stringValue; + cellValueStr = XmlHelper.EncodeXml(description); } else diff --git a/tests/MiniExcel.Csv.Tests/AsyncIssueTests.cs b/tests/MiniExcel.Csv.Tests/AsyncIssueTests.cs index a4274917..a8176b55 100644 --- a/tests/MiniExcel.Csv.Tests/AsyncIssueTests.cs +++ b/tests/MiniExcel.Csv.Tests/AsyncIssueTests.cs @@ -254,7 +254,7 @@ public async Task Issue142() { using var path = AutoDeletingPath.Create(); Issue142VoDuplicateColumnName[] input = [new() { MyProperty1 = 0, MyProperty2 = 0, MyProperty3 = 0, MyProperty4 = 0 }]; - Assert.Throws(() => _openXmlExporter.Export(path.ToString(), input)); + Assert.Throws(() => _openXmlExporter.Export(path.ToString(), input)); } } diff --git a/tests/MiniExcel.Csv.Tests/IssueTests.cs b/tests/MiniExcel.Csv.Tests/IssueTests.cs index 2c09ec34..6eb398b6 100644 --- a/tests/MiniExcel.Csv.Tests/IssueTests.cs +++ b/tests/MiniExcel.Csv.Tests/IssueTests.cs @@ -339,7 +339,7 @@ public void TestIssue316() _openXmlExporter.Export(path, value, configuration: config); //Datetime error - Assert.Throws(() => + Assert.Throws(() => { var conf = new OpenXmlConfiguration { @@ -415,7 +415,7 @@ public void TestIssue316() _csvExporter.Export(path, value, configuration: config); //Datetime error - Assert.Throws(() => + Assert.Throws(() => { var conf = new CsvConfiguration { @@ -850,7 +850,7 @@ public void Issue142() [ new() { MyProperty1 = 0, MyProperty2 = 0, MyProperty3 = 0, MyProperty4 = 0 } ]; - Assert.Throws(() => _csvExporter.Export(path.ToString(), input)); + Assert.Throws(() => _csvExporter.Export(path.ToString(), input)); } } @@ -862,7 +862,7 @@ public void Issue142_Query() var rows = _openXmlImporter.Query(path).ToList(); Assert.Equal(0, rows[0].MyProperty1); - Assert.Throws(() => _openXmlImporter.Query(path).ToList()); + Assert.Throws(() => _openXmlImporter.Query(path).ToList()); var rowsXlsx = _openXmlImporter.Query(path).ToList(); Assert.Equal("CustomColumnName", rowsXlsx[0].MyProperty1); diff --git a/tests/MiniExcel.Csv.Tests/MiniExcelCsvTests.cs b/tests/MiniExcel.Csv.Tests/MiniExcelCsvTests.cs index a95e9fbd..9b152d83 100644 --- a/tests/MiniExcel.Csv.Tests/MiniExcelCsvTests.cs +++ b/tests/MiniExcel.Csv.Tests/MiniExcelCsvTests.cs @@ -193,7 +193,7 @@ public void SaveAsByDictionary() Assert.Equal(2, rowsWritten[0]); using var reader = new StreamReader(path.ToString()); - using var csv = new global::CsvHelper.CsvReader(reader, CultureInfo.InvariantCulture); + using var csv = new CsvReader(reader, CultureInfo.InvariantCulture); var records = csv.GetRecords().ToList(); Assert.Equal(@"""<>+-*//}{\\n", records[0].a); @@ -229,7 +229,7 @@ public void SaveAsByDictionary() _csvExporter.Export(path.ToString(), values); using (var reader = new StreamReader(path.ToString())) - using (var csv = new global::CsvHelper.CsvReader(reader, CultureInfo.InvariantCulture)) + using (var csv = new CsvReader(reader, CultureInfo.InvariantCulture)) { var records = csv.GetRecords().ToList(); { @@ -280,7 +280,7 @@ public void SaveAsByDataTableTest() Assert.Equal(2, rowsWritten[0]); using (var reader = new StreamReader(path.ToString())) - using (var csv = new global::CsvHelper.CsvReader(reader, CultureInfo.InvariantCulture)) + using (var csv = new CsvReader(reader, CultureInfo.InvariantCulture)) { var records = csv.GetRecords().ToList(); Assert.Equal(@"""<>+-*//}{\\n", records[0].a); @@ -332,7 +332,7 @@ public void CsvExcelTypeTest() Assert.Equal("Test2", rows[1].B); using var reader = new StreamReader(path); - using var csv = new global::CsvHelper.CsvReader(reader, CultureInfo.InvariantCulture); + using var csv = new CsvReader(reader, CultureInfo.InvariantCulture); var records = csv.GetRecords().ToList(); Assert.Equal("Test1", records[0].A); Assert.Equal("Test2", records[0].B); @@ -408,7 +408,7 @@ public void CsvColumnNotFoundTest() using (var stream = File.OpenRead(path)) { - var exception = Assert.Throws(() => _csvImporter.Query(stream).ToList()); + var exception = Assert.Throws(() => _csvImporter.Query(stream).ToList()); Assert.Equal("c2", exception.ColumnName); Assert.Equal(2, exception.RowIndex); @@ -418,7 +418,7 @@ public void CsvColumnNotFoundTest() } { - var exception = Assert.Throws(() => _csvImporter.Query(path).ToList()); + var exception = Assert.Throws(() => _csvImporter.Query(path).ToList()); Assert.Equal("c2", exception.ColumnName); Assert.Equal(2, exception.RowIndex); @@ -437,7 +437,7 @@ public void CsvColumnNotFoundWithAliasTest() File.WriteAllLines(path, ["col1,col2", "v1"]); using (var stream = File.OpenRead(path)) { - var exception = Assert.Throws(() => _csvImporter.Query(stream).ToList()); + var exception = Assert.Throws(() => _csvImporter.Query(stream).ToList()); Assert.Equal("c2", exception.ColumnName); Assert.Equal(2, exception.RowIndex); @@ -447,7 +447,7 @@ public void CsvColumnNotFoundWithAliasTest() } { - var exception = Assert.Throws(() => _csvImporter.Query(path).ToList()); + var exception = Assert.Throws(() => _csvImporter.Query(path).ToList()); Assert.Equal("c2", exception.ColumnName); Assert.Equal(2, exception.RowIndex); @@ -560,4 +560,105 @@ public async Task InsertCsvTest() } } -} \ No newline at end of file + private class CsvFieldMappingTest + { + [MiniExcelColumnName("Column1")] + public string Test1; + + [MiniExcelColumnName("Column2")] + public int Test2; + + [MiniExcelColumnIndex(0)] + public decimal Test; + } + + [Fact] + public void ExportAndQueryFieldsStrongMappingTest() + { + using var file = AutoDeletingPath.Create(ExcelType.Csv); + var path = file.ToString(); + + var input = Enumerable.Range(1, 3) + .Select(i => new CsvFieldMappingTest { Test1 = $"T{i}", Test2 = i, Test = i + (decimal)i/10 }); + + _csvExporter.Export(path, input); + + var rows = _csvImporter.Query(path).ToList(); + Assert.Equal(3, rows.Count); + Assert.Equal("T1", rows[0].Test1); + Assert.Equal(1, rows[0].Test2); + Assert.Equal(1.1m, rows[0].Test); + } + + [Fact] + public void QueryFieldsAsDynamicTest() + { + using var file = AutoDeletingPath.Create(ExcelType.Csv); + var path = file.ToString(); + + var input = new[] { new CsvFieldMappingTest { Test1 = "X1", Test2 = 5, Test = 2.3m } }; + _csvExporter.Export(path, input); + + using var reader = new StreamReader(path); + using var csv = new CsvReader(reader, CultureInfo.InvariantCulture); + var records = csv.GetRecords().ToList(); + var first = records[0] as IDictionary; + + Assert.Contains("Column1", first!.Keys); + Assert.Contains("Column2", first.Keys); + Assert.Contains("Test", first.Keys); + } + + private class MixedFieldPropertyTest + { + [MiniExcelColumnName("F1")] + public string Field1; + + [MiniExcelColumnName("P1")] + public string Prop1 { get; set; } + } + + [Fact] + public void ExportAndQueryMixedFieldAndPropertyTest() + { + using var file = AutoDeletingPath.Create(ExcelType.Csv); + var path = file.ToString(); + + var input = new[] { new MixedFieldPropertyTest { Field1 = "F", Prop1 = "P" } }; + _csvExporter.Export(path, input); + + using var reader = new StreamReader(path); + using var csv = new CsvReader(reader, CultureInfo.InvariantCulture); + var records = csv.GetRecords().ToList(); + var first = records[0] as IDictionary; + + Assert.Contains("F1", first.Keys); + Assert.Contains("P1", first.Keys); + } + + private class CsvFieldsWithoutAttributeDemo + { + public string NotMappedField; + + [MiniExcelColumnName("Mapped")] + public string MappedField; + } + + [Fact] + public void ExportAndQueryFieldsWithoutAttributeTest() + { + using var file = AutoDeletingPath.Create(ExcelType.Csv); + var path = file.ToString(); + + var input = new[] { new CsvFieldsWithoutAttributeDemo { NotMappedField = "NO", MappedField = "YES" } }; + _csvExporter.Export(path, input); + + using var reader = new StreamReader(path); + using var csv = new CsvReader(reader, CultureInfo.InvariantCulture); + var records = csv.GetRecords().ToList(); + var first = records[0] as IDictionary; + + Assert.Contains("Mapped", first.Keys); + Assert.DoesNotContain("NotMappedField", first.Keys); + } +} diff --git a/tests/MiniExcel.OpenXml.Tests/MiniExcelIssueAsyncTests.cs b/tests/MiniExcel.OpenXml.Tests/MiniExcelIssueAsyncTests.cs index bfcffb32..2188cd9b 100644 --- a/tests/MiniExcel.OpenXml.Tests/MiniExcelIssueAsyncTests.cs +++ b/tests/MiniExcel.OpenXml.Tests/MiniExcelIssueAsyncTests.cs @@ -1,4 +1,5 @@ -using MiniExcelLib.OpenXml.Tests.Utils; +using MiniExcelLib.Core.Exceptions; +using MiniExcelLib.OpenXml.Tests.Utils; using MiniExcelLib.Tests.Common.Utils; namespace MiniExcelLib.OpenXml.Tests; @@ -1040,7 +1041,7 @@ public async Task Issue142_Query() } { - await Assert.ThrowsAsync(async () => + await Assert.ThrowsAsync(async () => { var q = _excelImporter.QueryAsync(path).ToBlockingEnumerable().ToList(); }); diff --git a/tests/MiniExcel.OpenXml.Tests/MiniExcelIssueTests.cs b/tests/MiniExcel.OpenXml.Tests/MiniExcelIssueTests.cs index 32ed7d05..f0983649 100644 --- a/tests/MiniExcel.OpenXml.Tests/MiniExcelIssueTests.cs +++ b/tests/MiniExcel.OpenXml.Tests/MiniExcelIssueTests.cs @@ -896,13 +896,13 @@ public void TestIssue209() var path = PathHelper.GetFile("xlsx/TestIssue309.xlsx"); var rows = _excelImporter.Query(path).ToList(); } - catch (MiniExcelInvalidCastException ex) + catch (ValueNotAssignableException ex) { Assert.Equal("SEQ", ex.ColumnName); Assert.Equal(4, ex.Row); Assert.Equal("Error", ex.Value); - Assert.Equal(typeof(int), ex.InvalidCastType); - Assert.Equal("ColumnName: SEQ, CellRow: 4, Value: Error. The value cannot be cast to type Int32.", ex.Message); + Assert.Equal(typeof(int), ex.ColumnType); + Assert.Equal("The value Error cannot be assigned to type Int32.", ex.Message); } } @@ -1367,7 +1367,7 @@ public void TestIssueI3X2ZL() catch (InvalidCastException ex) { Assert.Equal( - "ColumnName: Col2, CellRow: 6, Value: error. The value cannot be cast to type DateTime.", + "The value error cannot be assigned to type DateTime.", ex.Message ); } @@ -1380,7 +1380,7 @@ public void TestIssueI3X2ZL() catch (InvalidCastException ex) { Assert.Equal( - "ColumnName: Col1, CellRow: 3, Value: error. The value cannot be cast to type Int32.", + "The value error cannot be assigned to type Int32.", ex.Message ); } @@ -3713,7 +3713,7 @@ public void TestIssue869(string fileName, DateOnlyConversionMode mode, bool thro var testFn = () => _excelImporter.Query(path, configuration: config).ToList(); if (throwsException) { - Assert.Throws(testFn); + Assert.Throws(testFn); } else { @@ -3760,7 +3760,7 @@ public void TestIssue880_ShouldThrowNotSerializableException() { Issue880[] toExport = [new() { Test = "test" }]; - Assert.Throws(() => + Assert.Throws(() => { using var ms = new MemoryStream(); _excelExporter.Export(ms, toExport); @@ -3770,7 +3770,7 @@ public void TestIssue880_ShouldThrowNotSerializableException() [Fact] public void TestIssue881() { - Assert.Throws(() => + Assert.Throws(() => { _ = _excelImporter.Query(PathHelper.GetFile("xlsx/TestIssue881.xlsx")).ToList(); }); diff --git a/tests/MiniExcel.OpenXml.Tests/MiniExcelOpenXmlAsyncTests.cs b/tests/MiniExcel.OpenXml.Tests/MiniExcelOpenXmlAsyncTests.cs index f4e8dd5d..a1471543 100644 --- a/tests/MiniExcel.OpenXml.Tests/MiniExcelOpenXmlAsyncTests.cs +++ b/tests/MiniExcel.OpenXml.Tests/MiniExcelOpenXmlAsyncTests.cs @@ -1,5 +1,6 @@ using ClosedXML.Excel; using ExcelDataReader; +using MiniExcelLib.Core.Exceptions; using MiniExcelLib.OpenXml.Tests.Utils; using MiniExcelLib.Tests.Common.Utils; @@ -74,7 +75,7 @@ private class ExcelAttributeDemo2 public async Task CustomAttributeWihoutVaildPropertiesTest() { var path = PathHelper.GetFile("xlsx/TestCustomExcelColumnAttribute.xlsx"); - await Assert.ThrowsAsync(async () => + await Assert.ThrowsAsync(async () => { _ = _excelImporter.QueryAsync(path).ToBlockingEnumerable().ToList(); }); diff --git a/tests/MiniExcel.OpenXml.Tests/MiniExcelOpenXmlTests.cs b/tests/MiniExcel.OpenXml.Tests/MiniExcelOpenXmlTests.cs index c49651ec..0a1027e1 100644 --- a/tests/MiniExcel.OpenXml.Tests/MiniExcelOpenXmlTests.cs +++ b/tests/MiniExcel.OpenXml.Tests/MiniExcelOpenXmlTests.cs @@ -1,8 +1,10 @@ using ClosedXML.Excel; using ExcelDataReader; +using MiniExcelLib.Core.Exceptions; using MiniExcelLib.OpenXml.Tests.Utils; using MiniExcelLib.Tests.Common.Utils; using FileHelper = MiniExcelLib.OpenXml.Tests.Utils.FileHelper; +using Path = System.IO.Path; namespace MiniExcelLib.OpenXml.Tests; @@ -18,18 +20,19 @@ public void GetColumnsTest() { var tmPath = PathHelper.GetFile("xlsx/TestTypeMapping.xlsx"); var tePath = PathHelper.GetFile("xlsx/TestEmpty.xlsx"); + { var columns = _excelImporter.GetColumnNames (tmPath); Assert.Equal(["A", "B", "C", "D", "E", "F", "G", "H"], columns); } { - var columns = _excelImporter.GetColumnNames (tmPath); + var columns = _excelImporter.GetColumnNames(tmPath); Assert.Equal(8, columns.Count); } { - var columns = _excelImporter.GetColumnNames (tePath); + var columns = _excelImporter.GetColumnNames(tePath); Assert.Empty(columns); } } @@ -80,7 +83,7 @@ private class ExcelAttributeDemo public void CustomAttributeWihoutVaildPropertiesTest() { var path = PathHelper.GetFile("xlsx/TestCustomExcelColumnAttribute.xlsx"); - Assert.Throws(() => _excelImporter.Query(path).ToList()); + Assert.Throws(() => _excelImporter.Query(path).ToList()); } [Fact] @@ -330,7 +333,7 @@ public void QueryStrongTypeMapping_Test() var path = PathHelper.GetFile("xlsx/TestTypeMapping.xlsx"); using (var stream = File.OpenRead(path)) { - var rows = _excelImporter.Query(stream).ToList(); + var rows = _excelImporter.Query(stream).ToList(); Assert.Equal(100, rows.Count); Assert.Equal(Guid.Parse("78DE23D2-DCB6-BD3D-EC67-C112BBC322A2"), rows[0].ID); @@ -1386,7 +1389,6 @@ public void DynamicColumnsConfigurationIsUsedWhenCreatingExcelUsingDataTable() using var stream = File.OpenRead(path.ToString()); var rows = _excelImporter.Query(stream, useHeaderRow: true) - .Select(x => (IDictionary)x) .Select(x => (IDictionary)x) .ToList(); @@ -1618,4 +1620,102 @@ public void SheetDimensionsTest_MultiSheet() Assert.Equal(1, dim[2].Columns.StartIndex); Assert.Equal(2, dim[2].Columns.EndIndex); } + + // todo: add more tests for fields mapping + private class ExcelFieldMappingTest + { + [MiniExcelColumnName("Column1")] + public string Test1; + + [MiniExcelColumnName("Column2")] + public int Test2; + + [MiniExcelColumnIndex(0)] + public decimal Test; + } + + [Fact] + public void ExportAndQueryFieldsStrongMappingTest() + { + using var path = AutoDeletingPath.Create(); + var input = Enumerable.Range(1, 3) + .Select(i => new ExcelFieldMappingTest + { + Test1 = $"T{i}", + Test2 = i, + Test = i + (decimal)i / 10 + }); + + _excelExporter.Export(path.ToString(), input); + + var rows = _excelImporter.Query(path.ToString()).ToList(); + Assert.Equal(3, rows.Count); + Assert.Equal("T1", rows[0].Test1); + Assert.Equal(1, rows[0].Test2); + Assert.Equal(1.1m, rows[0].Test); + } + + [Fact] + public void QueryFieldsAsDynamicTest() + { + using var path = AutoDeletingPath.Create(); + ExcelFieldMappingTest[] input = [new() { Test1 = "X1", Test2 = 5, Test = 7.3m }]; + + _excelExporter.Export(path.ToString(), input); + + var rows = _excelImporter.Query(path.ToString(), true).ToList(); + var first = rows[0] as IDictionary; + + // Column headers should include the column names from field attributes + Assert.Contains("Column1", first!.Keys); + Assert.Contains("Column2", first.Keys); + } + + private class MixedFieldPropertyTest + { + [MiniExcelColumnName("F1")] + public string Field1; + + [MiniExcelColumnName("P1")] + public string Prop1 { get; set; } + } + + [Fact] + public void ExportAndQueryMixedFieldAndPropertyTest() + { + using var path = AutoDeletingPath.Create(); + var input = new[] { new MixedFieldPropertyTest { Field1 = "F", Prop1 = "P" } }; + + _excelExporter.Export(path.ToString(), input); + + var rows = _excelImporter.Query(path.ToString(), true).ToList(); + var first = rows[0] as IDictionary; + + Assert.Contains("F1", first!.Keys); + Assert.Contains("P1", first.Keys); + } + + private class FieldsWithoutAttributeTest + { + // field without attribute should not be included for export + public string NotMappedField; + + [MiniExcelColumnName("Mapped")] + public string MappedField; + } + + [Fact] + public void ExportAndQueryFieldsWithoutAttributeTest() + { + using var path = AutoDeletingPath.Create(); + var input = new[] { new FieldsWithoutAttributeTest { NotMappedField = "NO", MappedField = "YES" } }; + + _excelExporter.Export(path.ToString(), input); + + var rows = _excelImporter.Query(path.ToString(), true).ToList(); + var first = rows[0] as IDictionary; + + Assert.Contains("Mapped", first?.Keys); + Assert.DoesNotContain("NotMappedField", first?.Keys); + } } \ No newline at end of file