Преглед на файлове

Merge pull request #1 from LithWang/master

init
Lith преди 7 месеца
родител
ревизия
95854d2da6
променени са 88 файла, в които са добавени 6948 реда и са изтрити 1 реда
  1. 101 0
      .github/workflows/ki_devops3_build.yml
  2. 45 0
      .github/workflows/ki_devops3_test.yml
  3. 201 0
      LICENSE
  4. 86 0
      Publish/DevOps3/build-bash/10.Test.bash
  5. 42 0
      Publish/DevOps3/build-bash/19.get-app-version.bash
  6. 47 0
      Publish/DevOps3/build-bash/20.change-app-version.bash
  7. 48 0
      Publish/DevOps3/build-bash/21.change-to-next-version.bash
  8. 42 0
      Publish/DevOps3/build-bash/22.add-suffix-to-app-version.bash
  9. 43 0
      Publish/DevOps3/build-bash/30.nuget-pack.sh
  10. 92 0
      Publish/DevOps3/build-bash/40.Station-publish.sh
  11. 23 0
      Publish/DevOps3/build-bash/41.extra-publish.sh
  12. 46 0
      Publish/DevOps3/build-bash/50.docker-image-copy.sh
  13. 27 0
      Publish/DevOps3/build-bash/51.docker-deploy-copy.sh
  14. 23 0
      Publish/DevOps3/build-bash/52.docker-extra-copy.sh
  15. 32 0
      Publish/DevOps3/build-bash/startup.bash
  16. 23 0
      Publish/DevOps3/build-cmd/30.nuget-pack.bat
  17. 9 0
      Publish/DevOps3/build-cmd/Clean-All.bat
  18. 1 0
      Publish/DevOps3/build-cmd/Clean-ReleaseFiles.bat
  19. BIN
      Publish/DevOps3/build-cmd/VsTool.exe
  20. 30 0
      Publish/DevOps3/environment/README.md
  21. 20 0
      Publish/DevOps3/environment/build-bash__10.Test__#1.InitEnv.sh
  22. 18 0
      Publish/DevOps3/environment/build-bash__10.Test__#3.CleanEnv.sh
  23. 21 0
      Publish/DevOps3/environment/build-bash__40.Station-publish__#4_copyExtraReleaseFiles.sh.bak
  24. 20 0
      Publish/DevOps3/environment/build-bash__41.extra-publish.sh.bak
  25. 17 0
      Publish/DevOps3/environment/build-bash__52.docker-extra-copy.sh.bak
  26. 1 0
      Publish/DevOps3/environment/env.APPNAME.txt
  27. 1 0
      Publish/DevOps3/environment/env.envName.txt
  28. 2 0
      Publish/DevOps3/environment/jenkins_NoNeedApprovalForBuild.txt
  29. 30 0
      Publish/DevOps3/github-bash/74.github-push-to-webdav.sh
  30. 60 0
      Publish/DevOps3/github-bash/76.github-push-release.sh
  31. 71 0
      Publish/DevOps3/github-bash/startup.bash
  32. 169 0
      Publish/DevOps3/jenkins-bash/CICD.ki.git_Multibranch.deploy.jenkinsfile
  33. 43 0
      Publish/DevOps3/release-bash/71.file-zip.sh
  34. 38 0
      Publish/DevOps3/release-bash/72.nuget-push.sh
  35. 39 0
      Publish/DevOps3/release-bash/73.docker-image-build-push.sh
  36. 41 0
      Publish/DevOps3/release-bash/74.docker-image-build-push_amd64.bash
  37. 71 0
      Publish/DevOps3/release-bash/75.docker-image-build-push_cross.bash
  38. 47 0
      Publish/DevOps3/release-bash/78.push-releaseFiles-to-webdav.bash
  39. 57 0
      Publish/DevOps3/release-bash/startup.bash
  40. 306 1
      README.md
  41. 56 0
      Vitorm.Excel.sln
  42. 10 0
      clean-temp.bat
  43. 1 0
      doc/ReleaseLog.md
  44. 2 0
      doc/TODO.md
  45. BIN
      doc/vitorm_logo_v1.png
  46. 6 0
      src/Versions.props
  47. 20 0
      src/Vitorm.Excel/DataProvider.cs
  48. 47 0
      src/Vitorm.Excel/DbConfig.cs
  49. 66 0
      src/Vitorm.Excel/DbContext.cs
  50. 562 0
      src/Vitorm.Excel/DbSet.cs
  51. 39 0
      src/Vitorm.Excel/Vitorm.Excel.csproj
  52. 76 0
      test/Vitorm.Excel.Data.MsTest/CommonTest/Common_Test.cs
  53. 20 0
      test/Vitorm.Excel.Data.MsTest/CommonTest/UserTest.Entity.cs
  54. 291 0
      test/Vitorm.Excel.Data.MsTest/CommonTest/UserTest.cs
  55. 214 0
      test/Vitorm.Excel.Data.MsTest/CommonTest/UserTestAsync.cs
  56. 37 0
      test/Vitorm.Excel.Data.MsTest/Vitorm.Excel.Data.MsTest.csproj
  57. 11 0
      test/Vitorm.Excel.Data.MsTest/appsettings.json
  58. 170 0
      test/Vitorm.Excel.MsTest/CommonTest/CRUDAsync_Test.cs
  59. 168 0
      test/Vitorm.Excel.MsTest/CommonTest/CRUD_Test.cs
  60. 80 0
      test/Vitorm.Excel.MsTest/CommonTest/ChangeDatabase_Test.cs
  61. 71 0
      test/Vitorm.Excel.MsTest/CommonTest/ChangeTable_Test.cs
  62. 169 0
      test/Vitorm.Excel.MsTest/CommonTest/EntityLoader_CustomLoader_Test.cs
  63. 56 0
      test/Vitorm.Excel.MsTest/CommonTest/EntityLoader_Test.cs
  64. 89 0
      test/Vitorm.Excel.MsTest/CommonTest/Property_Bool_Test.cs
  65. 58 0
      test/Vitorm.Excel.MsTest/CommonTest/Property_DateTime_Test.cs
  66. 37 0
      test/Vitorm.Excel.MsTest/CommonTest/Property_Numeric_Calculate_Test.cs
  67. 129 0
      test/Vitorm.Excel.MsTest/CommonTest/Property_Numeric_Test.cs
  68. 39 0
      test/Vitorm.Excel.MsTest/CommonTest/Property_String_Calculate_Test.cs
  69. 50 0
      test/Vitorm.Excel.MsTest/CommonTest/Property_String_Like_Test.cs
  70. 94 0
      test/Vitorm.Excel.MsTest/CommonTest/Property_String_Test.cs
  71. 57 0
      test/Vitorm.Excel.MsTest/CommonTest/Property_Test.cs
  72. 235 0
      test/Vitorm.Excel.MsTest/CommonTest/Query_Count_Test.cs
  73. 68 0
      test/Vitorm.Excel.MsTest/CommonTest/Query_Distinct_Test.cs
  74. 42 0
      test/Vitorm.Excel.MsTest/CommonTest/Query_FilterRule_Test.cs
  75. 40 0
      test/Vitorm.Excel.MsTest/CommonTest/Query_FilterRule_WithJoin_Test.cs
  76. 176 0
      test/Vitorm.Excel.MsTest/CommonTest/Query_Group_Test.cs
  77. 210 0
      test/Vitorm.Excel.MsTest/CommonTest/Query_InnerJoin_ByJoin_Test.cs
  78. 187 0
      test/Vitorm.Excel.MsTest/CommonTest/Query_InnerJoin_BySelectMany_Test.cs
  79. 105 0
      test/Vitorm.Excel.MsTest/CommonTest/Query_LeftJoin_ByGroupJoin_Test.cs
  80. 186 0
      test/Vitorm.Excel.MsTest/CommonTest/Query_LeftJoin_BySelectMany_Test.cs
  81. 388 0
      test/Vitorm.Excel.MsTest/CommonTest/Query_Method_Test.cs
  82. 86 0
      test/Vitorm.Excel.MsTest/CommonTest/Query_ScopeParam_LeftJoin_Test.cs
  83. 77 0
      test/Vitorm.Excel.MsTest/CommonTest/Query_ScopeParam_Test.cs
  84. 168 0
      test/Vitorm.Excel.MsTest/CommonTest/Query_Select_Test.cs
  85. 24 0
      test/Vitorm.Excel.MsTest/CommonTest/Schema_Test.cs
  86. 57 0
      test/Vitorm.Excel.MsTest/CommonTest/Truncate_Test.cs
  87. 112 0
      test/Vitorm.Excel.MsTest/DataSource.cs
  88. 29 0
      test/Vitorm.Excel.MsTest/Vitorm.Excel.MsTest.csproj

+ 101 - 0
.github/workflows/ki_devops3_build.yml

@@ -0,0 +1,101 @@
+# This is a basic workflow to help you get started with Actions
+
+name: ki_devops3_build
+
+# Controls when the action will run. 
+on:
+  
+  #push:
+    # Triggers the workflow on push events for the branches start with release
+    #branches:: [ 'release/**' ]
+
+    # Triggers the workflow on push events but only for the master branch
+    #branches: [ master ]
+
+    # Triggers the workflow on push tag
+    #tags: ['*']
+    
+  # Allows you to run this workflow manually from the Actions tab
+  workflow_dispatch:
+  merge_group:
+
+# A workflow run is made up of one or more jobs that can run sequentially or in parallel
+jobs:
+
+  # This workflow contains a single job called "devops3_test"
+  devops3_test:
+    # The type of runner that the job will run on
+    runs-on: ubuntu-latest
+
+    # Steps represent a sequence of tasks that will be executed as part of the job
+    steps:
+      # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
+      - uses: actions/checkout@v4
+
+      # Runs a set of commands using the runners shell
+      - name: Run test
+        run: |
+           set -e
+           echo call ./Publish/DevOps3/build-bash/10.Test.bash to test
+
+           cd ./Publish/DevOps3/build-bash
+           bash 10.Test.bash;
+           echo run test succeed!
+
+  # This workflow contains a single job called "devops3_build"
+  devops3_build:
+    # Requiring successful dependent jobs
+    needs: devops3_test
+
+    # The type of runner that the job will run on
+    runs-on: ubuntu-latest
+
+    # Steps represent a sequence of tasks that will be executed as part of the job
+    steps:
+      # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
+      - uses: actions/checkout@v4
+
+      # Runs a set of commands using the runners shell
+      - name: Run build
+        run: |
+           set -e
+           echo call ./Publish/DevOps3/github-bash/startup.bash to build
+
+           export DOCKER_ImagePrefix="serset/"
+           export DOCKER_USERNAME="${{ secrets.DOCKER_USERNAME  }}"
+           export DOCKER_PASSWORD="${{ secrets.DOCKER_PASSWORD }}"
+           export NUGET_SERVER="${{ secrets.NUGET_SERVER  }}"
+           export NUGET_KEY="${{ secrets.NUGET_KEY }}"
+           export WebDav_BaseUrl="${{ secrets.WebDav_BaseUrl }}"
+           export WebDav_User="${{ secrets.WebDav_User }}"
+           cd ./Publish/DevOps3/github-bash
+           bash startup.bash
+           echo build succeed!
+
+           # echo "appName=${APPNAME}" >> $GITHUB_ENV
+
+
+      - name: release_create
+        id: release_create
+        uses: actions/create-release@v1
+        #if: hashFiles(env.release_assetPath)
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+        with:
+          release_name: ${{ env.release_name }}
+          tag_name: ${{ env.release_tag }}
+          draft: ${{ env.release_draft }}
+          prerelease: ${{ env.release_prerelease }}
+          body: ${{ env.release_body }}
+
+
+      # release_upload_asset
+
+      - uses: actions/upload-release-asset@v1
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+        with:
+          upload_url: ${{ steps.release_create.outputs.upload_url }}
+          asset_path: ${{ env.release_dirPath }}/${{ env.appName }}-nuget-${{ env.release_version }}.zip
+          asset_name: ${{ env.appName }}-nuget-${{ env.release_version }}.zip
+          asset_content_type: application/zip

+ 45 - 0
.github/workflows/ki_devops3_test.yml

@@ -0,0 +1,45 @@
+# This is a basic workflow to help you get started with Actions
+
+name: ki_devops3_test
+
+# Controls when the action will run. 
+on:
+  
+  #push:
+    # Triggers the workflow on push events for the branches start with release
+    #branches:: [ 'release/**' ]
+
+    # Triggers the workflow on push events but only for the master branch
+    #branches: [ master ]
+
+    # Triggers the workflow on push tag
+    #tags: ['*']
+    
+  # Allows you to run this workflow manually from the Actions tab
+  workflow_dispatch:
+
+  pull_request:
+    types: [opened, synchronize, reopened]
+  merge_group:
+
+# A workflow run is made up of one or more jobs that can run sequentially or in parallel
+jobs:
+  # This workflow contains a single job called "devops3_test"
+  devops3_test:
+    # The type of runner that the job will run on
+    runs-on: ubuntu-latest
+
+    # Steps represent a sequence of tasks that will be executed as part of the job
+    steps:
+      # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
+      - uses: actions/checkout@v4
+
+      # Runs a set of commands using the runners shell
+      - name: Run test
+        run: |
+           set -e
+           echo call ./Publish/DevOps3/build-bash/10.Test.bash to test
+
+           cd ./Publish/DevOps3/build-bash
+           bash 10.Test.bash;
+           echo run test succeed!

+ 201 - 0
LICENSE

@@ -0,0 +1,201 @@
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.

+ 86 - 0
Publish/DevOps3/build-bash/10.Test.bash

