Skip to content
Merged
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
51 changes: 48 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ PM> Install-Package EPPlus.Core.Extensions

### **Dependencies**

**.NET 10.0** — *EPPlus >= 8.5.3*
**.NET 10.0** — *EPPlus >= 8.6.1*

### **License Setup**

Expand All @@ -24,6 +24,8 @@ For commercial use, see [EPPlus licensing](https://epplussoftware.com/developers

- Converts `IEnumerable<T>` into an Excel worksheet or package
- Reads data from Excel packages and converts them into a `List<T>`
- Supports result-based imports that collect mapping, casting, and validation errors
- Provides safe `TryGetWorksheet` and `TryGetTable` lookup helpers
- Supports reading headers from any row via `WithHeaderRowIndex`
- Maps nested class properties as flat columns via `[ExcelNestedColumn]`
- Supports data annotations for validation (`[Required]`, `[MaxLength]`, `[Range]`, etc.)
Expand Down Expand Up @@ -65,10 +67,47 @@ public class PersonDto

```cs
// From the first worksheet:
List<PersonDto> persons = excelPackage.ToList<PersonDto>(c => c.SkipCastingErrors());
List<PersonDto> firstWorksheetPersons = excelPackage.ToList<PersonDto>(c => c.SkipCastingErrors());

// From a named worksheet:
List<PersonDto> persons = excelPackage.GetWorksheet("Persons").ToList<PersonDto>();
List<PersonDto> namedWorksheetPersons = excelPackage.GetWorksheet("Persons").ToList<PersonDto>();

// Or read a named worksheet directly from the package:
List<PersonDto> directNamedWorksheetPersons = excelPackage.ToListFromWorksheet<PersonDto>("Persons");
```

#### Reading without exceptions

Use `Read<T>` when a workbook is user input and all usable rows and errors should be returned together:

```cs
ExcelReadResult<PersonDto> result = excelPackage.Read<PersonDto>("Persons");

foreach (PersonDto person in result.Items)
{
// Rows with invalid cells are retained and may be partially populated.
}

foreach (ExcelReadError error in result.Errors)
{
Console.WriteLine($"{error.Kind}: {error.Context.CellAddress} - {error.Message}");
}
```

`ToList<T>` and `AsEnumerable<T>` keep their existing throw/skip behavior. `Read<T>` captures missing-column mappings, casting failures, and data annotation validation errors.

#### Safe workbook lookups

```cs
if (excelPackage.TryGetWorksheet("Persons", out ExcelWorksheet worksheet))
{
List<PersonDto> persons = worksheet.ToList<PersonDto>();
}

if (excelPackage.TryGetTable("PeopleTable", out ExcelTable table))
{
ExcelReadResult<PersonDto> result = table.Read<PersonDto>();
}
```

#### Reading when the header row is not on row 1
Expand Down Expand Up @@ -117,6 +156,12 @@ ExcelPackage excelPackage = persons.ToExcelPackage();
// Convert to byte array
byte[] xlsx = persons.ToXlsx();

// Specify the worksheet name
byte[] namedXlsx = persons.ToXlsx("Persons", addHeaderRow: true);

// Start the fluent builder with the default worksheet name (typeof(T).Name)
WorksheetWrapper<PersonDto> worksheet = persons.ToWorksheet();

// Fluent multi-worksheet builder
List<PersonDto> pre50 = persons.Where(x => x.YearBorn < 1950).ToList();
List<PersonDto> post50 = persons.Where(x => x.YearBorn >= 1950).ToList();
Expand Down
4 changes: 2 additions & 2 deletions common.props
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project>
<PropertyGroup>
<VersionPrefix>3.1.0</VersionPrefix>
<VersionPrefix>3.2.0</VersionPrefix>
<Description>An extensions library for EPPlus to generate and manipulate Excel files easily in .NET 10.</Description>

<NoWarn>$(NoWarn);CS1591</NoWarn>
Expand All @@ -24,7 +24,7 @@
<GenerateAssemblyVersionAttribute>true</GenerateAssemblyVersionAttribute>
<GenerateAssemblyFileVersionAttribute>true</GenerateAssemblyFileVersionAttribute>
<GenerateDocumentationFile>true</GenerateDocumentationFile>

<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>

</PropertyGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,5 +61,23 @@ public virtual ExcelReadConfiguration<T> Intercept(OnCaught<T> onCaught)
OnCaught = onCaught;
return this;
}

/// <summary>
/// Runs an action after each row has been mapped.
/// </summary>
public virtual ExcelReadConfiguration<T> OnRow(OnCaught<T> onCaught) => Intercept(onCaught);

internal System.Action<Results.ExcelReadError> ErrorCollector { get; private set; }

internal bool CaptureMappingErrors { get; private set; }

