feat: Initialize Strata.Excel.Core library with export, import, and test utilities

This commit introduces the `Strata.Excel.Core` project, a .NET library built on ClosedXML for robust Excel document generation and data import.

Key features include:
- **Export:** Flexible data export to Excel, supporting custom formatting, titles, subtitles, humanized headings, and batched processing for large datasets. Includes `ExcelContentResult` and `ZipFileContentResult` for ASP.NET Core integration.
- **Import:** Utilities to easily import data from Excel worksheets into C# objects.
- **Test Utilities:** Comprehensive helpers for comparing Excel workbooks in tests, handling resource extraction, and performing load tests.
- **Build Infrastructure:** Sets up a Dockerfile for building the library, including SonarQube and Dependency-Check scanning.
- **Project Structure:** Establishes `.gitignore`, `.dockerignore`, `nuget.config`, and a `README.md` with usage instructions and versioning guidelines.

This foundational commit provides a reusable and well-tested framework for Excel operations within Strata applications.
This commit is contained in:
Thom Lamb
2026-06-23 11:08:58 -05:00
parent 9a9b7f7437
commit 0c144f5c66
30 changed files with 3087 additions and 218 deletions
+10
View File
@@ -0,0 +1,10 @@
.dockerignore
Dockerfile
Dockerfile.local
.env
.gitignore
.vs
.vscode
**/bin
**/obj
**/.toolstarget
+85 -217
View File
@@ -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 theyre 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 #
############
# its 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/
+89
View File
@@ -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
+80 -1
View File
@@ -1,2 +1,81 @@
# excel.core
# Strata.Excel.Core
This provides a library for Strata standard ClosedXML Excel documents
[![build](https://github.com/stratadecision/excel.core/actions/workflows/build.yaml/badge.svg)](https://github.com/stratadecision/excel.core/actions/workflows/build.yaml)
[![SonarQube](https://img.shields.io/badge/SonarQube-Strata.excel.core-004880?logo=sonarqube)](https://sonarqube.sdt.local/dashboard?id=Strata.excel.core)
[![Strata.Excel.Core](https://img.shields.io/badge/nuget-Strata.Excel.Core-004880?logo=nuget&logoColor=004880)](https://proget.ops.stratanetwork.net/feeds/nuget/Strata.Excel.Core/versions?HidePrerelease=True)
[![Strata.Excel.TestUtilities](https://img.shields.io/badge/nuget-Strata.Excel.TestUtilities-004880?logo=nuget&logoColor=004880)](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
/// <param name="items">Drop down list items</param>
/// <param name="hiddenWorksheetName">Name of hidden worksheet that holds data</param>
/// <param name="orderItems">Sort items in ascending order</param>
CreateDataValidation(this IXLColumn column, IEnumerable<string> 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<string>(){"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.
+37
View File
@@ -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
+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<config>
<add key="defaultPushSource" value="https://proget.ops.stratanetwork.net/nuget/nuget" />
</config>
<packageSources>
<add key="NuGet official package source" value="https://api.nuget.org/v3/index.json" />
<add key="Strata package source" value="https://proget.ops.stratanetwork.net/nuget/nuget" />
</packageSources>
</configuration>
+151
View File
@@ -0,0 +1,151 @@
<?xml version="1.0" encoding="UTF-8"?>
<suppressions xmlns="https://jeremylong.github.io/DependencyCheck/dependency-suppression.1.3.xsd">
<suppress>
<notes><![CDATA[
file name: TeamCity.ServiceMessages.dll
]]></notes>
<packageUrl regex="true">^pkg:generic/TeamCity\.ServiceMessages@.*$</packageUrl>
<cpe>cpe:/a:jetbrains:teamcity</cpe>
<cve>CVE-2014-10002</cve>
</suppress>
<suppress>
<notes><![CDATA[
file name: Microsoft.AspNetCore.Authentication.JwtBearer.dll
]]></notes>
<packageUrl regex="true">^pkg:generic/Microsoft\.AspNetCore\.Authentication\.JwtBearer@.*$</packageUrl>
<cve>CVE-2020-1108</cve>
</suppress>
<suppress>
<notes><![CDATA[
file name: Microsoft.VisualStudio.CodeCoverage.Shim.dll
]]></notes>
<packageUrl regex="true">^pkg:generic/Microsoft\.VisualStudio\.CodeCoverage\.Shim@.*$</packageUrl>
<cve>CVE-2020-1171</cve>
</suppress>
<suppress>
<notes><![CDATA[
file name: Microsoft.VisualStudio.CodeCoverage.Shim.dll
]]></notes>
<packageUrl regex="true">^pkg:generic/Microsoft\.VisualStudio\.CodeCoverage\.Shim@.*$</packageUrl>
<cve>CVE-2020-1192</cve>
</suppress>
<suppress>
<notes><![CDATA[
file name: SonarScanner.MSBuild.Tasks.dll
]]></notes>
<packageUrl regex="true">^pkg:generic/SonarScanner\.MSBuild\.Tasks@.*$</packageUrl>
<cve>CVE-2020-22475</cve>
</suppress>
<suppress>
<notes><![CDATA[
file name: browserslist:4.14.2
]]></notes>
<packageUrl regex="true">^pkg:npm/browserslist@.*$</packageUrl>
<vulnerabilityName>1747</vulnerabilityName>
</suppress>
<suppress>
<notes><![CDATA[
file name: css-what:3.4.2
]]></notes>
<packageUrl regex="true">^pkg:npm/css\-what@.*$</packageUrl>
<vulnerabilityName>1754</vulnerabilityName>
</suppress>
<suppress>
<notes><![CDATA[
file name: dns-packet:1.3.1
]]></notes>
<packageUrl regex="true">^pkg:npm/dns\-packet@.*$</packageUrl>
<vulnerabilityName>1745</vulnerabilityName>
</suppress>
<suppress>
<notes><![CDATA[
file name: normalize-url:3.3.0
]]></notes>
<packageUrl regex="true">^pkg:npm/normalize\-url@.*$</packageUrl>
<vulnerabilityName>1755</vulnerabilityName>
</suppress>
<suppress>
<notes><![CDATA[
file name: trim-newlines:1.0.0
]]></notes>
<packageUrl regex="true">^pkg:npm/trim\-newlines@.*$</packageUrl>
<vulnerabilityName>1753</vulnerabilityName>
</suppress>
<suppress>
<notes><![CDATA[
file name: TeamCity.ServiceMessages.dll
]]></notes>
<packageUrl regex="true">^pkg:generic/TeamCity\.ServiceMessages@.*$</packageUrl>
<cve>CVE-2014-10036</cve>
</suppress>
<suppress>
<notes><![CDATA[
file name: TeamCity.ServiceMessages.dll
]]></notes>
<packageUrl regex="true">^pkg:generic/TeamCity\.ServiceMessages@.*$</packageUrl>
<cve>CVE-2019-12156</cve>
</suppress>
<suppress>
<notes><![CDATA[
file name: TeamCity.ServiceMessages.dll
]]></notes>
<packageUrl regex="true">^pkg:generic/TeamCity\.ServiceMessages@.*$</packageUrl>
<cve>CVE-2019-12157</cve>
</suppress>
<suppress>
<notes><![CDATA[
file name: TeamCity.ServiceMessages.dll
]]></notes>
<packageUrl regex="true">^pkg:generic/TeamCity\.ServiceMessages@.*$</packageUrl>
<cve>CVE-2019-12841</cve>
</suppress>
<suppress>
<notes><![CDATA[
file name: TeamCity.ServiceMessages.dll
]]></notes>
<packageUrl regex="true">^pkg:generic/TeamCity\.ServiceMessages@.*$</packageUrl>
<cve>CVE-2019-12842</cve>
</suppress>
<suppress>
<notes><![CDATA[
file name: TeamCity.ServiceMessages.dll
]]></notes>
<packageUrl regex="true">^pkg:generic/TeamCity\.ServiceMessages@.*$</packageUrl>
<cve>CVE-2019-12843</cve>
</suppress>
<suppress>
<notes><![CDATA[
file name: TeamCity.ServiceMessages.dll
]]></notes>
<packageUrl regex="true">^pkg:generic/TeamCity\.ServiceMessages@.*$</packageUrl>
<cve>CVE-2019-12844</cve>
</suppress>
<suppress>
<notes><![CDATA[
file name: TeamCity.ServiceMessages.dll
]]></notes>
<packageUrl regex="true">^pkg:generic/TeamCity\.ServiceMessages@.*$</packageUrl>
<cve>CVE-2019-12845</cve>
</suppress>
<suppress>
<notes><![CDATA[
file name: TeamCity.VSTest.TestLogger.dll
]]></notes>
<packageUrl regex="true">^pkg:generic/TeamCity\.VSTest\.TestLogger@.*$</packageUrl>
<cpe>cpe:/a:jetbrains:teamcity</cpe>
</suppress>
<suppress>
<notes><![CDATA[
file name: TeamCity.VSTest.TestAdapter.dll
]]></notes>
<packageUrl regex="true">^pkg:generic/TeamCity\.VSTest\.TestAdapter@.*$</packageUrl>
<cpe>cpe:/a:jetbrains:teamcity</cpe>
</suppress>
<suppress>
<notes><![CDATA[
file name: dependency-check-core-5.3.2.jar: jquery-3.4.1.min.js
]]></notes>
<packageUrl regex="true">^pkg:javascript/jquery@.*$</packageUrl>
<vulnerabilityName>Regex in its jQuery.htmlPrefilter sometimes may introduce XSS</vulnerabilityName>
</suppress>
</suppressions>
@@ -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"
};
}
}
}
}
@@ -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"
};
}
}
}
}
@@ -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;
}
}
@@ -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> ColumnOptions { get; set; }
public ExportOptions()
{
DefaultWorksheetName = "Sheet1";
EmptyMessage = "No Data Available";
ColumnOptions = new List<ColumnOptions>();
}
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);
}
}
+418
View File
@@ -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
{
/// <summary>
/// 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 @
/// </summary>
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<string, int> NumberFormatDictionary => new Dictionary<string, int>()
{
{"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<T>(this XLWorkbook wb, IEnumerable<T> items, string worksheetName)
=> wb.CreateWorksheet(items, worksheetName, new ExportOptions());
public static IXLWorksheet CreateWorksheet<T>(this XLWorkbook wb, IEnumerable<T> 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<T>(this XLWorkbook wb, IEnumerable<T> 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<object> data, ExportOptions options)
{
if (data is null || !data.Any())
{
return GetEmptyWorkbook(options);
}
// https://github.com/ClosedXML/ClosedXML/wiki/Graphic-Engine
// Only workbooks created with the options will use the engine
// https://github.com/ClosedXML/ClosedXML/wiki/Turning-off-events
// Disabling Event Tracking to save memory and increase performance
var loadOptions = new LoadOptions
{
GraphicEngine = new DefaultGraphicEngine("Times New Roman"),
EventTracking = XLEventTracking.Disabled
};
// load data into worksheet
var workbook = new XLWorkbook(loadOptions);
if (!string.IsNullOrEmpty(options.Title))
{
workbook.CreateTitlePage(options.Title, options.SubTitle);
}
var ws = workbook.CreateWorksheet(data, options.DefaultWorksheetName, options);
// format columns
options.ColumnOptions.ForEach(ws.SetColumnFormatByHeader);
return workbook;
}
private static XLWorkbook GetEmptyWorkbook(ExportOptions options)
{
// https://github.com/ClosedXML/ClosedXML/wiki/Graphic-Engine
// Only workbooks created with the options will use the engine
// https://github.com/ClosedXML/ClosedXML/wiki/Turning-off-events
// Disabling Event Tracking to save memory and increase performance
var loadOptions = new LoadOptions
{
GraphicEngine = new DefaultGraphicEngine("Times New Roman"),
EventTracking = XLEventTracking.Disabled
};
var workbook = new XLWorkbook(loadOptions);
if (!string.IsNullOrEmpty(options.Title))
{
workbook.CreateTitlePage(options.Title, options.SubTitle);
}
var ws = workbook.AddWorksheet("Default Worksheet");
ws.Cell(1, 1).Value = "No Data";
return workbook;
}
internal static IXLCell CellByHeader(this IXLWorksheet ws, string columnHeader)
{
return ws.Tables.First().HeadersRow().CellsUsed(c => c.Value.ToString() == columnHeader).FirstOrDefault();
}
internal static IXLColumn ColumnByHeader(this IXLWorksheet ws, string columnHeader)
{
return ws.Tables.FirstOrDefault()?.HeadersRow()
.CellsUsed(c => c.Value.ToString() == columnHeader).FirstOrDefault()?.WorksheetColumn();
}
internal static void SetColumnFormatByHeader(this IXLWorksheet ws, ColumnOptions columnOptions)
{
SetColumnFormatByHeader(ws, columnOptions.Column, columnOptions.Format);
var column = ws.ColumnByHeader(columnOptions.Column);
column.Style.Alignment.SetHorizontal(columnOptions.Alignment);
}
internal static void SetColumnFormatByHeader(this IXLWorksheet ws, string columnHeader, string formatName)
{
var column = ws.ColumnByHeader(columnHeader);
if (column == null) throw new ArgumentOutOfRangeException(nameof(columnHeader));
if (NumberFormatDictionary.TryGetValue(formatName, out var formatId))
{
column.Style.NumberFormat.NumberFormatId = formatId;
}
else
{
column.Style.NumberFormat.Format = formatName;
}
}
/// <summary>
/// Batches the source sequence into sized buckets.
/// </summary>
/// <typeparam name="TSource">Type of elements in <paramref name="source"/> sequence.</typeparam>
/// <param name="source">The source sequence.</param>
/// <param name="size">Size of buckets.</param>
/// <returns>A sequence of equally sized buckets containing elements of the source collection.</returns>
/// <remarks>
/// This operator uses deferred execution and streams its results (buckets and bucket content).
/// It is also identical to <see cref="Partition{TSource}(System.Collections.Generic.IEnumerable{TSource},int)"/>.
/// </remarks>
//https://code.google.com/p/morelinq/source/browse/MoreLinq/Batch.cs
internal static IEnumerable<IEnumerable<TSource>> Batch<TSource>(this IEnumerable<TSource> source, int size)
{
return Batch(source, size, x =>
{
var enumerable = x as IList<TSource> ?? x.ToList();
return enumerable;
});
}
/// <summary>
/// Batches the source sequence into sized buckets and applies a projection to each bucket.
/// </summary>
/// <typeparam name="TSource">Type of elements in <paramref name="source"/> sequence.</typeparam>
/// <typeparam name="TResult">Type of result returned by <paramref name="resultSelector"/>.</typeparam>
/// <param name="source">The source sequence.</param>
/// <param name="size">Size of buckets.</param>
/// <param name="resultSelector">The projection to apply to each bucket.</param>
/// <returns>A sequence of projections on equally sized buckets containing elements of the source collection.</returns>
/// <remarks>
/// This operator uses deferred execution and streams its results (buckets and bucket content).
/// It is also identical to <see cref="Partition{TSource}(System.Collections.Generic.IEnumerable{TSource},int)"/>.
/// </remarks>
internal static IEnumerable<TResult> Batch<TSource, TResult>(this IEnumerable<TSource> source, int size,
Func<IEnumerable<TSource>, TResult> resultSelector)
{
if (source == null) throw new ArgumentNullException(nameof(source));
if (size <= 0) throw new ArgumentOutOfRangeException(nameof(size));
if (resultSelector == null) throw new ArgumentNullException(nameof(resultSelector));
return BatchImpl(source, size, resultSelector);
}
private static IEnumerable<TResult> BatchImpl<TSource, TResult>(this IEnumerable<TSource> source, int size,
Func<IEnumerable<TSource>, TResult> resultSelector)
{
TSource[] bucket = null;
var count = 0;
foreach (var item in source)
{
if (bucket == null)
{
bucket = new TSource[size];
}
bucket[count++] = item;
// The bucket is fully buffered before it's yielded
if (count != size)
{
continue;
}
// Select is necessary so bucket contents are streamed too
yield return resultSelector(bucket.Select(x => x));
bucket = null;
count = 0;
}
// Return the last bucket with all remaining elements
if (bucket != null && count > 0)
{
yield return resultSelector(bucket.Take(count));
}
}
}
}
@@ -0,0 +1,32 @@
using ClosedXML.Excel;
using System.Collections.Generic;
using System.Linq;
namespace Strata.Excel.Core.Export
{
public static class XLColumnExtensions
{
/// <summary>
/// Add items to a dropdown list within a column. Items are saved in a hidden worksheet used to validate user input against.
/// </summary>
/// <param name="column"></param>
/// <param name="items">Drop down list items</param>
/// <param name="hiddenWorksheetName">Name of hidden worksheet that holds data</param>
/// <param name="orderItems">Sort items in ascending order</param>
public static void CreateDataValidation(this IXLColumn column, IEnumerable<string> items, string hiddenWorksheetName, bool orderItems = true)
{
if (orderItems)
{
items = items.OrderBy(x => x).ToList();
}
var ws = column.Worksheet;
var wb = ws.Workbook;
var validationWorkSheet = wb.Worksheets.Add(hiddenWorksheetName);
var range = validationWorkSheet.Cell(1, 1).InsertData(items);
validationWorkSheet.Hide();
column.CreateDataValidation().List(range);
}
}
}
+105
View File
@@ -0,0 +1,105 @@
using ClosedXML.Excel;
using System;
using System.Collections.Generic;
using System.Data;
using System.Reflection;
namespace Strata.Excel.Core.Import
{
public static class ImportUtils
{
/// <summary>
/// Convert Worksheet to DataTable
/// </summary>
/// <param name="worksheet"></param>
/// <returns></returns>
public static List<T> GetDataFromExcel<T>(this IXLWorksheet worksheet)
{
//Save the uploaded Excel file.
//Create a new DataTable.
var dt = new DataTable();
//Loop through the Worksheet rows.
bool firstRow = true;
foreach (IXLRow row in worksheet.Rows())
{
// skip blank rows
if (!firstRow && row.IsEmpty()) continue;
//Use the first row to add columns to DataTable.
if (firstRow)
{
foreach (IXLCell cell in row.Cells())
{
if (cell.IsEmpty() || string.IsNullOrEmpty(cell.GetString())) { break; }
dt.Columns.Add(cell.GetString());
}
firstRow = false;
}
else
{
int i = 0;
DataRow toInsert = dt.NewRow();
foreach (IXLCell cell in row.Cells(1, dt.Columns.Count))
{
toInsert[i] = cell.GetString();
i++;
}
dt.Rows.Add(toInsert);
}
}
return ConvertDataTable<T>(dt);
}
private static List<T> ConvertDataTable<T>(DataTable dt)
{
List<T> data = new List<T>();
foreach (DataRow row in dt.Rows)
{
T item = GetItem<T>(row);
data.Add(item);
}
return data;
}
private static T GetItem<T>(DataRow dr)
{
Type temp = typeof(T);
T obj = Activator.CreateInstance<T>();
foreach (DataColumn column in dr.Table.Columns)
{
var columnName = column.ColumnName.Replace(" ", "");
foreach (PropertyInfo pro in temp.GetProperties())
{
if (pro.Name.Equals(columnName, StringComparison.CurrentCultureIgnoreCase)
|| pro.Name.Equals(column.ColumnName, StringComparison.CurrentCultureIgnoreCase))
{
switch (pro.PropertyType.Name.ToLower())
{
case "bool":
pro.SetValue(obj, Convert.ToBoolean(dr[column.ColumnName]), null);
break;
case "datetime":
pro.SetValue(obj, Convert.ToDateTime(dr[column.ColumnName]), null);
break;
case "double":
pro.SetValue(obj, Convert.ToDouble(dr[column.ColumnName]), null);
break;
case "int32":
pro.SetValue(obj, Convert.ToInt32(dr[column.ColumnName]), null);
break;
default:
pro.SetValue(obj, dr[column.ColumnName], null);
break;
}
}
}
}
return obj;
}
}
}
@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net5.0;netstandard2.0</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ClosedXML" Version="0.97.0" />
<PackageReference Include="DotNetZip" Version="1.16.0" />
<PackageReference Include="Humanizer" Version="2.14.1" />
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.2.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="Strata.ApiCommunication.Http" Version="5.2.1" />
</ItemGroup>
</Project>
@@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ClosedXML" Version="0.97.0" />
<PackageReference Include="FluentAssertions" Version="6.7.0" />
</ItemGroup>
</Project>
@@ -0,0 +1,52 @@
using ClosedXML.Excel;
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.IO.Packaging;
namespace Strata.Excel.TestUtilities
{
[ExcludeFromCodeCoverage]
public static class ExcelDocsComparer
{
public static bool Compare(string left, string right, out string message)
{
using (FileStream leftStream = File.OpenRead(left))
using (FileStream rightStream = File.OpenRead(right))
{
return Compare(leftStream, rightStream, out message);
}
}
public static bool Compare(this Stream result, Stream expected, out string message)
{
using (Package leftPackage = Package.Open(result, FileMode.Open, FileAccess.Read))
using (Package rightPackage = Package.Open(expected, FileMode.Open, FileAccess.Read))
{
return PackageHelper.Compare(leftPackage, rightPackage, false, ExcludeMethod, out message);
}
}
public static bool Compare(this IXLWorkbook result, IXLWorkbook expected, out string message)
{
using (var resultStream = new MemoryStream())
using (var wbStream = new MemoryStream())
{
expected.SaveAs(wbStream);
result.SaveAs(resultStream);
return wbStream.Compare(resultStream, out message);
}
}
private static bool ExcludeMethod(Uri uri)
{
//Exclude service data
if (uri.OriginalString.EndsWith(".rels") ||
uri.OriginalString.EndsWith(".psmdcp"))
{
return true;
}
return false;
}
}
}
@@ -0,0 +1,7 @@
namespace Strata.Excel.TestUtilities
{
public interface IXLExample
{
void Create(string filePath);
}
}
@@ -0,0 +1,533 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.IO.Packaging;
using System.Linq;
using System.Net.Mime;
using System.Text;
using System.Xml.Serialization;
namespace Strata.Excel.TestUtilities
{
[ExcludeFromCodeCoverage]
public static class PackageHelper
{
public static void WriteXmlPart(Package package, Uri uri, object content, XmlSerializer serializer)
{
if (package.PartExists(uri))
{
package.DeletePart(uri);
}
PackagePart part = package.CreatePart(uri, MediaTypeNames.Text.Xml, CompressionOption.Fast);
using (Stream stream = part.GetStream())
{
serializer.Serialize(stream, content);
}
}
public static object ReadXmlPart(Package package, Uri uri, XmlSerializer serializer)
{
if (!package.PartExists(uri))
{
throw new ApplicationException($"Package part '{uri.OriginalString}' doesn't exists!");
}
PackagePart part = package.GetPart(uri);
using (Stream stream = part.GetStream())
{
return serializer.Deserialize(stream);
}
}
public static void WriteBinaryPart(Package package, Uri uri, Stream content)
{
if (package.PartExists(uri))
{
package.DeletePart(uri);
}
PackagePart part = package.CreatePart(uri, MediaTypeNames.Application.Octet, CompressionOption.Fast);
using (Stream stream = part.GetStream())
{
StreamHelper.StreamToStreamAppend(content, stream);
}
}
/// <summary>
/// Returns part's stream
/// </summary>
/// <param name="package"></param>
/// <param name="uri"></param>
/// <returns></returns>
public static Stream ReadBinaryPart(Package package, Uri uri)
{
if (!package.PartExists(uri))
{
throw new ApplicationException("Package part doesn't exists!");
}
PackagePart part = package.GetPart(uri);
return part.GetStream();
}
public static void CopyPart(Uri uri, Package source, Package dest)
{
CopyPart(uri, source, dest, true);
}
public static void CopyPart(Uri uri, Package source, Package dest, bool overwrite)
{
#region Check
if (ReferenceEquals(uri, null))
{
throw new ArgumentNullException(nameof(uri));
}
if (ReferenceEquals(source, null))
{
throw new ArgumentNullException(nameof(source));
}
if (ReferenceEquals(dest, null))
{
throw new ArgumentNullException(nameof(dest));
}
#endregion Check
if (dest.PartExists(uri))
{
if (!overwrite)
{
throw new ArgumentException("Specified part already exists", nameof(uri));
}
dest.DeletePart(uri);
}
PackagePart sourcePart = source.GetPart(uri);
PackagePart destPart = dest.CreatePart(uri, sourcePart.ContentType, sourcePart.CompressionOption);
using (Stream sourceStream = sourcePart.GetStream())
{
using (Stream destStream = destPart.GetStream())
{
StreamHelper.StreamToStreamAppend(sourceStream, destStream);
}
}
}
public static void WritePart<T>(Package package, PackagePartDescriptor descriptor, T content,
Action<Stream, T> serializeAction)
{
#region Check
if (ReferenceEquals(package, null))
{
throw new ArgumentNullException(nameof(package));
}
if (ReferenceEquals(descriptor, null))
{
throw new ArgumentNullException(nameof(descriptor));
}
if (ReferenceEquals(serializeAction, null))
{
throw new ArgumentNullException(nameof(serializeAction));
}
#endregion Check
if (package.PartExists(descriptor.Uri))
{
package.DeletePart(descriptor.Uri);
}
PackagePart part = package.CreatePart(descriptor.Uri, descriptor.ContentType, descriptor.CompressOption);
using (Stream stream = part.GetStream())
{
serializeAction(stream, content);
}
}
public static void WritePart(Package package, PackagePartDescriptor descriptor, Action<Stream> serializeAction)
{
#region Check
if (ReferenceEquals(package, null))
{
throw new ArgumentNullException(nameof(package));
}
if (ReferenceEquals(descriptor, null))
{
throw new ArgumentNullException(nameof(descriptor));
}
if (ReferenceEquals(serializeAction, null))
{
throw new ArgumentNullException(nameof(serializeAction));
}
#endregion Check
if (package.PartExists(descriptor.Uri))
{
package.DeletePart(descriptor.Uri);
}
PackagePart part = package.CreatePart(descriptor.Uri, descriptor.ContentType, descriptor.CompressOption);
using (Stream stream = part.GetStream())
{
serializeAction(stream);
}
}
public static T ReadPart<T>(Package package, Uri uri, Func<Stream, T> deserializeFunc)
{
#region Check
if (ReferenceEquals(package, null))
{
throw new ArgumentNullException(nameof(package));
}
if (ReferenceEquals(uri, null))
{
throw new ArgumentNullException(nameof(uri));
}
if (ReferenceEquals(deserializeFunc, null))
{
throw new ArgumentNullException(nameof(deserializeFunc));
}
#endregion Check
if (!package.PartExists(uri))
{
throw new ApplicationException($"Package part '{uri.OriginalString}' doesn't exists!");
}
PackagePart part = package.GetPart(uri);
using (Stream stream = part.GetStream())
{
return deserializeFunc(stream);
}
}
public static void ReadPart(Package package, Uri uri, Action<Stream> deserializeAction)
{
#region Check
if (ReferenceEquals(package, null))
{
throw new ArgumentNullException(nameof(package));
}
if (ReferenceEquals(uri, null))
{
throw new ArgumentNullException(nameof(uri));
}
if (ReferenceEquals(deserializeAction, null))
{
throw new ArgumentNullException(nameof(deserializeAction));
}
#endregion Check
if (!package.PartExists(uri))
{
throw new ApplicationException($"Package part '{uri.OriginalString}' doesn't exists!");
}
PackagePart part = package.GetPart(uri);
using (Stream stream = part.GetStream())
{
deserializeAction(stream);
}
}
public static bool TryReadPart(Package package, Uri uri, Action<Stream> deserializeAction)
{
#region Check
if (ReferenceEquals(package, null))
{
throw new ArgumentNullException(nameof(package));
}
if (ReferenceEquals(uri, null))
{
throw new ArgumentNullException(nameof(uri));
}
if (ReferenceEquals(deserializeAction, null))
{
throw new ArgumentNullException(nameof(deserializeAction));
}
#endregion Check
if (!package.PartExists(uri))
{
return false;
}
PackagePart part = package.GetPart(uri);
using (Stream stream = part.GetStream())
{
deserializeAction(stream);
}
return true;
}
/// <summary>
/// Compare to packages by parts like streams
/// </summary>
/// <param name="left"></param>
/// <param name="right"></param>
/// <param name="compareToFirstDifference"></param>
/// <param name="excludeMethod"></param>
/// <param name="message"></param>
/// <returns></returns>
public static bool Compare(Package left, Package right, bool compareToFirstDifference, out string message)
{
return Compare(left, right, compareToFirstDifference, null, out message);
}
/// <summary>
/// Compare to packages by parts like streams
/// </summary>
/// <param name="left"></param>
/// <param name="right"></param>
/// <param name="compareToFirstDifference"></param>
/// <param name="excludeMethod"></param>
/// <param name="message"></param>
/// <returns></returns>
public static bool Compare(Package left, Package right, bool compareToFirstDifference,
Func<Uri, bool> excludeMethod, out string message)
{
#region Check
if (left == null)
{
throw new ArgumentNullException(nameof(left));
}
if (right == null)
{
throw new ArgumentNullException(nameof(right));
}
#endregion Check
excludeMethod = excludeMethod ?? (uri => false);
PackagePartCollection leftParts = left.GetParts();
PackagePartCollection rightParts = right.GetParts();
var pairs = new Dictionary<Uri, PartPair>();
foreach (PackagePart part in leftParts)
{
if (excludeMethod(part.Uri))
{
continue;
}
pairs.Add(part.Uri, new PartPair(part.Uri, CompareStatus.OnlyOnLeft));
}
foreach (PackagePart part in rightParts)
{
if (excludeMethod(part.Uri))
{
continue;
}
if (pairs.TryGetValue(part.Uri, out PartPair pair))
{
pair.Status = CompareStatus.Equal;
}
else
{
pairs.Add(part.Uri, new PartPair(part.Uri, CompareStatus.OnlyOnRight));
}
}
if (compareToFirstDifference && pairs.Any(pair => pair.Value.Status != CompareStatus.Equal))
{
goto EXIT;
}
foreach (PartPair pair in pairs.Values)
{
if (pair.Status != CompareStatus.Equal)
{
continue;
}
var leftPart = left.GetPart(pair.Uri);
var rightPart = right.GetPart(pair.Uri);
using (Stream leftPackagePartStream = leftPart.GetStream(FileMode.Open, FileAccess.Read))
using (Stream rightPackagePartStream = rightPart.GetStream(FileMode.Open, FileAccess.Read))
using (var leftMemoryStream = new MemoryStream())
using (var rightMemoryStream = new MemoryStream())
{
leftPackagePartStream.CopyTo(leftMemoryStream);
rightPackagePartStream.CopyTo(rightMemoryStream);
leftMemoryStream.Seek(0, SeekOrigin.Begin);
rightMemoryStream.Seek(0, SeekOrigin.Begin);
bool stripColumnWidthsFromSheet = TestHelper.StripColumnWidths &&
leftPart.ContentType == @"application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml" &&
rightPart.ContentType == @"application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml";
var tuple1 = new Tuple<Uri, Stream>(pair.Uri, leftMemoryStream);
var tuple2 = new Tuple<Uri, Stream>(pair.Uri, rightMemoryStream);
if (!StreamHelper.Compare(tuple1, tuple2, stripColumnWidthsFromSheet))
{
pair.Status = CompareStatus.NonEqual;
if (compareToFirstDifference)
{
goto EXIT;
}
}
}
}
EXIT:
List<PartPair> sortedPairs = pairs.Values.ToList();
sortedPairs.Sort((one, other) => one.Uri.OriginalString.CompareTo(other.Uri.OriginalString));
var sbuilder = new StringBuilder();
foreach (PartPair pair in sortedPairs)
{
if (pair.Status == CompareStatus.Equal)
{
continue;
}
sbuilder.AppendFormat("{0} :{1}", pair.Uri, pair.Status);
sbuilder.AppendLine();
}
message = sbuilder.ToString();
return message.Length == 0;
}
#region Nested type: PackagePartDescriptor
public sealed class PackagePartDescriptor
{
#region Private fields
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
private readonly CompressionOption _compressOption;
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
private readonly string _contentType;
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
private readonly Uri _uri;
#endregion Private fields
#region Constructor
/// <summary>
/// Instance constructor
/// </summary>
/// <param name=nameof(uri)>Part uri</param>
/// <param name="contentType">Content type from <see cref="MediaTypeNames" /></param>
/// <param name="compressOption"></param>
public PackagePartDescriptor(Uri uri, string contentType, CompressionOption compressOption)
{
#region Check
if (ReferenceEquals(uri, null))
{
throw new ArgumentNullException(nameof(uri));
}
if (string.IsNullOrEmpty(contentType))
{
throw new ArgumentNullException(nameof(contentType));
}
#endregion Check
_uri = uri;
_contentType = contentType;
_compressOption = compressOption;
}
#endregion Constructor
#region Public properties
public Uri Uri
{
[DebuggerStepThrough]
get => _uri;
}
public string ContentType
{
[DebuggerStepThrough]
get => _contentType;
}
public CompressionOption CompressOption
{
[DebuggerStepThrough]
get { return _compressOption; }
}
#endregion Public properties
#region Public methods
public override string ToString() => $"Uri:{_uri} ContentType: {_contentType}, Compression: {_compressOption}";
#endregion Public methods
}
#endregion Nested type: PackagePartDescriptor
#region Nested type: CompareStatus
private enum CompareStatus
{
OnlyOnLeft,
OnlyOnRight,
Equal,
NonEqual
}
#endregion Nested type: CompareStatus
#region Nested type: PartPair
private sealed class PartPair
{
#region Private fields
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
private readonly Uri _uri;
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
private CompareStatus _status;
#endregion Private fields
#region Constructor
public PartPair(Uri uri, CompareStatus status)
{
_uri = uri;
_status = status;
}
#endregion Constructor
#region Public properties
public Uri Uri
{
[DebuggerStepThrough]
get { return _uri; }
}
public CompareStatus Status
{
[DebuggerStepThrough]
get { return _status; }
[DebuggerStepThrough]
set { _status = value; }
}
#endregion Public properties
}
#endregion Nested type: PartPair
//--
}
}
@@ -0,0 +1,253 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Reflection;
namespace Strata.Excel.TestUtilities
{
/// <summary>
/// Summary description for ResourceFileExtractor.
/// </summary>
[ExcludeFromCodeCoverage]
public sealed class ResourceFileExtractor
{
#region Static
#region Private fields
private static readonly IDictionary<string, ResourceFileExtractor> extractors = new ConcurrentDictionary<string, ResourceFileExtractor>();
#endregion Private fields
#region Public properties
/// <summary>Instance of resource extractor for executing assembly </summary>
public static ResourceFileExtractor Instance
{
get
{
var _assembly = Assembly.GetCallingAssembly();
var _key = _assembly.GetName().FullName;
if (extractors.TryGetValue(_key, out var extractor) ||
extractors.TryGetValue(_key, out extractor)) return extractor;
extractor = new ResourceFileExtractor(_assembly, true, null);
extractors.Add(_key, extractor);
return extractor;
}
}
#endregion Public properties
#endregion Static
#region Private fields
//private readonly Assembly m_assembly;
private readonly ResourceFileExtractor m_baseExtractor;
//private bool m_isStatic;
//private string ResourceFilePath { get; }
#endregion Private fields
#region Constructors
/// <summary>
/// Create instance
/// </summary>
/// <param name="resourceFilePath"><c>ResourceFilePath</c> in assembly. Example: .Properties.Scripts.</param>
/// <param name="baseExtractor"></param>
public ResourceFileExtractor(string resourceFilePath, ResourceFileExtractor baseExtractor)
: this(Assembly.GetCallingAssembly(), baseExtractor)
{
ResourceFilePath = resourceFilePath;
}
/// <summary>
/// Create instance
/// </summary>
/// <param name="baseExtractor"></param>
public ResourceFileExtractor(ResourceFileExtractor baseExtractor)
: this(Assembly.GetCallingAssembly(), baseExtractor)
{
}
/// <summary>
/// Create instance
/// </summary>
/// <param name="resourcePath"><c>ResourceFilePath</c> in assembly. Example: .Properties.Scripts.</param>
public ResourceFileExtractor(string resourcePath)
: this(Assembly.GetCallingAssembly(), resourcePath)
{
}
/// <summary>
/// Instance constructor
/// </summary>
/// <param name="assembly"></param>
/// <param name="resourcePath"></param>
public ResourceFileExtractor(Assembly assembly, string resourcePath)
: this(assembly ?? Assembly.GetCallingAssembly())
{
ResourceFilePath = resourcePath;
}
/// <summary>
/// Instance constructor
/// </summary>
public ResourceFileExtractor()
: this(Assembly.GetCallingAssembly())
{
}
/// <summary>
/// Instance constructor
/// </summary>
/// <param name="assembly"></param>
public ResourceFileExtractor(Assembly assembly)
: this(assembly ?? Assembly.GetCallingAssembly(), (ResourceFileExtractor)null)
{
}
/// <summary>
/// Instance constructor
/// </summary>
/// <param name="assembly"></param>
/// <param name="baseExtractor"></param>
public ResourceFileExtractor(Assembly assembly, ResourceFileExtractor baseExtractor)
: this(assembly ?? Assembly.GetCallingAssembly(), false, baseExtractor)
{
}
/// <summary>
/// Instance constructor
/// </summary>
/// <param name="assembly"></param>
/// <param name="isStatic"></param>
/// <param name="baseExtractor"></param>
/// <exception cref="ArgumentNullException">Argument is null.</exception>
private ResourceFileExtractor(Assembly assembly, bool isStatic, ResourceFileExtractor baseExtractor)
{
#region Check
if (assembly is null)
{
throw new ArgumentNullException(nameof(assembly));
}
#endregion Check
Assembly = assembly;
m_baseExtractor = baseExtractor;
AssemblyName = Assembly.GetName().Name;
IsStatic = isStatic;
ResourceFilePath = ".Resources.";
}
#endregion Constructors
#region Public properties
/// <summary> Work assembly </summary>
public Assembly Assembly { get; }
/// <summary> Work assembly name </summary>
public string AssemblyName { get; }
/// <summary>
/// Path to read resource files. Example: .Resources.Upgrades.
/// </summary>
public string ResourceFilePath { get; }
public bool IsStatic { get; set; }
public IEnumerable<string> GetFileNames(Func<String, Boolean> predicate = null)
{
predicate = predicate ?? (s => true);
var _path = AssemblyName + ResourceFilePath;
foreach (string _resourceName in Assembly.GetManifestResourceNames())
{
if (_resourceName.StartsWith(_path) && predicate(_resourceName))
{
yield return _resourceName.Replace(_path, string.Empty);
}
}
}
#endregion Public properties
#region Public methods
public string ReadFileFromResource(string fileName)
{
var _stream = ReadFileFromResourceToStream(fileName);
string _result;
var sr = new StreamReader(_stream);
try
{
_result = sr.ReadToEnd();
}
finally
{
sr.Close();
}
return _result;
}
public string ReadFileFromResourceFormat(string fileName, params object[] formatArgs)
{
return string.Format(ReadFileFromResource(fileName), formatArgs);
}
/// <summary>
/// Read file in current assembly by specific path
/// </summary>
/// <param name="specificPath">Specific path</param>
/// <param name="fileName">Read file name</param>
/// <returns></returns>
public string ReadSpecificFileFromResource(string specificPath, string fileName)
{
ResourceFileExtractor _ext = new ResourceFileExtractor(Assembly, specificPath);
return _ext.ReadFileFromResource(fileName);
}
/// <summary>
/// Read file in current assembly by specific file name
/// </summary>
/// <param name="fileName"></param>
/// <returns></returns>
/// <exception cref="ApplicationException"><c>ApplicationException</c>.</exception>
public Stream ReadFileFromResourceToStream(string fileName)
{
var _nameResFile = AssemblyName + ResourceFilePath + fileName;
var _stream = Assembly.GetManifestResourceStream(_nameResFile);
#region Not found
if (_stream is null)
{
#region Get from base extractor
if (!(m_baseExtractor is null))
{
return m_baseExtractor.ReadFileFromResourceToStream(fileName);
}
#endregion Get from base extractor
throw new ArgumentException("Can't find resource file " + _nameResFile, nameof(fileName));
}
#endregion Not found
return _stream;
}
#endregion Public methods
}
}
@@ -0,0 +1,191 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
namespace Strata.Excel.TestUtilities
{
/// <summary>
/// Help methods for work with streams
/// </summary>
[ExcludeFromCodeCoverage]
public static class StreamHelper
{
/// <summary>
/// Convert stream to byte array
/// </summary>
/// <param name="pStream">Stream</param>
/// <returns>Byte array</returns>
public static byte[] StreamToArray(Stream pStream)
{
long iLength = pStream.Length;
var bytes = new byte[iLength];
for (int i = 0; i < iLength; i++)
{
bytes[i] = (byte)pStream.ReadByte();
}
pStream.Close();
return bytes;
}
/// <summary>
/// Convert byte array to stream
/// </summary>
/// <param name="pBynaryArray">Byte array</param>
/// <param name="pStream">Open stream</param>
/// <returns></returns>
public static Stream ArrayToStreamAppend(byte[] pBynaryArray, Stream pStream)
{
#region Check params
if (ReferenceEquals(pBynaryArray, null))
{
throw new ArgumentNullException(nameof(pBynaryArray));
}
if (ReferenceEquals(pStream, null))
{
throw new ArgumentNullException(nameof(pStream));
}
if (!pStream.CanWrite)
{
throw new ArgumentException("Can't write to stream", nameof(pStream));
}
#endregion Check params
foreach (byte b in pBynaryArray)
{
pStream.WriteByte(b);
}
return pStream;
}
public static void StreamToStreamAppend(Stream streamIn, Stream streamToWrite)
{
StreamToStreamAppend(streamIn, streamToWrite, 0);
}
public static void StreamToStreamAppend(Stream streamIn, Stream streamToWrite, long dataLength)
{
#region Check params
if (ReferenceEquals(streamIn, null))
{
throw new ArgumentNullException(nameof(streamIn));
}
if (ReferenceEquals(streamToWrite, null))
{
throw new ArgumentNullException(nameof(streamToWrite));
}
if (!streamIn.CanRead)
{
throw new ArgumentException("Can't read from stream", nameof(streamIn));
}
if (!streamToWrite.CanWrite)
{
throw new ArgumentException("Can't write to stream", nameof(streamToWrite));
}
#endregion Check params
var buf = new byte[512];
long length;
if (dataLength == 0)
{
length = streamIn.Length - streamIn.Position;
}
else
{
length = dataLength;
}
long rest = length;
while (rest > 0)
{
int len1 = streamIn.Read(buf, 0, rest >= 512 ? 512 : (int)rest);
streamToWrite.Write(buf, 0, len1);
rest -= len1;
}
}
/// <summary>
/// Compare two streams by converting them to strings and comparing the strings
/// </summary>
/// <param name="one"></param>
/// <param name="other"></param>
/// /// <param name="stripColumnWidths"></param>
/// <returns></returns>
public static bool Compare(Tuple<Uri, Stream> tuple1, Tuple<Uri, Stream> tuple2, bool stripColumnWidths)
{
#region Check
if (tuple1 == null || tuple1.Item1 == null || tuple1.Item2 == null)
{
throw new ArgumentNullException(nameof(tuple1));
}
if (tuple2 == null || tuple2.Item1 == null || tuple2.Item2 == null)
{
throw new ArgumentNullException(nameof(tuple2));
}
if (tuple1.Item2.Position != 0)
{
throw new ArgumentException("Must be in position 0", nameof(tuple1));
}
if (tuple2.Item2.Position != 0)
{
throw new ArgumentException("Must be in position 0", nameof(tuple2));
}
#endregion Check
var stringOne = new StreamReader(tuple1.Item2).ReadToEnd().RemoveIgnoredParts(tuple1.Item1, stripColumnWidths, ignoreGuids: true);
var stringOther = new StreamReader(tuple2.Item2).ReadToEnd().RemoveIgnoredParts(tuple2.Item1, stripColumnWidths, ignoreGuids: true);
return stringOne == stringOther;
}
private static string RemoveIgnoredParts(this string s, Uri uri, bool ignoreColumnWidths, bool ignoreGuids)
{
s = uriSpecificIgnores.Where(p => p.Key.Equals(uri.OriginalString)).Aggregate(s, (current, pair) => pair.Value.Replace(current, ""));
// Collapse empty xml elements
s = emptyXmlElementRegex.Replace(s, "<$1 />");
if (ignoreColumnWidths)
s = RemoveColumnWidths(s);
if (ignoreGuids)
s = RemoveGuids(s);
return s;
}
private static IEnumerable<KeyValuePair<string, Regex>> uriSpecificIgnores = new List<KeyValuePair<string, Regex>>()
{
// Remove dcterms elements
new KeyValuePair<string, Regex>("/docProps/core.xml", new Regex(@"<dcterms:(\w+).*?<\/dcterms:\1>", RegexOptions.Compiled))
};
private static Regex emptyXmlElementRegex = new Regex(@"<([\w:]+)><\/\1>", RegexOptions.Compiled);
private static Regex columnRegex = new Regex("<x:col.*?width=\"\\d+(\\.\\d+)?\".*?\\/>", RegexOptions.Compiled);
private static Regex widthRegex = new Regex("width=\"\\d+(\\.\\d+)?\"\\s+", RegexOptions.Compiled);
private static string RemoveColumnWidths(string s)
{
var replacements = new Dictionary<string, string>();
foreach (var m in columnRegex.Matches(s).OfType<Match>())
{
var original = m.Groups[0].Value;
var replacement = widthRegex.Replace(original, "");
replacements.Add(original, replacement);
}
return replacements.Aggregate(s, (current, r) => current.Replace(r.Key, r.Value));
}
private static Regex guidRegex = new Regex(@"{[0-9A-Fa-f]{8}-([0-9A-Fa-f]{4}-){3}[0-9A-Fa-f]{12}}", RegexOptions.Compiled | RegexOptions.Multiline);
private static string RemoveGuids(string s) => guidRegex.Replace(s, m => string.Empty);
}
}
@@ -0,0 +1,145 @@
using ClosedXML.Excel;
using FluentAssertions;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Threading;
namespace Strata.Excel.TestUtilities
{
[ExcludeFromCodeCoverage]
public static class TestHelper
{
public static string CurrencySymbol => Thread.CurrentThread.CurrentCulture.NumberFormat.CurrencySymbol;
//Note: Run example tests parameters
public static string TestsOutputDirectory => Path.Combine(Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location), "Generated");
public const string ActualTestResultPostFix = "";
public static readonly string ExampleTestsOutputDirectory = Path.Combine(TestsOutputDirectory, "Examples");
private const bool CompareWithResources = true;
private static readonly ResourceFileExtractor _extractor = new ResourceFileExtractor(".Resource.");
public static void SaveWorkbook(XLWorkbook workbook, params string[] fileNameParts)
{
workbook.SaveAs(Path.Combine(new string[] { TestsOutputDirectory }.Concat(fileNameParts).ToArray()), true);
}
// Because different fonts are installed on Unix,
// the columns widths after AdjustToContents() will
// cause the tests to fail.
// Therefore we ignore the width attribute when running on Unix
public static bool StripColumnWidths => IsRunningOnUnix;
public static bool IsRunningOnUnix
{
get
{
var p = (int)Environment.OSVersion.Platform;
return ((p == 4) || (p == 6) || (p == 128));
}
}
public static void RunTestExample<T>(string filePartName, bool evaluateFormulae = false)
where T : IXLExample, new()
{
// Make sure tests run on a deterministic culture
Thread.CurrentThread.CurrentCulture = new System.Globalization.CultureInfo("en-US");
var example = new T();
var pathParts = filePartName.Split(new char[] { '\\' });
var filePath1 = Path.Combine(new List<string>() { ExampleTestsOutputDirectory }.Concat(pathParts).ToArray());
var extension = Path.GetExtension(filePath1);
var directory = Path.GetDirectoryName(filePath1);
var fileName = Path.GetFileNameWithoutExtension(filePath1);
fileName += ActualTestResultPostFix;
fileName = Path.ChangeExtension(fileName, extension);
filePath1 = Path.Combine(directory, "z" + fileName);
var filePath2 = Path.Combine(directory, fileName);
//Run test
example.Create(filePath1);
using (var wb = new XLWorkbook(filePath1))
wb.SaveAs(filePath2, validate: true, evaluateFormulae);
// Also load from template and save it again - but not necessary to test against reference file
// We're just testing that it can save.
using (var ms = new MemoryStream())
using (var wb = XLWorkbook.OpenFromTemplate(filePath1))
wb.SaveAs(ms, validate: true, evaluateFormulae);
var resourcePath = "Examples." + filePartName.Replace('\\', '.').TrimStart('.');
using (var streamExpected = _extractor.ReadFileFromResourceToStream(resourcePath))
using (var streamActual = File.OpenRead(filePath2))
{
var success = ExcelDocsComparer.Compare(streamActual, streamExpected, out string message);
var formattedMessage =
$"Actual file '{filePath2}' is different than the expected file '{resourcePath}'. The difference is: '{message}'";
success.Should().BeTrue(formattedMessage);
}
}
public static void CreateAndCompare(Func<IXLWorkbook> workbookGenerator, string referenceResource, bool evaluateFormulae = false)
{
Thread.CurrentThread.CurrentCulture = new System.Globalization.CultureInfo("en-US");
var pathParts = referenceResource.Split(new char[] { '\\' });
var filePath1 = Path.Combine(new List<string>() { TestsOutputDirectory }.Concat(pathParts).ToArray());
var extension = Path.GetExtension(filePath1);
var directory = Path.GetDirectoryName(filePath1);
var fileName = Path.GetFileNameWithoutExtension(filePath1);
fileName += ActualTestResultPostFix;
fileName = Path.ChangeExtension(fileName, extension);
var filePath2 = Path.Combine(directory, fileName);
using (var wb = workbookGenerator.Invoke())
wb.SaveAs(filePath2, true, evaluateFormulae);
var resourcePath = referenceResource.Replace('\\', '.').TrimStart('.');
using (var streamExpected = _extractor.ReadFileFromResourceToStream(resourcePath))
using (var streamActual = File.OpenRead(filePath2))
{
var success = ExcelDocsComparer.Compare(streamActual, streamExpected, out string message);
var formattedMessage =
$"Actual file '{filePath2}' is different than the expected file '{resourcePath}'. The difference is: '{message}'";
success.Should().BeTrue(formattedMessage);
}
}
public static string GetResourcePath(string filePartName)
{
return filePartName.Replace('\\', '.').TrimStart('.');
}
public static Stream GetStreamFromResource(string resourcePath)
{
return _extractor.ReadFileFromResourceToStream(resourcePath);
}
public static void LoadFile(string filePartName)
{
IXLWorkbook wb;
using var stream = GetStreamFromResource(GetResourcePath(filePartName));
Action action = () =>
{
wb = new XLWorkbook(stream);
};
action.Should().NotThrow($"Unable to load resource {filePartName}");
}
public static IEnumerable<String> ListResourceFiles(Func<String, Boolean> predicate = null)
{
return _extractor.GetFileNames(predicate);
}
}
}
@@ -0,0 +1,160 @@
using ClosedXML.Excel;
using FluentAssertions;
using Newtonsoft.Json;
using NUnit.Framework;
using Strata.Excel.Core.Import;
using Strata.Excel.TestUtilities;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using static Strata.Excel.Core.ExportUtils;
namespace Strata.Excel.Core.Test.Unit.ExcelExportTests
{
[TestFixture]
public class TestExcelExport
{
[OneTimeSetUp]
public void RunBeforeAnyTests()
{
Environment.CurrentDirectory = TestContext.CurrentContext.TestDirectory;
// or identically under the hoods
Directory.SetCurrentDirectory(TestContext.CurrentContext.TestDirectory);
}
private static readonly ResourceFileExtractor _extractor = new ResourceFileExtractor(".ExpectedResults.");
private static readonly ResourceFileExtractor _dataExtractor = new ResourceFileExtractor(".ExcelExportTests.");
[Test, Ignore("File and stream do not match")]
public void TestCreateExcelWorkbook()
{
var currentDir = Directory.GetCurrentDirectory();
var jsonData = _dataExtractor.ReadFileFromResource("data.json");
var data = JsonConvert.DeserializeObject<List<TestData>>(jsonData)
.OrderBy(t => t.OrgPin)
.ThenBy(t => t.DatabaseFriendlyName)
.ThenBy(t => t.Description);
var options = new ExportOptions
{
EmptyMessage = "No Mappings"
};
options.AddColumnOptions("Confidence Score", NumberFormatId.ZeroPercent, XLAlignmentHorizontalValues.Right);
options.AddColumnOptions("Date Mapped", NumberFormatId.ShortDateSlash, XLAlignmentHorizontalValues.Right);
var expected = $@"{TestContext.CurrentContext.Test.Name}.xlsx";
var wb = CreateExcelWorkbook(data, options);
#pragma warning disable S125 // Sections of code should not be commented out
// wb.SaveAs(Path.Combine(@"C:\Git\excel.core\tests\Strata.Excel.Core.Test.Unit\ExcelExportTests\", expected));
#pragma warning restore S125 // Sections of code should not be commented
// assert
wb.Worksheets.Should().HaveCount(1);
var ws = wb.Worksheet(1);
ws.Should().NotBeNull();
using (var expectedStream = _extractor.ReadFileFromResourceToStream(expected))
using (var actualStream = new MemoryStream())
{
wb.SaveAs(actualStream);
actualStream.Compare(expectedStream, out var message).Should().BeTrue(message);
}
}
[Test, Ignore("File and stream do not match")]
public void TestCreateExcelWorkbookWithoutHumanizedHeadings()
{
var currentDir = Directory.GetCurrentDirectory();
var jsonData = _dataExtractor.ReadFileFromResource("data.json");
var data = JsonConvert.DeserializeObject<List<TestData>>(jsonData)
.OrderBy(t => t.OrgPin)
.ThenBy(t => t.DatabaseFriendlyName)
.ThenBy(t => t.Description);
var options = new ExportOptions
{
EmptyMessage = "No Mappings",
HumanizeHeading = false
};
options.AddColumnOptions("ConfidenceScore", NumberFormatId.ZeroPercent, XLAlignmentHorizontalValues.Right);
options.AddColumnOptions("DateMapped", NumberFormatId.ShortDateSlash, XLAlignmentHorizontalValues.Right);
var expected = $@"{TestContext.CurrentContext.Test.Name}.xlsx";
var wb = CreateExcelWorkbook(data, options);
#pragma warning disable S125 // Sections of code should not be commented out
//wb.SaveAs(Path.Combine(@"C:\Git\excel.core\tests\Strata.Excel.Core.Test.Unit\ExcelExportTests\", expected));
#pragma warning restore S125 // Sections of code should not be commented
// assert
wb.Worksheets.Should().HaveCount(1);
var ws = wb.Worksheet(1);
ws.Should().NotBeNull();
using (var expectedStream = _extractor.ReadFileFromResourceToStream(expected))
using (var actualStream = new MemoryStream())
{
wb.SaveAs(actualStream);
actualStream.Compare(expectedStream, out var message).Should().BeTrue(message);
}
}
[Test, Ignore("This keeps requiring that we update the stream on the test")]
public void TestCreateExcelWorkbookWithTitlePage()
{
var currentDir = Directory.GetCurrentDirectory();
var jsonData = _dataExtractor.ReadFileFromResource("data.json");
var data = JsonConvert.DeserializeObject<List<TestData>>(jsonData)
.OrderBy(t => t.OrgPin)
.ThenBy(t => t.DatabaseFriendlyName)
.ThenBy(t => t.Description);
var options = new ExportOptions
{
Title = "Account Mappgings",
SubTitle = "Strata Decision Technology®",
EmptyMessage = "No Mappings"
};
options.AddColumnOptions("Confidence Score", NumberFormatId.ZeroPercent, XLAlignmentHorizontalValues.Right);
options.AddColumnOptions("Date Mapped", NumberFormatId.ShortDateSlash, XLAlignmentHorizontalValues.Right);
var expected = $@"{TestContext.CurrentContext.Test.Name}.xlsx";
var wb = CreateExcelWorkbook(data, options);
#pragma warning disable S125 // Sections of code should not be commented out
//wb.SaveAs(Path.Combine(@"C:\Git\excel.core\tests\Strata.Excel.Core.Test.Unit\ExcelExportTests\", expected));
#pragma warning restore S125 // Sections of code should not be commented
// assert
wb.Worksheets.Should().HaveCount(2);
var ws = wb.Worksheet(2);
ws.Should().NotBeNull();
ws.GetDataFromExcel<TestData>().Should().HaveCount(data.Count());
using (var expectedStream = _extractor.ReadFileFromResourceToStream(expected))
using (var actualStream = new MemoryStream())
{
wb.SaveAs(actualStream);
actualStream.Compare(expectedStream, out var message).Should().BeTrue(message);
}
}
internal class TestData
{
public string OrgPin { get; set; }
public string DatabaseName { get; set; }
public string DatabaseFriendlyName { get; set; }
public string SphAccountRollupStatement { get; set; }
public string SphAccountRollupCategory { get; set; }
public string SphAccountRollupLineItem { get; set; }
public string SphAccountRollupName { get; set; }
public double ConfidenceScore { get; set; }
public string DateMapped { get; set; }
public int AccountId { get; set; }
public string AccountCode { get; set; }
public string Description { get; set; }
public string GLRollup { get; set; }
public string OBDollarsFinancialReporting { get; set; }
public string DSSAccountRollup1Name { get; set; }
}
}
}
@@ -0,0 +1,342 @@
[
{
"OrgPin": "0430",
"DatabaseName": "jazz tst GM Platform Demo.1 B onboarding 20200922.1",
"DatabaseFriendlyName": "StrataJazz Platform Cubekillers Demo 1 B Legacy onboarding",
"SphAccountRollupStatement": "Balance Sheet",
"SphAccountRollupCategory": "Assets - Current",
"SphAccountRollupLineItem": "Accounts Receivable - Other",
"SphAccountRollupName": "Balance Sheet - Assets - Current - Accounts Receivable - Other",
"ConfidenceScore": 0.8767915097336801,
"DateMapped": "7/26/2022",
"AccountId": 420,
"AccountCode": "105219",
"Description": "340B DIFFERENTIAL RECEIVABLE",
"GLRollup": "SKCURRENTASSETS",
"OBDollarsFinancialReporting": "Bal - CURRENT ASSETS - Other current assets",
"DSSAccountRollup1Name": "Exclude"
},
{
"OrgPin": "0430",
"DatabaseName": "jazz tst GM Platform Demo.1 B onboarding 20200922.1",
"DatabaseFriendlyName": "StrataJazz Platform Cubekillers Demo 1 B Legacy onboarding",
"SphAccountRollupStatement": "Balance Sheet",
"SphAccountRollupCategory": "Assets - Inventory",
"SphAccountRollupLineItem": "Inventory",
"SphAccountRollupName": "Balance Sheet - Assets - Inventory - Inventory",
"ConfidenceScore": 0.9104504962709786,
"DateMapped": "7/26/2022",
"AccountId": 496,
"AccountCode": "110024",
"Description": "340B RETAIL INVENTORY",
"GLRollup": "SKCURRENTASSETS",
"OBDollarsFinancialReporting": "Bal - CURRENT ASSETS - Inventory",
"DSSAccountRollup1Name": "Exclude"
},
{
"OrgPin": "0430",
"DatabaseName": "jazz tst GM Platform Demo.1 B onboarding 20200922.1",
"DatabaseFriendlyName": "StrataJazz Platform Cubekillers Demo 1 B Legacy onboarding",
"SphAccountRollupStatement": "Income Statement",
"SphAccountRollupCategory": "Revenue - Patient - Gross",
"SphAccountRollupLineItem": "Revenue - Outpatient",
"SphAccountRollupName": "Income Statement - Revenue - Patient - Gross - Revenue - Outpatient",
"ConfidenceScore": 0.9840537253695426,
"DateMapped": "7/26/2022",
"AccountId": 2359,
"AccountCode": "420800",
"Description": "340B RETAIL REVENUE",
"GLRollup": "OUTPATIENT REVENUE",
"OBDollarsFinancialReporting": "IS - REVENUE: - Outpatient revenue",
"DSSAccountRollup1Name": "Exclude"
},
{
"OrgPin": "0430",
"DatabaseName": "jazz tst GM Platform Demo.1 B onboarding 20200922.1",
"DatabaseFriendlyName": "StrataJazz Platform Cubekillers Demo 1 B Legacy onboarding",
"SphAccountRollupStatement": "Income Statement",
"SphAccountRollupCategory": "Revenue - Other - Operating",
"SphAccountRollupLineItem": "Other Operating Revenue",
"SphAccountRollupName": "Income Statement - Revenue - Other - Operating - Other Operating Revenue",
"ConfidenceScore": 0.8907208071926644,
"DateMapped": "7/26/2022",
"AccountId": 2305,
"AccountCode": "575506",
"Description": "340B RETAIL REVENUE",
"GLRollup": "OTHER REVENUE",
"OBDollarsFinancialReporting": "IS - Other Operating Rev - Other operating revenue",
"DSSAccountRollup1Name": "Other Operating Revenue"
},
{
"OrgPin": "0430",
"DatabaseName": "jazz tst GM Platform Demo.1 B onboarding 20200922.1",
"DatabaseFriendlyName": "StrataJazz Platform Cubekillers Demo 1 B Legacy onboarding",
"SphAccountRollupStatement": "Income Statement",
"SphAccountRollupCategory": "Revenue - Other - Operating",
"SphAccountRollupLineItem": "Other Operating Revenue",
"SphAccountRollupName": "Income Statement - Revenue - Other - Operating - Other Operating Revenue",
"ConfidenceScore": 0.8907208071926644,
"DateMapped": "7/26/2022",
"AccountId": 2302,
"AccountCode": "575030",
"Description": "340B RETAIL REVENUE",
"GLRollup": "OTHER REVENUE",
"OBDollarsFinancialReporting": "IS - Other Operating Rev - Other operating revenue",
"DSSAccountRollup1Name": "Exclude"
},
{
"OrgPin": "0430",
"DatabaseName": "jazz tst GM Platform Demo.1 B onboarding 20200922.1",
"DatabaseFriendlyName": "StrataJazz Platform Cubekillers Demo 1 B Legacy onboarding",
"SphAccountRollupStatement": "Balance Sheet",
"SphAccountRollupCategory": "Liabilities - Current",
"SphAccountRollupLineItem": "Accrued Employee Compensation and Benefits",
"SphAccountRollupName": "Balance Sheet - Liabilities - Current - Accrued Employee Compensation and Benefits",
"ConfidenceScore": 0.39884468761403535,
"DateMapped": "7/26/2022",
"AccountId": 1302,
"AccountCode": "244130",
"Description": "401K CONTR CATCH UP OVER 50",
"GLRollup": "SKCURRENTLIABILITIES",
"OBDollarsFinancialReporting": "Bal - CURRENT LIABILITIES - Other current liabilities",
"DSSAccountRollup1Name": "Exclude"
},
{
"OrgPin": "0430",
"DatabaseName": "jazz tst GM Platform Demo.1 B onboarding 20200922.1",
"DatabaseFriendlyName": "StrataJazz Platform Cubekillers Demo 1 B Legacy onboarding",
"SphAccountRollupStatement": "Balance Sheet",
"SphAccountRollupCategory": "Liabilities - Current",
"SphAccountRollupLineItem": "Accrued Employee Compensation and Benefits",
"SphAccountRollupName": "Balance Sheet - Liabilities - Current - Accrued Employee Compensation and Benefits",
"ConfidenceScore": 0.7123256655440802,
"DateMapped": "7/26/2022",
"AccountId": 1301,
"AccountCode": "244126",
"Description": "401K CONTR MATCH PBL",
"GLRollup": "SKCURRENTLIABILITIES",
"OBDollarsFinancialReporting": "Bal - CURRENT LIABILITIES - Other current liabilities",
"DSSAccountRollup1Name": "Exclude"
},
{
"OrgPin": "0430",
"DatabaseName": "jazz tst GM Platform Demo.1 B onboarding 20200922.1",
"DatabaseFriendlyName": "StrataJazz Platform Cubekillers Demo 1 B Legacy onboarding",
"SphAccountRollupStatement": "Balance Sheet",
"SphAccountRollupCategory": "Liabilities - Current",
"SphAccountRollupLineItem": "Accrued Employee Compensation and Benefits",
"SphAccountRollupName": "Balance Sheet - Liabilities - Current - Accrued Employee Compensation and Benefits",
"ConfidenceScore": 0.530717787014235,
"DateMapped": "7/26/2022",
"AccountId": 1304,
"AccountCode": "244140",
"Description": "401K CONTR MILITARY MAKE UP",
"GLRollup": "SKCURRENTLIABILITIES",
"OBDollarsFinancialReporting": "Bal - CURRENT LIABILITIES - Other current liabilities",
"DSSAccountRollup1Name": "Exclude"
},
{
"OrgPin": "0430",
"DatabaseName": "jazz tst GM Platform Demo.1 B onboarding 20200922.1",
"DatabaseFriendlyName": "StrataJazz Platform Cubekillers Demo 1 B Legacy onboarding",
"SphAccountRollupStatement": "Balance Sheet",
"SphAccountRollupCategory": "Liabilities - Current",
"SphAccountRollupLineItem": "Accrued Employee Compensation and Benefits",
"SphAccountRollupName": "Balance Sheet - Liabilities - Current - Accrued Employee Compensation and Benefits",
"ConfidenceScore": 0.7266931053978312,
"DateMapped": "7/26/2022",
"AccountId": 1303,
"AccountCode": "244135",
"Description": "401K CONTR NO MATCH",
"GLRollup": "SKCURRENTLIABILITIES",
"OBDollarsFinancialReporting": "Bal - CURRENT LIABILITIES - Other current liabilities",
"DSSAccountRollup1Name": "Exclude"
},
{
"OrgPin": "0430",
"DatabaseName": "jazz tst GM Platform Demo.1 B onboarding 20200922.1",
"DatabaseFriendlyName": "StrataJazz Platform Cubekillers Demo 1 B Legacy onboarding",
"SphAccountRollupStatement": "Income Statement",
"SphAccountRollupCategory": "Revenue - Other - Nonoperating Items",
"SphAccountRollupLineItem": "Contributions",
"SphAccountRollupName": "Income Statement - Revenue - Other - Nonoperating Items - Contributions",
"ConfidenceScore": 0.5031995471555109,
"DateMapped": "7/26/2022",
"AccountId": 1300,
"AccountCode": "244125",
"Description": "401K CONTRIBUTIONS",
"GLRollup": "SKCURRENTLIABILITIES",
"OBDollarsFinancialReporting": "Bal - CURRENT LIABILITIES - Other current liabilities",
"DSSAccountRollup1Name": "Exclude"
},
{
"OrgPin": "0430",
"DatabaseName": "jazz tst GM Platform Demo.1 B onboarding 20200922.1",
"DatabaseFriendlyName": "StrataJazz Platform Cubekillers Demo 1 B Legacy onboarding",
"SphAccountRollupStatement": "Income Statement",
"SphAccountRollupCategory": "Expenses - Operating",
"SphAccountRollupLineItem": "Supplies",
"SphAccountRollupName": "Income Statement - Expenses - Operating - Supplies",
"ConfidenceScore": 0.4405172973818624,
"DateMapped": "7/26/2022",
"AccountId": 2799,
"AccountCode": "646009",
"Description": "A2CL BLOOD ALLOCATION",
"GLRollup": "SUPPLIES",
"OBDollarsFinancialReporting": "IS - EXPENSES: - Medical supplies",
"DSSAccountRollup1Name": "Medical Supplies"
},
{
"OrgPin": "0430",
"DatabaseName": "jazz tst GM Platform Demo.1 B onboarding 20200922.1",
"DatabaseFriendlyName": "StrataJazz Platform Cubekillers Demo 1 B Legacy onboarding",
"SphAccountRollupStatement": "Income Statement",
"SphAccountRollupCategory": "Revenue - Other - Operating",
"SphAccountRollupLineItem": "Other Operating Revenue",
"SphAccountRollupName": "Income Statement - Revenue - Other - Operating - Other Operating Revenue",
"ConfidenceScore": 0.9150939588019593,
"DateMapped": "7/26/2022",
"AccountId": 2287,
"AccountCode": "571901",
"Description": "A2CL RVU TRANSFER",
"GLRollup": "OTHER REVENUE",
"OBDollarsFinancialReporting": "IS - Other Operating Rev - Other operating revenue",
"DSSAccountRollup1Name": "Exclude"
},
{
"OrgPin": "0430",
"DatabaseName": "jazz tst GM Platform Demo.1 B onboarding 20200922.1",
"DatabaseFriendlyName": "StrataJazz Platform Cubekillers Demo 1 B Legacy onboarding",
"SphAccountRollupStatement": "Balance Sheet",
"SphAccountRollupCategory": "Assets - Current",
"SphAccountRollupLineItem": "Accounts Receivable - Other",
"SphAccountRollupName": "Balance Sheet - Assets - Current - Accounts Receivable - Other",
"ConfidenceScore": 0.7593035979954855,
"DateMapped": "7/26/2022",
"AccountId": 443,
"AccountCode": "105320",
"Description": "A2CL SERVICES RECEIVABLE",
"GLRollup": "SKCURRENTASSETS",
"OBDollarsFinancialReporting": "Bal - CURRENT ASSETS - Other current assets",
"DSSAccountRollup1Name": "Exclude"
},
{
"OrgPin": "0430",
"DatabaseName": "jazz tst GM Platform Demo.1 B onboarding 20200922.1",
"DatabaseFriendlyName": "StrataJazz Platform Cubekillers Demo 1 B Legacy onboarding",
"SphAccountRollupStatement": "Balance Sheet",
"SphAccountRollupCategory": "Liabilities - Current",
"SphAccountRollupLineItem": "Accrued Expenses",
"SphAccountRollupName": "Balance Sheet - Liabilities - Current - Accrued Expenses",
"ConfidenceScore": 0.8831137893820182,
"DateMapped": "7/26/2022",
"AccountId": 1348,
"AccountCode": "247152",
"Description": "AACN PHYSICIAN PMTS PAYABLE",
"GLRollup": "SKCURRENTLIABILITIES",
"OBDollarsFinancialReporting": "Bal - CURRENT LIABILITIES - Other current liabilities",
"DSSAccountRollup1Name": "Exclude"
},
{
"OrgPin": "0430",
"DatabaseName": "jazz tst GM Platform Demo.1 B onboarding 20200922.1",
"DatabaseFriendlyName": "StrataJazz Platform Cubekillers Demo 1 B Legacy onboarding",
"SphAccountRollupStatement": "Balance Sheet",
"SphAccountRollupCategory": "Assets - Current",
"SphAccountRollupLineItem": "Cash and Cash Equivalents",
"SphAccountRollupName": "Balance Sheet - Assets - Current - Cash and Cash Equivalents",
"ConfidenceScore": 0.7556597792049811,
"DateMapped": "7/26/2022",
"AccountId": 67,
"AccountCode": "100507",
"Description": "AAH CONTRLLD DISB ACCT M AND I",
"GLRollup": "SKCURRENTASSETS",
"OBDollarsFinancialReporting": "Bal - CURRENT ASSETS - Operating cash",
"DSSAccountRollup1Name": "Exclude"
},
{
"OrgPin": "0430",
"DatabaseName": "jazz tst GM Platform Demo.1 B onboarding 20200922.1",
"DatabaseFriendlyName": "StrataJazz Platform Cubekillers Demo 1 B Legacy onboarding",
"SphAccountRollupStatement": "Balance Sheet",
"SphAccountRollupCategory": "Assets - Current",
"SphAccountRollupLineItem": "Cash and Cash Equivalents",
"SphAccountRollupName": "Balance Sheet - Assets - Current - Cash and Cash Equivalents",
"ConfidenceScore": 0.4848799811631093,
"DateMapped": "7/26/2022",
"AccountId": 68,
"AccountCode": "100508",
"Description": "AAH CONTROLLED DISB ACCT TPA",
"GLRollup": "SKCURRENTASSETS",
"OBDollarsFinancialReporting": "Bal - CURRENT ASSETS - Operating cash",
"DSSAccountRollup1Name": "Exclude"
},
{
"OrgPin": "0430",
"DatabaseName": "jazz tst GM Platform Demo.1 B onboarding 20200922.1",
"DatabaseFriendlyName": "StrataJazz Platform Cubekillers Demo 1 B Legacy onboarding",
"SphAccountRollupStatement": "Balance Sheet",
"SphAccountRollupCategory": "Liabilities - Current",
"SphAccountRollupLineItem": "Accrued Expenses",
"SphAccountRollupName": "Balance Sheet - Liabilities - Current - Accrued Expenses",
"ConfidenceScore": 0.5334519575935484,
"DateMapped": "7/26/2022",
"AccountId": 1349,
"AccountCode": "247153",
"Description": "AAH PHYS PMT LIABILITY",
"GLRollup": "SKCURRENTLIABILITIES",
"OBDollarsFinancialReporting": "Bal - CURRENT LIABILITIES - Other current liabilities",
"DSSAccountRollup1Name": "Exclude"
},
{
"OrgPin": "0430",
"DatabaseName": "jazz tst GM Platform Demo.1 B onboarding 20200922.1",
"DatabaseFriendlyName": "StrataJazz Platform Cubekillers Demo 1 B Legacy onboarding",
"SphAccountRollupStatement": "Balance Sheet",
"SphAccountRollupCategory": "Liabilities - Current",
"SphAccountRollupLineItem": "Accrued Expenses",
"SphAccountRollupName": "Balance Sheet - Liabilities - Current - Accrued Expenses",
"ConfidenceScore": 0.6824925674536929,
"DateMapped": "7/26/2022",
"AccountId": 1357,
"AccountCode": "247170",
"Description": "AAH PT INS REFUND REFUND PAYABLE",
"GLRollup": "SKCURRENTLIABILITIES",
"OBDollarsFinancialReporting": "Bal - CURRENT LIABILITIES - Other current liabilities",
"DSSAccountRollup1Name": "Exclude"
},
{
"OrgPin": "0430",
"DatabaseName": "jazz tst GM Platform Demo.1 B onboarding 20200922.1",
"DatabaseFriendlyName": "StrataJazz Platform Cubekillers Demo 1 B Legacy onboarding",
"SphAccountRollupStatement": "Income Statement",
"SphAccountRollupCategory": "Expenses - Operating",
"SphAccountRollupLineItem": "Facilities - Rent",
"SphAccountRollupName": "Income Statement - Expenses - Operating - Facilities - Rent",
"ConfidenceScore": 0.5506624314109377,
"DateMapped": "7/26/2022",
"AccountId": 895,
"AccountCode": "760005",
"Description": "ABBOTT HEMATOLOGY LEASE",
"GLRollup": "BUILDING AND EQUIPMENT RENTAL",
"OBDollarsFinancialReporting": "IS - EXPENSES: - Other expenses",
"DSSAccountRollup1Name": "Other Expense"
},
{
"OrgPin": "0430",
"DatabaseName": "jazz tst GM Platform Demo.1 B onboarding 20200922.1",
"DatabaseFriendlyName": "StrataJazz Platform Cubekillers Demo 1 B Legacy onboarding",
"SphAccountRollupStatement": "Not Specified",
"SphAccountRollupCategory": "Not Specified",
"SphAccountRollupLineItem": "",
"SphAccountRollupName": "Not Specified - Not Specified - Not Specified",
"ConfidenceScore": 0.0,
"DateMapped": "",
"AccountId": 2563,
"AccountCode": "903155",
"Description": "ABHS VISITS",
"GLRollup": "STATISTICS",
"OBDollarsFinancialReporting": "Not Specified - Not Specified - Not Specified",
"DSSAccountRollup1Name": "Exclude"
}
]
@@ -0,0 +1,80 @@
using ClosedXML.Excel;
using FluentAssertions;
using Newtonsoft.Json;
using NUnit.Framework;
using Strata.Excel.Core.Import;
using Strata.Excel.TestUtilities;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using static Strata.Excel.Core.Test.Unit.ExcelExportTests.TestExcelExport;
namespace Strata.Excel.Core.Test.Unit.ExcelImportTests
{
[TestFixture]
public class TestExcelImport
{
[OneTimeSetUp]
public void RunBeforeAnyTests()
{
Environment.CurrentDirectory = TestContext.CurrentContext.TestDirectory;
// or identically under the hoods
Directory.SetCurrentDirectory(TestContext.CurrentContext.TestDirectory);
}
private static readonly ResourceFileExtractor _extractor = new ResourceFileExtractor(".ExpectedResults.");
private static readonly ResourceFileExtractor _dataExtractor = new ResourceFileExtractor(".ExcelExportTests.");
[Test]
public void TestImportExcelWorksheet()
{
using (Stream stream = _extractor.ReadFileFromResourceToStream("TestCreateExcelWorkbook.xlsx"))
{
var wb = new XLWorkbook(stream);
wb.SaveAs(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "test.xlsx"));
var ws = wb.Worksheets.First();
var result = ws.GetDataFromExcel<TestData>();
var data = _dataExtractor.ReadFileFromResource("data.json");
var expected = JsonConvert.DeserializeObject<List<TestData>>(data);
result.Should().HaveCount(expected.Count)
.And.Contain(row => expected.Select(d => d.AccountCode).Contains(row.AccountCode))
.And.Contain(row => expected.Select(d => d.DatabaseName).Contains(row.DatabaseName))
.And.Contain(row => expected.Select(d => d.DatabaseFriendlyName).Contains(row.DatabaseFriendlyName))
.And.Contain(row => expected.Select(d => d.SphAccountRollupCategory).Contains(row.SphAccountRollupCategory))
.And.Contain(row => expected.Select(d => d.Description).Contains(row.Description));
}
}
[Test]
public void TestImportWhatsNeededExcelWorksheet()
{
using (Stream stream = _extractor.ReadFileFromResourceToStream("TestCreateExcelWorkbook.xlsx"))
{
var wb = new XLWorkbook(stream);
wb.SaveAs(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "test.xlsx"));
var ws = wb.Worksheets.First();
var result = ws.GetDataFromExcel<ImportWhatsNeeded>();
var data = _dataExtractor.ReadFileFromResource("data.json");
var expected = JsonConvert.DeserializeObject<List<ImportWhatsNeeded>>(data);
result.Should().HaveCount(expected.Count)
.And.Contain(row => expected.Select(d => d.AccountCode).Contains(row.AccountCode))
.And.Contain(row => expected.Select(d => d.DatabaseName).Contains(row.DatabaseName))
.And.Contain(row => expected.Select(d => d.DatabaseFriendlyName).Contains(row.DatabaseFriendlyName))
.And.Contain(row => expected.Select(d => d.SphAccountRollupCategory).Contains(row.SphAccountRollupCategory))
.And.Contain(row => expected.Select(d => d.Description).Contains(row.Description));
}
}
internal class ImportWhatsNeeded
{
public string DatabaseName { get; set; }
public string DatabaseFriendlyName { get; set; }
public string SphAccountRollupCategory { get; set; }
public string AccountCode { get; set; }
public string Description { get; set; }
}
}
}
@@ -0,0 +1,76 @@
using ClosedXML.Excel;
using FluentAssertions;
using Newtonsoft.Json;
using NUnit.Framework;
using Strata.Excel.TestUtilities;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using static Strata.Excel.Core.ExportUtils;
using static Strata.Excel.Core.Test.Unit.ExcelExportTests.TestExcelExport;
namespace Strata.Excel.Core.Test.Unit.ExcelLoadTests
{
[TestFixture]
public class TestExcelLoad
{
[OneTimeSetUp]
public void RunBeforeAnyTests()
{
Environment.CurrentDirectory = TestContext.CurrentContext.TestDirectory;
// or identically under the hoods
Directory.SetCurrentDirectory(TestContext.CurrentContext.TestDirectory);
}
private static readonly ResourceFileExtractor _dataExtractor = new ResourceFileExtractor(".ExcelExportTests.");
[TestCaseSource(nameof(LoadTestCases))]
[Ignore("need to look for alternative that can handle large data")]
public void ExcelLoadTest(int load, int batchSize)
{
var jsonData = _dataExtractor.ReadFileFromResource("data.json");
var loadData = JsonConvert.DeserializeObject<List<TestData>>(jsonData);
var data = new List<TestData>();
while (data.Count < load)
{
data.AddRange(loadData);
}
Action action = () =>
{
var expectedLoad = data.Count + 1; // Add 1 to include the header row
var options = new ExportOptions
{
EmptyMessage = "No Mappings",
BatchSize = batchSize
};
options.AddColumnOptions("Confidence Score", NumberFormatId.ZeroPercent, XLAlignmentHorizontalValues.Right);
options.AddColumnOptions("Date Mapped", NumberFormatId.ShortDateSlash, XLAlignmentHorizontalValues.Right);
var wb = CreateExcelWorkbook(data, options);
#pragma warning disable S125 // Sections of code should not be commented out
//var expected = $@"Excel Load Test {TestContext.CurrentContext.Test.Name}.xlsx";
//wb.SaveAs(Path.Combine(@"C:\Git\excel.core\tests\Strata.Excel.Core.Test.Unit\ExcelLoadTests\", expected));
#pragma warning restore S125 // Sections of code should not be commented
// assert
wb.Worksheets.Should().HaveCount(1);
var ws = wb.Worksheet(1);
ws.Should().NotBeNull();
var table = ws.Tables.First();
table.RowCount().Should().Be(expectedLoad);
};
action.Should().NotThrow()
.And.Subject.ExecutionTime().Should().BeLessThan(TimeSpan.FromMinutes(load / 3000));
}
public static IEnumerable<TestCaseData> LoadTestCases()
{
var batchSize = 100000;
for (var i = 48; i < 50; i += 2)
{
yield return new TestCaseData(i * 10000, batchSize).SetName($"{i}0000 rows with batches of {batchSize}");
}
}
}
}
@@ -0,0 +1,43 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<None Remove="ExcelExportTests\data.json" />
<None Remove="ExpectedResults\TestCreateExcelWorkbook.xlsx" />
<None Remove="ExpectedResults\TestCreateExcelWorkbookWithoutHumanizedHeadings.xlsx" />
<None Remove="ExpectedResults\TestCreateExcelWorkbookWithTitlePage.xlsx" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="ExcelExportTests\data.json">
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
</EmbeddedResource>
<EmbeddedResource Include="ExpectedResults\TestCreateExcelWorkbook.xlsx" />
<EmbeddedResource Include="ExpectedResults\TestCreateExcelWorkbookWithoutHumanizedHeadings.xlsx" />
<EmbeddedResource Include="ExpectedResults\TestCreateExcelWorkbookWithTitlePage.xlsx" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="coverlet.msbuild" Version="3.0.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Include="FluentAssertions" Version="6.7.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.1" />
<PackageReference Include="Moq" Version="4.16.1" />
<PackageReference Include="NUnit" Version="3.13.1" />
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Strata.Excel.Core\Strata.Excel.Core.csproj" />
<ProjectReference Include="..\..\src\Strata.Excel.TestUtilities\Strata.Excel.TestUtilities.csproj" />
</ItemGroup>
</Project>