Skip to content

Commit

Permalink
! Improved nullable/null handling
Browse files Browse the repository at this point in the history
+ Added Validate<> method + tests
+ ExcelTableConvertExceptionArgs now holds exact cell address
+ Prepared for release 1.1
  • Loading branch information
zorgoz committed Dec 25, 2016
1 parent 548e115 commit 65b1cf9
Show file tree
Hide file tree
Showing 9 changed files with 198 additions and 30 deletions.
3 changes: 1 addition & 2 deletions EPPlus.TableAsEnumerable.Tests/ComplexExampleTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -115,8 +115,7 @@ public override string ToString()
[TestMethod]
public void TestComplexExample()
{
var table = excelPackage.Workbook.Worksheets["TEST3"].Tables["TEST3"];

var table = excelPackage.GetTable("TEST3");

IEnumerable<Cars> enumerable = table.AsEnumerable<Cars>();
IList<Cars> list = null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
<ItemGroup>
<Compile Include="ComplexExampleTest.cs" />
<Compile Include="ExcelColumnAttributeTests.cs" />
<Compile Include="TableValidateTests.cs" />
<Compile Include="ExcelPackageExtensionsTests.cs" />
<Compile Include="TypeExtensionTests.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
Expand Down
Binary file modified EPPlus.TableAsEnumerable.Tests/Resources/testsheets.xlsx
Binary file not shown.
88 changes: 88 additions & 0 deletions EPPlus.TableAsEnumerable.Tests/TableValidateTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using OfficeOpenXml;
using System.IO;
using System.Reflection;
using EPPlus.Extensions;
using System.Linq;

namespace TESTS
{
[TestClass]
public class TableValidateTests
{
private TestContext testContextInstance;
private static ExcelPackage excelPackage;

/// <summary>
///Gets or sets the test context which provides
///information about and functionality for the current test run.
///</summary>
public TestContext TestContext
{
get
{
return testContextInstance;
}
set
{
testContextInstance = value;
}
}

/// <summary>
/// Initializes EPPLus excelPackage with the embedded content
/// </summary>
[ClassInitialize()]
public static void MyClassInitialize(TestContext testContext)
{
var assembly = Assembly.GetExecutingAssembly();
var resourceName = "TESTS.Resources.testsheets.xlsx";

using (Stream stream = assembly.GetManifestResourceStream(resourceName))
{
excelPackage = new ExcelPackage(stream);
}
}

/// <summary>
/// Frees up excelPackage
/// </summary>
[ClassCleanup()]
public static void MyClassCleanup()
{
excelPackage.Dispose();
}

enum Manufacturers { Opel = 1, Ford, Mercedes};
class WrongCars
{
[ExcelTableColumn(ColumnName = "License plate")]
public string licensePlate { get; set; }

[ExcelTableColumn]
public Manufacturers manufacturer { get; set; }

[ExcelTableColumn(ColumnName = "Manufacturing date")]
public DateTime manufacturingDate { get; set; }

[ExcelTableColumn(ColumnName = "Is ready for traffic?")]
public bool ready { get; set; }
}

[TestMethod]
public void Test_TableValidation()
{
var table = excelPackage.GetTable("TEST3");

Assert.IsNotNull(table, "We have TEST3 table");

var validation = table.Validate<WrongCars>().ToList();

Assert.IsNotNull(validation, "we have errors here");
Assert.AreEqual(2, validation.Count, "We have 2 errors");
Assert.IsTrue(validation.Exists(x => x.cellAddress.Address.Equals("C6", StringComparison.InvariantCultureIgnoreCase)), "Toyota is not in the enumeration");
Assert.IsTrue(validation.Exists(x => x.cellAddress.Address.Equals("D7", StringComparison.InvariantCultureIgnoreCase)), "Date is null");
}
}
}
2 changes: 1 addition & 1 deletion EPPlus.TableAsEnumerable/EPPlus.TableAsEnumerable.nuspec
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<description>Generic extension method enabling retrival of ExcelTable rows as an enumeration of typed objects.</description>
<summary>This project adds an extension method to EPPlus ExcelTable objects that enables generic retrival of the data within. Solution supports numeric types, strings, datetime, nullables and enums of a POCO with help of decorating attributes that enable property-column mapping.</summary>
<releaseNotes>Corrected namespace character case, added extension methods to ExcelPackage to easy access tables in the whole workbook.</releaseNotes>
<releaseNotes>Corrected namespace character case, added new features. Corrected header and totals row handling and null handling.</releaseNotes>
<copyright>Copyright 2016</copyright>
<tags>library .net Excel EPPlus helper generic extension</tags>
</metadata>
Expand Down
108 changes: 88 additions & 20 deletions EPPlus.TableAsEnumerable/EPPlusAsEnumerableExtension.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
* IN THE SOFTWARE.
*/

using OfficeOpenXml;
using OfficeOpenXml.Table;
using System;
using System.Collections;
Expand Down Expand Up @@ -64,6 +65,67 @@ internal static bool IsNumeric(this Type type)
}
#endregion