internal ExcelReadConfiguration<T> CollectErrors(System.Action<Results.ExcelReadError> errorCollector)
{
ErrorCollector = errorCollector;
CaptureMappingErrors = true;
ThrowValidationExceptions = false;
ThrowCastingExceptions = false;
return this;
}
}
}
}
4 changes: 2 additions & 2 deletions src/EPPlus.Core.Extensions/EPPlus.Core.Extensions.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
<PackageReadmeFile>README.md</PackageReadmeFile>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="EPPlus" Version="8.5.3" />
<PackageReference Include="EPPlus" Version="8.6.1" />
</ItemGroup>
<ItemGroup>
<None Include="..\..\README.md" Pack="true" PackagePath="\"/>
</ItemGroup>
</Project>
</Project>
17 changes: 17 additions & 0 deletions src/EPPlus.Core.Extensions/ExcelPackageExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@ public static ExcelTable GetTable(this ExcelPackage package, string tableName)
return package.GetAllTables().FirstOrDefault(t => t.Name.Equals(tableName, StringComparison.InvariantCultureIgnoreCase));
}

public static bool TryGetTable(this ExcelPackage package, string tableName, out ExcelTable table)
{
table = package.GetTable(tableName);
return table != null;
}

/// <summary>
/// Checks whether a table is existing in the package or not
/// </summary>
Expand Down Expand Up @@ -69,6 +75,8 @@ public static DataSet ToDataSet(this ExcelPackage package, bool hasHeaderRow = t
/// <returns></returns>
public static IEnumerable<T> AsEnumerable<T>(this ExcelPackage package, int worksheetIndex = 0, Action<ExcelReadConfiguration<T>> configurationAction = null) where T : new() => package.GetWorksheet(worksheetIndex).AsEnumerable(configurationAction);

public static IEnumerable<T> AsEnumerableFromWorksheet<T>(this ExcelPackage package, string worksheetName, Action<ExcelReadConfiguration<T>> configurationAction = null) where T : new() => package.GetWorksheet(worksheetName).AsEnumerable(configurationAction);

/// <summary>
/// Converts given package into list of objects
/// </summary>
Expand All @@ -78,12 +86,21 @@ public static DataSet ToDataSet(this ExcelPackage package, bool hasHeaderRow = t
/// <param name="configurationAction"></param>
/// <returns></returns>
public static List<T> ToList<T>(this ExcelPackage package, int worksheetIndex = 0, Action<ExcelReadConfiguration<T>> configurationAction = null) where T : new() => package.AsEnumerable(worksheetIndex, configurationAction).ToList();

public static List<T> ToListFromWorksheet<T>(this ExcelPackage package, string worksheetName, Action<ExcelReadConfiguration<T>> configurationAction = null) where T : new() => package.AsEnumerableFromWorksheet(worksheetName, configurationAction).ToList();

public static Results.ExcelReadResult<T> Read<T>(this ExcelPackage package, int worksheetIndex = 0, Action<ExcelReadConfiguration<T>> configurationAction = null) where T : new() => package.GetWorksheet(worksheetIndex).Read(configurationAction);

public static Results.ExcelReadResult<T> Read<T>(this ExcelPackage package, string worksheetName, Action<ExcelReadConfiguration<T>> configurationAction = null) where T : new() => package.GetWorksheet(worksheetName).Read(configurationAction);

public static ExcelWorksheet AddWorksheet(this ExcelPackage package, string worksheetName) => package.Workbook.Worksheets.Add(worksheetName);

public static ExcelWorksheet AddWorksheet(this ExcelPackage package, string worksheetName, ExcelWorksheet copyWorksheet) => package.Workbook.Worksheets.Add(worksheetName, copyWorksheet);

public static ExcelWorksheet GetWorksheet(this ExcelPackage package, string worksheetName) => package.Workbook.GetWorksheet(worksheetName);

public static ExcelWorksheet GetWorksheet(this ExcelPackage package, int worksheetIndex) => package.Workbook.GetWorksheet(worksheetIndex);

public static bool TryGetWorksheet(this ExcelPackage package, string worksheetName, out ExcelWorksheet worksheet) => package.Workbook.TryGetWorksheet(worksheetName, out worksheet);
}
}
77 changes: 67 additions & 10 deletions src/EPPlus.Core.Extensions/ExcelTableExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,12 @@ public static ExcelAddress GetDataBounds(this ExcelTable table)
ExcelReadConfiguration<T> configuration = ExcelReadConfiguration<T>.Instance;
configurationAction?.Invoke(configuration);

return AsEnumerable(table, configuration);
}

