diff --git a/.gitignore b/.gitignore
index fd71b28..7064b2c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -290,3 +290,4 @@ __pycache__/
*.odx.cs
*.xsd.cs
nuget.txt
+/Benchmark/BenchmarkDotNet.Artifacts/*
diff --git a/ClosedXML.Report.Benchmarks/Benchmarks/ReportBenchmarks.cs b/ClosedXML.Report.Benchmarks/Benchmarks/ReportBenchmarks.cs
new file mode 100644
index 0000000..5abc2c6
--- /dev/null
+++ b/ClosedXML.Report.Benchmarks/Benchmarks/ReportBenchmarks.cs
@@ -0,0 +1,46 @@
+using System.Reflection;
+using BenchmarkDotNet.Attributes;
+using ClosedXML.Report.Benchmarks.Models;
+
+namespace ClosedXML.Report.Benchmarks.Benchmarks;
+
+[MemoryDiagnoser]
+public class ReportBenchmarks
+{
+ private Customer _customer = null!;
+ private byte[] _templateData = [];
+ const string ResourceName = "ClosedXML.Report.Benchmarks.Resources.Benchmark.xlsx";
+
+ [GlobalSetup]
+ public void Setup()
+ {
+ // Load embedded resource outside of the benchmark
+ using var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(ResourceName);
+ if (stream == null)
+ throw new Exception($"Resource {ResourceName} not found");
+
+ // Create a memory stream to hold the resource data
+ var memoryStream = new MemoryStream();
+ stream.CopyTo(memoryStream);
+ _templateData = memoryStream.ToArray();
+ memoryStream.Position = 0;
+
+ // Prepare data
+ var dataBuilder = new DataBuilder();
+ _customer = dataBuilder.Create();
+ }
+
+ [Benchmark]
+ public void ReportGeneration()
+ {
+ using var memoryStream = new MemoryStream(_templateData);
+ using var xlTemplate = new XLTemplate(memoryStream);
+
+ // Add variables and generate report
+ xlTemplate.AddVariable(_customer);
+ xlTemplate.Generate();
+
+ //Not testing the save, as it has a high overhead.
+ //xlTemplate.SaveAs(Path.Combine("Output", "BenchmarkOutput.xlsx"));
+ }
+}
\ No newline at end of file
diff --git a/ClosedXML.Report.Benchmarks/ClosedXML.Report.Benchmarks.csproj b/ClosedXML.Report.Benchmarks/ClosedXML.Report.Benchmarks.csproj
new file mode 100644
index 0000000..51cd886
--- /dev/null
+++ b/ClosedXML.Report.Benchmarks/ClosedXML.Report.Benchmarks.csproj
@@ -0,0 +1,25 @@
+
+
+
+ Exe
+ net8.0
+ enable
+ enable
+ default
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ClosedXML.Report.Benchmarks/Models/Customer.cs b/ClosedXML.Report.Benchmarks/Models/Customer.cs
new file mode 100644
index 0000000..ab5a144
--- /dev/null
+++ b/ClosedXML.Report.Benchmarks/Models/Customer.cs
@@ -0,0 +1,27 @@
+namespace ClosedXML.Report.Benchmarks.Models;
+
+public class Customer
+{
+ public required string Company { get; set; }
+
+ public required string Addr1 { get; set; }
+
+ public required string Addr2 { get; set; }
+
+
+ public required string City { get; set; }
+
+ public required string State { get; set; }
+
+ public required string Country { get; set; }
+
+ public required string Phone { get; set; }
+
+ public required string Email { get; set; }
+
+ public required string Zip { get; set; }
+
+ public string Fax { get; set; } = string.Empty;
+
+ public List Orders { get; init; } = [];
+}
\ No newline at end of file
diff --git a/ClosedXML.Report.Benchmarks/Models/Order.cs b/ClosedXML.Report.Benchmarks/Models/Order.cs
new file mode 100644
index 0000000..5221c57
--- /dev/null
+++ b/ClosedXML.Report.Benchmarks/Models/Order.cs
@@ -0,0 +1,22 @@
+namespace ClosedXML.Report.Benchmarks.Models;
+
+public class Order
+{
+ public required string OrderNo { get; set; }
+
+ public DateTime SaleDate { get; set; }
+
+ public DateTime ShipDate { get; set; }
+
+ public string ShipToAddr1 { get; set; } = string.Empty;
+
+ public string ShipToAddr2 { get; set; } = string.Empty;
+
+ public string PaymentMethod { get; set; } = string.Empty;
+
+ public int ItemsTotal { get; set; }
+
+ public decimal TaxRate { get; set; }
+
+ public decimal AmountPaid { get; set; }
+}
\ No newline at end of file
diff --git a/ClosedXML.Report.Benchmarks/Program.cs b/ClosedXML.Report.Benchmarks/Program.cs
new file mode 100644
index 0000000..4797db5
--- /dev/null
+++ b/ClosedXML.Report.Benchmarks/Program.cs
@@ -0,0 +1,58 @@
+using BenchmarkDotNet.Running;
+using ClosedXML.Report.Benchmarks.Models;
+
+namespace ClosedXML.Report.Benchmarks;
+
+public class Program
+{
+ static void Main(string[] args)
+ {
+ BenchmarkRunner.Run(typeof(Program).Assembly);
+ }
+}
+
+public class DataBuilder
+{
+ public Order CreateOrder(int orderNo = 1, int amount = 10000)
+ {
+ var orderNoStr = orderNo.ToString("D6");
+ var isOdd = orderNo % 2 == 1;
+
+ return new Order
+ {
+ AmountPaid = amount,
+ ItemsTotal = isOdd ? 1 : 2,
+ OrderNo = orderNoStr,
+ PaymentMethod = isOdd ? "Credit" : "Visa",
+ SaleDate = DateTime.Now,
+ ShipDate = DateTime.Now,
+ ShipToAddr1 = "ShipToAddr1",
+ ShipToAddr2 = "ShipToAddr2",
+ TaxRate = 0.1m
+ };
+ }
+
+ public Customer Create()
+ {
+ var c = new Customer
+ {
+ City = "City",
+ Addr1 = "1 Main St",
+ Addr2 = "Townsville",
+ Company = "Company Name",
+ Country = "Australia",
+ Email = "boss@gmail.com",
+ Fax = "0011 123 456 789",
+ Phone = "0011 123 456 789",
+ State = "Qld",
+ Zip = "4000",
+ Orders = []
+ };
+ for (var i = 0; i < 10000; i++)
+ {
+ c.Orders.Add(CreateOrder(i, 100 + i));
+ }
+
+ return c;
+ }
+}
\ No newline at end of file
diff --git a/ClosedXML.Report.Benchmarks/Resources/Benchmark.xlsx b/ClosedXML.Report.Benchmarks/Resources/Benchmark.xlsx
new file mode 100644
index 0000000..5be7377
Binary files /dev/null and b/ClosedXML.Report.Benchmarks/Resources/Benchmark.xlsx differ
diff --git a/ClosedXML.Report.sln b/ClosedXML.Report.sln
index fd5a12a..16b2e22 100644
--- a/ClosedXML.Report.sln
+++ b/ClosedXML.Report.sln
@@ -11,6 +11,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{8438BAA8
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ClosedXML.Report.Tests", "tests\ClosedXML.Report.Tests\ClosedXML.Report.Tests.csproj", "{D529A371-3AEA-4D02-9A0D-308A6276D411}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ClosedXML.Report.Benchmarks", "ClosedXML.Report.Benchmarks\ClosedXML.Report.Benchmarks.csproj", "{E61B9B2F-96AF-4B19-9327-C7B724BA0B1D}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -25,6 +27,10 @@ Global
{D529A371-3AEA-4D02-9A0D-308A6276D411}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D529A371-3AEA-4D02-9A0D-308A6276D411}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D529A371-3AEA-4D02-9A0D-308A6276D411}.Release|Any CPU.Build.0 = Release|Any CPU
+ {E61B9B2F-96AF-4B19-9327-C7B724BA0B1D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {E61B9B2F-96AF-4B19-9327-C7B724BA0B1D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {E61B9B2F-96AF-4B19-9327-C7B724BA0B1D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {E61B9B2F-96AF-4B19-9327-C7B724BA0B1D}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -32,6 +38,7 @@ Global
GlobalSection(NestedProjects) = preSolution
{DEF4A219-D1CF-42A2-A5AC-FE7F2F005AB0} = {885EF47A-C124-45F0-B979-303B30FB586B}
{D529A371-3AEA-4D02-9A0D-308A6276D411} = {8438BAA8-E968-4C14-9500-E3A52434E32A}
+ {E61B9B2F-96AF-4B19-9327-C7B724BA0B1D} = {8438BAA8-E968-4C14-9500-E3A52434E32A}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {42DA0EC5-D733-4746-9613-413A64EEDC4B}
diff --git a/ClosedXML.Report/Excel/CellPosition.cs b/ClosedXML.Report/Excel/CellPosition.cs
new file mode 100644
index 0000000..1d48dde
--- /dev/null
+++ b/ClosedXML.Report/Excel/CellPosition.cs
@@ -0,0 +1,14 @@
+namespace ClosedXML.Report.Excel;
+
+struct CellPosition
+{
+ public CellPosition(int row, int column)
+ {
+ Row = row;
+ Column = column;
+ }
+
+ public int Row { get; set; }
+
+ public int Column { get; set; }
+}
\ No newline at end of file
diff --git a/ClosedXML.Report/Excel/Subtotal.cs b/ClosedXML.Report/Excel/Subtotal.cs
index 76d0af5..9b2b052 100644
--- a/ClosedXML.Report/Excel/Subtotal.cs
+++ b/ClosedXML.Report/Excel/Subtotal.cs
@@ -298,7 +298,7 @@ public SubtotalGroup[] ScanForGroups(int groupBy)
var result = new List(grRanges.Length);
var rows = Sheet.Rows(_range.RangeAddress.FirstAddress.RowNumber, _range.RangeAddress.LastAddress.RowNumber);
if (!rows.Any())
- return new SubtotalGroup[0];
+ return Array.Empty();
var level = Math.Min(8, rows.Max(r => r.OutlineLevel) + 1);
diff --git a/ClosedXML.Report/Excel/SubtotalGroup.cs b/ClosedXML.Report/Excel/SubtotalGroup.cs
index c810162..75eca77 100644
--- a/ClosedXML.Report/Excel/SubtotalGroup.cs
+++ b/ClosedXML.Report/Excel/SubtotalGroup.cs
@@ -1,4 +1,3 @@
-using System;
using ClosedXML.Excel;
namespace ClosedXML.Report.Excel
diff --git a/ClosedXML.Report/Excel/SubtotalSummaryFunc.cs b/ClosedXML.Report/Excel/SubtotalSummaryFunc.cs
index 8d1bd51..f617395 100644
--- a/ClosedXML.Report/Excel/SubtotalSummaryFunc.cs
+++ b/ClosedXML.Report/Excel/SubtotalSummaryFunc.cs
@@ -1,14 +1,13 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
-using System.Linq.Expressions;
using ClosedXML.Report.Utils;
namespace ClosedXML.Report.Excel
{
public class SubtotalSummaryFunc
{
- private static readonly Dictionary> TotalFuncs = new Dictionary>
+ private static readonly Dictionary> TotalFuncs = new()
{
{"average", new FuncData(1)},
{"avg", new FuncData(1)},
@@ -27,7 +26,7 @@ public class SubtotalSummaryFunc
private static IFuncData GetFunc(string funcName)
{
- var func = TotalFuncs.ContainsKey(funcName) ? TotalFuncs[funcName] : null;
+ var func = TotalFuncs.TryGetValue(funcName, out var totalFunc) ? totalFunc : null;
if (func == null)
Debug.WriteLine("Unknown function " + funcName);
return func;
@@ -70,7 +69,6 @@ internal object Calculate(IDataSource dataSource)
var agg = _func.CreateAggregator();
var dlg = GetCalculateDelegate(items[0].GetType());
- //var dlg = lambda.Compile();
foreach (var item in items)
{
try
@@ -171,25 +169,25 @@ public void Aggregate(object value)
private class CountAggregator : IAggregator
{
- private int _cnt = 0;
+ private int _cnt;
public void Aggregate(object value)
{
if (value.GetType().IsNumeric())
_cnt++;
}
- public object Result { get { return _cnt; } }
+ public object Result => _cnt;
}
private class CountAAggregator : IAggregator
{
- private int _cnt = 0;
+ private int _cnt;
public void Aggregate(object value)
{
_cnt++;
}
- public object Result { get { return _cnt; } }
+ public object Result => _cnt;
}
private class MinAggregator : IAggregator
@@ -206,7 +204,7 @@ public void Aggregate(object value)
_min = value;
}
- public object Result { get { return _min; } }
+ public object Result => _min;
}
private class MaxAggregator : IAggregator
@@ -223,12 +221,12 @@ public void Aggregate(object value)
_max = value;
}
- public object Result { get { return _max; } }
+ public object Result => _max;
}
private class AverageAggregator : IAggregator
{
- protected readonly List