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:
@@ -0,0 +1,10 @@
|
||||
.dockerignore
|
||||
Dockerfile
|
||||
Dockerfile.local
|
||||
.env
|
||||
.gitignore
|
||||
.vs
|
||||
.vscode
|
||||
**/bin
|
||||
**/obj
|
||||
**/.toolstarget
|
||||
+85
-217
@@ -1,12 +1,67 @@
|
||||
# ---> VisualStudio
|
||||
## Ignore Visual Studio temporary files, build results, and
|
||||
## files generated by popular Visual Studio add-ons.
|
||||
##
|
||||
## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
|
||||
|
||||
###################
|
||||
# compiled source #
|
||||
###################
|
||||
*.com
|
||||
*.class
|
||||
*.dll
|
||||
*.exe
|
||||
*.pdb
|
||||
*.dll.config
|
||||
*.cache
|
||||
*.suo
|
||||
# Include dlls if they’re in the NuGet packages directory
|
||||
!/packages/*/lib/*.dll
|
||||
# Include dlls if they're in the CommonReferences directory
|
||||
!*CommonReferences/*.dll
|
||||
####################
|
||||
# VS Upgrade stuff #
|
||||
####################
|
||||
_UpgradeReport_Files/
|
||||
###############
|
||||
# Directories #
|
||||
###############
|
||||
bin/
|
||||
obj/
|
||||
TestResults/
|
||||
###################
|
||||
# Web publish log #
|
||||
###################
|
||||
*.Publish.xml
|
||||
#############
|
||||
# Resharper #
|
||||
#############
|
||||
/_ReSharper.*
|
||||
*.ReSharper.*
|
||||
############
|
||||
# Packages #
|
||||
############
|
||||
# it’s better to unpack these files and commit the raw source
|
||||
# git has its own built in compression methods
|
||||
*.7z
|
||||
*.dmg
|
||||
*.gz
|
||||
*.iso
|
||||
*.jar
|
||||
*.rar
|
||||
*.tar
|
||||
*.zip
|
||||
######################
|
||||
# Logs and databases #
|
||||
######################
|
||||
*.log
|
||||
*.sqlite
|
||||
# OS generated files #
|
||||
######################
|
||||
.DS_Store?
|
||||
ehthumbs.db
|
||||
Icon?
|
||||
Thumbs.db
|
||||
|
||||
|
||||
# User-specific files
|
||||
*.rsuser
|
||||
*.suo
|
||||
*.user
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
@@ -14,9 +69,6 @@
|
||||
# User-specific files (MonoDevelop/Xamarin Studio)
|
||||
*.userprefs
|
||||
|
||||
# Mono auto generated files
|
||||
mono_crash.*
|
||||
|
||||
# Build results
|
||||
[Dd]ebug/
|
||||
[Dd]ebugPublic/
|
||||
@@ -24,76 +76,47 @@ mono_crash.*
|
||||
[Rr]eleases/
|
||||
x64/
|
||||
x86/
|
||||
[Ww][Ii][Nn]32/
|
||||
[Aa][Rr][Mm]/
|
||||
[Aa][Rr][Mm]64/
|
||||
build/
|
||||
bld/
|
||||
[Bb]in/
|
||||
[Oo]bj/
|
||||
[Ll]og/
|
||||
[Ll]ogs/
|
||||
|
||||
# Visual Studio 2015/2017 cache/options directory
|
||||
# Visual Studo 2015 cache/options directory
|
||||
.vs/
|
||||
# Uncomment if you have tasks that create the project's static files in wwwroot
|
||||
#wwwroot/
|
||||
|
||||
# Visual Studio 2017 auto generated files
|
||||
Generated\ Files/
|
||||
|
||||
# MSTest test Results
|
||||
[Tt]est[Rr]esult*/
|
||||
[Bb]uild[Ll]og.*
|
||||
|
||||
# NUnit
|
||||
# NUNIT
|
||||
*.VisualState.xml
|
||||
TestResult.xml
|
||||
nunit-*.xml
|
||||
|
||||
# Build Results of an ATL Project
|
||||
[Dd]ebugPS/
|
||||
[Rr]eleasePS/
|
||||
dlldata.c
|
||||
|
||||
# Benchmark Results
|
||||
BenchmarkDotNet.Artifacts/
|
||||
|
||||
# .NET Core
|
||||
# DNX
|
||||
project.lock.json
|
||||
project.fragment.lock.json
|
||||
artifacts/
|
||||
|
||||
# ASP.NET Scaffolding
|
||||
ScaffoldingReadMe.txt
|
||||
|
||||
# StyleCop
|
||||
StyleCopReport.xml
|
||||
|
||||
# Files built by Visual Studio
|
||||
*_i.c
|
||||
*_p.c
|
||||
*_h.h
|
||||
*_i.h
|
||||
*.ilk
|
||||
*.meta
|
||||
*.obj
|
||||
*.iobj
|
||||
*.pch
|
||||
*.pdb
|
||||
*.ipdb
|
||||
*.pgc
|
||||
*.pgd
|
||||
*.rsp
|
||||
# but not Directory.Build.rsp, as it configures directory-level build defaults
|
||||
!Directory.Build.rsp
|
||||
*.sbr
|
||||
*.tlb
|
||||
*.tli
|
||||
*.tlh
|
||||
*.tmp
|
||||
*.tmp_proj
|
||||
*_wpftmp.csproj
|
||||
*.log
|
||||
*.tlog
|
||||
*.vspscc
|
||||
*.vssscc
|
||||
.builds
|
||||
@@ -108,21 +131,14 @@ _Chutzpah*
|
||||
ipch/
|
||||
*.aps
|
||||
*.ncb
|
||||
*.opendb
|
||||
*.opensdf
|
||||
*.sdf
|
||||
*.cachefile
|
||||
*.VC.db
|
||||
*.VC.VC.opendb
|
||||
|
||||
# Visual Studio profiler
|
||||
*.psess
|
||||
*.vsp
|
||||
*.vspx
|
||||
*.sap
|
||||
|
||||
# Visual Studio Trace Files
|
||||
*.e2e
|
||||
|
||||
# TFS 2012 Local Workspace
|
||||
$tf/
|
||||
@@ -135,29 +151,18 @@ _ReSharper*/
|
||||
*.[Rr]e[Ss]harper
|
||||
*.DotSettings.user
|
||||
|
||||
# JustCode is a .NET coding add-in
|
||||
.JustCode
|
||||
|
||||
# TeamCity is a build add-in
|
||||
_TeamCity*
|
||||
|
||||
# DotCover is a Code Coverage Tool
|
||||
*.dotCover
|
||||
|
||||
# AxoCover is a Code Coverage Tool
|
||||
.axoCover/*
|
||||
!.axoCover/settings.json
|
||||
|
||||
# Coverlet is a free, cross platform Code Coverage Tool
|
||||
coverage*.json
|
||||
coverage*.xml
|
||||
coverage*.info
|
||||
|
||||
# Visual Studio code coverage results
|
||||
*.coverage
|
||||
*.coveragexml
|
||||
|
||||
# NCrunch
|
||||
_NCrunch_*
|
||||
.*crunch*.local.xml
|
||||
nCrunchTemp_*
|
||||
|
||||
# MightyMoose
|
||||
*.mm.*
|
||||
@@ -181,76 +186,50 @@ DocProject/Help/html
|
||||
|
||||
# Click-Once directory
|
||||
publish/
|
||||
ClickOnce/
|
||||
|
||||
# Publish Web Output
|
||||
*.[Pp]ublish.xml
|
||||
*.azurePubxml
|
||||
# Note: Comment the next line if you want to checkin your web deploy settings,
|
||||
# TODO: Comment the next line if you want to checkin your web deploy settings
|
||||
# but database connection strings (with potential passwords) will be unencrypted
|
||||
*.pubxml
|
||||
*.publishproj
|
||||
|
||||
# Microsoft Azure Web App publish settings. Comment the next line if you want to
|
||||
# checkin your Azure Web App publish settings, but sensitive information contained
|
||||
# in these scripts will be unencrypted
|
||||
PublishScripts/
|
||||
|
||||
# NuGet Packages
|
||||
*.nupkg
|
||||
# NuGet Symbol Packages
|
||||
*.snupkg
|
||||
# The packages folder can be ignored because of Package Restore
|
||||
**/[Pp]ackages/*
|
||||
**/packages/*
|
||||
# except build/, which is used as an MSBuild target.
|
||||
!**/[Pp]ackages/build/
|
||||
!**/packages/build/
|
||||
# Uncomment if necessary however generally it will be regenerated when needed
|
||||
#!**/[Pp]ackages/repositories.config
|
||||
# NuGet v3's project.json files produces more ignorable files
|
||||
*.nuget.props
|
||||
*.nuget.targets
|
||||
#!**/packages/repositories.config
|
||||
|
||||
# Microsoft Azure Build Output
|
||||
# Windows Azure Build Output
|
||||
csx/
|
||||
*.build.csdef
|
||||
|
||||
# Microsoft Azure Emulator
|
||||
ecf/
|
||||
rcf/
|
||||
|
||||
# Windows Store app package directories and files
|
||||
# Windows Store app package directory
|
||||
AppPackages/
|
||||
BundleArtifacts/
|
||||
Package.StoreAssociation.xml
|
||||
_pkginfo.txt
|
||||
*.appx
|
||||
*.appxbundle
|
||||
*.appxupload
|
||||
|
||||
# Visual Studio cache files
|
||||
# files ending in .cache can be ignored
|
||||
*.[Cc]ache
|
||||
# but keep track of directories ending in .cache
|
||||
!?*.[Cc]ache/
|
||||
!*.[Cc]ache/
|
||||
|
||||
# Others
|
||||
ClientBin/
|
||||
[Ss]tyle[Cc]op.*
|
||||
~$*
|
||||
*~
|
||||
*.dbmdl
|
||||
*.dbproj.schemaview
|
||||
*.jfm
|
||||
*.pfx
|
||||
*.publishsettings
|
||||
node_modules/
|
||||
bower_components/
|
||||
orleans.codegen.cs
|
||||
|
||||
# Including strong name files can present a security risk
|
||||
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
|
||||
#*.snk
|
||||
|
||||
# Since there are multiple workflows, uncomment next line to ignore bower_components
|
||||
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
|
||||
#bower_components/
|
||||
|
||||
# RIA/Silverlight projects
|
||||
Generated_Code/
|
||||
|
||||
@@ -261,32 +240,21 @@ _UpgradeReport_Files/
|
||||
Backup*/
|
||||
UpgradeLog*.XML
|
||||
UpgradeLog*.htm
|
||||
ServiceFabricBackup/
|
||||
*.rptproj.bak
|
||||
|
||||
# SQL Server files
|
||||
*.mdf
|
||||
*.ldf
|
||||
*.ndf
|
||||
|
||||
**/node_modules/*
|
||||
# Business Intelligence projects
|
||||
*.rdl.data
|
||||
*.bim.layout
|
||||
*.bim_*.settings
|
||||
*.rptproj.rsuser
|
||||
*- [Bb]ackup.rdl
|
||||
*- [Bb]ackup ([0-9]).rdl
|
||||
*- [Bb]ackup ([0-9][0-9]).rdl
|
||||
|
||||
# Microsoft Fakes
|
||||
FakesAssemblies/
|
||||
|
||||
# GhostDoc plugin setting file
|
||||
*.GhostDoc.xml
|
||||
|
||||
# Node.js Tools for Visual Studio
|
||||
.ntvs_analysis.dat
|
||||
node_modules/
|
||||
|
||||
# Visual Studio 6 build log
|
||||
*.plg
|
||||
@@ -294,109 +262,9 @@ node_modules/
|
||||
# Visual Studio 6 workspace options file
|
||||
*.opt
|
||||
|
||||
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
|
||||
*.vbw
|
||||
|
||||
# Visual Studio 6 auto-generated project file (contains which files were open etc.)
|
||||
*.vbp
|
||||
|
||||
# Visual Studio 6 workspace and project file (working project files containing files to include in project)
|
||||
*.dsw
|
||||
*.dsp
|
||||
|
||||
# Visual Studio 6 technical files
|
||||
*.ncb
|
||||
*.aps
|
||||
|
||||
# Visual Studio LightSwitch build output
|
||||
**/*.HTMLClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/ModelManifest.xml
|
||||
**/*.Server/GeneratedArtifacts
|
||||
**/*.Server/ModelManifest.xml
|
||||
_Pvt_Extensions
|
||||
|
||||
# Paket dependency manager
|
||||
.paket/paket.exe
|
||||
paket-files/
|
||||
|
||||
# FAKE - F# Make
|
||||
.fake/
|
||||
|
||||
# CodeRush personal settings
|
||||
.cr/personal
|
||||
|
||||
# Python Tools for Visual Studio (PTVS)
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
# Cake - Uncomment if you are using it
|
||||
# tools/**
|
||||
# !tools/packages.config
|
||||
|
||||
# Tabs Studio
|
||||
*.tss
|
||||
|
||||
# Telerik's JustMock configuration file
|
||||
*.jmconfig
|
||||
|
||||
# BizTalk build output
|
||||
*.btp.cs
|
||||
*.btm.cs
|
||||
*.odx.cs
|
||||
*.xsd.cs
|
||||
|
||||
# OpenCover UI analysis results
|
||||
OpenCover/
|
||||
|
||||
# Azure Stream Analytics local run output
|
||||
ASALocalRun/
|
||||
|
||||
# MSBuild Binary and Structured Log
|
||||
*.binlog
|
||||
|
||||
# NVidia Nsight GPU debugger configuration file
|
||||
*.nvuser
|
||||
|
||||
# MFractors (Xamarin productivity tool) working folder
|
||||
.mfractor/
|
||||
|
||||
# Local History for Visual Studio
|
||||
.localhistory/
|
||||
|
||||
# Visual Studio History (VSHistory) files
|
||||
.vshistory/
|
||||
|
||||
# BeatPulse healthcheck temp database
|
||||
healthchecksdb
|
||||
|
||||
# Backup folder for Package Reference Convert tool in Visual Studio 2017
|
||||
MigrationBackup/
|
||||
|
||||
# Ionide (cross platform F# VS Code tools) working folder
|
||||
.ionide/
|
||||
|
||||
# Fody - auto-generated XML schema
|
||||
FodyWeavers.xsd
|
||||
|
||||
# VS Code files for those working on multiple tools
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
*.code-workspace
|
||||
|
||||
# Local History for Visual Studio Code
|
||||
.history/
|
||||
|
||||
# Windows Installer files from build outputs
|
||||
*.cab
|
||||
*.msi
|
||||
*.msix
|
||||
*.msm
|
||||
*.msp
|
||||
|
||||
# JetBrains Rider
|
||||
*.sln.iml
|
||||
# SonarQube files
|
||||
sonarqube/
|
||||
|
||||
.vs/
|
||||
*.orig
|
||||
buildlogs/
|
||||
@@ -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
|
||||
@@ -1,2 +1,81 @@
|
||||
# excel.core
|
||||
# Strata.Excel.Core
|
||||
This provides a library for Strata standard ClosedXML Excel documents
|
||||
|
||||
|
||||
[](https://github.com/stratadecision/excel.core/actions/workflows/build.yaml)
|
||||
|
||||
|
||||
[](https://sonarqube.sdt.local/dashboard?id=Strata.excel.core)
|
||||
|
||||
[](https://proget.ops.stratanetwork.net/feeds/nuget/Strata.Excel.Core/versions?HidePrerelease=True)
|
||||
[](https://proget.ops.stratanetwork.net/feeds/nuget/Strata.Excel.TestUtilities/versions?HidePrerelease=True)
|
||||
|
||||
|
||||
|
||||
## Branching and Versioning
|
||||
|
||||
| branch | version format | example |
|
||||
| ---------- | --------------------- | --------------- |
|
||||
| main | #.#.# | 1.2.3 |
|
||||
| feature/\* | #.#+1.0-featureName.# | 1.3.0-newfeat.1 |
|
||||
| fix/\* | #.#.#+1-fixName.# | 1.2.4-bug.1 |
|
||||
|
||||
See our [confluence page](https://confluence.sdt.local/display/DOP/Branching+and+Versioning) for more information
|
||||
|
||||
## Usage
|
||||
|
||||
### Build and Run
|
||||
|
||||
```
|
||||
docker-compose up -d --build
|
||||
```
|
||||
|
||||
- https://localhost:8443/index (swagger api)
|
||||
- https://localhost:8443/hangfire (hangfire dashboard)
|
||||
- http://localhost:8081 (redis commander)
|
||||
|
||||
### Extracting nuget package
|
||||
|
||||
```
|
||||
docker create --name throwaway strataexcel-excel-core
|
||||
docker cp throwaway:/pack .
|
||||
docker rm throwaway
|
||||
```
|
||||
|
||||
### Cleanup
|
||||
|
||||
```
|
||||
docker-compose down
|
||||
```
|
||||
|
||||
|
||||
# Helpful Features
|
||||
### Adding a drop down list to a worksheet
|
||||
|
||||
ClosedXML offers the functionality for adding drop down lists to columns with validation out of the box. However, that implementation has a data length limitation of a formula field. For lists longer than trivial selections this extension method can be used:
|
||||
|
||||
```csharp
|
||||
/// <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.
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -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>
|
||||
Reference in New Issue
Block a user