diff --git a/README.md b/README.md index 6e4091a..d73d628 100644 --- a/README.md +++ b/README.md @@ -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** @@ -24,6 +24,8 @@ For commercial use, see [EPPlus licensing](https://epplussoftware.com/developers - Converts `IEnumerable` into an Excel worksheet or package - Reads data from Excel packages and converts them into a `List` +- 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.) @@ -65,10 +67,47 @@ public class PersonDto ```cs // From the first worksheet: -List persons = excelPackage.ToList(c => c.SkipCastingErrors()); +List firstWorksheetPersons = excelPackage.ToList(c => c.SkipCastingErrors()); // From a named worksheet: -List persons = excelPackage.GetWorksheet("Persons").ToList(); +List namedWorksheetPersons = excelPackage.GetWorksheet("Persons").ToList(); + +// Or read a named worksheet directly from the package: +List directNamedWorksheetPersons = excelPackage.ToListFromWorksheet("Persons"); +``` + +#### Reading without exceptions + +Use `Read` when a workbook is user input and all usable rows and errors should be returned together: + +```cs +ExcelReadResult result = excelPackage.Read("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` and `AsEnumerable` keep their existing throw/skip behavior. `Read` captures missing-column mappings, casting failures, and data annotation validation errors. + +#### Safe workbook lookups + +```cs +if (excelPackage.TryGetWorksheet("Persons", out ExcelWorksheet worksheet)) +{ + List persons = worksheet.ToList(); +} + +if (excelPackage.TryGetTable("PeopleTable", out ExcelTable table)) +{ + ExcelReadResult result = table.Read(); +} ``` #### Reading when the header row is not on row 1 @@ -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 worksheet = persons.ToWorksheet(); + // Fluent multi-worksheet builder List pre50 = persons.Where(x => x.YearBorn < 1950).ToList(); List post50 = persons.Where(x => x.YearBorn >= 1950).ToList(); diff --git a/common.props b/common.props index ed7e8d9..d797dcf 100644 --- a/common.props +++ b/common.props @@ -1,6 +1,6 @@ - 3.1.0 + 3.2.0 An extensions library for EPPlus to generate and manipulate Excel files easily in .NET 10. $(NoWarn);CS1591 @@ -24,7 +24,7 @@ true true true - + true diff --git a/src/EPPlus.Core.Extensions/Configuration/ExcelReadConfiguration.cs b/src/EPPlus.Core.Extensions/Configuration/ExcelReadConfiguration.cs index 43d61c1..c46bcba 100644 --- a/src/EPPlus.Core.Extensions/Configuration/ExcelReadConfiguration.cs +++ b/src/EPPlus.Core.Extensions/Configuration/ExcelReadConfiguration.cs @@ -61,5 +61,23 @@ public virtual ExcelReadConfiguration Intercept(OnCaught onCaught) OnCaught = onCaught; return this; } + + /// + /// Runs an action after each row has been mapped. + /// + public virtual ExcelReadConfiguration OnRow(OnCaught onCaught) => Intercept(onCaught); + + internal System.Action ErrorCollector { get; private set; } + + internal bool CaptureMappingErrors { get; private set; } + + internal ExcelReadConfiguration CollectErrors(System.Action errorCollector) + { + ErrorCollector = errorCollector; + CaptureMappingErrors = true; + ThrowValidationExceptions = false; + ThrowCastingExceptions = false; + return this; + } } -} \ No newline at end of file +} diff --git a/src/EPPlus.Core.Extensions/EPPlus.Core.Extensions.csproj b/src/EPPlus.Core.Extensions/EPPlus.Core.Extensions.csproj index 44dd12a..0ea6cc6 100644 --- a/src/EPPlus.Core.Extensions/EPPlus.Core.Extensions.csproj +++ b/src/EPPlus.Core.Extensions/EPPlus.Core.Extensions.csproj @@ -6,9 +6,9 @@ README.md - + - \ No newline at end of file + diff --git a/src/EPPlus.Core.Extensions/ExcelPackageExtensions.cs b/src/EPPlus.Core.Extensions/ExcelPackageExtensions.cs index f794809..5909e1c 100644 --- a/src/EPPlus.Core.Extensions/ExcelPackageExtensions.cs +++ b/src/EPPlus.Core.Extensions/ExcelPackageExtensions.cs @@ -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; + } + /// /// Checks whether a table is existing in the package or not /// @@ -69,6 +75,8 @@ public static DataSet ToDataSet(this ExcelPackage package, bool hasHeaderRow = t /// public static IEnumerable AsEnumerable(this ExcelPackage package, int worksheetIndex = 0, Action> configurationAction = null) where T : new() => package.GetWorksheet(worksheetIndex).AsEnumerable(configurationAction); + public static IEnumerable AsEnumerableFromWorksheet(this ExcelPackage package, string worksheetName, Action> configurationAction = null) where T : new() => package.GetWorksheet(worksheetName).AsEnumerable(configurationAction); + /// /// Converts given package into list of objects /// @@ -78,6 +86,13 @@ public static DataSet ToDataSet(this ExcelPackage package, bool hasHeaderRow = t /// /// public static List ToList(this ExcelPackage package, int worksheetIndex = 0, Action> configurationAction = null) where T : new() => package.AsEnumerable(worksheetIndex, configurationAction).ToList(); + + public static List ToListFromWorksheet(this ExcelPackage package, string worksheetName, Action> configurationAction = null) where T : new() => package.AsEnumerableFromWorksheet(worksheetName, configurationAction).ToList(); + + public static Results.ExcelReadResult Read(this ExcelPackage package, int worksheetIndex = 0, Action> configurationAction = null) where T : new() => package.GetWorksheet(worksheetIndex).Read(configurationAction); + + public static Results.ExcelReadResult Read(this ExcelPackage package, string worksheetName, Action> 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); @@ -85,5 +100,7 @@ public static DataSet ToDataSet(this ExcelPackage package, bool hasHeaderRow = t 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); } } diff --git a/src/EPPlus.Core.Extensions/ExcelTableExtensions.cs b/src/EPPlus.Core.Extensions/ExcelTableExtensions.cs index e11a22e..4e1e71b 100644 --- a/src/EPPlus.Core.Extensions/ExcelTableExtensions.cs +++ b/src/EPPlus.Core.Extensions/ExcelTableExtensions.cs @@ -108,6 +108,12 @@ public static ExcelAddress GetDataBounds(this ExcelTable table) ExcelReadConfiguration configuration = ExcelReadConfiguration.Instance; configurationAction?.Invoke(configuration); + return AsEnumerable(table, configuration); + } + + internal static IEnumerable AsEnumerable(ExcelTable table, ExcelReadConfiguration configuration) where T : new() + { + if (table.IsEmpty(configuration.HasHeaderRow)) { yield break; @@ -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)); } } @@ -176,6 +191,38 @@ public static ExcelAddress GetDataBounds(this ExcelTable table) public static List ToList(this ExcelTable table, Action> configurationAction = null) where T : new() => AsEnumerable(table, configurationAction).ToList(); + /// + /// Imports all rows and returns both mapped items and captured mapping, casting and validation errors. + /// + public static Results.ExcelReadResult Read(this ExcelTable table, Action> configurationAction = null) where T : new() + { + ExcelReadConfiguration configuration = ExcelReadConfiguration.Instance; + configurationAction?.Invoke(configuration); + + return Read(table, configuration); + } + + internal static Results.ExcelReadResult Read(ExcelTable table, ExcelReadConfiguration configuration) where T : new() + { + var items = new List(); + var errors = new List(); + 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(items, errors); + } + + items.AddRange(AsEnumerable(table, configuration)); + return new Results.ExcelReadResult(items, errors); + } + /// /// Checks whether the given table is empty or not /// @@ -217,7 +264,9 @@ private static IEnumerable PrepareMappings(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; } @@ -228,15 +277,23 @@ private static IEnumerable PrepareMappings(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); diff --git a/src/EPPlus.Core.Extensions/ExcelWorkbookExtensions.cs b/src/EPPlus.Core.Extensions/ExcelWorkbookExtensions.cs index 95b5d9a..af24136 100644 --- a/src/EPPlus.Core.Extensions/ExcelWorkbookExtensions.cs +++ b/src/EPPlus.Core.Extensions/ExcelWorkbookExtensions.cs @@ -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; + } } } diff --git a/src/EPPlus.Core.Extensions/ExcelWorksheetExtensions.cs b/src/EPPlus.Core.Extensions/ExcelWorksheetExtensions.cs index 39fcd86..c7cfa76 100644 --- a/src/EPPlus.Core.Extensions/ExcelWorksheetExtensions.cs +++ b/src/EPPlus.Core.Extensions/ExcelWorksheetExtensions.cs @@ -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; + } + /// /// Creates an Excel table using the data bounds of the worksheet. /// @@ -175,6 +181,21 @@ public static DataTable ToDataTable(this ExcelWorksheet worksheet, bool hasHeade public static List ToList(this ExcelWorksheet worksheet, Action> configurationAction = null) where T : new() => worksheet.AsEnumerable(configurationAction).ToList(); + /// + /// Imports all rows and returns both mapped items and captured mapping, casting and validation errors. + /// + public static Results.ExcelReadResult Read(this ExcelWorksheet worksheet, Action> configurationAction = null) where T : new() + { + ExcelReadConfiguration configuration = ExcelReadConfiguration.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 configureCell = null) { configureCell?.Invoke(worksheet.Cells[rowIndex, columnIndex]); @@ -581,4 +602,4 @@ private static ExcelTable AsExcelTableFromRow(this ExcelWorksheet worksheet, str return worksheet.Tables[tableName]; } } -} \ No newline at end of file +} diff --git a/src/EPPlus.Core.Extensions/Results/ExcelReadError.cs b/src/EPPlus.Core.Extensions/Results/ExcelReadError.cs new file mode 100644 index 0000000..8299899 --- /dev/null +++ b/src/EPPlus.Core.Extensions/Results/ExcelReadError.cs @@ -0,0 +1,28 @@ +using System; + +using EPPlus.Core.Extensions.Exceptions; + +namespace EPPlus.Core.Extensions.Results +{ + /// + /// Describes an error captured while importing an Excel row. + /// + 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; } + } +} diff --git a/src/EPPlus.Core.Extensions/Results/ExcelReadErrorKind.cs b/src/EPPlus.Core.Extensions/Results/ExcelReadErrorKind.cs new file mode 100644 index 0000000..c10b236 --- /dev/null +++ b/src/EPPlus.Core.Extensions/Results/ExcelReadErrorKind.cs @@ -0,0 +1,12 @@ +namespace EPPlus.Core.Extensions.Results +{ + /// + /// Describes the stage at which an Excel import error occurred. + /// + public enum ExcelReadErrorKind + { + Mapping, + Casting, + Validation + } +} diff --git a/src/EPPlus.Core.Extensions/Results/ExcelReadResult.cs b/src/EPPlus.Core.Extensions/Results/ExcelReadResult.cs new file mode 100644 index 0000000..c8a12e2 --- /dev/null +++ b/src/EPPlus.Core.Extensions/Results/ExcelReadResult.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using System.Linq; + +namespace EPPlus.Core.Extensions.Results +{ + /// + /// Contains imported items and any errors captured while mapping them. + /// + public sealed class ExcelReadResult + { + internal ExcelReadResult(IEnumerable items, IEnumerable errors) + { + Items = items.ToList().AsReadOnly(); + Errors = errors.ToList().AsReadOnly(); + } + + public IReadOnlyList Items { get; } + + public IReadOnlyList Errors { get; } + + public bool HasErrors => Errors.Count > 0; + + public bool IsSuccess => !HasErrors; + } +} diff --git a/src/EPPlus.Core.Extensions/ToExcelExtensions.cs b/src/EPPlus.Core.Extensions/ToExcelExtensions.cs index b3e03d3..1d4870b 100644 --- a/src/EPPlus.Core.Extensions/ToExcelExtensions.cs +++ b/src/EPPlus.Core.Extensions/ToExcelExtensions.cs @@ -12,6 +12,11 @@ namespace EPPlus.Core.Extensions { public static class ToExcelExtensions { + /// + /// Generates an Excel worksheet using the row type name as the worksheet name. + /// + public static WorksheetWrapper ToWorksheet(this IEnumerable rows) => rows.ToWorksheet(typeof(T).Name); + /// /// Generates an Excel worksheet from given list /// @@ -227,6 +232,21 @@ public static byte[] ToXlsx(this IEnumerable rows, bool addHeaderRow = tru return worksheet.ToXlsx(); } + /// + /// Converts the given rows into an Excel file using the specified worksheet name. + /// + public static byte[] ToXlsx(this IEnumerable rows, string worksheetName, bool addHeaderRow) + { + WorksheetWrapper worksheet = rows.ToWorksheet(worksheetName); + + if (!addHeaderRow) + { + worksheet.WithoutHeader(); + } + + return worksheet.ToXlsx(); + } + /// /// Generates Excel file, and returns a byte array. /// diff --git a/src/EPPlus.Core.Extensions/WorksheetWrapper.cs b/src/EPPlus.Core.Extensions/WorksheetWrapper.cs index 8464b4c..d5a0a60 100644 --- a/src/EPPlus.Core.Extensions/WorksheetWrapper.cs +++ b/src/EPPlus.Core.Extensions/WorksheetWrapper.cs @@ -38,6 +38,9 @@ internal void AppendWorksheet() Package = new ExcelPackage(); } + List rows = Rows as List ?? Rows?.ToList(); + Rows = rows; + ExcelWorksheet worksheet = Package.Workbook.Worksheets.Add(Name); var rowOffset = 0; @@ -85,15 +88,15 @@ internal void AppendWorksheet() CreateTableIfPossible(worksheet); //render data - if (Rows != null) + if (rows != null) { - for (var r = 0; r < Rows.Count(); r++) + for (var r = 0; r < rows.Count; r++) { for (var c = 0; c < Columns.Count(); c++) { - worksheet.Cells[r + rowOffset + 1, c + 1].Value = Columns[c].Map(Rows.ElementAt(r)); + worksheet.Cells[r + rowOffset + 1, c + 1].Value = Columns[c].Map(rows[r]); - Configuration.ConfigureCell?.Invoke(worksheet.Cells[r + rowOffset + 1, c + 1], Rows.ElementAt(r)); + Configuration.ConfigureCell?.Invoke(worksheet.Cells[r + rowOffset + 1, c + 1], rows[r]); } } } diff --git a/test/EPPlus.Core.Extensions.Tests/ExcelPackageExtensions_Tests.cs b/test/EPPlus.Core.Extensions.Tests/ExcelPackageExtensions_Tests.cs index a920fc4..3618fa7 100644 --- a/test/EPPlus.Core.Extensions.Tests/ExcelPackageExtensions_Tests.cs +++ b/test/EPPlus.Core.Extensions.Tests/ExcelPackageExtensions_Tests.cs @@ -3,9 +3,12 @@ using System.IO; using System.Linq; +using EPPlus.Core.Extensions.Results; + using FluentAssertions; using OfficeOpenXml; +using OfficeOpenXml.Table; using Xunit; @@ -192,5 +195,70 @@ public void Should_generate_ExcelPackage_with_optional_columns() var generatedWorksheet = result.GetWorksheet(expectedWorksheetName); generatedWorksheet.ToList().Count.Should().Be(2); } + + [Fact] + public void Try_get_helpers_should_return_found_objects_and_false_for_missing_objects() + { + ExcelPackage1.TryGetWorksheet("TEST1", out ExcelWorksheet worksheet).Should().BeTrue(); + worksheet.Should().NotBeNull(); + + ExcelPackage1.Workbook.TryGetWorksheet("TEST1", out ExcelWorksheet workbookWorksheet).Should().BeTrue(); + workbookWorksheet.Should().BeSameAs(worksheet); + + ExcelPackage1.TryGetTable("test1", out ExcelTable packageTable).Should().BeTrue(); + packageTable.Should().NotBeNull(); + + worksheet.TryGetTable("TEST1", out ExcelTable worksheetTable).Should().BeTrue(); + worksheetTable.Should().BeSameAs(packageTable); + + ExcelPackage1.TryGetWorksheet("missing", out _).Should().BeFalse(); + ExcelPackage1.TryGetTable("missing", out _).Should().BeFalse(); + worksheet.TryGetTable("missing", out _).Should().BeFalse(); + } + + [Fact] + public void Package_should_read_rows_by_worksheet_name() + { + List list = ExcelPackage1.ToListFromWorksheet("TEST1"); + IEnumerable enumerable = ExcelPackage1.AsEnumerableFromWorksheet("TEST1"); + + list.Should().HaveCount(5); + enumerable.Should().HaveCount(5); + } + + [Fact] + public void On_row_should_be_a_discoverable_alias_for_intercept() + { + List list = ExcelPackage1.ToListFromWorksheet("TEST1", configuration => + configuration.OnRow((item, rowIndex) => item.NotMappedProperty = rowIndex)); + + list.Should().OnlyContain(item => item.NotMappedProperty > 0); + } + + [Fact] + public void Package_read_should_support_named_worksheets_and_header_configuration() + { + using var package = new ExcelPackage(); + ExcelWorksheet worksheet = package.Workbook.Worksheets.Add("People"); + worksheet.Cells[1, 1].Value = "Title"; + worksheet.Cells[2, 1].Value = "Name"; + worksheet.Cells[2, 2].Value = "Gender"; + worksheet.Cells[3, 1].Value = "Ada"; + worksheet.Cells[3, 2].Value = "Female"; + + ExcelReadResult result = package.Read("People", configuration => configuration.WithHeaderRowIndex(2)); + + result.IsSuccess.Should().BeTrue(); + result.Items.Should().ContainSingle(); + result.Items[0].FirstName.Should().Be("Ada"); + } + + [Fact] + public void Existing_package_default_literal_call_should_remain_unambiguous() + { + List rows = ExcelPackage1.ToList(default); + + rows.Should().HaveCount(5); + } } } diff --git a/test/EPPlus.Core.Extensions.Tests/ExcelTableExtensions_Tests.cs b/test/EPPlus.Core.Extensions.Tests/ExcelTableExtensions_Tests.cs index fefdc4e..0cca8cb 100644 --- a/test/EPPlus.Core.Extensions.Tests/ExcelTableExtensions_Tests.cs +++ b/test/EPPlus.Core.Extensions.Tests/ExcelTableExtensions_Tests.cs @@ -4,6 +4,7 @@ using EPPlus.Core.Extensions.Attributes; using EPPlus.Core.Extensions.Exceptions; +using EPPlus.Core.Extensions.Results; using FluentAssertions; @@ -480,6 +481,67 @@ public void Should_validate_Excel_table_and_return_casting_errors() validationResults.Exists(x => x.CellAddress.Address.Equals("C6", StringComparison.InvariantCultureIgnoreCase)).Should().BeTrue("Toyota is not in the enumeration"); validationResults.Exists(x => x.CellAddress.Address.Equals("D7", StringComparison.InvariantCultureIgnoreCase)).Should().BeTrue("Date is null"); } + + [Fact] + public void Read_should_not_capture_exceptions_thrown_by_user_callbacks() + { + ExcelTable table = ExcelPackage1.GetWorksheet("TEST1").GetTable("TEST1"); + + Action action = () => table.Read(configuration => + configuration.OnRow((item, rowIndex) => throw new InvalidOperationException("Callback failed."))); + + action.Should().Throw().WithMessage("Callback failed."); + } + + [Fact] + public void Read_should_capture_casting_errors_and_keep_partially_mapped_items() + { + ExcelTable table = ExcelPackage1.GetWorksheet("TEST1").GetTable("TEST1"); + + ExcelReadResult result = table.Read(); + + result.Items.Should().HaveCount(5); + result.Errors.Should().NotBeEmpty(); + result.Errors.Should().OnlyContain(error => error.Kind == ExcelReadErrorKind.Casting); + result.Errors.Should().OnlyContain(error => error.Context != null && error.Exception != null); + result.HasErrors.Should().BeTrue(); + result.IsSuccess.Should().BeFalse(); + } + + [Fact] + public void Read_should_capture_missing_column_mappings() + { + ExcelTable table = ExcelPackage1.GetWorksheet("TEST1").GetTable("TEST1"); + + ExcelReadResult result = table.Read(); + + result.Items.Should().HaveCount(5); + result.Errors.Should().ContainSingle(error => error.Kind == ExcelReadErrorKind.Mapping); + result.Errors.Single().Context.PropertyName.Should().Be(nameof(ObjectWithWrongAttributeMappings.LastName)); + } + + [Fact] + public void Read_should_capture_out_of_range_index_mappings() + { + ExcelTable table = ExcelPackage1.GetWorksheet("TEST1").GetTable("TEST1"); + + ExcelReadResult result = table.Read(); + + result.Items.Should().HaveCount(5); + result.Errors.Should().ContainSingle(error => error.Kind == ExcelReadErrorKind.Mapping); + result.Errors.Single().Context.PropertyName.Should().Be(nameof(MissingIndexMap.Value)); + } + + [Fact] + public void Read_should_report_a_mapping_error_when_the_type_has_no_column_attributes() + { + ExcelTable table = ExcelPackage1.GetWorksheet("TEST1").GetTable("TEST1"); + + ExcelReadResult result = table.Read(); + + result.Items.Should().BeEmpty(); + result.Errors.Should().ContainSingle(error => error.Kind == ExcelReadErrorKind.Mapping); + } } } diff --git a/test/EPPlus.Core.Extensions.Tests/ExcelWorksheetExtensions_Tests.cs b/test/EPPlus.Core.Extensions.Tests/ExcelWorksheetExtensions_Tests.cs index 5dda4c1..e59c45f 100644 --- a/test/EPPlus.Core.Extensions.Tests/ExcelWorksheetExtensions_Tests.cs +++ b/test/EPPlus.Core.Extensions.Tests/ExcelWorksheetExtensions_Tests.cs @@ -5,6 +5,7 @@ using System.Linq; using EPPlus.Core.Extensions.Exceptions; +using EPPlus.Core.Extensions.Results; using EPPlus.Core.Extensions.Style; using FluentAssertions; @@ -867,5 +868,24 @@ public void Should_not_throw_exception_if_a_column_is_marked_as_Optional_and_mis results.All(x => x.MissingColumn2 == null).Should().BeTrue(); results.All(x => x.MissingColumn3 == null).Should().BeTrue(); } + + [Fact] + public void Read_should_capture_data_annotation_validation_errors() + { + using var package = new ExcelPackage(); + ExcelWorksheet worksheet = package.Workbook.Worksheets.Add("Validation"); + worksheet.Cells[1, 1].Value = "Barcode"; + worksheet.Cells[1, 2].Value = "Quantity"; + worksheet.Cells[1, 3].Value = "UpdatedDate"; + worksheet.Cells[2, 1].Value = "ABC"; + worksheet.Cells[2, 2].Value = 1; + worksheet.Cells[2, 3].Value = DateTime.Today; + + ExcelReadResult result = worksheet.Read(); + + result.Items.Should().ContainSingle(); + result.Errors.Should().ContainSingle(error => error.Kind == ExcelReadErrorKind.Validation); + result.Errors.Single().Context.CellAddress.Address.Should().Be("B2"); + } } } diff --git a/test/EPPlus.Core.Extensions.Tests/FeatureTests.cs b/test/EPPlus.Core.Extensions.Tests/FeatureTests.cs index b5c7921..e07a68a 100644 --- a/test/EPPlus.Core.Extensions.Tests/FeatureTests.cs +++ b/test/EPPlus.Core.Extensions.Tests/FeatureTests.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.IO; using System.Linq; using EPPlus.Core.Extensions.Attributes; @@ -112,4 +113,24 @@ public void ToList_without_nested_columns_should_still_work() result[0].FirstName.Should().Be("Alice"); } } + + public class EpplusCompatibilityTests + { + [Fact] + public void Epplus_8_6_1_should_calculate_regex_formulas_and_round_trip_workbooks() + { + ExcelPackage.License.SetNonCommercialPersonal("EPPlus.Core.Extensions"); + using var package = new ExcelPackage(); + ExcelWorksheet worksheet = package.Workbook.Worksheets.Add("Regex"); + worksheet.Cells["A1"].Formula = "REGEXTEST(\"abc123\",\"[0-9]+\")"; + + package.Workbook.Calculate(); + + worksheet.Cells["A1"].Value.Should().Be(true); + + byte[] buffer = package.GetAsByteArray(); + using var reopened = new ExcelPackage(new MemoryStream(buffer)); + reopened.Workbook.Worksheets["Regex"].Cells["A1"].Formula.Should().EndWith("REGEXTEST(\"abc123\",\"[0-9]+\")"); + } + } } diff --git a/test/EPPlus.Core.Extensions.Tests/ObjectMother.cs b/test/EPPlus.Core.Extensions.Tests/ObjectMother.cs index 0d4d417..3a30779 100644 --- a/test/EPPlus.Core.Extensions.Tests/ObjectMother.cs +++ b/test/EPPlus.Core.Extensions.Tests/ObjectMother.cs @@ -293,4 +293,10 @@ internal class HomeAddress [ExcelTableColumn("City")] public string City { get; set; } } + + internal class MissingIndexMap + { + [ExcelTableColumn(999)] + public string Value { get; set; } + } } diff --git a/test/EPPlus.Core.Extensions.Tests/ToExcelExtensions_Tests.cs b/test/EPPlus.Core.Extensions.Tests/ToExcelExtensions_Tests.cs index 2789f92..afd74d7 100644 --- a/test/EPPlus.Core.Extensions.Tests/ToExcelExtensions_Tests.cs +++ b/test/EPPlus.Core.Extensions.Tests/ToExcelExtensions_Tests.cs @@ -668,5 +668,46 @@ public void Should_add_multiple_titles() worksheet.Cells[2, 1].Value.Should().Be("title 2"); worksheet.Cells[3, 1].Value.Should().Be("title 3"); } + + [Fact] + public void Export_should_offer_default_and_explicit_worksheet_names() + { + var people = new[] { new Person { LastName = "Lovelace", YearBorn = 1815 } }; + + using ExcelPackage defaultPackage = people.ToWorksheet().ToExcelPackage(); + defaultPackage.Workbook.Worksheets.Single().Name.Should().Be(nameof(Person)); + + byte[] buffer = people.ToXlsx("People", addHeaderRow: true); + using var namedPackage = new ExcelPackage(new MemoryStream(buffer)); + namedPackage.Workbook.Worksheets.Single().Name.Should().Be("People"); + } + + [Fact] + public void Export_should_enumerate_source_rows_once() + { + var enumerationCount = 0; + + IEnumerable Rows() + { + enumerationCount++; + yield return new Person { LastName = "Lovelace", YearBorn = 1815 }; + yield return new Person { LastName = "Hopper", YearBorn = 1906 }; + } + + using ExcelPackage package = Rows().ToExcelPackage(); + + enumerationCount.Should().Be(1); + package.Workbook.Worksheets.Single().Dimension.Rows.Should().Be(3); + } + + [Fact] + public void Existing_export_default_literal_call_should_remain_unambiguous() + { + var people = new[] { new Person { LastName = "Lovelace", YearBorn = 1815 } }; + + byte[] buffer = people.ToXlsx(default); + + buffer.Should().NotBeEmpty(); + } } }