[
  {
    "path": ".codacy.yml",
    "content": "exclude_paths:\n  - samples/\n  - test/"
  },
  {
    "path": ".devcontainer/Dockerfile",
    "content": "FROM mcr.microsoft.com/dotnet/sdk:10.0\nRUN apt-get update \\ \n    && apt-get install -y libgdiplus libc6-dev \\ \n    && ln -s /usr/lib/libgdiplus.so /usr/lib/gdiplus.dll\n"
  },
  {
    "path": ".devcontainer/devcontainer.json",
    "content": "{\n    \"name\": \"CodeSpace\",\n    \"dockerFile\": \"Dockerfile\",\n    \"customizations\": {\n        \"vscode\": {\n            \"extensions\": [\n                \"ms-dotnettools.csharp\",\n                \"davidanson.vscode-markdownlint\"\n            ]\n        }\n    }\n}"
  },
  {
    "path": ".editorconfig",
    "content": "# EditorConfig is awesome:http://EditorConfig.org\n\n# top-most EditorConfig file\nroot = true\n\n# Don't use tabs for indentation.\n[*]\nindent_style = space\n# (Please don't specify an indent_size here; that has too many unintended consequences.)\n\n# Code files\n[*.{cs,csx,vb,vbx}]\nindent_size = 4\ninsert_final_newline = true\ncharset = utf-8-bom\n\n# Xml project files\n[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}]\nindent_size = 2\n\n# Xml config files\n[*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}]\nindent_size = 2\n\n# JSON files\n[*.json]\nindent_size = 2\n\n# Dotnet code style settings:\n[*.{cs,vb}]\n# File header\nfile_header_template = Copyright (c) Weihan Li. All rights reserved.\\nLicensed under the Apache license.\n\n# Sort using and Import directives with System.* appearing first\ndotnet_sort_system_directives_first = false\n# Avoid \"this.\" and \"Me.\" if not necessary\ndotnet_style_qualification_for_field = false:suggestion\ndotnet_style_qualification_for_property = false:suggestion\ndotnet_style_qualification_for_method = false:suggestion\ndotnet_style_qualification_for_event = false:suggestion\n\n# Use language keywords instead of framework type names for type references\ndotnet_style_predefined_type_for_locals_parameters_members = true:suggestion\ndotnet_style_predefined_type_for_member_access = true:suggestion\n\n# Suggest more modern language features when available\ndotnet_style_object_initializer = true:suggestion\ndotnet_style_collection_initializer = true:suggestion\ndotnet_style_coalesce_expression = true:suggestion\ndotnet_style_null_propagation = true:suggestion\ndotnet_style_explicit_tuple_names = true:suggestion\n\n# CSharp code style settings:\n[*.cs]\n# namespace style\ncsharp_style_namespace_declarations=file_scoped:warning\n\n# Prefer \"var\" everywhere\ncsharp_style_var_for_built_in_types = true:suggestion\ncsharp_style_var_when_type_is_apparent = true:suggestion\ncsharp_style_var_elsewhere = true:suggestion\n\n# Prefer method-like constructs to have a block body\ncsharp_style_expression_bodied_methods = false:none\ncsharp_style_expression_bodied_constructors = false:none\ncsharp_style_expression_bodied_operators = false:none\n\n# Prefer property-like constructs to have an expression-body\ncsharp_style_expression_bodied_properties = true:none\ncsharp_style_expression_bodied_indexers = true:none\ncsharp_style_expression_bodied_accessors = true:none\n\n# Suggest more modern language features when available\ncsharp_style_pattern_matching_over_is_with_cast_check = true:suggestion\ncsharp_style_pattern_matching_over_as_with_null_check = true:suggestion\ncsharp_style_inlined_variable_declaration = true:suggestion\ncsharp_style_throw_expression = true:suggestion\ncsharp_style_conditional_delegate_call = true:suggestion\n\n# Newline settings\ncsharp_new_line_before_open_brace = all\ncsharp_new_line_before_else = true\ncsharp_new_line_before_catch = true\ncsharp_new_line_before_finally = true\ncsharp_new_line_before_members_in_object_initializers = true\ncsharp_new_line_before_members_in_anonymous_types = true\n\n# Fix formatting, https://docs.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/formatting-rules#rule-id-ide0055-fix-formatting\ndotnet_diagnostic.IDE00055.severity = warning\n# Remove unnecessary usings, https://docs.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0005\ndotnet_diagnostic.IDE0005.severity = warning\n# File header template\ndotnet_diagnostic.IDE0073.severity = warning\n"
  },
  {
    "path": ".gitattributes",
    "content": "###############################################################################\n# Set default behavior to automatically normalize line endings.\n###############################################################################\n* text=auto\n\n*.sh text eol=lf\n*.ps1 text eol=crlf\n\n###############################################################################\n# Set default behavior for command prompt diff.\n#\n# This is need for earlier builds of msysgit that does not have it on by\n# default for csharp files.\n# Note: This is only used by command line\n###############################################################################\n#*.cs     diff=csharp\n\n###############################################################################\n# Set the merge driver for project and solution files\n#\n# Merging from the command prompt will add diff markers to the files if there\n# are conflicts (Merging from VS is not affected by the settings below, in VS\n# the diff markers are never inserted). Diff markers may cause the following \n# file extensions to fail to load in VS. An alternative would be to treat\n# these files as binary and thus will always conflict and require user\n# intervention with every merge. To do so, just uncomment the entries below\n###############################################################################\n#*.sln       merge=binary\n#*.csproj    merge=binary\n#*.vbproj    merge=binary\n#*.vcxproj   merge=binary\n#*.vcproj    merge=binary\n#*.dbproj    merge=binary\n#*.fsproj    merge=binary\n#*.lsproj    merge=binary\n#*.wixproj   merge=binary\n#*.modelproj merge=binary\n#*.sqlproj   merge=binary\n#*.wwaproj   merge=binary\n\n###############################################################################\n# behavior for image files\n#\n# image files are treated as binary by default.\n###############################################################################\n#*.jpg   binary\n#*.png   binary\n#*.gif   binary\n\n###############################################################################\n# diff behavior for common document formats\n# \n# Convert binary document formats to text before diffing them. This feature\n# is only available from the command line. Turn it on by uncommenting the \n# entries below.\n###############################################################################\n#*.doc   diff=astextplain\n#*.DOC   diff=astextplain\n#*.docx  diff=astextplain\n#*.DOCX  diff=astextplain\n#*.dot   diff=astextplain\n#*.DOT   diff=astextplain\n#*.pdf   diff=astextplain\n#*.PDF   diff=astextplain\n#*.rtf   diff=astextplain\n#*.RTF   diff=astextplain\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**Describe the bug**\n\nA clear and concise description of what the bug is.\n\n**To Reproduce**\n\nSteps to reproduce the behavior:\n1. ...\n2. ....\n3. ....\n\n**Expected behavior**\n\nA clear and concise description of what you expected to happen.\n\n**Screenshots**\n\nIf applicable, add screenshots to help explain your problem.\n\n**Runtime Version**\n\n- dotnet version: \n- `WeihanLi.Npoi` version:\n\n**Additional context**\n\nAdd any other context about the problem here.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "content": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**Is your feature request related to a problem? Please describe.**\nA clear and concise description of what the problem is. Ex. I'm always frustrated when [...]\n\n**Describe the solution you'd like**\nA clear and concise description of what you want to happen.\n\n**Describe alternatives you've considered**\nA clear and concise description of any alternative solutions or features you've considered.\n\n**Additional context**\nAdd any other context or screenshots about the feature request here.\n"
  },
  {
    "path": ".github/stale.yml",
    "content": "# Number of days of inactivity before an issue becomes stale\ndaysUntilStale: 30\n# Number of days of inactivity before a stale issue is closed\ndaysUntilClose: 7\n# Issues with these labels will never be considered stale\nexemptLabels:\n  - pinned\n  - security\n# Label to use when marking an issue as stale\nstaleLabel: inactive\n# Comment to post when marking an issue as stale. Set to `false` to disable\nmarkComment: >\n  This issue has been automatically marked as stale because it has not had\n  recent activity. It will be closed if no further activity occurs. Thank you\n  for your contributions.\n# Comment to post when closing a stale issue. Set to `false` to disable\ncloseComment: false\n"
  },
  {
    "path": ".github/workflows/docfx.yml",
    "content": "name: docfx\non:\n  push:\n    branches:\n      - \"main\"\n      - \"master\"\n      - \"dev\"\n\n# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages\npermissions:\n  actions: read\n  pages: write\n  id-token: write\n\n# Allow only one concurrent deployment, skipping runs queued between the run in progress and the latest queued.\n# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.\nconcurrency:\n  group: \"pages\"\n  cancel-in-progress: false\n\njobs:\n  build:\n    name: \"publish docs\"\n    environment:\n      name: github-pages\n      url: ${{ steps.deployment.outputs.page_url }}\n    runs-on: ubuntu-latest\n    steps:\n      # Check out the branch that triggered this workflow to the 'source' subdirectory\n      - name: Checkout Code\n        uses: actions/checkout@v6\n      - name: Setup .NET SDK\n        uses: WeihanLi/dotnet-install@v0.2.0\n        with:\n          version: |\n            10.0.x\n      - name: install dotnet tools\n        run: |\n          dotnet tool install -g dotnet-execute\n          dotnet tool install -g docfx\n      # Run a build\n      - name: Build docs\n        run: |\n          dotnet-exec info\n          dotnet build\n          cp ./README.md ./docs/index.md\n          docfx ./docs/docfx.json\n      - name: Upload artifact\n        uses: actions/upload-pages-artifact@v3\n        with:\n          path: 'docs/_site'\n      - name: Deploy to GitHub Pages\n        id: deployment\n        uses: actions/deploy-pages@v4\n      - name: cloudflare-pages\n        uses: cloudflare/wrangler-action@v3\n        with:\n          accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}\n          apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}\n          command: pages deploy docs/_site --project-name=weihanli-npoi\n"
  },
  {
    "path": ".github/workflows/dotnet-format-pr-validation.yml",
    "content": "name: dotnet-format-validation\n\non: [pull_request]\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n    - uses: actions/checkout@v6\n    - name: Setup .NET SDK\n      uses: WeihanLi/dotnet-install@v0.2.0\n      with:\n        version: |\n            10.0.x\n    - name: build\n      run: dotnet build\n    - name: check format\n      run: dotnet format --verify-no-changes\n"
  },
  {
    "path": ".github/workflows/dotnet-format.yml",
    "content": "name: dotnet-format\n\non:\n  push:\n    branches: [ dev ]\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n    - uses: actions/checkout@v6\n    - name: Setup .NET SDK\n      uses: WeihanLi/dotnet-install@v0.2.0\n      with:\n        version: |\n          10.0.x\n    - name: build\n      run: dotnet build\n    - name: format\n      run: dotnet format\n    - name: check for changes\n      run: |\n        if git diff --exit-code; then\n          echo \"has_changes=false\" >> $GITHUB_ENV\n        else\n          echo \"has_changes=true\" >> $GITHUB_ENV\n        fi\n    - name: Commit and Push\n      if: ${{ env.has_changes == 'true' }}\n      shell: bash\n      run: |\n        git config --local user.name \"github-actions[bot]\"\n        git config --local user.email \"weihanli@outlook.com\"\n        git add -u\n        git commit -m \"Automated dotnet-format update from commit ${GITHUB_SHA} on ${GITHUB_REF}\"\n        git log -1\n        remote_repo=\"https://${GITHUB_ACTOR}:${{secrets.GITHUB_TOKEN}}@github.com/${GITHUB_REPOSITORY}.git\"\n        git push \"${remote_repo}\" HEAD:${GITHUB_REF}\n"
  },
  {
    "path": ".github/workflows/dotnet.yml",
    "content": "name: default\n\non: [push, pull_request]\n\njobs:\n  mac-build:\n    runs-on: macos-latest\n    steps:\n    - uses: actions/checkout@v6\n    - name: Setup .NET SDK\n      uses: WeihanLi/dotnet-install@v0.2.0\n      with:\n        version: |\n            10.0.x\n    - name: dotnet info\n      run: |\n        dotnet tool install -g dotnet-execute\n        dotnet-exec info\n    - name: build\n      run: dotnet build.cs --target=test\n\n  linux-build:\n    runs-on: ubuntu-latest\n    steps:\n    - uses: actions/checkout@v6\n    - name: Setup .NET SDK\n      uses: WeihanLi/dotnet-install@v0.2.0\n      with:\n        version: |\n            10.0.x\n    - name: dotnet info\n      run: |\n        dotnet tool install -g dotnet-execute\n        dotnet-exec info\n    # - name: font configure\n    #   run: |\n    #      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\n    - name: build\n      run: dotnet build.cs --target=build\n      \n  windows-build:\n    runs-on: windows-latest\n    steps:\n    - uses: actions/checkout@v6\n    - name: Setup .NET SDK\n      uses: WeihanLi/dotnet-install@v0.2.0\n      with:\n        version: |\n            10.0.x\n    - name: dotnet info\n      run: |\n        dotnet tool install -g dotnet-execute\n        dotnet-exec info\n    - name: build\n      run: dotnet build.cs\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: Release\non:\n  push:\n    branches: [ master ]\njobs:\n  build:\n    name: Release\n    runs-on: windows-latest\n    steps:\n    - uses: actions/checkout@v6\n    - name: Setup .NET SDK\n      uses: WeihanLi/dotnet-install@v0.2.0\n      with:\n        version: |\n            10.0.x\n    - name: Build\n      shell: pwsh\n      run: dotnet build.cs --stable=true\n    - name: Get Release Version\n      shell: pwsh\n      run: .\\build\\getReleaseVersion.ps1\n    - name: Create GitHub release\n      uses: marvinpinto/action-automatic-releases@latest\n      with:\n        repo_token: \"${{ secrets.GITHUB_TOKEN }}\"\n        automatic_release_tag: ${{ env.ReleaseVersion }}\n        title: ${{ env.ReleaseVersion }}\n        prerelease: false\n"
  },
  {
    "path": ".gitignore",
    "content": "## Ignore Visual Studio temporary files, build results, and\n## files generated by popular Visual Studio add-ons.\n##\n## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore\n\n# Cutom files\nlocalBuild/\n_site\n.vscode\n\n# User-specific files\n*.suo\n*.user\n*.userosscache\n*.sln.docstates\n\n# User-specific files (MonoDevelop/Xamarin Studio)\n*.userprefs\n\n# Build results\n[Dd]ebug/\n[Dd]ebugPublic/\n[Rr]elease/\n[Rr]eleases/\nx64/\nx86/\nbld/\n[Bb]in/\n[Oo]bj/\n[Ll]og/\n\n# Visual Studio 2015 cache/options directory\n.vs/\n# Uncomment if you have tasks that create the project's static files in wwwroot\n#wwwroot/\n\n# MSTest test Results\n[Tt]est[Rr]esult*/\n[Bb]uild[Ll]og.*\n\n# NUNIT\n*.VisualState.xml\nTestResult.xml\n\n# Build Results of an ATL Project\n[Dd]ebugPS/\n[Rr]eleasePS/\ndlldata.c\n\n# .NET Core\nproject.lock.json\nproject.fragment.lock.json\nartifacts/\n**/Properties/launchSettings.json\n\n*_i.c\n*_p.c\n*_i.h\n*.ilk\n*.meta\n*.obj\n*.pch\n*.pdb\n*.pgc\n*.pgd\n*.rsp\n*.sbr\n*.tlb\n*.tli\n*.tlh\n*.tmp\n*.tmp_proj\n*.log\n*.vspscc\n*.vssscc\n.builds\n*.pidb\n*.svclog\n*.scc\n\n# Chutzpah Test files\n_Chutzpah*\n\n# Visual C++ cache files\nipch/\n*.aps\n*.ncb\n*.opendb\n*.opensdf\n*.sdf\n*.cachefile\n*.VC.db\n*.VC.VC.opendb\n\n# Visual Studio profiler\n*.psess\n*.vsp\n*.vspx\n*.sap\n\n# TFS 2012 Local Workspace\n$tf/\n\n# Guidance Automation Toolkit\n*.gpState\n\n# ReSharper is a .NET coding add-in\n_ReSharper*/\n*.[Rr]e[Ss]harper\n*.DotSettings.user\n\n# JustCode is a .NET coding add-in\n.JustCode\n\n# TeamCity is a build add-in\n_TeamCity*\n\n# DotCover is a Code Coverage Tool\n*.dotCover\n\n# Visual Studio code coverage results\n*.coverage\n*.coveragexml\n\n# NCrunch\n_NCrunch_*\n.*crunch*.local.xml\nnCrunchTemp_*\n\n# MightyMoose\n*.mm.*\nAutoTest.Net/\n\n# Web workbench (sass)\n.sass-cache/\n\n# Installshield output folder\n[Ee]xpress/\n\n# DocProject is a documentation generator add-in\nDocProject/buildhelp/\nDocProject/Help/*.HxT\nDocProject/Help/*.HxC\nDocProject/Help/*.hhc\nDocProject/Help/*.hhk\nDocProject/Help/*.hhp\nDocProject/Help/Html2\nDocProject/Help/html\n\n# Click-Once directory\npublish/\n\n# Publish Web Output\n*.[Pp]ublish.xml\n*.azurePubxml\n# TODO: Comment the next line if you want to checkin your web deploy settings\n# but database connection strings (with potential passwords) will be unencrypted\n*.pubxml\n*.publishproj\n\n# Microsoft Azure Web App publish settings. Comment the next line if you want to\n# checkin your Azure Web App publish settings, but sensitive information contained\n# in these scripts will be unencrypted\nPublishScripts/\n\n# NuGet Packages\n*.nupkg\n# The packages folder can be ignored because of Package Restore\n**/packages/*\n# except build/, which is used as an MSBuild target.\n!**/packages/build/\n# Uncomment if necessary however generally it will be regenerated when needed\n#!**/packages/repositories.config\n# NuGet v3's project.json files produces more ignorable files\n*.nuget.props\n*.nuget.targets\n\n# Microsoft Azure Build Output\ncsx/\n*.build.csdef\n\n# Microsoft Azure Emulator\necf/\nrcf/\n\n# Windows Store app package directories and files\nAppPackages/\nBundleArtifacts/\nPackage.StoreAssociation.xml\n_pkginfo.txt\n\n# Visual Studio cache files\n# files ending in .cache can be ignored\n*.[Cc]ache\n# but keep track of directories ending in .cache\n!*.[Cc]ache/\n\n# Others\nClientBin/\n~$*\n*~\n*.dbmdl\n*.dbproj.schemaview\n*.jfm\n*.pfx\n*.publishsettings\norleans.codegen.cs\n\n# Since there are multiple workflows, uncomment next line to ignore bower_components\n# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)\n#bower_components/\n\n# RIA/Silverlight projects\nGenerated_Code/\n\n# Backup & report files from converting an old project file\n# to a newer Visual Studio version. Backup files are not needed,\n# because we have git ;-)\n_UpgradeReport_Files/\nBackup*/\nUpgradeLog*.XML\nUpgradeLog*.htm\n\n# SQL Server files\n*.mdf\n*.ldf\n*.ndf\n\n# Business Intelligence projects\n*.rdl.data\n*.bim.layout\n*.bim_*.settings\n\n# Microsoft Fakes\nFakesAssemblies/\n\n# GhostDoc plugin setting file\n*.GhostDoc.xml\n\n# Node.js Tools for Visual Studio\n.ntvs_analysis.dat\nnode_modules/\n\n# Typescript v1 declaration files\ntypings/\n\n# Visual Studio 6 build log\n*.plg\n\n# Visual Studio 6 workspace options file\n*.opt\n\n# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)\n*.vbw\n\n# Visual Studio LightSwitch build output\n**/*.HTMLClient/GeneratedArtifacts\n**/*.DesktopClient/GeneratedArtifacts\n**/*.DesktopClient/ModelManifest.xml\n**/*.Server/GeneratedArtifacts\n**/*.Server/ModelManifest.xml\n_Pvt_Extensions\n\n# Paket dependency manager\n.paket/paket.exe\npaket-files/\n\n# FAKE - F# Make\n.fake/\n\n# JetBrains Rider\n.idea/\n*.sln.iml\n\n# CodeRush\n.cr/\n\n# Python Tools for Visual Studio (PTVS)\n__pycache__/\n*.pyc\n\n# Cake - Uncomment if you are using it\ntools/**\n\n# Telerik's JustMock configuration file\n*.jmconfig\n\n# BizTalk build output\n*.btp.cs\n*.btm.cs\n*.odx.cs\n*.xsd.cs\n"
  },
  {
    "path": ".travis.yml",
    "content": "language: csharp\n\n# runtime config\nmono: none\ndotnet: 5.0.100\ndist: bionic\n\n# branch build config\nbranches:\n    only:        \n        - master\n        - preview\n        - dev\n\n    except:\n        - gh-pages\n\n# git config\ngit:\n    # depth: 1\n    lfs_skip_smudge: true # disable the download of LFS objects when cloning\n\nscript:\n    - bash build.sh --target=build\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nIn the interest of fostering an open and welcoming environment, we as\ncontributors and maintainers pledge to making participation in our project and\nour community a harassment-free experience for everyone, regardless of age, body\nsize, disability, ethnicity, sex characteristics, gender identity and expression,\nlevel of experience, education, socio-economic status, nationality, personal\nappearance, race, religion, or sexual identity and orientation.\n\n## Our Standards\n\nExamples of behavior that contributes to creating a positive environment\ninclude:\n\n* Using welcoming and inclusive language\n* Being respectful of differing viewpoints and experiences\n* Gracefully accepting constructive criticism\n* Focusing on what is best for the community\n* Showing empathy towards other community members\n\nExamples of unacceptable behavior by participants include:\n\n* The use of sexualized language or imagery and unwelcome sexual attention or\n advances\n* Trolling, insulting/derogatory comments, and personal or political attacks\n* Public or private harassment\n* Publishing others' private information, such as a physical or electronic\n address, without explicit permission\n* Other conduct which could reasonably be considered inappropriate in a\n professional setting\n\n## Our Responsibilities\n\nProject maintainers are responsible for clarifying the standards of acceptable\nbehavior and are expected to take appropriate and fair corrective action in\nresponse to any instances of unacceptable behavior.\n\nProject maintainers have the right and responsibility to remove, edit, or\nreject comments, commits, code, wiki edits, issues, and other contributions\nthat are not aligned to this Code of Conduct, or to ban temporarily or\npermanently any contributor for other behaviors that they deem inappropriate,\nthreatening, offensive, or harmful.\n\n## Scope\n\nThis Code of Conduct applies both within project spaces and in public spaces\nwhen an individual is representing the project or its community. Examples of\nrepresenting a project or community include using an official project e-mail\naddress, posting via an official social media account, or acting as an appointed\nrepresentative at an online or offline event. Representation of a project may be\nfurther defined and clarified by project maintainers.\n\n## Enforcement\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be\nreported by contacting the project team at weihanli@outlook.com. All\ncomplaints will be reviewed and investigated and will result in a response that\nis deemed necessary and appropriate to the circumstances. The project team is\nobligated to maintain confidentiality with regard to the reporter of an incident.\nFurther details of specific enforcement policies may be posted separately.\n\nProject maintainers who do not follow or enforce the Code of Conduct in good\nfaith may face temporary or permanent repercussions as determined by other\nmembers of the project's leadership.\n\n## Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,\navailable at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html\n\n[homepage]: https://www.contributor-covenant.org\n\nFor answers to common questions about this code of conduct, see\nhttps://www.contributor-covenant.org/faq\n"
  },
  {
    "path": "Directory.Build.props",
    "content": "<Project>\n  <Import Project=\"./build/version.props\"/>\n  <PropertyGroup>\n    <TargetFramework>net10.0</TargetFramework>\n    <LangVersion>preview</LangVersion>\n    <Nullable>enable</Nullable>\n    <ImplicitUsings>enable</ImplicitUsings>\n    <Authors>WeihanLi.Npoi Contributors</Authors>\n    <Copyright>Copyright 2017-$([System.DateTime]::Now.Year) (c) WeihanLi</Copyright>\n    <NoWarn>$(NoWarn);NU5048;</NoWarn>\n  </PropertyGroup>\n</Project>\n"
  },
  {
    "path": "Directory.Packages.props",
    "content": "<Project>\n  <PropertyGroup>\n    <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>\n    <!-- https://learn.microsoft.com/en-us/nuget/concepts/auditing-packages -->\n    <NuGetAudit>true</NuGetAudit>\n    <NuGetAuditMode>direct</NuGetAuditMode>\n  </PropertyGroup>\n  <ItemGroup>\n    <PackageVersion Include=\"BenchmarkDotNet\" Version=\"0.15.8\" />\n    <PackageVersion Include=\"coverlet.collector\" Version=\"10.0.0\" />\n    <PackageVersion Include=\"EPPlus.Core.Extensions\" Version=\"2.4.0\" />\n    <PackageVersion Include=\"GitHubActionsTestLogger\" Version=\"3.0.3\" />\n    <PackageVersion Include=\"Microsoft.NET.Test.Sdk\" Version=\"18.4.0\" />\n    <PackageVersion Include=\"NPOI\" Version=\"2.7.6\" />\n    <PackageVersion Include=\"WeihanLi.Common\" Version=\"1.0.88\" />\n    <PackageVersion Include=\"xunit.v3.mtp-v2\" Version=\"3.2.2\" />\n    <PackageVersion Include=\"Xunit.DependencyInjection\" Version=\"11.2.1\" />\n  </ItemGroup>\n</Project>\n"
  },
  {
    "path": "LICENSE",
    "content": "Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"{}\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright 2018 WeihanLi\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License."
  },
  {
    "path": "README.md",
    "content": "# WeihanLi.Npoi\n\n[![WeihanLi.Npoi](https://img.shields.io/nuget/v/WeihanLi.Npoi)](https://www.nuget.org/packages/WeihanLi.Npoi/)\n[![WeihanLi.Npoi Latest](https://img.shields.io/nuget/vpre/WeihanLi.Npoi)](https://www.nuget.org/packages/WeihanLi.Npoi/absoluteLatest)\n[![NuGet Downloads](https://img.shields.io/nuget/dt/WeihanLi.Npoi)](https://www.nuget.org/packages/WeihanLi.Npoi/) \n[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/WeihanLi/WeihanLi.Npoi)\n\n[![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)\n\n## Introduction\n\n[NPOI](https://www.nuget.org/packages/NPOI/) extensions based on target framework `netstandard2.0`.\n\n`WeihanLi.Npoi` provides a powerful and easy-to-use toolkit for working with Excel and CSV files in .NET applications. It offers:\n\n- **Simple API**: Intuitive extension methods for common import/export operations\n- **Flexible Configuration**: Support for both Attribute-based and FluentAPI configuration\n- **High Performance**: Optimized for handling large datasets efficiently\n- **Rich Features**: Advanced capabilities like template export, multi-sheet support, and shadow properties\n- **CSV Support**: Full support for CSV file operations alongside Excel\n\n## Core Features\n\n### 📥 Data Import\n\n- Import Excel files to `List<TEntity>` or `IEnumerable<TEntity>`\n- Import Excel files to `DataTable`\n- Import CSV files to entities or DataTable\n- Support for custom header rows and sheet selection\n- Automatic type conversion and data mapping\n\n### 📤 Data Export\n\n- Export `IEnumerable<TEntity>` or `DataTable` to Excel files (.xls/.xlsx)\n- Export data to Excel byte arrays or streams\n- Export to CSV files or byte arrays\n- Template-based export with placeholders for complex layouts\n- Multi-sheet export in a single workbook\n\n### ⚙️ Configuration Options\n\n- **Attribute Configuration**: Simple decoration with `[Column]` and `[Sheet]` attributes\n- **FluentAPI Configuration**: Powerful and flexible configuration with fluent syntax (Recommended)\n- Custom column mapping, formatting, and transformations\n- Support for shadow properties (columns not in the model)\n\n### 🎨 Advanced Capabilities\n\n- **InputFormatter/OutputFormatter**: Transform data during import/export operations\n- **ColumnInputFormatter/ColumnOutputFormatter**: Column-specific data transformations\n- **CellReader**: Custom cell reading logic\n- **Template Export**: Export data based on pre-designed Excel templates\n- **Multi-Sheet Support**: Handle multiple sheets in a single workbook\n- **Shadow Properties**: Define additional export columns not present in your models\n- **Auto Column Width**: Automatic column width adjustment\n- **Freeze Panes**: Set freeze panes for better data viewing\n- **Filters**: Add auto-filters to your Excel sheets\n\n### GetStarted\n\n#### Installation\n\n```sh\ndotnet add package WeihanLi.Npoi\n```\n\n#### Quick Start\n\n1. Export list/dataTable to Excel/csv\n\n    ``` csharp\n    var entities = new List<Entity>();\n    \n    // Export to Excel file\n    entities.ToExcelFile(string excelPath);\n    \n    // Export to Excel bytes\n    entities.ToExcelBytes(ExcelFormat excelFormat);\n    \n    // Export to CSV file\n    entities.ToCsvFile(string csvPath);\n    \n    // Export to CSV bytes\n    entities.ToCsvBytes();\n    ```\n\n2. Import Excel/csv to List\n\n    ``` csharp\n    // Read Excel first sheet content to List<T>\n    var entityList = ExcelHelper.ToEntityList<T>(string excelPath);\n\n    // Read Excel first sheet content to IEnumerable<T>\n    var entityList = ExcelHelper.ToEntities<T>(string excelPath);\n\n    // Read Excel specific sheet content to List<T>\n    // You can customize header row index via sheet attribute or fluent api HasSheet\n    var entityList1 = ExcelHelper.ToEntityList<T>(string excelPath, int sheetIndex);\n\n    // Import CSV to List<T>\n    var entityList2 = CsvHelper.ToEntityList<T>(string csvPath);\n    var entityList3 = CsvHelper.ToEntityList<T>(byte[] csvBytes);\n    ```\n\n3. Import Excel/csv to DataTable\n\n    ``` csharp\n    // Read Excel to DataTable directly, by default read the first sheet content\n    var dataTable = ExcelHelper.ToDataTable(string excelPath);\n\n    // Read Excel workbook's specific sheet to DataTable\n    var dataTableOfSheetIndex = ExcelHelper.ToDataTable(string excelPath, int sheetIndex);\n\n    // Read Excel with custom header row index\n    var dataTableOfSheetIndex = ExcelHelper.ToDataTable(string excelPath, int sheetIndex, int headerRowIndex);\n\n    // Read Excel to DataTable using mapping relations and settings from typeof(T)\n    var dataTableT = ExcelHelper.ToDataTable<T>(string excelPath);\n\n    // Read CSV file data to DataTable\n    var dataTable1 = CsvHelper.ToDataTable(string csvFilePath);\n    ```\n\nMore Api documentation: <https://weihanli.github.io/WeihanLi.Npoi/api/WeihanLi.Npoi.html>\n\n### Configuration\n\n#### 1. Using Attributes\n\nAdd `ColumnAttribute` on the properties of your entity for export or import operations.\n\nAdd `SheetAttribute` on the entity to configure sheet settings. You can set the `StartRowIndex` as needed (default is `1`).\n\nExample:\n\n``` csharp\n[Sheet(SheetName = \"TestSheet\", SheetIndex = 0, AutoColumnWidthEnabled = true)]\npublic class TestEntity\n{\n    [Column(\"ID\", Index = 0)]\n    public int PKID { get; set; }\n\n    [Column(\"Bill Title\", Index = 1)]\n    public string BillTitle { get; set; }\n\n    [Column(\"Bill Details\", Index = 2)]\n    public string BillDetails { get; set; }\n\n    [Column(\"Created By\", Index = 3)]\n    public string CreatedBy { get; set; }\n\n    [Column(\"Created Time\", Index = 4, Formatter = \"yyyy-MM-dd HH:mm:ss\")]\n    public DateTime CreatedTime { get; set; }\n    \n    [Column(IsIgnored = true)]\n    public string InternalNote { get; set; }\n}\n\npublic class TestEntity1\n{\n    [Column(\"Username\")]\n    public string Username { get; set; }\n\n    [Column(IsIgnored = true)]\n    public string PasswordHash { get; set; }\n\n    [Column(\"Amount\")]\n    public decimal Amount { get; set; } = 1000M;\n\n    [Column(\"WeChat OpenID\")]\n    public string WechatOpenId { get; set; }\n\n    [Column(\"Is Active\")]\n    public bool IsActive { get; set; }\n}\n```\n\n#### 2. Using FluentAPI (Recommended)\n\nFluentAPI provides greater flexibility and more powerful configuration options.\n\nExample:\n\n``` csharp\nvar setting = FluentSettings.For<TestEntity>();\n\n// Excel document settings\nsetting.HasAuthor(\"WeihanLi\")\n    .HasTitle(\"WeihanLi.Npoi test\")\n    .HasDescription(\"WeihanLi.Npoi test\")\n    .HasSubject(\"WeihanLi.Npoi test\");\n\n// Sheet configuration (sheetIndex, sheetName, startRowIndex, autoColumnWidth)\nsetting.HasSheetConfiguration(0, \"SystemSettingsList\", 1, true);\n\n// Apply filters and freeze panes\n// setting.HasFilter(0, 1).HasFreezePane(0, 1, 2, 1);\n\n// Configure individual properties\nsetting.Property(_ => _.SettingId)\n    .HasColumnIndex(0);\n\nsetting.Property(_ => _.SettingName)\n    .HasColumnTitle(\"SettingName\")\n    .HasColumnIndex(1);\n\nsetting.Property(_ => _.DisplayName)\n    .HasOutputFormatter((entity, displayName) => $\"AAA_{entity.SettingName}_{displayName}\")\n    .HasInputFormatter((entity, originVal) => originVal.Split(new[] { '_' })[2])\n    .HasColumnTitle(\"DisplayName\")\n    .HasColumnIndex(2);\n\nsetting.Property(_ => _.SettingValue)\n    .HasColumnTitle(\"SettingValue\")\n    .HasColumnIndex(3);\n\nsetting.Property(_ => _.CreatedTime)\n    .HasColumnTitle(\"CreatedTime\")\n    .HasColumnIndex(4)\n    .HasColumnWidth(10)\n    .HasColumnFormatter(\"yyyy-MM-dd HH:mm:ss\");\n\nsetting.Property(_ => _.CreatedBy)\n    .HasColumnInputFormatter(x => x += \"_test\")\n    .HasColumnIndex(5)\n    .HasColumnTitle(\"CreatedBy\");\n\nsetting.Property(x => x.Enabled)\n    .HasColumnInputFormatter(val => \"Enabled\".Equals(val))\n    .HasColumnOutputFormatter(v => v ? \"Enabled\" : \"Disabled\");\n\n// Shadow property - define a column that doesn't exist in the model\nsetting.Property(\"HiddenProp\")\n    .HasOutputFormatter((entity, val) => $\"HiddenProp_{entity.PKID}\");\n\n// Ignore specific properties\nsetting.Property(_ => _.PKID).Ignored();\nsetting.Property(_ => _.UpdatedBy).Ignored();\nsetting.Property(_ => _.UpdatedTime).Ignored();\n```\n\n### Advanced Features\n\n#### Template-based Export\n\nExport data based on pre-designed Excel templates with placeholder support:\n\n```csharp\nentities.ToExcelFileByTemplate(\n    templatePath: \"path/to/template.xlsx\",\n    excelPath: \"path/to/output.xlsx\",\n    extraData: new { Author = \"WeihanLi\", Title = \"Export Result\" }\n);\n```\n\nLearn more: [Template Export Documentation](https://weihanli.github.io/WeihanLi.Npoi/articles/en/TemplateExport.html)\n\n#### Multi-Sheet Export\n\nExport multiple collections to different sheets in a single workbook:\n\n```csharp\nvar workbook = ExcelHelper.PrepareWorkbook(ExcelFormat.Xlsx);\nworkbook.ImportData(collection1, sheetIndex: 0);\nworkbook.ImportData(collection2, sheetIndex: 1);\nworkbook.WriteToFile(\"multi-sheets.xlsx\");\n```\n\nLearn more: [Multi-Sheet Documentation](https://weihanli.github.io/WeihanLi.Npoi/articles/en/MultiSheets.html)\n\n#### Shadow Properties\n\nDefine additional export columns that don't exist in your model:\n\n```csharp\nvar settings = FluentSettings.For<TestEntity>();\nsettings.Property(\"Employee ID\")\n    .HasOutputFormatter((entity, val) => $\"{entity.UserFields[2].Value}\");\nsettings.Property(\"Department\")\n    .HasOutputFormatter((entity, val) => $\"{entity.UserFields[1].Value}\");\n```\n\nLearn more: [Shadow Property Documentation](https://weihanli.github.io/WeihanLi.Npoi/articles/en/ShadowProperty.html)\n\n### Documentation\n\n- 📖 [Complete Documentation](https://weihanli.github.io/WeihanLi.Npoi/)\n- 🚀 [Getting Started Guide](https://weihanli.github.io/WeihanLi.Npoi/articles/en/GetStarted.html)\n- 📚 [API Reference](https://weihanli.github.io/WeihanLi.Npoi/api/WeihanLi.Npoi.html)\n- 🌐 [Articles](https://weihanli.github.io/WeihanLi.Npoi/articles/intro.html)\n\n### More\n\nsee some articles here: <https://weihanli.github.io/WeihanLi.Npoi/articles/intro.html>\n\nmore usage:\n\n<details>\n<summary>Get a workbook</summary>\n\n``` csharp\n// load excel workbook from file\nvar workbook = LoadExcel(string excelPath);\n\n// prepare a workbook accounting to excelPath\nvar workbook = PrepareWorkbook(string excelPath);\n\n// prepare a workbook accounting to excelPath and custom excel settings\nvar workbook = PrepareWorkbook(string excelPath, ExcelSetting excelSetting);\n\n// prepare a workbook whether *.xls file\nvar workbook = PrepareWorkbook(bool isXls);\n\n// prepare a workbook whether *.xls file and custom excel setting\nvar workbook = PrepareWorkbook(bool isXlsx, ExcelSetting excelSetting);\n```\n\n</details>\n\n<details>\n<summary>Rich extensions</summary>\n\n``` csharp\nList<TEntity> ToEntityList<TEntity>([NotNull]this IWorkbook workbook)\n\nDataTable ToDataTable([NotNull]this IWorkbook workbook)\n\nISheet ImportData<TEntity>([NotNull] this ISheet sheet, DataTable dataTable)\n\nint ImportData<TEntity>([NotNull] this IWorkbook workbook, IEnumerable<TEntity> list,\n            int sheetIndex)\n\nint ImportData<TEntity>([NotNull] this ISheet sheet, IEnumerable<TEntity> list)\n\nint ImportData<TEntity>([NotNull] this IWorkbook workbook, [NotNull] DataTable dataTable,\n            int sheetIndex)\n\nToExcelFile<TEntity>([NotNull] this IEnumerable<TEntity> entityList,\n            [NotNull] string excelPath)\n\nint ToExcelStream<TEntity>([NotNull] this IEnumerable<TEntity> entityList,\n            [NotNull] Stream stream)\n\nbyte[] ToExcelBytes<TEntity>([NotNull] this IEnumerable<TEntity> entityList)\n\nint ToExcelFile([NotNull] this DataTable dataTable, [NotNull] string excelPath)\n\nint ToExcelStream([NotNull] this DataTable dataTable, [NotNull] Stream stream)\n\nbyte[] ToExcelBytes([NotNull] this DataTable dataTable)\n\nbyte[] ToExcelBytes([NotNull] this IWorkbook workbook)\n\nint WriteToFile([NotNull] this IWorkbook workbook, string filePath)\n\nobject GetCellValue([NotNull] this ICell cell, Type propertyType)\n\nT GetCellValue<T>([NotNull] this ICell cell)\n\nvoid SetCellValue([NotNull] this ICell cell, object value)\n\nbyte[] ToCsvBytes<TEntity>(this IEnumerable<TEntity> entities, bool includeHeader)\n\nToCsvFile<TEntity>(this IEnumerable<TEntity> entities, string filePath, bool includeHeader)\n\nvoid ToCsvFile(this DataTable dt, string filePath, bool includeHeader)\n\nbyte[] ToCsvBytes(this DataTable dt, bool includeHeader)\n\n```\n\n</details>\n\n### Samples\n\n- [.NET Core Sample](https://github.com/WeihanLi/WeihanLi.Npoi/blob/dev/samples/DotNetCoreSample/Program.cs)\n- [More Samples in Unit Tests](https://github.com/WeihanLi/WeihanLi.Npoi/blob/dev/test/WeihanLi.Npoi.Test/ExcelTest.cs)\n- [Guide Posts](https://weihanli.github.io/WeihanLi.Npoi/articles/intro.html)\n\n### Contributing\n\nContributions are welcome! Please feel free to submit a Pull Request.\n\n1. Fork the repository\n2. Create your feature branch (`git checkout -b feature/AmazingFeature`)\n3. Commit your changes (`git commit -m 'Add some AmazingFeature'`)\n4. Push to the branch (`git push origin feature/AmazingFeature`)\n5. Open a Pull Request\n\n### Acknowledgements\n\n- Thanks to all the [contributors](https://github.com/WeihanLi/WeihanLi.Npoi/graphs/contributors) and users of this project\n- Thanks to [NPOI](https://github.com/tonyqus/npoi) for the excellent Excel library\n- Thanks to [FluentExcel](https://github.com/Arch/FluentExcel/) for the FluentAPI inspiration\n- Thanks to JetBrains for the free Rider license\n\n### License\n\nThis project is licensed under the Apache License 2.0 - see the [LICENSE](LICENSE) file for details.\n\n### Contact & Support\n\n- 📧 Report Issues/Questions/Discussions: [GitHub Issues](https://github.com/WeihanLi/WeihanLi.Npoi/issues)\n- 📦 NuGet Package: [WeihanLi.Npoi](https://www.nuget.org/packages/WeihanLi.Npoi/)\n"
  },
  {
    "path": "WeihanLi.Npoi.sln.DotSettings",
    "content": "﻿<wpf:ResourceDictionary xml:space=\"preserve\" xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\" xmlns:s=\"clr-namespace:System;assembly=mscorlib\" xmlns:ss=\"urn:shemas-jetbrains-com:settings-storage-xaml\" xmlns:wpf=\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\">\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=weihan/@EntryIndexedValue\">True</s:Boolean></wpf:ResourceDictionary>"
  },
  {
    "path": "WeihanLi.Npoi.slnx",
    "content": "<Solution>\n  <Folder Name=\"/perf/\">\n    <Project Path=\"perf/WeihanLi.Npoi.Benchmark/WeihanLi.Npoi.Benchmark.csproj\" />\n  </Folder>\n  <Folder Name=\"/samples/\">\n    <Project Path=\"samples/DotNetCoreSample/DotNetCoreSample.csproj\" />\n  </Folder>\n  <Folder Name=\"/Solution Items/\">\n    <File Path=\".editorconfig\" />\n    <File Path=\"build/version.props\" />\n    <File Path=\"Directory.Build.props\" />\n    <File Path=\"Directory.Packages.props\" />\n    <File Path=\"docs/ReleaseNotes.md\" />\n    <File Path=\"README.md\" />\n  </Folder>\n  <Folder Name=\"/src/\">\n    <Project Path=\"src/WeihanLi.Npoi/WeihanLi.Npoi.csproj\" />\n  </Folder>\n  <Folder Name=\"/test/\">\n    <Project Path=\"test/WeihanLi.Npoi.Test/WeihanLi.Npoi.Test.csproj\" />\n  </Folder>\n</Solution>\n"
  },
  {
    "path": "azure-pipelines.yml",
    "content": "trigger:\n  branches:\n    include:\n    - '*' # must quote since \"*\" is a YAML reserved character; we want a string\n  paths:\n    exclude:\n    - '*.md'\n\npool:\n  vmImage: 'windows-latest'\n\nsteps:\n- task: UseDotNet@2\n  displayName: 'Use .NET SDK'\n  inputs:\n    packageType: sdk\n    version: 10.0.x\n\n- script: dotnet --info\n  displayName: 'dotnet info'\n\n- powershell: dotnet build.cs\n  displayName: 'Powershell Script'\n  env:\n    NuGet__ApiKey: $(nugetApiKey)\n    NuGet__Source: $(nugetSourceUrl)\n"
  },
  {
    "path": "build/common.props",
    "content": "<Project>\n  <PropertyGroup>        \n    <TargetFramework>netstandard2.0</TargetFramework>  \n    <Title>WeihanLi.Npoi</Title>\n    <PackageId>WeihanLi.Npoi</PackageId>\n    <Authors>WeihanLi</Authors>\n    <Company>WeihanLi</Company>\n    <Product>WeihanLi.Npoi</Product>\n    <PackageIconUrl>https://avatars3.githubusercontent.com/u/7604648</PackageIconUrl>\n    <PackageLicenseExpression>Apache-2.0</PackageLicenseExpression>\n    <PackageProjectUrl>https://github.com/WeihanLi/WeihanLi.Npoi</PackageProjectUrl>\n    <RepositoryUrl>https://github.com/WeihanLi/WeihanLi.Npoi</RepositoryUrl>\n    <RepositoryType>git</RepositoryType>\n    <PackageTags>npoi excel csv</PackageTags>\n    <Description>\n      Amazing NPOI Extensions\n      Import/Export excel or csv to/from entity list or DataTable(DataSet)\t\n      Custom configuration and mappings via Attribute/FluentAPI\n    </Description>\n    <PackageReleaseNotes>\n      https://github.com/WeihanLi/WeihanLi.Npoi/tree/dev/docs/ReleaseNotes.md\n    </PackageReleaseNotes>\n    <EnablePackageValidation>true</EnablePackageValidation>\n    <PackageValidationBaselineVersion>3.3.0</PackageValidationBaselineVersion>\n  </PropertyGroup>\n</Project>\n"
  },
  {
    "path": "build/getReleaseVersion.ps1",
    "content": "$versionPath=$PSScriptRoot+\"/version.props\"\r\n$versionXml=([xml](Get-Content $versionPath))\r\n$versionProperty=$versionXml.Project.PropertyGroup\r\n$ReleaseVersion=$versionProperty.VersionMajor+\".\"+$versionProperty.VersionMinor+\".\"+$versionProperty.VersionPatch\r\n$ReleaseVersion\r\nAdd-Content -Path $env:GITHUB_ENV -Value \"ReleaseVersion=${ReleaseVersion}\""
  },
  {
    "path": "build/sign.props",
    "content": "<Project>\n  <PropertyGroup Condition=\"'$(OS)' == 'Windows_NT' AND '$(Configuration)' == 'Release'\">\n    <SignAssembly>True</SignAssembly>\n    <AssemblyOriginatorKeyFile>$(MSBuildThisFileDirectory)weihanli.snk</AssemblyOriginatorKeyFile>\n    <DelaySign>False</DelaySign>\n  </PropertyGroup>\n</Project>"
  },
  {
    "path": "build/version.props",
    "content": "<Project>\n  <PropertyGroup>\n    <VersionMajor>3</VersionMajor>\n    <VersionMinor>3</VersionMinor>\n    <VersionPatch>0</VersionPatch>\n    <VersionPrefix>$(VersionMajor).$(VersionMinor).$(VersionPatch)</VersionPrefix>\n    <InformationalVersion>$(PackageVersion)</InformationalVersion>\n  </PropertyGroup>\n</Project>\n"
  },
  {
    "path": "build.cs",
    "content": "﻿// Copyright (c) Weihan Li. All rights reserved.\n// Licensed under the Apache license.\n\n#:package WeihanLi.Common\n\nusing WeihanLi.Common.Helpers;\n\nvar solutionPath = \"./WeihanLi.Npoi.slnx\";\nstring[] srcProjects = [ \n    \"./src/WeihanLi.Npoi/WeihanLi.Npoi.csproj\"\n];\nstring[] testProjects = [ \n    \"./test/WeihanLi.Npoi.Test/WeihanLi.Npoi.Test.csproj\"\n];\nstring[] runFileSamplesFolders = [\n    \"./samples/run-file-samples\"\n];\n\nawait DotNetPackageBuildProcess\n    .Create(options => \n    {\n        options.SolutionPath = solutionPath;\n        options.SrcProjects = srcProjects;\n        options.TestProjects = testProjects;\n        options.RunFileSampleFolders = runFileSamplesFolders;\n    })\n    .ExecuteAsync(args);\n"
  },
  {
    "path": "docs/ReleaseNotes.md",
    "content": "# WeihanLi.Npoi Release Notes\n\n## [3.3.0](https://www.nuget.org/packages/WeihanLi.Npoi/3.3.0)\n\n- Support `HasPostImportAction` to support post handler when import entity\n- Code Refactoring to keep clean code\n- Modernize dependenncies and integrate GithubActionsTestLogger and improve build script\n\n## [3.2.0](https://www.nuget.org/packages/WeihanLi.Npoi/3.2.0)\n\n- Upgrade dependencies\n- Update samples/tests to .NET 10\n- refine build scripts\n\n## [3.1.0](https://www.nuget.org/packages/WeihanLi.Npoi/3.1.0)\n\n- Upgrade NPOI package to fix merged region handling bug which causes export excel by template with merged region exception\n- Migrate to slnx, xunit v3\n\n## [3.0.0](https://www.nuget.org/packages/WeihanLi.Npoi/3.0.0)\n\n- Remove `net6.0` target, and update build sdk and samples/tests to `net8.0`\n- Adjust column index enhancements, respect property index by default and support `WithPropertyComparer`\n\n## [2.5.0](https://www.nuget.org/packages/WeihanLi.Npoi/2.5.0)\n\n- Upgrade dependencies to fix upstream breaking changes\n- Enable central package version management\n\n## [2.4.0](https://www.nuget.org/packages/WeihanLi.Npoi/2.4.0)\n\n- Fixes <https://github.com/WeihanLi/WeihanLi.Npoi/issues/146>, fix csv encoding handling issue, thanks @yesyeey for spotting the issue\n- `CsvHelper` enhancements\n\n## [2.3.0](https://www.nuget.org/packages/WeihanLi.Npoi/2.3.0)\n\n- Add check before `WriteToFile`\n- Close workbook when the workbook would not be used anymore\n- Fixes <https://github.com/WeihanLi/WeihanLi.Npoi/issues/142>, great thanks for @hansolehuang's help\n\n## [2.2.0](https://www.nuget.org/packages/WeihanLi.Npoi/2.2.0)\n\n- Fix exception when read header that cell format is not string, <https://github.com/WeihanLi/WeihanLi.Npoi/pull/140>, great thanks for @ensleep's help\n- Fix exception when export excel path without directory info, <https://github.com/WeihanLi/WeihanLi.Npoi/pull/140>, great thanks for @ensleep's help\n\n## [2.1.0](https://www.nuget.org/packages/WeihanLi.Npoi/2.1.0)\n\n- Add `HasCellReader` to support more read flexibility\n\n## [2.0.0](https://www.nuget.org/packages/WeihanLi.Npoi/2.0.0)\n\n- Add `net6.0` target support\n- Refactor `CsvHelper`\n- Add `CsvOptions` for `CsvHelper`\n- Add support for validation, fixes #102\n- Add support for `ToEntities`, fixes #113\n\n## [1.21.0](https://www.nuget.org/packages/WeihanLi.Npoi/1.21.0)\n\n- Add support for duplicate column name for dataTable\n- Fix sheet name not applied bug #127\n\n## [1.20.0](https://www.nuget.org/packages/WeihanLi.Npoi/1.20.0)\n\n- The `ExcelHelper.ToDataTable` was extended with two arguments `bool removeEmptyRows = false, int? maxColumns = null`\n- Fix possible `IndexOutOfRangeException` when loading rows\n\n## [1.19.1](https://www.nuget.org/packages/WeihanLi.Npoi/1.19.1)\n\n- Fix `ExcelHelper.ToDataTable` bug when the imported excel column value is not the string value, thanks for @Ninjanaut's contribution\n\n## [1.19.0](https://www.nuget.org/packages/WeihanLi.Npoi/1.19.0)\n\n- Fix `ExcelHelper.ToDataTable` bug when the imported excel file first column is empty, thanks for @Ninjanaut's contribution\n- `FluentSettings.LoadMappingProfile` enhancement\n\n## [1.18.0](https://www.nuget.org/packages/WeihanLi.Npoi/1.18.0)\n\n- add `MappingProfile` support so that we could split mappings into separate mapping profiles\n\n## [1.17.0](https://www.nuget.org/packages/WeihanLi.Npoi/1.17.0)\n\n- add `DrawingPatriarch` null check for `GetPicturesAndPosition`\n\n## [1.16.0](https://www.nuget.org/packages/WeihanLi.Npoi/1.16.0)\n\n- add `CellAction`/`RowAction`/`SheetAction` for more flexible export\n\n## [1.15.0](https://www.nuget.org/packages/WeihanLi.Npoi/1.15.0)\n\n- add support for image import/export\n\n## [1.14.0](https://www.nuget.org/packages/WeihanLi.Npoi/1.14.0)\n\n- enable nullable reference\n- remove `net45` target\n\n## [1.13.0](https://www.nuget.org/packages/WeihanLi.Npoi/1.13.0)\n\n- add support for `EntityList`/`DataTable` export auto split sheets when needed\n\n## [1.12.0](https://www.nuget.org/packages/WeihanLi.Npoi/1.12.0)\n\n- refactor `ExcelSetting` and `SheetSetting`\n- add support for `RowFilter` and `CellFilter`(mainly for import)\n- add support for reading file when file opened by another process\n\n## [1.11.0](https://www.nuget.org/packages/WeihanLi.Npoi/1.11.0)\n\n- add support for formula value import\n\n## [1.10.0](https://www.nuget.org/packages/WeihanLi.Npoi/1.10.0)\n\n- add `EndRowIndex` for `SheetSetting`(zero-based, included)\n- add FluentAPI `WithDataValidation` for excel setting, if set will ignore invalid data when import excel\n- remove `CSVHelper` `TEntity` `new()` constraint\n\n## [1.9.6](https://www.nuget.org/packages/WeihanLi.Npoi/1.9.6)\n\n- fix xlsx workbook `AppVersion` property value caused warning\n\n## [1.9.5](https://www.nuget.org/packages/WeihanLi.Npoi/1.9.5)\n\n- fix `ExcelHelper.ToDataTable` bug with blank cell, thanks for hokis's feedback\n\n## [1.9.4](https://www.nuget.org/packages/WeihanLi.Npoi/1.9.4)\n\n- expose `CsvHelper.GetCsvText` extensions\n\n## [1.9.2](https://www.nuget.org/packages/WeihanLi.Npoi/1.9.2)\n\n- fix `CsvHelper.ParseLine` bug with quoted value, thanks for hokis's effort\n\n## [1.9.0](https://www.nuget.org/packages/WeihanLi.Npoi/1.9.0)\n\n- remove `return 1` code fix #64\n- optimize fluent formatter performance\n- add `FluentSettings.For` instead of `ExcelHelper.SettingsFor`, fluent settings is not only for excel, but also work with csv\n\n## [1.8.2](https://www.nuget.org/packages/WeihanLi.Npoi/1.8.2)\n\n- add `TemplateHelper.ConfigureTemplateOptions` to allow user config templateParamFormat\n\n## [1.8.1](https://www.nuget.org/packages/WeihanLi.Npoi/1.8.1)\n\n- add `ExportExcelByTemplate`, fix #33\n- update `NpoiRowCollection`\n- optimize `DataTable` support for csv\n- export csv as utf8 encoding\n- update export generic type constraint, remove `new()` constraint\n\n## [1.7.0](https://www.nuget.org/packages/WeihanLi.Npoi/1.7.0)\n\n- add `HasColumnInputFormatter`/`HasColumnOutputFormatter`\n- simply `IExcelConfiguration` SheetConfiguration\n\n## [1.6.1](https://www.nuget.org/packages/WeihanLi.Npoi/1.6.1)\n\n- fix inherit property configure bug\n- fix empty column skipped bug, fix with `row.Cells.Count` => `row.LastCellNumber`\n- optimize `AdjustColumnIndex`\n- allow use `Ignored(false)` to unignore property\n\n## [1.6.0](https://www.nuget.org/packages/WeihanLi.Npoi/1.6.0)\n\n- add shadow property support\n- add version info when export `*.xlsx` excel\n\n## [1.5.0](https://www.nuget.org/packages/WeihanLi.Npoi/1.5.0)\n\n- add support for more format, treat as xlsx\n- add `AutoColumnWidthEnabled` setting to `SheetSetting`, no autoSizeColumn by default\n- add `CsvHelper.ToEntityList(byte[] bytes)`/`CsvHelper.ToEntityList(Stream stream)`\n- use xls for default ExcelFormat(better performance)\n\n## [1.4.5](https://www.nuget.org/packages/WeihanLi.Npoi/1.4.5)\n\n- try to auto adjust column index when import excel(do not update existing settings)\n- add `InputFormatter`/`OutputFormatter`\n- apply column settings for CSV\n- remove unused SheetConfiguration\n\n## [1.4.4](https://www.nuget.org/packages/WeihanLi.Npoi/1.4.4)\n\n- add `ExcelHelper.LoadExcel()`/`ExcelHelper.ToEntityList` override for stream/bytes\n\n## [1.4.3](https://www.nuget.org/packages/WeihanLi.Npoi/1.4.3)\n\n- fix possible `NullReferenceException` when `ExcelHelper.ToEntityList()`/`ToExcelFile()`\n- fix treat `string.Empty` as `null` bug, `SetCellType` after `SetCellValue` so that `null` => `CellType.Blank`, `string.Empty` => `CellType.String`\n\n## [1.4.0](https://www.nuget.org/packages/WeihanLi.Npoi/1.4.0)\n\n- add support for custom column width\n- fix `ToExcelFile`/`ImportData` extension not applied configuration bug\n- add support for specific sheetIndex when export excel\n\n## [1.3.8](https://www.nuget.org/packages/WeihanLi.Npoi/1.3.8)\n\n- fix : `CsvHelper.ToDataTable()` and export DataTable to csv (Thanks for Arek's feedback)\n- add support for no header when export excel(to fix #26 )\n\n## [1.3.7](https://www.nuget.org/packages/WeihanLi.Npoi/1.3.7)\n\n- add `HasColumnFormatter<TEntity, TProperty>(Func<TEntity, TProperty, object> columnFormatter)` to support for custom column output for export\n\n## [1.3.6](https://www.nuget.org/packages/WeihanLi.Npoi/1.3.6)\n\n- add [sourcelink](http://github.com/dotnet/sourcelink) support\n\n## [1.3.5](https://www.nuget.org/packages/WeihanLi.Npoi/1.3.5)\n\n- add support for csv escape\n\n## [1.3.3](https://www.nuget.org/packages/WeihanLi.Npoi/1.3.3)\n\n- fix csv custom columnIndex bug\n- Optimize csv operation for entity list\n\n## [1.3.0](https://www.nuget.org/packages/WeihanLi.Npoi/1.3.0)\n\n- Update NPOI package to 2.4.1\n- Add support for struct types\n- Add default excel settings\n\n## [1.2.0](https://www.nuget.org/packages/WeihanLi.Npoi/1.2.0)\n\n- Update NPOI package to 2.4.0, use NPOI for netstandard2.0 also\n- Add CsvHelper for import and export csv file, and mapping to entities\n\n## [1.1.0](https://www.nuget.org/packages/WeihanLi.Npoi/1.1.0)\n\n- StrongNaming package\n"
  },
  {
    "path": "docs/api/.gitignore",
    "content": "###############\n#  temp file  #\n###############\n*.yml\n.manifest\n"
  },
  {
    "path": "docs/api/index.md",
    "content": "# PLACEHOLDER\n\nTODO: Add .NET projects to the *src* folder and run `docfx` to generate **REAL** *API Documentation*!\n"
  },
  {
    "path": "docs/articles/en/CustomizeStyle.md",
    "content": "# Customize Excel Styles\n\n## Introduction\n\nWeihanLi.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.\n\n## Auto Column Width\n\nThe simplest styling feature is enabling automatic column width adjustment:\n\n```csharp\nvar settings = FluentSettings.For<TestEntity>();\nsettings.HasSheetSetting(config =>\n{\n    config.AutoColumnWidthEnabled = true;\n});\n```\n\nThis automatically adjusts column widths based on content, making your spreadsheet more readable without manual width adjustments.\n\n## Styling Header Rows\n\nUse `RowAction` to customize the appearance of rows. A common use case is styling the header row:\n\n```csharp\nsettings.HasSheetSetting(config =>\n{\n    config.StartRowIndex = 1;\n    config.SheetName = \"StyledSheet\";\n    config.AutoColumnWidthEnabled = true;\n\n    config.RowAction = row =>\n    {\n        if (row.RowNum == 0) // Header row\n        {\n            // Create a cell style\n            var style = row.Sheet.Workbook.CreateCellStyle();\n            style.Alignment = HorizontalAlignment.Center;\n            \n            // Create and configure font\n            var font = row.Sheet.Workbook.CreateFont();\n            font.FontName = \"Arial\";\n            font.IsBold = true;\n            font.FontHeight = 220; // 11pt (font height is in 1/20th of a point)\n            font.Color = IndexedColors.White.Index;\n            \n            style.SetFont(font);\n            \n            // Set background color\n            style.FillForegroundColor = IndexedColors.DarkBlue.Index;\n            style.FillPattern = FillPattern.SolidForeground;\n            \n            // Apply style to all cells in the row\n            row.Cells.ForEach(c => c.CellStyle = style);\n        }\n    };\n});\n```\n\n### Font Properties\n\nAvailable font properties include:\n\n- `FontName`: Font family (e.g., \"Arial\", \"Calibri\", \"Times New Roman\")\n- `FontHeight`: Font size in 1/20th of a point (e.g., 200 = 10pt, 220 = 11pt)\n- `IsBold`: Bold text\n- `IsItalic`: Italic text\n- `IsStrikeout`: Strikethrough text\n- `Underline`: Text underline style\n- `Color`: Font color using `IndexedColors`\n\n### Cell Style Properties\n\nKey cell style properties:\n\n- `Alignment`: Horizontal alignment (`Left`, `Center`, `Right`, `Justify`)\n- `VerticalAlignment`: Vertical alignment (`Top`, `Center`, `Bottom`)\n- `FillForegroundColor`: Background color\n- `FillPattern`: Fill pattern (usually `SolidForeground` for solid colors)\n- `BorderTop`, `BorderBottom`, `BorderLeft`, `BorderRight`: Border styles\n- `WrapText`: Enable text wrapping\n\n## Styling Individual Cells\n\nUse `CellAction` to customize individual cells based on their position or content:\n\n```csharp\nsettings.HasSheetSetting(config =>\n{\n    config.CellAction = cell =>\n    {\n        // Style specific columns\n        if (cell.ColumnIndex == 0) // First column\n        {\n            var style = cell.Sheet.Workbook.CreateCellStyle();\n            var font = cell.Sheet.Workbook.CreateFont();\n            font.IsBold = true;\n            style.SetFont(font);\n            cell.CellStyle = style;\n        }\n        \n        // Conditional styling based on content\n        if (cell.RowIndex > 0 && cell.ColumnIndex == 3) // Data rows, 4th column\n        {\n            if (cell.NumericCellValue < 0) // Negative numbers in red\n            {\n                var style = cell.Sheet.Workbook.CreateCellStyle();\n                var font = cell.Sheet.Workbook.CreateFont();\n                font.Color = IndexedColors.Red.Index;\n                style.SetFont(font);\n                cell.CellStyle = style;\n            }\n        }\n    };\n});\n```\n\n## Data Validation\n\nAdd dropdown lists and other validation rules to cells:\n\n```csharp\nsettings.HasSheetSetting(config =>\n{\n    config.CellAction = cell =>\n    {\n        // Add dropdown validation for header cell matching \"Status\"\n        if (cell.RowIndex == 0 && cell.StringCellValue == \"Status\")\n        {\n            var validationHelper = cell.Sheet.GetDataValidationHelper();\n            \n            // Define allowed values\n            var statusValues = new[] { \"Active\", \"Inactive\", \"Pending\" };\n            var constraint = validationHelper.CreateExplicitListConstraint(statusValues);\n            \n            // Apply validation to data rows (rows 1-100, current column)\n            var addressList = new CellRangeAddressList(1, 100, cell.ColumnIndex, cell.ColumnIndex);\n            var validation = validationHelper.CreateValidation(constraint, addressList);\n            \n            validation.ShowErrorBox = true;\n            validation.CreateErrorBox(\"Invalid Status\", \"Please select from the dropdown list\");\n            validation.ShowPromptBox = true;\n            validation.CreatePromptBox(\"Status Selection\", \"Choose a status from the list\");\n            \n            cell.Sheet.AddValidationData(validation);\n        }\n    };\n});\n```\n\n### Validation Types\n\nDifferent validation types are available:\n\n```csharp\n// List validation (dropdown)\nvar constraint = validationHelper.CreateExplicitListConstraint(new[] { \"Option1\", \"Option2\" });\n\n// Integer validation\nvar intConstraint = validationHelper.CreateIntegerConstraint(\n    OperatorType.Between, \"1\", \"100\");\n\n// Decimal validation\nvar decimalConstraint = validationHelper.CreateDecimalConstraint(\n    OperatorType.GreaterThan, \"0\", null);\n\n// Date validation\nvar dateConstraint = validationHelper.CreateDateConstraint(\n    OperatorType.Between, \"2024-01-01\", \"2024-12-31\", \"yyyy-MM-dd\");\n\n// Text length validation\nvar textConstraint = validationHelper.CreateTextLengthConstraint(\n    OperatorType.LessThan, \"100\", null);\n```\n\n## Complete Example\n\nHere's a comprehensive example combining multiple styling features:\n\n```csharp\npublic class StyledEntityProfile : IMappingProfile<StyledEntity>\n{\n    public void Configure(IExcelConfiguration<StyledEntity> configuration)\n    {\n        configuration.HasAuthor(\"Your Name\")\n            .HasTitle(\"Styled Report\")\n            .HasDescription(\"Professional styled Excel report\");\n\n        configuration.HasSheetSetting(config =>\n        {\n            config.SheetName = \"Report\";\n            config.StartRowIndex = 1;\n            config.AutoColumnWidthEnabled = true;\n\n            // Style header row\n            config.RowAction = row =>\n            {\n                if (row.RowNum == 0)\n                {\n                    var headerStyle = row.Sheet.Workbook.CreateCellStyle();\n                    headerStyle.Alignment = HorizontalAlignment.Center;\n                    headerStyle.VerticalAlignment = VerticalAlignment.Center;\n                    headerStyle.FillForegroundColor = IndexedColors.Grey25Percent.Index;\n                    headerStyle.FillPattern = FillPattern.SolidForeground;\n                    \n                    var headerFont = row.Sheet.Workbook.CreateFont();\n                    headerFont.FontName = \"Calibri\";\n                    headerFont.IsBold = true;\n                    headerFont.FontHeight = 240; // 12pt\n                    headerStyle.SetFont(headerFont);\n                    \n                    // Add borders\n                    headerStyle.BorderBottom = BorderStyle.Thin;\n                    headerStyle.BorderTop = BorderStyle.Thin;\n                    headerStyle.BorderLeft = BorderStyle.Thin;\n                    headerStyle.BorderRight = BorderStyle.Thin;\n                    \n                    row.Cells.ForEach(c => c.CellStyle = headerStyle);\n                }\n            };\n\n            // Add validation and conditional formatting\n            config.CellAction = cell =>\n            {\n                // Add validation for status column\n                if (cell.RowIndex == 0 && cell.StringCellValue == \"Status\")\n                {\n                    var validationHelper = cell.Sheet.GetDataValidationHelper();\n                    var statusList = new[] { \"Approved\", \"Pending\", \"Rejected\" };\n                    var constraint = validationHelper.CreateExplicitListConstraint(statusList);\n                    var addressList = new CellRangeAddressList(1, 1000, cell.ColumnIndex, cell.ColumnIndex);\n                    var validation = validationHelper.CreateValidation(constraint, addressList);\n                    validation.ShowErrorBox = true;\n                    cell.Sheet.AddValidationData(validation);\n                }\n                \n                // Highlight negative amounts in red\n                if (cell.RowIndex > 0 && cell.ColumnIndex == 2) // Amount column\n                {\n                    try\n                    {\n                        if (cell.NumericCellValue < 0)\n                        {\n                            var redStyle = cell.Sheet.Workbook.CreateCellStyle();\n                            var redFont = cell.Sheet.Workbook.CreateFont();\n                            redFont.Color = IndexedColors.Red.Index;\n                            redFont.IsBold = true;\n                            redStyle.SetFont(redFont);\n                            cell.CellStyle = redStyle;\n                        }\n                    }\n                    catch { } // Skip if not a numeric cell\n                }\n            };\n        });\n\n        // Configure properties\n        configuration.Property(x => x.Id).HasColumnIndex(0);\n        configuration.Property(x => x.Name).HasColumnIndex(1);\n        configuration.Property(x => x.Amount).HasColumnIndex(2);\n        configuration.Property(x => x.Status).HasColumnIndex(3);\n        configuration.Property(x => x.Date)\n            .HasColumnIndex(4)\n            .HasColumnFormatter(\"yyyy-MM-dd\");\n    }\n}\n```\n\n## Best Practices\n\n1. **Reuse Styles**: Create styles once and reuse them rather than creating new styles for each cell to improve performance and reduce file size.\n\n2. **Performance**: Be mindful of performance when applying styles to large datasets. Consider styling only header rows or specific columns.\n\n3. **Color Consistency**: Use `IndexedColors` for consistent coloring across different Excel versions.\n\n4. **Font Sizes**: Remember that font height is in 1/20th of a point (multiply point size by 20).\n\n5. **Validation Ranges**: Set appropriate ranges for data validation to cover expected data rows.\n\n## References\n\n- [Sample Implementation](https://github.com/WeihanLi/WeihanLi.Npoi/blob/dev/samples/DotNetCoreSample/Program.cs)\n- [NPOI Documentation](http://poi.apache.org/)\n"
  },
  {
    "path": "docs/articles/en/GetStarted.md",
    "content": "# `WeihanLi.Npoi` Getting Started\n\n## Introduction\n\n`WeihanLi.Npoi` is an Excel import/export library based on NPOI that provides many useful extension methods and also supports CSV import/export.\n\n- Import excel/csv data into `DataTable` or `List<TEntity>`\n- Export `IEnumerable<TEntity>` or `DataTable` to Excel files, byte arrays, or streams\n- Export `IEnumerable<TEntity>` or `DataTable` to CSV files or byte arrays\n- Configuration through `Attributes` or `FluentAPI` (inspired by [FluentExcel](https://github.com/Arch/FluentExcel/))\n\n## Basic Examples\n\n``` csharp\ninternal class BaseModel\n{\n    public int Id { get; set; }\n}\n\ninternal class Notice : BaseModel\n{\n    public string Title { get; set; }\n\n    public string Content { get; set; }\n\n    public DateTime PublishedAt { get; set; }\n\n    public string Publisher { get; set; }\n}\n```\n\nBasic import and export:\n\n``` csharp\n// entities excel import/export\n[Theory]\n[InlineData(ExcelFormat.Xls)]\n[InlineData(ExcelFormat.Xlsx)]\npublic void BasicImportExportTest(ExcelFormat excelFormat)\n{\n    var list = new List<Notice>();\n    for (var i = 0; i < 10; i++)\n    {\n        list.Add(new Notice()\n        {\n            Id = i + 1,\n            Content = $\"content_{i}\",\n            Title = $\"title_{i}\",\n            PublishedAt = DateTime.UtcNow.AddDays(-i),\n            Publisher = $\"publisher_{i}\"\n        });\n    }\n    list.Add(new Notice() { Title = \"nnnn\" });\n    list.Add(null);\n    var excelBytes = list.ToExcelBytes(excelFormat);\n\n    var importedList = ExcelHelper.ToEntityList<Notice>(excelBytes, excelFormat);\n    Assert.Equal(list.Count, importedList.Count);\n    for (var i = 0; i < list.Count; i++)\n    {\n        if (list[i] is null)\n        {\n            Assert.Null(importedList[i]);\n        }\n        else\n        {\n            Assert.Equal(list[i].Id, importedList[i].Id);\n            Assert.Equal(list[i].Title, importedList[i].Title);\n            Assert.Equal(list[i].Content, importedList[i].Content);\n            Assert.Equal(list[i].Publisher, importedList[i].Publisher);\n            Assert.Equal(list[i].PublishedAt.ToTimeString(), importedList[i].PublishedAt.ToTimeString());\n        }\n    }\n}\n\n// DataTable Excel import/export\n[Theory]\n[InlineData(ExcelFormat.Xls)]\n[InlineData(ExcelFormat.Xlsx)]\npublic void DataTableImportExportTest(ExcelFormat excelFormat)\n{\n    var dt = new DataTable();\n    dt.Columns.AddRange(new[]\n    {\n        new DataColumn(\"Name\"),\n        new DataColumn(\"Age\"),\n        new DataColumn(\"Desc\"),\n    });\n    for (var i = 0; i < 10; i++)\n    {\n        var row = dt.NewRow();\n        row.ItemArray = new object[] { $\"Test_{i}\", i + 10, $\"Desc_{i}\" };\n        dt.Rows.Add(row);\n    }\n    //\n    var excelBytes = dt.ToExcelBytes(excelFormat);\n    var importedData = ExcelHelper.ToDataTable(excelBytes, excelFormat);\n    Assert.NotNull(importedData);\n    Assert.Equal(dt.Rows.Count, importedData.Rows.Count);\n    for (var i = 0; i < dt.Rows.Count; i++)\n    {\n        Assert.Equal(dt.Rows[i].ItemArray.Length, importedData.Rows[i].ItemArray.Length);\n        for (var j = 0; j < dt.Rows[i].ItemArray.Length; j++)\n        {\n            Assert.Equal(dt.Rows[i].ItemArray[j], importedData.Rows[i].ItemArray[j]);\n        }\n    }\n}\n\n// entities csv import/export\n[Fact]\npublic void BasicImportExportTest()\n{\n    var list = new List<Notice>();\n    for (var i = 0; i < 10; i++)\n    {\n        list.Add(new Notice()\n        {\n            Id = i + 1,\n            Content = $\"content_{i}\",\n            Title = $\"title_{i}\",\n            PublishedAt = DateTime.UtcNow.AddDays(-i),\n            Publisher = $\"publisher_{i}\"\n        });\n    }\n    list.Add(new Notice()\n    {\n        Id = 11,\n        Content = $\"content\",\n        Title = $\"title\",\n        PublishedAt = DateTime.UtcNow.AddDays(1),\n    });\n    var csvBytes = list.ToCsvBytes();\n    var importedList = CsvHelper.ToEntityList<Notice>(csvBytes);\n    Assert.Equal(list.Count, importedList.Count);\n    for (var i = 0; i < list.Count; i++)\n    {\n        Assert.Equal(list[i].Id, importedList[i].Id);\n        Assert.Equal(list[i].Title ?? \"\", importedList[i].Title);\n        Assert.Equal(list[i].Content ?? \"\", importedList[i].Content);\n        Assert.Equal(list[i].Publisher ?? \"\", importedList[i].Publisher);\n        Assert.Equal(list[i].PublishedAt.ToTimeString(), importedList[i].PublishedAt.ToTimeString());\n    }\n}\n// DataTable csv import/export\n[Fact]\npublic void DataTableImportExportTest()\n{\n    var dt = new DataTable();\n    dt.Columns.AddRange(new[]\n    {\n        new DataColumn(\"Name\"),\n        new DataColumn(\"Age\"),\n        new DataColumn(\"Desc\"),\n    });\n    for (var i = 0; i < 10; i++)\n    {\n        var row = dt.NewRow();\n        row.ItemArray = new object[] { $\"Test_{i}\", i + 10, $\"Desc_{i}\" };\n        dt.Rows.Add(row);\n    }\n    //\n    var csvBytes = dt.ToCsvBytes();\n    var importedData = CsvHelper.ToDataTable(csvBytes);\n    Assert.NotNull(importedData);\n    Assert.Equal(dt.Rows.Count, importedData.Rows.Count);\n    for (var i = 0; i < dt.Rows.Count; i++)\n    {\n        Assert.Equal(dt.Rows[i].ItemArray.Length, importedData.Rows[i].ItemArray.Length);\n        for (var j = 0; j < dt.Rows[i].ItemArray.Length; j++)\n        {\n            Assert.Equal(dt.Rows[i].ItemArray[j], importedData.Rows[i].ItemArray[j]);\n        }\n    }\n}\n```\n\n## Custom Mapping and Configuration\n\nUsing Attributes:\n\n``` csharp\ninternal class Model\n{\n    [Column(\"Hotel ID\", Index = 0)]\n    public string HotelId { get; set; }\n\n    [Column(\"Order No\", Index = 1)]\n    public string OrderNo { get; set; }\n\n    [Column(\"Hotel Name\", Index = 2)]\n    public string HotelName { get; set; }\n\n    [Column(\"Customer Name\", Index = 3)]\n    public string CustomerName { get; set; }\n\n    [Column(nameof(RoomType), Index = 4)]\n    public string RoomType { get; set; }\n\n    [Column(nameof(CheckInDate), Index = 5, Formatter = \"yyyy/M/d\")]\n    public DateTime CheckInDate { get; set; }\n\n    [Column(nameof(CheckOutDate), Index = 6, Formatter = \"yyyy/M/d\")]\n    public DateTime CheckOutDate { get; set; }\n\n    [Column(nameof(RoomNights), Index = 7)]\n    public int RoomNights { get; set; }\n\n    [Column(nameof(PaymentType), Index = 8)]\n    public string PaymentType { get; set; }\n\n    [Column(nameof(OrderAmount), Index = 9)]\n    public decimal OrderAmount { get; set; }\n\n    [Column(nameof(CommissionRate), Index = 10)]\n    public decimal CommissionRate { get; set; }\n\n    [Column(nameof(ServiceFee), Index = 11)]\n    public decimal ServiceFee { get; set; }\n}\n\n[Sheet(SheetIndex = 0, SheetName = \"TestSheet\", AutoColumnWidthEnabled = true)]\ninternal class TestEntity2\n{\n    [Column(Index = 0)]\n    public int Id { get; set; }\n\n    [Column(Index = 1)]\n    public string Title { get; set; }\n\n    [Column(Index = 2, Width = 50)]\n    public string Description { get; set; }\n\n    [Column(Index = 3, Width = 20)]\n    public string Extra { get; set; } = \"{}\";\n}\n```\n\nUsing FluentAPI (Recommended for greater flexibility):\n\n``` csharp\nvar setting = FluentSettings.For<TestEntity>();\n// ExcelSetting\nsetting.HasAuthor(\"WeihanLi\")\n    .HasTitle(\"WeihanLi.Npoi test\")\n    .HasDescription(\"WeihanLi.Npoi test\")\n    .HasSubject(\"WeihanLi.Npoi test\");\n\nsetting.HasSheetConfiguration(0, \"SystemSettingsList\", 1, true); // sheet configuration\n\n// setting\n//     .HasFilter(0, 1) // Set filter on columns\n//     .HasFreezePane(0, 1, 2, 1); // Set freeze pane\n\nsetting.Property(_ => _.SettingId)\n    .HasColumnIndex(0);\n\nsetting.Property(_ => _.SettingName)\n    .HasColumnTitle(\"SettingName\")\n    .HasColumnIndex(1);\n\nsetting.Property(_ => _.DisplayName)\n    .HasOutputFormatter((entity, displayName) => $\"AAA_{entity.SettingName}_{displayName}\")\n    .HasInputFormatter((entity, originVal) => originVal.Split(new[] { '_' })[2])\n    .HasColumnTitle(\"DisplayName\")\n    .HasColumnIndex(2);\n\nsetting.Property(_ => _.SettingValue)\n    .HasColumnTitle(\"SettingValue\")\n    .HasColumnIndex(3);\n\nsetting.Property(_ => _.CreatedTime)\n    .HasColumnTitle(\"CreatedTime\")\n    .HasColumnIndex(4)\n    .HasColumnWidth(10) // Set column width\n    .HasColumnFormatter(\"yyyy-MM-dd HH:mm:ss\");\n\nsetting.Property(_ => _.CreatedBy)\n    .HasColumnInputFormatter(x => x += \"_test\")\n    .HasColumnIndex(4)\n    .HasColumnTitle(\"CreatedBy\");\n\nsetting.Property(x => x.Enabled)\n    .HasColumnInputFormatter(val => \"Enabled\".Equals(val))\n    .HasColumnOutputFormatter(v => v ? \"Enabled\" : \"Disabled\");\n\nsetting.Property(\"ShadowProperty\")\n    .HasOutputFormatter((entity, val) => $\"HiddenProp_{entity.PKID}\");\n\nsetting.Property(_ => _.PKID).Ignored(); // ignore column\n```\n"
  },
  {
    "path": "docs/articles/en/InputOutputFormatter.md",
    "content": "# InputOutputFormatter Introduction\n\n## Introduction\n\nWeihanLi.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.\n\n## InputFormatter/OutputFormatter\n\nExample Model:\n\n``` csharp\ninternal abstract class BaseEntity\n{\n    public int PKID { get; set; }\n}\n\ninternal class TestEntity : BaseEntity\n{\n    public Guid SettingId { get; set; }\n\n    public string SettingName { get; set; }\n\n    public string DisplayName { get; set; }\n    public string SettingValue { get; set; }\n\n    public string CreatedBy { get; set; } = \"liweihan\";\n\n    public DateTime CreatedTime { get; set; } = DateTime.Now;\n\n    public string UpdatedBy { get; set; }\n\n    public DateTime UpdatedTime { get; set; }\n\n    public bool Enabled { get; set; }\n}\n```\n\nExample Configuration:\n\n``` csharp\nvar setting = FluentSettings.For<TestEntity>();\n// ExcelSetting\nsetting.HasAuthor(\"WeihanLi\")\n    .HasTitle(\"WeihanLi.Npoi test\")\n    .HasDescription(\"WeihanLi.Npoi test\")\n    .HasSubject(\"WeihanLi.Npoi test\");\n\nsetting.HasSheetConfiguration(0, \"SystemSettingsList\", 1, true);\n\nsetting.Property(_ => _.SettingId)\n    .HasColumnIndex(0);\n\nsetting.Property(_ => _.SettingName)\n    .HasColumnTitle(\"SettingName\")\n    .HasColumnIndex(1);\n\nsetting.Property(_ => _.DisplayName)\n    .HasOutputFormatter((entity, displayName) => $\"AAA_{entity.SettingName}_{displayName}\")\n    .HasInputFormatter((entity, originVal) => originVal.Split(new[] { '_' })[2])\n    .HasColumnTitle(\"DisplayName\")\n    .HasColumnIndex(2);\n\nsetting.Property(_ => _.SettingValue)\n    .HasColumnTitle(\"SettingValue\")\n    .HasColumnIndex(3);\n\nsetting.Property(_ => _.CreatedTime)\n    .HasColumnTitle(\"CreatedTime\")\n    .HasColumnIndex(4)\n    .HasColumnWidth(10)\n    .HasColumnFormatter(\"yyyy-MM-dd HH:mm:ss\");\n\nsetting.Property(_ => _.CreatedBy)\n    .HasColumnInputFormatter(x => x += \"_test\")\n    .HasColumnIndex(4)\n    .HasColumnTitle(\"CreatedBy\");\n\nsetting.Property(x => x.Enabled)\n    .HasColumnInputFormatter(val => \"Enabled\".Equals(val))\n    .HasColumnOutputFormatter(v => v ? \"Enabled\" : \"Disabled\");\n\nsetting.Property(\"HiddenProp\")\n    .HasOutputFormatter((entity, val) => $\"HiddenProp_{entity.PKID}\");\n\nsetting.Property(_ => _.PKID).Ignored();\nsetting.Property(_ => _.UpdatedBy).Ignored();\nsetting.Property(_ => _.UpdatedTime).Ignored();\n```\n\nTest Code:\n\n``` csharp\nvar entities = new List<TestEntity>()\n{\n    new TestEntity()\n    {\n        PKID = 1,\n        SettingId = Guid.NewGuid(),\n        SettingName = \"Setting1\",\n        SettingValue = \"Value1\",\n        DisplayName = \"ddd1\"\n    },\n    new TestEntity()\n    {\n        PKID=2,\n        SettingId = Guid.NewGuid(),\n        SettingName = \"Setting2\",\n        SettingValue = \"Value2\",\n        Enabled = true\n    },\n};\nvar path = $@\"{tempDirPath}\\test.xlsx\";\nentities.ToExcelFile(path);\nvar entitiesT0 = ExcelHelper.ToEntityList<TestEntity>(path);\n```\n\nExport Result:\n\n![Export Result](../images/489462-20200104112133779-1180097402.png)\n\n\nImport Result:\n\n![Import Result 1](../images/489462-20200104112017420-1450911242.png)\n\n![Import Result 2](../images/489462-20200104112025927-873408781.png)\n"
  },
  {
    "path": "docs/articles/en/MultiSheets.md",
    "content": "# Multi-Sheet Export\n\n## Introduction\n\nSometimes 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:\n\n## Sample\n\n```c#\nvar collection1 = new[]\n{\n    new TestEntity1() { Id = 1, Title = \"test1\" },\n    new TestEntity1() { Id = 2, Title = \"test2\" }\n};\nvar collection2 = new[]\n{\n    new TestEntity2() { Id = 1, Title = \"test1\", Description = \"description\"},\n    new TestEntity2() { Id = 2, Title = \"test2\" }\n};\n// Prepare a workbook\nvar workbook = ExcelHelper.PrepareWorkbook(ExcelFormat.Xlsx);\n// Import collection1 to the first sheet\nworkbook.ImportData(collection1);\n// Import collection2 to the second sheet\nworkbook.ImportData(collection2, 1);\n// Export workbook to local file\nworkbook.WriteToFile(\"multi-sheets.xlsx\");\n```\n\nIf you need to customize configurations, it works the same as before - you can use attributes or fluent API:\n\n```c#\n[Sheet(SheetName = \"TestSheet\", SheetIndex = 0)]\nfile sealed class TestEntity1\n{\n    [Column(\"ID\", Index = 0)]\n    public int Id { get; set; }\n    public string Title { get; set; } = string.Empty;\n}\n\nfile sealed class TestEntity2\n{\n    public int Id { get; set; }\n    public string Title { get; set; } = string.Empty;\n    public string Description { get; set; }\n}\n```\n\nFluent API configuration:\n\n```c#\nvar settings = FluentSettings.For<TestEntity2>();\nsettings.HasSheetSetting(sheet => sheet.SheetName = \"TestEntity2\", 1);\nsettings.Property(x => x.Id)\n    .HasColumnIndex(0)\n    .HasColumnOutputFormatter(v => v.ToString(\"#0000\"))\n    ;\nsettings.Property(x => x.Title)\n    .HasColumnIndex(1)\n    ;\nsettings.Property(x => x.Description)\n    .HasColumnIndex(2)\n    ;\n```\n\nExport results:\n\n![Sheet 0](../images/image-20241029231320957.png)\n\n![Sheet 1](../images/image-20241029231519274.png)\n\n## References\n\n- <https://github.com/WeihanLi/SamplesInPractice/blob/main/NPOISample/MultiSheetsSample.cs>\n- <https://github.com/WeihanLi/WeihanLi.Npoi/issues/157>\n"
  },
  {
    "path": "docs/articles/en/ShadowProperty.md",
    "content": "# WeihanLi.Npoi Now Supports `ShadowProperty`\n\n## Introduction\n\nIn 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`.\n\nWhen 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.\n\nTherefore, 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.\n\n## Usage Example\n\nHere's a simple usage example (from a user-submitted issue: <https://github.com/WeihanLi/WeihanLi.Npoi/issues/51>)\n\n``` csharp\nusing System;\nusing System.Collections.Generic;\nusing System.IO;\nusing WeihanLi.Npoi;\n\nnamespace NpoiTest\n{\n    public class Program\n    {\n        public static void Main(string[] args)\n        {\n            var settings = FluentSettings.For<TestEntity>();\n            settings.Property(x => x.Name)\n                .HasColumnIndex(0);\n            // settings.Property(x => x.UserFields)\n            //     .HasOutputFormatter((entity, value) => $\"{value[0].Value},{value[2].Value}\")\n            //     .HasColumnTitle(\"Name,Employee ID\")\n            //     .HasColumnIndex(1);\n            settings.Property(x=>x.UserFields).Ignored();\n            settings.Property(\"Employee ID\")\n                .HasOutputFormatter((entity,val)=> $\"{entity.UserFields[2].Value}\")\n                 ;\n            settings.Property(\"Department\")\n                .HasOutputFormatter((entity,val)=> $\"{entity.UserFields[1].Value}\")\n                 ;\n\n            var data = new List<TestEntity>()\n            {\n                new TestEntity()\n                {\n                    Name = \"xiaoming\",\n                    TotalScore = 100,\n                    UserFields = new UserField[]\n                    {\n                        new UserField()\n                        {\n                            Name = \"Name\",\n                            Value = \"xaioming\",\n                        },\n                        new UserField()\n                        {\n                            Name = \"Department\",\n                            Value = \"1212\"\n                        },\n                        new UserField()\n                        {\n                            Name = \"Employee ID\",\n                            Value = \"121213131\"\n                        },\n                    }\n                }\n            };\n            data.ToExcelFile($@\"{Directory.GetCurrentDirectory()}\\output.xls\");\n            Console.WriteLine(\"complete.\");\n        }\n\n        private class TestEntity\n        {\n            public string Name { get; set; }\n\n            public UserField[] UserFields { get; set; }\n\n            public int TotalScore { get; set; }\n        }\n\n        private class UserField\n        {\n            public string Fid { get; set; }\n            public string Name { get; set; }\n            public string Value { get; set; }\n        }\n    }\n}\n```\n\nExport result:\n\n![Shadow Property Example](../images/489462-20191213084226066-1767559517.png)\n\nAs 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.\n"
  },
  {
    "path": "docs/articles/en/TemplateExport.md",
    "content": "# Export Excel Based on Template\n\n## Introduction\n\nThe 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.\n\n## Usage Example\n\n### Example Template\n\n![Template Example](../images/489462-20200128142956273-1478084552.png)\n\nThe template can have three types of data:\n\n- Global: Parameters that can be specified during export as global parameters. Default format: `$(Global:PropName)`\n- Header: Display names for configured properties. Default is the property name. Default format: `$(Header:PropName)`\n- Data: Property values of the corresponding data. Default format: `$(Data:PropName)`\n\n\nDefault template parameter format (customizable through `TemplateHelper.ConfigureTemplateOptions` method since version 1.8.2):\n\n- Global parameter: `$(Global:{0})`\n- Header parameter: `$(Header:{0})`\n- Data parameter: `$(Data:{0})`\n- Data Begin: `<Data>`\n- Data End: `</Data>`\n\n\n\nTemplate specifications:\n\nThe 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.\n\n\n\n### Example Code\n\nExample configuration:\n\n``` csharp\nvar setting = FluentSettings.For<TestEntity>();\n// ExcelSetting\nsetting.HasAuthor(\"WeihanLi\")\n    .HasTitle(\"WeihanLi.Npoi test\")\n    .HasDescription(\"WeihanLi.Npoi test\")\n    .HasSubject(\"WeihanLi.Npoi test\");\n\nsetting.HasSheetConfiguration(0, \"SystemSettingsList\", 1, true);\n\nsetting.Property(_ => _.SettingId)\n    .HasColumnIndex(0);\n\nsetting.Property(_ => _.SettingName)\n    .HasColumnTitle(\"SettingName\")\n    .HasColumnIndex(1);\n\nsetting.Property(_ => _.DisplayName)\n    .HasOutputFormatter((entity, displayName) => $\"AAA_{entity.SettingName}_{displayName}\")\n    .HasInputFormatter((entity, originVal) => originVal.Split(new[] { '_' })[2])\n    .HasColumnTitle(\"DisplayName\")\n    .HasColumnIndex(2);\n\nsetting.Property(_ => _.SettingValue)\n    .HasColumnTitle(\"SettingValue\")\n    .HasColumnIndex(3);\n\nsetting.Property(x => x.Enabled)\n    .HasColumnInputFormatter(val => \"Enabled\".Equals(val))\n    .HasColumnOutputFormatter(v => v ? \"Enabled\" : \"Disabled\");\n\nsetting.Property(\"HiddenProp\")\n    .HasOutputFormatter((entity, val) => $\"HiddenProp_{entity.PKID}\");\n\nsetting.Property(_ => _.PKID).Ignored();\nsetting.Property(_ => _.UpdatedBy).Ignored();\nsetting.Property(_ => _.UpdatedTime).Ignored();\n```\n\nTemplate-based export example code:\n\n``` csharp\nvar entities = new List<TestEntity>()\n{\n    new TestEntity()\n    {\n        PKID = 1,\n        SettingId = Guid.NewGuid(),\n        SettingName = \"Setting1\",\n        SettingValue = \"Value1\",\n        DisplayName = \"ddd1\"\n    },\n    new TestEntity()\n    {\n        PKID=2,\n        SettingId = Guid.NewGuid(),\n        SettingName = \"Setting2\",\n        SettingValue = \"Value2\",\n        Enabled = true\n    },\n};\nvar csvFilePath = $@\"{tempDirPath}\\test.csv\";\nentities.ToExcelFileByTemplate(\n    Path.Combine(ApplicationHelper.AppRoot, \"Templates\", \"testTemplate.xlsx\"),\n    ApplicationHelper.MapPath(\"templateTestEntities.xlsx\"),\n    extraData: new\n    {\n        Author = \"WeihanLi\",\n        Title = \"Export Result\"\n    }\n);\n```\n\n### Export Result\n\n![Export Result](../images/489462-20200128143038865-1452547986.png)\n\n\n## More\n\nFor convenience, several extension methods have been added:\n\n``` csharp\npublic static int ToExcelFileByTemplate<TEntity>([NotNull]this IEnumerable<TEntity> entities, string templatePath, string excelPath, int sheetIndex = 0, object extraData = null);\n\npublic static int ToExcelFileByTemplate<TEntity>([NotNull]this IEnumerable<TEntity> entities, byte[] templateBytes, string excelPath, ExcelFormat excelFormat = ExcelFormat.Xls, int sheetIndex = 0, object extraData = null);\n\npublic static int ToExcelFileByTemplate<TEntity>([NotNull]this IEnumerable<TEntity> entities, IWorkbook templateWorkbook, string excelPath, int sheetIndex = 0, object extraData = null);\n\npublic static byte[] ToExcelBytesByTemplate<TEntity>([NotNull]this IEnumerable<TEntity> entities, string templatePath, int sheetIndex = 0, object extraData = null);\n\npublic static byte[] ToExcelBytesByTemplate<TEntity>([NotNull]this IEnumerable<TEntity> entities, byte[] templateBytes, ExcelFormat excelFormat = ExcelFormat.Xls, int sheetIndex = 0, object extraData = null);\n\npublic static byte[] ToExcelBytesByTemplate<TEntity>([NotNull]this IEnumerable<TEntity> entities, Stream templateStream, ExcelFormat excelFormat = ExcelFormat.Xls, int sheetIndex = 0, object extraData = null);\n\npublic static byte[] ToExcelBytesByTemplate<TEntity>([NotNull]this IEnumerable<TEntity> entities, IWorkbook templateWorkbook, int sheetIndex = 0, object extraData = null);\n\npublic static byte[] ToExcelBytesByTemplate<TEntity>([NotNull]this IEnumerable<TEntity> entities, ISheet templateSheet, object extraData = null);\n\n```\n\n\n## References\n\n- <https://github.com/WeihanLi/WeihanLi.Npoi>\n- <https://github.com/WeihanLi/WeihanLi.Npoi/blob/917e8fb798e9cbae52d121a7d593e37639870911/samples/DotNetCoreSample/Program.cs#L94>\n"
  },
  {
    "path": "docs/articles/intro.md",
    "content": "# WeihanLi.Npoi\n\n## Intro\n\n`WeihanLi.Npoi` is an extension for excel/csv import/export based on NPOI\n\n## Recommend Articles\n\nEnglish:\n\n- [Getting Started](./en/GetStarted.md)\n- [InputOutputFormatter Usage](./en/InputOutputFormatter.md)\n- [ShadowProperty Usage](./en/ShadowProperty.md)\n- [Template Export](./en/TemplateExport.md)\n- [Multi-Sheet Export](./en/MultiSheets.md)\n- [Customize Styles](./en/CustomizeStyle.md)\n\n中文：\n\n- [基本示例](./zh/GetStarted.md)\n- [InputOutputFormatter 使用](./zh/InputOutputFormatter.md)\n- [ShadowProperty 使用](./zh/ShadowProperty.md)\n- [根据模板导出](./zh/TemplateExport.md)\n- [多 sheet 导出](./zh/MultiSheets.md)\n- [自定义样式](./zh/CustomizeStyle.md)\n\n## More\n\nif you wanna add your articles here, welcome pr\n"
  },
  {
    "path": "docs/articles/toc.yml",
    "content": "- name: Introduction\n  href: intro.md\n- name: English Articles\n  items:\n    - name: Getting Started\n      href: en/GetStarted.md\n    - name: InputOutputFormatter\n      href: en/InputOutputFormatter.md\n    - name: ShadowProperty\n      href: en/ShadowProperty.md\n    - name: Template Export\n      href: en/TemplateExport.md\n    - name: Multi-Sheet Export\n      href: en/MultiSheets.md\n    - name: Customize Styles\n      href: en/CustomizeStyle.md\n- name: 中文文章\n  items:\n    - name: GetStarted\n      href: zh/GetStarted.md\n    - name: InputOutputFormatter\n      href: zh/InputOutputFormatter.md\n    - name: ShadowProperty\n      href: zh/ShadowProperty.md\n    - name: TemplateExport\n      href: zh/TemplateExport.md\n    - name: MultiSheets\n      href: zh/MultiSheets.md\n    - name: 自定义样式\n      href: zh/CustomizeStyle.md\n"
  },
  {
    "path": "docs/articles/zh/CustomizeStyle.md",
    "content": "# 自定义 Excel 样式\n\n## 简介\n\nWeihanLi.Npoi 通过 sheet 配置中的 `RowAction` 和 `CellAction` 回调提供了强大的样式定制功能。你可以自定义字体、颜色、对齐方式、边框，并添加数据验证，以创建专业外观的 Excel 文件。\n\n## 自动列宽\n\n最简单的样式功能是启用自动列宽调整：\n\n```csharp\nvar settings = FluentSettings.For<TestEntity>();\nsettings.HasSheetSetting(config =>\n{\n    config.AutoColumnWidthEnabled = true;\n});\n```\n\n这会根据内容自动调整列宽，使你的电子表格更易读，无需手动调整宽度。\n\n## 样式化标题行\n\n使用 `RowAction` 自定义行的外观。一个常见的用例是样式化标题行：\n\n```csharp\nsettings.HasSheetSetting(config =>\n{\n    config.StartRowIndex = 1;\n    config.SheetName = \"StyledSheet\";\n    config.AutoColumnWidthEnabled = true;\n\n    config.RowAction = row =>\n    {\n        if (row.RowNum == 0) // 标题行\n        {\n            // 创建单元格样式\n            var style = row.Sheet.Workbook.CreateCellStyle();\n            style.Alignment = HorizontalAlignment.Center;\n            \n            // 创建和配置字体\n            var font = row.Sheet.Workbook.CreateFont();\n            font.FontName = \"Arial\";\n            font.IsBold = true;\n            font.FontHeight = 220; // 11pt (字体高度以 1/20 磅为单位)\n            font.Color = IndexedColors.White.Index;\n            \n            style.SetFont(font);\n            \n            // 设置背景颜色\n            style.FillForegroundColor = IndexedColors.DarkBlue.Index;\n            style.FillPattern = FillPattern.SolidForeground;\n            \n            // 将样式应用于行中的所有单元格\n            row.Cells.ForEach(c => c.CellStyle = style);\n        }\n    };\n});\n```\n\n### 字体属性\n\n可用的字体属性包括：\n\n- `FontName`: 字体族（例如：\"宋体\"、\"微软雅黑\"、\"Arial\"）\n- `FontHeight`: 字体大小，以 1/20 磅为单位（例如：200 = 10pt, 220 = 11pt）\n- `IsBold`: 粗体文本\n- `IsItalic`: 斜体文本\n- `IsStrikeout`: 删除线文本\n- `Underline`: 文本下划线样式\n- `Color`: 使用 `IndexedColors` 设置字体颜色\n\n### 单元格样式属性\n\n主要的单元格样式属性：\n\n- `Alignment`: 水平对齐（`Left`、`Center`、`Right`、`Justify`）\n- `VerticalAlignment`: 垂直对齐（`Top`、`Center`、`Bottom`）\n- `FillForegroundColor`: 背景颜色\n- `FillPattern`: 填充模式（通常使用 `SolidForeground` 实现纯色）\n- `BorderTop`、`BorderBottom`、`BorderLeft`、`BorderRight`: 边框样式\n- `WrapText`: 启用文本换行\n\n## 样式化单个单元格\n\n使用 `CellAction` 根据单元格位置或内容自定义单个单元格：\n\n```csharp\nsettings.HasSheetSetting(config =>\n{\n    config.CellAction = cell =>\n    {\n        // 样式化特定列\n        if (cell.ColumnIndex == 0) // 第一列\n        {\n            var style = cell.Sheet.Workbook.CreateCellStyle();\n            var font = cell.Sheet.Workbook.CreateFont();\n            font.IsBold = true;\n            style.SetFont(font);\n            cell.CellStyle = style;\n        }\n        \n        // 基于内容的条件样式\n        if (cell.RowIndex > 0 && cell.ColumnIndex == 3) // 数据行，第 4 列\n        {\n            if (cell.NumericCellValue < 0) // 负数显示为红色\n            {\n                var style = cell.Sheet.Workbook.CreateCellStyle();\n                var font = cell.Sheet.Workbook.CreateFont();\n                font.Color = IndexedColors.Red.Index;\n                style.SetFont(font);\n                cell.CellStyle = style;\n            }\n        }\n    };\n});\n```\n\n## 数据验证\n\n为单元格添加下拉列表和其他验证规则：\n\n```csharp\nsettings.HasSheetSetting(config =>\n{\n    config.CellAction = cell =>\n    {\n        // 为匹配\"状态\"的标题单元格添加下拉验证\n        if (cell.RowIndex == 0 && cell.StringCellValue == \"状态\")\n        {\n            var validationHelper = cell.Sheet.GetDataValidationHelper();\n            \n            // 定义允许的值\n            var statusValues = new[] { \"活跃\", \"停用\", \"待定\" };\n            var constraint = validationHelper.CreateExplicitListConstraint(statusValues);\n            \n            // 将验证应用于数据行（第 1-100 行，当前列）\n            var addressList = new CellRangeAddressList(1, 100, cell.ColumnIndex, cell.ColumnIndex);\n            var validation = validationHelper.CreateValidation(constraint, addressList);\n            \n            validation.ShowErrorBox = true;\n            validation.CreateErrorBox(\"状态无效\", \"请从下拉列表中选择\");\n            validation.ShowPromptBox = true;\n            validation.CreatePromptBox(\"状态选择\", \"从列表中选择一个状态\");\n            \n            cell.Sheet.AddValidationData(validation);\n        }\n    };\n});\n```\n\n### 验证类型\n\n可用的不同验证类型：\n\n```csharp\n// 列表验证（下拉列表）\nvar constraint = validationHelper.CreateExplicitListConstraint(new[] { \"选项1\", \"选项2\" });\n\n// 整数验证\nvar intConstraint = validationHelper.CreateIntegerConstraint(\n    OperatorType.Between, \"1\", \"100\");\n\n// 小数验证\nvar decimalConstraint = validationHelper.CreateDecimalConstraint(\n    OperatorType.GreaterThan, \"0\", null);\n\n// 日期验证\nvar dateConstraint = validationHelper.CreateDateConstraint(\n    OperatorType.Between, \"2024-01-01\", \"2024-12-31\", \"yyyy-MM-dd\");\n\n// 文本长度验证\nvar textConstraint = validationHelper.CreateTextLengthConstraint(\n    OperatorType.LessThan, \"100\", null);\n```\n\n## 完整示例\n\n这是一个结合多种样式功能的综合示例：\n\n```csharp\npublic class StyledEntityProfile : IMappingProfile<StyledEntity>\n{\n    public void Configure(IExcelConfiguration<StyledEntity> configuration)\n    {\n        configuration.HasAuthor(\"您的名字\")\n            .HasTitle(\"样式化报表\")\n            .HasDescription(\"专业样式的 Excel 报表\");\n\n        configuration.HasSheetSetting(config =>\n        {\n            config.SheetName = \"报表\";\n            config.StartRowIndex = 1;\n            config.AutoColumnWidthEnabled = true;\n\n            // 样式化标题行\n            config.RowAction = row =>\n            {\n                if (row.RowNum == 0)\n                {\n                    var headerStyle = row.Sheet.Workbook.CreateCellStyle();\n                    headerStyle.Alignment = HorizontalAlignment.Center;\n                    headerStyle.VerticalAlignment = VerticalAlignment.Center;\n                    headerStyle.FillForegroundColor = IndexedColors.Grey25Percent.Index;\n                    headerStyle.FillPattern = FillPattern.SolidForeground;\n                    \n                    var headerFont = row.Sheet.Workbook.CreateFont();\n                    headerFont.FontName = \"微软雅黑\";\n                    headerFont.IsBold = true;\n                    headerFont.FontHeight = 240; // 12pt\n                    headerStyle.SetFont(headerFont);\n                    \n                    // 添加边框\n                    headerStyle.BorderBottom = BorderStyle.Thin;\n                    headerStyle.BorderTop = BorderStyle.Thin;\n                    headerStyle.BorderLeft = BorderStyle.Thin;\n                    headerStyle.BorderRight = BorderStyle.Thin;\n                    \n                    row.Cells.ForEach(c => c.CellStyle = headerStyle);\n                }\n            };\n\n            // 添加验证和条件格式\n            config.CellAction = cell =>\n            {\n                // 为状态列添加验证\n                if (cell.RowIndex == 0 && cell.StringCellValue == \"状态\")\n                {\n                    var validationHelper = cell.Sheet.GetDataValidationHelper();\n                    var statusList = new[] { \"已批准\", \"待定\", \"已拒绝\" };\n                    var constraint = validationHelper.CreateExplicitListConstraint(statusList);\n                    var addressList = new CellRangeAddressList(1, 1000, cell.ColumnIndex, cell.ColumnIndex);\n                    var validation = validationHelper.CreateValidation(constraint, addressList);\n                    validation.ShowErrorBox = true;\n                    cell.Sheet.AddValidationData(validation);\n                }\n                \n                // 用红色突出显示负数金额\n                if (cell.RowIndex > 0 && cell.ColumnIndex == 2) // 金额列\n                {\n                    try\n                    {\n                        if (cell.NumericCellValue < 0)\n                        {\n                            var redStyle = cell.Sheet.Workbook.CreateCellStyle();\n                            var redFont = cell.Sheet.Workbook.CreateFont();\n                            redFont.Color = IndexedColors.Red.Index;\n                            redFont.IsBold = true;\n                            redStyle.SetFont(redFont);\n                            cell.CellStyle = redStyle;\n                        }\n                    }\n                    catch { } // 如果不是数值单元格则跳过\n                }\n            };\n        });\n\n        // 配置属性\n        configuration.Property(x => x.Id).HasColumnIndex(0);\n        configuration.Property(x => x.Name).HasColumnIndex(1);\n        configuration.Property(x => x.Amount).HasColumnIndex(2);\n        configuration.Property(x => x.Status).HasColumnIndex(3);\n        configuration.Property(x => x.Date)\n            .HasColumnIndex(4)\n            .HasColumnFormatter(\"yyyy-MM-dd\");\n    }\n}\n```\n\n## 最佳实践\n\n1. **重用样式**：创建样式一次并重用，而不是为每个单元格创建新样式，以提高性能并减小文件大小。\n\n2. **性能考虑**：在处理大型数据集时应用样式时要注意性能。考虑仅样式化标题行或特定列。\n\n3. **颜色一致性**：使用 `IndexedColors` 以在不同的 Excel 版本之间保持一致的颜色。\n\n4. **字体大小**：记住字体高度以 1/20 磅为单位（将磅值乘以 20）。\n\n5. **验证范围**：为数据验证设置适当的范围以覆盖预期的数据行。\n\n## 参考\n\n- [示例实现](https://github.com/WeihanLi/WeihanLi.Npoi/blob/dev/samples/DotNetCoreSample/Program.cs)\n- [NPOI 文档](http://poi.apache.org/)\n"
  },
  {
    "path": "docs/articles/zh/GetStarted.md",
    "content": "# `WeihanLi.Npoi` 基础示例\n\n## Intro\n\n`WeihanLi.Npoi` 是基于 NPOI 扩展的 Excel 导入导出库，并提供了很多实用的扩展方法，也支持 CSV 的导入导出，\n\n- 将 excel/csv 数据导入到 `DataTable` 或 `List<TEntity>`\n- `IEnumerable<TEntity>` 或 `DataTable` 导出到 Excel，可以导出成 excel 文件或字节数组或者一个流\n- `IEnumerable<TEntity>` 或 `DataTable` 导出到 csv 文件或者 csv 字节数组\n- 通过 `Attribute` 或者 `FluentAPI`(借鉴了 [FluentExcel](https://github.com/Arch/FluentExcel/) 项目)\n\n## BasicSample\n\n``` csharp\ninternal class BaseModel\n{\n    public int Id { get; set; }\n}\n\ninternal class Notice : BaseModel\n{\n    public string Title { get; set; }\n\n    public string Content { get; set; }\n\n    public DateTime PublishedAt { get; set; }\n\n    public string Publisher { get; set; }\n}\n```\n\n基本的导入导出：\n\n``` csharp\n// entities excel import/export\n[Theory]\n[InlineData(ExcelFormat.Xls)]\n[InlineData(ExcelFormat.Xlsx)]\npublic void BasicImportExportTest(ExcelFormat excelFormat)\n{\n    var list = new List<Notice>();\n    for (var i = 0; i < 10; i++)\n    {\n        list.Add(new Notice()\n        {\n            Id = i + 1,\n            Content = $\"content_{i}\",\n            Title = $\"title_{i}\",\n            PublishedAt = DateTime.UtcNow.AddDays(-i),\n            Publisher = $\"publisher_{i}\"\n        });\n    }\n    list.Add(new Notice() { Title = \"nnnn\" });\n    list.Add(null);\n    var excelBytes = list.ToExcelBytes(excelFormat);\n\n    var importedList = ExcelHelper.ToEntityList<Notice>(excelBytes, excelFormat);\n    Assert.Equal(list.Count, importedList.Count);\n    for (var i = 0; i < list.Count; i++)\n    {\n        if (list[i] is null)\n        {\n            Assert.Null(importedList[i]);\n        }\n        else\n        {\n            Assert.Equal(list[i].Id, importedList[i].Id);\n            Assert.Equal(list[i].Title, importedList[i].Title);\n            Assert.Equal(list[i].Content, importedList[i].Content);\n            Assert.Equal(list[i].Publisher, importedList[i].Publisher);\n            Assert.Equal(list[i].PublishedAt.ToTimeString(), importedList[i].PublishedAt.ToTimeString());\n        }\n    }\n}\n\n// DataTable Excel import/export\n[Theory]\n[InlineData(ExcelFormat.Xls)]\n[InlineData(ExcelFormat.Xlsx)]\npublic void DataTableImportExportTest(ExcelFormat excelFormat)\n{\n    var dt = new DataTable();\n    dt.Columns.AddRange(new[]\n    {\n        new DataColumn(\"Name\"),\n        new DataColumn(\"Age\"),\n        new DataColumn(\"Desc\"),\n    });\n    for (var i = 0; i < 10; i++)\n    {\n        var row = dt.NewRow();\n        row.ItemArray = new object[] { $\"Test_{i}\", i + 10, $\"Desc_{i}\" };\n        dt.Rows.Add(row);\n    }\n    //\n    var excelBytes = dt.ToExcelBytes(excelFormat);\n    var importedData = ExcelHelper.ToDataTable(excelBytes, excelFormat);\n    Assert.NotNull(importedData);\n    Assert.Equal(dt.Rows.Count, importedData.Rows.Count);\n    for (var i = 0; i < dt.Rows.Count; i++)\n    {\n        Assert.Equal(dt.Rows[i].ItemArray.Length, importedData.Rows[i].ItemArray.Length);\n        for (var j = 0; j < dt.Rows[i].ItemArray.Length; j++)\n        {\n            Assert.Equal(dt.Rows[i].ItemArray[j], importedData.Rows[i].ItemArray[j]);\n        }\n    }\n}\n\n// entities csv import/export\n[Fact]\npublic void BasicImportExportTest()\n{\n    var list = new List<Notice>();\n    for (var i = 0; i < 10; i++)\n    {\n        list.Add(new Notice()\n        {\n            Id = i + 1,\n            Content = $\"content_{i}\",\n            Title = $\"title_{i}\",\n            PublishedAt = DateTime.UtcNow.AddDays(-i),\n            Publisher = $\"publisher_{i}\"\n        });\n    }\n    list.Add(new Notice()\n    {\n        Id = 11,\n        Content = $\"content\",\n        Title = $\"title\",\n        PublishedAt = DateTime.UtcNow.AddDays(1),\n    });\n    var csvBytes = list.ToCsvBytes();\n    var importedList = CsvHelper.ToEntityList<Notice>(csvBytes);\n    Assert.Equal(list.Count, importedList.Count);\n    for (var i = 0; i < list.Count; i++)\n    {\n        Assert.Equal(list[i].Id, importedList[i].Id);\n        Assert.Equal(list[i].Title ?? \"\", importedList[i].Title);\n        Assert.Equal(list[i].Content ?? \"\", importedList[i].Content);\n        Assert.Equal(list[i].Publisher ?? \"\", importedList[i].Publisher);\n        Assert.Equal(list[i].PublishedAt.ToTimeString(), importedList[i].PublishedAt.ToTimeString());\n    }\n}\n// DataTable csv import/export\n[Fact]\npublic void DataTableImportExportTest()\n{\n    var dt = new DataTable();\n    dt.Columns.AddRange(new[]\n    {\n        new DataColumn(\"Name\"),\n        new DataColumn(\"Age\"),\n        new DataColumn(\"Desc\"),\n    });\n    for (var i = 0; i < 10; i++)\n    {\n        var row = dt.NewRow();\n        row.ItemArray = new object[] { $\"Test_{i}\", i + 10, $\"Desc_{i}\" };\n        dt.Rows.Add(row);\n    }\n    //\n    var csvBytes = dt.ToCsvBytes();\n    var importedData = CsvHelper.ToDataTable(csvBytes);\n    Assert.NotNull(importedData);\n    Assert.Equal(dt.Rows.Count, importedData.Rows.Count);\n    for (var i = 0; i < dt.Rows.Count; i++)\n    {\n        Assert.Equal(dt.Rows[i].ItemArray.Length, importedData.Rows[i].ItemArray.Length);\n        for (var j = 0; j < dt.Rows[i].ItemArray.Length; j++)\n        {\n            Assert.Equal(dt.Rows[i].ItemArray[j], importedData.Rows[i].ItemArray[j]);\n        }\n    }\n}\n```\n\n## 自定义映射关系，配置\n\n使用 Attribute 配置：\n\n``` csharp\ninternal class Model\n{\n    [Column(\"酒店编号\", Index = 0)]\n    public string HotelId { get; set; }\n\n    [Column(\"订单号\", Index = 1)]\n    public string OrderNo { get; set; }\n\n    [Column(\"酒店名称\", Index = 2)]\n    public string HotelName { get; set; }\n\n    [Column(\"客户名称\", Index = 3)]\n    public string CustomerName { get; set; }\n\n    [Column(nameof(房型名称), Index = 4)]\n    public string 房型名称 { get; set; }\n\n    [Column(nameof(入住日期), Index = 5, Formatter = \"yyyy/M/d\")]\n    public DateTime 入住日期 { get; set; }\n\n    [Column(nameof(离店日期), Index = 6, Formatter = \"yyyy/M/d\")]\n    public DateTime 离店日期 { get; set; }\n\n    [Column(nameof(间夜), Index = 7)]\n    public int 间夜 { get; set; }\n\n    [Column(nameof(支付类型), Index = 8)]\n    public string 支付类型 { get; set; }\n\n    [Column(nameof(订单金额), Index = 9)]\n    public decimal 订单金额 { get; set; }\n\n    [Column(nameof(佣金率), Index = 10)]\n    public decimal 佣金率 { get; set; }\n\n    [Column(nameof(服务费), Index = 11)]\n    public decimal 服务费 { get; set; }\n}\n\n[Sheet(SheetIndex = 0, SheetName = \"TestSheet\", AutoColumnWidthEnabled = true)]\ninternal class TestEntity2\n{\n    [Column(Index = 0)]\n    public int Id { get; set; }\n\n    [Column(Index = 1)]\n    public string Title { get; set; }\n\n    [Column(Index = 2, Width = 50)]\n    public string Description { get; set; }\n\n    [Column(Index = 3, Width = 20)]\n    public string Extra { get; set; } = \"{}\";\n}\n```\n\n使用 FluentAPI 配置（推荐，更灵活）\n\n``` csharp\nvar setting = FluentSettings.For<TestEntity>();\n// ExcelSetting\nsetting.HasAuthor(\"WeihanLi\")\n    .HasTitle(\"WeihanLi.Npoi test\")\n    .HasDescription(\"WeihanLi.Npoi test\")\n    .HasSubject(\"WeihanLi.Npoi test\");\n\nsetting.HasSheetConfiguration(0, \"SystemSettingsList\", 1, true); // sheet 配置\n\n// setting\n//     .HasFilter(0, 1) //在列上设置筛选\n//     .HasFreezePane(0, 1, 2, 1); // 设置冻结区域\n\nsetting.Property(_ => _.SettingId)\n    .HasColumnIndex(0);\n\nsetting.Property(_ => _.SettingName)\n    .HasColumnTitle(\"SettingName\")\n    .HasColumnIndex(1);\n\nsetting.Property(_ => _.DisplayName)\n    .HasOutputFormatter((entity, displayName) => $\"AAA_{entity.SettingName}_{displayName}\")\n    .HasInputFormatter((entity, originVal) => originVal.Split(new[] { '_' })[2])\n    .HasColumnTitle(\"DisplayName\")\n    .HasColumnIndex(2);\n\nsetting.Property(_ => _.SettingValue)\n    .HasColumnTitle(\"SettingValue\")\n    .HasColumnIndex(3);\n\nsetting.Property(_ => _.CreatedTime)\n    .HasColumnTitle(\"CreatedTime\")\n    .HasColumnIndex(4)\n    .HasColumnWidth(10) // 设置列宽\n    .HasColumnFormatter(\"yyyy-MM-dd HH:mm:ss\");\n\nsetting.Property(_ => _.CreatedBy)\n    .HasColumnInputFormatter(x => x += \"_test\")\n    .HasColumnIndex(4)\n    .HasColumnTitle(\"CreatedBy\");\n\nsetting.Property(x => x.Enabled)\n    .HasColumnInputFormatter(val => \"启用\".Equals(val))\n    .HasColumnOutputFormatter(v => v ? \"启用\" : \"禁用\");\n\nsetting.Property(\"ShadowProperty\")\n    .HasOutputFormatter((entity, val) => $\"HiddenProp_{entity.PKID}\");\n\nsetting.Property(_ => _.PKID).Ignored(); // ignore column\n```\n"
  },
  {
    "path": "docs/articles/zh/InputOutputFormatter.md",
    "content": "# InputOutputFormatter 介绍\n\n## Intro\n\nWeihanLi.Npoi 引入了 `OutputFormatter`/`InputFormatter`/`ColumnInputFormatter`/`ColumnOutputFormatter`，极大程度上增强了导入导出的灵活性，只支持通过 FluentAPI 配置，来看下面的示例\n\n## InputFormatter/OutputFormatter\n\n示例 Model:\n\n``` csharp\ninternal abstract class BaseEntity\n{\n    public int PKID { get; set; }\n}\n\ninternal class TestEntity : BaseEntity\n{\n    public Guid SettingId { get; set; }\n\n    public string SettingName { get; set; }\n\n    public string DisplayName { get; set; }\n    public string SettingValue { get; set; }\n\n    public string CreatedBy { get; set; } = \"liweihan\";\n\n    public DateTime CreatedTime { get; set; } = DateTime.Now;\n\n    public string UpdatedBy { get; set; }\n\n    public DateTime UpdatedTime { get; set; }\n\n    public bool Enabled { get; set; }\n}\n```\n\n示例配置：\n\n``` csharp\nvar setting = FluentSettings.For<TestEntity>();\n// ExcelSetting\nsetting.HasAuthor(\"WeihanLi\")\n    .HasTitle(\"WeihanLi.Npoi test\")\n    .HasDescription(\"WeihanLi.Npoi test\")\n    .HasSubject(\"WeihanLi.Npoi test\");\n\nsetting.HasSheetConfiguration(0, \"SystemSettingsList\", 1, true);\n\nsetting.Property(_ => _.SettingId)\n    .HasColumnIndex(0);\n\nsetting.Property(_ => _.SettingName)\n    .HasColumnTitle(\"SettingName\")\n    .HasColumnIndex(1);\n\nsetting.Property(_ => _.DisplayName)\n    .HasOutputFormatter((entity, displayName) => $\"AAA_{entity.SettingName}_{displayName}\")\n    .HasInputFormatter((entity, originVal) => originVal.Split(new[] { '_' })[2])\n    .HasColumnTitle(\"DisplayName\")\n    .HasColumnIndex(2);\n\nsetting.Property(_ => _.SettingValue)\n    .HasColumnTitle(\"SettingValue\")\n    .HasColumnIndex(3);\n\nsetting.Property(_ => _.CreatedTime)\n    .HasColumnTitle(\"CreatedTime\")\n    .HasColumnIndex(4)\n    .HasColumnWidth(10)\n    .HasColumnFormatter(\"yyyy-MM-dd HH:mm:ss\");\n\nsetting.Property(_ => _.CreatedBy)\n    .HasColumnInputFormatter(x => x += \"_test\")\n    .HasColumnIndex(4)\n    .HasColumnTitle(\"CreatedBy\");\n\nsetting.Property(x => x.Enabled)\n    .HasColumnInputFormatter(val => \"启用\".Equals(val))\n    .HasColumnOutputFormatter(v => v ? \"启用\" : \"禁用\");\n\nsetting.Property(\"HiddenProp\")\n    .HasOutputFormatter((entity, val) => $\"HiddenProp_{entity.PKID}\");\n\nsetting.Property(_ => _.PKID).Ignored();\nsetting.Property(_ => _.UpdatedBy).Ignored();\nsetting.Property(_ => _.UpdatedTime).Ignored();\n```\n\n测试代码：\n\n``` csharp\nvar entities = new List<TestEntity>()\n{\n    new TestEntity()\n    {\n        PKID = 1,\n        SettingId = Guid.NewGuid(),\n        SettingName = \"Setting1\",\n        SettingValue = \"Value1\",\n        DisplayName = \"ddd1\"\n    },\n    new TestEntity()\n    {\n        PKID=2,\n        SettingId = Guid.NewGuid(),\n        SettingName = \"Setting2\",\n        SettingValue = \"Value2\",\n        Enabled = true\n    },\n};\nvar path = $@\"{tempDirPath}\\test.xlsx\";\nentities.ToExcelFile(path);\nvar entitiesT0 = ExcelHelper.ToEntityList<TestEntity>(path);\n```\n\n导出结果：\n\n![](../images/489462-20200104112133779-1180097402.png)\n\n\n导入结果：\n\n![](../images/489462-20200104112017420-1450911242.png)\n\n![](../images/489462-20200104112025927-873408781.png)\n"
  },
  {
    "path": "docs/articles/zh/MultiSheets.md",
    "content": "# 多 sheet 导出\n\n## Intro\n\n有时我们可能会希望在一个 excel 里导出多个 sheet 导出多个集合的数据，可以参考下面的示例代码：\n\n## Sample\n\n```c#\nvar collection1 = new[]\n{\n    new TestEntity1() { Id = 1, Title = \"test1\" },\n    new TestEntity1() { Id = 2, Title = \"test2\" }\n};\nvar collection2 = new[]\n{\n    new TestEntity2() { Id = 1, Title = \"test1\", Description = \"description\"},\n    new TestEntity2() { Id = 2, Title = \"test2\" }\n};\n// 准备一个 workbook\nvar workbook = ExcelHelper.PrepareWorkbook(ExcelFormat.Xlsx);\n// 导入 collection1 到第一个 sheet\nworkbook.ImportData(collection1);\n// 导入 collection2 到第二个 sheet\nworkbook.ImportData(collection2, 1);\n// 导出 workbook 到本地文件\nworkbook.WriteToFile(\"multi-sheets.xlsx\");\n```\n\n如果需要自定义一些配置还是和之前是一样的，可以使用 attribute 的方式也可以使用 fluent API 的方式\n\n```c#\n[Sheet(SheetName = \"TestSheet\", SheetIndex = 0)]\nfile sealed class TestEntity1\n{\n    [Column(\"ID\", Index = 0)]\n    public int Id { get; set; }\n    public string Title { get; set; } = string.Empty;\n}\n\nfile sealed class TestEntity2\n{\n    public int Id { get; set; }\n    public string Title { get; set; } = string.Empty;\n    public string Description { get; set; }\n}\n```\n\nFluent API 配置如下：\n\n```c#\nvar settings = FluentSettings.For<TestEntity2>();\nsettings.HasSheetSetting(sheet => sheet.SheetName = \"TestEntity2\", 1);\nsettings.Property(x => x.Id)\n    .HasColumnIndex(0)\n    .HasColumnOutputFormatter(v => v.ToString(\"#0000\"))\n    ;\nsettings.Property(x => x.Title)\n    .HasColumnIndex(1)\n    ;\nsettings.Property(x => x.Description)\n    .HasColumnIndex(2)\n    ;\n```\n\n导出结果如下：\n\n![sheet0](../images/image-20241029231320957.png)\n\n![sheet1](../images/image-20241029231519274.png)\n\n## References\n\n- <https://github.com/WeihanLi/SamplesInPractice/blob/main/NPOISample/MultiSheetsSample.cs>\n- <https://github.com/WeihanLi/WeihanLi.Npoi/issues/157>\n"
  },
  {
    "path": "docs/articles/zh/ShadowProperty.md",
    "content": "# WeihanLi.Npoi 支持 `ShadowProperty` 了\n\n## Intro\n\n在 EF 里有个 `ShadowProperty` (阴影属性/影子属性)的概念，你可以通过 FluentAPI 的方式来定义一个不在 .NET model 里定义的属性，只能通过 EF 里的 `Change Tracker` 来操作这种属性。\n\n在导出 Excel 的时候，可能希望导出的列并不是都定义好在我们的 model 中的，有的可能只是想增加一列导出某个属性中的嵌套属性之中的某一个属性值，或者我就是单纯的想多定义一列，而这个时候可能 model 是别的地方写死的，不方便改。\n\n于是 `WeihanLi.Npoi` 从 1.6.0 版本开始支持 `ShadowProperty` ，将  EF 里的 `ShadowProperty` 引入到 excel 导出里，目前来说 `ShadowProperty` 是不可写的，读取的话也只是返回一个类型的默认值，不支持 `ChangeTracker`，不支持改。\n\n## 使用示例\n\n来看一个简单使用示例：(示例来源于网友提出的这个issue： <https://github.com/WeihanLi/WeihanLi.Npoi/issues/51>)\n\n``` csharp\nusing System;\nusing System.Collections.Generic;\nusing System.IO;\nusing WeihanLi.Npoi;\n\nnamespace NpoiTest\n{\n    public class Program\n    {\n        public static void Main(string[] args)\n        {\n            var settings = FluentSettings.For<TestEntity>();\n            settings.Property(x => x.Name)\n                .HasColumnIndex(0);\n            // settings.Property(x => x.UserFields)\n            //     .HasOutputFormatter((entity, value) => $\"{value[0].Value},{value[2].Value}\")\n            //     .HasColumnTitle(\"姓名,工号\")\n            //     .HasColumnIndex(1);\n            settings.Property(x=>x.UserFields).Ignored();\n            settings.Property(\"工号\")\n                .HasOutputFormatter((entity,val)=> $\"{entity.UserFields[2].Value}\")\n                 ;\n            settings.Property(\"部门\")\n                .HasOutputFormatter((entity,val)=> $\"{entity.UserFields[1].Value}\")\n                 ;\n\n            var data = new List<TestEntity>()\n            {\n                new TestEntity()\n                {\n                    Name = \"xiaoming\",\n                    TotalScore = 100,\n                    UserFields = new UserField[]\n                    {\n                        new UserField()\n                        {\n                            Name = \"姓名\",\n                            Value = \"xaioming\",\n                        },\n                        new UserField()\n                        {\n                            Name = \"部门\",\n                            Value = \"1212\"\n                        },\n                        new UserField()\n                        {\n                            Name = \"工号\",\n                            Value = \"121213131\"\n                        },\n                    }\n                }\n            };\n            data.ToExcelFile($@\"{Directory.GetCurrentDirectory()}\\output.xls\");\n            Console.WriteLine(\"complete.\");\n        }\n\n        private class TestEntity\n        {\n            public string Name { get; set; }\n\n            public UserField[] UserFields { get; set; }\n\n            public int TotalScore { get; set; }\n        }\n\n        private class UserField\n        {\n            public string Fid { get; set; }\n            public string Name { get; set; }\n            public string Value { get; set; }\n        }\n    }\n}\n```\n\n导出效果如下：\n\n![](../images/489462-20191213084226066-1767559517.png)\n\n可以看到，我们为导出的 Excel 增加在原本的 Model 里没有定义的两列，借助于此，我们可以更灵活的定制要导出的内容\n"
  },
  {
    "path": "docs/articles/zh/TemplateExport.md",
    "content": "# 根据模板导出Excel\n\n## Intro\n\n原来的导出方式比较适用于比较简单的导出，每一条数据在一行，数据列虽然自定义程度比较高，如果要一条数据对应多行就做不到了，于是就想支持根据模板导出，在 1.8.0 版本中引入了根据模板导出的功能\n\n## 使用示例\n\n### 示例模板\n\n![](../images/489462-20200128142956273-1478084552.png)\n\n模板规划的可以有三种数据：\n\n- Global：一个是导出的时候可以指定一些参数，作为 Global 参数，默认参数格式使用: `$(Global:PropName)` 的格式\n- Header：配置的对应属性的显示名称，默认是属性名称，默认参数格式：`$(Header:PropName)`\n- Data：对应数据的属性值，默认参数格式：`$(Data:PropName)`\n\n\n默认模板参数格式（从 1.8.2 版本开始支持通过 `TemplateHelper.ConfigureTemplateOptions` 方法来自定义）：\n\n- Global 参数：`$(Global:{0})`\n- Header 参数：`$(Header:{0})`\n- Data 参数：`$(Data:{0})`\n- Data Begin: `<Data>`\n- Data End: `</Data>`\n\n\n\n模板规范：\n\n模板需要通过 Data Begin 和 Data End 来配置数据模板的开始和结束以识别每一个数据对应的开始行和结束行\n\n\n\n### 示例代码\n\n示例配置\n\n``` csharp\nvar setting = FluentSettings.For<TestEntity>();\n// ExcelSetting\nsetting.HasAuthor(\"WeihanLi\")\n    .HasTitle(\"WeihanLi.Npoi test\")\n    .HasDescription(\"WeihanLi.Npoi test\")\n    .HasSubject(\"WeihanLi.Npoi test\");\n\nsetting.HasSheetConfiguration(0, \"SystemSettingsList\", 1, true);\n\nsetting.Property(_ => _.SettingId)\n    .HasColumnIndex(0);\n\nsetting.Property(_ => _.SettingName)\n    .HasColumnTitle(\"SettingName\")\n    .HasColumnIndex(1);\n\nsetting.Property(_ => _.DisplayName)\n    .HasOutputFormatter((entity, displayName) => $\"AAA_{entity.SettingName}_{displayName}\")\n    .HasInputFormatter((entity, originVal) => originVal.Split(new[] { '_' })[2])\n    .HasColumnTitle(\"DisplayName\")\n    .HasColumnIndex(2);\n\nsetting.Property(_ => _.SettingValue)\n    .HasColumnTitle(\"SettingValue\")\n    .HasColumnIndex(3);\n\nsetting.Property(x => x.Enabled)\n    .HasColumnInputFormatter(val => \"启用\".Equals(val))\n    .HasColumnOutputFormatter(v => v ? \"启用\" : \"禁用\");\n\nsetting.Property(\"HiddenProp\")\n    .HasOutputFormatter((entity, val) => $\"HiddenProp_{entity.PKID}\");\n\nsetting.Property(_ => _.PKID).Ignored();\nsetting.Property(_ => _.UpdatedBy).Ignored();\nsetting.Property(_ => _.UpdatedTime).Ignored();\n```\n\n根据模板导出示例代码：\n\n``` csharp\nvar entities = new List<TestEntity>()\n{\n    new TestEntity()\n    {\n        PKID = 1,\n        SettingId = Guid.NewGuid(),\n        SettingName = \"Setting1\",\n        SettingValue = \"Value1\",\n        DisplayName = \"ddd1\"\n    },\n    new TestEntity()\n    {\n        PKID=2,\n        SettingId = Guid.NewGuid(),\n        SettingName = \"Setting2\",\n        SettingValue = \"Value2\",\n        Enabled = true\n    },\n};\nvar csvFilePath = $@\"{tempDirPath}\\test.csv\";\nentities.ToExcelFileByTemplate(\n    Path.Combine(ApplicationHelper.AppRoot, \"Templates\", \"testTemplate.xlsx\"),\n    ApplicationHelper.MapPath(\"templateTestEntities.xlsx\"),\n    extraData: new\n    {\n        Author = \"WeihanLi\",\n        Title = \"导出结果\"\n    }\n);\n```\n\n### 导出结果\n\n![](../images/489462-20200128143038865-1452547986.png)\n\n\n## More\n\n为了方便使用，增加了一些方便的扩展方法：\n\n``` csharp\npublic static int ToExcelFileByTemplate<TEntity>([NotNull]this IEnumerable<TEntity> entities, string templatePath, string excelPath, int sheetIndex = 0, object extraData = null);\n\npublic static int ToExcelFileByTemplate<TEntity>([NotNull]this IEnumerable<TEntity> entities, byte[] templateBytes, string excelPath, ExcelFormat excelFormat = ExcelFormat.Xls, int sheetIndex = 0, object extraData = null);\n\npublic static int ToExcelFileByTemplate<TEntity>([NotNull]this IEnumerable<TEntity> entities, IWorkbook templateWorkbook, string excelPath, int sheetIndex = 0, object extraData = null);\n\npublic static byte[] ToExcelBytesByTemplate<TEntity>([NotNull]this IEnumerable<TEntity> entities, string templatePath, int sheetIndex = 0, object extraData = null);\n\npublic static byte[] ToExcelBytesByTemplate<TEntity>([NotNull]this IEnumerable<TEntity> entities, byte[] templateBytes, ExcelFormat excelFormat = ExcelFormat.Xls, int sheetIndex = 0, object extraData = null);\n\npublic static byte[] ToExcelBytesByTemplate<TEntity>([NotNull]this IEnumerable<TEntity> entities, Stream templateStream, ExcelFormat excelFormat = ExcelFormat.Xls, int sheetIndex = 0, object extraData = null);\n\npublic static byte[] ToExcelBytesByTemplate<TEntity>([NotNull]this IEnumerable<TEntity> entities, IWorkbook templateWorkbook, int sheetIndex = 0, object extraData = null);\n\npublic static byte[] ToExcelBytesByTemplate<TEntity>([NotNull]this IEnumerable<TEntity> entities, ISheet templateSheet, object extraData = null);\n\n```\n\n\n## Reference\n\n- <https://github.com/WeihanLi/WeihanLi.Npoi>\n- <https://github.com/WeihanLi/WeihanLi.Npoi/blob/917e8fb798e9cbae52d121a7d593e37639870911/samples/DotNetCoreSample/Program.cs#L94>\n"
  },
  {
    "path": "docs/docfx.json",
    "content": "{\n  \"metadata\": [\n    {\n      \"src\": [\n        {\n          \"src\": \"../src\",\n          \"files\": [\n            \"**/*.csproj\"\n          ]\n        }\n      ],\n      \"dest\": \"api\"\n    }\n  ],\n  \"build\": {\n    \"content\": [\n      {\n        \"files\": [\n          \"**/*.{md,yml}\"\n        ],\n        \"exclude\": [\n          \"_site/**\"\n        ]\n      }\n    ],\n    \"resource\": [\n      {\n        \"files\": [\n          \"**/images/**\"\n        ]\n      }\n    ],\n    \"output\": \"_site\",\n    \"template\": [\n      \"default\",\n      \"modern\"\n    ],\n    \"globalMetadata\": {\n      \"_appName\": \"WeihanLi.Npoi\",\n      \"_appTitle\": \"WeihanLi.Npoi\",\n      \"_enableSearch\": true,\n      \"pdf\": true\n    }\n  }\n}"
  },
  {
    "path": "docs/toc.yml",
    "content": "- name: Home\n  href: index.md\n- name: API Documentation\n  href: api/\n- name: Release Notes\n  href: ReleaseNotes.md\n- name: Articles\n  href: articles/\n  homepage: articles/intro.md\n- name: Github\n  href: https://github.com/WeihanLi/WeihanLi.Npoi\n"
  },
  {
    "path": "global.json",
    "content": "{\n  \"sdk\": {\n    \"rollForward\": \"major\",\n    \"version\": \"10.0.100\"\n  },\n  \"test\": {\n    \"runner\": \"Microsoft.Testing.Platform\"\n  }\n}\n"
  },
  {
    "path": "nuget.config",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<configuration>\n  <config>\n    <add key=\"defaultPushSource\" value=\"nuget\" />\n  </config>\n  <packageSources>\n    <!--To inherit the global NuGet package sources remove the <clear/> line below -->\n    <clear />\n    <add key=\"nuget\" value=\"https://api.nuget.org/v3/index.json\" />\n  </packageSources>\n</configuration>"
  },
  {
    "path": "perf/WeihanLi.Npoi.Benchmark/BenchmarkDotNet.Artifacts/results/WeihanLi.Npoi.Benchmark.ExportExcelTest-report-github.md",
    "content": "``` ini\n\nBenchmarkDotNet=v0.11.5, OS=Windows 10.0.18362\nIntel Core i5-3470 CPU 3.20GHz (Ivy Bridge), 1 CPU, 4 logical and 4 physical cores\n.NET Core SDK=3.0.100\n  [Host]     : .NET Core 2.2.6 (CoreCLR 4.6.27817.03, CoreFX 4.6.27818.02), 64bit RyuJIT\n  Job-WDPKYY : .NET Core 2.2.6 (CoreCLR 4.6.27817.03, CoreFX 4.6.27818.02), 64bit RyuJIT\n\nIterationCount=5  LaunchCount=1  WarmupCount=1  \n\n```\n|                          Method | RowsCount |        Mean |       Error |     StdDev |         Min |         Max |      Median |       Gen 0 |      Gen 1 |     Gen 2 | Allocated |\n|-------------------------------- |---------- |------------:|------------:|-----------:|------------:|------------:|------------:|------------:|-----------:|----------:|----------:|\n|            **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** |\n|        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 |\n|       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 |\n|         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 |\n|      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 |\n|  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 |\n| 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 |\n|   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 |\n|            **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** |\n|        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 |\n|       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 |\n|         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 |\n|      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 |\n|  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 |\n| 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 |\n|   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 |\n|            **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** |\n|        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 |\n|       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 |\n|         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 |\n|      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 |\n|  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 |\n| 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 |\n|   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 |\n|            **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** |\n|        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 |\n|       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 |\n|         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 |\n|      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 |\n|  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 |\n| 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 |\n|   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 |\n"
  },
  {
    "path": "perf/WeihanLi.Npoi.Benchmark/BenchmarkDotNet.Artifacts/results/WeihanLi.Npoi.Benchmark.ExportExcelTest-report.csv",
    "content": "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\nExportToCsvBytesTest,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\nNpoiExportToXlsBytesTest,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\nNpoiExportToXlsxBytesTest,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\nEpplusExportToBytesTest,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\nStructExportToCsvBytesTest,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\nNpoiStructExportToXlsBytesTest,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\nNpoiStructExportToXlsxBytesTest,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\nEpplusStructExportToBytesTest,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\nExportToCsvBytesTest,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\nNpoiExportToXlsBytesTest,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\nNpoiExportToXlsxBytesTest,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\nEpplusExportToBytesTest,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\nStructExportToCsvBytesTest,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\nNpoiStructExportToXlsBytesTest,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\nNpoiStructExportToXlsxBytesTest,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\nEpplusStructExportToBytesTest,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\nExportToCsvBytesTest,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\nNpoiExportToXlsBytesTest,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\nNpoiExportToXlsxBytesTest,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\nEpplusExportToBytesTest,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\nStructExportToCsvBytesTest,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\nNpoiStructExportToXlsBytesTest,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\nNpoiStructExportToXlsxBytesTest,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\nEpplusStructExportToBytesTest,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\nExportToCsvBytesTest,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\nNpoiExportToXlsBytesTest,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\nNpoiExportToXlsxBytesTest,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\nEpplusExportToBytesTest,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\nStructExportToCsvBytesTest,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\nNpoiStructExportToXlsBytesTest,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\nNpoiStructExportToXlsxBytesTest,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\nEpplusStructExportToBytesTest,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\n"
  },
  {
    "path": "perf/WeihanLi.Npoi.Benchmark/BenchmarkDotNet.Artifacts/results/WeihanLi.Npoi.Benchmark.ExportExcelTest-report.html",
    "content": "<!DOCTYPE html>\n<html lang='en'>\n<head>\n<meta charset='utf-8' />\n<title>WeihanLi.Npoi.Benchmark.ExportExcelTest-20191108-071719</title>\n\n<style type=\"text/css\">\n\ttable { border-collapse: collapse; display: block; width: 100%; overflow: auto; }\n\ttd, th { padding: 6px 13px; border: 1px solid #ddd; }\n\ttr { background-color: #fff; border-top: 1px solid #ccc; }\n\ttr:nth-child(even) { background: #f8f8f8; }\n</style>\n</head>\n<body>\n<pre><code>\nBenchmarkDotNet=v0.11.5, OS=Windows 10.0.18362\nIntel Core i5-3470 CPU 3.20GHz (Ivy Bridge), 1 CPU, 4 logical and 4 physical cores\n.NET Core SDK=3.0.100\n  [Host]     : .NET Core 2.2.6 (CoreCLR 4.6.27817.03, CoreFX 4.6.27818.02), 64bit RyuJIT\n  Job-WDPKYY : .NET Core 2.2.6 (CoreCLR 4.6.27817.03, CoreFX 4.6.27818.02), 64bit RyuJIT\n</code></pre>\n<pre><code>IterationCount=5  LaunchCount=1  WarmupCount=1  \n</code></pre>\n\n<table>\n<thead><tr><th>                   Method</th><th>RowsCount</th><th> Mean</th><th>Error</th><th>StdDev</th><th>  Min</th><th>  Max</th><th>Median</th><th>Gen 0</th><th>Gen 1</th><th>Gen 2</th><th>Allocated</th>\n</tr>\n</thead><tbody><tr><td>ExportToCsvBytesTest</td><td>10000</td><td>22.77 ms</td><td>0.5667 ms</td><td>0.1472 ms</td><td>22.54 ms</td><td>22.95 ms</td><td>22.77 ms</td><td>2281.2500</td><td>937.5000</td><td>281.2500</td><td>12.12 MB</td>\n</tr><tr><td>NpoiExportToXlsBytesTest</td><td>10000</td><td>219.87 ms</td><td>3.8409 ms</td><td>0.9975 ms</td><td>218.92 ms</td><td>221.40 ms</td><td>219.60 ms</td><td>6000.0000</td><td>2000.0000</td><td>1000.0000</td><td>43.99 MB</td>\n</tr><tr><td>NpoiExportToXlsxBytesTest</td><td>10000</td><td>470.79 ms</td><td>9.3139 ms</td><td>2.4188 ms</td><td>467.01 ms</td><td>473.70 ms</td><td>471.26 ms</td><td>20000.0000</td><td>6000.0000</td><td>2000.0000</td><td>104.31 MB</td>\n</tr><tr><td>EpplusExportToBytesTest</td><td>10000</td><td>203.82 ms</td><td>5.0193 ms</td><td>1.3035 ms</td><td>203.13 ms</td><td>206.15 ms</td><td>203.25 ms</td><td>11000.0000</td><td>4000.0000</td><td>1000.0000</td><td>56.66 MB</td>\n</tr><tr><td>StructExportToCsvBytesTest</td><td>10000</td><td>21.46 ms</td><td>0.7542 ms</td><td>0.1959 ms</td><td>21.30 ms</td><td>21.73 ms</td><td>21.34 ms</td><td>2281.2500</td><td>937.5000</td><td>281.2500</td><td>12.12 MB</td>\n</tr><tr><td>NpoiStructExportToXlsBytesTest</td><td>10000</td><td>212.54 ms</td><td>12.7005 ms</td><td>3.2983 ms</td><td>209.21 ms</td><td>216.65 ms</td><td>211.42 ms</td><td>7000.0000</td><td>3000.0000</td><td>1000.0000</td><td>44.37 MB</td>\n</tr><tr><td>NpoiStructExportToXlsxBytesTest</td><td>10000</td><td>495.55 ms</td><td>45.1010 ms</td><td>11.7126 ms</td><td>482.73 ms</td><td>514.33 ms</td><td>492.62 ms</td><td>20000.0000</td><td>7000.0000</td><td>2000.0000</td><td>104.69 MB</td>\n</tr><tr><td>EpplusStructExportToBytesTest</td><td>10000</td><td>208.60 ms</td><td>5.3520 ms</td><td>1.3899 ms</td><td>206.94 ms</td><td>210.32 ms</td><td>208.32 ms</td><td>11000.0000</td><td>4000.0000</td><td>1000.0000</td><td>56.66 MB</td>\n</tr><tr><td>ExportToCsvBytesTest</td><td>30000</td><td>72.16 ms</td><td>3.2924 ms</td><td>0.8550 ms</td><td>71.34 ms</td><td>73.59 ms</td><td>71.86 ms</td><td>6000.0000</td><td>1571.4286</td><td>571.4286</td><td>36.47 MB</td>\n</tr><tr><td>NpoiExportToXlsBytesTest</td><td>30000</td><td>839.63 ms</td><td>22.8596 ms</td><td>5.9366 ms</td><td>835.13 ms</td><td>849.85 ms</td><td>837.76 ms</td><td>20000.0000</td><td>8000.0000</td><td>2000.0000</td><td>124.18 MB</td>\n</tr><tr><td>NpoiExportToXlsxBytesTest</td><td>30000</td><td>1,478.00 ms</td><td>48.6880 ms</td><td>12.6441 ms</td><td>1,457.50 ms</td><td>1,489.99 ms</td><td>1,480.52 ms</td><td>59000.0000</td><td>16000.0000</td><td>4000.0000</td><td>315.85 MB</td>\n</tr><tr><td>EpplusExportToBytesTest</td><td>30000</td><td>624.67 ms</td><td>40.5093 ms</td><td>10.5201 ms</td><td>616.60 ms</td><td>642.67 ms</td><td>621.37 ms</td><td>31000.0000</td><td>10000.0000</td><td>3000.0000</td><td>172.73 MB</td>\n</tr><tr><td>StructExportToCsvBytesTest</td><td>30000</td><td>63.51 ms</td><td>2.5019 ms</td><td>0.6497 ms</td><td>62.53 ms</td><td>64.25 ms</td><td>63.58 ms</td><td>5875.0000</td><td>1375.0000</td><td>375.0000</td><td>36.47 MB</td>\n</tr><tr><td>NpoiStructExportToXlsBytesTest</td><td>30000</td><td>853.48 ms</td><td>33.3918 ms</td><td>8.6718 ms</td><td>839.53 ms</td><td>862.13 ms</td><td>856.08 ms</td><td>20000.0000</td><td>8000.0000</td><td>2000.0000</td><td>125.32 MB</td>\n</tr><tr><td>NpoiStructExportToXlsxBytesTest</td><td>30000</td><td>1,516.05 ms</td><td>141.2409 ms</td><td>36.6798 ms</td><td>1,474.05 ms</td><td>1,553.23 ms</td><td>1,534.14 ms</td><td>59000.0000</td><td>16000.0000</td><td>4000.0000</td><td>317 MB</td>\n</tr><tr><td>EpplusStructExportToBytesTest</td><td>30000</td><td>624.10 ms</td><td>18.2990 ms</td><td>4.7522 ms</td><td>616.20 ms</td><td>627.98 ms</td><td>624.45 ms</td><td>31000.0000</td><td>10000.0000</td><td>3000.0000</td><td>172.72 MB</td>\n</tr><tr><td>ExportToCsvBytesTest</td><td>50000</td><td>113.06 ms</td><td>1.6585 ms</td><td>0.4307 ms</td><td>112.55 ms</td><td>113.55 ms</td><td>113.01 ms</td><td>10000.0000</td><td>2000.0000</td><td>800.0000</td><td>60.81 MB</td>\n</tr><tr><td>NpoiExportToXlsBytesTest</td><td>50000</td><td>1,666.19 ms</td><td>43.1443 ms</td><td>11.2044 ms</td><td>1,651.36 ms</td><td>1,677.04 ms</td><td>1,669.51 ms</td><td>33000.0000</td><td>12000.0000</td><td>3000.0000</td><td>212.25 MB</td>\n</tr><tr><td>NpoiExportToXlsxBytesTest</td><td>50000</td><td>2,562.64 ms</td><td>130.8702 ms</td><td>33.9866 ms</td><td>2,516.62 ms</td><td>2,595.77 ms</td><td>2,573.09 ms</td><td>96000.0000</td><td>24000.0000</td><td>4000.0000</td><td>532.54 MB</td>\n</tr><tr><td>EpplusExportToBytesTest</td><td>50000</td><td>1,059.02 ms</td><td>76.3548 ms</td><td>19.8291 ms</td><td>1,041.61 ms</td><td>1,093.02 ms</td><td>1,052.11 ms</td><td>51000.0000</td><td>13000.0000</td><td>2000.0000</td><td>270.94 MB</td>\n</tr><tr><td>StructExportToCsvBytesTest</td><td>50000</td><td>108.91 ms</td><td>4.0316 ms</td><td>1.0470 ms</td><td>107.28 ms</td><td>110.19 ms</td><td>108.95 ms</td><td>10000.0000</td><td>2000.0000</td><td>800.0000</td><td>60.81 MB</td>\n</tr><tr><td>NpoiStructExportToXlsBytesTest</td><td>50000</td><td>1,675.32 ms</td><td>63.9457 ms</td><td>16.6065 ms</td><td>1,660.80 ms</td><td>1,703.80 ms</td><td>1,669.06 ms</td><td>33000.0000</td><td>12000.0000</td><td>3000.0000</td><td>214.15 MB</td>\n</tr><tr><td>NpoiStructExportToXlsxBytesTest</td><td>50000</td><td>2,505.01 ms</td><td>231.4485 ms</td><td>60.1064 ms</td><td>2,443.93 ms</td><td>2,576.48 ms</td><td>2,494.21 ms</td><td>96000.0000</td><td>24000.0000</td><td>4000.0000</td><td>534.45 MB</td>\n</tr><tr><td>EpplusStructExportToBytesTest</td><td>50000</td><td>1,031.01 ms</td><td>17.5706 ms</td><td>4.5630 ms</td><td>1,027.89 ms</td><td>1,038.94 ms</td><td>1,029.03 ms</td><td>51000.0000</td><td>13000.0000</td><td>2000.0000</td><td>270.93 MB</td>\n</tr><tr><td>ExportToCsvBytesTest</td><td>65535</td><td>147.35 ms</td><td>1.3296 ms</td><td>0.3453 ms</td><td>147.03 ms</td><td>147.83 ms</td><td>147.16 ms</td><td>12750.0000</td><td>2000.0000</td><td>500.0000</td><td>79.73 MB</td>\n</tr><tr><td>NpoiExportToXlsBytesTest</td><td>65535</td><td>2,456.24 ms</td><td>21.4291 ms</td><td>5.5651 ms</td><td>2,448.12 ms</td><td>2,462.39 ms</td><td>2,455.63 ms</td><td>43000.0000</td><td>15000.0000</td><td>3000.0000</td><td>277.42 MB</td>\n</tr><tr><td>NpoiExportToXlsxBytesTest</td><td>65535</td><td>3,292.05 ms</td><td>120.2493 ms</td><td>31.2284 ms</td><td>3,257.61 ms</td><td>3,327.38 ms</td><td>3,280.70 ms</td><td>127000.0000</td><td>30000.0000</td><td>5000.0000</td><td>685.91 MB</td>\n</tr><tr><td>EpplusExportToBytesTest</td><td>65535</td><td>1,365.16 ms</td><td>27.3326 ms</td><td>7.0982 ms</td><td>1,355.48 ms</td><td>1,374.40 ms</td><td>1,363.77 ms</td><td>68000.0000</td><td>17000.0000</td><td>3000.0000</td><td>343.35 MB</td>\n</tr><tr><td>StructExportToCsvBytesTest</td><td>65535</td><td>141.21 ms</td><td>6.6211 ms</td><td>1.7195 ms</td><td>139.06 ms</td><td>143.00 ms</td><td>141.42 ms</td><td>12750.0000</td><td>2000.0000</td><td>500.0000</td><td>79.73 MB</td>\n</tr><tr><td>NpoiStructExportToXlsBytesTest</td><td>65535</td><td>2,505.47 ms</td><td>83.5632 ms</td><td>21.7011 ms</td><td>2,483.53 ms</td><td>2,536.38 ms</td><td>2,504.97 ms</td><td>43000.0000</td><td>15000.0000</td><td>3000.0000</td><td>279.92 MB</td>\n</tr><tr><td>NpoiStructExportToXlsxBytesTest</td><td>65535</td><td>3,235.57 ms</td><td>116.3763 ms</td><td>30.2226 ms</td><td>3,205.37 ms</td><td>3,284.36 ms</td><td>3,230.79 ms</td><td>127000.0000</td><td>30000.0000</td><td>5000.0000</td><td>688.39 MB</td>\n</tr><tr><td>EpplusStructExportToBytesTest</td><td>65535</td><td>1,349.52 ms</td><td>8.8304 ms</td><td>2.2932 ms</td><td>1,346.47 ms</td><td>1,352.90 ms</td><td>1,349.45 ms</td><td>67000.0000</td><td>16000.0000</td><td>3000.0000</td><td>343.35 MB</td>\n</tr></tbody></table>\n</body>\n</html>\n"
  },
  {
    "path": "perf/WeihanLi.Npoi.Benchmark/BenchmarkDotNet.Artifacts/results/WeihanLi.Npoi.Benchmark.ImportExcelTest-report-github.md",
    "content": "``` ini\n\nBenchmarkDotNet=v0.11.5, OS=Windows 10.0.18362\nIntel Core i5-3470 CPU 3.20GHz (Ivy Bridge), 1 CPU, 4 logical and 4 physical cores\n.NET Core SDK=3.0.100\n  [Host]     : .NET Core 2.2.6 (CoreCLR 4.6.27817.03, CoreFX 4.6.27818.02), 64bit RyuJIT\n  Job-WDPKYY : .NET Core 2.2.6 (CoreCLR 4.6.27817.03, CoreFX 4.6.27818.02), 64bit RyuJIT\n\nIterationCount=5  LaunchCount=1  WarmupCount=1  \n\n```\n|                  Method | RowsCount |       Mean |      Error |     StdDev |        Min |        Max |     Median |       Gen 0 |      Gen 1 |     Gen 2 | Allocated |\n|------------------------ |---------- |-----------:|-----------:|-----------:|-----------:|-----------:|-----------:|------------:|-----------:|----------:|----------:|\n|  **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** |\n|  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 |\n| 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 |\n|  **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** |\n|  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 |\n| 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 |\n|  **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** |\n|  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 |\n| 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 |\n|  **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** |\n|  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 |\n| 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 |\n"
  },
  {
    "path": "perf/WeihanLi.Npoi.Benchmark/BenchmarkDotNet.Artifacts/results/WeihanLi.Npoi.Benchmark.ImportExcelTest-report.csv",
    "content": "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\nImportFromCsvBytesTest,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\nImportFromXlsBytesTest,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\nImportFromXlsxBytesTest,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\nImportFromCsvBytesTest,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\nImportFromXlsBytesTest,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\nImportFromXlsxBytesTest,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\nImportFromCsvBytesTest,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\nImportFromXlsBytesTest,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\nImportFromXlsxBytesTest,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\nImportFromCsvBytesTest,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\nImportFromXlsBytesTest,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\nImportFromXlsxBytesTest,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\n"
  },
  {
    "path": "perf/WeihanLi.Npoi.Benchmark/BenchmarkDotNet.Artifacts/results/WeihanLi.Npoi.Benchmark.ImportExcelTest-report.html",
    "content": "<!DOCTYPE html>\n<html lang='en'>\n<head>\n<meta charset='utf-8' />\n<title>WeihanLi.Npoi.Benchmark.ImportExcelTest-20191108-072305</title>\n\n<style type=\"text/css\">\n\ttable { border-collapse: collapse; display: block; width: 100%; overflow: auto; }\n\ttd, th { padding: 6px 13px; border: 1px solid #ddd; }\n\ttr { background-color: #fff; border-top: 1px solid #ccc; }\n\ttr:nth-child(even) { background: #f8f8f8; }\n</style>\n</head>\n<body>\n<pre><code>\nBenchmarkDotNet=v0.11.5, OS=Windows 10.0.18362\nIntel Core i5-3470 CPU 3.20GHz (Ivy Bridge), 1 CPU, 4 logical and 4 physical cores\n.NET Core SDK=3.0.100\n  [Host]     : .NET Core 2.2.6 (CoreCLR 4.6.27817.03, CoreFX 4.6.27818.02), 64bit RyuJIT\n  Job-WDPKYY : .NET Core 2.2.6 (CoreCLR 4.6.27817.03, CoreFX 4.6.27818.02), 64bit RyuJIT\n</code></pre>\n<pre><code>IterationCount=5  LaunchCount=1  WarmupCount=1  \n</code></pre>\n\n<table>\n<thead><tr><th>           Method</th><th>RowsCount</th><th>Mean</th><th>Error</th><th>StdDev</th><th> Min</th><th> Max</th><th>Median</th><th>Gen 0</th><th>Gen 1</th><th>Gen 2</th><th>Allocated</th>\n</tr>\n</thead><tbody><tr><td>ImportFromCsvBytesTest</td><td>10000</td><td>171.2 ms</td><td>2.904 ms</td><td>0.7541 ms</td><td>170.0 ms</td><td>172.0 ms</td><td>171.2 ms</td><td>9333.3333</td><td>2000.0000</td><td>333.3333</td><td>36.44 MB</td>\n</tr><tr><td>ImportFromXlsBytesTest</td><td>10000</td><td>353.8 ms</td><td>10.170 ms</td><td>2.6412 ms</td><td>351.8 ms</td><td>358.4 ms</td><td>352.9 ms</td><td>15000.0000</td><td>6000.0000</td><td>2000.0000</td><td>64.71 MB</td>\n</tr><tr><td>ImportFromXlsxBytesTest</td><td>10000</td><td>758.2 ms</td><td>10.212 ms</td><td>2.6520 ms</td><td>755.6 ms</td><td>762.3 ms</td><td>757.8 ms</td><td>25000.0000</td><td>11000.0000</td><td>3000.0000</td><td>110.87 MB</td>\n</tr><tr><td>ImportFromCsvBytesTest</td><td>30000</td><td>511.5 ms</td><td>17.159 ms</td><td>4.4562 ms</td><td>504.0 ms</td><td>514.9 ms</td><td>512.7 ms</td><td>31000.0000</td><td>7000.0000</td><td>1000.0000</td><td>109.1 MB</td>\n</tr><tr><td>ImportFromXlsBytesTest</td><td>30000</td><td>1,050.8 ms</td><td>33.740 ms</td><td>8.7622 ms</td><td>1,040.2 ms</td><td>1,064.3 ms</td><td>1,049.8 ms</td><td>47000.0000</td><td>18000.0000</td><td>3000.0000</td><td>186.28 MB</td>\n</tr><tr><td>ImportFromXlsxBytesTest</td><td>30000</td><td>2,358.6 ms</td><td>284.891 ms</td><td>73.9854 ms</td><td>2,228.2 ms</td><td>2,405.3 ms</td><td>2,378.8 ms</td><td>67000.0000</td><td>23000.0000</td><td>4000.0000</td><td>331.5 MB</td>\n</tr><tr><td>ImportFromCsvBytesTest</td><td>50000</td><td>860.7 ms</td><td>11.026 ms</td><td>2.8634 ms</td><td>857.3 ms</td><td>865.1 ms</td><td>860.0 ms</td><td>50000.0000</td><td>12000.0000</td><td>1000.0000</td><td>182 MB</td>\n</tr><tr><td>ImportFromXlsBytesTest</td><td>50000</td><td>1,703.0 ms</td><td>23.050 ms</td><td>5.9861 ms</td><td>1,695.9 ms</td><td>1,709.7 ms</td><td>1,703.3 ms</td><td>76000.0000</td><td>28000.0000</td><td>2000.0000</td><td>315.85 MB</td>\n</tr><tr><td>ImportFromXlsxBytesTest</td><td>50000</td><td>3,869.8 ms</td><td>141.111 ms</td><td>36.6460 ms</td><td>3,817.0 ms</td><td>3,909.6 ms</td><td>3,872.4 ms</td><td>110000.0000</td><td>36000.0000</td><td>3000.0000</td><td>552.76 MB</td>\n</tr><tr><td>ImportFromCsvBytesTest</td><td>65535</td><td>1,144.7 ms</td><td>65.693 ms</td><td>17.0602 ms</td><td>1,121.2 ms</td><td>1,162.6 ms</td><td>1,149.4 ms</td><td>65000.0000</td><td>16000.0000</td><td>1000.0000</td><td>238.24 MB</td>\n</tr><tr><td>ImportFromXlsBytesTest</td><td>65535</td><td>2,324.6 ms</td><td>23.299 ms</td><td>6.0507 ms</td><td>2,318.4 ms</td><td>2,333.9 ms</td><td>2,323.8 ms</td><td>99000.0000</td><td>35000.0000</td><td>3000.0000</td><td>413.07 MB</td>\n</tr><tr><td>ImportFromXlsxBytesTest</td><td>65535</td><td>5,011.3 ms</td><td>108.362 ms</td><td>28.1414 ms</td><td>4,977.1 ms</td><td>5,054.4 ms</td><td>5,012.6 ms</td><td>145000.0000</td><td>48000.0000</td><td>3000.0000</td><td>723.66 MB</td>\n</tr></tbody></table>\n</body>\n</html>\n"
  },
  {
    "path": "perf/WeihanLi.Npoi.Benchmark/BenchmarkDotNet.Artifacts/results/WeihanLi.Npoi.Benchmark.WorkbookBasicTest-report-github.md",
    "content": "``` ini\n\nBenchmarkDotNet=v0.11.5, OS=Windows 10.0.18362\nIntel Core i5-3470 CPU 3.20GHz (Ivy Bridge), 1 CPU, 4 logical and 4 physical cores\n.NET Core SDK=3.0.100\n  [Host]     : .NET Core 2.2.6 (CoreCLR 4.6.27817.03, CoreFX 4.6.27818.02), 64bit RyuJIT\n  Job-CBYTBY : .NET Core 2.2.6 (CoreCLR 4.6.27817.03, CoreFX 4.6.27818.02), 64bit RyuJIT\n\nIterationCount=5  LaunchCount=1  WarmupCount=1  \n\n```\n|               Method | RowsCount |       Mean |      Error |      StdDev |        Min |        Max |     Median | Ratio | RatioSD |       Gen 0 |      Gen 1 |     Gen 2 |  Allocated |\n|--------------------- |---------- |-----------:|-----------:|------------:|-----------:|-----------:|-----------:|------:|--------:|------------:|-----------:|----------:|-----------:|\n|  **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** |\n| 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 |\n|   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 |\n|                      |           |            |            |             |            |            |            |       |         |             |            |           |            |\n|  **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** |\n| 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 |\n|   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 |\n|                      |           |            |            |             |            |            |            |       |         |             |            |           |            |\n|  **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** |\n| 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 |\n|   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 |\n|                      |           |            |            |             |            |            |            |       |         |             |            |           |            |\n|  **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** |\n| 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 |\n|   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 |\n"
  },
  {
    "path": "perf/WeihanLi.Npoi.Benchmark/BenchmarkDotNet.Artifacts/results/WeihanLi.Npoi.Benchmark.WorkbookBasicTest-report.csv",
    "content": "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\nNpoiXlsWorkbookInit,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\nNpoiXlsxWorkbookInit,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\nEpplusWorkbookInit,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\nNpoiXlsWorkbookInit,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\nNpoiXlsxWorkbookInit,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\nEpplusWorkbookInit,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\nNpoiXlsWorkbookInit,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\nNpoiXlsxWorkbookInit,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\nEpplusWorkbookInit,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\nNpoiXlsWorkbookInit,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\nNpoiXlsxWorkbookInit,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\nEpplusWorkbookInit,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\n"
  },
  {
    "path": "perf/WeihanLi.Npoi.Benchmark/BenchmarkDotNet.Artifacts/results/WeihanLi.Npoi.Benchmark.WorkbookBasicTest-report.html",
    "content": "<!DOCTYPE html>\n<html lang='en'>\n<head>\n<meta charset='utf-8' />\n<title>WeihanLi.Npoi.Benchmark.WorkbookBasicTest-20191108-065532</title>\n\n<style type=\"text/css\">\n\ttable { border-collapse: collapse; display: block; width: 100%; overflow: auto; }\n\ttd, th { padding: 6px 13px; border: 1px solid #ddd; }\n\ttr { background-color: #fff; border-top: 1px solid #ccc; }\n\ttr:nth-child(even) { background: #f8f8f8; }\n</style>\n</head>\n<body>\n<pre><code>\nBenchmarkDotNet=v0.11.5, OS=Windows 10.0.18362\nIntel Core i5-3470 CPU 3.20GHz (Ivy Bridge), 1 CPU, 4 logical and 4 physical cores\n.NET Core SDK=3.0.100\n  [Host]     : .NET Core 2.2.6 (CoreCLR 4.6.27817.03, CoreFX 4.6.27818.02), 64bit RyuJIT\n  Job-CBYTBY : .NET Core 2.2.6 (CoreCLR 4.6.27817.03, CoreFX 4.6.27818.02), 64bit RyuJIT\n</code></pre>\n<pre><code>IterationCount=5  LaunchCount=1  WarmupCount=1  \n</code></pre>\n\n<table>\n<thead><tr><th>        Method</th><th>RowsCount</th><th>Mean</th><th>Error</th><th>StdDev</th><th> Min</th><th> Max</th><th>Median</th><th>Ratio</th><th>RatioSD</th><th>Gen 0</th><th>Gen 1</th><th>Gen 2</th><th>Allocated</th>\n</tr>\n</thead><tbody><tr><td>NpoiXlsWorkbookInit</td><td>10000</td><td>324.7 ms</td><td>1.583 ms</td><td>0.4110 ms</td><td>324.3 ms</td><td>325.4 ms</td><td>324.6 ms</td><td>1.00</td><td>0.00</td><td>10000.0000</td><td>5000.0000</td><td>2000.0000</td><td>78.6 MB</td>\n</tr><tr><td>NpoiXlsxWorkbookInit</td><td>10000</td><td>1,369.0 ms</td><td>73.747 ms</td><td>19.1517 ms</td><td>1,341.3 ms</td><td>1,384.4 ms</td><td>1,381.1 ms</td><td>4.22</td><td>0.06</td><td>57000.0000</td><td>14000.0000</td><td>4000.0000</td><td>306.45 MB</td>\n</tr><tr><td>EpplusWorkbookInit</td><td>10000</td><td>552.9 ms</td><td>12.740 ms</td><td>3.3085 ms</td><td>549.7 ms</td><td>557.7 ms</td><td>552.4 ms</td><td>1.70</td><td>0.01</td><td>18000.0000</td><td>7000.0000</td><td>3000.0000</td><td>121.05 MB</td>\n</tr><tr><td>NpoiXlsWorkbookInit</td><td>30000</td><td>1,222.4 ms</td><td>33.717 ms</td><td>8.7562 ms</td><td>1,209.0 ms</td><td>1,233.1 ms</td><td>1,222.5 ms</td><td>1.00</td><td>0.00</td><td>29000.0000</td><td>11000.0000</td><td>3000.0000</td><td>235.03 MB</td>\n</tr><tr><td>NpoiXlsxWorkbookInit</td><td>30000</td><td>4,226.2 ms</td><td>299.833 ms</td><td>77.8658 ms</td><td>4,109.5 ms</td><td>4,308.6 ms</td><td>4,257.2 ms</td><td>3.46</td><td>0.08</td><td>174000.0000</td><td>34000.0000</td><td>6000.0000</td><td>913.9 MB</td>\n</tr><tr><td>EpplusWorkbookInit</td><td>30000</td><td>1,695.4 ms</td><td>31.751 ms</td><td>8.2457 ms</td><td>1,686.3 ms</td><td>1,706.5 ms</td><td>1,694.2 ms</td><td>1.39</td><td>0.02</td><td>48000.0000</td><td>17000.0000</td><td>5000.0000</td><td>358.51 MB</td>\n</tr><tr><td>NpoiXlsWorkbookInit</td><td>50000</td><td>2,323.5 ms</td><td>236.041 ms</td><td>61.2990 ms</td><td>2,286.0 ms</td><td>2,431.9 ms</td><td>2,294.2 ms</td><td>1.00</td><td>0.00</td><td>47000.0000</td><td>18000.0000</td><td>4000.0000</td><td>417.1 MB</td>\n</tr><tr><td>NpoiXlsxWorkbookInit</td><td>50000</td><td>7,055.2 ms</td><td>279.256 ms</td><td>72.5218 ms</td><td>6,982.8 ms</td><td>7,150.2 ms</td><td>7,027.2 ms</td><td>3.04</td><td>0.10</td><td>288000.0000</td><td>51000.0000</td><td>6000.0000</td><td>1545.32 MB</td>\n</tr><tr><td>EpplusWorkbookInit</td><td>50000</td><td>2,806.9 ms</td><td>56.266 ms</td><td>14.6121 ms</td><td>2,792.9 ms</td><td>2,829.1 ms</td><td>2,804.6 ms</td><td>1.21</td><td>0.03</td><td>79000.0000</td><td>27000.0000</td><td>7000.0000</td><td>578.46 MB</td>\n</tr><tr><td>NpoiXlsWorkbookInit</td><td>65535</td><td>3,646.8 ms</td><td>131.129 ms</td><td>34.0537 ms</td><td>3,603.0 ms</td><td>3,696.3 ms</td><td>3,642.5 ms</td><td>1.00</td><td>0.00</td><td>61000.0000</td><td>21000.0000</td><td>4000.0000</td><td>504.46 MB</td>\n</tr><tr><td>NpoiXlsxWorkbookInit</td><td>65535</td><td>9,295.6 ms</td><td>486.761 ms</td><td>126.4104 ms</td><td>9,163.3 ms</td><td>9,468.6 ms</td><td>9,330.5 ms</td><td>2.55</td><td>0.04</td><td>390000.0000</td><td>67000.0000</td><td>8000.0000</td><td>2048.14 MB</td>\n</tr><tr><td>EpplusWorkbookInit</td><td>65535</td><td>3,721.6 ms</td><td>124.945 ms</td><td>32.4478 ms</td><td>3,680.7 ms</td><td>3,766.8 ms</td><td>3,714.1 ms</td><td>1.02</td><td>0.01</td><td>102000.0000</td><td>35000.0000</td><td>8000.0000</td><td>747.85 MB</td>\n</tr></tbody></table>\n</body>\n</html>\n"
  },
  {
    "path": "perf/WeihanLi.Npoi.Benchmark/ExportExcelTest.cs",
    "content": "﻿// Copyright (c) Weihan Li. All rights reserved.\n// Licensed under the Apache license.\n\nusing BenchmarkDotNet.Attributes;\nusing EPPlus.Core.Extensions;\nusing EPPlus.Core.Extensions.Attributes;\nusing System.Runtime.CompilerServices;\n\nnamespace WeihanLi.Npoi.Benchmark;\n\n[SimpleJob(launchCount: 1, warmupCount: 1, iterationCount: 5)]\n[MemoryDiagnoser]\n[MinColumn, MaxColumn, MeanColumn, MedianColumn]\npublic class ExportExcelTest\n{\n    private class TestEntity\n    {\n        [ExcelTableColumn(\"PKID\")]\n        public int PKID { get; set; }\n\n        [ExcelTableColumn(\"UserName\")]\n        public string? Username { get; set; }\n\n        [ExcelTableColumn(\"PasswordHash\")]\n        public string? PasswordHash { get; set; }\n\n        [ExcelTableColumn(\"Amount\")]\n        public decimal Amount { get; set; }\n\n        [ExcelTableColumn(\"WechatOpenId\")]\n        public string? WechatOpenId { get; set; }\n\n        [ExcelTableColumn(\"IsActive\")]\n        public bool IsActive { get; set; }\n\n        [ExcelTableColumn(\"CreateTime\")]\n        public DateTime CreateTime => DateTime.Now;\n    }\n\n    private struct TestStruct\n    {\n        [ExcelTableColumn(\"PKID\")]\n        public int PKID { get; set; }\n\n        [ExcelTableColumn(\"UserName\")]\n        public string? Username { get; set; }\n\n        [ExcelTableColumn(\"PasswordHash\")]\n        public string? PasswordHash { get; set; }\n\n        [ExcelTableColumn(\"Amount\")]\n        public decimal Amount { get; set; }\n\n        [ExcelTableColumn(\"WechatOpenId\")]\n        public string? WechatOpenId { get; set; }\n\n        [ExcelTableColumn(\"IsActive\")]\n        public bool IsActive { get; set; }\n\n        [ExcelTableColumn(\"CreateTime\")]\n        public DateTime CreateTime => DateTime.Now;\n    }\n\n    private readonly List<TestEntity> testData = new(51200);\n    private readonly List<TestStruct> testStructData = new(51200);\n\n    [Params(10000, 30000, 50000, 65535)]\n    public int RowsCount;\n\n    [GlobalSetup]\n    public void GlobalSetup()\n    {\n        for (var i = 1; i <= RowsCount; i++)\n        {\n            testData.Add(new TestEntity()\n            {\n                Amount = 1000,\n                Username = \"xxxx\",\n                PKID = i,\n            });\n\n            testStructData.Add(new TestStruct()\n            {\n                Amount = 1000,\n                Username = \"xxxx\",\n                PKID = i,\n            });\n        }\n    }\n\n    [GlobalCleanup]\n    public void GlobalCleanup()\n    {\n        // Disposing logic\n        testData.Clear();\n    }\n\n    [Benchmark]\n    [MethodImpl(MethodImplOptions.NoInlining)]\n    public byte[] ExportToCsvBytesTest()\n    {\n        return testData.ToCsvBytes();\n    }\n\n    [Benchmark]\n    [MethodImpl(MethodImplOptions.NoInlining)]\n    public byte[] NpoiExportToXlsBytesTest()\n    {\n        return testData.ToExcelBytes();\n    }\n\n    [Benchmark]\n    [MethodImpl(MethodImplOptions.NoInlining)]\n    public byte[] NpoiExportToXlsxBytesTest()\n    {\n        return testData.ToExcelBytes(ExcelFormat.Xlsx);\n    }\n\n    [Benchmark]\n    [MethodImpl(MethodImplOptions.NoInlining)]\n    public byte[] EpplusExportToBytesTest()\n    {\n        return testData.ToExcelPackage().GetAsByteArray();\n    }\n\n    [Benchmark]\n    [MethodImpl(MethodImplOptions.NoInlining)]\n    public byte[] StructExportToCsvBytesTest()\n    {\n        return testStructData.ToCsvBytes();\n    }\n\n    [Benchmark]\n    [MethodImpl(MethodImplOptions.NoInlining)]\n    public byte[] NpoiStructExportToXlsBytesTest()\n    {\n        return testStructData.ToExcelBytes();\n    }\n\n    [Benchmark]\n    [MethodImpl(MethodImplOptions.NoInlining)]\n    public byte[] NpoiStructExportToXlsxBytesTest()\n    {\n        return testStructData.ToExcelBytes(ExcelFormat.Xlsx);\n    }\n\n    [Benchmark]\n    [MethodImpl(MethodImplOptions.NoInlining)]\n    public byte[] EpplusStructExportToBytesTest()\n    {\n        return testStructData.ToExcelPackage().GetAsByteArray();\n    }\n}\n"
  },
  {
    "path": "perf/WeihanLi.Npoi.Benchmark/ImportExcelTest.cs",
    "content": "﻿// Copyright (c) Weihan Li. All rights reserved.\n// Licensed under the Apache license.\n\nusing BenchmarkDotNet.Attributes;\nusing System.Runtime.CompilerServices;\n\nnamespace WeihanLi.Npoi.Benchmark;\n\n[SimpleJob(launchCount: 1, warmupCount: 1, iterationCount: 5)]\n[MemoryDiagnoser]\n[MinColumn, MaxColumn, MeanColumn, MedianColumn]\npublic class ImportExcelTest\n{\n    private class TestEntity\n    {\n        public int PKID { get; set; }\n\n        public string? Username { get; set; }\n\n        public string? PasswordHash { get; set; }\n\n        public decimal Amount { get; set; }\n\n        public string? WechatOpenId { get; set; }\n\n        public bool IsActive { get; set; }\n\n        public DateTime CreateTime { get; set; } = DateTime.Now;\n    }\n\n    private readonly List<TestEntity> testData = new(51200);\n    private byte[] xlsBytes = Array.Empty<byte>(), xlsxBytes = Array.Empty<byte>(), csvBytes = Array.Empty<byte>();\n\n    [Params(10000, 30000, 50000, 65535)]\n    public int RowsCount;\n\n    [GlobalSetup]\n    public void GlobalSetup()\n    {\n        for (var i = 1; i <= RowsCount; i++)\n        {\n            testData.Add(new TestEntity()\n            {\n                Amount = 1000,\n                Username = \"xxxx\",\n                PKID = i,\n            });\n        }\n\n        xlsBytes = testData.ToExcelBytes();\n        xlsxBytes = testData.ToExcelBytes(ExcelFormat.Xlsx);\n        csvBytes = testData.ToCsvBytes();\n    }\n\n    [GlobalCleanup]\n    public void GlobalCleanup()\n    {\n        // Disposing logic\n        testData.Clear();\n    }\n\n    [Benchmark]\n    [MethodImpl(MethodImplOptions.NoInlining)]\n    public int ImportFromCsvBytesTest()\n    {\n        var list = CsvHelper.ToEntityList<TestEntity>(csvBytes);\n        return list.Count;\n    }\n\n    [Benchmark]\n    [MethodImpl(MethodImplOptions.NoInlining)]\n    public int ImportFromXlsBytesTest()\n    {\n        var list = ExcelHelper.ToEntityList<TestEntity>(xlsBytes, ExcelFormat.Xls);\n        return list.Count;\n    }\n\n    [Benchmark]\n    [MethodImpl(MethodImplOptions.NoInlining)]\n    public int ImportFromXlsxBytesTest()\n    {\n        var list = ExcelHelper.ToEntityList<TestEntity>(xlsxBytes, ExcelFormat.Xlsx);\n        return list.Count;\n    }\n}\n"
  },
  {
    "path": "perf/WeihanLi.Npoi.Benchmark/Program.cs",
    "content": "﻿// Copyright (c) Weihan Li. All rights reserved.\n// Licensed under the Apache license.\n\nusing BenchmarkDotNet.Running;\n\nnamespace WeihanLi.Npoi.Benchmark;\n\npublic class Program\n{\n    public static void Main(string[] args)\n    {\n        // BenchmarkRunner.Run<WorkbookBasicTest>();\n        BenchmarkRunner.Run<ExportExcelTest>();\n        BenchmarkRunner.Run<ImportExcelTest>();\n    }\n}\n"
  },
  {
    "path": "perf/WeihanLi.Npoi.Benchmark/WeihanLi.Npoi.Benchmark.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk\">\n\n  <PropertyGroup>\n    <OutputType>Exe</OutputType>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\..\\src\\WeihanLi.Npoi\\WeihanLi.Npoi.csproj\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <PackageReference Include=\"BenchmarkDotNet\" />\n    <PackageReference Include=\"EPPlus.Core.Extensions\" />\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "perf/WeihanLi.Npoi.Benchmark/WorkbookBasicTest.cs",
    "content": "﻿// Copyright (c) Weihan Li. All rights reserved.\n// Licensed under the Apache license.\n\nusing BenchmarkDotNet.Attributes;\nusing OfficeOpenXml;\nusing System.Runtime.CompilerServices;\n\nnamespace WeihanLi.Npoi.Benchmark;\n\n[SimpleJob(launchCount: 1, warmupCount: 1, iterationCount: 5)]\n[MemoryDiagnoser]\n[MinColumn, MaxColumn, MeanColumn, MedianColumn]\npublic class WorkbookBasicTest\n{\n    private const int ColsCount = 10;\n\n    [Params(10000, 30000, 50000, 65535)]\n    public int RowsCount;\n\n    [Benchmark(Baseline = true)]\n    public byte[] NpoiXlsWorkbookInit()\n    {\n        var workbook = ExcelHelper.PrepareWorkbook(ExcelFormat.Xls);\n\n        var sheet = workbook.CreateSheet(\"tempSheet\");\n\n        for (var i = 0; i < RowsCount; i++)\n        {\n            var row = sheet.CreateRow(i);\n            for (var j = 0; j < ColsCount; j++)\n            {\n                var cell = row.CreateCell(j);\n                cell.SetCellValue($\"as ({i}, {j}) sa\");\n            }\n        }\n\n        return workbook.ToExcelBytes();\n    }\n\n    [Benchmark]\n    [MethodImpl(MethodImplOptions.NoInlining)]\n    public byte[] NpoiXlsxWorkbookInit()\n    {\n        var workbook = ExcelHelper.PrepareWorkbook(ExcelFormat.Xlsx);\n\n        var sheet = workbook.CreateSheet(\"tempSheet\");\n\n        for (var i = 0; i < RowsCount; i++)\n        {\n            var row = sheet.CreateRow(i);\n            for (var j = 0; j < ColsCount; j++)\n            {\n                var cell = row.CreateCell(j);\n                cell.SetCellValue($\"as ({i}, {j}) sa\");\n            }\n        }\n\n        return workbook.ToExcelBytes();\n    }\n\n    [Benchmark]\n    [MethodImpl(MethodImplOptions.NoInlining)]\n    public byte[] EpplusWorkbookInit()\n    {\n        var excel = new ExcelPackage();\n\n        var sheet = excel.Workbook.Worksheets.Add(\"tempSheet\");\n\n        for (var i = 1; i <= RowsCount; i++)\n        {\n            for (var j = 1; j <= ColsCount; j++)\n            {\n                sheet.Cells[i, j].Value = $\"as ({i}, {j}) sa\";\n            }\n        }\n\n        return excel.GetAsByteArray();\n    }\n}\n"
  },
  {
    "path": "samples/Directory.Build.props",
    "content": "<Project>\n  <!-- See https://aka.ms/dotnet/msbuild/customize for more details on customizing your build -->\n  <Import Project=\"$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))\"\n          Condition=\"$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../')) != ''\" />\n  <PropertyGroup>\n  </PropertyGroup>\n  <ItemGroup>\n    <ProjectReference Include=\"..\\..\\src\\WeihanLi.Npoi\\WeihanLi.Npoi.csproj\" />\n    <Using Include=\"WeihanLi.Npoi\" />\n    <Using Include=\"WeihanLi.Npoi.Attributes\" />\n    <Using Include=\"WeihanLi.Npoi.Configurations\" />\n    <Using Include=\"WeihanLi.Npoi.Attributes.ColumnAttribute\" Alias=\"ColumnAttribute\" />\n  </ItemGroup>\n</Project>\n"
  },
  {
    "path": "samples/DotNetCoreSample/DotNetCoreSample.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk\">\n\n  <PropertyGroup>\n    <OutputType>Exe</OutputType>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <None Update=\"Templates\\testTemplate.xlsx\">\n      <CopyToOutputDirectory>Always</CopyToOutputDirectory>\n    </None>\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "samples/DotNetCoreSample/ImportImageTestModel.cs",
    "content": "﻿// Copyright (c) Weihan Li. All rights reserved.\n// Licensed under the Apache license.\n\nnamespace DotNetCoreSample;\n\ninternal class ImportImageTestModel\n{\n    public int Id { get; set; }\n    public string? Name { get; set; }\n    public byte[]? Image { get; set; }\n}\n"
  },
  {
    "path": "samples/DotNetCoreSample/IssueSamples.cs",
    "content": "﻿// Copyright (c) Weihan Li. All rights reserved.\n// Licensed under the Apache license.\n\nusing WeihanLi.Extensions;\n\npublic static partial class IssueSamples\n{\n    public static void Issue169Sample()\n    {\n        var filePath = @\"C:\\Users\\Weiha\\Downloads\\test\\2.xlsx\";\n        var workbook = ExcelHelper.LoadExcel(\n            File.OpenRead(filePath),\n            ExcelFormat.Xlsx\n        );\n        var settings = FluentSettings.For<Issue169Dto>();\n        settings.WithPostImportAction((x, rowIndex) => x?.RowNum = rowIndex + 1);\n        var list = workbook.ToEntityList<Issue169Dto>();\n        foreach (var item in list)\n        {\n            Console.WriteLine(item.ToJson());\n        }\n    }\n}\n\n[Sheet(SheetIndex = 0, StartRowIndex = 6)]\npublic class Issue169Dto\n{\n    [Column(IsIgnored = true)]\n    public int RowNum { get; set; }\n\n    /// <summary>\n    /// 编号\n    /// </summary>\n    [Column(\"物料编号\", Index = 0)]\n    public string No { get; set; }\n\n    /// <summary>\n    /// 物料名称\n    /// </summary>\n    [Column(\"物料名称\", Index = 1)]\n    public string Name { get; set; }\n\n    /// <summary>\n    /// 规格型号\n    /// </summary>\n    [Column(\"规格型号\", Index = 2)]\n    public string Specification { get; set; }\n\n    /// <summary>\n    /// 特殊库存\n    /// </summary>\n    [Column(\"特殊库存\", Index = 3)]\n    public string QuantityM { get; set; }\n\n    /// <summary>\n    /// 单位\n    /// </summary>\n    [Column(\"计量单位\", Index = 4)]\n    public string Unit { get; set; }\n\n    [Column(\"会计年度\", Index = 5)]\n    public int Ytime { get; set; }\n\n    [Column(\"会计期间\", Index = 6)]\n    public int Mtime { get; set; }\n\n    // 期初余额\n    [Column(\"方向\", Index = 7)]\n    public string Direction1 { get; set; }\n\n    /// <summary>\n    /// 数量\n    /// </summary>\n    [Column(\"数量\", Index = 8)]\n    public decimal Quantity1 { get; set; }\n\n    /// <summary>\n    /// 单价\n    /// </summary>\n    [Column(\"实际单价\", Index = 9)]\n    public decimal UnitPrice1 { get; set; }\n\n    /// <summary>\n    /// 总金额\n    /// </summary>\n    [Column(\"实际金额\", Index = 10)]\n    public decimal TotalAmount1 { get; set; }\n\n    /// <summary>\n    /// 标准单价\n    /// </summary>\n    [Column(\"标准单价\", Index = 11)]\n    public decimal StandardUnitPrice1 { get; set; }\n\n    /// <summary>\n    /// 标准金额\n    /// </summary>\n    [Column(\"标准金额\", Index = 12)]\n    public decimal StandardotalAmount1 { get; set; }\n\n    // 期初余额\n\n    //本期借方\n    /// <summary>\n    /// 数量\n    /// </summary>\n    [Column(\"数量\", Index = 13)]\n    public decimal Quantity2 { get; set; }\n\n    /// <summary>\n    /// 单价\n    /// </summary>\n    [Column(\"实际单价\", Index = 14)]\n    public decimal UnitPrice2 { get; set; }\n\n    /// <summary>\n    /// 总金额\n    /// </summary>\n    [Column(\"实际金额\", Index = 15)]\n    public decimal TotalAmount2 { get; set; }\n\n    /// <summary>\n    /// 标准单价\n    /// </summary>\n    [Column(\"标准单价\", Index = 16)]\n    public decimal StandardUnitPrice2 { get; set; }\n\n    /// <summary>\n    /// 标准金额\n    /// </summary>\n    [Column(\"标准金额\", Index = 17)]\n    public decimal StandardotalAmount2 { get; set; }\n\n    //本期贷方\n\n    /// <summary>\n    /// 数量\n    /// </summary>\n    [Column(\"数量\", Index = 18)]\n    public decimal Quantity3 { get; set; }\n\n    /// <summary>\n    /// 单价\n    /// </summary>\n    [Column(\"实际单价\", Index = 19)]\n    public decimal UnitPrice3 { get; set; }\n\n    /// <summary>\n    /// 总金额\n    /// </summary>\n    [Column(\"实际金额\", Index = 20)]\n    public decimal TotalAmount3 { get; set; }\n\n    /// <summary>\n    /// 标准单价\n    /// </summary>\n    [Column(\"标准单价\", Index = 21)]\n    public decimal StandardUnitPrice3 { get; set; }\n\n    /// <summary>\n    /// 标准金额\n    /// </summary>\n    [Column(\"标准金额\", Index = 22)]\n    public decimal StandardotalAmount3 { get; set; }\n\n    //期末余额\n    [Column(\"方向\", Index = 23)]\n    public string Direction2 { get; set; }\n\n    /// <summary>\n    /// 数量\n    /// </summary>\n    [Column(\"数量\", Index = 24)]\n    public decimal Quantity4 { get; set; }\n\n    /// <summary>\n    /// 实际单价\n    /// </summary>\n    [Column(\"实际单价\", Index = 25)]\n    public decimal UnitPrice4 { get; set; }\n\n    /// <summary>\n    /// 总金额\n    /// </summary>\n    [Column(\"实际金额\", Index = 26)]\n    public decimal TotalAmount4 { get; set; }\n\n    [Column(\"核算类别名称\", Index = 27)]\n    public string CategoryName { get; set; }\n\n    [Column(\"是否可用\", Index = 28)]\n    public string IsStop { get; set; }\n}\n"
  },
  {
    "path": "samples/DotNetCoreSample/ProductPriceMapping.cs",
    "content": "﻿// Copyright (c) Weihan Li. All rights reserved.\n// Licensed under the Apache license.\n\n\n// ReSharper disable InconsistentNaming\nnamespace DotNetCoreSample;\n\ninternal class ProductPriceMapping\n{\n    public string? Type { get; set; }\n\n    public string? Pid { get; set; }\n\n    public string? ShopCode { get; set; }\n\n    public decimal Price { get; set; }\n\n    public long ItemID { get; set; }\n\n    public long SkuID { get; set; }\n\n    public DateTime LastUpdateDateTime { get; set; }\n}\n"
  },
  {
    "path": "samples/DotNetCoreSample/Program.cs",
    "content": "﻿// Copyright (c) Weihan Li. All rights reserved.\n// Licensed under the Apache license.\n\nusing NPOI.SS.UserModel;\nusing NPOI.SS.Util;\nusing WeihanLi.Common.Helpers;\nusing WeihanLi.Common.Logging;\nusing WeihanLi.Extensions;\n\n// ReSharper disable All\n\nLogHelper.ConfigureLogging(x => x.WithMinimumLevel(LogHelperLogLevel.Info).AddConsole());\n\n{\n    IssueSamples.Issue169Sample();\n    ConsoleHelper.ReadLineWithPrompt();\n}\n\n// multi sheets sample\n{\n    var collection1 = new List<TestEntity>()\n    {\n        new TestEntity()\n        {\n            PKID = 1,\n            SettingId = Guid.NewGuid(),\n            SettingName = \"Setting1\",\n            SettingValue = \"Value1\",\n            DisplayName = \"dd\"\n        },\n        new TestEntity()\n        {\n            PKID=2,\n            SettingId = Guid.NewGuid(),\n            SettingName = \"Setting2\",\n            SettingValue = \"Value2\",\n            Enabled = true\n        },\n    };\n    await collection1.ToCsvFileAsync($\"{nameof(collection1)}.csv\");\n    var collection2 = new[]\n    {\n        new TestEntity2()\n        {\n            Id = 999,\n            Title = \"test\"\n        }\n    };\n    // prepare a workbook\n    var workbook = ExcelHelper.PrepareWorkbook(ExcelFormat.Xlsx);\n    workbook.ImportData(collection1);\n    workbook.ImportData(collection2, 1);\n    workbook.WriteToFile(\"multi-sheets-sample.xlsx\");\n\n    // using var ms = new MemoryStream();\n    // workbook.Write(ms);\n\n    Console.WriteLine(\"multi-sheets-sample excel generated.\");\n    Console.ReadLine();\n}\n\n//\n// var testSurveyExcelPath = @\"C:\\Users\\Weiha\\Desktop\\temp\\QuizBulkUpload.xlsx\";\n// var surveyList = ExcelHelper.ToEntityList<SurveyImportDto>(testSurveyExcelPath);\n\n// SheetNameTest();\n\nFluentSettings.LoadMappingProfile<TestEntity, TestEntityExcelMappingProfile>();\nvar tempDirPath = $@\"{Environment.GetEnvironmentVariable(\"USERPROFILE\")}\\Desktop\\temp\\test\";\n\n{\n    // custom CsvSeparatorCharacter sample\n    var csvOptions = new CsvOptions() { SeparatorCharacter = '\\t' };\n    var text = CsvHelper.GetCsvText(new[]\n    {\n        new\n        {\n            Title = \"123\",\n            Desc = \"234\"\n        }\n    }, csvOptions);\n    var dt1233 = CsvHelper.ToDataTable(text.GetBytes(), csvOptions);\n}\n\n// image export/import test\n//var imageExcelPath = @\"C:\\Users\\Weiha\\Desktop\\temp\\test\\imageTest.xls\";\n//var imgaeModelList = ExcelHelper.ToEntityList<ImportImageTestModel>(imageExcelPath);\n//Console.WriteLine(imgaeModelList.Count(x => x?.Image is not null));\n//imgaeModelList.ToExcelFile(imageExcelPath + \".1.xls\");\n//var imgModeList2 = ExcelHelper.ToEntityList<ImportImageTestModel>(imageExcelPath + \".1.xls\");\n//Console.WriteLine($\"{imgaeModelList[0]?.Image?.Length},{imgModeList2[0]?.Image?.Length}\");\n//imgaeModelList.ToExcelFile(imageExcelPath + \".1.xlsx\");\n//Console.ReadLine();\n\n//FluentSettings.For<ppDto>()\n//    .HasSheetSetting(sheet =>\n//    {\n//        sheet.CellFilter = cell => cell.ColumnIndex <= 10;\n//    });\n//var tempExcelPath = Path.Combine(tempDirPath, \"testdata.xlsx\");\n//var t_list = ExcelHelper.ToEntityList<ppDto>(tempExcelPath);\n//var tempTable = ExcelHelper.ToDataTable(tempExcelPath);\n\n//var entityList = ExcelHelper.ToEntityList<TestEntity>(ApplicationHelper.MapPath(\"test.xlsx\"));\n\n//Console.WriteLine(\"Success!\");\n\n//var mapping = ExcelHelper.ToEntityList<ProductPriceMapping>($@\"{Environment.GetEnvironmentVariable(\"USERPROFILE\")}\\Desktop\\temp\\tempFiles\\mapping.xlsx\");\n\n//var mappingTemp = ExcelHelper.ToEntityList<ProductPriceMapping>($@\"{Environment.GetEnvironmentVariable(\"USERPROFILE\")}\\Desktop\\temp\\tempFiles\\mapping_temp.xlsx\");\n\n//Console.WriteLine($\"-----normal({mapping.Count}【{mapping.Select(_ => _.Pid).Distinct().Count()}】)----\");\n//foreach (var shop in mapping.GroupBy(_ => _.ShopCode).OrderBy(_ => _.Key))\n//{\n//    Console.WriteLine($\"{shop.Key}---{shop.Count()}---distinct pid count:{shop.Select(_ => _.Pid).Distinct().Count()}\");\n//}\n\n//Console.WriteLine($\"-----temp({mappingTemp.Count}【{mappingTemp.Select(_ => _.Pid).Distinct().Count()}】)----\");\n//foreach (var shop in mappingTemp.GroupBy(_ => _.ShopCode).OrderBy(_ => _.Key))\n//{\n//    Console.WriteLine($\"{shop.Key}---{shop.Count()}---distinct pid count:{shop.Select(_ => _.Pid).Distinct().Count()}\");\n//}\n\n//Console.WriteLine(\"Press Enter to continue...\");\n//Console.ReadLine();\n//var list2 = new List<TestEntity2?>();\n//list2.Add(null);\n//for (var i = 0; i < 100_000; i++)\n//{\n//    list2.Add(new TestEntity2\n//    {\n//        Id = i + 1,\n//        Title = $\"Title_{i}\",\n//        Description = $\"{Enumerable.Range(1, 200).StringJoin(\",\")}__{i}\",\n//    });\n//}\n//list2.Add(new TestEntity2()\n//{\n//    Id = 999,\n//    Title = $\"{Enumerable.Repeat(1, 10).StringJoin(\",\")}\",\n//    Description = null\n//});\n//var watch = Stopwatch.StartNew();\n//list2.ToExcelFile($@\"{tempDirPath}\\testEntity2.xls\");\n//watch.Stop();\n//Console.WriteLine($\"ElapsedMilliseconds: {watch.ElapsedMilliseconds}ms\");\n////var listTemp = ExcelHelper.ToEntityList<TestEntity2>($@\"{tempDirPath}\\testEntity2.xlsx\");\n//var dataTableTemp = ExcelHelper.ToDataTable($@\"{tempDirPath}\\testEntity2.xlsx\");\n\n//Console.WriteLine(\"Press Enter to continue...\");\n//Console.ReadLine();\n\nvar entities = new List<TestEntity>()\n    {\n        new TestEntity()\n        {\n            PKID = 1,\n            SettingId = Guid.NewGuid(),\n            SettingName = \"Setting1\",\n            SettingValue = \"Value1\",\n            DisplayName = \"dd\\\"d,1\"\n        },\n        new TestEntity()\n        {\n            PKID=2,\n            SettingId = Guid.NewGuid(),\n            SettingName = \"Setting2\",\n            SettingValue = \"Value2\",\n            Enabled = true,\n            CreatedBy = \"li\\\"_\"\n        },\n    };\nentities.ToCsvFile(\"test.csv\");\n\nvar csvFilePath = $@\"{tempDirPath}\\test.csv\";\n//entities.ToExcelFileByTemplate(\n//    Path.Combine(ApplicationHelper.AppRoot, \"Templates\", \"testTemplate.xlsx\"),\n//    ApplicationHelper.MapPath(\"templateTestEntities.xlsx\"),\n//    extraData: new\n//    {\n//        Author = \"WeihanLi\",\n//        Title = \"Export Result\"\n//    }\n//);\nentities.ToExcelFile(csvFilePath.Replace(\".csv\", \".xlsx\"));\nentities.ToCsvFile(csvFilePath);\n\nvar entitiesT0 = ExcelHelper.ToEntityList<TestEntity>(csvFilePath.Replace(\".csv\", \".xlsx\"));\n\nvar dataTable = entities.ToDataTable();\ndataTable.ToCsvFile(csvFilePath.Replace(\".csv\", \".datatable.csv\"));\nvar dt = CsvHelper.ToDataTable(csvFilePath.Replace(\".csv\", \".datatable.csv\"));\nConsole.WriteLine(dt.Columns.Count);\nvar entities1 = CsvHelper.ToEntityList<TestEntity>(csvFilePath);\n\nentities1[1]!.DisplayName = \",tadadada\";\nentities1[0]!.SettingValue = \"value2,345\";\nentities1.ToCsvFile(csvFilePath.Replace(\".csv\", \".1.csv\"));\nentities1.ToDataTable().ToCsvFile(csvFilePath.Replace(\".csv\", \".1.datatable.csv\"));\n\nvar list = CsvHelper.ToEntityList<TestEntity>(csvFilePath.Replace(\".csv\", \".1.csv\"));\ndt = CsvHelper.ToDataTable(csvFilePath.Replace(\".csv\", \".1.datatable.csv\"));\nConsole.WriteLine(dt.Columns.Count);\nvar entities2 = CsvHelper.ToEntityList<TestEntity>(csvFilePath.Replace(\".csv\", \".1.csv\"));\n\nentities.ToExcelFile(csvFilePath.Replace(\".csv\", \".xlsx\"));\n\nvar vals = new[] { 1, 2, 3, 5, 4 };\nvals.ToCsvFile(csvFilePath);\n\nvar numList = CsvHelper.ToEntityList<int>(csvFilePath);\nConsole.WriteLine(numList.StringJoin(\",\"));\n\nConsole.ReadLine();\n\nSheetNameTest();\n\nstatic void SheetNameTest()\n{\n    List<ExcelExportDTO> exprotDataList = new List<ExcelExportDTO>();\n    for (int i = 0; i < 10; i++)\n    {\n        var temp = new ExcelExportDTO\n        {\n            Name = \"张三\" + i,\n            Address = \"北京海淀\" + i,\n            Birthday = DateTime.Now,\n            Remark = \"Remark\" + i\n        };\n        exprotDataList.Add(temp);\n    }\n    var setting = FluentSettings.For<ExcelExportDTO>();\n    setting.HasSheetConfiguration(1, \"我是一个Sheet_111\", true);\n    setting.HasSheetSetting(s =>\n    {\n        s.SheetName = \"Shee-0000\";\n    });\n\n    var deskTopFullPath = System.Environment.GetFolderPath(Environment.SpecialFolder.Desktop);\n    var exportFileName = Path.Combine(deskTopFullPath, \"Test_for_weihanli.xlsx\");\n    exprotDataList.ToExcelFile(exportFileName);\n}\n\nclass TestEntityExcelMappingProfile : IMappingProfile<TestEntity>\n{\n    public void Configure(IExcelConfiguration<TestEntity> setting)\n    {\n        // ExcelSetting\n        setting.HasAuthor(\"WeihanLi\")\n            .HasTitle(\"WeihanLi.Npoi test\")\n            .HasDescription(\"WeihanLi.Npoi test\")\n            .HasSubject(\"WeihanLi.Npoi test\")\n            ;\n\n        setting.HasSheetSetting(config =>\n        {\n            config.StartRowIndex = 1;\n            config.SheetName = \"SystemSettingsList\";\n            config.AutoColumnWidthEnabled = true;\n\n            config.RowAction = row =>\n            {\n                if (row.RowNum == 0)\n                {\n                    var style = row.Sheet.Workbook.CreateCellStyle();\n                    style.Alignment = HorizontalAlignment.Center;\n                    var font = row.Sheet.Workbook.CreateFont();\n                    font.FontName = \"JetBrains Mono\";\n                    font.IsBold = true;\n                    font.FontHeight = 200;\n                    style.SetFont(font);\n                    row.Cells.ForEach(c => c.CellStyle = style);\n                }\n            };\n            config.CellAction = cell =>\n            {\n                if (cell.RowIndex == 0 && cell.StringCellValue == \"EntityType\")\n                {\n                    var enumNames = Enum.GetNames<EntityType>();\n                    var validationHelper = cell.Sheet.GetDataValidationHelper();\n                    var constraint = validationHelper.CreateExplicitListConstraint(enumNames);\n                    var addressList = new CellRangeAddressList(1, 3, cell.ColumnIndex, cell.ColumnIndex); // Col B\n                    var validation = validationHelper.CreateValidation(constraint, addressList);\n                    validation.ShowErrorBox = true;\n                    cell.Sheet.AddValidationData(validation);\n                }\n            };\n        });\n\n        // setting.HasFilter(0, 1).HasFreezePane(0, 1, 2, 1);\n\n        setting.Property(_ => _.SettingId)\n            .HasColumnIndex(0);\n\n        setting.Property(_ => _.SettingName)\n            .HasColumnTitle(\"SettingName\")\n            .HasColumnIndex(1);\n\n        setting.Property(_ => _.DisplayName)\n            .HasOutputFormatter((entity, displayName) => $\"AAA_{entity?.SettingName}_{displayName}\")\n            .HasInputFormatter((entity, originVal) => originVal?.Split(new[] { '_' })[2])\n            .HasColumnTitle(\"DisplayName\")\n            .HasColumnIndex(2);\n\n        setting.Property(_ => _.SettingValue)\n            .HasColumnTitle(\"SettingValue\")\n            .HasColumnIndex(3);\n\n        setting.Property(_ => _.CreatedTime)\n            .HasColumnTitle(\"CreatedTime\")\n            .HasColumnIndex(4)\n            .HasColumnWidth(10)\n            .HasColumnFormatter(\"yyyy-MM-dd HH:mm:ss\");\n\n        setting.Property(_ => _.CreatedBy)\n            .HasColumnInputFormatter(x => x += \"_test\")\n            .HasColumnIndex(4)\n            .HasColumnTitle(\"CreatedBy\");\n\n        setting.Property(x => x.Enabled)\n            .HasColumnInputFormatter(val => \"Enabled\".EqualsIgnoreCase(val))\n            .HasColumnOutputFormatter(v => v ? \"Enabled\" : \"Disabled\");\n\n        setting.Property(\"HiddenProp\")\n            .HasOutputFormatter((entity, val) => $\"HiddenProp_{entity?.PKID}\");\n\n        setting.Property(x => x.Type)\n            .HasColumnTitle(\"EntityType\")\n            .HasColumnIndex(8);\n\n        setting.Property(_ => _.PKID).Ignored();\n        setting.Property(_ => _.UpdatedBy).Ignored();\n        setting.Property(_ => _.UpdatedTime).Ignored();\n    }\n}\n\n\ninternal abstract class BaseEntity\n{\n    public int PKID { get; set; }\n}\n\ninternal class TestEntity : BaseEntity\n{\n    public Guid SettingId { get; set; }\n\n    public string? SettingName { get; set; }\n\n    public string? DisplayName { get; set; }\n    public string? SettingValue { get; set; }\n\n    public string CreatedBy { get; set; } = \"liweihan\";\n\n    public DateTime CreatedTime { get; set; } = DateTime.Now;\n\n    public string? UpdatedBy { get; set; }\n\n    public DateTime UpdatedTime { get; set; }\n\n    public bool Enabled { get; set; }\n\n    public EntityType Type { get; set; }\n}\n\npublic enum EntityType\n{\n    Default = 0,\n    Special = 1\n}\n\n[Sheet(SheetIndex = 0, SheetName = \"TestSheet\", AutoColumnWidthEnabled = true)]\ninternal class TestEntity2\n{\n    [Column(Index = 0)]\n    public int Id { get; set; }\n\n    [Column(Index = 1)]\n    public string? Title { get; set; }\n\n    [Column(Index = 2, Width = 50)]\n    public string? Description { get; set; }\n\n    [Column(Index = 3, Width = 20)]\n    public string? Extra { get; set; } = \"{}\";\n}\n\npublic class ExcelExportDTO\n{\n    [Column(\"姓名\")]\n    public string? Name { get; set; }\n    [Column(\"住址\")]\n    public string? Address { get; set; }\n    [Column(\"出生日期\")]\n    public DateTime Birthday { get; set; }\n    public string? Remark { get; set; }\n}\n\n#nullable disable\ninternal sealed class SurveyImportDto\n{\n    [Column(IsIgnored = true)]\n    public string ExternalId { get; set; }\n\n    [Column(0)]\n    public string ContentSource { get; set; }\n\n    [Column(1)]\n    public string ExternalKey { get; set; }\n\n    [Column(2)]\n    public string Title { get; set; }\n\n    [Column(3)]\n    public string OptionA { get; set; }\n\n    [Column(4)]\n    public string OptionB { get; set; }\n\n    [Column(5)]\n    public string OptionC { get; set; }\n\n    [Column(6)]\n    public string CorrectAnswer { get; set; }\n\n    [Column(7)]\n    public string Tips { get; set; }\n}\n\n#nullable restore\n"
  },
  {
    "path": "samples/DotNetCoreSample/TestModel.cs",
    "content": "﻿// Copyright (c) Weihan Li. All rights reserved.\n// Licensed under the Apache license.\n\nnamespace DotNetCoreSample;\n\n[Sheet(SheetIndex = 0, SheetName = \"Abc\", StartRowIndex = 1, EndRowIndex = 10, StartColumnIndex = 1, EndColumnIndex = 19)]\ninternal class ppDto\n{\n    [Column(\"创建日期\")]\n    public DateTime? CreateDate { get; set; }\n\n    [Column(\"体积\")]\n    public decimal? Volume { get; set; }\n\n    [Column(\"总重量\")]\n    public decimal? TotalWeight { get; set; }\n\n    [Column(\"出错\")]\n    public string? Error { get; set; }\n\n    [Column(\"真假\")]\n    public Boolean? TrueOrFalse { get; set; }\n\n    [Column(\"范围\")]\n    public string? Range { get; set; }\n\n    [Column(\"从\")]\n    public long? CtnFm { get; set; }\n\n    [Column(\"到\")]\n    public long? CtnTo { get; set; }\n\n    [Column(\"开始序列号\")]\n    public long? SerialStart { get; set; }\n\n    [Column(\"截止序列号\")]\n    public long? SerialEnd { get; set; }\n\n    [Column(\"包装代码\")]\n    public string? PackCode { get; set; }\n\n    [Column(\"行号\")]\n    public string? Row { get; set; }\n\n    [Column(\"买方项目号\")]\n    public string? BuyerNo { get; set; }\n\n    [Column(\"SKU号\")]\n    public string? SKUNo { get; set; }\n\n    [Column(\"订单号码\")]\n    public string? TradingPO { get; set; }\n\n    [Column(\"MAIN LINE #\")]\n    public string? Item { get; set; }\n\n    [Column(\"Color Name\")]\n    public string? ColorCode { get; set; }\n\n    [Column(\"Size\")]\n    public string? Size { get; set; }\n\n    [Column(\"简短描述\")]\n    public string? ContractColor { get; set; }\n\n    [Column(\"发货方式\")]\n    public string? ShipWay { get; set; }\n\n    [Column(\"数量\")]\n    public int? TtlQty { get; set; }\n\n    [Column(\"内部包装的项目数量\")]\n    public int? Qty { get; set; }\n\n    [Column(\"内包装计数\")]\n    public int? RatioQty { get; set; }\n\n    [Column(\"箱数\")]\n    public int? CntQty { get; set; }\n\n    [Column(\"R\")]\n    public string? LastCarton { get; set; }\n\n    [Column(\"外箱代码\")]\n    public string? CartonCode { get; set; }\n\n    [Column(\"净净重\")]\n    public decimal? NNWeight { get; set; }\n\n    [Column(\"净重\")]\n    public decimal? NWeight { get; set; }\n\n    [Column(\"毛重\")]\n    public decimal? GWeight { get; set; }\n\n    [Column(\"单位\")]\n    public string? Unit { get; set; }\n\n    [Column(\"长\")]\n    public decimal? cartonL { get; set; }\n\n    [Column(\"宽\")]\n    public decimal? cartonW { get; set; }\n\n    [Column(\"高\")]\n    public decimal? cartonH { get; set; }\n\n    [Column(\"单位2\")]\n    public string? Unit2 { get; set; }\n\n    [Column(\"扫描ID\")]\n    public string? ScanID { get; set; }\n}\n"
  },
  {
    "path": "samples/run-file-samples/issue-169.cs",
    "content": "﻿var filePath = @\"C:\\Users\\Weiha\\Downloads\\test\\2.xlsx\";\nvar workbook = ExcelHelper.LoadExcel(\n    File.OpenRead(filePath),\n    ExcelFormat.Xlsx\n);\nvar settings = FluentSettings.For<MaterielDetailDto>();\nsettings.WithPostImportAction((x, rowIndex) => x?.RowNum = rowIndex + 1);\nvar list = workbook.ToEntityList<MaterielDetailDto>();\nforeach (var item in list)\n{\n    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}\");\n}\n\n[Sheet(SheetIndex = 0, StartRowIndex = 6)]\npublic class MaterielDetailDto\n{\n    public int RowNum { get; set; }\n\n    /// <summary>\n    /// 编号\n    /// </summary>\n    [Column(\"物料编号\", Index = 0)]\n    public string No { get; set; }\n\n    /// <summary>\n    /// 物料名称\n    /// </summary>\n    [Column(\"物料名称\", Index = 1)]\n    public string Name { get; set; }\n\n    /// <summary>\n    /// 规格型号\n    /// </summary>\n    [Column(\"规格型号\", Index = 2)]\n    public string Specification { get; set; }\n\n    /// <summary>\n    /// 特殊库存\n    /// </summary>\n    [Column(\"特殊库存\", Index = 3)]\n    public string QuantityM { get; set; }\n\n    /// <summary>\n    /// 单位\n    /// </summary>\n    [Column(\"计量单位\", Index = 4)]\n    public string Unit { get; set; }\n\n    [Column(\"会计年度\", Index = 5)]\n    public int Ytime { get; set; }\n\n    [Column(\"会计期间\", Index = 6)]\n    public int Mtime { get; set; }\n\n    // 期初余额\n    [Column(\"方向\", Index = 7)]\n    public string Direction1 { get; set; }\n\n    /// <summary>\n    /// 数量\n    /// </summary>\n    [Column(\"数量\", Index = 8)]\n    public decimal Quantity1 { get; set; }\n\n    /// <summary>\n    /// 单价\n    /// </summary>\n    [Column(\"实际单价\", Index = 9)]\n    public decimal UnitPrice1 { get; set; }\n\n    /// <summary>\n    /// 总金额\n    /// </summary>\n    [Column(\"实际金额\", Index = 10)]\n    public decimal TotalAmount1 { get; set; }\n\n    /// <summary>\n    /// 标准单价\n    /// </summary>\n    [Column(\"标准单价\", Index = 11)]\n    public decimal StandardUnitPrice1 { get; set; }\n\n    /// <summary>\n    /// 标准金额\n    /// </summary>\n    [Column(\"标准金额\", Index = 12)]\n    public decimal StandardotalAmount1 { get; set; }\n\n    // 期初余额\n\n    //本期借方\n    /// <summary>\n    /// 数量\n    /// </summary>\n    [Column(\"数量\", Index = 13)]\n    public decimal Quantity2 { get; set; }\n\n    /// <summary>\n    /// 单价\n    /// </summary>\n    [Column(\"实际单价\", Index = 14)]\n    public decimal UnitPrice2 { get; set; }\n\n    /// <summary>\n    /// 总金额\n    /// </summary>\n    [Column(\"实际金额\", Index = 15)]\n    public decimal TotalAmount2 { get; set; }\n\n    /// <summary>\n    /// 标准单价\n    /// </summary>\n    [Column(\"标准单价\", Index = 16)]\n    public decimal StandardUnitPrice2 { get; set; }\n\n    /// <summary>\n    /// 标准金额\n    /// </summary>\n    [Column(\"标准金额\", Index = 17)]\n    public decimal StandardotalAmount2 { get; set; }\n\n    //本期贷方\n\n    /// <summary>\n    /// 数量\n    /// </summary>\n    [Column(\"数量\", Index = 18)]\n    public decimal Quantity3 { get; set; }\n\n    /// <summary>\n    /// 单价\n    /// </summary>\n    [Column(\"实际单价\", Index = 19)]\n    public decimal UnitPrice3 { get; set; }\n\n    /// <summary>\n    /// 总金额\n    /// </summary>\n    [Column(\"实际金额\", Index = 20)]\n    public decimal TotalAmount3 { get; set; }\n\n    /// <summary>\n    /// 标准单价\n    /// </summary>\n    [Column(\"标准单价\", Index = 21)]\n    public decimal StandardUnitPrice3 { get; set; }\n\n    /// <summary>\n    /// 标准金额\n    /// </summary>\n    [Column(\"标准金额\", Index = 22)]\n    public decimal StandardotalAmount3 { get; set; }\n\n    //期末余额\n    [Column(\"方向\", Index = 23)]\n    public string Direction2 { get; set; }\n\n    /// <summary>\n    /// 数量\n    /// </summary>\n    [Column(\"数量\", Index = 24)]\n    public decimal Quantity4 { get; set; }\n\n    /// <summary>\n    /// 实际单价\n    /// </summary>\n    [Column(\"实际单价\", Index = 25)]\n    public decimal UnitPrice4 { get; set; }\n\n    /// <summary>\n    /// 总金额\n    /// </summary>\n    [Column(\"实际金额\", Index = 26)]\n    public decimal TotalAmount4 { get; set; }\n\n    [Column(\"核算类别名称\", Index = 27)]\n    public string CategoryName { get; set; }\n\n    [Column(\"是否可用\", Index = 28)]\n    public string IsStop { get; set; }\n}\n"
  },
  {
    "path": "samples/run-file-samples/style-customization-sample.cs",
    "content": "﻿using NPOI.SS.UserModel;\nusing NPOI.SS.Util;\n\nFluentSettings.LoadMappingProfile<StyledEntity, StyledEntityProfile>();\n\nvar list = new List<StyledEntity>\n{\n    new StyledEntity { Id = 1, Name = \"Alice\", Amount = 1500.50m, Status = \"Approved\", Date = DateTime.Now.AddDays(-10) },\n    new StyledEntity { Id = 2, Name = \"Bob\", Amount = -200.75m, Status = \"Pending\", Date = DateTime.Now.AddDays(-5) },\n    new StyledEntity { Id = 3, Name = \"Charlie\", Amount = 300.00m, Status = \"Rejected\", Date = DateTime.Now.AddDays(-2) },\n    new StyledEntity { Id = 4, Name = \"Diana\", Amount = 450.25m, Status = \"Approved\", Date = DateTime.Now.AddDays(-1) },\n};\nconst string path = @\"C:\\Users\\Weiha\\Downloads\\test\\styled-report.xlsx\";\nlist.ToExcelFile(path);\nConsole.WriteLine($\"Excel file generated at: {path}\");\n\npublic class StyledEntity\n{\n    public int Id { get; set; }\n    public required string Name { get; set; }\n    public decimal Amount { get; set; }\n    public required string Status { get; set; }\n    public DateTime Date { get; set; }\n}\n\npublic class StyledEntityProfile : IMappingProfile<StyledEntity>\n{\n    public void Configure(IExcelConfiguration<StyledEntity> configuration)\n    {\n        configuration.HasAuthor(\"Spark\")\n            .HasTitle(\"Styled Report\")\n            .HasDescription(\"Professional styled Excel report\");\n\n        configuration.HasSheetSetting(config =>\n        {\n            config.SheetName = \"Report\";\n            config.StartRowIndex = 1;\n            config.AutoColumnWidthEnabled = true;\n\n            // Style header row\n            config.RowAction = row =>\n            {\n                if (row.RowNum == 0)\n                {\n                    var headerStyle = row.Sheet.Workbook.CreateCellStyle();\n                    headerStyle.Alignment = HorizontalAlignment.Center;\n                    headerStyle.VerticalAlignment = VerticalAlignment.Center;\n                    headerStyle.FillForegroundColor = IndexedColors.Grey25Percent.Index;\n                    headerStyle.FillPattern = FillPattern.SolidForeground;\n                    \n                    var headerFont = row.Sheet.Workbook.CreateFont();\n                    headerFont.FontName = \"JetBrains Mono\";\n                    headerFont.IsBold = true;\n                    headerFont.FontHeight = 240; // 12pt\n                    headerStyle.SetFont(headerFont);\n                    \n                    // Add borders\n                    headerStyle.BorderBottom = BorderStyle.Thin;\n                    headerStyle.BorderTop = BorderStyle.Thin;\n                    headerStyle.BorderLeft = BorderStyle.Thin;\n                    headerStyle.BorderRight = BorderStyle.Thin;\n                    \n                    row.Cells.ForEach(c => c.CellStyle = headerStyle);\n                }\n            };\n\n            // Add validation and conditional formatting\n            config.CellAction = cell =>\n            {\n                // Add validation for status column\n                if (cell.RowIndex == 0 && cell.StringCellValue == \"Status\")\n                {\n                    var validationHelper = cell.Sheet.GetDataValidationHelper();\n                    var statusList = new[] { \"Approved\", \"Pending\", \"Rejected\" };\n                    var constraint = validationHelper.CreateExplicitListConstraint(statusList);\n                    var addressList = new CellRangeAddressList(1, 1000, cell.ColumnIndex, cell.ColumnIndex);\n                    var validation = validationHelper.CreateValidation(constraint, addressList);\n                    validation.ShowErrorBox = true;\n                    cell.Sheet.AddValidationData(validation);\n                }\n                \n                // Highlight negative amounts in red\n                if (cell.RowIndex > 0 && cell.ColumnIndex == 2) // Amount column\n                {\n                    try\n                    {\n                        if (cell.NumericCellValue < 0)\n                        {\n                            var redStyle = cell.Sheet.Workbook.CreateCellStyle();\n                            var redFont = cell.Sheet.Workbook.CreateFont();\n                            redFont.Color = IndexedColors.Red.Index;\n                            redFont.IsBold = true;\n                            redStyle.SetFont(redFont);\n                            cell.CellStyle = redStyle;\n                        }\n                    }\n                    catch { } // Skip if not a numeric cell\n                }\n            };\n        });\n\n        // Configure properties\n        configuration.Property(x => x.Id).HasColumnIndex(0);\n        configuration.Property(x => x.Name).HasColumnIndex(1);\n        configuration.Property(x => x.Amount).HasColumnIndex(2);\n        configuration.Property(x => x.Status).HasColumnIndex(3);\n        configuration.Property(x => x.Date)\n            .HasColumnIndex(4)\n            .HasColumnFormatter(\"yyyy-MM-dd\");\n    }\n}\n"
  },
  {
    "path": "src/Directory.Build.props",
    "content": "<Project>\n  <Import Project=\"$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))\" />\n  <Import Project=\"../build/sign.props\" />\n  <PropertyGroup>    \n    <GenerateDocumentationFile>true</GenerateDocumentationFile>\n    <NoWarn>$(NoWarn);1591</NoWarn>\n    <RepositoryUrl>https://github.com/WeihanLi/WeihanLi.Common</RepositoryUrl>\n    <!-- Optional: Publish the repository URL in the built .nupkg (in the NuSpec <Repository> element) -->\n    <PublishRepositoryUrl>true</PublishRepositoryUrl>\n    <!-- Optional: Embed source files that are not tracked by the source control manager in the PDB -->\n    <EmbedUntrackedSources>true</EmbedUntrackedSources>\n    <!-- Optional: Build symbol package (.snupkg) to distribute the PDB containing Source Link -->\n    <IncludeSymbols>true</IncludeSymbols>\n    <SymbolPackageFormat>snupkg</SymbolPackageFormat>    \n    <PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>\n    <PackageReleaseNotes>\n      https://github.com/WeihanLi/WeihanLi.Common/blob/dev/docs/ReleaseNotes.md\n    </PackageReleaseNotes>\n    <PackageIcon>icon.jpg</PackageIcon>\n    <PackageReadmeFile>README.md</PackageReadmeFile>\n    <PackageLicenseExpression>MIT</PackageLicenseExpression>\n    <ContinuousIntegrationBuild Condition=\"'$(TF_BUILD)' == 'true' or '$(GITHUB_ACTIONS)' == 'true'\">true</ContinuousIntegrationBuild>\n  </PropertyGroup>\n  <ItemGroup>\n    <None Include=\"$([MSBuild]::GetPathOfFileAbove('README.md', '$(MSBuildThisFileDirectory)../'))\" Pack=\"true\" PackagePath=\"\\\" />\n    <None Include=\"$([MSBuild]::GetPathOfFileAbove('icon.jpg', '$(MSBuildThisFileDirectory)../'))\" Pack=\"true\" Visible=\"false\" PackagePath=\"\" />    \n  </ItemGroup>\n</Project>\n"
  },
  {
    "path": "src/WeihanLi.Npoi/Abstract/ICell.cs",
    "content": "﻿// Copyright (c) Weihan Li. All rights reserved.\n// Licensed under the Apache license.\n\nnamespace WeihanLi.Npoi.Abstract;\n\ninternal interface ICell\n{\n    CellType CellType { get; set; }\n\n    object? Value { get; set; }\n}\n\ninternal enum CellType\n{\n    Unknown = -1, // 0xFFFFFFFF\n    String = 0,\n    Numeric = 1,\n    Formula = 2,\n    Blank = 3,\n    Boolean = 4,\n    Error = 5\n}\n"
  },
  {
    "path": "src/WeihanLi.Npoi/Abstract/IRow.cs",
    "content": "﻿// Copyright (c) Weihan Li. All rights reserved.\n// Licensed under the Apache license.\n\nnamespace WeihanLi.Npoi.Abstract;\n\ninternal interface IRow\n{\n    /// <summary>\n    ///     Gets the number of defined cells (NOT number of cells in the actual row!).\n    ///     That is to say if only columns 0,4,5 have values then there would be 3.\n    /// </summary>\n    /// <returns>int representing the number of defined cells in the row.</returns>\n    int CellsCount { get; }\n\n    /// <summary>\n    ///     1-based column number of the first cell\n    /// </summary>\n    int FirstCellNum { get; }\n\n    /// <summary>\n    ///     1-based column number of the last cell\n    /// </summary>\n    int LastCellNum { get; }\n\n    /// <summary>\n    ///     UnderlyingValue\n    /// </summary>\n    object? UnderlyingValue { get; }\n\n    ICell? GetCell(int cellIndex);\n\n    /// <summary>\n    ///     Create a cell\n    /// </summary>\n    /// <param name=\"cellIndex\">\n    ///     cellIndex\n    ///     maxValue: (255 for *.xls, 1048576 for *.xlsx)\n    /// </param>\n    /// <returns></returns>\n    ICell CreateCell(int cellIndex);\n}\n"
  },
  {
    "path": "src/WeihanLi.Npoi/Abstract/ISheet.cs",
    "content": "﻿// Copyright (c) Weihan Li. All rights reserved.\n// Licensed under the Apache license.\n\nnamespace WeihanLi.Npoi.Abstract;\n\ninternal interface ISheet\n{\n    /// <summary>\n    ///     FirstRowNum, 1 based rowNum\n    ///     0 if no rows here\n    /// </summary>\n    int FirstRowNum { get; }\n\n    /// <summary>\n    ///     lastRowIndex +1, 1 based rowNum\n    ///     0 if no rows here\n    /// </summary>\n    int LastRowNum { get; }\n\n    IRow? GetRow(int rowIndex);\n\n    IRow CreateRow(int rowIndex);\n\n    void SetColumnWidth(int columnIndex, int width);\n\n    void AutoSizeColumn(int columnIndex);\n\n    void CreateFreezePane(int colSplit, int rowSplit, int leftMostCol, int topRow);\n\n    void SetAutoFilter(int firstRowIndex, int lastRowIndex, int firstColumnIndex, int lastColumnIndex);\n\n    void ShiftRows(int startRow, int endRow, int n);\n\n    IRow CopyRow(int sourceIndex, int targetIndex);\n\n    void RemoveRow(IRow row);\n}\n"
  },
  {
    "path": "src/WeihanLi.Npoi/Abstract/IWorkbook.cs",
    "content": "﻿// Copyright (c) Weihan Li. All rights reserved.\n// Licensed under the Apache license.\n\nnamespace WeihanLi.Npoi.Abstract;\n\ninternal interface IWorkbook\n{\n    int SheetCount { get; }\n\n    ISheet? GetSheet(int sheetIndex);\n\n    ISheet CreateSheet(string sheetName);\n\n    byte[] ToBytes();\n}\n"
  },
  {
    "path": "src/WeihanLi.Npoi/Abstract/NPOIWorkbook.cs",
    "content": "﻿// Copyright (c) Weihan Li. All rights reserved.\n// Licensed under the Apache license.\n\nusing NPOI.SS.Util;\nusing NModel = NPOI.SS.UserModel;\n\nnamespace WeihanLi.Npoi.Abstract;\n\n/// <summary>\n/// Thin adapter that exposes the internal NPOI workbook via the abstraction interfaces.\n/// </summary>\ninternal sealed class NPOIWorkbook : IWorkbook\n{\n    private readonly NModel.IWorkbook _workbook;\n\n    /// <summary>\n    ///     Creates a new adapter for the provided NPOI workbook.\n    /// </summary>\n    /// <param name=\"workbook\">Underlying workbook instance.</param>\n    public NPOIWorkbook(NModel.IWorkbook workbook) => _workbook = workbook;\n\n    /// <summary>\n    ///     Gets the number of sheets available.\n    /// </summary>\n    public int SheetCount => _workbook.NumberOfSheets;\n\n    /// <summary>\n    ///     Wraps the specified sheet index in an <see cref=\"ISheet\" /> adapter.\n    /// </summary>\n    public ISheet GetSheet(int sheetIndex) => new NPOISheet(_workbook.GetSheetAt(sheetIndex));\n\n    /// <summary>\n    ///     Creates a new sheet and returns the adapter around it.\n    /// </summary>\n    public ISheet CreateSheet(string sheetName) => new NPOISheet(_workbook.CreateSheet(sheetName));\n\n    /// <summary>\n    ///     Serializes the workbook to bytes using helper extensions.\n    /// </summary>\n    public byte[] ToBytes() => _workbook.ToExcelBytes();\n}\n\n/// <summary>\n/// Adapter for <see cref=\"NModel.ISheet\" />.\n/// </summary>\ninternal sealed class NPOISheet : ISheet\n{\n    private readonly NModel.ISheet _sheet;\n\n    /// <summary>\n    ///     Initializes the adapter with the underlying sheet.\n    /// </summary>\n    public NPOISheet(NModel.ISheet sheet) => _sheet = sheet;\n\n    /// <summary>\n    ///     Gets the first row index using one-based indexing to align with the abstractions.\n    /// </summary>\n    public int FirstRowNum => _sheet.FirstRowNum + 1;\n\n    /// <summary>\n    ///     Gets the last row index using one-based indexing to align with the abstractions.\n    /// </summary>\n    public int LastRowNum => _sheet.LastRowNum + 1;\n\n    /// <summary>\n    ///     Retrieves the requested row and wraps it, if present.\n    /// </summary>\n    public IRow? GetRow(int rowIndex)\n    {\n        var nRow = _sheet.GetRow(rowIndex);\n        if (null == nRow)\n        {\n            return null;\n        }\n\n        return new NPOIRow(nRow);\n    }\n\n    /// <summary>\n    ///     Creates a new row and returns its adapter.\n    /// </summary>\n    public IRow CreateRow(int rowIndex) => new NPOIRow(_sheet.CreateRow(rowIndex));\n\n    /// <summary>\n    ///     Sets the column width in the underlying sheet.\n    /// </summary>\n    public void SetColumnWidth(int columnIndex, int width) => _sheet.SetColumnWidth(columnIndex, width);\n\n    /// <summary>\n    ///     Auto sizes the requested column.\n    /// </summary>\n    public void AutoSizeColumn(int columnIndex) => _sheet.AutoSizeColumn(columnIndex);\n\n    /// <summary>\n    ///     Applies a freeze pane to the sheet.\n    /// </summary>\n    public void CreateFreezePane(int colSplit, int rowSplit, int leftMostCol, int topRow) =>\n        _sheet.CreateFreezePane(colSplit, rowSplit, leftMostCol, topRow);\n\n    /// <summary>\n    ///     Applies an auto-filter range to the sheet.\n    /// </summary>\n    public void SetAutoFilter(int firstRowIndex, int lastRowIndex, int firstColumnIndex, int lastColumnIndex) =>\n        _sheet.SetAutoFilter(new CellRangeAddress(firstRowIndex, lastRowIndex, firstColumnIndex, lastColumnIndex));\n\n    /// <summary>\n    ///     Shifts the specified row range.\n    /// </summary>\n    public void ShiftRows(int startRow, int endRow, int n) => _sheet.ShiftRows(startRow, endRow, n);\n\n    /// <summary>\n    ///     Copies a row and wraps the result.\n    /// </summary>\n    public IRow CopyRow(int sourceIndex, int targetIndex) => new NPOIRow(_sheet.CopyRow(sourceIndex, targetIndex));\n\n    /// <summary>\n    ///     Removes the given row from the sheet.\n    /// </summary>\n    public void RemoveRow(IRow row) => _sheet.RemoveRow(row.UnderlyingValue as NModel.IRow);\n}\n\n/// <summary>\n/// Adapter for <see cref=\"NModel.IRow\" />.\n/// </summary>\ninternal sealed class NPOIRow : IRow\n{\n    private readonly NModel.IRow _row;\n\n    /// <summary>\n    ///     Initializes the adapter with the underlying row.\n    /// </summary>\n    public NPOIRow(NModel.IRow row) => _row = row;\n\n    /// <summary>\n    ///     Gets the number of physical cells.\n    /// </summary>\n    public int CellsCount => _row.PhysicalNumberOfCells;\n\n    /// <summary>\n    ///     Gets the first cell index using one-based indexing.\n    /// </summary>\n    public int FirstCellNum => _row.FirstCellNum + 1;\n\n    /// <summary>\n    ///     Gets the last cell index.\n    /// </summary>\n    public int LastCellNum => _row.LastCellNum;\n\n    /// <summary>\n    ///     Retrieves and wraps the specified cell.\n    /// </summary>\n    public ICell? GetCell(int cellIndex)\n    {\n        var nCell = _row.GetCell(cellIndex);\n        if (nCell is null)\n        {\n            return null;\n        }\n\n        return new NPOICell(nCell);\n    }\n\n    /// <summary>\n    ///     Creates and wraps a new cell.\n    /// </summary>\n    public ICell CreateCell(int cellIndex) => new NPOICell(_row.CreateCell(cellIndex));\n\n    /// <summary>\n    ///     Provides direct access to the underlying NPOI object.\n    /// </summary>\n    public object UnderlyingValue => _row;\n}\n\n/// <summary>\n/// Adapter for <see cref=\"NModel.ICell\" />.\n/// </summary>\ninternal sealed class NPOICell : ICell\n{\n    private readonly NModel.ICell _cell;\n\n    /// <summary>\n    ///     Initializes the adapter with the underlying cell.\n    /// </summary>\n    public NPOICell(NModel.ICell cell) => _cell = cell;\n\n    /// <summary>\n    ///     Gets or sets the cell type using the abstraction enum.\n    /// </summary>\n    public CellType CellType\n    {\n        get => (CellType)Enum.Parse(typeof(CellType), _cell.CellType.ToString());\n        set => _cell.SetCellType((NModel.CellType)Enum.Parse(typeof(NModel.CellType), value.ToString()));\n    }\n\n    /// <summary>\n    ///     Gets or sets the cell value while translating common NPOI types.\n    /// </summary>\n    public object? Value\n    {\n        get\n        {\n            if (_cell.CellType == NModel.CellType.Blank || _cell.CellType == NModel.CellType.Error)\n            {\n                return null;\n            }\n\n            switch (_cell.CellType)\n            {\n                case NModel.CellType.Numeric:\n                    if (NModel.DateUtil.IsCellDateFormatted(_cell))\n                    {\n                        return _cell.DateCellValue;\n                    }\n\n                    return _cell.NumericCellValue;\n\n                case NModel.CellType.String:\n                    return _cell.StringCellValue;\n\n                case NModel.CellType.Boolean:\n                    return _cell.BooleanCellValue;\n\n                default:\n                    return _cell.ToString();\n            }\n        }\n\n        set => _cell.SetCellValue(value ?? string.Empty);\n    }\n}\n"
  },
  {
    "path": "src/WeihanLi.Npoi/Attributes/ColumnAttribute.cs",
    "content": "﻿// Copyright (c) Weihan Li. All rights reserved.\n// Licensed under the Apache license.\n\nusing WeihanLi.Npoi.Configurations;\n\nnamespace WeihanLi.Npoi.Attributes;\n\n/// <summary>\n/// Describes column-level metadata for a property.\n/// </summary>\n[AttributeUsage(AttributeTargets.Property)]\npublic sealed class ColumnAttribute : Attribute\n{\n    /// <summary>\n    ///     Initializes a column attribute with default configuration.\n    /// </summary>\n    public ColumnAttribute() => PropertyConfiguration = new PropertyConfiguration();\n\n    /// <summary>\n    ///     Initializes a column attribute targeting the specified index.\n    /// </summary>\n    /// <param name=\"index\">Target column index.</param>\n    public ColumnAttribute(int index) => PropertyConfiguration = new PropertyConfiguration { ColumnIndex = index };\n\n    /// <summary>\n    ///     Initializes a column attribute with the provided title.\n    /// </summary>\n    /// <param name=\"title\">Column header title.</param>\n    public ColumnAttribute(string title) => PropertyConfiguration = new PropertyConfiguration\n    {\n        ColumnTitle = title ?? throw new ArgumentNullException(nameof(title))\n    };\n\n    internal PropertyConfiguration PropertyConfiguration { get; }\n\n    /// <summary>\n    ///     ColumnIndex\n    /// </summary>\n    public int Index\n    {\n        get => PropertyConfiguration.ColumnIndex;\n        set\n        {\n            if (value >= 0)\n            {\n                PropertyConfiguration.ColumnIndex = value;\n            }\n        }\n    }\n\n    /// <summary>\n    ///     ColumnTitle\n    /// </summary>\n    public string Title\n    {\n        get => PropertyConfiguration.ColumnTitle;\n        set => PropertyConfiguration.ColumnTitle = value;\n    }\n\n    /// <summary>\n    ///     Formatter\n    /// </summary>\n    public string? Formatter\n    {\n        get => PropertyConfiguration.ColumnFormatter;\n        set => PropertyConfiguration.ColumnFormatter = value;\n    }\n\n    /// <summary>\n    ///     IsIgnored\n    /// </summary>\n    public bool IsIgnored\n    {\n        get => PropertyConfiguration.IsIgnored;\n        set => PropertyConfiguration.IsIgnored = value;\n    }\n\n    /// <summary>\n    ///     ColumnWidth\n    ///     Characters Count\n    /// </summary>\n    public int Width\n    {\n        get => PropertyConfiguration.ColumnWidth;\n        set => PropertyConfiguration.ColumnWidth = value;\n    }\n}\n"
  },
  {
    "path": "src/WeihanLi.Npoi/Attributes/FilterAttribute.cs",
    "content": "﻿// Copyright (c) Weihan Li. All rights reserved.\n// Licensed under the Apache license.\n\nusing WeihanLi.Npoi.Settings;\n\nnamespace WeihanLi.Npoi.Attributes;\n\n/// <summary>\n/// Specifies the auto-filter range that should be applied to a sheet.\n/// </summary>\n[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]\npublic sealed class FilterAttribute : Attribute\n{\n    /// <summary>\n    ///     Initializes the attribute for a single column.\n    /// </summary>\n    public FilterAttribute(int firstColumn) : this(firstColumn, null)\n    {\n    }\n\n    /// <summary>\n    ///     Initializes the attribute for an explicit column range.\n    /// </summary>\n    /// <param name=\"firstColumn\">First column index.</param>\n    /// <param name=\"lastColumn\">Last column index.</param>\n    public FilterAttribute(int firstColumn, int? lastColumn) =>\n        FilterSetting = new FilterSetting(firstColumn, lastColumn);\n\n    internal FilterSetting FilterSetting { get; }\n\n    /// <summary>\n    ///     Gets or sets the first column index.\n    /// </summary>\n    public int FirstColumn => FilterSetting.FirstColumn;\n\n    /// <summary>\n    ///     Gets or sets the last column index.\n    /// </summary>\n    public int? LastColumn => FilterSetting.LastColumn;\n}\n"
  },
  {
    "path": "src/WeihanLi.Npoi/Attributes/FreezeAttribute.cs",
    "content": "﻿// Copyright (c) Weihan Li. All rights reserved.\n// Licensed under the Apache license.\n\nusing WeihanLi.Npoi.Settings;\n\nnamespace WeihanLi.Npoi.Attributes;\n\n/// <summary>\n/// Declares a freeze pane for a mapped sheet.\n/// </summary>\n[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]\npublic sealed class FreezeAttribute : Attribute\n{\n    /// <summary>\n    ///     Initializes a freeze pane using default anchors.\n    /// </summary>\n    public FreezeAttribute(int colSplit, int rowSplit) : this(colSplit, rowSplit, 0, 1)\n    {\n    }\n\n    /// <summary>\n    ///     Initializes a freeze pane with explicit anchors.\n    /// </summary>\n    /// <param name=\"colSplit\">Horizontal split position.</param>\n    /// <param name=\"rowSplit\">Vertical split position.</param>\n    /// <param name=\"leftmostColumn\">Left column visible in right pane.</param>\n    /// <param name=\"topRow\">Top row visible in bottom pane.</param>\n    public FreezeAttribute(int colSplit, int rowSplit, int leftmostColumn, int topRow) =>\n        FreezeSetting = new FreezeSetting(colSplit, rowSplit, leftmostColumn, topRow);\n\n    internal FreezeSetting FreezeSetting { get; }\n\n    /// <summary>\n    ///     Horizontal position of split\n    /// </summary>\n    public int ColSplit => FreezeSetting.ColSplit;\n\n    /// <summary>\n    ///     Vertical position of split\n    /// </summary>\n    public int RowSplit => FreezeSetting.RowSplit;\n\n    /// <summary>\n    ///     Top row visible in bottom pane\n    /// </summary>\n    public int LeftMostColumn => FreezeSetting.LeftMostColumn;\n\n    /// <summary>\n    ///     Left column visible in right pane\n    /// </summary>\n    public int TopRow => FreezeSetting.TopRow;\n}\n"
  },
  {
    "path": "src/WeihanLi.Npoi/Attributes/SheetAttribute.cs",
    "content": "﻿// Copyright (c) Weihan Li. All rights reserved.\n// Licensed under the Apache license.\n\nusing WeihanLi.Npoi.Settings;\n\nnamespace WeihanLi.Npoi.Attributes;\n\n/// <summary>\n/// Declares per-sheet metadata for an entity mapping.\n/// </summary>\n[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]\npublic sealed class SheetAttribute : Attribute\n{\n    private int _endColumnIndex = -1;\n\n    private int _startColumnIndex;\n\n    /// <summary>\n    ///     Initializes an attribute backed by a fresh <see cref=\"SheetSetting\" />.\n    /// </summary>\n    public SheetAttribute() => SheetSetting = new SheetSetting();\n\n    /// <summary>\n    ///     Target sheet index (zero-based).\n    /// </summary>\n    public int SheetIndex { get; set; }\n\n    /// <summary>\n    ///     Gets or sets the sheet name override.\n    /// </summary>\n    public string SheetName\n    {\n        get => SheetSetting.SheetName;\n        set => SheetSetting.SheetName = value ?? throw new ArgumentNullException(nameof(value));\n    }\n\n    /// <summary>\n    ///     Gets or sets the first row to start reading/writing (zero-based).\n    /// </summary>\n    public int StartRowIndex\n    {\n        get => SheetSetting.StartRowIndex;\n        set => SheetSetting.StartRowIndex = value;\n    }\n\n    /// <summary>\n    ///     Gets the header row index.\n    /// </summary>\n    public int HeaderRowIndex => SheetSetting.HeaderRowIndex;\n\n    /// <summary>\n    ///     Gets or sets the last row (inclusive) participating in the mapping.\n    /// </summary>\n    public int EndRowIndex\n    {\n        get => SheetSetting.EndRowIndex ?? -1;\n        set => SheetSetting.EndRowIndex = value >= 0 ? value : -1;\n    }\n\n    /// <summary>\n    ///     StartColumnIndex\n    ///     Start Column Index when import\n    /// </summary>\n    public int StartColumnIndex\n    {\n        get => _startColumnIndex;\n        set\n        {\n            if (value >= 0)\n            {\n                _startColumnIndex = value;\n                if (_endColumnIndex >= value)\n                {\n                    SheetSetting.CellFilter = cell =>\n                            cell.ColumnIndex >= _startColumnIndex && cell.ColumnIndex <= _endColumnIndex\n                        ;\n                }\n                else\n                {\n                    SheetSetting.CellFilter = cell => cell.ColumnIndex >= _startColumnIndex;\n                }\n            }\n        }\n    }\n\n    /// <summary>\n    ///     EndColumnIndex\n    ///     End Column Index when import\n    /// </summary>\n    public int EndColumnIndex\n    {\n        get => _endColumnIndex;\n        set\n        {\n            if (value >= _startColumnIndex)\n            {\n                _endColumnIndex = value;\n                SheetSetting.CellFilter = cell =>\n                        cell.ColumnIndex >= _startColumnIndex && cell.ColumnIndex <= _endColumnIndex\n                    ;\n            }\n            else\n            {\n                SheetSetting.CellFilter = cell => cell.ColumnIndex >= _startColumnIndex;\n                _endColumnIndex = -1;\n            }\n        }\n    }\n\n    /// <summary>\n    ///     Gets or sets whether column widths should be auto-sized.\n    /// </summary>\n    public bool AutoColumnWidthEnabled\n    {\n        get => SheetSetting.AutoColumnWidthEnabled;\n        set => SheetSetting.AutoColumnWidthEnabled = value;\n    }\n\n    internal SheetSetting SheetSetting { get; }\n}\n"
  },
  {
    "path": "src/WeihanLi.Npoi/CellPosition.cs",
    "content": "﻿// Copyright (c) Weihan Li. All rights reserved.\n// Licensed under the Apache license.\n\nnamespace WeihanLi.Npoi;\n\n/// <summary>\n/// Represents a zero-based row/column coordinate within a sheet.\n/// </summary>\n/// <param name=\"Row\">Row index.</param>\n/// <param name=\"Column\">Column index.</param>\npublic readonly record struct CellPosition(int Row, int Column);\n"
  },
  {
    "path": "src/WeihanLi.Npoi/Compat.cs",
    "content": "﻿// Copyright (c) Weihan Li. All rights reserved.\n// Licensed under the Apache license.\n#if NETSTANDARD\n// ReSharper disable once CheckNamespace\nnamespace System.Runtime.CompilerServices;\n\ninternal sealed class IsExternalInit;\n#endif\n"
  },
  {
    "path": "src/WeihanLi.Npoi/ConfigurationExtensions.cs",
    "content": "﻿// Copyright (c) Weihan Li. All rights reserved.\n// Licensed under the Apache license.\n\nusing WeihanLi.Common.Services;\nusing WeihanLi.Npoi.Configurations;\n\nnamespace WeihanLi.Npoi;\n\n/// <summary>\n/// Provides convenience extension methods for configuring Excel import/export metadata.\n/// </summary>\npublic static class ConfigurationExtensions\n{\n    /// <summary>\n    ///     Sheet Configuration\n    /// </summary>\n    /// <param name=\"configuration\">excel configuration</param>\n    /// <param name=\"sheetIndex\">sheetIndex</param>\n    /// <param name=\"sheetName\">sheetName</param>\n    /// <returns>current excel configuration</returns>\n    public static IExcelConfiguration HasSheetConfiguration(this IExcelConfiguration configuration, int sheetIndex,\n        string sheetName) => configuration.HasSheetSetting(config => { config.SheetName = sheetName; }, sheetIndex);\n\n    /// <summary>\n    ///     Sheet Configuration\n    /// </summary>\n    /// <param name=\"configuration\">excel configuration</param>\n    /// <param name=\"sheetIndex\">sheetIndex</param>\n    /// <param name=\"sheetName\">sheetName</param>\n    /// <param name=\"enableAutoColumnWidth\">enable auto column width if true otherwise false</param>\n    /// <returns>current excel configuration</returns>\n    public static IExcelConfiguration HasSheetConfiguration(this IExcelConfiguration configuration, int sheetIndex,\n        string sheetName, bool enableAutoColumnWidth) => configuration.HasSheetSetting(config =>\n    {\n        config.SheetName = sheetName;\n        config.AutoColumnWidthEnabled = enableAutoColumnWidth;\n    }, sheetIndex);\n\n    /// <summary>\n    ///     Sheet Configuration\n    /// </summary>\n    /// <param name=\"configuration\">excel configuration</param>\n    /// <param name=\"sheetIndex\">sheetIndex</param>\n    /// <param name=\"sheetName\">sheetName</param>\n    /// <param name=\"startRowIndex\">startRowIndex</param>\n    /// <returns>current excel configuration</returns>\n    public static IExcelConfiguration HasSheetConfiguration(this IExcelConfiguration configuration, int sheetIndex,\n        string sheetName, int startRowIndex) => configuration.HasSheetSetting(config =>\n    {\n        config.SheetName = sheetName;\n        config.StartRowIndex = startRowIndex;\n    }, sheetIndex);\n\n    /// <summary>\n    ///     Sheet Configuration\n    /// </summary>\n    /// <param name=\"configuration\">excel configuration</param>\n    /// <param name=\"sheetIndex\">sheetIndex</param>\n    /// <param name=\"sheetName\">sheetName</param>\n    /// <param name=\"startRowIndex\">startRowIndex</param>\n    /// <param name=\"enableAutoColumnWidth\">enable auto column width if true otherwise false</param>\n    /// <param name=\"endRowIndex\">endRowIndex, set this if you wanna control where to end(included)</param>\n    /// <returns>current excel configuration<see cref=\"IExcelConfiguration\" /></returns>\n    public static IExcelConfiguration HasSheetConfiguration(this IExcelConfiguration configuration, int sheetIndex,\n        string sheetName, int startRowIndex,\n        bool enableAutoColumnWidth, int? endRowIndex = null) => configuration.HasSheetSetting(config =>\n    {\n        config.SheetName = sheetName;\n        config.StartRowIndex = startRowIndex;\n        config.AutoColumnWidthEnabled = enableAutoColumnWidth;\n        config.EndRowIndex = endRowIndex;\n    }, sheetIndex);\n\n    /// <summary>\n    ///     Configure excel author\n    /// </summary>\n    /// <param name=\"configuration\">excel configuration</param>\n    /// <param name=\"author\">excel document author name</param>\n    /// <returns>current excel configuration<see cref=\"IExcelConfiguration\" /></returns>\n    public static IExcelConfiguration HasAuthor(this IExcelConfiguration configuration, string author) =>\n        configuration.HasExcelSetting(setting => { setting.Author = author; });\n\n    /// <summary>\n    ///     Configure excel author\n    /// </summary>\n    /// <param name=\"configuration\">excel configuration</param>\n    /// <param name=\"title\">excel document title</param>\n    /// <returns>current excel configuration<see cref=\"IExcelConfiguration\" /></returns>\n    public static IExcelConfiguration HasTitle(this IExcelConfiguration configuration, string title) =>\n        configuration.HasExcelSetting(setting => { setting.Title = title; });\n\n    /// <summary>\n    ///     Configure excel author\n    /// </summary>\n    /// <param name=\"configuration\">excel configuration</param>\n    /// <param name=\"description\">excel document description</param>\n    /// <returns>current excel configuration<see cref=\"IExcelConfiguration\" /></returns>\n    public static IExcelConfiguration HasDescription(this IExcelConfiguration configuration, string description) =>\n        configuration.HasExcelSetting(setting => { setting.Description = description; });\n\n    /// <summary>\n    ///     Configure excel author\n    /// </summary>\n    /// <param name=\"configuration\">excel configuration</param>\n    /// <param name=\"subject\">excel document subject</param>\n    /// <returns>current excel configuration<see cref=\"IExcelConfiguration\" /></returns>\n    public static IExcelConfiguration HasSubject(this IExcelConfiguration configuration, string subject) =>\n        configuration.HasExcelSetting(setting => { setting.Subject = subject; });\n\n    /// <summary>\n    ///     Configure excel author\n    /// </summary>\n    /// <param name=\"configuration\">excel configuration</param>\n    /// <param name=\"company\">excel document company</param>\n    /// <returns>current excel configuration<see cref=\"IExcelConfiguration\" /></returns>\n    public static IExcelConfiguration HasCompany(this IExcelConfiguration configuration, string company) =>\n        configuration.HasExcelSetting(setting => { setting.Company = company; });\n\n    /// <summary>\n    ///     Configure excel author\n    /// </summary>\n    /// <param name=\"configuration\">excel configuration</param>\n    /// <param name=\"category\">excel document category</param>\n    /// <returns>current excel configuration<see cref=\"IExcelConfiguration\" /></returns>\n    public static IExcelConfiguration HasCategory(this IExcelConfiguration configuration, string category) =>\n        configuration.HasExcelSetting(setting => { setting.Category = category; });\n\n    /// <summary>\n    ///     excel data validator\n    /// </summary>\n    /// <param name=\"configuration\">configuration</param>\n    /// <param name=\"validator\">validator</param>\n    /// <typeparam name=\"TEntity\">entity type</typeparam>\n    /// <returns>current configuration</returns>\n    public static IExcelConfiguration<TEntity> WithValidator<TEntity>(this IExcelConfiguration<TEntity> configuration,\n        IValidator<TEntity> validator)\n    {\n        return configuration.WithValidator(validator.GetCommonValidator());\n    }\n\n    /// <summary>\n    ///     property configuration\n    /// </summary>\n    /// <typeparam name=\"TEntity\">TEntity</typeparam>\n    /// <param name=\"excelConfiguration\">excelConfiguration</param>\n    /// <param name=\"propertyName\">propertyName</param>\n    /// <returns>PropertyConfiguration</returns>\n    public static IPropertyConfiguration<TEntity, string> Property<TEntity>(\n        this IExcelConfiguration<TEntity> excelConfiguration, string propertyName) =>\n        excelConfiguration.Property<string>(propertyName);\n\n    /// <summary>\n    ///     has column output formatter\n    /// </summary>\n    /// <typeparam name=\"TEntity\">entity type</typeparam>\n    /// <typeparam name=\"TProperty\">property type</typeparam>\n    /// <param name=\"configuration\">property configuration</param>\n    /// <param name=\"formatter\">column output formatter</param>\n    /// <returns>property configuration</returns>\n    public static IPropertyConfiguration<TEntity, TProperty> HasColumnOutputFormatter<TEntity, TProperty>(\n        this IPropertyConfiguration<TEntity, TProperty> configuration, Func<TProperty?, object?>? formatter)\n    {\n        if (formatter is null)\n        {\n            return configuration.HasOutputFormatter(null);\n        }\n\n        return configuration.HasOutputFormatter((_, prop) => formatter.Invoke(prop));\n    }\n}\n"
  },
  {
    "path": "src/WeihanLi.Npoi/Configurations/CsvOptions.cs",
    "content": "﻿// Copyright (c) Weihan Li. All rights reserved.\n// Licensed under the Apache license.\n\nusing System.Text;\n\nnamespace WeihanLi.Npoi.Configurations;\n\n/// <summary>\n/// Represents the configurable behaviors of the CSV helper.\n/// </summary>\npublic sealed class CsvOptions\n{\n    /// <summary>\n    ///     Gets the separator character expressed as a string.\n    /// </summary>\n    public string SeparatorString => new(SeparatorCharacter, 1);\n\n    /// <summary>\n    ///     Gets the quote character expressed as a string.\n    /// </summary>\n    public string QuoteString => new(QuoteCharacter, 1);\n\n    /// <summary>\n    ///     Gets or sets the separator used between values.\n    /// </summary>\n    public char SeparatorCharacter { get; set; }\n\n    /// <summary>\n    ///     Gets or sets the character used to wrap textual values.\n    /// </summary>\n    public char QuoteCharacter { get; set; }\n\n    /// <summary>\n    ///     Gets or sets whether the header row should be emitted.\n    /// </summary>\n    public bool IncludeHeader { get; set; }\n\n    /// <summary>\n    ///     Gets or sets the synthetic property name to use for basic types.\n    /// </summary>\n    public string PropertyNameForBasicType { get; set; }\n\n    /// <summary>\n    ///     Gets or sets the encoding of the generated CSV.\n    /// </summary>\n    public Encoding Encoding { get; set; }\n\n    /// <summary>\n    ///     Initializes options with default separator, quote, and encoding.\n    /// </summary>\n    public CsvOptions()\n    {\n        SeparatorCharacter = CsvHelper.CsvSeparatorCharacter;\n        QuoteCharacter = CsvHelper.CsvQuoteCharacter;\n        IncludeHeader = true;\n        PropertyNameForBasicType = InternalConstants.DefaultPropertyNameForBasicType;\n        Encoding = Encoding.UTF8;\n    }\n    /// <summary>\n    ///     Provides a shared instance representing sensible defaults.\n    /// </summary>\n    public static readonly CsvOptions Default = new()\n    {\n        SeparatorCharacter = ',',\n        QuoteCharacter = '\"',\n        PropertyNameForBasicType = InternalConstants.DefaultPropertyNameForBasicType\n    };\n}\n"
  },
  {
    "path": "src/WeihanLi.Npoi/Configurations/ExcelConfiguration.cs",
    "content": "﻿// Copyright (c) Weihan Li. All rights reserved.\n// Licensed under the Apache license.\n\nusing System.Linq.Expressions;\nusing System.Reflection;\nusing WeihanLi.Common;\nusing WeihanLi.Common.Services;\nusing WeihanLi.Extensions;\nusing WeihanLi.Npoi.Settings;\n\nnamespace WeihanLi.Npoi.Configurations;\n\ninternal abstract class ExcelConfiguration : IExcelConfiguration\n{\n    /// <summary>\n    ///     PropertyConfigurationDictionary\n    /// </summary>\n    public IDictionary<PropertyInfo, PropertyConfiguration> PropertyConfigurationDictionary { get; } =\n        new Dictionary<PropertyInfo, PropertyConfiguration>();\n\n    /// <summary>\n    ///     Gets the Excel-level document metadata.\n    /// </summary>\n    public ExcelSetting ExcelSetting { get; } = ExcelHelper.DefaultExcelSetting;\n\n    /// <summary>\n    ///     Gets the configured freeze panes for the workbook.\n    /// </summary>\n    public IList<FreezeSetting> FreezeSettings { get; } = new List<FreezeSetting>();\n\n    /// <summary>\n    ///     Gets or sets the filter configuration for the sheet.\n    /// </summary>\n    public FilterSetting? FilterSetting { get; set; }\n\n    /// <summary>\n    ///     Gets the registered sheet settings keyed by sheet index.\n    /// </summary>\n    public IDictionary<int, SheetSetting> SheetSettings { get; } =\n        new Dictionary<int, SheetSetting> { { 0, new SheetSetting() } };\n\n    #region ExcelSettings FluentAPI\n\n\n\n    /// <summary>\n    ///     Updates the Excel document metadata in a fluent manner.\n    /// </summary>\n    /// <param name=\"configAction\">Configuration delegate.</param>\n    /// <returns>The current configuration instance.</returns>\n    public IExcelConfiguration HasExcelSetting(Action<ExcelSetting> configAction)\n    {\n        // ReSharper disable once ConditionalAccessQualifierIsNonNullableAccordingToAPIContract\n        // allow nullable but we do not want a null\n        configAction?.Invoke(ExcelSetting);\n        return this;\n    }\n\n    #endregion ExcelSettings FluentAPI\n\n    #region Sheet\n\n    /// <summary>\n    ///     Updates the sheet configuration for the specified index.\n    /// </summary>\n    /// <param name=\"configAction\">Configuration delegate.</param>\n    /// <param name=\"sheetIndex\">Target sheet index.</param>\n    /// <returns>The current configuration instance.</returns>\n    public IExcelConfiguration HasSheetSetting(Action<SheetSetting> configAction, int sheetIndex = 0)\n    {\n        if (configAction is null)\n        {\n            throw new ArgumentNullException(nameof(configAction));\n        }\n\n        if (sheetIndex >= 0)\n        {\n            if (!SheetSettings.TryGetValue(sheetIndex, out var sheetSetting))\n            {\n                SheetSettings[sheetIndex]\n                    = sheetSetting\n                        = new SheetSetting();\n            }\n\n            configAction.Invoke(sheetSetting);\n        }\n\n        return this;\n    }\n\n    #endregion Sheet\n\n    #region FreezePane\n\n    /// <summary>\n    ///     Adds a freeze pane using default anchor values.\n    /// </summary>\n    public IExcelConfiguration HasFreezePane(int colSplit, int rowSplit)\n    {\n        FreezeSettings.Add(new FreezeSetting(colSplit, rowSplit));\n        return this;\n    }\n\n    /// <summary>\n    ///     Adds a freeze pane with the specified anchor.\n    /// </summary>\n    public IExcelConfiguration HasFreezePane(int colSplit, int rowSplit, int leftmostColumn, int topRow)\n    {\n        FreezeSettings.Add(new FreezeSetting(colSplit, rowSplit, leftmostColumn, topRow));\n        return this;\n    }\n\n    #endregion FreezePane\n\n    #region Filter\n\n    /// <summary>\n    ///     Adds an auto-filter that starts at the specified column.\n    /// </summary>\n    public IExcelConfiguration HasFilter(int firstColumn) => HasFilter(firstColumn, null);\n\n    /// <summary>\n    ///     Adds an auto-filter covering the specified column range.\n    /// </summary>\n    public IExcelConfiguration HasFilter(int firstColumn, int? lastColumn)\n    {\n        FilterSetting = new FilterSetting(firstColumn, lastColumn);\n        return this;\n    }\n\n    #endregion Filter\n}\n\ninternal sealed class ExcelConfiguration<TEntity> : ExcelConfiguration, IExcelConfiguration<TEntity>\n{\n    /// <summary>\n    ///     Gets the entity type represented by this configuration.\n    /// </summary>\n    public Type EntityType => typeof(TEntity);\n\n    internal Func<TEntity?, bool>? DataFilter { get; private set; }\n\n    internal Action<TEntity?, int>? PostImportAction { get; private set; }\n\n    internal IComparer<PropertyInfo>? PropertyComparer { get; private set; }\n\n    internal IValidator? Validator { get; private set; }\n\n    #region Property\n\n    /// <summary>\n    ///     Assigns a custom validator.\n    /// </summary>\n    public IExcelConfiguration<TEntity> WithValidator(IValidator? validator)\n    {\n        Validator = validator;\n        return this;\n    }\n\n    /// <summary>\n    ///     Applies a data filter that can skip entities during export.\n    /// </summary>\n    public IExcelConfiguration<TEntity> WithDataFilter(Func<TEntity?, bool>? dataFilter)\n    {\n        DataFilter = dataFilter;\n        return this;\n    }\n\n    public IExcelConfiguration<TEntity> WithPostImportAction(Action<TEntity?, int>? postAction)\n    {\n        PostImportAction = postAction;\n        return this;\n    }\n\n    /// <summary>\n    ///     Controls the ordering of properties.\n    /// </summary>\n    public IExcelConfiguration<TEntity> WithPropertyComparer(IComparer<PropertyInfo>? propertyComparer)\n    {\n        PropertyComparer = propertyComparer;\n        return this;\n    }\n\n    /// <summary>\n    ///     Gets the property configuration by the specified property expression for the specified\n    ///     <typeparamref name=\"TEntity\" /> and its <typeparamref name=\"TProperty\" />.\n    /// </summary>\n    /// <returns>The <see cref=\"IPropertyConfiguration\" />.</returns>\n    /// <param name=\"propertyExpression\">The property expression.</param>\n    /// <typeparam name=\"TProperty\">The type of parameter.</typeparam>\n    public IPropertyConfiguration<TEntity, TProperty> Property<TProperty>(\n        Expression<Func<TEntity, TProperty>> propertyExpression)\n    {\n        var memberInfo = propertyExpression.GetMemberInfo();\n        if (memberInfo is PropertyInfo property &&\n            PropertyConfigurationDictionary.TryGetValue(property, out var propertyConfiguration))\n            return (IPropertyConfiguration<TEntity, TProperty>)propertyConfiguration;\n\n        property = CacheUtil.GetTypeProperties(EntityType).FirstOrDefault(p => p.Name == memberInfo.Name)\n                   ?? throw new InvalidOperationException($\"the property [{memberInfo.Name}] does not exists\");\n        return (IPropertyConfiguration<TEntity, TProperty>)PropertyConfigurationDictionary[property];\n    }\n\n    /// <summary>\n    ///     Retrieves (or builds) a configuration for the specified property name.\n    /// </summary>\n    /// <typeparam name=\"TProperty\">Property type.</typeparam>\n    /// <param name=\"propertyName\">Property name.</param>\n    /// <returns>Property configuration.</returns>\n    public IPropertyConfiguration<TEntity, TProperty> Property<TProperty>(string propertyName)\n    {\n        var property = PropertyConfigurationDictionary.Keys.FirstOrDefault(p => p.Name == propertyName);\n        if (property is not null)\n        {\n            return (IPropertyConfiguration<TEntity, TProperty>)PropertyConfigurationDictionary[property];\n        }\n\n        var propertyType = typeof(TProperty);\n\n        property = new FakePropertyInfo(EntityType, propertyType, propertyName);\n\n        var propertyConfigurationType =\n            typeof(PropertyConfiguration<,>).MakeGenericType(EntityType, propertyType);\n        var propertyConfiguration =\n            (PropertyConfiguration)Guard.NotNull(Activator.CreateInstance(propertyConfigurationType, property));\n\n        PropertyConfigurationDictionary[property] = propertyConfiguration;\n\n        return (IPropertyConfiguration<TEntity, TProperty>)propertyConfiguration;\n    }\n\n    #endregion Property\n}\n"
  },
  {
    "path": "src/WeihanLi.Npoi/Configurations/IExcelConfiguration.cs",
    "content": "﻿// Copyright (c) Weihan Li. All rights reserved.\n// Licensed under the Apache license.\n\nusing System.Linq.Expressions;\nusing System.Reflection;\nusing WeihanLi.Common.Services;\nusing WeihanLi.Npoi.Settings;\n\nnamespace WeihanLi.Npoi.Configurations;\n\n/// <summary>\n/// Abstraction describing the fluent configuration surface exposed to consumers.\n/// </summary>\npublic interface IExcelConfiguration\n{\n    /// <summary>\n    ///     Sheet Configuration\n    /// </summary>\n    /// <param name=\"configAction\">sheet config delegate</param>\n    /// <param name=\"sheetIndex\">sheetIndex, 0 is the default value</param>\n    /// <returns>current excel configuration<see ref=\"IExcelConfiguration\" /></returns>\n    IExcelConfiguration HasSheetSetting(Action<SheetSetting> configAction, int sheetIndex = 0);\n\n    /// <summary>\n    ///     excel setting configure\n    /// </summary>\n    /// <param name=\"configAction\">config delegate</param>\n    /// <returns>current excel configuration<see ref=\"IExcelConfiguration\" /></returns>\n    IExcelConfiguration HasExcelSetting(Action<ExcelSetting> configAction);\n\n    /// <summary>\n    ///     setting freeze pane\n    ///     Creates a split (freeze pane). Any existing freeze pane or split pane is overwritten.\n    /// </summary>\n    /// <param name=\"colSplit\">Horizontal position of split</param>\n    /// <param name=\"rowSplit\">Vertical position of split</param>\n    /// <returns>current excel configuration<see ref=\"IExcelConfiguration\" /></returns>\n    IExcelConfiguration HasFreezePane(int colSplit, int rowSplit);\n\n    /// <summary>\n    ///     setting freeze pane\n    ///     Creates a split (freeze pane). Any existing freeze pane or split pane is overwritten.\n    /// </summary>\n    /// <param name=\"colSplit\">Horizontal position of split</param>\n    /// <param name=\"rowSplit\">Vertical position of split</param>\n    /// <param name=\"leftmostColumn\">Top row visible in bottom pane</param>\n    /// <param name=\"topRow\">Left column visible in right pane</param>\n    /// <returns>current excel configuration<see ref=\"IExcelConfiguration\" /></returns>\n    IExcelConfiguration HasFreezePane(int colSplit, int rowSplit, int leftmostColumn, int topRow);\n\n    /// <summary>\n    ///     setting filter\n    /// </summary>\n    /// <param name=\"firstColumn\">firstCol Index of first column</param>\n    /// <returns>current excel configuration<see ref=\"IExcelConfiguration\" /></returns>\n    IExcelConfiguration HasFilter(int firstColumn);\n\n    /// <summary>\n    ///     setting filter\n    /// </summary>\n    /// <param name=\"firstColumn\">firstCol Index of first column</param>\n    /// <param name=\"lastColumn\">lastCol Index of last column (inclusive), must be equal to or larger than {@code firstCol}</param>\n    /// <returns>current excel configuration<see ref=\"IExcelConfiguration\" /></returns>\n    IExcelConfiguration HasFilter(int firstColumn, int? lastColumn);\n}\n\n/// <summary>\n/// Strongly typed configuration contract for a specific entity.\n/// </summary>\n/// <typeparam name=\"TEntity\">Entity type.</typeparam>\npublic interface IExcelConfiguration<TEntity> : IExcelConfiguration\n{\n    /// <summary>\n    ///     register validator for excel import\n    /// </summary>\n    /// <param name=\"validator\">validator</param>\n    /// <returns>current excel configuration</returns>\n    IExcelConfiguration<TEntity> WithValidator(IValidator? validator);\n\n    /// <summary>\n    ///     register data filter\n    /// </summary>\n    /// <param name=\"dataFilter\">data filter logic</param>\n    /// <returns>current excel configuration</returns>\n    IExcelConfiguration<TEntity> WithDataFilter(Func<TEntity?, bool>? dataFilter);\n\n    /// <summary>\n    ///     register post action for T and rowIndex based func\n    /// </summary>\n    /// <param name=\"postAction\">postAction</param>\n    /// <returns></returns>\n    IExcelConfiguration<TEntity> WithPostImportAction(Action<TEntity?, int>? postAction);\n\n    /// <summary>\n    ///     register property comparer\n    /// </summary>\n    /// <param name=\"propertyComparer\">propertyComparer</param>\n    /// <returns>current excel configuration</returns>\n    IExcelConfiguration<TEntity> WithPropertyComparer(IComparer<PropertyInfo>? propertyComparer);\n\n    /// <summary>\n    ///     property configuration\n    /// </summary>\n    /// <typeparam name=\"TProperty\">PropertyType</typeparam>\n    /// <param name=\"propertyExpression\">propertyExpression to get property info</param>\n    /// <returns>current property configuration</returns>\n    IPropertyConfiguration<TEntity, TProperty> Property<TProperty>(\n        Expression<Func<TEntity, TProperty>> propertyExpression);\n\n    /// <summary>\n    ///     property configuration\n    /// </summary>\n    /// <typeparam name=\"TProperty\">PropertyType</typeparam>\n    /// <param name=\"propertyName\">propertyName</param>\n    /// <returns>current property configuration</returns>\n    IPropertyConfiguration<TEntity, TProperty> Property<TProperty>(string propertyName);\n}\n"
  },
  {
    "path": "src/WeihanLi.Npoi/Configurations/IPropertyConfiguration.cs",
    "content": "﻿// Copyright (c) Weihan Li. All rights reserved.\n// Licensed under the Apache license.\n\nusing NPOI.SS.UserModel;\n\nnamespace WeihanLi.Npoi.Configurations;\n\n/// <summary>\n///     PropertyConfiguration\n/// </summary>\npublic interface IPropertyConfiguration;\n\n/// <summary>\n/// Describes the fluent property-level configuration API for an entity.\n/// </summary>\n/// <typeparam name=\"TEntity\">Entity type.</typeparam>\n/// <typeparam name=\"TProperty\">Property type.</typeparam>\npublic interface IPropertyConfiguration<out TEntity, TProperty> : IPropertyConfiguration\n{\n    /// <summary>\n    ///     HasColumnIndex\n    /// </summary>\n    /// <param name=\"index\">index</param>\n    /// <returns></returns>\n    IPropertyConfiguration<TEntity, TProperty> HasColumnIndex(int index);\n\n    /// <summary>\n    ///     HasColumnWidth\n    /// </summary>\n    /// <param name=\"width\">width</param>\n    /// <returns></returns>\n    IPropertyConfiguration<TEntity, TProperty> HasColumnWidth(int width);\n\n    /// <summary>\n    ///     HasColumnTitle\n    /// </summary>\n    /// <param name=\"title\">title</param>\n    /// <returns></returns>\n    IPropertyConfiguration<TEntity, TProperty> HasColumnTitle(string title);\n\n    /// <summary>\n    ///     HasColumnFormatter\n    /// </summary>\n    /// <param name=\"formatter\">formatter</param>\n    /// <returns></returns>\n    IPropertyConfiguration<TEntity, TProperty> HasColumnFormatter(string? formatter);\n\n    /// <summary>\n    ///     Ignored\n    /// </summary>\n    /// <returns></returns>\n    IPropertyConfiguration<TEntity, TProperty> Ignored(bool ignored = true);\n\n\n    /// <summary>\n    ///     HasCellReader(For excel only)\n    /// </summary>\n    /// <param name=\"cellReader\">custom cell value reader</param>\n    /// <returns></returns>\n    IPropertyConfiguration<TEntity, TProperty> HasCellReader(\n        Func<ICell, TProperty>? cellReader);\n\n    /// <summary>\n    ///     HasColumnInputFormatter\n    /// </summary>\n    /// <param name=\"formatterFunc\">formatterFunc</param>\n    /// <returns></returns>\n    IPropertyConfiguration<TEntity, TProperty> HasColumnInputFormatter(Func<string?, TProperty?>? formatterFunc);\n\n    /// <summary>\n    ///     HasOutputFormatter\n    /// </summary>\n    /// <param name=\"formatterFunc\">columnFormatter</param>\n    /// <returns></returns>\n    IPropertyConfiguration<TEntity, TProperty> HasOutputFormatter(\n        Func<TEntity?, TProperty?, object?>? formatterFunc);\n\n    /// <summary>\n    ///     HasInputFormatter\n    /// </summary>\n    /// <param name=\"formatterFunc\">columnFormatter</param>\n    /// <returns></returns>\n    IPropertyConfiguration<TEntity, TProperty> HasInputFormatter(\n        Func<TEntity?, TProperty?, TProperty>? formatterFunc);\n}\n"
  },
  {
    "path": "src/WeihanLi.Npoi/Configurations/PropertyConfiguration.cs",
    "content": "﻿// Copyright (c) Weihan Li. All rights reserved.\n// Licensed under the Apache license.\n\nusing NPOI.SS.UserModel;\nusing System.Reflection;\nusing WeihanLi.Extensions;\n\nnamespace WeihanLi.Npoi.Configurations;\n\ninternal class PropertyConfiguration : IPropertyConfiguration\n{\n    /// <summary>\n    ///     ColumnIndex\n    /// </summary>\n    public int ColumnIndex { get; set; } = -1;\n\n    /// <summary>\n    ///     ColumnWidth\n    /// </summary>\n    public int ColumnWidth { get; set; }\n\n    /// <summary>\n    ///     Title\n    /// </summary>\n    public string ColumnTitle { get; set; } = string.Empty;\n\n    /// <summary>\n    ///     Formatter\n    /// </summary>\n    public string? ColumnFormatter { get; set; }\n\n    /// <summary>\n    ///     the property is ignored.\n    /// </summary>\n    public bool IsIgnored { get; set; }\n\n    /// <summary>\n    ///     PropertyName\n    /// </summary>\n    public string? PropertyName { get; set; }\n}\n\ninternal sealed class PropertyConfiguration<TEntity, TProperty> : PropertyConfiguration,\n    IPropertyConfiguration<TEntity, TProperty>\n{\n    private readonly PropertyInfo _propertyInfo;\n\n    /// <summary>\n    ///     Initializes a configuration wrapper for the specified property.\n    /// </summary>\n    /// <param name=\"propertyInfo\">Property metadata.</param>\n    public PropertyConfiguration(PropertyInfo propertyInfo)\n    {\n        _propertyInfo = propertyInfo;\n        PropertyName = propertyInfo.Name;\n        ColumnTitle = propertyInfo.Name;\n    }\n\n    /// <summary>\n    ///     Sets the column index explicitly.\n    /// </summary>\n    public IPropertyConfiguration<TEntity, TProperty> HasColumnIndex(int index)\n    {\n        if (index >= 0)\n        {\n            ColumnIndex = index;\n        }\n\n        return this;\n    }\n\n    /// <summary>\n    ///     Assigns the header text used for the column.\n    /// </summary>\n    public IPropertyConfiguration<TEntity, TProperty> HasColumnTitle(string title)\n    {\n        ColumnTitle = title ?? throw new ArgumentNullException(nameof(title));\n        return this;\n    }\n\n    /// <summary>\n    ///     Sets the column width (characters).\n    /// </summary>\n    public IPropertyConfiguration<TEntity, TProperty> HasColumnWidth(int width)\n    {\n        ColumnWidth = width;\n        return this;\n    }\n\n    /// <summary>\n    ///     Assigns the formatter string used when writing out the column.\n    /// </summary>\n    public IPropertyConfiguration<TEntity, TProperty> HasColumnFormatter(string? formatter)\n    {\n        ColumnFormatter = formatter;\n        return this;\n    }\n\n    /// <summary>\n    ///     Marks the property as ignored when exporting/importing.\n    /// </summary>\n    public IPropertyConfiguration<TEntity, TProperty> Ignored(bool ignored = true)\n    {\n        IsIgnored = ignored;\n        return this;\n    }\n\n    /// <summary>\n    ///     Registers a custom cell reader for imports.\n    /// </summary>\n    public IPropertyConfiguration<TEntity, TProperty> HasCellReader(Func<ICell, TProperty>? cellReader)\n    {\n        InternalCache.CellReaderFuncCache.AddOrUpdate(_propertyInfo, cellReader);\n        return this;\n    }\n\n    /// <summary>\n    ///     Registers a formatter used when exporting the property.\n    /// </summary>\n    public IPropertyConfiguration<TEntity, TProperty> HasOutputFormatter(\n        Func<TEntity?, TProperty?, object?>? formatterFunc)\n    {\n        InternalCache.OutputFormatterFuncCache.AddOrUpdate(_propertyInfo, formatterFunc);\n        return this;\n    }\n\n    /// <summary>\n    ///     Registers a formatter used when importing cell values.\n    /// </summary>\n    public IPropertyConfiguration<TEntity, TProperty> HasInputFormatter(\n        Func<TEntity?, TProperty?, TProperty?>? formatterFunc)\n    {\n        InternalCache.InputFormatterFuncCache.AddOrUpdate(_propertyInfo, formatterFunc);\n        return this;\n    }\n\n    /// <summary>\n    ///     Registers a formatter that manipulates the raw column text before parsing.\n    /// </summary>\n    public IPropertyConfiguration<TEntity, TProperty> HasColumnInputFormatter(\n        Func<string?, TProperty?>? formatterFunc)\n    {\n        InternalCache.ColumnInputFormatterFuncCache.AddOrUpdate(_propertyInfo, formatterFunc);\n        return this;\n    }\n}\n"
  },
  {
    "path": "src/WeihanLi.Npoi/CsvHelper.cs",
    "content": "﻿// Copyright (c) Weihan Li. All rights reserved.\n// Licensed under the Apache license.\n\nusing System.Data;\nusing System.Diagnostics;\nusing System.Text;\nusing WeihanLi.Common;\nusing WeihanLi.Common.Helpers;\nusing WeihanLi.Extensions;\nusing WeihanLi.Npoi.Configurations;\n\nnamespace WeihanLi.Npoi;\n\n/// <summary>\n///     CsvHelper provides utilities for reading and writing CSV files, \n///     supporting conversion between CSV data and DataTables or strongly-typed entities.\n/// </summary>\npublic static class CsvHelper\n{\n    /// <summary>\n    ///     CSV separator character, ',' by default.\n    ///     Can be changed to support different CSV formats (e.g., ';' for European format).\n    /// </summary>\n    public static char CsvSeparatorCharacter = ',';\n\n    /// <summary>\n    ///     CSV quote character used to escape values containing special characters, <c>\"</c> by default.\n    ///     Values containing the separator character will be wrapped with this quote character.\n    /// </summary>\n    public static char CsvQuoteCharacter = '\"';\n\n    /// <summary>\n    ///     Saves a DataTable to a CSV file with default options (includes header).\n    /// </summary>\n    /// <param name=\"dt\">The DataTable to export</param>\n    /// <param name=\"filePath\">The destination file path</param>\n    /// <returns>True if the file was successfully created; otherwise, false</returns>\n    public static bool ToCsvFile(this DataTable dt, string filePath) => ToCsvFile(dt, filePath, CsvOptions.Default);\n\n    /// <summary>\n    ///     Saves a DataTable to a CSV file with optional header.\n    /// </summary>\n    /// <param name=\"dataTable\">The DataTable to export</param>\n    /// <param name=\"filePath\">The destination file path</param>\n    /// <param name=\"includeHeader\">Whether to include column headers in the output</param>\n    /// <returns>True if the file was successfully created; otherwise, false</returns>\n    public static bool ToCsvFile(this DataTable dataTable, string filePath, bool includeHeader)\n    {\n        return ToCsvFile(dataTable, filePath, includeHeader ? CsvOptions.Default : new CsvOptions()\n        {\n            IncludeHeader = false\n        });\n    }\n\n    /// <summary>\n    ///     Saves a DataTable to a CSV file with custom CSV options.\n    /// </summary>\n    /// <param name=\"dataTable\">The DataTable to export</param>\n    /// <param name=\"filePath\">The destination file path</param>\n    /// <param name=\"csvOptions\">Custom CSV formatting options (encoding, separator, quote character, etc.)</param>\n    /// <returns>True if the file was successfully created; otherwise, false</returns>\n    public static bool ToCsvFile(this DataTable dataTable, string filePath, CsvOptions csvOptions)\n    {\n        if (dataTable is null)\n        {\n            throw new ArgumentNullException(nameof(dataTable));\n        }\n\n        var csvText = GetCsvText(dataTable, csvOptions);\n        if (csvText.IsNullOrEmpty())\n        {\n            return false;\n        }\n        InternalHelper.EnsureFileIsNotReadOnly(filePath);\n        var dir = Path.GetDirectoryName(filePath);\n        if (dir.IsNotNullOrEmpty())\n        {\n            if (!Directory.Exists(dir))\n            {\n                Directory.CreateDirectory(dir);\n            }\n        }\n\n        File.WriteAllText(filePath, csvText, csvOptions.Encoding);\n        return true;\n    }\n\n    /// <summary>\n    ///     Converts a DataTable to CSV formatted byte array with default encoding (includes header).\n    /// </summary>\n    /// <param name=\"dt\">The DataTable to convert</param>\n    /// <returns>CSV data as a byte array</returns>\n    public static byte[] ToCsvBytes(this DataTable dt) => ToCsvBytes(dt, true);\n\n    /// <summary>\n    ///     Converts a DataTable to CSV formatted byte array with optional header.\n    /// </summary>\n    /// <param name=\"dataTable\">The DataTable to convert</param>\n    /// <param name=\"includeHeader\">Whether to include column headers in the output</param>\n    /// <returns>CSV data as a byte array</returns>\n    public static byte[] ToCsvBytes(this DataTable dataTable, bool includeHeader) =>\n        GetCsvText(dataTable, includeHeader).GetBytes();\n\n    /// <summary>\n    ///     Converts a DataTable to CSV formatted byte array with custom options.\n    /// </summary>\n    /// <param name=\"dataTable\">The DataTable to convert</param>\n    /// <param name=\"csvOptions\">Custom CSV formatting options</param>\n    /// <returns>CSV data as a byte array</returns>\n    public static byte[] ToCsvBytes(this DataTable dataTable, CsvOptions csvOptions) =>\n        GetCsvText(dataTable, csvOptions).GetBytes();\n\n    /// <summary>\n    ///     Converts CSV byte data to a DataTable with default options.\n    /// </summary>\n    /// <param name=\"csvBytes\">CSV data as byte array</param>\n    /// <returns>A DataTable populated with CSV data</returns>\n    public static DataTable ToDataTable(byte[] csvBytes)\n        => ToDataTable(csvBytes, CsvOptions.Default);\n\n    /// <summary>\n    ///     Converts CSV byte data to a DataTable with custom CSV options.\n    /// </summary>\n    /// <param name=\"csvBytes\">CSV data as byte array</param>\n    /// <param name=\"csvOptions\">Custom CSV parsing options (encoding, separator, etc.)</param>\n    /// <returns>A DataTable populated with CSV data</returns>\n    public static DataTable ToDataTable(byte[] csvBytes, CsvOptions csvOptions)\n    {\n        if (csvBytes is null)\n        {\n            throw new ArgumentNullException(nameof(csvBytes));\n        }\n\n        using var ms = new MemoryStream(csvBytes);\n        return ToDataTable(ms, csvOptions);\n    }\n\n    /// <summary>\n    ///     Converts CSV stream data to a DataTable with default options.\n    /// </summary>\n    /// <param name=\"stream\">Stream containing CSV data</param>\n    /// <returns>A DataTable populated with CSV data</returns>\n    public static DataTable ToDataTable(Stream stream) => ToDataTable(stream, CsvOptions.Default);\n\n    /// <summary>\n    ///     Converts CSV stream data to a DataTable with custom CSV options.\n    ///     The first row is treated as column headers.\n    /// </summary>\n    /// <param name=\"stream\">Stream containing CSV data</param>\n    /// <param name=\"csvOptions\">Custom CSV parsing options</param>\n    /// <returns>A DataTable populated with CSV data</returns>\n    public static DataTable ToDataTable(Stream stream, CsvOptions csvOptions)\n    {\n        Guard.NotNull(stream);\n        Guard.NotNull(csvOptions);\n\n        var dt = new DataTable();\n\n        if (stream.CanRead)\n        {\n            using var sr = new StreamReader(stream, csvOptions.Encoding);\n            string strLine;\n            var isFirst = true;\n            while ((strLine = sr.ReadLine()!).IsNotNullOrEmpty())\n            {\n                var rowData = ParseLine(strLine, csvOptions);\n                var dtColumns = rowData.Count;\n                if (isFirst)\n                {\n                    for (var i = 0; i < dtColumns; i++)\n                    {\n                        var columnName = rowData[i];\n                        if (dt.Columns.Contains(columnName))\n                        {\n                            columnName = InternalHelper.GetEncodedColumnName(columnName);\n                        }\n\n                        dt.Columns.Add(columnName);\n                    }\n\n                    isFirst = false;\n                }\n                else\n                {\n                    var dataRow = dt.NewRow();\n                    for (var j = 0; j < dt.Columns.Count; j++)\n                    {\n                        dataRow[j] = rowData[j];\n                    }\n\n                    dt.Rows.Add(dataRow);\n                }\n            }\n        }\n\n        return dt;\n    }\n\n    /// <summary>\n    ///     Converts CSV file data to a DataTable with default options.\n    /// </summary>\n    /// <param name=\"filePath\">Path to the CSV file</param>\n    /// <returns>A DataTable populated with CSV data</returns>\n    /// <exception cref=\"ArgumentNullException\">Thrown when filePath is null</exception>\n    /// <exception cref=\"ArgumentException\">Thrown when the file does not exist</exception>\n    public static DataTable ToDataTable(string filePath)\n    {\n        if (filePath is null)\n        {\n            throw new ArgumentNullException(nameof(filePath));\n        }\n        if (!File.Exists(filePath))\n        {\n            throw new ArgumentException(Resource.FileNotFound, nameof(filePath));\n        }\n\n        using var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);\n        return ToDataTable(fs);\n    }\n\n    /// <summary>\n    ///     Converts CSV file data to a strongly-typed entity list with default options.\n    /// </summary>\n    /// <typeparam name=\"TEntity\">The entity type to map CSV data to</typeparam>\n    /// <param name=\"filePath\">Path to the CSV file</param>\n    /// <returns>A list of entities populated from CSV data</returns>\n    public static List<TEntity?> ToEntityList<TEntity>(string filePath)\n        => ToEntityList<TEntity>(filePath, CsvOptions.Default);\n\n    /// <summary>\n    ///     Converts CSV file data to a strongly-typed entity list with custom options.\n    /// </summary>\n    /// <typeparam name=\"TEntity\">The entity type to map CSV data to</typeparam>\n    /// <param name=\"filePath\">Path to the CSV file</param>\n    /// <param name=\"csvOptions\">Custom CSV parsing options</param>\n    /// <returns>A list of entities populated from CSV data</returns>\n    /// <exception cref=\"ArgumentException\">Thrown when the file does not exist</exception>\n    public static List<TEntity?> ToEntityList<TEntity>(string filePath, CsvOptions csvOptions)\n    {\n        Guard.NotNull(filePath);\n        if (!File.Exists(filePath))\n        {\n            throw new ArgumentException(Resource.FileNotFound, nameof(filePath));\n        }\n\n        using var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);\n        return ToEntityList<TEntity>(fs, csvOptions);\n    }\n\n    /// <summary>\n    ///     Converts CSV file data to a lazy-loaded sequence of strongly-typed entities.\n    ///     Use this method for large files to avoid loading all data into memory at once.\n    /// </summary>\n    /// <typeparam name=\"TEntity\">The entity type to map CSV data to</typeparam>\n    /// <param name=\"filePath\">Path to the CSV file</param>\n    /// <param name=\"csvOptions\">Optional custom CSV parsing options</param>\n    /// <returns>A lazy-loaded enumerable of entities</returns>\n    /// <exception cref=\"ArgumentException\">Thrown when the file does not exist</exception>\n    public static IEnumerable<TEntity?> ToEntities<TEntity>(string filePath, CsvOptions? csvOptions = null)\n    {\n        Guard.NotNull(filePath);\n        if (!File.Exists(filePath))\n        {\n            throw new ArgumentException(Resource.FileNotFound, nameof(filePath));\n        }\n        using var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);\n        // https://stackoverflow.com/questions/1539114/yield-return-statement-inside-a-using-block-disposes-before-executing\n        foreach (var entity in ToEntities<TEntity>(fs, csvOptions))\n        {\n            yield return entity;\n        }\n    }\n\n    /// <summary>\n    ///     Converts CSV byte data to a strongly-typed entity list with default options.\n    /// </summary>\n    /// <typeparam name=\"TEntity\">The entity type to map CSV data to</typeparam>\n    /// <param name=\"csvBytes\">CSV data as byte array</param>\n    /// <returns>A list of entities populated from CSV data</returns>\n    public static List<TEntity?> ToEntityList<TEntity>(byte[] csvBytes)\n        => ToEntityList<TEntity>(csvBytes, CsvOptions.Default);\n\n    /// <summary>\n    ///     Converts CSV byte data to a strongly-typed entity list with custom options.\n    /// </summary>\n    /// <typeparam name=\"TEntity\">The entity type to map CSV data to</typeparam>\n    /// <param name=\"csvBytes\">CSV data as byte array</param>\n    /// <param name=\"csvOptions\">Custom CSV parsing options</param>\n    /// <returns>A list of entities populated from CSV data</returns>\n    public static List<TEntity?> ToEntityList<TEntity>(byte[] csvBytes, CsvOptions csvOptions)\n    {\n        Guard.NotNull(csvBytes);\n        using var ms = new MemoryStream(csvBytes);\n        return ToEntityList<TEntity>(ms, csvOptions);\n    }\n\n    /// <summary>\n    ///     Converts CSV byte data to a lazy-loaded sequence of strongly-typed entities.\n    /// </summary>\n    /// <typeparam name=\"TEntity\">The entity type to map CSV data to</typeparam>\n    /// <param name=\"csvBytes\">CSV data as byte array</param>\n    /// <param name=\"csvOptions\">Optional custom CSV parsing options</param>\n    /// <returns>A lazy-loaded enumerable of entities</returns>\n    public static IEnumerable<TEntity?> ToEntities<TEntity>(byte[] csvBytes, CsvOptions? csvOptions = null)\n    {\n        Guard.NotNull(csvBytes);\n        using var ms = new MemoryStream(csvBytes);\n        foreach (var entity in ToEntities<TEntity>(ms, csvOptions))\n        {\n            yield return entity;\n        }\n    }\n\n    /// <summary>\n    ///     Converts CSV stream data to a strongly-typed entity list with default options.\n    /// </summary>\n    /// <typeparam name=\"TEntity\">The entity type to map CSV data to</typeparam>\n    /// <param name=\"csvStream\">Stream containing CSV data</param>\n    /// <returns>A list of entities populated from CSV data</returns>\n    public static List<TEntity?> ToEntityList<TEntity>(Stream csvStream)\n        => ToEntityList<TEntity>(csvStream, CsvOptions.Default);\n\n    /// <summary>\n    ///     Converts CSV stream data to a strongly-typed entity list with custom options.\n    /// </summary>\n    /// <typeparam name=\"TEntity\">The entity type to map CSV data to</typeparam>\n    /// <param name=\"csvStream\">Stream containing CSV data</param>\n    /// <param name=\"csvOptions\">Custom CSV parsing options</param>\n    /// <returns>A list of entities populated from CSV data</returns>\n    public static List<TEntity?> ToEntityList<TEntity>(Stream csvStream, CsvOptions csvOptions)\n    {\n        Guard.NotNull(csvStream);\n        return ToEntities<TEntity>(csvStream, csvOptions).ToList();\n    }\n\n    /// <summary>\n    ///     Converts CSV stream data to a lazy-loaded sequence of strongly-typed entities.\n    ///     This method is memory-efficient for large CSV files.\n    /// </summary>\n    /// <typeparam name=\"TEntity\">The entity type to map CSV data to</typeparam>\n    /// <param name=\"csvStream\">Stream containing CSV data</param>\n    /// <param name=\"csvOptions\">Optional custom CSV parsing options</param>\n    /// <returns>A lazy-loaded enumerable of entities</returns>\n    public static IEnumerable<TEntity?> ToEntities<TEntity>(Stream csvStream, CsvOptions? csvOptions = null)\n    {\n        Guard.NotNull(csvStream);\n\n        var lines = GetLines();\n        foreach (var entity in GetEntities<TEntity>(lines, csvOptions))\n        {\n            yield return entity;\n        }\n\n        IEnumerable<string> GetLines()\n        {\n            csvStream.Seek(0, SeekOrigin.Begin);\n            using var reader = new StreamReader(csvStream, csvOptions?.Encoding ?? Encoding.UTF8);\n            while (true)\n            {\n                var strLine = reader.ReadLine();\n                if (strLine.IsNullOrEmpty())\n                    yield break;\n\n                yield return strLine;\n            }\n        }\n    }\n\n    /// <summary>\n    ///     Parses CSV text and converts it to a strongly-typed entity list.\n    /// </summary>\n    /// <typeparam name=\"TEntity\">The entity type to map CSV data to</typeparam>\n    /// <param name=\"csvText\">CSV data as a string</param>\n    /// <param name=\"csvOptions\">Optional custom CSV parsing options</param>\n    /// <returns>A list of entities populated from CSV data</returns>\n    public static List<TEntity?> GetEntityList<TEntity>(string csvText, CsvOptions? csvOptions = null)\n        => GetEntities<TEntity>(csvText, csvOptions).ToList();\n\n    /// <summary>\n    ///     Parses CSV text and converts it to a lazy-loaded sequence of strongly-typed entities.\n    /// </summary>\n    /// <typeparam name=\"TEntity\">The entity type to map CSV data to</typeparam>\n    /// <param name=\"csvText\">CSV data as a string</param>\n    /// <param name=\"csvOptions\">Optional custom CSV parsing options</param>\n    /// <returns>A lazy-loaded enumerable of entities</returns>\n    public static IEnumerable<TEntity?> GetEntities<TEntity>(string csvText, CsvOptions? csvOptions = null)\n    {\n        Guard.NotNull(csvText);\n        var lines = GetLines();\n        foreach (var entity in GetEntities<TEntity>(lines, csvOptions))\n        {\n            yield return entity;\n        }\n\n        IEnumerable<string> GetLines()\n        {\n            using var reader = new StringReader(csvText);\n            while (true)\n            {\n                var strLine = reader.ReadLine();\n                if (strLine.IsNullOrEmpty())\n                    yield break;\n\n                yield return strLine;\n            }\n        }\n    }\n\n    /// <summary>\n    ///     Converts CSV lines to a strongly-typed entity list.\n    /// </summary>\n    /// <typeparam name=\"TEntity\">The entity type to map CSV data to</typeparam>\n    /// <param name=\"csvLines\">Enumerable collection of CSV lines</param>\n    /// <param name=\"csvOptions\">Optional custom CSV parsing options</param>\n    /// <returns>A list of entities populated from CSV data</returns>\n    public static List<TEntity?> GetEntityList<TEntity>(IEnumerable<string> csvLines, CsvOptions? csvOptions = null)\n        => GetEntities<TEntity>(csvLines, csvOptions).ToList();\n\n    /// <summary>\n    ///     Converts CSV lines to a lazy-loaded sequence of strongly-typed entities.\n    ///     Supports both basic types and complex objects with property mapping.\n    ///     For complex types, column headers are matched to property names or configured column titles.\n    /// </summary>\n    /// <typeparam name=\"TEntity\">The entity type to map CSV data to</typeparam>\n    /// <param name=\"csvLines\">Enumerable collection of CSV lines</param>\n    /// <param name=\"csvOptions\">Optional custom CSV parsing options</param>\n    /// <returns>A lazy-loaded enumerable of entities</returns>\n    public static IEnumerable<TEntity?> GetEntities<TEntity>(IEnumerable<string> csvLines, CsvOptions? csvOptions = null)\n    {\n        if (csvLines is null)\n        {\n            throw new ArgumentNullException(nameof(csvLines));\n        }\n        csvOptions ??= CsvOptions.Default;\n        var entityType = typeof(TEntity);\n        if (entityType.IsBasicType())\n        {\n            var lines = csvOptions.IncludeHeader ? csvLines.Skip(1) : csvLines;\n            foreach (var strLine in lines)\n            {\n                yield return strLine.To<TEntity>();\n            }\n        }\n        else\n        {\n            var configuration = InternalHelper.GetExcelConfigurationMapping<TEntity>();\n            var propertyColumnDictionary = InternalHelper.GetPropertyColumnDictionary<TEntity>();\n            var propertyColumnDic = csvOptions.IncludeHeader\n                ? propertyColumnDictionary.ToDictionary(p => p.Key, p => new PropertyConfiguration\n                {\n                    ColumnIndex = -1,\n                    ColumnFormatter = p.Value.ColumnFormatter,\n                    ColumnTitle = p.Value.ColumnTitle,\n                    ColumnWidth = p.Value.ColumnWidth,\n                    IsIgnored = p.Value.IsIgnored\n                })\n                : propertyColumnDictionary;\n            var isFirstLine = csvOptions.IncludeHeader;\n            var lineIndex = -1;\n            foreach (var strLine in csvLines)\n            {\n                var cols = ParseLine(strLine, csvOptions);\n                lineIndex++;\n                if (isFirstLine)\n                {\n                    for (var index = 0; index < cols.Count; index++)\n                    {\n                        var setting = propertyColumnDic.GetPropertySetting(cols[index]);\n                        if (setting is not null)\n                        {\n                            setting.ColumnIndex = index;\n                        }\n                    }\n\n                    if (propertyColumnDic.Values.Any(p => p.ColumnIndex < 0))\n                    {\n                        propertyColumnDic = propertyColumnDictionary;\n                    }\n\n                    isFirstLine = false;\n                }\n                else\n                {\n                    var entity = NewFuncHelper<TEntity>.Instance();\n                    if (entityType.IsValueType)\n                    {\n                        var obj = (object)entity!; // boxing for value types\n\n                        foreach (var key in propertyColumnDic.Keys)\n                        {\n                            var colIndex = propertyColumnDic[key].ColumnIndex;\n                            if (colIndex >= 0 && colIndex < cols.Count && key.CanWrite)\n                            {\n                                var columnValue = key.PropertyType.GetDefaultValue();\n                                var valueApplied = false;\n                                if (InternalCache.ColumnInputFormatterFuncCache.TryGetValue(key,\n                                        out var formatterFunc) && formatterFunc?.Method is not null)\n                                {\n                                    var cellValue = cols[colIndex];\n                                    try\n                                    {\n                                        // apply custom formatterFunc\n                                        columnValue = formatterFunc.DynamicInvoke(cellValue);\n                                        valueApplied = true;\n                                    }\n                                    catch (Exception e)\n                                    {\n                                        Debug.WriteLine(e);\n                                        InvokeHelper.OnInvokeException?.Invoke(e);\n                                    }\n                                }\n\n                                if (valueApplied == false)\n                                {\n                                    columnValue = cols[colIndex].ToOrDefault(key.PropertyType);\n                                }\n\n                                key.GetValueSetter()?.Invoke(entity!, columnValue);\n                            }\n                        }\n\n                        entity = (TEntity)obj; // unboxing\n                    }\n                    else\n                    {\n                        foreach (var key in propertyColumnDic.Keys)\n                        {\n                            var colIndex = propertyColumnDic[key].ColumnIndex;\n                            if (colIndex >= 0 && colIndex < cols.Count && key.CanWrite)\n                            {\n                                var columnValue = key.PropertyType.GetDefaultValue();\n\n                                var valueApplied = false;\n                                if (InternalCache.ColumnInputFormatterFuncCache.TryGetValue(key,\n                                        out var formatterFunc) && formatterFunc?.Method is not null)\n                                {\n                                    var cellValue = cols[colIndex];\n                                    try\n                                    {\n                                        // apply custom formatterFunc\n                                        columnValue = formatterFunc.DynamicInvoke(cellValue);\n                                        valueApplied = true;\n                                    }\n                                    catch (Exception e)\n                                    {\n                                        Debug.WriteLine(e);\n                                        InvokeHelper.OnInvokeException?.Invoke(e);\n                                    }\n                                }\n\n                                if (valueApplied == false)\n                                {\n                                    columnValue = cols[colIndex].ToOrDefault(key.PropertyType);\n                                }\n\n                                key.GetValueSetter()?.Invoke(entity!, columnValue);\n                            }\n                        }\n                    }\n\n                    if (entity is not null)\n                    {\n                        foreach (var propertyInfo in propertyColumnDic.Keys.Where(p => p.CanWrite))\n                        {\n                            var propertyValue = propertyInfo.GetValueGetter()?.Invoke(entity);\n                            if (InternalCache.InputFormatterFuncCache.TryGetValue(propertyInfo,\n                                    out var formatterFunc) && formatterFunc?.Method is not null)\n                            {\n                                try\n                                {\n                                    // apply custom formatterFunc\n                                    var formattedValue = formatterFunc.DynamicInvoke(entity, propertyValue);\n                                    propertyInfo.GetValueSetter()?.Invoke(entity, formattedValue);\n                                }\n                                catch (Exception e)\n                                {\n                                    Debug.WriteLine(e);\n                                    InvokeHelper.OnInvokeException?.Invoke(e);\n                                }\n                            }\n                        }\n                    }\n\n                    if (configuration.DataFilter?.Invoke(entity) == false)\n                    {\n                        continue;\n                    }\n\n                    configuration.PostImportAction?.Invoke(entity, lineIndex);\n\n                    yield return entity;\n                }\n            }\n        }\n    }\n\n    /// <summary>\n    ///     Parses a single CSV line into individual field values using default options.\n    /// </summary>\n    /// <param name=\"line\">The CSV line to parse</param>\n    /// <returns>A read-only list of field values</returns>\n    public static IReadOnlyList<string> ParseLine(string line) => ParseLine(line, CsvOptions.Default);\n\n    /// <summary>\n    ///     Parses a single CSV line into individual field values with custom options.\n    ///     Handles quoted values, escaped quotes, and separator characters within quoted fields.\n    /// </summary>\n    /// <param name=\"line\">The CSV line to parse</param>\n    /// <param name=\"csvOptions\">Custom CSV parsing options (separator, quote character)</param>\n    /// <returns>A read-only list of field values</returns>\n    /// <exception cref=\"ArgumentException\">Thrown when the line contains improperly escaped quotes</exception>\n    public static IReadOnlyList<string> ParseLine(string line, CsvOptions csvOptions)\n    {\n        if (string.IsNullOrEmpty(line))\n        {\n            return Array.Empty<string>();\n        }\n\n        var columnBuilder = new StringBuilder();\n        var fields = new List<string>();\n\n        var inColumn = false;\n        var inQuotes = false;\n\n        // Iterate through every character in the line\n        for (var i = 0; i < line.Length; i++)\n        {\n            var character = line[i];\n\n            // If we are not currently inside a column\n            if (!inColumn)\n            {\n                // If the current character is a double quote then the column value is contained within\n                // double quotes, otherwise append the next character\n                inColumn = true;\n                if (character == csvOptions.QuoteCharacter)\n                {\n                    inQuotes = true;\n                    continue;\n                }\n            }\n\n            // If we are in between double quotes\n            if (inQuotes)\n            {\n                if (i + 1 == line.Length)\n                {\n                    break;\n                }\n\n                if (character == csvOptions.QuoteCharacter && line[i + 1] == csvOptions.SeparatorCharacter) // quotes end\n                {\n                    inQuotes = false;\n                    inColumn = false;\n                    i++; //skip next\n                }\n                else if (character == csvOptions.QuoteCharacter && line[i + 1] == csvOptions.QuoteCharacter) // quotes\n                {\n                    i++; //skip next\n                }\n                else if (character == csvOptions.QuoteCharacter)\n                {\n                    throw new ArgumentException($\"unable to escape {line}\");\n                }\n            }\n            else if (character == csvOptions.SeparatorCharacter)\n            {\n                inColumn = false;\n            }\n\n            // If we are no longer in the column clear the builder and add the columns to the list\n            if (!inColumn)\n            {\n                fields.Add(columnBuilder.ToString());\n                columnBuilder.Clear();\n            }\n            else // append the current column\n            {\n                columnBuilder.Append(character);\n            }\n        }\n\n        fields.Add(columnBuilder.ToString());\n\n        return fields;\n    }\n\n    /// <summary>\n    ///     Saves a collection of entities to a CSV file with default options (includes header).\n    /// </summary>\n    /// <typeparam name=\"TEntity\">The entity type to export</typeparam>\n    /// <param name=\"entities\">The collection of entities to export</param>\n    /// <param name=\"filePath\">The destination file path</param>\n    /// <returns>True if the file was successfully created; otherwise, false</returns>\n    public static bool ToCsvFile<TEntity>(this IEnumerable<TEntity> entities, string filePath) =>\n        ToCsvFile(entities, filePath, CsvOptions.Default);\n\n    /// <summary>\n    ///     Saves a collection of entities to a CSV file with optional header.\n    /// </summary>\n    /// <typeparam name=\"TEntity\">The entity type to export</typeparam>\n    /// <param name=\"entities\">The collection of entities to export</param>\n    /// <param name=\"filePath\">The destination file path</param>\n    /// <param name=\"includeHeader\">Whether to include property names as column headers</param>\n    /// <returns>True if the file was successfully created; otherwise, false</returns>\n    public static bool ToCsvFile<TEntity>(this IEnumerable<TEntity> entities, string filePath, bool includeHeader)\n    {\n        return ToCsvFile(Guard.NotNull(entities), filePath, includeHeader ? CsvOptions.Default : new CsvOptions()\n        {\n            IncludeHeader = false\n        });\n    }\n\n    /// <summary>\n    ///     Saves a collection of entities to a CSV file with custom CSV options.\n    ///     Property values are formatted according to configured output formatters.\n    /// </summary>\n    /// <typeparam name=\"TEntity\">The entity type to export</typeparam>\n    /// <param name=\"entities\">The collection of entities to export</param>\n    /// <param name=\"filePath\">The destination file path</param>\n    /// <param name=\"csvOptions\">Custom CSV formatting options</param>\n    /// <returns>True if the file was successfully created; otherwise, false</returns>\n    public static bool ToCsvFile<TEntity>(this IEnumerable<TEntity> entities, string filePath, CsvOptions csvOptions)\n    {\n        if (entities is null)\n        {\n            throw new ArgumentNullException(nameof(entities));\n        }\n        Guard.NotNull(csvOptions);\n\n        var csvTextData = GetCsvText(entities, csvOptions);\n        if (csvTextData.IsNullOrEmpty())\n        {\n            return false;\n        }\n\n        var dir = Path.GetDirectoryName(filePath);\n        if (dir.IsNotNullOrEmpty())\n        {\n            if (!Directory.Exists(dir))\n            {\n                Directory.CreateDirectory(dir);\n            }\n        }\n\n        File.WriteAllText(filePath, csvTextData, csvOptions.Encoding);\n        return true;\n    }\n\n    /// <summary>\n    ///     Asynchronously saves a collection of entities to a CSV file.\n    ///     This method is more memory-efficient for large collections as it streams lines to the file.\n    /// </summary>\n    /// <typeparam name=\"TEntity\">The entity type to export</typeparam>\n    /// <param name=\"entities\">The collection of entities to export</param>\n    /// <param name=\"filePath\">The destination file path</param>\n    /// <param name=\"csvOptions\">Optional custom CSV formatting options</param>\n    /// <returns>A task that represents the asynchronous operation, containing true if successful</returns>\n    public static async Task<bool> ToCsvFileAsync<TEntity>(this IEnumerable<TEntity> entities, string filePath, CsvOptions? csvOptions = null)\n    {\n        if (entities is null)\n        {\n            throw new ArgumentNullException(nameof(entities));\n        }\n\n        csvOptions ??= CsvOptions.Default;\n\n        InternalHelper.EnsureFileIsNotReadOnly(filePath);\n        var dir = Path.GetDirectoryName(filePath);\n        if (dir.IsNotNullOrEmpty())\n        {\n            if (!Directory.Exists(dir))\n            {\n                Directory.CreateDirectory(dir);\n            }\n        }\n\n        var lines = GetCsvLines(entities, csvOptions);\n        using var file = File.CreateText(filePath);\n        foreach (var line in lines)\n        {\n            await file.WriteLineAsync(line).ConfigureAwait(false);\n        }\n        return true;\n    }\n\n    /// <summary>\n    ///     Converts a collection of entities to CSV formatted byte array with default encoding (includes header).\n    /// </summary>\n    /// <typeparam name=\"TEntity\">The entity type to convert</typeparam>\n    /// <param name=\"entities\">The collection of entities to convert</param>\n    /// <returns>CSV data as a byte array</returns>\n    public static byte[] ToCsvBytes<TEntity>(this IEnumerable<TEntity> entities) => ToCsvBytes(entities, CsvOptions.Default);\n\n    /// <summary>\n    ///     Converts a collection of entities to CSV formatted byte array with optional header.\n    /// </summary>\n    /// <typeparam name=\"TEntity\">The entity type to convert</typeparam>\n    /// <param name=\"entities\">The collection of entities to convert</param>\n    /// <param name=\"includeHeader\">Whether to include property names as column headers</param>\n    /// <returns>CSV data as a byte array</returns>\n    public static byte[] ToCsvBytes<TEntity>(this IEnumerable<TEntity> entities, bool includeHeader) =>\n        GetCsvText(entities, includeHeader).GetBytes();\n\n    /// <summary>\n    ///     Converts a collection of entities to CSV formatted byte array with custom options.\n    /// </summary>\n    /// <typeparam name=\"TEntity\">The entity type to convert</typeparam>\n    /// <param name=\"entities\">The collection of entities to convert</param>\n    /// <param name=\"csvOptions\">Custom CSV formatting options</param>\n    /// <returns>CSV data as a byte array</returns>\n    public static byte[] ToCsvBytes<TEntity>(this IEnumerable<TEntity> entities, CsvOptions csvOptions) =>\n        GetCsvText(entities, csvOptions).GetBytes(csvOptions.Encoding);\n\n    /// <summary>\n    ///     Converts a collection of entities to CSV formatted text with optional header (default includes header).\n    /// </summary>\n    /// <typeparam name=\"TEntity\">The entity type to convert</typeparam>\n    /// <param name=\"entities\">The collection of entities to convert</param>\n    /// <param name=\"includeHeader\">Whether to include property names as column headers</param>\n    /// <returns>CSV data as a string</returns>\n    public static string GetCsvText<TEntity>(this IEnumerable<TEntity> entities, bool includeHeader = true)\n    {\n        return GetCsvText(Guard.NotNull(entities), includeHeader ? CsvOptions.Default : new CsvOptions()\n        {\n            IncludeHeader = false\n        });\n    }\n\n    /// <summary>\n    ///     Converts a collection of entities to CSV formatted text with custom options.\n    /// </summary>\n    /// <typeparam name=\"TEntity\">The entity type to convert</typeparam>\n    /// <param name=\"entities\">The collection of entities to convert</param>\n    /// <param name=\"csvOptions\">Custom CSV formatting options</param>\n    /// <returns>CSV data as a string</returns>\n    public static string GetCsvText<TEntity>(this IEnumerable<TEntity> entities, CsvOptions csvOptions) =>\n        GetCsvLines(entities, csvOptions).StringJoin(Environment.NewLine);\n\n    /// <summary>\n    ///     Converts a collection of entities to a sequence of CSV formatted lines.\n    ///     For basic types, each entity is converted to a single line.\n    ///     For complex types, properties are mapped to columns with proper CSV escaping.\n    ///     Values containing separator characters are automatically quoted.\n    /// </summary>\n    /// <param name=\"entities\">The collection of entities to convert</param>\n    /// <param name=\"csvOptions\">Optional custom CSV formatting options</param>\n    /// <typeparam name=\"TEntity\">The entity type to convert</typeparam>\n    /// <returns>CSV formatted lines</returns>\n    public static IEnumerable<string> GetCsvLines<TEntity>(this IEnumerable<TEntity> entities, CsvOptions? csvOptions = null)\n    {\n        if (entities is null)\n        {\n            throw new ArgumentNullException(nameof(entities));\n        }\n        csvOptions ??= CsvOptions.Default;\n\n        var isBasicType = typeof(TEntity).IsBasicType();\n        if (isBasicType)\n        {\n            if (csvOptions.IncludeHeader)\n            {\n                yield return csvOptions.PropertyNameForBasicType;\n            }\n            foreach (var entity in entities)\n            {\n                if (entity is IFormattable formattableEntity)\n                    yield return formattableEntity.ToString();\n                else\n                    yield return Convert.ToString(entity) ?? string.Empty;\n            }\n        }\n        else\n        {\n            var dic = InternalHelper.GetPropertyColumnDictionary<TEntity>();\n            var props = InternalHelper.GetPropertiesForCsvHelper<TEntity>();\n            if (csvOptions.IncludeHeader)\n            {\n                yield return Enumerable.Range(0, props.Count)\n                    .Select(i => dic[props[i]].ColumnTitle)\n                    .StringJoin(csvOptions.SeparatorString);\n            }\n\n            foreach (var entity in entities)\n            {\n                var line = GetCsvLine().StringJoin(csvOptions.SeparatorString);\n                yield return line;\n\n                IEnumerable<string> GetCsvLine()\n                {\n                    for (var i = 0; i < props.Count; i++)\n                    {\n                        var propertyValue = props[i].GetValueGetter<TEntity>()?.Invoke(entity);\n                        if (InternalCache.OutputFormatterFuncCache.TryGetValue(props[i], out var formatterFunc) &&\n                            formatterFunc?.Method is not null)\n                        {\n                            try\n                            {\n                                // apply custom formatterFunc\n                                propertyValue = formatterFunc.DynamicInvoke(entity, propertyValue);\n                            }\n                            catch (Exception e)\n                            {\n                                Debug.WriteLine(e);\n                                InvokeHelper.OnInvokeException?.Invoke(e);\n                            }\n                        }\n\n                        // https://stackoverflow.com/questions/4617935/is-there-a-way-to-include-commas-in-csv-columns-without-breaking-the-formatting\n                        var val = propertyValue?.ToString()?.Replace(\n                            csvOptions.QuoteString,\n                            $\"{csvOptions.QuoteString}{csvOptions.QuoteString}\"\n                        );\n                        if (val is { Length: > 0 })\n                        {\n                            yield return val.IndexOf(csvOptions.SeparatorCharacter) > -1 ? $\"{csvOptions.QuoteCharacter}{val}{csvOptions.QuoteCharacter}\" : val;\n                        }\n                        else\n                        {\n                            yield return string.Empty;\n                        }\n                    }\n                }\n            }\n        }\n    }\n\n    /// <summary>\n    ///     Converts a DataTable to CSV formatted text with optional header (default includes header).\n    /// </summary>\n    /// <param name=\"dataTable\">The DataTable to convert</param>\n    /// <param name=\"includeHeader\">Whether to include column names as headers</param>\n    /// <returns>CSV data as a string</returns>\n    public static string GetCsvText(this DataTable? dataTable, bool includeHeader = true)\n    {\n        return GetCsvText(dataTable, includeHeader ? CsvOptions.Default : new CsvOptions()\n        {\n            IncludeHeader = false\n        });\n    }\n\n    /// <summary>\n    ///     Converts a DataTable to CSV formatted text with custom options.\n    ///     Column names are decoded if they were previously encoded to handle duplicates.\n    ///     Values containing separator characters are automatically quoted.\n    /// </summary>\n    /// <param name=\"dataTable\">The DataTable to convert</param>\n    /// <param name=\"csvOptions\">Custom CSV formatting options</param>\n    /// <returns>CSV data as a string, or empty string if the DataTable is null or empty</returns>\n    public static string GetCsvText(this DataTable? dataTable, CsvOptions csvOptions)\n    {\n        Guard.NotNull(csvOptions);\n        if (dataTable is null || dataTable.Rows.Count == 0 || dataTable.Columns.Count == 0)\n        {\n            return string.Empty;\n        }\n\n        var data = new StringBuilder();\n\n        if (csvOptions.IncludeHeader)\n        {\n            for (var i = 0; i < dataTable.Columns.Count; i++)\n            {\n                if (i > 0)\n                {\n                    data.Append(csvOptions.SeparatorCharacter);\n                }\n\n                var columnName = InternalHelper.GetDecodeColumnName(dataTable.Columns[i].ColumnName);\n                data.Append(columnName);\n            }\n\n            data.AppendLine();\n        }\n\n        for (var i = 0; i < dataTable.Rows.Count; i++)\n        {\n            for (var j = 0; j < dataTable.Columns.Count; j++)\n            {\n                if (j > 0)\n                {\n                    data.Append(csvOptions.SeparatorCharacter);\n                }\n\n                // https://stackoverflow.com/questions/4617935/is-there-a-way-to-include-commas-in-csv-columns-without-breaking-the-formatting\n                var val = dataTable.Rows[i][j].ToString()?.Replace(csvOptions.QuoteString, $\"{csvOptions.QuoteString}{csvOptions.QuoteString}\");\n                if (val is { Length: > 0 })\n                {\n                    data.Append(val.IndexOf(csvOptions.SeparatorCharacter) > -1 ? $\"{csvOptions.QuoteString}{val}{csvOptions.QuoteString}\" : val);\n                }\n            }\n\n            data.AppendLine();\n        }\n\n        return data.ToString();\n    }\n}\n"
  },
  {
    "path": "src/WeihanLi.Npoi/ExcelFormat.cs",
    "content": "﻿// Copyright (c) Weihan Li. All rights reserved.\n// Licensed under the Apache license.\n\nnamespace WeihanLi.Npoi;\n\n/// <summary>\n///     ExcelFormat\n/// </summary>\npublic enum ExcelFormat : byte\n{\n    /// <summary>\n    ///     xls by default\n    /// </summary>\n    Xls = 0,\n\n    /// <summary>\n    ///     xlsx\n    /// </summary>\n    Xlsx = 1\n}\n"
  },
  {
    "path": "src/WeihanLi.Npoi/ExcelHelper.cs",
    "content": "﻿// Copyright (c) Weihan Li. All rights reserved.\n// Licensed under the Apache license.\n\nusing NPOI.HPSF;\nusing NPOI.HSSF.UserModel;\nusing NPOI.SS.UserModel;\nusing NPOI.XSSF.UserModel;\nusing System.Data;\nusing WeihanLi.Common;\nusing WeihanLi.Common.Models;\nusing WeihanLi.Common.Services;\nusing WeihanLi.Extensions;\nusing WeihanLi.Npoi.Settings;\n\nnamespace WeihanLi.Npoi;\n\n/// <summary>\n///     ExcelHelper\n/// </summary>\npublic static class ExcelHelper\n{\n    private static readonly Version AppVersion = typeof(ExcelHelper).Assembly.GetName().Version!;\n\n    /// <summary>\n    ///     Default excel setting for export excel files\n    /// </summary>\n    public static ExcelSetting DefaultExcelSetting\n    {\n        get;\n        set => field = Guard.NotNull(value);\n    } = new();\n\n    /// <summary>\n    /// Default Data Validator\n    /// </summary>\n    public static IValidator DefaultDataValidator\n    {\n        get;\n        set => field = Guard.NotNull(value);\n    } = DataAnnotationValidator.Instance;\n\n    /// <summary>\n    ///     Validate whether the Excel path valid\n    /// </summary>\n    /// <param name=\"excelPath\">excel path</param>\n    /// <param name=\"msg\">error message</param>\n    /// <param name=\"isExport\">is export operation</param>\n    /// <returns>is valid Excel path</returns>\n    private static bool ValidateExcelFilePath(string excelPath, out string msg, bool isExport = false)\n    {\n        if (string.IsNullOrWhiteSpace(excelPath))\n        {\n            throw new ArgumentNullException(nameof(excelPath));\n        }\n\n        if (isExport || File.Exists(excelPath))\n        {\n            var ext = Path.GetExtension(excelPath);\n            if (ext.EqualsIgnoreCase(\".xls\") || ext.EqualsIgnoreCase(\".xlsx\"))\n            {\n                msg = string.Empty;\n                return true;\n            }\n\n            msg = Resource.InvalidExcelFile;\n            return false;\n        }\n\n        msg = Resource.FileNotFound;\n        return false;\n    }\n\n    /// <summary>\n    ///     load excel from filepath\n    /// </summary>\n    /// <param name=\"excelPath\">excel file path</param>\n    /// <returns>workbook</returns>\n    public static IWorkbook LoadExcel(string excelPath)\n    {\n        if (!ValidateExcelFilePath(excelPath, out var msg))\n        {\n            throw new ArgumentException(msg);\n        }\n\n        using var stream = new FileStream(excelPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);\n        return Path.GetExtension(excelPath).EqualsIgnoreCase(\".xls\")\n            ? new HSSFWorkbook(stream)\n            : new XSSFWorkbook(stream);\n    }\n\n    /// <summary>\n    ///     load excel from excelBytes\n    /// </summary>\n    /// <param name=\"excelBytes\">excel file bytes</param>\n    /// <returns>workbook</returns>\n    public static IWorkbook LoadExcel(byte[] excelBytes) => LoadExcel(excelBytes, ExcelFormat.Xls);\n\n    /// <summary>\n    ///     load excel from excelBytes\n    /// </summary>\n    /// <param name=\"excelBytes\">excel file bytes</param>\n    /// <param name=\"excelFormat\">excelFormat</param>\n    /// <returns>workbook</returns>\n    public static IWorkbook LoadExcel(byte[] excelBytes, ExcelFormat excelFormat)\n    {\n        if (excelBytes is null)\n        {\n            throw new ArgumentNullException(nameof(excelBytes));\n        }\n\n        using var stream = new MemoryStream(excelBytes);\n        return LoadExcel(stream, excelFormat);\n    }\n\n    /// <summary>\n    ///     load excel from excelBytes\n    /// </summary>\n    /// <param name=\"excelStream\">excel file stream</param>\n    /// <returns>workbook</returns>\n    public static IWorkbook LoadExcel(Stream excelStream) => LoadExcel(excelStream, ExcelFormat.Xls);\n\n    /// <summary>\n    ///     load excel from excelBytes\n    /// </summary>\n    /// <param name=\"excelStream\">excel file stream</param>\n    /// <param name=\"excelFormat\">excelFormat</param>\n    /// <returns>workbook</returns>\n    public static IWorkbook LoadExcel(Stream excelStream, ExcelFormat excelFormat)\n    {\n        if (excelStream is null)\n        {\n            throw new ArgumentNullException(nameof(excelStream));\n        }\n\n        return excelFormat switch\n        {\n            ExcelFormat.Xls => new HSSFWorkbook(excelStream),\n            _ => new XSSFWorkbook(excelStream)\n        };\n    }\n\n    /// <summary>\n    ///     prepare a workbook for export\n    /// </summary>\n    /// <param name=\"excelPath\">excelPath</param>\n    /// <returns></returns>\n    public static IWorkbook PrepareWorkbook(string excelPath) => PrepareWorkbook(excelPath, null);\n\n    /// <summary>\n    ///     prepare a workbook for export\n    /// </summary>\n    /// <param name=\"excelPath\">excelPath</param>\n    /// <param name=\"excelSetting\">excelSetting</param>\n    /// <returns></returns>\n    public static IWorkbook PrepareWorkbook(string excelPath, ExcelSetting? excelSetting)\n    {\n        if (!ValidateExcelFilePath(excelPath, out var msg, true))\n        {\n            throw new ArgumentException(msg);\n        }\n\n        return PrepareWorkbook(!Path.GetExtension(excelPath).EqualsIgnoreCase(\".xls\"), excelSetting);\n    }\n\n    /// <summary>\n    ///     prepare a workbook for export\n    /// </summary>\n    /// <param name=\"excelFormat\">excelFormat</param>\n    /// <param name=\"excelSetting\">excelSetting</param>\n    /// <returns></returns>\n    public static IWorkbook PrepareWorkbook(ExcelFormat excelFormat, ExcelSetting? excelSetting) =>\n        PrepareWorkbook(excelFormat == ExcelFormat.Xlsx, excelSetting);\n\n    /// <summary>\n    ///     get a excel workbook(*.xlsx)\n    /// </summary>\n    /// <returns></returns>\n    public static IWorkbook PrepareWorkbook() => PrepareWorkbook(true);\n\n    /// <summary>\n    ///     get a excel workbook\n    /// </summary>\n    /// <param name=\"excelFormat\">excelFormat</param>\n    /// <returns></returns>\n    public static IWorkbook PrepareWorkbook(ExcelFormat excelFormat) =>\n        PrepareWorkbook(excelFormat == ExcelFormat.Xlsx);\n\n    /// <summary>\n    ///     get a excel workbook\n    /// </summary>\n    /// <param name=\"isXlsx\">is for *.xlsx file</param>\n    /// <returns></returns>\n    public static IWorkbook PrepareWorkbook(bool isXlsx) => PrepareWorkbook(isXlsx, null);\n\n    /// <summary>\n    ///     get a excel workbook\n    /// </summary>\n    /// <param name=\"isXlsx\">is for *.xlsx file</param>\n    /// <param name=\"excelSetting\">excelSettings</param>\n    /// <returns></returns>\n    public static IWorkbook PrepareWorkbook(bool isXlsx, ExcelSetting? excelSetting)\n    {\n        var setting = excelSetting ?? DefaultExcelSetting;\n\n        if (isXlsx)\n        {\n            var workbook = new XSSFWorkbook();\n            var props = workbook.GetProperties();\n            props.CoreProperties.Creator = setting.Author;\n            props.CoreProperties.Created = DateTime.Now;\n            props.CoreProperties.Modified = DateTime.Now;\n            props.CoreProperties.Title = setting.Title;\n            props.CoreProperties.Subject = setting.Subject;\n            props.CoreProperties.Category = setting.Category;\n            props.CoreProperties.Description = setting.Description;\n            props.ExtendedProperties.GetUnderlyingProperties().Company = setting.Company;\n            props.ExtendedProperties.GetUnderlyingProperties().Application = InternalConstants.ApplicationName;\n            props.ExtendedProperties.GetUnderlyingProperties().AppVersion = AppVersion.ToString(3);\n            return workbook;\n        }\n        else\n        {\n            var workbook = new HSSFWorkbook();\n            // create a entry of DocumentSummaryInformation\n            var dsi = new DocumentSummaryInformation\n            {\n                Company = setting.Company,\n                Category = setting.Category\n            };\n            workbook.DocumentSummaryInformation = dsi;\n            // create a entry of SummaryInformation\n            var si = new SummaryInformation\n            {\n                Title = setting.Title,\n                Subject = setting.Subject,\n                Author = setting.Author,\n                CreateDateTime = DateTime.Now,\n                Comments = setting.Description,\n                ApplicationName = InternalConstants.ApplicationName\n            };\n            workbook.SummaryInformation = si;\n            return workbook;\n        }\n    }\n\n    /// <summary>\n    ///     read first sheet of excel from excel file bytes to a list\n    /// </summary>\n    /// <typeparam name=\"TEntity\">EntityType</typeparam>\n    /// <param name=\"excelBytes\">excelBytes</param>\n    /// <returns>List</returns>\n    public static List<TEntity?> ToEntityList<TEntity>(byte[] excelBytes) where TEntity : new()\n        => ToEntityList<TEntity>(excelBytes, ExcelFormat.Xls, 0);\n\n    /// <summary>\n    ///     read (sheetIndex) sheet of excel from excel file bytes to a list\n    /// </summary>\n    /// <typeparam name=\"TEntity\">EntityType</typeparam>\n    /// <param name=\"excelBytes\">excelBytes</param>\n    /// <param name=\"sheetIndex\">sheetIndex</param>\n    /// <returns>List</returns>\n    public static List<TEntity?> ToEntityList<TEntity>(byte[] excelBytes, int sheetIndex)\n        where TEntity : new()\n        => ToEntityList<TEntity>(excelBytes, ExcelFormat.Xls, sheetIndex);\n\n    /// <summary>\n    ///     read first sheet of excel from excel file bytes to a list\n    /// </summary>\n    /// <typeparam name=\"TEntity\">EntityType</typeparam>\n    /// <param name=\"excelBytes\">excelBytes</param>\n    /// <param name=\"excelFormat\">excelFormat</param>\n    /// <returns>List</returns>\n    public static List<TEntity?> ToEntityList<TEntity>(byte[] excelBytes, ExcelFormat excelFormat)\n        where TEntity : new() => ToEntityList<TEntity>(excelBytes, excelFormat, 0);\n\n    /// <summary>\n    ///     read (sheetIndex) sheet of excel from excel bytes to a list\n    /// </summary>\n    /// <typeparam name=\"TEntity\">EntityType</typeparam>\n    /// <param name=\"excelBytes\">excelBytes</param>\n    /// <param name=\"excelFormat\">excelFormat</param>\n    /// <param name=\"sheetIndex\">sheetIndex</param>\n    /// <returns>List</returns>\n    public static List<TEntity?> ToEntityList<TEntity>(byte[] excelBytes, ExcelFormat excelFormat, int sheetIndex)\n        where TEntity : new()\n    {\n        using var workbook = LoadExcel(excelBytes, excelFormat);\n        return workbook.ToEntityList<TEntity>(sheetIndex);\n    }\n\n    /// <summary>\n    ///     Lazily converts the specified sheet within an in-memory Excel payload into entities.\n    /// </summary>\n    /// <typeparam name=\"TEntity\">Entity type.</typeparam>\n    /// <param name=\"excelBytes\">Excel file bytes.</param>\n    /// <param name=\"excelFormat\">Workbook format.</param>\n    /// <param name=\"sheetIndex\">Zero-based sheet index.</param>\n    /// <returns>Sequence that yields entities row by row.</returns>\n    public static IEnumerable<TEntity?> ToEntities<TEntity>(byte[] excelBytes, ExcelFormat excelFormat = ExcelFormat.Xls, int sheetIndex = 0)\n        where TEntity : new()\n    {\n        using var workbook = LoadExcel(excelBytes, excelFormat);\n        foreach (var entity in workbook.ToEntities<TEntity>(sheetIndex))\n        {\n            yield return entity;\n        }\n    }\n\n    /// <summary>\n    ///     read (sheetIndex) sheet of excel from excel bytes to a list\n    /// </summary>\n    /// <typeparam name=\"TEntity\">EntityType</typeparam>\n    /// <param name=\"excelBytes\">excelBytes</param>\n    /// <param name=\"excelFormat\">excelFormat</param>\n    /// <param name=\"sheetIndex\">sheetIndex</param>\n    /// <param name=\"validator\">validator</param>\n    /// <returns>List and validationResult</returns>\n    public static (List<TEntity?> EntityList, Dictionary<int, ValidationResult> ValidationResults)\n        ToEntityListWithValidationResult<TEntity>(\n            byte[] excelBytes,\n            ExcelFormat excelFormat = ExcelFormat.Xls,\n            int sheetIndex = 0,\n            IValidator<TEntity>? validator = null)\n        where TEntity : new()\n    {\n        using var workbook = LoadExcel(excelBytes, excelFormat);\n        return workbook.GetSheetAt(sheetIndex).ToEntityListWithValidationResult(sheetIndex, validator);\n    }\n\n    /// <summary>\n    ///     read first sheet of excel from excel stream to a list\n    /// </summary>\n    /// <typeparam name=\"TEntity\">EntityType</typeparam>\n    /// <param name=\"excelStream\">excelStream</param>\n    /// <returns>List</returns>\n    public static List<TEntity?> ToEntityList<TEntity>(Stream excelStream) where TEntity : new()\n        => ToEntityList<TEntity>(excelStream, ExcelFormat.Xls, 0);\n\n    /// <summary>\n    ///     read (sheetIndex) sheet of excel from excel file bytes to a list\n    /// </summary>\n    /// <typeparam name=\"TEntity\">EntityType</typeparam>\n    /// <param name=\"excelStream\">excelStream</param>\n    /// <param name=\"sheetIndex\">sheetIndex</param>\n    /// <returns>List</returns>\n    public static List<TEntity?> ToEntityList<TEntity>(Stream excelStream, int sheetIndex)\n        where TEntity : new()\n        => ToEntityList<TEntity>(excelStream, ExcelFormat.Xls, sheetIndex);\n\n    /// <summary>\n    ///     read first sheet of excel from excel file bytes to a list\n    /// </summary>\n    /// <typeparam name=\"TEntity\">EntityType</typeparam>\n    /// <param name=\"excelStream\">excelStream</param>\n    /// <param name=\"excelFormat\">excelFormat</param>\n    /// <returns>List</returns>\n    public static List<TEntity?> ToEntityList<TEntity>(Stream excelStream, ExcelFormat excelFormat)\n        where TEntity : new()\n        => ToEntityList<TEntity>(excelStream, excelFormat, 0);\n\n    /// <summary>\n    ///     read (sheetIndex) sheet of excel from excel bytes path to a list\n    /// </summary>\n    /// <typeparam name=\"TEntity\">EntityType</typeparam>\n    /// <param name=\"excelStream\">excelStream</param>\n    /// <param name=\"excelFormat\">excelFormat</param>\n    /// <param name=\"sheetIndex\">sheetIndex</param>\n    /// <returns>List</returns>\n    public static List<TEntity?> ToEntityList<TEntity>(Stream excelStream, ExcelFormat excelFormat, int sheetIndex)\n        where TEntity : new()\n    {\n        using var workbook = LoadExcel(excelStream, excelFormat);\n        return workbook.ToEntityList<TEntity>(sheetIndex);\n    }\n\n    /// <summary>\n    ///     Lazily converts the specified sheet within an Excel stream into entities.\n    /// </summary>\n    /// <typeparam name=\"TEntity\">Entity type.</typeparam>\n    /// <param name=\"excelStream\">Excel stream.</param>\n    /// <param name=\"excelFormat\">Workbook format.</param>\n    /// <param name=\"sheetIndex\">Zero-based sheet index.</param>\n    /// <returns>Sequence that yields entities row by row.</returns>\n    public static IEnumerable<TEntity?> ToEntities<TEntity>(Stream excelStream, ExcelFormat excelFormat = ExcelFormat.Xls, int sheetIndex = 0)\n        where TEntity : new()\n    {\n        using var workbook = LoadExcel(excelStream, excelFormat);\n        foreach (var entity in workbook.ToEntities<TEntity>(sheetIndex))\n        {\n            yield return entity;\n        }\n    }\n\n    /// <summary>\n    ///     read (sheetIndex) sheet of excel from excel bytes path to a list\n    /// </summary>\n    /// <typeparam name=\"TEntity\">EntityType</typeparam>\n    /// <param name=\"excelStream\">excelStream</param>\n    /// <param name=\"excelFormat\">excelFormat</param>\n    /// <param name=\"sheetIndex\">sheetIndex</param>\n    /// <param name=\"validator\">data validator</param>\n    /// <returns>List</returns>\n    public static (List<TEntity?> EntityList, Dictionary<int, ValidationResult> ValidationResults) ToEntityListWithValidationResult<TEntity>(\n            Stream excelStream, ExcelFormat excelFormat = ExcelFormat.Xls,\n            int sheetIndex = 0,\n            IValidator<TEntity>? validator = null)\n        where TEntity : new()\n    {\n        using var workbook = LoadExcel(excelStream, excelFormat);\n        return workbook.GetSheetAt(sheetIndex).ToEntityListWithValidationResult(sheetIndex, validator);\n    }\n\n    /// <summary>\n    ///     read first sheet of excel from excel file path to a list\n    /// </summary>\n    /// <typeparam name=\"TEntity\">EntityType</typeparam>\n    /// <param name=\"excelPath\">excelPath</param>\n    /// <returns>List</returns>\n    public static List<TEntity?> ToEntityList<TEntity>(string excelPath) where TEntity : new() =>\n        ToEntityList<TEntity>(excelPath, 0);\n\n    /// <summary>\n    ///     read (sheetIndex) sheet of excel from excel file path to a list\n    /// </summary>\n    /// <typeparam name=\"TEntity\">EntityType</typeparam>\n    /// <param name=\"excelPath\">excelPath</param>\n    /// <param name=\"sheetIndex\">sheetIndex</param>\n    /// <returns>List</returns>\n    public static List<TEntity?> ToEntityList<TEntity>(string excelPath, int sheetIndex) where TEntity : new()\n    {\n        using var workbook = LoadExcel(excelPath);\n        return workbook.ToEntityList<TEntity>(sheetIndex);\n    }\n\n    /// <summary>\n    ///     Lazily converts the specified sheet within an Excel file path into entities.\n    /// </summary>\n    /// <typeparam name=\"TEntity\">Entity type.</typeparam>\n    /// <param name=\"excelPath\">Excel file path.</param>\n    /// <param name=\"sheetIndex\">Zero-based sheet index.</param>\n    /// <returns>Sequence that yields entities row by row.</returns>\n    public static IEnumerable<TEntity?> ToEntities<TEntity>(string excelPath, int sheetIndex) where TEntity : new()\n    {\n        using var workbook = LoadExcel(excelPath);\n        foreach (var entity in workbook.ToEntities<TEntity>(sheetIndex))\n        {\n            yield return entity;\n        }\n    }\n\n    /// <summary>\n    ///     read (sheetIndex) sheet of excel from excel file path to a list\n    /// </summary>\n    /// <typeparam name=\"TEntity\">EntityType</typeparam>\n    /// <param name=\"excelPath\">excelPath</param>\n    /// <param name=\"sheetIndex\">sheetIndex</param>\n    /// <param name=\"validator\">validator</param>\n    /// <returns>List and validationResult</returns>\n    public static (List<TEntity?> EntityList, Dictionary<int, ValidationResult> ValidationResults) ToEntityListWithValidationResult<TEntity>(\n        string excelPath, int sheetIndex = 0, IValidator<TEntity>? validator = null\n    ) where TEntity : new()\n    {\n        using var workbook = LoadExcel(excelPath);\n        return workbook.GetSheetAt(sheetIndex).ToEntityListWithValidationResult(sheetIndex, validator);\n    }\n\n    /// <summary>\n    ///     read first sheet of excel from excel file path to a data table\n    /// </summary>\n    /// <typeparam name=\"TEntity\">EntityType</typeparam>\n    /// <param name=\"excelPath\">excelPath</param>\n    /// <returns>DataTable</returns>\n    public static DataTable ToDataTable<TEntity>(string excelPath) where TEntity : new() =>\n        ToDataTable<TEntity>(excelPath, 0);\n\n    /// <summary>\n    ///     read (sheetIndex) sheet of excel from excel file path to a list(for specific class type)\n    /// </summary>\n    /// <typeparam name=\"TEntity\">EntityType</typeparam>\n    /// <param name=\"excelPath\">excelPath</param>\n    /// <param name=\"sheetIndex\">sheetIndex</param>\n    /// <returns>DataTable</returns>\n    public static DataTable ToDataTable<TEntity>(string excelPath, int sheetIndex) where TEntity : new()\n        => ToEntityList<TEntity>(excelPath, sheetIndex).ToDataTable();\n\n    /// <summary>\n    ///     read first sheet of excel from excel file path to a data table\n    /// </summary>\n    /// <param name=\"excelPath\">excelPath</param>\n    /// <returns>DataTable</returns>\n    public static DataTable ToDataTable(string excelPath) => ToDataTable(excelPath, 0, 0);\n\n    /// <summary>\n    ///     read first sheet of excel from excel file path to a data table\n    /// </summary>\n    /// <param name=\"excelPath\">excelPath</param>\n    /// <param name=\"sheetIndex\">sheetIndex</param>\n    /// <returns>DataTable</returns>\n    public static DataTable ToDataTable(string excelPath, int sheetIndex) =>\n        ToDataTable(excelPath, sheetIndex, 0);\n\n    /// <summary>\n    ///     read (sheetIndex) sheet of excel from excel file path to a data table\n    /// </summary>\n    /// <param name=\"excelPath\">excelPath</param>\n    /// <param name=\"sheetIndex\">sheetIndex</param>\n    /// <param name=\"headerRowIndex\">headerRowIndex</param>\n    /// <param name=\"removeEmptyRows\">removeEmptyRows</param>\n    /// <param name=\"maxColumns\">maxColumns</param>\n    /// <returns>DataTable</returns>\n    public static DataTable ToDataTable(string excelPath, int sheetIndex, int headerRowIndex,\n        bool removeEmptyRows = false, int? maxColumns = null)\n    {\n        using var workbook = LoadExcel(excelPath);\n        if (workbook.NumberOfSheets <= sheetIndex)\n        {\n            throw new ArgumentOutOfRangeException(nameof(sheetIndex),\n                string.Format(Resource.IndexOutOfRange, nameof(sheetIndex), workbook.NumberOfSheets));\n        }\n\n        return workbook.GetSheetAt(sheetIndex).ToDataTable(headerRowIndex, removeEmptyRows, maxColumns);\n    }\n\n    /// <summary>\n    ///     read first sheet of excel from excelBytes to a data table\n    /// </summary>\n    /// <param name=\"excelBytes\">excelBytes</param>\n    /// <param name=\"excelFormat\"></param>\n    /// <param name=\"removeEmptyRows\">removeEmptyRows</param>\n    /// <param name=\"maxColumns\">maxColumns</param>\n    /// <returns>DataTable</returns>\n    public static DataTable ToDataTable(byte[] excelBytes, ExcelFormat excelFormat, bool removeEmptyRows = false,\n        int? maxColumns = null)\n        => ToDataTable(excelBytes, excelFormat, 0, removeEmptyRows, maxColumns);\n\n    /// <summary>\n    ///     read (sheetIndex) sheet of excel from excelBytes to a data table\n    /// </summary>\n    /// <param name=\"excelBytes\">excelBytes</param>\n    /// <param name=\"excelFormat\"></param>\n    /// <param name=\"sheetIndex\">sheetIndex</param>\n    /// <param name=\"removeEmptyRows\">removeEmptyRows</param>\n    /// <param name=\"maxColumns\">maxColumns</param>\n    /// <returns>DataTable</returns>\n    public static DataTable ToDataTable(byte[] excelBytes, ExcelFormat excelFormat, int sheetIndex,\n        bool removeEmptyRows = false, int? maxColumns = null)\n        => ToDataTable(excelBytes, excelFormat, sheetIndex, 0, removeEmptyRows, maxColumns);\n\n    /// <summary>\n    ///     read (sheetIndex) sheet of excel from excelBytes to a data table\n    /// </summary>\n    /// <param name=\"excelBytes\">excelBytes</param>\n    /// <param name=\"excelFormat\"></param>\n    /// <param name=\"sheetIndex\">sheetIndex</param>\n    /// <param name=\"headerRowIndex\">headerRowIndex</param>\n    /// <param name=\"removeEmptyRows\">removeEmptyRows</param>\n    /// <param name=\"maxColumns\">maxColumns</param>\n    /// <returns>DataTable</returns>\n    public static DataTable ToDataTable(byte[] excelBytes, ExcelFormat excelFormat, int sheetIndex,\n        int headerRowIndex, bool removeEmptyRows = false, int? maxColumns = null)\n    {\n        using var workbook = LoadExcel(excelBytes, excelFormat);\n        if (workbook.NumberOfSheets <= sheetIndex)\n        {\n            throw new ArgumentOutOfRangeException(nameof(sheetIndex),\n                string.Format(Resource.IndexOutOfRange, nameof(sheetIndex), workbook.NumberOfSheets));\n        }\n\n        return workbook.GetSheetAt(sheetIndex).ToDataTable(headerRowIndex, removeEmptyRows, maxColumns);\n    }\n\n    /// <summary>\n    ///     read first sheet of excel from excel file path to a DataSet from second row\n    /// </summary>\n    /// <param name=\"excelPath\">excelPath</param>\n    /// <returns></returns>\n    public static DataSet ToDataSet(string excelPath) => ToDataSet(excelPath, 0);\n\n    /// <summary>\n    ///     read first sheet of excel from excel file path to a DataSet from (headerRowIndex+1) row\n    /// </summary>\n    /// <param name=\"excelPath\">excelPath</param>\n    /// <param name=\"headerRowIndex\">headerRowIndex</param>\n    /// <returns></returns>\n    public static DataSet ToDataSet(string excelPath, int headerRowIndex)\n    {\n        using var workbook = LoadExcel(excelPath);\n        return workbook.ToDataSet(headerRowIndex);\n    }\n}\n"
  },
  {
    "path": "src/WeihanLi.Npoi/FakePropertyInfo.cs",
    "content": "﻿// Copyright (c) Weihan Li. All rights reserved.\n// Licensed under the Apache license.\n\nusing System.Globalization;\nusing System.Reflection;\nusing WeihanLi.Extensions;\n\nnamespace WeihanLi.Npoi;\n#nullable disable\n\ninternal sealed class FakePropertyInfo : PropertyInfo\n{\n    private readonly Func<object> _getValueFunc;\n    private readonly object _value;\n\n    public FakePropertyInfo(Type entityType, Type propertyType, string propertyName)\n    {\n        DeclaringType = entityType;\n        ReflectedType = entityType;\n        PropertyType = propertyType;\n        Name = propertyName;\n        _value = propertyType.GetDefaultValue();\n        _getValueFunc = () => _value;\n        Attributes = PropertyAttributes.None;\n    }\n\n    public override Type DeclaringType { get; }\n    public override string Name { get; }\n    public override Type ReflectedType { get; }\n\n    public override bool CanRead => false;\n    public override bool CanWrite => false;\n    public override Type PropertyType { get; }\n\n    public override PropertyAttributes Attributes { get; }\n\n    public override MethodInfo GetGetMethod(bool nonPublic) => _getValueFunc.Method;\n\n    public override MethodInfo GetSetMethod(bool nonPublic) => null;\n\n    public override string ToString() => $\"{PropertyType.Name}, {Name}\";\n\n    public override object[] GetCustomAttributes(bool inherit) => throw new NotSupportedException();\n\n    public override object[] GetCustomAttributes(Type attributeType, bool inherit) =>\n        throw new NotSupportedException();\n\n    public override bool IsDefined(Type attributeType, bool inherit) => throw new NotSupportedException();\n\n    public override MethodInfo[] GetAccessors(bool nonPublic) => throw new NotSupportedException();\n\n    public override ParameterInfo[] GetIndexParameters() => throw new NotSupportedException();\n\n    public override object GetValue(object obj, BindingFlags invokeAttr, Binder binder, object[] index,\n        CultureInfo culture) => _value;\n\n    public override void SetValue(object obj, object value, BindingFlags invokeAttr, Binder binder, object[] index,\n        CultureInfo culture) => throw new NotSupportedException();\n}\n\n#nullable restore\n"
  },
  {
    "path": "src/WeihanLi.Npoi/FluentSettings.cs",
    "content": "﻿// Copyright (c) Weihan Li. All rights reserved.\n// Licensed under the Apache license.\n\nusing System.Reflection;\nusing WeihanLi.Common;\nusing WeihanLi.Common.Helpers;\nusing WeihanLi.Extensions;\nusing WeihanLi.Npoi.Configurations;\n\nnamespace WeihanLi.Npoi;\n\n/// <summary>\n/// Central entry point for configuring Excel mappings via a fluent API.\n/// </summary>\npublic static class FluentSettings\n{\n    private const string MappingProfileConfigureMethodName = \"Configure\";\n    private static readonly Type s_profileGenericTypeDefinition = typeof(IMappingProfile<>);\n\n    /// <summary>\n    ///     Fluent Setting For TEntity\n    /// </summary>\n    /// <typeparam name=\"TEntity\">TEntity</typeparam>\n    /// <returns>excel configuration for entity</returns>\n    public static IExcelConfiguration<TEntity> For<TEntity>() =>\n        InternalHelper.GetExcelConfigurationMapping<TEntity>();\n\n    /// <summary>\n    ///     Load mapping profiles\n    /// </summary>\n    /// <param name=\"assemblies\">assemblies</param>\n    public static void LoadMappingProfiles(params Assembly[] assemblies)\n    {\n        Guard.NotNull(assemblies, nameof(assemblies));\n        if (assemblies.Length == 0)\n        {\n            assemblies = ReflectHelper.GetAssemblies();\n        }\n\n        LoadMappingProfiles(assemblies.SelectMany(ass => ass.GetExportedTypes()).ToArray());\n    }\n\n    /// <summary>\n    ///     Load mapping profiles\n    /// </summary>\n    /// <param name=\"types\">mapping profile types</param>\n    public static void LoadMappingProfiles(params Type[] types)\n    {\n        Guard.NotNull(types, nameof(types));\n        foreach (var type in types.Where(x => x.IsAssignableTo<IMappingProfile>()))\n        {\n            if (Activator.CreateInstance(type) is IMappingProfile profile)\n            {\n                LoadMappingProfile(profile);\n            }\n        }\n    }\n\n    /// <summary>\n    ///     Load mapping profile for TEntity\n    /// </summary>\n    /// <typeparam name=\"TEntity\">entity type</typeparam>\n    /// <typeparam name=\"TMappingProfile\">entity type mapping profile</typeparam>\n    public static void LoadMappingProfile<TEntity, TMappingProfile>()\n        where TMappingProfile : IMappingProfile<TEntity>, new()\n    {\n        var profile = new TMappingProfile();\n        profile.Configure(InternalHelper.GetExcelConfigurationMapping<TEntity>());\n    }\n\n    /// <summary>\n    ///     Load mapping profile for TEntity\n    /// </summary>\n    /// <param name=\"profile\">profile</param>\n    /// <typeparam name=\"TEntity\">entity type</typeparam>\n    /// <typeparam name=\"TMappingProfile\">mapping profile type</typeparam>\n    public static void LoadMappingProfile<TEntity, TMappingProfile>(TMappingProfile profile) where TMappingProfile : IMappingProfile<TEntity>\n    {\n        Guard.NotNull(profile);\n        profile.Configure(InternalHelper.GetExcelConfigurationMapping<TEntity>());\n    }\n\n    /// <summary>\n    ///     Load mapping profile for TEntity\n    /// </summary>\n    /// <typeparam name=\"TMappingProfile\">entity type mapping profile</typeparam>\n    public static void LoadMappingProfile<TMappingProfile>() where TMappingProfile : IMappingProfile, new() =>\n        LoadMappingProfile(new TMappingProfile());\n\n    /// <summary>\n    ///     Load mapping profile for TEntity\n    /// </summary>\n    /// <param name=\"profile\">profile</param>\n    private static void LoadMappingProfile<TMappingProfile>(TMappingProfile profile) where TMappingProfile : IMappingProfile\n    {\n        Guard.NotNull(profile, nameof(profile));\n        var profileInterfaceType = profile.GetType()\n            .GetImplementedInterfaces()\n            .FirstOrDefault(x => x.IsGenericType && x.GetGenericTypeDefinition() == s_profileGenericTypeDefinition);\n        if (profileInterfaceType is null)\n        {\n            return;\n        }\n\n        var entityType = profileInterfaceType.GetGenericArguments()[0];\n        var configuration = InternalHelper.GetExcelConfigurationMapping(entityType);\n        var method = profileInterfaceType.GetMethod(MappingProfileConfigureMethodName,\n            [typeof(IExcelConfiguration<>).MakeGenericType(entityType)]);\n        method?.Invoke(profile, [configuration]);\n    }\n}\n"
  },
  {
    "path": "src/WeihanLi.Npoi/IMappingProfile.cs",
    "content": "﻿// Copyright (c) Weihan Li. All rights reserved.\n// Licensed under the Apache license.\n\nusing WeihanLi.Npoi.Configurations;\n\nnamespace WeihanLi.Npoi;\n\n/// <summary>\n/// Marker interface for describing fluent configuration profiles.\n/// </summary>\npublic interface IMappingProfile;\n\n/// <summary>\n/// Strongly typed mapping profile contract.\n/// </summary>\n/// <typeparam name=\"T\">Entity type being configured.</typeparam>\npublic interface IMappingProfile<T> : IMappingProfile\n{\n    /// <summary>\n    ///     Configures the Excel mapping metadata for the given entity type.\n    /// </summary>\n    /// <param name=\"configuration\">Excel configuration builder.</param>\n    void Configure(IExcelConfiguration<T> configuration);\n}\n"
  },
  {
    "path": "src/WeihanLi.Npoi/InternalCache.cs",
    "content": "﻿// Copyright (c) Weihan Li. All rights reserved.\n// Licensed under the Apache license.\n\nusing System.Collections.Concurrent;\nusing System.Reflection;\nusing WeihanLi.Npoi.Configurations;\n\nnamespace WeihanLi.Npoi;\n\ninternal static class InternalCache\n{\n    /// <summary>\n    ///     TypeExcelConfigurationCache\n    /// </summary>\n    public static readonly ConcurrentDictionary<Type, IExcelConfiguration> TypeExcelConfigurationDictionary = new();\n\n    /// <summary>\n    ///     Cacheable delegates that format cell values when exporting.\n    /// </summary>\n    public static readonly ConcurrentDictionary<PropertyInfo, Delegate?> OutputFormatterFuncCache = new();\n\n    /// <summary>\n    ///     Cacheable delegates that post-process property values after import.\n    /// </summary>\n    public static readonly ConcurrentDictionary<PropertyInfo, Delegate?> InputFormatterFuncCache = new();\n\n    /// <summary>\n    ///     Cacheable delegates that pre-process property values before the column formatter runs.\n    /// </summary>\n    public static readonly ConcurrentDictionary<PropertyInfo, Delegate?> ColumnInputFormatterFuncCache = new();\n\n    /// <summary>\n    ///     Cacheable delegates that read a cell value into the desired property type.\n    /// </summary>\n    public static readonly ConcurrentDictionary<PropertyInfo, Delegate?> CellReaderFuncCache = new();\n}\n"
  },
  {
    "path": "src/WeihanLi.Npoi/InternalConstants.cs",
    "content": "﻿// Copyright (c) Weihan Li. All rights reserved.\n// Licensed under the Apache license.\n\nnamespace WeihanLi.Npoi;\n\ninternal static class InternalConstants\n{\n    /// <summary>\n    ///     Maximum number of sheets supported by the legacy XLS format.\n    /// </summary>\n    public const int MaxSheetCountXls = 256;\n\n    /// <summary>\n    ///     Maximum number of sheets supported by the XLSX format.\n    /// </summary>\n    public const int MaxSheetCountXlsx = 16384;\n\n    /// <summary>\n    ///     Maximum row count in a single XLS sheet.\n    /// </summary>\n    public const int MaxRowCountXls = 65536;\n\n    /// <summary>\n    ///     Maximum row count in a single XLSX sheet.\n    /// </summary>\n    public const int MaxRowCountXlsx = 1_048_576;\n\n    /// <summary>\n    ///     DefaultPropertyNameForBasicType\n    /// </summary>\n    public const string DefaultPropertyNameForBasicType = \"Value\";\n\n    /// <summary>\n    ///     ApplicationName\n    /// </summary>\n    public const string ApplicationName = \"WeihanLi.Npoi\";\n\n    /// <summary>\n    ///     Marker appended when duplicate column titles are encountered.\n    /// </summary>\n    public const string DuplicateColumnMark = \"__dup_mark__\";\n\n    #region TemplateParamFormat\n\n    /// <summary>\n    ///     Placeholder format for template-wide global parameters.\n    /// </summary>\n    public const string TemplateGlobalParamFormat = \"$(Global:{0})\";\n\n    /// <summary>\n    ///     Placeholder format for header-level template parameters.\n    /// </summary>\n    public const string TemplateHeaderParamFormat = \"$(Header:{0})\";\n\n    /// <summary>\n    ///     Placeholder format for body data template parameters.\n    /// </summary>\n    public const string TemplateDataParamFormat = \"$(Data:{0})\";\n\n    /// <summary>\n    ///     Prefix indicating a template data placeholder.\n    /// </summary>\n    public const string TemplateDataPrefix = \"$(Data:\";\n\n    /// <summary>\n    ///     Opening tag that surrounds template data sections.\n    /// </summary>\n    public const string TemplateDataBegin = \"<Data>\";\n\n    /// <summary>\n    ///     Closing tag that surrounds template data sections.\n    /// </summary>\n    public const string TemplateDataEnd = \"</Data>\";\n\n    #endregion TemplateParamFormat\n}\n"
  },
  {
    "path": "src/WeihanLi.Npoi/InternalExtensions.cs",
    "content": "﻿// Copyright (c) Weihan Li. All rights reserved.\n// Licensed under the Apache license.\n\nusing NPOI.SS.UserModel;\nusing System.Globalization;\nusing System.Reflection;\nusing WeihanLi.Common;\nusing WeihanLi.Common.Models;\nusing WeihanLi.Common.Services;\nusing WeihanLi.Extensions;\nusing WeihanLi.Npoi.Configurations;\nusing CellType = WeihanLi.Npoi.Abstract.CellType;\nusing ICell = WeihanLi.Npoi.Abstract.ICell;\n\nnamespace WeihanLi.Npoi;\n\ninternal static class InternalExtensions\n{\n    /// <summary>\n    ///     Parse obj to paramDictionary\n    /// </summary>\n    /// <param name=\"paramInfo\">param object</param>\n    /// <returns></returns>\n    public static IDictionary<string, object?> ParseParamInfo(this object? paramInfo)\n    {\n        var paramDic = paramInfo.ParseParamDictionary();\n        return paramDic;\n    }\n\n    /// <summary>\n    ///     Wraps a strongly typed validator so it can be used without generics.\n    /// </summary>\n    /// <typeparam name=\"T\">Entity type handled by the validator.</typeparam>\n    /// <param name=\"validator\">Type-specific validator.</param>\n    /// <returns>Validator that operates on <see cref=\"object\" /> instances.</returns>\n    public static IValidator GetCommonValidator<T>(this IValidator<T> validator)\n    {\n        return new CustomValidator(o =>\n        {\n            if (o is T t)\n            {\n                return validator.Validate(t);\n            }\n            return ValidationResult.Failed(\"Invalid value\");\n        });\n    }\n\n    /// <summary>\n    ///     GetCellValue\n    /// </summary>\n    /// <param name=\"cell\">cell</param>\n    /// <param name=\"propertyType\">propertyType</param>\n    /// <param name=\"formulaEvaluator\">formulaEvaluator</param>\n    /// <returns>cellValue</returns>\n    public static object? GetCellValue(this ICell? cell, Type propertyType, IFormulaEvaluator? formulaEvaluator)\n    {\n        if (cell is null || cell.CellType == CellType.Blank || cell.CellType == CellType.Error)\n        {\n            return propertyType.GetDefaultValue();\n        }\n\n        return cell.Value?.ToOrDefault(propertyType);\n    }\n\n    /// <summary>\n    ///     GetCellValue\n    /// </summary>\n    /// <typeparam name=\"T\">Type</typeparam>\n    /// <param name=\"cell\">cell</param>\n    /// <returns></returns>\n    public static T? GetCellValue<T>(this ICell? cell) => (cell?.Value).ToOrDefault<T>();\n\n    /// <summary>\n    ///     SetCellValue\n    /// </summary>\n    /// <param name=\"cell\">ICell</param>\n    /// <param name=\"value\">value</param>\n    public static void SetCellValue(this ICell? cell, object? value) => cell?.SetCellValue(value, null);\n\n    /// <summary>\n    ///     SetCellValue\n    /// </summary>\n    /// <param name=\"cell\">ICell</param>\n    /// <param name=\"value\">value</param>\n    /// <param name=\"formatter\">formatter</param>\n    public static void SetCellValue(this ICell cell, object? value, string? formatter)\n    {\n        if (value is null)\n        {\n            cell.CellType = CellType.Blank;\n            return;\n        }\n\n        if (value is DateTime time)\n        {\n            cell.Value = string.IsNullOrWhiteSpace(formatter)\n                ? time.Date == time ? time.ToDateString() : time.ToTimeString()\n                : time.ToString(formatter);\n            cell.CellType = CellType.String;\n        }\n        else\n        {\n            var type = value.GetType();\n            if (\n                type == typeof(double) ||\n                type == typeof(int) ||\n                type == typeof(long) ||\n                type == typeof(float) ||\n                type == typeof(decimal)\n            )\n            {\n                cell.Value = Convert.ToDouble(value);\n                cell.CellType = CellType.Numeric;\n            }\n            else if (type == typeof(bool))\n            {\n                cell.Value = value;\n                cell.CellType = CellType.Boolean;\n            }\n            else\n            {\n                cell.Value = value is IFormattable val && formatter.IsNotNullOrWhiteSpace()\n                    ? val.ToString(formatter, CultureInfo.CurrentCulture)\n                    : value.ToString();\n                cell.CellType = CellType.String;\n            }\n        }\n    }\n\n    /// <summary>\n    ///     GetPropertySettingByPropertyName\n    /// </summary>\n    /// <param name=\"mappingDictionary\">mappingDictionary</param>\n    /// <param name=\"propertyName\">propertyName</param>\n    /// <returns></returns>\n    internal static PropertyConfiguration? GetPropertySettingByPropertyName(\n        this IDictionary<PropertyInfo, PropertyConfiguration> mappingDictionary, string propertyName)\n        => mappingDictionary.Values.FirstOrDefault(c => c.PropertyName.EqualsIgnoreCase(propertyName));\n\n    /// <summary>\n    ///     GetPropertyConfigurationByColumnName\n    /// </summary>\n    /// <param name=\"mappingDictionary\">mappingDictionary</param>\n    /// <param name=\"columnTitle\">columnTitle</param>\n    /// <returns></returns>\n    internal static PropertyConfiguration? GetPropertySetting(\n        this IDictionary<PropertyInfo, PropertyConfiguration> mappingDictionary, string columnTitle) =>\n        mappingDictionary.Values.FirstOrDefault(k => k.ColumnTitle.EqualsIgnoreCase(columnTitle)) ??\n        mappingDictionary.GetPropertySettingByPropertyName(columnTitle);\n\n    private sealed class CustomValidator(Func<object?, ValidationResult> func) : IValidator\n    {\n        private readonly Func<object?, ValidationResult> _func = Guard.NotNull(func);\n\n        /// <summary>\n        ///     Executes the wrapped validation delegate.\n        /// </summary>\n        /// <param name=\"value\">Value to validate.</param>\n        /// <returns>Validation result.</returns>\n        public ValidationResult Validate(object? value)\n        {\n            return _func.Invoke(value);\n        }\n    }\n}\n"
  },
  {
    "path": "src/WeihanLi.Npoi/InternalHelper.cs",
    "content": "﻿// Copyright (c) Weihan Li. All rights reserved.\n// Licensed under the Apache license.\n\nusing System.Reflection;\nusing WeihanLi.Common;\nusing WeihanLi.Npoi.Attributes;\nusing WeihanLi.Npoi.Configurations;\n\nnamespace WeihanLi.Npoi;\n\ninternal static class InternalHelper\n{\n    /// <summary>\n    ///     Ensures the supplied file path is writable, throwing if it is read-only.\n    /// </summary>\n    /// <param name=\"filePath\">File path to inspect.</param>\n    public static void EnsureFileIsNotReadOnly(string filePath)\n    {\n        if (!File.Exists(filePath)) return;\n\n        var attributes = File.GetAttributes(filePath);\n        if ((attributes & FileAttributes.ReadOnly) != 0)\n        {\n            throw new InvalidOperationException($\"The file({filePath}) is read-only\");\n        }\n    }\n\n    /// <summary>\n    ///     Get ExcelConfigurationMapping by type\n    /// </summary>\n    /// <param name=\"entityType\">entityType</param>\n    /// <returns>excel configuration</returns>\n    public static IExcelConfiguration GetExcelConfigurationMapping(Type entityType) =>\n        InternalCache.TypeExcelConfigurationDictionary.GetOrAdd(entityType, type =>\n        {\n            var excelConfiguration = CreateExcelConfiguration(type, () => (ExcelConfiguration)\n                Guard.NotNull(Activator.CreateInstance(typeof(ExcelConfiguration<>).MakeGenericType(entityType)))\n                );\n            return excelConfiguration;\n        });\n\n    /// <summary>\n    ///     Get GenericType ExcelConfigurationMapping\n    /// </summary>\n    /// <typeparam name=\"TEntity\">TEntity</typeparam>\n    /// <returns>IExcelConfiguration</returns>\n    public static ExcelConfiguration<TEntity> GetExcelConfigurationMapping<TEntity>() =>\n        (ExcelConfiguration<TEntity>)InternalCache.TypeExcelConfigurationDictionary.GetOrAdd(typeof(TEntity),\n            type =>\n            {\n                var excelConfiguration =\n                    CreateExcelConfiguration(type, () => new ExcelConfiguration<TEntity>());\n                return excelConfiguration;\n            });\n\n    private static ExcelConfiguration CreateExcelConfiguration(Type type,\n        Func<ExcelConfiguration> newConfigurationFunc)\n    {\n        var excelConfiguration = newConfigurationFunc();\n        excelConfiguration.FilterSetting = type.GetCustomAttribute<FilterAttribute>()?.FilterSetting;\n        foreach (var sheetAttribute in type.GetCustomAttributes<SheetAttribute>())\n        {\n            if (sheetAttribute.SheetIndex >= 0)\n            {\n                excelConfiguration.SheetSettings[sheetAttribute.SheetIndex] = sheetAttribute.SheetSetting;\n            }\n        }\n\n        foreach (var freezeAttribute in type.GetCustomAttributes<FreezeAttribute>())\n        {\n            excelConfiguration.FreezeSettings.Add(freezeAttribute.FreezeSetting);\n        }\n\n        var propertyInfos = CacheUtil.GetTypeProperties(type);\n        foreach (var propertyInfo in propertyInfos)\n        {\n            var column = propertyInfo.GetCustomAttribute<ColumnAttribute>() ?? new ColumnAttribute();\n            if (string.IsNullOrWhiteSpace(column.Title))\n            {\n                column.Title = propertyInfo.Name;\n            }\n\n            var propertyConfigurationType =\n                typeof(PropertyConfiguration<,>).MakeGenericType(type, propertyInfo.PropertyType);\n            var propertyConfiguration = Guard.NotNull(Activator.CreateInstance(propertyConfigurationType, propertyInfo));\n\n            propertyConfigurationType.GetProperty(nameof(column.PropertyConfiguration.ColumnTitle))\n                ?.GetSetMethod()?\n                .Invoke(propertyConfiguration, [column.PropertyConfiguration.ColumnTitle]);\n            propertyConfigurationType.GetProperty(nameof(column.PropertyConfiguration.ColumnIndex))\n                ?.GetSetMethod()?\n                .Invoke(propertyConfiguration, [column.PropertyConfiguration.ColumnIndex]);\n            propertyConfigurationType.GetProperty(nameof(column.PropertyConfiguration.ColumnFormatter))\n                ?.GetSetMethod()?\n                .Invoke(propertyConfiguration, [column.PropertyConfiguration.ColumnFormatter]);\n            propertyConfigurationType.GetProperty(nameof(column.PropertyConfiguration.IsIgnored))\n                ?.GetSetMethod()?\n                .Invoke(propertyConfiguration, [column.PropertyConfiguration.IsIgnored]);\n            propertyConfigurationType.GetProperty(nameof(column.PropertyConfiguration.ColumnWidth))\n                ?.GetSetMethod()?\n                .Invoke(propertyConfiguration, [column.PropertyConfiguration.ColumnWidth]);\n\n            excelConfiguration.PropertyConfigurationDictionary.Add(propertyInfo,\n                (PropertyConfiguration)propertyConfiguration);\n        }\n\n        return excelConfiguration;\n    }\n\n    /// <summary>\n    ///     Adjust Column Index\n    /// </summary>\n    /// <typeparam name=\"TEntity\">TEntity</typeparam>\n    /// <param name=\"excelConfiguration\">excelConfiguration</param>\n    private static void AdjustColumnIndex<TEntity>(ExcelConfiguration<TEntity> excelConfiguration)\n    {\n        ICollection<int> validColumnIndex = excelConfiguration.PropertyConfigurationDictionary.Values\n            .Where(c => !c.IsIgnored)\n            .Select(x => x.ColumnIndex)\n            .ToArray();\n        // return if already adjusted\n        if (validColumnIndex.All(idx => idx >= 0) &&\n            validColumnIndex.Distinct().Count() == validColumnIndex.Count\n            )\n        {\n            return;\n        }\n\n        var colIndexList = new List<int>(validColumnIndex.Count);\n        var properties = excelConfiguration.PropertyConfigurationDictionary\n            .Where(p => !p.Value.IsIgnored)\n            .OrderBy(p => p.Value.ColumnIndex >= 0 ? p.Value.ColumnIndex : int.MaxValue);\n        if (excelConfiguration.PropertyComparer is not null)\n        {\n            properties = properties.ThenBy(p => p.Key, excelConfiguration.PropertyComparer);\n        }\n\n        foreach (var item in properties.Select(p => p.Value))\n        {\n            while (colIndexList.Contains(item.ColumnIndex) || item.ColumnIndex < 0)\n            {\n                if (colIndexList.Count > 0)\n                {\n                    item.ColumnIndex = colIndexList.Max() + 1;\n                }\n                else\n                {\n                    item.ColumnIndex++;\n                }\n            }\n\n            colIndexList.Add(item.ColumnIndex);\n        }\n    }\n\n    /// <summary>\n    ///     GetPropertyColumnDictionary\n    /// </summary>\n    /// <typeparam name=\"TEntity\">TEntity Type</typeparam>\n    /// <returns></returns>\n    public static Dictionary<PropertyInfo, PropertyConfiguration> GetPropertyColumnDictionary<TEntity>() =>\n        GetPropertyColumnDictionary(GetExcelConfigurationMapping<TEntity>());\n\n    /// <summary>\n    ///     GetPropertyColumnDictionary\n    /// </summary>\n    /// <typeparam name=\"TEntity\">TEntity Type</typeparam>\n    /// <returns></returns>\n    public static Dictionary<PropertyInfo, PropertyConfiguration> GetPropertyColumnDictionary<TEntity>(\n        ExcelConfiguration<TEntity> configuration)\n    {\n        AdjustColumnIndex(configuration);\n        return configuration.PropertyConfigurationDictionary\n            .Where(p => !p.Value.IsIgnored)\n            .ToDictionary(p => p.Key, p => p.Value);\n    }\n\n    /// <summary>\n    ///     GetProperties\n    /// </summary>\n    /// <typeparam name=\"TEntity\">TEntity Type</typeparam>\n    /// <returns></returns>\n    public static IReadOnlyList<PropertyInfo> GetPropertiesForCsvHelper<TEntity>()\n    {\n        var configuration = GetExcelConfigurationMapping<TEntity>();\n        AdjustColumnIndex(configuration);\n        return configuration.PropertyConfigurationDictionary\n            .Where(p => !p.Value.IsIgnored)\n            .OrderBy(p => p.Value.ColumnIndex)\n            .Select(p => p.Key)\n            .ToArray();\n    }\n\n    /// <summary>\n    ///     Generates a unique column name to temporarily disambiguate duplicates.\n    /// </summary>\n    /// <param name=\"columnName\">Original column title.</param>\n    /// <returns>Encoded column name with a duplicate marker.</returns>\n    public static string GetEncodedColumnName(string columnName) =>\n        $\"{columnName}{InternalConstants.DuplicateColumnMark}{Guid.NewGuid():N}\";\n\n    /// <summary>\n    ///     Removes the duplicate marker from a previously encoded column name.\n    /// </summary>\n    /// <param name=\"columnName\">Encoded column title.</param>\n    /// <returns>Original column name.</returns>\n    public static string GetDecodeColumnName(string columnName)\n    {\n        var duplicateMarkIndex = columnName.IndexOf(InternalConstants.DuplicateColumnMark, StringComparison.OrdinalIgnoreCase);\n        return duplicateMarkIndex > 0 ? columnName.Substring(0, duplicateMarkIndex) : columnName;\n    }\n}\n"
  },
  {
    "path": "src/WeihanLi.Npoi/NpoiCollection.cs",
    "content": "﻿// Copyright (c) Weihan Li. All rights reserved.\n// Licensed under the Apache license.\n\nusing NPOI.SS.UserModel;\nusing System.Collections;\nusing WeihanLi.Common;\n\nnamespace WeihanLi.Npoi;\n\n/// <summary>\n///     npoi sheet row collection\n/// </summary>\npublic sealed class NpoiRowCollection(ISheet sheet) : IReadOnlyCollection<IRow>\n{\n    private readonly ISheet _sheet = Guard.NotNull(sheet);\n\n    /// <summary>\n    ///     Gets the number of rows within the wrapped sheet.\n    /// </summary>\n    public int Count => _sheet.LastRowNum - _sheet.FirstRowNum + 1;\n\n    /// <summary>\n    ///     Iterates over each row within the sheet range.\n    /// </summary>\n    public IEnumerator<IRow> GetEnumerator()\n    {\n        for (var i = _sheet.FirstRowNum; i <= _sheet.LastRowNum; i++)\n        {\n            yield return _sheet.GetRow(i);\n        }\n    }\n\n    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();\n}\n\n/// <summary>\n///     npoi row cell collection\n/// </summary>\npublic sealed class NpoiCellCollection(IRow row) : IReadOnlyCollection<ICell>\n{\n    private readonly IRow _row = Guard.NotNull(row);\n\n    /// <summary>\n    ///     Gets the number of cells in the wrapped row.\n    /// </summary>\n    public int Count => _row.LastCellNum - _row.FirstCellNum;\n\n    /// <summary>\n    ///     Iterates over each concrete cell in the current row.\n    /// </summary>\n    public IEnumerator<ICell> GetEnumerator()\n    {\n        for (var i = _row.FirstCellNum; i < _row.LastCellNum; i++)\n        {\n            yield return _row.GetCell(i);\n        }\n    }\n\n    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();\n}\n"
  },
  {
    "path": "src/WeihanLi.Npoi/NpoiExtensions.cs",
    "content": "﻿// Copyright (c) Weihan Li. All rights reserved.\n// Licensed under the Apache license.\n\nusing NPOI.HSSF.UserModel;\nusing NPOI.SS.UserModel;\nusing NPOI.XSSF.Streaming;\nusing NPOI.XSSF.UserModel;\nusing System.Data;\nusing System.Diagnostics;\nusing System.Globalization;\nusing WeihanLi.Common;\nusing WeihanLi.Common.Helpers;\nusing WeihanLi.Common.Models;\nusing WeihanLi.Common.Services;\nusing WeihanLi.Extensions;\nusing WeihanLi.Npoi.Settings;\n\nnamespace WeihanLi.Npoi;\n\n/// <summary>\n/// Extension methods that convert between NPOI primitives and the strongly-typed configuration layer.\n/// </summary>\npublic static class NpoiExtensions\n{\n    /// <summary>\n    ///     Workbook2EntityList\n    /// </summary>\n    /// <typeparam name=\"TEntity\">EntityType</typeparam>\n    /// <param name=\"workbook\">excel workbook</param>\n    /// <returns>entity list</returns>\n    public static List<TEntity?> ToEntityList<TEntity>(this IWorkbook workbook) where TEntity : new() =>\n        workbook.ToEntityList<TEntity>(0);\n\n    /// <summary>\n    ///     Workbook2EntityList\n    /// </summary>\n    /// <typeparam name=\"TEntity\">EntityType</typeparam>\n    /// <param name=\"workbook\">excel workbook</param>\n    /// <param name=\"sheetIndex\">sheetIndex</param>\n    /// <returns>entity list</returns>\n    public static List<TEntity?> ToEntityList<TEntity>(this IWorkbook workbook, int sheetIndex)\n        where TEntity : new()\n    {\n        return ToEntities<TEntity>(workbook, sheetIndex).ToList();\n    }\n\n    /// <summary>\n    ///     Lazily materializes entities from the specified sheet without building a list first.\n    /// </summary>\n    /// <typeparam name=\"TEntity\">Entity type.</typeparam>\n    /// <param name=\"workbook\">Excel workbook.</param>\n    /// <param name=\"sheetIndex\">Zero-based sheet index.</param>\n    /// <returns>Sequence that yields entities row by row.</returns>\n    public static IEnumerable<TEntity?> ToEntities<TEntity>(this IWorkbook workbook, int sheetIndex)\n        where TEntity : new()\n    {\n        Guard.NotNull(workbook);\n\n        if (workbook.NumberOfSheets <= sheetIndex)\n        {\n            throw new ArgumentOutOfRangeException(nameof(sheetIndex),\n                string.Format(Resource.IndexOutOfRange, nameof(sheetIndex), workbook.NumberOfSheets));\n        }\n\n        var sheet = workbook.GetSheetAt(sheetIndex);\n        return NpoiHelper.SheetToEntities<TEntity>(sheet, sheetIndex);\n    }\n\n    /// <summary>\n    ///     Sheet2EntityList\n    /// </summary>\n    /// <typeparam name=\"TEntity\">EntityType</typeparam>\n    /// <param name=\"sheet\">excel sheet</param>\n    /// <returns>entity list</returns>\n    public static List<TEntity?> ToEntityList<TEntity>(this ISheet sheet) where TEntity : new() =>\n        sheet.ToEntityList<TEntity>(0);\n\n    /// <summary>\n    ///     Sheet2EntityList\n    /// </summary>\n    /// <typeparam name=\"TEntity\">EntityType</typeparam>\n    /// <param name=\"sheet\">excel sheet</param>\n    /// <param name=\"sheetIndex\">sheetIndex</param>\n    /// <returns>entity list</returns>\n    public static List<TEntity?> ToEntityList<TEntity>(this ISheet sheet, int sheetIndex)\n        where TEntity : new() => NpoiHelper.SheetToEntities<TEntity>(sheet, sheetIndex).ToList();\n\n    /// <summary>\n    ///     Lazily materializes entities from the provided sheet.\n    /// </summary>\n    /// <typeparam name=\"TEntity\">Entity type.</typeparam>\n    /// <param name=\"sheet\">Excel sheet.</param>\n    /// <param name=\"sheetIndex\">Zero-based sheet index.</param>\n    /// <returns>Sequence that yields entities row by row.</returns>\n    public static IEnumerable<TEntity?> ToEntities<TEntity>(this ISheet sheet, int sheetIndex)\n        where TEntity : new() => NpoiHelper.SheetToEntities<TEntity>(sheet, sheetIndex);\n\n    /// <summary>\n    ///     Sheet2EntityList and validate\n    /// </summary>\n    /// <typeparam name=\"TEntity\">EntityType</typeparam>\n    /// <param name=\"sheet\">excel sheet</param>\n    /// <param name=\"sheetIndex\">sheetIndex</param>\n    /// <param name=\"validator\">validator</param>\n    /// <returns>entity list and validation results</returns>\n    public static (List<TEntity?> EntityList, Dictionary<int, ValidationResult> ValidationResults)\n        ToEntityListWithValidationResult<TEntity>(this ISheet sheet, int sheetIndex = 0, IValidator<TEntity>? validator = null)\n        where TEntity : new()\n    {\n        var validationResults = new Dictionary<int, ValidationResult>();\n\n        var entities = NpoiHelper.SheetToEntities<TEntity>(sheet, sheetIndex, (entity, configuration, rowIndex) =>\n        {\n            var validatorEffective = configuration.Validator;\n            if (validator is not null)\n            {\n                validatorEffective = validator.GetCommonValidator();\n            }\n            validatorEffective ??= ExcelHelper.DefaultDataValidator;\n            var validationResult = validatorEffective.Validate(entity);\n            if (!validationResult.Valid)\n            {\n                validationResults[rowIndex] = validationResult;\n            }\n        }).ToList();\n\n        return (entities, validationResults);\n    }\n\n    /// <summary>\n    ///     Workbook2ToDataTable\n    /// </summary>\n    /// <param name=\"workbook\">excel workbook</param>\n    /// <param name=\"removeEmptyRows\">removeEmptyRows</param>\n    /// <param name=\"maxColumns\">maxColumns</param>\n    /// <returns>DataTable</returns>\n    public static DataTable ToDataTable(this IWorkbook workbook, bool removeEmptyRows = false,\n        int? maxColumns = null)\n        => workbook.ToDataTable(0, 0, removeEmptyRows, maxColumns);\n\n    /// <summary>\n    ///     Workbook2ToDataSet\n    /// </summary>\n    /// <param name=\"workbook\">excel workbook</param>\n    /// <param name=\"removeEmptyRows\">removeEmptyRows</param>\n    /// <param name=\"maxColumns\">maxColumns</param>\n    /// <returns>DataSet</returns>\n    public static DataSet ToDataSet(this IWorkbook workbook, bool removeEmptyRows = false, int? maxColumns = null)\n        => workbook.ToDataSet(0, removeEmptyRows, maxColumns);\n\n    /// <summary>\n    ///     Workbook2ToDataSet\n    /// </summary>\n    /// <param name=\"workbook\">excel workbook</param>\n    /// <param name=\"headerRowIndex\">headerRowIndex</param>\n    /// <param name=\"removeEmptyRows\">removeEmptyRows</param>\n    /// <param name=\"maxColumns\">maxColumns</param>\n    /// <returns>DataSet</returns>\n    public static DataSet ToDataSet(this IWorkbook workbook, int headerRowIndex, bool removeEmptyRows = false,\n        int? maxColumns = null)\n    {\n        Guard.NotNull(workbook);\n\n        var ds = new DataSet();\n        for (var i = 0; i < workbook.NumberOfSheets; i++)\n        {\n            ds.Tables.Add(workbook.GetSheetAt(i).ToDataTable(headerRowIndex, removeEmptyRows, maxColumns));\n        }\n\n        return ds;\n    }\n\n    /// <summary>\n    ///     Workbook2ToDataTable\n    /// </summary>\n    /// <param name=\"workbook\">excel workbook</param>\n    /// <param name=\"sheetIndex\">sheetIndex</param>\n    /// <param name=\"headerRowIndex\">headerRowIndex</param>\n    /// <param name=\"removeEmptyRows\">removeEmptyRows</param>\n    /// <param name=\"maxColumns\">maxColumns</param>\n    /// <returns>DataTable</returns>\n    public static DataTable ToDataTable(this IWorkbook workbook, int sheetIndex, int headerRowIndex,\n        bool removeEmptyRows = false, int? maxColumns = null)\n    {\n        Guard.NotNull(workbook);\n\n        if (workbook.NumberOfSheets <= sheetIndex)\n        {\n            throw new ArgumentOutOfRangeException(nameof(sheetIndex),\n                string.Format(Resource.IndexOutOfRange, nameof(sheetIndex), workbook.NumberOfSheets));\n        }\n\n        return workbook.GetSheetAt(sheetIndex).ToDataTable(headerRowIndex, removeEmptyRows, maxColumns);\n    }\n\n    /// <summary>\n    ///     Sheet2DataTable\n    /// </summary>\n    /// <param name=\"sheet\">excel sheet</param>\n    /// <param name=\"removeEmptyRows\">removeEmptyRows</param>\n    /// <param name=\"maxColumns\">maxColumns</param>\n    /// <returns>DataTable</returns>\n    public static DataTable ToDataTable(this ISheet sheet, bool removeEmptyRows = false, int? maxColumns = null)\n        => sheet.ToDataTable(0, removeEmptyRows, maxColumns);\n\n    /// <summary>\n    ///     Sheet2DataTable\n    /// </summary>\n    /// <param name=\"sheet\">excel sheet</param>\n    /// <param name=\"headerRowIndex\">headerRowIndex</param>\n    /// <param name=\"removeEmptyRows\">removeEmptyRows</param>\n    /// <param name=\"maxColumns\">maxColumns</param>\n    /// <returns>DataTable</returns>\n    public static DataTable ToDataTable(this ISheet sheet, int headerRowIndex, bool removeEmptyRows = false,\n        int? maxColumns = null)\n    {\n        Guard.NotNull(sheet);\n\n        if (sheet.LastRowNum <= headerRowIndex)\n        {\n            throw new ArgumentOutOfRangeException(nameof(headerRowIndex),\n                string.Format(Resource.IndexOutOfRange, nameof(headerRowIndex), sheet.PhysicalNumberOfRows));\n        }\n\n        var formulaEvaluator = sheet.Workbook.GetFormulaEvaluator();\n        var dataTable = new DataTable(sheet.SheetName);\n\n        foreach (var row in sheet.GetRowCollection())\n        {\n            if (\n                // ReSharper disable once ConditionIsAlwaysTrueOrFalse\n                row is null\n                || row.RowNum < headerRowIndex)\n            {\n                continue;\n            }\n\n            if (row.RowNum == headerRowIndex)\n            {\n                LoadHeader(formulaEvaluator, dataTable, row, maxColumns);\n            }\n            else\n            {\n                LoadRow(formulaEvaluator, dataTable, row, removeEmptyRows, maxColumns);\n            }\n        }\n\n        return dataTable;\n\n        static void LoadHeader(IFormulaEvaluator formulaEvaluator, DataTable dataTable, IRow row, int? maxColumns)\n        {\n            foreach (var cell in row)\n            {\n                if (cell is null)\n                {\n                    continue;\n                }\n\n                var columnName = cell.GetCellValue(typeof(string), formulaEvaluator)!.ToString()!.Trim();\n                if (dataTable.Columns.Contains(columnName))\n                {\n                    columnName = InternalHelper.GetEncodedColumnName(columnName);\n                }\n\n                dataTable.Columns.Add(columnName);\n\n                if (maxColumns is not null && cell.ColumnIndex + 1 == maxColumns)\n                {\n                    break;\n                }\n            }\n        }\n\n        static void LoadRow(IFormulaEvaluator formulaEvaluator, DataTable dataTable, IRow row, bool removeEmptyRows,\n            int? maxColumns)\n        {\n            var dataRow = dataTable.NewRow();\n            var maxColumnIndex = Math.Min(maxColumns.GetValueOrDefault(dataTable.Columns.Count), dataTable.Columns.Count);\n            for (var columnIndex = 0; columnIndex < maxColumnIndex; columnIndex++)\n            {\n                var cell = row.GetCell(columnIndex, MissingCellPolicy.CREATE_NULL_AS_BLANK);\n                dataRow[columnIndex] = cell.GetCellValue(typeof(string), formulaEvaluator);\n            }\n\n            if (removeEmptyRows)\n            {\n                var rowContainsData = dataRow.ItemArray.Any(value\n                    => value != DBNull.Value && !string.IsNullOrEmpty((string?)value));\n\n                if (rowContainsData)\n                {\n                    dataTable.Rows.Add(dataRow);\n                }\n            }\n            else\n            {\n                dataTable.Rows.Add(dataRow);\n            }\n        }\n    }\n\n    /// <summary>\n    ///     import entityList to workbook first sheet\n    /// </summary>\n    /// <typeparam name=\"TEntity\">TEntity</typeparam>\n    /// <param name=\"workbook\">workbook</param>\n    /// <param name=\"list\">entityList</param>\n    public static int ImportData<TEntity>(this IWorkbook workbook, IEnumerable<TEntity> list)\n        => workbook.ImportData(list, 0);\n\n    /// <summary>\n    ///     import entityList to workbook sheet\n    /// </summary>\n    /// <typeparam name=\"TEntity\">TEntity</typeparam>\n    /// <param name=\"workbook\">workbook</param>\n    /// <param name=\"list\">entityList</param>\n    /// <param name=\"sheetIndex\">sheetIndex</param>\n    /// <returns>the sheet LastRowNum</returns>\n    public static int ImportData<TEntity>(this IWorkbook workbook, IEnumerable<TEntity> list,\n        int sheetIndex)\n    {\n        Guard.NotNull(workbook);\n\n        if (workbook is HSSFWorkbook)\n        {\n            if (sheetIndex >= InternalConstants.MaxSheetCountXls)\n            {\n                throw new ArgumentException(\n                    string.Format(Resource.IndexOutOfRange, nameof(sheetIndex), InternalConstants.MaxSheetCountXls),\n                    nameof(sheetIndex));\n            }\n        }\n        else\n        {\n            if (sheetIndex >= InternalConstants.MaxSheetCountXlsx)\n            {\n                throw new ArgumentException(\n                    string.Format(Resource.IndexOutOfRange, nameof(sheetIndex), InternalConstants.MaxSheetCountXls),\n                    nameof(sheetIndex));\n            }\n        }\n\n        workbook.CreateSheets<TEntity>(sheetIndex);\n        var sheet = NpoiHelper.EntitiesToSheet(workbook.GetSheetAt(sheetIndex), list, sheetIndex);\n        return sheet.LastRowNum;\n    }\n\n    /// <summary>\n    /// CreateSheets\n    /// </summary>\n    /// <typeparam name=\"TEntity\">TEntity</typeparam>\n    /// <param name=\"workbook\">workbook</param>\n    /// <param name=\"sheetIndex\">max sheetIndex</param>\n    private static void CreateSheets<TEntity>(this IWorkbook workbook, int sheetIndex)\n    {\n        var configuration = InternalHelper.GetExcelConfigurationMapping<TEntity>();\n        while (workbook.NumberOfSheets <= sheetIndex)\n        {\n            if (configuration.SheetSettings.TryGetValue(sheetIndex, out var sheetSetting))\n            {\n                workbook.CreateSheet(sheetSetting.SheetName);\n            }\n            else\n            {\n                workbook.CreateSheet();\n            }\n        }\n    }\n\n    /// <summary>\n    ///     import entityList to sheet\n    /// </summary>\n    /// <typeparam name=\"TEntity\">EntityType</typeparam>\n    /// <param name=\"sheet\">sheet</param>\n    /// <param name=\"list\">entityList</param>\n    public static ISheet ImportData<TEntity>(this ISheet sheet, IEnumerable<TEntity> list)\n        => sheet.ImportData(list, 0);\n\n    /// <summary>\n    ///     import entityList to sheet\n    /// </summary>\n    /// <typeparam name=\"TEntity\">EntityType</typeparam>\n    /// <param name=\"sheet\">sheet</param>\n    /// <param name=\"list\">entityList</param>\n    /// <param name=\"sheetIndex\">sheetIndex</param>\n    public static ISheet ImportData<TEntity>(this ISheet sheet, IEnumerable<TEntity> list, int sheetIndex)\n        => NpoiHelper.EntitiesToSheet(sheet, list, sheetIndex);\n\n    /// <summary>\n    ///     import dataTable to workbook first sheet\n    /// </summary>\n    /// <typeparam name=\"TEntity\">TEntity</typeparam>\n    /// <param name=\"workbook\">workbook</param>\n    /// <param name=\"dataTable\">dataTable</param>\n    public static int ImportData<TEntity>(this IWorkbook workbook, DataTable dataTable)\n        => workbook.ImportData<TEntity>(dataTable, 0);\n\n    /// <summary>\n    ///     import dataTable to workbook first sheet\n    /// </summary>\n    /// <typeparam name=\"TEntity\">TEntity</typeparam>\n    /// <param name=\"workbook\">workbook</param>\n    /// <param name=\"dataTable\">dataTable</param>\n    /// <param name=\"sheetIndex\">sheetIndex</param>\n    /// <returns>the sheet LastRowNum</returns>\n    public static int ImportData<TEntity>(this IWorkbook workbook, DataTable dataTable,\n        int sheetIndex)\n    {\n        Guard.NotNull(workbook);\n\n        if (workbook is HSSFWorkbook)\n        {\n            if (sheetIndex >= InternalConstants.MaxSheetCountXls)\n            {\n                throw new ArgumentException(\n                    string.Format(Resource.IndexOutOfRange, nameof(sheetIndex), InternalConstants.MaxSheetCountXls),\n                    nameof(sheetIndex));\n            }\n        }\n        else\n        {\n            if (sheetIndex >= InternalConstants.MaxSheetCountXlsx)\n            {\n                throw new ArgumentException(\n                    string.Format(Resource.IndexOutOfRange, nameof(sheetIndex), InternalConstants.MaxSheetCountXls),\n                    nameof(sheetIndex));\n            }\n        }\n\n        workbook.CreateSheets<TEntity>(sheetIndex);\n        var sheet = NpoiHelper.DataTableToSheet<TEntity>(workbook.GetSheetAt(sheetIndex), dataTable, sheetIndex);\n        return sheet.LastRowNum;\n    }\n\n    /// <summary>\n    ///     import dataTable to sheet\n    /// </summary>\n    /// <typeparam name=\"TEntity\">EntityType</typeparam>\n    /// <param name=\"sheet\">sheet</param>\n    /// <param name=\"dataTable\">dataTable</param>\n    public static ISheet ImportData<TEntity>(this ISheet sheet, DataTable dataTable) =>\n        sheet.ImportData<TEntity>(dataTable, 0);\n\n    /// <summary>\n    ///     import dataTable to sheet\n    /// </summary>\n    /// <typeparam name=\"TEntity\">EntityType</typeparam>\n    /// <param name=\"sheet\">sheet</param>\n    /// <param name=\"dataTable\">dataTable</param>\n    /// <param name=\"sheetIndex\">sheetIndex</param>\n    public static ISheet ImportData<TEntity>(this ISheet sheet, DataTable dataTable, int sheetIndex)\n        => NpoiHelper.DataTableToSheet<TEntity>(sheet, dataTable, sheetIndex);\n\n    /// <summary>\n    ///     EntityList2ExcelFile\n    /// </summary>\n    /// <typeparam name=\"TEntity\">EntityType</typeparam>\n    /// <param name=\"entityList\">entityList</param>\n    /// <param name=\"excelPath\">excelPath</param>\n    public static void ToExcelFile<TEntity>(this IList<TEntity> entityList,\n        string excelPath)\n    {\n        if (entityList is null)\n        {\n            throw new ArgumentNullException(nameof(entityList));\n        }\n\n        var workbook =\n            entityList.GetWorkbookWithAutoSplitSheet(\n                excelPath.EndsWith(\".xls\", StringComparison.OrdinalIgnoreCase)\n                    ? ExcelFormat.Xls\n                    : ExcelFormat.Xlsx);\n        workbook.WriteToFile(excelPath, true);\n    }\n\n    /// <summary>\n    ///     EntityList2ExcelFile\n    /// </summary>\n    /// <typeparam name=\"TEntity\">EntityType</typeparam>\n    /// <param name=\"entityList\">entityList</param>\n    /// <param name=\"excelPath\">excelPath</param>\n    public static void ToExcelFile<TEntity>(this IEnumerable<TEntity> entityList,\n        string excelPath) => ToExcelFile(entityList, excelPath, 0);\n\n    /// <summary>\n    ///     EntityList2ExcelFile\n    /// </summary>\n    /// <typeparam name=\"TEntity\">EntityType</typeparam>\n    /// <param name=\"entityList\">entityList</param>\n    /// <param name=\"excelPath\">excelPath</param>\n    /// <param name=\"sheetIndex\">sheetIndex</param>\n    public static void ToExcelFile<TEntity>(this IEnumerable<TEntity> entityList,\n        string excelPath, int sheetIndex)\n    {\n        Guard.NotNull(entityList);\n\n        var configuration = InternalHelper.GetExcelConfigurationMapping<TEntity>();\n\n        var workbook = ExcelHelper.PrepareWorkbook(excelPath, configuration.ExcelSetting);\n        workbook.ImportData(entityList, sheetIndex);\n        workbook.WriteToFile(excelPath, true);\n    }\n\n    /// <summary>\n    ///     EntityList2ExcelStream(*.xls by default)\n    /// </summary>\n    /// <typeparam name=\"TEntity\">EntityType</typeparam>\n    /// <param name=\"entityList\">entityList</param>\n    /// <param name=\"stream\">stream where to write</param>\n    public static void ToExcelStream<TEntity>(this IEnumerable<TEntity> entityList,\n        Stream stream) => ToExcelStream(entityList, stream, ExcelFormat.Xls);\n\n    /// <summary>\n    ///     EntityList2ExcelStream\n    /// </summary>\n    /// <typeparam name=\"TEntity\">EntityType</typeparam>\n    /// <param name=\"entityList\">entityList</param>\n    /// <param name=\"stream\">stream where to write</param>\n    /// <param name=\"excelFormat\">excelFormat</param>\n    /// <param name=\"sheetIndex\">sheetIndex</param>\n    public static void ToExcelStream<TEntity>(this IEnumerable<TEntity> entityList,\n        Stream stream, ExcelFormat excelFormat, int sheetIndex)\n    {\n        Guard.NotNull(entityList);\n\n        var configuration = InternalHelper.GetExcelConfigurationMapping<TEntity>();\n\n        var workbook = ExcelHelper.PrepareWorkbook(excelFormat, configuration.ExcelSetting);\n        workbook.ImportData(entityList.ToArray(), sheetIndex);\n        workbook.Write(stream);\n    }\n\n    /// <summary>\n    ///     EntityList2ExcelStream\n    /// </summary>\n    /// <typeparam name=\"TEntity\">EntityType</typeparam>\n    /// <param name=\"entityList\">entityList</param>\n    /// <param name=\"stream\">stream where to write</param>\n    /// <param name=\"excelFormat\">excelFormat</param>\n    public static void ToExcelStream<TEntity>(this IEnumerable<TEntity> entityList,\n        Stream stream, ExcelFormat excelFormat) => ToExcelStream(entityList, stream, excelFormat, 0);\n\n    /// <summary>\n    ///     EntityList2ExcelStream\n    /// </summary>\n    /// <typeparam name=\"TEntity\">EntityType</typeparam>\n    /// <param name=\"entityList\">entityList</param>\n    /// <param name=\"stream\">stream where to write</param>\n    /// <param name=\"excelFormat\">excelFormat</param>\n    public static void ToExcelStream<TEntity>(this IList<TEntity> entityList,\n        Stream stream, ExcelFormat excelFormat = ExcelFormat.Xls)\n    {\n        Guard.NotNull(entityList);\n\n        var workbook = entityList.GetWorkbookWithAutoSplitSheet(excelFormat);\n        workbook.Write(stream);\n    }\n\n    /// <summary>\n    ///     EntityList2ExcelBytes(*.xls by default)\n    /// </summary>\n    /// <typeparam name=\"TEntity\">EntityType</typeparam>\n    /// <param name=\"entityList\">entityList</param>\n    public static byte[] ToExcelBytes<TEntity>(this IEnumerable<TEntity> entityList) =>\n        ToExcelBytes(entityList, ExcelFormat.Xls);\n\n    /// <summary>\n    ///     EntityList2ExcelBytes\n    /// </summary>\n    /// <typeparam name=\"TEntity\">EntityType</typeparam>\n    /// <param name=\"entityList\">entityList</param>\n    /// <param name=\"excelFormat\">excelFormat</param>\n    public static byte[] ToExcelBytes<TEntity>(this IEnumerable<TEntity> entityList, ExcelFormat excelFormat)\n        => ToExcelBytes(entityList, excelFormat, 0);\n\n    /// <summary>\n    ///     EntityList2ExcelBytes\n    /// </summary>\n    /// <typeparam name=\"TEntity\">EntityType</typeparam>\n    /// <param name=\"entityList\">entityList</param>\n    /// <param name=\"excelFormat\">excelFormat</param>\n    /// <param name=\"sheetIndex\">sheetIndex</param>\n    public static byte[] ToExcelBytes<TEntity>(this IEnumerable<TEntity> entityList, ExcelFormat excelFormat,\n        int sheetIndex)\n    {\n        if (entityList is null)\n        {\n            throw new ArgumentNullException(nameof(entityList));\n        }\n\n        var configuration = InternalHelper.GetExcelConfigurationMapping<TEntity>();\n\n        var workbook = ExcelHelper.PrepareWorkbook(excelFormat, configuration.ExcelSetting);\n        workbook.ImportData(entityList.ToArray(), sheetIndex);\n\n        return workbook.ToExcelBytes(true);\n    }\n\n    /// <summary>\n    ///     EntityList2ExcelBytes\n    /// </summary>\n    /// <typeparam name=\"TEntity\">EntityType</typeparam>\n    /// <param name=\"entityList\">entityList</param>\n    /// <param name=\"excelFormat\">excelFormat</param>\n    public static byte[] ToExcelBytes<TEntity>(this IList<TEntity> entityList,\n        ExcelFormat excelFormat = ExcelFormat.Xls)\n    {\n        if (entityList is null)\n        {\n            throw new ArgumentNullException(nameof(entityList));\n        }\n\n        var workbook = entityList.GetWorkbookWithAutoSplitSheet(excelFormat);\n        return workbook.ToExcelBytes(true);\n    }\n\n    /// <summary>\n    ///     GetWorkbookWithAutoSplitSheet\n    /// </summary>\n    /// <typeparam name=\"TEntity\">entity type</typeparam>\n    /// <param name=\"entityList\">entity list</param>\n    /// <param name=\"excelFormat\">excel format</param>\n    /// <returns>excel workbook with data</returns>\n    public static IWorkbook GetWorkbookWithAutoSplitSheet<TEntity>(this IList<TEntity> entityList,\n        ExcelFormat excelFormat)\n    {\n        Guard.NotNull(entityList);\n\n        var configuration = InternalHelper.GetExcelConfigurationMapping<TEntity>();\n\n        var workbook = ExcelHelper.PrepareWorkbook(excelFormat, configuration.ExcelSetting);\n        var maxRowCount = excelFormat == ExcelFormat.Xls\n            ? InternalConstants.MaxRowCountXls\n            : InternalConstants.MaxRowCountXlsx;\n        maxRowCount -= configuration.SheetSettings[0].StartRowIndex;\n\n        var sheetCount = (entityList.Count + maxRowCount - 1) / maxRowCount;\n        workbook.CreateSheets<TEntity>(sheetCount - 1);\n\n        if (entityList.Count > maxRowCount)\n        {\n            for (var sheetIndex = 0; sheetIndex < sheetCount; sheetIndex++)\n            {\n                workbook.GetSheetAt(sheetIndex)\n                    .ImportData(entityList.Skip(sheetIndex * maxRowCount).Take(maxRowCount), 0);\n            }\n        }\n        else\n        {\n            workbook.GetSheetAt(0).ImportData(entityList);\n        }\n\n        return workbook;\n    }\n\n    /// <summary>\n    ///     GetWorkbookWithAutoSplitSheet\n    /// </summary>\n    /// <param name=\"dataTable\">dataTable</param>\n    /// <param name=\"excelFormat\">excel format</param>\n    /// <param name=\"excelSetting\">excelSetting</param>\n    /// <returns>excel workbook with data</returns>\n    public static IWorkbook GetWorkbookWithAutoSplitSheet(this DataTable dataTable, ExcelFormat excelFormat,\n        ExcelSetting? excelSetting = null)\n    {\n        Guard.NotNull(dataTable);\n\n        var workbook = ExcelHelper.PrepareWorkbook(excelFormat, excelSetting ?? ExcelHelper.DefaultExcelSetting);\n        var maxRowCount = excelFormat == ExcelFormat.Xls\n            ? InternalConstants.MaxRowCountXls\n            : InternalConstants.MaxRowCountXlsx;\n        maxRowCount -= 1;\n\n        var sheetCount = (dataTable.Rows.Count + maxRowCount - 1) / maxRowCount;\n        do\n        {\n            workbook.CreateSheet();\n        } while (workbook.NumberOfSheets < sheetCount);\n\n        if (dataTable.Rows.Count > maxRowCount)\n        {\n            for (var sheetIndex = 0; sheetIndex < sheetCount; sheetIndex++)\n            {\n                var dt = new DataTable();\n                foreach (DataColumn col in dataTable.Columns)\n                {\n                    dt.Columns.Add(new DataColumn(col.ColumnName, col.DataType));\n                }\n\n                for (var i = 0; i < maxRowCount; i++)\n                {\n                    var rowIndex = sheetIndex * maxRowCount + i;\n                    if (rowIndex >= dataTable.Rows.Count)\n                    {\n                        break;\n                    }\n\n                    var row = dt.NewRow();\n                    row.ItemArray = dataTable.Rows[rowIndex].ItemArray;\n                    dt.Rows.Add(row);\n                }\n\n                workbook.GetSheetAt(sheetIndex).ImportData(dt);\n            }\n        }\n        else\n        {\n            workbook.GetSheetAt(0).ImportData(dataTable);\n        }\n\n        return workbook;\n    }\n\n    /// <summary>\n    ///     export DataTable to excel file\n    /// </summary>\n    /// <param name=\"dataTable\">dataTable</param>\n    /// <param name=\"excelPath\">excelPath</param>\n    /// <returns></returns>\n    public static void ToExcelFile(this DataTable dataTable, string excelPath) =>\n        ToExcelFile(dataTable, excelPath, null);\n\n    /// <summary>\n    ///     Import dataTable data\n    /// </summary>\n    /// <param name=\"sheet\">sheet</param>\n    /// <param name=\"dataTable\">dataTable</param>\n    public static void ImportData(this ISheet sheet, DataTable? dataTable)\n    {\n        Guard.NotNull(sheet);\n\n        if (dataTable is null)\n        {\n            return;\n        }\n\n        if (dataTable.Columns.Count > 0)\n        {\n            var headerRow = sheet.CreateRow(0);\n            for (var i = 0; i < dataTable.Columns.Count; i++)\n            {\n                var columnName = InternalHelper.GetDecodeColumnName(dataTable.Columns[i].ColumnName);\n                headerRow.CreateCell(i, CellType.String).SetCellValue(columnName);\n            }\n\n            for (var i = 1; i <= dataTable.Rows.Count; i++)\n            {\n                var row = sheet.CreateRow(i);\n                for (var j = 0; j < dataTable.Columns.Count; j++)\n                {\n                    row.CreateCell(j, CellType.String).SetCellValue(dataTable.Rows[i - 1][j]);\n                }\n            }\n        }\n    }\n\n    /// <summary>\n    ///     export DataTable to excel file\n    /// </summary>\n    /// <param name=\"dataTable\">dataTable</param>\n    /// <param name=\"excelPath\">excelPath</param>\n    /// <param name=\"excelSetting\">excelSetting</param>\n    /// <returns></returns>\n    public static void ToExcelFile(this DataTable dataTable, string excelPath, ExcelSetting? excelSetting)\n    {\n        Guard.NotNull(dataTable);\n\n        var workbook = dataTable.GetWorkbookWithAutoSplitSheet(\n            excelPath.EndsWith(\".xls\", StringComparison.OrdinalIgnoreCase) ? ExcelFormat.Xls : ExcelFormat.Xlsx,\n            excelSetting);\n        workbook.WriteToFile(excelPath, true);\n    }\n\n    /// <summary>\n    ///     DataTable2ExcelStream\n    /// </summary>\n    /// <param name=\"dataTable\">dataTable</param>\n    /// <param name=\"stream\">stream</param>\n    /// <returns></returns>\n    public static void ToExcelStream(this DataTable dataTable, Stream stream) =>\n        ToExcelStream(dataTable, stream, ExcelFormat.Xls);\n\n    /// <summary>\n    ///     DataTable2ExcelStream\n    /// </summary>\n    /// <param name=\"dataTable\">dataTable</param>\n    /// <param name=\"stream\">stream</param>\n    /// <param name=\"excelFormat\">excelFormat</param>\n    /// <returns></returns>\n    public static void ToExcelStream(this DataTable dataTable, Stream stream, ExcelFormat excelFormat) =>\n        ToExcelStream(dataTable, stream, excelFormat, null);\n\n    /// <summary>\n    ///     DataTable2ExcelStream\n    /// </summary>\n    /// <param name=\"dataTable\">dataTable</param>\n    /// <param name=\"stream\">stream</param>\n    /// <param name=\"excelFormat\">excelFormat</param>\n    /// <param name=\"excelSetting\">excelSetting</param>\n    /// <returns></returns>\n    public static void ToExcelStream(this DataTable dataTable, Stream stream, ExcelFormat excelFormat,\n        ExcelSetting? excelSetting)\n    {\n        Guard.NotNull(dataTable);\n\n        var workbook = dataTable.GetWorkbookWithAutoSplitSheet(excelFormat, excelSetting);\n        workbook.Write(stream);\n    }\n\n    /// <summary>\n    ///     DataTable2ExcelBytes(*.xlsx by default)\n    /// </summary>\n    /// <param name=\"dataTable\">dataTable</param>\n    public static byte[] ToExcelBytes(this DataTable dataTable) => ToExcelBytes(dataTable, ExcelFormat.Xls);\n\n    /// <summary>\n    ///     DataTable2ExcelBytes\n    /// </summary>\n    /// <param name=\"dataTable\">dataTable</param>\n    /// <param name=\"excelFormat\">excel格式</param>\n    public static byte[] ToExcelBytes(this DataTable dataTable, ExcelFormat excelFormat) =>\n        ToExcelBytes(dataTable, excelFormat, null);\n\n    /// <summary>\n    ///     DataTable2ExcelBytes\n    /// </summary>\n    /// <param name=\"dataTable\">dataTable</param>\n    /// <param name=\"excelFormat\">excelFormat</param>\n    /// <param name=\"excelSetting\">excelSetting</param>\n    public static byte[] ToExcelBytes(this DataTable dataTable, ExcelFormat excelFormat, ExcelSetting? excelSetting)\n    {\n        Guard.NotNull(dataTable);\n\n        var workbook = dataTable.GetWorkbookWithAutoSplitSheet(excelFormat, excelSetting);\n        return workbook.ToExcelBytes(true);\n    }\n\n    /// <summary>\n    ///     SetCellValue\n    /// </summary>\n    /// <param name=\"cell\">ICell</param>\n    /// <param name=\"value\">value</param>\n    public static void SetCellValue(this ICell cell, object? value) => cell.SetCellValue(value, null);\n\n    /// <summary>\n    ///     SetCellValue\n    /// </summary>\n    /// <param name=\"cell\">ICell</param>\n    /// <param name=\"value\">value</param>\n    /// <param name=\"formatter\">formatter</param>\n    public static void SetCellValue(this ICell cell, object? value, string? formatter)\n    {\n        Guard.NotNull(cell);\n\n        if (value is null || DBNull.Value == value)\n        {\n            cell.SetCellType(CellType.Blank);\n            return;\n        }\n\n        if (value is DateTime time)\n        {\n            cell.SetCellValue(string.IsNullOrWhiteSpace(formatter)\n                ? time.Date == time ? time.ToDateString() : time.ToTimeString()\n                : time.ToString(formatter));\n            cell.SetCellType(CellType.String);\n        }\n        else\n        {\n            var type = value.GetType();\n            if (\n                type == typeof(double) ||\n                type == typeof(int) ||\n                type == typeof(long) ||\n                type == typeof(float) ||\n                type == typeof(decimal)\n            )\n            {\n                cell.SetCellValue(Convert.ToDouble(value));\n                cell.SetCellType(CellType.Numeric);\n            }\n            else if (type == typeof(bool))\n            {\n                cell.SetCellValue((bool)value);\n                cell.SetCellType(CellType.Boolean);\n            }\n            else if (type == typeof(byte[]) && value is byte[] bytes)\n            {\n                cell.Sheet.TryAddPicture(cell.RowIndex, cell.ColumnIndex, bytes);\n            }\n            else\n            {\n                cell.SetCellValue(value is IFormattable val && formatter.IsNotNullOrWhiteSpace()\n                    ? val.ToString(formatter, CultureInfo.CurrentCulture)\n                    : value.ToString());\n                cell.SetCellType(CellType.String);\n            }\n        }\n    }\n\n    /// <summary>\n    ///     GetCellValue\n    /// </summary>\n    /// <param name=\"cell\">cell</param>\n    /// <param name=\"propertyType\">propertyType</param>\n    /// <param name=\"formulaEvaluator\">formulaEvaluator</param>\n    /// <returns>cellValue</returns>\n    public static object? GetCellValue(this ICell? cell, Type propertyType,\n        IFormulaEvaluator? formulaEvaluator = null)\n    {\n        if (cell is null || cell.CellType == CellType.Blank || cell.CellType == CellType.Error)\n        {\n            return propertyType.GetDefaultValue();\n        }\n\n        switch (cell.CellType)\n        {\n            case CellType.Numeric:\n                if (DateUtil.IsCellDateFormatted(cell))\n                {\n                    if (propertyType == typeof(DateTime))\n                    {\n                        return cell.DateCellValue;\n                    }\n\n                    return cell.DateCellValue.ToOrDefault(propertyType);\n                }\n\n                if (propertyType == typeof(double))\n                {\n                    return cell.NumericCellValue;\n                }\n\n                return cell.NumericCellValue.ToOrDefault(propertyType);\n\n            case CellType.String:\n                return cell.StringCellValue.ToOrDefault(propertyType);\n\n            case CellType.Boolean:\n                if (propertyType == typeof(bool))\n                {\n                    return cell.BooleanCellValue;\n                }\n\n                return cell.BooleanCellValue.ToOrDefault(propertyType);\n\n            case CellType.Formula:\n                try\n                {\n                    var evaluatedCellValue = formulaEvaluator?.Evaluate(cell);\n                    if (evaluatedCellValue is not null)\n                    {\n                        if (evaluatedCellValue.CellType == CellType.Blank\n                            || evaluatedCellValue.CellType == CellType.Error)\n                        {\n                            return propertyType.GetDefaultValue();\n                        }\n\n                        if (evaluatedCellValue.CellType == CellType.Numeric)\n                        {\n                            if (DateUtil.IsCellDateFormatted(cell))\n                            {\n                                if (propertyType == typeof(DateTime))\n                                {\n                                    return cell.DateCellValue;\n                                }\n\n                                return cell.DateCellValue.ToOrDefault(propertyType);\n                            }\n\n                            if (propertyType == typeof(double))\n                            {\n                                return cell.NumericCellValue;\n                            }\n\n                            return evaluatedCellValue.NumberValue.ToOrDefault(propertyType);\n                        }\n\n                        if (evaluatedCellValue.CellType == CellType.Boolean)\n                        {\n                            if (propertyType == typeof(bool))\n                            {\n                                return cell.BooleanCellValue;\n                            }\n\n                            return evaluatedCellValue.BooleanValue.ToOrDefault(propertyType);\n                        }\n\n                        if (evaluatedCellValue.CellType == CellType.String)\n                        {\n                            return evaluatedCellValue.StringValue.ToOrDefault(propertyType);\n                        }\n\n                        return evaluatedCellValue.FormatAsString().ToOrDefault(propertyType);\n                    }\n                }\n                catch (Exception e)\n                {\n                    InvokeHelper.OnInvokeException?.Invoke(e);\n                }\n\n                return cell.ToString().ToOrDefault(propertyType);\n\n            default:\n                return cell.ToString().ToOrDefault(propertyType);\n        }\n    }\n\n    /// <summary>\n    ///     GetCellValue\n    /// </summary>\n    /// <typeparam name=\"T\">Type</typeparam>\n    /// <param name=\"cell\">cell</param>\n    /// <param name=\"formulaEvaluator\"></param>\n    /// <returns>typed cell value</returns>\n    public static T GetCellValue<T>(this ICell? cell, IFormulaEvaluator? formulaEvaluator = null) =>\n        (T)cell.GetCellValue(typeof(T), formulaEvaluator)!;\n\n    /// <summary>\n    ///     Get Sheet Row Collection\n    /// </summary>\n    /// <param name=\"sheet\">excel sheet</param>\n    /// <returns>row collection</returns>\n    public static NpoiRowCollection GetRowCollection(this ISheet sheet) => new(sheet);\n\n    /// <summary>\n    ///     Get Row Cell Collection\n    /// </summary>\n    /// <param name=\"row\">excel sheet row</param>\n    /// <returns>row collection</returns>\n    public static NpoiCellCollection GetCellCollection(this IRow row) => new(row);\n\n    /// <summary>\n    ///     get workbook IFormulaEvaluator\n    /// </summary>\n    /// <param name=\"workbook\">workbook</param>\n    /// <returns></returns>\n    public static IFormulaEvaluator GetFormulaEvaluator(this IWorkbook workbook)\n    {\n        Guard.NotNull(workbook);\n\n        return workbook switch\n        {\n            HSSFWorkbook => new HSSFFormulaEvaluator(workbook),\n            XSSFWorkbook => new XSSFFormulaEvaluator(workbook),\n            SXSSFWorkbook sBook => new SXSSFFormulaEvaluator(sBook),\n            _ => throw new NotSupportedException()\n        };\n    }\n\n    /// <summary>\n    ///     get pictures with position in current sheet\n    /// </summary>\n    /// <param name=\"sheet\">sheet</param>\n    /// <returns></returns>\n    public static Dictionary<CellPosition, IPictureData> GetPicturesAndPosition(this ISheet sheet)\n    {\n        Guard.NotNull(sheet);\n\n        var dictionary = new Dictionary<CellPosition, IPictureData>();\n        if (sheet.DrawingPatriarch is null)\n        {\n            return dictionary;\n        }\n\n        if (sheet.Workbook is HSSFWorkbook)\n        {\n            foreach (var shape in ((HSSFPatriarch)sheet.DrawingPatriarch).Children)\n            {\n                if (shape is HSSFPicture picture)\n                {\n                    var position = new CellPosition(picture.ClientAnchor.Row1, picture.ClientAnchor.Col1);\n                    dictionary[position] = picture.PictureData;\n                }\n            }\n        }\n        else if (sheet.Workbook is XSSFWorkbook)\n        {\n            foreach (var shape in ((XSSFDrawing)sheet.DrawingPatriarch).GetShapes())\n            {\n                if (shape is XSSFPicture picture)\n                {\n                    var position = new CellPosition(picture.ClientAnchor.Row1, picture.ClientAnchor.Col1);\n                    dictionary[position] = picture.PictureData;\n                }\n            }\n        }\n\n        return dictionary;\n    }\n\n    /// <summary>\n    ///     TryAddPicture in specific cell\n    /// </summary>\n    /// <param name=\"sheet\">sheet</param>\n    /// <param name=\"row\">cell rowIndex</param>\n    /// <param name=\"col\">cell columnIndex</param>\n    /// <param name=\"pictureData\">pictureData</param>\n    /// <returns>whether add success</returns>\n    public static bool TryAddPicture(this ISheet sheet, int row, int col, IPictureData pictureData)\n        => TryAddPicture(sheet, row, col, pictureData.Data, pictureData.PictureType);\n\n    /// <summary>\n    ///     TryAddPicture in specific cell\n    /// </summary>\n    /// <param name=\"sheet\">sheet</param>\n    /// <param name=\"row\">cell rowIndex</param>\n    /// <param name=\"col\">cell columnIndex</param>\n    /// <param name=\"pictureBytes\">picture bytes</param>\n    /// <param name=\"pictureType\">picture type</param>\n    /// <returns>whether add success</returns>\n    public static bool TryAddPicture(this ISheet sheet, int row, int col, byte[] pictureBytes,\n        PictureType pictureType = PictureType.PNG)\n    {\n        Guard.NotNull(sheet);\n\n        try\n        {\n            var pictureIndex = sheet.Workbook.AddPicture(pictureBytes, pictureType);\n\n            var clientAnchor = sheet.Workbook.GetCreationHelper().CreateClientAnchor();\n            clientAnchor.Row1 = row;\n            clientAnchor.Col1 = col;\n\n            var picture = (sheet.DrawingPatriarch ?? sheet.CreateDrawingPatriarch())\n                .CreatePicture(clientAnchor, pictureIndex);\n            picture.Resize();\n            return true;\n        }\n        catch (Exception e)\n        {\n            Debug.WriteLine(e);\n            InvokeHelper.OnInvokeException?.Invoke(e);\n        }\n\n        return false;\n    }\n\n    /// <summary>\n    ///     Write workbook to excel file\n    /// </summary>\n    /// <param name=\"workbook\">workbook</param>\n    /// <param name=\"filePath\">file path</param>\n    public static void WriteToFile(this IWorkbook workbook, string filePath) => WriteToFile(workbook, filePath, false);\n\n    /// <summary>\n    ///     Write workbook to excel file\n    /// </summary>\n    /// <param name=\"workbook\">workbook</param>\n    /// <param name=\"filePath\">file path</param>\n    /// <param name=\"closeWorkbook\">whether to close the workbook</param>\n    public static void WriteToFile(this IWorkbook workbook, string filePath, bool closeWorkbook)\n    {\n        Guard.NotNull(workbook);\n        try\n        {\n            InternalHelper.EnsureFileIsNotReadOnly(filePath);\n            var dir = Path.GetDirectoryName(filePath);\n            if (!string.IsNullOrWhiteSpace(dir))\n            {\n                if (!Directory.Exists(dir))\n                {\n                    Directory.CreateDirectory(dir);\n                }\n            }\n\n            using var fileStream = File.Create(filePath);\n            workbook.Write(fileStream);\n        }\n        finally\n        {\n            if (closeWorkbook)\n                workbook.Close();\n        }\n    }\n\n    /// <summary>\n    ///     ToExcelBytes\n    /// </summary>\n    /// <param name=\"workbook\">workbook</param>\n    /// <returns>excel bytes</returns>\n    public static byte[] ToExcelBytes(this IWorkbook workbook) => ToExcelBytes(workbook, false);\n\n    /// <summary>\n    ///     ToExcelBytes\n    /// </summary>\n    /// <param name=\"workbook\">workbook</param>\n    /// <returns>excel bytes</returns>\n    /// <param name=\"closeWorkbook\">whether to close the workbook</param>\n    public static byte[] ToExcelBytes(this IWorkbook workbook, bool closeWorkbook)\n    {\n        Guard.NotNull(workbook);\n        try\n        {\n            using var ms = new MemoryStream();\n            workbook.Write(ms);\n            return ms.ToArray();\n        }\n        finally\n        {\n            if (closeWorkbook)\n                workbook.Close();\n        }\n    }\n\n    #region ExportByTemplate\n\n    /// <summary>\n    ///     export excel via template\n    /// </summary>\n    /// <typeparam name=\"TEntity\">Entity Type</typeparam>\n    /// <param name=\"entities\">entities</param>\n    /// <param name=\"templatePath\"></param>\n    /// <param name=\"excelPath\">templateBytes</param>\n    /// <param name=\"sheetIndex\">sheetIndex,zero by default</param>\n    /// <param name=\"extraData\">extraData</param>\n    /// <returns>exported excel bytes</returns>\n    public static void ToExcelFileByTemplate<TEntity>(this IEnumerable<TEntity> entities, string templatePath,\n        string excelPath, int sheetIndex = 0, object? extraData = null)\n    {\n        Guard.NotNull(entities);\n        Guard.NotNull(templatePath);\n        Guard.NotNull(excelPath);\n\n        var workbook = ExcelHelper.LoadExcel(templatePath);\n        entities.ToExcelFileByTemplate(workbook, excelPath, sheetIndex, extraData);\n    }\n\n    /// <summary>\n    ///     export excel via template\n    /// </summary>\n    /// <typeparam name=\"TEntity\">Entity Type</typeparam>\n    /// <param name=\"entities\">entities</param>\n    /// <param name=\"templateBytes\">templateBytes</param>\n    /// <param name=\"excelFormat\">excelFormat</param>\n    /// <param name=\"excelPath\">excelPath</param>\n    /// <param name=\"sheetIndex\">sheetIndex,zero by default</param>\n    /// <param name=\"extraData\">extraData</param>\n    /// <returns>exported excel bytes</returns>\n    public static void ToExcelFileByTemplate<TEntity>(this IEnumerable<TEntity> entities, byte[] templateBytes,\n        string excelPath, ExcelFormat excelFormat = ExcelFormat.Xls, int sheetIndex = 0, object? extraData = null)\n    {\n        Guard.NotNull(entities);\n        Guard.NotNull(templateBytes);\n        Guard.NotNull(excelPath);\n\n        var workbook = ExcelHelper.LoadExcel(templateBytes, excelFormat);\n        entities.ToExcelFileByTemplate(workbook, excelPath, sheetIndex, extraData);\n    }\n\n    /// <summary>\n    ///     export excel via template\n    /// </summary>\n    /// <typeparam name=\"TEntity\">Entity Type</typeparam>\n    /// <param name=\"entities\">entities</param>\n    /// <param name=\"templateWorkbook\">templateWorkbook</param>\n    /// <param name=\"excelPath\"></param>\n    /// <param name=\"sheetIndex\">sheetIndex</param>\n    /// <param name=\"extraData\">extraData</param>\n    /// <returns>exported excel bytes</returns>\n    public static void ToExcelFileByTemplate<TEntity>(this IEnumerable<TEntity> entities,\n        IWorkbook templateWorkbook, string excelPath, int sheetIndex = 0, object? extraData = null)\n    {\n        Guard.NotNull(entities);\n        Guard.NotNull(templateWorkbook);\n\n        if (sheetIndex < 0)\n        {\n            sheetIndex = 0;\n        }\n\n        var templateSheet = templateWorkbook.GetSheetAt(sheetIndex);\n        NpoiTemplateHelper.EntityListToSheetByTemplate(\n            templateSheet, entities, extraData\n        );\n        templateWorkbook.WriteToFile(excelPath);\n    }\n\n    /// <summary>\n    ///     export excel via template\n    /// </summary>\n    /// <typeparam name=\"TEntity\">Entity Type</typeparam>\n    /// <param name=\"entities\">entities</param>\n    /// <param name=\"templatePath\">templatePath</param>\n    /// <param name=\"sheetIndex\">sheetIndex,zero by default</param>\n    /// <param name=\"extraData\">extraData</param>\n    /// <returns>exported excel bytes</returns>\n    public static byte[] ToExcelBytesByTemplate<TEntity>(this IEnumerable<TEntity> entities, string templatePath,\n        int sheetIndex = 0, object? extraData = null) => ToExcelBytesByTemplate(entities,\n        ExcelHelper.LoadExcel(templatePath), sheetIndex, extraData);\n\n    /// <summary>\n    ///     export excel via template\n    /// </summary>\n    /// <typeparam name=\"TEntity\">Entity Type</typeparam>\n    /// <param name=\"entities\">entities</param>\n    /// <param name=\"templateBytes\">templateBytes</param>\n    /// <param name=\"excelFormat\">excelFormat</param>\n    /// <param name=\"sheetIndex\">sheetIndex,zero by default</param>\n    /// <param name=\"extraData\">extraData</param>\n    /// <returns>exported excel bytes</returns>\n    public static byte[] ToExcelBytesByTemplate<TEntity>(this IEnumerable<TEntity> entities, byte[] templateBytes,\n        ExcelFormat excelFormat = ExcelFormat.Xls, int sheetIndex = 0, object? extraData = null)\n    {\n        Guard.NotNull(entities);\n        Guard.NotNull(templateBytes);\n\n        var workbook = ExcelHelper.LoadExcel(templateBytes, excelFormat);\n        return ToExcelBytesByTemplate(entities, workbook, sheetIndex, extraData);\n    }\n\n    /// <summary>\n    ///     export excel via template\n    /// </summary>\n    /// <typeparam name=\"TEntity\">Entity Type</typeparam>\n    /// <param name=\"entities\">entities</param>\n    /// <param name=\"templateStream\">templateStream</param>\n    /// <param name=\"excelFormat\">excelFormat</param>\n    /// <param name=\"sheetIndex\">sheetIndex,zero by default</param>\n    /// <param name=\"extraData\">extraData</param>\n    /// <returns>exported excel bytes</returns>\n    public static byte[] ToExcelBytesByTemplate<TEntity>(this IEnumerable<TEntity> entities, Stream templateStream,\n        ExcelFormat excelFormat = ExcelFormat.Xls, int sheetIndex = 0, object? extraData = null)\n    {\n        Guard.NotNull(templateStream);\n\n        var workbook = ExcelHelper.LoadExcel(templateStream, excelFormat);\n        return ToExcelBytesByTemplate(entities, workbook, sheetIndex, extraData);\n    }\n\n    /// <summary>\n    ///     export excel via template\n    /// </summary>\n    /// <typeparam name=\"TEntity\">Entity Type</typeparam>\n    /// <param name=\"entities\">entities</param>\n    /// <param name=\"templateWorkbook\">templateWorkbook</param>\n    /// <param name=\"sheetIndex\">sheetIndex</param>\n    /// <param name=\"extraData\">extraData</param>\n    /// <returns>exported excel bytes</returns>\n    public static byte[] ToExcelBytesByTemplate<TEntity>(this IEnumerable<TEntity> entities,\n        IWorkbook templateWorkbook, int sheetIndex = 0, object? extraData = null)\n    {\n        Guard.NotNull(entities);\n        Guard.NotNull(templateWorkbook);\n\n        if (sheetIndex < 0)\n        {\n            sheetIndex = 0;\n        }\n\n        var templateSheet = templateWorkbook.GetSheetAt(sheetIndex);\n        NpoiTemplateHelper.EntityListToSheetByTemplate(\n            templateSheet, entities, extraData\n        );\n        return templateWorkbook.ToExcelBytes();\n    }\n\n    /// <summary>\n    ///     export excel via template\n    /// </summary>\n    /// <typeparam name=\"TEntity\">Entity Type</typeparam>\n    /// <param name=\"entities\">entities</param>\n    /// <param name=\"templateSheet\"></param>\n    /// <param name=\"extraData\">extraData</param>\n    /// <returns>exported excel bytes</returns>\n    public static byte[] ToExcelBytesByTemplate<TEntity>(this IEnumerable<TEntity> entities, ISheet templateSheet,\n        object? extraData = null)\n    {\n        Guard.NotNull(entities);\n\n        NpoiTemplateHelper.EntityListToSheetByTemplate(\n            templateSheet, entities, extraData\n        );\n        return templateSheet.Workbook.ToExcelBytes();\n    }\n\n    #endregion ExportByTemplate\n}\n"
  },
  {
    "path": "src/WeihanLi.Npoi/NpoiHelper.cs",
    "content": "﻿// Copyright (c) Weihan Li. All rights reserved.\n// Licensed under the Apache license.\n\nusing NPOI.SS.UserModel;\nusing NPOI.SS.Util;\nusing System.Data;\nusing System.Diagnostics;\nusing System.Reflection;\nusing WeihanLi.Common;\nusing WeihanLi.Common.Helpers;\nusing WeihanLi.Extensions;\nusing WeihanLi.Npoi.Configurations;\nusing WeihanLi.Npoi.Settings;\n\nnamespace WeihanLi.Npoi;\n\ninternal static class NpoiHelper\n{\n    private static SheetSetting GetSheetSetting(IDictionary<int, SheetSetting> sheetSettings, int sheetIndex) =>\n        sheetIndex > 0 && sheetSettings.TryGetValue(sheetIndex, out var sheetSetting)\n            ? sheetSetting\n            : sheetSettings[0];\n\n    /// <summary>\n    ///     Converts a sheet to entities while honoring configuration, filters, and pictures.\n    /// </summary>\n    /// <typeparam name=\"TEntity\">Entity type.</typeparam>\n    /// <param name=\"sheet\">Sheet instance to parse.</param>\n    /// <param name=\"sheetIndex\">Zero-based sheet index.</param>\n    /// <param name=\"dataAction\">Optional callback per entity.</param>\n    /// <returns>Sequence of entities, possibly containing <c>null</c> entries.</returns>\n    public static IEnumerable<TEntity?> SheetToEntities<TEntity>(ISheet? sheet, int sheetIndex, Action<TEntity?, ExcelConfiguration<TEntity>, int>? dataAction = null) where TEntity : new()\n    {\n        if (sheet is null || sheet.PhysicalNumberOfRows <= 0)\n        {\n            yield break;\n        }\n\n        var configuration = InternalHelper.GetExcelConfigurationMapping<TEntity>();\n        var sheetSetting = GetSheetSetting(configuration.SheetSettings, sheetIndex);\n\n        var propertyColumnDictionary = InternalHelper.GetPropertyColumnDictionary(configuration);\n        var propertyColumnDic = sheetSetting.HeaderRowIndex >= 0\n            ? propertyColumnDictionary.ToDictionary(p => p.Key,\n                p => new PropertyConfiguration\n                {\n                    ColumnIndex = -1,\n                    ColumnFormatter = p.Value.ColumnFormatter,\n                    ColumnTitle = p.Value.ColumnTitle,\n                    ColumnWidth = p.Value.ColumnWidth,\n                    IsIgnored = p.Value.IsIgnored\n                })\n            : propertyColumnDictionary;\n        var formulaEvaluator = sheet.Workbook.GetFormulaEvaluator();\n\n        var pictures = propertyColumnDic\n            .Any(p => p.Key.CanWrite &&\n                      (p.Key.PropertyType == typeof(byte[]) || p.Key.PropertyType == typeof(IPictureData)))\n            ? sheet.GetPicturesAndPosition()\n            : new Dictionary<CellPosition, IPictureData>();\n\n        for (var rowIndex = sheet.FirstRowNum;\n            rowIndex <= (sheetSetting.EndRowIndex ?? sheet.LastRowNum);\n            rowIndex++)\n        {\n            var row = sheet.GetRow(rowIndex);\n\n            // readerHeader and auto adjust the column index when columnIndex adjustment not disabled\n            if (rowIndex == sheetSetting.HeaderRowIndex && !sheetSetting.SkipHeaderRow)\n            {\n                if (row is not null)\n                {\n                    // adjust column index according to the imported data header\n                    for (var i = row.FirstCellNum; i < row.LastCellNum; i++)\n                    {\n                        if (row.GetCell(i) is null)\n                        {\n                            continue;\n                        }\n\n                        row.GetCell(i).SetCellType(CellType.String);\n                        var title = row.GetCell(i).StringCellValue.Trim();\n                        var col = propertyColumnDic.GetPropertySetting(title);\n                        col?.ColumnIndex = i;\n                    }\n                }\n\n                // use default column index if no headers\n                if (propertyColumnDic.Values.Any(p => p.ColumnIndex < 0))\n                {\n                    propertyColumnDic = propertyColumnDictionary;\n                }\n            }\n            else if (rowIndex >= sheetSetting.StartRowIndex)\n            {\n                if (sheetSetting.RowFilter?.Invoke(row) == false)\n                {\n                    continue;\n                }\n\n                if (row is null)\n                {\n                    yield return default;\n                }\n                else\n                {\n                    TEntity? entity;\n                    if (row.Cells.Count > 0)\n                    {\n                        entity = new TEntity();\n\n                        if (configuration.EntityType.IsValueType)\n                        {\n                            var obj = (object)entity; // boxing for value types\n\n                            ProcessImport(obj, row, rowIndex, propertyColumnDic, sheetSetting, formulaEvaluator,\n                                pictures);\n\n                            entity = (TEntity)obj; // unboxing\n                        }\n                        else\n                        {\n                            ProcessImport(entity, row, rowIndex, propertyColumnDic, sheetSetting, formulaEvaluator,\n                                pictures);\n                        }\n                    }\n                    else\n                    {\n                        entity = default;\n                    }\n\n                    if (entity is not null)\n                    {\n                        foreach (var propertyInfo in propertyColumnDic.Keys)\n                        {\n                            if (!propertyInfo.CanWrite) continue;\n\n                            var propertyValue = propertyInfo.GetValueGetter()?.Invoke(entity);\n                            if (!InternalCache.InputFormatterFuncCache.TryGetValue(propertyInfo,\n                                    out var formatterFunc) || formatterFunc?.Method is null) continue;\n\n                            var valueSetter = propertyInfo.GetValueSetter();\n                            if (valueSetter is null) continue;\n\n                            try\n                            {\n                                // apply custom formatterFunc\n                                var formattedValue = formatterFunc.DynamicInvoke(entity, propertyValue);\n                                valueSetter.Invoke(entity, formattedValue);\n                            }\n                            catch (Exception e)\n                            {\n                                Debug.WriteLine(e);\n                                InvokeHelper.OnInvokeException?.Invoke(e);\n                            }\n                        }\n                    }\n\n                    if (configuration.DataFilter?.Invoke(entity) == false)\n                    {\n                        continue;\n                    }\n\n                    dataAction?.Invoke(entity, configuration, rowIndex);\n                    configuration.PostImportAction?.Invoke(entity, rowIndex);\n\n                    yield return entity;\n                }\n            }\n        }\n    }\n\n    private static void ProcessImport(object entity, IRow row, int rowIndex,\n        Dictionary<PropertyInfo, PropertyConfiguration> propertyColumnDic, SheetSetting sheetSetting,\n        IFormulaEvaluator formulaEvaluator,\n        Dictionary<CellPosition, IPictureData> pictures)\n    {\n        foreach (var key in propertyColumnDic.Keys)\n        {\n            var colIndex = propertyColumnDic[key].ColumnIndex;\n            if (colIndex >= 0 && key.CanWrite)\n            {\n                var columnValue = key.PropertyType.GetDefaultValue();\n                var cell = row.GetCell(colIndex);\n\n                if (sheetSetting.CellFilter?.Invoke(cell) != false)\n                {\n                    var valueSetter = key.GetValueSetter();\n                    if (valueSetter is null) continue;\n\n                    if (key.PropertyType == typeof(byte[])\n                        || key.PropertyType == typeof(IPictureData))\n                    {\n                        if (pictures.TryGetValue(new CellPosition(rowIndex, colIndex), out var pic))\n                        {\n                            valueSetter.Invoke(entity,\n                                key.PropertyType == typeof(IPictureData) ? pic : pic.Data);\n                        }\n                    }\n                    else\n                    {\n                        var valueApplied = false;\n                        InternalCache.CellReaderFuncCache.TryGetValue(key, out var cellReader);\n                        if (cellReader?.Method is not null)\n                        {\n                            columnValue = cellReader.DynamicInvoke(cell);\n                            valueApplied = true;\n                        }\n                        else\n                        {\n                            InternalCache.ColumnInputFormatterFuncCache.TryGetValue(key,\n                                out var formatterFunc);\n                            if (formatterFunc?.Method is not null)\n                            {\n                                var cellValue = cell.GetCellValue<string>(formulaEvaluator);\n                                try\n                                {\n                                    // apply custom formatterFunc\n                                    columnValue = formatterFunc.DynamicInvoke(cellValue);\n                                    valueApplied = true;\n                                }\n                                catch (Exception e)\n                                {\n                                    Debug.WriteLine(e);\n                                    InvokeHelper.OnInvokeException?.Invoke(e);\n                                }\n                            }\n                        }\n\n                        if (valueApplied == false)\n                        {\n                            columnValue = cell.GetCellValue(key.PropertyType, formulaEvaluator);\n                        }\n\n                        valueSetter.Invoke(entity, columnValue);\n                    }\n                }\n            }\n        }\n    }\n\n    /// <summary>\n    ///     Export entity list to Excel sheet\n    /// </summary>\n    /// <typeparam name=\"TEntity\">entity type</typeparam>\n    /// <param name=\"sheet\">sheet</param>\n    /// <param name=\"entityList\">entity list</param>\n    /// <param name=\"sheetIndex\">sheetIndex</param>\n    /// <returns>sheet</returns>\n    public static ISheet EntitiesToSheet<TEntity>(ISheet sheet, IEnumerable<TEntity>? entityList, int sheetIndex)\n    {\n        Guard.NotNull(sheet);\n        if (entityList is null)\n        {\n            return sheet;\n        }\n\n        var configuration = InternalHelper.GetExcelConfigurationMapping<TEntity>();\n        var propertyColumnDictionary = InternalHelper.GetPropertyColumnDictionary(configuration);\n        if (propertyColumnDictionary.Keys.Count == 0)\n        {\n            return sheet;\n        }\n\n        var sheetSetting = GetSheetSetting(configuration.SheetSettings, sheetIndex);\n        if (sheetSetting.HeaderRowIndex >= 0)\n        {\n            var headerRow = sheet.CreateRow(sheetSetting.HeaderRowIndex);\n            foreach (var key in propertyColumnDictionary.Keys)\n            {\n                var cell = headerRow.CreateCell(propertyColumnDictionary[key].ColumnIndex);\n                cell.SetCellValue(propertyColumnDictionary[key].ColumnTitle);\n                sheetSetting.CellAction?.Invoke(cell);\n            }\n\n            sheetSetting.RowAction?.Invoke(headerRow);\n        }\n\n        var rowIndex = 0;\n        foreach (var entity in entityList)\n        {\n            var row = sheet.CreateRow(sheetSetting.StartRowIndex + rowIndex);\n            if (entity is not null)\n            {\n                foreach (var key in propertyColumnDictionary.Keys)\n                {\n                    var propertyValue = key.GetValueGetter<TEntity>()?.Invoke(entity);\n                    if (InternalCache.OutputFormatterFuncCache.TryGetValue(key, out var formatterFunc) &&\n                        formatterFunc?.Method is not null)\n                    {\n                        try\n                        {\n                            // apply custom formatterFunc\n                            propertyValue = formatterFunc.DynamicInvoke(entity, propertyValue);\n                        }\n                        catch (Exception e)\n                        {\n                            Debug.WriteLine(e);\n                            InvokeHelper.OnInvokeException?.Invoke(e);\n                        }\n                    }\n\n                    var cell = row.CreateCell(propertyColumnDictionary[key].ColumnIndex);\n                    cell.SetCellValue(propertyValue, propertyColumnDictionary[key].ColumnFormatter);\n                    sheetSetting.CellAction?.Invoke(cell);\n                }\n            }\n\n            sheetSetting.RowAction?.Invoke(row);\n            rowIndex++;\n        }\n\n        PostSheetProcess(sheet, sheetSetting, rowIndex, configuration, propertyColumnDictionary);\n\n        return sheet;\n    }\n\n    /// <summary>\n    ///     Generic type data table to excel sheet\n    /// </summary>\n    /// <typeparam name=\"TEntity\">entity type</typeparam>\n    /// <param name=\"sheet\">sheet</param>\n    /// <param name=\"dataTable\">data table</param>\n    /// <param name=\"sheetIndex\">sheetIndex</param>\n    /// <returns>sheet</returns>\n    public static ISheet DataTableToSheet<TEntity>(ISheet sheet, DataTable? dataTable, int sheetIndex)\n    {\n        Guard.NotNull(sheet);\n        if (dataTable is null || dataTable.Rows.Count == 0 || dataTable.Columns.Count == 0)\n        {\n            return sheet;\n        }\n\n        var configuration = InternalHelper.GetExcelConfigurationMapping<TEntity>();\n        var propertyColumnDictionary = InternalHelper.GetPropertyColumnDictionary(configuration);\n\n        if (propertyColumnDictionary.Keys.Count == 0)\n        {\n            return sheet;\n        }\n\n        var sheetSetting = GetSheetSetting(configuration.SheetSettings, sheetIndex);\n        if (sheetSetting.HeaderRowIndex >= 0)\n        {\n            var headerRow = sheet.CreateRow(sheetSetting.HeaderRowIndex);\n            for (var i = 0; i < dataTable.Columns.Count; i++)\n            {\n                var col = propertyColumnDictionary.GetPropertySettingByPropertyName(dataTable.Columns[i]\n                    .ColumnName);\n                if (null != col)\n                {\n                    var cell = headerRow.CreateCell(col.ColumnIndex);\n                    cell.SetCellValue(col.ColumnTitle);\n                    sheetSetting.CellAction?.Invoke(cell);\n                }\n            }\n\n            sheetSetting.RowAction?.Invoke(headerRow);\n        }\n\n        for (var i = 0; i < dataTable.Rows.Count; i++)\n        {\n            var row = sheet.CreateRow(sheetSetting.StartRowIndex + i);\n            for (var j = 0; j < dataTable.Columns.Count; j++)\n            {\n                var col = propertyColumnDictionary.GetPropertySettingByPropertyName(dataTable.Columns[j]\n                    .ColumnName);\n                var cell = row.CreateCell(col!.ColumnIndex);\n                cell.SetCellValue(dataTable.Rows[i][j], col.ColumnFormatter);\n                sheetSetting.CellAction?.Invoke(cell);\n            }\n\n            sheetSetting.RowAction?.Invoke(row);\n        }\n\n        PostSheetProcess(sheet, sheetSetting, dataTable.Rows.Count, configuration, propertyColumnDictionary);\n\n        return sheet;\n    }\n\n    private static void PostSheetProcess<TEntity>(ISheet sheet, SheetSetting sheetSetting, int rowsCount,\n        ExcelConfiguration<TEntity> excelConfiguration,\n        IDictionary<PropertyInfo, PropertyConfiguration> propertyColumnDictionary)\n    {\n        if (rowsCount > 0)\n        {\n            foreach (var setting in propertyColumnDictionary.Values)\n            {\n                if (setting.ColumnWidth > 0)\n                {\n                    sheet.SetColumnWidth(setting.ColumnIndex, setting.ColumnWidth * 256);\n                }\n                else\n                {\n                    if (sheetSetting.AutoColumnWidthEnabled)\n                    {\n                        sheet.AutoSizeColumn(setting.ColumnIndex);\n                    }\n                }\n            }\n\n            foreach (var freezeSetting in excelConfiguration.FreezeSettings)\n            {\n                sheet.CreateFreezePane(freezeSetting.ColSplit, freezeSetting.RowSplit, freezeSetting.LeftMostColumn,\n                    freezeSetting.TopRow);\n            }\n\n            if (excelConfiguration.FilterSetting is not null)\n            {\n                var headerIndex = sheetSetting.HeaderRowIndex >= 0 ? sheetSetting.HeaderRowIndex : 0;\n                sheet.SetAutoFilter(new CellRangeAddress(headerIndex, rowsCount + headerIndex,\n                    excelConfiguration.FilterSetting.FirstColumn,\n                    excelConfiguration.FilterSetting.LastColumn ??\n                    propertyColumnDictionary.Values.Max(c => c.ColumnIndex)));\n            }\n        }\n\n        sheetSetting.SheetAction?.Invoke(sheet);\n    }\n}\n"
  },
  {
    "path": "src/WeihanLi.Npoi/NpoiTemplateHelper.cs",
    "content": "﻿// Copyright (c) Weihan Li. All rights reserved.\n// Licensed under the Apache license.\n\nusing NPOI.SS.UserModel;\nusing System.Diagnostics;\nusing WeihanLi.Common.Helpers;\nusing WeihanLi.Extensions;\n\nnamespace WeihanLi.Npoi;\n\ninternal static class NpoiTemplateHelper\n{\n    /// <summary>\n    ///     Shared template options used when parsing placeholders.\n    /// </summary>\n    public static readonly TemplateOptions s_templateOptions = new();\n\n    /// <summary>\n    ///     Fills a template-driven sheet with the provided entities.\n    /// </summary>\n    /// <typeparam name=\"TEntity\">Entity type.</typeparam>\n    /// <param name=\"sheet\">Destination sheet containing template markers.</param>\n    /// <param name=\"entityList\">Data source.</param>\n    /// <param name=\"extraData\">Additional global parameters for the template.</param>\n    /// <returns>The populated sheet.</returns>\n    public static ISheet EntityListToSheetByTemplate<TEntity>(\n        ISheet sheet,\n        IEnumerable<TEntity>? entityList,\n        object? extraData = null)\n    {\n        if (sheet is null)\n        {\n            throw new ArgumentNullException(nameof(sheet));\n        }\n\n        if (entityList is null)\n        {\n            return sheet;\n        }\n\n        var configuration = InternalHelper.GetExcelConfigurationMapping<TEntity>();\n        var propertyColumnDictionary = InternalHelper.GetPropertyColumnDictionary(configuration);\n        var formulaEvaluator = sheet.Workbook.GetFormulaEvaluator();\n        var globalDictionary = extraData.ParseParamInfo()\n            .ToDictionary(x => s_templateOptions.TemplateGlobalParamFormat.FormatWith(x.Key), x => x.Value);\n        foreach (var propertyConfiguration in propertyColumnDictionary)\n        {\n            globalDictionary.Add(\n                s_templateOptions.TemplateHeaderParamFormat.FormatWith(propertyConfiguration.Key.Name),\n                propertyConfiguration.Value.ColumnTitle);\n        }\n\n        var dataFuncDictionary = propertyColumnDictionary\n            .ToDictionary(x => s_templateOptions.TemplateDataParamFormat.FormatWith(x.Key.Name),\n                x => x.Key.GetValueGetter<TEntity>());\n        foreach (var key in propertyColumnDictionary.Keys)\n        {\n            if (InternalCache.OutputFormatterFuncCache.TryGetValue(key, out var formatterFunc) &&\n                formatterFunc?.Method is not null)\n            {\n                dataFuncDictionary[s_templateOptions.TemplateDataParamFormat.FormatWith(key.Name)] = entity =>\n                {\n                    var val = key.GetValueGetter<TEntity>()?.Invoke(entity);\n                    try\n                    {\n                        var formattedValue = formatterFunc.DynamicInvoke(entity, val);\n                        return formattedValue;\n                    }\n                    catch (Exception e)\n                    {\n                        Debug.WriteLine(e);\n                        InvokeHelper.OnInvokeException?.Invoke(e);\n                    }\n\n                    return val;\n                };\n            }\n        }\n\n        // parseTemplate\n        int dataStartRow = -1, dataRowsCount = 0;\n        for (var rowIndex = sheet.FirstRowNum; rowIndex <= sheet.LastRowNum; rowIndex++)\n        {\n            var row = sheet.GetRow(rowIndex);\n            if (row is null)\n            {\n                continue;\n            }\n\n            for (var cellIndex = row.FirstCellNum; cellIndex < row.LastCellNum; cellIndex++)\n            {\n                var cell = row.GetCell(cellIndex);\n                if (cell is null)\n                {\n                    continue;\n                }\n\n                var cellValue = cell.GetCellValue<string>(formulaEvaluator);\n                if (!string.IsNullOrEmpty(cellValue))\n                {\n                    var beforeValue = cellValue;\n                    if (dataStartRow <= 0 || dataRowsCount <= 0)\n                    {\n                        if (dataStartRow >= 0)\n                        {\n                            if (cellValue!.Contains(s_templateOptions.TemplateDataEnd))\n                            {\n                                dataRowsCount = rowIndex - dataStartRow + 1;\n                                cellValue = cellValue.Replace(s_templateOptions.TemplateDataEnd, string.Empty);\n                            }\n                        }\n                        else\n                        {\n                            if (cellValue!.Contains(s_templateOptions.TemplateDataBegin))\n                            {\n                                dataStartRow = rowIndex;\n                                cellValue = cellValue.Replace(s_templateOptions.TemplateDataBegin, string.Empty);\n                            }\n                        }\n                    }\n\n                    foreach (var param in globalDictionary.Keys)\n                    {\n                        if (cellValue!.Contains(param))\n                        {\n                            cellValue = cellValue\n                                .Replace(param,\n                                    globalDictionary[param]?.ToString() ?? string.Empty);\n                        }\n                    }\n\n                    if (beforeValue != cellValue)\n                    {\n                        cell.SetCellValue(cellValue);\n                    }\n                }\n            }\n        }\n\n        if (dataStartRow >= 0 && dataRowsCount > 0)\n        {\n            foreach (var entity in entityList)\n            {\n                sheet.ShiftRows(dataStartRow, sheet.LastRowNum, dataRowsCount);\n                for (var i = 0; i < dataRowsCount; i++)\n                {\n                    var row = sheet.CopyRow(dataStartRow + dataRowsCount + i, dataStartRow + i);\n                    if (null != row)\n                    {\n                        for (var j = 0; j < row.LastCellNum; j++)\n                        {\n                            var cell = row.GetCell(j);\n                            if (null != cell)\n                            {\n                                var cellValue = cell.GetCellValue<string>(formulaEvaluator);\n                                if (!string.IsNullOrEmpty(cellValue) &&\n                                    cellValue!.Contains(s_templateOptions.TemplateDataPrefix))\n                                {\n                                    var beforeValue = cellValue;\n\n                                    foreach (var param in dataFuncDictionary.Keys)\n                                    {\n                                        if (cellValue.Contains(param))\n                                        {\n                                            cellValue = cellValue.Replace(param,\n                                                dataFuncDictionary[param]?.Invoke(entity)?.ToString() ??\n                                                string.Empty);\n                                        }\n                                    }\n\n                                    if (beforeValue != cellValue)\n                                    {\n                                        cell.SetCellValue(cellValue);\n                                    }\n                                }\n                            }\n                        }\n                    }\n                }\n\n                //\n                dataStartRow += dataRowsCount;\n            }\n\n            // remove data template\n            for (var i = 0; i < dataRowsCount; i++)\n            {\n                var row = sheet.GetRow(dataStartRow + i);\n                if (null != row)\n                {\n                    sheet.RemoveRow(row);\n                }\n            }\n\n            sheet.ShiftRows(dataStartRow + dataRowsCount, sheet.LastRowNum, -dataRowsCount);\n        }\n\n        return sheet;\n    }\n}\n"
  },
  {
    "path": "src/WeihanLi.Npoi/Resource.Designer.cs",
    "content": "﻿//------------------------------------------------------------------------------\n// <auto-generated>\n//     This code was generated by a tool.\n//     Runtime Version:4.0.30319.42000\n//\n//     Changes to this file may cause incorrect behavior and will be lost if\n//     the code is regenerated.\n// </auto-generated>\n//------------------------------------------------------------------------------\n\nnamespace WeihanLi.Npoi {\n    using System;\n    \n    \n    /// <summary>\n    ///   A strongly-typed resource class, for looking up localized strings, etc.\n    /// </summary>\n    // This class was auto-generated by the StronglyTypedResourceBuilder\n    // class via a tool like ResGen or Visual Studio.\n    // To add or remove a member, edit your .ResX file then rerun ResGen\n    // with the /str option, or rebuild your VS project.\n    [global::System.CodeDom.Compiler.GeneratedCodeAttribute(\"System.Resources.Tools.StronglyTypedResourceBuilder\", \"16.0.0.0\")]\n    [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]\n    [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]\n    internal class Resource {\n        \n        private static global::System.Resources.ResourceManager resourceMan;\n        \n        private static global::System.Globalization.CultureInfo resourceCulture;\n        \n        [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute(\"Microsoft.Performance\", \"CA1811:AvoidUncalledPrivateCode\")]\n        internal Resource() {\n        }\n        \n        /// <summary>\n        ///   Returns the cached ResourceManager instance used by this class.\n        /// </summary>\n        [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]\n        internal static global::System.Resources.ResourceManager ResourceManager {\n            get {\n                if (object.ReferenceEquals(resourceMan, null)) {\n                    global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager(\"WeihanLi.Npoi.Resource\", typeof(Resource).Assembly);\n                    resourceMan = temp;\n                }\n                return resourceMan;\n            }\n        }\n        \n        /// <summary>\n        ///   Overrides the current thread's CurrentUICulture property for all\n        ///   resource lookups using this strongly typed resource class.\n        /// </summary>\n        [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]\n        internal static global::System.Globalization.CultureInfo Culture {\n            get {\n                return resourceCulture;\n            }\n            set {\n                resourceCulture = value;\n            }\n        }\n        \n        /// <summary>\n        ///   Looks up a localized string similar to the argument can not be empty.\n        /// </summary>\n        internal static string ArgumentCanNotBeEmpty {\n            get {\n                return ResourceManager.GetString(\"ArgumentCanNotBeEmpty\", resourceCulture);\n            }\n        }\n        \n        /// <summary>\n        ///   Looks up a localized string similar to can not find the file.\n        /// </summary>\n        internal static string FileNotFound {\n            get {\n                return ResourceManager.GetString(\"FileNotFound\", resourceCulture);\n            }\n        }\n        \n        /// <summary>\n        ///   Looks up a localized string similar to {0} out of range, max value: {1}.\n        /// </summary>\n        internal static string IndexOutOfRange {\n            get {\n                return ResourceManager.GetString(\"IndexOutOfRange\", resourceCulture);\n            }\n        }\n        \n        /// <summary>\n        ///   Looks up a localized string similar to invalid excel file.\n        /// </summary>\n        internal static string InvalidExcelFile {\n            get {\n                return ResourceManager.GetString(\"InvalidExcelFile\", resourceCulture);\n            }\n        }\n        \n        /// <summary>\n        ///   Looks up a localized string similar to invalid file path.\n        /// </summary>\n        internal static string InvalidFilePath {\n            get {\n                return ResourceManager.GetString(\"InvalidFilePath\", resourceCulture);\n            }\n        }\n    }\n}"
  },
  {
    "path": "src/WeihanLi.Npoi/Resource.resx",
    "content": "﻿<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<root>\n  <!-- \n    Microsoft ResX Schema \n    \n    Version 2.0\n    \n    The primary goals of this format is to allow a simple XML format \n    that is mostly human readable. The generation and parsing of the \n    various data types are done through the TypeConverter classes \n    associated with the data types.\n    \n    Example:\n    \n    ... ado.net/XML headers & schema ...\n    <resheader name=\"resmimetype\">text/microsoft-resx</resheader>\n    <resheader name=\"version\">2.0</resheader>\n    <resheader name=\"reader\">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>\n    <resheader name=\"writer\">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>\n    <data name=\"Name1\"><value>this is my long string</value><comment>this is a comment</comment></data>\n    <data name=\"Color1\" type=\"System.Drawing.Color, System.Drawing\">Blue</data>\n    <data name=\"Bitmap1\" mimetype=\"application/x-microsoft.net.object.binary.base64\">\n        <value>[base64 mime encoded serialized .NET Framework object]</value>\n    </data>\n    <data name=\"Icon1\" type=\"System.Drawing.Icon, System.Drawing\" mimetype=\"application/x-microsoft.net.object.bytearray.base64\">\n        <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>\n        <comment>This is a comment</comment>\n    </data>\n                \n    There are any number of \"resheader\" rows that contain simple \n    name/value pairs.\n    \n    Each data row contains a name, and value. The row also contains a \n    type or mimetype. Type corresponds to a .NET class that support \n    text/value conversion through the TypeConverter architecture. \n    Classes that don't support this are serialized and stored with the \n    mimetype set.\n    \n    The mimetype is used for serialized objects, and tells the \n    ResXResourceReader how to depersist the object. This is currently not \n    extensible. For a given mimetype the value must be set accordingly:\n    \n    Note - application/x-microsoft.net.object.binary.base64 is the format \n    that the ResXResourceWriter will generate, however the reader can \n    read any of the formats listed below.\n    \n    mimetype: application/x-microsoft.net.object.binary.base64\n    value   : The object must be serialized with \n            : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter\n            : and then encoded with base64 encoding.\n    \n    mimetype: application/x-microsoft.net.object.soap.base64\n    value   : The object must be serialized with \n            : System.Runtime.Serialization.Formatters.Soap.SoapFormatter\n            : and then encoded with base64 encoding.\n    mimetype: application/x-microsoft.net.object.bytearray.base64\n    value   : The object must be serialized into a byte array \n            : using a System.ComponentModel.TypeConverter\n            : and then encoded with base64 encoding.\n    -->\n  <xsd:schema xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:msdata=\"urn:schemas-microsoft-com:xml-msdata\" id=\"root\"\n              xmlns=\"\">\n    <xsd:import namespace=\"http://www.w3.org/XML/1998/namespace\"/>\n    <xsd:element name=\"root\" msdata:IsDataSet=\"true\">\n      <xsd:complexType>\n        <xsd:choice maxOccurs=\"unbounded\">\n          <xsd:element name=\"metadata\">\n            <xsd:complexType>\n              <xsd:sequence>\n                <xsd:element name=\"value\" type=\"xsd:string\" minOccurs=\"0\"/>\n              </xsd:sequence>\n              <xsd:attribute name=\"name\" use=\"required\" type=\"xsd:string\"/>\n              <xsd:attribute name=\"type\" type=\"xsd:string\"/>\n              <xsd:attribute name=\"mimetype\" type=\"xsd:string\"/>\n              <xsd:attribute ref=\"xml:space\"/>\n            </xsd:complexType>\n          </xsd:element>\n          <xsd:element name=\"assembly\">\n            <xsd:complexType>\n              <xsd:attribute name=\"alias\" type=\"xsd:string\"/>\n              <xsd:attribute name=\"name\" type=\"xsd:string\"/>\n            </xsd:complexType>\n          </xsd:element>\n          <xsd:element name=\"data\">\n            <xsd:complexType>\n              <xsd:sequence>\n                <xsd:element name=\"value\" type=\"xsd:string\" minOccurs=\"0\" msdata:Ordinal=\"1\"/>\n                <xsd:element name=\"comment\" type=\"xsd:string\" minOccurs=\"0\" msdata:Ordinal=\"2\"/>\n              </xsd:sequence>\n              <xsd:attribute name=\"name\" type=\"xsd:string\" use=\"required\" msdata:Ordinal=\"1\"/>\n              <xsd:attribute name=\"type\" type=\"xsd:string\" msdata:Ordinal=\"3\"/>\n              <xsd:attribute name=\"mimetype\" type=\"xsd:string\" msdata:Ordinal=\"4\"/>\n              <xsd:attribute ref=\"xml:space\"/>\n            </xsd:complexType>\n          </xsd:element>\n          <xsd:element name=\"resheader\">\n            <xsd:complexType>\n              <xsd:sequence>\n                <xsd:element name=\"value\" type=\"xsd:string\" minOccurs=\"0\" msdata:Ordinal=\"1\"/>\n              </xsd:sequence>\n              <xsd:attribute name=\"name\" type=\"xsd:string\" use=\"required\"/>\n            </xsd:complexType>\n          </xsd:element>\n        </xsd:choice>\n      </xsd:complexType>\n    </xsd:element>\n  </xsd:schema>\n  <resheader name=\"resmimetype\">\n    <value>text/microsoft-resx</value>\n  </resheader>\n  <resheader name=\"version\">\n    <value>2.0</value>\n  </resheader>\n  <resheader name=\"reader\">\n    <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral,\n      PublicKeyToken=b77a5c561934e089\n    </value>\n  </resheader>\n  <resheader name=\"writer\">\n    <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral,\n      PublicKeyToken=b77a5c561934e089\n    </value>\n  </resheader>\n  <data name=\"ArgumentCanNotBeEmpty\" xml:space=\"preserve\">\n    <value>the argument can not be empty</value>\n  </data>\n  <data name=\"FileNotFound\" xml:space=\"preserve\">\n    <value>can not find the file</value>\n  </data>\n  <data name=\"IndexOutOfRange\" xml:space=\"preserve\">\n    <value>{0} out of range, max value: {1}</value>\n  </data>\n  <data name=\"InvalidExcelFile\" xml:space=\"preserve\">\n    <value>invalid excel file</value>\n  </data>\n  <data name=\"InvalidFilePath\" xml:space=\"preserve\">\n    <value>invalid file path</value>\n  </data>\n</root>"
  },
  {
    "path": "src/WeihanLi.Npoi/Settings/ExcelSetting.cs",
    "content": "﻿// Copyright (c) Weihan Li. All rights reserved.\n// Licensed under the Apache license.\n\nnamespace WeihanLi.Npoi.Settings;\n\n/// <summary>\n///     Excel Document Settings\n/// </summary>\npublic sealed class ExcelSetting\n{\n    /// <summary>\n    ///     Author\n    /// </summary>\n    public string? Author { get; set; } = \"WeihanLi\";\n\n    /// <summary>\n    ///     Company\n    /// </summary>\n    public string? Company { get; set; } = \"WeihanLi\";\n\n    /// <summary>\n    ///     Title\n    /// </summary>\n    public string? Title { get; set; } = \"WeihanLi.Npoi\";\n\n    /// <summary>\n    ///     Description\n    /// </summary>\n    public string? Description { get; set; } = \"WeihanLi.Npoi Generated\";\n\n    /// <summary>\n    ///     Subject\n    /// </summary>\n    public string? Subject { get; set; } = \"WeihanLi.Npoi\";\n\n    /// <summary>\n    ///     Category\n    /// </summary>\n    public string? Category { get; set; } = \"WeihanLi.Npoi\";\n}\n"
  },
  {
    "path": "src/WeihanLi.Npoi/Settings/FilterSetting.cs",
    "content": "﻿// Copyright (c) Weihan Li. All rights reserved.\n// Licensed under the Apache license.\n\nnamespace WeihanLi.Npoi.Settings;\n\ninternal sealed class FilterSetting\n{\n    /// <summary>\n    ///     Initializes a filter specification.\n    /// </summary>\n    /// <param name=\"firstColumn\">First column index.</param>\n    /// <param name=\"lastColumn\">Optional last column index.</param>\n    public FilterSetting(int firstColumn, int? lastColumn)\n    {\n        FirstColumn = firstColumn;\n        LastColumn = lastColumn;\n    }\n\n    /// <summary>\n    ///     Gets or sets the first column index.\n    /// </summary>\n    public int FirstColumn { get; }\n\n    /// <summary>\n    ///     Gets or sets the last column index.\n    /// </summary>\n    public int? LastColumn { get; }\n}\n"
  },
  {
    "path": "src/WeihanLi.Npoi/Settings/FreezeSetting.cs",
    "content": "﻿// Copyright (c) Weihan Li. All rights reserved.\n// Licensed under the Apache license.\n\nnamespace WeihanLi.Npoi.Settings;\n\ninternal sealed class FreezeSetting\n{\n    /// <summary>\n    ///     Initializes a freeze pane using default anchors.\n    /// </summary>\n    public FreezeSetting(int colSplit, int rowSplit) : this(colSplit, rowSplit, 0, 1)\n    {\n    }\n\n    /// <summary>\n    ///     Initializes a freeze pane with explicit anchors.\n    /// </summary>\n    /// <param name=\"colSplit\">Horizontal split position.</param>\n    /// <param name=\"rowSplit\">Vertical split position.</param>\n    /// <param name=\"leftmostColumn\">Leftmost column displayed in the right pane.</param>\n    /// <param name=\"topRow\">Top row displayed in the bottom pane.</param>\n    public FreezeSetting(int colSplit, int rowSplit, int leftmostColumn, int topRow)\n    {\n        ColSplit = colSplit;\n        RowSplit = rowSplit;\n        LeftMostColumn = leftmostColumn;\n        TopRow = topRow;\n    }\n\n    /// <summary>\n    ///     horizontal position of split\n    /// </summary>\n    public int ColSplit { get; }\n\n    /// <summary>\n    ///     Vertical position of split\n    /// </summary>\n    public int RowSplit { get; }\n\n    /// <summary>\n    ///     Top row visible in bottom pane\n    /// </summary>\n    public int LeftMostColumn { get; }\n\n    /// <summary>\n    ///     Left column visible in right pane\n    /// </summary>\n    public int TopRow { get; }\n}\n"
  },
  {
    "path": "src/WeihanLi.Npoi/Settings/SheetSetting.cs",
    "content": "﻿// Copyright (c) Weihan Li. All rights reserved.\n// Licensed under the Apache license.\n\nusing NPOI.SS.UserModel;\nusing WeihanLi.Extensions;\n\nnamespace WeihanLi.Npoi.Settings;\n\n/// <summary>\n///     Excel Sheet Settings\n/// </summary>\npublic sealed class SheetSetting\n{\n    private Func<ICell, bool> _cellFilter = _ => true;\n\n    /// <summary>\n    ///     SheetName\n    /// </summary>\n    public string SheetName\n    {\n        get;\n        set\n        {\n            if (value.IsNotNullOrWhiteSpace())\n            {\n                field = value;\n            }\n        }\n    } = \"Sheet0\";\n\n    /// <summary>\n    ///     StartRowIndex\n    /// </summary>\n    public int StartRowIndex\n    {\n        get;\n        set\n        {\n            if (value >= 0)\n            {\n                field = value;\n            }\n        }\n    } = 1;\n\n    /// <summary>\n    ///     HeaderRowIndex\n    /// </summary>\n    public int HeaderRowIndex => StartRowIndex - 1;\n\n    /// <summary>\n    ///     EndRowIndex, included\n    /// </summary>\n    public int? EndRowIndex { get; set; }\n\n    /// <summary>\n    ///    Gets or set whether to enable auto column width, disabled by default.\n    /// </summary>\n    public bool AutoColumnWidthEnabled { get; set; }\n\n    /// <summary>\n    ///    Gets or sets whether to skip column index adjustment based on header row during import.\n    ///    When false (default), column indices are automatically adjusted based on header row.\n    ///    When true, column indices are used as-is.\n    /// </summary>\n    public bool SkipHeaderRow { get; set; }\n\n    /// <summary>\n    ///     Cell Filter\n    /// </summary>\n    public Func<ICell, bool>? CellFilter\n    {\n        get => _cellFilter;\n        set => _cellFilter = value ?? (_ => true);\n    }\n\n    /// <summary>\n    ///     Row Filter\n    /// </summary>\n    public Func<IRow, bool>? RowFilter\n    {\n        get;\n        set => field = value ?? (_ => true);\n    } = _ => true;\n\n    /// <summary>\n    ///     Cell Action on export\n    /// </summary>\n    public Action<ICell>? CellAction { get; set; }\n\n    /// <summary>\n    ///     Row Action on export\n    /// </summary>\n    public Action<IRow>? RowAction { get; set; }\n\n    /// <summary>\n    ///     Sheet Action on export\n    /// </summary>\n    public Action<ISheet>? SheetAction { get; set; }\n}\n"
  },
  {
    "path": "src/WeihanLi.Npoi/TemplateHelper.cs",
    "content": "﻿// Copyright (c) Weihan Li. All rights reserved.\n// Licensed under the Apache license.\n\nusing WeihanLi.Common;\nusing WeihanLi.Extensions;\n\nnamespace WeihanLi.Npoi;\n\n/// <summary>\n/// Represents the configurable placeholders used by the templated export pipeline.\n/// </summary>\npublic sealed class TemplateOptions\n{\n    /// <summary>\n    ///     Global Param Format\n    /// </summary>\n    public string TemplateGlobalParamFormat\n    {\n        get;\n        set\n        {\n            if (value.IsNotNullOrWhiteSpace())\n            {\n                field = value;\n            }\n        }\n    } = InternalConstants.TemplateGlobalParamFormat;\n\n    /// <summary>\n    ///     Header Param Format\n    /// </summary>\n    public string TemplateHeaderParamFormat\n    {\n        get;\n        set\n        {\n            if (value.IsNotNullOrWhiteSpace())\n            {\n                field = value;\n            }\n        }\n    } = InternalConstants.TemplateHeaderParamFormat;\n\n    /// <summary>\n    ///     Data Param Format\n    /// </summary>\n    public string TemplateDataParamFormat\n    {\n        get;\n        set\n        {\n            if (value.IsNotNullOrWhiteSpace())\n            {\n                field = value;\n            }\n        }\n    } = InternalConstants.TemplateDataParamFormat;\n\n    /// <summary>\n    ///     Data Param Prefix\n    /// </summary>\n    public string TemplateDataPrefix\n    {\n        get;\n        set\n        {\n            if (value.IsNotNullOrWhiteSpace())\n            {\n                field = value;\n            }\n        }\n    } = InternalConstants.TemplateDataPrefix;\n\n    /// <summary>\n    ///     Data Begin markup\n    /// </summary>\n    public string TemplateDataBegin\n    {\n        get;\n        set\n        {\n            if (value.IsNotNullOrWhiteSpace())\n            {\n                field = value;\n            }\n        }\n    } = InternalConstants.TemplateDataBegin;\n\n    /// <summary>\n    ///     Data End markup\n    /// </summary>\n    public string TemplateDataEnd\n    {\n        get;\n        set\n        {\n            if (value.IsNotNullOrWhiteSpace())\n            {\n                field = value;\n            }\n        }\n    } = InternalConstants.TemplateDataEnd;\n}\n\n/// <summary>\n/// Provides helper APIs for configuring template-driven exports.\n/// </summary>\npublic static class TemplateHelper\n{\n    /// <summary>\n    ///     Configure TemplateOptions\n    /// </summary>\n    /// <param name=\"optionsAction\">optionsAction</param>\n    public static void ConfigureTemplateOptions(Action<TemplateOptions> optionsAction)\n    {\n        Guard.NotNull(optionsAction);\n\n        optionsAction.Invoke(NpoiTemplateHelper.s_templateOptions);\n    }\n}\n"
  },
  {
    "path": "src/WeihanLi.Npoi/WeihanLi.Npoi.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk\">\n  <Import Project=\"../../build/common.props\" />\n\n  <ItemGroup>\n    <PackageReference Include=\"NPOI\" />\n    <PackageReference Include=\"WeihanLi.Common\" />\n  </ItemGroup>\n  <ItemGroup>\n    <InternalsVisibleTo Condition=\"'$(Configuration)'=='Debug'\" Include=\"WeihanLi.Npoi.Test\" />\n  </ItemGroup>\n  <ItemGroup>\n    <Compile Update=\"Resource.Designer.cs\">\n      <DesignTime>True</DesignTime>\n      <AutoGen>True</AutoGen>\n      <DependentUpon>Resource.resx</DependentUpon>\n    </Compile>\n    <EmbeddedResource Update=\"Resource.resx\">\n      <Generator>ResXFileCodeGenerator</Generator>\n      <LastGenOutput>Resource.Designer.cs</LastGenOutput>\n    </EmbeddedResource>\n  </ItemGroup>\n</Project>\n"
  },
  {
    "path": "test/WeihanLi.Npoi.Test/CsvTest.cs",
    "content": "﻿// Copyright (c) Weihan Li. All rights reserved.\n// Licensed under the Apache license.\n\nusing System.Data;\nusing System.Text;\nusing WeihanLi.Extensions;\nusing WeihanLi.Npoi.Configurations;\nusing WeihanLi.Npoi.Test.Models;\nusing Xunit;\n\nnamespace WeihanLi.Npoi.Test;\n\npublic class CsvTest\n{\n    public CsvTest()\n    {\n        Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);\n    }\n\n    [Fact]\n    public void BasicImportExportTest()\n    {\n        var list = new List<Notice>();\n        for (var i = 0; i < 10; i++)\n        {\n            list.Add(new Notice()\n            {\n                Id = i + 1,\n                Content = $\"content_{i}\",\n                Title = $\"title_{i}\",\n                PublishedAt = DateTime.UtcNow.AddDays(-i),\n                Publisher = $\"publisher_{i}\"\n            });\n        }\n\n        list.Add(new Notice()\n        {\n            Id = 11,\n            Content = $\"content\",\n            Title = $\"title\",\n            PublishedAt = DateTime.UtcNow.AddDays(1),\n        });\n        var noticeSetting = FluentSettings.For<Notice>();\n        lock (noticeSetting)\n        {\n            var csvBytes = list.ToCsvBytes();\n            var importedList = CsvHelper.ToEntityList<Notice>(csvBytes);\n            Assert.Equal(list.Count, importedList.Count);\n            for (var i = 0; i < list.Count; i++)\n            {\n                Assert.NotNull(importedList[i]);\n                var item = importedList[i]!;\n                Assert.Equal(list[i].Id, item.Id);\n                Assert.Equal(list[i].Title ?? \"\", item.Title);\n                Assert.Equal(list[i].Content ?? \"\", item.Content);\n                Assert.Equal(list[i].Publisher ?? \"\", item.Publisher);\n                Assert.Equal(list[i].PublishedAt.ToTimeString(), item.PublishedAt.ToTimeString());\n            }\n        }\n    }\n\n    [Fact]\n    public void ImportWithNotSpecificColumnIndex()\n    {\n        IReadOnlyList<Notice> list = Enumerable.Range(0, 10).Select(i => new Notice()\n        {\n            Id = i + 1,\n            Content = $\"content_{i}\",\n            Title = $\"title_{i}\",\n            PublishedAt = DateTime.UtcNow.AddDays(-i),\n            Publisher = $\"publisher_{i}\"\n        }).ToArray();\n        //\n        var noticeSetting = FluentSettings.For<Notice>();\n        lock (noticeSetting)\n        {\n            var excelBytes = list.ToCsvBytes();\n\n            noticeSetting.Property(_ => _.Publisher)\n                .HasColumnIndex(4);\n            noticeSetting.Property(_ => _.PublishedAt)\n                .HasColumnIndex(3);\n\n            var importedList = CsvHelper.ToEntityList<Notice>(excelBytes);\n            Assert.Equal(list.Count, importedList.Count);\n            for (var i = 0; i < list.Count; i++)\n            {\n                Assert.NotNull(importedList[i]);\n                var item = importedList[i]!;\n                Assert.Equal(list[i].Id, item.Id);\n                Assert.Equal(list[i].Title ?? \"\", item.Title);\n                Assert.Equal(list[i].Content ?? \"\", item.Content);\n                Assert.Equal(list[i].Publisher ?? \"\", item.Publisher);\n                Assert.Equal(list[i].PublishedAt.ToTimeString(), item.PublishedAt.ToTimeString());\n            }\n\n            noticeSetting.Property(_ => _.Publisher)\n                .HasColumnIndex(3);\n            noticeSetting.Property(_ => _.PublishedAt)\n                .HasColumnIndex(4);\n        }\n    }\n\n    [Fact]\n    public void DataTableImportExportTest()\n    {\n        var dt = new DataTable();\n        dt.Columns.AddRange(new[] { new DataColumn(\"Name\"), new DataColumn(\"Age\"), new DataColumn(\"Desc\"), });\n        for (var i = 0; i < 10; i++)\n        {\n            var row = dt.NewRow();\n            row.ItemArray = new object[] { $\"Test_{i}\", i + 10, $\"Desc_{i}\" };\n            dt.Rows.Add(row);\n        }\n\n        //\n        var csvBytes = dt.ToCsvBytes();\n        var importedData = CsvHelper.ToDataTable(csvBytes);\n        Assert.NotNull(importedData);\n        Assert.Equal(dt.Rows.Count, importedData.Rows.Count);\n        for (var i = 0; i < dt.Rows.Count; i++)\n        {\n            Assert.Equal(dt.Rows[i].ItemArray.Length, importedData.Rows[i].ItemArray.Length);\n            for (var j = 0; j < dt.Rows[i].ItemArray.Length; j++)\n            {\n                Assert.Equal(dt.Rows[i].ItemArray[j], importedData.Rows[i].ItemArray[j]);\n            }\n        }\n    }\n\n    [Theory]\n    [InlineData(@\"TestData/EmptyColumns/emptyColumns.csv\")]\n    public void DataTableWithFirstLineEmpty(string testDataFilePath)\n    {\n        var bytes = File.ReadAllBytes(testDataFilePath);\n        var importedData = CsvHelper.ToDataTable(bytes);\n        var dt = new DataTable();\n        dt.Columns.AddRange(new[]\n        {\n            new DataColumn(\"A\"), new DataColumn(\"B\"), new DataColumn(\"C\"), new DataColumn(\"D\"),\n        });\n\n        var row = dt.NewRow();\n        row.ItemArray = new object[] { \"\", \"\", \"3\", \"4\" };\n        dt.Rows.Add(row);\n\n        row = dt.NewRow();\n        row.ItemArray = new object[] { \"\", \"2\", \"3\", \"\" };\n        dt.Rows.Add(row);\n\n        row = dt.NewRow();\n        row.ItemArray = new object[] { \"1\", \"2\", \"\", \"\" };\n        dt.Rows.Add(row);\n\n        row = dt.NewRow();\n        row.ItemArray = new object[] { \"1\", \"2\", \"3\", \"4\" };\n        dt.Rows.Add(row);\n\n        Assert.NotNull(importedData);\n\n        Assert.Equal(4, importedData.Rows.Count);\n\n        for (var rowIndex = 0; rowIndex < dt.Rows.Count; rowIndex++)\n        {\n            for (var colIndex = 0; colIndex < dt.Rows[rowIndex].ItemArray.Length; colIndex++)\n            {\n                var expectedValue = dt.Rows[rowIndex].ItemArray[colIndex]?.ToString();\n                var excelValue = importedData.Rows[rowIndex][colIndex].ToString();\n                Assert.Equal(expectedValue, excelValue);\n            }\n        }\n    }\n\n    [Theory]\n    [InlineData(@\"TestData/NonStringColumns/nonStringColumns.csv\")]\n    public void DataTableImportExportTestWithNonStringColumns(string testDataFilePath)\n    {\n        // Act\n        var importedData = CsvHelper.ToDataTable(testDataFilePath);\n\n        // Assert\n        var dt = new DataTable();\n        dt.Columns.AddRange(new[]\n        {\n            new DataColumn(\"A\"), new DataColumn(\"1000\"), new DataColumn(\"TRUE\"), new DataColumn(\"15/08/2021\")\n        });\n\n        var row = dt.NewRow();\n        row.ItemArray = new object[] { \"1\", \"2\", \"3\", \"4\" };\n        dt.Rows.Add(row);\n\n        Assert.NotNull(importedData);\n\n        Assert.Equal(1, importedData.Rows.Count);\n\n        // Check columns\n        for (var headerIndex = 0; headerIndex < dt.Columns.Count; headerIndex++)\n        {\n            var expectedValue = dt.Columns[headerIndex].ToString();\n            var excelValue = importedData.Columns[headerIndex].ToString();\n            Assert.Equal(expectedValue, excelValue);\n        }\n\n        // Check rows\n        for (var rowIndex = 0; rowIndex < dt.Rows.Count; rowIndex++)\n        {\n            for (var colIndex = 0; colIndex < dt.Rows[rowIndex].ItemArray.Length; colIndex++)\n            {\n                var expectedValue = dt.Rows[rowIndex].ItemArray[colIndex]?.ToString();\n                var excelValue = importedData.Rows[rowIndex][colIndex].ToString();\n                Assert.Equal(expectedValue, excelValue);\n            }\n        }\n    }\n\n    [Theory]\n    [InlineData(\"\\\"XXXXX\\\"\")]\n    [InlineData(\"XXX\")]\n    [InlineData(\"\\\"X,XXX\\\"\")]\n    [InlineData(\"XX\\\"X\")]\n    [InlineData(\"XX\\\"\\\"X\")]\n    [InlineData(\"\\\"dd\\\"\\\"d,1\\\"\")]\n    [InlineData(\"ddd\\nccc\")]\n    [InlineData(\"ddd\\r\\nccc\")]\n    [InlineData(@\"bbb\n        ccc\")]\n    [InlineData(\"\")]\n    public void ParseCsvLineTest(string str)\n    {\n        var data = new object[] { 1, \"tom\", 33, str };\n        var lineData = string.Join(CsvHelper.CsvSeparatorCharacter, data);\n        var cols = CsvHelper.ParseLine(lineData);\n        Assert.Equal(data.Length, cols.Count);\n\n        for (var i = 0; i < cols.Count; i++)\n        {\n            Assert.Equal(TrimQuotes(data[i].ToString()), cols[i]);\n        }\n    }\n\n    [Fact]\n    public void GetCsvTextTest_BasicType()\n    {\n        var text = Enumerable.Range(1, 5)\n            .GetCsvText(false);\n\n        var expected = Enumerable.Range(1, 5)\n            .StringJoin(Environment.NewLine);\n        Assert.Equal(expected, text);\n    }\n\n    [Theory]\n    [InlineData(true)]\n    [InlineData(false)]\n    public void GetCsvLines_BasicType(bool includeHeader)\n    {\n        var option = new CsvOptions() { IncludeHeader = includeHeader };\n        var list = new List<int>() { 1, 2, 3 };\n        var lines = list.GetCsvLines(option).ToArray();\n        Assert.Equal(includeHeader ? list.Count + 1 : list.Count, lines.Length);\n        var importedList = CsvHelper.GetEntityList<int>(lines, option);\n        Assert.Equal(list.Count, importedList.Count);\n\n        for (var i = 0; i < list.Count; i++)\n        {\n            Assert.Equal(list[i], importedList[i]);\n        }\n    }\n\n    [Theory]\n    [InlineData(\"test.csv\")]\n    [InlineData(\"/tmp/test.csv\")]\n    public async Task CsvFileTest(string csvPath)\n    {\n        var list = new List<Job>() { new Job() { Id = 1, Name = \"123\" }, new Job() { Id = 2, Name = \"234\" } };\n        Assert.True(list.ToCsvFile(csvPath));\n\n        var importedList = CsvHelper.ToEntityList<Job>(csvPath);\n        Assert.Equal(list.Count, importedList.Count);\n        for (var i = 0; i < list.Count; i++)\n        {\n            Assert.Equal(list[i], importedList[i]);\n        }\n\n        File.Delete(csvPath);\n\n        await Task.CompletedTask;\n    }\n\n    [Theory]\n    [InlineData(true)]\n    [InlineData(false)]\n    public void GetCsvLines_Entity(bool includeHeader)\n    {\n        var option = new CsvOptions() { IncludeHeader = includeHeader };\n\n        var list = new List<Job>() { new() { Id = 1, Name = \"123\" }, new() { Id = 2, Name = \"234\" } };\n        var lines = list.GetCsvLines(option).ToArray();\n        Assert.Equal(includeHeader ? list.Count + 1 : list.Count, lines.Length);\n        var importedList = CsvHelper.GetEntityList<Job>(lines, option);\n        Assert.Equal(list.Count, importedList.Count);\n        for (var i = 0; i < list.Count; i++)\n        {\n            Assert.Equal(list[i], importedList[i]);\n        }\n    }\n\n    [Fact]\n    public void GetCsvTextTest_Entity()\n    {\n        var list = Enumerable.Range(1, 5)\n            .Select(i => new Job() { Id = i + 1, Name = \"test\" }).ToArray();\n        var csvText = list.GetCsvText();\n        var bytes = csvText.GetBytes();\n\n        var importedList = CsvHelper.ToEntityList<Job>(bytes);\n        Assert.Equal(list.Length, importedList.Count);\n        for (var i = 0; i < list.Length; i++)\n        {\n            Assert.True(list[i] == importedList[i]);\n        }\n    }\n\n    [Fact]\n    public void CsvStringListTest()\n    {\n        var arr = Enumerable.Range(1, 10)\n            .Select(x => $\"str_{x}\")\n            .ToArray();\n\n        var csvBytes = arr.ToCsvBytes();\n        Assert.NotNull(csvBytes);\n        var list = CsvHelper.ToEntityList<string>(csvBytes);\n        Assert.Equal(arr.Length, list.Count);\n        Assert.True(arr.SequenceEqual(list));\n    }\n\n    [Fact]\n    public void DuplicateColumnTest()\n    {\n        var csvText = $@\"A,B,C,A,B,C{Environment.NewLine}1,2,3,4,5,6\";\n        var dataTable = CsvHelper.ToDataTable(csvText.GetBytes());\n        Assert.Equal(6, dataTable.Columns.Count);\n        Assert.Equal(1, dataTable.Rows.Count);\n\n        var newCsvText = dataTable.GetCsvText();\n        Assert.StartsWith(\"A,B,C,A,B,C\", newCsvText);\n        var newDataTable = CsvHelper.ToDataTable(newCsvText.GetBytes());\n\n        Assert.Equal(dataTable.Columns.Count, newDataTable.Columns.Count);\n        Assert.Equal(dataTable.Rows.Count, newDataTable.Rows.Count);\n    }\n\n    [Fact]\n    public void CsvOptionTest_CustomSeparatorCharacter()\n    {\n        var list = new Notice[]\n        {\n            new()\n            {\n                Id = 1,\n                Content = \"test\",\n                Title = \"test\",\n                Publisher = \"test\",\n                PublishedAt = DateTime.Now\n            }\n        };\n        var text = list.GetCsvText();\n        var text2 = list.GetCsvText(new CsvOptions() { SeparatorCharacter = '\\t' });\n        Assert.NotEqual(text, text2);\n        Assert.Equal(text, text2.Replace('\\t', ','));\n    }\n\n    [Fact]\n    public void CsvToListEncodingTest()\n    {\n        var list = new List<TestModel>() { new() { Age = 1, Name = \"中华小当家\" } };\n        var encoding = Encoding.GetEncoding(\"gb2312\");\n        var bytes = list.ToCsvBytes(new CsvOptions() { Encoding = encoding });\n        var importedList = CsvHelper.ToEntityList<TestModel>(bytes, new CsvOptions() { Encoding = encoding });\n        Assert.Equal(list.Count, importedList.Count);\n        for (var i = 0; i < list.Count; i++)\n        {\n            Assert.Equal(list[i], importedList[i]);\n        }\n    }\n\n    [Fact]\n    public void CsvToDataTableEncodingTest()\n    {\n        var list = new List<TestModel>() { new() { Age = 1, Name = \"中华小当家\" } };\n        var encoding = Encoding.GetEncoding(\"gb2312\");\n        var bytes = list.ToCsvBytes(new CsvOptions() { Encoding = encoding });\n        var dataTable = CsvHelper.ToDataTable(bytes, new CsvOptions() { Encoding = encoding });\n        Assert.Equal(list.Count, dataTable.Rows.Count);\n        for (var i = 0; i < list.Count; i++)\n        {\n            Assert.Equal(list[i].Name, dataTable.Rows[i][\"Name\"]);\n        }\n    }\n\n    [Fact]\n    public void CsvToListEncodingTest_NotTheSameEncoding()\n    {\n        var list = new List<TestModel>() { new() { Age = 1, Name = \"中华小当家\" } };\n        var encoding = Encoding.GetEncoding(\"gb2312\");\n        var bytes = list.ToCsvBytes(new CsvOptions() { Encoding = encoding });\n        var importedList = CsvHelper.ToEntityList<TestModel>(bytes);\n        Assert.Equal(list.Count, importedList.Count);\n        for (var i = 0; i < list.Count; i++)\n        {\n            Assert.NotEqual(list[i], importedList[i]);\n        }\n    }\n\n    private static string TrimQuotes(string? str)\n    {\n        if (string.IsNullOrEmpty(str))\n        {\n            return string.Empty;\n        }\n        //\n\n        if (str[0] == CsvHelper.CsvQuoteCharacter)\n        {\n            return str.Substring(1, str.Length - 2).Replace(\"\\\"\\\"\", \"\\\"\");\n        }\n\n        return str;\n    }\n\n    private sealed record TestModel\n    {\n        public string Name { get; set; } = string.Empty;\n        public int Age { get; set; }\n    }\n}\n"
  },
  {
    "path": "test/WeihanLi.Npoi.Test/ExcelFormatData.cs",
    "content": "﻿// Copyright (c) Weihan Li. All rights reserved.\n// Licensed under the Apache license.\n\nusing Xunit;\n\nnamespace WeihanLi.Npoi.Test;\n\npublic sealed class ExcelFormatData : TheoryData<ExcelFormat>\n{\n    public ExcelFormatData()\n    {\n        Add(ExcelFormat.Xls);\n        Add(ExcelFormat.Xlsx);\n    }\n}\n"
  },
  {
    "path": "test/WeihanLi.Npoi.Test/ExcelTest.cs",
    "content": "﻿// Copyright (c) Weihan Li. All rights reserved.\n// Licensed under the Apache license.\n\nusing NPOI.HSSF.UserModel;\nusing NPOI.SS.UserModel;\nusing System.Data;\nusing System.Globalization;\nusing System.Reflection;\nusing System.Text.RegularExpressions;\nusing WeihanLi.Common;\nusing WeihanLi.Common.Helpers;\nusing WeihanLi.Common.Models;\nusing WeihanLi.Common.Services;\nusing WeihanLi.Extensions;\nusing WeihanLi.Npoi.Attributes;\nusing WeihanLi.Npoi.Configurations;\nusing WeihanLi.Npoi.Test.Models;\nusing Xunit;\n\nnamespace WeihanLi.Npoi.Test;\n\npublic class ExcelTest\n{\n    [Theory]\n    [ClassData(typeof(ExcelFormatData))]\n    public void BasicImportExportTest(ExcelFormat excelFormat)\n    {\n        var list = new List<Notice?>();\n        for (var i = 0; i < 10; i++)\n        {\n            list.Add(new Notice()\n            {\n                Id = i + 1,\n                Content = $\"content_{i}\",\n                Title = $\"title_{i}\",\n                PublishedAt = DateTime.UtcNow.AddDays(-i),\n                Publisher = $\"publisher_{i}\"\n            });\n        }\n        list.Add(new Notice() { Title = \"nnnn\" });\n        list.Add(null);\n        var noticeSetting = FluentSettings.For<Notice>();\n        lock (noticeSetting)\n        {\n            var excelBytes = list.ToExcelBytes(excelFormat);\n\n            var importedList = ExcelHelper.ToEntityList<Notice>(excelBytes, excelFormat);\n            Assert.Equal(list.Count, importedList.Count);\n            for (var i = 0; i < list.Count; i++)\n            {\n                if (list[i] is null)\n                {\n                    Assert.Null(importedList[i]);\n                }\n                else\n                {\n                    Assert.NotNull(importedList[i]);\n                    var sourceItem = list[i]!;\n                    var item = importedList[i]!;\n                    Assert.Equal(sourceItem.Id, item.Id);\n                    Assert.Equal(sourceItem.Title, item.Title);\n                    Assert.Equal(sourceItem.Content, item.Content);\n                    Assert.Equal(sourceItem.Publisher, item.Publisher);\n                    Assert.Equal(sourceItem.PublishedAt.ToTimeString(), item.PublishedAt.ToTimeString());\n                }\n            }\n        }\n    }\n\n    [Theory]\n    [ClassData(typeof(ExcelFormatData))]\n    public void BasicImportExportTestWithEmptyValue(ExcelFormat excelFormat)\n    {\n        var list = new List<Notice?>();\n        for (var i = 0; i < 10; i++)\n        {\n            list.Add(new Notice()\n            {\n                Id = i + 1,\n                Content = i < 3 ? $\"content_{i}\" : string.Empty,\n                Title = $\"title_{i}\",\n                PublishedAt = DateTime.UtcNow.AddDays(-i),\n                Publisher = i < 3 ? $\"publisher_{i}\" : null\n            });\n        }\n        list.Add(new Notice() { Title = \"nnnn\" });\n        list.Add(null);\n        var noticeSetting = FluentSettings.For<Notice>();\n        lock (noticeSetting)\n        {\n            var excelBytes = list.ToExcelBytes(excelFormat);\n\n            var importedList = ExcelHelper.ToEntityList<Notice>(excelBytes, excelFormat);\n            Assert.Equal(list.Count, importedList.Count);\n            for (var i = 0; i < list.Count; i++)\n            {\n                if (list[i] is null)\n                {\n                    Assert.Null(importedList[i]);\n                }\n                else\n                {\n                    Assert.NotNull(importedList[i]);\n                    var sourceItem = list[i]!;\n                    var item = importedList[i]!;\n                    Assert.Equal(sourceItem.Id, item.Id);\n                    Assert.Equal(sourceItem.Title, item.Title);\n                    Assert.Equal(sourceItem.Content, item.Content);\n                    Assert.Equal(sourceItem.Publisher, item.Publisher);\n                    Assert.Equal(sourceItem.PublishedAt.ToTimeString(), item.PublishedAt.ToTimeString());\n                }\n            }\n        }\n    }\n\n    [Theory]\n    [ClassData(typeof(ExcelFormatData))]\n    public void BasicImportExportWithoutHeaderTest(ExcelFormat excelFormat)\n    {\n        var list = new List<Notice?>();\n        for (var i = 0; i < 10; i++)\n        {\n            list.Add(new Notice()\n            {\n                Id = i + 1,\n                Content = $\"content_{i}\",\n                Title = $\"title_{i}\",\n                PublishedAt = DateTime.UtcNow.AddDays(-i),\n                Publisher = $\"publisher_{i}\"\n            });\n        }\n        list.Add(new Notice() { Title = \"nnnn\" });\n        list.Add(null);\n\n        var noticeSetting = FluentSettings.For<Notice>();\n        lock (noticeSetting)\n        {\n            noticeSetting.HasSheetConfiguration(0, \"test\", 0);\n\n            var excelBytes = list.ToExcelBytes(excelFormat);\n\n            var importedList = ExcelHelper.ToEntityList<Notice>(excelBytes, excelFormat);\n            Assert.Equal(list.Count, importedList.Count);\n            for (var i = 0; i < list.Count; i++)\n            {\n                if (list[i] is null)\n                {\n                    Assert.Null(importedList[i]);\n                }\n                else\n                {\n                    Assert.NotNull(importedList[i]);\n                    var sourceItem = list[i]!;\n                    var item = importedList[i]!;\n                    Assert.Equal(sourceItem.Id, item.Id);\n                    Assert.Equal(sourceItem.Title, item.Title);\n                    Assert.Equal(sourceItem.Content, item.Content);\n                    Assert.Equal(sourceItem.Publisher, item.Publisher);\n                    Assert.Equal(sourceItem.PublishedAt.ToTimeString(), item.PublishedAt.ToTimeString());\n                }\n            }\n\n            noticeSetting.HasSheetConfiguration(0, \"test\", 1);\n        }\n    }\n\n    [Theory]\n    [ClassData(typeof(ExcelFormatData))]\n    public void ImportWithNotSpecificColumnIndex(ExcelFormat excelFormat)\n    {\n        IReadOnlyList<Notice> list = Enumerable.Range(0, 10).Select(i => new Notice()\n        {\n            Id = i + 1,\n            Content = $\"content_{i}\",\n            Title = $\"title_{i}\",\n            PublishedAt = DateTime.UtcNow.AddDays(-i),\n            Publisher = $\"publisher_{i}\"\n        }).ToArray();\n        //\n        var noticeSetting = FluentSettings.For<Notice>();\n        lock (noticeSetting)\n        {\n            var excelBytes = list.ToExcelBytes(excelFormat);\n\n            noticeSetting.Property(_ => _.Publisher)\n                .HasColumnIndex(4);\n            noticeSetting.Property(_ => _.PublishedAt)\n                .HasColumnIndex(3);\n\n            var importedList = ExcelHelper.ToEntityList<Notice>(excelBytes, excelFormat);\n            Assert.Equal(list.Count, importedList.Count);\n            for (var i = 0; i < list.Count; i++)\n            {\n                if (importedList[i] is null)\n                {\n                    Assert.Null(list[i]);\n                }\n                else\n                {\n                    var item = importedList[i];\n                    Assert.NotNull(item);\n                    Guard.NotNull(item);\n                    var sourceItem = list[i];\n                    Assert.Equal(sourceItem.Id, item.Id);\n                    Assert.Equal(sourceItem.Title, item.Title);\n                    Assert.Equal(sourceItem.Content, item.Content);\n                    Assert.Equal(sourceItem.Publisher, item.Publisher);\n                    Assert.Equal(sourceItem.PublishedAt.ToTimeString(), item.PublishedAt.ToTimeString());\n                }\n            }\n\n            noticeSetting.Property(_ => _.Publisher)\n                .HasColumnIndex(3);\n            noticeSetting.Property(_ => _.PublishedAt)\n                .HasColumnIndex(4);\n        }\n    }\n\n    [Theory]\n    [ClassData(typeof(ExcelFormatData))]\n    public void ShadowPropertyTest(ExcelFormat excelFormat)\n    {\n        IReadOnlyList<Notice> list = Enumerable.Range(0, 10).Select(i => new Notice()\n        {\n            Id = i + 1,\n            Content = $\"content_{i}\",\n            Title = $\"title_{i}\",\n            PublishedAt = DateTime.UtcNow.AddDays(-i),\n            Publisher = $\"publisher_{i}\"\n        }).ToArray();\n\n        var noticeSetting = FluentSettings.For<Notice>();\n        lock (noticeSetting)\n        {\n            noticeSetting.Property<string>(\"ShadowProperty\")\n                .HasOutputFormatter((x, _) => $\"{x?.Id}...\")\n                ;\n\n            var excelBytes = list.ToExcelBytes(excelFormat);\n            // list.ToExcelFile($\"{Directory.GetCurrentDirectory()}/output.xlsx\");\n\n            var importedList = ExcelHelper.ToEntityList<Notice>(excelBytes, excelFormat);\n            Assert.Equal(list.Count, importedList.Count);\n            for (var i = 0; i < list.Count; i++)\n            {\n                var item = importedList[i];\n                Assert.NotNull(item);\n                Guard.NotNull(item);\n                var sourceItem = list[i];\n                Assert.Equal(sourceItem.Id, item.Id);\n                Assert.Equal(sourceItem.Title, item.Title);\n                Assert.Equal(sourceItem.Content, item.Content);\n                Assert.Equal(sourceItem.Publisher, item.Publisher);\n                Assert.Equal(sourceItem.PublishedAt.ToTimeString(), item.PublishedAt.ToTimeString());\n            }\n        }\n    }\n\n    [Theory]\n    [ClassData(typeof(ExcelFormatData))]\n    public void IgnoreInheritPropertyTest(ExcelFormat excelFormat)\n    {\n        IReadOnlyList<Notice> list = Enumerable.Range(0, 10).Select(i => new Notice()\n        {\n            Id = i + 1,\n            Content = $\"content_{i}\",\n            Title = $\"title_{i}\",\n            PublishedAt = DateTime.UtcNow.AddDays(-i),\n            Publisher = $\"publisher_{i}\"\n        }).ToArray();\n\n        var settings = FluentSettings.For<Notice>();\n        lock (settings)\n        {\n            settings.Property(x => x.Id).Ignored();\n\n            var excelBytes = list.ToExcelBytes(excelFormat);\n            // list.ToExcelFile($\"{Directory.GetCurrentDirectory()}/ttt.xls\");\n            var importedList = ExcelHelper.ToEntityList<Notice>(excelBytes, excelFormat);\n            Assert.Equal(list.Count, importedList.Count);\n            for (var i = 0; i < list.Count; i++)\n            {\n                if (importedList[i] is null)\n                {\n                    Assert.Null(list[i]);\n                }\n                else\n                {\n                    var item = importedList[i];\n                    Assert.NotNull(item);\n                    Guard.NotNull(item);\n                    var sourceItem = list[i];\n                    //Assert.Equal(sourceItem.Id, item.Id);\n                    Assert.Equal(sourceItem.Title, item.Title);\n                    Assert.Equal(sourceItem.Content, item.Content);\n                    Assert.Equal(sourceItem.Publisher, item.Publisher);\n                    Assert.Equal(sourceItem.PublishedAt.ToTimeString(), item.PublishedAt.ToTimeString());\n                }\n            }\n\n            settings.Property(_ => _.Id)\n                .Ignored(false)\n                .HasColumnIndex(0);\n        }\n    }\n\n    [Theory]\n    [ClassData(typeof(ExcelFormatData))]\n    public void ColumnInputFormatterTest(ExcelFormat excelFormat)\n    {\n        IReadOnlyList<Notice> list = Enumerable.Range(0, 10).Select(i => new Notice()\n        {\n            Id = i + 1,\n            Content = $\"content_{i}\",\n            Title = $\"title_{i}\",\n            PublishedAt = DateTime.UtcNow.AddDays(-i),\n            Publisher = $\"publisher_{i}\"\n        }).ToArray();\n\n        var excelBytes = list.ToExcelBytes(excelFormat);\n\n        var settings = FluentSettings.For<Notice>();\n        lock (settings)\n        {\n            settings.Property(x => x.Title).HasColumnInputFormatter(x => $\"{x}_Test\");\n\n            var importedList = ExcelHelper.ToEntityList<Notice>(excelBytes, excelFormat);\n            Assert.Equal(list.Count, importedList.Count);\n            for (var i = 0; i < list.Count; i++)\n            {\n                var item = importedList[i];\n                Assert.NotNull(item);\n                Guard.NotNull(item);\n\n                Assert.Equal(list[i].Id, item.Id);\n                Assert.Equal(list[i].Title + \"_Test\", item.Title);\n                Assert.Equal(list[i].Content, item.Content);\n                Assert.Equal(list[i].Publisher, item.Publisher);\n                Assert.Equal(list[i].PublishedAt.ToTimeString(), item.PublishedAt.ToTimeString());\n            }\n\n            settings.Property(_ => _.Title).HasColumnInputFormatter(null);\n        }\n    }\n\n    [Theory]\n    [ClassData(typeof(ExcelFormatData))]\n    public void InputOutputColumnFormatterTest(ExcelFormat excelFormat)\n    {\n        IReadOnlyList<Notice> list = Enumerable.Range(0, 10).Select(i => new Notice()\n        {\n            Id = i + 1,\n            Content = $\"content_{i}\",\n            Title = $\"title_{i}\",\n            PublishedAt = DateTime.UtcNow.AddDays(-i),\n            Publisher = $\"publisher_{i}\"\n        }).ToArray();\n\n        var settings = FluentSettings.For<Notice>();\n        lock (settings)\n        {\n            settings.Property(x => x.Id)\n                .HasColumnOutputFormatter(x => $\"{x}_Test\")\n                .HasColumnInputFormatter(x => Convert.ToInt32(x?.Split(new[] { '_' }, StringSplitOptions.RemoveEmptyEntries)[0]));\n            var excelBytes = list.ToExcelBytes(excelFormat);\n\n            var importedList = ExcelHelper.ToEntityList<Notice>(excelBytes, excelFormat);\n            Assert.Equal(list.Count, importedList.Count);\n            for (var i = 0; i < list.Count; i++)\n            {\n                var item = importedList[i];\n                Assert.NotNull(item);\n                Guard.NotNull(item);\n                var sourceItem = list[i];\n                Assert.Equal(sourceItem.Id, item.Id);\n                Assert.Equal(sourceItem.Title, item.Title);\n                Assert.Equal(sourceItem.Content, item.Content);\n                Assert.Equal(sourceItem.Publisher, item.Publisher);\n                Assert.Equal(sourceItem.PublishedAt.ToTimeString(), item.PublishedAt.ToTimeString());\n            }\n\n            settings.Property(x => x.Id)\n                .HasColumnOutputFormatter(null)\n                .HasColumnInputFormatter(null);\n        }\n    }\n\n    [Theory]\n    [ClassData(typeof(ExcelFormatData))]\n    public void DataValidationTest(ExcelFormat excelFormat)\n    {\n        IReadOnlyList<Notice> list = Enumerable.Range(0, 10).Select(i => new Notice()\n        {\n            Id = i + 1,\n            Content = $\"content_{i}\",\n            Title = $\"title_{i}\",\n            PublishedAt = DateTime.UtcNow.AddDays(-i),\n            Publisher = $\"publisher_{i}\"\n        }).ToArray();\n        var excelBytes = list.ToExcelBytes(excelFormat);\n\n        var settings = FluentSettings.For<Notice>();\n        lock (settings)\n        {\n            settings.WithDataFilter(x => x?.Id > 5);\n\n            var importedList = ExcelHelper.ToEntityList<Notice>(excelBytes, excelFormat);\n            Assert.Equal(list.Count(x => x.Id > 5), importedList.Count);\n\n            int i = 0, k = 0;\n            while (list[k].Id != importedList[i]?.Id)\n            {\n                k++;\n            }\n\n            for (; i < importedList.Count; i++, k++)\n            {\n                var item = importedList[i];\n                Assert.NotNull(item);\n                Guard.NotNull(item);\n                var sourceItem = list[k];\n                Assert.Equal(sourceItem.Id, item.Id);\n                Assert.Equal(sourceItem.Title, item.Title);\n                Assert.Equal(sourceItem.Content, item.Content);\n                Assert.Equal(sourceItem.Publisher, item.Publisher);\n                Assert.Equal(sourceItem.PublishedAt.ToTimeString(), item.PublishedAt.ToTimeString());\n\n            }\n\n            settings.WithDataFilter(null);\n        }\n    }\n\n    [Theory]\n    [ClassData(typeof(ExcelFormatData))]\n    public void DataTableImportExportTest(ExcelFormat excelFormat)\n    {\n        var dt = new DataTable();\n        dt.Columns.AddRange(new[]\n        {\n                new DataColumn(\"Name\"),\n                new DataColumn(\"Age\"),\n                new DataColumn(\"Desc\"),\n            });\n        for (var i = 0; i < 10; i++)\n        {\n            var row = dt.NewRow();\n            row.ItemArray = new object[] { $\"Test_{i}\", i + 10, $\"Desc_{i}\" };\n            dt.Rows.Add(row);\n        }\n        //\n        var excelBytes = dt.ToExcelBytes(excelFormat);\n        var importedData = ExcelHelper.ToDataTable(excelBytes, excelFormat);\n        Assert.NotNull(importedData);\n        Assert.Equal(dt.Rows.Count, importedData.Rows.Count);\n        for (var i = 0; i < dt.Rows.Count; i++)\n        {\n            Assert.Equal(dt.Rows[i].ItemArray.Length, importedData.Rows[i].ItemArray.Length);\n            for (var j = 0; j < dt.Rows[i].ItemArray.Length; j++)\n            {\n                Assert.Equal(dt.Rows[i].ItemArray[j], importedData.Rows[i].ItemArray[j]);\n            }\n        }\n    }\n\n    [Theory]\n    [ClassData(typeof(ExcelFormatData))]\n    public void DataTableImportExportWithEmptyValueTest(ExcelFormat excelFormat)\n    {\n        var dt = new DataTable();\n        dt.Columns.AddRange(new[]\n        {\n                new DataColumn(\"Name\"),\n                new DataColumn(\"Age\"),\n                new DataColumn(\"Desc\"),\n            });\n        for (var i = 0; i < 10; i++)\n        {\n            var row = dt.NewRow();\n            row.ItemArray = new object?[]\n            {\n                    i < 4 ? $\"Test_{i}\" : null,\n                    i + 10,\n                    i % 2 == 0 ? $\"Desc_{i}\" : string.Empty\n            };\n            dt.Rows.Add(row);\n        }\n        //\n        var excelBytes = dt.ToExcelBytes(excelFormat);\n        var importedData = ExcelHelper.ToDataTable(excelBytes, excelFormat);\n        Assert.NotNull(importedData);\n        Assert.Equal(dt.Rows.Count, importedData.Rows.Count);\n        Assert.Equal(dt.Columns.Count, importedData.Columns.Count);\n        for (var i = 0; i < dt.Rows.Count; i++)\n        {\n            Assert.Equal(dt.Rows[i].ItemArray.Length, importedData.Rows[i].ItemArray.Length);\n            for (var j = 0; j < dt.Columns.Count; j++)\n            {\n                Assert.Equal(dt.Rows[i].ItemArray[j], importedData.Rows[i].ItemArray[j]);\n            }\n        }\n    }\n\n    [Theory]\n    [ClassData(typeof(ExcelFormatData))]\n    public void ExcelImportWithFormula(ExcelFormat excelFormat)\n    {\n        var setting = FluentSettings.For<ExcelFormulaTestModel>();\n        setting.HasSheetConfiguration(0, \"Test\", 0);\n        setting.Property(x => x.Num1).HasColumnIndex(0);\n        setting.Property(x => x.Num2).HasColumnIndex(1);\n        setting.Property(x => x.Sum).HasColumnIndex(2);\n\n        var workbook = ExcelHelper.PrepareWorkbook(excelFormat);\n        var sheet = workbook.CreateSheet();\n        var row = sheet.CreateRow(0);\n        row.CreateCell(0, CellType.Numeric).SetCellValue(1);\n        row.CreateCell(1, CellType.Numeric).SetCellValue(2);\n        row.CreateCell(2, CellType.Formula).SetCellFormula(\"$A1+$B1\");\n        var excelBytes = workbook.ToExcelBytes();\n        var list = ExcelHelper.ToEntityList<ExcelFormulaTestModel>(excelBytes, excelFormat);\n        Assert.NotNull(list);\n        Assert.NotEmpty(list);\n        Assert.NotNull(list[0]);\n        Assert.Equal(1, list[0]!.Num1);\n        Assert.Equal(2, list[0]!.Num2);\n        Assert.Equal(3, list[0]!.Sum);\n    }\n\n    // ReSharper disable UnusedAutoPropertyAccessor.Local\n    private class ExcelFormulaTestModel\n    {\n        public int Num1 { get; set; }\n        public int Num2 { get; set; }\n\n        public int Sum { get; set; }\n    }\n\n    [Theory]\n    [ClassData(typeof(ExcelFormatData))]\n    public void ExcelImportWithCellFilter(ExcelFormat excelFormat)\n    {\n        IReadOnlyList<Notice> list = Enumerable.Range(0, 10).Select(i => new Notice()\n        {\n            Id = i + 1,\n            Content = $\"content_{i}\",\n            Title = $\"title_{i}\",\n            PublishedAt = DateTime.UtcNow.AddDays(-i),\n            Publisher = $\"publisher_{i}\"\n        }).ToArray();\n        var excelBytes = list.ToExcelBytes(excelFormat);\n\n        var settings = FluentSettings.For<Notice>();\n        lock (settings)\n        {\n            settings.HasSheetSetting(setting =>\n            {\n                setting.CellFilter = cell => cell.ColumnIndex == 0;\n            });\n\n            var importedList = ExcelHelper.ToEntityList<Notice>(excelBytes, excelFormat);\n            Assert.Equal(list.Count, importedList.Count);\n            for (var i = 0; i < list.Count; i++)\n            {\n                var item = importedList[i];\n                Assert.NotNull(item);\n                Guard.NotNull(item);\n\n                Assert.Equal(list[i].Id, item.Id);\n                Assert.Null(item.Title);\n                Assert.Null(item.Content);\n                Assert.Null(item.Publisher);\n                Assert.Equal(default(DateTime).ToTimeString(), item.PublishedAt.ToTimeString());\n            }\n\n            settings.HasSheetSetting(setting =>\n            {\n                setting.CellFilter = _ => true;\n            });\n        }\n    }\n\n    [Theory]\n    [ClassData(typeof(ExcelFormatData))]\n    public void ExcelImportWithCellFilterAttributeTest(ExcelFormat excelFormat)\n    {\n        IReadOnlyList<CellFilterAttributeTest> list = Enumerable.Range(0, 10).Select(i => new CellFilterAttributeTest()\n        {\n            Id = i + 1,\n            Description = $\"content_{i}\",\n            Name = $\"title_{i}\",\n        }).ToArray();\n        var excelBytes = list.ToExcelBytes(excelFormat);\n        var importedList = ExcelHelper.ToEntityList<CellFilterAttributeTest>(excelBytes, excelFormat);\n        Assert.NotNull(importedList);\n        Assert.Equal(list.Count, importedList.Count);\n        for (var i = 0; i < importedList.Count; i++)\n        {\n            Assert.NotNull(importedList[i]);\n            var item = importedList[i]!;\n            Assert.Equal(list[i].Id, item.Id);\n            Assert.Equal(list[i].Name, item.Name);\n            Assert.Null(item.Description);\n        }\n    }\n\n    [Sheet(SheetName = \"test\", AutoColumnWidthEnabled = true, StartColumnIndex = 0, EndColumnIndex = 1)]\n    private class CellFilterAttributeTest\n    {\n        [Column(Index = 0)]\n        public int Id { get; set; }\n\n        [Column(Index = 1)]\n        public string? Name { get; set; }\n\n        [Column(Index = 2)]\n        public string? Description { get; set; }\n    }\n\n    [Theory]\n    [InlineData(ExcelFormat.Xls, 1000, 1)]\n    [InlineData(ExcelFormat.Xls, 65536, 2)]\n    [InlineData(ExcelFormat.Xls, 132_000, 3)]\n    //[InlineData(ExcelFormat.Xls, 1_000_000, 16)]\n    //[InlineData(ExcelFormat.Xlsx, 1_048_576, 2)]\n    public void EntityListAutoSplitSheetsTest(ExcelFormat excelFormat, int rowsCount, int expectedSheetCount)\n    {\n        var list = Enumerable.Range(1, rowsCount)\n            .Select(x => new Notice()\n            {\n                Id = x,\n                Content = $\"content_{x}\",\n                Title = $\"title_{x}\",\n                Publisher = $\"publisher_{x}\"\n            })\n            .ToArray();\n\n        var bytes = list.ToExcelBytes(excelFormat);\n        var workbook = ExcelHelper.LoadExcel(bytes, excelFormat);\n        Assert.Equal(expectedSheetCount, workbook.NumberOfSheets);\n    }\n\n    [Theory]\n    [InlineData(ExcelFormat.Xls, 1000, 1)]\n    [InlineData(ExcelFormat.Xls, 65536, 2)]\n    [InlineData(ExcelFormat.Xls, 132_000, 3)]\n    //[InlineData(ExcelFormat.Xls, 1_000_000, 16)]\n    //[InlineData(ExcelFormat.Xlsx, 1_048_576, 2)]\n    public void DataTableAutoSplitSheetsTest(ExcelFormat excelFormat, int rowsCount, int expectedSheetCount)\n    {\n        var dataTable = new DataTable();\n        dataTable.Columns.Add(new DataColumn(\"Id\", typeof(int)));\n        for (var i = 0; i < rowsCount; i++)\n        {\n            var row = dataTable.NewRow();\n            row.ItemArray = new object[]\n            {\n                    i+1\n            };\n            dataTable.Rows.Add(row);\n        }\n        Assert.Equal(rowsCount, dataTable.Rows.Count);\n        var bytes = dataTable.ToExcelBytes(excelFormat);\n        var workbook = ExcelHelper.LoadExcel(bytes, excelFormat);\n        Assert.Equal(expectedSheetCount, workbook.NumberOfSheets);\n    }\n\n    [Theory]\n    [InlineData(@\"TestData/EmptyColumns/emptyColumns.xls\", ExcelFormat.Xls)]\n    [InlineData(@\"TestData/EmptyColumns/emptyColumns.xlsx\", ExcelFormat.Xlsx)]\n    public void DataTableImportExportTestWithFirstColumnsEmpty(string file, ExcelFormat excelFormat)\n    {\n        // Arrange\n        var excelBytes = File.ReadAllBytes(file);\n\n        // Act\n        var importedData = ExcelHelper.ToDataTable(excelBytes, excelFormat);\n\n        // Assert\n        var dt = new DataTable();\n        dt.Columns.AddRange(new[]\n        {\n                new DataColumn(\"A\"),\n                new DataColumn(\"B\"),\n                new DataColumn(\"C\"),\n                new DataColumn(\"D\"),\n            });\n\n        dt.AddNewRow(new object[] { \"\", \"\", \"3\", \"4\" });\n        dt.AddNewRow(new object[] { \"\", \"2\", \"3\", \"\" });\n        dt.AddNewRow(new object[] { \"1\", \"2\", \"\", \"\" });\n        dt.AddNewRow(new object[] { \"1\", \"2\", \"3\", \"4\" });\n\n        Assert.NotNull(importedData);\n\n        Assert.Equal(4, importedData.Rows.Count);\n\n        importedData.AssertEquals(dt);\n    }\n\n    [Theory]\n    [InlineData(@\"TestData/NonStringColumns/nonStringColumns.xls\", ExcelFormat.Xls)]\n    [InlineData(@\"TestData/NonStringColumns/nonStringColumns.xlsx\", ExcelFormat.Xlsx)]\n    public void DataTableImportExportTestWithNonStringColumns(string file, ExcelFormat excelFormat)\n    {\n        // Arrange\n        var excelBytes = File.ReadAllBytes(file);\n\n        // Act\n        var importedData = ExcelHelper.ToDataTable(excelBytes, excelFormat);\n\n        // Assert\n        var dt = new DataTable();\n        dt.Columns.AddRange(new[]\n        {\n                new DataColumn(\"A\"),\n                new DataColumn(\"1000\"),\n                new DataColumn(\"TRUE\"), // Excel value will loaded as \"True\".\n                new DataColumn(DateTime.ParseExact(\"15/08/2021\", \"dd/MM/yyyy\", CultureInfo.InvariantCulture).ToShortDateString()),\n            });\n\n        dt.AddNewRow(new object[] { \"1\", \"2\", \"3\", \"4\" });\n\n        Assert.NotNull(importedData);\n\n        Assert.Equal(1, importedData.Rows.Count);\n\n        importedData.AssertEquals(dt);\n    }\n\n    [Theory]\n    [InlineData(@\"TestData/EmptyRows/emptyRows.xls\", ExcelFormat.Xls)]\n    [InlineData(@\"TestData/EmptyRows/emptyRows.xlsx\", ExcelFormat.Xlsx)]\n    public void DataTableImportExportTestWithoutEmptyRowsAndAdditionalColumns(string file, ExcelFormat excelFormat)\n    {\n        // Arrange\n        var excelBytes = File.ReadAllBytes(file);\n\n        // Act\n        var importedData = ExcelHelper.ToDataTable(excelBytes, excelFormat, removeEmptyRows: true, maxColumns: 3);\n\n        // Assert\n        var dt = new DataTable();\n        dt.Columns.AddRange(new[]\n        {\n                new DataColumn(\"A\"),\n                new DataColumn(\"B\"),\n                new DataColumn(\"C\"),\n            });\n\n        dt.AddNewRow(new object[] { \"1\", \"2\", \"3\" });\n        dt.AddNewRow(new object[] { \"1\", \"\", \"\" });\n        dt.AddNewRow(new object[] { \"1\", \"2\", \"3\" });\n        dt.AddNewRow(new object[] { \"\", \"2\", \"3\" });\n\n        Assert.NotNull(importedData);\n\n        Assert.Equal(4, importedData.Rows.Count);\n\n        importedData.AssertEquals(dt);\n    }\n\n    [Theory]\n    [ClassData(typeof(ExcelFormatData))]\n    public async Task ImageImportExportTest(ExcelFormat excelFormat)\n    {\n        var imageBytes = await HttpHelper.HttpClient.GetByteArrayAsync(\n            \"https://www.nuget.org/profiles/weihanli/avatar?imageSize=64\", TestContext.Current.CancellationToken\n            );\n        var list = Enumerable.Range(1, 5)\n            .Select(x => new ImageTest() { Id = x, Image = imageBytes })\n            .ToList();\n        var excelBytes = list.ToExcelBytes(excelFormat);\n        var importResult = ExcelHelper.ToEntityList<ImageTest>(excelBytes, excelFormat);\n        Assert.NotNull(importResult);\n        Assert.Equal(list.Count, importResult.Count);\n        for (var i = 0; i < list.Count; i++)\n        {\n            Assert.NotNull(importResult[i]);\n            var result = importResult[i]!;\n            Assert.Equal(list[i].Id, result.Id);\n            Assert.True(list[i].Image.SequenceEqual(result.Image));\n        }\n    }\n\n    [Theory]\n    [ClassData(typeof(ExcelFormatData))]\n    public async Task ImageImportExportPictureDataTest(ExcelFormat excelFormat)\n    {\n        var imageBytes = await HttpHelper.HttpClient.GetByteArrayAsync(\n            \"https://www.nuget.org/profiles/weihanli/avatar?imageSize=64\", TestContext.Current.CancellationToken\n            );\n        var list = Enumerable.Range(1, 5)\n            .Select(x => new ImageTest() { Id = x, Image = imageBytes })\n            .ToList();\n        var excelBytes = list.ToExcelBytes(excelFormat);\n        var importResult = ExcelHelper.ToEntityList<ImageTestPicData>(excelBytes, excelFormat);\n        Assert.NotNull(importResult);\n        Assert.Equal(list.Count, importResult.Count);\n        for (var i = 0; i < list.Count; i++)\n        {\n            Assert.NotNull(importResult[i]);\n            var result = importResult[i]!;\n            Assert.Equal(list[i].Id, result.Id);\n            Assert.NotNull(result.Image);\n            Assert.True(list[i].Image.SequenceEqual(result.Image.Data));\n            Assert.Equal(PictureType.PNG, result.Image.PictureType);\n        }\n    }\n\n    [Fact]\n    public void DataTableDefaultValueTest()\n    {\n        var table = new DataTable();\n        table.Columns.Add(new DataColumn(\"Name\"));\n        table.Columns.Add(new DataColumn(\"Value\"));\n        table.Columns.Add(new DataColumn(\"Description\"));\n        var row = table.AddNewRow();\n        row[\"Value\"] = null;\n        row[\"Description\"] = \"test\";\n\n        Assert.Equal(DBNull.Value, table.Rows[0][\"Name\"]);\n        Assert.Equal(DBNull.Value, table.Rows[0][\"Value\"]);\n        Assert.NotNull(table.Rows[0][0]);\n        Assert.Equal(\"test\", table.Rows[0][\"Description\"]);\n    }\n\n    [Theory]\n    [ClassData(typeof(ExcelFormatData))]\n    public void SheetNameTest_ToExcelFile(ExcelFormat excelFormat)\n    {\n        IReadOnlyList<Notice> list = Enumerable.Range(0, 10).Select(i => new Notice()\n        {\n            Id = i + 1,\n            Content = $\"content_{i}\",\n            Title = $\"title_{i}\",\n            PublishedAt = DateTime.UtcNow.AddDays(-i),\n            Publisher = $\"publisher_{i}\"\n        }).ToArray();\n        var settings = FluentSettings.For<Notice>();\n        lock (settings)\n        {\n            settings.HasSheetSetting(s =>\n            {\n                s.SheetName = \"Test\";\n            });\n\n            var filePath = $\"{Path.GetTempFileName()}.{excelFormat.ToString().ToLower()}\";\n            list.ToExcelFile(filePath);\n\n            var excel = ExcelHelper.LoadExcel(filePath);\n            Assert.Equal(\"Test\", excel.GetSheetAt(0).SheetName);\n\n            settings.HasSheetSetting(s =>\n            {\n                s.SheetName = \"NoticeList\";\n            });\n        }\n\n\n    }\n\n    [Theory]\n    [ClassData(typeof(ExcelFormatData))]\n    public void SheetNameTest_ToExcelBytes(ExcelFormat excelFormat)\n    {\n        IReadOnlyList<Notice> list = Enumerable.Range(0, 10).Select(i => new Notice()\n        {\n            Id = i + 1,\n            Content = $\"content_{i}\",\n            Title = $\"title_{i}\",\n            PublishedAt = DateTime.UtcNow.AddDays(-i),\n            Publisher = $\"publisher_{i}\"\n        }).ToArray();\n        var settings = FluentSettings.For<Notice>();\n        lock (settings)\n        {\n            settings.HasSheetSetting(s =>\n            {\n                s.SheetName = \"Test\";\n            });\n\n            var excelBytes = list.ToExcelBytes(excelFormat);\n            var excel = ExcelHelper.LoadExcel(excelBytes, excelFormat);\n            Assert.Equal(\"Test\", excel.GetSheetAt(0).SheetName);\n\n            settings.HasSheetSetting(s =>\n            {\n                s.SheetName = \"NoticeList\";\n            });\n        }\n    }\n\n    [Theory]\n    [ClassData(typeof(ExcelFormatData))]\n    public void DuplicateColumnTest(ExcelFormat excelFormat)\n    {\n        var workbook = ExcelHelper.PrepareWorkbook(excelFormat);\n        var sheet = workbook.CreateSheet();\n        var headerRow = sheet.CreateRow(0);\n        headerRow.CreateCell(0).SetCellValue(\"A\");\n        headerRow.CreateCell(1).SetCellValue(\"B\");\n        headerRow.CreateCell(2).SetCellValue(\"C\");\n        headerRow.CreateCell(3).SetCellValue(\"A\");\n        headerRow.CreateCell(4).SetCellValue(\"B\");\n        headerRow.CreateCell(5).SetCellValue(\"C\");\n        var dataRow = sheet.CreateRow(1);\n        dataRow.CreateCell(0).SetCellValue(\"1\");\n        dataRow.CreateCell(1).SetCellValue(\"2\");\n        dataRow.CreateCell(2).SetCellValue(\"3\");\n        dataRow.CreateCell(3).SetCellValue(\"4\");\n        dataRow.CreateCell(4).SetCellValue(\"5\");\n        dataRow.CreateCell(5).SetCellValue(\"6\");\n        var dataTable = sheet.ToDataTable();\n        Assert.Equal(headerRow.Cells.Count, dataTable.Columns.Count);\n        Assert.Equal(1, dataTable.Rows.Count);\n\n        var newWorkbook = ExcelHelper.LoadExcel(dataTable.ToExcelBytes());\n        var newSheet = newWorkbook.GetSheetAt(0);\n        Assert.Equal(sheet.PhysicalNumberOfRows, newSheet.PhysicalNumberOfRows);\n        for (var i = 0; i < sheet.PhysicalNumberOfRows; i++)\n        {\n            Assert.Equal(sheet.GetRow(i).Cells.Count, newSheet.GetRow(i).Cells.Count);\n\n            for (var j = 0; j < headerRow.Cells.Count; j++)\n            {\n                Assert.Equal(\n                    sheet.GetRow(i).GetCell(j).GetCellValue<string>(),\n                    newSheet.GetRow(i).GetCell(j).GetCellValue<string>()\n                    );\n            }\n        }\n\n    }\n\n    [Theory]\n    [ClassData(typeof(ExcelFormatData))]\n    public void ValidatorTest(ExcelFormat excelFormat)\n    {\n        var list = new List<Job>()\n        {\n            new()\n            {\n                Id = 1,\n                Name = \"test\"\n            },\n            new()\n        };\n        var bytes = list.ToExcelBytes(excelFormat);\n        var result = ExcelHelper.ToEntityListWithValidationResult<Job>(bytes, excelFormat);\n        Assert.Equal(list.Count, result.EntityList.Count);\n        for (var i = 0; i < list.Count; i++)\n        {\n            Assert.True(list[i] == result.EntityList[i]);\n        }\n        Assert.Single(result.ValidationResults);\n    }\n\n    [Theory]\n    [ClassData(typeof(ExcelFormatData))]\n    public void ValidatorTest_CustomValidator(ExcelFormat excelFormat)\n    {\n        var list = new List<Job>()\n        {\n            new()\n            {\n                Id = 1,\n                Name = \"test\"\n            }\n        };\n        var validator = new DelegateValidator<Job>(_ => new ValidationResult()\n        {\n            Valid = false,\n            Errors = new Dictionary<string, string[]> { { \"\", [\"Mock error\"] } }\n        });\n        var bytes = list.ToExcelBytes(excelFormat);\n        var result = ExcelHelper.ToEntityListWithValidationResult(bytes, excelFormat, validator: validator);\n        Assert.Equal(list.Count, result.EntityList.Count);\n        for (var i = 0; i < list.Count; i++)\n        {\n            Assert.True(list[i] == result.EntityList[i]);\n        }\n        Assert.Single(result.ValidationResults);\n    }\n\n    [Theory]\n    [ClassData(typeof(ExcelFormatData))]\n    public void CellReaderTest(ExcelFormat excelFormat)\n    {\n        var jobs = new CellReaderTestModel[] { new() { Id = 1, Name = \"test\" }, new() { Id = 2 }, };\n        var bytes = jobs.ToExcelBytes(excelFormat);\n        var settings = FluentSettings.For<CellReaderTestModel>();\n        settings.Property(x => x.Name)\n            .HasCellReader(_ => \"CellValue\");\n\n        var list = ExcelHelper.ToEntityList<CellReaderTestModel>(bytes, excelFormat);\n        Assert.Equal(jobs.Length, list.Count);\n        for (var i = 0; i < jobs.Length; i++)\n        {\n            Assert.NotNull(list[i]);\n            var model = list[i];\n            Guard.NotNull(model);\n            Assert.Equal(jobs[i].Id, model.Id);\n            Assert.Equal(\"CellValue\", model.Name);\n        }\n\n        settings.Property(x => x.Name)\n            .HasCellReader(null);\n    }\n\n\n    [Theory]\n    [ClassData(typeof(ExcelFormatData))]\n    public void PostImportActionTest(ExcelFormat excelFormat)\n    {\n        var jobs = new PostImportActionTestModel[] { new() { Id = 1, Name = \"test\" }, new() { Id = 2 }, };\n        var bytes = jobs.ToExcelBytes(excelFormat);\n        var settings = FluentSettings.For<PostImportActionTestModel>();\n        settings.WithPostImportAction((entity, rowIndex) => entity?.RowNumber = rowIndex + 1);\n\n        var list = ExcelHelper.ToEntityList<PostImportActionTestModel>(bytes, excelFormat);\n        Assert.Equal(jobs.Length, list.Count);\n        for (var i = 0; i < jobs.Length; i++)\n        {\n            Assert.NotNull(list[i]);\n            var model = list[i];\n            Guard.NotNull(model);\n            Assert.Equal(jobs[i].Id, model.Id);\n            Assert.Equal(i + 2, model.RowNumber);\n        }\n\n        settings.Property(x => x.Name)\n            .HasCellReader(null);\n    }\n\n    [Theory]\n    [ClassData(typeof(ExcelFormatData))]\n    public void CellTypeTest(ExcelFormat excelFormat)\n    {\n        var workbook = ExcelHelper.PrepareWorkbook(excelFormat);\n        var sheet = workbook.CreateSheet();\n        var headerRow = sheet.CreateRow(0);\n        headerRow.CreateCell(0).SetCellValue(\"Id\");\n        headerRow.CreateCell(1).SetCellValue(\"1234\");\n\n        var dataRow = sheet.CreateRow(1);\n        dataRow.CreateCell(0).SetCellValue(1);\n        dataRow.CreateCell(1).SetCellValue(0.24);\n\n        var cell = dataRow.GetCell(1);\n        cell.CellStyle.DataFormat = HSSFDataFormat.GetBuiltinFormat(\"0%\");\n        Assert.Equal(CellType.Numeric, cell.CellType);\n        Assert.Equal(\"24%\", new DataFormatter().FormatCellValue(cell));\n\n        var excelBytes = workbook.ToExcelBytes();\n\n        var importedWorkbook = ExcelHelper.LoadExcel(excelBytes, excelFormat);\n        var importedSheet = importedWorkbook.GetSheetAt(0);\n        var row1 = importedSheet.GetRow(1);\n        var cell1 = row1.GetCell(1);\n        Assert.Equal(cell.CellStyle.DataFormat, cell1.CellStyle.DataFormat);\n\n        Assert.Equal(1, row1.GetCell(0).NumericCellValue.To<int>());\n        Assert.Equal(\"24%\", new DataFormatter().FormatCellValue(cell1));\n    }\n\n    [Theory]\n    [ClassData(typeof(ExcelFormatData))]\n    public void HeaderCellTypeTest(ExcelFormat excelFormat)\n    {\n        var workbook = ExcelHelper.PrepareWorkbook(excelFormat);\n        var sheet = workbook.CreateSheet();\n        var headerRow = sheet.CreateRow(0);\n        headerRow.CreateCell(0).SetCellValue(\"Id\");\n        var cell = headerRow.CreateCell(1);\n        cell.SetCellValue(1234);\n        Assert.Equal(CellType.Numeric, cell.CellType);\n\n        var dataRow = sheet.CreateRow(1);\n        dataRow.CreateCell(0).SetCellValue(1);\n        dataRow.CreateCell(1).SetCellValue(\"1234\");\n\n        var excelBytes = workbook.ToExcelBytes();\n\n        var list = ExcelHelper.ToEntityList<CellFormatTestModel>(excelBytes, excelFormat);\n        Assert.Single(list);\n        Assert.NotNull(list[0]);\n        var entity = Guard.NotNull(list[0]);\n        Assert.Equal(1, entity.Id);\n        Assert.Equal(\"1234\", entity.Name);\n    }\n\n    [Theory]\n    [ClassData(typeof(ExcelFormatData))]\n    public void ChineseDateFormatterTest(ExcelFormat excelFormat)\n    {\n        FluentSettings.For<ChineseDateFormatter.ChineDateTestModel>()\n            .Property(x => x.Date)\n            .HasColumnInputFormatter(ChineseDateFormatter.FormatInput)\n            .HasColumnOutputFormatter(ChineseDateFormatter.FormatOutput);\n\n        var model = new[]\n        {\n            new ChineseDateFormatter.ChineDateTestModel() { Date = DateTime.Parse(\"2022-01-01\") }\n        };\n        var excelBytes = model.ToExcelBytes(excelFormat);\n        var list = ExcelHelper.ToEntityList<ChineseDateFormatter.ChineDateTestModel>(excelBytes, excelFormat);\n        Assert.Single(list);\n        var item = list[0];\n        Assert.NotNull(item);\n        Guard.NotNull(item);\n        Assert.Equal(DateTime.Parse(\"2022-01-01\"), item.Date);\n    }\n\n    [Fact]\n    public void PropertyOrderTest()\n    {\n        var excelConfiguration = InternalHelper.GetExcelConfigurationMapping<OrderTestModel1>();\n        var propertyColumnDictionary = InternalHelper.GetPropertyColumnDictionary(excelConfiguration);\n        Assert.NotNull(propertyColumnDictionary);\n        if (excelConfiguration.Property(x => x.Id) is PropertyConfiguration<OrderTestModel1, int>\n            idPropertyConfiguration)\n        {\n            Assert.NotNull(idPropertyConfiguration.ColumnTitle);\n            Assert.Equal(0, idPropertyConfiguration.ColumnIndex);\n        }\n        else\n        {\n            Assert.Fail(\"Invalid property Id\");\n        }\n\n        if (excelConfiguration.Property(x => x.Title) is PropertyConfiguration<OrderTestModel1, string?>\n            titlePropertyConfiguration)\n        {\n            Assert.NotNull(titlePropertyConfiguration.ColumnTitle);\n            Assert.Equal(1, titlePropertyConfiguration.ColumnIndex);\n        }\n        else\n        {\n            Assert.Fail(\"Invalid property Title\");\n        }\n\n        if (excelConfiguration.Property(x => x.Description) is PropertyConfiguration<OrderTestModel1, string?>\n            descriptionPropertyConfiguration)\n        {\n            Assert.NotNull(descriptionPropertyConfiguration.ColumnTitle);\n            Assert.Equal(2, descriptionPropertyConfiguration.ColumnIndex);\n        }\n        else\n        {\n            Assert.Fail(\"Invalid property Description\");\n        }\n    }\n\n    [Fact]\n    public void PropertyOrderTest2()\n    {\n        var excelConfiguration = InternalHelper.GetExcelConfigurationMapping<OrderTestModel2>();\n        var propertyColumnDictionary = InternalHelper.GetPropertyColumnDictionary(excelConfiguration);\n        Assert.NotNull(propertyColumnDictionary);\n        if (excelConfiguration.Property(x => x.Id) is PropertyConfiguration<OrderTestModel2, int>\n            idPropertyConfiguration)\n        {\n            Assert.NotNull(idPropertyConfiguration.ColumnTitle);\n            Assert.Equal(1, idPropertyConfiguration.ColumnIndex);\n        }\n        else\n        {\n            Assert.Fail(\"Invalid property Id\");\n        }\n\n        if (excelConfiguration.Property(x => x.Title) is PropertyConfiguration<OrderTestModel2, string?>\n            titlePropertyConfiguration)\n        {\n            Assert.NotNull(titlePropertyConfiguration.ColumnTitle);\n            Assert.Equal(0, titlePropertyConfiguration.ColumnIndex);\n        }\n        else\n        {\n            Assert.Fail(\"Invalid property Title\");\n        }\n\n        if (excelConfiguration.Property(x => x.Description) is PropertyConfiguration<OrderTestModel2, string?>\n            descriptionPropertyConfiguration)\n        {\n            Assert.NotNull(descriptionPropertyConfiguration.ColumnTitle);\n            Assert.Equal(2, descriptionPropertyConfiguration.ColumnIndex);\n        }\n        else\n        {\n            Assert.Fail(\"Invalid property Description\");\n        }\n    }\n\n    [Fact]\n    public void PropertyOrderTest3()\n    {\n        var excelConfiguration = InternalHelper.GetExcelConfigurationMapping<OrderTestModel3>();\n        var propertyColumnDictionary = InternalHelper.GetPropertyColumnDictionary(excelConfiguration);\n        Assert.NotNull(propertyColumnDictionary);\n        if (excelConfiguration.Property(x => x.Id) is PropertyConfiguration<OrderTestModel3, int>\n            idPropertyConfiguration)\n        {\n            Assert.NotNull(idPropertyConfiguration.ColumnTitle);\n            Assert.Equal(0, idPropertyConfiguration.ColumnIndex);\n        }\n        else\n        {\n            Assert.Fail(\"Invalid property Id\");\n        }\n\n        if (excelConfiguration.Property(x => x.Title) is PropertyConfiguration<OrderTestModel3, string?>\n            titlePropertyConfiguration)\n        {\n            Assert.NotNull(titlePropertyConfiguration.ColumnTitle);\n            Assert.Equal(1, titlePropertyConfiguration.ColumnIndex);\n        }\n        else\n        {\n            Assert.Fail(\"Invalid property Title\");\n        }\n\n        if (excelConfiguration.Property(x => x.Description) is PropertyConfiguration<OrderTestModel3, string?>\n            descriptionPropertyConfiguration)\n        {\n            Assert.NotNull(descriptionPropertyConfiguration.ColumnTitle);\n            Assert.Equal(2, descriptionPropertyConfiguration.ColumnIndex);\n        }\n        else\n        {\n            Assert.Fail(\"Invalid property Description\");\n        }\n    }\n\n\n    [Fact]\n    public void PropertyOrderTest4_CustomOrdering()\n    {\n        var excelConfiguration = InternalHelper.GetExcelConfigurationMapping<OrderTestModel4>();\n        excelConfiguration.WithPropertyComparer(new PropertyNameBasedPropertyComparer());\n        var propertyColumnDictionary = InternalHelper.GetPropertyColumnDictionary(excelConfiguration);\n        Assert.NotNull(propertyColumnDictionary);\n        if (excelConfiguration.Property(x => x.Id) is PropertyConfiguration<OrderTestModel4, int>\n            idPropertyConfiguration)\n        {\n            Assert.NotNull(idPropertyConfiguration.ColumnTitle);\n            Assert.Equal(1, idPropertyConfiguration.ColumnIndex);\n        }\n        else\n        {\n            Assert.Fail(\"Invalid property Id\");\n        }\n\n        if (excelConfiguration.Property(x => x.Title) is PropertyConfiguration<OrderTestModel4, string?>\n            titlePropertyConfiguration)\n        {\n            Assert.NotNull(titlePropertyConfiguration.ColumnTitle);\n            Assert.Equal(2, titlePropertyConfiguration.ColumnIndex);\n        }\n        else\n        {\n            Assert.Fail(\"Invalid property Title\");\n        }\n\n        if (excelConfiguration.Property(x => x.Description) is PropertyConfiguration<OrderTestModel4, string?>\n            descriptionPropertyConfiguration)\n        {\n            Assert.NotNull(descriptionPropertyConfiguration.ColumnTitle);\n            Assert.Equal(0, descriptionPropertyConfiguration.ColumnIndex);\n        }\n        else\n        {\n            Assert.Fail(\"Invalid property Description\");\n        }\n    }\n\n    private sealed class CellFormatTestModel\n    {\n        public int Id { get; set; }\n        [Column(\"1234\")]\n        public string? Name { get; set; }\n    }\n\n    private sealed record CellReaderTestModel\n    {\n        public int Id { get; set; }\n        public string? Name { get; set; }\n    }\n\n    private sealed record PostImportActionTestModel\n    {\n        public int Id { get; set; }\n        public string? Name { get; set; }\n        [Column(IsIgnored = true)]\n        public int RowNumber { get; set; }\n    }\n\n    private sealed class ImageTest\n    {\n        public int Id { get; set; }\n\n        public byte[] Image { get; set; } = null!;\n    }\n\n    private sealed class ImageTestPicData\n    {\n        public int Id { get; set; }\n\n        public IPictureData Image { get; set; } = null!;\n    }\n}\n\nfile sealed class PropertyNameBasedPropertyComparer : IComparer<PropertyInfo>\n{\n    public int Compare(PropertyInfo? x, PropertyInfo? y)\n    {\n        return (x, y) switch\n        {\n            (null, null) => 0,\n            (null, _) => -1,\n            (_, null) => 1,\n            _ => string.CompareOrdinal(x.Name, y.Name)\n        };\n    }\n}\n\nfile sealed class ChineseDateFormatter\n{\n    public sealed class ChineDateTestModel\n    {\n        public DateTime Date { get; set; }\n    }\n\n    public static DateTime FormatInput(string? input)\n    {\n        if (DateTimeUtils.TransStrToDateTime(input, out var dt))\n        {\n            return dt;\n        }\n        throw new ArgumentException(\"Invalid date input\");\n    }\n\n    public static string FormatOutput(DateTime input)\n    {\n        return \"二〇二二年一月一日\";\n    }\n}\n\n// http://luoma.pro/Content/Detail/671?parentId=1\nfile static class DateTimeUtils\n{\n    /// <summary>\n    /// 字符串日期转 DateTime  \n    /// </summary>\n    /// <param name=\"str\">字符串日期</param>\n    /// <param name=\"dt\">转换成功赋值</param>\n    /// <returns>转换成功返回 true</returns>\n    public static bool TransStrToDateTime(string? str, out DateTime dt)\n    {\n        dt = default;\n        if (str.IsNullOrEmpty())\n            return false;\n\n        //第一次转换\n        if (DateTime.TryParse(str, out dt))\n        {\n            return true;\n        }\n        //第二次转换\n        string[] format = new string[]\n        {\n            \"yyyyMMdd\",\n            \"yyyyMdHHmmss\",\n            \"yyyyMMddHHmmss\",\n            \"yyyy-M-d\",\n            \"yyyy-MM-dd\",\n            \"yyyy-MM-dd HH:mm:ss\",\n            \"yyyy/M/d\",\n            \"yyyy/MM/dd\",\n            \"yyyy/MM/dd HH:mm:ss\",\n            \"yyyy.M.d\",\n            \"yyyy.MM.dd\",\n            \"yyyy.MM.dd HH:mm:ss\",\n            \"yyyy年M月d日\",\n            \"yyyy年MM月dd日\",\n            \"yyyy年MM月dd日HH:mm:ss\",\n            \"yyyy年MM月dd日 HH时mm分ss秒\"\n        };\n        if (DateTime.TryParseExact(str, format, CultureInfo.InvariantCulture, DateTimeStyles.None, out dt))\n        {\n            return true;\n        }\n        //第三次转换\n        try\n        {\n            if (Regex.IsMatch(str, \"^(零|〇|一|二|三|四|五|六|七|八|九|十){2,4}年((正|一|二|三|四|五|六|七|八|九|十|十一|十二)月((一|二|三|四|五|六|七|八|九|十){1,3}(日)?)?)?$\"))\n            {\n                var match = Regex.Match(str, @\"^(.+)年(.+)月(.+)日$\");\n                if (match.Success)\n                {\n                    int year = GetYear(match.Groups[1].Value);\n                    int month = GetMonth(match.Groups[2].Value);\n                    long dayL = ParseCnToInt(match.Groups[3].Value);\n                    dt = new DateTime(year, month, int.Parse(dayL.ToString()));\n                    return true;\n                }\n            }\n        }\n        catch\n        {\n            return false;\n        }\n        return false;\n    }\n    /// <summary>\n    /// 使用正则表达式判断是否为日期\n    /// </summary>\n    /// <param name=\"str\">日期格式字符串</param>\n    /// <returns>是日期格式字符串返回 true</returns>\n    public static bool IsDateTime(string str)\n    {\n        bool isDateTime;\n        // yyyy/MM/dd  - 年月日数字\n        if (Regex.IsMatch(str, \"^(?<year>\\\\d{2,4})/(?<month>\\\\d{1,2})/(?<day>\\\\d{1,2})$\"))\n            isDateTime = true;\n        // yyyy-MM-dd - 年月日数字  \n        else if (Regex.IsMatch(str, \"^(?<year>\\\\d{2,4})-(?<month>\\\\d{1,2})-(?<day>\\\\d{1,2})$\"))\n            isDateTime = true;\n        // yyyy.MM.dd - 年月日数字  \n        else if (Regex.IsMatch(str, \"^(?<year>\\\\d{2,4})[.](?<month>\\\\d{1,2})[.](?<day>\\\\d{1,2})$\"))\n            isDateTime = true;\n        // yyyy年MM月dd日 - 年月日数字  \n        else if (Regex.IsMatch(str, \"^((?<year>\\\\d{2,4})年)?(?<month>\\\\d{1,2})月((?<day>\\\\d{1,2})日)?$\"))\n            isDateTime = true;\n        // yyyy年MM月dd日  - 年月日中文 \n        else if (Regex.IsMatch(str, \"^(零|〇|一|二|三|四|五|六|七|八|九|十){2,4}年((正|一|二|三|四|五|六|七|八|九|十|十一|十二)月((一|二|三|四|五|六|七|八|九|十){1,3}(日)?)?)?$\"))\n            isDateTime = true;\n        // yyyy年MM月dd日  - 年(数字)，月(中文)，日(中文)\n        //else if (Regex.IsMatch(str, \"^((?<year>\\\\d{2,4})年)?(正|一|二|三|四|五|六|七|八|九|十|十一|十二)月((一|二|三|四|五|六|七|八|九|十){1,3}日)?$\"))\n        //    isDateTime = true;\n        // yyyy年  \n        //else if (Regex.IsMatch(str, \"^(?<year>\\\\d{2,4})年$\"))  \n        //    isDateTime = true;  \n        // 农历1  \n        //else if (Regex.IsMatch(str, \"^(甲|乙|丙|丁|戊|己|庚|辛|壬|癸)(子|丑|寅|卯|辰|巳|午|未|申|酉|戌|亥)年((正|一|二|三|四|五|六|七|八|九|十|十一|十二)月((一|二|三|四|五|六|七|八|九|十){1,3}(日)?)?)?$\"))\n        //    isDateTime = true;\n        //// 农历2  \n        //else if (Regex.IsMatch(str, \"^((甲|乙|丙|丁|戊|己|庚|辛|壬|癸)(子|丑|寅|卯|辰|巳|午|未|申|酉|戌|亥)年)?(正|一|二|三|四|五|六|七|八|九|十|十一|十二)月初(一|二|三|四|五|六|七|八|九|十)$\"))\n        //    isDateTime = true;\n        //// XX时XX分XX秒  \n        //else if (Regex.IsMatch(str, \"^(?<hour>\\\\d{1,2})(时|点)(?<minute>\\\\d{1,2})分((?<second>\\\\d{1,2})秒)?$\"))\n        //    isDateTime = true;\n        //// XX时XX分XX秒  \n        //else if (Regex.IsMatch(str, \"^((零|一|二|三|四|五|六|七|八|九|十){1,3})(时|点)((零|一|二|三|四|五|六|七|八|九|十){1,3})分(((零|一|二|三|四|五|六|七|八|九|十){1,3})秒)?$\"))\n        //    isDateTime = true;\n        //// XX分XX秒  \n        //else if (Regex.IsMatch(str, \"^(?<minute>\\\\d{1,2})分(?<second>\\\\d{1,2})秒$\"))\n        //    isDateTime = true;\n        //// XX分XX秒  \n        //else if (Regex.IsMatch(str, \"^((零|一|二|三|四|五|六|七|八|九|十){1,3})分((零|一|二|三|四|五|六|七|八|九|十){1,3})秒$\"))\n        //    isDateTime = true;\n        //// XX时  \n        //else if (Regex.IsMatch(str, \"\\\\b(?<hour>\\\\d{1,2})(时|点钟)\\\\b\"))\n        //    isDateTime = true;\n        else\n            isDateTime = false;\n        return isDateTime;\n    }\n    #region 年月获取\n    /// <summary>\n    /// 获取年份\n    /// </summary>\n    /// <param name=\"str\">年份</param>\n    /// <returns>数字年份</returns>\n    public static int GetYear(string str)\n    {\n        var strNumber = \"\";\n        foreach (var item in str)\n        {\n            switch (item.ToString())\n            {\n                case \"零\":\n                case \"〇\":\n                    strNumber += \"0\";\n                    break;\n                case \"一\":\n                    strNumber += \"1\";\n                    break;\n                case \"二\":\n                    strNumber += \"2\";\n                    break;\n                case \"三\":\n                    strNumber += \"3\";\n                    break;\n                case \"四\":\n                    strNumber += \"4\";\n                    break;\n                case \"五\":\n                    strNumber += \"5\";\n                    break;\n                case \"六\":\n                    strNumber += \"6\";\n                    break;\n                case \"七\":\n                    strNumber += \"7\";\n                    break;\n                case \"八\":\n                    strNumber += \"8\";\n                    break;\n                case \"九\":\n                    strNumber += \"9\";\n                    break;\n                case \"十\":\n                    strNumber += \"10\";\n                    break;\n            }\n        }\n        int.TryParse(strNumber, out var number);\n        return number;\n    }\n    /// <summary>\n    ///获取月份\n    /// </summary>\n    /// <param name=\"str\">月份</param>\n    /// <returns>数字月份</returns>\n    public static int GetMonth(string str)\n    {\n        var strNumber = \"\";\n        switch (str)\n        {\n            case \"一\":\n            case \"正\":\n                strNumber += \"1\";\n                break;\n            case \"二\":\n                strNumber += \"2\";\n                break;\n            case \"三\":\n                strNumber += \"3\";\n                break;\n            case \"四\":\n                strNumber += \"4\";\n                break;\n            case \"五\":\n                strNumber += \"5\";\n                break;\n            case \"六\":\n                strNumber += \"6\";\n                break;\n            case \"七\":\n                strNumber += \"7\";\n                break;\n            case \"八\":\n                strNumber += \"8\";\n                break;\n            case \"九\":\n                strNumber += \"9\";\n                break;\n            case \"十\":\n                strNumber += \"10\";\n                break;\n            case \"十一\":\n                strNumber += \"11\";\n                break;\n            case \"十二\":\n                strNumber += \"12\";\n                break;\n        }\n        int.TryParse(strNumber, out var number);\n        return number;\n    }\n    #endregion\n    #region 中文数字和阿拉伯数字转换\n    /// <summary>\n    /// 阿拉伯数字转换成中文数字\n    /// </summary>\n    /// <param name=\"x\"></param>\n    /// <returns></returns>\n    public static string NumToChinese(string x)\n    {\n        string[] pArrayNum = { \"零\", \"一\", \"二\", \"三\", \"四\", \"五\", \"六\", \"七\", \"八\", \"九\" };\n        //为数字位数建立一个位数组\n        string[] pArrayDigit = { \"\", \"十\", \"百\", \"千\" };\n        //为数字单位建立一个单位数组\n        string[] pArrayUnits = { \"\", \"万\", \"亿\", \"万亿\" };\n        var pStrReturnValue = \"\"; //返回值\n        var finger = 0; //字符位置指针\n        var pIntM = x.Length % 4; //取模\n        int pIntK;\n        if (pIntM > 0)\n            pIntK = x.Length / 4 + 1;\n        else\n            pIntK = x.Length / 4;\n        //外层循环,四位一组,每组最后加上单位: \",万亿,\",\",亿,\",\",万,\"\n        for (var i = pIntK; i > 0; i--)\n        {\n            var pIntL = 4;\n            if (i == pIntK && pIntM != 0)\n                pIntL = pIntM;\n            //得到一组四位数\n            var four = x.Substring(finger, pIntL);\n            var pIntL1 = four.Length;\n            //内层循环在该组中的每一位数上循环\n            for (var j = 0; j < pIntL1; j++)\n            {\n                //处理组中的每一位数加上所在的位\n                var n = Convert.ToInt32(four.Substring(j, 1));\n                if (n == 0)\n                {\n                    if (j < pIntL1 - 1 && Convert.ToInt32(four.Substring(j + 1, 1)) > 0 && !pStrReturnValue.EndsWith(pArrayNum[n]))\n                        pStrReturnValue += pArrayNum[n];\n                }\n                else\n                {\n                    if (!(n == 1 && (pStrReturnValue.EndsWith(pArrayNum[0]) | pStrReturnValue.Length == 0) && j == pIntL1 - 2))\n                        pStrReturnValue += pArrayNum[n];\n                    pStrReturnValue += pArrayDigit[pIntL1 - j - 1];\n                }\n            }\n            finger += pIntL;\n            //每组最后加上一个单位:\",万,\",\",亿,\" 等\n            if (i < pIntK) //如果不是最高位的一组\n            {\n                if (Convert.ToInt32(four) != 0)\n                    //如果所有4位不全是0则加上单位\",万,\",\",亿,\"等\n                    pStrReturnValue += pArrayUnits[i - 1];\n            }\n            else\n            {\n                //处理最高位的一组,最后必须加上单位\n                pStrReturnValue += pArrayUnits[i - 1];\n            }\n        }\n        return pStrReturnValue;\n    }\n    /// <summary>\n    /// 转换数字\n    /// </summary>\n    public static long CharToNumber(char c)\n    {\n        switch (c)\n        {\n            case '一': return 1;\n            case '二': return 2;\n            case '三': return 3;\n            case '四': return 4;\n            case '五': return 5;\n            case '六': return 6;\n            case '七': return 7;\n            case '八': return 8;\n            case '九': return 9;\n            case '零': return 0;\n            default: return -1;\n        }\n    }\n    /// <summary>\n    /// 转换单位\n    /// </summary>\n    public static long CharToUnit(char c)\n    {\n        switch (c)\n        {\n            case '十': return 10;\n            case '百': return 100;\n            case '千': return 1000;\n            case '万': return 10000;\n            case '亿': return 100000000;\n            default: return 1;\n        }\n    }\n    /// <summary>\n    /// 将中文数字转换阿拉伯数字\n    /// </summary>\n    /// <param name=\"cnum\">汉字数字</param>\n    /// <returns>长整型阿拉伯数字</returns>\n    public static long ParseCnToInt(string cnum)\n    {\n        cnum = Regex.Replace(cnum, \"\\\\s+\", \"\");\n        long firstUnit = 1;//一级单位\n        long secondUnit = 1;//二级单位\n        long result = 0;//结果\n        for (var i = cnum.Length - 1; i > -1; --i)//从低到高位依次处理\n        {\n            var tmpUnit = CharToUnit(cnum[i]);//临时单位变量\n            if (tmpUnit > firstUnit)//判断此位是数字还是单位\n            {\n                firstUnit = tmpUnit;//是的话就赋值,以备下次循环使用\n                secondUnit = 1;\n                if (i == 0)//处理如果是\"十\",\"十一\"这样的开头的\n                {\n                    result += firstUnit * secondUnit;\n                }\n                continue;//结束本次循环\n            }\n            if (tmpUnit > secondUnit)\n            {\n                secondUnit = tmpUnit;\n                continue;\n            }\n            result += firstUnit * secondUnit * CharToNumber(cnum[i]);//如果是数字,则和单位想乘然后存到结果里\n        }\n        return result;\n    }\n    #endregion\n}\n"
  },
  {
    "path": "test/WeihanLi.Npoi.Test/Extensions.cs",
    "content": "﻿// Copyright (c) Weihan Li. All rights reserved.\n// Licensed under the Apache license.\n\nusing System.Data;\nusing Xunit;\n\nnamespace WeihanLi.Npoi.Test;\n\npublic static class Extensions\n{\n    public static DataRow AddNewRow(this DataTable datatable, object[]? rowData = null)\n    {\n        var row = datatable.NewRow();\n        if (rowData is not null)\n        {\n            row.ItemArray = rowData;\n        }\n        datatable.Rows.Add(row);\n        return row;\n    }\n\n    public static void AssertEquals(this DataTable actual, DataTable expected)\n    {\n        // Check columns\n        for (var headerIndex = 0; headerIndex < expected.Columns.Count; headerIndex++)\n        {\n            var expectedValue = expected.Columns[headerIndex].ToString();\n            var excelValue = actual.Columns[headerIndex].ToString();\n\n            // \"TRUE\" from header column is translated to \"True\".\n            // I don't know how to load display value of boolean, therefore I ignore letter casing.\n            Assert.Equal(expectedValue, excelValue, ignoreCase: true);\n        }\n\n        // Check rows\n        for (var rowIndex = 0; rowIndex < expected.Rows.Count; rowIndex++)\n        {\n            for (var colIndex = 0; colIndex < expected.Rows[rowIndex].ItemArray.Length; colIndex++)\n            {\n                var expectedValue = expected.Rows[rowIndex].ItemArray[colIndex]?.ToString();\n                var excelValue = actual.Rows[rowIndex][colIndex].ToString();\n                Assert.Equal(expectedValue, excelValue);\n            }\n        }\n    }\n\n}\n"
  },
  {
    "path": "test/WeihanLi.Npoi.Test/MappingProfiles/NoticeProfile.cs",
    "content": "﻿// Copyright (c) Weihan Li. All rights reserved.\n// Licensed under the Apache license.\n\nusing WeihanLi.Extensions;\nusing WeihanLi.Npoi.Configurations;\nusing WeihanLi.Npoi.Test.Models;\n\nnamespace WeihanLi.Npoi.Test.MappingProfiles;\n\npublic sealed class NoticeProfile : IMappingProfile<Notice>\n{\n    public void Configure(IExcelConfiguration<Notice> noticeSetting)\n    {\n        noticeSetting\n            .HasAuthor(\"WeihanLi\")\n            .HasTitle(\"WeihanLi.Npoi test\")\n            .HasSheetSetting(setting =>\n            {\n                setting.SheetName = \"NoticeList\";\n                setting.AutoColumnWidthEnabled = true;\n            })\n            ;\n        noticeSetting.Property(_ => _.Id)\n            .HasColumnIndex(0);\n        noticeSetting.Property(_ => _.Title)\n            .HasColumnIndex(1);\n        noticeSetting.Property(_ => _.Content)\n            .HasColumnIndex(2);\n        noticeSetting.Property(_ => _.Publisher)\n            .HasColumnIndex(3);\n        noticeSetting.Property(_ => _.PublishedAt)\n            .HasColumnIndex(4)\n            .HasColumnOutputFormatter(x => x.ToTimeString());\n    }\n}\n"
  },
  {
    "path": "test/WeihanLi.Npoi.Test/Models/Job.cs",
    "content": "﻿// Copyright (c) Weihan Li. All rights reserved.\n// Licensed under the Apache license.\n\nusing System.ComponentModel.DataAnnotations;\n\nnamespace WeihanLi.Npoi.Test.Models;\n\npublic sealed record Job\n{\n    public int Id { get; set; }\n\n    [Required]\n    public string Name { get; set; } = default!;\n}\n"
  },
  {
    "path": "test/WeihanLi.Npoi.Test/Models/Notice.cs",
    "content": "﻿// Copyright (c) Weihan Li. All rights reserved.\n// Licensed under the Apache license.\n\nnamespace WeihanLi.Npoi.Test.Models;\n\npublic class BaseModel\n{\n    public int Id { get; set; }\n}\n\npublic class Notice : BaseModel\n{\n    public string? Title { get; set; }\n\n    public string? Content { get; set; }\n\n    public DateTime PublishedAt { get; set; }\n\n    public string? Publisher { get; set; }\n}\n"
  },
  {
    "path": "test/WeihanLi.Npoi.Test/Models/OrderTestModels.cs",
    "content": "﻿// Copyright (c) Weihan Li. All rights reserved.\n// Licensed under the Apache license.\n\nusing WeihanLi.Npoi.Attributes;\n\nnamespace WeihanLi.Npoi.Test.Models;\n\npublic class OrderTestModel1\n{\n    public int Id { get; set; }\n\n    public string? Title { get; set; }\n\n    public string? Description { get; set; }\n}\n\npublic class OrderTestModel2\n{\n    [Column(Index = 1)]\n    public int Id { get; set; }\n\n    [Column(Index = 0)]\n    public string? Title { get; set; }\n\n    public string? Description { get; set; }\n}\n\npublic class OrderTestModel3\n{\n    public string? Description { get; set; }\n\n    [Column(Index = 0)]\n    public int Id { get; set; }\n\n    [Column(Index = 1)]\n    public string? Title { get; set; }\n}\n\npublic class OrderTestModel4\n{\n    public int Id { get; set; }\n\n    public string? Title { get; set; }\n\n    public string? Description { get; set; }\n}\n"
  },
  {
    "path": "test/WeihanLi.Npoi.Test/Startup.cs",
    "content": "﻿// Copyright (c) Weihan Li. All rights reserved.\n// Licensed under the Apache license.\n\nusing WeihanLi.Npoi.Test.MappingProfiles;\n\nnamespace WeihanLi.Npoi.Test;\n\npublic class Startup\n{\n    public void Configure()\n    {\n        AppContext.SetSwitch(\"System.Drawing.EnableUnixSupport\", true);\n        // ---------- load excel mapping profiles ----------------\n        FluentSettings.LoadMappingProfiles(typeof(NoticeProfile).Assembly);\n    }\n}\n"
  },
  {
    "path": "test/WeihanLi.Npoi.Test/TestData/EmptyColumns/emptyColumns.csv",
    "content": "A,B,C,D\n,,3,4\n,2,3,\n1,2,,\n1,2,3,4\n"
  },
  {
    "path": "test/WeihanLi.Npoi.Test/TestData/NonStringColumns/nonStringColumns.csv",
    "content": "﻿A,1000,TRUE,15/08/2021\n1,2,3,4\n"
  },
  {
    "path": "test/WeihanLi.Npoi.Test/WeihanLi.Npoi.Test.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk\">\n\n  <PropertyGroup>\n    <IsTestProject>true</IsTestProject>\n    <OutputType>exe</OutputType>\n    <UseMicrosoftTestingPlatformRunner>true</UseMicrosoftTestingPlatformRunner>\n    <TestingPlatformDotnetTestSupport>true</TestingPlatformDotnetTestSupport>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <PackageReference Include=\"GitHubActionsTestLogger\" />\n    <PackageReference Include=\"Microsoft.NET.Test.Sdk\" />\n    <PackageReference Include=\"xunit.v3.mtp-v2\" />\n    <PackageReference Include=\"Xunit.DependencyInjection\" />\n    <PackageReference Include=\"coverlet.collector\">\n      <PrivateAssets>all</PrivateAssets>\n      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>\n    </PackageReference>\n  </ItemGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\..\\src\\WeihanLi.Npoi\\WeihanLi.Npoi.csproj\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <None Update=\"TestData\\**\\*\">\n      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n    </None>\n  </ItemGroup>\n\n</Project>\n"
  }
]