/// <summary>
/// Method returns table data bounds with regards to header and totoals row visibility
/// </summary>
/// <param name="table">Extended object</param>
/// <returns>Address range</returns>
public static ExcelAddress GetDataBounds(this ExcelTable table)
{
return new ExcelAddress(
table.Address.Start.Row + (table.ShowHeader ? 1 : 0),
table.Address.Start.Column,
table.Address.End.Row - (table.ShowTotal ? 1 : 0),
table.Address.End.Column
);
}

/// <summary>
/// Method validates the excel table against the generating type. While AsEnumerable skips null cells, validation winn not.
/// </summary>
/// <typeparam name="T">Generating class type</typeparam>
/// <param name="table">Extended object</param>
/// <returns>An enumerable of <see cref="ExcelTableConvertExceptionArgs"/> containing </returns>
public static IEnumerable<ExcelTableConvertExceptionArgs> Validate<T>(this ExcelTable table) where T : class, new()
{
IList mapping = PrepareMappings<T>(table);
var result = new LinkedList<ExcelTableConvertExceptionArgs>();

var bounds = table.GetDataBounds();

T item = (T)Activator.CreateInstance(typeof(T));

// Parse table
for (int row = bounds.Start.Row; row <= bounds.End.Row; row++)
{
foreach (KeyValuePair<int, PropertyInfo> map in mapping)
{
object cell = table.WorkSheet.Cells[row, map.Key + table.Address.Start.Column].Value;

PropertyInfo property = map.Value;

try
{
TrySetProperty(item, property, cell);
}
catch
{
result.AddLast(
new ExcelTableConvertExceptionArgs
{
columnName = table.Columns[map.Key].Name,
expectedType = property.PropertyType,
propertyName = property.Name,
cellValue = cell,
cellAddress = new ExcelCellAddress(row, map.Key + table.Address.Start.Column)
});
}
}
}

return result;
}

/// <summary>
/// Generic extension method yielding objects of specified type from table.
/// </summary>
Expand All @@ -79,32 +141,22 @@ internal static bool IsNumeric(this Type type)
{
IList mapping = PrepareMappings<T>(table);

var bounds = table.GetDataBounds();

// Parse table
for (int row = table.Address.Start.Row + (table.ShowHeader ? 1 : 0);
row <= table.Address.End.Row - (table.ShowTotal ? 1 : 0);
row++)
for (int row = bounds.Start.Row; row <= bounds.End.Row; row++)
{
T item = (T)Activator.CreateInstance(typeof(T));

foreach (KeyValuePair<int, PropertyInfo> map in mapping)
{
object cell = table.WorkSheet.Cells[row, map.Key + table.Address.Start.Column].Value;

if (cell == null) continue;

var property = map.Value;

Type type = property.PropertyType;

// If type is nullable, get base type instead
if (property.PropertyType.IsNullable())
{
type = type.GetGenericArguments()[0];
}
PropertyInfo property = map.Value;

try
{
TrySetProperty(item, type, property, cell);
TrySetProperty(item, property, cell);
}
catch (Exception ex)
{
Expand All @@ -115,9 +167,10 @@ internal static bool IsNumeric(this Type type)
new ExcelTableConvertExceptionArgs
{
columnName = table.Columns[map.Key].Name,
expectedType = type,
expectedType = property.PropertyType,
propertyName = property.Name,
cellValue = cell
cellValue = cell,
cellAddress = new ExcelCellAddress(row, map.Key + table.Address.Start.Column)
}
);
}
Expand Down Expand Up @@ -177,9 +230,24 @@ private static IList PrepareMappings<T>(ExcelTable table)
return mapping;
}

private static void TrySetProperty(object item, Type type, PropertyInfo property, object cell)
/// <summary>
/// Method tries to set property of item
/// </summary>
/// <param name="item">target object</param>
/// <param name="property">property to be set</param>
/// <param name="cell">cell value</param>
private static void TrySetProperty(object item, PropertyInfo property, object cell)
{
var itemType = item.GetType();
Type type = property.PropertyType;
Type itemType = item.GetType();

// If type is nullable, get base type instead
if (property.PropertyType.IsNullable())
{
if (cell == null) return; // If it is nullable, and we have null we should not waste time

type = type.GetGenericArguments()[0];
}

if (type == typeof(string))
{
Expand All @@ -188,7 +256,7 @@ private static void TrySetProperty(object item, Type type, PropertyInfo property
BindingFlags.Public | BindingFlags.Instance | BindingFlags.SetProperty,
null,
item,
new object[] { cell.ToString() });
new object[] { cell?.ToString() });

return;
}
Expand Down
6 changes: 6 additions & 0 deletions EPPlus.TableAsEnumerable/ExcelTableConvertException.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
* IN THE SOFTWARE.
*/

using OfficeOpenXml;
using System;

namespace EPPlus.Extensions
Expand Down Expand Up @@ -40,6 +41,11 @@ public class ExcelTableConvertExceptionArgs
/// Cell value returned by EPPlus
/// </summary>
public object cellValue { get; set; }

/// <summary>
/// Absolute address of the cell, where the conversion error occured
/// </summary>
public ExcelCellAddress cellAddress { get; set; }
}

