Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions src/MiniExcel.Core/Helpers/Polyfills.cs
Original file line number Diff line number Diff line change
Expand Up @@ -156,3 +156,18 @@ public static ValueTask<ZipArchive> CreateAsync(Stream stream, ZipArchiveMode mo
}
#endif
}

#if NETSTANDARD2_0
/// <summary>
/// Custom equality comparer that uses reference equality instead of overridden object.Equals.
/// Required for .NET versions where ReferenceEqualityComparer is not built-in.
/// </summary>
public class ReferenceEqualityComparer : IEqualityComparer<object>
{
private ReferenceEqualityComparer() { }
public static ReferenceEqualityComparer Instance { get; } = new();

bool IEqualityComparer<object>.Equals(object? x, object? y) => ReferenceEquals(x, y);
int IEqualityComparer<object>.GetHashCode(object obj) => RuntimeHelpers.GetHashCode(obj);
}
#endif
5 changes: 4 additions & 1 deletion src/MiniExcel.Core/Helpers/TypeHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,11 @@ public static IEnumerable<IDictionary<string, object>> ToEnumerableDictionaries(
.Select(t => t.GetGenericArguments()[0]);
}

public static bool IsNumericType(Type type, bool isNullableUnderlyingType = false)
public static bool IsNumericType(Type? type, bool isNullableUnderlyingType = false)
{
if (type is null)
return false;

if (isNullableUnderlyingType)
type = Nullable.GetUnderlyingType(type) ?? type;

Expand Down
1 change: 1 addition & 0 deletions src/MiniExcel.OpenXml/Constants/Schemas.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ internal static class Schemas
public const string SpreadsheetmlXmlSpreadsheetDrawing = "http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing";
public const string SpreadsheetmlXmlDrawingml2006 = "http://schemas.openxmlformats.org/drawingml/2006/main";
public const string SpreadsheetmlXmlDrawing2014 = "http://schemas.microsoft.com/office/drawing/2014/main";
public const string SpreadsheetmlXmlWorksheetRelationship = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet";
public const string SpreadsheetmlXmlDrawingRelationship = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing";
public const string SpreadsheetmlXmlImageRelationship = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image";
public const string SpreadsheetmlXmlTableRelationship = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/table";
Expand Down
5 changes: 5 additions & 0 deletions src/MiniExcel.OpenXml/OpenXmlConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ public class OpenXmlConfiguration : MiniExcelBaseConfiguration
public bool EnableAutoWidth { get; set; }
public double MinWidth { get; set; } = 8.42857143;
public double MaxWidth { get; set; } = 200;

/// <summary>
/// This option sets the maximum level of nesting a property in a model passed to the <see cref="OpenXmlTemplater" /> is allowed to have
/// </summary>
public int RecursivePropertiesMaxDepth { get; set; } = 4;
}

public enum TableStyles
Expand Down
113 changes: 77 additions & 36 deletions src/MiniExcel.OpenXml/Templates/OpenXmlTemplate.Impl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -640,46 +640,16 @@ private async Task<GenerateCellValuesContext> GenerateCellValuesAsync(
? prop.Value.UnderlyingMemberType
: Nullable.GetUnderlyingType(propInfo.PropertyType) ?? propInfo.PropertyType;

string? cellValueStr;
if (type == typeof(bool))
{
cellValueStr = (bool)cellValue ? "1" : "0";
}
else if (type == typeof(DateTime))
{
cellValueStr = ConvertToDateTimeString(propInfo, cellValue);
}
else if (type?.IsEnum is true)
{
var stringValue = Enum.GetName(type, cellValue) ?? "";

var attr = type.GetField(stringValue)?.GetCustomAttribute<DescriptionAttribute>();
var description = attr?.Description ?? stringValue;

cellValueStr = XmlHelper.EncodeXml(description);
}
else
{
cellValueStr = XmlHelper.EncodeXml(cellValue?.ToString());
if (TypeHelper.IsNumericType(type))
{
if (decimal.TryParse(cellValueStr, out var decimalValue))
cellValueStr = decimalValue.ToString(CultureInfo.InvariantCulture);
}
}

// escaping formulas
var tempReplacement = cellValueStr ?? "";
var replacementValue = tempReplacement.StartsWith("$=") || tempReplacement.StartsWith("=")
? $"&apos;{tempReplacement}"
: tempReplacement;
var replacementValue = GetFormattedValue(propInfo, cellValue, type);

replacements[key] = replacementValue;
FlattenAndFormatValues(replacements, key, cellValue, _configuration.RecursivePropertiesMaxDepth, propInfo);

rowXml.Replace($"@header{{{{{key}}}}}", replacementValue);

if (isHeaderRow && row.Value.Contains(key))
{
currentHeader += cellValueStr;
currentHeader += replacementValue;
}
}

