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
66 changes: 66 additions & 0 deletions .github/workflows/publish-nuget.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
name: Publish NuGet package

on:
pull_request:
branches:
- main
types:
- closed

permissions:
contents: read

concurrency:
group: nuget-publish
cancel-in-progress: false

jobs:
publish:
name: Build, test, and publish
if: >-
github.event.pull_request.merged == true &&
github.event.pull_request.head.ref == 'develop' &&
github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
timeout-minutes: 20
environment: nuget
permissions:
contents: read
id-token: write

steps:
- name: Check out the merged commit
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.event.pull_request.merge_commit_sha }}
fetch-depth: 0

- name: Set up .NET
uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
with:
dotnet-version: 10.0.x

- name: Restore
run: dotnet restore ExcelWizard.sln

- name: Build
run: dotnet build ExcelWizard.sln --configuration Release --no-restore -p:ContinuousIntegrationBuild=true

- name: Test
run: dotnet test ExcelWizard.sln --configuration Release --no-build --no-restore

- name: Pack
run: dotnet pack ExcelWizard/ExcelWizard.csproj --configuration Release --no-build --no-restore --output artifacts -p:ContinuousIntegrationBuild=true

# Request the one-hour credential immediately before publishing.
- name: Sign in to NuGet.org with OIDC
id: login
uses: NuGet/login@8d196754b4036150537f80ac539e15c2f1028841 # v1.2.0
with:
user: ${{ secrets.NUGET_USER }}

- name: Publish package
run: >-
dotnet nuget push "artifacts/*.nupkg"
--api-key "${{ steps.login.outputs.NUGET_API_KEY }}"
--source https://api.nuget.org/v3/index.json
34 changes: 34 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Changelog

All notable changes to ExcelWizard are documented in this file.

## 3.1.0 - 2026-08-25

### Added

- Added `WrapText` to `ExcelSheetColumnAttribute` for model-bound grid columns.
- Added `WrapText` to `ExcelTableColumnAttribute` for model-bound table columns.
- Added `CellStyle.WrapText` for manually built cells and retained `CellStyle.Wordwrap` as a compatible alias.
- Added automatic row-height expansion for wrapped content when no explicit row height is configured.
- Added a generated-workbook integration test project alongside the unit test suite.

### Changed

- Updated ClosedXML to 0.105.1 and BlazorDownloadFile to 2.4.0.2.
- Overrode BlazorDownloadFile's legacy JSON dependency chain with System.Text.Json 10.0.11 to remove a critical transitive security advisory.
- Updated the test stack while retaining FluentAssertions 7 under its Apache 2.0 license.
- Updated the API and Blazor sample applications, unit tests, and integration tests to .NET 10.
- Preserved the published library target at .NET Standard 2.1 for consumer compatibility.
- Updated cell value handling for the current ClosedXML typed-value API.

### Fixed

- Preserved explicit row heights when wrapped text is enabled.
- Corrected cell border style mapping to ClosedXML border values.
- Made cell-level styles consistently override row-level styles.
- Validated invalid number, currency, and date values with clear exceptions.

### Testing

- Added unit coverage for wrap defaults, the legacy alias, column attributes, and builder mapping.
- Added integration coverage for grid, table, and manual wrapping; automatic and explicit row heights; multiline and Persian/English text; cell content types; metadata; multiple sheets; merged cells; RTL layout; protection; fills; and borders.
121 changes: 121 additions & 0 deletions ExcelWizard.IntegrationTests/CellContentTypeWorkbookTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
using ClosedXML.Excel;
using ExcelWizard.Models;
using ExcelWizard.Models.EWCell;
using ExcelWizard.Models.EWExcel;
using ExcelWizard.Models.EWRow;
using ExcelWizard.Models.EWSheet;

namespace ExcelWizard.IntegrationTests;