internal static IEnumerable<T> AsEnumerable<T>(ExcelTable table, ExcelReadConfiguration<T> configuration) where T : new()
{

if (table.IsEmpty(configuration.HasHeaderRow))
{
yield break;
Expand Down Expand Up @@ -166,6 +172,15 @@ public static ExcelAddress GetDataBounds(this ExcelTable table)
throw new ExcelException(string.Format(configuration.CastingExceptionMessage, exceptionArgs.ColumnName, exceptionArgs.CellAddress.Address, exceptionArgs.CellValue, exceptionArgs.ExpectedType.Name), ex)
.WithArguments(exceptionArgs);
}

Results.ExcelReadErrorKind errorKind = ex is ValidationException
? Results.ExcelReadErrorKind.Validation
: Results.ExcelReadErrorKind.Casting;
ExcelException capturedException = ex is ValidationException
? new ExcelValidationException(ex.Message, ex).WithArguments(exceptionArgs)
: new ExcelException(string.Format(configuration.CastingExceptionMessage, exceptionArgs.ColumnName, exceptionArgs.CellAddress.Address, exceptionArgs.CellValue, exceptionArgs.ExpectedType.Name), ex).WithArguments(exceptionArgs);

configuration.ErrorCollector?.Invoke(new Results.ExcelReadError(errorKind, capturedException.Message, capturedException, exceptionArgs));
}
}

Expand All @@ -176,6 +191,38 @@ public static ExcelAddress GetDataBounds(this ExcelTable table)

public static List<T> ToList<T>(this ExcelTable table, Action<ExcelReadConfiguration<T>> configurationAction = null) where T : new() => AsEnumerable(table, configurationAction).ToList();

/// <summary>
/// Imports all rows and returns both mapped items and captured mapping, casting and validation errors.
/// </summary>
public static Results.ExcelReadResult<T> Read<T>(this ExcelTable table, Action<ExcelReadConfiguration<T>> configurationAction = null) where T : new()
{
ExcelReadConfiguration<T> configuration = ExcelReadConfiguration<T>.Instance;
configurationAction?.Invoke(configuration);

return Read(table, configuration);
}

internal static Results.ExcelReadResult<T> Read<T>(ExcelTable table, ExcelReadConfiguration<T> configuration) where T : new()
{
var items = new List<T>();
var errors = new List<Results.ExcelReadError>();
configuration.CollectErrors(errors.Add);

try
{
typeof(T).GetExcelTableColumnAttributesWithPropertyInfo();
}
catch (InvalidOperationException ex)
{
var exceptionArgs = new ExcelExceptionArgs();
errors.Add(new Results.ExcelReadError(Results.ExcelReadErrorKind.Mapping, ex.Message, ex, exceptionArgs));
return new Results.ExcelReadResult<T>(items, errors);
}

items.AddRange(AsEnumerable(table, configuration));
return new Results.ExcelReadResult<T>(items, errors);
}