Expand Down Expand Up @@ -784,6 +754,47 @@ private async Task<GenerateCellValuesContext> GenerateCellValuesAsync(
};
}

/// <summary>
/// Formats the given cell value into a string representation suitable for OpenXml injection.
/// Handles specific types like booleans, dates, enums, and numeric values.
/// </summary>
private static string GetFormattedValue(PropertyInfo? propInfo, object? cellValue, Type? type)
{
string? cellValueStr;
if (type == typeof(bool))
{
cellValueStr = (bool)cellValue! ? "1" : "0";
}
else if (type == typeof(DateTime))
{
cellValueStr = ConvertToDateTimeString(propInfo, cellValue);
}
else if (type?.IsEnum is true)
{
// Use the DescriptionAttribute value if it exists, otherwise fallback to the enum string name.
var stringValue = Enum.GetName(type, cellValue!) ?? "";
var attr = type.GetField(stringValue)?.GetCustomAttribute<DescriptionAttribute>();
var description = attr?.Description ?? stringValue;

// Encode the final string to ensure it is safe for XML.
cellValueStr = XmlHelper.EncodeXml(description);
}
else
{
cellValueStr = XmlHelper.EncodeXml(cellValue?.ToString());
if (TypeHelper.IsNumericType(type) && decimal.TryParse(cellValueStr, out var decimalValue))
{
cellValueStr = decimalValue.ToString(CultureInfo.InvariantCulture);
}
}

// escaping formulas
var tempReplacement = cellValueStr ?? "";
return tempReplacement.StartsWith("$=") || tempReplacement.StartsWith("=")
? $"&apos;{tempReplacement}"
: tempReplacement;
}

private static void MergeCells(List<XRowInfo> xRowInfos)
{
var mergeTaggedColumns = new Dictionary<XChildNode, XChildNode>();
Expand Down Expand Up @@ -938,7 +949,7 @@ private void ProcessFormulas(StringBuilder rowXml, int rowIndex)
rowXml.Append(rowElement.FirstNode);
}

private static string? ConvertToDateTimeString(PropertyInfo? propInfo, object cellValue)
private static string? ConvertToDateTimeString(PropertyInfo? propInfo, object? cellValue)
{
//TODO:c.SetAttribute("t", "d"); and custom format
var format = propInfo?.GetAttributeValue((MiniExcelFormatAttribute x) => x.Format)
Expand Down Expand Up @@ -1204,8 +1215,38 @@ private void UpdateDimensionAndGetRowsInfo(IDictionary<string, object?> inputMap
v?.Value = v.Value.Replace($"{{{{{propNames[0]}.{propNames[1]}}}}}", "");
continue;
}

// auto check type https://github.com/mini-software/MiniExcel/issues/177
var type = prop.UnderlyingMemberType; //avoid nullable
var currentType = prop.UnderlyingMemberType;

// If the template expression exceeds two levels down the property chain to retrieve the deepest actual type.
for (int i = 2; i < propNames.Length; i++)
{
if (currentType == null)
break;

var searchType = Nullable.GetUnderlyingType(currentType) ?? currentType;

// Try to find a property first
var deepProp = searchType.GetProperty(propNames[i]);
if (deepProp != null)
{
currentType = Nullable.GetUnderlyingType(deepProp.PropertyType) ?? deepProp.PropertyType;
continue;
}

// Fallback to finding a field (for records or public fields)
if (searchType.GetField(propNames[i]) is { } deepField)
{
currentType = Nullable.GetUnderlyingType(deepField.FieldType) ?? deepField.FieldType;
continue;
}

// Break if neither property nor field is found
currentType = null;
}

var type = currentType;

if (isMultiMatch)
{
Expand Down
Loading
Loading