Repository: WeihanLi/WeihanLi.Npoi Branch: dev Commit: d65c688ac36e Files: 132 Total size: 531.0 KB Directory structure: gitextract_te4pkn3w/ ├── .codacy.yml ├── .devcontainer/ │ ├── Dockerfile │ └── devcontainer.json ├── .editorconfig ├── .gitattributes ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ └── feature_request.md │ ├── stale.yml │ └── workflows/ │ ├── docfx.yml │ ├── dotnet-format-pr-validation.yml │ ├── dotnet-format.yml │ ├── dotnet.yml │ └── release.yml ├── .gitignore ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── Directory.Build.props ├── Directory.Packages.props ├── LICENSE ├── README.md ├── WeihanLi.Npoi.sln.DotSettings ├── WeihanLi.Npoi.slnx ├── azure-pipelines.yml ├── build/ │ ├── common.props │ ├── getReleaseVersion.ps1 │ ├── sign.props │ ├── version.props │ └── weihanli.snk ├── build.cs ├── docs/ │ ├── ReleaseNotes.md │ ├── api/ │ │ ├── .gitignore │ │ └── index.md │ ├── articles/ │ │ ├── en/ │ │ │ ├── CustomizeStyle.md │ │ │ ├── GetStarted.md │ │ │ ├── InputOutputFormatter.md │ │ │ ├── MultiSheets.md │ │ │ ├── ShadowProperty.md │ │ │ └── TemplateExport.md │ │ ├── intro.md │ │ ├── toc.yml │ │ └── zh/ │ │ ├── CustomizeStyle.md │ │ ├── GetStarted.md │ │ ├── InputOutputFormatter.md │ │ ├── MultiSheets.md │ │ ├── ShadowProperty.md │ │ └── TemplateExport.md │ ├── docfx.json │ └── toc.yml ├── global.json ├── nuget.config ├── perf/ │ └── WeihanLi.Npoi.Benchmark/ │ ├── BenchmarkDotNet.Artifacts/ │ │ └── results/ │ │ ├── WeihanLi.Npoi.Benchmark.ExportExcelTest-report-github.md │ │ ├── WeihanLi.Npoi.Benchmark.ExportExcelTest-report.csv │ │ ├── WeihanLi.Npoi.Benchmark.ExportExcelTest-report.html │ │ ├── WeihanLi.Npoi.Benchmark.ImportExcelTest-report-github.md │ │ ├── WeihanLi.Npoi.Benchmark.ImportExcelTest-report.csv │ │ ├── WeihanLi.Npoi.Benchmark.ImportExcelTest-report.html │ │ ├── WeihanLi.Npoi.Benchmark.WorkbookBasicTest-report-github.md │ │ ├── WeihanLi.Npoi.Benchmark.WorkbookBasicTest-report.csv │ │ └── WeihanLi.Npoi.Benchmark.WorkbookBasicTest-report.html │ ├── ExportExcelTest.cs │ ├── ImportExcelTest.cs │ ├── Program.cs │ ├── WeihanLi.Npoi.Benchmark.csproj │ └── WorkbookBasicTest.cs ├── samples/ │ ├── Directory.Build.props │ ├── DotNetCoreSample/ │ │ ├── DotNetCoreSample.csproj │ │ ├── ImportImageTestModel.cs │ │ ├── IssueSamples.cs │ │ ├── ProductPriceMapping.cs │ │ ├── Program.cs │ │ ├── Templates/ │ │ │ └── testTemplate.xlsx │ │ └── TestModel.cs │ └── run-file-samples/ │ ├── issue-169.cs │ └── style-customization-sample.cs ├── src/ │ ├── Directory.Build.props │ └── WeihanLi.Npoi/ │ ├── Abstract/ │ │ ├── ICell.cs │ │ ├── IRow.cs │ │ ├── ISheet.cs │ │ ├── IWorkbook.cs │ │ └── NPOIWorkbook.cs │ ├── Attributes/ │ │ ├── ColumnAttribute.cs │ │ ├── FilterAttribute.cs │ │ ├── FreezeAttribute.cs │ │ └── SheetAttribute.cs │ ├── CellPosition.cs │ ├── Compat.cs │ ├── ConfigurationExtensions.cs │ ├── Configurations/ │ │ ├── CsvOptions.cs │ │ ├── ExcelConfiguration.cs │ │ ├── IExcelConfiguration.cs │ │ ├── IPropertyConfiguration.cs │ │ └── PropertyConfiguration.cs │ ├── CsvHelper.cs │ ├── ExcelFormat.cs │ ├── ExcelHelper.cs │ ├── FakePropertyInfo.cs │ ├── FluentSettings.cs │ ├── IMappingProfile.cs │ ├── InternalCache.cs │ ├── InternalConstants.cs │ ├── InternalExtensions.cs │ ├── InternalHelper.cs │ ├── NpoiCollection.cs │ ├── NpoiExtensions.cs │ ├── NpoiHelper.cs │ ├── NpoiTemplateHelper.cs │ ├── Resource.Designer.cs │ ├── Resource.resx │ ├── Settings/ │ │ ├── ExcelSetting.cs │ │ ├── FilterSetting.cs │ │ ├── FreezeSetting.cs │ │ └── SheetSetting.cs │ ├── TemplateHelper.cs │ └── WeihanLi.Npoi.csproj └── test/ └── WeihanLi.Npoi.Test/ ├── CsvTest.cs ├── ExcelFormatData.cs ├── ExcelTest.cs ├── Extensions.cs ├── MappingProfiles/ │ └── NoticeProfile.cs ├── Models/ │ ├── Job.cs │ ├── Notice.cs │ └── OrderTestModels.cs ├── Startup.cs ├── TestData/ │ ├── EmptyColumns/ │ │ ├── emptyColumns.csv │ │ ├── emptyColumns.xls │ │ └── emptyColumns.xlsx │ ├── EmptyRows/ │ │ ├── emptyRows.xls │ │ └── emptyRows.xlsx │ └── NonStringColumns/ │ ├── nonStringColumns.csv │ ├── nonStringColumns.xls │ └── nonStringColumns.xlsx └── WeihanLi.Npoi.Test.csproj ================================================ FILE CONTENTS ================================================ ================================================ FILE: .codacy.yml ================================================ exclude_paths: - samples/ - test/ ================================================ FILE: .devcontainer/Dockerfile ================================================ FROM mcr.microsoft.com/dotnet/sdk:10.0 RUN apt-get update \ && apt-get install -y libgdiplus libc6-dev \ && ln -s /usr/lib/libgdiplus.so /usr/lib/gdiplus.dll ================================================ FILE: .devcontainer/devcontainer.json ================================================ { "name": "CodeSpace", "dockerFile": "Dockerfile", "customizations": { "vscode": { "extensions": [ "ms-dotnettools.csharp", "davidanson.vscode-markdownlint" ] } } } ================================================ FILE: .editorconfig ================================================ # EditorConfig is awesome:http://EditorConfig.org # top-most EditorConfig file root = true # Don't use tabs for indentation. [*] indent_style = space # (Please don't specify an indent_size here; that has too many unintended consequences.) # Code files [*.{cs,csx,vb,vbx}] indent_size = 4 insert_final_newline = true charset = utf-8-bom # Xml project files [*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}] indent_size = 2 # Xml config files [*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}] indent_size = 2 # JSON files [*.json] indent_size = 2 # Dotnet code style settings: [*.{cs,vb}] # File header file_header_template = Copyright (c) Weihan Li. All rights reserved.\nLicensed under the Apache license. # Sort using and Import directives with System.* appearing first dotnet_sort_system_directives_first = false # Avoid "this." and "Me." if not necessary dotnet_style_qualification_for_field = false:suggestion dotnet_style_qualification_for_property = false:suggestion dotnet_style_qualification_for_method = false:suggestion dotnet_style_qualification_for_event = false:suggestion # Use language keywords instead of framework type names for type references dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion dotnet_style_predefined_type_for_member_access = true:suggestion # Suggest more modern language features when available dotnet_style_object_initializer = true:suggestion dotnet_style_collection_initializer = true:suggestion dotnet_style_coalesce_expression = true:suggestion dotnet_style_null_propagation = true:suggestion dotnet_style_explicit_tuple_names = true:suggestion # CSharp code style settings: [*.cs] # namespace style csharp_style_namespace_declarations=file_scoped:warning # Prefer "var" everywhere csharp_style_var_for_built_in_types = true:suggestion csharp_style_var_when_type_is_apparent = true:suggestion csharp_style_var_elsewhere = true:suggestion # Prefer method-like constructs to have a block body csharp_style_expression_bodied_methods = false:none csharp_style_expression_bodied_constructors = false:none csharp_style_expression_bodied_operators = false:none # Prefer property-like constructs to have an expression-body csharp_style_expression_bodied_properties = true:none csharp_style_expression_bodied_indexers = true:none csharp_style_expression_bodied_accessors = true:none # Suggest more modern language features when available csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion csharp_style_pattern_matching_over_as_with_null_check = true:suggestion csharp_style_inlined_variable_declaration = true:suggestion csharp_style_throw_expression = true:suggestion csharp_style_conditional_delegate_call = true:suggestion # Newline settings csharp_new_line_before_open_brace = all csharp_new_line_before_else = true csharp_new_line_before_catch = true csharp_new_line_before_finally = true csharp_new_line_before_members_in_object_initializers = true csharp_new_line_before_members_in_anonymous_types = true # Fix formatting, https://docs.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/formatting-rules#rule-id-ide0055-fix-formatting dotnet_diagnostic.IDE00055.severity = warning # Remove unnecessary usings, https://docs.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0005 dotnet_diagnostic.IDE0005.severity = warning # File header template dotnet_diagnostic.IDE0073.severity = warning ================================================ FILE: .gitattributes ================================================ ############################################################################### # Set default behavior to automatically normalize line endings. ############################################################################### * text=auto *.sh text eol=lf *.ps1 text eol=crlf ############################################################################### # Set default behavior for command prompt diff. # # This is need for earlier builds of msysgit that does not have it on by # default for csharp files. # Note: This is only used by command line ############################################################################### #*.cs diff=csharp ############################################################################### # Set the merge driver for project and solution files # # Merging from the command prompt will add diff markers to the files if there # are conflicts (Merging from VS is not affected by the settings below, in VS # the diff markers are never inserted). Diff markers may cause the following # file extensions to fail to load in VS. An alternative would be to treat # these files as binary and thus will always conflict and require user # intervention with every merge. To do so, just uncomment the entries below ############################################################################### #*.sln merge=binary #*.csproj merge=binary #*.vbproj merge=binary #*.vcxproj merge=binary #*.vcproj merge=binary #*.dbproj merge=binary #*.fsproj merge=binary #*.lsproj merge=binary #*.wixproj merge=binary #*.modelproj merge=binary #*.sqlproj merge=binary #*.wwaproj merge=binary ############################################################################### # behavior for image files # # image files are treated as binary by default. ############################################################################### #*.jpg binary #*.png binary #*.gif binary ############################################################################### # diff behavior for common document formats # # Convert binary document formats to text before diffing them. This feature # is only available from the command line. Turn it on by uncommenting the # entries below. ############################################################################### #*.doc diff=astextplain #*.DOC diff=astextplain #*.docx diff=astextplain #*.DOCX diff=astextplain #*.dot diff=astextplain #*.DOT diff=astextplain #*.pdf diff=astextplain #*.PDF diff=astextplain #*.rtf diff=astextplain #*.RTF diff=astextplain ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Create a report to help us improve title: '' labels: '' assignees: '' --- **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: 1. ... 2. .... 3. .... **Expected behavior** A clear and concise description of what you expected to happen. **Screenshots** If applicable, add screenshots to help explain your problem. **Runtime Version** - dotnet version: - `WeihanLi.Npoi` version: **Additional context** Add any other context about the problem here. ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature request about: Suggest an idea for this project title: '' labels: '' assignees: '' --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context or screenshots about the feature request here. ================================================ FILE: .github/stale.yml ================================================ # Number of days of inactivity before an issue becomes stale daysUntilStale: 30 # Number of days of inactivity before a stale issue is closed daysUntilClose: 7 # Issues with these labels will never be considered stale exemptLabels: - pinned - security # Label to use when marking an issue as stale staleLabel: inactive # Comment to post when marking an issue as stale. Set to `false` to disable markComment: > This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions. # Comment to post when closing a stale issue. Set to `false` to disable closeComment: false ================================================ FILE: .github/workflows/docfx.yml ================================================ name: docfx on: push: branches: - "main" - "master" - "dev" # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages permissions: actions: read pages: write id-token: write # Allow only one concurrent deployment, skipping runs queued between the run in progress and the latest queued. # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. concurrency: group: "pages" cancel-in-progress: false jobs: build: name: "publish docs" environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} runs-on: ubuntu-latest steps: # Check out the branch that triggered this workflow to the 'source' subdirectory - name: Checkout Code uses: actions/checkout@v6 - name: Setup .NET SDK uses: WeihanLi/dotnet-install@v0.2.0 with: version: | 10.0.x - name: install dotnet tools run: | dotnet tool install -g dotnet-execute dotnet tool install -g docfx # Run a build - name: Build docs run: | dotnet-exec info dotnet build cp ./README.md ./docs/index.md docfx ./docs/docfx.json - name: Upload artifact uses: actions/upload-pages-artifact@v3 with: path: 'docs/_site' - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@v4 - name: cloudflare-pages uses: cloudflare/wrangler-action@v3 with: accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} command: pages deploy docs/_site --project-name=weihanli-npoi ================================================ FILE: .github/workflows/dotnet-format-pr-validation.yml ================================================ name: dotnet-format-validation on: [pull_request] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - name: Setup .NET SDK uses: WeihanLi/dotnet-install@v0.2.0 with: version: | 10.0.x - name: build run: dotnet build - name: check format run: dotnet format --verify-no-changes ================================================ FILE: .github/workflows/dotnet-format.yml ================================================ name: dotnet-format on: push: branches: [ dev ] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - name: Setup .NET SDK uses: WeihanLi/dotnet-install@v0.2.0 with: version: | 10.0.x - name: build run: dotnet build - name: format run: dotnet format - name: check for changes run: | if git diff --exit-code; then echo "has_changes=false" >> $GITHUB_ENV else echo "has_changes=true" >> $GITHUB_ENV fi - name: Commit and Push if: ${{ env.has_changes == 'true' }} shell: bash run: | git config --local user.name "github-actions[bot]" git config --local user.email "weihanli@outlook.com" git add -u git commit -m "Automated dotnet-format update from commit ${GITHUB_SHA} on ${GITHUB_REF}" git log -1 remote_repo="https://${GITHUB_ACTOR}:${{secrets.GITHUB_TOKEN}}@github.com/${GITHUB_REPOSITORY}.git" git push "${remote_repo}" HEAD:${GITHUB_REF} ================================================ FILE: .github/workflows/dotnet.yml ================================================ name: default on: [push, pull_request] jobs: mac-build: runs-on: macos-latest steps: - uses: actions/checkout@v6 - name: Setup .NET SDK uses: WeihanLi/dotnet-install@v0.2.0 with: version: | 10.0.x - name: dotnet info run: | dotnet tool install -g dotnet-execute dotnet-exec info - name: build run: dotnet build.cs --target=test linux-build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - name: Setup .NET SDK uses: WeihanLi/dotnet-install@v0.2.0 with: version: | 10.0.x - name: dotnet info run: | dotnet tool install -g dotnet-execute dotnet-exec info # - name: font configure # run: | # sudo apt update && sudo apt-get install -y ttf-mscorefonts-installer fontconfig fonts-lato libgdiplus libc6-dev && sudo ln -s /usr/lib/libgdiplus.so /usr/lib/gdiplus.dll - name: build run: dotnet build.cs --target=build windows-build: runs-on: windows-latest steps: - uses: actions/checkout@v6 - name: Setup .NET SDK uses: WeihanLi/dotnet-install@v0.2.0 with: version: | 10.0.x - name: dotnet info run: | dotnet tool install -g dotnet-execute dotnet-exec info - name: build run: dotnet build.cs ================================================ FILE: .github/workflows/release.yml ================================================ name: Release on: push: branches: [ master ] jobs: build: name: Release runs-on: windows-latest steps: - uses: actions/checkout@v6 - name: Setup .NET SDK uses: WeihanLi/dotnet-install@v0.2.0 with: version: | 10.0.x - name: Build shell: pwsh run: dotnet build.cs --stable=true - name: Get Release Version shell: pwsh run: .\build\getReleaseVersion.ps1 - name: Create GitHub release uses: marvinpinto/action-automatic-releases@latest with: repo_token: "${{ secrets.GITHUB_TOKEN }}" automatic_release_tag: ${{ env.ReleaseVersion }} title: ${{ env.ReleaseVersion }} prerelease: false ================================================ FILE: .gitignore ================================================ ## 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/master/VisualStudio.gitignore # Cutom files localBuild/ _site .vscode # User-specific files *.suo *.user *.userosscache *.sln.docstates # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs # Build results [Dd]ebug/ [Dd]ebugPublic/ [Rr]elease/ [Rr]eleases/ x64/ x86/ bld/ [Bb]in/ [Oo]bj/ [Ll]og/ # Visual Studio 2015 cache/options directory .vs/ # Uncomment if you have tasks that create the project's static files in wwwroot #wwwroot/ # MSTest test Results [Tt]est[Rr]esult*/ [Bb]uild[Ll]og.* # NUNIT *.VisualState.xml TestResult.xml # Build Results of an ATL Project [Dd]ebugPS/ [Rr]eleasePS/ dlldata.c # .NET Core project.lock.json project.fragment.lock.json artifacts/ **/Properties/launchSettings.json *_i.c *_p.c *_i.h *.ilk *.meta *.obj *.pch *.pdb *.pgc *.pgd *.rsp *.sbr *.tlb *.tli *.tlh *.tmp *.tmp_proj *.log *.vspscc *.vssscc .builds *.pidb *.svclog *.scc # Chutzpah Test files _Chutzpah* # Visual C++ cache files ipch/ *.aps *.ncb *.opendb *.opensdf *.sdf *.cachefile *.VC.db *.VC.VC.opendb # Visual Studio profiler *.psess *.vsp *.vspx *.sap # TFS 2012 Local Workspace $tf/ # Guidance Automation Toolkit *.gpState # ReSharper is a .NET coding add-in _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 # Visual Studio code coverage results *.coverage *.coveragexml # NCrunch _NCrunch_* .*crunch*.local.xml nCrunchTemp_* # MightyMoose *.mm.* AutoTest.Net/ # Web workbench (sass) .sass-cache/ # Installshield output folder [Ee]xpress/ # DocProject is a documentation generator add-in DocProject/buildhelp/ DocProject/Help/*.HxT DocProject/Help/*.HxC DocProject/Help/*.hhc DocProject/Help/*.hhk DocProject/Help/*.hhp DocProject/Help/Html2 DocProject/Help/html # Click-Once directory publish/ # Publish Web Output *.[Pp]ublish.xml *.azurePubxml # 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 # The packages folder can be ignored because of Package Restore **/packages/* # except build/, which is used as an MSBuild target. !**/packages/build/ # Uncomment if necessary however generally it will be regenerated when needed #!**/packages/repositories.config # NuGet v3's project.json files produces more ignorable files *.nuget.props *.nuget.targets # Microsoft Azure Build Output csx/ *.build.csdef # Microsoft Azure Emulator ecf/ rcf/ # Windows Store app package directories and files AppPackages/ BundleArtifacts/ Package.StoreAssociation.xml _pkginfo.txt # Visual Studio cache files # files ending in .cache can be ignored *.[Cc]ache # but keep track of directories ending in .cache !*.[Cc]ache/ # Others ClientBin/ ~$* *~ *.dbmdl *.dbproj.schemaview *.jfm *.pfx *.publishsettings orleans.codegen.cs # 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/ # Backup & report files from converting an old project file # to a newer Visual Studio version. Backup files are not needed, # because we have git ;-) _UpgradeReport_Files/ Backup*/ UpgradeLog*.XML UpgradeLog*.htm # SQL Server files *.mdf *.ldf *.ndf # Business Intelligence projects *.rdl.data *.bim.layout *.bim_*.settings # Microsoft Fakes FakesAssemblies/ # GhostDoc plugin setting file *.GhostDoc.xml # Node.js Tools for Visual Studio .ntvs_analysis.dat node_modules/ # Typescript v1 declaration files typings/ # Visual Studio 6 build log *.plg # Visual Studio 6 workspace options file *.opt # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) *.vbw # 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/ # JetBrains Rider .idea/ *.sln.iml # CodeRush .cr/ # Python Tools for Visual Studio (PTVS) __pycache__/ *.pyc # Cake - Uncomment if you are using it tools/** # Telerik's JustMock configuration file *.jmconfig # BizTalk build output *.btp.cs *.btm.cs *.odx.cs *.xsd.cs ================================================ FILE: .travis.yml ================================================ language: csharp # runtime config mono: none dotnet: 5.0.100 dist: bionic # branch build config branches: only: - master - preview - dev except: - gh-pages # git config git: # depth: 1 lfs_skip_smudge: true # disable the download of LFS objects when cloning script: - bash build.sh --target=build ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at weihanli@outlook.com. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see https://www.contributor-covenant.org/faq ================================================ FILE: Directory.Build.props ================================================ net10.0 preview enable enable WeihanLi.Npoi Contributors Copyright 2017-$([System.DateTime]::Now.Year) (c) WeihanLi $(NoWarn);NU5048; ================================================ FILE: Directory.Packages.props ================================================ true true direct ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "{}" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright 2018 WeihanLi Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: README.md ================================================ # WeihanLi.Npoi [![WeihanLi.Npoi](https://img.shields.io/nuget/v/WeihanLi.Npoi)](https://www.nuget.org/packages/WeihanLi.Npoi/) [![WeihanLi.Npoi Latest](https://img.shields.io/nuget/vpre/WeihanLi.Npoi)](https://www.nuget.org/packages/WeihanLi.Npoi/absoluteLatest) [![NuGet Downloads](https://img.shields.io/nuget/dt/WeihanLi.Npoi)](https://www.nuget.org/packages/WeihanLi.Npoi/) [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/WeihanLi/WeihanLi.Npoi) [![Azure Pipeline Build Status](https://weihanli.visualstudio.com/Pipelines/_apis/build/status/WeihanLi.WeihanLi.Npoi?branchName=dev)](https://weihanli.visualstudio.com/Pipelines/_build/latest?definitionId=13&branchName=dev) [![Github Build Status](https://github.com/WeihanLi/WeihanLi.Npoi/actions/workflows/dotnet.yml/badge.svg)](https://github.com/WeihanLi/WeihanLi.Npoi/actions/workflows/dotnet.yml) ## Introduction [NPOI](https://www.nuget.org/packages/NPOI/) extensions based on target framework `netstandard2.0`. `WeihanLi.Npoi` provides a powerful and easy-to-use toolkit for working with Excel and CSV files in .NET applications. It offers: - **Simple API**: Intuitive extension methods for common import/export operations - **Flexible Configuration**: Support for both Attribute-based and FluentAPI configuration - **High Performance**: Optimized for handling large datasets efficiently - **Rich Features**: Advanced capabilities like template export, multi-sheet support, and shadow properties - **CSV Support**: Full support for CSV file operations alongside Excel ## Core Features ### 📥 Data Import - Import Excel files to `List` or `IEnumerable` - Import Excel files to `DataTable` - Import CSV files to entities or DataTable - Support for custom header rows and sheet selection - Automatic type conversion and data mapping ### 📤 Data Export - Export `IEnumerable` or `DataTable` to Excel files (.xls/.xlsx) - Export data to Excel byte arrays or streams - Export to CSV files or byte arrays - Template-based export with placeholders for complex layouts - Multi-sheet export in a single workbook ### ⚙️ Configuration Options - **Attribute Configuration**: Simple decoration with `[Column]` and `[Sheet]` attributes - **FluentAPI Configuration**: Powerful and flexible configuration with fluent syntax (Recommended) - Custom column mapping, formatting, and transformations - Support for shadow properties (columns not in the model) ### 🎨 Advanced Capabilities - **InputFormatter/OutputFormatter**: Transform data during import/export operations - **ColumnInputFormatter/ColumnOutputFormatter**: Column-specific data transformations - **CellReader**: Custom cell reading logic - **Template Export**: Export data based on pre-designed Excel templates - **Multi-Sheet Support**: Handle multiple sheets in a single workbook - **Shadow Properties**: Define additional export columns not present in your models - **Auto Column Width**: Automatic column width adjustment - **Freeze Panes**: Set freeze panes for better data viewing - **Filters**: Add auto-filters to your Excel sheets ### GetStarted #### Installation ```sh dotnet add package WeihanLi.Npoi ``` #### Quick Start 1. Export list/dataTable to Excel/csv ``` csharp var entities = new List(); // Export to Excel file entities.ToExcelFile(string excelPath); // Export to Excel bytes entities.ToExcelBytes(ExcelFormat excelFormat); // Export to CSV file entities.ToCsvFile(string csvPath); // Export to CSV bytes entities.ToCsvBytes(); ``` 2. Import Excel/csv to List ``` csharp // Read Excel first sheet content to List var entityList = ExcelHelper.ToEntityList(string excelPath); // Read Excel first sheet content to IEnumerable var entityList = ExcelHelper.ToEntities(string excelPath); // Read Excel specific sheet content to List // You can customize header row index via sheet attribute or fluent api HasSheet var entityList1 = ExcelHelper.ToEntityList(string excelPath, int sheetIndex); // Import CSV to List var entityList2 = CsvHelper.ToEntityList(string csvPath); var entityList3 = CsvHelper.ToEntityList(byte[] csvBytes); ``` 3. Import Excel/csv to DataTable ``` csharp // Read Excel to DataTable directly, by default read the first sheet content var dataTable = ExcelHelper.ToDataTable(string excelPath); // Read Excel workbook's specific sheet to DataTable var dataTableOfSheetIndex = ExcelHelper.ToDataTable(string excelPath, int sheetIndex); // Read Excel with custom header row index var dataTableOfSheetIndex = ExcelHelper.ToDataTable(string excelPath, int sheetIndex, int headerRowIndex); // Read Excel to DataTable using mapping relations and settings from typeof(T) var dataTableT = ExcelHelper.ToDataTable(string excelPath); // Read CSV file data to DataTable var dataTable1 = CsvHelper.ToDataTable(string csvFilePath); ``` More Api documentation: ### Configuration #### 1. Using Attributes Add `ColumnAttribute` on the properties of your entity for export or import operations. Add `SheetAttribute` on the entity to configure sheet settings. You can set the `StartRowIndex` as needed (default is `1`). Example: ``` csharp [Sheet(SheetName = "TestSheet", SheetIndex = 0, AutoColumnWidthEnabled = true)] public class TestEntity { [Column("ID", Index = 0)] public int PKID { get; set; } [Column("Bill Title", Index = 1)] public string BillTitle { get; set; } [Column("Bill Details", Index = 2)] public string BillDetails { get; set; } [Column("Created By", Index = 3)] public string CreatedBy { get; set; } [Column("Created Time", Index = 4, Formatter = "yyyy-MM-dd HH:mm:ss")] public DateTime CreatedTime { get; set; } [Column(IsIgnored = true)] public string InternalNote { get; set; } } public class TestEntity1 { [Column("Username")] public string Username { get; set; } [Column(IsIgnored = true)] public string PasswordHash { get; set; } [Column("Amount")] public decimal Amount { get; set; } = 1000M; [Column("WeChat OpenID")] public string WechatOpenId { get; set; } [Column("Is Active")] public bool IsActive { get; set; } } ``` #### 2. Using FluentAPI (Recommended) FluentAPI provides greater flexibility and more powerful configuration options. Example: ``` csharp var setting = FluentSettings.For(); // Excel document settings setting.HasAuthor("WeihanLi") .HasTitle("WeihanLi.Npoi test") .HasDescription("WeihanLi.Npoi test") .HasSubject("WeihanLi.Npoi test"); // Sheet configuration (sheetIndex, sheetName, startRowIndex, autoColumnWidth) setting.HasSheetConfiguration(0, "SystemSettingsList", 1, true); // Apply filters and freeze panes // setting.HasFilter(0, 1).HasFreezePane(0, 1, 2, 1); // Configure individual properties setting.Property(_ => _.SettingId) .HasColumnIndex(0); setting.Property(_ => _.SettingName) .HasColumnTitle("SettingName") .HasColumnIndex(1); setting.Property(_ => _.DisplayName) .HasOutputFormatter((entity, displayName) => $"AAA_{entity.SettingName}_{displayName}") .HasInputFormatter((entity, originVal) => originVal.Split(new[] { '_' })[2]) .HasColumnTitle("DisplayName") .HasColumnIndex(2); setting.Property(_ => _.SettingValue) .HasColumnTitle("SettingValue") .HasColumnIndex(3); setting.Property(_ => _.CreatedTime) .HasColumnTitle("CreatedTime") .HasColumnIndex(4) .HasColumnWidth(10) .HasColumnFormatter("yyyy-MM-dd HH:mm:ss"); setting.Property(_ => _.CreatedBy) .HasColumnInputFormatter(x => x += "_test") .HasColumnIndex(5) .HasColumnTitle("CreatedBy"); setting.Property(x => x.Enabled) .HasColumnInputFormatter(val => "Enabled".Equals(val)) .HasColumnOutputFormatter(v => v ? "Enabled" : "Disabled"); // Shadow property - define a column that doesn't exist in the model setting.Property("HiddenProp") .HasOutputFormatter((entity, val) => $"HiddenProp_{entity.PKID}"); // Ignore specific properties setting.Property(_ => _.PKID).Ignored(); setting.Property(_ => _.UpdatedBy).Ignored(); setting.Property(_ => _.UpdatedTime).Ignored(); ``` ### Advanced Features #### Template-based Export Export data based on pre-designed Excel templates with placeholder support: ```csharp entities.ToExcelFileByTemplate( templatePath: "path/to/template.xlsx", excelPath: "path/to/output.xlsx", extraData: new { Author = "WeihanLi", Title = "Export Result" } ); ``` Learn more: [Template Export Documentation](https://weihanli.github.io/WeihanLi.Npoi/articles/en/TemplateExport.html) #### Multi-Sheet Export Export multiple collections to different sheets in a single workbook: ```csharp var workbook = ExcelHelper.PrepareWorkbook(ExcelFormat.Xlsx); workbook.ImportData(collection1, sheetIndex: 0); workbook.ImportData(collection2, sheetIndex: 1); workbook.WriteToFile("multi-sheets.xlsx"); ``` Learn more: [Multi-Sheet Documentation](https://weihanli.github.io/WeihanLi.Npoi/articles/en/MultiSheets.html) #### Shadow Properties Define additional export columns that don't exist in your model: ```csharp var settings = FluentSettings.For(); settings.Property("Employee ID") .HasOutputFormatter((entity, val) => $"{entity.UserFields[2].Value}"); settings.Property("Department") .HasOutputFormatter((entity, val) => $"{entity.UserFields[1].Value}"); ``` Learn more: [Shadow Property Documentation](https://weihanli.github.io/WeihanLi.Npoi/articles/en/ShadowProperty.html) ### Documentation - 📖 [Complete Documentation](https://weihanli.github.io/WeihanLi.Npoi/) - 🚀 [Getting Started Guide](https://weihanli.github.io/WeihanLi.Npoi/articles/en/GetStarted.html) - 📚 [API Reference](https://weihanli.github.io/WeihanLi.Npoi/api/WeihanLi.Npoi.html) - 🌐 [Articles](https://weihanli.github.io/WeihanLi.Npoi/articles/intro.html) ### More see some articles here: more usage:
Get a workbook ``` csharp // load excel workbook from file var workbook = LoadExcel(string excelPath); // prepare a workbook accounting to excelPath var workbook = PrepareWorkbook(string excelPath); // prepare a workbook accounting to excelPath and custom excel settings var workbook = PrepareWorkbook(string excelPath, ExcelSetting excelSetting); // prepare a workbook whether *.xls file var workbook = PrepareWorkbook(bool isXls); // prepare a workbook whether *.xls file and custom excel setting var workbook = PrepareWorkbook(bool isXlsx, ExcelSetting excelSetting); ```
Rich extensions ``` csharp List ToEntityList([NotNull]this IWorkbook workbook) DataTable ToDataTable([NotNull]this IWorkbook workbook) ISheet ImportData([NotNull] this ISheet sheet, DataTable dataTable) int ImportData([NotNull] this IWorkbook workbook, IEnumerable list, int sheetIndex) int ImportData([NotNull] this ISheet sheet, IEnumerable list) int ImportData([NotNull] this IWorkbook workbook, [NotNull] DataTable dataTable, int sheetIndex) ToExcelFile([NotNull] this IEnumerable entityList, [NotNull] string excelPath) int ToExcelStream([NotNull] this IEnumerable entityList, [NotNull] Stream stream) byte[] ToExcelBytes([NotNull] this IEnumerable entityList) int ToExcelFile([NotNull] this DataTable dataTable, [NotNull] string excelPath) int ToExcelStream([NotNull] this DataTable dataTable, [NotNull] Stream stream) byte[] ToExcelBytes([NotNull] this DataTable dataTable) byte[] ToExcelBytes([NotNull] this IWorkbook workbook) int WriteToFile([NotNull] this IWorkbook workbook, string filePath) object GetCellValue([NotNull] this ICell cell, Type propertyType) T GetCellValue([NotNull] this ICell cell) void SetCellValue([NotNull] this ICell cell, object value) byte[] ToCsvBytes(this IEnumerable entities, bool includeHeader) ToCsvFile(this IEnumerable entities, string filePath, bool includeHeader) void ToCsvFile(this DataTable dt, string filePath, bool includeHeader) byte[] ToCsvBytes(this DataTable dt, bool includeHeader) ```
### Samples - [.NET Core Sample](https://github.com/WeihanLi/WeihanLi.Npoi/blob/dev/samples/DotNetCoreSample/Program.cs) - [More Samples in Unit Tests](https://github.com/WeihanLi/WeihanLi.Npoi/blob/dev/test/WeihanLi.Npoi.Test/ExcelTest.cs) - [Guide Posts](https://weihanli.github.io/WeihanLi.Npoi/articles/intro.html) ### Contributing Contributions are welcome! Please feel free to submit a Pull Request. 1. Fork the repository 2. Create your feature branch (`git checkout -b feature/AmazingFeature`) 3. Commit your changes (`git commit -m 'Add some AmazingFeature'`) 4. Push to the branch (`git push origin feature/AmazingFeature`) 5. Open a Pull Request ### Acknowledgements - Thanks to all the [contributors](https://github.com/WeihanLi/WeihanLi.Npoi/graphs/contributors) and users of this project - Thanks to [NPOI](https://github.com/tonyqus/npoi) for the excellent Excel library - Thanks to [FluentExcel](https://github.com/Arch/FluentExcel/) for the FluentAPI inspiration - Thanks to JetBrains for the free Rider license ### License This project is licensed under the Apache License 2.0 - see the [LICENSE](LICENSE) file for details. ### Contact & Support - 📧 Report Issues/Questions/Discussions: [GitHub Issues](https://github.com/WeihanLi/WeihanLi.Npoi/issues) - 📦 NuGet Package: [WeihanLi.Npoi](https://www.nuget.org/packages/WeihanLi.Npoi/) ================================================ FILE: WeihanLi.Npoi.sln.DotSettings ================================================  True ================================================ FILE: WeihanLi.Npoi.slnx ================================================ ================================================ FILE: azure-pipelines.yml ================================================ trigger: branches: include: - '*' # must quote since "*" is a YAML reserved character; we want a string paths: exclude: - '*.md' pool: vmImage: 'windows-latest' steps: - task: UseDotNet@2 displayName: 'Use .NET SDK' inputs: packageType: sdk version: 10.0.x - script: dotnet --info displayName: 'dotnet info' - powershell: dotnet build.cs displayName: 'Powershell Script' env: NuGet__ApiKey: $(nugetApiKey) NuGet__Source: $(nugetSourceUrl) ================================================ FILE: build/common.props ================================================ netstandard2.0 WeihanLi.Npoi WeihanLi.Npoi WeihanLi WeihanLi WeihanLi.Npoi https://avatars3.githubusercontent.com/u/7604648 Apache-2.0 https://github.com/WeihanLi/WeihanLi.Npoi https://github.com/WeihanLi/WeihanLi.Npoi git npoi excel csv Amazing NPOI Extensions Import/Export excel or csv to/from entity list or DataTable(DataSet) Custom configuration and mappings via Attribute/FluentAPI https://github.com/WeihanLi/WeihanLi.Npoi/tree/dev/docs/ReleaseNotes.md true 3.3.0 ================================================ FILE: build/getReleaseVersion.ps1 ================================================ $versionPath=$PSScriptRoot+"/version.props" $versionXml=([xml](Get-Content $versionPath)) $versionProperty=$versionXml.Project.PropertyGroup $ReleaseVersion=$versionProperty.VersionMajor+"."+$versionProperty.VersionMinor+"."+$versionProperty.VersionPatch $ReleaseVersion Add-Content -Path $env:GITHUB_ENV -Value "ReleaseVersion=${ReleaseVersion}" ================================================ FILE: build/sign.props ================================================ True $(MSBuildThisFileDirectory)weihanli.snk False ================================================ FILE: build/version.props ================================================ 3 3 0 $(VersionMajor).$(VersionMinor).$(VersionPatch) $(PackageVersion) ================================================ FILE: build.cs ================================================ // Copyright (c) Weihan Li. All rights reserved. // Licensed under the Apache license. #:package WeihanLi.Common using WeihanLi.Common.Helpers; var solutionPath = "./WeihanLi.Npoi.slnx"; string[] srcProjects = [ "./src/WeihanLi.Npoi/WeihanLi.Npoi.csproj" ]; string[] testProjects = [ "./test/WeihanLi.Npoi.Test/WeihanLi.Npoi.Test.csproj" ]; string[] runFileSamplesFolders = [ "./samples/run-file-samples" ]; await DotNetPackageBuildProcess .Create(options => { options.SolutionPath = solutionPath; options.SrcProjects = srcProjects; options.TestProjects = testProjects; options.RunFileSampleFolders = runFileSamplesFolders; }) .ExecuteAsync(args); ================================================ FILE: docs/ReleaseNotes.md ================================================ # WeihanLi.Npoi Release Notes ## [3.3.0](https://www.nuget.org/packages/WeihanLi.Npoi/3.3.0) - Support `HasPostImportAction` to support post handler when import entity - Code Refactoring to keep clean code - Modernize dependenncies and integrate GithubActionsTestLogger and improve build script ## [3.2.0](https://www.nuget.org/packages/WeihanLi.Npoi/3.2.0) - Upgrade dependencies - Update samples/tests to .NET 10 - refine build scripts ## [3.1.0](https://www.nuget.org/packages/WeihanLi.Npoi/3.1.0) - Upgrade NPOI package to fix merged region handling bug which causes export excel by template with merged region exception - Migrate to slnx, xunit v3 ## [3.0.0](https://www.nuget.org/packages/WeihanLi.Npoi/3.0.0) - Remove `net6.0` target, and update build sdk and samples/tests to `net8.0` - Adjust column index enhancements, respect property index by default and support `WithPropertyComparer` ## [2.5.0](https://www.nuget.org/packages/WeihanLi.Npoi/2.5.0) - Upgrade dependencies to fix upstream breaking changes - Enable central package version management ## [2.4.0](https://www.nuget.org/packages/WeihanLi.Npoi/2.4.0) - Fixes , fix csv encoding handling issue, thanks @yesyeey for spotting the issue - `CsvHelper` enhancements ## [2.3.0](https://www.nuget.org/packages/WeihanLi.Npoi/2.3.0) - Add check before `WriteToFile` - Close workbook when the workbook would not be used anymore - Fixes , great thanks for @hansolehuang's help ## [2.2.0](https://www.nuget.org/packages/WeihanLi.Npoi/2.2.0) - Fix exception when read header that cell format is not string, , great thanks for @ensleep's help - Fix exception when export excel path without directory info, , great thanks for @ensleep's help ## [2.1.0](https://www.nuget.org/packages/WeihanLi.Npoi/2.1.0) - Add `HasCellReader` to support more read flexibility ## [2.0.0](https://www.nuget.org/packages/WeihanLi.Npoi/2.0.0) - Add `net6.0` target support - Refactor `CsvHelper` - Add `CsvOptions` for `CsvHelper` - Add support for validation, fixes #102 - Add support for `ToEntities`, fixes #113 ## [1.21.0](https://www.nuget.org/packages/WeihanLi.Npoi/1.21.0) - Add support for duplicate column name for dataTable - Fix sheet name not applied bug #127 ## [1.20.0](https://www.nuget.org/packages/WeihanLi.Npoi/1.20.0) - The `ExcelHelper.ToDataTable` was extended with two arguments `bool removeEmptyRows = false, int? maxColumns = null` - Fix possible `IndexOutOfRangeException` when loading rows ## [1.19.1](https://www.nuget.org/packages/WeihanLi.Npoi/1.19.1) - Fix `ExcelHelper.ToDataTable` bug when the imported excel column value is not the string value, thanks for @Ninjanaut's contribution ## [1.19.0](https://www.nuget.org/packages/WeihanLi.Npoi/1.19.0) - Fix `ExcelHelper.ToDataTable` bug when the imported excel file first column is empty, thanks for @Ninjanaut's contribution - `FluentSettings.LoadMappingProfile` enhancement ## [1.18.0](https://www.nuget.org/packages/WeihanLi.Npoi/1.18.0) - add `MappingProfile` support so that we could split mappings into separate mapping profiles ## [1.17.0](https://www.nuget.org/packages/WeihanLi.Npoi/1.17.0) - add `DrawingPatriarch` null check for `GetPicturesAndPosition` ## [1.16.0](https://www.nuget.org/packages/WeihanLi.Npoi/1.16.0) - add `CellAction`/`RowAction`/`SheetAction` for more flexible export ## [1.15.0](https://www.nuget.org/packages/WeihanLi.Npoi/1.15.0) - add support for image import/export ## [1.14.0](https://www.nuget.org/packages/WeihanLi.Npoi/1.14.0) - enable nullable reference - remove `net45` target ## [1.13.0](https://www.nuget.org/packages/WeihanLi.Npoi/1.13.0) - add support for `EntityList`/`DataTable` export auto split sheets when needed ## [1.12.0](https://www.nuget.org/packages/WeihanLi.Npoi/1.12.0) - refactor `ExcelSetting` and `SheetSetting` - add support for `RowFilter` and `CellFilter`(mainly for import) - add support for reading file when file opened by another process ## [1.11.0](https://www.nuget.org/packages/WeihanLi.Npoi/1.11.0) - add support for formula value import ## [1.10.0](https://www.nuget.org/packages/WeihanLi.Npoi/1.10.0) - add `EndRowIndex` for `SheetSetting`(zero-based, included) - add FluentAPI `WithDataValidation` for excel setting, if set will ignore invalid data when import excel - remove `CSVHelper` `TEntity` `new()` constraint ## [1.9.6](https://www.nuget.org/packages/WeihanLi.Npoi/1.9.6) - fix xlsx workbook `AppVersion` property value caused warning ## [1.9.5](https://www.nuget.org/packages/WeihanLi.Npoi/1.9.5) - fix `ExcelHelper.ToDataTable` bug with blank cell, thanks for hokis's feedback ## [1.9.4](https://www.nuget.org/packages/WeihanLi.Npoi/1.9.4) - expose `CsvHelper.GetCsvText` extensions ## [1.9.2](https://www.nuget.org/packages/WeihanLi.Npoi/1.9.2) - fix `CsvHelper.ParseLine` bug with quoted value, thanks for hokis's effort ## [1.9.0](https://www.nuget.org/packages/WeihanLi.Npoi/1.9.0) - remove `return 1` code fix #64 - optimize fluent formatter performance - add `FluentSettings.For` instead of `ExcelHelper.SettingsFor`, fluent settings is not only for excel, but also work with csv ## [1.8.2](https://www.nuget.org/packages/WeihanLi.Npoi/1.8.2) - add `TemplateHelper.ConfigureTemplateOptions` to allow user config templateParamFormat ## [1.8.1](https://www.nuget.org/packages/WeihanLi.Npoi/1.8.1) - add `ExportExcelByTemplate`, fix #33 - update `NpoiRowCollection` - optimize `DataTable` support for csv - export csv as utf8 encoding - update export generic type constraint, remove `new()` constraint ## [1.7.0](https://www.nuget.org/packages/WeihanLi.Npoi/1.7.0) - add `HasColumnInputFormatter`/`HasColumnOutputFormatter` - simply `IExcelConfiguration` SheetConfiguration ## [1.6.1](https://www.nuget.org/packages/WeihanLi.Npoi/1.6.1) - fix inherit property configure bug - fix empty column skipped bug, fix with `row.Cells.Count` => `row.LastCellNumber` - optimize `AdjustColumnIndex` - allow use `Ignored(false)` to unignore property ## [1.6.0](https://www.nuget.org/packages/WeihanLi.Npoi/1.6.0) - add shadow property support - add version info when export `*.xlsx` excel ## [1.5.0](https://www.nuget.org/packages/WeihanLi.Npoi/1.5.0) - add support for more format, treat as xlsx - add `AutoColumnWidthEnabled` setting to `SheetSetting`, no autoSizeColumn by default - add `CsvHelper.ToEntityList(byte[] bytes)`/`CsvHelper.ToEntityList(Stream stream)` - use xls for default ExcelFormat(better performance) ## [1.4.5](https://www.nuget.org/packages/WeihanLi.Npoi/1.4.5) - try to auto adjust column index when import excel(do not update existing settings) - add `InputFormatter`/`OutputFormatter` - apply column settings for CSV - remove unused SheetConfiguration ## [1.4.4](https://www.nuget.org/packages/WeihanLi.Npoi/1.4.4) - add `ExcelHelper.LoadExcel()`/`ExcelHelper.ToEntityList` override for stream/bytes ## [1.4.3](https://www.nuget.org/packages/WeihanLi.Npoi/1.4.3) - fix possible `NullReferenceException` when `ExcelHelper.ToEntityList()`/`ToExcelFile()` - fix treat `string.Empty` as `null` bug, `SetCellType` after `SetCellValue` so that `null` => `CellType.Blank`, `string.Empty` => `CellType.String` ## [1.4.0](https://www.nuget.org/packages/WeihanLi.Npoi/1.4.0) - add support for custom column width - fix `ToExcelFile`/`ImportData` extension not applied configuration bug - add support for specific sheetIndex when export excel ## [1.3.8](https://www.nuget.org/packages/WeihanLi.Npoi/1.3.8) - fix : `CsvHelper.ToDataTable()` and export DataTable to csv (Thanks for Arek's feedback) - add support for no header when export excel(to fix #26 ) ## [1.3.7](https://www.nuget.org/packages/WeihanLi.Npoi/1.3.7) - add `HasColumnFormatter(Func columnFormatter)` to support for custom column output for export ## [1.3.6](https://www.nuget.org/packages/WeihanLi.Npoi/1.3.6) - add [sourcelink](http://github.com/dotnet/sourcelink) support ## [1.3.5](https://www.nuget.org/packages/WeihanLi.Npoi/1.3.5) - add support for csv escape ## [1.3.3](https://www.nuget.org/packages/WeihanLi.Npoi/1.3.3) - fix csv custom columnIndex bug - Optimize csv operation for entity list ## [1.3.0](https://www.nuget.org/packages/WeihanLi.Npoi/1.3.0) - Update NPOI package to 2.4.1 - Add support for struct types - Add default excel settings ## [1.2.0](https://www.nuget.org/packages/WeihanLi.Npoi/1.2.0) - Update NPOI package to 2.4.0, use NPOI for netstandard2.0 also - Add CsvHelper for import and export csv file, and mapping to entities ## [1.1.0](https://www.nuget.org/packages/WeihanLi.Npoi/1.1.0) - StrongNaming package ================================================ FILE: docs/api/.gitignore ================================================ ############### # temp file # ############### *.yml .manifest ================================================ FILE: docs/api/index.md ================================================ # PLACEHOLDER TODO: Add .NET projects to the *src* folder and run `docfx` to generate **REAL** *API Documentation*! ================================================ FILE: docs/articles/en/CustomizeStyle.md ================================================ # Customize Excel Styles ## Introduction WeihanLi.Npoi provides powerful styling capabilities through the `RowAction` and `CellAction` callbacks in sheet configuration. You can customize fonts, colors, alignment, borders, and add data validation to create professional-looking Excel files. ## Auto Column Width The simplest styling feature is enabling automatic column width adjustment: ```csharp var settings = FluentSettings.For(); settings.HasSheetSetting(config => { config.AutoColumnWidthEnabled = true; }); ``` This automatically adjusts column widths based on content, making your spreadsheet more readable without manual width adjustments. ## Styling Header Rows Use `RowAction` to customize the appearance of rows. A common use case is styling the header row: ```csharp settings.HasSheetSetting(config => { config.StartRowIndex = 1; config.SheetName = "StyledSheet"; config.AutoColumnWidthEnabled = true; config.RowAction = row => { if (row.RowNum == 0) // Header row { // Create a cell style var style = row.Sheet.Workbook.CreateCellStyle(); style.Alignment = HorizontalAlignment.Center; // Create and configure font var font = row.Sheet.Workbook.CreateFont(); font.FontName = "Arial"; font.IsBold = true; font.FontHeight = 220; // 11pt (font height is in 1/20th of a point) font.Color = IndexedColors.White.Index; style.SetFont(font); // Set background color style.FillForegroundColor = IndexedColors.DarkBlue.Index; style.FillPattern = FillPattern.SolidForeground; // Apply style to all cells in the row row.Cells.ForEach(c => c.CellStyle = style); } }; }); ``` ### Font Properties Available font properties include: - `FontName`: Font family (e.g., "Arial", "Calibri", "Times New Roman") - `FontHeight`: Font size in 1/20th of a point (e.g., 200 = 10pt, 220 = 11pt) - `IsBold`: Bold text - `IsItalic`: Italic text - `IsStrikeout`: Strikethrough text - `Underline`: Text underline style - `Color`: Font color using `IndexedColors` ### Cell Style Properties Key cell style properties: - `Alignment`: Horizontal alignment (`Left`, `Center`, `Right`, `Justify`) - `VerticalAlignment`: Vertical alignment (`Top`, `Center`, `Bottom`) - `FillForegroundColor`: Background color - `FillPattern`: Fill pattern (usually `SolidForeground` for solid colors) - `BorderTop`, `BorderBottom`, `BorderLeft`, `BorderRight`: Border styles - `WrapText`: Enable text wrapping ## Styling Individual Cells Use `CellAction` to customize individual cells based on their position or content: ```csharp settings.HasSheetSetting(config => { config.CellAction = cell => { // Style specific columns if (cell.ColumnIndex == 0) // First column { var style = cell.Sheet.Workbook.CreateCellStyle(); var font = cell.Sheet.Workbook.CreateFont(); font.IsBold = true; style.SetFont(font); cell.CellStyle = style; } // Conditional styling based on content if (cell.RowIndex > 0 && cell.ColumnIndex == 3) // Data rows, 4th column { if (cell.NumericCellValue < 0) // Negative numbers in red { var style = cell.Sheet.Workbook.CreateCellStyle(); var font = cell.Sheet.Workbook.CreateFont(); font.Color = IndexedColors.Red.Index; style.SetFont(font); cell.CellStyle = style; } } }; }); ``` ## Data Validation Add dropdown lists and other validation rules to cells: ```csharp settings.HasSheetSetting(config => { config.CellAction = cell => { // Add dropdown validation for header cell matching "Status" if (cell.RowIndex == 0 && cell.StringCellValue == "Status") { var validationHelper = cell.Sheet.GetDataValidationHelper(); // Define allowed values var statusValues = new[] { "Active", "Inactive", "Pending" }; var constraint = validationHelper.CreateExplicitListConstraint(statusValues); // Apply validation to data rows (rows 1-100, current column) var addressList = new CellRangeAddressList(1, 100, cell.ColumnIndex, cell.ColumnIndex); var validation = validationHelper.CreateValidation(constraint, addressList); validation.ShowErrorBox = true; validation.CreateErrorBox("Invalid Status", "Please select from the dropdown list"); validation.ShowPromptBox = true; validation.CreatePromptBox("Status Selection", "Choose a status from the list"); cell.Sheet.AddValidationData(validation); } }; }); ``` ### Validation Types Different validation types are available: ```csharp // List validation (dropdown) var constraint = validationHelper.CreateExplicitListConstraint(new[] { "Option1", "Option2" }); // Integer validation var intConstraint = validationHelper.CreateIntegerConstraint( OperatorType.Between, "1", "100"); // Decimal validation var decimalConstraint = validationHelper.CreateDecimalConstraint( OperatorType.GreaterThan, "0", null); // Date validation var dateConstraint = validationHelper.CreateDateConstraint( OperatorType.Between, "2024-01-01", "2024-12-31", "yyyy-MM-dd"); // Text length validation var textConstraint = validationHelper.CreateTextLengthConstraint( OperatorType.LessThan, "100", null); ``` ## Complete Example Here's a comprehensive example combining multiple styling features: ```csharp public class StyledEntityProfile : IMappingProfile { public void Configure(IExcelConfiguration configuration) { configuration.HasAuthor("Your Name") .HasTitle("Styled Report") .HasDescription("Professional styled Excel report"); configuration.HasSheetSetting(config => { config.SheetName = "Report"; config.StartRowIndex = 1; config.AutoColumnWidthEnabled = true; // Style header row config.RowAction = row => { if (row.RowNum == 0) { var headerStyle = row.Sheet.Workbook.CreateCellStyle(); headerStyle.Alignment = HorizontalAlignment.Center; headerStyle.VerticalAlignment = VerticalAlignment.Center; headerStyle.FillForegroundColor = IndexedColors.Grey25Percent.Index; headerStyle.FillPattern = FillPattern.SolidForeground; var headerFont = row.Sheet.Workbook.CreateFont(); headerFont.FontName = "Calibri"; headerFont.IsBold = true; headerFont.FontHeight = 240; // 12pt headerStyle.SetFont(headerFont); // Add borders headerStyle.BorderBottom = BorderStyle.Thin; headerStyle.BorderTop = BorderStyle.Thin; headerStyle.BorderLeft = BorderStyle.Thin; headerStyle.BorderRight = BorderStyle.Thin; row.Cells.ForEach(c => c.CellStyle = headerStyle); } }; // Add validation and conditional formatting config.CellAction = cell => { // Add validation for status column if (cell.RowIndex == 0 && cell.StringCellValue == "Status") { var validationHelper = cell.Sheet.GetDataValidationHelper(); var statusList = new[] { "Approved", "Pending", "Rejected" }; var constraint = validationHelper.CreateExplicitListConstraint(statusList); var addressList = new CellRangeAddressList(1, 1000, cell.ColumnIndex, cell.ColumnIndex); var validation = validationHelper.CreateValidation(constraint, addressList); validation.ShowErrorBox = true; cell.Sheet.AddValidationData(validation); } // Highlight negative amounts in red if (cell.RowIndex > 0 && cell.ColumnIndex == 2) // Amount column { try { if (cell.NumericCellValue < 0) { var redStyle = cell.Sheet.Workbook.CreateCellStyle(); var redFont = cell.Sheet.Workbook.CreateFont(); redFont.Color = IndexedColors.Red.Index; redFont.IsBold = true; redStyle.SetFont(redFont); cell.CellStyle = redStyle; } } catch { } // Skip if not a numeric cell } }; }); // Configure properties configuration.Property(x => x.Id).HasColumnIndex(0); configuration.Property(x => x.Name).HasColumnIndex(1); configuration.Property(x => x.Amount).HasColumnIndex(2); configuration.Property(x => x.Status).HasColumnIndex(3); configuration.Property(x => x.Date) .HasColumnIndex(4) .HasColumnFormatter("yyyy-MM-dd"); } } ``` ## Best Practices 1. **Reuse Styles**: Create styles once and reuse them rather than creating new styles for each cell to improve performance and reduce file size. 2. **Performance**: Be mindful of performance when applying styles to large datasets. Consider styling only header rows or specific columns. 3. **Color Consistency**: Use `IndexedColors` for consistent coloring across different Excel versions. 4. **Font Sizes**: Remember that font height is in 1/20th of a point (multiply point size by 20). 5. **Validation Ranges**: Set appropriate ranges for data validation to cover expected data rows. ## References - [Sample Implementation](https://github.com/WeihanLi/WeihanLi.Npoi/blob/dev/samples/DotNetCoreSample/Program.cs) - [NPOI Documentation](http://poi.apache.org/) ================================================ FILE: docs/articles/en/GetStarted.md ================================================ # `WeihanLi.Npoi` Getting Started ## Introduction `WeihanLi.Npoi` is an Excel import/export library based on NPOI that provides many useful extension methods and also supports CSV import/export. - Import excel/csv data into `DataTable` or `List` - Export `IEnumerable` or `DataTable` to Excel files, byte arrays, or streams - Export `IEnumerable` or `DataTable` to CSV files or byte arrays - Configuration through `Attributes` or `FluentAPI` (inspired by [FluentExcel](https://github.com/Arch/FluentExcel/)) ## Basic Examples ``` csharp internal class BaseModel { public int Id { get; set; } } internal class Notice : BaseModel { public string Title { get; set; } public string Content { get; set; } public DateTime PublishedAt { get; set; } public string Publisher { get; set; } } ``` Basic import and export: ``` csharp // entities excel import/export [Theory] [InlineData(ExcelFormat.Xls)] [InlineData(ExcelFormat.Xlsx)] public void BasicImportExportTest(ExcelFormat excelFormat) { var list = new List(); for (var i = 0; i < 10; i++) { list.Add(new Notice() { Id = i + 1, Content = $"content_{i}", Title = $"title_{i}", PublishedAt = DateTime.UtcNow.AddDays(-i), Publisher = $"publisher_{i}" }); } list.Add(new Notice() { Title = "nnnn" }); list.Add(null); var excelBytes = list.ToExcelBytes(excelFormat); var importedList = ExcelHelper.ToEntityList(excelBytes, excelFormat); Assert.Equal(list.Count, importedList.Count); for (var i = 0; i < list.Count; i++) { if (list[i] is null) { Assert.Null(importedList[i]); } else { Assert.Equal(list[i].Id, importedList[i].Id); Assert.Equal(list[i].Title, importedList[i].Title); Assert.Equal(list[i].Content, importedList[i].Content); Assert.Equal(list[i].Publisher, importedList[i].Publisher); Assert.Equal(list[i].PublishedAt.ToTimeString(), importedList[i].PublishedAt.ToTimeString()); } } } // DataTable Excel import/export [Theory] [InlineData(ExcelFormat.Xls)] [InlineData(ExcelFormat.Xlsx)] public void DataTableImportExportTest(ExcelFormat excelFormat) { var dt = new DataTable(); dt.Columns.AddRange(new[] { new DataColumn("Name"), new DataColumn("Age"), new DataColumn("Desc"), }); for (var i = 0; i < 10; i++) { var row = dt.NewRow(); row.ItemArray = new object[] { $"Test_{i}", i + 10, $"Desc_{i}" }; dt.Rows.Add(row); } // var excelBytes = dt.ToExcelBytes(excelFormat); var importedData = ExcelHelper.ToDataTable(excelBytes, excelFormat); Assert.NotNull(importedData); Assert.Equal(dt.Rows.Count, importedData.Rows.Count); for (var i = 0; i < dt.Rows.Count; i++) { Assert.Equal(dt.Rows[i].ItemArray.Length, importedData.Rows[i].ItemArray.Length); for (var j = 0; j < dt.Rows[i].ItemArray.Length; j++) { Assert.Equal(dt.Rows[i].ItemArray[j], importedData.Rows[i].ItemArray[j]); } } } // entities csv import/export [Fact] public void BasicImportExportTest() { var list = new List(); for (var i = 0; i < 10; i++) { list.Add(new Notice() { Id = i + 1, Content = $"content_{i}", Title = $"title_{i}", PublishedAt = DateTime.UtcNow.AddDays(-i), Publisher = $"publisher_{i}" }); } list.Add(new Notice() { Id = 11, Content = $"content", Title = $"title", PublishedAt = DateTime.UtcNow.AddDays(1), }); var csvBytes = list.ToCsvBytes(); var importedList = CsvHelper.ToEntityList(csvBytes); Assert.Equal(list.Count, importedList.Count); for (var i = 0; i < list.Count; i++) { Assert.Equal(list[i].Id, importedList[i].Id); Assert.Equal(list[i].Title ?? "", importedList[i].Title); Assert.Equal(list[i].Content ?? "", importedList[i].Content); Assert.Equal(list[i].Publisher ?? "", importedList[i].Publisher); Assert.Equal(list[i].PublishedAt.ToTimeString(), importedList[i].PublishedAt.ToTimeString()); } } // DataTable csv import/export [Fact] public void DataTableImportExportTest() { var dt = new DataTable(); dt.Columns.AddRange(new[] { new DataColumn("Name"), new DataColumn("Age"), new DataColumn("Desc"), }); for (var i = 0; i < 10; i++) { var row = dt.NewRow(); row.ItemArray = new object[] { $"Test_{i}", i + 10, $"Desc_{i}" }; dt.Rows.Add(row); } // var csvBytes = dt.ToCsvBytes(); var importedData = CsvHelper.ToDataTable(csvBytes); Assert.NotNull(importedData); Assert.Equal(dt.Rows.Count, importedData.Rows.Count); for (var i = 0; i < dt.Rows.Count; i++) { Assert.Equal(dt.Rows[i].ItemArray.Length, importedData.Rows[i].ItemArray.Length); for (var j = 0; j < dt.Rows[i].ItemArray.Length; j++) { Assert.Equal(dt.Rows[i].ItemArray[j], importedData.Rows[i].ItemArray[j]); } } } ``` ## Custom Mapping and Configuration Using Attributes: ``` csharp internal class Model { [Column("Hotel ID", Index = 0)] public string HotelId { get; set; } [Column("Order No", Index = 1)] public string OrderNo { get; set; } [Column("Hotel Name", Index = 2)] public string HotelName { get; set; } [Column("Customer Name", Index = 3)] public string CustomerName { get; set; } [Column(nameof(RoomType), Index = 4)] public string RoomType { get; set; } [Column(nameof(CheckInDate), Index = 5, Formatter = "yyyy/M/d")] public DateTime CheckInDate { get; set; } [Column(nameof(CheckOutDate), Index = 6, Formatter = "yyyy/M/d")] public DateTime CheckOutDate { get; set; } [Column(nameof(RoomNights), Index = 7)] public int RoomNights { get; set; } [Column(nameof(PaymentType), Index = 8)] public string PaymentType { get; set; } [Column(nameof(OrderAmount), Index = 9)] public decimal OrderAmount { get; set; } [Column(nameof(CommissionRate), Index = 10)] public decimal CommissionRate { get; set; } [Column(nameof(ServiceFee), Index = 11)] public decimal ServiceFee { get; set; } } [Sheet(SheetIndex = 0, SheetName = "TestSheet", AutoColumnWidthEnabled = true)] internal class TestEntity2 { [Column(Index = 0)] public int Id { get; set; } [Column(Index = 1)] public string Title { get; set; } [Column(Index = 2, Width = 50)] public string Description { get; set; } [Column(Index = 3, Width = 20)] public string Extra { get; set; } = "{}"; } ``` Using FluentAPI (Recommended for greater flexibility): ``` csharp var setting = FluentSettings.For(); // ExcelSetting setting.HasAuthor("WeihanLi") .HasTitle("WeihanLi.Npoi test") .HasDescription("WeihanLi.Npoi test") .HasSubject("WeihanLi.Npoi test"); setting.HasSheetConfiguration(0, "SystemSettingsList", 1, true); // sheet configuration // setting // .HasFilter(0, 1) // Set filter on columns // .HasFreezePane(0, 1, 2, 1); // Set freeze pane setting.Property(_ => _.SettingId) .HasColumnIndex(0); setting.Property(_ => _.SettingName) .HasColumnTitle("SettingName") .HasColumnIndex(1); setting.Property(_ => _.DisplayName) .HasOutputFormatter((entity, displayName) => $"AAA_{entity.SettingName}_{displayName}") .HasInputFormatter((entity, originVal) => originVal.Split(new[] { '_' })[2]) .HasColumnTitle("DisplayName") .HasColumnIndex(2); setting.Property(_ => _.SettingValue) .HasColumnTitle("SettingValue") .HasColumnIndex(3); setting.Property(_ => _.CreatedTime) .HasColumnTitle("CreatedTime") .HasColumnIndex(4) .HasColumnWidth(10) // Set column width .HasColumnFormatter("yyyy-MM-dd HH:mm:ss"); setting.Property(_ => _.CreatedBy) .HasColumnInputFormatter(x => x += "_test") .HasColumnIndex(4) .HasColumnTitle("CreatedBy"); setting.Property(x => x.Enabled) .HasColumnInputFormatter(val => "Enabled".Equals(val)) .HasColumnOutputFormatter(v => v ? "Enabled" : "Disabled"); setting.Property("ShadowProperty") .HasOutputFormatter((entity, val) => $"HiddenProp_{entity.PKID}"); setting.Property(_ => _.PKID).Ignored(); // ignore column ``` ================================================ FILE: docs/articles/en/InputOutputFormatter.md ================================================ # InputOutputFormatter Introduction ## Introduction WeihanLi.Npoi introduces `OutputFormatter`/`InputFormatter`/`ColumnInputFormatter`/`ColumnOutputFormatter`, greatly enhancing the flexibility of import and export operations. These are only supported through FluentAPI configuration. Let's look at the following example. ## InputFormatter/OutputFormatter Example Model: ``` csharp internal abstract class BaseEntity { public int PKID { get; set; } } internal class TestEntity : BaseEntity { public Guid SettingId { get; set; } public string SettingName { get; set; } public string DisplayName { get; set; } public string SettingValue { get; set; } public string CreatedBy { get; set; } = "liweihan"; public DateTime CreatedTime { get; set; } = DateTime.Now; public string UpdatedBy { get; set; } public DateTime UpdatedTime { get; set; } public bool Enabled { get; set; } } ``` Example Configuration: ``` csharp var setting = FluentSettings.For(); // ExcelSetting setting.HasAuthor("WeihanLi") .HasTitle("WeihanLi.Npoi test") .HasDescription("WeihanLi.Npoi test") .HasSubject("WeihanLi.Npoi test"); setting.HasSheetConfiguration(0, "SystemSettingsList", 1, true); setting.Property(_ => _.SettingId) .HasColumnIndex(0); setting.Property(_ => _.SettingName) .HasColumnTitle("SettingName") .HasColumnIndex(1); setting.Property(_ => _.DisplayName) .HasOutputFormatter((entity, displayName) => $"AAA_{entity.SettingName}_{displayName}") .HasInputFormatter((entity, originVal) => originVal.Split(new[] { '_' })[2]) .HasColumnTitle("DisplayName") .HasColumnIndex(2); setting.Property(_ => _.SettingValue) .HasColumnTitle("SettingValue") .HasColumnIndex(3); setting.Property(_ => _.CreatedTime) .HasColumnTitle("CreatedTime") .HasColumnIndex(4) .HasColumnWidth(10) .HasColumnFormatter("yyyy-MM-dd HH:mm:ss"); setting.Property(_ => _.CreatedBy) .HasColumnInputFormatter(x => x += "_test") .HasColumnIndex(4) .HasColumnTitle("CreatedBy"); setting.Property(x => x.Enabled) .HasColumnInputFormatter(val => "Enabled".Equals(val)) .HasColumnOutputFormatter(v => v ? "Enabled" : "Disabled"); setting.Property("HiddenProp") .HasOutputFormatter((entity, val) => $"HiddenProp_{entity.PKID}"); setting.Property(_ => _.PKID).Ignored(); setting.Property(_ => _.UpdatedBy).Ignored(); setting.Property(_ => _.UpdatedTime).Ignored(); ``` Test Code: ``` csharp var entities = new List() { new TestEntity() { PKID = 1, SettingId = Guid.NewGuid(), SettingName = "Setting1", SettingValue = "Value1", DisplayName = "ddd1" }, new TestEntity() { PKID=2, SettingId = Guid.NewGuid(), SettingName = "Setting2", SettingValue = "Value2", Enabled = true }, }; var path = $@"{tempDirPath}\test.xlsx"; entities.ToExcelFile(path); var entitiesT0 = ExcelHelper.ToEntityList(path); ``` Export Result: ![Export Result](../images/489462-20200104112133779-1180097402.png) Import Result: ![Import Result 1](../images/489462-20200104112017420-1450911242.png) ![Import Result 2](../images/489462-20200104112025927-873408781.png) ================================================ FILE: docs/articles/en/MultiSheets.md ================================================ # Multi-Sheet Export ## Introduction Sometimes we may want to export multiple collections of data in multiple sheets within a single Excel file. You can refer to the following example code: ## Sample ```c# var collection1 = new[] { new TestEntity1() { Id = 1, Title = "test1" }, new TestEntity1() { Id = 2, Title = "test2" } }; var collection2 = new[] { new TestEntity2() { Id = 1, Title = "test1", Description = "description"}, new TestEntity2() { Id = 2, Title = "test2" } }; // Prepare a workbook var workbook = ExcelHelper.PrepareWorkbook(ExcelFormat.Xlsx); // Import collection1 to the first sheet workbook.ImportData(collection1); // Import collection2 to the second sheet workbook.ImportData(collection2, 1); // Export workbook to local file workbook.WriteToFile("multi-sheets.xlsx"); ``` If you need to customize configurations, it works the same as before - you can use attributes or fluent API: ```c# [Sheet(SheetName = "TestSheet", SheetIndex = 0)] file sealed class TestEntity1 { [Column("ID", Index = 0)] public int Id { get; set; } public string Title { get; set; } = string.Empty; } file sealed class TestEntity2 { public int Id { get; set; } public string Title { get; set; } = string.Empty; public string Description { get; set; } } ``` Fluent API configuration: ```c# var settings = FluentSettings.For(); settings.HasSheetSetting(sheet => sheet.SheetName = "TestEntity2", 1); settings.Property(x => x.Id) .HasColumnIndex(0) .HasColumnOutputFormatter(v => v.ToString("#0000")) ; settings.Property(x => x.Title) .HasColumnIndex(1) ; settings.Property(x => x.Description) .HasColumnIndex(2) ; ``` Export results: ![Sheet 0](../images/image-20241029231320957.png) ![Sheet 1](../images/image-20241029231519274.png) ## References - - ================================================ FILE: docs/articles/en/ShadowProperty.md ================================================ # WeihanLi.Npoi Now Supports `ShadowProperty` ## Introduction In Entity Framework, there's a concept called `ShadowProperty` (Shadow Property). You can define a property through FluentAPI that is not defined in the .NET model, and this property can only be operated through EF's `Change Tracker`. When exporting to Excel, you might want some exported columns not to be defined in your model. Some columns might just be added to export a nested property value, or you simply want to define an additional column while the model is defined elsewhere and is inconvenient to modify. Therefore, starting from version 1.6.0, `WeihanLi.Npoi` supports `ShadowProperty`, bringing the concept from EF into Excel export. Currently, `ShadowProperty` is read-only - reading returns the default value of the type, and it doesn't support `ChangeTracker` or modifications. ## Usage Example Here's a simple usage example (from a user-submitted issue: ) ``` csharp using System; using System.Collections.Generic; using System.IO; using WeihanLi.Npoi; namespace NpoiTest { public class Program { public static void Main(string[] args) { var settings = FluentSettings.For(); settings.Property(x => x.Name) .HasColumnIndex(0); // settings.Property(x => x.UserFields) // .HasOutputFormatter((entity, value) => $"{value[0].Value},{value[2].Value}") // .HasColumnTitle("Name,Employee ID") // .HasColumnIndex(1); settings.Property(x=>x.UserFields).Ignored(); settings.Property("Employee ID") .HasOutputFormatter((entity,val)=> $"{entity.UserFields[2].Value}") ; settings.Property("Department") .HasOutputFormatter((entity,val)=> $"{entity.UserFields[1].Value}") ; var data = new List() { new TestEntity() { Name = "xiaoming", TotalScore = 100, UserFields = new UserField[] { new UserField() { Name = "Name", Value = "xaioming", }, new UserField() { Name = "Department", Value = "1212" }, new UserField() { Name = "Employee ID", Value = "121213131" }, } } }; data.ToExcelFile($@"{Directory.GetCurrentDirectory()}\output.xls"); Console.WriteLine("complete."); } private class TestEntity { public string Name { get; set; } public UserField[] UserFields { get; set; } public int TotalScore { get; set; } } private class UserField { public string Fid { get; set; } public string Name { get; set; } public string Value { get; set; } } } } ``` Export result: ![Shadow Property Example](../images/489462-20191213084226066-1767559517.png) As you can see, we added two columns to the exported Excel that were not defined in the original Model. With this feature, we can more flexibly customize the content to be exported. ================================================ FILE: docs/articles/en/TemplateExport.md ================================================ # Export Excel Based on Template ## Introduction The original export method is suitable for relatively simple exports where each data record corresponds to one row, and data columns have a high degree of customization. However, it couldn't handle cases where one data record corresponds to multiple rows. Therefore, template-based export was introduced in version 1.8.0. ## Usage Example ### Example Template ![Template Example](../images/489462-20200128142956273-1478084552.png) The template can have three types of data: - Global: Parameters that can be specified during export as global parameters. Default format: `$(Global:PropName)` - Header: Display names for configured properties. Default is the property name. Default format: `$(Header:PropName)` - Data: Property values of the corresponding data. Default format: `$(Data:PropName)` Default template parameter format (customizable through `TemplateHelper.ConfigureTemplateOptions` method since version 1.8.2): - Global parameter: `$(Global:{0})` - Header parameter: `$(Header:{0})` - Data parameter: `$(Data:{0})` - Data Begin: `` - Data End: `` Template specifications: The template needs to use Data Begin and Data End to configure the start and end of the data template to identify the start and end rows for each data record. ### Example Code Example configuration: ``` csharp var setting = FluentSettings.For(); // ExcelSetting setting.HasAuthor("WeihanLi") .HasTitle("WeihanLi.Npoi test") .HasDescription("WeihanLi.Npoi test") .HasSubject("WeihanLi.Npoi test"); setting.HasSheetConfiguration(0, "SystemSettingsList", 1, true); setting.Property(_ => _.SettingId) .HasColumnIndex(0); setting.Property(_ => _.SettingName) .HasColumnTitle("SettingName") .HasColumnIndex(1); setting.Property(_ => _.DisplayName) .HasOutputFormatter((entity, displayName) => $"AAA_{entity.SettingName}_{displayName}") .HasInputFormatter((entity, originVal) => originVal.Split(new[] { '_' })[2]) .HasColumnTitle("DisplayName") .HasColumnIndex(2); setting.Property(_ => _.SettingValue) .HasColumnTitle("SettingValue") .HasColumnIndex(3); setting.Property(x => x.Enabled) .HasColumnInputFormatter(val => "Enabled".Equals(val)) .HasColumnOutputFormatter(v => v ? "Enabled" : "Disabled"); setting.Property("HiddenProp") .HasOutputFormatter((entity, val) => $"HiddenProp_{entity.PKID}"); setting.Property(_ => _.PKID).Ignored(); setting.Property(_ => _.UpdatedBy).Ignored(); setting.Property(_ => _.UpdatedTime).Ignored(); ``` Template-based export example code: ``` csharp var entities = new List() { new TestEntity() { PKID = 1, SettingId = Guid.NewGuid(), SettingName = "Setting1", SettingValue = "Value1", DisplayName = "ddd1" }, new TestEntity() { PKID=2, SettingId = Guid.NewGuid(), SettingName = "Setting2", SettingValue = "Value2", Enabled = true }, }; var csvFilePath = $@"{tempDirPath}\test.csv"; entities.ToExcelFileByTemplate( Path.Combine(ApplicationHelper.AppRoot, "Templates", "testTemplate.xlsx"), ApplicationHelper.MapPath("templateTestEntities.xlsx"), extraData: new { Author = "WeihanLi", Title = "Export Result" } ); ``` ### Export Result ![Export Result](../images/489462-20200128143038865-1452547986.png) ## More For convenience, several extension methods have been added: ``` csharp public static int ToExcelFileByTemplate([NotNull]this IEnumerable entities, string templatePath, string excelPath, int sheetIndex = 0, object extraData = null); public static int ToExcelFileByTemplate([NotNull]this IEnumerable entities, byte[] templateBytes, string excelPath, ExcelFormat excelFormat = ExcelFormat.Xls, int sheetIndex = 0, object extraData = null); public static int ToExcelFileByTemplate([NotNull]this IEnumerable entities, IWorkbook templateWorkbook, string excelPath, int sheetIndex = 0, object extraData = null); public static byte[] ToExcelBytesByTemplate([NotNull]this IEnumerable entities, string templatePath, int sheetIndex = 0, object extraData = null); public static byte[] ToExcelBytesByTemplate([NotNull]this IEnumerable entities, byte[] templateBytes, ExcelFormat excelFormat = ExcelFormat.Xls, int sheetIndex = 0, object extraData = null); public static byte[] ToExcelBytesByTemplate([NotNull]this IEnumerable entities, Stream templateStream, ExcelFormat excelFormat = ExcelFormat.Xls, int sheetIndex = 0, object extraData = null); public static byte[] ToExcelBytesByTemplate([NotNull]this IEnumerable entities, IWorkbook templateWorkbook, int sheetIndex = 0, object extraData = null); public static byte[] ToExcelBytesByTemplate([NotNull]this IEnumerable entities, ISheet templateSheet, object extraData = null); ``` ## References - - ================================================ FILE: docs/articles/intro.md ================================================ # WeihanLi.Npoi ## Intro `WeihanLi.Npoi` is an extension for excel/csv import/export based on NPOI ## Recommend Articles English: - [Getting Started](./en/GetStarted.md) - [InputOutputFormatter Usage](./en/InputOutputFormatter.md) - [ShadowProperty Usage](./en/ShadowProperty.md) - [Template Export](./en/TemplateExport.md) - [Multi-Sheet Export](./en/MultiSheets.md) - [Customize Styles](./en/CustomizeStyle.md) 中文: - [基本示例](./zh/GetStarted.md) - [InputOutputFormatter 使用](./zh/InputOutputFormatter.md) - [ShadowProperty 使用](./zh/ShadowProperty.md) - [根据模板导出](./zh/TemplateExport.md) - [多 sheet 导出](./zh/MultiSheets.md) - [自定义样式](./zh/CustomizeStyle.md) ## More if you wanna add your articles here, welcome pr ================================================ FILE: docs/articles/toc.yml ================================================ - name: Introduction href: intro.md - name: English Articles items: - name: Getting Started href: en/GetStarted.md - name: InputOutputFormatter href: en/InputOutputFormatter.md - name: ShadowProperty href: en/ShadowProperty.md - name: Template Export href: en/TemplateExport.md - name: Multi-Sheet Export href: en/MultiSheets.md - name: Customize Styles href: en/CustomizeStyle.md - name: 中文文章 items: - name: GetStarted href: zh/GetStarted.md - name: InputOutputFormatter href: zh/InputOutputFormatter.md - name: ShadowProperty href: zh/ShadowProperty.md - name: TemplateExport href: zh/TemplateExport.md - name: MultiSheets href: zh/MultiSheets.md - name: 自定义样式 href: zh/CustomizeStyle.md ================================================ FILE: docs/articles/zh/CustomizeStyle.md ================================================ # 自定义 Excel 样式 ## 简介 WeihanLi.Npoi 通过 sheet 配置中的 `RowAction` 和 `CellAction` 回调提供了强大的样式定制功能。你可以自定义字体、颜色、对齐方式、边框,并添加数据验证,以创建专业外观的 Excel 文件。 ## 自动列宽 最简单的样式功能是启用自动列宽调整: ```csharp var settings = FluentSettings.For(); settings.HasSheetSetting(config => { config.AutoColumnWidthEnabled = true; }); ``` 这会根据内容自动调整列宽,使你的电子表格更易读,无需手动调整宽度。 ## 样式化标题行 使用 `RowAction` 自定义行的外观。一个常见的用例是样式化标题行: ```csharp settings.HasSheetSetting(config => { config.StartRowIndex = 1; config.SheetName = "StyledSheet"; config.AutoColumnWidthEnabled = true; config.RowAction = row => { if (row.RowNum == 0) // 标题行 { // 创建单元格样式 var style = row.Sheet.Workbook.CreateCellStyle(); style.Alignment = HorizontalAlignment.Center; // 创建和配置字体 var font = row.Sheet.Workbook.CreateFont(); font.FontName = "Arial"; font.IsBold = true; font.FontHeight = 220; // 11pt (字体高度以 1/20 磅为单位) font.Color = IndexedColors.White.Index; style.SetFont(font); // 设置背景颜色 style.FillForegroundColor = IndexedColors.DarkBlue.Index; style.FillPattern = FillPattern.SolidForeground; // 将样式应用于行中的所有单元格 row.Cells.ForEach(c => c.CellStyle = style); } }; }); ``` ### 字体属性 可用的字体属性包括: - `FontName`: 字体族(例如:"宋体"、"微软雅黑"、"Arial") - `FontHeight`: 字体大小,以 1/20 磅为单位(例如:200 = 10pt, 220 = 11pt) - `IsBold`: 粗体文本 - `IsItalic`: 斜体文本 - `IsStrikeout`: 删除线文本 - `Underline`: 文本下划线样式 - `Color`: 使用 `IndexedColors` 设置字体颜色 ### 单元格样式属性 主要的单元格样式属性: - `Alignment`: 水平对齐(`Left`、`Center`、`Right`、`Justify`) - `VerticalAlignment`: 垂直对齐(`Top`、`Center`、`Bottom`) - `FillForegroundColor`: 背景颜色 - `FillPattern`: 填充模式(通常使用 `SolidForeground` 实现纯色) - `BorderTop`、`BorderBottom`、`BorderLeft`、`BorderRight`: 边框样式 - `WrapText`: 启用文本换行 ## 样式化单个单元格 使用 `CellAction` 根据单元格位置或内容自定义单个单元格: ```csharp settings.HasSheetSetting(config => { config.CellAction = cell => { // 样式化特定列 if (cell.ColumnIndex == 0) // 第一列 { var style = cell.Sheet.Workbook.CreateCellStyle(); var font = cell.Sheet.Workbook.CreateFont(); font.IsBold = true; style.SetFont(font); cell.CellStyle = style; } // 基于内容的条件样式 if (cell.RowIndex > 0 && cell.ColumnIndex == 3) // 数据行,第 4 列 { if (cell.NumericCellValue < 0) // 负数显示为红色 { var style = cell.Sheet.Workbook.CreateCellStyle(); var font = cell.Sheet.Workbook.CreateFont(); font.Color = IndexedColors.Red.Index; style.SetFont(font); cell.CellStyle = style; } } }; }); ``` ## 数据验证 为单元格添加下拉列表和其他验证规则: ```csharp settings.HasSheetSetting(config => { config.CellAction = cell => { // 为匹配"状态"的标题单元格添加下拉验证 if (cell.RowIndex == 0 && cell.StringCellValue == "状态") { var validationHelper = cell.Sheet.GetDataValidationHelper(); // 定义允许的值 var statusValues = new[] { "活跃", "停用", "待定" }; var constraint = validationHelper.CreateExplicitListConstraint(statusValues); // 将验证应用于数据行(第 1-100 行,当前列) var addressList = new CellRangeAddressList(1, 100, cell.ColumnIndex, cell.ColumnIndex); var validation = validationHelper.CreateValidation(constraint, addressList); validation.ShowErrorBox = true; validation.CreateErrorBox("状态无效", "请从下拉列表中选择"); validation.ShowPromptBox = true; validation.CreatePromptBox("状态选择", "从列表中选择一个状态"); cell.Sheet.AddValidationData(validation); } }; }); ``` ### 验证类型 可用的不同验证类型: ```csharp // 列表验证(下拉列表) var constraint = validationHelper.CreateExplicitListConstraint(new[] { "选项1", "选项2" }); // 整数验证 var intConstraint = validationHelper.CreateIntegerConstraint( OperatorType.Between, "1", "100"); // 小数验证 var decimalConstraint = validationHelper.CreateDecimalConstraint( OperatorType.GreaterThan, "0", null); // 日期验证 var dateConstraint = validationHelper.CreateDateConstraint( OperatorType.Between, "2024-01-01", "2024-12-31", "yyyy-MM-dd"); // 文本长度验证 var textConstraint = validationHelper.CreateTextLengthConstraint( OperatorType.LessThan, "100", null); ``` ## 完整示例 这是一个结合多种样式功能的综合示例: ```csharp public class StyledEntityProfile : IMappingProfile { public void Configure(IExcelConfiguration configuration) { configuration.HasAuthor("您的名字") .HasTitle("样式化报表") .HasDescription("专业样式的 Excel 报表"); configuration.HasSheetSetting(config => { config.SheetName = "报表"; config.StartRowIndex = 1; config.AutoColumnWidthEnabled = true; // 样式化标题行 config.RowAction = row => { if (row.RowNum == 0) { var headerStyle = row.Sheet.Workbook.CreateCellStyle(); headerStyle.Alignment = HorizontalAlignment.Center; headerStyle.VerticalAlignment = VerticalAlignment.Center; headerStyle.FillForegroundColor = IndexedColors.Grey25Percent.Index; headerStyle.FillPattern = FillPattern.SolidForeground; var headerFont = row.Sheet.Workbook.CreateFont(); headerFont.FontName = "微软雅黑"; headerFont.IsBold = true; headerFont.FontHeight = 240; // 12pt headerStyle.SetFont(headerFont); // 添加边框 headerStyle.BorderBottom = BorderStyle.Thin; headerStyle.BorderTop = BorderStyle.Thin; headerStyle.BorderLeft = BorderStyle.Thin; headerStyle.BorderRight = BorderStyle.Thin; row.Cells.ForEach(c => c.CellStyle = headerStyle); } }; // 添加验证和条件格式 config.CellAction = cell => { // 为状态列添加验证 if (cell.RowIndex == 0 && cell.StringCellValue == "状态") { var validationHelper = cell.Sheet.GetDataValidationHelper(); var statusList = new[] { "已批准", "待定", "已拒绝" }; var constraint = validationHelper.CreateExplicitListConstraint(statusList); var addressList = new CellRangeAddressList(1, 1000, cell.ColumnIndex, cell.ColumnIndex); var validation = validationHelper.CreateValidation(constraint, addressList); validation.ShowErrorBox = true; cell.Sheet.AddValidationData(validation); } // 用红色突出显示负数金额 if (cell.RowIndex > 0 && cell.ColumnIndex == 2) // 金额列 { try { if (cell.NumericCellValue < 0) { var redStyle = cell.Sheet.Workbook.CreateCellStyle(); var redFont = cell.Sheet.Workbook.CreateFont(); redFont.Color = IndexedColors.Red.Index; redFont.IsBold = true; redStyle.SetFont(redFont); cell.CellStyle = redStyle; } } catch { } // 如果不是数值单元格则跳过 } }; }); // 配置属性 configuration.Property(x => x.Id).HasColumnIndex(0); configuration.Property(x => x.Name).HasColumnIndex(1); configuration.Property(x => x.Amount).HasColumnIndex(2); configuration.Property(x => x.Status).HasColumnIndex(3); configuration.Property(x => x.Date) .HasColumnIndex(4) .HasColumnFormatter("yyyy-MM-dd"); } } ``` ## 最佳实践 1. **重用样式**:创建样式一次并重用,而不是为每个单元格创建新样式,以提高性能并减小文件大小。 2. **性能考虑**:在处理大型数据集时应用样式时要注意性能。考虑仅样式化标题行或特定列。 3. **颜色一致性**:使用 `IndexedColors` 以在不同的 Excel 版本之间保持一致的颜色。 4. **字体大小**:记住字体高度以 1/20 磅为单位(将磅值乘以 20)。 5. **验证范围**:为数据验证设置适当的范围以覆盖预期的数据行。 ## 参考 - [示例实现](https://github.com/WeihanLi/WeihanLi.Npoi/blob/dev/samples/DotNetCoreSample/Program.cs) - [NPOI 文档](http://poi.apache.org/) ================================================ FILE: docs/articles/zh/GetStarted.md ================================================ # `WeihanLi.Npoi` 基础示例 ## Intro `WeihanLi.Npoi` 是基于 NPOI 扩展的 Excel 导入导出库,并提供了很多实用的扩展方法,也支持 CSV 的导入导出, - 将 excel/csv 数据导入到 `DataTable` 或 `List` - `IEnumerable` 或 `DataTable` 导出到 Excel,可以导出成 excel 文件或字节数组或者一个流 - `IEnumerable` 或 `DataTable` 导出到 csv 文件或者 csv 字节数组 - 通过 `Attribute` 或者 `FluentAPI`(借鉴了 [FluentExcel](https://github.com/Arch/FluentExcel/) 项目) ## BasicSample ``` csharp internal class BaseModel { public int Id { get; set; } } internal class Notice : BaseModel { public string Title { get; set; } public string Content { get; set; } public DateTime PublishedAt { get; set; } public string Publisher { get; set; } } ``` 基本的导入导出: ``` csharp // entities excel import/export [Theory] [InlineData(ExcelFormat.Xls)] [InlineData(ExcelFormat.Xlsx)] public void BasicImportExportTest(ExcelFormat excelFormat) { var list = new List(); for (var i = 0; i < 10; i++) { list.Add(new Notice() { Id = i + 1, Content = $"content_{i}", Title = $"title_{i}", PublishedAt = DateTime.UtcNow.AddDays(-i), Publisher = $"publisher_{i}" }); } list.Add(new Notice() { Title = "nnnn" }); list.Add(null); var excelBytes = list.ToExcelBytes(excelFormat); var importedList = ExcelHelper.ToEntityList(excelBytes, excelFormat); Assert.Equal(list.Count, importedList.Count); for (var i = 0; i < list.Count; i++) { if (list[i] is null) { Assert.Null(importedList[i]); } else { Assert.Equal(list[i].Id, importedList[i].Id); Assert.Equal(list[i].Title, importedList[i].Title); Assert.Equal(list[i].Content, importedList[i].Content); Assert.Equal(list[i].Publisher, importedList[i].Publisher); Assert.Equal(list[i].PublishedAt.ToTimeString(), importedList[i].PublishedAt.ToTimeString()); } } } // DataTable Excel import/export [Theory] [InlineData(ExcelFormat.Xls)] [InlineData(ExcelFormat.Xlsx)] public void DataTableImportExportTest(ExcelFormat excelFormat) { var dt = new DataTable(); dt.Columns.AddRange(new[] { new DataColumn("Name"), new DataColumn("Age"), new DataColumn("Desc"), }); for (var i = 0; i < 10; i++) { var row = dt.NewRow(); row.ItemArray = new object[] { $"Test_{i}", i + 10, $"Desc_{i}" }; dt.Rows.Add(row); } // var excelBytes = dt.ToExcelBytes(excelFormat); var importedData = ExcelHelper.ToDataTable(excelBytes, excelFormat); Assert.NotNull(importedData); Assert.Equal(dt.Rows.Count, importedData.Rows.Count); for (var i = 0; i < dt.Rows.Count; i++) { Assert.Equal(dt.Rows[i].ItemArray.Length, importedData.Rows[i].ItemArray.Length); for (var j = 0; j < dt.Rows[i].ItemArray.Length; j++) { Assert.Equal(dt.Rows[i].ItemArray[j], importedData.Rows[i].ItemArray[j]); } } } // entities csv import/export [Fact] public void BasicImportExportTest() { var list = new List(); for (var i = 0; i < 10; i++) { list.Add(new Notice() { Id = i + 1, Content = $"content_{i}", Title = $"title_{i}", PublishedAt = DateTime.UtcNow.AddDays(-i), Publisher = $"publisher_{i}" }); } list.Add(new Notice() { Id = 11, Content = $"content", Title = $"title", PublishedAt = DateTime.UtcNow.AddDays(1), }); var csvBytes = list.ToCsvBytes(); var importedList = CsvHelper.ToEntityList(csvBytes); Assert.Equal(list.Count, importedList.Count); for (var i = 0; i < list.Count; i++) { Assert.Equal(list[i].Id, importedList[i].Id); Assert.Equal(list[i].Title ?? "", importedList[i].Title); Assert.Equal(list[i].Content ?? "", importedList[i].Content); Assert.Equal(list[i].Publisher ?? "", importedList[i].Publisher); Assert.Equal(list[i].PublishedAt.ToTimeString(), importedList[i].PublishedAt.ToTimeString()); } } // DataTable csv import/export [Fact] public void DataTableImportExportTest() { var dt = new DataTable(); dt.Columns.AddRange(new[] { new DataColumn("Name"), new DataColumn("Age"), new DataColumn("Desc"), }); for (var i = 0; i < 10; i++) { var row = dt.NewRow(); row.ItemArray = new object[] { $"Test_{i}", i + 10, $"Desc_{i}" }; dt.Rows.Add(row); } // var csvBytes = dt.ToCsvBytes(); var importedData = CsvHelper.ToDataTable(csvBytes); Assert.NotNull(importedData); Assert.Equal(dt.Rows.Count, importedData.Rows.Count); for (var i = 0; i < dt.Rows.Count; i++) { Assert.Equal(dt.Rows[i].ItemArray.Length, importedData.Rows[i].ItemArray.Length); for (var j = 0; j < dt.Rows[i].ItemArray.Length; j++) { Assert.Equal(dt.Rows[i].ItemArray[j], importedData.Rows[i].ItemArray[j]); } } } ``` ## 自定义映射关系,配置 使用 Attribute 配置: ``` csharp internal class Model { [Column("酒店编号", Index = 0)] public string HotelId { get; set; } [Column("订单号", Index = 1)] public string OrderNo { get; set; } [Column("酒店名称", Index = 2)] public string HotelName { get; set; } [Column("客户名称", Index = 3)] public string CustomerName { get; set; } [Column(nameof(房型名称), Index = 4)] public string 房型名称 { get; set; } [Column(nameof(入住日期), Index = 5, Formatter = "yyyy/M/d")] public DateTime 入住日期 { get; set; } [Column(nameof(离店日期), Index = 6, Formatter = "yyyy/M/d")] public DateTime 离店日期 { get; set; } [Column(nameof(间夜), Index = 7)] public int 间夜 { get; set; } [Column(nameof(支付类型), Index = 8)] public string 支付类型 { get; set; } [Column(nameof(订单金额), Index = 9)] public decimal 订单金额 { get; set; } [Column(nameof(佣金率), Index = 10)] public decimal 佣金率 { get; set; } [Column(nameof(服务费), Index = 11)] public decimal 服务费 { get; set; } } [Sheet(SheetIndex = 0, SheetName = "TestSheet", AutoColumnWidthEnabled = true)] internal class TestEntity2 { [Column(Index = 0)] public int Id { get; set; } [Column(Index = 1)] public string Title { get; set; } [Column(Index = 2, Width = 50)] public string Description { get; set; } [Column(Index = 3, Width = 20)] public string Extra { get; set; } = "{}"; } ``` 使用 FluentAPI 配置(推荐,更灵活) ``` csharp var setting = FluentSettings.For(); // ExcelSetting setting.HasAuthor("WeihanLi") .HasTitle("WeihanLi.Npoi test") .HasDescription("WeihanLi.Npoi test") .HasSubject("WeihanLi.Npoi test"); setting.HasSheetConfiguration(0, "SystemSettingsList", 1, true); // sheet 配置 // setting // .HasFilter(0, 1) //在列上设置筛选 // .HasFreezePane(0, 1, 2, 1); // 设置冻结区域 setting.Property(_ => _.SettingId) .HasColumnIndex(0); setting.Property(_ => _.SettingName) .HasColumnTitle("SettingName") .HasColumnIndex(1); setting.Property(_ => _.DisplayName) .HasOutputFormatter((entity, displayName) => $"AAA_{entity.SettingName}_{displayName}") .HasInputFormatter((entity, originVal) => originVal.Split(new[] { '_' })[2]) .HasColumnTitle("DisplayName") .HasColumnIndex(2); setting.Property(_ => _.SettingValue) .HasColumnTitle("SettingValue") .HasColumnIndex(3); setting.Property(_ => _.CreatedTime) .HasColumnTitle("CreatedTime") .HasColumnIndex(4) .HasColumnWidth(10) // 设置列宽 .HasColumnFormatter("yyyy-MM-dd HH:mm:ss"); setting.Property(_ => _.CreatedBy) .HasColumnInputFormatter(x => x += "_test") .HasColumnIndex(4) .HasColumnTitle("CreatedBy"); setting.Property(x => x.Enabled) .HasColumnInputFormatter(val => "启用".Equals(val)) .HasColumnOutputFormatter(v => v ? "启用" : "禁用"); setting.Property("ShadowProperty") .HasOutputFormatter((entity, val) => $"HiddenProp_{entity.PKID}"); setting.Property(_ => _.PKID).Ignored(); // ignore column ``` ================================================ FILE: docs/articles/zh/InputOutputFormatter.md ================================================ # InputOutputFormatter 介绍 ## Intro WeihanLi.Npoi 引入了 `OutputFormatter`/`InputFormatter`/`ColumnInputFormatter`/`ColumnOutputFormatter`,极大程度上增强了导入导出的灵活性,只支持通过 FluentAPI 配置,来看下面的示例 ## InputFormatter/OutputFormatter 示例 Model: ``` csharp internal abstract class BaseEntity { public int PKID { get; set; } } internal class TestEntity : BaseEntity { public Guid SettingId { get; set; } public string SettingName { get; set; } public string DisplayName { get; set; } public string SettingValue { get; set; } public string CreatedBy { get; set; } = "liweihan"; public DateTime CreatedTime { get; set; } = DateTime.Now; public string UpdatedBy { get; set; } public DateTime UpdatedTime { get; set; } public bool Enabled { get; set; } } ``` 示例配置: ``` csharp var setting = FluentSettings.For(); // ExcelSetting setting.HasAuthor("WeihanLi") .HasTitle("WeihanLi.Npoi test") .HasDescription("WeihanLi.Npoi test") .HasSubject("WeihanLi.Npoi test"); setting.HasSheetConfiguration(0, "SystemSettingsList", 1, true); setting.Property(_ => _.SettingId) .HasColumnIndex(0); setting.Property(_ => _.SettingName) .HasColumnTitle("SettingName") .HasColumnIndex(1); setting.Property(_ => _.DisplayName) .HasOutputFormatter((entity, displayName) => $"AAA_{entity.SettingName}_{displayName}") .HasInputFormatter((entity, originVal) => originVal.Split(new[] { '_' })[2]) .HasColumnTitle("DisplayName") .HasColumnIndex(2); setting.Property(_ => _.SettingValue) .HasColumnTitle("SettingValue") .HasColumnIndex(3); setting.Property(_ => _.CreatedTime) .HasColumnTitle("CreatedTime") .HasColumnIndex(4) .HasColumnWidth(10) .HasColumnFormatter("yyyy-MM-dd HH:mm:ss"); setting.Property(_ => _.CreatedBy) .HasColumnInputFormatter(x => x += "_test") .HasColumnIndex(4) .HasColumnTitle("CreatedBy"); setting.Property(x => x.Enabled) .HasColumnInputFormatter(val => "启用".Equals(val)) .HasColumnOutputFormatter(v => v ? "启用" : "禁用"); setting.Property("HiddenProp") .HasOutputFormatter((entity, val) => $"HiddenProp_{entity.PKID}"); setting.Property(_ => _.PKID).Ignored(); setting.Property(_ => _.UpdatedBy).Ignored(); setting.Property(_ => _.UpdatedTime).Ignored(); ``` 测试代码: ``` csharp var entities = new List() { new TestEntity() { PKID = 1, SettingId = Guid.NewGuid(), SettingName = "Setting1", SettingValue = "Value1", DisplayName = "ddd1" }, new TestEntity() { PKID=2, SettingId = Guid.NewGuid(), SettingName = "Setting2", SettingValue = "Value2", Enabled = true }, }; var path = $@"{tempDirPath}\test.xlsx"; entities.ToExcelFile(path); var entitiesT0 = ExcelHelper.ToEntityList(path); ``` 导出结果: ![](../images/489462-20200104112133779-1180097402.png) 导入结果: ![](../images/489462-20200104112017420-1450911242.png) ![](../images/489462-20200104112025927-873408781.png) ================================================ FILE: docs/articles/zh/MultiSheets.md ================================================ # 多 sheet 导出 ## Intro 有时我们可能会希望在一个 excel 里导出多个 sheet 导出多个集合的数据,可以参考下面的示例代码: ## Sample ```c# var collection1 = new[] { new TestEntity1() { Id = 1, Title = "test1" }, new TestEntity1() { Id = 2, Title = "test2" } }; var collection2 = new[] { new TestEntity2() { Id = 1, Title = "test1", Description = "description"}, new TestEntity2() { Id = 2, Title = "test2" } }; // 准备一个 workbook var workbook = ExcelHelper.PrepareWorkbook(ExcelFormat.Xlsx); // 导入 collection1 到第一个 sheet workbook.ImportData(collection1); // 导入 collection2 到第二个 sheet workbook.ImportData(collection2, 1); // 导出 workbook 到本地文件 workbook.WriteToFile("multi-sheets.xlsx"); ``` 如果需要自定义一些配置还是和之前是一样的,可以使用 attribute 的方式也可以使用 fluent API 的方式 ```c# [Sheet(SheetName = "TestSheet", SheetIndex = 0)] file sealed class TestEntity1 { [Column("ID", Index = 0)] public int Id { get; set; } public string Title { get; set; } = string.Empty; } file sealed class TestEntity2 { public int Id { get; set; } public string Title { get; set; } = string.Empty; public string Description { get; set; } } ``` Fluent API 配置如下: ```c# var settings = FluentSettings.For(); settings.HasSheetSetting(sheet => sheet.SheetName = "TestEntity2", 1); settings.Property(x => x.Id) .HasColumnIndex(0) .HasColumnOutputFormatter(v => v.ToString("#0000")) ; settings.Property(x => x.Title) .HasColumnIndex(1) ; settings.Property(x => x.Description) .HasColumnIndex(2) ; ``` 导出结果如下: ![sheet0](../images/image-20241029231320957.png) ![sheet1](../images/image-20241029231519274.png) ## References - - ================================================ FILE: docs/articles/zh/ShadowProperty.md ================================================ # WeihanLi.Npoi 支持 `ShadowProperty` 了 ## Intro 在 EF 里有个 `ShadowProperty` (阴影属性/影子属性)的概念,你可以通过 FluentAPI 的方式来定义一个不在 .NET model 里定义的属性,只能通过 EF 里的 `Change Tracker` 来操作这种属性。 在导出 Excel 的时候,可能希望导出的列并不是都定义好在我们的 model 中的,有的可能只是想增加一列导出某个属性中的嵌套属性之中的某一个属性值,或者我就是单纯的想多定义一列,而这个时候可能 model 是别的地方写死的,不方便改。 于是 `WeihanLi.Npoi` 从 1.6.0 版本开始支持 `ShadowProperty` ,将 EF 里的 `ShadowProperty` 引入到 excel 导出里,目前来说 `ShadowProperty` 是不可写的,读取的话也只是返回一个类型的默认值,不支持 `ChangeTracker`,不支持改。 ## 使用示例 来看一个简单使用示例:(示例来源于网友提出的这个issue: ) ``` csharp using System; using System.Collections.Generic; using System.IO; using WeihanLi.Npoi; namespace NpoiTest { public class Program { public static void Main(string[] args) { var settings = FluentSettings.For(); settings.Property(x => x.Name) .HasColumnIndex(0); // settings.Property(x => x.UserFields) // .HasOutputFormatter((entity, value) => $"{value[0].Value},{value[2].Value}") // .HasColumnTitle("姓名,工号") // .HasColumnIndex(1); settings.Property(x=>x.UserFields).Ignored(); settings.Property("工号") .HasOutputFormatter((entity,val)=> $"{entity.UserFields[2].Value}") ; settings.Property("部门") .HasOutputFormatter((entity,val)=> $"{entity.UserFields[1].Value}") ; var data = new List() { new TestEntity() { Name = "xiaoming", TotalScore = 100, UserFields = new UserField[] { new UserField() { Name = "姓名", Value = "xaioming", }, new UserField() { Name = "部门", Value = "1212" }, new UserField() { Name = "工号", Value = "121213131" }, } } }; data.ToExcelFile($@"{Directory.GetCurrentDirectory()}\output.xls"); Console.WriteLine("complete."); } private class TestEntity { public string Name { get; set; } public UserField[] UserFields { get; set; } public int TotalScore { get; set; } } private class UserField { public string Fid { get; set; } public string Name { get; set; } public string Value { get; set; } } } } ``` 导出效果如下: ![](../images/489462-20191213084226066-1767559517.png) 可以看到,我们为导出的 Excel 增加在原本的 Model 里没有定义的两列,借助于此,我们可以更灵活的定制要导出的内容 ================================================ FILE: docs/articles/zh/TemplateExport.md ================================================ # 根据模板导出Excel ## Intro 原来的导出方式比较适用于比较简单的导出,每一条数据在一行,数据列虽然自定义程度比较高,如果要一条数据对应多行就做不到了,于是就想支持根据模板导出,在 1.8.0 版本中引入了根据模板导出的功能 ## 使用示例 ### 示例模板 ![](../images/489462-20200128142956273-1478084552.png) 模板规划的可以有三种数据: - Global:一个是导出的时候可以指定一些参数,作为 Global 参数,默认参数格式使用: `$(Global:PropName)` 的格式 - Header:配置的对应属性的显示名称,默认是属性名称,默认参数格式:`$(Header:PropName)` - Data:对应数据的属性值,默认参数格式:`$(Data:PropName)` 默认模板参数格式(从 1.8.2 版本开始支持通过 `TemplateHelper.ConfigureTemplateOptions` 方法来自定义): - Global 参数:`$(Global:{0})` - Header 参数:`$(Header:{0})` - Data 参数:`$(Data:{0})` - Data Begin: `` - Data End: `` 模板规范: 模板需要通过 Data Begin 和 Data End 来配置数据模板的开始和结束以识别每一个数据对应的开始行和结束行 ### 示例代码 示例配置 ``` csharp var setting = FluentSettings.For(); // ExcelSetting setting.HasAuthor("WeihanLi") .HasTitle("WeihanLi.Npoi test") .HasDescription("WeihanLi.Npoi test") .HasSubject("WeihanLi.Npoi test"); setting.HasSheetConfiguration(0, "SystemSettingsList", 1, true); setting.Property(_ => _.SettingId) .HasColumnIndex(0); setting.Property(_ => _.SettingName) .HasColumnTitle("SettingName") .HasColumnIndex(1); setting.Property(_ => _.DisplayName) .HasOutputFormatter((entity, displayName) => $"AAA_{entity.SettingName}_{displayName}") .HasInputFormatter((entity, originVal) => originVal.Split(new[] { '_' })[2]) .HasColumnTitle("DisplayName") .HasColumnIndex(2); setting.Property(_ => _.SettingValue) .HasColumnTitle("SettingValue") .HasColumnIndex(3); setting.Property(x => x.Enabled) .HasColumnInputFormatter(val => "启用".Equals(val)) .HasColumnOutputFormatter(v => v ? "启用" : "禁用"); setting.Property("HiddenProp") .HasOutputFormatter((entity, val) => $"HiddenProp_{entity.PKID}"); setting.Property(_ => _.PKID).Ignored(); setting.Property(_ => _.UpdatedBy).Ignored(); setting.Property(_ => _.UpdatedTime).Ignored(); ``` 根据模板导出示例代码: ``` csharp var entities = new List() { new TestEntity() { PKID = 1, SettingId = Guid.NewGuid(), SettingName = "Setting1", SettingValue = "Value1", DisplayName = "ddd1" }, new TestEntity() { PKID=2, SettingId = Guid.NewGuid(), SettingName = "Setting2", SettingValue = "Value2", Enabled = true }, }; var csvFilePath = $@"{tempDirPath}\test.csv"; entities.ToExcelFileByTemplate( Path.Combine(ApplicationHelper.AppRoot, "Templates", "testTemplate.xlsx"), ApplicationHelper.MapPath("templateTestEntities.xlsx"), extraData: new { Author = "WeihanLi", Title = "导出结果" } ); ``` ### 导出结果 ![](../images/489462-20200128143038865-1452547986.png) ## More 为了方便使用,增加了一些方便的扩展方法: ``` csharp public static int ToExcelFileByTemplate([NotNull]this IEnumerable entities, string templatePath, string excelPath, int sheetIndex = 0, object extraData = null); public static int ToExcelFileByTemplate([NotNull]this IEnumerable entities, byte[] templateBytes, string excelPath, ExcelFormat excelFormat = ExcelFormat.Xls, int sheetIndex = 0, object extraData = null); public static int ToExcelFileByTemplate([NotNull]this IEnumerable entities, IWorkbook templateWorkbook, string excelPath, int sheetIndex = 0, object extraData = null); public static byte[] ToExcelBytesByTemplate([NotNull]this IEnumerable entities, string templatePath, int sheetIndex = 0, object extraData = null); public static byte[] ToExcelBytesByTemplate([NotNull]this IEnumerable entities, byte[] templateBytes, ExcelFormat excelFormat = ExcelFormat.Xls, int sheetIndex = 0, object extraData = null); public static byte[] ToExcelBytesByTemplate([NotNull]this IEnumerable entities, Stream templateStream, ExcelFormat excelFormat = ExcelFormat.Xls, int sheetIndex = 0, object extraData = null); public static byte[] ToExcelBytesByTemplate([NotNull]this IEnumerable entities, IWorkbook templateWorkbook, int sheetIndex = 0, object extraData = null); public static byte[] ToExcelBytesByTemplate([NotNull]this IEnumerable entities, ISheet templateSheet, object extraData = null); ``` ## Reference - - ================================================ FILE: docs/docfx.json ================================================ { "metadata": [ { "src": [ { "src": "../src", "files": [ "**/*.csproj" ] } ], "dest": "api" } ], "build": { "content": [ { "files": [ "**/*.{md,yml}" ], "exclude": [ "_site/**" ] } ], "resource": [ { "files": [ "**/images/**" ] } ], "output": "_site", "template": [ "default", "modern" ], "globalMetadata": { "_appName": "WeihanLi.Npoi", "_appTitle": "WeihanLi.Npoi", "_enableSearch": true, "pdf": true } } } ================================================ FILE: docs/toc.yml ================================================ - name: Home href: index.md - name: API Documentation href: api/ - name: Release Notes href: ReleaseNotes.md - name: Articles href: articles/ homepage: articles/intro.md - name: Github href: https://github.com/WeihanLi/WeihanLi.Npoi ================================================ FILE: global.json ================================================ { "sdk": { "rollForward": "major", "version": "10.0.100" }, "test": { "runner": "Microsoft.Testing.Platform" } } ================================================ FILE: nuget.config ================================================ ================================================ FILE: perf/WeihanLi.Npoi.Benchmark/BenchmarkDotNet.Artifacts/results/WeihanLi.Npoi.Benchmark.ExportExcelTest-report-github.md ================================================ ``` ini BenchmarkDotNet=v0.11.5, OS=Windows 10.0.18362 Intel Core i5-3470 CPU 3.20GHz (Ivy Bridge), 1 CPU, 4 logical and 4 physical cores .NET Core SDK=3.0.100 [Host] : .NET Core 2.2.6 (CoreCLR 4.6.27817.03, CoreFX 4.6.27818.02), 64bit RyuJIT Job-WDPKYY : .NET Core 2.2.6 (CoreCLR 4.6.27817.03, CoreFX 4.6.27818.02), 64bit RyuJIT IterationCount=5 LaunchCount=1 WarmupCount=1 ``` | Method | RowsCount | Mean | Error | StdDev | Min | Max | Median | Gen 0 | Gen 1 | Gen 2 | Allocated | |-------------------------------- |---------- |------------:|------------:|-----------:|------------:|------------:|------------:|------------:|-----------:|----------:|----------:| | **ExportToCsvBytesTest** | **10000** | **22.77 ms** | **0.5667 ms** | **0.1472 ms** | **22.54 ms** | **22.95 ms** | **22.77 ms** | **2281.2500** | **937.5000** | **281.2500** | **12.12 MB** | | NpoiExportToXlsBytesTest | 10000 | 219.87 ms | 3.8409 ms | 0.9975 ms | 218.92 ms | 221.40 ms | 219.60 ms | 6000.0000 | 2000.0000 | 1000.0000 | 43.99 MB | | NpoiExportToXlsxBytesTest | 10000 | 470.79 ms | 9.3139 ms | 2.4188 ms | 467.01 ms | 473.70 ms | 471.26 ms | 20000.0000 | 6000.0000 | 2000.0000 | 104.31 MB | | EpplusExportToBytesTest | 10000 | 203.82 ms | 5.0193 ms | 1.3035 ms | 203.13 ms | 206.15 ms | 203.25 ms | 11000.0000 | 4000.0000 | 1000.0000 | 56.66 MB | | StructExportToCsvBytesTest | 10000 | 21.46 ms | 0.7542 ms | 0.1959 ms | 21.30 ms | 21.73 ms | 21.34 ms | 2281.2500 | 937.5000 | 281.2500 | 12.12 MB | | NpoiStructExportToXlsBytesTest | 10000 | 212.54 ms | 12.7005 ms | 3.2983 ms | 209.21 ms | 216.65 ms | 211.42 ms | 7000.0000 | 3000.0000 | 1000.0000 | 44.37 MB | | NpoiStructExportToXlsxBytesTest | 10000 | 495.55 ms | 45.1010 ms | 11.7126 ms | 482.73 ms | 514.33 ms | 492.62 ms | 20000.0000 | 7000.0000 | 2000.0000 | 104.69 MB | | EpplusStructExportToBytesTest | 10000 | 208.60 ms | 5.3520 ms | 1.3899 ms | 206.94 ms | 210.32 ms | 208.32 ms | 11000.0000 | 4000.0000 | 1000.0000 | 56.66 MB | | **ExportToCsvBytesTest** | **30000** | **72.16 ms** | **3.2924 ms** | **0.8550 ms** | **71.34 ms** | **73.59 ms** | **71.86 ms** | **6000.0000** | **1571.4286** | **571.4286** | **36.47 MB** | | NpoiExportToXlsBytesTest | 30000 | 839.63 ms | 22.8596 ms | 5.9366 ms | 835.13 ms | 849.85 ms | 837.76 ms | 20000.0000 | 8000.0000 | 2000.0000 | 124.18 MB | | NpoiExportToXlsxBytesTest | 30000 | 1,478.00 ms | 48.6880 ms | 12.6441 ms | 1,457.50 ms | 1,489.99 ms | 1,480.52 ms | 59000.0000 | 16000.0000 | 4000.0000 | 315.85 MB | | EpplusExportToBytesTest | 30000 | 624.67 ms | 40.5093 ms | 10.5201 ms | 616.60 ms | 642.67 ms | 621.37 ms | 31000.0000 | 10000.0000 | 3000.0000 | 172.73 MB | | StructExportToCsvBytesTest | 30000 | 63.51 ms | 2.5019 ms | 0.6497 ms | 62.53 ms | 64.25 ms | 63.58 ms | 5875.0000 | 1375.0000 | 375.0000 | 36.47 MB | | NpoiStructExportToXlsBytesTest | 30000 | 853.48 ms | 33.3918 ms | 8.6718 ms | 839.53 ms | 862.13 ms | 856.08 ms | 20000.0000 | 8000.0000 | 2000.0000 | 125.32 MB | | NpoiStructExportToXlsxBytesTest | 30000 | 1,516.05 ms | 141.2409 ms | 36.6798 ms | 1,474.05 ms | 1,553.23 ms | 1,534.14 ms | 59000.0000 | 16000.0000 | 4000.0000 | 317 MB | | EpplusStructExportToBytesTest | 30000 | 624.10 ms | 18.2990 ms | 4.7522 ms | 616.20 ms | 627.98 ms | 624.45 ms | 31000.0000 | 10000.0000 | 3000.0000 | 172.72 MB | | **ExportToCsvBytesTest** | **50000** | **113.06 ms** | **1.6585 ms** | **0.4307 ms** | **112.55 ms** | **113.55 ms** | **113.01 ms** | **10000.0000** | **2000.0000** | **800.0000** | **60.81 MB** | | NpoiExportToXlsBytesTest | 50000 | 1,666.19 ms | 43.1443 ms | 11.2044 ms | 1,651.36 ms | 1,677.04 ms | 1,669.51 ms | 33000.0000 | 12000.0000 | 3000.0000 | 212.25 MB | | NpoiExportToXlsxBytesTest | 50000 | 2,562.64 ms | 130.8702 ms | 33.9866 ms | 2,516.62 ms | 2,595.77 ms | 2,573.09 ms | 96000.0000 | 24000.0000 | 4000.0000 | 532.54 MB | | EpplusExportToBytesTest | 50000 | 1,059.02 ms | 76.3548 ms | 19.8291 ms | 1,041.61 ms | 1,093.02 ms | 1,052.11 ms | 51000.0000 | 13000.0000 | 2000.0000 | 270.94 MB | | StructExportToCsvBytesTest | 50000 | 108.91 ms | 4.0316 ms | 1.0470 ms | 107.28 ms | 110.19 ms | 108.95 ms | 10000.0000 | 2000.0000 | 800.0000 | 60.81 MB | | NpoiStructExportToXlsBytesTest | 50000 | 1,675.32 ms | 63.9457 ms | 16.6065 ms | 1,660.80 ms | 1,703.80 ms | 1,669.06 ms | 33000.0000 | 12000.0000 | 3000.0000 | 214.15 MB | | NpoiStructExportToXlsxBytesTest | 50000 | 2,505.01 ms | 231.4485 ms | 60.1064 ms | 2,443.93 ms | 2,576.48 ms | 2,494.21 ms | 96000.0000 | 24000.0000 | 4000.0000 | 534.45 MB | | EpplusStructExportToBytesTest | 50000 | 1,031.01 ms | 17.5706 ms | 4.5630 ms | 1,027.89 ms | 1,038.94 ms | 1,029.03 ms | 51000.0000 | 13000.0000 | 2000.0000 | 270.93 MB | | **ExportToCsvBytesTest** | **65535** | **147.35 ms** | **1.3296 ms** | **0.3453 ms** | **147.03 ms** | **147.83 ms** | **147.16 ms** | **12750.0000** | **2000.0000** | **500.0000** | **79.73 MB** | | NpoiExportToXlsBytesTest | 65535 | 2,456.24 ms | 21.4291 ms | 5.5651 ms | 2,448.12 ms | 2,462.39 ms | 2,455.63 ms | 43000.0000 | 15000.0000 | 3000.0000 | 277.42 MB | | NpoiExportToXlsxBytesTest | 65535 | 3,292.05 ms | 120.2493 ms | 31.2284 ms | 3,257.61 ms | 3,327.38 ms | 3,280.70 ms | 127000.0000 | 30000.0000 | 5000.0000 | 685.91 MB | | EpplusExportToBytesTest | 65535 | 1,365.16 ms | 27.3326 ms | 7.0982 ms | 1,355.48 ms | 1,374.40 ms | 1,363.77 ms | 68000.0000 | 17000.0000 | 3000.0000 | 343.35 MB | | StructExportToCsvBytesTest | 65535 | 141.21 ms | 6.6211 ms | 1.7195 ms | 139.06 ms | 143.00 ms | 141.42 ms | 12750.0000 | 2000.0000 | 500.0000 | 79.73 MB | | NpoiStructExportToXlsBytesTest | 65535 | 2,505.47 ms | 83.5632 ms | 21.7011 ms | 2,483.53 ms | 2,536.38 ms | 2,504.97 ms | 43000.0000 | 15000.0000 | 3000.0000 | 279.92 MB | | NpoiStructExportToXlsxBytesTest | 65535 | 3,235.57 ms | 116.3763 ms | 30.2226 ms | 3,205.37 ms | 3,284.36 ms | 3,230.79 ms | 127000.0000 | 30000.0000 | 5000.0000 | 688.39 MB | | EpplusStructExportToBytesTest | 65535 | 1,349.52 ms | 8.8304 ms | 2.2932 ms | 1,346.47 ms | 1,352.90 ms | 1,349.45 ms | 67000.0000 | 16000.0000 | 3000.0000 | 343.35 MB | ================================================ FILE: perf/WeihanLi.Npoi.Benchmark/BenchmarkDotNet.Artifacts/results/WeihanLi.Npoi.Benchmark.ExportExcelTest-report.csv ================================================ Method,Job,AnalyzeLaunchVariance,EvaluateOverhead,MaxAbsoluteError,MaxRelativeError,MinInvokeCount,MinIterationTime,OutlierMode,Affinity,EnvironmentVariables,Jit,Platform,Runtime,AllowVeryLargeObjects,Concurrent,CpuGroups,Force,HeapAffinitizeMask,HeapCount,NoAffinitize,RetainVm,Server,PowerPlan,Arguments,BuildConfiguration,Clock,EngineFactory,NuGetReferences,Toolchain,IsMutator,InvocationCount,IterationCount,IterationTime,LaunchCount,MaxIterationCount,MaxWarmupIterationCount,MinIterationCount,MinWarmupIterationCount,RunStrategy,UnrollFactor,WarmupCount,RowsCount,Mean,Error,StdDev,Min,Max,Median,Gen 0,Gen 1,Gen 2,Allocated ExportToCsvBytesTest,Default,False,Default,Default,Default,Default,Default,Default,1111,Empty,RyuJit,X64,Core,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,1,5,Default,1,Default,Default,Default,Default,Default,16,1,10000,22.77 ms,0.5667 ms,0.1472 ms,22.54 ms,22.95 ms,22.77 ms,2281.2500,937.5000,281.2500,12.12 MB NpoiExportToXlsBytesTest,Default,False,Default,Default,Default,Default,Default,Default,1111,Empty,RyuJit,X64,Core,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,1,5,Default,1,Default,Default,Default,Default,Default,16,1,10000,219.87 ms,3.8409 ms,0.9975 ms,218.92 ms,221.40 ms,219.60 ms,6000.0000,2000.0000,1000.0000,43.99 MB NpoiExportToXlsxBytesTest,Default,False,Default,Default,Default,Default,Default,Default,1111,Empty,RyuJit,X64,Core,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,1,5,Default,1,Default,Default,Default,Default,Default,16,1,10000,470.79 ms,9.3139 ms,2.4188 ms,467.01 ms,473.70 ms,471.26 ms,20000.0000,6000.0000,2000.0000,104.31 MB EpplusExportToBytesTest,Default,False,Default,Default,Default,Default,Default,Default,1111,Empty,RyuJit,X64,Core,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,1,5,Default,1,Default,Default,Default,Default,Default,16,1,10000,203.82 ms,5.0193 ms,1.3035 ms,203.13 ms,206.15 ms,203.25 ms,11000.0000,4000.0000,1000.0000,56.66 MB StructExportToCsvBytesTest,Default,False,Default,Default,Default,Default,Default,Default,1111,Empty,RyuJit,X64,Core,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,1,5,Default,1,Default,Default,Default,Default,Default,16,1,10000,21.46 ms,0.7542 ms,0.1959 ms,21.30 ms,21.73 ms,21.34 ms,2281.2500,937.5000,281.2500,12.12 MB NpoiStructExportToXlsBytesTest,Default,False,Default,Default,Default,Default,Default,Default,1111,Empty,RyuJit,X64,Core,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,1,5,Default,1,Default,Default,Default,Default,Default,16,1,10000,212.54 ms,12.7005 ms,3.2983 ms,209.21 ms,216.65 ms,211.42 ms,7000.0000,3000.0000,1000.0000,44.37 MB NpoiStructExportToXlsxBytesTest,Default,False,Default,Default,Default,Default,Default,Default,1111,Empty,RyuJit,X64,Core,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,1,5,Default,1,Default,Default,Default,Default,Default,16,1,10000,495.55 ms,45.1010 ms,11.7126 ms,482.73 ms,514.33 ms,492.62 ms,20000.0000,7000.0000,2000.0000,104.69 MB EpplusStructExportToBytesTest,Default,False,Default,Default,Default,Default,Default,Default,1111,Empty,RyuJit,X64,Core,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,1,5,Default,1,Default,Default,Default,Default,Default,16,1,10000,208.60 ms,5.3520 ms,1.3899 ms,206.94 ms,210.32 ms,208.32 ms,11000.0000,4000.0000,1000.0000,56.66 MB ExportToCsvBytesTest,Default,False,Default,Default,Default,Default,Default,Default,1111,Empty,RyuJit,X64,Core,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,1,5,Default,1,Default,Default,Default,Default,Default,16,1,30000,72.16 ms,3.2924 ms,0.8550 ms,71.34 ms,73.59 ms,71.86 ms,6000.0000,1571.4286,571.4286,36.47 MB NpoiExportToXlsBytesTest,Default,False,Default,Default,Default,Default,Default,Default,1111,Empty,RyuJit,X64,Core,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,1,5,Default,1,Default,Default,Default,Default,Default,16,1,30000,839.63 ms,22.8596 ms,5.9366 ms,835.13 ms,849.85 ms,837.76 ms,20000.0000,8000.0000,2000.0000,124.18 MB NpoiExportToXlsxBytesTest,Default,False,Default,Default,Default,Default,Default,Default,1111,Empty,RyuJit,X64,Core,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,1,5,Default,1,Default,Default,Default,Default,Default,16,1,30000,"1,478.00 ms",48.6880 ms,12.6441 ms,"1,457.50 ms","1,489.99 ms","1,480.52 ms",59000.0000,16000.0000,4000.0000,315.85 MB EpplusExportToBytesTest,Default,False,Default,Default,Default,Default,Default,Default,1111,Empty,RyuJit,X64,Core,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,1,5,Default,1,Default,Default,Default,Default,Default,16,1,30000,624.67 ms,40.5093 ms,10.5201 ms,616.60 ms,642.67 ms,621.37 ms,31000.0000,10000.0000,3000.0000,172.73 MB StructExportToCsvBytesTest,Default,False,Default,Default,Default,Default,Default,Default,1111,Empty,RyuJit,X64,Core,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,1,5,Default,1,Default,Default,Default,Default,Default,16,1,30000,63.51 ms,2.5019 ms,0.6497 ms,62.53 ms,64.25 ms,63.58 ms,5875.0000,1375.0000,375.0000,36.47 MB NpoiStructExportToXlsBytesTest,Default,False,Default,Default,Default,Default,Default,Default,1111,Empty,RyuJit,X64,Core,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,1,5,Default,1,Default,Default,Default,Default,Default,16,1,30000,853.48 ms,33.3918 ms,8.6718 ms,839.53 ms,862.13 ms,856.08 ms,20000.0000,8000.0000,2000.0000,125.32 MB NpoiStructExportToXlsxBytesTest,Default,False,Default,Default,Default,Default,Default,Default,1111,Empty,RyuJit,X64,Core,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,1,5,Default,1,Default,Default,Default,Default,Default,16,1,30000,"1,516.05 ms",141.2409 ms,36.6798 ms,"1,474.05 ms","1,553.23 ms","1,534.14 ms",59000.0000,16000.0000,4000.0000,317 MB EpplusStructExportToBytesTest,Default,False,Default,Default,Default,Default,Default,Default,1111,Empty,RyuJit,X64,Core,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,1,5,Default,1,Default,Default,Default,Default,Default,16,1,30000,624.10 ms,18.2990 ms,4.7522 ms,616.20 ms,627.98 ms,624.45 ms,31000.0000,10000.0000,3000.0000,172.72 MB ExportToCsvBytesTest,Default,False,Default,Default,Default,Default,Default,Default,1111,Empty,RyuJit,X64,Core,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,1,5,Default,1,Default,Default,Default,Default,Default,16,1,50000,113.06 ms,1.6585 ms,0.4307 ms,112.55 ms,113.55 ms,113.01 ms,10000.0000,2000.0000,800.0000,60.81 MB NpoiExportToXlsBytesTest,Default,False,Default,Default,Default,Default,Default,Default,1111,Empty,RyuJit,X64,Core,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,1,5,Default,1,Default,Default,Default,Default,Default,16,1,50000,"1,666.19 ms",43.1443 ms,11.2044 ms,"1,651.36 ms","1,677.04 ms","1,669.51 ms",33000.0000,12000.0000,3000.0000,212.25 MB NpoiExportToXlsxBytesTest,Default,False,Default,Default,Default,Default,Default,Default,1111,Empty,RyuJit,X64,Core,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,1,5,Default,1,Default,Default,Default,Default,Default,16,1,50000,"2,562.64 ms",130.8702 ms,33.9866 ms,"2,516.62 ms","2,595.77 ms","2,573.09 ms",96000.0000,24000.0000,4000.0000,532.54 MB EpplusExportToBytesTest,Default,False,Default,Default,Default,Default,Default,Default,1111,Empty,RyuJit,X64,Core,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,1,5,Default,1,Default,Default,Default,Default,Default,16,1,50000,"1,059.02 ms",76.3548 ms,19.8291 ms,"1,041.61 ms","1,093.02 ms","1,052.11 ms",51000.0000,13000.0000,2000.0000,270.94 MB StructExportToCsvBytesTest,Default,False,Default,Default,Default,Default,Default,Default,1111,Empty,RyuJit,X64,Core,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,1,5,Default,1,Default,Default,Default,Default,Default,16,1,50000,108.91 ms,4.0316 ms,1.0470 ms,107.28 ms,110.19 ms,108.95 ms,10000.0000,2000.0000,800.0000,60.81 MB NpoiStructExportToXlsBytesTest,Default,False,Default,Default,Default,Default,Default,Default,1111,Empty,RyuJit,X64,Core,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,1,5,Default,1,Default,Default,Default,Default,Default,16,1,50000,"1,675.32 ms",63.9457 ms,16.6065 ms,"1,660.80 ms","1,703.80 ms","1,669.06 ms",33000.0000,12000.0000,3000.0000,214.15 MB NpoiStructExportToXlsxBytesTest,Default,False,Default,Default,Default,Default,Default,Default,1111,Empty,RyuJit,X64,Core,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,1,5,Default,1,Default,Default,Default,Default,Default,16,1,50000,"2,505.01 ms",231.4485 ms,60.1064 ms,"2,443.93 ms","2,576.48 ms","2,494.21 ms",96000.0000,24000.0000,4000.0000,534.45 MB EpplusStructExportToBytesTest,Default,False,Default,Default,Default,Default,Default,Default,1111,Empty,RyuJit,X64,Core,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,1,5,Default,1,Default,Default,Default,Default,Default,16,1,50000,"1,031.01 ms",17.5706 ms,4.5630 ms,"1,027.89 ms","1,038.94 ms","1,029.03 ms",51000.0000,13000.0000,2000.0000,270.93 MB ExportToCsvBytesTest,Default,False,Default,Default,Default,Default,Default,Default,1111,Empty,RyuJit,X64,Core,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,1,5,Default,1,Default,Default,Default,Default,Default,16,1,65535,147.35 ms,1.3296 ms,0.3453 ms,147.03 ms,147.83 ms,147.16 ms,12750.0000,2000.0000,500.0000,79.73 MB NpoiExportToXlsBytesTest,Default,False,Default,Default,Default,Default,Default,Default,1111,Empty,RyuJit,X64,Core,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,1,5,Default,1,Default,Default,Default,Default,Default,16,1,65535,"2,456.24 ms",21.4291 ms,5.5651 ms,"2,448.12 ms","2,462.39 ms","2,455.63 ms",43000.0000,15000.0000,3000.0000,277.42 MB NpoiExportToXlsxBytesTest,Default,False,Default,Default,Default,Default,Default,Default,1111,Empty,RyuJit,X64,Core,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,1,5,Default,1,Default,Default,Default,Default,Default,16,1,65535,"3,292.05 ms",120.2493 ms,31.2284 ms,"3,257.61 ms","3,327.38 ms","3,280.70 ms",127000.0000,30000.0000,5000.0000,685.91 MB EpplusExportToBytesTest,Default,False,Default,Default,Default,Default,Default,Default,1111,Empty,RyuJit,X64,Core,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,1,5,Default,1,Default,Default,Default,Default,Default,16,1,65535,"1,365.16 ms",27.3326 ms,7.0982 ms,"1,355.48 ms","1,374.40 ms","1,363.77 ms",68000.0000,17000.0000,3000.0000,343.35 MB StructExportToCsvBytesTest,Default,False,Default,Default,Default,Default,Default,Default,1111,Empty,RyuJit,X64,Core,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,1,5,Default,1,Default,Default,Default,Default,Default,16,1,65535,141.21 ms,6.6211 ms,1.7195 ms,139.06 ms,143.00 ms,141.42 ms,12750.0000,2000.0000,500.0000,79.73 MB NpoiStructExportToXlsBytesTest,Default,False,Default,Default,Default,Default,Default,Default,1111,Empty,RyuJit,X64,Core,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,1,5,Default,1,Default,Default,Default,Default,Default,16,1,65535,"2,505.47 ms",83.5632 ms,21.7011 ms,"2,483.53 ms","2,536.38 ms","2,504.97 ms",43000.0000,15000.0000,3000.0000,279.92 MB NpoiStructExportToXlsxBytesTest,Default,False,Default,Default,Default,Default,Default,Default,1111,Empty,RyuJit,X64,Core,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,1,5,Default,1,Default,Default,Default,Default,Default,16,1,65535,"3,235.57 ms",116.3763 ms,30.2226 ms,"3,205.37 ms","3,284.36 ms","3,230.79 ms",127000.0000,30000.0000,5000.0000,688.39 MB EpplusStructExportToBytesTest,Default,False,Default,Default,Default,Default,Default,Default,1111,Empty,RyuJit,X64,Core,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,1,5,Default,1,Default,Default,Default,Default,Default,16,1,65535,"1,349.52 ms",8.8304 ms,2.2932 ms,"1,346.47 ms","1,352.90 ms","1,349.45 ms",67000.0000,16000.0000,3000.0000,343.35 MB ================================================ FILE: perf/WeihanLi.Npoi.Benchmark/BenchmarkDotNet.Artifacts/results/WeihanLi.Npoi.Benchmark.ExportExcelTest-report.html ================================================ WeihanLi.Npoi.Benchmark.ExportExcelTest-20191108-071719

BenchmarkDotNet=v0.11.5, OS=Windows 10.0.18362
Intel Core i5-3470 CPU 3.20GHz (Ivy Bridge), 1 CPU, 4 logical and 4 physical cores
.NET Core SDK=3.0.100
  [Host]     : .NET Core 2.2.6 (CoreCLR 4.6.27817.03, CoreFX 4.6.27818.02), 64bit RyuJIT
  Job-WDPKYY : .NET Core 2.2.6 (CoreCLR 4.6.27817.03, CoreFX 4.6.27818.02), 64bit RyuJIT
IterationCount=5  LaunchCount=1  WarmupCount=1  
MethodRowsCount MeanErrorStdDev Min MaxMedianGen 0Gen 1Gen 2Allocated
ExportToCsvBytesTest1000022.77 ms0.5667 ms0.1472 ms22.54 ms22.95 ms22.77 ms2281.2500937.5000281.250012.12 MB
NpoiExportToXlsBytesTest10000219.87 ms3.8409 ms0.9975 ms218.92 ms221.40 ms219.60 ms6000.00002000.00001000.000043.99 MB
NpoiExportToXlsxBytesTest10000470.79 ms9.3139 ms2.4188 ms467.01 ms473.70 ms471.26 ms20000.00006000.00002000.0000104.31 MB
EpplusExportToBytesTest10000203.82 ms5.0193 ms1.3035 ms203.13 ms206.15 ms203.25 ms11000.00004000.00001000.000056.66 MB
StructExportToCsvBytesTest1000021.46 ms0.7542 ms0.1959 ms21.30 ms21.73 ms21.34 ms2281.2500937.5000281.250012.12 MB
NpoiStructExportToXlsBytesTest10000212.54 ms12.7005 ms3.2983 ms209.21 ms216.65 ms211.42 ms7000.00003000.00001000.000044.37 MB
NpoiStructExportToXlsxBytesTest10000495.55 ms45.1010 ms11.7126 ms482.73 ms514.33 ms492.62 ms20000.00007000.00002000.0000104.69 MB
EpplusStructExportToBytesTest10000208.60 ms5.3520 ms1.3899 ms206.94 ms210.32 ms208.32 ms11000.00004000.00001000.000056.66 MB
ExportToCsvBytesTest3000072.16 ms3.2924 ms0.8550 ms71.34 ms73.59 ms71.86 ms6000.00001571.4286571.428636.47 MB
NpoiExportToXlsBytesTest30000839.63 ms22.8596 ms5.9366 ms835.13 ms849.85 ms837.76 ms20000.00008000.00002000.0000124.18 MB
NpoiExportToXlsxBytesTest300001,478.00 ms48.6880 ms12.6441 ms1,457.50 ms1,489.99 ms1,480.52 ms59000.000016000.00004000.0000315.85 MB
EpplusExportToBytesTest30000624.67 ms40.5093 ms10.5201 ms616.60 ms642.67 ms621.37 ms31000.000010000.00003000.0000172.73 MB
StructExportToCsvBytesTest3000063.51 ms2.5019 ms0.6497 ms62.53 ms64.25 ms63.58 ms5875.00001375.0000375.000036.47 MB
NpoiStructExportToXlsBytesTest30000853.48 ms33.3918 ms8.6718 ms839.53 ms862.13 ms856.08 ms20000.00008000.00002000.0000125.32 MB
NpoiStructExportToXlsxBytesTest300001,516.05 ms141.2409 ms36.6798 ms1,474.05 ms1,553.23 ms1,534.14 ms59000.000016000.00004000.0000317 MB
EpplusStructExportToBytesTest30000624.10 ms18.2990 ms4.7522 ms616.20 ms627.98 ms624.45 ms31000.000010000.00003000.0000172.72 MB
ExportToCsvBytesTest50000113.06 ms1.6585 ms0.4307 ms112.55 ms113.55 ms113.01 ms10000.00002000.0000800.000060.81 MB
NpoiExportToXlsBytesTest500001,666.19 ms43.1443 ms11.2044 ms1,651.36 ms1,677.04 ms1,669.51 ms33000.000012000.00003000.0000212.25 MB
NpoiExportToXlsxBytesTest500002,562.64 ms130.8702 ms33.9866 ms2,516.62 ms2,595.77 ms2,573.09 ms96000.000024000.00004000.0000532.54 MB
EpplusExportToBytesTest500001,059.02 ms76.3548 ms19.8291 ms1,041.61 ms1,093.02 ms1,052.11 ms51000.000013000.00002000.0000270.94 MB
StructExportToCsvBytesTest50000108.91 ms4.0316 ms1.0470 ms107.28 ms110.19 ms108.95 ms10000.00002000.0000800.000060.81 MB
NpoiStructExportToXlsBytesTest500001,675.32 ms63.9457 ms16.6065 ms1,660.80 ms1,703.80 ms1,669.06 ms33000.000012000.00003000.0000214.15 MB
NpoiStructExportToXlsxBytesTest500002,505.01 ms231.4485 ms60.1064 ms2,443.93 ms2,576.48 ms2,494.21 ms96000.000024000.00004000.0000534.45 MB
EpplusStructExportToBytesTest500001,031.01 ms17.5706 ms4.5630 ms1,027.89 ms1,038.94 ms1,029.03 ms51000.000013000.00002000.0000270.93 MB
ExportToCsvBytesTest65535147.35 ms1.3296 ms0.3453 ms147.03 ms147.83 ms147.16 ms12750.00002000.0000500.000079.73 MB
NpoiExportToXlsBytesTest655352,456.24 ms21.4291 ms5.5651 ms2,448.12 ms2,462.39 ms2,455.63 ms43000.000015000.00003000.0000277.42 MB
NpoiExportToXlsxBytesTest655353,292.05 ms120.2493 ms31.2284 ms3,257.61 ms3,327.38 ms3,280.70 ms127000.000030000.00005000.0000685.91 MB
EpplusExportToBytesTest655351,365.16 ms27.3326 ms7.0982 ms1,355.48 ms1,374.40 ms1,363.77 ms68000.000017000.00003000.0000343.35 MB
StructExportToCsvBytesTest65535141.21 ms6.6211 ms1.7195 ms139.06 ms143.00 ms141.42 ms12750.00002000.0000500.000079.73 MB
NpoiStructExportToXlsBytesTest655352,505.47 ms83.5632 ms21.7011 ms2,483.53 ms2,536.38 ms2,504.97 ms43000.000015000.00003000.0000279.92 MB
NpoiStructExportToXlsxBytesTest655353,235.57 ms116.3763 ms30.2226 ms3,205.37 ms3,284.36 ms3,230.79 ms127000.000030000.00005000.0000688.39 MB
EpplusStructExportToBytesTest655351,349.52 ms8.8304 ms2.2932 ms1,346.47 ms1,352.90 ms1,349.45 ms67000.000016000.00003000.0000343.35 MB
================================================ FILE: perf/WeihanLi.Npoi.Benchmark/BenchmarkDotNet.Artifacts/results/WeihanLi.Npoi.Benchmark.ImportExcelTest-report-github.md ================================================ ``` ini BenchmarkDotNet=v0.11.5, OS=Windows 10.0.18362 Intel Core i5-3470 CPU 3.20GHz (Ivy Bridge), 1 CPU, 4 logical and 4 physical cores .NET Core SDK=3.0.100 [Host] : .NET Core 2.2.6 (CoreCLR 4.6.27817.03, CoreFX 4.6.27818.02), 64bit RyuJIT Job-WDPKYY : .NET Core 2.2.6 (CoreCLR 4.6.27817.03, CoreFX 4.6.27818.02), 64bit RyuJIT IterationCount=5 LaunchCount=1 WarmupCount=1 ``` | Method | RowsCount | Mean | Error | StdDev | Min | Max | Median | Gen 0 | Gen 1 | Gen 2 | Allocated | |------------------------ |---------- |-----------:|-----------:|-----------:|-----------:|-----------:|-----------:|------------:|-----------:|----------:|----------:| | **ImportFromCsvBytesTest** | **10000** | **171.2 ms** | **2.904 ms** | **0.7541 ms** | **170.0 ms** | **172.0 ms** | **171.2 ms** | **9333.3333** | **2000.0000** | **333.3333** | **36.44 MB** | | ImportFromXlsBytesTest | 10000 | 353.8 ms | 10.170 ms | 2.6412 ms | 351.8 ms | 358.4 ms | 352.9 ms | 15000.0000 | 6000.0000 | 2000.0000 | 64.71 MB | | ImportFromXlsxBytesTest | 10000 | 758.2 ms | 10.212 ms | 2.6520 ms | 755.6 ms | 762.3 ms | 757.8 ms | 25000.0000 | 11000.0000 | 3000.0000 | 110.87 MB | | **ImportFromCsvBytesTest** | **30000** | **511.5 ms** | **17.159 ms** | **4.4562 ms** | **504.0 ms** | **514.9 ms** | **512.7 ms** | **31000.0000** | **7000.0000** | **1000.0000** | **109.1 MB** | | ImportFromXlsBytesTest | 30000 | 1,050.8 ms | 33.740 ms | 8.7622 ms | 1,040.2 ms | 1,064.3 ms | 1,049.8 ms | 47000.0000 | 18000.0000 | 3000.0000 | 186.28 MB | | ImportFromXlsxBytesTest | 30000 | 2,358.6 ms | 284.891 ms | 73.9854 ms | 2,228.2 ms | 2,405.3 ms | 2,378.8 ms | 67000.0000 | 23000.0000 | 4000.0000 | 331.5 MB | | **ImportFromCsvBytesTest** | **50000** | **860.7 ms** | **11.026 ms** | **2.8634 ms** | **857.3 ms** | **865.1 ms** | **860.0 ms** | **50000.0000** | **12000.0000** | **1000.0000** | **182 MB** | | ImportFromXlsBytesTest | 50000 | 1,703.0 ms | 23.050 ms | 5.9861 ms | 1,695.9 ms | 1,709.7 ms | 1,703.3 ms | 76000.0000 | 28000.0000 | 2000.0000 | 315.85 MB | | ImportFromXlsxBytesTest | 50000 | 3,869.8 ms | 141.111 ms | 36.6460 ms | 3,817.0 ms | 3,909.6 ms | 3,872.4 ms | 110000.0000 | 36000.0000 | 3000.0000 | 552.76 MB | | **ImportFromCsvBytesTest** | **65535** | **1,144.7 ms** | **65.693 ms** | **17.0602 ms** | **1,121.2 ms** | **1,162.6 ms** | **1,149.4 ms** | **65000.0000** | **16000.0000** | **1000.0000** | **238.24 MB** | | ImportFromXlsBytesTest | 65535 | 2,324.6 ms | 23.299 ms | 6.0507 ms | 2,318.4 ms | 2,333.9 ms | 2,323.8 ms | 99000.0000 | 35000.0000 | 3000.0000 | 413.07 MB | | ImportFromXlsxBytesTest | 65535 | 5,011.3 ms | 108.362 ms | 28.1414 ms | 4,977.1 ms | 5,054.4 ms | 5,012.6 ms | 145000.0000 | 48000.0000 | 3000.0000 | 723.66 MB | ================================================ FILE: perf/WeihanLi.Npoi.Benchmark/BenchmarkDotNet.Artifacts/results/WeihanLi.Npoi.Benchmark.ImportExcelTest-report.csv ================================================ Method,Job,AnalyzeLaunchVariance,EvaluateOverhead,MaxAbsoluteError,MaxRelativeError,MinInvokeCount,MinIterationTime,OutlierMode,Affinity,EnvironmentVariables,Jit,Platform,Runtime,AllowVeryLargeObjects,Concurrent,CpuGroups,Force,HeapAffinitizeMask,HeapCount,NoAffinitize,RetainVm,Server,PowerPlan,Arguments,BuildConfiguration,Clock,EngineFactory,NuGetReferences,Toolchain,IsMutator,InvocationCount,IterationCount,IterationTime,LaunchCount,MaxIterationCount,MaxWarmupIterationCount,MinIterationCount,MinWarmupIterationCount,RunStrategy,UnrollFactor,WarmupCount,RowsCount,Mean,Error,StdDev,Min,Max,Median,Gen 0,Gen 1,Gen 2,Allocated ImportFromCsvBytesTest,Default,False,Default,Default,Default,Default,Default,Default,1111,Empty,RyuJit,X64,Core,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,1,5,Default,1,Default,Default,Default,Default,Default,16,1,10000,171.2 ms,2.904 ms,0.7541 ms,170.0 ms,172.0 ms,171.2 ms,9333.3333,2000.0000,333.3333,36.44 MB ImportFromXlsBytesTest,Default,False,Default,Default,Default,Default,Default,Default,1111,Empty,RyuJit,X64,Core,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,1,5,Default,1,Default,Default,Default,Default,Default,16,1,10000,353.8 ms,10.170 ms,2.6412 ms,351.8 ms,358.4 ms,352.9 ms,15000.0000,6000.0000,2000.0000,64.71 MB ImportFromXlsxBytesTest,Default,False,Default,Default,Default,Default,Default,Default,1111,Empty,RyuJit,X64,Core,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,1,5,Default,1,Default,Default,Default,Default,Default,16,1,10000,758.2 ms,10.212 ms,2.6520 ms,755.6 ms,762.3 ms,757.8 ms,25000.0000,11000.0000,3000.0000,110.87 MB ImportFromCsvBytesTest,Default,False,Default,Default,Default,Default,Default,Default,1111,Empty,RyuJit,X64,Core,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,1,5,Default,1,Default,Default,Default,Default,Default,16,1,30000,511.5 ms,17.159 ms,4.4562 ms,504.0 ms,514.9 ms,512.7 ms,31000.0000,7000.0000,1000.0000,109.1 MB ImportFromXlsBytesTest,Default,False,Default,Default,Default,Default,Default,Default,1111,Empty,RyuJit,X64,Core,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,1,5,Default,1,Default,Default,Default,Default,Default,16,1,30000,"1,050.8 ms",33.740 ms,8.7622 ms,"1,040.2 ms","1,064.3 ms","1,049.8 ms",47000.0000,18000.0000,3000.0000,186.28 MB ImportFromXlsxBytesTest,Default,False,Default,Default,Default,Default,Default,Default,1111,Empty,RyuJit,X64,Core,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,1,5,Default,1,Default,Default,Default,Default,Default,16,1,30000,"2,358.6 ms",284.891 ms,73.9854 ms,"2,228.2 ms","2,405.3 ms","2,378.8 ms",67000.0000,23000.0000,4000.0000,331.5 MB ImportFromCsvBytesTest,Default,False,Default,Default,Default,Default,Default,Default,1111,Empty,RyuJit,X64,Core,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,1,5,Default,1,Default,Default,Default,Default,Default,16,1,50000,860.7 ms,11.026 ms,2.8634 ms,857.3 ms,865.1 ms,860.0 ms,50000.0000,12000.0000,1000.0000,182 MB ImportFromXlsBytesTest,Default,False,Default,Default,Default,Default,Default,Default,1111,Empty,RyuJit,X64,Core,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,1,5,Default,1,Default,Default,Default,Default,Default,16,1,50000,"1,703.0 ms",23.050 ms,5.9861 ms,"1,695.9 ms","1,709.7 ms","1,703.3 ms",76000.0000,28000.0000,2000.0000,315.85 MB ImportFromXlsxBytesTest,Default,False,Default,Default,Default,Default,Default,Default,1111,Empty,RyuJit,X64,Core,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,1,5,Default,1,Default,Default,Default,Default,Default,16,1,50000,"3,869.8 ms",141.111 ms,36.6460 ms,"3,817.0 ms","3,909.6 ms","3,872.4 ms",110000.0000,36000.0000,3000.0000,552.76 MB ImportFromCsvBytesTest,Default,False,Default,Default,Default,Default,Default,Default,1111,Empty,RyuJit,X64,Core,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,1,5,Default,1,Default,Default,Default,Default,Default,16,1,65535,"1,144.7 ms",65.693 ms,17.0602 ms,"1,121.2 ms","1,162.6 ms","1,149.4 ms",65000.0000,16000.0000,1000.0000,238.24 MB ImportFromXlsBytesTest,Default,False,Default,Default,Default,Default,Default,Default,1111,Empty,RyuJit,X64,Core,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,1,5,Default,1,Default,Default,Default,Default,Default,16,1,65535,"2,324.6 ms",23.299 ms,6.0507 ms,"2,318.4 ms","2,333.9 ms","2,323.8 ms",99000.0000,35000.0000,3000.0000,413.07 MB ImportFromXlsxBytesTest,Default,False,Default,Default,Default,Default,Default,Default,1111,Empty,RyuJit,X64,Core,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,1,5,Default,1,Default,Default,Default,Default,Default,16,1,65535,"5,011.3 ms",108.362 ms,28.1414 ms,"4,977.1 ms","5,054.4 ms","5,012.6 ms",145000.0000,48000.0000,3000.0000,723.66 MB ================================================ FILE: perf/WeihanLi.Npoi.Benchmark/BenchmarkDotNet.Artifacts/results/WeihanLi.Npoi.Benchmark.ImportExcelTest-report.html ================================================ WeihanLi.Npoi.Benchmark.ImportExcelTest-20191108-072305

BenchmarkDotNet=v0.11.5, OS=Windows 10.0.18362
Intel Core i5-3470 CPU 3.20GHz (Ivy Bridge), 1 CPU, 4 logical and 4 physical cores
.NET Core SDK=3.0.100
  [Host]     : .NET Core 2.2.6 (CoreCLR 4.6.27817.03, CoreFX 4.6.27818.02), 64bit RyuJIT
  Job-WDPKYY : .NET Core 2.2.6 (CoreCLR 4.6.27817.03, CoreFX 4.6.27818.02), 64bit RyuJIT
IterationCount=5  LaunchCount=1  WarmupCount=1  
MethodRowsCountMeanErrorStdDev Min MaxMedianGen 0Gen 1Gen 2Allocated
ImportFromCsvBytesTest10000171.2 ms2.904 ms0.7541 ms170.0 ms172.0 ms171.2 ms9333.33332000.0000333.333336.44 MB
ImportFromXlsBytesTest10000353.8 ms10.170 ms2.6412 ms351.8 ms358.4 ms352.9 ms15000.00006000.00002000.000064.71 MB
ImportFromXlsxBytesTest10000758.2 ms10.212 ms2.6520 ms755.6 ms762.3 ms757.8 ms25000.000011000.00003000.0000110.87 MB
ImportFromCsvBytesTest30000511.5 ms17.159 ms4.4562 ms504.0 ms514.9 ms512.7 ms31000.00007000.00001000.0000109.1 MB
ImportFromXlsBytesTest300001,050.8 ms33.740 ms8.7622 ms1,040.2 ms1,064.3 ms1,049.8 ms47000.000018000.00003000.0000186.28 MB
ImportFromXlsxBytesTest300002,358.6 ms284.891 ms73.9854 ms2,228.2 ms2,405.3 ms2,378.8 ms67000.000023000.00004000.0000331.5 MB
ImportFromCsvBytesTest50000860.7 ms11.026 ms2.8634 ms857.3 ms865.1 ms860.0 ms50000.000012000.00001000.0000182 MB
ImportFromXlsBytesTest500001,703.0 ms23.050 ms5.9861 ms1,695.9 ms1,709.7 ms1,703.3 ms76000.000028000.00002000.0000315.85 MB
ImportFromXlsxBytesTest500003,869.8 ms141.111 ms36.6460 ms3,817.0 ms3,909.6 ms3,872.4 ms110000.000036000.00003000.0000552.76 MB
ImportFromCsvBytesTest655351,144.7 ms65.693 ms17.0602 ms1,121.2 ms1,162.6 ms1,149.4 ms65000.000016000.00001000.0000238.24 MB
ImportFromXlsBytesTest655352,324.6 ms23.299 ms6.0507 ms2,318.4 ms2,333.9 ms2,323.8 ms99000.000035000.00003000.0000413.07 MB
ImportFromXlsxBytesTest655355,011.3 ms108.362 ms28.1414 ms4,977.1 ms5,054.4 ms5,012.6 ms145000.000048000.00003000.0000723.66 MB
================================================ FILE: perf/WeihanLi.Npoi.Benchmark/BenchmarkDotNet.Artifacts/results/WeihanLi.Npoi.Benchmark.WorkbookBasicTest-report-github.md ================================================ ``` ini BenchmarkDotNet=v0.11.5, OS=Windows 10.0.18362 Intel Core i5-3470 CPU 3.20GHz (Ivy Bridge), 1 CPU, 4 logical and 4 physical cores .NET Core SDK=3.0.100 [Host] : .NET Core 2.2.6 (CoreCLR 4.6.27817.03, CoreFX 4.6.27818.02), 64bit RyuJIT Job-CBYTBY : .NET Core 2.2.6 (CoreCLR 4.6.27817.03, CoreFX 4.6.27818.02), 64bit RyuJIT IterationCount=5 LaunchCount=1 WarmupCount=1 ``` | Method | RowsCount | Mean | Error | StdDev | Min | Max | Median | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | |--------------------- |---------- |-----------:|-----------:|------------:|-----------:|-----------:|-----------:|------:|--------:|------------:|-----------:|----------:|-----------:| | **NpoiXlsWorkbookInit** | **10000** | **324.7 ms** | **1.583 ms** | **0.4110 ms** | **324.3 ms** | **325.4 ms** | **324.6 ms** | **1.00** | **0.00** | **10000.0000** | **5000.0000** | **2000.0000** | **78.6 MB** | | NpoiXlsxWorkbookInit | 10000 | 1,369.0 ms | 73.747 ms | 19.1517 ms | 1,341.3 ms | 1,384.4 ms | 1,381.1 ms | 4.22 | 0.06 | 57000.0000 | 14000.0000 | 4000.0000 | 306.45 MB | | EpplusWorkbookInit | 10000 | 552.9 ms | 12.740 ms | 3.3085 ms | 549.7 ms | 557.7 ms | 552.4 ms | 1.70 | 0.01 | 18000.0000 | 7000.0000 | 3000.0000 | 121.05 MB | | | | | | | | | | | | | | | | | **NpoiXlsWorkbookInit** | **30000** | **1,222.4 ms** | **33.717 ms** | **8.7562 ms** | **1,209.0 ms** | **1,233.1 ms** | **1,222.5 ms** | **1.00** | **0.00** | **29000.0000** | **11000.0000** | **3000.0000** | **235.03 MB** | | NpoiXlsxWorkbookInit | 30000 | 4,226.2 ms | 299.833 ms | 77.8658 ms | 4,109.5 ms | 4,308.6 ms | 4,257.2 ms | 3.46 | 0.08 | 174000.0000 | 34000.0000 | 6000.0000 | 913.9 MB | | EpplusWorkbookInit | 30000 | 1,695.4 ms | 31.751 ms | 8.2457 ms | 1,686.3 ms | 1,706.5 ms | 1,694.2 ms | 1.39 | 0.02 | 48000.0000 | 17000.0000 | 5000.0000 | 358.51 MB | | | | | | | | | | | | | | | | | **NpoiXlsWorkbookInit** | **50000** | **2,323.5 ms** | **236.041 ms** | **61.2990 ms** | **2,286.0 ms** | **2,431.9 ms** | **2,294.2 ms** | **1.00** | **0.00** | **47000.0000** | **18000.0000** | **4000.0000** | **417.1 MB** | | NpoiXlsxWorkbookInit | 50000 | 7,055.2 ms | 279.256 ms | 72.5218 ms | 6,982.8 ms | 7,150.2 ms | 7,027.2 ms | 3.04 | 0.10 | 288000.0000 | 51000.0000 | 6000.0000 | 1545.32 MB | | EpplusWorkbookInit | 50000 | 2,806.9 ms | 56.266 ms | 14.6121 ms | 2,792.9 ms | 2,829.1 ms | 2,804.6 ms | 1.21 | 0.03 | 79000.0000 | 27000.0000 | 7000.0000 | 578.46 MB | | | | | | | | | | | | | | | | | **NpoiXlsWorkbookInit** | **65535** | **3,646.8 ms** | **131.129 ms** | **34.0537 ms** | **3,603.0 ms** | **3,696.3 ms** | **3,642.5 ms** | **1.00** | **0.00** | **61000.0000** | **21000.0000** | **4000.0000** | **504.46 MB** | | NpoiXlsxWorkbookInit | 65535 | 9,295.6 ms | 486.761 ms | 126.4104 ms | 9,163.3 ms | 9,468.6 ms | 9,330.5 ms | 2.55 | 0.04 | 390000.0000 | 67000.0000 | 8000.0000 | 2048.14 MB | | EpplusWorkbookInit | 65535 | 3,721.6 ms | 124.945 ms | 32.4478 ms | 3,680.7 ms | 3,766.8 ms | 3,714.1 ms | 1.02 | 0.01 | 102000.0000 | 35000.0000 | 8000.0000 | 747.85 MB | ================================================ FILE: perf/WeihanLi.Npoi.Benchmark/BenchmarkDotNet.Artifacts/results/WeihanLi.Npoi.Benchmark.WorkbookBasicTest-report.csv ================================================ Method,Job,AnalyzeLaunchVariance,EvaluateOverhead,MaxAbsoluteError,MaxRelativeError,MinInvokeCount,MinIterationTime,OutlierMode,Affinity,EnvironmentVariables,Jit,Platform,Runtime,AllowVeryLargeObjects,Concurrent,CpuGroups,Force,HeapAffinitizeMask,HeapCount,NoAffinitize,RetainVm,Server,PowerPlan,Arguments,BuildConfiguration,Clock,EngineFactory,NuGetReferences,Toolchain,IsMutator,InvocationCount,IterationCount,IterationTime,LaunchCount,MaxIterationCount,MaxWarmupIterationCount,MinIterationCount,MinWarmupIterationCount,RunStrategy,UnrollFactor,WarmupCount,RowsCount,Mean,Error,StdDev,Min,Max,Median,Ratio,RatioSD,Gen 0,Gen 1,Gen 2,Allocated NpoiXlsWorkbookInit,Default,False,Default,Default,Default,Default,Default,Default,1111,Empty,RyuJit,X64,Core,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,1,5,Default,1,Default,Default,Default,Default,Default,16,1,10000,324.7 ms,1.583 ms,0.4110 ms,324.3 ms,325.4 ms,324.6 ms,1.00,0.00,10000.0000,5000.0000,2000.0000,78.6 MB NpoiXlsxWorkbookInit,Default,False,Default,Default,Default,Default,Default,Default,1111,Empty,RyuJit,X64,Core,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,1,5,Default,1,Default,Default,Default,Default,Default,16,1,10000,"1,369.0 ms",73.747 ms,19.1517 ms,"1,341.3 ms","1,384.4 ms","1,381.1 ms",4.22,0.06,57000.0000,14000.0000,4000.0000,306.45 MB EpplusWorkbookInit,Default,False,Default,Default,Default,Default,Default,Default,1111,Empty,RyuJit,X64,Core,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,1,5,Default,1,Default,Default,Default,Default,Default,16,1,10000,552.9 ms,12.740 ms,3.3085 ms,549.7 ms,557.7 ms,552.4 ms,1.70,0.01,18000.0000,7000.0000,3000.0000,121.05 MB NpoiXlsWorkbookInit,Default,False,Default,Default,Default,Default,Default,Default,1111,Empty,RyuJit,X64,Core,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,1,5,Default,1,Default,Default,Default,Default,Default,16,1,30000,"1,222.4 ms",33.717 ms,8.7562 ms,"1,209.0 ms","1,233.1 ms","1,222.5 ms",1.00,0.00,29000.0000,11000.0000,3000.0000,235.03 MB NpoiXlsxWorkbookInit,Default,False,Default,Default,Default,Default,Default,Default,1111,Empty,RyuJit,X64,Core,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,1,5,Default,1,Default,Default,Default,Default,Default,16,1,30000,"4,226.2 ms",299.833 ms,77.8658 ms,"4,109.5 ms","4,308.6 ms","4,257.2 ms",3.46,0.08,174000.0000,34000.0000,6000.0000,913.9 MB EpplusWorkbookInit,Default,False,Default,Default,Default,Default,Default,Default,1111,Empty,RyuJit,X64,Core,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,1,5,Default,1,Default,Default,Default,Default,Default,16,1,30000,"1,695.4 ms",31.751 ms,8.2457 ms,"1,686.3 ms","1,706.5 ms","1,694.2 ms",1.39,0.02,48000.0000,17000.0000,5000.0000,358.51 MB NpoiXlsWorkbookInit,Default,False,Default,Default,Default,Default,Default,Default,1111,Empty,RyuJit,X64,Core,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,1,5,Default,1,Default,Default,Default,Default,Default,16,1,50000,"2,323.5 ms",236.041 ms,61.2990 ms,"2,286.0 ms","2,431.9 ms","2,294.2 ms",1.00,0.00,47000.0000,18000.0000,4000.0000,417.1 MB NpoiXlsxWorkbookInit,Default,False,Default,Default,Default,Default,Default,Default,1111,Empty,RyuJit,X64,Core,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,1,5,Default,1,Default,Default,Default,Default,Default,16,1,50000,"7,055.2 ms",279.256 ms,72.5218 ms,"6,982.8 ms","7,150.2 ms","7,027.2 ms",3.04,0.10,288000.0000,51000.0000,6000.0000,1545.32 MB EpplusWorkbookInit,Default,False,Default,Default,Default,Default,Default,Default,1111,Empty,RyuJit,X64,Core,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,1,5,Default,1,Default,Default,Default,Default,Default,16,1,50000,"2,806.9 ms",56.266 ms,14.6121 ms,"2,792.9 ms","2,829.1 ms","2,804.6 ms",1.21,0.03,79000.0000,27000.0000,7000.0000,578.46 MB NpoiXlsWorkbookInit,Default,False,Default,Default,Default,Default,Default,Default,1111,Empty,RyuJit,X64,Core,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,1,5,Default,1,Default,Default,Default,Default,Default,16,1,65535,"3,646.8 ms",131.129 ms,34.0537 ms,"3,603.0 ms","3,696.3 ms","3,642.5 ms",1.00,0.00,61000.0000,21000.0000,4000.0000,504.46 MB NpoiXlsxWorkbookInit,Default,False,Default,Default,Default,Default,Default,Default,1111,Empty,RyuJit,X64,Core,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,1,5,Default,1,Default,Default,Default,Default,Default,16,1,65535,"9,295.6 ms",486.761 ms,126.4104 ms,"9,163.3 ms","9,468.6 ms","9,330.5 ms",2.55,0.04,390000.0000,67000.0000,8000.0000,2048.14 MB EpplusWorkbookInit,Default,False,Default,Default,Default,Default,Default,Default,1111,Empty,RyuJit,X64,Core,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,1,5,Default,1,Default,Default,Default,Default,Default,16,1,65535,"3,721.6 ms",124.945 ms,32.4478 ms,"3,680.7 ms","3,766.8 ms","3,714.1 ms",1.02,0.01,102000.0000,35000.0000,8000.0000,747.85 MB ================================================ FILE: perf/WeihanLi.Npoi.Benchmark/BenchmarkDotNet.Artifacts/results/WeihanLi.Npoi.Benchmark.WorkbookBasicTest-report.html ================================================ WeihanLi.Npoi.Benchmark.WorkbookBasicTest-20191108-065532

BenchmarkDotNet=v0.11.5, OS=Windows 10.0.18362
Intel Core i5-3470 CPU 3.20GHz (Ivy Bridge), 1 CPU, 4 logical and 4 physical cores
.NET Core SDK=3.0.100
  [Host]     : .NET Core 2.2.6 (CoreCLR 4.6.27817.03, CoreFX 4.6.27818.02), 64bit RyuJIT
  Job-CBYTBY : .NET Core 2.2.6 (CoreCLR 4.6.27817.03, CoreFX 4.6.27818.02), 64bit RyuJIT
IterationCount=5  LaunchCount=1  WarmupCount=1  
MethodRowsCountMeanErrorStdDev Min MaxMedianRatioRatioSDGen 0Gen 1Gen 2Allocated
NpoiXlsWorkbookInit10000324.7 ms1.583 ms0.4110 ms324.3 ms325.4 ms324.6 ms1.000.0010000.00005000.00002000.000078.6 MB
NpoiXlsxWorkbookInit100001,369.0 ms73.747 ms19.1517 ms1,341.3 ms1,384.4 ms1,381.1 ms4.220.0657000.000014000.00004000.0000306.45 MB
EpplusWorkbookInit10000552.9 ms12.740 ms3.3085 ms549.7 ms557.7 ms552.4 ms1.700.0118000.00007000.00003000.0000121.05 MB
NpoiXlsWorkbookInit300001,222.4 ms33.717 ms8.7562 ms1,209.0 ms1,233.1 ms1,222.5 ms1.000.0029000.000011000.00003000.0000235.03 MB
NpoiXlsxWorkbookInit300004,226.2 ms299.833 ms77.8658 ms4,109.5 ms4,308.6 ms4,257.2 ms3.460.08174000.000034000.00006000.0000913.9 MB
EpplusWorkbookInit300001,695.4 ms31.751 ms8.2457 ms1,686.3 ms1,706.5 ms1,694.2 ms1.390.0248000.000017000.00005000.0000358.51 MB
NpoiXlsWorkbookInit500002,323.5 ms236.041 ms61.2990 ms2,286.0 ms2,431.9 ms2,294.2 ms1.000.0047000.000018000.00004000.0000417.1 MB
NpoiXlsxWorkbookInit500007,055.2 ms279.256 ms72.5218 ms6,982.8 ms7,150.2 ms7,027.2 ms3.040.10288000.000051000.00006000.00001545.32 MB
EpplusWorkbookInit500002,806.9 ms56.266 ms14.6121 ms2,792.9 ms2,829.1 ms2,804.6 ms1.210.0379000.000027000.00007000.0000578.46 MB
NpoiXlsWorkbookInit655353,646.8 ms131.129 ms34.0537 ms3,603.0 ms3,696.3 ms3,642.5 ms1.000.0061000.000021000.00004000.0000504.46 MB
NpoiXlsxWorkbookInit655359,295.6 ms486.761 ms126.4104 ms9,163.3 ms9,468.6 ms9,330.5 ms2.550.04390000.000067000.00008000.00002048.14 MB
EpplusWorkbookInit655353,721.6 ms124.945 ms32.4478 ms3,680.7 ms3,766.8 ms3,714.1 ms1.020.01102000.000035000.00008000.0000747.85 MB
================================================ FILE: perf/WeihanLi.Npoi.Benchmark/ExportExcelTest.cs ================================================ // Copyright (c) Weihan Li. All rights reserved. // Licensed under the Apache license. using BenchmarkDotNet.Attributes; using EPPlus.Core.Extensions; using EPPlus.Core.Extensions.Attributes; using System.Runtime.CompilerServices; namespace WeihanLi.Npoi.Benchmark; [SimpleJob(launchCount: 1, warmupCount: 1, iterationCount: 5)] [MemoryDiagnoser] [MinColumn, MaxColumn, MeanColumn, MedianColumn] public class ExportExcelTest { private class TestEntity { [ExcelTableColumn("PKID")] public int PKID { get; set; } [ExcelTableColumn("UserName")] public string? Username { get; set; } [ExcelTableColumn("PasswordHash")] public string? PasswordHash { get; set; } [ExcelTableColumn("Amount")] public decimal Amount { get; set; } [ExcelTableColumn("WechatOpenId")] public string? WechatOpenId { get; set; } [ExcelTableColumn("IsActive")] public bool IsActive { get; set; } [ExcelTableColumn("CreateTime")] public DateTime CreateTime => DateTime.Now; } private struct TestStruct { [ExcelTableColumn("PKID")] public int PKID { get; set; } [ExcelTableColumn("UserName")] public string? Username { get; set; } [ExcelTableColumn("PasswordHash")] public string? PasswordHash { get; set; } [ExcelTableColumn("Amount")] public decimal Amount { get; set; } [ExcelTableColumn("WechatOpenId")] public string? WechatOpenId { get; set; } [ExcelTableColumn("IsActive")] public bool IsActive { get; set; } [ExcelTableColumn("CreateTime")] public DateTime CreateTime => DateTime.Now; } private readonly List testData = new(51200); private readonly List testStructData = new(51200); [Params(10000, 30000, 50000, 65535)] public int RowsCount; [GlobalSetup] public void GlobalSetup() { for (var i = 1; i <= RowsCount; i++) { testData.Add(new TestEntity() { Amount = 1000, Username = "xxxx", PKID = i, }); testStructData.Add(new TestStruct() { Amount = 1000, Username = "xxxx", PKID = i, }); } } [GlobalCleanup] public void GlobalCleanup() { // Disposing logic testData.Clear(); } [Benchmark] [MethodImpl(MethodImplOptions.NoInlining)] public byte[] ExportToCsvBytesTest() { return testData.ToCsvBytes(); } [Benchmark] [MethodImpl(MethodImplOptions.NoInlining)] public byte[] NpoiExportToXlsBytesTest() { return testData.ToExcelBytes(); } [Benchmark] [MethodImpl(MethodImplOptions.NoInlining)] public byte[] NpoiExportToXlsxBytesTest() { return testData.ToExcelBytes(ExcelFormat.Xlsx); } [Benchmark] [MethodImpl(MethodImplOptions.NoInlining)] public byte[] EpplusExportToBytesTest() { return testData.ToExcelPackage().GetAsByteArray(); } [Benchmark] [MethodImpl(MethodImplOptions.NoInlining)] public byte[] StructExportToCsvBytesTest() { return testStructData.ToCsvBytes(); } [Benchmark] [MethodImpl(MethodImplOptions.NoInlining)] public byte[] NpoiStructExportToXlsBytesTest() { return testStructData.ToExcelBytes(); } [Benchmark] [MethodImpl(MethodImplOptions.NoInlining)] public byte[] NpoiStructExportToXlsxBytesTest() { return testStructData.ToExcelBytes(ExcelFormat.Xlsx); } [Benchmark] [MethodImpl(MethodImplOptions.NoInlining)] public byte[] EpplusStructExportToBytesTest() { return testStructData.ToExcelPackage().GetAsByteArray(); } } ================================================ FILE: perf/WeihanLi.Npoi.Benchmark/ImportExcelTest.cs ================================================ // Copyright (c) Weihan Li. All rights reserved. // Licensed under the Apache license. using BenchmarkDotNet.Attributes; using System.Runtime.CompilerServices; namespace WeihanLi.Npoi.Benchmark; [SimpleJob(launchCount: 1, warmupCount: 1, iterationCount: 5)] [MemoryDiagnoser] [MinColumn, MaxColumn, MeanColumn, MedianColumn] public class ImportExcelTest { private class TestEntity { public int PKID { get; set; } public string? Username { get; set; } public string? PasswordHash { get; set; } public decimal Amount { get; set; } public string? WechatOpenId { get; set; } public bool IsActive { get; set; } public DateTime CreateTime { get; set; } = DateTime.Now; } private readonly List testData = new(51200); private byte[] xlsBytes = Array.Empty(), xlsxBytes = Array.Empty(), csvBytes = Array.Empty(); [Params(10000, 30000, 50000, 65535)] public int RowsCount; [GlobalSetup] public void GlobalSetup() { for (var i = 1; i <= RowsCount; i++) { testData.Add(new TestEntity() { Amount = 1000, Username = "xxxx", PKID = i, }); } xlsBytes = testData.ToExcelBytes(); xlsxBytes = testData.ToExcelBytes(ExcelFormat.Xlsx); csvBytes = testData.ToCsvBytes(); } [GlobalCleanup] public void GlobalCleanup() { // Disposing logic testData.Clear(); } [Benchmark] [MethodImpl(MethodImplOptions.NoInlining)] public int ImportFromCsvBytesTest() { var list = CsvHelper.ToEntityList(csvBytes); return list.Count; } [Benchmark] [MethodImpl(MethodImplOptions.NoInlining)] public int ImportFromXlsBytesTest() { var list = ExcelHelper.ToEntityList(xlsBytes, ExcelFormat.Xls); return list.Count; } [Benchmark] [MethodImpl(MethodImplOptions.NoInlining)] public int ImportFromXlsxBytesTest() { var list = ExcelHelper.ToEntityList(xlsxBytes, ExcelFormat.Xlsx); return list.Count; } } ================================================ FILE: perf/WeihanLi.Npoi.Benchmark/Program.cs ================================================ // Copyright (c) Weihan Li. All rights reserved. // Licensed under the Apache license. using BenchmarkDotNet.Running; namespace WeihanLi.Npoi.Benchmark; public class Program { public static void Main(string[] args) { // BenchmarkRunner.Run(); BenchmarkRunner.Run(); BenchmarkRunner.Run(); } } ================================================ FILE: perf/WeihanLi.Npoi.Benchmark/WeihanLi.Npoi.Benchmark.csproj ================================================ Exe ================================================ FILE: perf/WeihanLi.Npoi.Benchmark/WorkbookBasicTest.cs ================================================ // Copyright (c) Weihan Li. All rights reserved. // Licensed under the Apache license. using BenchmarkDotNet.Attributes; using OfficeOpenXml; using System.Runtime.CompilerServices; namespace WeihanLi.Npoi.Benchmark; [SimpleJob(launchCount: 1, warmupCount: 1, iterationCount: 5)] [MemoryDiagnoser] [MinColumn, MaxColumn, MeanColumn, MedianColumn] public class WorkbookBasicTest { private const int ColsCount = 10; [Params(10000, 30000, 50000, 65535)] public int RowsCount; [Benchmark(Baseline = true)] public byte[] NpoiXlsWorkbookInit() { var workbook = ExcelHelper.PrepareWorkbook(ExcelFormat.Xls); var sheet = workbook.CreateSheet("tempSheet"); for (var i = 0; i < RowsCount; i++) { var row = sheet.CreateRow(i); for (var j = 0; j < ColsCount; j++) { var cell = row.CreateCell(j); cell.SetCellValue($"as ({i}, {j}) sa"); } } return workbook.ToExcelBytes(); } [Benchmark] [MethodImpl(MethodImplOptions.NoInlining)] public byte[] NpoiXlsxWorkbookInit() { var workbook = ExcelHelper.PrepareWorkbook(ExcelFormat.Xlsx); var sheet = workbook.CreateSheet("tempSheet"); for (var i = 0; i < RowsCount; i++) { var row = sheet.CreateRow(i); for (var j = 0; j < ColsCount; j++) { var cell = row.CreateCell(j); cell.SetCellValue($"as ({i}, {j}) sa"); } } return workbook.ToExcelBytes(); } [Benchmark] [MethodImpl(MethodImplOptions.NoInlining)] public byte[] EpplusWorkbookInit() { var excel = new ExcelPackage(); var sheet = excel.Workbook.Worksheets.Add("tempSheet"); for (var i = 1; i <= RowsCount; i++) { for (var j = 1; j <= ColsCount; j++) { sheet.Cells[i, j].Value = $"as ({i}, {j}) sa"; } } return excel.GetAsByteArray(); } } ================================================ FILE: samples/Directory.Build.props ================================================ ================================================ FILE: samples/DotNetCoreSample/DotNetCoreSample.csproj ================================================ Exe Always ================================================ FILE: samples/DotNetCoreSample/ImportImageTestModel.cs ================================================ // Copyright (c) Weihan Li. All rights reserved. // Licensed under the Apache license. namespace DotNetCoreSample; internal class ImportImageTestModel { public int Id { get; set; } public string? Name { get; set; } public byte[]? Image { get; set; } } ================================================ FILE: samples/DotNetCoreSample/IssueSamples.cs ================================================ // Copyright (c) Weihan Li. All rights reserved. // Licensed under the Apache license. using WeihanLi.Extensions; public static partial class IssueSamples { public static void Issue169Sample() { var filePath = @"C:\Users\Weiha\Downloads\test\2.xlsx"; var workbook = ExcelHelper.LoadExcel( File.OpenRead(filePath), ExcelFormat.Xlsx ); var settings = FluentSettings.For(); settings.WithPostImportAction((x, rowIndex) => x?.RowNum = rowIndex + 1); var list = workbook.ToEntityList(); foreach (var item in list) { Console.WriteLine(item.ToJson()); } } } [Sheet(SheetIndex = 0, StartRowIndex = 6)] public class Issue169Dto { [Column(IsIgnored = true)] public int RowNum { get; set; } /// /// 编号 /// [Column("物料编号", Index = 0)] public string No { get; set; } /// /// 物料名称 /// [Column("物料名称", Index = 1)] public string Name { get; set; } /// /// 规格型号 /// [Column("规格型号", Index = 2)] public string Specification { get; set; } /// /// 特殊库存 /// [Column("特殊库存", Index = 3)] public string QuantityM { get; set; } /// /// 单位 /// [Column("计量单位", Index = 4)] public string Unit { get; set; } [Column("会计年度", Index = 5)] public int Ytime { get; set; } [Column("会计期间", Index = 6)] public int Mtime { get; set; } // 期初余额 [Column("方向", Index = 7)] public string Direction1 { get; set; } /// /// 数量 /// [Column("数量", Index = 8)] public decimal Quantity1 { get; set; } /// /// 单价 /// [Column("实际单价", Index = 9)] public decimal UnitPrice1 { get; set; } /// /// 总金额 /// [Column("实际金额", Index = 10)] public decimal TotalAmount1 { get; set; } /// /// 标准单价 /// [Column("标准单价", Index = 11)] public decimal StandardUnitPrice1 { get; set; } /// /// 标准金额 /// [Column("标准金额", Index = 12)] public decimal StandardotalAmount1 { get; set; } // 期初余额 //本期借方 /// /// 数量 /// [Column("数量", Index = 13)] public decimal Quantity2 { get; set; } /// /// 单价 /// [Column("实际单价", Index = 14)] public decimal UnitPrice2 { get; set; } /// /// 总金额 /// [Column("实际金额", Index = 15)] public decimal TotalAmount2 { get; set; } /// /// 标准单价 /// [Column("标准单价", Index = 16)] public decimal StandardUnitPrice2 { get; set; } /// /// 标准金额 /// [Column("标准金额", Index = 17)] public decimal StandardotalAmount2 { get; set; } //本期贷方 /// /// 数量 /// [Column("数量", Index = 18)] public decimal Quantity3 { get; set; } /// /// 单价 /// [Column("实际单价", Index = 19)] public decimal UnitPrice3 { get; set; } /// /// 总金额 /// [Column("实际金额", Index = 20)] public decimal TotalAmount3 { get; set; } /// /// 标准单价 /// [Column("标准单价", Index = 21)] public decimal StandardUnitPrice3 { get; set; } /// /// 标准金额 /// [Column("标准金额", Index = 22)] public decimal StandardotalAmount3 { get; set; } //期末余额 [Column("方向", Index = 23)] public string Direction2 { get; set; } /// /// 数量 /// [Column("数量", Index = 24)] public decimal Quantity4 { get; set; } /// /// 实际单价 /// [Column("实际单价", Index = 25)] public decimal UnitPrice4 { get; set; } /// /// 总金额 /// [Column("实际金额", Index = 26)] public decimal TotalAmount4 { get; set; } [Column("核算类别名称", Index = 27)] public string CategoryName { get; set; } [Column("是否可用", Index = 28)] public string IsStop { get; set; } } ================================================ FILE: samples/DotNetCoreSample/ProductPriceMapping.cs ================================================ // Copyright (c) Weihan Li. All rights reserved. // Licensed under the Apache license. // ReSharper disable InconsistentNaming namespace DotNetCoreSample; internal class ProductPriceMapping { public string? Type { get; set; } public string? Pid { get; set; } public string? ShopCode { get; set; } public decimal Price { get; set; } public long ItemID { get; set; } public long SkuID { get; set; } public DateTime LastUpdateDateTime { get; set; } } ================================================ FILE: samples/DotNetCoreSample/Program.cs ================================================ // Copyright (c) Weihan Li. All rights reserved. // Licensed under the Apache license. using NPOI.SS.UserModel; using NPOI.SS.Util; using WeihanLi.Common.Helpers; using WeihanLi.Common.Logging; using WeihanLi.Extensions; // ReSharper disable All LogHelper.ConfigureLogging(x => x.WithMinimumLevel(LogHelperLogLevel.Info).AddConsole()); { IssueSamples.Issue169Sample(); ConsoleHelper.ReadLineWithPrompt(); } // multi sheets sample { var collection1 = new List() { new TestEntity() { PKID = 1, SettingId = Guid.NewGuid(), SettingName = "Setting1", SettingValue = "Value1", DisplayName = "dd" }, new TestEntity() { PKID=2, SettingId = Guid.NewGuid(), SettingName = "Setting2", SettingValue = "Value2", Enabled = true }, }; await collection1.ToCsvFileAsync($"{nameof(collection1)}.csv"); var collection2 = new[] { new TestEntity2() { Id = 999, Title = "test" } }; // prepare a workbook var workbook = ExcelHelper.PrepareWorkbook(ExcelFormat.Xlsx); workbook.ImportData(collection1); workbook.ImportData(collection2, 1); workbook.WriteToFile("multi-sheets-sample.xlsx"); // using var ms = new MemoryStream(); // workbook.Write(ms); Console.WriteLine("multi-sheets-sample excel generated."); Console.ReadLine(); } // // var testSurveyExcelPath = @"C:\Users\Weiha\Desktop\temp\QuizBulkUpload.xlsx"; // var surveyList = ExcelHelper.ToEntityList(testSurveyExcelPath); // SheetNameTest(); FluentSettings.LoadMappingProfile(); var tempDirPath = $@"{Environment.GetEnvironmentVariable("USERPROFILE")}\Desktop\temp\test"; { // custom CsvSeparatorCharacter sample var csvOptions = new CsvOptions() { SeparatorCharacter = '\t' }; var text = CsvHelper.GetCsvText(new[] { new { Title = "123", Desc = "234" } }, csvOptions); var dt1233 = CsvHelper.ToDataTable(text.GetBytes(), csvOptions); } // image export/import test //var imageExcelPath = @"C:\Users\Weiha\Desktop\temp\test\imageTest.xls"; //var imgaeModelList = ExcelHelper.ToEntityList(imageExcelPath); //Console.WriteLine(imgaeModelList.Count(x => x?.Image is not null)); //imgaeModelList.ToExcelFile(imageExcelPath + ".1.xls"); //var imgModeList2 = ExcelHelper.ToEntityList(imageExcelPath + ".1.xls"); //Console.WriteLine($"{imgaeModelList[0]?.Image?.Length},{imgModeList2[0]?.Image?.Length}"); //imgaeModelList.ToExcelFile(imageExcelPath + ".1.xlsx"); //Console.ReadLine(); //FluentSettings.For() // .HasSheetSetting(sheet => // { // sheet.CellFilter = cell => cell.ColumnIndex <= 10; // }); //var tempExcelPath = Path.Combine(tempDirPath, "testdata.xlsx"); //var t_list = ExcelHelper.ToEntityList(tempExcelPath); //var tempTable = ExcelHelper.ToDataTable(tempExcelPath); //var entityList = ExcelHelper.ToEntityList(ApplicationHelper.MapPath("test.xlsx")); //Console.WriteLine("Success!"); //var mapping = ExcelHelper.ToEntityList($@"{Environment.GetEnvironmentVariable("USERPROFILE")}\Desktop\temp\tempFiles\mapping.xlsx"); //var mappingTemp = ExcelHelper.ToEntityList($@"{Environment.GetEnvironmentVariable("USERPROFILE")}\Desktop\temp\tempFiles\mapping_temp.xlsx"); //Console.WriteLine($"-----normal({mapping.Count}【{mapping.Select(_ => _.Pid).Distinct().Count()}】)----"); //foreach (var shop in mapping.GroupBy(_ => _.ShopCode).OrderBy(_ => _.Key)) //{ // Console.WriteLine($"{shop.Key}---{shop.Count()}---distinct pid count:{shop.Select(_ => _.Pid).Distinct().Count()}"); //} //Console.WriteLine($"-----temp({mappingTemp.Count}【{mappingTemp.Select(_ => _.Pid).Distinct().Count()}】)----"); //foreach (var shop in mappingTemp.GroupBy(_ => _.ShopCode).OrderBy(_ => _.Key)) //{ // Console.WriteLine($"{shop.Key}---{shop.Count()}---distinct pid count:{shop.Select(_ => _.Pid).Distinct().Count()}"); //} //Console.WriteLine("Press Enter to continue..."); //Console.ReadLine(); //var list2 = new List(); //list2.Add(null); //for (var i = 0; i < 100_000; i++) //{ // list2.Add(new TestEntity2 // { // Id = i + 1, // Title = $"Title_{i}", // Description = $"{Enumerable.Range(1, 200).StringJoin(",")}__{i}", // }); //} //list2.Add(new TestEntity2() //{ // Id = 999, // Title = $"{Enumerable.Repeat(1, 10).StringJoin(",")}", // Description = null //}); //var watch = Stopwatch.StartNew(); //list2.ToExcelFile($@"{tempDirPath}\testEntity2.xls"); //watch.Stop(); //Console.WriteLine($"ElapsedMilliseconds: {watch.ElapsedMilliseconds}ms"); ////var listTemp = ExcelHelper.ToEntityList($@"{tempDirPath}\testEntity2.xlsx"); //var dataTableTemp = ExcelHelper.ToDataTable($@"{tempDirPath}\testEntity2.xlsx"); //Console.WriteLine("Press Enter to continue..."); //Console.ReadLine(); var entities = new List() { new TestEntity() { PKID = 1, SettingId = Guid.NewGuid(), SettingName = "Setting1", SettingValue = "Value1", DisplayName = "dd\"d,1" }, new TestEntity() { PKID=2, SettingId = Guid.NewGuid(), SettingName = "Setting2", SettingValue = "Value2", Enabled = true, CreatedBy = "li\"_" }, }; entities.ToCsvFile("test.csv"); var csvFilePath = $@"{tempDirPath}\test.csv"; //entities.ToExcelFileByTemplate( // Path.Combine(ApplicationHelper.AppRoot, "Templates", "testTemplate.xlsx"), // ApplicationHelper.MapPath("templateTestEntities.xlsx"), // extraData: new // { // Author = "WeihanLi", // Title = "Export Result" // } //); entities.ToExcelFile(csvFilePath.Replace(".csv", ".xlsx")); entities.ToCsvFile(csvFilePath); var entitiesT0 = ExcelHelper.ToEntityList(csvFilePath.Replace(".csv", ".xlsx")); var dataTable = entities.ToDataTable(); dataTable.ToCsvFile(csvFilePath.Replace(".csv", ".datatable.csv")); var dt = CsvHelper.ToDataTable(csvFilePath.Replace(".csv", ".datatable.csv")); Console.WriteLine(dt.Columns.Count); var entities1 = CsvHelper.ToEntityList(csvFilePath); entities1[1]!.DisplayName = ",tadadada"; entities1[0]!.SettingValue = "value2,345"; entities1.ToCsvFile(csvFilePath.Replace(".csv", ".1.csv")); entities1.ToDataTable().ToCsvFile(csvFilePath.Replace(".csv", ".1.datatable.csv")); var list = CsvHelper.ToEntityList(csvFilePath.Replace(".csv", ".1.csv")); dt = CsvHelper.ToDataTable(csvFilePath.Replace(".csv", ".1.datatable.csv")); Console.WriteLine(dt.Columns.Count); var entities2 = CsvHelper.ToEntityList(csvFilePath.Replace(".csv", ".1.csv")); entities.ToExcelFile(csvFilePath.Replace(".csv", ".xlsx")); var vals = new[] { 1, 2, 3, 5, 4 }; vals.ToCsvFile(csvFilePath); var numList = CsvHelper.ToEntityList(csvFilePath); Console.WriteLine(numList.StringJoin(",")); Console.ReadLine(); SheetNameTest(); static void SheetNameTest() { List exprotDataList = new List(); for (int i = 0; i < 10; i++) { var temp = new ExcelExportDTO { Name = "张三" + i, Address = "北京海淀" + i, Birthday = DateTime.Now, Remark = "Remark" + i }; exprotDataList.Add(temp); } var setting = FluentSettings.For(); setting.HasSheetConfiguration(1, "我是一个Sheet_111", true); setting.HasSheetSetting(s => { s.SheetName = "Shee-0000"; }); var deskTopFullPath = System.Environment.GetFolderPath(Environment.SpecialFolder.Desktop); var exportFileName = Path.Combine(deskTopFullPath, "Test_for_weihanli.xlsx"); exprotDataList.ToExcelFile(exportFileName); } class TestEntityExcelMappingProfile : IMappingProfile { public void Configure(IExcelConfiguration setting) { // ExcelSetting setting.HasAuthor("WeihanLi") .HasTitle("WeihanLi.Npoi test") .HasDescription("WeihanLi.Npoi test") .HasSubject("WeihanLi.Npoi test") ; setting.HasSheetSetting(config => { config.StartRowIndex = 1; config.SheetName = "SystemSettingsList"; config.AutoColumnWidthEnabled = true; config.RowAction = row => { if (row.RowNum == 0) { var style = row.Sheet.Workbook.CreateCellStyle(); style.Alignment = HorizontalAlignment.Center; var font = row.Sheet.Workbook.CreateFont(); font.FontName = "JetBrains Mono"; font.IsBold = true; font.FontHeight = 200; style.SetFont(font); row.Cells.ForEach(c => c.CellStyle = style); } }; config.CellAction = cell => { if (cell.RowIndex == 0 && cell.StringCellValue == "EntityType") { var enumNames = Enum.GetNames(); var validationHelper = cell.Sheet.GetDataValidationHelper(); var constraint = validationHelper.CreateExplicitListConstraint(enumNames); var addressList = new CellRangeAddressList(1, 3, cell.ColumnIndex, cell.ColumnIndex); // Col B var validation = validationHelper.CreateValidation(constraint, addressList); validation.ShowErrorBox = true; cell.Sheet.AddValidationData(validation); } }; }); // setting.HasFilter(0, 1).HasFreezePane(0, 1, 2, 1); setting.Property(_ => _.SettingId) .HasColumnIndex(0); setting.Property(_ => _.SettingName) .HasColumnTitle("SettingName") .HasColumnIndex(1); setting.Property(_ => _.DisplayName) .HasOutputFormatter((entity, displayName) => $"AAA_{entity?.SettingName}_{displayName}") .HasInputFormatter((entity, originVal) => originVal?.Split(new[] { '_' })[2]) .HasColumnTitle("DisplayName") .HasColumnIndex(2); setting.Property(_ => _.SettingValue) .HasColumnTitle("SettingValue") .HasColumnIndex(3); setting.Property(_ => _.CreatedTime) .HasColumnTitle("CreatedTime") .HasColumnIndex(4) .HasColumnWidth(10) .HasColumnFormatter("yyyy-MM-dd HH:mm:ss"); setting.Property(_ => _.CreatedBy) .HasColumnInputFormatter(x => x += "_test") .HasColumnIndex(4) .HasColumnTitle("CreatedBy"); setting.Property(x => x.Enabled) .HasColumnInputFormatter(val => "Enabled".EqualsIgnoreCase(val)) .HasColumnOutputFormatter(v => v ? "Enabled" : "Disabled"); setting.Property("HiddenProp") .HasOutputFormatter((entity, val) => $"HiddenProp_{entity?.PKID}"); setting.Property(x => x.Type) .HasColumnTitle("EntityType") .HasColumnIndex(8); setting.Property(_ => _.PKID).Ignored(); setting.Property(_ => _.UpdatedBy).Ignored(); setting.Property(_ => _.UpdatedTime).Ignored(); } } internal abstract class BaseEntity { public int PKID { get; set; } } internal class TestEntity : BaseEntity { public Guid SettingId { get; set; } public string? SettingName { get; set; } public string? DisplayName { get; set; } public string? SettingValue { get; set; } public string CreatedBy { get; set; } = "liweihan"; public DateTime CreatedTime { get; set; } = DateTime.Now; public string? UpdatedBy { get; set; } public DateTime UpdatedTime { get; set; } public bool Enabled { get; set; } public EntityType Type { get; set; } } public enum EntityType { Default = 0, Special = 1 } [Sheet(SheetIndex = 0, SheetName = "TestSheet", AutoColumnWidthEnabled = true)] internal class TestEntity2 { [Column(Index = 0)] public int Id { get; set; } [Column(Index = 1)] public string? Title { get; set; } [Column(Index = 2, Width = 50)] public string? Description { get; set; } [Column(Index = 3, Width = 20)] public string? Extra { get; set; } = "{}"; } public class ExcelExportDTO { [Column("姓名")] public string? Name { get; set; } [Column("住址")] public string? Address { get; set; } [Column("出生日期")] public DateTime Birthday { get; set; } public string? Remark { get; set; } } #nullable disable internal sealed class SurveyImportDto { [Column(IsIgnored = true)] public string ExternalId { get; set; } [Column(0)] public string ContentSource { get; set; } [Column(1)] public string ExternalKey { get; set; } [Column(2)] public string Title { get; set; } [Column(3)] public string OptionA { get; set; } [Column(4)] public string OptionB { get; set; } [Column(5)] public string OptionC { get; set; } [Column(6)] public string CorrectAnswer { get; set; } [Column(7)] public string Tips { get; set; } } #nullable restore ================================================ FILE: samples/DotNetCoreSample/TestModel.cs ================================================ // Copyright (c) Weihan Li. All rights reserved. // Licensed under the Apache license. namespace DotNetCoreSample; [Sheet(SheetIndex = 0, SheetName = "Abc", StartRowIndex = 1, EndRowIndex = 10, StartColumnIndex = 1, EndColumnIndex = 19)] internal class ppDto { [Column("创建日期")] public DateTime? CreateDate { get; set; } [Column("体积")] public decimal? Volume { get; set; } [Column("总重量")] public decimal? TotalWeight { get; set; } [Column("出错")] public string? Error { get; set; } [Column("真假")] public Boolean? TrueOrFalse { get; set; } [Column("范围")] public string? Range { get; set; } [Column("从")] public long? CtnFm { get; set; } [Column("到")] public long? CtnTo { get; set; } [Column("开始序列号")] public long? SerialStart { get; set; } [Column("截止序列号")] public long? SerialEnd { get; set; } [Column("包装代码")] public string? PackCode { get; set; } [Column("行号")] public string? Row { get; set; } [Column("买方项目号")] public string? BuyerNo { get; set; } [Column("SKU号")] public string? SKUNo { get; set; } [Column("订单号码")] public string? TradingPO { get; set; } [Column("MAIN LINE #")] public string? Item { get; set; } [Column("Color Name")] public string? ColorCode { get; set; } [Column("Size")] public string? Size { get; set; } [Column("简短描述")] public string? ContractColor { get; set; } [Column("发货方式")] public string? ShipWay { get; set; } [Column("数量")] public int? TtlQty { get; set; } [Column("内部包装的项目数量")] public int? Qty { get; set; } [Column("内包装计数")] public int? RatioQty { get; set; } [Column("箱数")] public int? CntQty { get; set; } [Column("R")] public string? LastCarton { get; set; } [Column("外箱代码")] public string? CartonCode { get; set; } [Column("净净重")] public decimal? NNWeight { get; set; } [Column("净重")] public decimal? NWeight { get; set; } [Column("毛重")] public decimal? GWeight { get; set; } [Column("单位")] public string? Unit { get; set; } [Column("长")] public decimal? cartonL { get; set; } [Column("宽")] public decimal? cartonW { get; set; } [Column("高")] public decimal? cartonH { get; set; } [Column("单位2")] public string? Unit2 { get; set; } [Column("扫描ID")] public string? ScanID { get; set; } } ================================================ FILE: samples/run-file-samples/issue-169.cs ================================================ var filePath = @"C:\Users\Weiha\Downloads\test\2.xlsx"; var workbook = ExcelHelper.LoadExcel( File.OpenRead(filePath), ExcelFormat.Xlsx ); var settings = FluentSettings.For(); settings.WithPostImportAction((x, rowIndex) => x?.RowNum = rowIndex + 1); var list = workbook.ToEntityList(); foreach (var item in list) { Console.WriteLine($"#{item.RowNum}\t => {item.No}\t{item.Name}\t{item.Specification}\t{item.QuantityM}\t{item.Unit}\t{item.TotalAmount4}\t{item.CategoryName}\t{item.IsStop}"); } [Sheet(SheetIndex = 0, StartRowIndex = 6)] public class MaterielDetailDto { public int RowNum { get; set; } /// /// 编号 /// [Column("物料编号", Index = 0)] public string No { get; set; } /// /// 物料名称 /// [Column("物料名称", Index = 1)] public string Name { get; set; } /// /// 规格型号 /// [Column("规格型号", Index = 2)] public string Specification { get; set; } /// /// 特殊库存 /// [Column("特殊库存", Index = 3)] public string QuantityM { get; set; } /// /// 单位 /// [Column("计量单位", Index = 4)] public string Unit { get; set; } [Column("会计年度", Index = 5)] public int Ytime { get; set; } [Column("会计期间", Index = 6)] public int Mtime { get; set; } // 期初余额 [Column("方向", Index = 7)] public string Direction1 { get; set; } /// /// 数量 /// [Column("数量", Index = 8)] public decimal Quantity1 { get; set; } /// /// 单价 /// [Column("实际单价", Index = 9)] public decimal UnitPrice1 { get; set; } /// /// 总金额 /// [Column("实际金额", Index = 10)] public decimal TotalAmount1 { get; set; } /// /// 标准单价 /// [Column("标准单价", Index = 11)] public decimal StandardUnitPrice1 { get; set; } /// /// 标准金额 /// [Column("标准金额", Index = 12)] public decimal StandardotalAmount1 { get; set; } // 期初余额 //本期借方 /// /// 数量 /// [Column("数量", Index = 13)] public decimal Quantity2 { get; set; } /// /// 单价 /// [Column("实际单价", Index = 14)] public decimal UnitPrice2 { get; set; } /// /// 总金额 /// [Column("实际金额", Index = 15)] public decimal TotalAmount2 { get; set; } /// /// 标准单价 /// [Column("标准单价", Index = 16)] public decimal StandardUnitPrice2 { get; set; } /// /// 标准金额 /// [Column("标准金额", Index = 17)] public decimal StandardotalAmount2 { get; set; } //本期贷方 /// /// 数量 /// [Column("数量", Index = 18)] public decimal Quantity3 { get; set; } /// /// 单价 /// [Column("实际单价", Index = 19)] public decimal UnitPrice3 { get; set; } /// /// 总金额 /// [Column("实际金额", Index = 20)] public decimal TotalAmount3 { get; set; } /// /// 标准单价 /// [Column("标准单价", Index = 21)] public decimal StandardUnitPrice3 { get; set; } /// /// 标准金额 /// [Column("标准金额", Index = 22)] public decimal StandardotalAmount3 { get; set; } //期末余额 [Column("方向", Index = 23)] public string Direction2 { get; set; } /// /// 数量 /// [Column("数量", Index = 24)] public decimal Quantity4 { get; set; } /// /// 实际单价 /// [Column("实际单价", Index = 25)] public decimal UnitPrice4 { get; set; } /// /// 总金额 /// [Column("实际金额", Index = 26)] public decimal TotalAmount4 { get; set; } [Column("核算类别名称", Index = 27)] public string CategoryName { get; set; } [Column("是否可用", Index = 28)] public string IsStop { get; set; } } ================================================ FILE: samples/run-file-samples/style-customization-sample.cs ================================================ using NPOI.SS.UserModel; using NPOI.SS.Util; FluentSettings.LoadMappingProfile(); var list = new List { new StyledEntity { Id = 1, Name = "Alice", Amount = 1500.50m, Status = "Approved", Date = DateTime.Now.AddDays(-10) }, new StyledEntity { Id = 2, Name = "Bob", Amount = -200.75m, Status = "Pending", Date = DateTime.Now.AddDays(-5) }, new StyledEntity { Id = 3, Name = "Charlie", Amount = 300.00m, Status = "Rejected", Date = DateTime.Now.AddDays(-2) }, new StyledEntity { Id = 4, Name = "Diana", Amount = 450.25m, Status = "Approved", Date = DateTime.Now.AddDays(-1) }, }; const string path = @"C:\Users\Weiha\Downloads\test\styled-report.xlsx"; list.ToExcelFile(path); Console.WriteLine($"Excel file generated at: {path}"); public class StyledEntity { public int Id { get; set; } public required string Name { get; set; } public decimal Amount { get; set; } public required string Status { get; set; } public DateTime Date { get; set; } } public class StyledEntityProfile : IMappingProfile { public void Configure(IExcelConfiguration configuration) { configuration.HasAuthor("Spark") .HasTitle("Styled Report") .HasDescription("Professional styled Excel report"); configuration.HasSheetSetting(config => { config.SheetName = "Report"; config.StartRowIndex = 1; config.AutoColumnWidthEnabled = true; // Style header row config.RowAction = row => { if (row.RowNum == 0) { var headerStyle = row.Sheet.Workbook.CreateCellStyle(); headerStyle.Alignment = HorizontalAlignment.Center; headerStyle.VerticalAlignment = VerticalAlignment.Center; headerStyle.FillForegroundColor = IndexedColors.Grey25Percent.Index; headerStyle.FillPattern = FillPattern.SolidForeground; var headerFont = row.Sheet.Workbook.CreateFont(); headerFont.FontName = "JetBrains Mono"; headerFont.IsBold = true; headerFont.FontHeight = 240; // 12pt headerStyle.SetFont(headerFont); // Add borders headerStyle.BorderBottom = BorderStyle.Thin; headerStyle.BorderTop = BorderStyle.Thin; headerStyle.BorderLeft = BorderStyle.Thin; headerStyle.BorderRight = BorderStyle.Thin; row.Cells.ForEach(c => c.CellStyle = headerStyle); } }; // Add validation and conditional formatting config.CellAction = cell => { // Add validation for status column if (cell.RowIndex == 0 && cell.StringCellValue == "Status") { var validationHelper = cell.Sheet.GetDataValidationHelper(); var statusList = new[] { "Approved", "Pending", "Rejected" }; var constraint = validationHelper.CreateExplicitListConstraint(statusList); var addressList = new CellRangeAddressList(1, 1000, cell.ColumnIndex, cell.ColumnIndex); var validation = validationHelper.CreateValidation(constraint, addressList); validation.ShowErrorBox = true; cell.Sheet.AddValidationData(validation); } // Highlight negative amounts in red if (cell.RowIndex > 0 && cell.ColumnIndex == 2) // Amount column { try { if (cell.NumericCellValue < 0) { var redStyle = cell.Sheet.Workbook.CreateCellStyle(); var redFont = cell.Sheet.Workbook.CreateFont(); redFont.Color = IndexedColors.Red.Index; redFont.IsBold = true; redStyle.SetFont(redFont); cell.CellStyle = redStyle; } } catch { } // Skip if not a numeric cell } }; }); // Configure properties configuration.Property(x => x.Id).HasColumnIndex(0); configuration.Property(x => x.Name).HasColumnIndex(1); configuration.Property(x => x.Amount).HasColumnIndex(2); configuration.Property(x => x.Status).HasColumnIndex(3); configuration.Property(x => x.Date) .HasColumnIndex(4) .HasColumnFormatter("yyyy-MM-dd"); } } ================================================ FILE: src/Directory.Build.props ================================================ true $(NoWarn);1591 https://github.com/WeihanLi/WeihanLi.Common true true true snupkg false https://github.com/WeihanLi/WeihanLi.Common/blob/dev/docs/ReleaseNotes.md icon.jpg README.md MIT true ================================================ FILE: src/WeihanLi.Npoi/Abstract/ICell.cs ================================================ // Copyright (c) Weihan Li. All rights reserved. // Licensed under the Apache license. namespace WeihanLi.Npoi.Abstract; internal interface ICell { CellType CellType { get; set; } object? Value { get; set; } } internal enum CellType { Unknown = -1, // 0xFFFFFFFF String = 0, Numeric = 1, Formula = 2, Blank = 3, Boolean = 4, Error = 5 } ================================================ FILE: src/WeihanLi.Npoi/Abstract/IRow.cs ================================================ // Copyright (c) Weihan Li. All rights reserved. // Licensed under the Apache license. namespace WeihanLi.Npoi.Abstract; internal interface IRow { /// /// Gets the number of defined cells (NOT number of cells in the actual row!). /// That is to say if only columns 0,4,5 have values then there would be 3. /// /// int representing the number of defined cells in the row. int CellsCount { get; } /// /// 1-based column number of the first cell /// int FirstCellNum { get; } /// /// 1-based column number of the last cell /// int LastCellNum { get; } /// /// UnderlyingValue /// object? UnderlyingValue { get; } ICell? GetCell(int cellIndex); /// /// Create a cell /// /// /// cellIndex /// maxValue: (255 for *.xls, 1048576 for *.xlsx) /// /// ICell CreateCell(int cellIndex); } ================================================ FILE: src/WeihanLi.Npoi/Abstract/ISheet.cs ================================================ // Copyright (c) Weihan Li. All rights reserved. // Licensed under the Apache license. namespace WeihanLi.Npoi.Abstract; internal interface ISheet { /// /// FirstRowNum, 1 based rowNum /// 0 if no rows here /// int FirstRowNum { get; } /// /// lastRowIndex +1, 1 based rowNum /// 0 if no rows here /// int LastRowNum { get; } IRow? GetRow(int rowIndex); IRow CreateRow(int rowIndex); void SetColumnWidth(int columnIndex, int width); void AutoSizeColumn(int columnIndex); void CreateFreezePane(int colSplit, int rowSplit, int leftMostCol, int topRow); void SetAutoFilter(int firstRowIndex, int lastRowIndex, int firstColumnIndex, int lastColumnIndex); void ShiftRows(int startRow, int endRow, int n); IRow CopyRow(int sourceIndex, int targetIndex); void RemoveRow(IRow row); } ================================================ FILE: src/WeihanLi.Npoi/Abstract/IWorkbook.cs ================================================ // Copyright (c) Weihan Li. All rights reserved. // Licensed under the Apache license. namespace WeihanLi.Npoi.Abstract; internal interface IWorkbook { int SheetCount { get; } ISheet? GetSheet(int sheetIndex); ISheet CreateSheet(string sheetName); byte[] ToBytes(); } ================================================ FILE: src/WeihanLi.Npoi/Abstract/NPOIWorkbook.cs ================================================ // Copyright (c) Weihan Li. All rights reserved. // Licensed under the Apache license. using NPOI.SS.Util; using NModel = NPOI.SS.UserModel; namespace WeihanLi.Npoi.Abstract; /// /// Thin adapter that exposes the internal NPOI workbook via the abstraction interfaces. /// internal sealed class NPOIWorkbook : IWorkbook { private readonly NModel.IWorkbook _workbook; /// /// Creates a new adapter for the provided NPOI workbook. /// /// Underlying workbook instance. public NPOIWorkbook(NModel.IWorkbook workbook) => _workbook = workbook; /// /// Gets the number of sheets available. /// public int SheetCount => _workbook.NumberOfSheets; /// /// Wraps the specified sheet index in an adapter. /// public ISheet GetSheet(int sheetIndex) => new NPOISheet(_workbook.GetSheetAt(sheetIndex)); /// /// Creates a new sheet and returns the adapter around it. /// public ISheet CreateSheet(string sheetName) => new NPOISheet(_workbook.CreateSheet(sheetName)); /// /// Serializes the workbook to bytes using helper extensions. /// public byte[] ToBytes() => _workbook.ToExcelBytes(); } /// /// Adapter for . /// internal sealed class NPOISheet : ISheet { private readonly NModel.ISheet _sheet; /// /// Initializes the adapter with the underlying sheet. /// public NPOISheet(NModel.ISheet sheet) => _sheet = sheet; /// /// Gets the first row index using one-based indexing to align with the abstractions. /// public int FirstRowNum => _sheet.FirstRowNum + 1; /// /// Gets the last row index using one-based indexing to align with the abstractions. /// public int LastRowNum => _sheet.LastRowNum + 1; /// /// Retrieves the requested row and wraps it, if present. /// public IRow? GetRow(int rowIndex) { var nRow = _sheet.GetRow(rowIndex); if (null == nRow) { return null; } return new NPOIRow(nRow); } /// /// Creates a new row and returns its adapter. /// public IRow CreateRow(int rowIndex) => new NPOIRow(_sheet.CreateRow(rowIndex)); /// /// Sets the column width in the underlying sheet. /// public void SetColumnWidth(int columnIndex, int width) => _sheet.SetColumnWidth(columnIndex, width); /// /// Auto sizes the requested column. /// public void AutoSizeColumn(int columnIndex) => _sheet.AutoSizeColumn(columnIndex); /// /// Applies a freeze pane to the sheet. /// public void CreateFreezePane(int colSplit, int rowSplit, int leftMostCol, int topRow) => _sheet.CreateFreezePane(colSplit, rowSplit, leftMostCol, topRow); /// /// Applies an auto-filter range to the sheet. /// public void SetAutoFilter(int firstRowIndex, int lastRowIndex, int firstColumnIndex, int lastColumnIndex) => _sheet.SetAutoFilter(new CellRangeAddress(firstRowIndex, lastRowIndex, firstColumnIndex, lastColumnIndex)); /// /// Shifts the specified row range. /// public void ShiftRows(int startRow, int endRow, int n) => _sheet.ShiftRows(startRow, endRow, n); /// /// Copies a row and wraps the result. /// public IRow CopyRow(int sourceIndex, int targetIndex) => new NPOIRow(_sheet.CopyRow(sourceIndex, targetIndex)); /// /// Removes the given row from the sheet. /// public void RemoveRow(IRow row) => _sheet.RemoveRow(row.UnderlyingValue as NModel.IRow); } /// /// Adapter for . /// internal sealed class NPOIRow : IRow { private readonly NModel.IRow _row; /// /// Initializes the adapter with the underlying row. /// public NPOIRow(NModel.IRow row) => _row = row; /// /// Gets the number of physical cells. /// public int CellsCount => _row.PhysicalNumberOfCells; /// /// Gets the first cell index using one-based indexing. /// public int FirstCellNum => _row.FirstCellNum + 1; /// /// Gets the last cell index. /// public int LastCellNum => _row.LastCellNum; /// /// Retrieves and wraps the specified cell. /// public ICell? GetCell(int cellIndex) { var nCell = _row.GetCell(cellIndex); if (nCell is null) { return null; } return new NPOICell(nCell); } /// /// Creates and wraps a new cell. /// public ICell CreateCell(int cellIndex) => new NPOICell(_row.CreateCell(cellIndex)); /// /// Provides direct access to the underlying NPOI object. /// public object UnderlyingValue => _row; } /// /// Adapter for . /// internal sealed class NPOICell : ICell { private readonly NModel.ICell _cell; /// /// Initializes the adapter with the underlying cell. /// public NPOICell(NModel.ICell cell) => _cell = cell; /// /// Gets or sets the cell type using the abstraction enum. /// public CellType CellType { get => (CellType)Enum.Parse(typeof(CellType), _cell.CellType.ToString()); set => _cell.SetCellType((NModel.CellType)Enum.Parse(typeof(NModel.CellType), value.ToString())); } /// /// Gets or sets the cell value while translating common NPOI types. /// public object? Value { get { if (_cell.CellType == NModel.CellType.Blank || _cell.CellType == NModel.CellType.Error) { return null; } switch (_cell.CellType) { case NModel.CellType.Numeric: if (NModel.DateUtil.IsCellDateFormatted(_cell)) { return _cell.DateCellValue; } return _cell.NumericCellValue; case NModel.CellType.String: return _cell.StringCellValue; case NModel.CellType.Boolean: return _cell.BooleanCellValue; default: return _cell.ToString(); } } set => _cell.SetCellValue(value ?? string.Empty); } } ================================================ FILE: src/WeihanLi.Npoi/Attributes/ColumnAttribute.cs ================================================ // Copyright (c) Weihan Li. All rights reserved. // Licensed under the Apache license. using WeihanLi.Npoi.Configurations; namespace WeihanLi.Npoi.Attributes; /// /// Describes column-level metadata for a property. /// [AttributeUsage(AttributeTargets.Property)] public sealed class ColumnAttribute : Attribute { /// /// Initializes a column attribute with default configuration. /// public ColumnAttribute() => PropertyConfiguration = new PropertyConfiguration(); /// /// Initializes a column attribute targeting the specified index. /// /// Target column index. public ColumnAttribute(int index) => PropertyConfiguration = new PropertyConfiguration { ColumnIndex = index }; /// /// Initializes a column attribute with the provided title. /// /// Column header title. public ColumnAttribute(string title) => PropertyConfiguration = new PropertyConfiguration { ColumnTitle = title ?? throw new ArgumentNullException(nameof(title)) }; internal PropertyConfiguration PropertyConfiguration { get; } /// /// ColumnIndex /// public int Index { get => PropertyConfiguration.ColumnIndex; set { if (value >= 0) { PropertyConfiguration.ColumnIndex = value; } } } /// /// ColumnTitle /// public string Title { get => PropertyConfiguration.ColumnTitle; set => PropertyConfiguration.ColumnTitle = value; } /// /// Formatter /// public string? Formatter { get => PropertyConfiguration.ColumnFormatter; set => PropertyConfiguration.ColumnFormatter = value; } /// /// IsIgnored /// public bool IsIgnored { get => PropertyConfiguration.IsIgnored; set => PropertyConfiguration.IsIgnored = value; } /// /// ColumnWidth /// Characters Count /// public int Width { get => PropertyConfiguration.ColumnWidth; set => PropertyConfiguration.ColumnWidth = value; } } ================================================ FILE: src/WeihanLi.Npoi/Attributes/FilterAttribute.cs ================================================ // Copyright (c) Weihan Li. All rights reserved. // Licensed under the Apache license. using WeihanLi.Npoi.Settings; namespace WeihanLi.Npoi.Attributes; /// /// Specifies the auto-filter range that should be applied to a sheet. /// [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] public sealed class FilterAttribute : Attribute { /// /// Initializes the attribute for a single column. /// public FilterAttribute(int firstColumn) : this(firstColumn, null) { } /// /// Initializes the attribute for an explicit column range. /// /// First column index. /// Last column index. public FilterAttribute(int firstColumn, int? lastColumn) => FilterSetting = new FilterSetting(firstColumn, lastColumn); internal FilterSetting FilterSetting { get; } /// /// Gets or sets the first column index. /// public int FirstColumn => FilterSetting.FirstColumn; /// /// Gets or sets the last column index. /// public int? LastColumn => FilterSetting.LastColumn; } ================================================ FILE: src/WeihanLi.Npoi/Attributes/FreezeAttribute.cs ================================================ // Copyright (c) Weihan Li. All rights reserved. // Licensed under the Apache license. using WeihanLi.Npoi.Settings; namespace WeihanLi.Npoi.Attributes; /// /// Declares a freeze pane for a mapped sheet. /// [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] public sealed class FreezeAttribute : Attribute { /// /// Initializes a freeze pane using default anchors. /// public FreezeAttribute(int colSplit, int rowSplit) : this(colSplit, rowSplit, 0, 1) { } /// /// Initializes a freeze pane with explicit anchors. /// /// Horizontal split position. /// Vertical split position. /// Left column visible in right pane. /// Top row visible in bottom pane. public FreezeAttribute(int colSplit, int rowSplit, int leftmostColumn, int topRow) => FreezeSetting = new FreezeSetting(colSplit, rowSplit, leftmostColumn, topRow); internal FreezeSetting FreezeSetting { get; } /// /// Horizontal position of split /// public int ColSplit => FreezeSetting.ColSplit; /// /// Vertical position of split /// public int RowSplit => FreezeSetting.RowSplit; /// /// Top row visible in bottom pane /// public int LeftMostColumn => FreezeSetting.LeftMostColumn; /// /// Left column visible in right pane /// public int TopRow => FreezeSetting.TopRow; } ================================================ FILE: src/WeihanLi.Npoi/Attributes/SheetAttribute.cs ================================================ // Copyright (c) Weihan Li. All rights reserved. // Licensed under the Apache license. using WeihanLi.Npoi.Settings; namespace WeihanLi.Npoi.Attributes; /// /// Declares per-sheet metadata for an entity mapping. /// [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] public sealed class SheetAttribute : Attribute { private int _endColumnIndex = -1; private int _startColumnIndex; /// /// Initializes an attribute backed by a fresh . /// public SheetAttribute() => SheetSetting = new SheetSetting(); /// /// Target sheet index (zero-based). /// public int SheetIndex { get; set; } /// /// Gets or sets the sheet name override. /// public string SheetName { get => SheetSetting.SheetName; set => SheetSetting.SheetName = value ?? throw new ArgumentNullException(nameof(value)); } /// /// Gets or sets the first row to start reading/writing (zero-based). /// public int StartRowIndex { get => SheetSetting.StartRowIndex; set => SheetSetting.StartRowIndex = value; } /// /// Gets the header row index. /// public int HeaderRowIndex => SheetSetting.HeaderRowIndex; /// /// Gets or sets the last row (inclusive) participating in the mapping. /// public int EndRowIndex { get => SheetSetting.EndRowIndex ?? -1; set => SheetSetting.EndRowIndex = value >= 0 ? value : -1; } /// /// StartColumnIndex /// Start Column Index when import /// public int StartColumnIndex { get => _startColumnIndex; set { if (value >= 0) { _startColumnIndex = value; if (_endColumnIndex >= value) { SheetSetting.CellFilter = cell => cell.ColumnIndex >= _startColumnIndex && cell.ColumnIndex <= _endColumnIndex ; } else { SheetSetting.CellFilter = cell => cell.ColumnIndex >= _startColumnIndex; } } } } /// /// EndColumnIndex /// End Column Index when import /// public int EndColumnIndex { get => _endColumnIndex; set { if (value >= _startColumnIndex) { _endColumnIndex = value; SheetSetting.CellFilter = cell => cell.ColumnIndex >= _startColumnIndex && cell.ColumnIndex <= _endColumnIndex ; } else { SheetSetting.CellFilter = cell => cell.ColumnIndex >= _startColumnIndex; _endColumnIndex = -1; } } } /// /// Gets or sets whether column widths should be auto-sized. /// public bool AutoColumnWidthEnabled { get => SheetSetting.AutoColumnWidthEnabled; set => SheetSetting.AutoColumnWidthEnabled = value; } internal SheetSetting SheetSetting { get; } } ================================================ FILE: src/WeihanLi.Npoi/CellPosition.cs ================================================ // Copyright (c) Weihan Li. All rights reserved. // Licensed under the Apache license. namespace WeihanLi.Npoi; /// /// Represents a zero-based row/column coordinate within a sheet. /// /// Row index. /// Column index. public readonly record struct CellPosition(int Row, int Column); ================================================ FILE: src/WeihanLi.Npoi/Compat.cs ================================================ // Copyright (c) Weihan Li. All rights reserved. // Licensed under the Apache license. #if NETSTANDARD // ReSharper disable once CheckNamespace namespace System.Runtime.CompilerServices; internal sealed class IsExternalInit; #endif ================================================ FILE: src/WeihanLi.Npoi/ConfigurationExtensions.cs ================================================ // Copyright (c) Weihan Li. All rights reserved. // Licensed under the Apache license. using WeihanLi.Common.Services; using WeihanLi.Npoi.Configurations; namespace WeihanLi.Npoi; /// /// Provides convenience extension methods for configuring Excel import/export metadata. /// public static class ConfigurationExtensions { /// /// Sheet Configuration /// /// excel configuration /// sheetIndex /// sheetName /// current excel configuration public static IExcelConfiguration HasSheetConfiguration(this IExcelConfiguration configuration, int sheetIndex, string sheetName) => configuration.HasSheetSetting(config => { config.SheetName = sheetName; }, sheetIndex); /// /// Sheet Configuration /// /// excel configuration /// sheetIndex /// sheetName /// enable auto column width if true otherwise false /// current excel configuration public static IExcelConfiguration HasSheetConfiguration(this IExcelConfiguration configuration, int sheetIndex, string sheetName, bool enableAutoColumnWidth) => configuration.HasSheetSetting(config => { config.SheetName = sheetName; config.AutoColumnWidthEnabled = enableAutoColumnWidth; }, sheetIndex); /// /// Sheet Configuration /// /// excel configuration /// sheetIndex /// sheetName /// startRowIndex /// current excel configuration public static IExcelConfiguration HasSheetConfiguration(this IExcelConfiguration configuration, int sheetIndex, string sheetName, int startRowIndex) => configuration.HasSheetSetting(config => { config.SheetName = sheetName; config.StartRowIndex = startRowIndex; }, sheetIndex); /// /// Sheet Configuration /// /// excel configuration /// sheetIndex /// sheetName /// startRowIndex /// enable auto column width if true otherwise false /// endRowIndex, set this if you wanna control where to end(included) /// current excel configuration public static IExcelConfiguration HasSheetConfiguration(this IExcelConfiguration configuration, int sheetIndex, string sheetName, int startRowIndex, bool enableAutoColumnWidth, int? endRowIndex = null) => configuration.HasSheetSetting(config => { config.SheetName = sheetName; config.StartRowIndex = startRowIndex; config.AutoColumnWidthEnabled = enableAutoColumnWidth; config.EndRowIndex = endRowIndex; }, sheetIndex); /// /// Configure excel author /// /// excel configuration /// excel document author name /// current excel configuration public static IExcelConfiguration HasAuthor(this IExcelConfiguration configuration, string author) => configuration.HasExcelSetting(setting => { setting.Author = author; }); /// /// Configure excel author /// /// excel configuration /// excel document title /// current excel configuration public static IExcelConfiguration HasTitle(this IExcelConfiguration configuration, string title) => configuration.HasExcelSetting(setting => { setting.Title = title; }); /// /// Configure excel author /// /// excel configuration /// excel document description /// current excel configuration public static IExcelConfiguration HasDescription(this IExcelConfiguration configuration, string description) => configuration.HasExcelSetting(setting => { setting.Description = description; }); /// /// Configure excel author /// /// excel configuration /// excel document subject /// current excel configuration public static IExcelConfiguration HasSubject(this IExcelConfiguration configuration, string subject) => configuration.HasExcelSetting(setting => { setting.Subject = subject; }); /// /// Configure excel author /// /// excel configuration /// excel document company /// current excel configuration public static IExcelConfiguration HasCompany(this IExcelConfiguration configuration, string company) => configuration.HasExcelSetting(setting => { setting.Company = company; }); /// /// Configure excel author /// /// excel configuration /// excel document category /// current excel configuration public static IExcelConfiguration HasCategory(this IExcelConfiguration configuration, string category) => configuration.HasExcelSetting(setting => { setting.Category = category; }); /// /// excel data validator /// /// configuration /// validator /// entity type /// current configuration public static IExcelConfiguration WithValidator(this IExcelConfiguration configuration, IValidator validator) { return configuration.WithValidator(validator.GetCommonValidator()); } /// /// property configuration /// /// TEntity /// excelConfiguration /// propertyName /// PropertyConfiguration public static IPropertyConfiguration Property( this IExcelConfiguration excelConfiguration, string propertyName) => excelConfiguration.Property(propertyName); /// /// has column output formatter /// /// entity type /// property type /// property configuration /// column output formatter /// property configuration public static IPropertyConfiguration HasColumnOutputFormatter( this IPropertyConfiguration configuration, Func? formatter) { if (formatter is null) { return configuration.HasOutputFormatter(null); } return configuration.HasOutputFormatter((_, prop) => formatter.Invoke(prop)); } } ================================================ FILE: src/WeihanLi.Npoi/Configurations/CsvOptions.cs ================================================ // Copyright (c) Weihan Li. All rights reserved. // Licensed under the Apache license. using System.Text; namespace WeihanLi.Npoi.Configurations; /// /// Represents the configurable behaviors of the CSV helper. /// public sealed class CsvOptions { /// /// Gets the separator character expressed as a string. /// public string SeparatorString => new(SeparatorCharacter, 1); /// /// Gets the quote character expressed as a string. /// public string QuoteString => new(QuoteCharacter, 1); /// /// Gets or sets the separator used between values. /// public char SeparatorCharacter { get; set; } /// /// Gets or sets the character used to wrap textual values. /// public char QuoteCharacter { get; set; } /// /// Gets or sets whether the header row should be emitted. /// public bool IncludeHeader { get; set; } /// /// Gets or sets the synthetic property name to use for basic types. /// public string PropertyNameForBasicType { get; set; } /// /// Gets or sets the encoding of the generated CSV. /// public Encoding Encoding { get; set; } /// /// Initializes options with default separator, quote, and encoding. /// public CsvOptions() { SeparatorCharacter = CsvHelper.CsvSeparatorCharacter; QuoteCharacter = CsvHelper.CsvQuoteCharacter; IncludeHeader = true; PropertyNameForBasicType = InternalConstants.DefaultPropertyNameForBasicType; Encoding = Encoding.UTF8; } /// /// Provides a shared instance representing sensible defaults. /// public static readonly CsvOptions Default = new() { SeparatorCharacter = ',', QuoteCharacter = '"', PropertyNameForBasicType = InternalConstants.DefaultPropertyNameForBasicType }; } ================================================ FILE: src/WeihanLi.Npoi/Configurations/ExcelConfiguration.cs ================================================ // Copyright (c) Weihan Li. All rights reserved. // Licensed under the Apache license. using System.Linq.Expressions; using System.Reflection; using WeihanLi.Common; using WeihanLi.Common.Services; using WeihanLi.Extensions; using WeihanLi.Npoi.Settings; namespace WeihanLi.Npoi.Configurations; internal abstract class ExcelConfiguration : IExcelConfiguration { /// /// PropertyConfigurationDictionary /// public IDictionary PropertyConfigurationDictionary { get; } = new Dictionary(); /// /// Gets the Excel-level document metadata. /// public ExcelSetting ExcelSetting { get; } = ExcelHelper.DefaultExcelSetting; /// /// Gets the configured freeze panes for the workbook. /// public IList FreezeSettings { get; } = new List(); /// /// Gets or sets the filter configuration for the sheet. /// public FilterSetting? FilterSetting { get; set; } /// /// Gets the registered sheet settings keyed by sheet index. /// public IDictionary SheetSettings { get; } = new Dictionary { { 0, new SheetSetting() } }; #region ExcelSettings FluentAPI /// /// Updates the Excel document metadata in a fluent manner. /// /// Configuration delegate. /// The current configuration instance. public IExcelConfiguration HasExcelSetting(Action configAction) { // ReSharper disable once ConditionalAccessQualifierIsNonNullableAccordingToAPIContract // allow nullable but we do not want a null configAction?.Invoke(ExcelSetting); return this; } #endregion ExcelSettings FluentAPI #region Sheet /// /// Updates the sheet configuration for the specified index. /// /// Configuration delegate. /// Target sheet index. /// The current configuration instance. public IExcelConfiguration HasSheetSetting(Action configAction, int sheetIndex = 0) { if (configAction is null) { throw new ArgumentNullException(nameof(configAction)); } if (sheetIndex >= 0) { if (!SheetSettings.TryGetValue(sheetIndex, out var sheetSetting)) { SheetSettings[sheetIndex] = sheetSetting = new SheetSetting(); } configAction.Invoke(sheetSetting); } return this; } #endregion Sheet #region FreezePane /// /// Adds a freeze pane using default anchor values. /// public IExcelConfiguration HasFreezePane(int colSplit, int rowSplit) { FreezeSettings.Add(new FreezeSetting(colSplit, rowSplit)); return this; } /// /// Adds a freeze pane with the specified anchor. /// public IExcelConfiguration HasFreezePane(int colSplit, int rowSplit, int leftmostColumn, int topRow) { FreezeSettings.Add(new FreezeSetting(colSplit, rowSplit, leftmostColumn, topRow)); return this; } #endregion FreezePane #region Filter /// /// Adds an auto-filter that starts at the specified column. /// public IExcelConfiguration HasFilter(int firstColumn) => HasFilter(firstColumn, null); /// /// Adds an auto-filter covering the specified column range. /// public IExcelConfiguration HasFilter(int firstColumn, int? lastColumn) { FilterSetting = new FilterSetting(firstColumn, lastColumn); return this; } #endregion Filter } internal sealed class ExcelConfiguration : ExcelConfiguration, IExcelConfiguration { /// /// Gets the entity type represented by this configuration. /// public Type EntityType => typeof(TEntity); internal Func? DataFilter { get; private set; } internal Action? PostImportAction { get; private set; } internal IComparer? PropertyComparer { get; private set; } internal IValidator? Validator { get; private set; } #region Property /// /// Assigns a custom validator. /// public IExcelConfiguration WithValidator(IValidator? validator) { Validator = validator; return this; } /// /// Applies a data filter that can skip entities during export. /// public IExcelConfiguration WithDataFilter(Func? dataFilter) { DataFilter = dataFilter; return this; } public IExcelConfiguration WithPostImportAction(Action? postAction) { PostImportAction = postAction; return this; } /// /// Controls the ordering of properties. /// public IExcelConfiguration WithPropertyComparer(IComparer? propertyComparer) { PropertyComparer = propertyComparer; return this; } /// /// Gets the property configuration by the specified property expression for the specified /// and its . /// /// The . /// The property expression. /// The type of parameter. public IPropertyConfiguration Property( Expression> propertyExpression) { var memberInfo = propertyExpression.GetMemberInfo(); if (memberInfo is PropertyInfo property && PropertyConfigurationDictionary.TryGetValue(property, out var propertyConfiguration)) return (IPropertyConfiguration)propertyConfiguration; property = CacheUtil.GetTypeProperties(EntityType).FirstOrDefault(p => p.Name == memberInfo.Name) ?? throw new InvalidOperationException($"the property [{memberInfo.Name}] does not exists"); return (IPropertyConfiguration)PropertyConfigurationDictionary[property]; } /// /// Retrieves (or builds) a configuration for the specified property name. /// /// Property type. /// Property name. /// Property configuration. public IPropertyConfiguration Property(string propertyName) { var property = PropertyConfigurationDictionary.Keys.FirstOrDefault(p => p.Name == propertyName); if (property is not null) { return (IPropertyConfiguration)PropertyConfigurationDictionary[property]; } var propertyType = typeof(TProperty); property = new FakePropertyInfo(EntityType, propertyType, propertyName); var propertyConfigurationType = typeof(PropertyConfiguration<,>).MakeGenericType(EntityType, propertyType); var propertyConfiguration = (PropertyConfiguration)Guard.NotNull(Activator.CreateInstance(propertyConfigurationType, property)); PropertyConfigurationDictionary[property] = propertyConfiguration; return (IPropertyConfiguration)propertyConfiguration; } #endregion Property } ================================================ FILE: src/WeihanLi.Npoi/Configurations/IExcelConfiguration.cs ================================================ // Copyright (c) Weihan Li. All rights reserved. // Licensed under the Apache license. using System.Linq.Expressions; using System.Reflection; using WeihanLi.Common.Services; using WeihanLi.Npoi.Settings; namespace WeihanLi.Npoi.Configurations; /// /// Abstraction describing the fluent configuration surface exposed to consumers. /// public interface IExcelConfiguration { /// /// Sheet Configuration /// /// sheet config delegate /// sheetIndex, 0 is the default value /// current excel configuration IExcelConfiguration HasSheetSetting(Action configAction, int sheetIndex = 0); /// /// excel setting configure /// /// config delegate /// current excel configuration IExcelConfiguration HasExcelSetting(Action configAction); /// /// setting freeze pane /// Creates a split (freeze pane). Any existing freeze pane or split pane is overwritten. /// /// Horizontal position of split /// Vertical position of split /// current excel configuration IExcelConfiguration HasFreezePane(int colSplit, int rowSplit); /// /// setting freeze pane /// Creates a split (freeze pane). Any existing freeze pane or split pane is overwritten. /// /// Horizontal position of split /// Vertical position of split /// Top row visible in bottom pane /// Left column visible in right pane /// current excel configuration IExcelConfiguration HasFreezePane(int colSplit, int rowSplit, int leftmostColumn, int topRow); /// /// setting filter /// /// firstCol Index of first column /// current excel configuration IExcelConfiguration HasFilter(int firstColumn); /// /// setting filter /// /// firstCol Index of first column /// lastCol Index of last column (inclusive), must be equal to or larger than {@code firstCol} /// current excel configuration IExcelConfiguration HasFilter(int firstColumn, int? lastColumn); } /// /// Strongly typed configuration contract for a specific entity. /// /// Entity type. public interface IExcelConfiguration : IExcelConfiguration { /// /// register validator for excel import /// /// validator /// current excel configuration IExcelConfiguration WithValidator(IValidator? validator); /// /// register data filter /// /// data filter logic /// current excel configuration IExcelConfiguration WithDataFilter(Func? dataFilter); /// /// register post action for T and rowIndex based func /// /// postAction /// IExcelConfiguration WithPostImportAction(Action? postAction); /// /// register property comparer /// /// propertyComparer /// current excel configuration IExcelConfiguration WithPropertyComparer(IComparer? propertyComparer); /// /// property configuration /// /// PropertyType /// propertyExpression to get property info /// current property configuration IPropertyConfiguration Property( Expression> propertyExpression); /// /// property configuration /// /// PropertyType /// propertyName /// current property configuration IPropertyConfiguration Property(string propertyName); } ================================================ FILE: src/WeihanLi.Npoi/Configurations/IPropertyConfiguration.cs ================================================ // Copyright (c) Weihan Li. All rights reserved. // Licensed under the Apache license. using NPOI.SS.UserModel; namespace WeihanLi.Npoi.Configurations; /// /// PropertyConfiguration /// public interface IPropertyConfiguration; /// /// Describes the fluent property-level configuration API for an entity. /// /// Entity type. /// Property type. public interface IPropertyConfiguration : IPropertyConfiguration { /// /// HasColumnIndex /// /// index /// IPropertyConfiguration HasColumnIndex(int index); /// /// HasColumnWidth /// /// width /// IPropertyConfiguration HasColumnWidth(int width); /// /// HasColumnTitle /// /// title /// IPropertyConfiguration HasColumnTitle(string title); /// /// HasColumnFormatter /// /// formatter /// IPropertyConfiguration HasColumnFormatter(string? formatter); /// /// Ignored /// /// IPropertyConfiguration Ignored(bool ignored = true); /// /// HasCellReader(For excel only) /// /// custom cell value reader /// IPropertyConfiguration HasCellReader( Func? cellReader); /// /// HasColumnInputFormatter /// /// formatterFunc /// IPropertyConfiguration HasColumnInputFormatter(Func? formatterFunc); /// /// HasOutputFormatter /// /// columnFormatter /// IPropertyConfiguration HasOutputFormatter( Func? formatterFunc); /// /// HasInputFormatter /// /// columnFormatter /// IPropertyConfiguration HasInputFormatter( Func? formatterFunc); } ================================================ FILE: src/WeihanLi.Npoi/Configurations/PropertyConfiguration.cs ================================================ // Copyright (c) Weihan Li. All rights reserved. // Licensed under the Apache license. using NPOI.SS.UserModel; using System.Reflection; using WeihanLi.Extensions; namespace WeihanLi.Npoi.Configurations; internal class PropertyConfiguration : IPropertyConfiguration { /// /// ColumnIndex /// public int ColumnIndex { get; set; } = -1; /// /// ColumnWidth /// public int ColumnWidth { get; set; } /// /// Title /// public string ColumnTitle { get; set; } = string.Empty; /// /// Formatter /// public string? ColumnFormatter { get; set; } /// /// the property is ignored. /// public bool IsIgnored { get; set; } /// /// PropertyName /// public string? PropertyName { get; set; } } internal sealed class PropertyConfiguration : PropertyConfiguration, IPropertyConfiguration { private readonly PropertyInfo _propertyInfo; /// /// Initializes a configuration wrapper for the specified property. /// /// Property metadata. public PropertyConfiguration(PropertyInfo propertyInfo) { _propertyInfo = propertyInfo; PropertyName = propertyInfo.Name; ColumnTitle = propertyInfo.Name; } /// /// Sets the column index explicitly. /// public IPropertyConfiguration HasColumnIndex(int index) { if (index >= 0) { ColumnIndex = index; } return this; } /// /// Assigns the header text used for the column. /// public IPropertyConfiguration HasColumnTitle(string title) { ColumnTitle = title ?? throw new ArgumentNullException(nameof(title)); return this; } /// /// Sets the column width (characters). /// public IPropertyConfiguration HasColumnWidth(int width) { ColumnWidth = width; return this; } /// /// Assigns the formatter string used when writing out the column. /// public IPropertyConfiguration HasColumnFormatter(string? formatter) { ColumnFormatter = formatter; return this; } /// /// Marks the property as ignored when exporting/importing. /// public IPropertyConfiguration Ignored(bool ignored = true) { IsIgnored = ignored; return this; } /// /// Registers a custom cell reader for imports. /// public IPropertyConfiguration HasCellReader(Func? cellReader) { InternalCache.CellReaderFuncCache.AddOrUpdate(_propertyInfo, cellReader); return this; } /// /// Registers a formatter used when exporting the property. /// public IPropertyConfiguration HasOutputFormatter( Func? formatterFunc) { InternalCache.OutputFormatterFuncCache.AddOrUpdate(_propertyInfo, formatterFunc); return this; } /// /// Registers a formatter used when importing cell values. /// public IPropertyConfiguration HasInputFormatter( Func? formatterFunc) { InternalCache.InputFormatterFuncCache.AddOrUpdate(_propertyInfo, formatterFunc); return this; } /// /// Registers a formatter that manipulates the raw column text before parsing. /// public IPropertyConfiguration HasColumnInputFormatter( Func? formatterFunc) { InternalCache.ColumnInputFormatterFuncCache.AddOrUpdate(_propertyInfo, formatterFunc); return this; } } ================================================ FILE: src/WeihanLi.Npoi/CsvHelper.cs ================================================ // Copyright (c) Weihan Li. All rights reserved. // Licensed under the Apache license. using System.Data; using System.Diagnostics; using System.Text; using WeihanLi.Common; using WeihanLi.Common.Helpers; using WeihanLi.Extensions; using WeihanLi.Npoi.Configurations; namespace WeihanLi.Npoi; /// /// CsvHelper provides utilities for reading and writing CSV files, /// supporting conversion between CSV data and DataTables or strongly-typed entities. /// public static class CsvHelper { /// /// CSV separator character, ',' by default. /// Can be changed to support different CSV formats (e.g., ';' for European format). /// public static char CsvSeparatorCharacter = ','; /// /// CSV quote character used to escape values containing special characters, " by default. /// Values containing the separator character will be wrapped with this quote character. /// public static char CsvQuoteCharacter = '"'; /// /// Saves a DataTable to a CSV file with default options (includes header). /// /// The DataTable to export /// The destination file path /// True if the file was successfully created; otherwise, false public static bool ToCsvFile(this DataTable dt, string filePath) => ToCsvFile(dt, filePath, CsvOptions.Default); /// /// Saves a DataTable to a CSV file with optional header. /// /// The DataTable to export /// The destination file path /// Whether to include column headers in the output /// True if the file was successfully created; otherwise, false public static bool ToCsvFile(this DataTable dataTable, string filePath, bool includeHeader) { return ToCsvFile(dataTable, filePath, includeHeader ? CsvOptions.Default : new CsvOptions() { IncludeHeader = false }); } /// /// Saves a DataTable to a CSV file with custom CSV options. /// /// The DataTable to export /// The destination file path /// Custom CSV formatting options (encoding, separator, quote character, etc.) /// True if the file was successfully created; otherwise, false public static bool ToCsvFile(this DataTable dataTable, string filePath, CsvOptions csvOptions) { if (dataTable is null) { throw new ArgumentNullException(nameof(dataTable)); } var csvText = GetCsvText(dataTable, csvOptions); if (csvText.IsNullOrEmpty()) { return false; } InternalHelper.EnsureFileIsNotReadOnly(filePath); var dir = Path.GetDirectoryName(filePath); if (dir.IsNotNullOrEmpty()) { if (!Directory.Exists(dir)) { Directory.CreateDirectory(dir); } } File.WriteAllText(filePath, csvText, csvOptions.Encoding); return true; } /// /// Converts a DataTable to CSV formatted byte array with default encoding (includes header). /// /// The DataTable to convert /// CSV data as a byte array public static byte[] ToCsvBytes(this DataTable dt) => ToCsvBytes(dt, true); /// /// Converts a DataTable to CSV formatted byte array with optional header. /// /// The DataTable to convert /// Whether to include column headers in the output /// CSV data as a byte array public static byte[] ToCsvBytes(this DataTable dataTable, bool includeHeader) => GetCsvText(dataTable, includeHeader).GetBytes(); /// /// Converts a DataTable to CSV formatted byte array with custom options. /// /// The DataTable to convert /// Custom CSV formatting options /// CSV data as a byte array public static byte[] ToCsvBytes(this DataTable dataTable, CsvOptions csvOptions) => GetCsvText(dataTable, csvOptions).GetBytes(); /// /// Converts CSV byte data to a DataTable with default options. /// /// CSV data as byte array /// A DataTable populated with CSV data public static DataTable ToDataTable(byte[] csvBytes) => ToDataTable(csvBytes, CsvOptions.Default); /// /// Converts CSV byte data to a DataTable with custom CSV options. /// /// CSV data as byte array /// Custom CSV parsing options (encoding, separator, etc.) /// A DataTable populated with CSV data public static DataTable ToDataTable(byte[] csvBytes, CsvOptions csvOptions) { if (csvBytes is null) { throw new ArgumentNullException(nameof(csvBytes)); } using var ms = new MemoryStream(csvBytes); return ToDataTable(ms, csvOptions); } /// /// Converts CSV stream data to a DataTable with default options. /// /// Stream containing CSV data /// A DataTable populated with CSV data public static DataTable ToDataTable(Stream stream) => ToDataTable(stream, CsvOptions.Default); /// /// Converts CSV stream data to a DataTable with custom CSV options. /// The first row is treated as column headers. /// /// Stream containing CSV data /// Custom CSV parsing options /// A DataTable populated with CSV data public static DataTable ToDataTable(Stream stream, CsvOptions csvOptions) { Guard.NotNull(stream); Guard.NotNull(csvOptions); var dt = new DataTable(); if (stream.CanRead) { using var sr = new StreamReader(stream, csvOptions.Encoding); string strLine; var isFirst = true; while ((strLine = sr.ReadLine()!).IsNotNullOrEmpty()) { var rowData = ParseLine(strLine, csvOptions); var dtColumns = rowData.Count; if (isFirst) { for (var i = 0; i < dtColumns; i++) { var columnName = rowData[i]; if (dt.Columns.Contains(columnName)) { columnName = InternalHelper.GetEncodedColumnName(columnName); } dt.Columns.Add(columnName); } isFirst = false; } else { var dataRow = dt.NewRow(); for (var j = 0; j < dt.Columns.Count; j++) { dataRow[j] = rowData[j]; } dt.Rows.Add(dataRow); } } } return dt; } /// /// Converts CSV file data to a DataTable with default options. /// /// Path to the CSV file /// A DataTable populated with CSV data /// Thrown when filePath is null /// Thrown when the file does not exist public static DataTable ToDataTable(string filePath) { if (filePath is null) { throw new ArgumentNullException(nameof(filePath)); } if (!File.Exists(filePath)) { throw new ArgumentException(Resource.FileNotFound, nameof(filePath)); } using var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); return ToDataTable(fs); } /// /// Converts CSV file data to a strongly-typed entity list with default options. /// /// The entity type to map CSV data to /// Path to the CSV file /// A list of entities populated from CSV data public static List ToEntityList(string filePath) => ToEntityList(filePath, CsvOptions.Default); /// /// Converts CSV file data to a strongly-typed entity list with custom options. /// /// The entity type to map CSV data to /// Path to the CSV file /// Custom CSV parsing options /// A list of entities populated from CSV data /// Thrown when the file does not exist public static List ToEntityList(string filePath, CsvOptions csvOptions) { Guard.NotNull(filePath); if (!File.Exists(filePath)) { throw new ArgumentException(Resource.FileNotFound, nameof(filePath)); } using var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); return ToEntityList(fs, csvOptions); } /// /// Converts CSV file data to a lazy-loaded sequence of strongly-typed entities. /// Use this method for large files to avoid loading all data into memory at once. /// /// The entity type to map CSV data to /// Path to the CSV file /// Optional custom CSV parsing options /// A lazy-loaded enumerable of entities /// Thrown when the file does not exist public static IEnumerable ToEntities(string filePath, CsvOptions? csvOptions = null) { Guard.NotNull(filePath); if (!File.Exists(filePath)) { throw new ArgumentException(Resource.FileNotFound, nameof(filePath)); } using var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); // https://stackoverflow.com/questions/1539114/yield-return-statement-inside-a-using-block-disposes-before-executing foreach (var entity in ToEntities(fs, csvOptions)) { yield return entity; } } /// /// Converts CSV byte data to a strongly-typed entity list with default options. /// /// The entity type to map CSV data to /// CSV data as byte array /// A list of entities populated from CSV data public static List ToEntityList(byte[] csvBytes) => ToEntityList(csvBytes, CsvOptions.Default); /// /// Converts CSV byte data to a strongly-typed entity list with custom options. /// /// The entity type to map CSV data to /// CSV data as byte array /// Custom CSV parsing options /// A list of entities populated from CSV data public static List ToEntityList(byte[] csvBytes, CsvOptions csvOptions) { Guard.NotNull(csvBytes); using var ms = new MemoryStream(csvBytes); return ToEntityList(ms, csvOptions); } /// /// Converts CSV byte data to a lazy-loaded sequence of strongly-typed entities. /// /// The entity type to map CSV data to /// CSV data as byte array /// Optional custom CSV parsing options /// A lazy-loaded enumerable of entities public static IEnumerable ToEntities(byte[] csvBytes, CsvOptions? csvOptions = null) { Guard.NotNull(csvBytes); using var ms = new MemoryStream(csvBytes); foreach (var entity in ToEntities(ms, csvOptions)) { yield return entity; } } /// /// Converts CSV stream data to a strongly-typed entity list with default options. /// /// The entity type to map CSV data to /// Stream containing CSV data /// A list of entities populated from CSV data public static List ToEntityList(Stream csvStream) => ToEntityList(csvStream, CsvOptions.Default); /// /// Converts CSV stream data to a strongly-typed entity list with custom options. /// /// The entity type to map CSV data to /// Stream containing CSV data /// Custom CSV parsing options /// A list of entities populated from CSV data public static List ToEntityList(Stream csvStream, CsvOptions csvOptions) { Guard.NotNull(csvStream); return ToEntities(csvStream, csvOptions).ToList(); } /// /// Converts CSV stream data to a lazy-loaded sequence of strongly-typed entities. /// This method is memory-efficient for large CSV files. /// /// The entity type to map CSV data to /// Stream containing CSV data /// Optional custom CSV parsing options /// A lazy-loaded enumerable of entities public static IEnumerable ToEntities(Stream csvStream, CsvOptions? csvOptions = null) { Guard.NotNull(csvStream); var lines = GetLines(); foreach (var entity in GetEntities(lines, csvOptions)) { yield return entity; } IEnumerable GetLines() { csvStream.Seek(0, SeekOrigin.Begin); using var reader = new StreamReader(csvStream, csvOptions?.Encoding ?? Encoding.UTF8); while (true) { var strLine = reader.ReadLine(); if (strLine.IsNullOrEmpty()) yield break; yield return strLine; } } } /// /// Parses CSV text and converts it to a strongly-typed entity list. /// /// The entity type to map CSV data to /// CSV data as a string /// Optional custom CSV parsing options /// A list of entities populated from CSV data public static List GetEntityList(string csvText, CsvOptions? csvOptions = null) => GetEntities(csvText, csvOptions).ToList(); /// /// Parses CSV text and converts it to a lazy-loaded sequence of strongly-typed entities. /// /// The entity type to map CSV data to /// CSV data as a string /// Optional custom CSV parsing options /// A lazy-loaded enumerable of entities public static IEnumerable GetEntities(string csvText, CsvOptions? csvOptions = null) { Guard.NotNull(csvText); var lines = GetLines(); foreach (var entity in GetEntities(lines, csvOptions)) { yield return entity; } IEnumerable GetLines() { using var reader = new StringReader(csvText); while (true) { var strLine = reader.ReadLine(); if (strLine.IsNullOrEmpty()) yield break; yield return strLine; } } } /// /// Converts CSV lines to a strongly-typed entity list. /// /// The entity type to map CSV data to /// Enumerable collection of CSV lines /// Optional custom CSV parsing options /// A list of entities populated from CSV data public static List GetEntityList(IEnumerable csvLines, CsvOptions? csvOptions = null) => GetEntities(csvLines, csvOptions).ToList(); /// /// Converts CSV lines to a lazy-loaded sequence of strongly-typed entities. /// Supports both basic types and complex objects with property mapping. /// For complex types, column headers are matched to property names or configured column titles. /// /// The entity type to map CSV data to /// Enumerable collection of CSV lines /// Optional custom CSV parsing options /// A lazy-loaded enumerable of entities public static IEnumerable GetEntities(IEnumerable csvLines, CsvOptions? csvOptions = null) { if (csvLines is null) { throw new ArgumentNullException(nameof(csvLines)); } csvOptions ??= CsvOptions.Default; var entityType = typeof(TEntity); if (entityType.IsBasicType()) { var lines = csvOptions.IncludeHeader ? csvLines.Skip(1) : csvLines; foreach (var strLine in lines) { yield return strLine.To(); } } else { var configuration = InternalHelper.GetExcelConfigurationMapping(); var propertyColumnDictionary = InternalHelper.GetPropertyColumnDictionary(); var propertyColumnDic = csvOptions.IncludeHeader ? propertyColumnDictionary.ToDictionary(p => p.Key, p => new PropertyConfiguration { ColumnIndex = -1, ColumnFormatter = p.Value.ColumnFormatter, ColumnTitle = p.Value.ColumnTitle, ColumnWidth = p.Value.ColumnWidth, IsIgnored = p.Value.IsIgnored }) : propertyColumnDictionary; var isFirstLine = csvOptions.IncludeHeader; var lineIndex = -1; foreach (var strLine in csvLines) { var cols = ParseLine(strLine, csvOptions); lineIndex++; if (isFirstLine) { for (var index = 0; index < cols.Count; index++) { var setting = propertyColumnDic.GetPropertySetting(cols[index]); if (setting is not null) { setting.ColumnIndex = index; } } if (propertyColumnDic.Values.Any(p => p.ColumnIndex < 0)) { propertyColumnDic = propertyColumnDictionary; } isFirstLine = false; } else { var entity = NewFuncHelper.Instance(); if (entityType.IsValueType) { var obj = (object)entity!; // boxing for value types foreach (var key in propertyColumnDic.Keys) { var colIndex = propertyColumnDic[key].ColumnIndex; if (colIndex >= 0 && colIndex < cols.Count && key.CanWrite) { var columnValue = key.PropertyType.GetDefaultValue(); var valueApplied = false; if (InternalCache.ColumnInputFormatterFuncCache.TryGetValue(key, out var formatterFunc) && formatterFunc?.Method is not null) { var cellValue = cols[colIndex]; try { // apply custom formatterFunc columnValue = formatterFunc.DynamicInvoke(cellValue); valueApplied = true; } catch (Exception e) { Debug.WriteLine(e); InvokeHelper.OnInvokeException?.Invoke(e); } } if (valueApplied == false) { columnValue = cols[colIndex].ToOrDefault(key.PropertyType); } key.GetValueSetter()?.Invoke(entity!, columnValue); } } entity = (TEntity)obj; // unboxing } else { foreach (var key in propertyColumnDic.Keys) { var colIndex = propertyColumnDic[key].ColumnIndex; if (colIndex >= 0 && colIndex < cols.Count && key.CanWrite) { var columnValue = key.PropertyType.GetDefaultValue(); var valueApplied = false; if (InternalCache.ColumnInputFormatterFuncCache.TryGetValue(key, out var formatterFunc) && formatterFunc?.Method is not null) { var cellValue = cols[colIndex]; try { // apply custom formatterFunc columnValue = formatterFunc.DynamicInvoke(cellValue); valueApplied = true; } catch (Exception e) { Debug.WriteLine(e); InvokeHelper.OnInvokeException?.Invoke(e); } } if (valueApplied == false) { columnValue = cols[colIndex].ToOrDefault(key.PropertyType); } key.GetValueSetter()?.Invoke(entity!, columnValue); } } } if (entity is not null) { foreach (var propertyInfo in propertyColumnDic.Keys.Where(p => p.CanWrite)) { var propertyValue = propertyInfo.GetValueGetter()?.Invoke(entity); if (InternalCache.InputFormatterFuncCache.TryGetValue(propertyInfo, out var formatterFunc) && formatterFunc?.Method is not null) { try { // apply custom formatterFunc var formattedValue = formatterFunc.DynamicInvoke(entity, propertyValue); propertyInfo.GetValueSetter()?.Invoke(entity, formattedValue); } catch (Exception e) { Debug.WriteLine(e); InvokeHelper.OnInvokeException?.Invoke(e); } } } } if (configuration.DataFilter?.Invoke(entity) == false) { continue; } configuration.PostImportAction?.Invoke(entity, lineIndex); yield return entity; } } } } /// /// Parses a single CSV line into individual field values using default options. /// /// The CSV line to parse /// A read-only list of field values public static IReadOnlyList ParseLine(string line) => ParseLine(line, CsvOptions.Default); /// /// Parses a single CSV line into individual field values with custom options. /// Handles quoted values, escaped quotes, and separator characters within quoted fields. /// /// The CSV line to parse /// Custom CSV parsing options (separator, quote character) /// A read-only list of field values /// Thrown when the line contains improperly escaped quotes public static IReadOnlyList ParseLine(string line, CsvOptions csvOptions) { if (string.IsNullOrEmpty(line)) { return Array.Empty(); } var columnBuilder = new StringBuilder(); var fields = new List(); var inColumn = false; var inQuotes = false; // Iterate through every character in the line for (var i = 0; i < line.Length; i++) { var character = line[i]; // If we are not currently inside a column if (!inColumn) { // If the current character is a double quote then the column value is contained within // double quotes, otherwise append the next character inColumn = true; if (character == csvOptions.QuoteCharacter) { inQuotes = true; continue; } } // If we are in between double quotes if (inQuotes) { if (i + 1 == line.Length) { break; } if (character == csvOptions.QuoteCharacter && line[i + 1] == csvOptions.SeparatorCharacter) // quotes end { inQuotes = false; inColumn = false; i++; //skip next } else if (character == csvOptions.QuoteCharacter && line[i + 1] == csvOptions.QuoteCharacter) // quotes { i++; //skip next } else if (character == csvOptions.QuoteCharacter) { throw new ArgumentException($"unable to escape {line}"); } } else if (character == csvOptions.SeparatorCharacter) { inColumn = false; } // If we are no longer in the column clear the builder and add the columns to the list if (!inColumn) { fields.Add(columnBuilder.ToString()); columnBuilder.Clear(); } else // append the current column { columnBuilder.Append(character); } } fields.Add(columnBuilder.ToString()); return fields; } /// /// Saves a collection of entities to a CSV file with default options (includes header). /// /// The entity type to export /// The collection of entities to export /// The destination file path /// True if the file was successfully created; otherwise, false public static bool ToCsvFile(this IEnumerable entities, string filePath) => ToCsvFile(entities, filePath, CsvOptions.Default); /// /// Saves a collection of entities to a CSV file with optional header. /// /// The entity type to export /// The collection of entities to export /// The destination file path /// Whether to include property names as column headers /// True if the file was successfully created; otherwise, false public static bool ToCsvFile(this IEnumerable entities, string filePath, bool includeHeader) { return ToCsvFile(Guard.NotNull(entities), filePath, includeHeader ? CsvOptions.Default : new CsvOptions() { IncludeHeader = false }); } /// /// Saves a collection of entities to a CSV file with custom CSV options. /// Property values are formatted according to configured output formatters. /// /// The entity type to export /// The collection of entities to export /// The destination file path /// Custom CSV formatting options /// True if the file was successfully created; otherwise, false public static bool ToCsvFile(this IEnumerable entities, string filePath, CsvOptions csvOptions) { if (entities is null) { throw new ArgumentNullException(nameof(entities)); } Guard.NotNull(csvOptions); var csvTextData = GetCsvText(entities, csvOptions); if (csvTextData.IsNullOrEmpty()) { return false; } var dir = Path.GetDirectoryName(filePath); if (dir.IsNotNullOrEmpty()) { if (!Directory.Exists(dir)) { Directory.CreateDirectory(dir); } } File.WriteAllText(filePath, csvTextData, csvOptions.Encoding); return true; } /// /// Asynchronously saves a collection of entities to a CSV file. /// This method is more memory-efficient for large collections as it streams lines to the file. /// /// The entity type to export /// The collection of entities to export /// The destination file path /// Optional custom CSV formatting options /// A task that represents the asynchronous operation, containing true if successful public static async Task ToCsvFileAsync(this IEnumerable entities, string filePath, CsvOptions? csvOptions = null) { if (entities is null) { throw new ArgumentNullException(nameof(entities)); } csvOptions ??= CsvOptions.Default; InternalHelper.EnsureFileIsNotReadOnly(filePath); var dir = Path.GetDirectoryName(filePath); if (dir.IsNotNullOrEmpty()) { if (!Directory.Exists(dir)) { Directory.CreateDirectory(dir); } } var lines = GetCsvLines(entities, csvOptions); using var file = File.CreateText(filePath); foreach (var line in lines) { await file.WriteLineAsync(line).ConfigureAwait(false); } return true; } /// /// Converts a collection of entities to CSV formatted byte array with default encoding (includes header). /// /// The entity type to convert /// The collection of entities to convert /// CSV data as a byte array public static byte[] ToCsvBytes(this IEnumerable entities) => ToCsvBytes(entities, CsvOptions.Default); /// /// Converts a collection of entities to CSV formatted byte array with optional header. /// /// The entity type to convert /// The collection of entities to convert /// Whether to include property names as column headers /// CSV data as a byte array public static byte[] ToCsvBytes(this IEnumerable entities, bool includeHeader) => GetCsvText(entities, includeHeader).GetBytes(); /// /// Converts a collection of entities to CSV formatted byte array with custom options. /// /// The entity type to convert /// The collection of entities to convert /// Custom CSV formatting options /// CSV data as a byte array public static byte[] ToCsvBytes(this IEnumerable entities, CsvOptions csvOptions) => GetCsvText(entities, csvOptions).GetBytes(csvOptions.Encoding); /// /// Converts a collection of entities to CSV formatted text with optional header (default includes header). /// /// The entity type to convert /// The collection of entities to convert /// Whether to include property names as column headers /// CSV data as a string public static string GetCsvText(this IEnumerable entities, bool includeHeader = true) { return GetCsvText(Guard.NotNull(entities), includeHeader ? CsvOptions.Default : new CsvOptions() { IncludeHeader = false }); } /// /// Converts a collection of entities to CSV formatted text with custom options. /// /// The entity type to convert /// The collection of entities to convert /// Custom CSV formatting options /// CSV data as a string public static string GetCsvText(this IEnumerable entities, CsvOptions csvOptions) => GetCsvLines(entities, csvOptions).StringJoin(Environment.NewLine); /// /// Converts a collection of entities to a sequence of CSV formatted lines. /// For basic types, each entity is converted to a single line. /// For complex types, properties are mapped to columns with proper CSV escaping. /// Values containing separator characters are automatically quoted. /// /// The collection of entities to convert /// Optional custom CSV formatting options /// The entity type to convert /// CSV formatted lines public static IEnumerable GetCsvLines(this IEnumerable entities, CsvOptions? csvOptions = null) { if (entities is null) { throw new ArgumentNullException(nameof(entities)); } csvOptions ??= CsvOptions.Default; var isBasicType = typeof(TEntity).IsBasicType(); if (isBasicType) { if (csvOptions.IncludeHeader) { yield return csvOptions.PropertyNameForBasicType; } foreach (var entity in entities) { if (entity is IFormattable formattableEntity) yield return formattableEntity.ToString(); else yield return Convert.ToString(entity) ?? string.Empty; } } else { var dic = InternalHelper.GetPropertyColumnDictionary(); var props = InternalHelper.GetPropertiesForCsvHelper(); if (csvOptions.IncludeHeader) { yield return Enumerable.Range(0, props.Count) .Select(i => dic[props[i]].ColumnTitle) .StringJoin(csvOptions.SeparatorString); } foreach (var entity in entities) { var line = GetCsvLine().StringJoin(csvOptions.SeparatorString); yield return line; IEnumerable GetCsvLine() { for (var i = 0; i < props.Count; i++) { var propertyValue = props[i].GetValueGetter()?.Invoke(entity); if (InternalCache.OutputFormatterFuncCache.TryGetValue(props[i], out var formatterFunc) && formatterFunc?.Method is not null) { try { // apply custom formatterFunc propertyValue = formatterFunc.DynamicInvoke(entity, propertyValue); } catch (Exception e) { Debug.WriteLine(e); InvokeHelper.OnInvokeException?.Invoke(e); } } // https://stackoverflow.com/questions/4617935/is-there-a-way-to-include-commas-in-csv-columns-without-breaking-the-formatting var val = propertyValue?.ToString()?.Replace( csvOptions.QuoteString, $"{csvOptions.QuoteString}{csvOptions.QuoteString}" ); if (val is { Length: > 0 }) { yield return val.IndexOf(csvOptions.SeparatorCharacter) > -1 ? $"{csvOptions.QuoteCharacter}{val}{csvOptions.QuoteCharacter}" : val; } else { yield return string.Empty; } } } } } } /// /// Converts a DataTable to CSV formatted text with optional header (default includes header). /// /// The DataTable to convert /// Whether to include column names as headers /// CSV data as a string public static string GetCsvText(this DataTable? dataTable, bool includeHeader = true) { return GetCsvText(dataTable, includeHeader ? CsvOptions.Default : new CsvOptions() { IncludeHeader = false }); } /// /// Converts a DataTable to CSV formatted text with custom options. /// Column names are decoded if they were previously encoded to handle duplicates. /// Values containing separator characters are automatically quoted. /// /// The DataTable to convert /// Custom CSV formatting options /// CSV data as a string, or empty string if the DataTable is null or empty public static string GetCsvText(this DataTable? dataTable, CsvOptions csvOptions) { Guard.NotNull(csvOptions); if (dataTable is null || dataTable.Rows.Count == 0 || dataTable.Columns.Count == 0) { return string.Empty; } var data = new StringBuilder(); if (csvOptions.IncludeHeader) { for (var i = 0; i < dataTable.Columns.Count; i++) { if (i > 0) { data.Append(csvOptions.SeparatorCharacter); } var columnName = InternalHelper.GetDecodeColumnName(dataTable.Columns[i].ColumnName); data.Append(columnName); } data.AppendLine(); } for (var i = 0; i < dataTable.Rows.Count; i++) { for (var j = 0; j < dataTable.Columns.Count; j++) { if (j > 0) { data.Append(csvOptions.SeparatorCharacter); } // https://stackoverflow.com/questions/4617935/is-there-a-way-to-include-commas-in-csv-columns-without-breaking-the-formatting var val = dataTable.Rows[i][j].ToString()?.Replace(csvOptions.QuoteString, $"{csvOptions.QuoteString}{csvOptions.QuoteString}"); if (val is { Length: > 0 }) { data.Append(val.IndexOf(csvOptions.SeparatorCharacter) > -1 ? $"{csvOptions.QuoteString}{val}{csvOptions.QuoteString}" : val); } } data.AppendLine(); } return data.ToString(); } } ================================================ FILE: src/WeihanLi.Npoi/ExcelFormat.cs ================================================ // Copyright (c) Weihan Li. All rights reserved. // Licensed under the Apache license. namespace WeihanLi.Npoi; /// /// ExcelFormat /// public enum ExcelFormat : byte { /// /// xls by default /// Xls = 0, /// /// xlsx /// Xlsx = 1 } ================================================ FILE: src/WeihanLi.Npoi/ExcelHelper.cs ================================================ // Copyright (c) Weihan Li. All rights reserved. // Licensed under the Apache license. using NPOI.HPSF; using NPOI.HSSF.UserModel; using NPOI.SS.UserModel; using NPOI.XSSF.UserModel; using System.Data; using WeihanLi.Common; using WeihanLi.Common.Models; using WeihanLi.Common.Services; using WeihanLi.Extensions; using WeihanLi.Npoi.Settings; namespace WeihanLi.Npoi; /// /// ExcelHelper /// public static class ExcelHelper { private static readonly Version AppVersion = typeof(ExcelHelper).Assembly.GetName().Version!; /// /// Default excel setting for export excel files /// public static ExcelSetting DefaultExcelSetting { get; set => field = Guard.NotNull(value); } = new(); /// /// Default Data Validator /// public static IValidator DefaultDataValidator { get; set => field = Guard.NotNull(value); } = DataAnnotationValidator.Instance; /// /// Validate whether the Excel path valid /// /// excel path /// error message /// is export operation /// is valid Excel path private static bool ValidateExcelFilePath(string excelPath, out string msg, bool isExport = false) { if (string.IsNullOrWhiteSpace(excelPath)) { throw new ArgumentNullException(nameof(excelPath)); } if (isExport || File.Exists(excelPath)) { var ext = Path.GetExtension(excelPath); if (ext.EqualsIgnoreCase(".xls") || ext.EqualsIgnoreCase(".xlsx")) { msg = string.Empty; return true; } msg = Resource.InvalidExcelFile; return false; } msg = Resource.FileNotFound; return false; } /// /// load excel from filepath /// /// excel file path /// workbook public static IWorkbook LoadExcel(string excelPath) { if (!ValidateExcelFilePath(excelPath, out var msg)) { throw new ArgumentException(msg); } using var stream = new FileStream(excelPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); return Path.GetExtension(excelPath).EqualsIgnoreCase(".xls") ? new HSSFWorkbook(stream) : new XSSFWorkbook(stream); } /// /// load excel from excelBytes /// /// excel file bytes /// workbook public static IWorkbook LoadExcel(byte[] excelBytes) => LoadExcel(excelBytes, ExcelFormat.Xls); /// /// load excel from excelBytes /// /// excel file bytes /// excelFormat /// workbook public static IWorkbook LoadExcel(byte[] excelBytes, ExcelFormat excelFormat) { if (excelBytes is null) { throw new ArgumentNullException(nameof(excelBytes)); } using var stream = new MemoryStream(excelBytes); return LoadExcel(stream, excelFormat); } /// /// load excel from excelBytes /// /// excel file stream /// workbook public static IWorkbook LoadExcel(Stream excelStream) => LoadExcel(excelStream, ExcelFormat.Xls); /// /// load excel from excelBytes /// /// excel file stream /// excelFormat /// workbook public static IWorkbook LoadExcel(Stream excelStream, ExcelFormat excelFormat) { if (excelStream is null) { throw new ArgumentNullException(nameof(excelStream)); } return excelFormat switch { ExcelFormat.Xls => new HSSFWorkbook(excelStream), _ => new XSSFWorkbook(excelStream) }; } /// /// prepare a workbook for export /// /// excelPath /// public static IWorkbook PrepareWorkbook(string excelPath) => PrepareWorkbook(excelPath, null); /// /// prepare a workbook for export /// /// excelPath /// excelSetting /// public static IWorkbook PrepareWorkbook(string excelPath, ExcelSetting? excelSetting) { if (!ValidateExcelFilePath(excelPath, out var msg, true)) { throw new ArgumentException(msg); } return PrepareWorkbook(!Path.GetExtension(excelPath).EqualsIgnoreCase(".xls"), excelSetting); } /// /// prepare a workbook for export /// /// excelFormat /// excelSetting /// public static IWorkbook PrepareWorkbook(ExcelFormat excelFormat, ExcelSetting? excelSetting) => PrepareWorkbook(excelFormat == ExcelFormat.Xlsx, excelSetting); /// /// get a excel workbook(*.xlsx) /// /// public static IWorkbook PrepareWorkbook() => PrepareWorkbook(true); /// /// get a excel workbook /// /// excelFormat /// public static IWorkbook PrepareWorkbook(ExcelFormat excelFormat) => PrepareWorkbook(excelFormat == ExcelFormat.Xlsx); /// /// get a excel workbook /// /// is for *.xlsx file /// public static IWorkbook PrepareWorkbook(bool isXlsx) => PrepareWorkbook(isXlsx, null); /// /// get a excel workbook /// /// is for *.xlsx file /// excelSettings /// public static IWorkbook PrepareWorkbook(bool isXlsx, ExcelSetting? excelSetting) { var setting = excelSetting ?? DefaultExcelSetting; if (isXlsx) { var workbook = new XSSFWorkbook(); var props = workbook.GetProperties(); props.CoreProperties.Creator = setting.Author; props.CoreProperties.Created = DateTime.Now; props.CoreProperties.Modified = DateTime.Now; props.CoreProperties.Title = setting.Title; props.CoreProperties.Subject = setting.Subject; props.CoreProperties.Category = setting.Category; props.CoreProperties.Description = setting.Description; props.ExtendedProperties.GetUnderlyingProperties().Company = setting.Company; props.ExtendedProperties.GetUnderlyingProperties().Application = InternalConstants.ApplicationName; props.ExtendedProperties.GetUnderlyingProperties().AppVersion = AppVersion.ToString(3); return workbook; } else { var workbook = new HSSFWorkbook(); // create a entry of DocumentSummaryInformation var dsi = new DocumentSummaryInformation { Company = setting.Company, Category = setting.Category }; workbook.DocumentSummaryInformation = dsi; // create a entry of SummaryInformation var si = new SummaryInformation { Title = setting.Title, Subject = setting.Subject, Author = setting.Author, CreateDateTime = DateTime.Now, Comments = setting.Description, ApplicationName = InternalConstants.ApplicationName }; workbook.SummaryInformation = si; return workbook; } } /// /// read first sheet of excel from excel file bytes to a list /// /// EntityType /// excelBytes /// List public static List ToEntityList(byte[] excelBytes) where TEntity : new() => ToEntityList(excelBytes, ExcelFormat.Xls, 0); /// /// read (sheetIndex) sheet of excel from excel file bytes to a list /// /// EntityType /// excelBytes /// sheetIndex /// List public static List ToEntityList(byte[] excelBytes, int sheetIndex) where TEntity : new() => ToEntityList(excelBytes, ExcelFormat.Xls, sheetIndex); /// /// read first sheet of excel from excel file bytes to a list /// /// EntityType /// excelBytes /// excelFormat /// List public static List ToEntityList(byte[] excelBytes, ExcelFormat excelFormat) where TEntity : new() => ToEntityList(excelBytes, excelFormat, 0); /// /// read (sheetIndex) sheet of excel from excel bytes to a list /// /// EntityType /// excelBytes /// excelFormat /// sheetIndex /// List public static List ToEntityList(byte[] excelBytes, ExcelFormat excelFormat, int sheetIndex) where TEntity : new() { using var workbook = LoadExcel(excelBytes, excelFormat); return workbook.ToEntityList(sheetIndex); } /// /// Lazily converts the specified sheet within an in-memory Excel payload into entities. /// /// Entity type. /// Excel file bytes. /// Workbook format. /// Zero-based sheet index. /// Sequence that yields entities row by row. public static IEnumerable ToEntities(byte[] excelBytes, ExcelFormat excelFormat = ExcelFormat.Xls, int sheetIndex = 0) where TEntity : new() { using var workbook = LoadExcel(excelBytes, excelFormat); foreach (var entity in workbook.ToEntities(sheetIndex)) { yield return entity; } } /// /// read (sheetIndex) sheet of excel from excel bytes to a list /// /// EntityType /// excelBytes /// excelFormat /// sheetIndex /// validator /// List and validationResult public static (List EntityList, Dictionary ValidationResults) ToEntityListWithValidationResult( byte[] excelBytes, ExcelFormat excelFormat = ExcelFormat.Xls, int sheetIndex = 0, IValidator? validator = null) where TEntity : new() { using var workbook = LoadExcel(excelBytes, excelFormat); return workbook.GetSheetAt(sheetIndex).ToEntityListWithValidationResult(sheetIndex, validator); } /// /// read first sheet of excel from excel stream to a list /// /// EntityType /// excelStream /// List public static List ToEntityList(Stream excelStream) where TEntity : new() => ToEntityList(excelStream, ExcelFormat.Xls, 0); /// /// read (sheetIndex) sheet of excel from excel file bytes to a list /// /// EntityType /// excelStream /// sheetIndex /// List public static List ToEntityList(Stream excelStream, int sheetIndex) where TEntity : new() => ToEntityList(excelStream, ExcelFormat.Xls, sheetIndex); /// /// read first sheet of excel from excel file bytes to a list /// /// EntityType /// excelStream /// excelFormat /// List public static List ToEntityList(Stream excelStream, ExcelFormat excelFormat) where TEntity : new() => ToEntityList(excelStream, excelFormat, 0); /// /// read (sheetIndex) sheet of excel from excel bytes path to a list /// /// EntityType /// excelStream /// excelFormat /// sheetIndex /// List public static List ToEntityList(Stream excelStream, ExcelFormat excelFormat, int sheetIndex) where TEntity : new() { using var workbook = LoadExcel(excelStream, excelFormat); return workbook.ToEntityList(sheetIndex); } /// /// Lazily converts the specified sheet within an Excel stream into entities. /// /// Entity type. /// Excel stream. /// Workbook format. /// Zero-based sheet index. /// Sequence that yields entities row by row. public static IEnumerable ToEntities(Stream excelStream, ExcelFormat excelFormat = ExcelFormat.Xls, int sheetIndex = 0) where TEntity : new() { using var workbook = LoadExcel(excelStream, excelFormat); foreach (var entity in workbook.ToEntities(sheetIndex)) { yield return entity; } } /// /// read (sheetIndex) sheet of excel from excel bytes path to a list /// /// EntityType /// excelStream /// excelFormat /// sheetIndex /// data validator /// List public static (List EntityList, Dictionary ValidationResults) ToEntityListWithValidationResult( Stream excelStream, ExcelFormat excelFormat = ExcelFormat.Xls, int sheetIndex = 0, IValidator? validator = null) where TEntity : new() { using var workbook = LoadExcel(excelStream, excelFormat); return workbook.GetSheetAt(sheetIndex).ToEntityListWithValidationResult(sheetIndex, validator); } /// /// read first sheet of excel from excel file path to a list /// /// EntityType /// excelPath /// List public static List ToEntityList(string excelPath) where TEntity : new() => ToEntityList(excelPath, 0); /// /// read (sheetIndex) sheet of excel from excel file path to a list /// /// EntityType /// excelPath /// sheetIndex /// List public static List ToEntityList(string excelPath, int sheetIndex) where TEntity : new() { using var workbook = LoadExcel(excelPath); return workbook.ToEntityList(sheetIndex); } /// /// Lazily converts the specified sheet within an Excel file path into entities. /// /// Entity type. /// Excel file path. /// Zero-based sheet index. /// Sequence that yields entities row by row. public static IEnumerable ToEntities(string excelPath, int sheetIndex) where TEntity : new() { using var workbook = LoadExcel(excelPath); foreach (var entity in workbook.ToEntities(sheetIndex)) { yield return entity; } } /// /// read (sheetIndex) sheet of excel from excel file path to a list /// /// EntityType /// excelPath /// sheetIndex /// validator /// List and validationResult public static (List EntityList, Dictionary ValidationResults) ToEntityListWithValidationResult( string excelPath, int sheetIndex = 0, IValidator? validator = null ) where TEntity : new() { using var workbook = LoadExcel(excelPath); return workbook.GetSheetAt(sheetIndex).ToEntityListWithValidationResult(sheetIndex, validator); } /// /// read first sheet of excel from excel file path to a data table /// /// EntityType /// excelPath /// DataTable public static DataTable ToDataTable(string excelPath) where TEntity : new() => ToDataTable(excelPath, 0); /// /// read (sheetIndex) sheet of excel from excel file path to a list(for specific class type) /// /// EntityType /// excelPath /// sheetIndex /// DataTable public static DataTable ToDataTable(string excelPath, int sheetIndex) where TEntity : new() => ToEntityList(excelPath, sheetIndex).ToDataTable(); /// /// read first sheet of excel from excel file path to a data table /// /// excelPath /// DataTable public static DataTable ToDataTable(string excelPath) => ToDataTable(excelPath, 0, 0); /// /// read first sheet of excel from excel file path to a data table /// /// excelPath /// sheetIndex /// DataTable public static DataTable ToDataTable(string excelPath, int sheetIndex) => ToDataTable(excelPath, sheetIndex, 0); /// /// read (sheetIndex) sheet of excel from excel file path to a data table /// /// excelPath /// sheetIndex /// headerRowIndex /// removeEmptyRows /// maxColumns /// DataTable public static DataTable ToDataTable(string excelPath, int sheetIndex, int headerRowIndex, bool removeEmptyRows = false, int? maxColumns = null) { using var workbook = LoadExcel(excelPath); if (workbook.NumberOfSheets <= sheetIndex) { throw new ArgumentOutOfRangeException(nameof(sheetIndex), string.Format(Resource.IndexOutOfRange, nameof(sheetIndex), workbook.NumberOfSheets)); } return workbook.GetSheetAt(sheetIndex).ToDataTable(headerRowIndex, removeEmptyRows, maxColumns); } /// /// read first sheet of excel from excelBytes to a data table /// /// excelBytes /// /// removeEmptyRows /// maxColumns /// DataTable public static DataTable ToDataTable(byte[] excelBytes, ExcelFormat excelFormat, bool removeEmptyRows = false, int? maxColumns = null) => ToDataTable(excelBytes, excelFormat, 0, removeEmptyRows, maxColumns); /// /// read (sheetIndex) sheet of excel from excelBytes to a data table /// /// excelBytes /// /// sheetIndex /// removeEmptyRows /// maxColumns /// DataTable public static DataTable ToDataTable(byte[] excelBytes, ExcelFormat excelFormat, int sheetIndex, bool removeEmptyRows = false, int? maxColumns = null) => ToDataTable(excelBytes, excelFormat, sheetIndex, 0, removeEmptyRows, maxColumns); /// /// read (sheetIndex) sheet of excel from excelBytes to a data table /// /// excelBytes /// /// sheetIndex /// headerRowIndex /// removeEmptyRows /// maxColumns /// DataTable public static DataTable ToDataTable(byte[] excelBytes, ExcelFormat excelFormat, int sheetIndex, int headerRowIndex, bool removeEmptyRows = false, int? maxColumns = null) { using var workbook = LoadExcel(excelBytes, excelFormat); if (workbook.NumberOfSheets <= sheetIndex) { throw new ArgumentOutOfRangeException(nameof(sheetIndex), string.Format(Resource.IndexOutOfRange, nameof(sheetIndex), workbook.NumberOfSheets)); } return workbook.GetSheetAt(sheetIndex).ToDataTable(headerRowIndex, removeEmptyRows, maxColumns); } /// /// read first sheet of excel from excel file path to a DataSet from second row /// /// excelPath /// public static DataSet ToDataSet(string excelPath) => ToDataSet(excelPath, 0); /// /// read first sheet of excel from excel file path to a DataSet from (headerRowIndex+1) row /// /// excelPath /// headerRowIndex /// public static DataSet ToDataSet(string excelPath, int headerRowIndex) { using var workbook = LoadExcel(excelPath); return workbook.ToDataSet(headerRowIndex); } } ================================================ FILE: src/WeihanLi.Npoi/FakePropertyInfo.cs ================================================ // Copyright (c) Weihan Li. All rights reserved. // Licensed under the Apache license. using System.Globalization; using System.Reflection; using WeihanLi.Extensions; namespace WeihanLi.Npoi; #nullable disable internal sealed class FakePropertyInfo : PropertyInfo { private readonly Func _getValueFunc; private readonly object _value; public FakePropertyInfo(Type entityType, Type propertyType, string propertyName) { DeclaringType = entityType; ReflectedType = entityType; PropertyType = propertyType; Name = propertyName; _value = propertyType.GetDefaultValue(); _getValueFunc = () => _value; Attributes = PropertyAttributes.None; } public override Type DeclaringType { get; } public override string Name { get; } public override Type ReflectedType { get; } public override bool CanRead => false; public override bool CanWrite => false; public override Type PropertyType { get; } public override PropertyAttributes Attributes { get; } public override MethodInfo GetGetMethod(bool nonPublic) => _getValueFunc.Method; public override MethodInfo GetSetMethod(bool nonPublic) => null; public override string ToString() => $"{PropertyType.Name}, {Name}"; public override object[] GetCustomAttributes(bool inherit) => throw new NotSupportedException(); public override object[] GetCustomAttributes(Type attributeType, bool inherit) => throw new NotSupportedException(); public override bool IsDefined(Type attributeType, bool inherit) => throw new NotSupportedException(); public override MethodInfo[] GetAccessors(bool nonPublic) => throw new NotSupportedException(); public override ParameterInfo[] GetIndexParameters() => throw new NotSupportedException(); public override object GetValue(object obj, BindingFlags invokeAttr, Binder binder, object[] index, CultureInfo culture) => _value; public override void SetValue(object obj, object value, BindingFlags invokeAttr, Binder binder, object[] index, CultureInfo culture) => throw new NotSupportedException(); } #nullable restore ================================================ FILE: src/WeihanLi.Npoi/FluentSettings.cs ================================================ // Copyright (c) Weihan Li. All rights reserved. // Licensed under the Apache license. using System.Reflection; using WeihanLi.Common; using WeihanLi.Common.Helpers; using WeihanLi.Extensions; using WeihanLi.Npoi.Configurations; namespace WeihanLi.Npoi; /// /// Central entry point for configuring Excel mappings via a fluent API. /// public static class FluentSettings { private const string MappingProfileConfigureMethodName = "Configure"; private static readonly Type s_profileGenericTypeDefinition = typeof(IMappingProfile<>); /// /// Fluent Setting For TEntity /// /// TEntity /// excel configuration for entity public static IExcelConfiguration For() => InternalHelper.GetExcelConfigurationMapping(); /// /// Load mapping profiles /// /// assemblies public static void LoadMappingProfiles(params Assembly[] assemblies) { Guard.NotNull(assemblies, nameof(assemblies)); if (assemblies.Length == 0) { assemblies = ReflectHelper.GetAssemblies(); } LoadMappingProfiles(assemblies.SelectMany(ass => ass.GetExportedTypes()).ToArray()); } /// /// Load mapping profiles /// /// mapping profile types public static void LoadMappingProfiles(params Type[] types) { Guard.NotNull(types, nameof(types)); foreach (var type in types.Where(x => x.IsAssignableTo())) { if (Activator.CreateInstance(type) is IMappingProfile profile) { LoadMappingProfile(profile); } } } /// /// Load mapping profile for TEntity /// /// entity type /// entity type mapping profile public static void LoadMappingProfile() where TMappingProfile : IMappingProfile, new() { var profile = new TMappingProfile(); profile.Configure(InternalHelper.GetExcelConfigurationMapping()); } /// /// Load mapping profile for TEntity /// /// profile /// entity type /// mapping profile type public static void LoadMappingProfile(TMappingProfile profile) where TMappingProfile : IMappingProfile { Guard.NotNull(profile); profile.Configure(InternalHelper.GetExcelConfigurationMapping()); } /// /// Load mapping profile for TEntity /// /// entity type mapping profile public static void LoadMappingProfile() where TMappingProfile : IMappingProfile, new() => LoadMappingProfile(new TMappingProfile()); /// /// Load mapping profile for TEntity /// /// profile private static void LoadMappingProfile(TMappingProfile profile) where TMappingProfile : IMappingProfile { Guard.NotNull(profile, nameof(profile)); var profileInterfaceType = profile.GetType() .GetImplementedInterfaces() .FirstOrDefault(x => x.IsGenericType && x.GetGenericTypeDefinition() == s_profileGenericTypeDefinition); if (profileInterfaceType is null) { return; } var entityType = profileInterfaceType.GetGenericArguments()[0]; var configuration = InternalHelper.GetExcelConfigurationMapping(entityType); var method = profileInterfaceType.GetMethod(MappingProfileConfigureMethodName, [typeof(IExcelConfiguration<>).MakeGenericType(entityType)]); method?.Invoke(profile, [configuration]); } } ================================================ FILE: src/WeihanLi.Npoi/IMappingProfile.cs ================================================ // Copyright (c) Weihan Li. All rights reserved. // Licensed under the Apache license. using WeihanLi.Npoi.Configurations; namespace WeihanLi.Npoi; /// /// Marker interface for describing fluent configuration profiles. /// public interface IMappingProfile; /// /// Strongly typed mapping profile contract. /// /// Entity type being configured. public interface IMappingProfile : IMappingProfile { /// /// Configures the Excel mapping metadata for the given entity type. /// /// Excel configuration builder. void Configure(IExcelConfiguration configuration); } ================================================ FILE: src/WeihanLi.Npoi/InternalCache.cs ================================================ // Copyright (c) Weihan Li. All rights reserved. // Licensed under the Apache license. using System.Collections.Concurrent; using System.Reflection; using WeihanLi.Npoi.Configurations; namespace WeihanLi.Npoi; internal static class InternalCache { /// /// TypeExcelConfigurationCache /// public static readonly ConcurrentDictionary TypeExcelConfigurationDictionary = new(); /// /// Cacheable delegates that format cell values when exporting. /// public static readonly ConcurrentDictionary OutputFormatterFuncCache = new(); /// /// Cacheable delegates that post-process property values after import. /// public static readonly ConcurrentDictionary InputFormatterFuncCache = new(); /// /// Cacheable delegates that pre-process property values before the column formatter runs. /// public static readonly ConcurrentDictionary ColumnInputFormatterFuncCache = new(); /// /// Cacheable delegates that read a cell value into the desired property type. /// public static readonly ConcurrentDictionary CellReaderFuncCache = new(); } ================================================ FILE: src/WeihanLi.Npoi/InternalConstants.cs ================================================ // Copyright (c) Weihan Li. All rights reserved. // Licensed under the Apache license. namespace WeihanLi.Npoi; internal static class InternalConstants { /// /// Maximum number of sheets supported by the legacy XLS format. /// public const int MaxSheetCountXls = 256; /// /// Maximum number of sheets supported by the XLSX format. /// public const int MaxSheetCountXlsx = 16384; /// /// Maximum row count in a single XLS sheet. /// public const int MaxRowCountXls = 65536; /// /// Maximum row count in a single XLSX sheet. /// public const int MaxRowCountXlsx = 1_048_576; /// /// DefaultPropertyNameForBasicType /// public const string DefaultPropertyNameForBasicType = "Value"; /// /// ApplicationName /// public const string ApplicationName = "WeihanLi.Npoi"; /// /// Marker appended when duplicate column titles are encountered. /// public const string DuplicateColumnMark = "__dup_mark__"; #region TemplateParamFormat /// /// Placeholder format for template-wide global parameters. /// public const string TemplateGlobalParamFormat = "$(Global:{0})"; /// /// Placeholder format for header-level template parameters. /// public const string TemplateHeaderParamFormat = "$(Header:{0})"; /// /// Placeholder format for body data template parameters. /// public const string TemplateDataParamFormat = "$(Data:{0})"; /// /// Prefix indicating a template data placeholder. /// public const string TemplateDataPrefix = "$(Data:"; /// /// Opening tag that surrounds template data sections. /// public const string TemplateDataBegin = ""; /// /// Closing tag that surrounds template data sections. /// public const string TemplateDataEnd = ""; #endregion TemplateParamFormat } ================================================ FILE: src/WeihanLi.Npoi/InternalExtensions.cs ================================================ // Copyright (c) Weihan Li. All rights reserved. // Licensed under the Apache license. using NPOI.SS.UserModel; using System.Globalization; using System.Reflection; using WeihanLi.Common; using WeihanLi.Common.Models; using WeihanLi.Common.Services; using WeihanLi.Extensions; using WeihanLi.Npoi.Configurations; using CellType = WeihanLi.Npoi.Abstract.CellType; using ICell = WeihanLi.Npoi.Abstract.ICell; namespace WeihanLi.Npoi; internal static class InternalExtensions { /// /// Parse obj to paramDictionary /// /// param object /// public static IDictionary ParseParamInfo(this object? paramInfo) { var paramDic = paramInfo.ParseParamDictionary(); return paramDic; } /// /// Wraps a strongly typed validator so it can be used without generics. /// /// Entity type handled by the validator. /// Type-specific validator. /// Validator that operates on instances. public static IValidator GetCommonValidator(this IValidator validator) { return new CustomValidator(o => { if (o is T t) { return validator.Validate(t); } return ValidationResult.Failed("Invalid value"); }); } /// /// GetCellValue /// /// cell /// propertyType /// formulaEvaluator /// cellValue public static object? GetCellValue(this ICell? cell, Type propertyType, IFormulaEvaluator? formulaEvaluator) { if (cell is null || cell.CellType == CellType.Blank || cell.CellType == CellType.Error) { return propertyType.GetDefaultValue(); } return cell.Value?.ToOrDefault(propertyType); } /// /// GetCellValue /// /// Type /// cell /// public static T? GetCellValue(this ICell? cell) => (cell?.Value).ToOrDefault(); /// /// SetCellValue /// /// ICell /// value public static void SetCellValue(this ICell? cell, object? value) => cell?.SetCellValue(value, null); /// /// SetCellValue /// /// ICell /// value /// formatter public static void SetCellValue(this ICell cell, object? value, string? formatter) { if (value is null) { cell.CellType = CellType.Blank; return; } if (value is DateTime time) { cell.Value = string.IsNullOrWhiteSpace(formatter) ? time.Date == time ? time.ToDateString() : time.ToTimeString() : time.ToString(formatter); cell.CellType = CellType.String; } else { var type = value.GetType(); if ( type == typeof(double) || type == typeof(int) || type == typeof(long) || type == typeof(float) || type == typeof(decimal) ) { cell.Value = Convert.ToDouble(value); cell.CellType = CellType.Numeric; } else if (type == typeof(bool)) { cell.Value = value; cell.CellType = CellType.Boolean; } else { cell.Value = value is IFormattable val && formatter.IsNotNullOrWhiteSpace() ? val.ToString(formatter, CultureInfo.CurrentCulture) : value.ToString(); cell.CellType = CellType.String; } } } /// /// GetPropertySettingByPropertyName /// /// mappingDictionary /// propertyName /// internal static PropertyConfiguration? GetPropertySettingByPropertyName( this IDictionary mappingDictionary, string propertyName) => mappingDictionary.Values.FirstOrDefault(c => c.PropertyName.EqualsIgnoreCase(propertyName)); /// /// GetPropertyConfigurationByColumnName /// /// mappingDictionary /// columnTitle /// internal static PropertyConfiguration? GetPropertySetting( this IDictionary mappingDictionary, string columnTitle) => mappingDictionary.Values.FirstOrDefault(k => k.ColumnTitle.EqualsIgnoreCase(columnTitle)) ?? mappingDictionary.GetPropertySettingByPropertyName(columnTitle); private sealed class CustomValidator(Func func) : IValidator { private readonly Func _func = Guard.NotNull(func); /// /// Executes the wrapped validation delegate. /// /// Value to validate. /// Validation result. public ValidationResult Validate(object? value) { return _func.Invoke(value); } } } ================================================ FILE: src/WeihanLi.Npoi/InternalHelper.cs ================================================ // Copyright (c) Weihan Li. All rights reserved. // Licensed under the Apache license. using System.Reflection; using WeihanLi.Common; using WeihanLi.Npoi.Attributes; using WeihanLi.Npoi.Configurations; namespace WeihanLi.Npoi; internal static class InternalHelper { /// /// Ensures the supplied file path is writable, throwing if it is read-only. /// /// File path to inspect. public static void EnsureFileIsNotReadOnly(string filePath) { if (!File.Exists(filePath)) return; var attributes = File.GetAttributes(filePath); if ((attributes & FileAttributes.ReadOnly) != 0) { throw new InvalidOperationException($"The file({filePath}) is read-only"); } } /// /// Get ExcelConfigurationMapping by type /// /// entityType /// excel configuration public static IExcelConfiguration GetExcelConfigurationMapping(Type entityType) => InternalCache.TypeExcelConfigurationDictionary.GetOrAdd(entityType, type => { var excelConfiguration = CreateExcelConfiguration(type, () => (ExcelConfiguration) Guard.NotNull(Activator.CreateInstance(typeof(ExcelConfiguration<>).MakeGenericType(entityType))) ); return excelConfiguration; }); /// /// Get GenericType ExcelConfigurationMapping /// /// TEntity /// IExcelConfiguration public static ExcelConfiguration GetExcelConfigurationMapping() => (ExcelConfiguration)InternalCache.TypeExcelConfigurationDictionary.GetOrAdd(typeof(TEntity), type => { var excelConfiguration = CreateExcelConfiguration(type, () => new ExcelConfiguration()); return excelConfiguration; }); private static ExcelConfiguration CreateExcelConfiguration(Type type, Func newConfigurationFunc) { var excelConfiguration = newConfigurationFunc(); excelConfiguration.FilterSetting = type.GetCustomAttribute()?.FilterSetting; foreach (var sheetAttribute in type.GetCustomAttributes()) { if (sheetAttribute.SheetIndex >= 0) { excelConfiguration.SheetSettings[sheetAttribute.SheetIndex] = sheetAttribute.SheetSetting; } } foreach (var freezeAttribute in type.GetCustomAttributes()) { excelConfiguration.FreezeSettings.Add(freezeAttribute.FreezeSetting); } var propertyInfos = CacheUtil.GetTypeProperties(type); foreach (var propertyInfo in propertyInfos) { var column = propertyInfo.GetCustomAttribute() ?? new ColumnAttribute(); if (string.IsNullOrWhiteSpace(column.Title)) { column.Title = propertyInfo.Name; } var propertyConfigurationType = typeof(PropertyConfiguration<,>).MakeGenericType(type, propertyInfo.PropertyType); var propertyConfiguration = Guard.NotNull(Activator.CreateInstance(propertyConfigurationType, propertyInfo)); propertyConfigurationType.GetProperty(nameof(column.PropertyConfiguration.ColumnTitle)) ?.GetSetMethod()? .Invoke(propertyConfiguration, [column.PropertyConfiguration.ColumnTitle]); propertyConfigurationType.GetProperty(nameof(column.PropertyConfiguration.ColumnIndex)) ?.GetSetMethod()? .Invoke(propertyConfiguration, [column.PropertyConfiguration.ColumnIndex]); propertyConfigurationType.GetProperty(nameof(column.PropertyConfiguration.ColumnFormatter)) ?.GetSetMethod()? .Invoke(propertyConfiguration, [column.PropertyConfiguration.ColumnFormatter]); propertyConfigurationType.GetProperty(nameof(column.PropertyConfiguration.IsIgnored)) ?.GetSetMethod()? .Invoke(propertyConfiguration, [column.PropertyConfiguration.IsIgnored]); propertyConfigurationType.GetProperty(nameof(column.PropertyConfiguration.ColumnWidth)) ?.GetSetMethod()? .Invoke(propertyConfiguration, [column.PropertyConfiguration.ColumnWidth]); excelConfiguration.PropertyConfigurationDictionary.Add(propertyInfo, (PropertyConfiguration)propertyConfiguration); } return excelConfiguration; } /// /// Adjust Column Index /// /// TEntity /// excelConfiguration private static void AdjustColumnIndex(ExcelConfiguration excelConfiguration) { ICollection validColumnIndex = excelConfiguration.PropertyConfigurationDictionary.Values .Where(c => !c.IsIgnored) .Select(x => x.ColumnIndex) .ToArray(); // return if already adjusted if (validColumnIndex.All(idx => idx >= 0) && validColumnIndex.Distinct().Count() == validColumnIndex.Count ) { return; } var colIndexList = new List(validColumnIndex.Count); var properties = excelConfiguration.PropertyConfigurationDictionary .Where(p => !p.Value.IsIgnored) .OrderBy(p => p.Value.ColumnIndex >= 0 ? p.Value.ColumnIndex : int.MaxValue); if (excelConfiguration.PropertyComparer is not null) { properties = properties.ThenBy(p => p.Key, excelConfiguration.PropertyComparer); } foreach (var item in properties.Select(p => p.Value)) { while (colIndexList.Contains(item.ColumnIndex) || item.ColumnIndex < 0) { if (colIndexList.Count > 0) { item.ColumnIndex = colIndexList.Max() + 1; } else { item.ColumnIndex++; } } colIndexList.Add(item.ColumnIndex); } } /// /// GetPropertyColumnDictionary /// /// TEntity Type /// public static Dictionary GetPropertyColumnDictionary() => GetPropertyColumnDictionary(GetExcelConfigurationMapping()); /// /// GetPropertyColumnDictionary /// /// TEntity Type /// public static Dictionary GetPropertyColumnDictionary( ExcelConfiguration configuration) { AdjustColumnIndex(configuration); return configuration.PropertyConfigurationDictionary .Where(p => !p.Value.IsIgnored) .ToDictionary(p => p.Key, p => p.Value); } /// /// GetProperties /// /// TEntity Type /// public static IReadOnlyList GetPropertiesForCsvHelper() { var configuration = GetExcelConfigurationMapping(); AdjustColumnIndex(configuration); return configuration.PropertyConfigurationDictionary .Where(p => !p.Value.IsIgnored) .OrderBy(p => p.Value.ColumnIndex) .Select(p => p.Key) .ToArray(); } /// /// Generates a unique column name to temporarily disambiguate duplicates. /// /// Original column title. /// Encoded column name with a duplicate marker. public static string GetEncodedColumnName(string columnName) => $"{columnName}{InternalConstants.DuplicateColumnMark}{Guid.NewGuid():N}"; /// /// Removes the duplicate marker from a previously encoded column name. /// /// Encoded column title. /// Original column name. public static string GetDecodeColumnName(string columnName) { var duplicateMarkIndex = columnName.IndexOf(InternalConstants.DuplicateColumnMark, StringComparison.OrdinalIgnoreCase); return duplicateMarkIndex > 0 ? columnName.Substring(0, duplicateMarkIndex) : columnName; } } ================================================ FILE: src/WeihanLi.Npoi/NpoiCollection.cs ================================================ // Copyright (c) Weihan Li. All rights reserved. // Licensed under the Apache license. using NPOI.SS.UserModel; using System.Collections; using WeihanLi.Common; namespace WeihanLi.Npoi; /// /// npoi sheet row collection /// public sealed class NpoiRowCollection(ISheet sheet) : IReadOnlyCollection { private readonly ISheet _sheet = Guard.NotNull(sheet); /// /// Gets the number of rows within the wrapped sheet. /// public int Count => _sheet.LastRowNum - _sheet.FirstRowNum + 1; /// /// Iterates over each row within the sheet range. /// public IEnumerator GetEnumerator() { for (var i = _sheet.FirstRowNum; i <= _sheet.LastRowNum; i++) { yield return _sheet.GetRow(i); } } IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } /// /// npoi row cell collection /// public sealed class NpoiCellCollection(IRow row) : IReadOnlyCollection { private readonly IRow _row = Guard.NotNull(row); /// /// Gets the number of cells in the wrapped row. /// public int Count => _row.LastCellNum - _row.FirstCellNum; /// /// Iterates over each concrete cell in the current row. /// public IEnumerator GetEnumerator() { for (var i = _row.FirstCellNum; i < _row.LastCellNum; i++) { yield return _row.GetCell(i); } } IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } ================================================ FILE: src/WeihanLi.Npoi/NpoiExtensions.cs ================================================ // Copyright (c) Weihan Li. All rights reserved. // Licensed under the Apache license. using NPOI.HSSF.UserModel; using NPOI.SS.UserModel; using NPOI.XSSF.Streaming; using NPOI.XSSF.UserModel; using System.Data; using System.Diagnostics; using System.Globalization; using WeihanLi.Common; using WeihanLi.Common.Helpers; using WeihanLi.Common.Models; using WeihanLi.Common.Services; using WeihanLi.Extensions; using WeihanLi.Npoi.Settings; namespace WeihanLi.Npoi; /// /// Extension methods that convert between NPOI primitives and the strongly-typed configuration layer. /// public static class NpoiExtensions { /// /// Workbook2EntityList /// /// EntityType /// excel workbook /// entity list public static List ToEntityList(this IWorkbook workbook) where TEntity : new() => workbook.ToEntityList(0); /// /// Workbook2EntityList /// /// EntityType /// excel workbook /// sheetIndex /// entity list public static List ToEntityList(this IWorkbook workbook, int sheetIndex) where TEntity : new() { return ToEntities(workbook, sheetIndex).ToList(); } /// /// Lazily materializes entities from the specified sheet without building a list first. /// /// Entity type. /// Excel workbook. /// Zero-based sheet index. /// Sequence that yields entities row by row. public static IEnumerable ToEntities(this IWorkbook workbook, int sheetIndex) where TEntity : new() { Guard.NotNull(workbook); if (workbook.NumberOfSheets <= sheetIndex) { throw new ArgumentOutOfRangeException(nameof(sheetIndex), string.Format(Resource.IndexOutOfRange, nameof(sheetIndex), workbook.NumberOfSheets)); } var sheet = workbook.GetSheetAt(sheetIndex); return NpoiHelper.SheetToEntities(sheet, sheetIndex); } /// /// Sheet2EntityList /// /// EntityType /// excel sheet /// entity list public static List ToEntityList(this ISheet sheet) where TEntity : new() => sheet.ToEntityList(0); /// /// Sheet2EntityList /// /// EntityType /// excel sheet /// sheetIndex /// entity list public static List ToEntityList(this ISheet sheet, int sheetIndex) where TEntity : new() => NpoiHelper.SheetToEntities(sheet, sheetIndex).ToList(); /// /// Lazily materializes entities from the provided sheet. /// /// Entity type. /// Excel sheet. /// Zero-based sheet index. /// Sequence that yields entities row by row. public static IEnumerable ToEntities(this ISheet sheet, int sheetIndex) where TEntity : new() => NpoiHelper.SheetToEntities(sheet, sheetIndex); /// /// Sheet2EntityList and validate /// /// EntityType /// excel sheet /// sheetIndex /// validator /// entity list and validation results public static (List EntityList, Dictionary ValidationResults) ToEntityListWithValidationResult(this ISheet sheet, int sheetIndex = 0, IValidator? validator = null) where TEntity : new() { var validationResults = new Dictionary(); var entities = NpoiHelper.SheetToEntities(sheet, sheetIndex, (entity, configuration, rowIndex) => { var validatorEffective = configuration.Validator; if (validator is not null) { validatorEffective = validator.GetCommonValidator(); } validatorEffective ??= ExcelHelper.DefaultDataValidator; var validationResult = validatorEffective.Validate(entity); if (!validationResult.Valid) { validationResults[rowIndex] = validationResult; } }).ToList(); return (entities, validationResults); } /// /// Workbook2ToDataTable /// /// excel workbook /// removeEmptyRows /// maxColumns /// DataTable public static DataTable ToDataTable(this IWorkbook workbook, bool removeEmptyRows = false, int? maxColumns = null) => workbook.ToDataTable(0, 0, removeEmptyRows, maxColumns); /// /// Workbook2ToDataSet /// /// excel workbook /// removeEmptyRows /// maxColumns /// DataSet public static DataSet ToDataSet(this IWorkbook workbook, bool removeEmptyRows = false, int? maxColumns = null) => workbook.ToDataSet(0, removeEmptyRows, maxColumns); /// /// Workbook2ToDataSet /// /// excel workbook /// headerRowIndex /// removeEmptyRows /// maxColumns /// DataSet public static DataSet ToDataSet(this IWorkbook workbook, int headerRowIndex, bool removeEmptyRows = false, int? maxColumns = null) { Guard.NotNull(workbook); var ds = new DataSet(); for (var i = 0; i < workbook.NumberOfSheets; i++) { ds.Tables.Add(workbook.GetSheetAt(i).ToDataTable(headerRowIndex, removeEmptyRows, maxColumns)); } return ds; } /// /// Workbook2ToDataTable /// /// excel workbook /// sheetIndex /// headerRowIndex /// removeEmptyRows /// maxColumns /// DataTable public static DataTable ToDataTable(this IWorkbook workbook, int sheetIndex, int headerRowIndex, bool removeEmptyRows = false, int? maxColumns = null) { Guard.NotNull(workbook); if (workbook.NumberOfSheets <= sheetIndex) { throw new ArgumentOutOfRangeException(nameof(sheetIndex), string.Format(Resource.IndexOutOfRange, nameof(sheetIndex), workbook.NumberOfSheets)); } return workbook.GetSheetAt(sheetIndex).ToDataTable(headerRowIndex, removeEmptyRows, maxColumns); } /// /// Sheet2DataTable /// /// excel sheet /// removeEmptyRows /// maxColumns /// DataTable public static DataTable ToDataTable(this ISheet sheet, bool removeEmptyRows = false, int? maxColumns = null) => sheet.ToDataTable(0, removeEmptyRows, maxColumns); /// /// Sheet2DataTable /// /// excel sheet /// headerRowIndex /// removeEmptyRows /// maxColumns /// DataTable public static DataTable ToDataTable(this ISheet sheet, int headerRowIndex, bool removeEmptyRows = false, int? maxColumns = null) { Guard.NotNull(sheet); if (sheet.LastRowNum <= headerRowIndex) { throw new ArgumentOutOfRangeException(nameof(headerRowIndex), string.Format(Resource.IndexOutOfRange, nameof(headerRowIndex), sheet.PhysicalNumberOfRows)); } var formulaEvaluator = sheet.Workbook.GetFormulaEvaluator(); var dataTable = new DataTable(sheet.SheetName); foreach (var row in sheet.GetRowCollection()) { if ( // ReSharper disable once ConditionIsAlwaysTrueOrFalse row is null || row.RowNum < headerRowIndex) { continue; } if (row.RowNum == headerRowIndex) { LoadHeader(formulaEvaluator, dataTable, row, maxColumns); } else { LoadRow(formulaEvaluator, dataTable, row, removeEmptyRows, maxColumns); } } return dataTable; static void LoadHeader(IFormulaEvaluator formulaEvaluator, DataTable dataTable, IRow row, int? maxColumns) { foreach (var cell in row) { if (cell is null) { continue; } var columnName = cell.GetCellValue(typeof(string), formulaEvaluator)!.ToString()!.Trim(); if (dataTable.Columns.Contains(columnName)) { columnName = InternalHelper.GetEncodedColumnName(columnName); } dataTable.Columns.Add(columnName); if (maxColumns is not null && cell.ColumnIndex + 1 == maxColumns) { break; } } } static void LoadRow(IFormulaEvaluator formulaEvaluator, DataTable dataTable, IRow row, bool removeEmptyRows, int? maxColumns) { var dataRow = dataTable.NewRow(); var maxColumnIndex = Math.Min(maxColumns.GetValueOrDefault(dataTable.Columns.Count), dataTable.Columns.Count); for (var columnIndex = 0; columnIndex < maxColumnIndex; columnIndex++) { var cell = row.GetCell(columnIndex, MissingCellPolicy.CREATE_NULL_AS_BLANK); dataRow[columnIndex] = cell.GetCellValue(typeof(string), formulaEvaluator); } if (removeEmptyRows) { var rowContainsData = dataRow.ItemArray.Any(value => value != DBNull.Value && !string.IsNullOrEmpty((string?)value)); if (rowContainsData) { dataTable.Rows.Add(dataRow); } } else { dataTable.Rows.Add(dataRow); } } } /// /// import entityList to workbook first sheet /// /// TEntity /// workbook /// entityList public static int ImportData(this IWorkbook workbook, IEnumerable list) => workbook.ImportData(list, 0); /// /// import entityList to workbook sheet /// /// TEntity /// workbook /// entityList /// sheetIndex /// the sheet LastRowNum public static int ImportData(this IWorkbook workbook, IEnumerable list, int sheetIndex) { Guard.NotNull(workbook); if (workbook is HSSFWorkbook) { if (sheetIndex >= InternalConstants.MaxSheetCountXls) { throw new ArgumentException( string.Format(Resource.IndexOutOfRange, nameof(sheetIndex), InternalConstants.MaxSheetCountXls), nameof(sheetIndex)); } } else { if (sheetIndex >= InternalConstants.MaxSheetCountXlsx) { throw new ArgumentException( string.Format(Resource.IndexOutOfRange, nameof(sheetIndex), InternalConstants.MaxSheetCountXls), nameof(sheetIndex)); } } workbook.CreateSheets(sheetIndex); var sheet = NpoiHelper.EntitiesToSheet(workbook.GetSheetAt(sheetIndex), list, sheetIndex); return sheet.LastRowNum; } /// /// CreateSheets /// /// TEntity /// workbook /// max sheetIndex private static void CreateSheets(this IWorkbook workbook, int sheetIndex) { var configuration = InternalHelper.GetExcelConfigurationMapping(); while (workbook.NumberOfSheets <= sheetIndex) { if (configuration.SheetSettings.TryGetValue(sheetIndex, out var sheetSetting)) { workbook.CreateSheet(sheetSetting.SheetName); } else { workbook.CreateSheet(); } } } /// /// import entityList to sheet /// /// EntityType /// sheet /// entityList public static ISheet ImportData(this ISheet sheet, IEnumerable list) => sheet.ImportData(list, 0); /// /// import entityList to sheet /// /// EntityType /// sheet /// entityList /// sheetIndex public static ISheet ImportData(this ISheet sheet, IEnumerable list, int sheetIndex) => NpoiHelper.EntitiesToSheet(sheet, list, sheetIndex); /// /// import dataTable to workbook first sheet /// /// TEntity /// workbook /// dataTable public static int ImportData(this IWorkbook workbook, DataTable dataTable) => workbook.ImportData(dataTable, 0); /// /// import dataTable to workbook first sheet /// /// TEntity /// workbook /// dataTable /// sheetIndex /// the sheet LastRowNum public static int ImportData(this IWorkbook workbook, DataTable dataTable, int sheetIndex) { Guard.NotNull(workbook); if (workbook is HSSFWorkbook) { if (sheetIndex >= InternalConstants.MaxSheetCountXls) { throw new ArgumentException( string.Format(Resource.IndexOutOfRange, nameof(sheetIndex), InternalConstants.MaxSheetCountXls), nameof(sheetIndex)); } } else { if (sheetIndex >= InternalConstants.MaxSheetCountXlsx) { throw new ArgumentException( string.Format(Resource.IndexOutOfRange, nameof(sheetIndex), InternalConstants.MaxSheetCountXls), nameof(sheetIndex)); } } workbook.CreateSheets(sheetIndex); var sheet = NpoiHelper.DataTableToSheet(workbook.GetSheetAt(sheetIndex), dataTable, sheetIndex); return sheet.LastRowNum; } /// /// import dataTable to sheet /// /// EntityType /// sheet /// dataTable public static ISheet ImportData(this ISheet sheet, DataTable dataTable) => sheet.ImportData(dataTable, 0); /// /// import dataTable to sheet /// /// EntityType /// sheet /// dataTable /// sheetIndex public static ISheet ImportData(this ISheet sheet, DataTable dataTable, int sheetIndex) => NpoiHelper.DataTableToSheet(sheet, dataTable, sheetIndex); /// /// EntityList2ExcelFile /// /// EntityType /// entityList /// excelPath public static void ToExcelFile(this IList entityList, string excelPath) { if (entityList is null) { throw new ArgumentNullException(nameof(entityList)); } var workbook = entityList.GetWorkbookWithAutoSplitSheet( excelPath.EndsWith(".xls", StringComparison.OrdinalIgnoreCase) ? ExcelFormat.Xls : ExcelFormat.Xlsx); workbook.WriteToFile(excelPath, true); } /// /// EntityList2ExcelFile /// /// EntityType /// entityList /// excelPath public static void ToExcelFile(this IEnumerable entityList, string excelPath) => ToExcelFile(entityList, excelPath, 0); /// /// EntityList2ExcelFile /// /// EntityType /// entityList /// excelPath /// sheetIndex public static void ToExcelFile(this IEnumerable entityList, string excelPath, int sheetIndex) { Guard.NotNull(entityList); var configuration = InternalHelper.GetExcelConfigurationMapping(); var workbook = ExcelHelper.PrepareWorkbook(excelPath, configuration.ExcelSetting); workbook.ImportData(entityList, sheetIndex); workbook.WriteToFile(excelPath, true); } /// /// EntityList2ExcelStream(*.xls by default) /// /// EntityType /// entityList /// stream where to write public static void ToExcelStream(this IEnumerable entityList, Stream stream) => ToExcelStream(entityList, stream, ExcelFormat.Xls); /// /// EntityList2ExcelStream /// /// EntityType /// entityList /// stream where to write /// excelFormat /// sheetIndex public static void ToExcelStream(this IEnumerable entityList, Stream stream, ExcelFormat excelFormat, int sheetIndex) { Guard.NotNull(entityList); var configuration = InternalHelper.GetExcelConfigurationMapping(); var workbook = ExcelHelper.PrepareWorkbook(excelFormat, configuration.ExcelSetting); workbook.ImportData(entityList.ToArray(), sheetIndex); workbook.Write(stream); } /// /// EntityList2ExcelStream /// /// EntityType /// entityList /// stream where to write /// excelFormat public static void ToExcelStream(this IEnumerable entityList, Stream stream, ExcelFormat excelFormat) => ToExcelStream(entityList, stream, excelFormat, 0); /// /// EntityList2ExcelStream /// /// EntityType /// entityList /// stream where to write /// excelFormat public static void ToExcelStream(this IList entityList, Stream stream, ExcelFormat excelFormat = ExcelFormat.Xls) { Guard.NotNull(entityList); var workbook = entityList.GetWorkbookWithAutoSplitSheet(excelFormat); workbook.Write(stream); } /// /// EntityList2ExcelBytes(*.xls by default) /// /// EntityType /// entityList public static byte[] ToExcelBytes(this IEnumerable entityList) => ToExcelBytes(entityList, ExcelFormat.Xls); /// /// EntityList2ExcelBytes /// /// EntityType /// entityList /// excelFormat public static byte[] ToExcelBytes(this IEnumerable entityList, ExcelFormat excelFormat) => ToExcelBytes(entityList, excelFormat, 0); /// /// EntityList2ExcelBytes /// /// EntityType /// entityList /// excelFormat /// sheetIndex public static byte[] ToExcelBytes(this IEnumerable entityList, ExcelFormat excelFormat, int sheetIndex) { if (entityList is null) { throw new ArgumentNullException(nameof(entityList)); } var configuration = InternalHelper.GetExcelConfigurationMapping(); var workbook = ExcelHelper.PrepareWorkbook(excelFormat, configuration.ExcelSetting); workbook.ImportData(entityList.ToArray(), sheetIndex); return workbook.ToExcelBytes(true); } /// /// EntityList2ExcelBytes /// /// EntityType /// entityList /// excelFormat public static byte[] ToExcelBytes(this IList entityList, ExcelFormat excelFormat = ExcelFormat.Xls) { if (entityList is null) { throw new ArgumentNullException(nameof(entityList)); } var workbook = entityList.GetWorkbookWithAutoSplitSheet(excelFormat); return workbook.ToExcelBytes(true); } /// /// GetWorkbookWithAutoSplitSheet /// /// entity type /// entity list /// excel format /// excel workbook with data public static IWorkbook GetWorkbookWithAutoSplitSheet(this IList entityList, ExcelFormat excelFormat) { Guard.NotNull(entityList); var configuration = InternalHelper.GetExcelConfigurationMapping(); var workbook = ExcelHelper.PrepareWorkbook(excelFormat, configuration.ExcelSetting); var maxRowCount = excelFormat == ExcelFormat.Xls ? InternalConstants.MaxRowCountXls : InternalConstants.MaxRowCountXlsx; maxRowCount -= configuration.SheetSettings[0].StartRowIndex; var sheetCount = (entityList.Count + maxRowCount - 1) / maxRowCount; workbook.CreateSheets(sheetCount - 1); if (entityList.Count > maxRowCount) { for (var sheetIndex = 0; sheetIndex < sheetCount; sheetIndex++) { workbook.GetSheetAt(sheetIndex) .ImportData(entityList.Skip(sheetIndex * maxRowCount).Take(maxRowCount), 0); } } else { workbook.GetSheetAt(0).ImportData(entityList); } return workbook; } /// /// GetWorkbookWithAutoSplitSheet /// /// dataTable /// excel format /// excelSetting /// excel workbook with data public static IWorkbook GetWorkbookWithAutoSplitSheet(this DataTable dataTable, ExcelFormat excelFormat, ExcelSetting? excelSetting = null) { Guard.NotNull(dataTable); var workbook = ExcelHelper.PrepareWorkbook(excelFormat, excelSetting ?? ExcelHelper.DefaultExcelSetting); var maxRowCount = excelFormat == ExcelFormat.Xls ? InternalConstants.MaxRowCountXls : InternalConstants.MaxRowCountXlsx; maxRowCount -= 1; var sheetCount = (dataTable.Rows.Count + maxRowCount - 1) / maxRowCount; do { workbook.CreateSheet(); } while (workbook.NumberOfSheets < sheetCount); if (dataTable.Rows.Count > maxRowCount) { for (var sheetIndex = 0; sheetIndex < sheetCount; sheetIndex++) { var dt = new DataTable(); foreach (DataColumn col in dataTable.Columns) { dt.Columns.Add(new DataColumn(col.ColumnName, col.DataType)); } for (var i = 0; i < maxRowCount; i++) { var rowIndex = sheetIndex * maxRowCount + i; if (rowIndex >= dataTable.Rows.Count) { break; } var row = dt.NewRow(); row.ItemArray = dataTable.Rows[rowIndex].ItemArray; dt.Rows.Add(row); } workbook.GetSheetAt(sheetIndex).ImportData(dt); } } else { workbook.GetSheetAt(0).ImportData(dataTable); } return workbook; } /// /// export DataTable to excel file /// /// dataTable /// excelPath /// public static void ToExcelFile(this DataTable dataTable, string excelPath) => ToExcelFile(dataTable, excelPath, null); /// /// Import dataTable data /// /// sheet /// dataTable public static void ImportData(this ISheet sheet, DataTable? dataTable) { Guard.NotNull(sheet); if (dataTable is null) { return; } if (dataTable.Columns.Count > 0) { var headerRow = sheet.CreateRow(0); for (var i = 0; i < dataTable.Columns.Count; i++) { var columnName = InternalHelper.GetDecodeColumnName(dataTable.Columns[i].ColumnName); headerRow.CreateCell(i, CellType.String).SetCellValue(columnName); } for (var i = 1; i <= dataTable.Rows.Count; i++) { var row = sheet.CreateRow(i); for (var j = 0; j < dataTable.Columns.Count; j++) { row.CreateCell(j, CellType.String).SetCellValue(dataTable.Rows[i - 1][j]); } } } } /// /// export DataTable to excel file /// /// dataTable /// excelPath /// excelSetting /// public static void ToExcelFile(this DataTable dataTable, string excelPath, ExcelSetting? excelSetting) { Guard.NotNull(dataTable); var workbook = dataTable.GetWorkbookWithAutoSplitSheet( excelPath.EndsWith(".xls", StringComparison.OrdinalIgnoreCase) ? ExcelFormat.Xls : ExcelFormat.Xlsx, excelSetting); workbook.WriteToFile(excelPath, true); } /// /// DataTable2ExcelStream /// /// dataTable /// stream /// public static void ToExcelStream(this DataTable dataTable, Stream stream) => ToExcelStream(dataTable, stream, ExcelFormat.Xls); /// /// DataTable2ExcelStream /// /// dataTable /// stream /// excelFormat /// public static void ToExcelStream(this DataTable dataTable, Stream stream, ExcelFormat excelFormat) => ToExcelStream(dataTable, stream, excelFormat, null); /// /// DataTable2ExcelStream /// /// dataTable /// stream /// excelFormat /// excelSetting /// public static void ToExcelStream(this DataTable dataTable, Stream stream, ExcelFormat excelFormat, ExcelSetting? excelSetting) { Guard.NotNull(dataTable); var workbook = dataTable.GetWorkbookWithAutoSplitSheet(excelFormat, excelSetting); workbook.Write(stream); } /// /// DataTable2ExcelBytes(*.xlsx by default) /// /// dataTable public static byte[] ToExcelBytes(this DataTable dataTable) => ToExcelBytes(dataTable, ExcelFormat.Xls); /// /// DataTable2ExcelBytes /// /// dataTable /// excel格式 public static byte[] ToExcelBytes(this DataTable dataTable, ExcelFormat excelFormat) => ToExcelBytes(dataTable, excelFormat, null); /// /// DataTable2ExcelBytes /// /// dataTable /// excelFormat /// excelSetting public static byte[] ToExcelBytes(this DataTable dataTable, ExcelFormat excelFormat, ExcelSetting? excelSetting) { Guard.NotNull(dataTable); var workbook = dataTable.GetWorkbookWithAutoSplitSheet(excelFormat, excelSetting); return workbook.ToExcelBytes(true); } /// /// SetCellValue /// /// ICell /// value public static void SetCellValue(this ICell cell, object? value) => cell.SetCellValue(value, null); /// /// SetCellValue /// /// ICell /// value /// formatter public static void SetCellValue(this ICell cell, object? value, string? formatter) { Guard.NotNull(cell); if (value is null || DBNull.Value == value) { cell.SetCellType(CellType.Blank); return; } if (value is DateTime time) { cell.SetCellValue(string.IsNullOrWhiteSpace(formatter) ? time.Date == time ? time.ToDateString() : time.ToTimeString() : time.ToString(formatter)); cell.SetCellType(CellType.String); } else { var type = value.GetType(); if ( type == typeof(double) || type == typeof(int) || type == typeof(long) || type == typeof(float) || type == typeof(decimal) ) { cell.SetCellValue(Convert.ToDouble(value)); cell.SetCellType(CellType.Numeric); } else if (type == typeof(bool)) { cell.SetCellValue((bool)value); cell.SetCellType(CellType.Boolean); } else if (type == typeof(byte[]) && value is byte[] bytes) { cell.Sheet.TryAddPicture(cell.RowIndex, cell.ColumnIndex, bytes); } else { cell.SetCellValue(value is IFormattable val && formatter.IsNotNullOrWhiteSpace() ? val.ToString(formatter, CultureInfo.CurrentCulture) : value.ToString()); cell.SetCellType(CellType.String); } } } /// /// GetCellValue /// /// cell /// propertyType /// formulaEvaluator /// cellValue public static object? GetCellValue(this ICell? cell, Type propertyType, IFormulaEvaluator? formulaEvaluator = null) { if (cell is null || cell.CellType == CellType.Blank || cell.CellType == CellType.Error) { return propertyType.GetDefaultValue(); } switch (cell.CellType) { case CellType.Numeric: if (DateUtil.IsCellDateFormatted(cell)) { if (propertyType == typeof(DateTime)) { return cell.DateCellValue; } return cell.DateCellValue.ToOrDefault(propertyType); } if (propertyType == typeof(double)) { return cell.NumericCellValue; } return cell.NumericCellValue.ToOrDefault(propertyType); case CellType.String: return cell.StringCellValue.ToOrDefault(propertyType); case CellType.Boolean: if (propertyType == typeof(bool)) { return cell.BooleanCellValue; } return cell.BooleanCellValue.ToOrDefault(propertyType); case CellType.Formula: try { var evaluatedCellValue = formulaEvaluator?.Evaluate(cell); if (evaluatedCellValue is not null) { if (evaluatedCellValue.CellType == CellType.Blank || evaluatedCellValue.CellType == CellType.Error) { return propertyType.GetDefaultValue(); } if (evaluatedCellValue.CellType == CellType.Numeric) { if (DateUtil.IsCellDateFormatted(cell)) { if (propertyType == typeof(DateTime)) { return cell.DateCellValue; } return cell.DateCellValue.ToOrDefault(propertyType); } if (propertyType == typeof(double)) { return cell.NumericCellValue; } return evaluatedCellValue.NumberValue.ToOrDefault(propertyType); } if (evaluatedCellValue.CellType == CellType.Boolean) { if (propertyType == typeof(bool)) { return cell.BooleanCellValue; } return evaluatedCellValue.BooleanValue.ToOrDefault(propertyType); } if (evaluatedCellValue.CellType == CellType.String) { return evaluatedCellValue.StringValue.ToOrDefault(propertyType); } return evaluatedCellValue.FormatAsString().ToOrDefault(propertyType); } } catch (Exception e) { InvokeHelper.OnInvokeException?.Invoke(e); } return cell.ToString().ToOrDefault(propertyType); default: return cell.ToString().ToOrDefault(propertyType); } } /// /// GetCellValue /// /// Type /// cell /// /// typed cell value public static T GetCellValue(this ICell? cell, IFormulaEvaluator? formulaEvaluator = null) => (T)cell.GetCellValue(typeof(T), formulaEvaluator)!; /// /// Get Sheet Row Collection /// /// excel sheet /// row collection public static NpoiRowCollection GetRowCollection(this ISheet sheet) => new(sheet); /// /// Get Row Cell Collection /// /// excel sheet row /// row collection public static NpoiCellCollection GetCellCollection(this IRow row) => new(row); /// /// get workbook IFormulaEvaluator /// /// workbook /// public static IFormulaEvaluator GetFormulaEvaluator(this IWorkbook workbook) { Guard.NotNull(workbook); return workbook switch { HSSFWorkbook => new HSSFFormulaEvaluator(workbook), XSSFWorkbook => new XSSFFormulaEvaluator(workbook), SXSSFWorkbook sBook => new SXSSFFormulaEvaluator(sBook), _ => throw new NotSupportedException() }; } /// /// get pictures with position in current sheet /// /// sheet /// public static Dictionary GetPicturesAndPosition(this ISheet sheet) { Guard.NotNull(sheet); var dictionary = new Dictionary(); if (sheet.DrawingPatriarch is null) { return dictionary; } if (sheet.Workbook is HSSFWorkbook) { foreach (var shape in ((HSSFPatriarch)sheet.DrawingPatriarch).Children) { if (shape is HSSFPicture picture) { var position = new CellPosition(picture.ClientAnchor.Row1, picture.ClientAnchor.Col1); dictionary[position] = picture.PictureData; } } } else if (sheet.Workbook is XSSFWorkbook) { foreach (var shape in ((XSSFDrawing)sheet.DrawingPatriarch).GetShapes()) { if (shape is XSSFPicture picture) { var position = new CellPosition(picture.ClientAnchor.Row1, picture.ClientAnchor.Col1); dictionary[position] = picture.PictureData; } } } return dictionary; } /// /// TryAddPicture in specific cell /// /// sheet /// cell rowIndex /// cell columnIndex /// pictureData /// whether add success public static bool TryAddPicture(this ISheet sheet, int row, int col, IPictureData pictureData) => TryAddPicture(sheet, row, col, pictureData.Data, pictureData.PictureType); /// /// TryAddPicture in specific cell /// /// sheet /// cell rowIndex /// cell columnIndex /// picture bytes /// picture type /// whether add success public static bool TryAddPicture(this ISheet sheet, int row, int col, byte[] pictureBytes, PictureType pictureType = PictureType.PNG) { Guard.NotNull(sheet); try { var pictureIndex = sheet.Workbook.AddPicture(pictureBytes, pictureType); var clientAnchor = sheet.Workbook.GetCreationHelper().CreateClientAnchor(); clientAnchor.Row1 = row; clientAnchor.Col1 = col; var picture = (sheet.DrawingPatriarch ?? sheet.CreateDrawingPatriarch()) .CreatePicture(clientAnchor, pictureIndex); picture.Resize(); return true; } catch (Exception e) { Debug.WriteLine(e); InvokeHelper.OnInvokeException?.Invoke(e); } return false; } /// /// Write workbook to excel file /// /// workbook /// file path public static void WriteToFile(this IWorkbook workbook, string filePath) => WriteToFile(workbook, filePath, false); /// /// Write workbook to excel file /// /// workbook /// file path /// whether to close the workbook public static void WriteToFile(this IWorkbook workbook, string filePath, bool closeWorkbook) { Guard.NotNull(workbook); try { InternalHelper.EnsureFileIsNotReadOnly(filePath); var dir = Path.GetDirectoryName(filePath); if (!string.IsNullOrWhiteSpace(dir)) { if (!Directory.Exists(dir)) { Directory.CreateDirectory(dir); } } using var fileStream = File.Create(filePath); workbook.Write(fileStream); } finally { if (closeWorkbook) workbook.Close(); } } /// /// ToExcelBytes /// /// workbook /// excel bytes public static byte[] ToExcelBytes(this IWorkbook workbook) => ToExcelBytes(workbook, false); /// /// ToExcelBytes /// /// workbook /// excel bytes /// whether to close the workbook public static byte[] ToExcelBytes(this IWorkbook workbook, bool closeWorkbook) { Guard.NotNull(workbook); try { using var ms = new MemoryStream(); workbook.Write(ms); return ms.ToArray(); } finally { if (closeWorkbook) workbook.Close(); } } #region ExportByTemplate /// /// export excel via template /// /// Entity Type /// entities /// /// templateBytes /// sheetIndex,zero by default /// extraData /// exported excel bytes public static void ToExcelFileByTemplate(this IEnumerable entities, string templatePath, string excelPath, int sheetIndex = 0, object? extraData = null) { Guard.NotNull(entities); Guard.NotNull(templatePath); Guard.NotNull(excelPath); var workbook = ExcelHelper.LoadExcel(templatePath); entities.ToExcelFileByTemplate(workbook, excelPath, sheetIndex, extraData); } /// /// export excel via template /// /// Entity Type /// entities /// templateBytes /// excelFormat /// excelPath /// sheetIndex,zero by default /// extraData /// exported excel bytes public static void ToExcelFileByTemplate(this IEnumerable entities, byte[] templateBytes, string excelPath, ExcelFormat excelFormat = ExcelFormat.Xls, int sheetIndex = 0, object? extraData = null) { Guard.NotNull(entities); Guard.NotNull(templateBytes); Guard.NotNull(excelPath); var workbook = ExcelHelper.LoadExcel(templateBytes, excelFormat); entities.ToExcelFileByTemplate(workbook, excelPath, sheetIndex, extraData); } /// /// export excel via template /// /// Entity Type /// entities /// templateWorkbook /// /// sheetIndex /// extraData /// exported excel bytes public static void ToExcelFileByTemplate(this IEnumerable entities, IWorkbook templateWorkbook, string excelPath, int sheetIndex = 0, object? extraData = null) { Guard.NotNull(entities); Guard.NotNull(templateWorkbook); if (sheetIndex < 0) { sheetIndex = 0; } var templateSheet = templateWorkbook.GetSheetAt(sheetIndex); NpoiTemplateHelper.EntityListToSheetByTemplate( templateSheet, entities, extraData ); templateWorkbook.WriteToFile(excelPath); } /// /// export excel via template /// /// Entity Type /// entities /// templatePath /// sheetIndex,zero by default /// extraData /// exported excel bytes public static byte[] ToExcelBytesByTemplate(this IEnumerable entities, string templatePath, int sheetIndex = 0, object? extraData = null) => ToExcelBytesByTemplate(entities, ExcelHelper.LoadExcel(templatePath), sheetIndex, extraData); /// /// export excel via template /// /// Entity Type /// entities /// templateBytes /// excelFormat /// sheetIndex,zero by default /// extraData /// exported excel bytes public static byte[] ToExcelBytesByTemplate(this IEnumerable entities, byte[] templateBytes, ExcelFormat excelFormat = ExcelFormat.Xls, int sheetIndex = 0, object? extraData = null) { Guard.NotNull(entities); Guard.NotNull(templateBytes); var workbook = ExcelHelper.LoadExcel(templateBytes, excelFormat); return ToExcelBytesByTemplate(entities, workbook, sheetIndex, extraData); } /// /// export excel via template /// /// Entity Type /// entities /// templateStream /// excelFormat /// sheetIndex,zero by default /// extraData /// exported excel bytes public static byte[] ToExcelBytesByTemplate(this IEnumerable entities, Stream templateStream, ExcelFormat excelFormat = ExcelFormat.Xls, int sheetIndex = 0, object? extraData = null) { Guard.NotNull(templateStream); var workbook = ExcelHelper.LoadExcel(templateStream, excelFormat); return ToExcelBytesByTemplate(entities, workbook, sheetIndex, extraData); } /// /// export excel via template /// /// Entity Type /// entities /// templateWorkbook /// sheetIndex /// extraData /// exported excel bytes public static byte[] ToExcelBytesByTemplate(this IEnumerable entities, IWorkbook templateWorkbook, int sheetIndex = 0, object? extraData = null) { Guard.NotNull(entities); Guard.NotNull(templateWorkbook); if (sheetIndex < 0) { sheetIndex = 0; } var templateSheet = templateWorkbook.GetSheetAt(sheetIndex); NpoiTemplateHelper.EntityListToSheetByTemplate( templateSheet, entities, extraData ); return templateWorkbook.ToExcelBytes(); } /// /// export excel via template /// /// Entity Type /// entities /// /// extraData /// exported excel bytes public static byte[] ToExcelBytesByTemplate(this IEnumerable entities, ISheet templateSheet, object? extraData = null) { Guard.NotNull(entities); NpoiTemplateHelper.EntityListToSheetByTemplate( templateSheet, entities, extraData ); return templateSheet.Workbook.ToExcelBytes(); } #endregion ExportByTemplate } ================================================ FILE: src/WeihanLi.Npoi/NpoiHelper.cs ================================================ // Copyright (c) Weihan Li. All rights reserved. // Licensed under the Apache license. using NPOI.SS.UserModel; using NPOI.SS.Util; using System.Data; using System.Diagnostics; using System.Reflection; using WeihanLi.Common; using WeihanLi.Common.Helpers; using WeihanLi.Extensions; using WeihanLi.Npoi.Configurations; using WeihanLi.Npoi.Settings; namespace WeihanLi.Npoi; internal static class NpoiHelper { private static SheetSetting GetSheetSetting(IDictionary sheetSettings, int sheetIndex) => sheetIndex > 0 && sheetSettings.TryGetValue(sheetIndex, out var sheetSetting) ? sheetSetting : sheetSettings[0]; /// /// Converts a sheet to entities while honoring configuration, filters, and pictures. /// /// Entity type. /// Sheet instance to parse. /// Zero-based sheet index. /// Optional callback per entity. /// Sequence of entities, possibly containing null entries. public static IEnumerable SheetToEntities(ISheet? sheet, int sheetIndex, Action, int>? dataAction = null) where TEntity : new() { if (sheet is null || sheet.PhysicalNumberOfRows <= 0) { yield break; } var configuration = InternalHelper.GetExcelConfigurationMapping(); var sheetSetting = GetSheetSetting(configuration.SheetSettings, sheetIndex); var propertyColumnDictionary = InternalHelper.GetPropertyColumnDictionary(configuration); var propertyColumnDic = sheetSetting.HeaderRowIndex >= 0 ? propertyColumnDictionary.ToDictionary(p => p.Key, p => new PropertyConfiguration { ColumnIndex = -1, ColumnFormatter = p.Value.ColumnFormatter, ColumnTitle = p.Value.ColumnTitle, ColumnWidth = p.Value.ColumnWidth, IsIgnored = p.Value.IsIgnored }) : propertyColumnDictionary; var formulaEvaluator = sheet.Workbook.GetFormulaEvaluator(); var pictures = propertyColumnDic .Any(p => p.Key.CanWrite && (p.Key.PropertyType == typeof(byte[]) || p.Key.PropertyType == typeof(IPictureData))) ? sheet.GetPicturesAndPosition() : new Dictionary(); for (var rowIndex = sheet.FirstRowNum; rowIndex <= (sheetSetting.EndRowIndex ?? sheet.LastRowNum); rowIndex++) { var row = sheet.GetRow(rowIndex); // readerHeader and auto adjust the column index when columnIndex adjustment not disabled if (rowIndex == sheetSetting.HeaderRowIndex && !sheetSetting.SkipHeaderRow) { if (row is not null) { // adjust column index according to the imported data header for (var i = row.FirstCellNum; i < row.LastCellNum; i++) { if (row.GetCell(i) is null) { continue; } row.GetCell(i).SetCellType(CellType.String); var title = row.GetCell(i).StringCellValue.Trim(); var col = propertyColumnDic.GetPropertySetting(title); col?.ColumnIndex = i; } } // use default column index if no headers if (propertyColumnDic.Values.Any(p => p.ColumnIndex < 0)) { propertyColumnDic = propertyColumnDictionary; } } else if (rowIndex >= sheetSetting.StartRowIndex) { if (sheetSetting.RowFilter?.Invoke(row) == false) { continue; } if (row is null) { yield return default; } else { TEntity? entity; if (row.Cells.Count > 0) { entity = new TEntity(); if (configuration.EntityType.IsValueType) { var obj = (object)entity; // boxing for value types ProcessImport(obj, row, rowIndex, propertyColumnDic, sheetSetting, formulaEvaluator, pictures); entity = (TEntity)obj; // unboxing } else { ProcessImport(entity, row, rowIndex, propertyColumnDic, sheetSetting, formulaEvaluator, pictures); } } else { entity = default; } if (entity is not null) { foreach (var propertyInfo in propertyColumnDic.Keys) { if (!propertyInfo.CanWrite) continue; var propertyValue = propertyInfo.GetValueGetter()?.Invoke(entity); if (!InternalCache.InputFormatterFuncCache.TryGetValue(propertyInfo, out var formatterFunc) || formatterFunc?.Method is null) continue; var valueSetter = propertyInfo.GetValueSetter(); if (valueSetter is null) continue; try { // apply custom formatterFunc var formattedValue = formatterFunc.DynamicInvoke(entity, propertyValue); valueSetter.Invoke(entity, formattedValue); } catch (Exception e) { Debug.WriteLine(e); InvokeHelper.OnInvokeException?.Invoke(e); } } } if (configuration.DataFilter?.Invoke(entity) == false) { continue; } dataAction?.Invoke(entity, configuration, rowIndex); configuration.PostImportAction?.Invoke(entity, rowIndex); yield return entity; } } } } private static void ProcessImport(object entity, IRow row, int rowIndex, Dictionary propertyColumnDic, SheetSetting sheetSetting, IFormulaEvaluator formulaEvaluator, Dictionary pictures) { foreach (var key in propertyColumnDic.Keys) { var colIndex = propertyColumnDic[key].ColumnIndex; if (colIndex >= 0 && key.CanWrite) { var columnValue = key.PropertyType.GetDefaultValue(); var cell = row.GetCell(colIndex); if (sheetSetting.CellFilter?.Invoke(cell) != false) { var valueSetter = key.GetValueSetter(); if (valueSetter is null) continue; if (key.PropertyType == typeof(byte[]) || key.PropertyType == typeof(IPictureData)) { if (pictures.TryGetValue(new CellPosition(rowIndex, colIndex), out var pic)) { valueSetter.Invoke(entity, key.PropertyType == typeof(IPictureData) ? pic : pic.Data); } } else { var valueApplied = false; InternalCache.CellReaderFuncCache.TryGetValue(key, out var cellReader); if (cellReader?.Method is not null) { columnValue = cellReader.DynamicInvoke(cell); valueApplied = true; } else { InternalCache.ColumnInputFormatterFuncCache.TryGetValue(key, out var formatterFunc); if (formatterFunc?.Method is not null) { var cellValue = cell.GetCellValue(formulaEvaluator); try { // apply custom formatterFunc columnValue = formatterFunc.DynamicInvoke(cellValue); valueApplied = true; } catch (Exception e) { Debug.WriteLine(e); InvokeHelper.OnInvokeException?.Invoke(e); } } } if (valueApplied == false) { columnValue = cell.GetCellValue(key.PropertyType, formulaEvaluator); } valueSetter.Invoke(entity, columnValue); } } } } } /// /// Export entity list to Excel sheet /// /// entity type /// sheet /// entity list /// sheetIndex /// sheet public static ISheet EntitiesToSheet(ISheet sheet, IEnumerable? entityList, int sheetIndex) { Guard.NotNull(sheet); if (entityList is null) { return sheet; } var configuration = InternalHelper.GetExcelConfigurationMapping(); var propertyColumnDictionary = InternalHelper.GetPropertyColumnDictionary(configuration); if (propertyColumnDictionary.Keys.Count == 0) { return sheet; } var sheetSetting = GetSheetSetting(configuration.SheetSettings, sheetIndex); if (sheetSetting.HeaderRowIndex >= 0) { var headerRow = sheet.CreateRow(sheetSetting.HeaderRowIndex); foreach (var key in propertyColumnDictionary.Keys) { var cell = headerRow.CreateCell(propertyColumnDictionary[key].ColumnIndex); cell.SetCellValue(propertyColumnDictionary[key].ColumnTitle); sheetSetting.CellAction?.Invoke(cell); } sheetSetting.RowAction?.Invoke(headerRow); } var rowIndex = 0; foreach (var entity in entityList) { var row = sheet.CreateRow(sheetSetting.StartRowIndex + rowIndex); if (entity is not null) { foreach (var key in propertyColumnDictionary.Keys) { var propertyValue = key.GetValueGetter()?.Invoke(entity); if (InternalCache.OutputFormatterFuncCache.TryGetValue(key, out var formatterFunc) && formatterFunc?.Method is not null) { try { // apply custom formatterFunc propertyValue = formatterFunc.DynamicInvoke(entity, propertyValue); } catch (Exception e) { Debug.WriteLine(e); InvokeHelper.OnInvokeException?.Invoke(e); } } var cell = row.CreateCell(propertyColumnDictionary[key].ColumnIndex); cell.SetCellValue(propertyValue, propertyColumnDictionary[key].ColumnFormatter); sheetSetting.CellAction?.Invoke(cell); } } sheetSetting.RowAction?.Invoke(row); rowIndex++; } PostSheetProcess(sheet, sheetSetting, rowIndex, configuration, propertyColumnDictionary); return sheet; } /// /// Generic type data table to excel sheet /// /// entity type /// sheet /// data table /// sheetIndex /// sheet public static ISheet DataTableToSheet(ISheet sheet, DataTable? dataTable, int sheetIndex) { Guard.NotNull(sheet); if (dataTable is null || dataTable.Rows.Count == 0 || dataTable.Columns.Count == 0) { return sheet; } var configuration = InternalHelper.GetExcelConfigurationMapping(); var propertyColumnDictionary = InternalHelper.GetPropertyColumnDictionary(configuration); if (propertyColumnDictionary.Keys.Count == 0) { return sheet; } var sheetSetting = GetSheetSetting(configuration.SheetSettings, sheetIndex); if (sheetSetting.HeaderRowIndex >= 0) { var headerRow = sheet.CreateRow(sheetSetting.HeaderRowIndex); for (var i = 0; i < dataTable.Columns.Count; i++) { var col = propertyColumnDictionary.GetPropertySettingByPropertyName(dataTable.Columns[i] .ColumnName); if (null != col) { var cell = headerRow.CreateCell(col.ColumnIndex); cell.SetCellValue(col.ColumnTitle); sheetSetting.CellAction?.Invoke(cell); } } sheetSetting.RowAction?.Invoke(headerRow); } for (var i = 0; i < dataTable.Rows.Count; i++) { var row = sheet.CreateRow(sheetSetting.StartRowIndex + i); for (var j = 0; j < dataTable.Columns.Count; j++) { var col = propertyColumnDictionary.GetPropertySettingByPropertyName(dataTable.Columns[j] .ColumnName); var cell = row.CreateCell(col!.ColumnIndex); cell.SetCellValue(dataTable.Rows[i][j], col.ColumnFormatter); sheetSetting.CellAction?.Invoke(cell); } sheetSetting.RowAction?.Invoke(row); } PostSheetProcess(sheet, sheetSetting, dataTable.Rows.Count, configuration, propertyColumnDictionary); return sheet; } private static void PostSheetProcess(ISheet sheet, SheetSetting sheetSetting, int rowsCount, ExcelConfiguration excelConfiguration, IDictionary propertyColumnDictionary) { if (rowsCount > 0) { foreach (var setting in propertyColumnDictionary.Values) { if (setting.ColumnWidth > 0) { sheet.SetColumnWidth(setting.ColumnIndex, setting.ColumnWidth * 256); } else { if (sheetSetting.AutoColumnWidthEnabled) { sheet.AutoSizeColumn(setting.ColumnIndex); } } } foreach (var freezeSetting in excelConfiguration.FreezeSettings) { sheet.CreateFreezePane(freezeSetting.ColSplit, freezeSetting.RowSplit, freezeSetting.LeftMostColumn, freezeSetting.TopRow); } if (excelConfiguration.FilterSetting is not null) { var headerIndex = sheetSetting.HeaderRowIndex >= 0 ? sheetSetting.HeaderRowIndex : 0; sheet.SetAutoFilter(new CellRangeAddress(headerIndex, rowsCount + headerIndex, excelConfiguration.FilterSetting.FirstColumn, excelConfiguration.FilterSetting.LastColumn ?? propertyColumnDictionary.Values.Max(c => c.ColumnIndex))); } } sheetSetting.SheetAction?.Invoke(sheet); } } ================================================ FILE: src/WeihanLi.Npoi/NpoiTemplateHelper.cs ================================================ // Copyright (c) Weihan Li. All rights reserved. // Licensed under the Apache license. using NPOI.SS.UserModel; using System.Diagnostics; using WeihanLi.Common.Helpers; using WeihanLi.Extensions; namespace WeihanLi.Npoi; internal static class NpoiTemplateHelper { /// /// Shared template options used when parsing placeholders. /// public static readonly TemplateOptions s_templateOptions = new(); /// /// Fills a template-driven sheet with the provided entities. /// /// Entity type. /// Destination sheet containing template markers. /// Data source. /// Additional global parameters for the template. /// The populated sheet. public static ISheet EntityListToSheetByTemplate( ISheet sheet, IEnumerable? entityList, object? extraData = null) { if (sheet is null) { throw new ArgumentNullException(nameof(sheet)); } if (entityList is null) { return sheet; } var configuration = InternalHelper.GetExcelConfigurationMapping(); var propertyColumnDictionary = InternalHelper.GetPropertyColumnDictionary(configuration); var formulaEvaluator = sheet.Workbook.GetFormulaEvaluator(); var globalDictionary = extraData.ParseParamInfo() .ToDictionary(x => s_templateOptions.TemplateGlobalParamFormat.FormatWith(x.Key), x => x.Value); foreach (var propertyConfiguration in propertyColumnDictionary) { globalDictionary.Add( s_templateOptions.TemplateHeaderParamFormat.FormatWith(propertyConfiguration.Key.Name), propertyConfiguration.Value.ColumnTitle); } var dataFuncDictionary = propertyColumnDictionary .ToDictionary(x => s_templateOptions.TemplateDataParamFormat.FormatWith(x.Key.Name), x => x.Key.GetValueGetter()); foreach (var key in propertyColumnDictionary.Keys) { if (InternalCache.OutputFormatterFuncCache.TryGetValue(key, out var formatterFunc) && formatterFunc?.Method is not null) { dataFuncDictionary[s_templateOptions.TemplateDataParamFormat.FormatWith(key.Name)] = entity => { var val = key.GetValueGetter()?.Invoke(entity); try { var formattedValue = formatterFunc.DynamicInvoke(entity, val); return formattedValue; } catch (Exception e) { Debug.WriteLine(e); InvokeHelper.OnInvokeException?.Invoke(e); } return val; }; } } // parseTemplate int dataStartRow = -1, dataRowsCount = 0; for (var rowIndex = sheet.FirstRowNum; rowIndex <= sheet.LastRowNum; rowIndex++) { var row = sheet.GetRow(rowIndex); if (row is null) { continue; } for (var cellIndex = row.FirstCellNum; cellIndex < row.LastCellNum; cellIndex++) { var cell = row.GetCell(cellIndex); if (cell is null) { continue; } var cellValue = cell.GetCellValue(formulaEvaluator); if (!string.IsNullOrEmpty(cellValue)) { var beforeValue = cellValue; if (dataStartRow <= 0 || dataRowsCount <= 0) { if (dataStartRow >= 0) { if (cellValue!.Contains(s_templateOptions.TemplateDataEnd)) { dataRowsCount = rowIndex - dataStartRow + 1; cellValue = cellValue.Replace(s_templateOptions.TemplateDataEnd, string.Empty); } } else { if (cellValue!.Contains(s_templateOptions.TemplateDataBegin)) { dataStartRow = rowIndex; cellValue = cellValue.Replace(s_templateOptions.TemplateDataBegin, string.Empty); } } } foreach (var param in globalDictionary.Keys) { if (cellValue!.Contains(param)) { cellValue = cellValue .Replace(param, globalDictionary[param]?.ToString() ?? string.Empty); } } if (beforeValue != cellValue) { cell.SetCellValue(cellValue); } } } } if (dataStartRow >= 0 && dataRowsCount > 0) { foreach (var entity in entityList) { sheet.ShiftRows(dataStartRow, sheet.LastRowNum, dataRowsCount); for (var i = 0; i < dataRowsCount; i++) { var row = sheet.CopyRow(dataStartRow + dataRowsCount + i, dataStartRow + i); if (null != row) { for (var j = 0; j < row.LastCellNum; j++) { var cell = row.GetCell(j); if (null != cell) { var cellValue = cell.GetCellValue(formulaEvaluator); if (!string.IsNullOrEmpty(cellValue) && cellValue!.Contains(s_templateOptions.TemplateDataPrefix)) { var beforeValue = cellValue; foreach (var param in dataFuncDictionary.Keys) { if (cellValue.Contains(param)) { cellValue = cellValue.Replace(param, dataFuncDictionary[param]?.Invoke(entity)?.ToString() ?? string.Empty); } } if (beforeValue != cellValue) { cell.SetCellValue(cellValue); } } } } } } // dataStartRow += dataRowsCount; } // remove data template for (var i = 0; i < dataRowsCount; i++) { var row = sheet.GetRow(dataStartRow + i); if (null != row) { sheet.RemoveRow(row); } } sheet.ShiftRows(dataStartRow + dataRowsCount, sheet.LastRowNum, -dataRowsCount); } return sheet; } } ================================================ FILE: src/WeihanLi.Npoi/Resource.Designer.cs ================================================ //------------------------------------------------------------------------------ // // This code was generated by a tool. // Runtime Version:4.0.30319.42000 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. // //------------------------------------------------------------------------------ namespace WeihanLi.Npoi { using System; /// /// A strongly-typed resource class, for looking up localized strings, etc. /// // This class was auto-generated by the StronglyTypedResourceBuilder // class via a tool like ResGen or Visual Studio. // To add or remove a member, edit your .ResX file then rerun ResGen // with the /str option, or rebuild your VS project. [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class Resource { private static global::System.Resources.ResourceManager resourceMan; private static global::System.Globalization.CultureInfo resourceCulture; [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] internal Resource() { } /// /// Returns the cached ResourceManager instance used by this class. /// [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] internal static global::System.Resources.ResourceManager ResourceManager { get { if (object.ReferenceEquals(resourceMan, null)) { global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("WeihanLi.Npoi.Resource", typeof(Resource).Assembly); resourceMan = temp; } return resourceMan; } } /// /// Overrides the current thread's CurrentUICulture property for all /// resource lookups using this strongly typed resource class. /// [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] internal static global::System.Globalization.CultureInfo Culture { get { return resourceCulture; } set { resourceCulture = value; } } /// /// Looks up a localized string similar to the argument can not be empty. /// internal static string ArgumentCanNotBeEmpty { get { return ResourceManager.GetString("ArgumentCanNotBeEmpty", resourceCulture); } } /// /// Looks up a localized string similar to can not find the file. /// internal static string FileNotFound { get { return ResourceManager.GetString("FileNotFound", resourceCulture); } } /// /// Looks up a localized string similar to {0} out of range, max value: {1}. /// internal static string IndexOutOfRange { get { return ResourceManager.GetString("IndexOutOfRange", resourceCulture); } } /// /// Looks up a localized string similar to invalid excel file. /// internal static string InvalidExcelFile { get { return ResourceManager.GetString("InvalidExcelFile", resourceCulture); } } /// /// Looks up a localized string similar to invalid file path. /// internal static string InvalidFilePath { get { return ResourceManager.GetString("InvalidFilePath", resourceCulture); } } } } ================================================ FILE: src/WeihanLi.Npoi/Resource.resx ================================================  text/microsoft-resx 2.0 System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 the argument can not be empty can not find the file {0} out of range, max value: {1} invalid excel file invalid file path ================================================ FILE: src/WeihanLi.Npoi/Settings/ExcelSetting.cs ================================================ // Copyright (c) Weihan Li. All rights reserved. // Licensed under the Apache license. namespace WeihanLi.Npoi.Settings; /// /// Excel Document Settings /// public sealed class ExcelSetting { /// /// Author /// public string? Author { get; set; } = "WeihanLi"; /// /// Company /// public string? Company { get; set; } = "WeihanLi"; /// /// Title /// public string? Title { get; set; } = "WeihanLi.Npoi"; /// /// Description /// public string? Description { get; set; } = "WeihanLi.Npoi Generated"; /// /// Subject /// public string? Subject { get; set; } = "WeihanLi.Npoi"; /// /// Category /// public string? Category { get; set; } = "WeihanLi.Npoi"; } ================================================ FILE: src/WeihanLi.Npoi/Settings/FilterSetting.cs ================================================ // Copyright (c) Weihan Li. All rights reserved. // Licensed under the Apache license. namespace WeihanLi.Npoi.Settings; internal sealed class FilterSetting { /// /// Initializes a filter specification. /// /// First column index. /// Optional last column index. public FilterSetting(int firstColumn, int? lastColumn) { FirstColumn = firstColumn; LastColumn = lastColumn; } /// /// Gets or sets the first column index. /// public int FirstColumn { get; } /// /// Gets or sets the last column index. /// public int? LastColumn { get; } } ================================================ FILE: src/WeihanLi.Npoi/Settings/FreezeSetting.cs ================================================ // Copyright (c) Weihan Li. All rights reserved. // Licensed under the Apache license. namespace WeihanLi.Npoi.Settings; internal sealed class FreezeSetting { /// /// Initializes a freeze pane using default anchors. /// public FreezeSetting(int colSplit, int rowSplit) : this(colSplit, rowSplit, 0, 1) { } /// /// Initializes a freeze pane with explicit anchors. /// /// Horizontal split position. /// Vertical split position. /// Leftmost column displayed in the right pane. /// Top row displayed in the bottom pane. public FreezeSetting(int colSplit, int rowSplit, int leftmostColumn, int topRow) { ColSplit = colSplit; RowSplit = rowSplit; LeftMostColumn = leftmostColumn; TopRow = topRow; } /// /// horizontal position of split /// public int ColSplit { get; } /// /// Vertical position of split /// public int RowSplit { get; } /// /// Top row visible in bottom pane /// public int LeftMostColumn { get; } /// /// Left column visible in right pane /// public int TopRow { get; } } ================================================ FILE: src/WeihanLi.Npoi/Settings/SheetSetting.cs ================================================ // Copyright (c) Weihan Li. All rights reserved. // Licensed under the Apache license. using NPOI.SS.UserModel; using WeihanLi.Extensions; namespace WeihanLi.Npoi.Settings; /// /// Excel Sheet Settings /// public sealed class SheetSetting { private Func _cellFilter = _ => true; /// /// SheetName /// public string SheetName { get; set { if (value.IsNotNullOrWhiteSpace()) { field = value; } } } = "Sheet0"; /// /// StartRowIndex /// public int StartRowIndex { get; set { if (value >= 0) { field = value; } } } = 1; /// /// HeaderRowIndex /// public int HeaderRowIndex => StartRowIndex - 1; /// /// EndRowIndex, included /// public int? EndRowIndex { get; set; } /// /// Gets or set whether to enable auto column width, disabled by default. /// public bool AutoColumnWidthEnabled { get; set; } /// /// Gets or sets whether to skip column index adjustment based on header row during import. /// When false (default), column indices are automatically adjusted based on header row. /// When true, column indices are used as-is. /// public bool SkipHeaderRow { get; set; } /// /// Cell Filter /// public Func? CellFilter { get => _cellFilter; set => _cellFilter = value ?? (_ => true); } /// /// Row Filter /// public Func? RowFilter { get; set => field = value ?? (_ => true); } = _ => true; /// /// Cell Action on export /// public Action? CellAction { get; set; } /// /// Row Action on export /// public Action? RowAction { get; set; } /// /// Sheet Action on export /// public Action? SheetAction { get; set; } } ================================================ FILE: src/WeihanLi.Npoi/TemplateHelper.cs ================================================ // Copyright (c) Weihan Li. All rights reserved. // Licensed under the Apache license. using WeihanLi.Common; using WeihanLi.Extensions; namespace WeihanLi.Npoi; /// /// Represents the configurable placeholders used by the templated export pipeline. /// public sealed class TemplateOptions { /// /// Global Param Format /// public string TemplateGlobalParamFormat { get; set { if (value.IsNotNullOrWhiteSpace()) { field = value; } } } = InternalConstants.TemplateGlobalParamFormat; /// /// Header Param Format /// public string TemplateHeaderParamFormat { get; set { if (value.IsNotNullOrWhiteSpace()) { field = value; } } } = InternalConstants.TemplateHeaderParamFormat; /// /// Data Param Format /// public string TemplateDataParamFormat { get; set { if (value.IsNotNullOrWhiteSpace()) { field = value; } } } = InternalConstants.TemplateDataParamFormat; /// /// Data Param Prefix /// public string TemplateDataPrefix { get; set { if (value.IsNotNullOrWhiteSpace()) { field = value; } } } = InternalConstants.TemplateDataPrefix; /// /// Data Begin markup /// public string TemplateDataBegin { get; set { if (value.IsNotNullOrWhiteSpace()) { field = value; } } } = InternalConstants.TemplateDataBegin; /// /// Data End markup /// public string TemplateDataEnd { get; set { if (value.IsNotNullOrWhiteSpace()) { field = value; } } } = InternalConstants.TemplateDataEnd; } /// /// Provides helper APIs for configuring template-driven exports. /// public static class TemplateHelper { /// /// Configure TemplateOptions /// /// optionsAction public static void ConfigureTemplateOptions(Action optionsAction) { Guard.NotNull(optionsAction); optionsAction.Invoke(NpoiTemplateHelper.s_templateOptions); } } ================================================ FILE: src/WeihanLi.Npoi/WeihanLi.Npoi.csproj ================================================ True True Resource.resx ResXFileCodeGenerator Resource.Designer.cs ================================================ FILE: test/WeihanLi.Npoi.Test/CsvTest.cs ================================================ // Copyright (c) Weihan Li. All rights reserved. // Licensed under the Apache license. using System.Data; using System.Text; using WeihanLi.Extensions; using WeihanLi.Npoi.Configurations; using WeihanLi.Npoi.Test.Models; using Xunit; namespace WeihanLi.Npoi.Test; public class CsvTest { public CsvTest() { Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); } [Fact] public void BasicImportExportTest() { var list = new List(); for (var i = 0; i < 10; i++) { list.Add(new Notice() { Id = i + 1, Content = $"content_{i}", Title = $"title_{i}", PublishedAt = DateTime.UtcNow.AddDays(-i), Publisher = $"publisher_{i}" }); } list.Add(new Notice() { Id = 11, Content = $"content", Title = $"title", PublishedAt = DateTime.UtcNow.AddDays(1), }); var noticeSetting = FluentSettings.For(); lock (noticeSetting) { var csvBytes = list.ToCsvBytes(); var importedList = CsvHelper.ToEntityList(csvBytes); Assert.Equal(list.Count, importedList.Count); for (var i = 0; i < list.Count; i++) { Assert.NotNull(importedList[i]); var item = importedList[i]!; Assert.Equal(list[i].Id, item.Id); Assert.Equal(list[i].Title ?? "", item.Title); Assert.Equal(list[i].Content ?? "", item.Content); Assert.Equal(list[i].Publisher ?? "", item.Publisher); Assert.Equal(list[i].PublishedAt.ToTimeString(), item.PublishedAt.ToTimeString()); } } } [Fact] public void ImportWithNotSpecificColumnIndex() { IReadOnlyList list = Enumerable.Range(0, 10).Select(i => new Notice() { Id = i + 1, Content = $"content_{i}", Title = $"title_{i}", PublishedAt = DateTime.UtcNow.AddDays(-i), Publisher = $"publisher_{i}" }).ToArray(); // var noticeSetting = FluentSettings.For(); lock (noticeSetting) { var excelBytes = list.ToCsvBytes(); noticeSetting.Property(_ => _.Publisher) .HasColumnIndex(4); noticeSetting.Property(_ => _.PublishedAt) .HasColumnIndex(3); var importedList = CsvHelper.ToEntityList(excelBytes); Assert.Equal(list.Count, importedList.Count); for (var i = 0; i < list.Count; i++) { Assert.NotNull(importedList[i]); var item = importedList[i]!; Assert.Equal(list[i].Id, item.Id); Assert.Equal(list[i].Title ?? "", item.Title); Assert.Equal(list[i].Content ?? "", item.Content); Assert.Equal(list[i].Publisher ?? "", item.Publisher); Assert.Equal(list[i].PublishedAt.ToTimeString(), item.PublishedAt.ToTimeString()); } noticeSetting.Property(_ => _.Publisher) .HasColumnIndex(3); noticeSetting.Property(_ => _.PublishedAt) .HasColumnIndex(4); } } [Fact] public void DataTableImportExportTest() { var dt = new DataTable(); dt.Columns.AddRange(new[] { new DataColumn("Name"), new DataColumn("Age"), new DataColumn("Desc"), }); for (var i = 0; i < 10; i++) { var row = dt.NewRow(); row.ItemArray = new object[] { $"Test_{i}", i + 10, $"Desc_{i}" }; dt.Rows.Add(row); } // var csvBytes = dt.ToCsvBytes(); var importedData = CsvHelper.ToDataTable(csvBytes); Assert.NotNull(importedData); Assert.Equal(dt.Rows.Count, importedData.Rows.Count); for (var i = 0; i < dt.Rows.Count; i++) { Assert.Equal(dt.Rows[i].ItemArray.Length, importedData.Rows[i].ItemArray.Length); for (var j = 0; j < dt.Rows[i].ItemArray.Length; j++) { Assert.Equal(dt.Rows[i].ItemArray[j], importedData.Rows[i].ItemArray[j]); } } } [Theory] [InlineData(@"TestData/EmptyColumns/emptyColumns.csv")] public void DataTableWithFirstLineEmpty(string testDataFilePath) { var bytes = File.ReadAllBytes(testDataFilePath); var importedData = CsvHelper.ToDataTable(bytes); var dt = new DataTable(); dt.Columns.AddRange(new[] { new DataColumn("A"), new DataColumn("B"), new DataColumn("C"), new DataColumn("D"), }); var row = dt.NewRow(); row.ItemArray = new object[] { "", "", "3", "4" }; dt.Rows.Add(row); row = dt.NewRow(); row.ItemArray = new object[] { "", "2", "3", "" }; dt.Rows.Add(row); row = dt.NewRow(); row.ItemArray = new object[] { "1", "2", "", "" }; dt.Rows.Add(row); row = dt.NewRow(); row.ItemArray = new object[] { "1", "2", "3", "4" }; dt.Rows.Add(row); Assert.NotNull(importedData); Assert.Equal(4, importedData.Rows.Count); for (var rowIndex = 0; rowIndex < dt.Rows.Count; rowIndex++) { for (var colIndex = 0; colIndex < dt.Rows[rowIndex].ItemArray.Length; colIndex++) { var expectedValue = dt.Rows[rowIndex].ItemArray[colIndex]?.ToString(); var excelValue = importedData.Rows[rowIndex][colIndex].ToString(); Assert.Equal(expectedValue, excelValue); } } } [Theory] [InlineData(@"TestData/NonStringColumns/nonStringColumns.csv")] public void DataTableImportExportTestWithNonStringColumns(string testDataFilePath) { // Act var importedData = CsvHelper.ToDataTable(testDataFilePath); // Assert var dt = new DataTable(); dt.Columns.AddRange(new[] { new DataColumn("A"), new DataColumn("1000"), new DataColumn("TRUE"), new DataColumn("15/08/2021") }); var row = dt.NewRow(); row.ItemArray = new object[] { "1", "2", "3", "4" }; dt.Rows.Add(row); Assert.NotNull(importedData); Assert.Equal(1, importedData.Rows.Count); // Check columns for (var headerIndex = 0; headerIndex < dt.Columns.Count; headerIndex++) { var expectedValue = dt.Columns[headerIndex].ToString(); var excelValue = importedData.Columns[headerIndex].ToString(); Assert.Equal(expectedValue, excelValue); } // Check rows for (var rowIndex = 0; rowIndex < dt.Rows.Count; rowIndex++) { for (var colIndex = 0; colIndex < dt.Rows[rowIndex].ItemArray.Length; colIndex++) { var expectedValue = dt.Rows[rowIndex].ItemArray[colIndex]?.ToString(); var excelValue = importedData.Rows[rowIndex][colIndex].ToString(); Assert.Equal(expectedValue, excelValue); } } } [Theory] [InlineData("\"XXXXX\"")] [InlineData("XXX")] [InlineData("\"X,XXX\"")] [InlineData("XX\"X")] [InlineData("XX\"\"X")] [InlineData("\"dd\"\"d,1\"")] [InlineData("ddd\nccc")] [InlineData("ddd\r\nccc")] [InlineData(@"bbb ccc")] [InlineData("")] public void ParseCsvLineTest(string str) { var data = new object[] { 1, "tom", 33, str }; var lineData = string.Join(CsvHelper.CsvSeparatorCharacter, data); var cols = CsvHelper.ParseLine(lineData); Assert.Equal(data.Length, cols.Count); for (var i = 0; i < cols.Count; i++) { Assert.Equal(TrimQuotes(data[i].ToString()), cols[i]); } } [Fact] public void GetCsvTextTest_BasicType() { var text = Enumerable.Range(1, 5) .GetCsvText(false); var expected = Enumerable.Range(1, 5) .StringJoin(Environment.NewLine); Assert.Equal(expected, text); } [Theory] [InlineData(true)] [InlineData(false)] public void GetCsvLines_BasicType(bool includeHeader) { var option = new CsvOptions() { IncludeHeader = includeHeader }; var list = new List() { 1, 2, 3 }; var lines = list.GetCsvLines(option).ToArray(); Assert.Equal(includeHeader ? list.Count + 1 : list.Count, lines.Length); var importedList = CsvHelper.GetEntityList(lines, option); Assert.Equal(list.Count, importedList.Count); for (var i = 0; i < list.Count; i++) { Assert.Equal(list[i], importedList[i]); } } [Theory] [InlineData("test.csv")] [InlineData("/tmp/test.csv")] public async Task CsvFileTest(string csvPath) { var list = new List() { new Job() { Id = 1, Name = "123" }, new Job() { Id = 2, Name = "234" } }; Assert.True(list.ToCsvFile(csvPath)); var importedList = CsvHelper.ToEntityList(csvPath); Assert.Equal(list.Count, importedList.Count); for (var i = 0; i < list.Count; i++) { Assert.Equal(list[i], importedList[i]); } File.Delete(csvPath); await Task.CompletedTask; } [Theory] [InlineData(true)] [InlineData(false)] public void GetCsvLines_Entity(bool includeHeader) { var option = new CsvOptions() { IncludeHeader = includeHeader }; var list = new List() { new() { Id = 1, Name = "123" }, new() { Id = 2, Name = "234" } }; var lines = list.GetCsvLines(option).ToArray(); Assert.Equal(includeHeader ? list.Count + 1 : list.Count, lines.Length); var importedList = CsvHelper.GetEntityList(lines, option); Assert.Equal(list.Count, importedList.Count); for (var i = 0; i < list.Count; i++) { Assert.Equal(list[i], importedList[i]); } } [Fact] public void GetCsvTextTest_Entity() { var list = Enumerable.Range(1, 5) .Select(i => new Job() { Id = i + 1, Name = "test" }).ToArray(); var csvText = list.GetCsvText(); var bytes = csvText.GetBytes(); var importedList = CsvHelper.ToEntityList(bytes); Assert.Equal(list.Length, importedList.Count); for (var i = 0; i < list.Length; i++) { Assert.True(list[i] == importedList[i]); } } [Fact] public void CsvStringListTest() { var arr = Enumerable.Range(1, 10) .Select(x => $"str_{x}") .ToArray(); var csvBytes = arr.ToCsvBytes(); Assert.NotNull(csvBytes); var list = CsvHelper.ToEntityList(csvBytes); Assert.Equal(arr.Length, list.Count); Assert.True(arr.SequenceEqual(list)); } [Fact] public void DuplicateColumnTest() { var csvText = $@"A,B,C,A,B,C{Environment.NewLine}1,2,3,4,5,6"; var dataTable = CsvHelper.ToDataTable(csvText.GetBytes()); Assert.Equal(6, dataTable.Columns.Count); Assert.Equal(1, dataTable.Rows.Count); var newCsvText = dataTable.GetCsvText(); Assert.StartsWith("A,B,C,A,B,C", newCsvText); var newDataTable = CsvHelper.ToDataTable(newCsvText.GetBytes()); Assert.Equal(dataTable.Columns.Count, newDataTable.Columns.Count); Assert.Equal(dataTable.Rows.Count, newDataTable.Rows.Count); } [Fact] public void CsvOptionTest_CustomSeparatorCharacter() { var list = new Notice[] { new() { Id = 1, Content = "test", Title = "test", Publisher = "test", PublishedAt = DateTime.Now } }; var text = list.GetCsvText(); var text2 = list.GetCsvText(new CsvOptions() { SeparatorCharacter = '\t' }); Assert.NotEqual(text, text2); Assert.Equal(text, text2.Replace('\t', ',')); } [Fact] public void CsvToListEncodingTest() { var list = new List() { new() { Age = 1, Name = "中华小当家" } }; var encoding = Encoding.GetEncoding("gb2312"); var bytes = list.ToCsvBytes(new CsvOptions() { Encoding = encoding }); var importedList = CsvHelper.ToEntityList(bytes, new CsvOptions() { Encoding = encoding }); Assert.Equal(list.Count, importedList.Count); for (var i = 0; i < list.Count; i++) { Assert.Equal(list[i], importedList[i]); } } [Fact] public void CsvToDataTableEncodingTest() { var list = new List() { new() { Age = 1, Name = "中华小当家" } }; var encoding = Encoding.GetEncoding("gb2312"); var bytes = list.ToCsvBytes(new CsvOptions() { Encoding = encoding }); var dataTable = CsvHelper.ToDataTable(bytes, new CsvOptions() { Encoding = encoding }); Assert.Equal(list.Count, dataTable.Rows.Count); for (var i = 0; i < list.Count; i++) { Assert.Equal(list[i].Name, dataTable.Rows[i]["Name"]); } } [Fact] public void CsvToListEncodingTest_NotTheSameEncoding() { var list = new List() { new() { Age = 1, Name = "中华小当家" } }; var encoding = Encoding.GetEncoding("gb2312"); var bytes = list.ToCsvBytes(new CsvOptions() { Encoding = encoding }); var importedList = CsvHelper.ToEntityList(bytes); Assert.Equal(list.Count, importedList.Count); for (var i = 0; i < list.Count; i++) { Assert.NotEqual(list[i], importedList[i]); } } private static string TrimQuotes(string? str) { if (string.IsNullOrEmpty(str)) { return string.Empty; } // if (str[0] == CsvHelper.CsvQuoteCharacter) { return str.Substring(1, str.Length - 2).Replace("\"\"", "\""); } return str; } private sealed record TestModel { public string Name { get; set; } = string.Empty; public int Age { get; set; } } } ================================================ FILE: test/WeihanLi.Npoi.Test/ExcelFormatData.cs ================================================ // Copyright (c) Weihan Li. All rights reserved. // Licensed under the Apache license. using Xunit; namespace WeihanLi.Npoi.Test; public sealed class ExcelFormatData : TheoryData { public ExcelFormatData() { Add(ExcelFormat.Xls); Add(ExcelFormat.Xlsx); } } ================================================ FILE: test/WeihanLi.Npoi.Test/ExcelTest.cs ================================================ // Copyright (c) Weihan Li. All rights reserved. // Licensed under the Apache license. using NPOI.HSSF.UserModel; using NPOI.SS.UserModel; using System.Data; using System.Globalization; using System.Reflection; using System.Text.RegularExpressions; using WeihanLi.Common; using WeihanLi.Common.Helpers; using WeihanLi.Common.Models; using WeihanLi.Common.Services; using WeihanLi.Extensions; using WeihanLi.Npoi.Attributes; using WeihanLi.Npoi.Configurations; using WeihanLi.Npoi.Test.Models; using Xunit; namespace WeihanLi.Npoi.Test; public class ExcelTest { [Theory] [ClassData(typeof(ExcelFormatData))] public void BasicImportExportTest(ExcelFormat excelFormat) { var list = new List(); for (var i = 0; i < 10; i++) { list.Add(new Notice() { Id = i + 1, Content = $"content_{i}", Title = $"title_{i}", PublishedAt = DateTime.UtcNow.AddDays(-i), Publisher = $"publisher_{i}" }); } list.Add(new Notice() { Title = "nnnn" }); list.Add(null); var noticeSetting = FluentSettings.For(); lock (noticeSetting) { var excelBytes = list.ToExcelBytes(excelFormat); var importedList = ExcelHelper.ToEntityList(excelBytes, excelFormat); Assert.Equal(list.Count, importedList.Count); for (var i = 0; i < list.Count; i++) { if (list[i] is null) { Assert.Null(importedList[i]); } else { Assert.NotNull(importedList[i]); var sourceItem = list[i]!; var item = importedList[i]!; Assert.Equal(sourceItem.Id, item.Id); Assert.Equal(sourceItem.Title, item.Title); Assert.Equal(sourceItem.Content, item.Content); Assert.Equal(sourceItem.Publisher, item.Publisher); Assert.Equal(sourceItem.PublishedAt.ToTimeString(), item.PublishedAt.ToTimeString()); } } } } [Theory] [ClassData(typeof(ExcelFormatData))] public void BasicImportExportTestWithEmptyValue(ExcelFormat excelFormat) { var list = new List(); for (var i = 0; i < 10; i++) { list.Add(new Notice() { Id = i + 1, Content = i < 3 ? $"content_{i}" : string.Empty, Title = $"title_{i}", PublishedAt = DateTime.UtcNow.AddDays(-i), Publisher = i < 3 ? $"publisher_{i}" : null }); } list.Add(new Notice() { Title = "nnnn" }); list.Add(null); var noticeSetting = FluentSettings.For(); lock (noticeSetting) { var excelBytes = list.ToExcelBytes(excelFormat); var importedList = ExcelHelper.ToEntityList(excelBytes, excelFormat); Assert.Equal(list.Count, importedList.Count); for (var i = 0; i < list.Count; i++) { if (list[i] is null) { Assert.Null(importedList[i]); } else { Assert.NotNull(importedList[i]); var sourceItem = list[i]!; var item = importedList[i]!; Assert.Equal(sourceItem.Id, item.Id); Assert.Equal(sourceItem.Title, item.Title); Assert.Equal(sourceItem.Content, item.Content); Assert.Equal(sourceItem.Publisher, item.Publisher); Assert.Equal(sourceItem.PublishedAt.ToTimeString(), item.PublishedAt.ToTimeString()); } } } } [Theory] [ClassData(typeof(ExcelFormatData))] public void BasicImportExportWithoutHeaderTest(ExcelFormat excelFormat) { var list = new List(); for (var i = 0; i < 10; i++) { list.Add(new Notice() { Id = i + 1, Content = $"content_{i}", Title = $"title_{i}", PublishedAt = DateTime.UtcNow.AddDays(-i), Publisher = $"publisher_{i}" }); } list.Add(new Notice() { Title = "nnnn" }); list.Add(null); var noticeSetting = FluentSettings.For(); lock (noticeSetting) { noticeSetting.HasSheetConfiguration(0, "test", 0); var excelBytes = list.ToExcelBytes(excelFormat); var importedList = ExcelHelper.ToEntityList(excelBytes, excelFormat); Assert.Equal(list.Count, importedList.Count); for (var i = 0; i < list.Count; i++) { if (list[i] is null) { Assert.Null(importedList[i]); } else { Assert.NotNull(importedList[i]); var sourceItem = list[i]!; var item = importedList[i]!; Assert.Equal(sourceItem.Id, item.Id); Assert.Equal(sourceItem.Title, item.Title); Assert.Equal(sourceItem.Content, item.Content); Assert.Equal(sourceItem.Publisher, item.Publisher); Assert.Equal(sourceItem.PublishedAt.ToTimeString(), item.PublishedAt.ToTimeString()); } } noticeSetting.HasSheetConfiguration(0, "test", 1); } } [Theory] [ClassData(typeof(ExcelFormatData))] public void ImportWithNotSpecificColumnIndex(ExcelFormat excelFormat) { IReadOnlyList list = Enumerable.Range(0, 10).Select(i => new Notice() { Id = i + 1, Content = $"content_{i}", Title = $"title_{i}", PublishedAt = DateTime.UtcNow.AddDays(-i), Publisher = $"publisher_{i}" }).ToArray(); // var noticeSetting = FluentSettings.For(); lock (noticeSetting) { var excelBytes = list.ToExcelBytes(excelFormat); noticeSetting.Property(_ => _.Publisher) .HasColumnIndex(4); noticeSetting.Property(_ => _.PublishedAt) .HasColumnIndex(3); var importedList = ExcelHelper.ToEntityList(excelBytes, excelFormat); Assert.Equal(list.Count, importedList.Count); for (var i = 0; i < list.Count; i++) { if (importedList[i] is null) { Assert.Null(list[i]); } else { var item = importedList[i]; Assert.NotNull(item); Guard.NotNull(item); var sourceItem = list[i]; Assert.Equal(sourceItem.Id, item.Id); Assert.Equal(sourceItem.Title, item.Title); Assert.Equal(sourceItem.Content, item.Content); Assert.Equal(sourceItem.Publisher, item.Publisher); Assert.Equal(sourceItem.PublishedAt.ToTimeString(), item.PublishedAt.ToTimeString()); } } noticeSetting.Property(_ => _.Publisher) .HasColumnIndex(3); noticeSetting.Property(_ => _.PublishedAt) .HasColumnIndex(4); } } [Theory] [ClassData(typeof(ExcelFormatData))] public void ShadowPropertyTest(ExcelFormat excelFormat) { IReadOnlyList list = Enumerable.Range(0, 10).Select(i => new Notice() { Id = i + 1, Content = $"content_{i}", Title = $"title_{i}", PublishedAt = DateTime.UtcNow.AddDays(-i), Publisher = $"publisher_{i}" }).ToArray(); var noticeSetting = FluentSettings.For(); lock (noticeSetting) { noticeSetting.Property("ShadowProperty") .HasOutputFormatter((x, _) => $"{x?.Id}...") ; var excelBytes = list.ToExcelBytes(excelFormat); // list.ToExcelFile($"{Directory.GetCurrentDirectory()}/output.xlsx"); var importedList = ExcelHelper.ToEntityList(excelBytes, excelFormat); Assert.Equal(list.Count, importedList.Count); for (var i = 0; i < list.Count; i++) { var item = importedList[i]; Assert.NotNull(item); Guard.NotNull(item); var sourceItem = list[i]; Assert.Equal(sourceItem.Id, item.Id); Assert.Equal(sourceItem.Title, item.Title); Assert.Equal(sourceItem.Content, item.Content); Assert.Equal(sourceItem.Publisher, item.Publisher); Assert.Equal(sourceItem.PublishedAt.ToTimeString(), item.PublishedAt.ToTimeString()); } } } [Theory] [ClassData(typeof(ExcelFormatData))] public void IgnoreInheritPropertyTest(ExcelFormat excelFormat) { IReadOnlyList list = Enumerable.Range(0, 10).Select(i => new Notice() { Id = i + 1, Content = $"content_{i}", Title = $"title_{i}", PublishedAt = DateTime.UtcNow.AddDays(-i), Publisher = $"publisher_{i}" }).ToArray(); var settings = FluentSettings.For(); lock (settings) { settings.Property(x => x.Id).Ignored(); var excelBytes = list.ToExcelBytes(excelFormat); // list.ToExcelFile($"{Directory.GetCurrentDirectory()}/ttt.xls"); var importedList = ExcelHelper.ToEntityList(excelBytes, excelFormat); Assert.Equal(list.Count, importedList.Count); for (var i = 0; i < list.Count; i++) { if (importedList[i] is null) { Assert.Null(list[i]); } else { var item = importedList[i]; Assert.NotNull(item); Guard.NotNull(item); var sourceItem = list[i]; //Assert.Equal(sourceItem.Id, item.Id); Assert.Equal(sourceItem.Title, item.Title); Assert.Equal(sourceItem.Content, item.Content); Assert.Equal(sourceItem.Publisher, item.Publisher); Assert.Equal(sourceItem.PublishedAt.ToTimeString(), item.PublishedAt.ToTimeString()); } } settings.Property(_ => _.Id) .Ignored(false) .HasColumnIndex(0); } } [Theory] [ClassData(typeof(ExcelFormatData))] public void ColumnInputFormatterTest(ExcelFormat excelFormat) { IReadOnlyList list = Enumerable.Range(0, 10).Select(i => new Notice() { Id = i + 1, Content = $"content_{i}", Title = $"title_{i}", PublishedAt = DateTime.UtcNow.AddDays(-i), Publisher = $"publisher_{i}" }).ToArray(); var excelBytes = list.ToExcelBytes(excelFormat); var settings = FluentSettings.For(); lock (settings) { settings.Property(x => x.Title).HasColumnInputFormatter(x => $"{x}_Test"); var importedList = ExcelHelper.ToEntityList(excelBytes, excelFormat); Assert.Equal(list.Count, importedList.Count); for (var i = 0; i < list.Count; i++) { var item = importedList[i]; Assert.NotNull(item); Guard.NotNull(item); Assert.Equal(list[i].Id, item.Id); Assert.Equal(list[i].Title + "_Test", item.Title); Assert.Equal(list[i].Content, item.Content); Assert.Equal(list[i].Publisher, item.Publisher); Assert.Equal(list[i].PublishedAt.ToTimeString(), item.PublishedAt.ToTimeString()); } settings.Property(_ => _.Title).HasColumnInputFormatter(null); } } [Theory] [ClassData(typeof(ExcelFormatData))] public void InputOutputColumnFormatterTest(ExcelFormat excelFormat) { IReadOnlyList list = Enumerable.Range(0, 10).Select(i => new Notice() { Id = i + 1, Content = $"content_{i}", Title = $"title_{i}", PublishedAt = DateTime.UtcNow.AddDays(-i), Publisher = $"publisher_{i}" }).ToArray(); var settings = FluentSettings.For(); lock (settings) { settings.Property(x => x.Id) .HasColumnOutputFormatter(x => $"{x}_Test") .HasColumnInputFormatter(x => Convert.ToInt32(x?.Split(new[] { '_' }, StringSplitOptions.RemoveEmptyEntries)[0])); var excelBytes = list.ToExcelBytes(excelFormat); var importedList = ExcelHelper.ToEntityList(excelBytes, excelFormat); Assert.Equal(list.Count, importedList.Count); for (var i = 0; i < list.Count; i++) { var item = importedList[i]; Assert.NotNull(item); Guard.NotNull(item); var sourceItem = list[i]; Assert.Equal(sourceItem.Id, item.Id); Assert.Equal(sourceItem.Title, item.Title); Assert.Equal(sourceItem.Content, item.Content); Assert.Equal(sourceItem.Publisher, item.Publisher); Assert.Equal(sourceItem.PublishedAt.ToTimeString(), item.PublishedAt.ToTimeString()); } settings.Property(x => x.Id) .HasColumnOutputFormatter(null) .HasColumnInputFormatter(null); } } [Theory] [ClassData(typeof(ExcelFormatData))] public void DataValidationTest(ExcelFormat excelFormat) { IReadOnlyList list = Enumerable.Range(0, 10).Select(i => new Notice() { Id = i + 1, Content = $"content_{i}", Title = $"title_{i}", PublishedAt = DateTime.UtcNow.AddDays(-i), Publisher = $"publisher_{i}" }).ToArray(); var excelBytes = list.ToExcelBytes(excelFormat); var settings = FluentSettings.For(); lock (settings) { settings.WithDataFilter(x => x?.Id > 5); var importedList = ExcelHelper.ToEntityList(excelBytes, excelFormat); Assert.Equal(list.Count(x => x.Id > 5), importedList.Count); int i = 0, k = 0; while (list[k].Id != importedList[i]?.Id) { k++; } for (; i < importedList.Count; i++, k++) { var item = importedList[i]; Assert.NotNull(item); Guard.NotNull(item); var sourceItem = list[k]; Assert.Equal(sourceItem.Id, item.Id); Assert.Equal(sourceItem.Title, item.Title); Assert.Equal(sourceItem.Content, item.Content); Assert.Equal(sourceItem.Publisher, item.Publisher); Assert.Equal(sourceItem.PublishedAt.ToTimeString(), item.PublishedAt.ToTimeString()); } settings.WithDataFilter(null); } } [Theory] [ClassData(typeof(ExcelFormatData))] public void DataTableImportExportTest(ExcelFormat excelFormat) { var dt = new DataTable(); dt.Columns.AddRange(new[] { new DataColumn("Name"), new DataColumn("Age"), new DataColumn("Desc"), }); for (var i = 0; i < 10; i++) { var row = dt.NewRow(); row.ItemArray = new object[] { $"Test_{i}", i + 10, $"Desc_{i}" }; dt.Rows.Add(row); } // var excelBytes = dt.ToExcelBytes(excelFormat); var importedData = ExcelHelper.ToDataTable(excelBytes, excelFormat); Assert.NotNull(importedData); Assert.Equal(dt.Rows.Count, importedData.Rows.Count); for (var i = 0; i < dt.Rows.Count; i++) { Assert.Equal(dt.Rows[i].ItemArray.Length, importedData.Rows[i].ItemArray.Length); for (var j = 0; j < dt.Rows[i].ItemArray.Length; j++) { Assert.Equal(dt.Rows[i].ItemArray[j], importedData.Rows[i].ItemArray[j]); } } } [Theory] [ClassData(typeof(ExcelFormatData))] public void DataTableImportExportWithEmptyValueTest(ExcelFormat excelFormat) { var dt = new DataTable(); dt.Columns.AddRange(new[] { new DataColumn("Name"), new DataColumn("Age"), new DataColumn("Desc"), }); for (var i = 0; i < 10; i++) { var row = dt.NewRow(); row.ItemArray = new object?[] { i < 4 ? $"Test_{i}" : null, i + 10, i % 2 == 0 ? $"Desc_{i}" : string.Empty }; dt.Rows.Add(row); } // var excelBytes = dt.ToExcelBytes(excelFormat); var importedData = ExcelHelper.ToDataTable(excelBytes, excelFormat); Assert.NotNull(importedData); Assert.Equal(dt.Rows.Count, importedData.Rows.Count); Assert.Equal(dt.Columns.Count, importedData.Columns.Count); for (var i = 0; i < dt.Rows.Count; i++) { Assert.Equal(dt.Rows[i].ItemArray.Length, importedData.Rows[i].ItemArray.Length); for (var j = 0; j < dt.Columns.Count; j++) { Assert.Equal(dt.Rows[i].ItemArray[j], importedData.Rows[i].ItemArray[j]); } } } [Theory] [ClassData(typeof(ExcelFormatData))] public void ExcelImportWithFormula(ExcelFormat excelFormat) { var setting = FluentSettings.For(); setting.HasSheetConfiguration(0, "Test", 0); setting.Property(x => x.Num1).HasColumnIndex(0); setting.Property(x => x.Num2).HasColumnIndex(1); setting.Property(x => x.Sum).HasColumnIndex(2); var workbook = ExcelHelper.PrepareWorkbook(excelFormat); var sheet = workbook.CreateSheet(); var row = sheet.CreateRow(0); row.CreateCell(0, CellType.Numeric).SetCellValue(1); row.CreateCell(1, CellType.Numeric).SetCellValue(2); row.CreateCell(2, CellType.Formula).SetCellFormula("$A1+$B1"); var excelBytes = workbook.ToExcelBytes(); var list = ExcelHelper.ToEntityList(excelBytes, excelFormat); Assert.NotNull(list); Assert.NotEmpty(list); Assert.NotNull(list[0]); Assert.Equal(1, list[0]!.Num1); Assert.Equal(2, list[0]!.Num2); Assert.Equal(3, list[0]!.Sum); } // ReSharper disable UnusedAutoPropertyAccessor.Local private class ExcelFormulaTestModel { public int Num1 { get; set; } public int Num2 { get; set; } public int Sum { get; set; } } [Theory] [ClassData(typeof(ExcelFormatData))] public void ExcelImportWithCellFilter(ExcelFormat excelFormat) { IReadOnlyList list = Enumerable.Range(0, 10).Select(i => new Notice() { Id = i + 1, Content = $"content_{i}", Title = $"title_{i}", PublishedAt = DateTime.UtcNow.AddDays(-i), Publisher = $"publisher_{i}" }).ToArray(); var excelBytes = list.ToExcelBytes(excelFormat); var settings = FluentSettings.For(); lock (settings) { settings.HasSheetSetting(setting => { setting.CellFilter = cell => cell.ColumnIndex == 0; }); var importedList = ExcelHelper.ToEntityList(excelBytes, excelFormat); Assert.Equal(list.Count, importedList.Count); for (var i = 0; i < list.Count; i++) { var item = importedList[i]; Assert.NotNull(item); Guard.NotNull(item); Assert.Equal(list[i].Id, item.Id); Assert.Null(item.Title); Assert.Null(item.Content); Assert.Null(item.Publisher); Assert.Equal(default(DateTime).ToTimeString(), item.PublishedAt.ToTimeString()); } settings.HasSheetSetting(setting => { setting.CellFilter = _ => true; }); } } [Theory] [ClassData(typeof(ExcelFormatData))] public void ExcelImportWithCellFilterAttributeTest(ExcelFormat excelFormat) { IReadOnlyList list = Enumerable.Range(0, 10).Select(i => new CellFilterAttributeTest() { Id = i + 1, Description = $"content_{i}", Name = $"title_{i}", }).ToArray(); var excelBytes = list.ToExcelBytes(excelFormat); var importedList = ExcelHelper.ToEntityList(excelBytes, excelFormat); Assert.NotNull(importedList); Assert.Equal(list.Count, importedList.Count); for (var i = 0; i < importedList.Count; i++) { Assert.NotNull(importedList[i]); var item = importedList[i]!; Assert.Equal(list[i].Id, item.Id); Assert.Equal(list[i].Name, item.Name); Assert.Null(item.Description); } } [Sheet(SheetName = "test", AutoColumnWidthEnabled = true, StartColumnIndex = 0, EndColumnIndex = 1)] private class CellFilterAttributeTest { [Column(Index = 0)] public int Id { get; set; } [Column(Index = 1)] public string? Name { get; set; } [Column(Index = 2)] public string? Description { get; set; } } [Theory] [InlineData(ExcelFormat.Xls, 1000, 1)] [InlineData(ExcelFormat.Xls, 65536, 2)] [InlineData(ExcelFormat.Xls, 132_000, 3)] //[InlineData(ExcelFormat.Xls, 1_000_000, 16)] //[InlineData(ExcelFormat.Xlsx, 1_048_576, 2)] public void EntityListAutoSplitSheetsTest(ExcelFormat excelFormat, int rowsCount, int expectedSheetCount) { var list = Enumerable.Range(1, rowsCount) .Select(x => new Notice() { Id = x, Content = $"content_{x}", Title = $"title_{x}", Publisher = $"publisher_{x}" }) .ToArray(); var bytes = list.ToExcelBytes(excelFormat); var workbook = ExcelHelper.LoadExcel(bytes, excelFormat); Assert.Equal(expectedSheetCount, workbook.NumberOfSheets); } [Theory] [InlineData(ExcelFormat.Xls, 1000, 1)] [InlineData(ExcelFormat.Xls, 65536, 2)] [InlineData(ExcelFormat.Xls, 132_000, 3)] //[InlineData(ExcelFormat.Xls, 1_000_000, 16)] //[InlineData(ExcelFormat.Xlsx, 1_048_576, 2)] public void DataTableAutoSplitSheetsTest(ExcelFormat excelFormat, int rowsCount, int expectedSheetCount) { var dataTable = new DataTable(); dataTable.Columns.Add(new DataColumn("Id", typeof(int))); for (var i = 0; i < rowsCount; i++) { var row = dataTable.NewRow(); row.ItemArray = new object[] { i+1 }; dataTable.Rows.Add(row); } Assert.Equal(rowsCount, dataTable.Rows.Count); var bytes = dataTable.ToExcelBytes(excelFormat); var workbook = ExcelHelper.LoadExcel(bytes, excelFormat); Assert.Equal(expectedSheetCount, workbook.NumberOfSheets); } [Theory] [InlineData(@"TestData/EmptyColumns/emptyColumns.xls", ExcelFormat.Xls)] [InlineData(@"TestData/EmptyColumns/emptyColumns.xlsx", ExcelFormat.Xlsx)] public void DataTableImportExportTestWithFirstColumnsEmpty(string file, ExcelFormat excelFormat) { // Arrange var excelBytes = File.ReadAllBytes(file); // Act var importedData = ExcelHelper.ToDataTable(excelBytes, excelFormat); // Assert var dt = new DataTable(); dt.Columns.AddRange(new[] { new DataColumn("A"), new DataColumn("B"), new DataColumn("C"), new DataColumn("D"), }); dt.AddNewRow(new object[] { "", "", "3", "4" }); dt.AddNewRow(new object[] { "", "2", "3", "" }); dt.AddNewRow(new object[] { "1", "2", "", "" }); dt.AddNewRow(new object[] { "1", "2", "3", "4" }); Assert.NotNull(importedData); Assert.Equal(4, importedData.Rows.Count); importedData.AssertEquals(dt); } [Theory] [InlineData(@"TestData/NonStringColumns/nonStringColumns.xls", ExcelFormat.Xls)] [InlineData(@"TestData/NonStringColumns/nonStringColumns.xlsx", ExcelFormat.Xlsx)] public void DataTableImportExportTestWithNonStringColumns(string file, ExcelFormat excelFormat) { // Arrange var excelBytes = File.ReadAllBytes(file); // Act var importedData = ExcelHelper.ToDataTable(excelBytes, excelFormat); // Assert var dt = new DataTable(); dt.Columns.AddRange(new[] { new DataColumn("A"), new DataColumn("1000"), new DataColumn("TRUE"), // Excel value will loaded as "True". new DataColumn(DateTime.ParseExact("15/08/2021", "dd/MM/yyyy", CultureInfo.InvariantCulture).ToShortDateString()), }); dt.AddNewRow(new object[] { "1", "2", "3", "4" }); Assert.NotNull(importedData); Assert.Equal(1, importedData.Rows.Count); importedData.AssertEquals(dt); } [Theory] [InlineData(@"TestData/EmptyRows/emptyRows.xls", ExcelFormat.Xls)] [InlineData(@"TestData/EmptyRows/emptyRows.xlsx", ExcelFormat.Xlsx)] public void DataTableImportExportTestWithoutEmptyRowsAndAdditionalColumns(string file, ExcelFormat excelFormat) { // Arrange var excelBytes = File.ReadAllBytes(file); // Act var importedData = ExcelHelper.ToDataTable(excelBytes, excelFormat, removeEmptyRows: true, maxColumns: 3); // Assert var dt = new DataTable(); dt.Columns.AddRange(new[] { new DataColumn("A"), new DataColumn("B"), new DataColumn("C"), }); dt.AddNewRow(new object[] { "1", "2", "3" }); dt.AddNewRow(new object[] { "1", "", "" }); dt.AddNewRow(new object[] { "1", "2", "3" }); dt.AddNewRow(new object[] { "", "2", "3" }); Assert.NotNull(importedData); Assert.Equal(4, importedData.Rows.Count); importedData.AssertEquals(dt); } [Theory] [ClassData(typeof(ExcelFormatData))] public async Task ImageImportExportTest(ExcelFormat excelFormat) { var imageBytes = await HttpHelper.HttpClient.GetByteArrayAsync( "https://www.nuget.org/profiles/weihanli/avatar?imageSize=64", TestContext.Current.CancellationToken ); var list = Enumerable.Range(1, 5) .Select(x => new ImageTest() { Id = x, Image = imageBytes }) .ToList(); var excelBytes = list.ToExcelBytes(excelFormat); var importResult = ExcelHelper.ToEntityList(excelBytes, excelFormat); Assert.NotNull(importResult); Assert.Equal(list.Count, importResult.Count); for (var i = 0; i < list.Count; i++) { Assert.NotNull(importResult[i]); var result = importResult[i]!; Assert.Equal(list[i].Id, result.Id); Assert.True(list[i].Image.SequenceEqual(result.Image)); } } [Theory] [ClassData(typeof(ExcelFormatData))] public async Task ImageImportExportPictureDataTest(ExcelFormat excelFormat) { var imageBytes = await HttpHelper.HttpClient.GetByteArrayAsync( "https://www.nuget.org/profiles/weihanli/avatar?imageSize=64", TestContext.Current.CancellationToken ); var list = Enumerable.Range(1, 5) .Select(x => new ImageTest() { Id = x, Image = imageBytes }) .ToList(); var excelBytes = list.ToExcelBytes(excelFormat); var importResult = ExcelHelper.ToEntityList(excelBytes, excelFormat); Assert.NotNull(importResult); Assert.Equal(list.Count, importResult.Count); for (var i = 0; i < list.Count; i++) { Assert.NotNull(importResult[i]); var result = importResult[i]!; Assert.Equal(list[i].Id, result.Id); Assert.NotNull(result.Image); Assert.True(list[i].Image.SequenceEqual(result.Image.Data)); Assert.Equal(PictureType.PNG, result.Image.PictureType); } } [Fact] public void DataTableDefaultValueTest() { var table = new DataTable(); table.Columns.Add(new DataColumn("Name")); table.Columns.Add(new DataColumn("Value")); table.Columns.Add(new DataColumn("Description")); var row = table.AddNewRow(); row["Value"] = null; row["Description"] = "test"; Assert.Equal(DBNull.Value, table.Rows[0]["Name"]); Assert.Equal(DBNull.Value, table.Rows[0]["Value"]); Assert.NotNull(table.Rows[0][0]); Assert.Equal("test", table.Rows[0]["Description"]); } [Theory] [ClassData(typeof(ExcelFormatData))] public void SheetNameTest_ToExcelFile(ExcelFormat excelFormat) { IReadOnlyList list = Enumerable.Range(0, 10).Select(i => new Notice() { Id = i + 1, Content = $"content_{i}", Title = $"title_{i}", PublishedAt = DateTime.UtcNow.AddDays(-i), Publisher = $"publisher_{i}" }).ToArray(); var settings = FluentSettings.For(); lock (settings) { settings.HasSheetSetting(s => { s.SheetName = "Test"; }); var filePath = $"{Path.GetTempFileName()}.{excelFormat.ToString().ToLower()}"; list.ToExcelFile(filePath); var excel = ExcelHelper.LoadExcel(filePath); Assert.Equal("Test", excel.GetSheetAt(0).SheetName); settings.HasSheetSetting(s => { s.SheetName = "NoticeList"; }); } } [Theory] [ClassData(typeof(ExcelFormatData))] public void SheetNameTest_ToExcelBytes(ExcelFormat excelFormat) { IReadOnlyList list = Enumerable.Range(0, 10).Select(i => new Notice() { Id = i + 1, Content = $"content_{i}", Title = $"title_{i}", PublishedAt = DateTime.UtcNow.AddDays(-i), Publisher = $"publisher_{i}" }).ToArray(); var settings = FluentSettings.For(); lock (settings) { settings.HasSheetSetting(s => { s.SheetName = "Test"; }); var excelBytes = list.ToExcelBytes(excelFormat); var excel = ExcelHelper.LoadExcel(excelBytes, excelFormat); Assert.Equal("Test", excel.GetSheetAt(0).SheetName); settings.HasSheetSetting(s => { s.SheetName = "NoticeList"; }); } } [Theory] [ClassData(typeof(ExcelFormatData))] public void DuplicateColumnTest(ExcelFormat excelFormat) { var workbook = ExcelHelper.PrepareWorkbook(excelFormat); var sheet = workbook.CreateSheet(); var headerRow = sheet.CreateRow(0); headerRow.CreateCell(0).SetCellValue("A"); headerRow.CreateCell(1).SetCellValue("B"); headerRow.CreateCell(2).SetCellValue("C"); headerRow.CreateCell(3).SetCellValue("A"); headerRow.CreateCell(4).SetCellValue("B"); headerRow.CreateCell(5).SetCellValue("C"); var dataRow = sheet.CreateRow(1); dataRow.CreateCell(0).SetCellValue("1"); dataRow.CreateCell(1).SetCellValue("2"); dataRow.CreateCell(2).SetCellValue("3"); dataRow.CreateCell(3).SetCellValue("4"); dataRow.CreateCell(4).SetCellValue("5"); dataRow.CreateCell(5).SetCellValue("6"); var dataTable = sheet.ToDataTable(); Assert.Equal(headerRow.Cells.Count, dataTable.Columns.Count); Assert.Equal(1, dataTable.Rows.Count); var newWorkbook = ExcelHelper.LoadExcel(dataTable.ToExcelBytes()); var newSheet = newWorkbook.GetSheetAt(0); Assert.Equal(sheet.PhysicalNumberOfRows, newSheet.PhysicalNumberOfRows); for (var i = 0; i < sheet.PhysicalNumberOfRows; i++) { Assert.Equal(sheet.GetRow(i).Cells.Count, newSheet.GetRow(i).Cells.Count); for (var j = 0; j < headerRow.Cells.Count; j++) { Assert.Equal( sheet.GetRow(i).GetCell(j).GetCellValue(), newSheet.GetRow(i).GetCell(j).GetCellValue() ); } } } [Theory] [ClassData(typeof(ExcelFormatData))] public void ValidatorTest(ExcelFormat excelFormat) { var list = new List() { new() { Id = 1, Name = "test" }, new() }; var bytes = list.ToExcelBytes(excelFormat); var result = ExcelHelper.ToEntityListWithValidationResult(bytes, excelFormat); Assert.Equal(list.Count, result.EntityList.Count); for (var i = 0; i < list.Count; i++) { Assert.True(list[i] == result.EntityList[i]); } Assert.Single(result.ValidationResults); } [Theory] [ClassData(typeof(ExcelFormatData))] public void ValidatorTest_CustomValidator(ExcelFormat excelFormat) { var list = new List() { new() { Id = 1, Name = "test" } }; var validator = new DelegateValidator(_ => new ValidationResult() { Valid = false, Errors = new Dictionary { { "", ["Mock error"] } } }); var bytes = list.ToExcelBytes(excelFormat); var result = ExcelHelper.ToEntityListWithValidationResult(bytes, excelFormat, validator: validator); Assert.Equal(list.Count, result.EntityList.Count); for (var i = 0; i < list.Count; i++) { Assert.True(list[i] == result.EntityList[i]); } Assert.Single(result.ValidationResults); } [Theory] [ClassData(typeof(ExcelFormatData))] public void CellReaderTest(ExcelFormat excelFormat) { var jobs = new CellReaderTestModel[] { new() { Id = 1, Name = "test" }, new() { Id = 2 }, }; var bytes = jobs.ToExcelBytes(excelFormat); var settings = FluentSettings.For(); settings.Property(x => x.Name) .HasCellReader(_ => "CellValue"); var list = ExcelHelper.ToEntityList(bytes, excelFormat); Assert.Equal(jobs.Length, list.Count); for (var i = 0; i < jobs.Length; i++) { Assert.NotNull(list[i]); var model = list[i]; Guard.NotNull(model); Assert.Equal(jobs[i].Id, model.Id); Assert.Equal("CellValue", model.Name); } settings.Property(x => x.Name) .HasCellReader(null); } [Theory] [ClassData(typeof(ExcelFormatData))] public void PostImportActionTest(ExcelFormat excelFormat) { var jobs = new PostImportActionTestModel[] { new() { Id = 1, Name = "test" }, new() { Id = 2 }, }; var bytes = jobs.ToExcelBytes(excelFormat); var settings = FluentSettings.For(); settings.WithPostImportAction((entity, rowIndex) => entity?.RowNumber = rowIndex + 1); var list = ExcelHelper.ToEntityList(bytes, excelFormat); Assert.Equal(jobs.Length, list.Count); for (var i = 0; i < jobs.Length; i++) { Assert.NotNull(list[i]); var model = list[i]; Guard.NotNull(model); Assert.Equal(jobs[i].Id, model.Id); Assert.Equal(i + 2, model.RowNumber); } settings.Property(x => x.Name) .HasCellReader(null); } [Theory] [ClassData(typeof(ExcelFormatData))] public void CellTypeTest(ExcelFormat excelFormat) { var workbook = ExcelHelper.PrepareWorkbook(excelFormat); var sheet = workbook.CreateSheet(); var headerRow = sheet.CreateRow(0); headerRow.CreateCell(0).SetCellValue("Id"); headerRow.CreateCell(1).SetCellValue("1234"); var dataRow = sheet.CreateRow(1); dataRow.CreateCell(0).SetCellValue(1); dataRow.CreateCell(1).SetCellValue(0.24); var cell = dataRow.GetCell(1); cell.CellStyle.DataFormat = HSSFDataFormat.GetBuiltinFormat("0%"); Assert.Equal(CellType.Numeric, cell.CellType); Assert.Equal("24%", new DataFormatter().FormatCellValue(cell)); var excelBytes = workbook.ToExcelBytes(); var importedWorkbook = ExcelHelper.LoadExcel(excelBytes, excelFormat); var importedSheet = importedWorkbook.GetSheetAt(0); var row1 = importedSheet.GetRow(1); var cell1 = row1.GetCell(1); Assert.Equal(cell.CellStyle.DataFormat, cell1.CellStyle.DataFormat); Assert.Equal(1, row1.GetCell(0).NumericCellValue.To()); Assert.Equal("24%", new DataFormatter().FormatCellValue(cell1)); } [Theory] [ClassData(typeof(ExcelFormatData))] public void HeaderCellTypeTest(ExcelFormat excelFormat) { var workbook = ExcelHelper.PrepareWorkbook(excelFormat); var sheet = workbook.CreateSheet(); var headerRow = sheet.CreateRow(0); headerRow.CreateCell(0).SetCellValue("Id"); var cell = headerRow.CreateCell(1); cell.SetCellValue(1234); Assert.Equal(CellType.Numeric, cell.CellType); var dataRow = sheet.CreateRow(1); dataRow.CreateCell(0).SetCellValue(1); dataRow.CreateCell(1).SetCellValue("1234"); var excelBytes = workbook.ToExcelBytes(); var list = ExcelHelper.ToEntityList(excelBytes, excelFormat); Assert.Single(list); Assert.NotNull(list[0]); var entity = Guard.NotNull(list[0]); Assert.Equal(1, entity.Id); Assert.Equal("1234", entity.Name); } [Theory] [ClassData(typeof(ExcelFormatData))] public void ChineseDateFormatterTest(ExcelFormat excelFormat) { FluentSettings.For() .Property(x => x.Date) .HasColumnInputFormatter(ChineseDateFormatter.FormatInput) .HasColumnOutputFormatter(ChineseDateFormatter.FormatOutput); var model = new[] { new ChineseDateFormatter.ChineDateTestModel() { Date = DateTime.Parse("2022-01-01") } }; var excelBytes = model.ToExcelBytes(excelFormat); var list = ExcelHelper.ToEntityList(excelBytes, excelFormat); Assert.Single(list); var item = list[0]; Assert.NotNull(item); Guard.NotNull(item); Assert.Equal(DateTime.Parse("2022-01-01"), item.Date); } [Fact] public void PropertyOrderTest() { var excelConfiguration = InternalHelper.GetExcelConfigurationMapping(); var propertyColumnDictionary = InternalHelper.GetPropertyColumnDictionary(excelConfiguration); Assert.NotNull(propertyColumnDictionary); if (excelConfiguration.Property(x => x.Id) is PropertyConfiguration idPropertyConfiguration) { Assert.NotNull(idPropertyConfiguration.ColumnTitle); Assert.Equal(0, idPropertyConfiguration.ColumnIndex); } else { Assert.Fail("Invalid property Id"); } if (excelConfiguration.Property(x => x.Title) is PropertyConfiguration titlePropertyConfiguration) { Assert.NotNull(titlePropertyConfiguration.ColumnTitle); Assert.Equal(1, titlePropertyConfiguration.ColumnIndex); } else { Assert.Fail("Invalid property Title"); } if (excelConfiguration.Property(x => x.Description) is PropertyConfiguration descriptionPropertyConfiguration) { Assert.NotNull(descriptionPropertyConfiguration.ColumnTitle); Assert.Equal(2, descriptionPropertyConfiguration.ColumnIndex); } else { Assert.Fail("Invalid property Description"); } } [Fact] public void PropertyOrderTest2() { var excelConfiguration = InternalHelper.GetExcelConfigurationMapping(); var propertyColumnDictionary = InternalHelper.GetPropertyColumnDictionary(excelConfiguration); Assert.NotNull(propertyColumnDictionary); if (excelConfiguration.Property(x => x.Id) is PropertyConfiguration idPropertyConfiguration) { Assert.NotNull(idPropertyConfiguration.ColumnTitle); Assert.Equal(1, idPropertyConfiguration.ColumnIndex); } else { Assert.Fail("Invalid property Id"); } if (excelConfiguration.Property(x => x.Title) is PropertyConfiguration titlePropertyConfiguration) { Assert.NotNull(titlePropertyConfiguration.ColumnTitle); Assert.Equal(0, titlePropertyConfiguration.ColumnIndex); } else { Assert.Fail("Invalid property Title"); } if (excelConfiguration.Property(x => x.Description) is PropertyConfiguration descriptionPropertyConfiguration) { Assert.NotNull(descriptionPropertyConfiguration.ColumnTitle); Assert.Equal(2, descriptionPropertyConfiguration.ColumnIndex); } else { Assert.Fail("Invalid property Description"); } } [Fact] public void PropertyOrderTest3() { var excelConfiguration = InternalHelper.GetExcelConfigurationMapping(); var propertyColumnDictionary = InternalHelper.GetPropertyColumnDictionary(excelConfiguration); Assert.NotNull(propertyColumnDictionary); if (excelConfiguration.Property(x => x.Id) is PropertyConfiguration idPropertyConfiguration) { Assert.NotNull(idPropertyConfiguration.ColumnTitle); Assert.Equal(0, idPropertyConfiguration.ColumnIndex); } else { Assert.Fail("Invalid property Id"); } if (excelConfiguration.Property(x => x.Title) is PropertyConfiguration titlePropertyConfiguration) { Assert.NotNull(titlePropertyConfiguration.ColumnTitle); Assert.Equal(1, titlePropertyConfiguration.ColumnIndex); } else { Assert.Fail("Invalid property Title"); } if (excelConfiguration.Property(x => x.Description) is PropertyConfiguration descriptionPropertyConfiguration) { Assert.NotNull(descriptionPropertyConfiguration.ColumnTitle); Assert.Equal(2, descriptionPropertyConfiguration.ColumnIndex); } else { Assert.Fail("Invalid property Description"); } } [Fact] public void PropertyOrderTest4_CustomOrdering() { var excelConfiguration = InternalHelper.GetExcelConfigurationMapping(); excelConfiguration.WithPropertyComparer(new PropertyNameBasedPropertyComparer()); var propertyColumnDictionary = InternalHelper.GetPropertyColumnDictionary(excelConfiguration); Assert.NotNull(propertyColumnDictionary); if (excelConfiguration.Property(x => x.Id) is PropertyConfiguration idPropertyConfiguration) { Assert.NotNull(idPropertyConfiguration.ColumnTitle); Assert.Equal(1, idPropertyConfiguration.ColumnIndex); } else { Assert.Fail("Invalid property Id"); } if (excelConfiguration.Property(x => x.Title) is PropertyConfiguration titlePropertyConfiguration) { Assert.NotNull(titlePropertyConfiguration.ColumnTitle); Assert.Equal(2, titlePropertyConfiguration.ColumnIndex); } else { Assert.Fail("Invalid property Title"); } if (excelConfiguration.Property(x => x.Description) is PropertyConfiguration descriptionPropertyConfiguration) { Assert.NotNull(descriptionPropertyConfiguration.ColumnTitle); Assert.Equal(0, descriptionPropertyConfiguration.ColumnIndex); } else { Assert.Fail("Invalid property Description"); } } private sealed class CellFormatTestModel { public int Id { get; set; } [Column("1234")] public string? Name { get; set; } } private sealed record CellReaderTestModel { public int Id { get; set; } public string? Name { get; set; } } private sealed record PostImportActionTestModel { public int Id { get; set; } public string? Name { get; set; } [Column(IsIgnored = true)] public int RowNumber { get; set; } } private sealed class ImageTest { public int Id { get; set; } public byte[] Image { get; set; } = null!; } private sealed class ImageTestPicData { public int Id { get; set; } public IPictureData Image { get; set; } = null!; } } file sealed class PropertyNameBasedPropertyComparer : IComparer { public int Compare(PropertyInfo? x, PropertyInfo? y) { return (x, y) switch { (null, null) => 0, (null, _) => -1, (_, null) => 1, _ => string.CompareOrdinal(x.Name, y.Name) }; } } file sealed class ChineseDateFormatter { public sealed class ChineDateTestModel { public DateTime Date { get; set; } } public static DateTime FormatInput(string? input) { if (DateTimeUtils.TransStrToDateTime(input, out var dt)) { return dt; } throw new ArgumentException("Invalid date input"); } public static string FormatOutput(DateTime input) { return "二〇二二年一月一日"; } } // http://luoma.pro/Content/Detail/671?parentId=1 file static class DateTimeUtils { /// /// 字符串日期转 DateTime /// /// 字符串日期 /// 转换成功赋值 /// 转换成功返回 true public static bool TransStrToDateTime(string? str, out DateTime dt) { dt = default; if (str.IsNullOrEmpty()) return false; //第一次转换 if (DateTime.TryParse(str, out dt)) { return true; } //第二次转换 string[] format = new string[] { "yyyyMMdd", "yyyyMdHHmmss", "yyyyMMddHHmmss", "yyyy-M-d", "yyyy-MM-dd", "yyyy-MM-dd HH:mm:ss", "yyyy/M/d", "yyyy/MM/dd", "yyyy/MM/dd HH:mm:ss", "yyyy.M.d", "yyyy.MM.dd", "yyyy.MM.dd HH:mm:ss", "yyyy年M月d日", "yyyy年MM月dd日", "yyyy年MM月dd日HH:mm:ss", "yyyy年MM月dd日 HH时mm分ss秒" }; if (DateTime.TryParseExact(str, format, CultureInfo.InvariantCulture, DateTimeStyles.None, out dt)) { return true; } //第三次转换 try { if (Regex.IsMatch(str, "^(零|〇|一|二|三|四|五|六|七|八|九|十){2,4}年((正|一|二|三|四|五|六|七|八|九|十|十一|十二)月((一|二|三|四|五|六|七|八|九|十){1,3}(日)?)?)?$")) { var match = Regex.Match(str, @"^(.+)年(.+)月(.+)日$"); if (match.Success) { int year = GetYear(match.Groups[1].Value); int month = GetMonth(match.Groups[2].Value); long dayL = ParseCnToInt(match.Groups[3].Value); dt = new DateTime(year, month, int.Parse(dayL.ToString())); return true; } } } catch { return false; } return false; } /// /// 使用正则表达式判断是否为日期 /// /// 日期格式字符串 /// 是日期格式字符串返回 true public static bool IsDateTime(string str) { bool isDateTime; // yyyy/MM/dd - 年月日数字 if (Regex.IsMatch(str, "^(?\\d{2,4})/(?\\d{1,2})/(?\\d{1,2})$")) isDateTime = true; // yyyy-MM-dd - 年月日数字 else if (Regex.IsMatch(str, "^(?\\d{2,4})-(?\\d{1,2})-(?\\d{1,2})$")) isDateTime = true; // yyyy.MM.dd - 年月日数字 else if (Regex.IsMatch(str, "^(?\\d{2,4})[.](?\\d{1,2})[.](?\\d{1,2})$")) isDateTime = true; // yyyy年MM月dd日 - 年月日数字 else if (Regex.IsMatch(str, "^((?\\d{2,4})年)?(?\\d{1,2})月((?\\d{1,2})日)?$")) isDateTime = true; // yyyy年MM月dd日 - 年月日中文 else if (Regex.IsMatch(str, "^(零|〇|一|二|三|四|五|六|七|八|九|十){2,4}年((正|一|二|三|四|五|六|七|八|九|十|十一|十二)月((一|二|三|四|五|六|七|八|九|十){1,3}(日)?)?)?$")) isDateTime = true; // yyyy年MM月dd日 - 年(数字),月(中文),日(中文) //else if (Regex.IsMatch(str, "^((?\\d{2,4})年)?(正|一|二|三|四|五|六|七|八|九|十|十一|十二)月((一|二|三|四|五|六|七|八|九|十){1,3}日)?$")) // isDateTime = true; // yyyy年 //else if (Regex.IsMatch(str, "^(?\\d{2,4})年$")) // isDateTime = true; // 农历1 //else if (Regex.IsMatch(str, "^(甲|乙|丙|丁|戊|己|庚|辛|壬|癸)(子|丑|寅|卯|辰|巳|午|未|申|酉|戌|亥)年((正|一|二|三|四|五|六|七|八|九|十|十一|十二)月((一|二|三|四|五|六|七|八|九|十){1,3}(日)?)?)?$")) // isDateTime = true; //// 农历2 //else if (Regex.IsMatch(str, "^((甲|乙|丙|丁|戊|己|庚|辛|壬|癸)(子|丑|寅|卯|辰|巳|午|未|申|酉|戌|亥)年)?(正|一|二|三|四|五|六|七|八|九|十|十一|十二)月初(一|二|三|四|五|六|七|八|九|十)$")) // isDateTime = true; //// XX时XX分XX秒 //else if (Regex.IsMatch(str, "^(?\\d{1,2})(时|点)(?\\d{1,2})分((?\\d{1,2})秒)?$")) // isDateTime = true; //// XX时XX分XX秒 //else if (Regex.IsMatch(str, "^((零|一|二|三|四|五|六|七|八|九|十){1,3})(时|点)((零|一|二|三|四|五|六|七|八|九|十){1,3})分(((零|一|二|三|四|五|六|七|八|九|十){1,3})秒)?$")) // isDateTime = true; //// XX分XX秒 //else if (Regex.IsMatch(str, "^(?\\d{1,2})分(?\\d{1,2})秒$")) // isDateTime = true; //// XX分XX秒 //else if (Regex.IsMatch(str, "^((零|一|二|三|四|五|六|七|八|九|十){1,3})分((零|一|二|三|四|五|六|七|八|九|十){1,3})秒$")) // isDateTime = true; //// XX时 //else if (Regex.IsMatch(str, "\\b(?\\d{1,2})(时|点钟)\\b")) // isDateTime = true; else isDateTime = false; return isDateTime; } #region 年月获取 /// /// 获取年份 /// /// 年份 /// 数字年份 public static int GetYear(string str) { var strNumber = ""; foreach (var item in str) { switch (item.ToString()) { case "零": case "〇": strNumber += "0"; break; case "一": strNumber += "1"; break; case "二": strNumber += "2"; break; case "三": strNumber += "3"; break; case "四": strNumber += "4"; break; case "五": strNumber += "5"; break; case "六": strNumber += "6"; break; case "七": strNumber += "7"; break; case "八": strNumber += "8"; break; case "九": strNumber += "9"; break; case "十": strNumber += "10"; break; } } int.TryParse(strNumber, out var number); return number; } /// ///获取月份 /// /// 月份 /// 数字月份 public static int GetMonth(string str) { var strNumber = ""; switch (str) { case "一": case "正": strNumber += "1"; break; case "二": strNumber += "2"; break; case "三": strNumber += "3"; break; case "四": strNumber += "4"; break; case "五": strNumber += "5"; break; case "六": strNumber += "6"; break; case "七": strNumber += "7"; break; case "八": strNumber += "8"; break; case "九": strNumber += "9"; break; case "十": strNumber += "10"; break; case "十一": strNumber += "11"; break; case "十二": strNumber += "12"; break; } int.TryParse(strNumber, out var number); return number; } #endregion #region 中文数字和阿拉伯数字转换 /// /// 阿拉伯数字转换成中文数字 /// /// /// public static string NumToChinese(string x) { string[] pArrayNum = { "零", "一", "二", "三", "四", "五", "六", "七", "八", "九" }; //为数字位数建立一个位数组 string[] pArrayDigit = { "", "十", "百", "千" }; //为数字单位建立一个单位数组 string[] pArrayUnits = { "", "万", "亿", "万亿" }; var pStrReturnValue = ""; //返回值 var finger = 0; //字符位置指针 var pIntM = x.Length % 4; //取模 int pIntK; if (pIntM > 0) pIntK = x.Length / 4 + 1; else pIntK = x.Length / 4; //外层循环,四位一组,每组最后加上单位: ",万亿,",",亿,",",万," for (var i = pIntK; i > 0; i--) { var pIntL = 4; if (i == pIntK && pIntM != 0) pIntL = pIntM; //得到一组四位数 var four = x.Substring(finger, pIntL); var pIntL1 = four.Length; //内层循环在该组中的每一位数上循环 for (var j = 0; j < pIntL1; j++) { //处理组中的每一位数加上所在的位 var n = Convert.ToInt32(four.Substring(j, 1)); if (n == 0) { if (j < pIntL1 - 1 && Convert.ToInt32(four.Substring(j + 1, 1)) > 0 && !pStrReturnValue.EndsWith(pArrayNum[n])) pStrReturnValue += pArrayNum[n]; } else { if (!(n == 1 && (pStrReturnValue.EndsWith(pArrayNum[0]) | pStrReturnValue.Length == 0) && j == pIntL1 - 2)) pStrReturnValue += pArrayNum[n]; pStrReturnValue += pArrayDigit[pIntL1 - j - 1]; } } finger += pIntL; //每组最后加上一个单位:",万,",",亿," 等 if (i < pIntK) //如果不是最高位的一组 { if (Convert.ToInt32(four) != 0) //如果所有4位不全是0则加上单位",万,",",亿,"等 pStrReturnValue += pArrayUnits[i - 1]; } else { //处理最高位的一组,最后必须加上单位 pStrReturnValue += pArrayUnits[i - 1]; } } return pStrReturnValue; } /// /// 转换数字 /// public static long CharToNumber(char c) { switch (c) { case '一': return 1; case '二': return 2; case '三': return 3; case '四': return 4; case '五': return 5; case '六': return 6; case '七': return 7; case '八': return 8; case '九': return 9; case '零': return 0; default: return -1; } } /// /// 转换单位 /// public static long CharToUnit(char c) { switch (c) { case '十': return 10; case '百': return 100; case '千': return 1000; case '万': return 10000; case '亿': return 100000000; default: return 1; } } /// /// 将中文数字转换阿拉伯数字 /// /// 汉字数字 /// 长整型阿拉伯数字 public static long ParseCnToInt(string cnum) { cnum = Regex.Replace(cnum, "\\s+", ""); long firstUnit = 1;//一级单位 long secondUnit = 1;//二级单位 long result = 0;//结果 for (var i = cnum.Length - 1; i > -1; --i)//从低到高位依次处理 { var tmpUnit = CharToUnit(cnum[i]);//临时单位变量 if (tmpUnit > firstUnit)//判断此位是数字还是单位 { firstUnit = tmpUnit;//是的话就赋值,以备下次循环使用 secondUnit = 1; if (i == 0)//处理如果是"十","十一"这样的开头的 { result += firstUnit * secondUnit; } continue;//结束本次循环 } if (tmpUnit > secondUnit) { secondUnit = tmpUnit; continue; } result += firstUnit * secondUnit * CharToNumber(cnum[i]);//如果是数字,则和单位想乘然后存到结果里 } return result; } #endregion } ================================================ FILE: test/WeihanLi.Npoi.Test/Extensions.cs ================================================ // Copyright (c) Weihan Li. All rights reserved. // Licensed under the Apache license. using System.Data; using Xunit; namespace WeihanLi.Npoi.Test; public static class Extensions { public static DataRow AddNewRow(this DataTable datatable, object[]? rowData = null) { var row = datatable.NewRow(); if (rowData is not null) { row.ItemArray = rowData; } datatable.Rows.Add(row); return row; } public static void AssertEquals(this DataTable actual, DataTable expected) { // Check columns for (var headerIndex = 0; headerIndex < expected.Columns.Count; headerIndex++) { var expectedValue = expected.Columns[headerIndex].ToString(); var excelValue = actual.Columns[headerIndex].ToString(); // "TRUE" from header column is translated to "True". // I don't know how to load display value of boolean, therefore I ignore letter casing. Assert.Equal(expectedValue, excelValue, ignoreCase: true); } // Check rows for (var rowIndex = 0; rowIndex < expected.Rows.Count; rowIndex++) { for (var colIndex = 0; colIndex < expected.Rows[rowIndex].ItemArray.Length; colIndex++) { var expectedValue = expected.Rows[rowIndex].ItemArray[colIndex]?.ToString(); var excelValue = actual.Rows[rowIndex][colIndex].ToString(); Assert.Equal(expectedValue, excelValue); } } } } ================================================ FILE: test/WeihanLi.Npoi.Test/MappingProfiles/NoticeProfile.cs ================================================ // Copyright (c) Weihan Li. All rights reserved. // Licensed under the Apache license. using WeihanLi.Extensions; using WeihanLi.Npoi.Configurations; using WeihanLi.Npoi.Test.Models; namespace WeihanLi.Npoi.Test.MappingProfiles; public sealed class NoticeProfile : IMappingProfile { public void Configure(IExcelConfiguration noticeSetting) { noticeSetting .HasAuthor("WeihanLi") .HasTitle("WeihanLi.Npoi test") .HasSheetSetting(setting => { setting.SheetName = "NoticeList"; setting.AutoColumnWidthEnabled = true; }) ; noticeSetting.Property(_ => _.Id) .HasColumnIndex(0); noticeSetting.Property(_ => _.Title) .HasColumnIndex(1); noticeSetting.Property(_ => _.Content) .HasColumnIndex(2); noticeSetting.Property(_ => _.Publisher) .HasColumnIndex(3); noticeSetting.Property(_ => _.PublishedAt) .HasColumnIndex(4) .HasColumnOutputFormatter(x => x.ToTimeString()); } } ================================================ FILE: test/WeihanLi.Npoi.Test/Models/Job.cs ================================================ // Copyright (c) Weihan Li. All rights reserved. // Licensed under the Apache license. using System.ComponentModel.DataAnnotations; namespace WeihanLi.Npoi.Test.Models; public sealed record Job { public int Id { get; set; } [Required] public string Name { get; set; } = default!; } ================================================ FILE: test/WeihanLi.Npoi.Test/Models/Notice.cs ================================================ // Copyright (c) Weihan Li. All rights reserved. // Licensed under the Apache license. namespace WeihanLi.Npoi.Test.Models; public class BaseModel { public int Id { get; set; } } public class Notice : BaseModel { public string? Title { get; set; } public string? Content { get; set; } public DateTime PublishedAt { get; set; } public string? Publisher { get; set; } } ================================================ FILE: test/WeihanLi.Npoi.Test/Models/OrderTestModels.cs ================================================ // Copyright (c) Weihan Li. All rights reserved. // Licensed under the Apache license. using WeihanLi.Npoi.Attributes; namespace WeihanLi.Npoi.Test.Models; public class OrderTestModel1 { public int Id { get; set; } public string? Title { get; set; } public string? Description { get; set; } } public class OrderTestModel2 { [Column(Index = 1)] public int Id { get; set; } [Column(Index = 0)] public string? Title { get; set; } public string? Description { get; set; } } public class OrderTestModel3 { public string? Description { get; set; } [Column(Index = 0)] public int Id { get; set; } [Column(Index = 1)] public string? Title { get; set; } } public class OrderTestModel4 { public int Id { get; set; } public string? Title { get; set; } public string? Description { get; set; } } ================================================ FILE: test/WeihanLi.Npoi.Test/Startup.cs ================================================ // Copyright (c) Weihan Li. All rights reserved. // Licensed under the Apache license. using WeihanLi.Npoi.Test.MappingProfiles; namespace WeihanLi.Npoi.Test; public class Startup { public void Configure() { AppContext.SetSwitch("System.Drawing.EnableUnixSupport", true); // ---------- load excel mapping profiles ---------------- FluentSettings.LoadMappingProfiles(typeof(NoticeProfile).Assembly); } } ================================================ FILE: test/WeihanLi.Npoi.Test/TestData/EmptyColumns/emptyColumns.csv ================================================ A,B,C,D ,,3,4 ,2,3, 1,2,, 1,2,3,4 ================================================ FILE: test/WeihanLi.Npoi.Test/TestData/NonStringColumns/nonStringColumns.csv ================================================ A,1000,TRUE,15/08/2021 1,2,3,4 ================================================ FILE: test/WeihanLi.Npoi.Test/WeihanLi.Npoi.Test.csproj ================================================ true exe true true all runtime; build; native; contentfiles; analyzers; buildtransitive PreserveNewest