public class CellContentTypeWorkbookTests
{
[Fact]
public void AllSupportedCellContentTypes_RoundTripWithExpectedExcelTypes()
{
var date = new DateTime(2026, 8, 25, 10, 30, 0, DateTimeKind.Unspecified);
var cells = new[]
{
CreateCell(1, "general", CellContentType.General),
CreateCell(2, 42.5m, CellContentType.Number),
CreateCell(3, 1234.6m, CellContentType.Currency),
CreateCell(4, date, CellContentType.GregorianDateTime),
CreateCell(5, 123, CellContentType.Text),
CreateCell(6, 25, CellContentType.Percentage),
CreateCell(7, "B1*2", CellContentType.Formula),
CreateCell(8, true, CellContentType.General),
CreateCell(9, null, CellContentType.General)
};

using var workbook = WorkbookTestHelper.GenerateWorkbook(BuildWorkbook(cells));
var worksheet = workbook.Worksheet("Cell Types");

worksheet.Cell("A1").DataType.Should().Be(XLDataType.Text);
worksheet.Cell("A1").GetString().Should().Be("general");
worksheet.Cell("B1").DataType.Should().Be(XLDataType.Number);
worksheet.Cell("B1").GetDouble().Should().Be(42.5);
worksheet.Cell("C1").DataType.Should().Be(XLDataType.Number);
worksheet.Cell("C1").GetDouble().Should().Be(1235);
worksheet.Cell("C1").Style.NumberFormat.Format.Should().Be("#,##0");
worksheet.Cell("D1").DataType.Should().Be(XLDataType.DateTime);
worksheet.Cell("D1").GetDateTime().Should().Be(date);
worksheet.Cell("E1").DataType.Should().Be(XLDataType.Text);
worksheet.Cell("E1").GetString().Should().Be("123");
worksheet.Cell("F1").DataType.Should().Be(XLDataType.Text);
worksheet.Cell("F1").GetString().Should().Be("25%");
worksheet.Cell("G1").HasFormula.Should().BeTrue();
worksheet.Cell("G1").FormulaA1.Should().Be("B1*2");
worksheet.Cell("H1").DataType.Should().Be(XLDataType.Boolean);
worksheet.Cell("H1").GetBoolean().Should().BeTrue();
worksheet.Cell("I1").IsEmpty().Should().BeTrue();
}

[Fact]
public void NumberContentType_ParsesNumericText()
{
var cell = CreateCell(1, "123.5", CellContentType.Number);

using var workbook = WorkbookTestHelper.GenerateWorkbook(BuildWorkbook(new[] { cell }));

workbook.Worksheet("Cell Types").Cell("A1").GetDouble().Should().Be(123.5);
}

[Fact]
public void NumberContentType_RejectsNonNumericValues()
{
var cell = CreateCell(1, "not-a-number", CellContentType.Number);

var action = () => WorkbookTestHelper.GenerateWorkbook(BuildWorkbook(new[] { cell }));

action.Should().Throw<Exception>().WithMessage("*Number CellType*");
}

[Fact]
public void CurrencyContentType_RejectsNonNumericValues()
{
var cell = CreateCell(1, "not-currency", CellContentType.Currency);

var action = () => WorkbookTestHelper.GenerateWorkbook(BuildWorkbook(new[] { cell }));

action.Should().Throw<Exception>().WithMessage("*Currency CellType*");
}

[Fact]
public void GregorianDateTimeContentType_RejectsNonDateValues()
{
var cell = CreateCell(1, "2026-08-25", CellContentType.GregorianDateTime);

var action = () => WorkbookTestHelper.GenerateWorkbook(BuildWorkbook(new[] { cell }));

action.Should().Throw<Exception>().WithMessage("*GregorianDateTime CellType*");
}

private static Cell CreateCell(int column, object? value, CellContentType contentType)
{
return CellBuilder.SetLocation(column, 1)
.SetValue(value)
.SetContentType(contentType)
.Build();
}

private static IExcelBuilder BuildWorkbook(IEnumerable<Cell> cells)
{
var row = RowBuilder
.SetCells(cells.Cast<ICellBuilder>().ToList())
.RowHasNoMerging()
.RowHasNoCustomStyle()
.Build();
var sheet = SheetBuilder
.SetName("Cell Types")
.SetRows(row)
.NoMoreTablesRowsOrCells()
.SheetHasNoCustomStyle()
.Build();

return ExcelBuilder
.SetGeneratedFileName("cell-types")
.CreateComplexLayoutExcel()
.SetSheets(sheet)
.SheetsHaveNoDefaultStyle()
.Build();
}
}
30 changes: 30 additions & 0 deletions ExcelWizard.IntegrationTests/ExcelWizard.IntegrationTests.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<OutputType>Exe</OutputType>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="ClosedXML" Version="0.105.1" />
<PackageReference Include="FluentAssertions" Version="7.2.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.9.0" />
<PackageReference Include="xunit.v3.mtp-off" Version="4.0.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="4.0.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="10.0.1">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\ExcelWizard\ExcelWizard.csproj" />
</ItemGroup>