/// <summary>
/// Checks whether the given table is empty or not
/// </summary>
Expand Down Expand Up @@ -217,7 +264,9 @@ private static IEnumerable<ExcelTableColumnDetails> PrepareMappings<T>(ExcelTabl
col = table.Columns[propertyInfo.Name].Position;
}
else if (columnAttribute.ColumnIndex > 0
&& CheckColumnByIndexIfExists(table, columnAttribute.ColumnIndex - 1, columnAttribute.IsOptional)) // Column index was specified
&& (configuration.CaptureMappingErrors
? columnAttribute.ColumnIndex <= table.Columns.Count
: CheckColumnByIndexIfExists(table, columnAttribute.ColumnIndex - 1, columnAttribute.IsOptional))) // Column index was specified
{
col = table.Columns[columnAttribute.ColumnIndex - 1].Position;
}
Expand All @@ -228,15 +277,23 @@ private static IEnumerable<ExcelTableColumnDetails> PrepareMappings<T>(ExcelTabl

if (!columnAttribute.IsOptional && col == -1)
{
throw new ExcelValidationException(string.Format(configuration.ColumnValidationExceptionMessage, columnAttribute.ColumnName ?? propertyInfo.Name))
.WithArguments(new ExcelExceptionArgs
{
ColumnName = columnAttribute.ColumnName,
ExpectedType = propertyInfo.PropertyType,
PropertyName = propertyInfo.Name,
CellValue = table.WorkSheet.Cells[table.Address.Start.Row, columnAttribute.ColumnIndex + table.Address.Start.Column].Value,
CellAddress = new ExcelCellAddress(table.Address.Start.Row, columnAttribute.ColumnIndex + table.Address.Start.Column)
});
var exceptionArgs = new ExcelExceptionArgs
{
ColumnName = columnAttribute.ColumnName,
ExpectedType = propertyInfo.PropertyType,
PropertyName = propertyInfo.Name,
CellValue = table.WorkSheet.Cells[table.Address.Start.Row, columnAttribute.ColumnIndex + table.Address.Start.Column].Value,
CellAddress = new ExcelCellAddress(table.Address.Start.Row, columnAttribute.ColumnIndex + table.Address.Start.Column)
};
ExcelException exception = new ExcelValidationException(string.Format(configuration.ColumnValidationExceptionMessage, columnAttribute.ColumnName ?? propertyInfo.Name))
.WithArguments(exceptionArgs);

if (!configuration.CaptureMappingErrors)
{
throw exception;
}

configuration.ErrorCollector?.Invoke(new Results.ExcelReadError(Results.ExcelReadErrorKind.Mapping, exception.Message, exception, exceptionArgs));
}

yield return new ExcelTableColumnDetails(col, propertyInfo, columnAttribute, propertyInfoAndColumnAttribute.OwnerPropertyInfo);
Expand Down
6 changes: 6 additions & 0 deletions src/EPPlus.Core.Extensions/ExcelWorkbookExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,11 @@ public static class ExcelWorkbookExtensions
public static ExcelWorksheet GetWorksheet(this ExcelWorkbook workbook, string worksheetName) => workbook.Worksheets.FirstOrDefault(x => x.Name == worksheetName);

public static ExcelWorksheet GetWorksheet(this ExcelWorkbook workbook, int worksheetIndex) => workbook.Worksheets[worksheetIndex];

public static bool TryGetWorksheet(this ExcelWorkbook workbook, string worksheetName, out ExcelWorksheet worksheet)
{
worksheet = workbook.GetWorksheet(worksheetName);
return worksheet != null;
}
}
}
23 changes: 22 additions & 1 deletion src/EPPlus.Core.Extensions/ExcelWorksheetExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,12 @@ public static ExcelTable AsExcelTable(this ExcelWorksheet worksheet, bool hasHea

public static ExcelTable GetTable(this ExcelWorksheet worksheet, int tableIndex) => worksheet.Tables[tableIndex];

public static bool TryGetTable(this ExcelWorksheet worksheet, string tableName, out ExcelTable table)
{
table = worksheet.GetTable(tableName);
return table != null;
}

/// <summary>
/// Creates an Excel table using the data bounds of the worksheet.
/// </summary>
Expand Down Expand Up @@ -175,6 +181,21 @@ public static DataTable ToDataTable(this ExcelWorksheet worksheet, bool hasHeade
public static List<T> ToList<T>(this ExcelWorksheet worksheet, Action<ExcelReadConfiguration<T>> configurationAction = null) where T : new()
=> worksheet.AsEnumerable(configurationAction).ToList();

/// <summary>
/// Imports all rows and returns both mapped items and captured mapping, casting and validation errors.
/// </summary>
public static Results.ExcelReadResult<T> Read<T>(this ExcelWorksheet worksheet, Action<ExcelReadConfiguration<T>> configurationAction = null) where T : new()
{
ExcelReadConfiguration<T> configuration = ExcelReadConfiguration<T>.Instance;
configurationAction?.Invoke(configuration);

ExcelTable table = configuration.HeaderRowIndex.HasValue
? worksheet.AsExcelTableFromRow(StringHelper.GenerateRandomTableName(), configuration.HasHeaderRow, configuration.HeaderRowIndex.Value)
: worksheet.AsExcelTable(configuration.HasHeaderRow);

return ExcelTableExtensions.Read(table, configuration);
}

public static ExcelWorksheet ChangeCellValue(this ExcelWorksheet worksheet, int rowIndex, int columnIndex, object value, Action<ExcelRange> configureCell = null)
{
configureCell?.Invoke(worksheet.Cells[rowIndex, columnIndex]);
Expand Down Expand Up @@ -581,4 +602,4 @@ private static ExcelTable AsExcelTableFromRow(this ExcelWorksheet worksheet, str
return worksheet.Tables[tableName];
}
}
}
}
28 changes: 28 additions & 0 deletions src/EPPlus.Core.Extensions/Results/ExcelReadError.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using System;

using EPPlus.Core.Extensions.Exceptions;

namespace EPPlus.Core.Extensions.Results
{
/// <summary>
/// Describes an error captured while importing an Excel row.
/// </summary>
public sealed class ExcelReadError
{
internal ExcelReadError(ExcelReadErrorKind kind, string message, Exception exception, ExcelExceptionArgs context)
{
Kind = kind;
Message = message;
Exception = exception;
Context = context;
}

public ExcelReadErrorKind Kind { get; }

public string Message { get; }

public Exception Exception { get; }

public ExcelExceptionArgs Context { get; }
}
}
12 changes: 12 additions & 0 deletions src/EPPlus.Core.Extensions/Results/ExcelReadErrorKind.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace EPPlus.Core.Extensions.Results
{
/// <summary>
/// Describes the stage at which an Excel import error occurred.
/// </summary>
public enum ExcelReadErrorKind
{
Mapping,
Casting,
Validation
}
}
Loading
Loading