diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..c834108
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,10 @@
+.dockerignore
+Dockerfile
+Dockerfile.local
+.env
+.gitignore
+.vs
+.vscode
+**/bin
+**/obj
+**/.toolstarget
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index ed6d1d2..3f7ac83 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,12 +1,67 @@
-# ---> VisualStudio
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
-##
-## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
+
+###################
+# compiled source #
+###################
+*.com
+*.class
+*.dll
+*.exe
+*.pdb
+*.dll.config
+*.cache
+*.suo
+# Include dlls if they’re in the NuGet packages directory
+!/packages/*/lib/*.dll
+# Include dlls if they're in the CommonReferences directory
+!*CommonReferences/*.dll
+####################
+# VS Upgrade stuff #
+####################
+_UpgradeReport_Files/
+###############
+# Directories #
+###############
+bin/
+obj/
+TestResults/
+###################
+# Web publish log #
+###################
+*.Publish.xml
+#############
+# Resharper #
+#############
+/_ReSharper.*
+*.ReSharper.*
+############
+# Packages #
+############
+# it’s better to unpack these files and commit the raw source
+# git has its own built in compression methods
+*.7z
+*.dmg
+*.gz
+*.iso
+*.jar
+*.rar
+*.tar
+*.zip
+######################
+# Logs and databases #
+######################
+*.log
+*.sqlite
+# OS generated files #
+######################
+.DS_Store?
+ehthumbs.db
+Icon?
+Thumbs.db
+
# User-specific files
-*.rsuser
-*.suo
*.user
*.userosscache
*.sln.docstates
@@ -14,9 +69,6 @@
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
-# Mono auto generated files
-mono_crash.*
-
# Build results
[Dd]ebug/
[Dd]ebugPublic/
@@ -24,76 +76,47 @@ mono_crash.*
[Rr]eleases/
x64/
x86/
-[Ww][Ii][Nn]32/
-[Aa][Rr][Mm]/
-[Aa][Rr][Mm]64/
+build/
bld/
[Bb]in/
[Oo]bj/
-[Ll]og/
-[Ll]ogs/
-# Visual Studio 2015/2017 cache/options directory
+# Visual Studo 2015 cache/options directory
.vs/
-# Uncomment if you have tasks that create the project's static files in wwwroot
-#wwwroot/
-
-# Visual Studio 2017 auto generated files
-Generated\ Files/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
-# NUnit
+# NUNIT
*.VisualState.xml
TestResult.xml
-nunit-*.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
-# Benchmark Results
-BenchmarkDotNet.Artifacts/
-
-# .NET Core
+# DNX
project.lock.json
-project.fragment.lock.json
artifacts/
-# ASP.NET Scaffolding
-ScaffoldingReadMe.txt
-
-# StyleCop
-StyleCopReport.xml
-
-# Files built by Visual Studio
*_i.c
*_p.c
-*_h.h
+*_i.h
*.ilk
*.meta
*.obj
-*.iobj
*.pch
-*.pdb
-*.ipdb
*.pgc
*.pgd
*.rsp
-# but not Directory.Build.rsp, as it configures directory-level build defaults
-!Directory.Build.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
-*_wpftmp.csproj
-*.log
-*.tlog
*.vspscc
*.vssscc
.builds
@@ -108,21 +131,14 @@ _Chutzpah*
ipch/
*.aps
*.ncb
-*.opendb
*.opensdf
*.sdf
*.cachefile
-*.VC.db
-*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
-*.sap
-
-# Visual Studio Trace Files
-*.e2e
# TFS 2012 Local Workspace
$tf/
@@ -135,29 +151,18 @@ _ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
+# JustCode is a .NET coding add-in
+.JustCode
+
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
-# AxoCover is a Code Coverage Tool
-.axoCover/*
-!.axoCover/settings.json
-
-# Coverlet is a free, cross platform Code Coverage Tool
-coverage*.json
-coverage*.xml
-coverage*.info
-
-# Visual Studio code coverage results
-*.coverage
-*.coveragexml
-
# NCrunch
_NCrunch_*
.*crunch*.local.xml
-nCrunchTemp_*
# MightyMoose
*.mm.*
@@ -181,76 +186,50 @@ DocProject/Help/html
# Click-Once directory
publish/
+ClickOnce/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
-# Note: Comment the next line if you want to checkin your web deploy settings,
+# TODO: Comment the next line if you want to checkin your web deploy settings
# but database connection strings (with potential passwords) will be unencrypted
-*.pubxml
*.publishproj
-# Microsoft Azure Web App publish settings. Comment the next line if you want to
-# checkin your Azure Web App publish settings, but sensitive information contained
-# in these scripts will be unencrypted
-PublishScripts/
-
# NuGet Packages
*.nupkg
-# NuGet Symbol Packages
-*.snupkg
# The packages folder can be ignored because of Package Restore
-**/[Pp]ackages/*
+**/packages/*
# except build/, which is used as an MSBuild target.
-!**/[Pp]ackages/build/
+!**/packages/build/
# Uncomment if necessary however generally it will be regenerated when needed
-#!**/[Pp]ackages/repositories.config
-# NuGet v3's project.json files produces more ignorable files
-*.nuget.props
-*.nuget.targets
+#!**/packages/repositories.config
-# Microsoft Azure Build Output
+# Windows Azure Build Output
csx/
*.build.csdef
-# Microsoft Azure Emulator
-ecf/
-rcf/
-
-# Windows Store app package directories and files
+# Windows Store app package directory
AppPackages/
-BundleArtifacts/
-Package.StoreAssociation.xml
-_pkginfo.txt
-*.appx
-*.appxbundle
-*.appxupload
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
-!?*.[Cc]ache/
+!*.[Cc]ache/
# Others
ClientBin/
+[Ss]tyle[Cc]op.*
~$*
*~
*.dbmdl
*.dbproj.schemaview
-*.jfm
*.pfx
*.publishsettings
+node_modules/
+bower_components/
orleans.codegen.cs
-# Including strong name files can present a security risk
-# (https://github.com/github/gitignore/pull/2483#issue-259490424)
-#*.snk
-
-# Since there are multiple workflows, uncomment next line to ignore bower_components
-# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
-#bower_components/
-
# RIA/Silverlight projects
Generated_Code/
@@ -261,32 +240,21 @@ _UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
-ServiceFabricBackup/
-*.rptproj.bak
# SQL Server files
*.mdf
*.ldf
-*.ndf
-
+**/node_modules/*
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
-*.rptproj.rsuser
-*- [Bb]ackup.rdl
-*- [Bb]ackup ([0-9]).rdl
-*- [Bb]ackup ([0-9][0-9]).rdl
# Microsoft Fakes
FakesAssemblies/
-# GhostDoc plugin setting file
-*.GhostDoc.xml
-
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
-node_modules/
# Visual Studio 6 build log
*.plg
@@ -294,109 +262,9 @@ node_modules/
# Visual Studio 6 workspace options file
*.opt
-# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
-*.vbw
-
-# Visual Studio 6 auto-generated project file (contains which files were open etc.)
-*.vbp
-
-# Visual Studio 6 workspace and project file (working project files containing files to include in project)
-*.dsw
-*.dsp
-
-# Visual Studio 6 technical files
-*.ncb
-*.aps
-
-# Visual Studio LightSwitch build output
-**/*.HTMLClient/GeneratedArtifacts
-**/*.DesktopClient/GeneratedArtifacts
-**/*.DesktopClient/ModelManifest.xml
-**/*.Server/GeneratedArtifacts
-**/*.Server/ModelManifest.xml
-_Pvt_Extensions
-
-# Paket dependency manager
-.paket/paket.exe
-paket-files/
-
-# FAKE - F# Make
-.fake/
-
-# CodeRush personal settings
-.cr/personal
-
-# Python Tools for Visual Studio (PTVS)
-__pycache__/
-*.pyc
-
-# Cake - Uncomment if you are using it
-# tools/**
-# !tools/packages.config
-
-# Tabs Studio
-*.tss
-
-# Telerik's JustMock configuration file
-*.jmconfig
-
-# BizTalk build output
-*.btp.cs
-*.btm.cs
-*.odx.cs
-*.xsd.cs
-
-# OpenCover UI analysis results
-OpenCover/
-
-# Azure Stream Analytics local run output
-ASALocalRun/
-
-# MSBuild Binary and Structured Log
-*.binlog
-
-# NVidia Nsight GPU debugger configuration file
-*.nvuser
-
-# MFractors (Xamarin productivity tool) working folder
-.mfractor/
-
-# Local History for Visual Studio
-.localhistory/
-
-# Visual Studio History (VSHistory) files
-.vshistory/
-
-# BeatPulse healthcheck temp database
-healthchecksdb
-
-# Backup folder for Package Reference Convert tool in Visual Studio 2017
-MigrationBackup/
-
-# Ionide (cross platform F# VS Code tools) working folder
-.ionide/
-
-# Fody - auto-generated XML schema
-FodyWeavers.xsd
-
-# VS Code files for those working on multiple tools
-.vscode/*
-!.vscode/settings.json
-!.vscode/tasks.json
-!.vscode/launch.json
-!.vscode/extensions.json
-*.code-workspace
-
-# Local History for Visual Studio Code
-.history/
-
-# Windows Installer files from build outputs
-*.cab
-*.msi
-*.msix
-*.msm
-*.msp
-
-# JetBrains Rider
-*.sln.iml
+# SonarQube files
+sonarqube/
+.vs/
+*.orig
+buildlogs/
\ No newline at end of file
diff --git a/Dockerfile.lib b/Dockerfile.lib
new file mode 100644
index 0000000..0e66784
--- /dev/null
+++ b/Dockerfile.lib
@@ -0,0 +1,89 @@
+ARG PROJECT=Strata.Excel.Core
+ARG VERSION=0.0.0
+ARG SONARURL=''
+ARG SONARLOGIN=''
+ARG SONARBRANCH=''
+
+###############
+# Build image #
+###############
+FROM ecr.ops.stratanetwork.net/strata.microsoft.dotnet.sdk:6.0 AS build
+ARG PROJECT
+ARG VERSION
+ARG SONARLOGIN
+ARG SONARURL
+ARG SONARBRANCH
+
+RUN apt-get --allow-releaseinfo-change update && apt-get install -y libgdiplus
+
+WORKDIR /src
+
+# Copy project files and restore
+COPY ["${PROJECT}.sln", "${PROJECT}.sln"]
+COPY src/**/*.csproj ./
+RUN for file in $(ls *.csproj); do mkdir -p src/${file%.*}/ && mv $file src/${file%.*}/; done
+COPY tests/**/*.csproj ./
+RUN for file in $(ls *.csproj); do mkdir -p tests/${file%.*}/ && mv $file tests/${file%.*}/; done
+
+RUN dotnet restore "${PROJECT}.sln" \
+ --source https://api.nuget.org/v3/index.json \
+ --source https://proget.sdt.local/nuget/nuget/v3/index.json
+
+
+# Copy files and build
+COPY . .
+
+RUN if [ "${SONARLOGIN}" != "" ] ; then dotnet sonarscanner begin \
+ /k:"${PROJECT}" \
+ /d:sonar.scm.provider=git \
+ /d:sonar.host.url="${SONARURL}" \
+ /d:sonar.login="${SONARLOGIN}" \
+ ${SONARBRANCH} \
+ /d:sonar.cs.opencover.reportsPaths="/src/cover.xml" \
+ /d:sonar.dependencyCheck.jsonReportPath=/src/buildlogs/dependency-check-report.json \
+ /d:sonar.dependencyCheck.htmlReportPath=/src/buildlogs/dependency-check-report.html \
+ /v:sonar.projectVersion="${VERSION}"; fi
+
+
+RUN dotnet build "${PROJECT}.sln" \
+ --configuration Release \
+ --no-restore
+
+# Run unit tests
+RUN dotnet test "${PROJECT}.sln" \
+ --configuration Release \
+ --no-restore \
+ --no-build \
+ --verbosity=normal \
+ -p:CollectCoverage=true \
+ -p:CoverletOutputFormat="opencover" \
+ -p:CoverletOutput=/src/cover.xml
+
+RUN dotnet pack "src/Strata.Excel.Core/Strata.Excel.Core.csproj" \
+ --configuration Release \
+ --no-restore \
+ --no-build \
+ --include-symbols \
+ --include-source \
+ --output /pack \
+ -property:PackageVersion=${VERSION}
+
+RUN dotnet pack "src/Strata.Excel.TestUtilities/Strata.Excel.TestUtilities.csproj" \
+ --configuration Release \
+ --no-restore \
+ --no-build \
+ --include-symbols \
+ --include-source \
+ --output /pack \
+ -property:PackageVersion=${VERSION}
+
+
+RUN if [ "${SONARLOGIN}" != "" ] ; then \
+ /dc/dependency-check/bin/dependency-check.sh -f JSON -f HTML -s . -o ./buildlogs \
+ --suppression ./sonarsuppressions.xml \
+ --noupdate --nodeAuditSkipDevDependencies --disableNodeJS \
+ --dbDriverName "com.microsoft.sqlserver.jdbc.SQLServerDriver" \
+ --connectionString "jdbc:sqlserver://ddensqldevops01.sdt.local;Database=DependencyCheck;encrypt=true;trustServerCertificate=true;" \
+ --dbUser "dcuser" --dbPassword "${SONARLOGIN}" && \
+ dotnet sonarscanner end /d:sonar.login="${SONARLOGIN}"; \
+ fi
diff --git a/README.md b/README.md
index 5cd2718..f044ac4 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,81 @@
-# excel.core
+# Strata.Excel.Core
+This provides a library for Strata standard ClosedXML Excel documents
+
+[](https://github.com/stratadecision/excel.core/actions/workflows/build.yaml)
+
+
+[](https://sonarqube.sdt.local/dashboard?id=Strata.excel.core)
+
+[](https://proget.ops.stratanetwork.net/feeds/nuget/Strata.Excel.Core/versions?HidePrerelease=True)
+[](https://proget.ops.stratanetwork.net/feeds/nuget/Strata.Excel.TestUtilities/versions?HidePrerelease=True)
+
+
+
+## Branching and Versioning
+
+| branch | version format | example |
+| ---------- | --------------------- | --------------- |
+| main | #.#.# | 1.2.3 |
+| feature/\* | #.#+1.0-featureName.# | 1.3.0-newfeat.1 |
+| fix/\* | #.#.#+1-fixName.# | 1.2.4-bug.1 |
+
+See our [confluence page](https://confluence.sdt.local/display/DOP/Branching+and+Versioning) for more information
+
+## Usage
+
+### Build and Run
+
+```
+docker-compose up -d --build
+```
+
+- https://localhost:8443/index (swagger api)
+- https://localhost:8443/hangfire (hangfire dashboard)
+- http://localhost:8081 (redis commander)
+
+### Extracting nuget package
+
+```
+docker create --name throwaway strataexcel-excel-core
+docker cp throwaway:/pack .
+docker rm throwaway
+```
+
+### Cleanup
+
+```
+docker-compose down
+```
+
+
+# Helpful Features
+### Adding a drop down list to a worksheet
+
+ClosedXML offers the functionality for adding drop down lists to columns with validation out of the box. However, that implementation has a data length limitation of a formula field. For lists longer than trivial selections this extension method can be used:
+
+```csharp
+/// Drop down list items
+/// Name of hidden worksheet that holds data
+/// Sort items in ascending order
+CreateDataValidation(this IXLColumn column, IEnumerable items, string hiddenWorksheetName, bool orderItems = true)
+```
+### Example usage
+```csharp
+var workbook = new XLWorkbook();
+var worksheet= wb.Worksheets.Add("Strata Data Worksheet");
+
+// Illustrating a list longer than standard Data Validation can handle
+var dropDownListItems = new List(){"Department A", "Department B", "Department C", "Department D", "Department E", "Department F"
+"Department G", "Department H", "Department I", "Department J", "Department K", "Department L"
+"Department M", "Department N", "Department O", "Department P", "Department Q", "Department R"};
+
+// Add drop down with list items to all cells in column 1
+worksheet.Column(1).CreateDataValidation(dropDownListItems, "Name of Worksheet");
+```
+
+### Notes
+* Worksheets must have unique names in excel therefore an overload for worksheet name is provided. When adding more than one drop down to a single worksheet it will be necessary to ensure each worksheet has a unique name.
+* Data validation is implemented by adding a hidden worksheet to your workbook that contains all of the desired list items, validation is performed against the hidden worksheet's list.
+
+
diff --git a/Strata.Excel.Core.sln b/Strata.Excel.Core.sln
new file mode 100644
index 0000000..c9fce72
--- /dev/null
+++ b/Strata.Excel.Core.sln
@@ -0,0 +1,37 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 16
+VisualStudioVersion = 16.0.29613.14
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Strata.Excel.Core.Test.Unit", "tests\Strata.Excel.Core.Test.Unit\Strata.Excel.Core.Test.Unit.csproj", "{D23434FF-E782-41BE-B583-B0518EADC97E}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Strata.Excel.Core", "src\Strata.Excel.Core\Strata.Excel.Core.csproj", "{404933A0-C95F-4E2E-9A76-95EA761D7F80}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Strata.Excel.TestUtilities", "src\Strata.Excel.TestUtilities\Strata.Excel.TestUtilities.csproj", "{85C88D7C-F2CB-4A2D-952D-C695CF3DDA60}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {D23434FF-E782-41BE-B583-B0518EADC97E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {D23434FF-E782-41BE-B583-B0518EADC97E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {D23434FF-E782-41BE-B583-B0518EADC97E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {D23434FF-E782-41BE-B583-B0518EADC97E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {404933A0-C95F-4E2E-9A76-95EA761D7F80}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {404933A0-C95F-4E2E-9A76-95EA761D7F80}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {404933A0-C95F-4E2E-9A76-95EA761D7F80}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {404933A0-C95F-4E2E-9A76-95EA761D7F80}.Release|Any CPU.Build.0 = Release|Any CPU
+ {85C88D7C-F2CB-4A2D-952D-C695CF3DDA60}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {85C88D7C-F2CB-4A2D-952D-C695CF3DDA60}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {85C88D7C-F2CB-4A2D-952D-C695CF3DDA60}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {85C88D7C-F2CB-4A2D-952D-C695CF3DDA60}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {7EBD3752-3378-42B4-8E49-2CFCE925B854}
+ EndGlobalSection
+EndGlobal
diff --git a/nuget.config b/nuget.config
new file mode 100644
index 0000000..3bf3f7b
--- /dev/null
+++ b/nuget.config
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/sonarsuppressions.xml b/sonarsuppressions.xml
new file mode 100644
index 0000000..6f4969c
--- /dev/null
+++ b/sonarsuppressions.xml
@@ -0,0 +1,151 @@
+
+
+
+
+ ^pkg:generic/TeamCity\.ServiceMessages@.*$
+ cpe:/a:jetbrains:teamcity
+ CVE-2014-10002
+
+
+
+ ^pkg:generic/Microsoft\.AspNetCore\.Authentication\.JwtBearer@.*$
+ CVE-2020-1108
+
+
+
+ ^pkg:generic/Microsoft\.VisualStudio\.CodeCoverage\.Shim@.*$
+ CVE-2020-1171
+
+
+
+ ^pkg:generic/Microsoft\.VisualStudio\.CodeCoverage\.Shim@.*$
+ CVE-2020-1192
+
+
+
+ ^pkg:generic/SonarScanner\.MSBuild\.Tasks@.*$
+ CVE-2020-22475
+
+
+
+ ^pkg:npm/browserslist@.*$
+ 1747
+
+
+
+ ^pkg:npm/css\-what@.*$
+ 1754
+
+
+
+ ^pkg:npm/dns\-packet@.*$
+ 1745
+
+
+
+ ^pkg:npm/normalize\-url@.*$
+ 1755
+
+
+
+ ^pkg:npm/trim\-newlines@.*$
+ 1753
+
+
+
+ ^pkg:generic/TeamCity\.ServiceMessages@.*$
+ CVE-2014-10036
+
+
+
+ ^pkg:generic/TeamCity\.ServiceMessages@.*$
+ CVE-2019-12156
+
+
+
+ ^pkg:generic/TeamCity\.ServiceMessages@.*$
+ CVE-2019-12157
+
+
+
+ ^pkg:generic/TeamCity\.ServiceMessages@.*$
+ CVE-2019-12841
+
+
+
+ ^pkg:generic/TeamCity\.ServiceMessages@.*$
+ CVE-2019-12842
+
+
+
+ ^pkg:generic/TeamCity\.ServiceMessages@.*$
+ CVE-2019-12843
+
+
+
+ ^pkg:generic/TeamCity\.ServiceMessages@.*$
+ CVE-2019-12844
+
+
+
+ ^pkg:generic/TeamCity\.ServiceMessages@.*$
+ CVE-2019-12845
+
+
+
+ ^pkg:generic/TeamCity\.VSTest\.TestLogger@.*$
+ cpe:/a:jetbrains:teamcity
+
+
+
+ ^pkg:generic/TeamCity\.VSTest\.TestAdapter@.*$
+ cpe:/a:jetbrains:teamcity
+
+
+
+ ^pkg:javascript/jquery@.*$
+ Regex in its jQuery.htmlPrefilter sometimes may introduce XSS
+
+
\ No newline at end of file
diff --git a/src/Strata.Excel.Core/ActionResults/ExcelContentResult.cs b/src/Strata.Excel.Core/ActionResults/ExcelContentResult.cs
new file mode 100644
index 0000000..9a7e579
--- /dev/null
+++ b/src/Strata.Excel.Core/ActionResults/ExcelContentResult.cs
@@ -0,0 +1,51 @@
+using ClosedXML.Excel;
+using Microsoft.AspNetCore.Mvc;
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.IO;
+using System.Threading.Tasks;
+
+namespace Strata.Excel.Core
+{
+ [ExcludeFromCodeCoverage]
+ public class ExcelContentResult : ActionResult
+ {
+ public IXLWorkbook Workbook { get; set; }
+ public string FileName { get; set; }
+
+ public ExcelContentResult()
+ {
+ }
+
+ public ExcelContentResult(IXLWorkbook workbook, string fileName = "Export") : base()
+ {
+ Workbook = workbook;
+ FileName = fileName;
+ }
+
+ public override void ExecuteResult(ActionContext context)
+ {
+ GetFileContentResult().ExecuteResult(context);
+ }
+
+ public override Task ExecuteResultAsync(ActionContext context)
+ {
+ return GetFileContentResult().ExecuteResultAsync(context);
+ }
+
+ protected FileContentResult GetFileContentResult()
+ {
+ // Flush the workbook to the Response.OutputStream
+ using (var memoryStream = new MemoryStream())
+ {
+ Workbook.SaveAs(memoryStream);
+
+ return new FileContentResult(memoryStream.ToArray(),
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
+ {
+ FileDownloadName = $"{FileName}_{DateTime.UtcNow:MM-dd-yyyy}.xlsx"
+ };
+ }
+ }
+ }
+}
diff --git a/src/Strata.Excel.Core/ActionResults/ZipFileContentResult.cs b/src/Strata.Excel.Core/ActionResults/ZipFileContentResult.cs
new file mode 100644
index 0000000..e6f3cfe
--- /dev/null
+++ b/src/Strata.Excel.Core/ActionResults/ZipFileContentResult.cs
@@ -0,0 +1,56 @@
+using ClosedXML.Excel;
+using Ionic.Zip;
+using Microsoft.AspNetCore.Mvc;
+using System.Diagnostics.CodeAnalysis;
+using System.IO;
+using System.Threading.Tasks;
+
+namespace Strata.Excel.Core
+{
+ [ExcludeFromCodeCoverage]
+ public class ZipFileContentResult : ActionResult
+ {
+ public IXLWorkbook Workbook { get; set; }
+ public string FileName { get; set; }
+
+ public ZipFileContentResult()
+ {
+ }
+
+ public ZipFileContentResult(IXLWorkbook workbook, string fileName = "Export") : base()
+ {
+ Workbook = workbook;
+ FileName = fileName;
+ }
+
+ public override void ExecuteResult(ActionContext context)
+ {
+ GetFileContentResult().ExecuteResult(context);
+ }
+
+ public override Task ExecuteResultAsync(ActionContext context)
+ {
+ return GetFileContentResult().ExecuteResultAsync(context);
+ }
+
+ protected FileContentResult GetFileContentResult()
+ {
+ var zip = new ZipFile($"{FileName}.zip");
+ using (var fs = new MemoryStream())
+ {
+ Workbook.SaveAs(fs);
+ fs.Position = 0;
+ zip.AddEntry($"{FileName}.xlsx", fs);
+ // Flush the zipfile to the Response.OutputStream
+ MemoryStream output = new MemoryStream();
+ zip.Save(output);
+ return new FileContentResult(output.ToArray(),
+ "application/zip")
+ {
+ FileDownloadName = $"{FileName}.zip"
+ };
+ }
+ }
+
+ }
+}
diff --git a/src/Strata.Excel.Core/Export/ColumnOptions.cs b/src/Strata.Excel.Core/Export/ColumnOptions.cs
new file mode 100644
index 0000000..1c8df79
--- /dev/null
+++ b/src/Strata.Excel.Core/Export/ColumnOptions.cs
@@ -0,0 +1,11 @@
+using ClosedXML.Excel;
+
+namespace Strata.Excel.Core
+{
+ public class ColumnOptions
+ {
+ public string Column { get; set; }
+ public string Format { get; set; }
+ public XLAlignmentHorizontalValues Alignment { get; set; } = XLAlignmentHorizontalValues.Left;
+ }
+}
diff --git a/src/Strata.Excel.Core/Export/ExportOptions.cs b/src/Strata.Excel.Core/Export/ExportOptions.cs
new file mode 100644
index 0000000..979dbd0
--- /dev/null
+++ b/src/Strata.Excel.Core/Export/ExportOptions.cs
@@ -0,0 +1,42 @@
+using ClosedXML.Excel;
+using Humanizer;
+using System;
+using System.Collections.Generic;
+using static Strata.Excel.Core.ExportUtils;
+
+namespace Strata.Excel.Core
+{
+ public class ExportOptions
+ {
+ public string Title { get; set; }
+ public string SubTitle { get; set; }
+ public string DefaultWorksheetName { get; set; }
+ public string EmptyMessage { get; set; }
+ public XLEventTracking EventTracking { get; set; } = XLEventTracking.Disabled;
+ public bool HumanizeHeading { get; set; } = true;
+ public LetterCasing HumanizeLetterCasing { get; set; } = LetterCasing.Title;
+ public int BatchSize { get; set; } = 0;
+
+ public List ColumnOptions { get; set; }
+
+ public ExportOptions()
+ {
+ DefaultWorksheetName = "Sheet1";
+ EmptyMessage = "No Data Available";
+ ColumnOptions = new List();
+ }
+
+ public void AddColumnOptions(string column, string format)
+ => this.ColumnOptions.Add(new ColumnOptions() { Column = column, Format = format });
+
+ public void AddColumnOptions(string column, string format, XLAlignmentHorizontalValues alignment)
+ => this.ColumnOptions.Add(new ColumnOptions() { Column = column, Format = format, Alignment = alignment });
+
+ public void AddColumnOptions(string column, NumberFormatId numberFormatId)
+ => AddColumnOptions(column, Enum.GetName(typeof(NumberFormatId), numberFormatId));
+
+ public void AddColumnOptions(string column, NumberFormatId numberFormatId, XLAlignmentHorizontalValues alignment)
+ => AddColumnOptions(column, Enum.GetName(typeof(NumberFormatId), numberFormatId), alignment);
+
+ }
+}
diff --git a/src/Strata.Excel.Core/Export/ExportUtils.cs b/src/Strata.Excel.Core/Export/ExportUtils.cs
new file mode 100644
index 0000000..b6c90ce
--- /dev/null
+++ b/src/Strata.Excel.Core/Export/ExportUtils.cs
@@ -0,0 +1,418 @@
+using ClosedXML.Excel;
+using ClosedXML.Graphics;
+using Humanizer;
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+
+namespace Strata.Excel.Core
+{
+ [ExcludeFromCodeCoverage]
+ public static class ExportUtils
+ {
+ ///
+ /// 0 General
+ ///1 0
+ ///2 0.00
+ ///3 #,##0
+ ///4 #,##0.00
+ ///9 0%
+ ///10 0.00%
+ ///11 0.00E+00
+ ///12 # ?/?
+ ///13 # ??/??
+ ///14 d/m/yyyy
+ ///15 d-mmm-yy
+ ///16 d-mmm
+ ///17 mmm-yy
+ ///18 h:mm tt
+ ///19 h:mm:ss tt
+ ///20 H:mm
+ ///21 H:mm:ss
+ ///22 m/d/yyyy H:mm
+ ///37 #,##0 ;(#,##0)
+ ///38 #,##0 ;[Red](#,##0)
+ ///39 #,##0.00;(#,##0.00)
+ ///40 #,##0.00;[Red](#,##0.00)
+ ///45 mm:ss
+ ///46 [h]:mm:ss
+ ///47 mmss.0
+ ///48 ##0.0E+0
+ ///49 @
+ ///
+ public enum NumberFormatId
+ {
+ General,
+ Zero,
+ ZeroDotZeroZero,
+ PoundCommaPoundPoundZero,
+ PoundCommaPoundPoundZeroDotZeroZero,
+ ZeroPercent = 9,
+ ZeroDotZeroZeroPercent = 10,
+ ScientificNotation = 11,
+ PoundSpaceQuestionSlashQuestion = 12,
+ PoundSpaceQuestionQuestionSlashQuestionQuestion = 13,
+ ShortDateSlash = 14,
+ ShortDateDash = 15,
+ DDashMMM = 16,
+ MMMDashYY = 17,
+ ShortTime = 18,
+ ShortTimeSeconds = 19,
+ ShortTime24 = 20,
+ ShortTimeSeconds24 = 21,
+ ShortDateTime = 22,
+ PoundCommaPoundPoundZeroNegative = 37,
+ PoundCommaPoundPoundZeroRedNegative = 38,
+ PoundCommaPoundPoundZeroDotZeroZeroNegative = 39,
+ PoundCommaPoundPoundZeroDotZeroZeroRedNegative = 40,
+ MinutesSeconds = 45,
+ HoursOptionalMinutesSeconds = 46,
+ MinutesSecondsMilliseconds = 47,
+ PoundPoundScientificNotation = 48,
+ HideZeroValues = 49
+ }
+
+ internal static Dictionary NumberFormatDictionary => new Dictionary()
+ {
+ {"General", 0},
+ {"0", 1},
+ {"Accounting",1},
+ {"0.00", 2},
+ {"Accounting2Dec", 2},
+ {"#,##0", 3},
+ {"Currency",3},
+ {"#,##0.00", 4},
+ {"Currency2Dec", 4},
+ {"0%", 9},
+ {"ZeroPercent", 9},
+ {"0.00%", 10},
+ {"ZeroPerentWithDecimals", 10},
+ {"0.00E+00", 11},
+ {"Scientific", 11},
+ {"# ?/?", 12},
+ {"Fractions", 12},
+ {"# ??/??", 13},
+ {"mm-dd-yy", 14},
+ {"ShortDateSlash", 14},
+ {"d-mmm-yy", 15},
+ {"ShortDateDash", 15},
+ {"d-mmm", 16},
+ {"DDashMMM", 16},
+ {"mmm-yy", 17},
+ {"MMMDashYY", 17},
+ {"h:mm AM/PM", 18},
+ {"ShortTime", 18},
+ {"h:mm:ss AM/PM", 19},
+ {"ShortTimeSeconds",19},
+ {"h:mm", 20},
+ {"ShortTime24", 20},
+ {"h:mm:ss", 21},
+ {"ShortTimeSeconds24",21},
+ {"m/d/yy h:mm", 22},
+ {"ShortDateTime",22},
+ {"#,##0 ;(#,##0)", 37},
+ {"NoLeadingZeroNegative",37},
+ {"#,##0 ;[Red](#,##0)", 38},
+ {"NoLeadingZeroRedNegative",38},
+ {"#,##0.00;(#,##0.00)", 39},
+ {"NoLeadingZeroDecimalsNegative",39},
+ {"#,##0.00;[Red](#,##0.00)", 40},
+ {"NoLeadingZeroDecimalsRedNegative",40},
+ {"mm:ss", 45},
+ {"MinutesSeconds", 45},
+ {"[h]:mm:ss", 46},
+ {"HoursOptionalMinutesSeconds",46},
+ {"mmss.0", 47},
+ {"MinutesSecondsMilliseconds", 47},
+ {"##0.0E+0", 48},
+ {"ScientificNotation",48},
+ {"@", 49},
+ {"HideZeroValues", 49}
+ };
+
+ public static string CellName(this IXLWorksheet ws, int row, int col) => ws.Cell(row, col).Address.ToString();
+
+ public static IXLWorksheet WriteFormField(this IXLWorksheet ws, int row, int col, string label, string value)
+ {
+ ws.Cell(row, col).Value = $"{label}:";
+ ws.Cell(row, col + 1).Value = value;
+ return ws;
+ }
+
+ public static IXLWorksheet WriteSheetTitle(this IXLWorksheet ws, int row, int col, int offset, string title)
+ {
+ var cellRow = ws.Row(row);
+ cellRow.Height = 30;
+ var cells = ws.Range(row, col, row, offset);
+ cells.Merge();
+ cells.Style.Font.Bold = true;
+ cells.Style.Font.FontColor = XLColor.FromArgb(31, 73, 125);
+ cells.Style.Font.FontSize = 18;
+ cells.Style.Alignment.Vertical = XLAlignmentVerticalValues.Center;
+ cells.Value = title;
+ return ws;
+ }
+
+ public static IXLWorksheet WriteSheetSubTitle(this IXLWorksheet ws, int row, int col, int offset, string subtitle)
+ {
+ var cells = ws.Range(row, col, row, offset);
+ cells.Merge();
+ cells.Style.Font.Italic = true;
+ cells.Style.Font.FontColor = XLColor.FromArgb(237, 125, 49);
+ cells.Value = subtitle;
+ return ws;
+ }
+
+ public static IXLWorksheet WriteHeader(this IXLWorksheet ws, int row, int col, int offset, string label)
+ {
+ var cells = ws.Range(row, col, row, offset);
+ cells.Merge();
+ cells.Style.Font.Bold = true;
+ cells.Style.Font.FontColor = XLColor.FromArgb(31, 73, 125);
+ cells.Style.Font.FontSize = 16;
+ cells.Style.Border.BottomBorder = XLBorderStyleValues.Medium;
+ cells.Style.Border.BottomBorderColor = XLColor.FromArgb(148, 179, 215);
+ cells.Value = label;
+ return ws;
+ }
+
+ public static IXLWorksheet WriteCellValue(this IXLWorksheet ws, int row, int col, object value)
+ {
+ var cell = ws.Cell(row, col);
+ cell.Value = value;
+ return ws;
+ }
+
+ internal static IXLWorksheet WriteHeading(this IXLWorksheet ws, int row, int col, string value)
+ {
+ var cell = ws.Cell(row, col);
+ cell.Style.Font.Bold = true;
+ cell.Style.Alignment.Vertical = XLAlignmentVerticalValues.Center;
+ cell.Value = value;
+ return ws;
+ }
+
+ public static IXLWorksheet CreateTitlePage(this XLWorkbook wb, string title, string subTitle)
+ {
+ var ws = wb.AddWorksheet(title);
+ ws.WriteSheetTitle(1, 1, 8, title)
+ .WriteHeader(3, 1, 8, subTitle)
+ .WriteFormField(5, 1, "Report Date", DateTime.Today.ToString("g"));
+ ws.Columns().AdjustToContents();
+ return ws;
+ }
+
+ public static IXLWorksheet CreateWorksheet(this XLWorkbook wb, IEnumerable items, string worksheetName)
+ => wb.CreateWorksheet(items, worksheetName, new ExportOptions());
+
+ public static IXLWorksheet CreateWorksheet(this XLWorkbook wb, IEnumerable items, string worksheetName, ExportOptions options)
+ {
+ wb.AddWorksheet(worksheetName);
+ var ws = wb.Worksheet(worksheetName);
+
+ IXLTable table = null;
+ if (options.BatchSize <= 0 || options.BatchSize > items.Count())
+ {
+ table = ws.Cell(1, 1).InsertTable(items);
+ ws.HumanizeHeadings(table, options);
+ }
+ else
+ {
+ var batches = items.Batch(options.BatchSize);
+ table = ws.Cell(1, 1).InsertTable(batches.First());
+ ws.HumanizeHeadings(table, options);
+ foreach (var batch in batches.Skip(1))
+ {
+ table.AppendData(batch);
+ }
+ }
+ ws.Columns().AdjustToContents();
+ return ws;
+ }
+
+ internal static IXLWorksheet HumanizeHeadings(this IXLWorksheet ws, IXLTable table, ExportOptions options)
+ {
+ var columns = table.ColumnsUsed().Select(t => t.ColumnNumber());
+ foreach (var colNo in columns)
+ {
+ var heading = ws.Cell(1, colNo).GetString();
+ ws.Cell(1, colNo).Value = options.HumanizeHeading
+ ? heading.Humanize(options.HumanizeLetterCasing)
+ : heading.Replace(" ", "");
+ }
+ return ws;
+ }
+
+ public static IXLWorksheet CreateWorksheet(this XLWorkbook wb, IEnumerable items, ExportOptions options)
+ {
+ var ws = wb.CreateWorksheet(items, options.DefaultWorksheetName, options);
+ // format columns
+ options.ColumnOptions.ForEach(ws.SetColumnFormatByHeader);
+ return ws;
+ }
+
+ public static XLWorkbook CreateExcelWorkbook(IEnumerable