</Project>
2 changes: 2 additions & 0 deletions ExcelWizard.IntegrationTests/Usings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
global using FluentAssertions;
global using Xunit;
97 changes: 97 additions & 0 deletions ExcelWizard.IntegrationTests/WorkbookRegressionTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
using System.Drawing;
using ClosedXML.Excel;
using ExcelWizard.Models;
using ExcelWizard.Models.EWCell;
using ExcelWizard.Models.EWExcel;
using ExcelWizard.Models.EWMerge;
using ExcelWizard.Models.EWRow;
using ExcelWizard.Models.EWSheet;
using ExcelWizard.Models.EWStyles;
using ExcelWizard.Service;

namespace ExcelWizard.IntegrationTests;

public class WorkbookRegressionTests
{
[Fact]
public void GeneratedFile_PreservesRequestedMetadataAndCanBeReopened()
{
var service = new ExcelWizardService(new FakeBlazorDownloadFileService());
var file = service.GenerateExcel(BuildComplexWorkbook());

file.FileName.Should().Be("regression-workbook");
file.Extension.Should().Be("xlsx");
file.MimeType.Should().Be("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
file.Content.Should().NotBeNullOrEmpty();

using var workbook = new XLWorkbook(new MemoryStream(file.Content!));
workbook.Worksheets.Count.Should().Be(2);
}

[Fact]
public void ComplexWorkbook_RoundTripsMergesStylesDirectionAndProtection()
{
using var workbook = WorkbookTestHelper.GenerateWorkbook(BuildComplexWorkbook());
var worksheet = workbook.Worksheet("Protected RTL");

worksheet.RightToLeft.Should().BeTrue();
worksheet.Protection.IsProtected.Should().BeTrue();
worksheet.Range("A1:B1").IsMerged().Should().BeTrue();
worksheet.Cell("A1").GetString().Should().Be("Merged title");
worksheet.Cell("A1").Style.Fill.BackgroundColor.Color.ToArgb().Should().Be(Color.LightBlue.ToArgb());
worksheet.Cell("A2").Style.Border.LeftBorder.Should().Be(XLBorderStyleValues.Thick);
workbook.Worksheet("Second Sheet").Cell("A1").GetString().Should().Be("second");
}

private static IExcelBuilder BuildComplexWorkbook()
{
var titleRow = RowBuilder
.SetCells(
CellBuilder.SetLocation("A", 1).SetValue("Merged title").Build(),
CellBuilder.SetLocation("B", 1).Build())
.RowHasNoMerging()
.SetRowStyle(new RowStyle { BackgroundColor = Color.LightBlue })
.Build();
var dataRow = RowBuilder
.SetCells(
CellBuilder.SetLocation("A", 2)
.SetValue("bordered")
.SetCellStyle(new CellStyle
{
CellBorder = new Border(LineStyle.Thick, Color.DarkBlue)
})
.Build())
.RowHasNoMerging()
.RowHasNoCustomStyle()
.Build();
var merge = MergeBuilder
.SetMergingStartPoint("A", 1)
.SetMergingFinishPoint("B", 1)
.SetMergingAreaBackgroundColor(Color.LightBlue)
.Build();
var protectedSheet = SheetBuilder
.SetName("Protected RTL")
.SetRows(titleRow, dataRow)
.NoMoreTablesRowsOrCells()
.SetSheetStyle(new SheetStyle { SheetDirection = SheetDirection.RightToLeft })
.SetMergedCells(merge)
.SetSheetLocked(true)
.SetSheetProtected()
.SetProtectionLevel(new ProtectionLevel { Password = "test", HardProtect = true })
.Build();
var secondSheet = SheetBuilder
.SetName("Second Sheet")
.SetCells(CellBuilder.SetLocation("A", 1).SetValue("second").Build())
.NoMoreTablesRowsOrCells()
.SheetHasNoCustomStyle()
.Build();

return ExcelBuilder
.SetGeneratedFileName("regression-workbook")
.CreateComplexLayoutExcel()
.SetSheets(protectedSheet, secondSheet)
.SheetsHaveNoDefaultStyle()
.SetDefaultLockedStatus(false)
.Build();
}
}
18 changes: 18 additions & 0 deletions ExcelWizard.IntegrationTests/WorkbookTestHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using ClosedXML.Excel;
using ExcelWizard.Models;
using ExcelWizard.Service;

namespace ExcelWizard.IntegrationTests;

internal static class WorkbookTestHelper
{
public static XLWorkbook GenerateWorkbook(IExcelBuilder builder)
{
var service = new ExcelWizardService(new FakeBlazorDownloadFileService());
var generatedFile = service.GenerateExcel(builder);

generatedFile.Content.Should().NotBeNullOrEmpty();

return new XLWorkbook(new MemoryStream(generatedFile.Content!));
}
}
Loading