From fd93cea6dce3dd1ce8c7510da803cdcce9df596c Mon Sep 17 00:00:00 2001
From: swmal <897655+swmal@users.noreply.github.com>
Date: Tue, 14 Apr 2026 16:07:15 +0200
Subject: [PATCH] #2331, #2332 - fix for combining EpplusTableColumn and
Display attributes in LoadFromCollection.
---
.../EpplusTableColumnAttributeBase.cs | 27 +-
.../LoadFunctions/LoadFromCollection.cs | 2 +-
.../ReflectionHelpers/MemberPath.cs | 2 +-
.../ReflectionHelpers/SortOrderExtensions.cs | 2 +-
src/EPPlusTest/EPPlus.Test.csproj | 11 +
.../ClassWithDisplayOrderOnly.cs | 29 ++
.../ClassWithDisplayResourceType.cs | 35 +++
.../ClassWithDisplayResourceTypeOnly.cs | 30 ++
.../ClassWithEpplusHeaderAndDisplayName.cs | 30 ++
...assWithEpplusNoOrderAndDisplayWithOrder.cs | 29 ++
.../ClassWithEpplusOrderAndDisplayName.cs | 34 ++
...ssWithEpplusOrderAndDisplayWithoutOrder.cs | 34 ++
...assWithEpplusTableColumnAndDisplayOrder.cs | 45 +++
.../ClassWithNegativeEpplusOrder.cs | 33 ++
.../LoadFromCollResources.Designer.cs | 90 ++++++
.../LoadFromCollResources.resx | 129 ++++++++
.../LoadFromCollectionDisplayTests.cs | 292 ++++++++++++++++++
17 files changed, 848 insertions(+), 6 deletions(-)
create mode 100644 src/EPPlusTest/LoadFunctions/AttributesTestClasses/ClassWithDisplayOrderOnly.cs
create mode 100644 src/EPPlusTest/LoadFunctions/AttributesTestClasses/ClassWithDisplayResourceType.cs
create mode 100644 src/EPPlusTest/LoadFunctions/AttributesTestClasses/ClassWithDisplayResourceTypeOnly.cs
create mode 100644 src/EPPlusTest/LoadFunctions/AttributesTestClasses/ClassWithEpplusHeaderAndDisplayName.cs
create mode 100644 src/EPPlusTest/LoadFunctions/AttributesTestClasses/ClassWithEpplusNoOrderAndDisplayWithOrder.cs
create mode 100644 src/EPPlusTest/LoadFunctions/AttributesTestClasses/ClassWithEpplusOrderAndDisplayName.cs
create mode 100644 src/EPPlusTest/LoadFunctions/AttributesTestClasses/ClassWithEpplusOrderAndDisplayWithoutOrder.cs
create mode 100644 src/EPPlusTest/LoadFunctions/AttributesTestClasses/ClassWithEpplusTableColumnAndDisplayOrder.cs
create mode 100644 src/EPPlusTest/LoadFunctions/AttributesTestClasses/ClassWithNegativeEpplusOrder.cs
create mode 100644 src/EPPlusTest/LoadFunctions/AttributesTestClasses/LoadFromCollResources.Designer.cs
create mode 100644 src/EPPlusTest/LoadFunctions/AttributesTestClasses/LoadFromCollResources.resx
create mode 100644 src/EPPlusTest/LoadFunctions/LoadFromCollectionDisplayTests.cs
diff --git a/src/EPPlus/Attributes/EpplusTableColumnAttributeBase.cs b/src/EPPlus/Attributes/EpplusTableColumnAttributeBase.cs
index c3c86e7629..c1895936c4 100644
--- a/src/EPPlus/Attributes/EpplusTableColumnAttributeBase.cs
+++ b/src/EPPlus/Attributes/EpplusTableColumnAttributeBase.cs
@@ -24,14 +24,35 @@ namespace OfficeOpenXml.Attributes
public abstract class EpplusTableColumnAttributeBase : Attribute
{
+ private int _order = int.MaxValue;
+ private bool _orderIsSet = false;
+
///
/// Order of the columns value, default value is 0
///
public int Order
{
- get;
- set;
- } = int.MaxValue;
+ get
+ {
+ return _order;
+ }
+ set
+ {
+ _order = value;
+ _orderIsSet = true;
+ }
+ }
+
+ ///
+ /// Returns true if the Order property has been explicitly set.
+ ///
+ internal bool OrderIsSet
+ {
+ get
+ {
+ return _orderIsSet;
+ }
+ }
///
/// Name shown in the header row, overriding the property name
diff --git a/src/EPPlus/LoadFunctions/LoadFromCollection.cs b/src/EPPlus/LoadFunctions/LoadFromCollection.cs
index 5777a9090a..49be91997f 100644
--- a/src/EPPlus/LoadFunctions/LoadFromCollection.cs
+++ b/src/EPPlus/LoadFunctions/LoadFromCollection.cs
@@ -389,7 +389,7 @@ private string GetHeaderFromDotNetAttributes(MemberInfo member)
var displayAttribute = member.GetFirstAttributeOfType();
if (displayAttribute != null)
{
- return displayAttribute.Name;
+ return displayAttribute.GetName();
}
#endif
return default;
diff --git a/src/EPPlus/LoadFunctions/ReflectionHelpers/MemberPath.cs b/src/EPPlus/LoadFunctions/ReflectionHelpers/MemberPath.cs
index a1e2548a2c..5247744745 100644
--- a/src/EPPlus/LoadFunctions/ReflectionHelpers/MemberPath.cs
+++ b/src/EPPlus/LoadFunctions/ReflectionHelpers/MemberPath.cs
@@ -114,7 +114,7 @@ public override string GetHeader()
#if !NET35
else if (last.Member.HasAttributeOfType(out DisplayAttribute displayAttr))
{
- header = displayAttr.Name;
+ header = displayAttr.GetName();
}
#endif
if (string.IsNullOrEmpty(header))
diff --git a/src/EPPlus/LoadFunctions/ReflectionHelpers/SortOrderExtensions.cs b/src/EPPlus/LoadFunctions/ReflectionHelpers/SortOrderExtensions.cs
index d33c9fc19f..906c4b88e8 100644
--- a/src/EPPlus/LoadFunctions/ReflectionHelpers/SortOrderExtensions.cs
+++ b/src/EPPlus/LoadFunctions/ReflectionHelpers/SortOrderExtensions.cs
@@ -52,7 +52,7 @@ public static int GetSortOrder(this MemberInfo member, List filterMe
{
sortOrder = entcAttr.Order;
}
- else if(member.HasAttributeOfType(out EpplusTableColumnAttribute etcAttr))
+ else if (member.HasAttributeOfType(out EpplusTableColumnAttribute etcAttr) && etcAttr.OrderIsSet)
{
sortOrder = etcAttr.Order;
}
diff --git a/src/EPPlusTest/EPPlus.Test.csproj b/src/EPPlusTest/EPPlus.Test.csproj
index a1cdb5ed95..f0e6dced4e 100644
--- a/src/EPPlusTest/EPPlus.Test.csproj
+++ b/src/EPPlusTest/EPPlus.Test.csproj
@@ -50,6 +50,11 @@
+
+ True
+ True
+ LoadFromCollResources.resx
+
@@ -272,6 +277,12 @@
+
+
+ PublicResXFileCodeGenerator
+ LoadFromCollResources.Designer.cs
+
+
3.1
diff --git a/src/EPPlusTest/LoadFunctions/AttributesTestClasses/ClassWithDisplayOrderOnly.cs b/src/EPPlusTest/LoadFunctions/AttributesTestClasses/ClassWithDisplayOrderOnly.cs
new file mode 100644
index 0000000000..947fee0f12
--- /dev/null
+++ b/src/EPPlusTest/LoadFunctions/AttributesTestClasses/ClassWithDisplayOrderOnly.cs
@@ -0,0 +1,29 @@
+/*************************************************************************************************
+ Required Notice: Copyright (C) EPPlus Software AB.
+ This software is licensed under PolyForm Noncommercial License 1.0.0
+ and may only be used for noncommercial purposes
+ https://polyformproject.org/licenses/noncommercial/1.0.0/
+ A commercial license to use this software can be purchased at https://epplussoftware.com
+ *************************************************************************************************/
+#if !NET35
+using System.ComponentModel.DataAnnotations;
+
+namespace EPPlusTest.LoadFunctions.AttributesTestClasses
+{
+ ///
+ /// Test class where only DisplayAttribute is present (no EpplusTableColumnAttribute).
+ /// DisplayAttribute.Order should be used in this case.
+ ///
+ public class ClassWithDisplayOrderOnly
+ {
+ [Display(Name = "Id Column", Order = 3)]
+ public int Id { get; set; }
+
+ [Display(Name = "Name Column", Order = 1)]
+ public string Name { get; set; }
+
+ [Display(Name = "Description Column", Order = 2)]
+ public string Description { get; set; }
+ }
+}
+#endif
\ No newline at end of file
diff --git a/src/EPPlusTest/LoadFunctions/AttributesTestClasses/ClassWithDisplayResourceType.cs b/src/EPPlusTest/LoadFunctions/AttributesTestClasses/ClassWithDisplayResourceType.cs
new file mode 100644
index 0000000000..3359d4bd87
--- /dev/null
+++ b/src/EPPlusTest/LoadFunctions/AttributesTestClasses/ClassWithDisplayResourceType.cs
@@ -0,0 +1,35 @@
+/*************************************************************************************************
+ Required Notice: Copyright (C) EPPlus Software AB.
+ This software is licensed under PolyForm Noncommercial License 1.0.0
+ and may only be used for noncommercial purposes
+ https://polyformproject.org/licenses/noncommercial/1.0.0/
+
+ A commercial license to use this software can be purchased at https://epplussoftware.com
+ *************************************************************************************************/
+using OfficeOpenXml.Attributes;
+#if !NET35
+using System.ComponentModel.DataAnnotations;
+
+namespace EPPlusTest.LoadFunctions.AttributesTestClasses
+{
+ ///
+ /// Test class using DisplayAttribute with ResourceType.
+ /// When ResourceType is set, GetName() should be used instead of Name
+ /// to get the localized value from the resource class.
+ ///
+ public class ClassWithDisplayResourceType
+ {
+ [EpplusTableColumn(Order = 1)]
+ [Display(Name = "IdHeader", ResourceType = typeof(LoadFromCollResources))]
+ public int Id { get; set; }
+
+ [EpplusTableColumn(Order = 2)]
+ [Display(Name = "NameHeader", ResourceType = typeof(LoadFromCollResources))]
+ public string Name { get; set; }
+
+ [EpplusTableColumn(Order = 3)]
+ [Display(Name = "DescriptionHeader", ResourceType = typeof(LoadFromCollResources))]
+ public string Description { get; set; }
+ }
+}
+#endif
\ No newline at end of file
diff --git a/src/EPPlusTest/LoadFunctions/AttributesTestClasses/ClassWithDisplayResourceTypeOnly.cs b/src/EPPlusTest/LoadFunctions/AttributesTestClasses/ClassWithDisplayResourceTypeOnly.cs
new file mode 100644
index 0000000000..bf77ddbf75
--- /dev/null
+++ b/src/EPPlusTest/LoadFunctions/AttributesTestClasses/ClassWithDisplayResourceTypeOnly.cs
@@ -0,0 +1,30 @@
+/*************************************************************************************************
+ Required Notice: Copyright (C) EPPlus Software AB.
+ This software is licensed under PolyForm Noncommercial License 1.0.0
+ and may only be used for noncommercial purposes
+ https://polyformproject.org/licenses/noncommercial/1.0.0/
+
+ A commercial license to use this software can be purchased at https://epplussoftware.com
+ *************************************************************************************************/
+#if !NET35
+using System.ComponentModel.DataAnnotations;
+
+namespace EPPlusTest.LoadFunctions.AttributesTestClasses
+{
+ ///
+ /// Test class using DisplayAttribute with ResourceType but without EpplusTableColumnAttribute.
+ /// GetName() should return the localized value from the resource class.
+ ///
+ public class ClassWithDisplayResourceTypeOnly
+ {
+ [Display(Name = "IdHeader", ResourceType = typeof(LoadFromCollResources), Order = 1)]
+ public int Id { get; set; }
+
+ [Display(Name = "NameHeader", ResourceType = typeof(LoadFromCollResources), Order = 2)]
+ public string Name { get; set; }
+
+ [Display(Name = "DescriptionHeader", ResourceType = typeof(LoadFromCollResources), Order = 3)]
+ public string Description { get; set; }
+ }
+}
+#endif
\ No newline at end of file
diff --git a/src/EPPlusTest/LoadFunctions/AttributesTestClasses/ClassWithEpplusHeaderAndDisplayName.cs b/src/EPPlusTest/LoadFunctions/AttributesTestClasses/ClassWithEpplusHeaderAndDisplayName.cs
new file mode 100644
index 0000000000..c2b35d9209
--- /dev/null
+++ b/src/EPPlusTest/LoadFunctions/AttributesTestClasses/ClassWithEpplusHeaderAndDisplayName.cs
@@ -0,0 +1,30 @@
+/*************************************************************************************************
+ Required Notice: Copyright (C) EPPlus Software AB.
+ This software is licensed under PolyForm Noncommercial License 1.0.0
+ and may only be used for noncommercial purposes
+ https://polyformproject.org/licenses/noncommercial/1.0.0/
+
+ A commercial license to use this software can be purchased at https://epplussoftware.com
+ *************************************************************************************************/
+using OfficeOpenXml.Attributes;
+#if !NET35
+using System.ComponentModel.DataAnnotations;
+
+namespace EPPlusTest.LoadFunctions.AttributesTestClasses
+{
+ ///
+ /// Test class where EpplusTableColumnAttribute.Header IS set.
+ /// It should take precedence over DisplayAttribute.Name.
+ ///
+ public class ClassWithEpplusHeaderAndDisplayName
+ {
+ [EpplusTableColumn(Order = 1, Header = "EPPlus Id")]
+ [Display(Name = "Display Id")]
+ public int Id { get; set; }
+
+ [EpplusTableColumn(Order = 2, Header = "EPPlus Name")]
+ [Display(Name = "Display Name")]
+ public string Name { get; set; }
+ }
+}
+#endif
\ No newline at end of file
diff --git a/src/EPPlusTest/LoadFunctions/AttributesTestClasses/ClassWithEpplusNoOrderAndDisplayWithOrder.cs b/src/EPPlusTest/LoadFunctions/AttributesTestClasses/ClassWithEpplusNoOrderAndDisplayWithOrder.cs
new file mode 100644
index 0000000000..ae38b75bb7
--- /dev/null
+++ b/src/EPPlusTest/LoadFunctions/AttributesTestClasses/ClassWithEpplusNoOrderAndDisplayWithOrder.cs
@@ -0,0 +1,29 @@
+using OfficeOpenXml.Attributes;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace EPPlusTest.LoadFunctions.AttributesTestClasses
+{
+ ///
+ /// Test class where EpplusTableColumnAttribute exists but Order is NOT set.
+ /// Should fall back to DisplayAttribute.Order.
+ ///
+ public class ClassWithEpplusNoOrderAndDisplayWithOrder
+ {
+ [EpplusTableColumn(NumberFormat = "0")]
+ [Display(Name = "Id Column", Order = 3)]
+ public int Id { get; set; }
+
+ [EpplusTableColumn]
+ [Display(Name = "Name Column", Order = 1)]
+ public string Name { get; set; }
+
+ [EpplusTableColumn]
+ [Display(Name = "Description Column", Order = 2)]
+ public string Description { get; set; }
+ }
+}
diff --git a/src/EPPlusTest/LoadFunctions/AttributesTestClasses/ClassWithEpplusOrderAndDisplayName.cs b/src/EPPlusTest/LoadFunctions/AttributesTestClasses/ClassWithEpplusOrderAndDisplayName.cs
new file mode 100644
index 0000000000..13d294f45b
--- /dev/null
+++ b/src/EPPlusTest/LoadFunctions/AttributesTestClasses/ClassWithEpplusOrderAndDisplayName.cs
@@ -0,0 +1,34 @@
+/*************************************************************************************************
+ Required Notice: Copyright (C) EPPlus Software AB.
+ This software is licensed under PolyForm Noncommercial License 1.0.0
+ and may only be used for noncommercial purposes
+ https://polyformproject.org/licenses/noncommercial/1.0.0/
+
+ A commercial license to use this software can be purchased at https://epplussoftware.com
+ *************************************************************************************************/
+using OfficeOpenXml.Attributes;
+#if !NET35
+using System.ComponentModel.DataAnnotations;
+
+namespace EPPlusTest.LoadFunctions.AttributesTestClasses
+{
+ ///
+ /// Test class where EpplusTableColumnAttribute has Order set but NOT Header.
+ /// Header should fall back to DisplayAttribute.GetName().
+ ///
+ public class ClassWithEpplusOrderAndDisplayName
+ {
+ [EpplusTableColumn(Order = 3)]
+ [Display(Name = "The Id")]
+ public int Id { get; set; }
+
+ [EpplusTableColumn(Order = 1)]
+ [Display(Name = "The Name")]
+ public string Name { get; set; }
+
+ [EpplusTableColumn(Order = 2)]
+ [Display(Name = "The Description")]
+ public string Description { get; set; }
+ }
+}
+#endif
\ No newline at end of file
diff --git a/src/EPPlusTest/LoadFunctions/AttributesTestClasses/ClassWithEpplusOrderAndDisplayWithoutOrder.cs b/src/EPPlusTest/LoadFunctions/AttributesTestClasses/ClassWithEpplusOrderAndDisplayWithoutOrder.cs
new file mode 100644
index 0000000000..28c767fe3c
--- /dev/null
+++ b/src/EPPlusTest/LoadFunctions/AttributesTestClasses/ClassWithEpplusOrderAndDisplayWithoutOrder.cs
@@ -0,0 +1,34 @@
+/*************************************************************************************************
+ Required Notice: Copyright (C) EPPlus Software AB.
+ This software is licensed under PolyForm Noncommercial License 1.0.0
+ and may only be used for noncommercial purposes
+ https://polyformproject.org/licenses/noncommercial/1.0.0/
+ A commercial license to use this software can be purchased at https://epplussoftware.com
+ *************************************************************************************************/
+using OfficeOpenXml.Attributes;
+#if !NET35
+using System.ComponentModel.DataAnnotations;
+
+namespace EPPlusTest.LoadFunctions.AttributesTestClasses
+{
+ ///
+ /// Test class where EpplusTableColumnAttribute has Order set but
+ /// DisplayAttribute does NOT have Order set.
+ /// EpplusTableColumnAttribute.Order should be used.
+ ///
+ public class ClassWithEpplusOrderAndDisplayWithoutOrder
+ {
+ [EpplusTableColumn(Order = 3)]
+ [Display(Name = "Id Column")]
+ public int Id { get; set; }
+
+ [EpplusTableColumn(Order = 1)]
+ [Display(Name = "Name Column")]
+ public string Name { get; set; }
+
+ [EpplusTableColumn(Order = 2)]
+ [Display(Name = "Description Column")]
+ public string Description { get; set; }
+ }
+}
+#endif
\ No newline at end of file
diff --git a/src/EPPlusTest/LoadFunctions/AttributesTestClasses/ClassWithEpplusTableColumnAndDisplayOrder.cs b/src/EPPlusTest/LoadFunctions/AttributesTestClasses/ClassWithEpplusTableColumnAndDisplayOrder.cs
new file mode 100644
index 0000000000..a642e1afca
--- /dev/null
+++ b/src/EPPlusTest/LoadFunctions/AttributesTestClasses/ClassWithEpplusTableColumnAndDisplayOrder.cs
@@ -0,0 +1,45 @@
+/*************************************************************************************************
+ Required Notice: Copyright (C) EPPlus Software AB.
+ This software is licensed under PolyForm Noncommercial License 1.0.0
+ and may only be used for noncommercial purposes
+ https://polyformproject.org/licenses/noncommercial/1.0.0/
+
+ A commercial license to use this software can be purchased at https://epplussoftware.com
+ *************************************************************************************************/
+using OfficeOpenXml.Attributes;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+#if !NET35
+using System.ComponentModel.DataAnnotations;
+#endif
+
+namespace EPPlusTest.LoadFunctions.AttributesTestClasses
+{
+#if !NET35
+ ///
+ /// Test class where EpplusTableColumnAttribute.Order should take precedence
+ /// over DisplayAttribute.Order when both are present on the same property.
+ ///
+ public class ClassWithEpplusTableColumnAndDisplayOrder
+ {
+ // EpplusTableColumn Order = 3, Display Order = 1
+ // Expected column order should be 3 (EpplusTableColumn wins)
+ [EpplusTableColumn(Order = 3)]
+ [Display(Name = "Id Column", Order = 1)]
+ public int Id { get; set; }
+
+ // EpplusTableColumn Order = 1, Display Order = 3
+ // Expected column order should be 1 (EpplusTableColumn wins)
+ [EpplusTableColumn(Order = 1)]
+ [Display(Name = "Name Column", Order = 3)]
+ public string Name { get; set; }
+
+ // EpplusTableColumn Order = 2, Display Order = 2
+ [EpplusTableColumn(Order = 2)]
+ [Display(Name = "Description Column", Order = 2)]
+ public string Description { get; set; }
+ }
+#endif
+}
\ No newline at end of file
diff --git a/src/EPPlusTest/LoadFunctions/AttributesTestClasses/ClassWithNegativeEpplusOrder.cs b/src/EPPlusTest/LoadFunctions/AttributesTestClasses/ClassWithNegativeEpplusOrder.cs
new file mode 100644
index 0000000000..9d8c3d6b3c
--- /dev/null
+++ b/src/EPPlusTest/LoadFunctions/AttributesTestClasses/ClassWithNegativeEpplusOrder.cs
@@ -0,0 +1,33 @@
+/*************************************************************************************************
+ Required Notice: Copyright (C) EPPlus Software AB.
+ This software is licensed under PolyForm Noncommercial License 1.0.0
+ and may only be used for noncommercial purposes
+ https://polyformproject.org/licenses/noncommercial/1.0.0/
+ A commercial license to use this software can be purchased at https://epplussoftware.com
+ *************************************************************************************************/
+using OfficeOpenXml.Attributes;
+#if !NET35
+using System.ComponentModel.DataAnnotations;
+
+namespace EPPlusTest.LoadFunctions.AttributesTestClasses
+{
+ ///
+ /// Test class with a negative Order value on EpplusTableColumnAttribute,
+ /// matching the customer's scenario (Order = -90).
+ ///
+ public class ClassWithNegativeEpplusOrder
+ {
+ [EpplusTableColumn(Order = -90)]
+ [Display(Name = "NumRegistro", Order = 5)]
+ public int? NumRegistro { get; set; }
+
+ [EpplusTableColumn(Order = 1)]
+ [Display(Name = "Nombre", Order = 1)]
+ public string Nombre { get; set; }
+
+ [EpplusTableColumn(Order = 2)]
+ [Display(Name = "Descripcion", Order = 2)]
+ public string Descripcion { get; set; }
+ }
+}
+#endif
\ No newline at end of file
diff --git a/src/EPPlusTest/LoadFunctions/AttributesTestClasses/LoadFromCollResources.Designer.cs b/src/EPPlusTest/LoadFunctions/AttributesTestClasses/LoadFromCollResources.Designer.cs
new file mode 100644
index 0000000000..19fce98d98
--- /dev/null
+++ b/src/EPPlusTest/LoadFunctions/AttributesTestClasses/LoadFromCollResources.Designer.cs
@@ -0,0 +1,90 @@
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.42000
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+namespace EPPlusTest.LoadFunctions.AttributesTestClasses {
+ using System;
+
+
+ ///
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ ///
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "18.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ public class LoadFromCollResources {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal LoadFromCollResources() {
+ }
+
+ ///
+ /// Returns the cached ResourceManager instance used by this class.
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ public static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("EPPlusTest.LoadFunctions.AttributesTestClasses.LoadFromCollResources", typeof(LoadFromCollResources).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ ///
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ public static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Item Description.
+ ///
+ public static string DescriptionHeader {
+ get {
+ return ResourceManager.GetString("DescriptionHeader", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Identifier.
+ ///
+ public static string IdHeader {
+ get {
+ return ResourceManager.GetString("IdHeader", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Full Name.
+ ///
+ public static string NameHeader {
+ get {
+ return ResourceManager.GetString("NameHeader", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/src/EPPlusTest/LoadFunctions/AttributesTestClasses/LoadFromCollResources.resx b/src/EPPlusTest/LoadFunctions/AttributesTestClasses/LoadFromCollResources.resx
new file mode 100644
index 0000000000..ccca8c2c1f
--- /dev/null
+++ b/src/EPPlusTest/LoadFunctions/AttributesTestClasses/LoadFromCollResources.resx
@@ -0,0 +1,129 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ Item Description
+
+
+ Identifier
+
+
+ Full Name
+
+
\ No newline at end of file
diff --git a/src/EPPlusTest/LoadFunctions/LoadFromCollectionDisplayTests.cs b/src/EPPlusTest/LoadFunctions/LoadFromCollectionDisplayTests.cs
new file mode 100644
index 0000000000..a3f6783b2c
--- /dev/null
+++ b/src/EPPlusTest/LoadFunctions/LoadFromCollectionDisplayTests.cs
@@ -0,0 +1,292 @@
+/*************************************************************************************************
+ Required Notice: Copyright (C) EPPlus Software AB.
+ This software is licensed under PolyForm Noncommercial License 1.0.0
+ and may only be used for noncommercial purposes
+ https://polyformproject.org/licenses/noncommercial/1.0.0/
+
+ A commercial license to use this software can be purchased at https://epplussoftware.com
+ *************************************************************************************************/
+#if !NET35
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using OfficeOpenXml;
+using EPPlusTest.LoadFunctions.AttributesTestClasses;
+using System.Collections.Generic;
+
+namespace EPPlusTest.LoadFunctions
+{
+ [TestClass]
+ public class LoadFromCollectionDisplayTests
+ {
+ private ExcelPackage _package;
+ private ExcelWorksheet _sheet;
+
+ [TestInitialize]
+ public void Initialize()
+ {
+ _package = new ExcelPackage();
+ _sheet = _package.Workbook.Worksheets.Add("test");
+ }
+
+ [TestCleanup]
+ public void Cleanup()
+ {
+ _package.Dispose();
+ }
+
+ #region Order precedence tests
+
+ [TestMethod]
+ public void ShouldUseEpplusTableColumnOrderOverDisplayOrder()
+ {
+ // Arrange
+ // EpplusTableColumn Order: Id=3, Name=1, Description=2
+ // Display Order: Id=1, Name=3, Description=2
+ // Expected: EpplusTableColumn.Order wins
+ var items = new List
+ {
+ new ClassWithEpplusTableColumnAndDisplayOrder
+ {
+ Id = 1,
+ Name = "Test",
+ Description = "A test"
+ }
+ };
+
+ // Act
+ _sheet.Cells["A1"].LoadFromCollection(items, true);
+
+ // Assert - column order should follow EpplusTableColumnAttribute.Order
+ Assert.AreEqual("Name Column", _sheet.Cells["A1"].Value, "First column should be Name (EpplusTableColumn Order=1)");
+ Assert.AreEqual("Description Column", _sheet.Cells["B1"].Value, "Second column should be Description (EpplusTableColumn Order=2)");
+ Assert.AreEqual("Id Column", _sheet.Cells["C1"].Value, "Third column should be Id (EpplusTableColumn Order=3)");
+ }
+
+ [TestMethod]
+ public void ShouldUseEpplusTableColumnOrderWithNegativeValues()
+ {
+ // Arrange - matches the customer scenario with Order = -90
+ // EpplusTableColumn Order: NumRegistro=-90, Nombre=1, Descripcion=2
+ // Display Order: NumRegistro=5, Nombre=1, Descripcion=2
+ // Expected: EpplusTableColumn.Order wins, NumRegistro first
+ var items = new List
+ {
+ new ClassWithNegativeEpplusOrder
+ {
+ NumRegistro = 42,
+ Nombre = "Test",
+ Descripcion = "A test"
+ }
+ };
+
+ // Act
+ _sheet.Cells["A1"].LoadFromCollection(items, true);
+
+ // Assert
+ Assert.AreEqual("NumRegistro", _sheet.Cells["A1"].Value, "First column should be NumRegistro (EpplusTableColumn Order=-90)");
+ Assert.AreEqual("Nombre", _sheet.Cells["B1"].Value, "Second column should be Nombre (EpplusTableColumn Order=1)");
+ Assert.AreEqual("Descripcion", _sheet.Cells["C1"].Value, "Third column should be Descripcion (EpplusTableColumn Order=2)");
+ }
+
+ [TestMethod]
+ public void ShouldUseEpplusTableColumnOrderWithNegativeValues_DataOrder()
+ {
+ // Arrange - verify that the data row also follows the correct column order
+ var items = new List
+ {
+ new ClassWithNegativeEpplusOrder
+ {
+ NumRegistro = 42,
+ Nombre = "Test",
+ Descripcion = "A test"
+ }
+ };
+
+ // Act
+ _sheet.Cells["A1"].LoadFromCollection(items, true);
+
+ // Assert - row 2 is data
+ Assert.AreEqual(42, _sheet.Cells["A2"].Value, "First data column should be NumRegistro");
+ Assert.AreEqual("Test", _sheet.Cells["B2"].Value, "Second data column should be Nombre");
+ Assert.AreEqual("A test", _sheet.Cells["C2"].Value, "Third data column should be Descripcion");
+ }
+
+ [TestMethod]
+ public void ShouldFallBackToDisplayOrderWhenEpplusOrderNotSet()
+ {
+ // Arrange
+ // EpplusTableColumn exists but Order is NOT explicitly set
+ // Display Order: Id=3, Name=1, Description=2
+ // Expected: falls back to DisplayAttribute.Order
+ var items = new List
+ {
+ new ClassWithEpplusNoOrderAndDisplayWithOrder
+ {
+ Id = 1,
+ Name = "Test",
+ Description = "A test"
+ }
+ };
+
+ // Act
+ _sheet.Cells["A1"].LoadFromCollection(items, true);
+
+ // Assert - should follow DisplayAttribute.Order as fallback
+ Assert.AreEqual("Name Column", _sheet.Cells["A1"].Value, "First column should be Name (Display Order=1)");
+ Assert.AreEqual("Description Column", _sheet.Cells["B1"].Value, "Second column should be Description (Display Order=2)");
+ Assert.AreEqual("Id Column", _sheet.Cells["C1"].Value, "Third column should be Id (Display Order=3)");
+ }
+
+ [TestMethod]
+ public void ShouldUseDisplayOrderWhenNoEpplusTableColumnAttribute()
+ {
+ // Arrange - only DisplayAttribute present
+ // Display Order: Id=3, Name=1, Description=2
+ var items = new List
+ {
+ new ClassWithDisplayOrderOnly
+ {
+ Id = 1,
+ Name = "Test",
+ Description = "A test"
+ }
+ };
+
+ // Act
+ _sheet.Cells["A1"].LoadFromCollection(items, true);
+
+ // Assert
+ Assert.AreEqual("Name Column", _sheet.Cells["A1"].Value, "First column should be Name (Display Order=1)");
+ Assert.AreEqual("Description Column", _sheet.Cells["B1"].Value, "Second column should be Description (Display Order=2)");
+ Assert.AreEqual("Id Column", _sheet.Cells["C1"].Value, "Third column should be Id (Display Order=3)");
+ }
+
+ #endregion
+
+ #region Header precedence tests
+
+ [TestMethod]
+ public void ShouldUseEpplusHeaderOverDisplayName()
+ {
+ // Arrange - both EpplusTableColumn.Header and Display.Name are set
+ // Expected: EpplusTableColumn.Header wins
+ var items = new List
+ {
+ new ClassWithEpplusHeaderAndDisplayName
+ {
+ Id = 1,
+ Name = "Test"
+ }
+ };
+
+ // Act
+ _sheet.Cells["A1"].LoadFromCollection(items, true);
+
+ // Assert
+ Assert.AreEqual("EPPlus Id", _sheet.Cells["A1"].Value, "Header should come from EpplusTableColumn.Header");
+ Assert.AreEqual("EPPlus Name", _sheet.Cells["B1"].Value, "Header should come from EpplusTableColumn.Header");
+ }
+
+ [TestMethod]
+ public void ShouldFallBackToDisplayNameWhenEpplusHeaderNotSet()
+ {
+ // Arrange - EpplusTableColumn has Order but NOT Header
+ // Display has Name set
+ // Expected: falls back to DisplayAttribute.GetName()
+ var items = new List
+ {
+ new ClassWithEpplusOrderAndDisplayName
+ {
+ Id = 1,
+ Name = "Test",
+ Description = "A test"
+ }
+ };
+
+ // Act
+ _sheet.Cells["A1"].LoadFromCollection(items, true);
+
+ // Assert - headers should come from Display.Name, order from EpplusTableColumn.Order
+ Assert.AreEqual("The Name", _sheet.Cells["A1"].Value, "Header should fall back to Display.Name");
+ Assert.AreEqual("The Description", _sheet.Cells["B1"].Value, "Header should fall back to Display.Name");
+ Assert.AreEqual("The Id", _sheet.Cells["C1"].Value, "Header should fall back to Display.Name");
+ }
+
+ [TestMethod]
+ public void ShouldUseDisplayNameAsHeaderWhenNoEpplusAttribute()
+ {
+ // Arrange - only DisplayAttribute present
+ var items = new List
+ {
+ new ClassWithDisplayOrderOnly
+ {
+ Id = 1,
+ Name = "Test",
+ Description = "A test"
+ }
+ };
+
+ // Act
+ _sheet.Cells["A1"].LoadFromCollection(items, true);
+
+ // Assert
+ Assert.AreEqual("Name Column", _sheet.Cells["A1"].Value);
+ Assert.AreEqual("Description Column", _sheet.Cells["B1"].Value);
+ Assert.AreEqual("Id Column", _sheet.Cells["C1"].Value);
+ }
+
+ [TestMethod]
+ public void ShouldUseGetNameForDisplayAttributeWithResourceType()
+ {
+ // Arrange - DisplayAttribute uses ResourceType for localization
+ // GetName() should return the localized value from the resource class,
+ // not the raw Name property (which is just the resource key).
+ // Display Name="IdHeader" with ResourceType=TestDisplayResources
+ // -> GetName() returns "Identifier"
+ // -> Name returns "IdHeader"
+ var items = new List
+ {
+ new ClassWithDisplayResourceType
+ {
+ Id = 1,
+ Name = "Test",
+ Description = "A test"
+ }
+ };
+
+ // Act
+ _sheet.Cells["A1"].LoadFromCollection(items, true);
+
+ // Assert - headers should be the localized values from TestDisplayResources
+ Assert.AreEqual("Identifier", _sheet.Cells["A1"].Value, "Header should be localized value from resource class via GetName()");
+ Assert.AreEqual("Full Name", _sheet.Cells["B1"].Value, "Header should be localized value from resource class via GetName()");
+ Assert.AreEqual("Item Description", _sheet.Cells["C1"].Value, "Header should be localized value from resource class via GetName()");
+ }
+
+ [TestMethod]
+ public void ShouldUseGetNameForDisplayAttributeWithResourceType_NoEpplusAttribute()
+ {
+ // Arrange - same as above but verify the resource type pattern also works
+ // when no EpplusTableColumnAttribute is present (regression test)
+ var items = new List
+ {
+ new ClassWithDisplayResourceTypeOnly
+ {
+ Id = 1,
+ Name = "Test",
+ Description = "A test"
+ }
+ };
+
+ // Act
+ _sheet.Cells["A1"].LoadFromCollection(items, true);
+
+ // Assert
+ Assert.AreEqual("Identifier", _sheet.Cells["A1"].Value, "Header should be localized value from resource class via GetName()");
+ Assert.AreEqual("Full Name", _sheet.Cells["B1"].Value, "Header should be localized value from resource class via GetName()");
+ Assert.AreEqual("Item Description", _sheet.Cells["C1"].Value, "Header should be localized value from resource class via GetName()");
+ }
+
+ #endregion
+ }
+}
+#endif
\ No newline at end of file