Skip to content

Commit

Permalink
#1058, #969 - LoadFromCollection, support for Dictionary property
Browse files Browse the repository at this point in the history
  • Loading branch information
swmal committed Nov 22, 2023
1 parent a7c1806 commit 64a52a0
Show file tree
Hide file tree
Showing 7 changed files with 149 additions and 84 deletions.
6 changes: 5 additions & 1 deletion src/EPPlus/Attributes/EPPlusDictionaryColumnAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ public int Order
/// </summary>
public string[] ColumnHeaders { get; set; }

public string HeadersKey { get; set; }
/// <summary>
/// Should be unique within all attributes. Will be used to retrieve the keys of the Dictionary
/// that also will be used to create the columns for this property.
/// </summary>
public string KeyId { get; set; }
}
}
34 changes: 0 additions & 34 deletions src/EPPlus/LoadFunctions/IDictionaryKeysProvider.cs

This file was deleted.

47 changes: 31 additions & 16 deletions src/EPPlus/LoadFunctions/LoadFromCollection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -201,21 +201,22 @@ private void SetValuesAndFormulas(object[,] values, Dictionary<int, FormulaCell>
{
if(!string.IsNullOrEmpty(colInfo.Path) && colInfo.Path.Contains("."))
{
if(colInfo.IsDictionaryProperty)
{
var dict = GetValueByPath(item, colInfo.Path) as Dictionary<string, object>;
if(dict != null)
{
if (dict.ContainsKey(colInfo.DictinaryKey))
{
values[row, col++] = dict[colInfo.DictinaryKey];
}
}
}
else
{
values[row, col++] = GetValueByPath(item, colInfo.Path);
}
//if(colInfo.IsDictionaryProperty)
//{
// var dict = GetValueByPath(item, colInfo.Path) as Dictionary<string, object>;
// if(dict != null)
// {
// if (dict.ContainsKey(colInfo.DictinaryKey))
// {
// values[row, col++] = dict[colInfo.DictinaryKey];
// }
// }
//}
//else
//{
// values[row, col++] = GetValueByPath(item, colInfo.Path);
//}
values[row, col++] = GetValueByPath(item, colInfo.Path);
continue;
}
var obj = item;
Expand Down Expand Up @@ -297,8 +298,9 @@ private object GetValueByPath(object obj, string path)
{
var members = path.Split('.');
object o = obj;
foreach(var member in members)
for(var ix = 0; ix < members.Length; ix++)
{
var member = members[ix];
if (o == null) return null;
var memberInfos = o.GetType().GetMember(member);
if(memberInfos == null || memberInfos.Length == 0)
Expand All @@ -322,6 +324,19 @@ private object GetValueByPath(object obj, string path)
{
throw new NotSupportedException("Invalid member: '" + memberInfo.Name + "', not supported member type '" + memberInfo.GetType().FullName + "'");
}
if(o is Dictionary<string, object> dict && ix < members.Length + 1)
{
var key = members[ix + 1];
if(dict.ContainsKey(key))
{
o = dict[key];
}
else
{
o = null;
}
break;
}
}
return o;
}
Expand Down
14 changes: 9 additions & 5 deletions src/EPPlus/LoadFunctions/LoadFromCollectionColumns.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,10 @@ internal class LoadFromCollectionColumns<T>

public LoadFromCollectionColumns(LoadFromCollectionParams parameters, List<string> sortOrderColumns)
{
_params = parameters;
_bindingFlags = parameters.BindingFlags;
_sortOrderColumns = sortOrderColumns;
_filterMembers = parameters.Members;
_keysProvider = parameters.KeysProvider;
_includedTypes = new HashSet<Type>
{
typeof(T)
Expand All @@ -50,10 +50,10 @@ public LoadFromCollectionColumns(LoadFromCollectionParams parameters, List<strin
}


private readonly LoadFromCollectionParams _params;
private readonly BindingFlags _bindingFlags;
private readonly List<string> _sortOrderColumns;
private readonly Dictionary<Type, HashSet<string>> _members;
private readonly IDictionaryKeysProvider _keysProvider;
private MemberInfo[] _filterMembers;
private readonly HashSet<Type> _includedTypes;
private const int SortOrderOffset = ExcelPackage.MaxColumns;
Expand Down Expand Up @@ -243,14 +243,18 @@ private void HandleDictionaryColumnsAttribute(List<ColumnInfo> result, MemberInf
var sortOrderColumnsIndex = _sortOrderColumns != null ? _sortOrderColumns.IndexOf(memberPath) : -1;
var so = sortOrderColumnsIndex > -1 ? sortOrderColumnsIndex : attr.Order + SortOrderOffset;
var columnHeaders = Enumerable.Empty<string>();
if(!string.IsNullOrEmpty(attr.HeadersKey) && _keysProvider != null)
if(!string.IsNullOrEmpty(attr.KeyId))
{
columnHeaders = _keysProvider.GetKeys(attr.HeadersKey);
columnHeaders = _params.GetDictionaryKeys(attr.KeyId);
}
else if(attr.ColumnHeaders != null && attr.ColumnHeaders.Length > 0)
{
columnHeaders = attr.ColumnHeaders;
}
else
{
columnHeaders = _params.GetDefaultDictionaryKeys();
}
foreach (var key in columnHeaders)
{
result.Add(new ColumnInfo
Expand All @@ -259,7 +263,7 @@ private void HandleDictionaryColumnsAttribute(List<ColumnInfo> result, MemberInf
MemberInfo = member,
IsDictionaryProperty = true,
DictinaryKey = key,
Path = memberPath,
Path = $"{memberPath}.{key}",
Header = key,
SortOrder = so
});
Expand Down
53 changes: 53 additions & 0 deletions src/EPPlus/LoadFunctions/Params/LoadFromCollectionParams.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@
*************************************************************************************************
07/16/2020 EPPlus Software AB EPPlus 5.2.1
*************************************************************************************************/
using OfficeOpenXml.Attributes;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;

Expand All @@ -22,6 +24,8 @@ namespace OfficeOpenXml.LoadFunctions.Params
/// </summary>
public class LoadFromCollectionParams : LoadFunctionFunctionParamsBase
{
private readonly Dictionary<string, IEnumerable<string>> _dictionaryKeys = new Dictionary<string, IEnumerable<string>>();
private readonly string DefaultDictionaryKeyId = Guid.NewGuid().ToString("N");
/// <summary>
/// Default value for the BindingFlags property
/// </summary>
Expand All @@ -41,5 +45,54 @@ public class LoadFromCollectionParams : LoadFunctionFunctionParamsBase
/// Sets how headers should be parsed before added to the worksheet, see <see cref="HeaderParsingTypes"/>
/// </summary>
public HeaderParsingTypes HeaderParsingType { get; set; } = HeaderParsingTypes.UnderscoreToSpace;

/// <summary>
/// Register keys to a property decorated with the <see cref="EPPlusDictionaryColumnAttribute"/>. These will also
/// be used to create the column for this property.
/// The <paramref name="keyId"/> should map to the <see cref="EPPlusDictionaryColumnAttribute.KeyId">KeyId property of the attribute.</see>
/// </summary>
/// <param name="keyId">Key id used to store this set of keys</param>
/// <param name="keys">Keys for the </param>
public void RegisterDictionaryKeys(string keyId, IEnumerable<string> keys)
{
if(string.IsNullOrEmpty(keyId))
{
throw new ArgumentNullException($"{nameof(keyId)} cannot be null or empty");
}
if(_dictionaryKeys.ContainsKey(keyId))
{
throw new InvalidOperationException($"The keyId '{keyId}' has already been used.");
}
if(keys == null)
{
throw new ArgumentNullException(nameof(keys));
}
if(!keys.Any())
{
throw new ArgumentException($"Parameter {nameof(keys)} cannot be empty");
}
_dictionaryKeys.Add(keyId, keys);
}

/// <summary>
/// Registers default keys for properties decorated with the <see cref="EPPlusDictionaryColumnAttribute"/>. These will also
/// be used to create the column for this property.
/// </summary>
/// <param name="keys">The keys to register</param>
public void RegisterDictionaryKeys(IEnumerable<string> keys)
{
RegisterDictionaryKeys(DefaultDictionaryKeyId, keys);
}

internal IEnumerable<string> GetDictionaryKeys(string keyId)
{
if(_dictionaryKeys.ContainsKey(keyId)) return _dictionaryKeys[keyId];
return Enumerable.Empty<string>();
}

internal IEnumerable<string> GetDefaultDictionaryKeys()
{
return GetDictionaryKeys(DefaultDictionaryKeyId);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,5 @@ public bool UseBuiltInStylesForHyperlinks
get;
set;
} = true;

/// <summary>
/// A custom class that implements the <see cref="IDictionaryKeysProvider"/>
/// </summary>
public IDictionaryKeysProvider KeysProvider { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,31 +19,16 @@ public class TestClass
[EpplusTableColumn(Order = 3)]
public string Name { get; set; }

[EPPlusDictionaryColumn(Order = 2, HeadersKey = "1")]
[EPPlusDictionaryColumn(Order = 2, KeyId = "1")]
public Dictionary<string, object> Columns { get; set; }
}

public class TestClass2 : TestClass
{
[EPPlusDictionaryColumn(Order = 1, HeadersKey = "2")]
[EPPlusDictionaryColumn(Order = 1, KeyId = "2")]
public Dictionary<string, object> Columns2 { get; set; }
}

public class MyKeysProvider : IDictionaryKeysProvider
{
public IEnumerable<string> GetKeys(string key)
{
switch(key)
{
case "1":
return new string[] { "A", "B", "C" };
case "2":
return new string[] { "C", "D", "E" };
default:
return Enumerable.Empty<string>();
}
}
}

[TestMethod]
public void ShouldReadColumnsAndValuesFromDictionaryProperty()
Expand All @@ -53,18 +38,21 @@ public void ShouldReadColumnsAndValuesFromDictionaryProperty()
Name = "test 1",
Columns = new Dictionary<string, object> { { "A", 1 }, { "B", 2 } }
};
var keys1 = new string[] { "A", "B", "C" };
var keys2 = new string[] { "C", "D", "E" };
var items = new List<TestClass> { item1 };
using(var package = new ExcelPackage())
{
var sheet = package.Workbook.Worksheets.Add("test");
sheet.Cells["A1"].LoadFromCollection(items, c =>
{
c.PrintHeaders = true;
c.KeysProvider = new MyKeysProvider();
c.RegisterDictionaryKeys("1", keys1);
c.RegisterDictionaryKeys("2", keys2);
});
Assert.AreEqual("C", sheet.Cells["A1"].Value);
Assert.AreEqual("D", sheet.Cells["B1"].Value);
Assert.AreEqual("E", sheet.Cells["C1"].Value);
Assert.AreEqual("A", sheet.Cells["A1"].Value);
Assert.AreEqual("B", sheet.Cells["B1"].Value);
Assert.AreEqual("C", sheet.Cells["C1"].Value);
Assert.AreEqual("Name", sheet.Cells["D1"].Value);
Assert.AreEqual(1, sheet.Cells["A2"].Value);
Assert.AreEqual(2, sheet.Cells["B2"].Value);
Expand All @@ -82,14 +70,17 @@ public void ShouldReadColumnsAndValuesFromDictionaryProperty2()
Columns = new Dictionary<string, object> { { "A", 3 } },
Columns2 = new Dictionary<string, object> { { "C", 1 }, { "D", 2 } }
};
var keys1 = new string[] { "A", "B", "C" };
var keys2 = new string[] { "C", "D", "E" };
var items = new List<TestClass2> { item1 };
using (var package = new ExcelPackage())
{
var sheet = package.Workbook.Worksheets.Add("test");
sheet.Cells["A1"].LoadFromCollection(items, c =>
{
c.PrintHeaders = true;
c.KeysProvider = new MyKeysProvider();
c.RegisterDictionaryKeys("1", keys1);
c.RegisterDictionaryKeys("2", keys2);
});
Assert.AreEqual("C", sheet.Cells["A1"].Value);
Assert.AreEqual("D", sheet.Cells["B1"].Value);
Expand All @@ -101,8 +92,45 @@ public void ShouldReadColumnsAndValuesFromDictionaryProperty2()
Assert.AreEqual(1, sheet.Cells["A2"].Value);
Assert.AreEqual(2, sheet.Cells["B2"].Value);
Assert.IsNull(sheet.Cells["C2"].Value);
Assert.AreEqual(2, sheet.Cells["D2"].Value);
Assert.AreEqual(3, sheet.Cells["D2"].Value);
Assert.IsNull(sheet.Cells["E2"].Value);
Assert.AreEqual("test 1", sheet.Cells["G2"].Value);
}
}

[TestMethod]
public void ShouldReadColumnsAndValuesFromDictionaryProperty3()
{
var item1 = new TestClass2
{
Name = "test 1",
Columns = new Dictionary<string, object> { { "A", 3 } },
Columns2 = new Dictionary<string, object> { { "C", 1 }, { "D", 2 } }
};
var keys1 = new string[] { "C", "B", "A" };
var keys2 = new string[] { "C", "D", "E" };
var items = new List<TestClass2> { item1 };
using (var package = new ExcelPackage())
{
var sheet = package.Workbook.Worksheets.Add("test");
sheet.Cells["A1"].LoadFromCollection(items, c =>
{
c.PrintHeaders = true;
c.RegisterDictionaryKeys("1", keys1);
c.RegisterDictionaryKeys("2", keys2);
});
Assert.AreEqual("C", sheet.Cells["A1"].Value);
Assert.AreEqual("D", sheet.Cells["B1"].Value);
Assert.AreEqual("E", sheet.Cells["C1"].Value);
Assert.AreEqual("C", sheet.Cells["D1"].Value);
Assert.AreEqual("B", sheet.Cells["E1"].Value);
Assert.AreEqual("A", sheet.Cells["F1"].Value);
Assert.AreEqual("Name", sheet.Cells["G1"].Value);
Assert.AreEqual(1, sheet.Cells["A2"].Value);
Assert.AreEqual(2, sheet.Cells["B2"].Value);
Assert.IsNull(sheet.Cells["C2"].Value);
Assert.IsNull(sheet.Cells["E2"].Value);
Assert.AreEqual(3, sheet.Cells["F2"].Value);
Assert.AreEqual("test 1", sheet.Cells["G2"].Value);
}
}
Expand Down

0 comments on commit 64a52a0

Please sign in to comment.