@@ -0,0 +1,86 @@
+set -e
+
+
+#---------------------------------------------------------------------
+# args
+args_="
+
+export basePath=/root/temp/svn
+export NUGET_PATH=$basePath/Publish/release/.nuget
+
+# "
+
+
+#----------------------------------------------
+# init args
+if [ -z "$basePath" ]; then export basePath=$PWD/../../..; fi
+export devOpsPath="$PWD/.."
+
+if [ ! $NUGET_PATH ]; then NUGET_PATH=$basePath/Publish/release/.nuget; fi
+
+
+#----------------------------------------------
+echo "#10.Test.bash -> #1 init test environment"
+bashFile="$devOpsPath/environment/build-bash__10.Test__#1.InitEnv.sh"
+if [ -f "$bashFile" ]; then
+	echo "#10.Test.bash -> #1 init test environment - Run bash"
+	sh "$bashFile"
+fi
+
+
+#----------------------------------------------
+echo "#10.Test.bash -> #2 find test projects and run test"
+
+docker run -i --rm \
+--net=host \
+--env LANG=C.UTF-8 \
+-v $NUGET_PATH:/root/.nuget \
+-v "$basePath":/root/code \
+-v "$basePath":"$basePath" \
+serset/dotnet:sdk-6.0 \
+bash -c "
+set -e
+
+cd /root/code
+
+#2.1 skip if no test projects
+if grep '<test>' -r --include *.csproj; then
+	echo '#10.Test.bash -> got projects need to test'
+else
+	echo '#10.Test.bash -> skip for no project needs to test'
+	exit 0
+fi
+
+#2.2 run test
+echo '#10.Test.bash -> #2.2 run test...'
+for file in \$(grep -a '<test>' . -rl --include *.csproj)
+do
+	echo '#10.Test.bash -> #2.2.1 run test:'
+	echo run test for project \"\$file\"
+
+	# run test
+	cd /root/code
+	cd \$(dirname \"\$file\")
+	dotnet test
+done
+
+
+"
+#----------------------------------------------
+echo "#10.Test.bash -> #3 clean test environment"
+bashFile="$devOpsPath/environment/build-bash__10.Test__#3.CleanEnv.sh"
+if [ -f "$bashFile" ]; then
+	echo "#10.Test.bash -> #1 Clean test environment - Run bash"
+	sh "$bashFile"
+fi
+
+
+
+#----------------------------------------------
+
+echo '#10.Test.bash -> success!'
+
+
+
+
+

+ 42 - 0
Publish/DevOps3/build-bash/19.get-app-version.bash

@@ -0,0 +1,42 @@
+set -e
+
+# export versionSuffix='.1234.preview'
+# bash 19.get-app-version.bash
+
+
+#---------------------------------------------------------------------
+# args
+args_="
+
+export versionSuffix='  '
+
+# "
+
+# remove spaces
+versionSuffix=${versionSuffix// /}
+
+#----------------------------------------------
+# basePath
+if [ -z "$basePath" ]; then basePath=$PWD/../../..; fi
+
+
+
+#----------------------------------------------
+echo "#1 get appVersion"
+# get csproj file with appVersion tag, if not exist get file with pack or publish tag
+csprojPath=$(find ${basePath} -name *.props -exec grep '<Version>' -l {} \; | head -n 1);
+if [ ! -f "$csprojPath" ]; then csprojPath=$(find ${basePath} -name *.csproj -exec grep '<appVersion>' -l {} \; | head -n 1);  fi
+if [ ! -f "$csprojPath" ]; then csprojPath=$(find ${basePath} -name *.csproj -exec grep '<pack>\|<publish>' -l {} \; | head -n 1);  fi
+if [ -f "$csprojPath" ]; then export appVersion=`grep '<Version>' "$csprojPath" | grep -oE '\>(.*)\<' | tr -d '<>/'`;  fi
+echo "appVersion from csproj: $appVersion"
+
+# get v1 v2 v3
+v1=$(echo $appVersion | tr '.' '\n' | sed -n 1p)
+v2=$(echo $appVersion | tr '.' '\n' | sed -n 2p)
+v3=$(echo $appVersion | tr '.-' '\n' | sed -n 3p)
+
+
+#export nextAppVersion="${appVersion%%-*}$versionSuffix"
+export nextAppVersion="$v1.$v2.$v3$versionSuffix"
+echo "nextAppVersion: $nextAppVersion"
+

+ 47 - 0
Publish/DevOps3/build-bash/20.change-app-version.bash

@@ -0,0 +1,47 @@
+set -e
+
+# export versionSuffix='.1234.preview'
+# bash 20.change-app-version.bash
+
+
+#---------------------------------------------------------------------
+# args
+args_="
+
+export versionSuffix='  '
+
+# "
+
+# remove spaces
+versionSuffix=${versionSuffix// /}
+
+#----------------------------------------------
+# basePath
+if [ -z "$basePath" ]; then basePath=$PWD/../../..; fi
+
+
+
+#----------------------------------------------
+echo "#1 get appVersion"
+# get csproj file with appVersion tag, if not exist get file with pack or publish tag
+csprojPath=$(find ${basePath} -name *.props -exec grep '<Version>' -l {} \; | head -n 1);
+if [ ! -f "$csprojPath" ]; then csprojPath=$(find ${basePath} -name *.csproj -exec grep '<appVersion>' -l {} \; | head -n 1);  fi
+if [ ! -f "$csprojPath" ]; then csprojPath=$(find ${basePath} -name *.csproj -exec grep '<pack>\|<publish>' -l {} \; | head -n 1);  fi
+if [ -f "$csprojPath" ]; then export appVersion=`grep '<Version>' "$csprojPath" | grep -oE '\>(.*)\<' | tr -d '<>/'`;  fi
+echo "appVersion from csproj: $appVersion"
+
+# get v1 v2 v3
+v1=$(echo $appVersion | tr '.' '\n' | sed -n 1p)
+v2=$(echo $appVersion | tr '.' '\n' | sed -n 2p)
+v3=$(echo $appVersion | tr '.-' '\n' | sed -n 3p)
+
+
+#export nextAppVersion="${appVersion%%-*}$versionSuffix"
+export nextAppVersion="$v1.$v2.$v3$versionSuffix"
+echo "nextAppVersion: $nextAppVersion"
+
+
+#----------------------------------------------
+echo "#2 change app version from [$appVersion] to [$nextAppVersion]" 
+sed -i 's/'"<Version>$appVersion<\/Version>"'/'"<Version>$nextAppVersion<\/Version>"'/g'  `find ${basePath} -name *.csproj -exec grep '<pack>\|<publish>' -l {} \;`
+

+ 48 - 0
Publish/DevOps3/build-bash/21.change-to-next-version.bash

@@ -0,0 +1,48 @@
+set -e
+
+# export versionSuffix='.1234.preview'
+# bash 21.change-to-next-version.bash
+
+
+#---------------------------------------------------------------------
+# args
+args_="
+
+export versionSuffix='  '
+
+# "
+
+# remove spaces
+versionSuffix=${versionSuffix// /}
+
+#----------------------------------------------
+# basePath
+if [ -z "$basePath" ]; then basePath=$PWD/../../..; fi
+
+
+
+#----------------------------------------------
+echo "#1 get appVersion"
+# get csproj file with appVersion tag, if not exist get file with pack or publish tag
+csprojPath=$(find ${basePath} -name *.props -exec grep '<Version>' -l {} \; | head -n 1);
+if [ ! -f "$csprojPath" ]; then csprojPath=$(find ${basePath} -name *.csproj -exec grep '<appVersion>' -l {} \; | head -n 1);  fi
+if [ ! -f "$csprojPath" ]; then csprojPath=$(find ${basePath} -name *.csproj -exec grep '<pack>\|<publish>' -l {} \; | head -n 1);  fi
+if [ -f "$csprojPath" ]; then export appVersion=`grep '<Version>' "$csprojPath" | grep -oE '\>(.*)\<' | tr -d '<>/'`;  fi
+echo "appVersion from csproj: $appVersion"
+
+# get v1 v2 v3
+v1=$(echo $appVersion | tr '.' '\n' | sed -n 1p)
+v2=$(echo $appVersion | tr '.' '\n' | sed -n 2p)
+v3=$(echo $appVersion | tr '.-' '\n' | sed -n 3p)
+((v3++));
+
+
+#export nextAppVersion="${appVersion%%-*}$versionSuffix"
+export nextAppVersion="$v1.$v2.$v3$versionSuffix"
+echo "nextAppVersion: $nextAppVersion"
+
+#----------------------------------------------
+echo "#2 change app version from [$appVersion] to [$nextAppVersion]" 
+sed -i 's/'"<Version>$appVersion<\/Version>"'/'"<Version>$nextAppVersion<\/Version>"'/g'  `find ${basePath} -name *.csproj -exec grep '<pack>\|<publish>' -l {} \;`
+
+

+ 42 - 0
Publish/DevOps3/build-bash/22.add-suffix-to-app-version.bash

@@ -0,0 +1,42 @@
+set -e
+
+# export versionSuffix='.1234.preview'
+# bash 22.add-suffix-to-app-version.bash
+
+
+#---------------------------------------------------------------------
+# args
+args_="
+
+export versionSuffix='  '
+
+# "
+
+# remove spaces
+versionSuffix=${versionSuffix// /}
+
+#----------------------------------------------
+# basePath
+if [ -z "$basePath" ]; then basePath=$PWD/../../..; fi
+
+
+
+#----------------------------------------------
+echo "#1 get appVersion"
+# get csproj file with appVersion tag, if not exist get file with pack or publish tag
+csprojPath=$(find ${basePath} -name *.props -exec grep '<Version>' -l {} \; | head -n 1);
+if [ ! -f "$csprojPath" ]; then csprojPath=$(find ${basePath} -name *.csproj -exec grep '<appVersion>' -l {} \; | head -n 1);  fi
+if [ ! -f "$csprojPath" ]; then csprojPath=$(find ${basePath} -name *.csproj -exec grep '<pack>\|<publish>' -l {} \; | head -n 1);  fi
+if [ -f "$csprojPath" ]; then export appVersion=`grep '<Version>' "$csprojPath" | grep -oE '\>(.*)\<' | tr -d '<>/'`;  fi
+echo "appVersion from csproj: $appVersion"
+
+
+
+export nextAppVersion="${appVersion}${versionSuffix}"
+echo "nextAppVersion: $nextAppVersion"
+
+
+#----------------------------------------------
+echo "#2 change app version from [$appVersion] to [$nextAppVersion]" 
+sed -i 's/'"<Version>$appVersion<\/Version>"'/'"<Version>$nextAppVersion<\/Version>"'/g'  `find ${basePath} -name *.csproj -exec grep '<pack>\|<publish>' -l {} \;`
+

+ 43 - 0
Publish/DevOps3/build-bash/30.nuget-pack.sh

@@ -0,0 +1,43 @@
+set -e
+
+# bash 30.nuget-pack.sh
+
+
+#---------------------------------------------------------------------
+# args
+args_="
+
+export basePath=/root/temp/svn
+export NUGET_PATH=$basePath/Publish/release/.nuget
+
+# "
+
+if [ ! $NUGET_PATH ]; then NUGET_PATH=$basePath/Publish/release/.nuget; fi
+
+
+
+#----------------------------------------------
+echo "30.nuget-pack.sh"
+docker run -i --rm \
+--env LANG=C.UTF-8 \
+-v $NUGET_PATH:/root/.nuget \
+-v $basePath:/root/code \
+serset/dotnet:sdk-6.0 \
+bash -c "
+
+publishPath=/root/code/Publish/release/release/nuget
+
+cd /root/code
+for file in \$(grep -a '<pack>nuget</pack>' . -rl --include *.csproj)
+do
+	echo pack \$file
+	mkdir -p \$publishPath
+	cd /root/code
+	cd \$(dirname \"\$file\")
+	dotnet build --configuration Release
+	dotnet pack --configuration Release --output \"\$publishPath\"
+done
+"
+
+
+ 

+ 92 - 0
Publish/DevOps3/build-bash/40.Station-publish.sh

@@ -0,0 +1,92 @@
+set -e
+
+
+#---------------------------------------------------------------------
+# args
+args_="
+
+export basePath=/root/temp/svn
+export NUGET_PATH=$basePath/Publish/release/.nuget
+
+# "
+
+if [ ! $NUGET_PATH ]; then NUGET_PATH=$basePath/Publish/release/.nuget; fi
+
+
+#----------------------------------------------
+echo "#40.Station-publish.sh -> find projects and build"
+
+export devOpsPath="$PWD/.."
+
+docker run -i --rm \
+--env LANG=C.UTF-8 \
+-v $NUGET_PATH:/root/.nuget \
+-v "$basePath":/root/code \
+-v "$basePath":"$basePath" \
+serset/dotnet:sdk-6.0 \
+bash -c "
+set -e
+
+cd /root/code
+
+if grep '<publish>' -r --include *.csproj; then
+	echo '#40.Station-publish.sh -> got projects need to be built'
+else
+	echo '#40.Station-publish.sh -> skip for no project needs to be built'
+	exit 0
+fi
+
+echo '#1 get netVersion'
+export netVersion=\$(grep '<TargetFramework>' \$(grep '<publish>' -rl --include *.csproj | head -n 1) | grep -oP '>(.*)<' | tr -d '<>')
+echo netVersion: \$netVersion
+
+
+export publishPath=/root/code/Publish/release/release/Station\(\$netVersion\)
+mkdir -p \$publishPath
+
+echo '#2 publish station'
+for file in \$(grep -a '<publish>' . -rl --include *.csproj)
+do	
+	# get publishName
+	cd /root/code
+	publishName=\`grep '<publish>' \$file -r | grep -oP '>(.*)<' | tr -d '<>'\`
+
+	echo publish \$publishName
+
+	# publish
+	cd \$(dirname \"\$file\")
+	dotnet build --configuration Release
+	dotnet publish --configuration Release --output \"\$publishPath/\$publishName\"
+
+	#copy xml
+	for filePath in bin/Release/\$netVersion/*.xml ; do \\cp -rf \$filePath \"\$publishPath/\$publishName\";done
+done
+
+
+#3 copy station release files
+if [ -d \"\/root/code/Publish/ReleaseFile/Station\" ]; then
+	echo '#3 copy station release files'
+	\cp -rf \/root/code/Publish/ReleaseFile/Station/. \"\$publishPath\"
+fi
+
+
+#4 copy extra release files
+bashFile=\"$devOpsPath/environment/build-bash__40.Station-publish__#4_copyExtraReleaseFiles.sh\"
+if [ -f \"\$bashFile\" ]; then
+	echo '#4 copy extra release files'
+	sh \"\$bashFile\"
+fi
+
+
+
+
+"
+
+
+
+echo '#40.Station-publish.sh -> success!'
+
+
+
+
+

+ 23 - 0
Publish/DevOps3/build-bash/41.extra-publish.sh

@@ -0,0 +1,23 @@
+set -e
+
+
+#---------------------------------------------------------------------
+# args
+args_="
+
+export basePath=/root/temp/svn
+export NUGET_PATH=$basePath/Publish/release/.nuget
+
+# "
+
+
+#---------------------------------------------------------------------
+#41.extra-publish.sh
+bashFile="$PWD/../environment/build-bash__41.extra-publish.sh"
+if [ -f "$bashFile" ]; then
+	echo '#41.extra-publish.sh'
+	sh "$bashFile"
+fi
+
+
+

+ 46 - 0
Publish/DevOps3/build-bash/50.docker-image-copy.sh

@@ -0,0 +1,46 @@
+set -e
+
+
+#---------------------------------------------------------------------
+# args
+args_="
+
+export basePath=/root/temp/svn
+
+# "
+
+ 
+#---------------------------------------------------------------------
+#1 copy docker image from ReleaseFile
+
+publishPath="$basePath/Publish/release/release/Station(net6.0)"
+dockerPath=$basePath/Publish/release/release/docker-image
+
+
+if [ -d "$basePath/Publish/ReleaseFile/docker-image" ]; then
+	echo "50.docker-image-copy.sh -> #1 copy docker image from ReleaseFile"
+	\cp -rf "$basePath/Publish/ReleaseFile/docker-image/." "$dockerPath"
+fi
+
+
+
+
+#---------------------------------------------------------------------
+echo "50.docker-image-copy.sh -> #2 copy station"
+for file in $(find $basePath -name *.csproj -exec grep '<docker>' -l {} \;)
+do
+	cd $basePath
+	
+	publishName=`grep '<publish>' $file -r | grep -oE '\>(.*)\<' | tr -d '<>/'`
+	
+	dockerName=`grep '<docker>' $file -r | grep -oE '\>(.*)\<' | tr -d '<>/'`
+
+	echo "#2.* copy $dockerName, publishName:$publishName"
+	\cp -rf "$publishPath/$publishName/." "$dockerPath/$dockerName/app"
+done
+
+
+
+
+
+

+ 27 - 0
Publish/DevOps3/build-bash/51.docker-deploy-copy.sh

@@ -0,0 +1,27 @@
+set -e
+
+
+#---------------------------------------------------------------------
+# args
+args_="
+
+export basePath=/root/temp/svn
+
+# "
+
+ 
+#---------------------------------------------------------------------
+#2
+publishPath="$basePath/Publish/release/release/Station(net6.0)"
+deployPath="$basePath/Publish/release/release/docker-deploy"
+
+
+
+#----------------------------------------------
+#3 copy dir
+if [ -d "$basePath/Publish/ReleaseFile/docker-deploy" ]; then
+	echo "51.docker-deploy-copy.sh -> copy dir"
+	\cp -rf "$basePath/Publish/ReleaseFile/docker-deploy/." "$deployPath"
+fi
+
+

+ 23 - 0
Publish/DevOps3/build-bash/52.docker-extra-copy.sh

@@ -0,0 +1,23 @@
+set -e
+
+
+#---------------------------------------------------------------------
+# args
+args_="
+
+export basePath=/root/temp/svn
+export NUGET_PATH=$basePath/Publish/release/.nuget
+
+# "
+
+
+#---------------------------------------------------------------------
+#52.docker-extra-copy.sh
+bashFile="$PWD/../environment/build-bash__52.docker-extra-copy.sh"
+if [ -f "$bashFile" ]; then
+	echo '#52.docker-extra-copy.sh'
+	sh "$bashFile"
+fi
+
+
+

+ 32 - 0
Publish/DevOps3/build-bash/startup.bash

@@ -0,0 +1,32 @@
+set -e
+
+# cd build-bash; bash startup.bash;
+
+#----------------------------------------------
+# cur path
+curPath=$PWD
+
+cd $curPath/../../..
+export basePath="$PWD"
+cd $curPath
+
+
+
+
+
+#----------------------------------------------
+echo "build-bash/startup.bash"
+
+for file in *.sh
+do
+    echo "-----------------------------------------------------------------"
+    echo "[$(date "+%H:%M:%S")] sh $file"
+    sh $file
+done
+
+
+
+ 
+#----------------------------------------------
+#
+cd $curPath

+ 23 - 0
Publish/DevOps3/build-cmd/30.nuget-pack.bat

@@ -0,0 +1,23 @@
+@echo off
+
+::1 get basePath
+set curPath=%cd%
+cd /d "%~dp0"
+cd /d ../../..
+set basePath=%cd%
+set nugetPath=%basePath%/Publish/release/release/nuget
+
+::2 find nuget projects and pack
+for /f "delims=" %%f in ('findstr /M /s /i "<pack>nuget</pack>" *.csproj') do (
+	echo pack %basePath%\%%f\..
+	cd /d "%basePath%\%%f\.."
+	dotnet build --configuration Release
+	dotnet pack --configuration Release --output "%nugetPath%"
+	@if errorlevel 1 (echo . & echo .  & echo error & pause) 
+)
+
+
+echo %~n0.bat success
+
+
+cd /d "%curPath%"

+ 9 - 0
Publish/DevOps3/build-cmd/Clean-All.bat

@@ -0,0 +1,9 @@
+VsTool.exe delete --path "..\..\.." --file "*.suo|*.user" --directory "obj|bin|.vs|packages"
+
+
+rd /s/q ..\..\release
+
+
+echo %~n0.bat success
+
+pause

+ 1 - 0
Publish/DevOps3/build-cmd/Clean-ReleaseFiles.bat

@@ -0,0 +1 @@
+rd /s/q ..\..\release

BIN
Publish/DevOps3/build-cmd/VsTool.exe


+ 30 - 0
Publish/DevOps3/environment/README.md

@@ -0,0 +1,30 @@
+
+# DevOps 3.6
+
+
+# build-bash
+extra steps for build, all sh files are optional (remove if not need).
+
+
+# jenkins_NoNeedApprovalForBuild.txt
+if this file exists, will not need approval for jenkins build.
+
+
+----------------------------------------------
+# ReleaseLog
+
+-----------------------
+# 3.6
+> 2024-08-30
+
+- able to get release version from .props file
+- [github actions] auto trigger test workflow for PR in checks
+- [github actions] auto trigger test workflow for PR in checks
+
+-----------------------
+# 3.5
+> 2024-08-24
+
+- support tester
+
+

+ 20 - 0
Publish/DevOps3/environment/build-bash__10.Test__#1.InitEnv.sh

@@ -0,0 +1,20 @@
+set -e
+
+
+#---------------------------------------------------------------------
+# args
+
+args_="
+
+export basePath=/root/temp
+
+# "
+
+
+#---------------------------------------------------------------------
+echo '#build-bash__10.Test__#1.InitEnv.sh '
+
+
+
+#---------------------------------------------------------------------
+echo '#build-bash__10.Test__#1.InitEnv.sh -> #9 init test environment success!'

+ 18 - 0
Publish/DevOps3/environment/build-bash__10.Test__#3.CleanEnv.sh

@@ -0,0 +1,18 @@
+set -e
+
+
+#---------------------------------------------------------------------
+# args
+
+args_="
+
+export basePath=/root/temp
+
+# "
+
+
+#---------------------------------------------------------------------
+echo '#build-bash__10.Test_#3.CleanEnv.sh'
+
+
+

+ 21 - 0
Publish/DevOps3/environment/build-bash__40.Station-publish__#4_copyExtraReleaseFiles.sh.bak

@@ -0,0 +1,21 @@
+set -e
+
+#---------------------------------------------------------------------
+# args
+args_="
+
+export basePath=/root/temp
+export netVersion=net6.0
+export publishPath=\"$basePath/Publish/release/release/Station($netVersion)\"
+
+# "
+
+
+
+#----------------------------------------------
+echo '#build-bash__40.Station-publish__#4_copyExtraReleaseFiles.sh'
+
+
+
+
+

+ 20 - 0
Publish/DevOps3/environment/build-bash__41.extra-publish.sh.bak

@@ -0,0 +1,20 @@
+set -e
+
+
+#---------------------------------------------------------------------
+# args
+args_="
+
+export basePath=/root/temp
+export NUGET_PATH=$basePath/Publish/release/.nuget
+
+# "
+
+if [ ! $NUGET_PATH ]; then NUGET_PATH=$basePath/Publish/release/.nuget; fi
+
+
+#---------------------------------------------------------------------
+echo '#build-bash__41.extra-publish.sh'
+
+
+

+ 17 - 0
Publish/DevOps3/environment/build-bash__52.docker-extra-copy.sh.bak

@@ -0,0 +1,17 @@
+set -e
+
+
+#---------------------------------------------------------------------
+# args
+
+args_="
+
+export basePath=/root/temp
+
+# "
+
+
+#---------------------------------------------------------------------
+echo '#build-bash__52.docker-extra-copy.sh'
+
+

+ 1 - 0
Publish/DevOps3/environment/env.APPNAME.txt

@@ -0,0 +1 @@
+Vitorm.Excel

+ 1 - 0
Publish/DevOps3/environment/env.envName.txt

@@ -0,0 +1 @@
+ki

+ 2 - 0
Publish/DevOps3/environment/jenkins_NoNeedApprovalForBuild.txt

@@ -0,0 +1,2 @@
+# jenkins_NoNeedApprovalForBuild.txt
+if this file exists, will not need approval for jenkins build.

+ 30 - 0
Publish/DevOps3/github-bash/74.github-push-to-webdav.sh

@@ -0,0 +1,30 @@
+set -e
+
+
+#---------------------------------------------------------------------
+# args
+args_="
+
+export basePath=/root/temp/svn
+
+export APPNAME=xxxx
+export appVersion=xxxx
+
+export WebDav_BaseUrl="https://nextcloud.xxx.com/remote.php/dav/files/release/releaseFiles/ki_jenkins"
+export WebDav_User="username:pwd"
+
+# "
+
+
+
+
+
+#----------------------------------------------
+if [ -z "$WebDav_BaseUrl" ]; then
+	echo "github skip pushing release file to WebDav because invalid WebDav endpoint"
+else
+	echo "github push release file to WebDav"
+	bash "$PWD/../release-bash/78.push-releaseFiles-to-webdav.bash";
+fi
+
+

+ 60 - 0
Publish/DevOps3/github-bash/76.github-push-release.sh

@@ -0,0 +1,60 @@
+set -e
+
+
+#---------------------------------------------------------------------
+# args
+args_="
+
+export basePath=/root/temp/svn
+
+export appVersion=1.0-preview
+
+export APPNAME=xxxxxx
+
+# "
+
+
+ 
+
+
+#---------------------------------------------------------------------
+#2 init environment for github release
+
+
+
+echo "appName=${APPNAME}" >> $GITHUB_ENV
+
+echo "release_name=${APPNAME}-${appVersion}" >> $GITHUB_ENV
+echo "release_tag=${appVersion}" >> $GITHUB_ENV
+
+echo "release_draft=false" >> $GITHUB_ENV
+echo "release_prerelease=false" >> $GITHUB_ENV
+
+echo "release_body=${APPNAME} ${appVersion}" >> $GITHUB_ENV
+
+
+echo "release_dirPath=${basePath}/Publish/release/release-zip" >> $GITHUB_ENV
+echo "release_version=${appVersion}" >> $GITHUB_ENV
+
+#filePath=$basePath/Publish/release/release-zip/Sers-ServiceCenter(net6.0)-${appVersion}.zip
+#fileType="${filePath##*.}"
+#echo "release_assetPath=${filePath}" >> $GITHUB_ENV
+#echo "release_assetName=${APPNAME}-${appVersion}.${fileType}" >> $GITHUB_ENV
+#echo "release_contentType=application/zip" >> $GITHUB_ENV
+
+
+# draft or preivew
+if [ "preview" = "$(echo $appVersion | tr -d \"0-9\-\\.\")" ]
+then
+  echo preivew
+  echo "release_prerelease=true" >> $GITHUB_ENV
+else
+  if  [ "" = "$(echo $appVersion | tr -d \"0-9\-\\.\")" ]
+  then
+    echo release
+  else
+    echo draft
+    echo "release_draft=true" >> $GITHUB_ENV
+  fi
+fi
+

+ 71 - 0
Publish/DevOps3/github-bash/startup.bash

@@ -0,0 +1,71 @@
+set -e
+
+# cd github-bash; bash startup.bash;
+
+#---------------------------------------------------------------------
+# args
+args_="
+
+#export APPNAME=xxxxxx
+
+export DOCKER_USERNAME=serset
+export DOCKER_PASSWORD=xxxxxx
+
+export NUGET_SERVER=https://api.nuget.org/v3/index.json
+export NUGET_KEY=xxxxxx
+
+export WebDav_BaseUrl="https://nextcloud.xxx.com/remote.php/dav/files/release/releaseFiles/ki_jenkins"
+export WebDav_User="username:pwd"
+
+# "
+
+#----------------------------------------------
+# cur path
+curPath=$PWD
+
+cd $curPath/../../..
+export basePath="$PWD"
+cd $curPath
+
+export devOpsPath="$PWD/.."
+
+# export basePath=/root/temp/svn
+
+if [ ! $APPNAME ]; then 
+	export APPNAME=$(cat "$devOpsPath/environment/env.APPNAME.txt" | tr -d '\n')
+	echo "APPNAME: [${APPNAME}]" 
+fi
+
+
+#---------------------------------------------- 
+echo '#1 build'
+cd "$devOpsPath/build-bash"; bash startup.bash;
+
+#---------------------------------------------- 
+echo '#2 release-bash'
+cd "$devOpsPath/release-bash"; bash startup.bash;
+ 
+
+
+#----------------------------------------------
+echo "#3 get appVersion" 
+cd "$devOpsPath/build-bash"; source 19.get-app-version.bash;
+
+
+
+#----------------------------------------------
+echo "#4 bash"
+cd $curPath
+for file in *.sh
+do
+    echo "-----------------------------------------------------------------"
+    echo "[$(date "+%H:%M:%S")]" sh $file
+    sh $file
+done
+
+
+
+
+
+
+

+ 169 - 0
Publish/DevOps3/jenkins-bash/CICD.ki.git_Multibranch.deploy.jenkinsfile

@@ -0,0 +1,169 @@
+def remote = [:]
+remote.name = "dind-ssh"
+remote.host = "dind"
+remote.port = 22
+remote.user = "  "
+remote.password = "  "
+remote.allowAnyHosts = true
+
+pipeline {
+    agent any
+
+    environment {
+        // get APPNAME
+        // APPNAME = "Sers"
+        APPNAME = readFile("Publish/DevOps3/environment/env.APPNAME.txt")
+
+        envName = readFile("Publish/DevOps3/environment/env.envName.txt")
+        versionSuffix = "-${envName}${env.build_number}"
+
+        // basePath = "/root/docker-cache/jenkins/jenkins_home/workspace/${APPNAME}/${envName}/${env.BRANCH_NAME}/${env.build_number}"
+        basePath = "${env.WORKSPACE}"
+
+        // NUGET_PATH = "/root/docker-cache/jenkins/jenkins_home/workspace/.nuget"
+        NUGET_PATH = credentials("NUGET_PATH")
+        NUGET_SERVER = credentials("NUGET_SERVER")
+        NUGET_KEY = credentials("NUGET_KEY")
+
+        DOCKER_SERVER = credentials("DOCKER_SERVER")
+        DOCKER_ImagePrefix = "${DOCKER_SERVER}/${envName}/"
+        DOCKER_Buildx = false
+        DOCKER_USERNAME = " "
+        DOCKER_PASSWORD = " "
+
+        // set to "  "  if want to skip save releaseFiles to WebDav
+        WebDav_BaseUrl = credentials("WebDav_BaseUrl")
+        // WebDav_User = "username:pwd"
+        WebDav_User = credentials("WebDav_User")
+
+        build_crossPlatform = "no"
+
+        dind_ssh_account = credentials("dind_ssh_account")
+    }
+
+    stages {
+
+        stage('#1 deploy ?') {
+            steps {
+                timeout(time:600,unit:'SECONDS') {
+                    script {
+                        remote.user = "${dind_ssh_account_USR}"
+                        remote.password = "${dind_ssh_account_PSW}"
+
+                        env.codePath = "/root/docker-cache/jenkins/" + basePath.substring(5, basePath.length()) 
+
+                        echo "-------- APPNAME: [$APPNAME]"
+                        echo "-------- basePath: [$basePath]"
+                        echo "-------- DOCKER_ImagePrefix: [$DOCKER_ImagePrefix]"
+                        echo "-------- codePath: [$codePath]"
+
+                        if ( fileExists("Publish/DevOps3/environment/jenkins_NoNeedApprovalForBuild.txt") ) {
+                            echo "-------- do not need approval for build"
+	                        env.ApprovalForBuild = "yes"
+                        } else {
+                            echo "-------- waiting approval for build"
+	                        env.ApprovalForBuild = "no"
+	                        env.ApprovalForBuild = input message: "deploy ?", 
+		                        ok: 'Proceed?', 
+		                        parameters: [choice(choices:["yes","no"], description: 'if not please select no', name: 'choice')]
+                        }
+
+                    }
+                }
+            }
+        }
+
+        stage('#2 change version') {
+            when { expression { env.ApprovalForBuild == "yes" } }
+            steps {
+                script {
+                    echo "#2.1 change-app-version"
+                    sshCommand remote: remote, command:  "sh -c 'set -e; export versionSuffix=$versionSuffix;    cd $codePath/Publish/DevOps3/build-bash; source 22.add-suffix-to-app-version.bash;    echo -n \"\$nextAppVersion\" > $codePath/Publish/DevOps3/environment/env.appVersion.txt '"
+
+                    echo "#2.2 get app version"
+                    env.appVersion = readFile("Publish/DevOps3/environment/env.appVersion.txt")
+                    echo "-------- appVersion: [${env.appVersion}]"
+                }
+            }
+        }
+
+        stage('#3.0 build - run test') {
+            when { expression { env.ApprovalForBuild == "yes" } }
+            steps {
+                script {
+                    sshCommand remote: remote, command:  "sh -c 'set -e; export APPNAME=$APPNAME; export NUGET_PATH=$NUGET_PATH;    cd $codePath/Publish/DevOps3/build-bash; sh 10.Test.bash;  '"
+                }
+            }
+        }
+
+        stage('#3.1 build - single platflorm') {
+            when { expression { env.ApprovalForBuild == "yes" } }
+            steps {
+                script {
+                    sshCommand remote: remote, command:  "sh -c 'set -e; export APPNAME=$APPNAME; export NUGET_PATH=$NUGET_PATH;    cd $codePath/Publish/DevOps3/build-bash; sh startup.bash;  '"
+                }
+            }
+        }
+
+        stage('#3.2 build - cross platform') {
+            when { expression { env.ApprovalForBuild == "yes" && env.build_crossPlatform == "yes" } }
+            steps {
+                script {
+                    sshCommand remote: remote, command:  "sh -c 'set -e; export APPNAME=$APPNAME; export NUGET_PATH=$NUGET_PATH;    cd $codePath/Publish/DevOps3/build-bash; sh 40.Station-publish-multiple.bash;  '"
+                }
+            }
+        }
+
+        stage('#4 publish') {
+            when { expression { env.ApprovalForBuild == "yes" } }
+            steps {
+                script {
+                    sshCommand remote: remote, command:  "sh -c 'set -e; export APPNAME=$APPNAME;export NUGET_PATH=$NUGET_PATH;    export NUGET_SERVER=$NUGET_SERVER;export NUGET_KEY=$NUGET_KEY;  export DOCKER_Buildx=${env.DOCKER_Buildx};export DOCKER_ImagePrefix=${env.DOCKER_ImagePrefix};export DOCKER_USERNAME=${env.DOCKER_USERNAME};export DOCKER_PASSWORD=${env.DOCKER_PASSWORD};export DOCKER_BuildxExtArgs=\"--output=type=registry,registry.insecure=true\";    cd $codePath/Publish/DevOps3/release-bash; sh startup.bash;  '"
+                }
+            }
+        }
+
+        stage('#5 save releaseFiles') {
+            when { expression { env.ApprovalForBuild == "yes" && env.WebDav_BaseUrl != "  " } }
+            steps {
+                script {
+                    sshCommand remote: remote, command:  "sh -c 'set -e; export basePath=\"$codePath\"; export APPNAME=$APPNAME; export appVersion=\"$appVersion\";   export WebDav_BaseUrl=\"$WebDav_BaseUrl\"; export WebDav_User=\"$WebDav_User\";    cd $codePath/Publish/DevOps3/release-bash; sh 78.push-releaseFiles-to-webdav.bash;  '"
+                }
+            }
+        }
+
+    }
+
+    post {
+        always {
+            timeout(time:600,unit:'SECONDS') {
+                script {
+                    env.CleanFiles = "yes"
+                    env.CleanFiles = input message: "Clean temp files, \n will wait for 3600 seconds. \n click abort to skip clean.", 
+                            ok: 'Proceed', 
+                            parameters: [choice(choices:["yes","no"], name: 'choice')]
+               }
+            }
+            script {
+                if ( CleanFiles == "yes" ) {
+                    echo "clean up workspace directory"
+                    cleanWs()
+
+                    // clean up tmp directory
+                    dir("${workspace}@tmp") {
+                        deleteDir()
+                    }
+                }
+            }
+        }
+        success {
+            echo "build success !"
+        }
+        failure {
+            echo "build failure !"
+        }
+        aborted {
+            echo "build aborted !"
+        }
+    }
+}

+ 43 - 0
Publish/DevOps3/release-bash/71.file-zip.sh

@@ -0,0 +1,43 @@
+set -e
+
+
+#---------------------------------------------------------------------
+# args
+args_="
+
+export basePath=/root/temp/svn
+
+export appVersion=1.0
+
+export APPNAME=xxxxxx
+
+# "
+
+#----------------------------------------------
+echo "71.file-zip.sh"
+
+if [ ! -d "$basePath/Publish/release/release" ]; then
+    echo '71.file-zip.sh -> skip for no files exist'
+    exit 0
+fi
+
+docker run --rm -i \
+-v $basePath:/root/code \
+serset/filezip bash -c "
+set -e
+
+releasePath=/root/code/Publish/release
+rm -rf \$releasePath/release-zip
+
+for dirname in \`ls /root/code/Publish/release/release\`
+do
+  if [ -d \$releasePath/release/\$dirname ]
+  then
+    filezip zip -p -i \$releasePath/release/\$dirname -o \$releasePath/release-zip/${APPNAME}-\${dirname}-${appVersion}.zip 
+  fi
+done
+
+echo zip files:
+ls /root/code/Publish/release/release-zip
+
+"

+ 38 - 0
Publish/DevOps3/release-bash/72.nuget-push.sh

@@ -0,0 +1,38 @@
+set -e
+
+
+
+#---------------------------------------------------------------------
+# args
+args_="
+
+export basePath=/root/temp/svn
+
+export NUGET_SERVER=https://api.nuget.org/v3/index.json
+export NUGET_KEY=xxxxxxxxxx
+
+# "
+
+#----------------------------------------------
+echo "72.nuget-push.sh"
+
+if [ ! -d "$basePath/Publish/release/release/nuget" ]; then
+    echo '71.file-zip.sh -> skip for no nuget files exist'
+    exit 0
+fi
+
+
+docker run -i --rm \
+--env LANG=C.UTF-8 \
+-v $basePath:/root/code \
+serset/dotnet:sdk-6.0 \
+bash -c "
+for file in /root/code/Publish/release/release/nuget/*.nupkg
+do
+    echo nuget push \$file
+    dotnet nuget push \$file -k ${NUGET_KEY} -s ${NUGET_SERVER} --skip-duplicate
+done
+" || true
+
+
+ 

+ 39 - 0
Publish/DevOps3/release-bash/73.docker-image-build-push.sh

@@ -0,0 +1,39 @@
+set -e
+
+
+#---------------------------------------------------------------------
+# args
+args_="
+
+export basePath=/root/temp/svn
+
+export appVersion=1.0
+
+export DOCKER_ImagePrefix=serset/
+export DOCKER_USERNAME=serset
+export DOCKER_PASSWORD=xxx
+
+export DOCKER_Buildx=false #default: true
+export DOCKER_BuildxExtArgs=
+
+# "
+
+
+
+
+#---------------------------------------------------------------------
+echo "73.docker-image-build-push.sh"
+
+if [ ! -d "$basePath/Publish/release/release/docker-image" ]; then
+    echo '73.docker-image-build-push.sh -> skip for no docker image files exist'
+    exit 0
+fi
+
+if [ "$DOCKER_Buildx" != "false" ]; then
+    sh 75.docker-image-build-push_cross.bash
+else
+    sh 74.docker-image-build-push_amd64.bash
+fi
+
+
+

+ 41 - 0
Publish/DevOps3/release-bash/74.docker-image-build-push_amd64.bash

@@ -0,0 +1,41 @@
+set -e
+
+
+#---------------------------------------------------------------------
+# args
+args_="
+
+export basePath=/root/temp/svn
+
+export appVersion=1.0
+
+export DOCKER_ImagePrefix=serset/
+export DOCKER_USERNAME=serset
+export DOCKER_PASSWORD=xxx
+export DOCKER_BuildxExtArgs=
+
+# "
+
+
+#---------------------------------------------------------------------
+echo "74.docker-image-build-push_amd64.bash"
+
+echo "#1 login if UserName is not empty"
+if [ -n "$DOCKER_USERNAME" ]; then docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD; fi
+
+dockerPath=$basePath/Publish/release/release/docker-image
+
+for dockerName in `ls $dockerPath`
+do
+  if [ -d $dockerPath/$dockerName ]
+  then
+    echo "#2.* docker build $dockerName"
+    echo "docker build $dockerPath/$dockerName -t ${DOCKER_ImagePrefix}$dockerName:$appVersion -t ${DOCKER_ImagePrefix}$dockerName"
+    docker build $dockerPath/$dockerName -t ${DOCKER_ImagePrefix}$dockerName:$appVersion -t ${DOCKER_ImagePrefix}$dockerName
+    docker push ${DOCKER_ImagePrefix}$dockerName:$appVersion
+    docker push ${DOCKER_ImagePrefix}$dockerName
+    docker rmi -f ${DOCKER_ImagePrefix}$dockerName:$appVersion ${DOCKER_ImagePrefix}$dockerName
+  fi
+done
+
+

+ 71 - 0
Publish/DevOps3/release-bash/75.docker-image-build-push_cross.bash

@@ -0,0 +1,71 @@
+set -e
+
+
+#---------------------------------------------------------------------
+# args
+args_="
+
+export basePath=/root/temp/svn
+
+export appVersion=1.0
+
+export DOCKER_ImagePrefix=serset/
+export DOCKER_USERNAME=serset
+export DOCKER_PASSWORD=xxx
+export DOCKER_BuildxExtArgs=
+
+# "
+
+
+
+
+#---------------------------------------------------------------------
+echo "75.docker-image-build-push_cross.bash -> #1 docker - init buildx"
+
+
+export builderName="mybuilder__${appVersion}__"
+echo "builderName: $builderName"
+
+
+echo "#1.1 docker buildx version"
+docker buildx version
+
+echo "#1.2 install binfmt_misc"
+docker run --privileged --rm tonistiigi/binfmt --install all
+
+echo "#1.3 create builder"
+if [ ! "$(docker buildx ls | grep $builderName)" ]; then docker buildx create --use --name $builderName --buildkitd-flags '--allow-insecure-entitlement security.insecure'; fi
+
+echo "#1.4 start builder"
+docker buildx inspect $builderName --bootstrap
+
+echo "#1.5 show builders and supported CPU platform"
+docker buildx ls
+
+
+
+#---------------------------------------------------------------------
+echo "75.docker-image-build-push_cross.bash -> #2 docker - build and push"
+
+echo "#2.1 login if UserName is not empty"
+if [ -n "$DOCKER_USERNAME" ]; then docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD; fi
+
+dockerPath=$basePath/Publish/release/release/docker-image
+
+for dockerName in `ls $dockerPath`
+do
+  if [ -d $dockerPath/$dockerName ]
+  then
+    platform="linux/amd64,linux/arm64,linux/arm/v7"
+    if [ -f "$dockerPath/$dockerName/Dockerfile.platform" ]; then platform=`cat "$dockerPath/$dockerName/Dockerfile.platform"`; fi
+
+    echo "#2.* docker build $dockerName, platform: $platform"
+    echo "docker buildx build --allow security.insecure $dockerPath/$dockerName -t ${DOCKER_ImagePrefix}$dockerName:$appVersion -t ${DOCKER_ImagePrefix}$dockerName --platform=$platform --push $DOCKER_BuildxExtArgs --builder $builderName"
+    docker buildx build --allow security.insecure $dockerPath/$dockerName -t ${DOCKER_ImagePrefix}$dockerName:$appVersion -t ${DOCKER_ImagePrefix}$dockerName --platform=$platform --push $DOCKER_BuildxExtArgs --builder $builderName
+  fi
+done
+
+
+#---------------------------------------------------------------------
+echo "75.docker-image-build-push_cross.bash -> #3 remove builder"
+if [ "$(docker buildx ls | grep $builderName)" ]; then docker buildx rm $builderName; fi

+ 47 - 0
Publish/DevOps3/release-bash/78.push-releaseFiles-to-webdav.bash

@@ -0,0 +1,47 @@
+set -e
+
+
+#---------------------------------------------------------------------
+# args
+args_="
+
+export basePath=/root/temp/svn
+
+export APPNAME=Vit.Library
+export appVersion=2.2.14
+
+export WebDav_BaseUrl="https://nextcloud.xxx.com/remote.php/dav/files/release/releaseFiles/ki_jenkins"
+export WebDav_User="username:pwd"
+
+# "
+
+
+# will not throw errors if WebDav service is not available
+
+
+#---------------------------------------------------------------------
+echo "78.push-releaseFiles-to-webdav.bash -> #1 create dir"
+
+docker run -i --rm curlimages/curl sh -c "curl -X MKCOL -u \"$WebDav_User\" \"$WebDav_BaseUrl/$APPNAME\" " || true
+docker run -i --rm curlimages/curl sh -c "curl -X MKCOL -u \"$WebDav_User\" \"$WebDav_BaseUrl/$APPNAME/$appVersion\" " || true
+
+#---------------------------------------------------------------------
+echo "78.push-releaseFiles-to-webdav.bash -> #2 push release files"
+
+docker run -i --rm \
+-v "$basePath/Publish/release/release-zip":/releaseFiles \
+curlimages/curl \
+sh -c "
+cd /releaseFiles
+for file in /releaseFiles/*
+do
+    echo ''
+    echo '----------------------------'
+    fileName=\"\${file##*/}\"
+    echo push file: \$fileName
+    curl -X PUT -u \"$WebDav_User\" -T "/releaseFiles/\$fileName" \"$WebDav_BaseUrl/$APPNAME/$appVersion/\$fileName\"
+done
+" || true
+
+#---------------------------------------------------------------------
+echo "78.push-releaseFiles-to-webdav.bash -> #3 success"

+ 57 - 0
Publish/DevOps3/release-bash/startup.bash

@@ -0,0 +1,57 @@
+set -e
+
+# cd /root/temp/svn/Publish/DevOps/release-bash;bash startup.bash;
+
+#---------------------------------------------------------------------
+# args
+args_="
+
+export APPNAME=xxxxxx
+
+export DOCKER_ImagePrefix=serset/
+export DOCKER_USERNAME=serset
+export DOCKER_PASSWORD=xxx
+
+export NUGET_SERVER=https://api.nuget.org/v3/index.json
+export NUGET_KEY=xxxxxxxxxx
+# "
+
+
+
+#----------------------------------------------
+# cur path
+curPath="$PWD"
+
+cd "$curPath/../../.."
+export basePath="$PWD"
+cd "$curPath"
+
+# export basePath=/root/temp/svn
+
+
+
+#----------------------------------------------
+echo "#1 get appVersion"
+cd "$curPath/../build-bash"; source 19.get-app-version.bash;
+
+
+
+
+
+
+#----------------------------------------------
+echo "#2 bash"
+cd $curPath
+for file in *.sh
+do
+    echo "-----------------------------------------------------------------"
+    echo "[$(date "+%H:%M:%S")] sh $file"
+    sh "$file"
+done
+
+
+cd "$curPath"
+
+
+
+

+ 306 - 1
README.md

@@ -1 +1,306 @@
-# Vitorm.Excel
+
+# Vitorm.Excel
+Vitorm.Excel: an simple orm for Excel
+>source address: [https://github.com/Vit-Orm/Vitorm.Excel](https://github.com/Vit-Orm/Vitorm.Excel "https://github.com/Vit-Orm/Vitorm.Excel")    
+
+![](https://img.shields.io/github/license/Vit-Orm/Vitorm.Excel.svg)  
+![](https://img.shields.io/github/repo-size/Vit-Orm/Vitorm.Excel.svg)  ![](https://img.shields.io/github/last-commit/Vit-Orm/Vitorm.Excel.svg)  
+ 
+
+| Build | NuGet |
+| -------- | -------- |
+|![](https://github.com/Vit-Orm/Vitorm.Excel/workflows/ki_devops3_build/badge.svg) | [![](https://img.shields.io/nuget/v/Vitorm.Excel.svg)](https://www.nuget.org/packages/Vitorm.Excel) ![](https://img.shields.io/nuget/dt/Vitorm.Excel.svg) |
+
+
+
+
+# Vitorm.Excel Documentation
+This guide will walk you through the steps to set up and use Vitorm.Excel.
+
+supported features:
+
+| feature    |  method   |  remarks   |     |
+| --- | --- | --- | --- |
+|  create table   |  TryCreateTable   |     |     |
+|  drop table   |  TryDropTable   |     |     |
+| --- | --- | --- | --- |
+|  create records   |  Add AddRange   |     |     |
+|  retrieve  records |  Query Get   |     |     |
+|  update records   |  Update UpdateRange ExecuteUpdate  |     |     |
+|  delete records   |  Delete DeleteRange DeleteByKey DeleteByKeys ExecuteDelete   |     |     |
+| --- | --- | --- | --- |
+|  change table   |  ChangeTable    |  change mapping table from database   |   |
+|  change database  |  ChangeDatabase   | change database to be connected  |   |
+| --- | --- | --- | --- |
+|  collection total count   |  TotalCount    |  Collection Total Count without Take and Skip   |   |
+|  collection total count and list  |  ToListAndTotalCount   | query List and TotalCount in one request  |   |
+|     |     |   |   |
+
+
+## Installation
+Before using Vitorm, install the necessary package:    
+``` bash
+dotnet add package Vitorm.Excel
+```
+
+## Minimum viable demo
+> code address: [Program_Min.cs](https://github.com/Vit-Orm/Vitorm.Excel/tree/master/test/Vitorm.Excel.Console/Program_Min.cs)    
+``` csharp
+using System;
+using System.Linq;
+using System.Threading;
+namespace App
+{
+    public class Program_Min
+    {
+        static void Main2(string[] args)
+        {
+            // #1 Init
+            using var dbContext = new Vitorm.Excel.DbContext("Excel://mongoadmin:mongoadminsecret@localhost:27017");
+            dbContext.TryDropTable<User>();
+            dbContext.TryCreateTable<User>();
+            dbContext.Add(new User { id = 1, name = "lith" });
+            Thread.Sleep(2000);
+
+            // #2 Query
+            var user = dbContext.Get<User>(1);
+            var users = dbContext.Query<User>().Where(u => u.name.Contains("li")).ToList();
+        }
+
+        // Entity Definition
+        [System.ComponentModel.DataAnnotations.Schema.Table("User")]
+        public class User
+        {
+            [System.ComponentModel.DataAnnotations.Key]
+            public int id { get; set; }
+            public string name { get; set; }
+            public DateTime? birth { get; set; }
+            public int? fatherId { get; set; }
+        }
+    }
+}
+```
+
+
+## Full Example
+> This example provides a comprehensive guide to utilizing Vitorm for basic and advanced database operations while maintaining lightweight performance.    
+> code address: [Program.cs](https://github.com/Vit-Orm/Vitorm.Excel/tree/master/test/Vitorm.Excel.Console/Program.cs)    
+``` csharp
+using System;
+using System.Linq;
+using Vitorm;
+namespace App
+{
+    public class Program
+    {
+        static void Main(string[] args)
+        {
+            // #1 Configures Vitorm
+            using var dbContext = new Vitorm.Excel.DbContext("Excel://mongoadmin:mongoadminsecret@localhost:27017");
+
+            // #2 Create Table
+            dbContext.TryDropTable<User>();
+            dbContext.TryCreateTable<User>();
+
+            // #3 Insert Records
+            dbContext.Add(new User { id = 1, name = "lith" });
+            dbContext.AddRange(new[] {
+                new User {   id = 2, name = "lith", fatherId = 1 },
+                new User {   id = 3, name = "lith", fatherId = 1 }
+            });
+
+            // #4 Query Records
+            {
+                var user = dbContext.Get<User>(1);
+                var users = dbContext.Query<User>().Where(u => u.name.Contains("li")).ToList();
+                var sql = dbContext.Query<User>().Where(u => u.name.Contains("li")).ToExecuteString();
+            }
+
+            // #5 Update Records
+            dbContext.Update(new User { id = 1, name = "lith1" });
+            dbContext.UpdateRange(new[] {
+                new User {   id = 2, name = "lith2", fatherId = 1 },
+                new User {   id = 3, name = "lith3", fatherId = 2 }
+            });
+            //dbContext.Query<User>().Where(u => u.name.Contains("li"))
+            //    .ExecuteUpdate(u => new User { name = "Lith" + u.id });
+
+            // #6 Delete Records
+            dbContext.Delete<User>(new User { id = 1 });
+            dbContext.DeleteRange(new[] {
+                new User {  id = 2 },
+                new User {  id = 3 }
+            });
+            dbContext.DeleteByKey<User>(1);
+            dbContext.DeleteByKeys<User, int>(new[] { 1, 2 });
+            //dbContext.Query<User>().Where(u => u.name.Contains("li"))
+            //    .ExecuteDelete();
+
+            // #7 Join Queries
+
+            // #8 Transactions
+
+            // #9 Database Functions
+        }
+
+        // Entity Definition
+        [System.ComponentModel.DataAnnotations.Schema.Table("User")]
+        public class User
+        {
+            [System.ComponentModel.DataAnnotations.Key]
+            public int id { get; set; }
+            public string name { get; set; }
+            public DateTime? birth { get; set; }
+            public int? fatherId { get; set; }
+        }
+    }
+}
+```
+
+## Explanation   
+1. **Setup**: Initializes the database and configures Vitorm.
+2. **Create Table**: Drops and recreates the `User` table.
+3. **Insert Records**: Adds single and multiple user records.
+4. **Query Records**: Retrieves user records using various querying methods.
+5. **Update Records**: Updates single and multiple user records.
+6. **Delete Records**: Deletes single and multiple user records.
+7. **Join Queries**: Performs a join operation between user and father records.
+8. **Transactions**: Demonstrates nested transactions and rollback/commit operations.
+9. **Database Functions**: Uses custom database functions in queries.
+
+
+
+# Vitorm.Data Documentation    
+Vitorm.Data is a static class that allows you to use Vitorm without explicitly creating or disposing of a DbContext.    
+ 
+## Installation    
+Before using Vitorm.Data, install the necessary package:    
+``` bash
+dotnet add package Vitorm.Data
+dotnet add package Vitorm.Excel
+```
+
+## Config settings
+``` json
+// appsettings.json
+{
+  "Vitorm": {
+    "Data": [
+      {
+        "provider": "Excel",
+        "namespace": "App",
+        "connectionString": "db_orm.xlsx"
+      }
+    ]
+  }
+}
+```
+
+## Minimum viable demo
+> After configuring the `appsettings.json` file, you can directly perform queries without any additional configuration or initialization, `Vitorm.Data` is that easy to use.    
+> code address: [Program_Min.cs](https://github.com/Vit-Orm/Vitorm/tree/master/test/Vitorm.Data.Console/Program_Min.cs)    
+``` csharp
+using Vitorm;
+namespace App
+{
+    public class Program_Min
+    {
+        static void Main2(string[] args)
+        {
+            //  Query Records
+            var user = Data.Get<User>(1);
+            var users = Data.Query<User>().Where(u => u.name.Contains("li")).ToList();
+        }
+
+        // Entity Definition
+        [System.ComponentModel.DataAnnotations.Schema.Table("User")]
+        public class User
+        {
+            [System.ComponentModel.DataAnnotations.Key]
+            public int id { get; set; }
+            public string name { get; set; }
+            public DateTime? birth { get; set; }
+            public int? fatherId { get; set; }
+        }
+    }
+}
+
+```
+
+## Full Example    
+> code address: [Program.cs](https://github.com/Vit-Orm/Vitorm/tree/master/test/Vitorm.Data.Console/Program.cs)    
+``` csharp
+using Vitorm;
+
+namespace App
+{
+    public class Program
+    {
+        static void Main(string[] args)
+        {
+            // #1 No need to init Vitorm.Data
+
+            // #2 Create Table
+            Data.TryDropTable<User>();
+            Data.TryCreateTable<User>();
+
+            // #3 Insert Records
+            Data.Add(new User { id = 1, name = "lith" });
+            Data.AddRange(new[] {
+                new User { id = 2, name = "lith", fatherId = 1 },
+                new User { id = 3, name = "lith", fatherId = 1 }
+            });
+
+            // #4 Query Records
+            {
+                var user = Data.Get<User>(1);
+                var users = Data.Query<User>().Where(u => u.name.Contains("li")).ToList();
+                var sql = Data.Query<User>().Where(u => u.name.Contains("li")).ToExecuteString();
+            }
+
+            // #5 Update Records
+            Data.Update(new User { id = 1, name = "lith1" });
+            Data.UpdateRange(new[] {
+                new User { id = 2, name = "lith2", fatherId = 1 },
+                new User { id = 3, name = "lith3", fatherId = 2 }
+            });
+            Data.Query<User>().Where(u => u.name.Contains("li"))
+                .ExecuteUpdate(u => new User { name = "Lith" + u.id });
+
+            // #6 Delete Records
+            Data.Delete<User>(new User { id = 1, name = "lith1" });
+            Data.DeleteRange(new[] {
+                new User { id = 2, name = "lith2", fatherId = 1 },
+                new User { id = 3, name = "lith3", fatherId = 2 }
+            });
+            Data.DeleteByKey<User>(1);
+            Data.DeleteByKeys<User, int>(new[] { 1, 2 });
+            Data.Query<User>().Where(u => u.name.Contains("li"))
+                .ExecuteDelete();
+
+            // #7 Join Queries
+
+            // #8 Transactions
+
+        }
+
+        // Entity Definition
+        [System.ComponentModel.DataAnnotations.Schema.Table("User")]
+        public class User
+        {
+            [System.ComponentModel.DataAnnotations.Key]
+            public int id { get; set; }
+            public string name { get; set; }
+            public DateTime? birth { get; set; }
+            public int? fatherId { get; set; }
+        }
+    }
+}
+```
+
+
+# Examples:  
+[Test Example](https://github.com/Vit-Orm/Vitorm.Excel/tree/master/test/Vitorm.Excel.MsTest)    
+
+
+

+ 56 - 0
Vitorm.Excel.sln

@@ -0,0 +1,56 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.0.31606.5
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "doc", "doc", "{75C25D0B-8529-4E3F-AF43-6EC1E9E40828}"
+	ProjectSection(SolutionItems) = preProject
+		README.md = README.md
+		doc\ReleaseLog.md = doc\ReleaseLog.md
+		doc\TODO.md = doc\TODO.md
+	EndProjectSection
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{7904FE51-04FF-4477-8E3A-CC340389EE32}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{05176905-A2A5-4015-9F04-2904506C902F}"
+	ProjectSection(SolutionItems) = preProject
+		src\Versions.props = src\Versions.props
+	EndProjectSection
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Vitorm.Excel", "src\Vitorm.Excel\Vitorm.Excel.csproj", "{7AC01F47-AD5A-4B61-9F05-38094C1EF23B}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Vitorm.Excel.MsTest", "test\Vitorm.Excel.MsTest\Vitorm.Excel.MsTest.csproj", "{42E0FE60-9E3B-4B59-A69E-E50A20C89D6D}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Vitorm.Excel.Data.MsTest", "test\Vitorm.Excel.Data.MsTest\Vitorm.Excel.Data.MsTest.csproj", "{EB69697B-0CDB-4A28-AACF-22C31137DDE9}"
+EndProject
+Global
+	GlobalSection(SolutionConfigurationPlatforms) = preSolution
+		Debug|Any CPU = Debug|Any CPU
+		Release|Any CPU = Release|Any CPU
+	EndGlobalSection
+	GlobalSection(ProjectConfigurationPlatforms) = postSolution
+		{7AC01F47-AD5A-4B61-9F05-38094C1EF23B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{7AC01F47-AD5A-4B61-9F05-38094C1EF23B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{7AC01F47-AD5A-4B61-9F05-38094C1EF23B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{7AC01F47-AD5A-4B61-9F05-38094C1EF23B}.Release|Any CPU.Build.0 = Release|Any CPU
+		{42E0FE60-9E3B-4B59-A69E-E50A20C89D6D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{42E0FE60-9E3B-4B59-A69E-E50A20C89D6D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{42E0FE60-9E3B-4B59-A69E-E50A20C89D6D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{42E0FE60-9E3B-4B59-A69E-E50A20C89D6D}.Release|Any CPU.Build.0 = Release|Any CPU
+		{EB69697B-0CDB-4A28-AACF-22C31137DDE9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{EB69697B-0CDB-4A28-AACF-22C31137DDE9}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{EB69697B-0CDB-4A28-AACF-22C31137DDE9}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{EB69697B-0CDB-4A28-AACF-22C31137DDE9}.Release|Any CPU.Build.0 = Release|Any CPU
+	EndGlobalSection
+	GlobalSection(SolutionProperties) = preSolution
+		HideSolutionNode = FALSE
+	EndGlobalSection
+	GlobalSection(NestedProjects) = preSolution
+		{7AC01F47-AD5A-4B61-9F05-38094C1EF23B} = {05176905-A2A5-4015-9F04-2904506C902F}
+		{42E0FE60-9E3B-4B59-A69E-E50A20C89D6D} = {7904FE51-04FF-4477-8E3A-CC340389EE32}
+		{EB69697B-0CDB-4A28-AACF-22C31137DDE9} = {7904FE51-04FF-4477-8E3A-CC340389EE32}
+	EndGlobalSection
+	GlobalSection(ExtensibilityGlobals) = postSolution
+		SolutionGuid = {C7DA16E3-9949-49FA-B0B4-F830636DE60F}
+	EndGlobalSection
+EndGlobal

+ 10 - 0
clean-temp.bat

@@ -0,0 +1,10 @@
+cd Publish\DevOps3\build-cmd
+VsTool.exe delete --path "..\..\.." --file "*.suo|*.user" --directory "obj|bin|.vs|packages|TestResults"
+
+
+rd /s/q ..\..\release
+
+
+echo %~n0.bat success£¡
+
+:: pause

+ 1 - 0
doc/ReleaseLog.md

@@ -0,0 +1 @@
+# Vitorm.Excel ReleaseLog

+ 2 - 0
doc/TODO.md

@@ -0,0 +1,2 @@
+# Vitorm.Excel TODO
+

BIN
doc/vitorm_logo_v1.png


+ 6 - 0
src/Versions.props

@@ -0,0 +1,6 @@
+<Project>
+    <PropertyGroup>
+        <Version>2.2.0-develop</Version>
+        <Vitorm_Version>[2.2.0-preview, 2.3.0)</Vitorm_Version>
+    </PropertyGroup>
+</Project>

+ 20 - 0
src/Vitorm.Excel/DataProvider.cs

@@ -0,0 +1,20 @@
+using System.Collections.Generic;
+
+namespace Vitorm.Excel
+{
+    public partial class DataProvider : Vitorm.DataProvider.DataProvider
+    {
+        protected Dictionary<string, object> config;
+        protected DbConfig dbConfig;
+
+        public override void Init(Dictionary<string, object> config)
+        {
+            this.config = config;
+            this.dbConfig = new(config);
+        }
+
+        public override Vitorm.DbContext CreateDbContext() => new Vitorm.Excel.DbContext(dbConfig);
+
+
+    }
+}

+ 47 - 0
src/Vitorm.Excel/DbConfig.cs

@@ -0,0 +1,47 @@
+using System.Collections.Generic;
+using System.IO;
+
+using OfficeOpenXml;
+
+
+namespace Vitorm.Excel
+{
+    public class DbConfig
+    {
+        static DbConfig()
+        {
+            ExcelPackage.LicenseContext = LicenseContext.NonCommercial;
+        }
+
+        public DbConfig(string connectionString)
+        {
+            this.connectionString = connectionString;
+        }
+
+        public DbConfig(string connectionString, string readOnlyConnectionString)
+        {
+            this.connectionString = connectionString;
+        }
+
+        public DbConfig(Dictionary<string, object> config)
+        {
+            object value;
+            if (config.TryGetValue("connectionString", out value))
+                this.connectionString = value as string;
+        }
+
+        public string connectionString { get; set; }
+
+
+        public virtual string databaseName => Path.GetFileNameWithoutExtension(connectionString);
+
+        public virtual DbConfig WithDatabase(string databaseName)
+        {
+            var _connectionString = Path.Combine(Path.GetDirectoryName(connectionString), databaseName + ".xlsx");
+
+            return new DbConfig(_connectionString);
+        }
+
+        internal string dbHashCode => connectionString.GetHashCode().ToString();
+    }
+}

+ 66 - 0
src/Vitorm.Excel/DbContext.cs

@@ -0,0 +1,66 @@
+using System.Data;
+
+using OfficeOpenXml;
+
+namespace Vitorm.Excel
+{
+    public partial class DbContext : Vitorm.DbContext
+    {
+        public DbConfig dbConfig { get; protected set; }
+
+        public DbContext(DbConfig dbConfig) : base(DbSetConstructor.CreateDbSet)
+        {
+            this.dbConfig = dbConfig;
+        }
+
+        public DbContext(string connectionString) : this(new DbConfig(connectionString))
+        {
+        }
+
+
+        #region Transaction
+        public virtual IDbTransaction BeginTransaction() => throw new System.NotImplementedException();
+        public virtual IDbTransaction GetCurrentTransaction() => throw new System.NotImplementedException();
+
+        #endregion
+
+
+
+        public virtual string databaseName => dbConfig.databaseName;
+        public virtual void ChangeDatabase(string databaseName)
+        {
+            dbConfig = dbConfig.WithDatabase(databaseName);
+        }
+
+
+
+        #region dbConnection
+
+        protected ExcelPackage _dbConnection;
+        public virtual ExcelPackage dbConnection => _dbConnection ??= new ExcelPackage(dbConfig.connectionString);
+        public virtual ExcelPackage readOnlyDbConnection => dbConnection;
+
+        #endregion
+
+
+        public override void Dispose()
+        {
+            if (_dbConnection != null)
+            {
+                try
+                {
+                    //_dbConnection.Save();
+                    _dbConnection.Dispose();
+                    _dbConnection = null;
+                }
+                catch (System.Exception ex)
+                {
+                }
+            }
+
+            base.Dispose();
+        }
+
+
+    }
+}

+ 562 - 0
src/Vitorm.Excel/DbSet.cs

@@ -0,0 +1,562 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Linq.Expressions;
+using System.Reflection;
+using System.Threading.Tasks;
+
+using OfficeOpenXml;
+
+using Vit.Linq;
+using Vit.Linq.FilterRules;
+using Vit.Linq.FilterRules.ComponentModel;
+
+using Vitorm.Entity;
+
+namespace Vitorm.Excel
+{
+    // https://epplussoftware.com/en/Developers/
+    // https://github.com/EPPlusSoftware/EPPlus.Samples.CSharp
+
+    public class DbSetConstructor
+    {
+        public static IDbSet CreateDbSet(IDbContext dbContext, IEntityDescriptor entityDescriptor)
+        {
+            return _CreateDbSet.MakeGenericMethod(entityDescriptor.entityType)
+                     .Invoke(null, new object[] { dbContext, entityDescriptor }) as IDbSet;
+        }
+
+        static readonly MethodInfo _CreateDbSet = new Func<DbContext, IEntityDescriptor, IDbSet>(CreateDbSet<object>)
+                   .Method.GetGenericMethodDefinition();
+        public static IDbSet<Entity> CreateDbSet<Entity>(DbContext dbContext, IEntityDescriptor entityDescriptor)
+        {
+            return new DbSet<Entity>(dbContext, entityDescriptor);
+        }
+
+    }
+
+
+    public partial class DbSet<Entity> : IDbSet<Entity>
+    {
+        public virtual IDbContext dbContext { get; protected set; }
+        public virtual DbContext DbContext => (DbContext)dbContext;
+
+
+        protected IEntityDescriptor _entityDescriptor;
+        public virtual IEntityDescriptor entityDescriptor => _entityDescriptor;
+
+
+        public DbSet(DbContext dbContext, IEntityDescriptor entityDescriptor)
+        {
+            this.dbContext = dbContext;
+            this._entityDescriptor = entityDescriptor;
+        }
+
+        // #0 Schema :  ChangeTable
+        public virtual IEntityDescriptor ChangeTable(string tableName) => _entityDescriptor = _entityDescriptor.WithTable(tableName);
+        public virtual IEntityDescriptor ChangeTableBack() => _entityDescriptor = _entityDescriptor.GetOriginEntityDescriptor();
+
+
+        public virtual ExcelPackage package => DbContext.dbConnection;
+        public virtual ExcelWorksheet sheet => package.Workbook.Worksheets[entityDescriptor.tableName];
+
+
+
+        #region columnIndexs
+        Dictionary<string, int> _columnIndexes;
+
+        Dictionary<string, int> columnIndexes =>
+            _columnIndexes ??=
+                Enumerable.Range(1, sheet.Dimension?.End.Column ?? 0)
+                .Select(i => (index: i, columnName: sheet.GetValue<string>(1, i)))
+                .GroupBy(m => m.columnName).Select(g => g.First())
+                .ToDictionary(item => item.columnName, item => item.index)
+            ;
+        #endregion
+
+
+
+        #region Excel Methods
+        protected virtual void Save()
+        {
+            _columnIndexes = null;
+            package.Save();
+        }
+        protected virtual async Task SaveAsync()
+        {
+            _columnIndexes = null;
+            await package.SaveAsync();
+        }
+
+
+
+        public virtual void AddColumnsIfNotExist()
+        {
+            var colsToAdd = entityDescriptor.allColumns.Where(col => !columnIndexes.TryGetValue(col.columnName, out var _)).ToList();
+
+            if (!colsToAdd.Any()) return;
+
+            int colIndex = sheet.Columns.EndColumn;
+            foreach (var col in colsToAdd)
+            {
+                colIndex++;
+
+                var column = sheet.Column(colIndex);
+                sheet.SetValue(1, colIndex, col.columnName);
+            }
+            _columnIndexes = null;
+        }
+
+
+        public virtual void SetDateTimeFormat(string format = "yyyy-MM-dd HH:mm:ss")
+        {
+            foreach (var col in entityDescriptor.allColumns.Where(col => TypeUtil.GetUnderlyingType(col.type) == typeof(DateTime)))
+            {
+                if (!columnIndexes.TryGetValue(col.columnName, out var colIndex)) continue;
+
+                var column = sheet.Columns[colIndex];
+
+                column.Style.Numberformat.Format = format;
+                column.AutoFit();
+            }
+        }
+
+        protected virtual int UpdateRangeWithoutSave(IEnumerable<Entity> entities)
+        {
+            var method = (new Func<IEnumerable<Entity>, int>(UpdateRangeWithoutSave<string>))
+                          .GetMethodInfo()
+                          .GetGenericMethodDefinition().MakeGenericMethod(entityDescriptor.key.type);
+
+            return (int)method.Invoke(this, new object[] { entities });
+        }
+        protected virtual void SetRow(Entity entity, int rowIndex)
+        {
+            foreach (var col in entityDescriptor.allColumns)
+            {
+                if (!columnIndexes.TryGetValue(col.columnName, out var colIndex)) continue;
+                var value = col.GetValue(entity);
+                sheet.SetValue(rowIndex, colIndex, value);
+            }
+        }
+        protected virtual int DeleteByKeyWithoutSave(object keyValue)
+        {
+            var method = (new Func<string, int>(DeleteByKeyWithoutSave<string>))
+                          .GetMethodInfo()
+                          .GetGenericMethodDefinition().MakeGenericMethod(entityDescriptor.key.type);
+
+            return (int)method.Invoke(this, new object[] { keyValue });
+        }
+        protected virtual int DeleteByKeyWithoutSave<Key>(Key keyValue)
+        {
+            int colIndex = columnIndexes.TryGetValue(entityDescriptor.key.columnName, out var i) ? i : throw new ArgumentOutOfRangeException("key column not exist.");
+
+            var lastRowIndex = sheet.Dimension.End.Row;
+
+            int count = 0;
+            for (var rowIndex = lastRowIndex; rowIndex >= 2; rowIndex--)
+            {
+                var key = (Key)TypeUtil.ConvertToType(sheet.GetValue(rowIndex, colIndex), typeof(Key));
+                if (keyValue?.Equals(key) == true)
+                {
+                    sheet.DeleteRow(rowIndex);
+                    count++;
+                }
+            }
+            return count;
+        }
+
+
+        protected virtual int DeleteByKeysWithoutSave<Key>(IEnumerable<Key> keys)
+        {
+            var method = (new Func<IEnumerable<string>, int>(DeleteByKeys_<string, int>))
+                        .GetMethodInfo()
+                        .GetGenericMethodDefinition().MakeGenericMethod(typeof(Key), entityDescriptor.key.type);
+
+            return (int)method.Invoke(this, new object[] { keys });
+        }
+
+        protected virtual int DeleteByKeys_<Key, EntityKey>(IEnumerable<Key> keys)
+        {
+            if (typeof(Key) == typeof(EntityKey)) return DeleteByKeysWithoutSave(keys);
+
+            var entityKeys = keys.Select(key => (EntityKey)TypeUtil.ConvertToType(key, typeof(EntityKey)));
+            return DeleteByKeys_(entityKeys);
+        }
+
+        protected virtual int DeleteByKeys_<Key>(IEnumerable<Key> keys)
+        {
+            int colIndex = columnIndexes.TryGetValue(entityDescriptor.key.columnName, out var i) ? i : throw new ArgumentOutOfRangeException("key column not exist.");
+
+            var lastRowIndex = sheet.Dimension.End.Row;
+
+            int count = 0;
+            for (var rowIndex = lastRowIndex; rowIndex >= 2; rowIndex--)
+            {
+                var key = (Key)TypeUtil.ConvertToType(sheet.GetValue(rowIndex, colIndex), typeof(Key));
+                if (keys.Contains(key))
+                {
+                    sheet.DeleteRow(rowIndex);
+                    count++;
+                }
+            }
+            return count;
+        }
+
+        protected virtual IEnumerable<(int rowIndex, Entity entity)> GetEntities()
+        {
+            if (sheet?.Dimension == null) yield break;
+
+            var lastRowIndex = sheet.Dimension.End.Row;
+            for (var rowIndex = 2; rowIndex <= lastRowIndex; rowIndex++)
+            {
+                var entity = (Entity)Activator.CreateInstance(entityDescriptor.entityType);
+                try
+                {
+                    foreach (var col in entityDescriptor.allColumns)
+                    {
+                        if (!columnIndexes.TryGetValue(col.columnName, out var colIndex)) continue;
+
+                        if (col.type == typeof(DateTime) || col.type == typeof(DateTime?))
+                        {
+                            var value = sheet.GetValue<DateTime>(rowIndex, colIndex);
+                            col.SetValue(entity, value);
+                        }
+                        else
+                        {
+                            var value = sheet.GetValue(rowIndex, colIndex);
+                            value = TypeUtil.ConvertToType(value, col.type);
+                            col.SetValue(entity, value);
+                        }
+                    }
+
+                }
+                catch (Exception ex)
+                {
+                    throw;
+                }
+                yield return (rowIndex, entity);
+            }
+
+        }
+        public virtual Expression<Func<Entity, bool>> GetKeyPredicate(object keyValue)
+        {
+            var filter = new FilterRule { field = entityDescriptor.key.propertyName, @operator = "=", value = keyValue };
+            var predicate = FilterService.Instance.ConvertToCode_PredicateExpression<Entity>(filter);
+            return predicate;
+        }
+
+        public virtual Expression<Func<Entity, bool>> GetKeyPredicate<Key>(IEnumerable<Key> keys)
+        {
+            var filter = new FilterRule { field = entityDescriptor.key.propertyName, @operator = "In", value = keys };
+            var predicate = FilterService.Instance.ConvertToCode_PredicateExpression<Entity>(filter);
+            return predicate;
+        }
+        #endregion
+
+
+
+
+        #region #0 Schema :  Create Drop
+
+        public virtual bool TableExist()
+        {
+            return sheet != null;
+        }
+
+        public virtual void TryCreateTable()
+        {
+            if (TableExist()) return;
+
+            var sheet = package.Workbook.Worksheets.Add(entityDescriptor.tableName);
+
+            int colIndex = 0;
+            foreach (var col in entityDescriptor.allColumns)
+            {
+                colIndex++;
+
+                var column = sheet.Column(colIndex);
+                sheet.SetValue(1, colIndex, col.columnName);
+            }
+
+            SetDateTimeFormat();
+
+            Save();
+        }
+
+
+        public virtual async Task TryCreateTableAsync()
+        {
+            if (TableExist()) return;
+
+            var sheet = package.Workbook.Worksheets.Add(entityDescriptor.tableName);
+
+            int colIndex = 0;
+            foreach (var col in entityDescriptor.allColumns)
+            {
+                colIndex++;
+
+                var column = sheet.Column(colIndex);
+                sheet.SetValue(1, colIndex, col.columnName);
+            }
+
+
+            SetDateTimeFormat();
+
+            await SaveAsync();
+        }
+
+        public virtual void TryDropTable()
+        {
+            if (!TableExist()) return;
+            package.Workbook.Worksheets.Delete(entityDescriptor.tableName);
+            Save();
+        }
+        public virtual async Task TryDropTableAsync()
+        {
+            if (!TableExist()) return;
+
+            package.Workbook.Worksheets.Delete(entityDescriptor.tableName);
+            await SaveAsync();
+        }
+
+
+        public virtual void Truncate()
+        {
+            var lastRowIndex = sheet.Dimension?.End.Row ?? 0;
+            if (lastRowIndex < 2) return;
+
+            sheet.DeleteRow(2, lastRowIndex);
+            Save();
+        }
+
+        public virtual async Task TruncateAsync()
+        {
+            var lastRowIndex = sheet.Dimension?.End.Row ?? 0;
+            if (lastRowIndex < 2) return;
+
+            sheet.DeleteRow(2, lastRowIndex);
+            await SaveAsync();
+        }
+
+        #endregion
+
+
+        #region #1 Create :  Add AddRange
+        public virtual Entity Add(Entity entity)
+        {
+            AddRange(new[] { entity });
+            return entity;
+        }
+
+
+        public virtual async Task<Entity> AddAsync(Entity entity)
+        {
+            await AddRangeAsync(new[] { entity });
+            return entity;
+        }
+        public virtual void AddRange(IEnumerable<Entity> entities)
+        {
+            AddColumnsIfNotExist();
+
+            var lastRowIndex = sheet.Dimension?.End.Row ?? 0;
+            var range = sheet.Cells[lastRowIndex + 1, 1];
+
+            //range.LoadFromCollection(entities, PrintHeaders: false);
+            //var dictionaries = entities.Select(entity => entityDescriptor.allColumns.ToDictionary(col => col.columnName, col => col.GetValue(entity)));
+            //range.LoadFromDictionaries(dictionaries, printHeaders: false);
+
+            foreach (var entity in entities)
+            {
+                lastRowIndex++;
+                SetRow(entity, lastRowIndex);
+            }
+
+            Save();
+        }
+
+
+        public virtual async Task AddRangeAsync(IEnumerable<Entity> entities)
+        {
+            AddColumnsIfNotExist();
+
+            var lastRowIndex = sheet.Dimension?.End.Row ?? 0;
+            var range = sheet.Cells[lastRowIndex + 1, 1];
+
+            //range.LoadFromCollection(entities, PrintHeaders: false);
+            //var dictionaries = entities.Select(entity => entityDescriptor.allColumns.ToDictionary(col => col.columnName, col => col.GetValue(entity)));
+            //range.LoadFromDictionaries(dictionaries, printHeaders: false);
+
+            foreach (var entity in entities)
+            {
+                lastRowIndex++;
+                SetRow(entity, lastRowIndex);
+            }
+
+            await SaveAsync();
+        }
+
+
+
+
+
+        #endregion
+
+
+        #region #2 Retrieve : Get Query
+
+        public virtual Entity Get(object keyValue)
+        {
+            var predicate = GetKeyPredicate(keyValue);
+            return Query().FirstOrDefault(predicate);
+        }
+
+        public virtual Task<Entity> GetAsync(object keyValue)
+        {
+            return Task.Run(() => Get(keyValue));
+        }
+
+        public virtual IQueryable<Entity> Query()
+        {
+            return GetEntities().Select(m => m.entity).AsQueryable();
+        }
+
+        #endregion
+
+
+        #region #3 Update: Update UpdateRange
+        public virtual int Update(Entity entity)
+        {
+            int count = UpdateWithoutSave(entity);
+            Save();
+            return count;
+        }
+        public virtual async Task<int> UpdateAsync(Entity entity)
+        {
+            int count = UpdateWithoutSave(entity);
+            await SaveAsync();
+            return count;
+        }
+
+        protected virtual int UpdateWithoutSave(Entity entity)
+        {
+            AddColumnsIfNotExist();
+
+            var key = entityDescriptor.key.GetValue(entity);
+            int count = 0;
+            foreach (var item in GetEntities())
+            {
+                var oldEntity = item.entity;
+                var oldKey = entityDescriptor.key.GetValue(oldEntity);
+                if (!key.Equals(oldKey)) continue;
+
+                var rowIndex = item.rowIndex;
+                SetRow(entity, rowIndex);
+                count++;
+                break;
+            }
+            return count;
+        }
+
+
+        public virtual int UpdateRange(IEnumerable<Entity> entities)
+        {
+            int count = UpdateRangeWithoutSave(entities);
+            Save();
+            return count;
+        }
+        public virtual async Task<int> UpdateRangeAsync(IEnumerable<Entity> entities)
+        {
+            int count = UpdateRangeWithoutSave(entities);
+            await SaveAsync();
+            return count;
+        }
+
+
+        protected int UpdateRangeWithoutSave<Key>(IEnumerable<Entity> entities)
+        {
+
+            AddColumnsIfNotExist();
+
+            // key -> entity
+            var entityMap =
+                 entities.Select(entity => (key: (Key)entityDescriptor.key.GetValue(entity), entity: entity))
+                 .GroupBy(item => item.key).Select(group => (key: group.Key, entity: group.Last().entity))
+                 .ToDictionary(item => item.key, item => item.entity);
+
+            int count = 0;
+            foreach (var item in GetEntities())
+            {
+                var oldEntity = item.entity;
+                var key = (Key)entityDescriptor.key.GetValue(oldEntity);
+                if (!entityMap.TryGetValue(key, out var entity)) continue;
+
+                var rowIndex = item.rowIndex;
+                SetRow(entity, rowIndex);
+                count++;
+            }
+            return count;
+        }
+
+        #endregion
+
+
+        #region #4 Delete : Delete DeleteRange DeleteByKey DeleteByKeys
+
+        public virtual int Delete(Entity entity)
+        {
+            var keyValue = entityDescriptor.key.GetValue(entity);
+            return DeleteByKey(keyValue);
+        }
+        public virtual Task<int> DeleteAsync(Entity entity)
+        {
+            var keyValue = entityDescriptor.key.GetValue(entity);
+            return DeleteByKeyAsync(keyValue);
+        }
+
+
+
+        public virtual int DeleteRange(IEnumerable<Entity> entities)
+        {
+            var keys = entities.Select(entity => entityDescriptor.key.GetValue(entity));
+            return DeleteByKeys(keys);
+        }
+        public virtual Task<int> DeleteRangeAsync(IEnumerable<Entity> entities)
+        {
+            var keys = entities.Select(entity => entityDescriptor.key.GetValue(entity));
+            return DeleteByKeysAsync<object>(keys);
+        }
+
+
+        public virtual int DeleteByKey(object keyValue)
+        {
+            int count = DeleteByKeyWithoutSave(keyValue);
+            Save();
+            return count;
+        }
+        public virtual async Task<int> DeleteByKeyAsync(object keyValue)
+        {
+            int count = DeleteByKeyWithoutSave(keyValue);
+            await SaveAsync();
+            return count;
+        }
+
+
+
+        public virtual int DeleteByKeys<Key>(IEnumerable<Key> keys)
+        {
+            int count = DeleteByKeysWithoutSave(keys);
+            Save();
+            return count;
+        }
+        public virtual async Task<int> DeleteByKeysAsync<Key>(IEnumerable<Key> keys)
+        {
+            int count = DeleteByKeysWithoutSave(keys);
+            await SaveAsync();
+            return count;
+        }
+
+        #endregion
+
+
+    }
+}

+ 39 - 0
src/Vitorm.Excel/Vitorm.Excel.csproj

@@ -0,0 +1,39 @@
+<Project Sdk="Microsoft.NET.Sdk">
+    <Import Project="..\Versions.props" />
+
+    <PropertyGroup>
+        <pack>nuget</pack>
+    </PropertyGroup>
+
+    <PropertyGroup>
+        <TargetFramework>netstandard2.0</TargetFramework>
+        <LangVersion>9.0</LangVersion>
+    </PropertyGroup>
+
+    <PropertyGroup>
+        <Authors>Lith</Authors>
+        <Description>orm for Excel</Description>
+        <PackageProjectUrl>https://github.com/Vit-Orm/Vitorm.Excel</PackageProjectUrl>
+        <PackageIcon>vitorm_logo_v1.png</PackageIcon>
+        <PackageReadmeFile>README.md</PackageReadmeFile>
+        <PackageTags>orm vitorm database Excel</PackageTags>
+    </PropertyGroup>
+
+    <ItemGroup>
+        <None Include="..\..\doc\vitorm_logo_v1.png">
+            <Pack>True</Pack>
+            <PackagePath>\</PackagePath>
+        </None>
+        <None Include="..\..\README.md">
+            <Pack>True</Pack>
+            <PackagePath>\</PackagePath>
+        </None>
+    </ItemGroup>
+
+    <ItemGroup>
+        <PackageReference Include="EPPlus" Version="7.3.2" />
+        <PackageReference Include="Vitorm" Version="$(Vitorm_Version)" />
+    </ItemGroup>
+
+
+</Project>

+ 76 - 0
test/Vitorm.Excel.Data.MsTest/CommonTest/Common_Test.cs

@@ -0,0 +1,76 @@
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+
+namespace Vitorm.MsTest.CommonTest
+{
+    public class User : Vitorm.MsTest.CommonTest.UserBase
+    {
+    }
+
+
+    [TestClass]
+    public partial class Common_Test : UserTest<User>
+    {
+        [TestMethod]
+        public void Test()
+        {
+            Init();
+
+            Test_DbContext();
+            //Test_Transaction();
+            Test_Get();
+            Test_Query();
+            //Test_QueryJoin();
+            //Test_ToExecuteString();
+            //Test_ExecuteUpdate();
+            //Test_ExecuteDelete();
+            Test_Create();
+            Test_Update();
+            Test_Delete();
+        }
+
+        [TestMethod]
+        public async Task TestAsync()
+        {
+            Init();
+
+            await Test_GetAsync();
+            //await Test_QueryAsync();
+            //await Test_QueryJoinAsync();
+            //await Test_ExecuteUpdateAsync();
+            //await Test_ExecuteDeleteAsync();
+            await Test_UpdateAsync();
+            await Test_DeleteAsync();
+        }
+
+        public override User NewUser(int id, bool forAdd = false) => new User { id = id, name = "testUser" + id };
+
+        public override void WaitForUpdate() => Thread.Sleep(2000);
+
+        public void Init()
+        {
+            using var dbContext = Data.DataProvider<User>()?.CreateDbContext();
+
+            //dbContext.TryDropTable<User>();
+            dbContext.TryCreateTable<User>();
+            dbContext.Truncate<User>();
+
+            var users = new List<User> {
+                    new User { id=1, name="u146", fatherId=4, motherId=6 },
+                    new User { id=2, name="u246", fatherId=4, motherId=6 },
+                    new User { id=3, name="u356", fatherId=5, motherId=6 },
+                    new User { id=4, name="u400" },
+                    new User { id=5, name="u500" },
+                    new User { id=6, name="u600" },
+                };
+            users.ForEach(user => { user.birth = DateTime.Parse("2021-01-01 00:00:00").AddHours(user.id); });
+
+            dbContext.AddRange(users);
+
+            WaitForUpdate();
+
+        }
+
+
+    }
+}

+ 20 - 0
test/Vitorm.Excel.Data.MsTest/CommonTest/UserTest.Entity.cs

@@ -0,0 +1,20 @@
+
+namespace Vitorm.MsTest.CommonTest
+{
+
+    [System.ComponentModel.DataAnnotations.Schema.Table("User")]
+    public class UserBase
+    {
+        [System.ComponentModel.DataAnnotations.Key]
+        public virtual int id { get; set; }
+        public string name { get; set; }
+        public DateTime? birth { get; set; }
+
+        public int? fatherId { get; set; }
+        public int? motherId { get; set; }
+
+        [System.ComponentModel.DataAnnotations.Schema.NotMapped]
+        public string test { get; set; }
+    }
+
+}

+ 291 - 0
test/Vitorm.Excel.Data.MsTest/CommonTest/UserTest.cs

@@ -0,0 +1,291 @@
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+
+namespace Vitorm.MsTest.CommonTest
+{
+    public abstract partial class UserTest<User> where User : UserBase, new()
+    {
+        public virtual void WaitForUpdate() { }
+        public abstract User NewUser(int id, bool forAdd = false);
+
+        public virtual List<User> NewUsers(int startId, int count = 1, bool forAdd = false)
+        {
+            return Enumerable.Range(startId, count).Select(id => NewUser(id, forAdd)).ToList();
+        }
+
+
+        public void Test_DbContext()
+        {
+            #region #0 get DbContext and entityDescriptor
+            {
+                using var dbContext = Data.DataProvider<User>()?.CreateDbContext();
+                var entityDescriptor = dbContext.GetEntityDescriptor(typeof(User));
+                Assert.IsNotNull(entityDescriptor);
+            }
+            #endregion
+        }
+
+        public void Test_Transaction()
+        {
+            #region #0 Transaction
+            {
+                using var dbContext = Data.DataProvider<User>()?.CreateSqlDbContext();
+
+                Assert.AreEqual("u400", dbContext.Get<User>(4).name);
+
+                using (var tran1 = dbContext.BeginTransaction())
+                {
+                    dbContext.Update(new User { id = 4, name = "u4001" });
+                    Assert.AreEqual("u4001", dbContext.Get<User>(4).name);
+
+                    using (var tran2 = dbContext.BeginTransaction())
+                    {
+                        dbContext.Update(new User { id = 4, name = "u4002" });
+                        Assert.AreEqual("u4002", dbContext.Get<User>(4).name);
+
+                        var userSet = dbContext.DbSet<User>();
+                        Assert.AreEqual("u4002", userSet.Get(4).name);
+                    }
+                    Assert.AreEqual("u4001", dbContext.Get<User>(4).name);
+
+                    using (var tran2 = dbContext.BeginTransaction())
+                    {
+                        dbContext.Update(new User { id = 4, name = "u4002" });
+                        Assert.AreEqual("u4002", dbContext.Get<User>(4).name);
+                        tran2.Rollback();
+                    }
+                    Assert.AreEqual("u4001", dbContext.Get<User>(4).name);
+
+                    using (var tran2 = dbContext.BeginTransaction())
+                    {
+                        dbContext.Update(new User { id = 4, name = "u4003" });
+                        Assert.AreEqual("u4003", dbContext.Get<User>(4).name);
+                        tran2.Commit();
+                    }
+                    Assert.AreEqual("u4003", dbContext.Get<User>(4).name);
+
+                    //Assert.AreEqual("u400", Data.Get<User>(4).name);
+                }
+
+                Assert.AreEqual("u400", dbContext.Get<User>(4).name);
+            }
+            #endregion
+        }
+
+
+
+        public void Test_Get()
+        {
+            #region Get
+            {
+                var user = Data.Get<User>(1);
+                Assert.AreEqual(1, user?.id);
+            }
+            #endregion
+        }
+
+        public void Test_Query()
+        {
+            #region Query
+            {
+                var userList = Data.Query<User>().Where(u => u.id == 1).ToList();
+                Assert.AreEqual(1, userList.Count);
+                Assert.AreEqual(1, userList.First().id);
+            }
+            #endregion
+        }
+
+        public void Test_QueryJoin()
+        {
+            #region Query
+            {
+                var query =
+                    from user in Data.Query<User>()
+                    from father in Data.Query<User>().Where(father => user.fatherId == father.id)
+                    where user.id > 2
+                    select new { user, father };
+
+                var userList = query.ToList();
+                Assert.AreEqual(1, userList.Count);
+                Assert.AreEqual(3, userList.First().user.id);
+                Assert.AreEqual(5, userList.First().father.id);
+
+            }
+            #endregion
+        }
+        public void Test_ToExecuteString()
+        {
+            #region ToExecuteString
+            {
+                var query = Data.Query<User>().Where(u => u.id == 1);
+
+                var sql = query.ToExecuteString();
+                Assert.IsNotNull(sql);
+            }
+            #endregion
+        }
+        public void Test_ExecuteUpdate()
+        {
+            #region ExecuteUpdate
+            {
+                var query = Data.Query<User>();
+
+                var count = query.ExecuteUpdate(row => new User
+                {
+                    name = "u_" + row.id + "_" + (row.fatherId.ToString() ?? "") + "_" + (row.motherId.ToString() ?? ""),
+                    birth = DateTime.Parse("2021-01-11 00:00:00")
+                });
+
+                Assert.AreEqual(6, count);
+
+                WaitForUpdate();
+
+                var userList = query.ToList();
+                Assert.AreEqual("u_1_4_6", userList.First().name);
+                Assert.AreEqual(DateTime.Parse("2021-01-11 00:00:00"), userList.First().birth);
+                Assert.AreEqual("u_6__", userList.Last().name);
+            }
+            #endregion
+        }
+        public void Test_ExecuteDelete()
+        {
+            #region ExecuteDelete
+            {
+                var query = Data.Query<User>();
+
+                var count = query.Where(u => u.id == 6).ExecuteDelete();
+
+                //Assert.AreEqual(1, count);
+                WaitForUpdate();
+
+                var userList = query.ToList();
+                Assert.AreEqual(5, userList.Count());
+            }
+            #endregion
+        }
+        public void Test_Create()
+        {
+            #region Create :  Add AddRange
+            {
+                var newUserList = NewUsers(7, 4, forAdd: true);
+
+                // #1 Add
+                Data.Add<User>(newUserList[0]);
+
+                // #2 AddRange
+                Data.AddRange<User>(newUserList.Skip(1));
+
+                WaitForUpdate();
+
+                // assert
+                {
+                    var userList = Data.Query<User>().Where(user => user.id >= 7).ToList();
+                    Assert.AreEqual(newUserList.Count, userList.Count());
+                    Assert.AreEqual(0, userList.Select(m => m.id).Except(newUserList.Select(m => m.id)).Count());
+                    Assert.AreEqual(0, userList.Select(m => m.name).Except(newUserList.Select(m => m.name)).Count());
+                }
+            }
+            #endregion
+        }
+
+        public void Test_Update()
+        {
+            #region Update: Update UpdateRange
+            {
+                var ids = Data.Query<User>().OrderBy(u => u.id).Select(u => u.id).ToArray()[^2..];
+
+                var newUserList = ids.Select(id => NewUser(id)).Append(NewUser(ids.Last() + 1)).ToList();
+
+                // Update
+                {
+                    var rowCount = Data.Update(newUserList[0]);
+                    Assert.AreEqual(1, rowCount);
+                }
+
+                // UpdateRange
+                {
+                    var rowCount = Data.UpdateRange(newUserList.Skip(1));
+                    Assert.AreEqual(1, rowCount);
+                }
+
+                WaitForUpdate();
+
+                // assert
+                {
+                    var userList = Data.Query<User>().Where(m => ids.Contains(m.id)).ToList();
+                    Assert.AreEqual(2, userList.Count());
+                    Assert.AreEqual(0, userList.Select(m => m.id).Except(newUserList.Select(m => m.id)).Count());
+                    Assert.AreEqual(0, userList.Select(m => m.name).Except(newUserList.Select(m => m.name)).Count());
+                }
+
+            }
+            #endregion
+        }
+
+        public void Test_Delete()
+        {
+            #region Delete : Delete DeleteRange DeleteByKey DeleteByKeys
+            {
+                // #1 Delete
+                {
+                    var rowCount = Data.Delete(NewUser(1));
+                    Assert.AreEqual(1, rowCount);
+                }
+
+                // #2 DeleteRange
+                {
+                    var rowCount = Data.DeleteRange(NewUsers(2, 2));
+                    Assert.AreEqual(2, rowCount);
+                }
+
+                // #3 DeleteByKey
+                {
+                    using var dbContext = Data.DataProvider<User>()?.CreateDbContext();
+                    var entityDescriptor = dbContext.GetEntityDescriptor(typeof(User));
+                    var key = entityDescriptor.key;
+
+                    var user = NewUser(4);
+                    var keyValue = key.GetValue(user);
+                    var rowCount = Data.DeleteByKey<User>(keyValue);
+                    Assert.AreEqual(1, rowCount);
+                }
+
+
+                // assert
+                {
+                    WaitForUpdate();
+                    var userList = Data.Query<User>().Where(u => u.id <= 4).ToList();
+                    Assert.AreEqual(0, userList.Count());
+                }
+
+
+                // #4 DeleteByKeys
+                {
+                    using var dbContext = Data.DataProvider<User>()?.CreateDbContext();
+                    var entityDescriptor = dbContext.GetEntityDescriptor(typeof(User));
+                    var key = entityDescriptor.key;
+
+                    var users = Data.Query<User>().ToList();
+                    var keyValues = users.Select(user => key.GetValue(user));
+                    var rowCount = Data.DeleteByKeys<User, object>(keyValues);
+                    Assert.AreEqual(users.Count, rowCount);
+                }
+
+
+                // assert
+                {
+                    WaitForUpdate();
+                    var userList = Data.Query<User>().ToList();
+                    Assert.AreEqual(0, userList.Count());
+                }
+            }
+            #endregion
+        }
+
+        public void Test_Truncate()
+        {
+            Data.Truncate<User>();
+        }
+
+    }
+}

+ 214 - 0
test/Vitorm.Excel.Data.MsTest/CommonTest/UserTestAsync.cs

@@ -0,0 +1,214 @@
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+using Vit.Linq;
+
+namespace Vitorm.MsTest.CommonTest
+{
+    public abstract partial class UserTest<User>
+    {
+
+        public async Task Test_GetAsync()
+        {
+            #region Get
+            {
+                var user = await Data.GetAsync<User>(1);
+                Assert.AreEqual(1, user?.id);
+            }
+            #endregion
+        }
+
+        public async Task Test_QueryAsync()
+        {
+            #region Query
+            {
+                var userList = await Data.Query<User>().Where(u => u.id == 1).ToListAsync();
+                Assert.AreEqual(1, userList.Count);
+                Assert.AreEqual(1, userList.First().id);
+            }
+            #endregion
+        }
+
+        public async Task Test_QueryJoinAsync()
+        {
+            #region Query
+            {
+                var query =
+                    from user in Data.Query<User>()
+                    from father in Data.Query<User>().Where(father => user.fatherId == father.id)
+                    where user.id > 2
+                    select new { user, father };
+
+                var userList = await query.ToListAsync();
+                Assert.AreEqual(1, userList.Count);
+                Assert.AreEqual(3, userList.First().user.id);
+                Assert.AreEqual(5, userList.First().father.id);
+
+            }
+            #endregion
+        }
+
+        public async Task Test_ExecuteUpdateAsync()
+        {
+            #region ExecuteUpdate
+            {
+                var query = Data.Query<User>();
+
+                var count = await query.ExecuteUpdateAsync(row => new User
+                {
+                    name = "u_" + row.id + "_" + (row.fatherId.ToString() ?? "") + "_" + (row.motherId.ToString() ?? ""),
+                    birth = DateTime.Parse("2021-01-11 00:00:00")
+                });
+
+                Assert.AreEqual(6, count);
+
+                WaitForUpdate();
+
+                var userList = query.ToList();
+                Assert.AreEqual("u_1_4_6", userList.First().name);
+                Assert.AreEqual(DateTime.Parse("2021-01-11 00:00:00"), userList.First().birth);
+                Assert.AreEqual("u_6__", userList.Last().name);
+            }
+            #endregion
+        }
+        public async Task Test_ExecuteDeleteAsync()
+        {
+            #region ExecuteDelete
+            {
+                var query = Data.Query<User>();
+
+                var count = await query.Where(u => u.id == 6).ExecuteDeleteAsync();
+
+                //Assert.AreEqual(1, count);
+                WaitForUpdate();
+
+                var userList = query.ToList();
+                Assert.AreEqual(5, userList.Count());
+            }
+            #endregion
+        }
+        public async Task Test_CreateAsync()
+        {
+            #region Create :  Add AddRange
+            {
+                var newUserList = NewUsers(7, 4, forAdd: true);
+
+                // #1 Add
+                await Data.AddAsync<User>(newUserList[0]);
+
+                // #2 AddRange
+                await Data.AddRangeAsync<User>(newUserList.Skip(1));
+
+                WaitForUpdate();
+
+                // assert
+                {
+                    var userList = Data.Query<User>().Where(user => user.id >= 7).ToList();
+                    Assert.AreEqual(newUserList.Count, userList.Count());
+                    Assert.AreEqual(0, userList.Select(m => m.id).Except(newUserList.Select(m => m.id)).Count());
+                    Assert.AreEqual(0, userList.Select(m => m.name).Except(newUserList.Select(m => m.name)).Count());
+                }
+            }
+            #endregion
+        }
+
+        public async Task Test_UpdateAsync()
+        {
+            #region Update: Update UpdateRange
+            {
+                var ids = Data.Query<User>().OrderBy(u => u.id).Select(u => u.id).ToArray()[^2..];
+
+                var newUserList = ids.Select(id => NewUser(id)).Append(NewUser(ids.Last() + 1)).ToList();
+
+                // Update
+                {
+                    var rowCount = await Data.UpdateAsync(newUserList[0]);
+                    Assert.AreEqual(1, rowCount);
+                }
+
+                // UpdateRange
+                {
+                    var rowCount = await Data.UpdateRangeAsync(newUserList.Skip(1));
+                    Assert.AreEqual(1, rowCount);
+                }
+
+                WaitForUpdate();
+
+                // assert
+                {
+                    var userList = Data.Query<User>().Where(m => ids.Contains(m.id)).ToList();
+                    Assert.AreEqual(2, userList.Count());
+                    Assert.AreEqual(0, userList.Select(m => m.id).Except(newUserList.Select(m => m.id)).Count());
+                    Assert.AreEqual(0, userList.Select(m => m.name).Except(newUserList.Select(m => m.name)).Count());
+                }
+
+            }
+            #endregion
+        }
+
+        public async Task Test_DeleteAsync()
+        {
+            #region Delete : Delete DeleteRange DeleteByKey DeleteByKeys
+            {
+                // #1 Delete
+                {
+                    var rowCount = await Data.DeleteAsync(NewUser(1));
+                    //Assert.AreEqual(1, rowCount);
+                }
+
+                // #2 DeleteRange
+                {
+                    var rowCount = await Data.DeleteRangeAsync(NewUsers(2, 2));
+                    //Assert.AreEqual(2, rowCount);
+                }
+
+                // #3 DeleteByKey
+                {
+                    using var dbContext = Data.DataProvider<User>()?.CreateDbContext();
+                    var entityDescriptor = dbContext.GetEntityDescriptor(typeof(User));
+                    var key = entityDescriptor.key;
+
+                    var user = NewUser(4);
+                    var keyValue = key.GetValue(user);
+                    var rowCount = await Data.DeleteByKeyAsync<User>(keyValue);
+                    //Assert.AreEqual(1, rowCount);
+                }
+
+
+                // assert
+                {
+                    WaitForUpdate();
+                    var userList = Data.Query<User>().Where(u => u.id <= 4).ToList();
+                    Assert.AreEqual(0, userList.Count());
+                }
+
+
+                // #4 DeleteByKeys
+                {
+                    using var dbContext = Data.DataProvider<User>()?.CreateDbContext();
+                    var entityDescriptor = dbContext.GetEntityDescriptor(typeof(User));
+                    var key = entityDescriptor.key;
+
+                    var users = Data.Query<User>().ToList();
+                    var keyValues = users.Select(user => key.GetValue(user));
+                    var rowCount = await Data.DeleteByKeysAsync<User, object>(keyValues);
+                    //Assert.AreEqual(users.Count, rowCount);
+                }
+
+
+                // assert
+                {
+                    WaitForUpdate();
+                    var userList = Data.Query<User>().ToList();
+                    Assert.AreEqual(0, userList.Count());
+                }
+            }
+            #endregion
+        }
+
+        public async Task Test_TruncateAsync()
+        {
+            await Data.TruncateAsync<User>();
+        }
+
+    }
+}

+ 37 - 0
test/Vitorm.Excel.Data.MsTest/Vitorm.Excel.Data.MsTest.csproj

@@ -0,0 +1,37 @@
+<Project Sdk="Microsoft.NET.Sdk">
+    <Import Project="..\..\src\Versions.props" />
+
+    <PropertyGroup>
+        <test>MSTest</test>
+    </PropertyGroup>
+
+    <PropertyGroup>
+        <TargetFramework>net6.0</TargetFramework>
+        <ImplicitUsings>enable</ImplicitUsings>
+
+        <IsPackable>false</IsPackable>
+        <IsTestProject>true</IsTestProject>
+        <RootNamespace>Vitorm.MsTest</RootNamespace>
+    </PropertyGroup>
+
+    <ItemGroup>
+        <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.0" />
+        <PackageReference Include="MSTest.TestAdapter" Version="3.5.2" />
+        <PackageReference Include="MSTest.TestFramework" Version="3.5.2" />
+
+        <PackageReference Include="Vit.Core" Version="2.2.0" />
+        <PackageReference Include="Vitorm.Data" Version="$(Vitorm_Version)"  />
+    </ItemGroup>
+
+    <ItemGroup>
+        <ProjectReference Include="..\..\src\Vitorm.Excel\Vitorm.Excel.csproj" />
+    </ItemGroup>
+
+
+    <ItemGroup>
+        <None Update="appsettings.json">
+            <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+        </None>
+    </ItemGroup>
+
+</Project>

+ 11 - 0
test/Vitorm.Excel.Data.MsTest/appsettings.json

@@ -0,0 +1,11 @@
+{
+  "Vitorm": {
+    "Data": [
+      {
+        "provider": "Excel",
+        "namespace": "Vitorm.MsTest",
+        "connectionString": "db_orm.xlsx"
+      }
+    ]
+  }
+}

+ 170 - 0
test/Vitorm.Excel.MsTest/CommonTest/CRUDAsync_Test.cs

@@ -0,0 +1,170 @@
+using System.Data;
+
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+using Vit.Linq;
+
+namespace Vitorm.MsTest.CommonTest
+{
+
+    [TestClass]
+    public partial class CRUDAsync_Test
+    {
+        static DbContext CreateDbContext() => DataSource.CreateDbContextForWriting();
+
+        #region #0 Schema
+        [TestMethod]
+        public async Task Test_Schema()
+        {
+            using var dbContext = CreateDbContext();
+
+            await dbContext.TryDropTableAsync<User>();
+            await dbContext.TryDropTableAsync<User>();
+
+            await dbContext.TryCreateTableAsync<User>();
+            await dbContext.TryCreateTableAsync<User>();
+        }
+        #endregion
+
+
+        #region #1 Create
+        [TestMethod]
+        public async Task Test_Create()
+        {
+            using var dbContext = CreateDbContext();
+
+            var newUserList = User.NewUsers(7, 4, forAdd: true);
+
+
+            // #1 Add
+            await dbContext.AddAsync(newUserList[0]);
+
+            // #2 AddRange
+            await dbContext.AddRangeAsync(newUserList.Skip(1));
+
+
+            DataSource.WaitForUpdate();
+
+            // assert
+            {
+                var userList = dbContext.Query<User>().Where(user => user.id >= 7).ToList();
+                Assert.AreEqual(newUserList.Count, userList.Count());
+                Assert.AreEqual(0, userList.Select(m => m.id).Except(newUserList.Select(m => m.id)).Count());
+                Assert.AreEqual(0, userList.Select(m => m.name).Except(newUserList.Select(m => m.name)).Count());
+            }
+
+            try
+            {
+                await dbContext.AddAsync(newUserList[0]);
+                //Assert.Fail("should not be able to add same key twice");
+            }
+            catch (Exception ex) when (ex is not AssertFailedException)
+            {
+            }
+
+        }
+        #endregion
+
+        #region #2 Retrieve : Get Query
+        [TestMethod]
+        public async Task Test_Retrieve()
+        {
+            using var dbContext = CreateDbContext();
+
+            // #1 Get
+            {
+                var user = await dbContext.GetAsync<User>(1);
+                Assert.AreEqual(1, user.id);
+            }
+
+            // #2 Query
+            //{
+            //    var userList = await dbContext.Query<User>().ToListAsync();
+            //    Assert.AreEqual(6, userList.Count());
+            //}
+        }
+        #endregion
+
+
+        #region #3 Update
+        [TestMethod]
+        public async Task Test_Update()
+        {
+            using var dbContext = CreateDbContext();
+
+            // Update
+            {
+                var rowCount = await dbContext.UpdateAsync(User.NewUser(4));
+                Assert.AreEqual(1, rowCount);
+            }
+
+            // UpdateRange
+            {
+                var rowCount = await dbContext.UpdateRangeAsync(User.NewUsers(5, 3));
+                Assert.AreEqual(2, rowCount);
+            }
+
+            DataSource.WaitForUpdate();
+
+            // assert
+            {
+                var newUserList = User.NewUsers(4, 3, forAdd: false);
+                var userList = dbContext.Query<User>().Where(m => m.id >= 4).ToList();
+                Assert.AreEqual(newUserList.Count, userList.Count());
+                Assert.AreEqual(0, userList.Select(m => m.id).Except(newUserList.Select(m => m.id)).Count());
+                Assert.AreEqual(0, userList.Select(m => m.name).Except(newUserList.Select(m => m.name)).Count());
+            }
+
+        }
+        #endregion
+
+
+        #region #4 Delete
+        [TestMethod]
+        public async Task Test_Delete()
+        {
+            using var dbContext = CreateDbContext();
+
+            // #1 Delete
+            {
+                var rowCount = await dbContext.DeleteAsync(User.NewUser(1));
+                Assert.AreEqual(1, rowCount);
+            }
+
+            // #2 DeleteRange
+            {
+                var rowCount = await dbContext.DeleteRangeAsync(User.NewUsers(2, 2));
+                Assert.AreEqual(2, rowCount);
+            }
+
+            // #3 DeleteByKey
+            {
+                var user = User.NewUser(4);
+                var key = dbContext.GetEntityDescriptor(typeof(User)).key;
+                var keyValue = key.GetValue(user);
+                var rowCount = await dbContext.DeleteByKeyAsync<User>(keyValue);
+                Assert.AreEqual(1, rowCount);
+            }
+
+            // #4 DeleteByKeys
+            {
+                var users = User.NewUsers(5, 2);
+                var key = dbContext.GetEntityDescriptor(typeof(User)).key;
+                var keyValues = users.Select(user => key.GetValue(user));
+                var rowCount = await dbContext.DeleteByKeysAsync<User, object>(keyValues);
+                Assert.AreEqual(2, rowCount);
+            }
+
+            DataSource.WaitForUpdate();
+
+            // assert
+            {
+                var userList = dbContext.Query<User>().ToList();
+                Assert.AreEqual(0, userList.Count());
+            }
+        }
+        #endregion
+
+
+    }
+}

+ 168 - 0
test/Vitorm.Excel.MsTest/CommonTest/CRUD_Test.cs

@@ -0,0 +1,168 @@
+using System.Data;
+
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace Vitorm.MsTest.CommonTest
+{
+
+    [TestClass]
+    public partial class CRUD_Test
+    {
+        static DbContext CreateDbContext() => DataSource.CreateDbContextForWriting();
+
+        #region #0 Schema
+        [TestMethod]
+        public void Test_Schema()
+        {
+            using var dbContext = CreateDbContext();
+
+            dbContext.TryDropTable<User>();
+            dbContext.TryDropTable<User>();
+
+            dbContext.TryCreateTable<User>();
+            dbContext.TryCreateTable<User>();
+        }
+        #endregion
+
+        #region #1 Create
+        [TestMethod]
+        public void Test_Create()
+        {
+            using var dbContext = CreateDbContext();
+
+            var newUserList = User.NewUsers(7, 4, forAdd: true);
+
+
+            // #1 Add
+            dbContext.Add(newUserList[0]);
+
+            // #2 AddRange
+            dbContext.AddRange(newUserList.Skip(1));
+
+
+            DataSource.WaitForUpdate();
+
+            // assert
+            {
+                var userList = dbContext.Query<User>().Where(user => user.id >= 7).ToList();
+                Assert.AreEqual(newUserList.Count, userList.Count);
+                Assert.AreEqual(0, userList.Select(m => m.id).Except(newUserList.Select(m => m.id)).Count());
+                Assert.AreEqual(0, userList.Select(m => m.name).Except(newUserList.Select(m => m.name)).Count());
+            }
+
+            try
+            {
+                dbContext.Add(newUserList[0]);
+                //Assert.Fail("should not be able to add same key twice");
+            }
+            catch (Exception ex) when (ex is not AssertFailedException)
+            {
+            }
+
+
+        }
+        #endregion
+
+        #region #2 Retrieve : Get Query
+        [TestMethod]
+        public void Test_Retrieve()
+        {
+            using var dbContext = CreateDbContext();
+
+            // #1 Get
+            {
+                var user = dbContext.Get<User>(1);
+                Assert.AreEqual(1, user.id);
+            }
+
+            // #2 Query
+            {
+                var userList = dbContext.Query<User>().ToList();
+                Assert.AreEqual(6, userList.Count);
+            }
+        }
+        #endregion
+
+
+        #region #3 Update
+        [TestMethod]
+        public void Test_Update()
+        {
+            using var dbContext = CreateDbContext();
+
+            // Update
+            {
+                var rowCount = dbContext.Update(User.NewUser(4));
+                Assert.AreEqual(1, rowCount);
+            }
+
+            // UpdateRange
+            {
+                var rowCount = dbContext.UpdateRange(User.NewUsers(5, 3));
+                Assert.AreEqual(2, rowCount);
+            }
+
+            DataSource.WaitForUpdate();
+
+            // assert
+            {
+                var newUserList = User.NewUsers(4, 3, forAdd: false);
+                var userList = dbContext.Query<User>().Where(m => m.id >= 4).ToList();
+                Assert.AreEqual(newUserList.Count, userList.Count);
+                Assert.AreEqual(0, userList.Select(m => m.id).Except(newUserList.Select(m => m.id)).Count());
+                Assert.AreEqual(0, userList.Select(m => m.name).Except(newUserList.Select(m => m.name)).Count());
+            }
+
+        }
+        #endregion
+
+
+        #region #4 Delete
+        [TestMethod]
+        public void Test_Delete()
+        {
+            using var dbContext = CreateDbContext();
+
+            // #1 Delete
+            {
+                var rowCount = dbContext.Delete(User.NewUser(1));
+                Assert.AreEqual(1, rowCount);
+            }
+
+            // #2 DeleteRange
+            {
+                var rowCount = dbContext.DeleteRange(User.NewUsers(2, 2));
+                Assert.AreEqual(2, rowCount);
+            }
+
+            // #3 DeleteByKey
+            {
+                var user = User.NewUser(4);
+                var key = dbContext.GetEntityDescriptor(typeof(User)).key;
+                var keyValue = key.GetValue(user);
+                var rowCount = dbContext.DeleteByKey<User>(keyValue);
+                Assert.AreEqual(1, rowCount);
+            }
+
+            // #4 DeleteByKeys
+            {
+                var users = User.NewUsers(5, 2);
+                var key = dbContext.GetEntityDescriptor(typeof(User)).key;
+                var keyValues = users.Select(user => key.GetValue(user));
+                var rowCount = dbContext.DeleteByKeys<User, object>(keyValues);
+                Assert.AreEqual(2, rowCount);
+            }
+
+            DataSource.WaitForUpdate();
+
+            // assert
+            {
+                var userList = dbContext.Query<User>().ToList();
+                Assert.AreEqual(0, userList.Count);
+            }
+        }
+        #endregion
+
+
+    }
+}

+ 80 - 0
test/Vitorm.Excel.MsTest/CommonTest/ChangeDatabase_Test.cs

@@ -0,0 +1,80 @@
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace Vitorm.MsTest.CommonTest
+{
+
+    [TestClass]
+    public partial class ChangeDatabase_Test
+    {
+
+        [TestMethod]
+        public void Test_ChangeDatabase()
+        {
+            string databaseName;
+
+            // database
+            {
+                using var dbContext = DataSource.CreateDbContextForWriting(autoInit: false);
+
+                databaseName = dbContext.databaseName;
+
+                dbContext.TryDropTable<User>();
+                dbContext.TryCreateTable<User>();
+
+                var user = dbContext.Get<User>(1);
+                Assert.IsNull(user);
+
+                user = User.NewUser(id: 1, forAdd: true);
+                user.name = "Hello database";
+                dbContext.Add(user);
+            }
+
+            // database2
+            {
+                using var dbContext = DataSource.CreateDbContextForWriting(autoInit: false);
+
+                dbContext.ChangeDatabase(databaseName + "2");
+
+                dbContext.TryDropTable<User>();
+                dbContext.TryCreateTable<User>();
+
+                var user = dbContext.Get<User>(1);
+                Assert.IsNull(user);
+
+                user = User.NewUser(id: 1, forAdd: true);
+                user.name = "Hello database2";
+                dbContext.Add(user);
+            }
+
+
+            DataSource.WaitForUpdate();
+
+            // database
+            {
+                using var dbContext = DataSource.CreateDbContextForWriting(autoInit: false);
+
+                dbContext.ChangeDatabase(databaseName);
+                var user = dbContext.Get<User>(1);
+                Assert.AreEqual("Hello database", user.name);
+            }
+
+            // database2
+            {
+                using var dbContext = DataSource.CreateDbContextForWriting(autoInit: false);
+
+                dbContext.ChangeDatabase(databaseName + "2");
+
+                var user = dbContext.Get<User>(1);
+                Assert.AreEqual("Hello database2", user.name);
+            }
+
+
+        }
+
+
+
+
+
+
+    }
+}

+ 71 - 0
test/Vitorm.Excel.MsTest/CommonTest/ChangeTable_Test.cs

@@ -0,0 +1,71 @@
+using System.Data;
+
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace Vitorm.MsTest.CommonTest
+{
+
+    [TestClass]
+    public partial class ChangeTable_Test
+    {
+
+        [TestMethod]
+        public void Test_ChangeTable()
+        {
+            string tableName;
+
+            using var dbContext = DataSource.CreateDbContextForWriting();
+
+            var dbSet = dbContext.DbSet<User>();
+            tableName = dbSet.entityDescriptor.tableName;
+            User user;
+
+            // User
+            {
+                user = dbSet.Get(1);
+                Assert.IsNotNull(user);
+            }
+
+            // User2
+            {
+                dbSet.ChangeTable(tableName + "2");
+
+                dbSet.TryDropTable();
+                dbSet.TryCreateTable();
+
+                user = dbSet.Get(1);
+                Assert.IsNull(user);
+
+                var users = User.NewUsers(startId: 1, count: 5, forAdd: true);
+                user = users[1];
+                user.name = "Hello User2";
+                dbSet.AddRange(users);
+
+                dbSet.DeleteByKey(1);
+
+            }
+
+            DataSource.WaitForUpdate();
+
+            // Assert User2
+            {
+                var userList = dbSet.Query().Where(user => new[] { 1, 2, 3 }.Contains(user.id)).OrderBy(u => u.id).ToList();
+
+                Assert.AreEqual(2, userList.Count);
+                Assert.AreEqual(2, userList[0].id);
+                Assert.AreEqual("Hello User2", userList[0].name);
+            }
+
+            // Assert User
+            {
+                //dbSet.ChangeTableBack();
+                dbSet.ChangeTable(tableName);
+                Assert.AreEqual("u246", dbSet.Get(2)?.name);
+            }
+
+        }
+
+
+
+    }
+}

+ 169 - 0
test/Vitorm.Excel.MsTest/CommonTest/EntityLoader_CustomLoader_Test.cs

@@ -0,0 +1,169 @@
+using System.Reflection;
+
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+using Vitorm.Entity;
+using Vitorm.Entity.Loader.DataAnnotations;
+
+namespace Vitorm.MsTest.CommonTest
+{
+    [TestClass]
+    public class EntityLoader_CustomLoader_Test
+    {
+        [TestMethod]
+        public void Test_EntityDescriptor()
+        {
+            using var dbContext = DataSource.CreateDbContext();
+            var entityDescriptor = dbContext.GetEntityDescriptor(typeof(User));
+            var key = entityDescriptor.key;
+
+            Assert.AreEqual("userId", key.columnName);
+        }
+
+
+        [TestMethod]
+        public void Test_EntityLoader()
+        {
+            using var dbContext = DataSource.CreateDbContext();
+
+            // #1 EntityLoaderAttribute
+            {
+                var users = dbContext.Query<CustomUser2>().Where(m => m.name == "u146").ToList();
+                Assert.AreEqual(1, users.Count());
+                Assert.AreEqual(1, users[0].id);
+            }
+
+            // #2 defaultEntityLoader
+            {
+                EntityLoaders.Instance.loaders.Insert(0, new CustomEntityLoaderAttribute());
+
+                var users = dbContext.Query<CustomUser>().Where(m => m.name == "u146").ToList();
+                Assert.AreEqual(1, users.Count());
+                Assert.AreEqual(1, users[0].id);
+            }
+
+        }
+
+
+        #region Custom Entity
+
+        [CustomEntityLoader]
+        public class CustomUser2 : CustomUser
+        {
+        }
+
+
+        [Property(name = "TableName", value = "User")]
+        //[Property(name = "Schema", value = "orm")]
+        public class CustomUser
+        {
+            [Label("Key")]
+            [Label("Identity")]
+            [Property(name = "ColumnName", value = "userId")]
+            public int id { get; set; }
+
+            [Property(name = "ColumnName", value = "userName")]
+            [Property(name = "TypeName", value = "varchar(1000)")]
+            [Label("Required")]
+            public string name { get; set; }
+
+            [Property(name = "ColumnName", value = "userBirth")]
+            public DateTime? birth { get; set; }
+
+            [Property(name = "ColumnName", value = "userFatherId")]
+            public int? fatherId { get; set; }
+            [Property(name = "ColumnName", value = "userMotherId")]
+            public int? motherId { get; set; }
+
+            [Label("NotMapped")]
+            public string test { get; set; }
+
+            public static CustomUser NewUser(int id, bool forAdd = false) => new CustomUser { id = id, name = "testUser" + id };
+        }
+
+        #endregion
+
+
+        #region Custom EntityLoader Attribute
+
+        [System.AttributeUsage(System.AttributeTargets.All, Inherited = true, AllowMultiple = true)]
+        public class LabelAttribute : Attribute
+        {
+            public LabelAttribute(string label) { this.label = label; }
+            public string label { get; init; }
+        }
+
+        [System.AttributeUsage(System.AttributeTargets.All, Inherited = true, AllowMultiple = true)]
+        public class PropertyAttribute : Attribute
+        {
+            public string name { get; set; }
+            public string value { get; set; }
+        }
+        #endregion
+
+        #region Custom EntityLoader
+        public class CustomEntityLoaderAttribute : Attribute, IEntityLoader
+        {
+            public void CleanCache() { }
+            public (bool success, IEntityDescriptor entityDescriptor) LoadDescriptor(Type entityType) => LoadFromType(entityType);
+            public (bool success, IEntityDescriptor entityDescriptor) LoadDescriptorWithoutCache(Type entityType) => LoadFromType(entityType);
+
+            public static bool GetTableName(Type entityType, out string tableName, out string schema)
+            {
+                var properties = entityType?.GetCustomAttributes<PropertyAttribute>();
+                tableName = properties?.FirstOrDefault(attr => attr.name == "TableName")?.value;
+                schema = properties?.FirstOrDefault(attr => attr.name == "Schema")?.value;
+                return tableName != null;
+            }
+
+            public static (bool success, IEntityDescriptor EntityDescriptor) LoadFromType(Type entityType)
+            {
+                if (!GetTableName(entityType, out var tableName, out var schema)) return default;
+
+                IColumnDescriptor[] allColumns = entityType?.GetProperties(BindingFlags.Public | BindingFlags.Instance)
+                    .Select(propertyInfo =>
+                    {
+                        var labels = propertyInfo?.GetCustomAttributes<LabelAttribute>();
+                        var properties = propertyInfo?.GetCustomAttributes<PropertyAttribute>();
+
+                        if (labels.Any(m => m.label == "NotMapped")) return null;
+
+                        // #1 isKey
+                        bool isKey = labels.Any(m => m.label == "Key");
+
+                        // #2 column name and type
+                        var columnName = properties.FirstOrDefault(attr => attr.name == "ColumnName")?.value ?? propertyInfo.Name;
+                        var columnDbType = properties.FirstOrDefault(attr => attr.name == "TypeName")?.value;
+                        int? columnOrder = int.TryParse(properties.FirstOrDefault(attr => attr.name == "ColumnOrder")?.value, out var order) ? order : null;
+
+                        // #3 isIdentity
+                        var isIdentity = labels.Any(m => m.label == "Identity");
+
+                        // #4 isNullable
+                        bool isNullable;
+                        if (labels.Any(m => m.label == "Required")) isNullable = false;
+                        else
+                        {
+                            var type = propertyInfo.PropertyType;
+                            if (type == typeof(string)) isNullable = true;
+                            else
+                            {
+                                isNullable = TypeUtil.IsNullable(type);
+                            }
+                        }
+
+                        return new ColumnDescriptor(
+                            propertyInfo, columnName: columnName,
+                            isKey: isKey, isIdentity: isIdentity, isNullable: isNullable,
+                            columnDbType: columnDbType,
+                            columnOrder: columnOrder
+                            );
+                    }).Where(column => column != null).ToArray();
+
+                return (true, new EntityDescriptor(entityType, allColumns, tableName, schema));
+            }
+        }
+        #endregion
+
+    }
+}

+ 56 - 0
test/Vitorm.Excel.MsTest/CommonTest/EntityLoader_Test.cs

@@ -0,0 +1,56 @@
+using System.ComponentModel.DataAnnotations.Schema;
+
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace Vitorm.MsTest.CommonTest
+{
+    [TestClass]
+    public class EntityLoader_Test
+    {
+        [TestMethod]
+        public void Test_EntityDescriptor()
+        {
+            using var dbContext = DataSource.CreateDbContext();
+
+            {
+                var entityDescriptor = dbContext.GetEntityDescriptor<User>();
+                Assert.AreEqual("id", entityDescriptor.key?.columnName);
+                Assert.AreEqual("User", entityDescriptor.tableName);
+            }
+
+            {
+                var entityDescriptor = dbContext.GetEntityDescriptor<User2>();
+                Assert.IsNull(entityDescriptor.key);
+                Assert.AreEqual("User", entityDescriptor.tableName);
+            }
+
+            {
+                var entityDescriptor = dbContext.GetEntityDescriptor<User3>();
+                Assert.IsNull(entityDescriptor);
+            }
+        }
+
+
+        #region Custom Entity
+
+        public class User
+        {
+            public int id { get; set; }
+            public string name { get; set; }
+        }
+
+        [Vitorm.Entity.Loader.DataAnnotations.StrictEntityLoader]
+        [Table("User")]
+        public class User2 : User { }
+
+
+        [Vitorm.Entity.Loader.DataAnnotations.StrictEntityLoader]
+        public class User3 : User { }
+
+        #endregion
+
+
+
+
+    }
+}

+ 89 - 0
test/Vitorm.Excel.MsTest/CommonTest/Property_Bool_Test.cs

@@ -0,0 +1,89 @@
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+using System.Data;
+
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace Vitorm.MsTest.CommonTest
+{
+
+    [TestClass]
+    public class Property_Bool_Test
+    {
+        [TestMethod]
+        public void Test()
+        {
+            using var dbContext = DataSource.CreateDbContextForWriting();
+
+            // #1 Init
+            var name = Guid.NewGuid().ToString();
+            var dbSet = dbContext.DbSet<MyUser>();
+            dbSet.TryDropTable();
+            dbSet.TryCreateTable();
+            dbSet.Add(new MyUser { id = 1, enable = true, isEven = null });
+            dbSet.Add(new MyUser { id = 2, enable = true, isEven = true });
+            dbSet.Add(new MyUser { id = 3, enable = false, isEven = false });
+
+            DataSource.WaitForUpdate();
+
+            // #2 Assert
+            {
+                var user = dbSet.Query().Where(m => m.enable == true).OrderBy(m => m.id).First();
+                Assert.AreEqual(1, user.id);
+            }
+            {
+                var user = dbSet.Query().Where(m => m.enable == false).OrderBy(m => m.id).First();
+                Assert.AreEqual(3, user.id);
+            }
+            {
+                var user = dbSet.Query().Where(m => m.enable).OrderBy(m => m.id).First();
+                Assert.AreEqual(1, user.id);
+            }
+            {
+                var user = dbSet.Query().Where(m => !m.enable).OrderBy(m => m.id).First();
+                Assert.AreEqual(3, user.id);
+            }
+            {
+                var user = dbSet.Query().Where(m => m.isEven == null).OrderBy(m => m.id).First();
+                Assert.AreEqual(1, user.id);
+            }
+            {
+                var user = dbSet.Query().Where(m => m.isEven == true).OrderBy(m => m.id).First();
+                Assert.AreEqual(2, user.id);
+            }
+            {
+                var user = dbSet.Query().Where(m => m.isEven == false).OrderBy(m => m.id).First();
+                Assert.AreEqual(3, user.id);
+            }
+            //{
+            //    var user = dbSet.Query().Where(m => m.isEven.Value).OrderBy(m => m.id).First();
+            //    Assert.AreEqual(2, user.id);
+            //}
+            //{
+            //    var user = dbSet.Query().Where(m => !m.isEven.Value).OrderBy(m => m.id).First();
+            //    Assert.AreEqual(3, user.id);
+            //}
+            {
+                var query = dbSet.Query().Where(m => m.isEven == true).OrderBy(m => m.isEven).Select(m => new { m.id, m.isEven });
+                var users = query.ToList();
+                var user = users.First();
+                Assert.AreEqual(2, user.id);
+                Assert.AreEqual(true, user.isEven);
+            }
+        }
+
+
+
+        // Entity
+        [Table("MyUser")]
+        public class MyUser
+        {
+            [Key]
+            public int id { get; set; }
+            public bool? isEven { get; set; }
+            public bool enable { get; set; }
+        }
+
+
+    }
+}

+ 58 - 0
test/Vitorm.Excel.MsTest/CommonTest/Property_DateTime_Test.cs

@@ -0,0 +1,58 @@
+using System.Data;
+
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace Vitorm.MsTest.CommonTest
+{
+
+    [TestClass]
+    public class Property_DateTime_Test
+    {
+
+        [TestMethod]
+        public void Test_Equal()
+        {
+            using var dbContext = DataSource.CreateDbContext();
+            var userQuery = dbContext.Query<User>();
+
+            // ==
+            {
+                var userList = userQuery.Where(u => u.birth == new DateTime(2021, 01, 01, 03, 00, 00)).ToList();
+                Assert.AreEqual(1, userList.Count);
+                Assert.AreEqual(3, userList.First().id);
+            }
+        }
+
+        [TestMethod]
+        public void Test_Compare()
+        {
+            using var dbContext = DataSource.CreateDbContext();
+            var userQuery = dbContext.Query<User>();
+
+            {
+                var userList = userQuery.Where(u => u.birth >= new DateTime(2021, 01, 01, 05, 00, 00)).ToList();
+                Assert.AreEqual(2, userList.Count);
+                Assert.AreEqual(0, userList.Select(m => m.id).Except(new[] { 5, 6 }).Count());
+            }
+
+        }
+
+
+        [TestMethod]
+        public void Test_Calculate()
+        {
+            using var dbContext = DataSource.CreateDbContext();
+            var userQuery = dbContext.Query<User>();
+
+            {
+                var userList = userQuery.Where(u => u.birth == DateTime.Parse("2021-01-01 01:00:00").AddHours(2)).ToList();
+                Assert.AreEqual(1, userList.Count);
+                Assert.AreEqual(3, userList.First().id);
+            }
+
+        }
+
+
+
+    }
+}

+ 37 - 0
test/Vitorm.Excel.MsTest/CommonTest/Property_Numeric_Calculate_Test.cs

@@ -0,0 +1,37 @@
+using System.Data;
+
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace Vitorm.MsTest.CommonTest
+{
+
+    [TestClass]
+    public class Property_Numeric_Calculate_Test
+    {
+
+        [TestMethod]
+        public void Test_Calculate()
+        {
+            using var dbContext = DataSource.CreateDbContext();
+            var userQuery = dbContext.Query<User>();
+
+            {
+                var userList = userQuery.Where(u => u.id + 1 == 4).ToList();
+                Assert.AreEqual(1, userList.Count);
+                Assert.AreEqual(3, userList.First().id);
+            }
+            {
+                var userList = userQuery.Where(u => 4 == u.id + 1).ToList();
+                Assert.AreEqual(1, userList.Count);
+                Assert.AreEqual(3, userList.First().id);
+            }
+            {
+                var userList = userQuery.Where(u => u.id == 4 - 1).ToList();
+                Assert.AreEqual(1, userList.Count);
+                Assert.AreEqual(3, userList.First().id);
+            }
+        }
+
+
+    }
+}

+ 129 - 0
test/Vitorm.Excel.MsTest/CommonTest/Property_Numeric_Test.cs

@@ -0,0 +1,129 @@
+using System.Data;
+
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace Vitorm.MsTest.CommonTest
+{
+
+    [TestClass]
+    public class Property_Numeric_Test
+    {
+        // Enumerable.Contains
+        // Queryable.Contains
+        [TestMethod]
+        public void Test_In()
+        {
+            using var dbContext = DataSource.CreateDbContext();
+            var userQuery = dbContext.Query<User>();
+
+            // Array.Contains
+            {
+                var userList = userQuery.Where(u => new[] { 3, 5 }.Contains(u.id)).ToList();
+                Assert.AreEqual(2, userList.Count);
+                Assert.AreEqual(0, userList.Select(m => m.id).Except(new[] { 3, 5 }).Count());
+            }
+
+            // List.Contains
+            {
+                var ids = new[] { 3, 5 }.ToList();
+                var userList = userQuery.Where(u => ids.Contains(u.id)).ToList();
+                Assert.AreEqual(2, userList.Count);
+                Assert.AreEqual(0, userList.Select(m => m.id).Except(new[] { 3, 5 }).Count());
+            }
+
+            // Enumerable.Contains
+            {
+                var ids = new[] { 3, 5 }.AsEnumerable();
+                var userList = userQuery.Where(u => ids.Contains(u.id)).ToList();
+                Assert.AreEqual(2, userList.Count);
+                Assert.AreEqual(0, userList.Select(m => m.id).Except(new[] { 3, 5 }).Count());
+            }
+
+            // Queryable.Contains
+            {
+                var ids = new[] { 3, 5 }.AsQueryable();
+                var userList = userQuery.Where(u => ids.Contains(u.id)).ToList();
+                Assert.AreEqual(2, userList.Count);
+                Assert.AreEqual(0, userList.Select(m => m.id).Except(new[] { 3, 5 }).Count());
+            }
+
+
+            // not Contains
+            {
+                var userList = userQuery.Where(u => !new[] { 3, 5 }.Contains(u.id)).ToList();
+                Assert.AreEqual(4, userList.Count);
+                Assert.AreEqual(0, userList.Select(m => m.id).Except(new[] { 1, 2, 4, 6 }).Count());
+            }
+        }
+
+
+        [TestMethod]
+        public void Test_Equal()
+        {
+            using var dbContext = DataSource.CreateDbContext();
+            var userQuery = dbContext.Query<User>();
+
+            // #1 ==
+            {
+                var userList = userQuery.Where(u => u.id == 3 || 5 == u.id).ToList();
+                Assert.AreEqual(2, userList.Count);
+                Assert.AreEqual(0, userList.Select(m => m.id).Except(new[] { 3, 5 }).Count());
+            }
+
+            // #2 !=
+            {
+                var userList = userQuery.Where(u => u.id != 1).ToList();
+                Assert.AreEqual(5, userList.Count);
+                Assert.AreEqual(0, userList.Select(m => m.id).Except(new[] { 2, 3, 4, 5, 6 }).Count());
+            }
+            {
+                var userList = userQuery.Where(u => 1 != u.id).ToList();
+                Assert.AreEqual(5, userList.Count);
+                Assert.AreEqual(0, userList.Select(m => m.id).Except(new[] { 2, 3, 4, 5, 6 }).Count());
+            }
+        }
+
+
+
+        [TestMethod]
+        public void Test_Compare()
+        {
+            using var dbContext = DataSource.CreateDbContext();
+            var userQuery = dbContext.Query<User>();
+
+            // #1 > and <
+            {
+                var userList = userQuery.Where(u => u.id > 2).Where(m => m.id < 4).ToList();
+                Assert.AreEqual(1, userList.Count);
+                Assert.AreEqual(3, userList.First().id);
+            }
+
+            // #2  > or <
+            {
+                var userList = userQuery.Where(u => u.id > 5 || u.id < 2).ToList();
+                Assert.AreEqual(2, userList.Count);
+                Assert.AreEqual(0, userList.Select(m => m.id).Except(new[] { 1, 6 }).Count());
+            }
+
+            // #3  >= or <=
+            {
+                var userList = userQuery.Where(u => u.id >= 6 || u.id <= 1).ToList();
+                Assert.AreEqual(2, userList.Count);
+                Assert.AreEqual(0, userList.Select(m => m.id).Except(new[] { 1, 6 }).Count());
+            }
+
+            // #4  in right side
+            {
+                var userList = userQuery.Where(u => 4 >= u.id && 3 <= u.id).ToList();
+                Assert.AreEqual(2, userList.Count);
+                Assert.AreEqual(0, userList.Select(m => m.id).Except(new[] { 3, 4 }).Count());
+            }
+
+        }
+
+
+
+
+
+    }
+}

+ 39 - 0
test/Vitorm.Excel.MsTest/CommonTest/Property_String_Calculate_Test.cs

@@ -0,0 +1,39 @@
+using System.Data;
+
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace Vitorm.MsTest.CommonTest
+{
+
+    [TestClass]
+    public class Property_String_Calculate_Test
+    {
+
+
+        [TestMethod]
+        public void Test_Calculate()
+        {
+            using var dbContext = DataSource.CreateDbContext();
+            var userQuery = dbContext.Query<User>();
+
+            {
+                var userList = userQuery.Where(u => u.name + 1 == "u3561").ToList();
+                Assert.AreEqual(1, userList.Count);
+                Assert.AreEqual(3, userList[0].id);
+            }
+            {
+                var userList = userQuery.Where(u => "u3561" == u.name + 1).ToList();
+                Assert.AreEqual(1, userList.Count);
+                Assert.AreEqual(3, userList[0].id);
+            }
+            {
+                var userList = userQuery.Where(u => u.name == "u35" + 6).ToList();
+                Assert.AreEqual(1, userList.Count);
+                Assert.AreEqual(3, userList[0].id);
+            }
+
+        }
+
+
+    }
+}

+ 50 - 0
test/Vitorm.Excel.MsTest/CommonTest/Property_String_Like_Test.cs

@@ -0,0 +1,50 @@
+using System.Data;
+
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace Vitorm.MsTest.CommonTest
+{
+
+    [TestClass]
+    public class Property_String_Like_Test
+    {
+
+        [TestMethod]
+        public void Test_Like()
+        {
+            using var dbContext = DataSource.CreateDbContext();
+            var userQuery = dbContext.Query<User>();
+
+            // StartsWith
+            {
+                var query = userQuery.Where(u => u.name.StartsWith("u35"));
+
+                var userList = query.ToList();
+                Assert.AreEqual(1, userList.Count);
+                Assert.AreEqual(3, userList.First().id);
+                Assert.AreEqual("u356", userList.First().name);
+            }
+            // EndsWith
+            {
+                var query = userQuery.Where(u => u.name.EndsWith("356"));
+
+                var userList = query.ToList();
+                Assert.AreEqual(1, userList.Count);
+                Assert.AreEqual(3, userList.First().id);
+                Assert.AreEqual("u356", userList.First().name);
+            }
+            // Contains
+            {
+                var query = userQuery.Where(u => u.name.Contains("35"));
+
+                var userList = query.ToList();
+                Assert.AreEqual(1, userList.Count);
+                Assert.AreEqual(3, userList.First().id);
+                Assert.AreEqual("u356", userList.First().name);
+            }
+        }
+
+
+
+    }
+}

+ 94 - 0
test/Vitorm.Excel.MsTest/CommonTest/Property_String_Test.cs

@@ -0,0 +1,94 @@
+using System.Data;
+
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace Vitorm.MsTest.CommonTest
+{
+
+    [TestClass]
+    public class Property_String_Test
+    {
+
+        // Enumerable.Contains
+        // Queryable.Contains
+        [TestMethod]
+        public void Test_In()
+        {
+            using var dbContext = DataSource.CreateDbContext();
+            var userQuery = dbContext.Query<User>();
+
+            // Array.Contains
+            {
+                var userList = userQuery.Where(u => new[] { "u356", "u500" }.Contains(u.name)).ToList();
+                Assert.AreEqual(2, userList.Count);
+                Assert.AreEqual(0, userList.Select(m => m.name).Except(new[] { "u356", "u500" }).Count());
+            }
+
+            // List.Contains
+            {
+                var ids = new[] { "u356", "u500" }.ToList();
+                var userList = userQuery.Where(u => ids.Contains(u.name)).ToList();
+                Assert.AreEqual(2, userList.Count);
+                Assert.AreEqual(0, userList.Select(m => m.name).Except(new[] { "u356", "u500" }).Count());
+            }
+
+
+            // Enumerable.Contains
+            {
+                var ids = new[] { "u356", "u500" }.AsEnumerable();
+                var userList = userQuery.Where(u => ids.Contains(u.name)).ToList();
+                Assert.AreEqual(2, userList.Count);
+                Assert.AreEqual(0, userList.Select(m => m.name).Except(new[] { "u356", "u500" }).Count());
+            }
+
+            // Queryable.Contains
+            {
+                var ids = new[] { "u356", "u500" }.AsQueryable();
+                var userList = userQuery.Where(u => ids.Contains(u.name)).ToList();
+                Assert.AreEqual(2, userList.Count);
+                Assert.AreEqual(0, userList.Select(m => m.name).Except(new[] { "u356", "u500" }).Count());
+            }
+
+
+            // not Contains
+            {
+                var userList = userQuery.Where(u => !new[] { "u356", "u500" }.Contains(u.name)).ToList();
+                Assert.AreEqual(4, userList.Count);
+                Assert.AreEqual(0, userList.Select(m => m.name).Except(new[] { "u146", "u246", "u400", "u600" }).Count());
+            }
+        }
+
+
+        [TestMethod]
+        public void Test_Equal()
+        {
+            using var dbContext = DataSource.CreateDbContext();
+            var userQuery = dbContext.Query<User>();
+
+            // #1 ==
+            {
+                var userList = userQuery.Where(u => u.name == "u356" || "u500" == u.name).ToList();
+                Assert.AreEqual(2, userList.Count);
+                Assert.AreEqual(0, userList.Select(m => m.name).Except(new[] { "u356", "u500" }).Count());
+            }
+
+            // #2 !=
+            {
+                var userList = userQuery.Where(u => u.name != "u146").ToList();
+                Assert.AreEqual(5, userList.Count);
+                Assert.AreEqual(0, userList.Select(m => m.name).Except(new[] { "u246", "u356", "u400", "u500", "u600" }).Count());
+            }
+            {
+                var userList = userQuery.Where(u => "u146" != u.name).ToList();
+                Assert.AreEqual(5, userList.Count);
+                Assert.AreEqual(0, userList.Select(m => m.name).Except(new[] { "u246", "u356", "u400", "u500", "u600" }).Count());
+            }
+        }
+
+
+
+
+
+
+    }
+}

+ 57 - 0
test/Vitorm.Excel.MsTest/CommonTest/Property_Test.cs

@@ -0,0 +1,57 @@
+using System.ComponentModel.DataAnnotations;
+
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace Vitorm.MsTest.CommonTest
+{
+
+    [TestClass]
+    public class Property_Test
+    {
+        [TestMethod]
+        public void Test_RequiredAndMaxLength()
+        {
+            using var dbContext = DataSource.CreateDbContext();
+            var dbSet = dbContext.DbSet<UserInfo>();
+
+            dbSet.TryDropTable();
+            dbSet.TryCreateTable();
+
+            {
+                dbSet.Add(new UserInfo { id = 1, name = "user1" });
+
+                try
+                {
+                    dbSet.Add(new UserInfo { id = 2 });
+                    Assert.Fail("name should be required");
+                }
+                catch (Exception ex)
+                { }
+
+                try
+                {
+                    dbSet.Add(new UserInfo { id = 3, name = "01234567890123456789" });
+                    Assert.Fail("max length of name should be 10");
+                }
+                catch (Exception ex)
+                { }
+            }
+        }
+
+
+
+        class UserInfo
+        {
+            [Key]
+            public int id { get; set; }
+
+            [Required]
+            [MaxLength(10)]
+            public string name { get; set; }
+        }
+
+
+
+
+    }
+}

+ 235 - 0
test/Vitorm.Excel.MsTest/CommonTest/Query_Count_Test.cs

@@ -0,0 +1,235 @@
+using System.Data;
+
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace Vitorm.MsTest.CommonTest
+{
+
+    [TestClass]
+    public class Query_Count_Test
+    {
+
+        [TestMethod]
+        public void Test_Count()
+        {
+            using var dbContext = DataSource.CreateDbContext();
+            var userQuery = dbContext.Query<User>();
+
+            TestAllCase(userQuery);
+        }
+
+
+
+        [TestMethod]
+        public void Test_SkipAndTake_Count()
+        {
+            using var dbContext = DataSource.CreateDbContext();
+            var userQuery = dbContext.Query<User>();
+
+            TestAllCase(userQuery, new Config { skip = 0 });
+            TestAllCase(userQuery, new Config { skip = 1 });
+            TestAllCase(userQuery, new Config { skip = 10 });
+
+            TestAllCase(userQuery, new Config { take = 0 });
+            TestAllCase(userQuery, new Config { take = 2 });
+            TestAllCase(userQuery, new Config { take = 20 });
+
+
+            TestAllCase(userQuery, new Config { skip = 0, take = 0 });
+            TestAllCase(userQuery, new Config { skip = 0, take = 2 });
+            TestAllCase(userQuery, new Config { skip = 0, take = 20 });
+
+            TestAllCase(userQuery, new Config { skip = 1, take = 0 });
+            TestAllCase(userQuery, new Config { skip = 1, take = 2 });
+            TestAllCase(userQuery, new Config { skip = 1, take = 20 });
+
+            TestAllCase(userQuery, new Config { skip = 10, take = 0 });
+            TestAllCase(userQuery, new Config { skip = 10, take = 2 });
+            TestAllCase(userQuery, new Config { skip = 10, take = 20 });
+        }
+
+
+        [TestMethod]
+        public void Test_Distinct_Count()
+        {
+            using var dbContext = DataSource.CreateDbContext();
+            var userQuery = dbContext.Query<User>();
+
+            TestAllCase(userQuery, new Config { distinct = true });
+        }
+
+
+
+
+        [TestMethod]
+        public void Test_Distinct_SkipAndTake_Count()
+        {
+            using var dbContext = DataSource.CreateDbContext();
+            var userQuery = dbContext.Query<User>();
+
+            TestAllCase(userQuery, new Config { distinct = true, skip = 1, take = 100 });
+        }
+
+
+
+
+
+        void TestAllCase(IQueryable<User> users, Config config = null)
+        {
+
+            #region no orderBy
+            {
+                // single table
+                Test(users.Select(user => user.fatherId), config);
+                Test(users.Select(user => new { user.fatherId }), config);
+
+                Test(users.Select(user => user), config);
+                Test(users.Select(user => new { user }), config);
+                Test(users.Select(user => new { user, user.fatherId }), config);
+                Test(users.Select(user => new { user.id, user.fatherId }), config);
+
+
+
+                // joinedTable
+                Test(users.SelectMany(user => users.Where(father => father.id == user.fatherId), (user, father) => user.fatherId), config);
+                Test(users.SelectMany(user => users.Where(father => father.id == user.fatherId), (user, father) => new { user.fatherId }), config);
+
+                Test(users.SelectMany(user => users.Where(father => father.id == user.fatherId), (user, father) => user), config);
+                Test(users.SelectMany(user => users.Where(father => father.id == user.fatherId), (user, father) => new { user }), config);
+                Test(users.SelectMany(user => users.Where(father => father.id == user.fatherId), (user, father) => new { user, user.id }), config);
+
+                Test(users.SelectMany(user => users.Where(father => father.id == user.fatherId), (user, father) => new { user, father }), config);
+
+
+
+                // groupedTable Lambda Expression
+                {
+                    var query =
+                         users
+                        .GroupBy(user => new { user.fatherId, user.motherId })
+                        .Select(userGroup => new
+                        {
+                            userGroup.Key.fatherId,
+                            userGroup.Key.motherId,
+                            sumId = userGroup.Sum(m => m.id),
+                        });
+
+                    Test(query, config);
+                }
+                // groupedTable Linq Expression
+                {
+                    var query =
+                         from user in users
+                         group user by new { user.fatherId, user.motherId } into userGroup
+                         select new
+                         {
+                             userGroup.Key.fatherId,
+                             userGroup.Key.motherId,
+                             sumId = userGroup.Sum(m => m.id),
+                         };
+
+                    Test(query, config);
+                }
+            }
+            #endregion
+
+            #region with orderBy
+            {
+                // single table
+                Test(users.OrderBy(m => m.fatherId).Select(user => user.fatherId), config);
+                Test(users.OrderBy(m => m.fatherId).Select(user => new { user.fatherId }), config);
+
+                Test(users.OrderBy(m => m.fatherId).Select(user => user), config);
+                Test(users.OrderBy(m => m.fatherId).Select(user => new { user }), config);
+                Test(users.OrderBy(m => m.fatherId).Select(user => new { user, user.fatherId }), config);
+                Test(users.OrderBy(m => m.fatherId).Select(user => new { user.id, user.fatherId }), config);
+
+
+
+                // joinedTable
+                Test(users.SelectMany(user => users.Where(father => father.id == user.fatherId), (user, father) => user.fatherId).OrderBy(m => m), config);
+                Test(users.SelectMany(user => users.Where(father => father.id == user.fatherId), (user, father) => new { user.fatherId }).OrderBy(m => m.fatherId), config);
+
+                Test(users.SelectMany(user => users.Where(father => father.id == user.fatherId), (user, father) => user).OrderBy(m => m.fatherId), config);
+                Test(users.SelectMany(user => users.Where(father => father.id == user.fatherId), (user, father) => new { user }).OrderBy(m => m.user.fatherId), config);
+                Test(users.SelectMany(user => users.Where(father => father.id == user.fatherId), (user, father) => new { user, user.id }).OrderBy(m => m.user.fatherId), config);
+
+                Test(users.SelectMany(user => users.Where(father => father.id == user.fatherId), (user, father) => new { user, father }).OrderBy(m => m.user.fatherId), config);
+
+
+                // order by alias column
+                {
+                    Test(users.Select(user => user.fatherId).OrderBy(m => m), config);
+                    Test(users.Select(user => new { user.fatherId }).OrderBy(m => m.fatherId), config);
+
+                    Test(users.Select(user => user).OrderBy(m => m.fatherId), config);
+                    Test(users.Select(user => new { user }).OrderBy(m => m.user.fatherId), config);
+                    Test(users.Select(user => new { user, user.fatherId }).OrderBy(m => m.fatherId), config);
+                    Test(users.Select(user => new { user.id, user.fatherId }).OrderBy(m => m.fatherId), config);
+
+                    Test(users.Select(user => new { user.id, fid = user.fatherId ?? -1 }).OrderBy(m => m.id), config);
+                }
+
+
+
+                // groupedTable Lambda Expression
+                {
+                    var query =
+                         users
+                        .GroupBy(user => new { user.fatherId, user.motherId })
+                        .OrderBy(m => m.Key.fatherId)
+                        .Select(userGroup => new
+                        {
+                            userGroup.Key.fatherId,
+                            userGroup.Key.motherId,
+                            sumId = userGroup.Sum(m => m.id),
+                        });
+
+                    Test(query, config);
+                }
+                // groupedTable Linq Expression
+                {
+                    var query =
+                         from user in users
+                         group user by new { user.fatherId, user.motherId } into userGroup
+                         orderby userGroup.Key.fatherId
+                         select new
+                         {
+                             userGroup.Key.fatherId,
+                             userGroup.Key.motherId,
+                             sumId = userGroup.Sum(m => m.id),
+                         };
+
+                    Test(query, config);
+                }
+
+            }
+            #endregion
+
+
+        }
+
+
+
+        void Test<Entity>(IQueryable<Entity> query, Config config = null)
+        {
+            if (config?.distinct == true) query = query.Distinct();
+            if (config?.skip.HasValue == true) query = query.Skip(config.skip.Value);
+            if (config?.take.HasValue == true) query = query.Take(config.take.Value);
+
+            var rows = query.ToList();
+            int expectedCount = rows.Count;
+
+
+            var count = query.Count();
+            Assert.AreEqual(expectedCount, count);
+        }
+        class Config
+        {
+            public bool? distinct;
+
+            public int? skip;
+            public int? take;
+        }
+    }
+}

+ 68 - 0
test/Vitorm.Excel.MsTest/CommonTest/Query_Distinct_Test.cs

@@ -0,0 +1,68 @@
+using System.Data;
+
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace Vitorm.MsTest.CommonTest
+{
+
+    [TestClass]
+    public class Query_Distinct_Test
+    {
+
+        [TestMethod]
+        public void Test_Distinct()
+        {
+            using var dbContext = DataSource.CreateDbContext();
+            var userQuery = dbContext.Query<User>();
+
+            {
+                var query = userQuery.Select(u => u.fatherId).Distinct();
+
+                var fatherIds = query.ToList();
+
+                Assert.AreEqual(3, fatherIds.Count);
+                Assert.AreEqual(0, fatherIds.Except(new int?[] { 4, 5, null }).Count());
+            }
+            {
+                var query = userQuery.Select(u => new { u.fatherId }).Distinct();
+
+                var userList = query.ToList();
+                var fatherIds = userList.Select(u => u.fatherId).ToList();
+
+                Assert.AreEqual(3, fatherIds.Count);
+                Assert.AreEqual(0, fatherIds.Except(new int?[] { 4, 5, null }).Count());
+            }
+            {
+                var query = userQuery.Select(u => new { u.fatherId, u.motherId }).Distinct();
+
+                query = query.Skip(1).Take(100);
+
+                var userList = query.ToList();
+                var fatherIds = userList.Select(u => u.fatherId).ToList();
+                var motherIds = userList.Select(u => u.motherId).ToList();
+
+                Assert.AreEqual(2, userList.Count);
+                Assert.AreEqual(0, fatherIds.Except(new int?[] { 4, 5, null }).Count());
+                Assert.AreEqual(0, motherIds.Except(new int?[] { 6, null }).Count());
+            }
+            {
+                var query = userQuery.Select(u => new { user = u, u.fatherId, u.motherId }).Distinct();
+
+                var userList = query.ToList();
+
+                Assert.AreEqual(6, userList.Count);
+            }
+            {
+                var query = userQuery.Distinct();
+
+                var userList = query.ToList();
+
+                Assert.AreEqual(6, userList.Count);
+            }
+
+        }
+
+
+
+    }
+}

+ 42 - 0
test/Vitorm.Excel.MsTest/CommonTest/Query_FilterRule_Test.cs

@@ -0,0 +1,42 @@
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+using Vit.Core.Module.Serialization;
+using Vit.Linq;
+using Vit.Linq.ComponentModel;
+using Vit.Linq.FilterRules.ComponentModel;
+
+namespace Vitorm.MsTest.CommonTest
+{
+
+    [TestClass]
+    public class Query_FilterRule_Test
+    {
+
+        [TestMethod]
+        public void Test()
+        {
+            using var dbContext = DataSource.CreateDbContext();
+            var userQuery = dbContext.Query<User>();
+
+            {
+                var strFilter = "{'field':'id',  'operator': '>',  'value': 1 }".Replace("'", "\"");
+                var filter = Json.Deserialize<FilterRule>(strFilter);
+
+                var items = userQuery.Where(filter).OrderByDescending(u => u.id).ToList();
+                Assert.AreEqual(5, items.Count);
+                Assert.AreEqual(6, items[0].id);
+            }
+
+            {
+                var strPagedQuery = "{ 'filter':{'field':'id',  'operator': '>',  'value': 1 },  'orders':[{'field':'id','asc':false}],  'page':{'pageSize':2, 'pageIndex':1}  }".Replace("'", "\"");
+                var pagedQuery = Json.Deserialize<PagedQuery>(strPagedQuery);
+
+                var pageData = userQuery.ToPageData(pagedQuery);
+                Assert.AreEqual(5, pageData.totalCount);
+                Assert.AreEqual(6, pageData.items[0].id);
+            }
+
+        }
+
+    }
+}

+ 40 - 0
test/Vitorm.Excel.MsTest/CommonTest/Query_FilterRule_WithJoin_Test.cs

@@ -0,0 +1,40 @@
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+using Vit.Core.Module.Serialization;
+using Vit.Linq;
+using Vit.Linq.ComponentModel;
+
+namespace Vitorm.MsTest.CommonTest
+{
+
+    [TestClass]
+    public class Query_FilterRule_WithJoin_Test
+    {
+
+        [TestMethod]
+        public void Test()
+        {
+            using var dbContext = DataSource.CreateDbContext();
+            var userQuery = dbContext.Query<User>();
+
+            {
+                var query =
+                    from user in userQuery
+                    from father in userQuery.Where(father => user.fatherId == father.id)
+                    select new { user, father };
+
+
+                var strPagedQuery = "{ 'filter':{'field':'father.name',  'operator': '=',  'value': 'u400' },  'orders':[{'field':'user.id','asc':false}],  'page':{'pageSize':2, 'pageIndex':1}  }".Replace("'", "\"");
+                var pagedQuery = Json.Deserialize<PagedQuery>(strPagedQuery);
+
+
+                var pageData = query.ToPageData(pagedQuery);
+
+                Assert.AreEqual(2, pageData.totalCount);
+                Assert.AreEqual(2, pageData.items[0].user.id);
+            }
+
+        }
+
+    }
+}

+ 176 - 0
test/Vitorm.Excel.MsTest/CommonTest/Query_Group_Test.cs

@@ -0,0 +1,176 @@
+using System.Data;
+
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+
+namespace Vitorm.MsTest.CommonTest
+{
+    [TestClass]
+    public class Query_Group_Test
+    {
+
+        [TestMethod]
+        public void Test_Group_Demo()
+        {
+            using var dbContext = DataSource.CreateDbContext();
+            var userQuery = dbContext.Query<User>();
+
+            // Linq Expression
+            {
+                var query =
+                        from user in userQuery
+                        group user by new { user.fatherId, user.motherId } into userGroup
+                        select new { userGroup.Key.fatherId, userGroup.Key.motherId };
+
+                var rows = query.ToList();
+
+                Assert.AreEqual(3, rows.Count);
+                Assert.AreEqual(0, rows.Select(u => u.fatherId).Except(new int?[] { 4, 5, null }).Count());
+                Assert.AreEqual(0, rows.Select(u => u.motherId).Except(new int?[] { 6, null }).Count());
+            }
+
+            // Lambda Expression
+            {
+                var query =
+                        userQuery
+                        .GroupBy(user => new { user.fatherId, user.motherId })
+                        .Select(userGroup => new
+                        {
+                            userGroup.Key.fatherId,
+                            userGroup.Key.motherId
+                        })
+                        ;
+
+                var rows = query.ToList();
+
+                Assert.AreEqual(3, rows.Count);
+                Assert.AreEqual(0, rows.Select(u => u.fatherId).Except(new int?[] { 4, 5, null }).Count());
+                Assert.AreEqual(0, rows.Select(u => u.motherId).Except(new int?[] { 6, null }).Count());
+            }
+        }
+
+
+        [TestMethod]
+        public void Test_Group_Complex()
+        {
+            using var dbContext = DataSource.CreateDbContext();
+            var userQuery = dbContext.Query<User>();
+
+            // Linq Expression
+            {
+                var query =
+                        from user in userQuery.Where(u => u.id > 1)
+                        group user by new { user.fatherId, user.motherId } into userGroup
+                        where userGroup.Key.motherId != null && userGroup.Count() >= 1
+                        orderby userGroup.Key.fatherId descending, userGroup.Count() descending
+                        select new { userGroup.Key.fatherId, userGroup.Key.motherId, rowCount = userGroup.Count(), maxId = userGroup.Max(m => m.id) };
+
+                query = query.Skip(1).Take(1);
+
+                var rows = query.ToList();
+
+                Assert.AreEqual(1, rows.Count);
+                Assert.AreEqual(4, rows[0].fatherId);
+                Assert.AreEqual(6, rows[0].motherId);
+                Assert.AreEqual(1, rows[0].rowCount);
+                Assert.AreEqual(2, rows[0].maxId);
+            }
+
+            // Lambda Expression
+            {
+                var query =
+                        userQuery
+                        .Where(u => u.id > 1)
+                        .GroupBy(user => new { user.fatherId, user.motherId })
+                        .Where(userGroup => userGroup.Key.motherId != null)
+                        .OrderByDescending(userGroup => userGroup.Key.fatherId)
+                        .Select(userGroup => new
+                        {
+                            userGroup.Key.fatherId,
+                            userGroup.Key.motherId,
+                            rowCount = userGroup.Count(),
+                            maxId = userGroup.Max(m => m.id)
+                        })
+                        .Skip(1)
+                        .Take(1)
+                        ;
+
+                var rows = query.ToList();
+
+                Assert.AreEqual(1, rows.Count);
+                Assert.AreEqual(4, rows[0].fatherId);
+                Assert.AreEqual(6, rows[0].motherId);
+                Assert.AreEqual(1, rows[0].rowCount);
+                Assert.AreEqual(2, rows[0].maxId);
+            }
+        }
+
+
+
+
+        [TestMethod]
+        public void Test_Others()
+        {
+            using var dbContext = DataSource.CreateDbContext();
+            var userQuery = dbContext.Query<User>();
+
+            {
+                var query =
+                    userQuery
+                    .Where(user => user.id < 7)
+                    .GroupBy(user => new { user.fatherId, user.motherId })
+                    .OrderByDescending(group => group.Count())
+                    .Select(userGroup => new
+                    {
+                        userGroup.Key.fatherId,
+                        rowCount = userGroup.Count(),
+                        maxId = userGroup.Max(m => m.id),
+                        minId = userGroup.Min(m => m.id),
+                        sumId = userGroup.Sum(m => m.id),
+                        avgId = userGroup.Average(m => (double)m.id)
+                    })
+                    ;
+
+                var rows = query.ToList();
+
+                Assert.AreEqual(3, rows.Count);
+                var row = rows[1];
+                Assert.AreEqual(2, row.rowCount);
+                Assert.AreEqual(2, row.maxId);
+                Assert.AreEqual(1, row.minId);
+                Assert.AreEqual(3, row.sumId);
+                Assert.AreEqual(1.5, row.avgId);
+            }
+            {
+                var query =
+                    userQuery
+                    .GroupBy(user => new { user.fatherId, user.motherId })
+                    .Where(userGroup => userGroup.Key.motherId != null)
+                    .OrderByDescending(userGroup => userGroup.Key.fatherId)
+                    .Select(userGroup => new { userGroup.Key.fatherId, userGroup.Key.motherId })
+                    ;
+
+                var rows = query.ToList();
+
+                Assert.AreEqual(2, rows.Count);
+                Assert.AreEqual(5, rows[0].fatherId);
+            }
+            {
+                var query =
+                    userQuery
+                    .GroupBy(user => user.fatherId)
+                    .Where(userGroup => userGroup.Key != null)
+                    .OrderByDescending(userGroup => userGroup.Key)
+                    .Select(userGroup => new { fatherId = userGroup.Key, rowCount = userGroup.Count() })
+                    ;
+
+                var rows = query.ToList();
+
+                Assert.AreEqual(2, rows.Count);
+                Assert.AreEqual(5, rows[0].fatherId);
+            }
+        }
+
+
+    }
+}

+ 210 - 0
test/Vitorm.Excel.MsTest/CommonTest/Query_InnerJoin_ByJoin_Test.cs

@@ -0,0 +1,210 @@
+using System.Data;
+
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+
+namespace Vitorm.MsTest.CommonTest
+{
+    [TestClass]
+    public class Query_InnerJoin_ByJoin_Test
+    {
+
+        [TestMethod]
+        public void Test_InnerJoin_Demo()
+        {
+            using var dbContext = DataSource.CreateDbContext();
+            var userQuery = dbContext.Query<User>();
+
+            // Linq Expression
+            {
+                var query =
+                    from user in userQuery
+                    from father in userQuery.Where(father => user.fatherId == father.id)
+                    where user.id > 2
+                    select new { user, father };
+
+                var userList = query.ToList();
+
+                Assert.AreEqual(1, userList.Count);
+                Assert.AreEqual(3, userList.First().user.id);
+                Assert.AreEqual(5, userList.First().father.id);
+            }
+
+            // Lambda Expression
+            {
+                var query =
+                    userQuery.SelectMany(
+                        user => userQuery.Where(father => user.fatherId == father.id)
+                        , (user, father) => new { user, father }
+                    )
+                    .Where(row => row.user.id > 2)
+                    .Select(row => new { row.user, row.father });
+
+                var userList = query.ToList();
+
+                Assert.AreEqual(1, userList.Count);
+                Assert.AreEqual(3, userList.First().user.id);
+                Assert.AreEqual(5, userList.First().father.id);
+            }
+
+        }
+
+
+        [TestMethod]
+        public void Test_InnerJoin_Complex()
+        {
+            using var dbContext = DataSource.CreateDbContext();
+            var userQuery = dbContext.Query<User>();
+
+            // Linq Expression
+            {
+                var query =
+                    from user in userQuery
+                    join father in userQuery on user.fatherId equals father.id
+                    join mother in userQuery on user.motherId equals mother.id
+                    where user.id > 1
+                    orderby father.id descending
+                    select new
+                    {
+                        user,
+                        father,
+                        mother,
+                        testId = user.id + 100,
+                        hasFather = father != null ? true : false
+                    };
+                query = query.Skip(1).Take(1);
+
+                var userList = query.ToList();
+
+                Assert.AreEqual(1, userList.Count);
+
+                var first = userList.First();
+                Assert.AreEqual(2, first.user.id);
+                Assert.AreEqual(4, first.father.id);
+                Assert.AreEqual(6, first.mother.id);
+                Assert.AreEqual(102, first.testId);
+                Assert.AreEqual(true, first.hasFather);
+            }
+
+            // Lambda Expression
+            {
+                var query =
+                    userQuery.Join(
+                        userQuery
+                        , user => user.fatherId
+                        , father => father.id
+                        , (user, father) => new { user, father }
+                    ).Join(
+                        userQuery
+                        , row => row.user.motherId
+                        , mother => mother.id
+                        , (row, mother) => new { row, mother }
+                    )
+                    .Where(row2 => row2.row.user.id > 1)
+                    .OrderByDescending(row2 => row2.row.father.id)
+                    .Select(row2 =>
+                        new
+                        {
+                            row2.row.user,
+                            row2.row.father,
+                            row2.mother,
+                            testId = row2.row.user.id + 100,
+                            hasFather = row2.row.father != null ? true : false
+                        }
+                    );
+
+                query = query.Skip(1).Take(1);
+
+                var userList = query.ToList();
+
+                Assert.AreEqual(1, userList.Count);
+
+                var first = userList.First();
+                Assert.AreEqual(2, first.user.id);
+                Assert.AreEqual(4, first.father.id);
+                Assert.AreEqual(6, first.mother.id);
+                Assert.AreEqual(102, first.testId);
+                Assert.AreEqual(true, first.hasFather);
+            }
+        }
+
+
+
+        [TestMethod]
+        public void Test_InnerJoin_Others()
+        {
+            using var dbContext = DataSource.CreateDbContext();
+            var userQuery = dbContext.Query<User>();
+
+            // simple
+            {
+                var query =
+                    userQuery.Join(
+                        userQuery
+                        , user => user.fatherId
+                        , father => father.id
+                        , (user, father) => new { user, father }
+                    );
+
+                var userList = query.ToList();
+
+                Assert.AreEqual(3, userList.Count);
+                Assert.AreEqual(1, userList.First().user.id);
+            }
+
+            // where
+            {
+                var query =
+                    userQuery.Join(
+                        userQuery
+                        , user => user.fatherId
+                        , father => father.id
+                        , (user, father) => new { user, father }
+                    ).Where(row => row.user.id > 2);
+
+                var userList = query.ToList();
+
+                Assert.AreEqual(1, userList.Count);
+                Assert.AreEqual(3, userList.First().user.id);
+            }
+            // select
+            {
+                var query =
+                    userQuery.Join(
+                        userQuery
+                        , user => user.fatherId
+                        , father => father.id
+                        , (user, father) => new { user, father }
+                    ).Where(row => row.user.id > 2)
+                    .Select(row => new { userId = row.user.id, fatherId = row.father.id });
+
+                var userList = query.ToList();
+
+                Assert.AreEqual(1, userList.Count);
+                Assert.AreEqual(3, userList.First().userId);
+                Assert.AreEqual(5, userList.First().fatherId);
+            }
+            // full feature
+            {
+                var query =
+                         from user in userQuery
+                         join father in userQuery on user.fatherId equals father.id
+                         where user.id > 1
+                         orderby user.id descending
+                         select new { user, father };
+
+                query = query.Skip(1).Take(1);
+
+                var userList = query.ToList();
+
+                Assert.AreEqual(1, userList.Count);
+                Assert.AreEqual(2, userList.First().user.id);
+            }
+
+        }
+
+
+
+
+    }
+}

+ 187 - 0
test/Vitorm.Excel.MsTest/CommonTest/Query_InnerJoin_BySelectMany_Test.cs

@@ -0,0 +1,187 @@
+using System.Data;
+
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+
+namespace Vitorm.MsTest.CommonTest
+{
+    [TestClass]
+    public class Query_InnerJoin_BySelectMany_Test
+    {
+
+        [TestMethod]
+        public void Test_InnerJoin_Demo()
+        {
+            using var dbContext = DataSource.CreateDbContext();
+            var userQuery = dbContext.Query<User>();
+
+            // Linq Expression
+            {
+                var query =
+                    from user in userQuery
+                    from father in userQuery.Where(father => user.fatherId == father.id)
+                    where user.id > 2
+                    select new { user, father };
+
+                var userList = query.ToList();
+
+                Assert.AreEqual(1, userList.Count);
+                Assert.AreEqual(3, userList.First().user.id);
+                Assert.AreEqual(5, userList.First().father.id);
+            }
+
+            // Lambda Expression
+            {
+                var query =
+                    userQuery.SelectMany(
+                        user => userQuery.Where(father => user.fatherId == father.id)
+                        , (user, father) => new { user, father }
+                    )
+                    .Where(row => row.user.id > 2)
+                    .Select(row => new { row.user, row.father });
+
+                var userList = query.ToList();
+
+                Assert.AreEqual(1, userList.Count);
+                Assert.AreEqual(3, userList.First().user.id);
+                Assert.AreEqual(5, userList.First().father.id);
+            }
+        }
+
+
+        [TestMethod]
+        public void Test_InnerJoin_Complex()
+        {
+            using var dbContext = DataSource.CreateDbContext();
+            var userQuery = dbContext.Query<User>();
+
+            // Linq Expression
+            {
+                var query =
+                    from user in userQuery
+                    from father in userQuery.Where(father => user.fatherId == father.id)
+                    from mother in userQuery.Where(mother => user.motherId == mother.id)
+                    where user.id > 1
+                    orderby father.id descending
+                    select new
+                    {
+                        user,
+                        father,
+                        mother,
+                        testId = user.id + 100,
+                        hasFather = father != null ? true : false
+                    };
+                query = query.Skip(1).Take(1);
+
+                var userList = query.ToList();
+
+                Assert.AreEqual(1, userList.Count);
+
+                var first = userList.First();
+                Assert.AreEqual(2, first.user.id);
+                Assert.AreEqual(4, first.father.id);
+                Assert.AreEqual(6, first.mother.id);
+                Assert.AreEqual(102, first.testId);
+                Assert.AreEqual(true, first.hasFather);
+            }
+
+            // Lambda Expression
+            {
+                var query =
+                    userQuery.SelectMany(
+                        user => userQuery.Where(father => user.fatherId == father.id)
+                        , (user, father) => new { user, father }
+                    ).SelectMany(
+                        row => userQuery.Where(mother => row.user.motherId == mother.id)
+                        , (row, mother) => new { row, mother }
+                    )
+                    .Where(row2 => row2.row.user.id > 1)
+                    .OrderByDescending(row2 => row2.row.father.id)
+                    .Select(row2 =>
+                        new
+                        {
+                            row2.row.user,
+                            row2.row.father,
+                            row2.mother,
+                            testId = row2.row.user.id + 100,
+                            hasFather = row2.row.father != null ? true : false
+                        }
+                    );
+
+                query = query.Skip(1).Take(1);
+
+                var userList = query.ToList();
+
+                Assert.AreEqual(1, userList.Count);
+
+                var first = userList.First();
+                Assert.AreEqual(2, first.user.id);
+                Assert.AreEqual(4, first.father.id);
+                Assert.AreEqual(6, first.mother.id);
+                Assert.AreEqual(102, first.testId);
+                Assert.AreEqual(true, first.hasFather);
+            }
+        }
+
+
+
+        [TestMethod]
+        public void Test_MultipleSelect()
+        {
+            using var dbContext = DataSource.CreateDbContext();
+            var userQuery = dbContext.Query<User>();
+
+            {
+                var query = from user in userQuery
+                            from father in userQuery.Where(father => user.fatherId == father.id)
+                            from mother in userQuery.Where(mother => user.motherId == mother.id)
+                            orderby user.id
+                            select new
+                            {
+                                uniqueId = user.id + "_" + father.id + "_" + mother.id,
+                                uniqueId1 = user.id + "_" + user.fatherId + "_" + user.motherId,
+                                user,
+                                user2 = user,
+                                user3 = user,
+                                father,
+                                hasFather = user.fatherId != null ? true : false,
+                                fatherName = father.name,
+                                mother
+                            };
+
+                var userList = query.ToList();
+                Assert.AreEqual(3, userList.Count);
+                Assert.AreEqual(1, userList.First().user.id);
+                Assert.AreEqual(3, userList.Last().user.id);
+                Assert.AreEqual(5, userList.Last().father?.id);
+            }
+
+
+            {
+                var query = from user in userQuery
+                            from father in userQuery.Where(father => user.fatherId == father.id)
+                            from mother in userQuery.Where(mother => user.motherId == mother.id)
+                            where user.id > 1
+                            orderby user.id
+                            select new
+                            {
+                                user,
+                                father,
+                                userId = user.id + 100,
+                                hasFather = user.fatherId != null ? true : false,
+                                hasFather2 = father != null,
+                                fatherName = father.name,
+                                motherName = mother.name,
+                            };
+
+                var userList = query.ToList();
+
+                Assert.AreEqual(2, userList.Count);
+                Assert.AreEqual(4, userList.First().father?.id);
+                Assert.AreEqual(5, userList.Last().father?.id);
+            }
+        }
+
+
+    }
+}

+ 105 - 0
test/Vitorm.Excel.MsTest/CommonTest/Query_LeftJoin_ByGroupJoin_Test.cs

@@ -0,0 +1,105 @@
+using System.Data;
+
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace Vitorm.MsTest.CommonTest
+{
+    [TestClass]
+    public class Query_LeftJoin_ByGroupJoin_Test
+    {
+        [TestMethod]
+        public void Test_LeftJoin_Demo()
+        {
+            using var dbContext = DataSource.CreateDbContext();
+            var userQuery = dbContext.Query<User>();
+
+            // Linq Expression
+            {
+                var query =
+                    from user in userQuery
+                    join father in userQuery on user.fatherId equals father.id into fathers
+                    from father in fathers.DefaultIfEmpty()
+                    where user.id > 2
+                    orderby user.id
+                    select new { user, father };
+
+                var userList = query.ToList();
+
+                Assert.AreEqual(4, userList.Count);
+                Assert.AreEqual(3, userList[0].user.id);
+                Assert.AreEqual(5, userList[0].father?.id);
+                Assert.AreEqual(4, userList[1].user.id);
+                Assert.AreEqual(null, userList[1].father?.name);
+            }
+
+            // Lambda Expression
+            {
+                var query =
+                    userQuery.GroupJoin(
+                        userQuery
+                        , user => user.fatherId
+                        , father => father.id
+                        , (user, fathers) => new { user, fathers }
+                    )
+                    .SelectMany(
+                        row => row.fathers.DefaultIfEmpty()
+                        , (row, father) => new { row, father }
+                    )
+                    .Where(row2 => row2.row.user.id > 2)
+                    .OrderBy(row2 => row2.row.user.id)
+                    .Select(row2 => new { row2.row.user, row2.father });
+
+                var userList = query.ToList();
+
+                Assert.AreEqual(4, userList.Count);
+                Assert.AreEqual(3, userList[0].user.id);
+                Assert.AreEqual(5, userList[0].father?.id);
+                Assert.AreEqual(4, userList[1].user.id);
+                Assert.AreEqual(null, userList[1].father?.name);
+            }
+        }
+
+
+        [TestMethod]
+        public void Test_LeftJoin_Complex()
+        {
+            using var dbContext = DataSource.CreateDbContext();
+            var userQuery = dbContext.Query<User>();
+
+            // Linq Expression
+            {
+                var query =
+                    from user in userQuery
+                    join father in userQuery on user.fatherId equals father.id into fathers
+                    from father in fathers.DefaultIfEmpty()
+                    join mother in userQuery on user.motherId equals mother.id into mothers
+                    from mother in mothers.DefaultIfEmpty()
+                    where user.id > 2
+                    orderby user.id
+                    select new
+                    {
+                        user,
+                        father,
+                        mother,
+                        testId = user.id + 100,
+                        hasFather = father.name != null ? true : false
+                    };
+
+                query = query.Skip(1).Take(2);
+
+                var userList = query.ToList();
+
+                Assert.AreEqual(2, userList.Count);
+
+                var first = userList.First();
+                Assert.AreEqual(4, first.user.id);
+                Assert.AreEqual(null, first.father?.name);
+                Assert.AreEqual(null, first.mother?.name);
+                Assert.AreEqual(104, first.testId);
+                Assert.AreEqual(false, first.hasFather);
+            }
+        }
+
+
+    }
+}

+ 186 - 0
test/Vitorm.Excel.MsTest/CommonTest/Query_LeftJoin_BySelectMany_Test.cs

@@ -0,0 +1,186 @@
+using System.Data;
+
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace Vitorm.MsTest.CommonTest
+{
+    [TestClass]
+    public class Query_LeftJoin_BySelectMany_Test
+    {
+
+
+
+
+        [TestMethod]
+        public void Test_LeftJoin_Demo()
+        {
+            using var dbContext = DataSource.CreateDbContext();
+            var userQuery = dbContext.Query<User>();
+
+
+            // Lambda Expression
+            {
+                var query =
+                        userQuery.SelectMany(
+                            user => userQuery.Where(father => user.fatherId == father.id).DefaultIfEmpty()
+                            , (user, father) => new { user, father }
+                        )
+                        .Where(row => row.user.id > 2)
+                        .OrderBy(row => row.user.id)
+                        .Select(row => new { row.user, row.father });
+
+                var userList = query.ToList();
+
+                Assert.AreEqual(4, userList.Count);
+                Assert.AreEqual(3, userList[0].user.id);
+                Assert.AreEqual(5, userList[0].father?.id);
+                Assert.AreEqual(4, userList[1].user.id);
+                Assert.AreEqual(null, userList[1].father?.name);
+            }
+        }
+
+
+
+        [TestMethod]
+        public void Test_LeftJoin_Complex()
+        {
+            using var dbContext = DataSource.CreateDbContext();
+            var userQuery = dbContext.Query<User>();
+
+            // Linq Expression
+            {
+                var query =
+                    from user in userQuery
+                    from father in userQuery.Where(father => user.fatherId == father.id).DefaultIfEmpty()
+                    from mother in userQuery.Where(mother => user.motherId == mother.id).DefaultIfEmpty()
+                    from userClass in dbContext.Query<UserClass>().Where(userClass => user.classId == userClass.id).DefaultIfEmpty()
+                    where user.id > 1 && userClass.id == 1
+                    orderby user.id
+                    select new
+                    {
+                        user,
+                        father,
+                        mother,
+                        userClass,
+                        userClass.name,
+                        testId = user.id + 100,
+                        hasFather = father.name != null ? true : false
+                    };
+
+                query = query.Skip(1).Take(2);
+
+                var userList = query.ToList();
+
+                Assert.AreEqual(2, userList.Count);
+
+                var first = userList.First();
+                Assert.AreEqual(4, first.user.id);
+                Assert.AreEqual(null, first.father?.name);
+                Assert.AreEqual(null, first.mother?.name);
+                Assert.AreEqual(104, first.testId);
+                Assert.AreEqual(false, first.hasFather);
+            }
+        }
+
+
+        [TestMethod]
+        public void Test_MultipleSelect()
+        {
+            using var dbContext = DataSource.CreateDbContext();
+            var userQuery = dbContext.Query<User>();
+
+
+            {
+                var query = from user in userQuery
+                            from father in userQuery.Where(father => user.fatherId == father.id).DefaultIfEmpty()
+                            where user.id > 2 && father.name != null
+                            orderby user.id
+                            select new
+                            {
+                                user,
+                                father
+                            };
+
+                var userList = query.ToList();
+
+                Assert.AreEqual(1, userList.Count);
+                Assert.AreEqual(3, userList.First().user.id);
+            }
+
+            {
+                var query = from user in userQuery
+                            from father in userQuery.Where(father => user.fatherId == father.id)
+                            from mother in userQuery.Where(mother => user.motherId == mother.id)
+                            orderby user.id
+                            select new
+                            {
+                                uniqueId = user.id + "_" + father.id + "_" + mother.id,
+                                uniqueId1 = user.id + "_" + user.fatherId + "_" + user.motherId,
+                                user,
+                                user2 = user,
+                                user3 = user,
+                                father,
+                                hasFather = user.fatherId != null ? true : false,
+                                fatherName = father.name,
+                                mother
+                            };
+
+                var userList = query.ToList();
+                Assert.AreEqual(3, userList.Count);
+                Assert.AreEqual(1, userList.First().user.id);
+                Assert.AreEqual(3, userList.Last().user.id);
+                Assert.AreEqual(5, userList.Last().father?.id);
+            }
+
+            {
+                var query = from user in userQuery
+                            from father in userQuery.Where(father => user.fatherId == father.id).DefaultIfEmpty()
+                            from mother in userQuery.Where(mother => user.motherId == mother.id).DefaultIfEmpty()
+                            orderby user.id
+                            select new
+                            {
+                                user,
+                                father,
+                                userId = user.id + 100,
+                                hasFather = user.fatherId != null ? true : false,
+                                hasFather2 = father != null,
+                                fatherName = father.name,
+                                motherName = mother.name,
+                            };
+
+                var userList = query.ToList();
+
+                Assert.AreEqual(6, userList.Count);
+                Assert.AreEqual(1, userList.First().user.id);
+                Assert.AreEqual(101, userList.First().userId);
+                Assert.AreEqual(6, userList.Last().user.id);
+                Assert.AreEqual(5, userList[2].father.id);
+            }
+
+
+        }
+
+
+
+        [TestMethod]
+        public void Test_Count()
+        {
+            using var dbContext = DataSource.CreateDbContext();
+            var userQuery = dbContext.Query<User>();
+
+            {
+                var count = (from user in userQuery
+                             from father in userQuery.Where(father => user.fatherId == father.id).DefaultIfEmpty()
+                             where user.id > 2 && father.name == null
+                             select new
+                             {
+                                 father
+                             }).Count();
+
+                Assert.AreEqual(3, count);
+            }
+        }
+
+
+    }
+}

+ 388 - 0
test/Vitorm.Excel.MsTest/CommonTest/Query_Method_Test.cs

@@ -0,0 +1,388 @@
+using System.Data;
+
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+using Vit.Linq;
+
+namespace Vitorm.MsTest.CommonTest
+{
+
+    [TestClass]
+    public class Query_Method_Test
+    {
+
+        [TestMethod]
+        public void Test_PlainQuery()
+        {
+            using var dbContext = DataSource.CreateDbContext();
+            var userQuery = dbContext.Query<User>();
+
+            {
+                var userList = userQuery.OrderBy(m => m.id).ToList();
+                Assert.AreEqual(6, userList.Count);
+                Assert.AreEqual(1, userList.First().id);
+                Assert.AreEqual(6, userList.Last().id);
+            }
+
+
+            {
+                var userList = userQuery.OrderBy(m => m.id).Select(u => u.id).ToList();
+                Assert.AreEqual(6, userList.Count);
+                Assert.AreEqual(1, userList.First());
+                Assert.AreEqual(6, userList.Last());
+            }
+        }
+
+
+        [TestMethod]
+        public void Test_AllFeatures()
+        {
+            using var dbContext = DataSource.CreateDbContext();
+            var userQuery = dbContext.Query<User>();
+
+            #region users.Where().OrderBy().Skip().Take().ToList
+            /*
+            users.Where(row => row.user.id > 2)
+            .OrderBy(user=>user.id)
+            .Select(row => new {row.user })
+            .Skip(1).Take(2);
+             */
+            {
+                var query = (from user in userQuery
+                             where user.id > 2
+                             orderby user.id descending
+                             select new
+                             {
+                                 user
+                             })
+                            .Skip(1).Take(2);
+
+                var list = query.ToList();
+
+                Assert.AreEqual(2, list.Count);
+                Assert.AreEqual(5, list[0].user.id);
+                Assert.AreEqual(4, list[1].user.id);
+            }
+            #endregion
+        }
+
+
+
+        [TestMethod]
+        public void Test_Get()
+        {
+            {
+                using var dbContext = DataSource.CreateDbContext();
+                var user = dbContext.Get<User>(3);
+                Assert.AreEqual(3, user?.id);
+            }
+            {
+                using var dbContext = DataSource.CreateDbContext();
+                var user = dbContext.DbSet<User>().Get(5);
+                Assert.AreEqual(5, user?.id);
+            }
+        }
+
+
+
+        [TestMethod]
+        public void Test_Select()
+        {
+            using var dbContext = DataSource.CreateDbContext();
+            var userQuery = dbContext.Query<User>();
+
+            {
+                var userList = userQuery.Select(u => u).Where(user => user.id == 3).Select(u => u).ToList();
+                Assert.AreEqual(1, userList.Count);
+                Assert.AreEqual(3, userList.First().id);
+            }
+
+            {
+                var userList = userQuery.Where(user => user.id == 3).Select(u => (float)u.id).ToList();
+                Assert.AreEqual(1, userList.Count);
+                Assert.AreEqual(3.0, userList.First());
+            }
+
+            {
+                var query =
+                    from user in userQuery
+                    orderby user.id
+                    select new
+                    {
+                        uniqueId1 = user.id + "_" + user.fatherId + "_" + user.motherId,
+                        uniqueId2 = $"{user.id}_{user.fatherId}_{user.motherId}"
+                    };
+
+                var userList = query.ToList();
+                Assert.AreEqual(6, userList.Count);
+                Assert.AreEqual("1_4_6", userList[0].uniqueId1);
+                Assert.AreEqual("1_4_6", userList[0].uniqueId2);
+                Assert.AreEqual("4__", userList[3].uniqueId1);
+                Assert.AreEqual("4__", userList[3].uniqueId2);
+            }
+
+        }
+
+
+        [TestMethod]
+        public void Test_OrderBy()
+        {
+            using var dbContext = DataSource.CreateDbContext();
+            var userQuery = dbContext.Query<User>();
+
+            {
+                var query = userQuery.OrderByDescending(user => user.id);
+
+                var userList = query.ToList();
+                Assert.AreEqual(6, userList.Count);
+                Assert.AreEqual(6, userList[0].id);
+            }
+
+            {
+                var query = userQuery.OrderByDescending(user => user.id).Select(user => new { fid = user.fatherId, user.id });
+
+                var userList = query.ToList();
+                Assert.AreEqual(6, userList.Count);
+                Assert.AreEqual(6, userList[0].id);
+            }
+        }
+
+
+
+        [TestMethod]
+        public void Test_Count()
+        {
+            using var dbContext = DataSource.CreateDbContext();
+            var userQuery = dbContext.Query<User>();
+
+            // Count
+            {
+                var query = userQuery.Where(user => user.id > 2);
+
+                var count = query.Count();
+                Assert.AreEqual(4, count);
+            }
+            // Skip Take Count
+            {
+                var query = userQuery.Where(user => user.id > 2);
+
+                query = query.Skip(1).Take(10);
+
+                var count = query.Count();
+                Assert.AreEqual(3, count);
+            }
+        }
+        /*
+
+        [TestMethod]
+        public void Test_TotalCount()
+        {
+            using var dbContext = DataSource.CreateDbContext();
+            var userQuery = dbContext.Query<User>();
+
+            // TotalCountAsync
+            {
+                var query = userQuery.Where(user => user.id > 2);
+
+                var count = query.TotalCount();
+                Assert.AreEqual(4, count);
+            }
+
+            // Skip Take TotalCountAsync
+            {
+                var query = userQuery.Where(user => user.id > 2);
+
+                query = query.Skip(1).Take(10);
+
+                var count = query.TotalCount();
+                Assert.AreEqual(4, count);
+            }
+        }
+
+
+        [TestMethod]
+        public void Test_ToListAndTotal()
+        {
+            using var dbContext = DataSource.CreateDbContext();
+
+            // ToListAndTotalCount
+            {
+                var query = dbContext.Query<User>().Where(user => user.id > 2).Skip(1).Take(2);
+                var (list, totalCount) = query.ToListAndTotalCount();
+                Assert.AreEqual(2, list.Count);
+                Assert.AreEqual(4, totalCount);
+            }
+        }
+        //*/
+
+
+        [TestMethod]
+        public void Test_FirstOrDefault()
+        {
+            using var dbContext = DataSource.CreateDbContext();
+            var userQuery = dbContext.Query<User>();
+
+            {
+                var id = userQuery.OrderBy(m => m.id).Select(u => u.id).FirstOrDefault();
+                Assert.AreEqual(1, id);
+            }
+
+            {
+                var user = userQuery.OrderBy(m => m.id).FirstOrDefault();
+                Assert.AreEqual(1, user?.id);
+            }
+
+            {
+                var user = userQuery.FirstOrDefault(user => user.id == 3);
+                Assert.AreEqual(3, user?.id);
+            }
+
+            {
+                var user = userQuery.FirstOrDefault(user => user.id == 13);
+                Assert.AreEqual(null, user?.id);
+            }
+
+            {
+                var user = userQuery.OrderByDescending(m => m.id).FirstOrDefault();
+                Assert.AreEqual(6, user?.id);
+            }
+
+            {
+                var user = userQuery.OrderBy(m => m.id).Skip(1).Take(2).FirstOrDefault();
+                Assert.AreEqual(2, user?.id);
+            }
+        }
+
+
+        [TestMethod]
+        public void Test_First()
+        {
+            using var dbContext = DataSource.CreateDbContext();
+            var userQuery = dbContext.Query<User>();
+
+            {
+                var user = userQuery.OrderBy(m => m.id).First();
+                Assert.AreEqual(1, user?.id);
+            }
+
+            {
+                var user = userQuery.First(user => user.id == 3);
+                Assert.AreEqual(3, user?.id);
+            }
+
+            {
+                try
+                {
+                    var user = userQuery.First(user => user.id == 13);
+                    Assert.Fail("IQueryable.First should throw Exception");
+                }
+                catch (Exception ex) when (ex is not AssertFailedException)
+                {
+                }
+            }
+
+            {
+                var user = userQuery.OrderBy(m => m.id).Skip(1).Take(2).First();
+                Assert.AreEqual(2, user?.id);
+            }
+        }
+
+
+
+        [TestMethod]
+        public void Test_LastOrDefault()
+        {
+            using var dbContext = DataSource.CreateDbContext();
+            var userQuery = dbContext.Query<User>();
+
+            {
+                var id = userQuery.OrderBy(m => m.id).Select(u => u.id).FirstOrDefault();
+                Assert.AreEqual(1, id);
+            }
+            {
+                var user = userQuery.OrderBy(m => m.id).LastOrDefault();
+                Assert.AreEqual(6, user?.id);
+            }
+
+            {
+                var user = userQuery.LastOrDefault(user => user.id == 3);
+                Assert.AreEqual(3, user?.id);
+            }
+
+            {
+                var user = userQuery.LastOrDefault(user => user.id == 13);
+                Assert.AreEqual(null, user?.id);
+            }
+
+            {
+                var user = userQuery.OrderByDescending(m => m.id).LastOrDefault();
+                Assert.AreEqual(1, user?.id);
+            }
+
+            {
+                var user = userQuery.OrderBy(m => m.id).Skip(1).Take(2).LastOrDefault();
+                Assert.AreEqual(3, user?.id);
+            }
+        }
+
+
+        [TestMethod]
+        public void Test_Last()
+        {
+            using var dbContext = DataSource.CreateDbContext();
+            var userQuery = dbContext.Query<User>();
+
+            {
+                var user = userQuery.OrderBy(m => m.id).Last();
+                Assert.AreEqual(6, user?.id);
+            }
+
+            {
+                var user = userQuery.Last(user => user.id == 3);
+                Assert.AreEqual(3, user?.id);
+            }
+
+            {
+                try
+                {
+                    var user = userQuery.Last(user => user.id == 13);
+                    Assert.Fail("IQueryable.Last should throw Exception");
+                }
+                catch (Exception ex) when (ex is not AssertFailedException)
+                {
+                }
+            }
+
+            {
+                var user = userQuery.OrderBy(m => m.id).Skip(1).Take(2).Last();
+                Assert.AreEqual(3, user?.id);
+            }
+        }
+
+        // Enumerable.ToArray
+        [TestMethod]
+        public void Test_Enumerable_ToArray()
+        {
+            using var dbContext = DataSource.CreateDbContext();
+            var userQuery = dbContext.Query<User>();
+
+            {
+                var userList = userQuery.OrderBy(m => m.id).ToArray();
+                Assert.AreEqual(6, userList.Length);
+                Assert.AreEqual(1, userList.First().id);
+                Assert.AreEqual(6, userList.Last().id);
+            }
+
+            {
+                var userList = userQuery.OrderBy(m => m.id).Select(u => u.id).ToArray();
+                Assert.AreEqual(6, userList.Length);
+                Assert.AreEqual(1, userList.First());
+                Assert.AreEqual(6, userList.Last());
+            }
+        }
+
+
+
+
+    }
+}

+ 86 - 0
test/Vitorm.Excel.MsTest/CommonTest/Query_ScopeParam_LeftJoin_Test.cs

@@ -0,0 +1,86 @@
+using System.Data;
+
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace Vitorm.MsTest.CommonTest
+{
+    [TestClass]
+    public class Query_ScopeParam_LeftJoin_Test
+    {
+
+        [TestMethod]
+        public void Test_Join_Demo()
+        {
+            using var dbContext = DataSource.CreateDbContext();
+            var userQuery = dbContext.Query<User>();
+
+            // params form method arg
+            QueryByArg(userQuery, 2);
+
+            // params from scope
+            {
+                var id = 2;
+                var query =
+                        from user in userQuery
+                        from father in userQuery.Where(father => user.fatherId == father.id).DefaultIfEmpty()
+                        where user.id > id
+                        orderby user.id
+                        select new { user, father };
+
+                var userList = query.ToList();
+
+                Assert.AreEqual(4, userList.Count);
+                Assert.AreEqual(3, userList[0].user.id);
+                Assert.AreEqual(5, userList[0].father?.id);
+                Assert.AreEqual(4, userList[1].user.id);
+                Assert.AreEqual(null, userList[1].father?.name);
+            }
+
+            // params from scope
+            {
+                var userArg = new { id = 2 };
+                var query =
+                        from user in userQuery
+                        from father in userQuery.Where(father => user.fatherId == father.id).DefaultIfEmpty()
+                        where user.id > userArg.id
+                        orderby user.id
+                        select new { user, father };
+
+                var userList = query.ToList();
+
+                Assert.AreEqual(4, userList.Count);
+                Assert.AreEqual(3, userList[0].user.id);
+                Assert.AreEqual(5, userList[0].father?.id);
+                Assert.AreEqual(4, userList[1].user.id);
+                Assert.AreEqual(null, userList[1].father?.name);
+            }
+
+        }
+
+
+        void QueryByArg(IQueryable<User> userQuery, int id)
+        {
+            // Linq Expression
+            {
+                var query =
+                        from user in userQuery
+                        from father in userQuery.Where(father => user.fatherId == father.id).DefaultIfEmpty()
+                        where user.id > id
+                        orderby user.id
+                        select new { user, father };
+
+                var userList = query.ToList();
+
+                Assert.AreEqual(4, userList.Count);
+                Assert.AreEqual(3, userList[0].user.id);
+                Assert.AreEqual(5, userList[0].father?.id);
+                Assert.AreEqual(4, userList[1].user.id);
+                Assert.AreEqual(null, userList[1].father?.name);
+            }
+        }
+
+
+
+
+    }
+}

+ 77 - 0
test/Vitorm.Excel.MsTest/CommonTest/Query_ScopeParam_Test.cs

@@ -0,0 +1,77 @@
+using System.Data;
+
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace Vitorm.MsTest.CommonTest
+{
+    [TestClass]
+    public class Query_Query_ScopeParam_Test_Test
+    {
+
+        [TestMethod]
+        public void Test_Join_Demo()
+        {
+            using var dbContext = DataSource.CreateDbContext();
+            var userQuery = dbContext.Query<User>();
+
+            // params form method arg
+            QueryByArg(userQuery, 2);
+
+            // params from scope
+            {
+                var id = 2;
+                var query =
+                        from user in userQuery
+                        where user.id > id
+                        orderby user.id
+                        select new { user };
+
+                var userList = query.ToList();
+
+                Assert.AreEqual(4, userList.Count);
+                Assert.AreEqual(3, userList[0].user.id);
+                Assert.AreEqual(4, userList[1].user.id);
+            }
+
+            // params from scope
+            {
+                var userArg = new { id = 2 };
+                var query =
+                        from user in userQuery
+                        where user.id > userArg.id
+                        orderby user.id
+                        select new { user };
+
+                var userList = query.ToList();
+
+                Assert.AreEqual(4, userList.Count);
+                Assert.AreEqual(3, userList[0].user.id);
+                Assert.AreEqual(4, userList[1].user.id);
+            }
+
+        }
+
+
+        void QueryByArg(IQueryable<User> userQuery, int id)
+        {
+            // Linq Expression
+            {
+                var query =
+                from user in userQuery
+                where user.id > id
+                orderby user.id
+                select new { user };
+
+                var userList = query.ToList();
+
+                Assert.AreEqual(4, userList.Count);
+                Assert.AreEqual(3, userList[0].user.id);
+                Assert.AreEqual(4, userList[1].user.id);
+            }
+        }
+
+
+
+
+    }
+}

+ 168 - 0
test/Vitorm.Excel.MsTest/CommonTest/Query_Select_Test.cs

@@ -0,0 +1,168 @@
+using System.Data;
+
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace Vitorm.MsTest.CommonTest
+{
+    [TestClass]
+    public class Query_Select_Test
+    {
+
+        [TestMethod]
+        public void Test_Select()
+        {
+            using var dbContext = DataSource.CreateDbContext();
+            var userQuery = dbContext.Query<User>();
+
+            // 
+            {
+                var userList = userQuery.ToList();
+            }
+
+            // userQuery.Select(user => user)
+            {
+                var rows = userQuery.Select(user => user).ToList();
+                Assert.AreEqual(6, rows.Count);
+            }
+
+            // userQuery.Select(user => user.id);
+            {
+                var rows = userQuery.Select(user => user.id).ToList();
+            }
+            {
+                var rows = userQuery.Select(user => user.id + 100).ToList();
+            }
+            {
+                var rows = userQuery.Select(user => 1000 + user.id + 100).ToList();
+            }
+            {
+                var rows = userQuery.Select(user => (double)user.id).ToList();
+            }
+            {
+                var rows = userQuery.Select(user => (double)user.id + 0.1).ToList();
+            }
+            {
+                var rows = userQuery.Select(user => user.id + 0.1).ToList();
+            }
+
+            {
+                var query =
+                    from user in userQuery
+                    where user.id > 1
+                    orderby user.id
+                    select new
+                    {
+                        id = user.id,
+                        user = user,
+                        ext = new
+                        {
+                            father = user,
+                            fid = user.fatherId
+                        },
+                        trueValue = true,
+                    };
+
+                var rows = query.ToList();
+            }
+
+
+            // String.Format(format: "{0}_{1}_{2}", "0", "1", "2")
+            {
+                var query =
+                    from user in userQuery
+                    orderby user.id
+                    select new
+                    {
+                        uniqueId1 = user.id + "_" + user.fatherId + "_" + user.motherId,
+                        uniqueId2 = $"{user.id}_{user.fatherId}_{user.motherId}"
+                    };
+
+                var userList = query.ToList();
+                Assert.AreEqual(6, userList.Count);
+                Assert.AreEqual("1_4_6", userList[0].uniqueId1);
+                Assert.AreEqual("1_4_6", userList[0].uniqueId2);
+                Assert.AreEqual("4__", userList[3].uniqueId1);
+                Assert.AreEqual("4__", userList[3].uniqueId2);
+            }
+
+
+        }
+
+
+
+
+        [TestMethod]
+        public void Test_Select_ExistClass()
+        {
+            using var dbContext = DataSource.CreateDbContext();
+            var userQuery = dbContext.Query<User>();
+
+            {
+                var query =
+                    from user in userQuery
+                    where user.id > 1
+                    orderby user.id
+                    select new User2(user.id)
+                    ;
+
+                var userList = query.ToList();
+                Assert.AreEqual(5, userList.Count);
+
+            }
+
+            {
+                var query =
+                    from user in userQuery
+                    where user.id > 1
+                    orderby user.id
+                    select new User2(user.name)
+                    ;
+
+                var userList = query.ToList();
+                Assert.AreEqual(5, userList.Count);
+            }
+            {
+                var query =
+                    from user in userQuery
+                    where user.id > 1
+                    orderby user.id
+                    select new User2(user.name)
+                    {
+                        id = user.id + 100,
+                        fatherId = user.fatherId,
+                        father = user,
+                    }
+                    ;
+
+                var userList = query.ToList();
+                Assert.AreEqual(5, userList.Count);
+
+            }
+
+            {
+                var query =
+                    from user in userQuery
+                    where user.id > 1
+                    orderby user.id
+                    select new User2
+                    {
+                        id = user.id,
+                        fatherId = 12,
+                    };
+
+                var userList = query.ToList();
+                Assert.AreEqual(5, userList.Count);
+            }
+        }
+
+        class User2 : User
+        {
+            public User2() { }
+            public User2(int nid) { id = nid; }
+            public User2(string name) { this.name = name; }
+            public User father;
+        }
+
+
+    }
+}

+ 24 - 0
test/Vitorm.Excel.MsTest/CommonTest/Schema_Test.cs

@@ -0,0 +1,24 @@
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace Vitorm.MsTest.CommonTest
+{
+
+    [TestClass]
+    public partial class Schema_Test
+    {
+
+        [TestMethod]
+        public void Test_CreateAndTropTable()
+        {
+            using var dbContext = DataSource.CreateDbContextForWriting();
+
+            dbContext.TryCreateTable<User>();
+            dbContext.TryCreateTable<User>();
+            dbContext.TryDropTable<User>();
+            dbContext.TryDropTable<User>();
+        }
+
+
+
+    }
+}

+ 57 - 0
test/Vitorm.Excel.MsTest/CommonTest/Truncate_Test.cs

@@ -0,0 +1,57 @@
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace Vitorm.MsTest.CommonTest
+{
+    [TestClass]
+    public partial class Truncate_Test
+    {
+
+        [TestMethod]
+        public void Test_Truncate()
+        {
+            using var dbContext = DataSource.CreateDbContextForWriting();
+
+            // assert
+            {
+                var count = dbContext.Query<User>().Count();
+                Assert.AreEqual(6, count);
+            }
+
+            dbContext.Truncate<User>();
+
+            DataSource.WaitForUpdate();
+
+            // assert
+            {
+                var count = dbContext.Query<User>().Count();
+                Assert.AreEqual(0, count);
+            }
+        }
+
+
+        [TestMethod]
+        public async Task Test_TruncateAsync()
+        {
+            using var dbContext = DataSource.CreateDbContextForWriting();
+
+            // assert
+            {
+                var count = dbContext.Query<User>().Count();
+                Assert.AreEqual(6, count);
+            }
+
+            await dbContext.TruncateAsync<User>();
+
+            DataSource.WaitForUpdate();
+
+            // assert
+            {
+                var count = dbContext.Query<User>().Count();
+                Assert.AreEqual(0, count);
+            }
+        }
+
+
+
+    }
+}

+ 112 - 0
test/Vitorm.Excel.MsTest/DataSource.cs

@@ -0,0 +1,112 @@
+namespace Vitorm.MsTest
+{
+    using DbContext = Vitorm.Excel.DbContext;
+
+    [System.ComponentModel.DataAnnotations.Schema.Table("User")]
+    public class User
+    {
+        public User() { }
+
+        public User(int id) { this.id = id; }
+        public User(string name) { this.name = name; }
+
+
+        [System.ComponentModel.DataAnnotations.Key]
+        [System.ComponentModel.DataAnnotations.Schema.Column("userId")]
+        public int id { get; set; }
+        [System.ComponentModel.DataAnnotations.Schema.Column("userName")]
+        public string name { get; set; }
+        [System.ComponentModel.DataAnnotations.Schema.Column("userBirth")]
+        public DateTime? birth { get; set; }
+        [System.ComponentModel.DataAnnotations.Schema.Column("userFatherId")]
+        public int? fatherId { get; set; }
+        [System.ComponentModel.DataAnnotations.Schema.Column("userMotherId")]
+        public int? motherId { get; set; }
+        [System.ComponentModel.DataAnnotations.Schema.Column("userClassId")]
+        public int? classId { get; set; }
+
+        [System.ComponentModel.DataAnnotations.Schema.NotMapped]
+        public string test { get; set; }
+
+        public static User NewUser(int id, bool forAdd = false) => new User { id = id, name = "testUser" + id };
+
+        public static List<User> NewUsers(int startId, int count = 1, bool forAdd = false)
+        {
+            return Enumerable.Range(startId, count).Select(id => NewUser(id, forAdd)).ToList();
+        }
+    }
+
+    [System.ComponentModel.DataAnnotations.Schema.Table("UserClass")]
+    public class UserClass
+    {
+        [System.ComponentModel.DataAnnotations.Key]
+        [System.ComponentModel.DataAnnotations.Schema.Column("classId")]
+        public int id { get; set; }
+        [System.ComponentModel.DataAnnotations.Schema.Column("className")]
+        public string name { get; set; }
+
+        public static List<UserClass> NewClasses(int startId, int count = 1)
+        {
+            return Enumerable.Range(startId, count).Select(id => new UserClass { id = id, name = "class" + id }).ToList();
+        }
+    }
+
+
+    public class DataSource
+    {
+        public static void WaitForUpdate() { }
+
+        public static DbContext CreateDbContextForWriting(bool autoInit = true) => CreateDbContext(autoInit);
+        public static DbContext CreateDbContext(bool autoInit = true)
+        {
+            var guid = Guid.NewGuid().ToString();
+            var filePath = Path.Combine(System.AppDomain.CurrentDomain.BaseDirectory, $"{guid}.xlsx");
+
+
+            var dbContext = new Vitorm.Excel.DbContext(filePath);
+
+
+
+            if (autoInit)
+                InitDbContext(dbContext);
+
+            return dbContext;
+        }
+
+
+        public static void InitDbContext(DbContext dbContext)
+        {
+            #region #1 init User
+            {
+                dbContext.TryDropTable<User>();
+                dbContext.TryCreateTable<User>();
+
+                var users = new List<User> {
+                    new User { id=1, name="u146", fatherId=4, motherId=6 },
+                    new User { id=2, name="u246", fatherId=4, motherId=6 },
+                    new User { id=3, name="u356", fatherId=5, motherId=6 },
+                    new User { id=4, name="u400" },
+                    new User { id=5, name="u500" },
+                    new User { id=6, name="u600" },
+                };
+                users.ForEach(user =>
+                {
+                    user.birth = DateTime.Parse("2021-01-01 00:00:00").AddHours(user.id);
+                    user.classId = user.id % 2 + 1;
+                });
+
+                dbContext.AddRange(users);
+            }
+            #endregion
+
+            #region #2 init Class
+            {
+                dbContext.TryDropTable<UserClass>();
+                dbContext.TryCreateTable<UserClass>();
+                dbContext.AddRange(UserClass.NewClasses(1, 6));
+            }
+            #endregion
+        }
+
+    }
+}

+ 29 - 0
test/Vitorm.Excel.MsTest/Vitorm.Excel.MsTest.csproj

@@ -0,0 +1,29 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+    <PropertyGroup>
+        <test>MSTest</test>
+    </PropertyGroup>
+
+    <PropertyGroup>
+        <TargetFramework>net6.0</TargetFramework>
+        <ImplicitUsings>enable</ImplicitUsings>
+
+        <IsPackable>false</IsPackable>
+        <IsTestProject>true</IsTestProject>
+        <RootNamespace>Vitorm.MsTest</RootNamespace>
+    </PropertyGroup>
+
+    <ItemGroup>
+        <PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="8.0.0" />
+        <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.0" />
+        <PackageReference Include="MSTest.TestAdapter" Version="3.5.2" />
+        <PackageReference Include="MSTest.TestFramework" Version="3.5.2" />
+
+        <PackageReference Include="Vit.Core" Version="2.2.0" />
+    </ItemGroup>
+
+    <ItemGroup>
+      <ProjectReference Include="..\..\src\Vitorm.Excel\Vitorm.Excel.csproj" />
+    </ItemGroup>
+
+</Project>