/// <summary>
Expand Down
6 changes: 3 additions & 3 deletions EPPlus.TableAsEnumerable/Properties/AssemblyInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("EPPlus.TableAsEnumerable")]
[assembly: AssemblyDescription("Generic extension method enabling retrival ExcelTable rows as an enumeration of typed objects.")]
[assembly: AssemblyDescription("Generic extension method enabling retrival of ExcelTable rows as an enumeration of typed objects.")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("EPPlus.TableAsEnumerable")]
Expand All @@ -33,5 +33,5 @@
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: AssemblyVersion("1.1.*")]
[assembly: AssemblyFileVersion("1.1.0.0")]
14 changes: 10 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ foreach(var car in table.AsEnumerable<Cars>())
```

## API reference
Code is troughout documented, but here are the key elements one needs to know when using this library.
Code is troughout documented, but here are the key elements one needs to know when using this library. Please read version history also.

### ExcelTableColumnAttribute
As can be noticed above, this attribute denotes property-column mapping. `ColumnName` parameter can be used to map by column name, while `ColumnIndex` uses the *n*th one-based column. If no parameter is added, the mapping is done using the name of the decorated property.
Expand All @@ -94,17 +94,23 @@ Both parameters can not be given. Empty column name is also not accepted.
### ExcelTableConvertException and ExcelTableConvertExceptionArgs
This cutom exception is thrown when setting a property to a cell value is failing and the extension method is told not to skip errors (this is the default case). The exception object has an `args` property of type `ExcelTableConvertExceptionArgs` that will hold the exact circumstances of the conversion error, including the original exception as inner exception.

### AsEnumerable extension method
### AsEnumerable<> extension method
This generic method is doing the job, as can be seen in the example above. It returns an IEnumerable, which means that it is executed only when enumerated. Thus you might get well trough this call and get the exception when iterating or converting the result.

### Validate<> extension method
While `AsEnumerable<>` stops at the first error, this generic method will return an enumeration of `ExcelTableConvertExceptionArgs` containing all errors encountered during a conversion attempt. This feature is usable to provide feedback to the user.

**Note:** only classes with parameterless constructor can be used as generating type.

### Version history
#### Pending (in repo, but not released yet)
#### 1.1
* Bugfix: taking into account table header and total row presence or absence
* Added extension methods to ExcelPackage type for easy access of tables: .GetTables, .HasTable, .GetTable
* Improved nullable/null handling. Still, as string is nullable by definition, can't be made *required* yet.
* Added extension methods to ExcelPackage type for easy access of tables: `.GetTables`, `.HasTable`, `.GetTable` (Names uniqueness across worksheet is are guaranteed by Excel and EPPlus as well)
* Corrected namespace name case
* Converted to be .Net 4.0 Client Profile compatible
* `Validate` generic method added to get all conversion errors.
* New test added, some tests improved

#### v1.0
* Every simple type can be mapped including numeric ones, bool, string, DateTime and enumerations.
Expand Down

0 comments on commit 65b1cf9

Please sign in to comment.