diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..d1afcf5 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,30 @@ +{ + "permissions": { + "allow": [ + "Bash(dotnet restore:*)", + "Bash(dotnet build:*)", + "Bash(dotnet test:*)", + "Bash(dotnet:*)", + "Bash(git status:*)", + "Bash(git add:*)", + "Bash(git commit:*)", + "Bash(sed 's/^A\\\\t//')", + "Bash(grep \"\\\\.csproj$\")", + "Bash(sed 's/^D\\\\t//')", + "Bash(git stash:*)", + "Bash(git pull:*)", + "Bash(python3 -c \":*)", + "Bash(for pkg:*)", + "Bash(do)", + "Bash(echo \"=== $pkg ===\")", + "Read(//c/Users/najaf/.nuget/packages/$pkg/**)", + "Bash(done)", + "Bash(git push:*)", + "Bash(git checkout:*)", + "Bash(find /c/Work/Projects/Published/DataFuse -name *.md -type f)", + "Bash(ls:*)", + "Bash(find /c/Work/Projects/Published/DataFuse/src -name *.cs -type f)", + "Bash(xargs grep:*)" + ] + } +} diff --git a/.github/workflows/Build-Master.yml b/.github/workflows/Build-Master.yml index 4e1915a..847bb27 100644 --- a/.github/workflows/Build-Master.yml +++ b/.github/workflows/Build-Master.yml @@ -10,17 +10,22 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Setup .NET uses: actions/setup-dotnet@v3 with: - dotnet-version: 6.0.x + dotnet-version: | + 8.0.x + 9.0.x + 10.0.x - name: Restore dependencies run: dotnet restore - name: Build run: dotnet build --no-restore - - name: Test - run: dotnet test --no-build --verbosity normal + - name: Unit Tests + run: dotnet test --no-build --verbosity normal --filter "FullyQualifiedName~Unit" + - name: Integration Tests + run: dotnet test --no-build --verbosity normal --filter "FullyQualifiedName~Integration" run-Lint: runs-on: ubuntu-latest @@ -28,7 +33,7 @@ jobs: github-token: '${{ secrets.GITHUB_TOKEN }}' steps: - name: Step-01 Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 - name: Step-02 Lint Code Base diff --git a/.github/workflows/Master-Build.yml b/.github/workflows/Master-Build.yml deleted file mode 100644 index fb70f0d..0000000 --- a/.github/workflows/Master-Build.yml +++ /dev/null @@ -1,40 +0,0 @@ -# This workflow will build a .NET project -# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-net - -name: build-master - -on: - push: - branches: [ "master" ] -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - name: Setup .NET - uses: actions/setup-dotnet@v3 - with: - dotnet-version: 6.0.x - - name: Restore dependencies - run: dotnet restore - - name: Build - run: dotnet build --no-restore - - name: Test - run: dotnet test --no-build --verbosity normal - - run-Lint: - runs-on: ubuntu-latest - env: - github-token: '${{ secrets.GITHUB_TOKEN }}' - steps: - - name: Step-01 Checkout code - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - name: Step-02 Lint Code Base - uses: github/super-linter@v4 - env: - VALIDATE_ALL_CODEBASE: false - FILTER_REGEX_INCLUDE: .*src/.* - DEFAULT_BRANCH: master - GITHUB_TOKEN: '${{ env.github-token }}' diff --git a/.github/workflows/Master-CodeQL.yml b/.github/workflows/Master-CodeQL.yml index b765a11..fd68322 100644 --- a/.github/workflows/Master-CodeQL.yml +++ b/.github/workflows/Master-CodeQL.yml @@ -13,64 +13,86 @@ name: "Master-CodeQL" on: push: - pull_request: - types: [opened, reopened, edited, synchronize] - paths-ignore: - - "**/*.md" - - "**/*.gitignore" - - "**/*.gitattributes" + branches: [ "master" ] schedule: - - cron: '35 15 * * 2' + - cron: '42 7 * * 5' jobs: analyze: - name: Analyze - runs-on: ubuntu-latest + name: Analyze (${{ matrix.language }}) + # Runner size impacts CodeQL analysis time. To learn more, please see: + # - https://gh.io/recommended-hardware-resources-for-running-codeql + # - https://gh.io/supported-runners-and-hardware-resources + # - https://gh.io/using-larger-runners (GitHub.com only) + # Consider using larger runners or machines with greater resources for possible analysis time improvements. + runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} permissions: + # required for all workflows + security-events: write + + # required to fetch internal or private CodeQL packs + packages: read + + # only required for workflows in private repositories actions: read contents: read - security-events: write strategy: fail-fast: false matrix: - language: [ 'csharp' ] - # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] - # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support - + include: + - language: actions + build-mode: none + - language: csharp + build-mode: none + # CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' + # Use `c-cpp` to analyze code written in C, C++ or both + # Use 'java-kotlin' to analyze code written in Java, Kotlin or both + # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both + # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis, + # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning. + # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how + # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 + + # Add any setup steps before running the `github/codeql-action/init` action. + # This includes steps like installing compilers or runtimes (`actions/setup-node` + # or others). This is typically only required for manual builds. + # - name: Setup runtime (example) + # uses: actions/setup-example@v1 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} # If you wish to specify custom queries, you can do so here or in a config file. # By default, queries listed here will override any specified in a config file. # Prefix the list here with "+" to use these queries and those in the config file. - # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs # queries: security-extended,security-and-quality - - # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). - # If this step fails, then you should remove it and run the build manually (see below) - - name: Autobuild - uses: github/codeql-action/autobuild@v2 - + # If the analyze step fails for one of the languages you are analyzing with + # "We were unable to automatically build your code", modify the matrix above + # to set the build mode to "manual" for that language. Then modify this step + # to build your code. # â„šī¸ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun - - # If the Autobuild fails above, remove it and uncomment the following three lines. - # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. - - # - run: | - # echo "Run, Build Application using script" - # ./location_of_script_within_repo/buildscript.sh + - if: matrix.build-mode == 'manual' + shell: bash + run: | + echo 'If you are using a "manual" build mode for one or more of the' \ + 'languages you are analyzing, replace this with the commands to build' \ + 'your code, for example:' + echo ' make bootstrap' + echo ' make release' + exit 1 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 with: - category: "/language:${{matrix.language}}" \ No newline at end of file + category: "/language:${{matrix.language}}" diff --git a/.github/workflows/PullRequest-CI.yml b/.github/workflows/PullRequest-CI.yml index 6d47951..3dea6d5 100644 --- a/.github/workflows/PullRequest-CI.yml +++ b/.github/workflows/PullRequest-CI.yml @@ -30,7 +30,7 @@ jobs: beta_Version: ${{ steps.gitversion.outputs.nuGetVersion }} branchName: ${{ steps.gitversion.outputs.branchName }} env: - working-directory: /home/runner/work/Motley/Motley + working-directory: /home/runner/work/DataFuse.Net/DataFuse.Net steps: - name: Step-01 Install GitVersion @@ -39,7 +39,7 @@ jobs: versionSpec: 5.x - name: Step-02 Check out Code - uses: actions/checkout@v2 + uses: actions/checkout@v4 with: fetch-depth: 0 @@ -52,28 +52,35 @@ jobs: - name: Step-04 Install .NET uses: actions/setup-dotnet@v3 with: - dotnet-version: 6.0.x + dotnet-version: | + 8.0.x + 9.0.x + 10.0.x - name: Step-05 Restore dependencies run: dotnet restore working-directory: '${{ env.working-directory }}' - name: Step-06 Build Version (Beta) - if: ${{ !startsWith(github.head_ref, 'release/')}} + if: ('!startsWith(github.ref, ''refs/heads/release'')') run: dotnet build --configuration Release --no-restore -p:PackageVersion=${{ steps.gitversion.outputs.nuGetVersion }} working-directory: '${{ env.working-directory }}' - name: Step-06 Build Version (Release) - if: ${{ startsWith(github.head_ref, 'release/')}} + if: ('startsWith(github.ref, ''refs/heads/release'')') run: dotnet build --configuration Release --no-restore -p:PackageVersion=${{ steps.gitversion.outputs.MajorMinorPatch }} working-directory: '${{ env.working-directory }}' - - name: Step-07 Test Solution - run: dotnet test --configuration Release --no-build --no-restore --verbosity normal + - name: Step-07 Unit Tests + run: dotnet test --configuration Release --no-build --no-restore --verbosity normal --filter "FullyQualifiedName~Unit" + working-directory: '${{ env.working-directory }}' + + - name: Step-07b Integration Tests + run: dotnet test --configuration Release --no-build --no-restore --verbosity normal --filter "FullyQualifiedName~Integration" working-directory: '${{ env.working-directory }}' - name: Step-08 Upload Build Artifacts - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: build-artifact path: ${{env.working-directory}} @@ -87,10 +94,10 @@ jobs: env: github-token: '${{ secrets.GH_Packages }}' nuget-token: '${{ secrets.NUGET_API_KEY }}' - working-directory: /home/runner/work/Motley/Motley + working-directory: /home/runner/work/DataFuse.Net/DataFuse.Net steps: - name: Step-01 Retrieve Build Artifacts - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: build-artifact path: ${{env.working-directory}} @@ -104,14 +111,14 @@ jobs: Release: name: Release to Nuget needs: [Package] - if: ${{ startsWith(github.head_ref, 'release/')}} + if: ('startsWith(github.ref, ''refs/heads/release'')') runs-on: ubuntu-latest env: github-token: '${{ secrets.GH_Packages }}' nuget-token: '${{ secrets.NUGET_API_KEY }}' - working-directory: /home/runner/work/Motley/Motley + working-directory: /home/runner/work/DataFuse.Net/DataFuse.Net steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 # - name: Setup dotnet # uses: actions/setup-dotnet@v1 @@ -119,15 +126,15 @@ jobs: # dotnet-version: '6.0.x' # Publish - - name: publish Motley package + - name: publish DataFuse package id: publish_nuget uses: Rebel028/publish-nuget@v2.8.0 with: # Filepath of the project to be packaged, relative to root of repository - PROJECT_FILE_PATH: Motley/Motley.csproj - + PROJECT_FILE_PATH: DataFuse/DataFuse.csproj + # NuGet package id, used for version detection & defaults to project name - PACKAGE_NAME: Motley + PACKAGE_NAME: DataFuse # Filepath with version info, relative to root of repository & defaults to PROJECT_FILE_PATH # VERSION_FILE_PATH: Directory.Build.props diff --git a/.github/workflows/PullRequest-CodeQL.yml b/.github/workflows/PullRequest-CodeQL.yml index c1b075e..7dcf2f5 100644 --- a/.github/workflows/PullRequest-CodeQL.yml +++ b/.github/workflows/PullRequest-CodeQL.yml @@ -12,65 +12,91 @@ name: "PullRequest-CodeQL" on: - push: pull_request: types: [opened, reopened, edited, synchronize] paths-ignore: - "**/*.md" - "**/*.gitignore" - - "**/*.gitattributes" + - "**/*.gitattributes" schedule: - - cron: '35 15 * * 2' + - cron: '42 7 * * 5' jobs: analyze: - name: Analyze - runs-on: ubuntu-latest + name: Analyze (${{ matrix.language }}) + # Runner size impacts CodeQL analysis time. To learn more, please see: + # - https://gh.io/recommended-hardware-resources-for-running-codeql + # - https://gh.io/supported-runners-and-hardware-resources + # - https://gh.io/using-larger-runners (GitHub.com only) + # Consider using larger runners or machines with greater resources for possible analysis time improvements. + runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} permissions: + # required for all workflows + security-events: write + + # required to fetch internal or private CodeQL packs + packages: read + + # only required for workflows in private repositories actions: read contents: read - security-events: write strategy: fail-fast: false matrix: - language: [ 'csharp' ] - # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] - # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support - + include: + - language: actions + build-mode: none + - language: csharp + build-mode: none + # CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' + # Use `c-cpp` to analyze code written in C, C++ or both + # Use 'java-kotlin' to analyze code written in Java, Kotlin or both + # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both + # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis, + # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning. + # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how + # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 + + # Add any setup steps before running the `github/codeql-action/init` action. + # This includes steps like installing compilers or runtimes (`actions/setup-node` + # or others). This is typically only required for manual builds. + # - name: Setup runtime (example) + # uses: actions/setup-example@v1 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} # If you wish to specify custom queries, you can do so here or in a config file. # By default, queries listed here will override any specified in a config file. # Prefix the list here with "+" to use these queries and those in the config file. - # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs # queries: security-extended,security-and-quality - - # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). - # If this step fails, then you should remove it and run the build manually (see below) - - name: Autobuild - uses: github/codeql-action/autobuild@v2 - + # If the analyze step fails for one of the languages you are analyzing with + # "We were unable to automatically build your code", modify the matrix above + # to set the build mode to "manual" for that language. Then modify this step + # to build your code. # â„šī¸ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun - - # If the Autobuild fails above, remove it and uncomment the following three lines. - # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. - - # - run: | - # echo "Run, Build Application using script" - # ./location_of_script_within_repo/buildscript.sh + - if: matrix.build-mode == 'manual' + shell: bash + run: | + echo 'If you are using a "manual" build mode for one or more of the' \ + 'languages you are analyzing, replace this with the commands to build' \ + 'your code, for example:' + echo ' make bootstrap' + echo ' make release' + exit 1 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 with: - category: "/language:${{matrix.language}}" \ No newline at end of file + category: "/language:${{matrix.language}}" diff --git a/.github/workflows/pages-static.yml b/.github/workflows/pages-static.yml new file mode 100644 index 0000000..0ba8230 --- /dev/null +++ b/.github/workflows/pages-static.yml @@ -0,0 +1,43 @@ +# Simple workflow for deploying static content to GitHub Pages +name: Deploy static content to Pages + +on: + # Runs on pushes targeting the default branch + push: + branches: ["master"] + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages +permissions: + contents: read + pages: write + id-token: write + +# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. +# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. +concurrency: + group: "pages" + cancel-in-progress: false + +jobs: + # Single deploy job since we're just deploying + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Setup Pages + uses: actions/configure-pages@v5 + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + # Upload entire repository + path: '.' + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..e69de29 diff --git a/DataFuse.sln b/DataFuse.sln new file mode 100644 index 0000000..f63816f --- /dev/null +++ b/DataFuse.sln @@ -0,0 +1,212 @@ +īģŋ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.33516.290 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{F41DA3D8-A0E9-4A05-8A35-78313C0F5804}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{07BAE427-96CF-4F9B-80A9-48CFB0A89CF3}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "solution", "solution", "{AF995FEF-BB94-48D0-B02B-6671DA73056B}" + ProjectSection(SolutionItems) = preProject + .gitignore = .gitignore + index.md = index.md + LICENSE.md = LICENSE.md + README.md = README.md + EndProjectSection +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DataFuse.Integration.Tests", "tests\DataFuse.Integration.Tests\DataFuse.Integration.Tests.csproj", "{B09236CB-BBD2-4DCF-A698-74CCCAB29FFB}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DataFuse.Adapters.SQL.Tests", "tests\DataFuse.Adapters.SQL.Tests\DataFuse.Adapters.SQL.Tests.csproj", "{1E319404-8EF0-40A1-A9D7-A404A71A98C4}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "github", "github", "{39FD806A-D320-43C9-800F-E9D9BCC90623}" + ProjectSection(SolutionItems) = preProject + .github\workflows\Build-Master.yml = .github\workflows\Build-Master.yml + GitVersion.yml = GitVersion.yml + .github\workflows\Master-CodeQL.yml = .github\workflows\Master-CodeQL.yml + .github\workflows\PullRequest-CI.yml = .github\workflows\PullRequest-CI.yml + .github\workflows\PullRequest-CodeQL.yml = .github\workflows\PullRequest-CodeQL.yml + EndProjectSection +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DataFuse.Adapters.EntityFramework.Tests", "tests\DataFuse.Adapters.EntityFramework.Tests\DataFuse.Adapters.EntityFramework.Tests.csproj", "{FE039675-00BE-4DC5-945F-14E7625170D0}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DataFuse.Integration", "src\DataFuse.Integration\DataFuse.Integration.csproj", "{4F9B6FE3-CAC4-4594-84AD-98D1ECDE180C}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DataFuse.Adapters.Abstraction", "src\DataFuse.Adapters.Abstraction\DataFuse.Adapters.Abstraction.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DataFuse.Adapters.EntityFramework", "src\DataFuse.Adapters.EntityFramework\DataFuse.Adapters.EntityFramework.csproj", "{E8F8C13E-2E05-4092-84FD-B7B7B12DABBB}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DataFuse.Adapters.SQL", "src\DataFuse.Adapters.SQL\DataFuse.Adapters.SQL.csproj", "{52986844-698F-486B-AEC9-846AAF50CF46}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DataFuse.Adapters.WebAPI", "src\DataFuse.Adapters.WebAPI\DataFuse.Adapters.WebAPI.csproj", "{0C1A05D2-653D-4F88-B397-BA53E0BA7281}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DataFuse.Adapters.WebAPI.Tests", "tests\DataFuse.Adapters.WebAPI.Tests\DataFuse.Adapters.WebAPI.Tests.csproj", "{D0EE40C1-87EF-413D-A046-DC37D58BC344}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DataFuse.Adapters.MongoDB", "src\DataFuse.Adapters.MongoDB\DataFuse.Adapters.MongoDB.csproj", "{035680B0-E7C9-4C37-BFA8-4D91E7B67BAC}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DataFuse.Adapters.MongoDB.Tests", "tests\DataFuse.Adapters.MongoDB.Tests\DataFuse.Adapters.MongoDB.Tests.csproj", "{A2B3C4D5-E6F7-8901-BCDE-F23456789012}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {B09236CB-BBD2-4DCF-A698-74CCCAB29FFB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B09236CB-BBD2-4DCF-A698-74CCCAB29FFB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B09236CB-BBD2-4DCF-A698-74CCCAB29FFB}.Debug|x64.ActiveCfg = Debug|Any CPU + {B09236CB-BBD2-4DCF-A698-74CCCAB29FFB}.Debug|x64.Build.0 = Debug|Any CPU + {B09236CB-BBD2-4DCF-A698-74CCCAB29FFB}.Debug|x86.ActiveCfg = Debug|Any CPU + {B09236CB-BBD2-4DCF-A698-74CCCAB29FFB}.Debug|x86.Build.0 = Debug|Any CPU + {B09236CB-BBD2-4DCF-A698-74CCCAB29FFB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B09236CB-BBD2-4DCF-A698-74CCCAB29FFB}.Release|Any CPU.Build.0 = Release|Any CPU + {B09236CB-BBD2-4DCF-A698-74CCCAB29FFB}.Release|x64.ActiveCfg = Release|Any CPU + {B09236CB-BBD2-4DCF-A698-74CCCAB29FFB}.Release|x64.Build.0 = Release|Any CPU + {B09236CB-BBD2-4DCF-A698-74CCCAB29FFB}.Release|x86.ActiveCfg = Release|Any CPU + {B09236CB-BBD2-4DCF-A698-74CCCAB29FFB}.Release|x86.Build.0 = Release|Any CPU + {1E319404-8EF0-40A1-A9D7-A404A71A98C4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1E319404-8EF0-40A1-A9D7-A404A71A98C4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1E319404-8EF0-40A1-A9D7-A404A71A98C4}.Debug|x64.ActiveCfg = Debug|Any CPU + {1E319404-8EF0-40A1-A9D7-A404A71A98C4}.Debug|x64.Build.0 = Debug|Any CPU + {1E319404-8EF0-40A1-A9D7-A404A71A98C4}.Debug|x86.ActiveCfg = Debug|Any CPU + {1E319404-8EF0-40A1-A9D7-A404A71A98C4}.Debug|x86.Build.0 = Debug|Any CPU + {1E319404-8EF0-40A1-A9D7-A404A71A98C4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1E319404-8EF0-40A1-A9D7-A404A71A98C4}.Release|Any CPU.Build.0 = Release|Any CPU + {1E319404-8EF0-40A1-A9D7-A404A71A98C4}.Release|x64.ActiveCfg = Release|Any CPU + {1E319404-8EF0-40A1-A9D7-A404A71A98C4}.Release|x64.Build.0 = Release|Any CPU + {1E319404-8EF0-40A1-A9D7-A404A71A98C4}.Release|x86.ActiveCfg = Release|Any CPU + {1E319404-8EF0-40A1-A9D7-A404A71A98C4}.Release|x86.Build.0 = Release|Any CPU + {FE039675-00BE-4DC5-945F-14E7625170D0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FE039675-00BE-4DC5-945F-14E7625170D0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FE039675-00BE-4DC5-945F-14E7625170D0}.Debug|x64.ActiveCfg = Debug|Any CPU + {FE039675-00BE-4DC5-945F-14E7625170D0}.Debug|x64.Build.0 = Debug|Any CPU + {FE039675-00BE-4DC5-945F-14E7625170D0}.Debug|x86.ActiveCfg = Debug|Any CPU + {FE039675-00BE-4DC5-945F-14E7625170D0}.Debug|x86.Build.0 = Debug|Any CPU + {FE039675-00BE-4DC5-945F-14E7625170D0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FE039675-00BE-4DC5-945F-14E7625170D0}.Release|Any CPU.Build.0 = Release|Any CPU + {FE039675-00BE-4DC5-945F-14E7625170D0}.Release|x64.ActiveCfg = Release|Any CPU + {FE039675-00BE-4DC5-945F-14E7625170D0}.Release|x64.Build.0 = Release|Any CPU + {FE039675-00BE-4DC5-945F-14E7625170D0}.Release|x86.ActiveCfg = Release|Any CPU + {FE039675-00BE-4DC5-945F-14E7625170D0}.Release|x86.Build.0 = Release|Any CPU + {4F9B6FE3-CAC4-4594-84AD-98D1ECDE180C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4F9B6FE3-CAC4-4594-84AD-98D1ECDE180C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4F9B6FE3-CAC4-4594-84AD-98D1ECDE180C}.Debug|x64.ActiveCfg = Debug|Any CPU + {4F9B6FE3-CAC4-4594-84AD-98D1ECDE180C}.Debug|x64.Build.0 = Debug|Any CPU + {4F9B6FE3-CAC4-4594-84AD-98D1ECDE180C}.Debug|x86.ActiveCfg = Debug|Any CPU + {4F9B6FE3-CAC4-4594-84AD-98D1ECDE180C}.Debug|x86.Build.0 = Debug|Any CPU + {4F9B6FE3-CAC4-4594-84AD-98D1ECDE180C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4F9B6FE3-CAC4-4594-84AD-98D1ECDE180C}.Release|Any CPU.Build.0 = Release|Any CPU + {4F9B6FE3-CAC4-4594-84AD-98D1ECDE180C}.Release|x64.ActiveCfg = Release|Any CPU + {4F9B6FE3-CAC4-4594-84AD-98D1ECDE180C}.Release|x64.Build.0 = Release|Any CPU + {4F9B6FE3-CAC4-4594-84AD-98D1ECDE180C}.Release|x86.ActiveCfg = Release|Any CPU + {4F9B6FE3-CAC4-4594-84AD-98D1ECDE180C}.Release|x86.Build.0 = Release|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|x64.ActiveCfg = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|x64.Build.0 = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|x86.ActiveCfg = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|x86.Build.0 = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.Build.0 = Release|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x64.ActiveCfg = Release|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x64.Build.0 = Release|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x86.ActiveCfg = Release|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x86.Build.0 = Release|Any CPU + {E8F8C13E-2E05-4092-84FD-B7B7B12DABBB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E8F8C13E-2E05-4092-84FD-B7B7B12DABBB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E8F8C13E-2E05-4092-84FD-B7B7B12DABBB}.Debug|x64.ActiveCfg = Debug|Any CPU + {E8F8C13E-2E05-4092-84FD-B7B7B12DABBB}.Debug|x64.Build.0 = Debug|Any CPU + {E8F8C13E-2E05-4092-84FD-B7B7B12DABBB}.Debug|x86.ActiveCfg = Debug|Any CPU + {E8F8C13E-2E05-4092-84FD-B7B7B12DABBB}.Debug|x86.Build.0 = Debug|Any CPU + {E8F8C13E-2E05-4092-84FD-B7B7B12DABBB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E8F8C13E-2E05-4092-84FD-B7B7B12DABBB}.Release|Any CPU.Build.0 = Release|Any CPU + {E8F8C13E-2E05-4092-84FD-B7B7B12DABBB}.Release|x64.ActiveCfg = Release|Any CPU + {E8F8C13E-2E05-4092-84FD-B7B7B12DABBB}.Release|x64.Build.0 = Release|Any CPU + {E8F8C13E-2E05-4092-84FD-B7B7B12DABBB}.Release|x86.ActiveCfg = Release|Any CPU + {E8F8C13E-2E05-4092-84FD-B7B7B12DABBB}.Release|x86.Build.0 = Release|Any CPU + {52986844-698F-486B-AEC9-846AAF50CF46}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {52986844-698F-486B-AEC9-846AAF50CF46}.Debug|Any CPU.Build.0 = Debug|Any CPU + {52986844-698F-486B-AEC9-846AAF50CF46}.Debug|x64.ActiveCfg = Debug|Any CPU + {52986844-698F-486B-AEC9-846AAF50CF46}.Debug|x64.Build.0 = Debug|Any CPU + {52986844-698F-486B-AEC9-846AAF50CF46}.Debug|x86.ActiveCfg = Debug|Any CPU + {52986844-698F-486B-AEC9-846AAF50CF46}.Debug|x86.Build.0 = Debug|Any CPU + {52986844-698F-486B-AEC9-846AAF50CF46}.Release|Any CPU.ActiveCfg = Release|Any CPU + {52986844-698F-486B-AEC9-846AAF50CF46}.Release|Any CPU.Build.0 = Release|Any CPU + {52986844-698F-486B-AEC9-846AAF50CF46}.Release|x64.ActiveCfg = Release|Any CPU + {52986844-698F-486B-AEC9-846AAF50CF46}.Release|x64.Build.0 = Release|Any CPU + {52986844-698F-486B-AEC9-846AAF50CF46}.Release|x86.ActiveCfg = Release|Any CPU + {52986844-698F-486B-AEC9-846AAF50CF46}.Release|x86.Build.0 = Release|Any CPU + {0C1A05D2-653D-4F88-B397-BA53E0BA7281}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0C1A05D2-653D-4F88-B397-BA53E0BA7281}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0C1A05D2-653D-4F88-B397-BA53E0BA7281}.Debug|x64.ActiveCfg = Debug|Any CPU + {0C1A05D2-653D-4F88-B397-BA53E0BA7281}.Debug|x64.Build.0 = Debug|Any CPU + {0C1A05D2-653D-4F88-B397-BA53E0BA7281}.Debug|x86.ActiveCfg = Debug|Any CPU + {0C1A05D2-653D-4F88-B397-BA53E0BA7281}.Debug|x86.Build.0 = Debug|Any CPU + {0C1A05D2-653D-4F88-B397-BA53E0BA7281}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0C1A05D2-653D-4F88-B397-BA53E0BA7281}.Release|Any CPU.Build.0 = Release|Any CPU + {0C1A05D2-653D-4F88-B397-BA53E0BA7281}.Release|x64.ActiveCfg = Release|Any CPU + {0C1A05D2-653D-4F88-B397-BA53E0BA7281}.Release|x64.Build.0 = Release|Any CPU + {0C1A05D2-653D-4F88-B397-BA53E0BA7281}.Release|x86.ActiveCfg = Release|Any CPU + {0C1A05D2-653D-4F88-B397-BA53E0BA7281}.Release|x86.Build.0 = Release|Any CPU + {D0EE40C1-87EF-413D-A046-DC37D58BC344}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D0EE40C1-87EF-413D-A046-DC37D58BC344}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D0EE40C1-87EF-413D-A046-DC37D58BC344}.Debug|x64.ActiveCfg = Debug|Any CPU + {D0EE40C1-87EF-413D-A046-DC37D58BC344}.Debug|x64.Build.0 = Debug|Any CPU + {D0EE40C1-87EF-413D-A046-DC37D58BC344}.Debug|x86.ActiveCfg = Debug|Any CPU + {D0EE40C1-87EF-413D-A046-DC37D58BC344}.Debug|x86.Build.0 = Debug|Any CPU + {D0EE40C1-87EF-413D-A046-DC37D58BC344}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D0EE40C1-87EF-413D-A046-DC37D58BC344}.Release|Any CPU.Build.0 = Release|Any CPU + {D0EE40C1-87EF-413D-A046-DC37D58BC344}.Release|x64.ActiveCfg = Release|Any CPU + {D0EE40C1-87EF-413D-A046-DC37D58BC344}.Release|x64.Build.0 = Release|Any CPU + {D0EE40C1-87EF-413D-A046-DC37D58BC344}.Release|x86.ActiveCfg = Release|Any CPU + {D0EE40C1-87EF-413D-A046-DC37D58BC344}.Release|x86.Build.0 = Release|Any CPU + {035680B0-E7C9-4C37-BFA8-4D91E7B67BAC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {035680B0-E7C9-4C37-BFA8-4D91E7B67BAC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {035680B0-E7C9-4C37-BFA8-4D91E7B67BAC}.Debug|x64.ActiveCfg = Debug|Any CPU + {035680B0-E7C9-4C37-BFA8-4D91E7B67BAC}.Debug|x64.Build.0 = Debug|Any CPU + {035680B0-E7C9-4C37-BFA8-4D91E7B67BAC}.Debug|x86.ActiveCfg = Debug|Any CPU + {035680B0-E7C9-4C37-BFA8-4D91E7B67BAC}.Debug|x86.Build.0 = Debug|Any CPU + {035680B0-E7C9-4C37-BFA8-4D91E7B67BAC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {035680B0-E7C9-4C37-BFA8-4D91E7B67BAC}.Release|Any CPU.Build.0 = Release|Any CPU + {035680B0-E7C9-4C37-BFA8-4D91E7B67BAC}.Release|x64.ActiveCfg = Release|Any CPU + {035680B0-E7C9-4C37-BFA8-4D91E7B67BAC}.Release|x64.Build.0 = Release|Any CPU + {035680B0-E7C9-4C37-BFA8-4D91E7B67BAC}.Release|x86.ActiveCfg = Release|Any CPU + {035680B0-E7C9-4C37-BFA8-4D91E7B67BAC}.Release|x86.Build.0 = Release|Any CPU + {A2B3C4D5-E6F7-8901-BCDE-F23456789012}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A2B3C4D5-E6F7-8901-BCDE-F23456789012}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A2B3C4D5-E6F7-8901-BCDE-F23456789012}.Debug|x64.ActiveCfg = Debug|Any CPU + {A2B3C4D5-E6F7-8901-BCDE-F23456789012}.Debug|x64.Build.0 = Debug|Any CPU + {A2B3C4D5-E6F7-8901-BCDE-F23456789012}.Debug|x86.ActiveCfg = Debug|Any CPU + {A2B3C4D5-E6F7-8901-BCDE-F23456789012}.Debug|x86.Build.0 = Debug|Any CPU + {A2B3C4D5-E6F7-8901-BCDE-F23456789012}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A2B3C4D5-E6F7-8901-BCDE-F23456789012}.Release|Any CPU.Build.0 = Release|Any CPU + {A2B3C4D5-E6F7-8901-BCDE-F23456789012}.Release|x64.ActiveCfg = Release|Any CPU + {A2B3C4D5-E6F7-8901-BCDE-F23456789012}.Release|x64.Build.0 = Release|Any CPU + {A2B3C4D5-E6F7-8901-BCDE-F23456789012}.Release|x86.ActiveCfg = Release|Any CPU + {A2B3C4D5-E6F7-8901-BCDE-F23456789012}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {B09236CB-BBD2-4DCF-A698-74CCCAB29FFB} = {07BAE427-96CF-4F9B-80A9-48CFB0A89CF3} + {1E319404-8EF0-40A1-A9D7-A404A71A98C4} = {07BAE427-96CF-4F9B-80A9-48CFB0A89CF3} + {39FD806A-D320-43C9-800F-E9D9BCC90623} = {AF995FEF-BB94-48D0-B02B-6671DA73056B} + {FE039675-00BE-4DC5-945F-14E7625170D0} = {07BAE427-96CF-4F9B-80A9-48CFB0A89CF3} + {4F9B6FE3-CAC4-4594-84AD-98D1ECDE180C} = {F41DA3D8-A0E9-4A05-8A35-78313C0F5804} + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890} = {F41DA3D8-A0E9-4A05-8A35-78313C0F5804} + {E8F8C13E-2E05-4092-84FD-B7B7B12DABBB} = {F41DA3D8-A0E9-4A05-8A35-78313C0F5804} + {52986844-698F-486B-AEC9-846AAF50CF46} = {F41DA3D8-A0E9-4A05-8A35-78313C0F5804} + {0C1A05D2-653D-4F88-B397-BA53E0BA7281} = {F41DA3D8-A0E9-4A05-8A35-78313C0F5804} + {D0EE40C1-87EF-413D-A046-DC37D58BC344} = {07BAE427-96CF-4F9B-80A9-48CFB0A89CF3} + {035680B0-E7C9-4C37-BFA8-4D91E7B67BAC} = {F41DA3D8-A0E9-4A05-8A35-78313C0F5804} + {A2B3C4D5-E6F7-8901-BCDE-F23456789012} = {07BAE427-96CF-4F9B-80A9-48CFB0A89CF3} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {C0FF62D6-1374-4939-A546-432862338528} + EndGlobalSection +EndGlobal diff --git a/GitVersion.yml b/GitVersion.yml index 280505f..2f85197 100644 --- a/GitVersion.yml +++ b/GitVersion.yml @@ -1,4 +1,4 @@ -next-version: 2.0.0 +next-version: 3.0.0 tag-prefix: '[vV]' mode: ContinuousDeployment branches: diff --git a/Images/data-integration-transparent.png b/Images/data-integration-transparent.png new file mode 100644 index 0000000..5e6aefb Binary files /dev/null and b/Images/data-integration-transparent.png differ diff --git a/Images/data-integration.png b/Images/data-integration.png new file mode 100644 index 0000000..1d1ddb2 Binary files /dev/null and b/Images/data-integration.png differ diff --git a/LICENSE b/LICENSE deleted file mode 100644 index daf3b65..0000000 --- a/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2024 Code Shayk - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/LICENSE.md b/LICENSE.md index daf3b65..0207e1c 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2024 Code Shayk +Copyright (c) 2026 Code Shayk Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 4591607..907b1da 100644 --- a/README.md +++ b/README.md @@ -1,76 +1,264 @@ -# ninja Schemio v2.0 -[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/CodeShayk/Schemio/blob/master/LICENSE.md) -[![Master-Build](https://github.com/CodeShayk/Schemio/actions/workflows/Build-Master.yml/badge.svg)](https://github.com/CodeShayk/Schemio/actions/workflows/Build-Master.yml) -[![GitHub Release](https://img.shields.io/github/v/release/CodeShayk/Schemio?logo=github&sort=semver)](https://github.com/CodeShayk/Schemio/releases/latest) -[![Master-CodeQL](https://github.com/CodeShayk/Schemio/actions/workflows/Master-CodeQL.yml/badge.svg)](https://github.com/CodeShayk/Schemio/actions/workflows/Master-CodeQL.yml) -[![.Net 9.0](https://img.shields.io/badge/.Net-9.0-blue)](https://dotnet.microsoft.com/en-us/download/dotnet/9.0) --- - #### Nuget Packages -| Package | Latest | Details | -| --------| --------| --------| -| Schemio.Core|[![NuGet version](https://badge.fury.io/nu/Schemio.Core.svg)](https://badge.fury.io/nu/Schemio.Core) | Provides `core` functionality to configure nested queries and transformers. With ability to map schema paths (XPath/JSONPath) to entity's object graph. `No QueryEngine` provided and requires implementing IQueryEngine to execute IQuery instances. | -| Schemio.SQL|[![NuGet version](https://badge.fury.io/nu/Schemio.SQL.svg)](https://badge.fury.io/nu/Schemio.SQL)| Provides schemio with query engine using `Dapper` to execute SQL queries. | -| Schemio.EntityFramework|[![NuGet version](https://badge.fury.io/nu/Schemio.EntityFramework.svg)](https://badge.fury.io/nu/Schemio.EntityFramework)| Provides schemio with `Entity Framework` query engine to execute queries using DbContext. | -| Schemio.API|[![NuGet version](https://badge.fury.io/nu/Schemio.Api.svg)](https://badge.fury.io/nu/Schemio.Api)| Provides schemio with `Web Api` query engine to execute apis using HttpClient. | - -## Concept -### What is Schemio? -`Schemio` is a data aggregation framework using queries that can target different data platforms. - -Key benefits: -- allows fetching `aggregated` data from `heterogeneous` data storages. You could combine queries targetting different data platforms (example. `SQL`, `API`, `Cache`) to return an aggregated data `entity`. -- allows `selectively` fetching only `parts` of the aggregated data entity. You could retrieve an aggregated data entity with only `sections` of its `object graph` populated with data depending upon the `schema paths` passed with the request. - -### When to use Schemio? -Schemio is perfect fit for many use cases. Few examples that require the service tier to dynamically fetch aggregated data with high performance, availability and scalability are -> - Aggregated APIs -> - Data Reporting -> - Document Generation (with templated data) -> - Content Management Systems -> - Many more - -## Getting Started? -### i. Installation -Install the latest nuget package as appropriate for `Core`, `Web API`, `SQL` using `Dapper` or `EntityFramework` using commands below. - -`Scemio.Core` - for installing schemio for `bespoke` implementation of query engine. +# data DataFuse v3.0.0 + +### Like GraphQL, but for your heterogeneous backend systems + +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/CodeShayk/DataFuse.Net/blob/master/LICENSE.md) +[![Master-Build](https://github.com/CodeShayk/DataFuse.Net/actions/workflows/Build-Master.yml/badge.svg)](https://github.com/CodeShayk/DataFuse.Net/actions/workflows/Build-Master.yml) +[![GitHub Release](https://img.shields.io/github/v/release/CodeShayk/DataFuse.Net?logo=github&sort=semver)](https://github.com/CodeShayk/DataFuse.Net/releases/latest) +[![Master-CodeQL](https://github.com/CodeShayk/DataFuse.Net/actions/workflows/Master-CodeQL.yml/badge.svg)](https://github.com/CodeShayk/DataFuse.Net/actions/workflows/Master-CodeQL.yml) +[![.Net 10.0](https://img.shields.io/badge/.Net-10.0-blue)](https://dotnet.microsoft.com/en-us/download/dotnet/10.0) + +**DataFuse** is a declarative framework that aggregates data from SQL databases, REST APIs, and any heterogeneous datsource into unified, strongly-typed objects — replacing hundreds of lines of manual orchestration code with clean, schema-driven configuration. + +--- + +## The Problem + +Building a product page? You need inventory from SQL Server, pricing from a REST API, and reviews from MongoDB. That's 50+ lines of manual fetch-assemble code — sequential, tightly coupled, and repeated across your codebase. + +```csharp +// Without DataFuse: manual orchestration everywhere +var product = await db.QueryFirstAsync("SELECT * FROM Products WHERE Id = @Id", new { Id = productId }); +var pricing = await httpClient.GetFromJsonAsync($"https://pricing-api/products/{productId}"); +var reviews = await httpClient.GetFromJsonAsync($"https://reviews-api/products/{productId}"); + +product.Price = pricing.Price; +product.Reviews = reviews.Select(r => new Review { Rating = r.Stars, Comment = r.Text }).ToArray(); ``` -NuGet\Install-Package Schemio.Core + +## The Solution + +```csharp +// With DataFuse: declare once, use everywhere +var product = dataProvider.GetData(new ProductRequest { ProductId = 42 }); ``` -`Schemio.SQL` - for installing schemio for SQL with `Dapper` engine. + +DataFuse automatically executes the SQL query first, then runs pricing and reviews API calls **in parallel**, passes parent results to child queries, and assembles the final typed object. + +--- + +## Quick Start + +### 1. Install + +```bash +dotnet add package DataFuse.Integration +dotnet add package DataFuse.Adapters.SQL # SQL with Dapper +dotnet add package DataFuse.Adapters.WebAPI # REST APIs +dotnet add package DataFuse.Adapters.MongoDB # MongoDB +``` + +### 2. Define Your Entity + +```csharp +public class Product : IEntity +{ + public int ProductId { get; set; } + public string Name { get; set; } + public decimal Price { get; set; } + public decimal Discount { get; set; } + public Review[] Reviews { get; set; } +} ``` -NuGet\Install-Package Schemio.SQL + +### 3. Create Queries + +```csharp +// SQL query — fetches product from database +public class ProductQuery : SQLQuery +{ + protected override Func> GetQuery( + IDataContext context, IQueryResult parentQueryResult) + { + var request = (ProductRequest)context.Request; + return connection => connection.QueryFirstOrDefaultAsync( + "SELECT ProductId as Id, Name, Price FROM Products WHERE ProductId = @Id", + new { Id = request.ProductId }); + } +} + +// MongoDB query — fetches reviews from MongoDB, receives parent result +public class ReviewsMongoQuery : MongoQuery> +{ + protected override Func>> GetQuery( + IDataContext context, IQueryResult? parentQueryResult) + { + var product = (ProductResult)parentQueryResult; + return async database => + { + var collection = database.GetCollection("reviews"); + var reviews = await collection.Find(r => r.ProductId == product.Id).ToListAsync(); + return new CollectionResult(reviews); + }; + } +} + +// API query — fetches live pricing from external service, receives parent result +public class PricingApiQuery : WebQuery +{ + public PricingApiQuery() : base("https://api.pricing.com/") { } + + protected override Func GetQuery(IDataContext context, IQueryResult parentQueryResult) + { + var product = (ProductResult)parentQueryResult; + return () => new Uri($"products/{product.Id}/pricing", UriKind.Relative); + } +} ``` -`Schemio.EntityFramework` - for installing schemio for SQL with `EntityFramework` engine. + +### 4. Create Transformers + +```csharp +public class ProductTransform : BaseTransformer +{ + public override void Transform(ProductResult queryResult, Product entity) + { + entity.ProductId = queryResult.Id; + entity.Name = queryResult.Name; + entity.Price = queryResult.Price; + } +} + +public class PricingTransform : BaseTransformer +{ + public override void Transform(PricingResult queryResult, Product entity) + { + entity.Price = queryResult.CurrentPrice; + entity.Discount = queryResult.DiscountPercent; + } +} + +public class ReviewsTransform : BaseTransformer, Product> +{ + public override void Transform(CollectionResult queryResult, Product entity) + { + entity.Reviews = queryResult?.Select(r => new Review + { + ReviewId = r.Id, Comment = r.Comment, Rating = r.Rating + }).ToArray() ?? Array.Empty(); + } +} ``` -NuGet\Install-Package Schemio.EntityFramework + +### 5. Configure the Schema + +```csharp +public class ProductConfiguration : EntityConfiguration +{ + public override IEnumerable> GetSchema() + { + return CreateSchema.For() + .Map(For.Paths("product"), + product => product.Dependents + .Map(For.Paths("product/pricing")) + .Map(For.Paths("product/reviews"))) + .End(); + } +} ``` -`Schemio.API` - for installing schemio for Web API with `HttpClient` engine. + +### 6. Register & Use + +```csharp +// DI registration +services.UseDataFuse() + .WithEngine(c => new QueryEngine(sqlConfiguration)) // SQL + .WithEngine() // REST API + .WithEngine(c => new DataFuse.Adapters.MongoDB.QueryEngine(mongoDatabase)) // MongoDB + .WithPathMatcher(c => new XPathMatcher()) + .WithEntityConfiguration(c => new ProductConfiguration()); + +services.AddHttpClient(); ``` -NuGet\Install-Package Schemio.API + +```csharp +// Usage — one line to get fully hydrated data +public class ProductService +{ + private readonly IDataProvider _dataProvider; + + public ProductService(IDataProvider dataProvider) => _dataProvider = dataProvider; + + public Product GetProduct(int productId) + => _dataProvider.GetData(new ProductRequest { ProductId = productId }); + + // Selective loading — only fetch product + reviews, skip other paths + public Product GetProductWithReviews(int productId) + => _dataProvider.GetData(new ProductRequest + { + ProductId = productId, + SchemaPaths = new[] { "product", "product/reviews" } + }); +} ``` -### ii. Developer Guide -Please see [Developer Guide](https://codeshayk.github.io/Schemio/) for details on how to implement schemio in your project. +--- + +## Key Features + +| Feature | Description | +|---|---| +| **Declarative Schema** | Define data relationships once with fluent configuration | +| **Automatic Parallelism** | Sibling queries run in parallel — no `Task.WhenAll` boilerplate | +| **Selective Loading** | Fetch only the data paths the consumer needs via `SchemaPaths` | +| **Parent-Child Dependencies** | Parent results flow to child queries automatically | +| **Cross-Source Mixing** | Combine SQL + REST API + MongoDB + EF Core queries in one entity | +| **Type Safety** | Strongly-typed queries, results, and transformers | +| **Built-in Caching** | `[CacheResult]` attribute for expensive query results | +| **Custom Adapters** | Add any data source by implementing `IQueryEngine` | + +--- + +## Packages + +| Package | Purpose | Install | +|---|---|---| +| **DataFuse.Integration** | Core orchestration, DI, helpers | `dotnet add package DataFuse.Integration` | +| **DataFuse.Adapters.SQL** | SQL via Dapper | `dotnet add package DataFuse.Adapters.SQL` | +| **DataFuse.Adapters.EntityFramework** | EF Core | `dotnet add package DataFuse.Adapters.EntityFramework` | +| **DataFuse.Adapters.WebAPI** | REST APIs via HttpClient | `dotnet add package DataFuse.Adapters.WebAPI` | +| **DataFuse.Adapters.MongoDB** | MongoDB via official driver | `dotnet add package DataFuse.Adapters.MongoDB` | +| **DataFuse.Adapters.Abstraction** | Interfaces & base classes | Included as dependency | + +### Compatibility + +| Package | .NET | .NET Standard | .NET Framework | +|---|---|---|---| +| DataFuse.Integration | 8.0, 9.0, 10.0 | 2.1 | - | +| DataFuse.Adapters.Abstraction | 8.0, 9.0, 10.0 | 2.1 | - | +| DataFuse.Adapters.SQL | 8.0, 9.0, 10.0 | 2.1 | - | +| DataFuse.Adapters.WebAPI | 8.0, 9.0, 10.0 | 2.1 | - | +| DataFuse.Adapters.MongoDB | 8.0, 9.0, 10.0 | 2.1 | - | +| DataFuse.Adapters.EntityFramework | 10.0 | - | - | + +--- + +## Documentation + +See the [Complete Developer Guide](https://codeshayk.github.io/DataFuse.Net/) for detailed documentation including: +- Real-world use cases (e-commerce, customer 360, reporting) +- Core concepts deep dive +- Query implementation guides (SQL, EF Core, Web API, MongoDB) +- Transformer patterns +- Advanced features (caching, selective loading, custom engines) +- Architecture overview + +--- ## Support -If you are having problems, please let me know by [raising a new issue](https://github.com/CodeShayk/Schemio/issues/new/choose). +If you are having problems, please let us know by [raising a new issue](https://github.com/CodeShayk/DataFuse.Net/issues/new/choose). ## License This project is licensed with the [MIT license](LICENSE). ## Version History -The main branch is now on .NET 8.0. The following previous versions are available: -| Version | Release Notes | Developer Guide | -| -------- | --------|--------| -| [`v1.0.0`](https://github.com/CodeShayk/Schemio/tree/v1.0.0) | [Notes](https://github.com/CodeShayk/Schemio/releases/tag/v1.0.0) | [Guide](https://github.com/CodeShayk/Schemio/blob/v1.0.0/index.md) | -| [`Pre-Release v2.0.0`](https://github.com/CodeShayk/Schemio/tree/v2.0.0) | [Notes](https://github.com/CodeShayk/Schemio/releases/tag/v2.0.0) | [Guide](https://github.com/CodeShayk/Schemio/blob/v2.0.0/index.md) | - -## Credits -Thank you for reading. Please fork, explore, contribute and report. Happy Coding !! :) - - +The main branch is now on .NET 10.0. Previous versions: +| Version | Release Notes | Developer Guide | +|---|---|---| +| [`v3.0.0`](https://github.com/CodeShayk/DataFuse.Net/tree/v3.0.0) | [Notes](https://github.com/CodeShayk/DataFuse.Net/releases/tag/v3.0.0) | [Guide](https://github.com/CodeShayk/DataFuse.Net/blob/v3.0.0/index.md) | +| [`v2.0.0`](https://github.com/CodeShayk/DataFuse.Net/tree/v2.0.0) | [Notes](https://github.com/CodeShayk/DataFuse.Net/releases/tag/v2.0.0) | [Guide](https://github.com/CodeShayk/DataFuse.Net/blob/v2.0.0/index.md) | +| [`v1.0.0`](https://github.com/CodeShayk/DataFuse.Net/tree/v1.0.0) | [Notes](https://github.com/CodeShayk/DataFuse.Net/releases/tag/v1.0.0) | [Guide](https://github.com/CodeShayk/DataFuse.Net/blob/v1.0.0/index.md) | diff --git a/Schemio.sln b/Schemio.sln deleted file mode 100644 index 75e0047..0000000 --- a/Schemio.sln +++ /dev/null @@ -1,99 +0,0 @@ -īģŋ -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.5.33516.290 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{F41DA3D8-A0E9-4A05-8A35-78313C0F5804}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{07BAE427-96CF-4F9B-80A9-48CFB0A89CF3}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "solution", "solution", "{AF995FEF-BB94-48D0-B02B-6671DA73056B}" - ProjectSection(SolutionItems) = preProject - .gitignore = .gitignore - index.md = index.md - LICENSE.md = LICENSE.md - README.md = README.md - EndProjectSection -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Schemio.Core.Tests", "tests\Schemio.Core.Tests\Schemio.Core.Tests.csproj", "{B09236CB-BBD2-4DCF-A698-74CCCAB29FFB}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Schemio.SQL.Tests", "tests\Schemio.SQL.Tests\Schemio.SQL.Tests.csproj", "{1E319404-8EF0-40A1-A9D7-A404A71A98C4}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "github", "github", "{39FD806A-D320-43C9-800F-E9D9BCC90623}" - ProjectSection(SolutionItems) = preProject - .github\workflows\Build-Master.yml = .github\workflows\Build-Master.yml - GitVersion.yml = GitVersion.yml - .github\workflows\Master-CodeQL.yml = .github\workflows\Master-CodeQL.yml - .github\workflows\PullRequest-CI.yml = .github\workflows\PullRequest-CI.yml - .github\workflows\PullRequest-CodeQL.yml = .github\workflows\PullRequest-CodeQL.yml - EndProjectSection -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Schemio.EntityFramework.Tests", "tests\Schemio.EntityFramework.Tests\Schemio.EntityFramework.Tests.csproj", "{FE039675-00BE-4DC5-945F-14E7625170D0}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Schemio.Core", "src\Schemio.Core\Schemio.Core.csproj", "{4F9B6FE3-CAC4-4594-84AD-98D1ECDE180C}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Schemio.EntityFramework", "src\Schemio.EntityFramework\Schemio.EntityFramework.csproj", "{E8F8C13E-2E05-4092-84FD-B7B7B12DABBB}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Schemio.SQL", "src\Schemio.SQL\Schemio.SQL.csproj", "{52986844-698F-486B-AEC9-846AAF50CF46}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Schemio.API", "src\Schemio.API\Schemio.API.csproj", "{0C1A05D2-653D-4F88-B397-BA53E0BA7281}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Schemio.API.Tests", "tests\Schemio.API.Tests\Schemio.API.Tests.csproj", "{D0EE40C1-87EF-413D-A046-DC37D58BC344}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {B09236CB-BBD2-4DCF-A698-74CCCAB29FFB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B09236CB-BBD2-4DCF-A698-74CCCAB29FFB}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B09236CB-BBD2-4DCF-A698-74CCCAB29FFB}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B09236CB-BBD2-4DCF-A698-74CCCAB29FFB}.Release|Any CPU.Build.0 = Release|Any CPU - {1E319404-8EF0-40A1-A9D7-A404A71A98C4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {1E319404-8EF0-40A1-A9D7-A404A71A98C4}.Debug|Any CPU.Build.0 = Debug|Any CPU - {1E319404-8EF0-40A1-A9D7-A404A71A98C4}.Release|Any CPU.ActiveCfg = Release|Any CPU - {1E319404-8EF0-40A1-A9D7-A404A71A98C4}.Release|Any CPU.Build.0 = Release|Any CPU - {FE039675-00BE-4DC5-945F-14E7625170D0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {FE039675-00BE-4DC5-945F-14E7625170D0}.Debug|Any CPU.Build.0 = Debug|Any CPU - {FE039675-00BE-4DC5-945F-14E7625170D0}.Release|Any CPU.ActiveCfg = Release|Any CPU - {FE039675-00BE-4DC5-945F-14E7625170D0}.Release|Any CPU.Build.0 = Release|Any CPU - {4F9B6FE3-CAC4-4594-84AD-98D1ECDE180C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {4F9B6FE3-CAC4-4594-84AD-98D1ECDE180C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {4F9B6FE3-CAC4-4594-84AD-98D1ECDE180C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {4F9B6FE3-CAC4-4594-84AD-98D1ECDE180C}.Release|Any CPU.Build.0 = Release|Any CPU - {E8F8C13E-2E05-4092-84FD-B7B7B12DABBB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E8F8C13E-2E05-4092-84FD-B7B7B12DABBB}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E8F8C13E-2E05-4092-84FD-B7B7B12DABBB}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E8F8C13E-2E05-4092-84FD-B7B7B12DABBB}.Release|Any CPU.Build.0 = Release|Any CPU - {52986844-698F-486B-AEC9-846AAF50CF46}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {52986844-698F-486B-AEC9-846AAF50CF46}.Debug|Any CPU.Build.0 = Debug|Any CPU - {52986844-698F-486B-AEC9-846AAF50CF46}.Release|Any CPU.ActiveCfg = Release|Any CPU - {52986844-698F-486B-AEC9-846AAF50CF46}.Release|Any CPU.Build.0 = Release|Any CPU - {0C1A05D2-653D-4F88-B397-BA53E0BA7281}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {0C1A05D2-653D-4F88-B397-BA53E0BA7281}.Debug|Any CPU.Build.0 = Debug|Any CPU - {0C1A05D2-653D-4F88-B397-BA53E0BA7281}.Release|Any CPU.ActiveCfg = Release|Any CPU - {0C1A05D2-653D-4F88-B397-BA53E0BA7281}.Release|Any CPU.Build.0 = Release|Any CPU - {D0EE40C1-87EF-413D-A046-DC37D58BC344}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D0EE40C1-87EF-413D-A046-DC37D58BC344}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D0EE40C1-87EF-413D-A046-DC37D58BC344}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D0EE40C1-87EF-413D-A046-DC37D58BC344}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {B09236CB-BBD2-4DCF-A698-74CCCAB29FFB} = {07BAE427-96CF-4F9B-80A9-48CFB0A89CF3} - {1E319404-8EF0-40A1-A9D7-A404A71A98C4} = {07BAE427-96CF-4F9B-80A9-48CFB0A89CF3} - {39FD806A-D320-43C9-800F-E9D9BCC90623} = {AF995FEF-BB94-48D0-B02B-6671DA73056B} - {FE039675-00BE-4DC5-945F-14E7625170D0} = {07BAE427-96CF-4F9B-80A9-48CFB0A89CF3} - {4F9B6FE3-CAC4-4594-84AD-98D1ECDE180C} = {F41DA3D8-A0E9-4A05-8A35-78313C0F5804} - {E8F8C13E-2E05-4092-84FD-B7B7B12DABBB} = {F41DA3D8-A0E9-4A05-8A35-78313C0F5804} - {52986844-698F-486B-AEC9-846AAF50CF46} = {F41DA3D8-A0E9-4A05-8A35-78313C0F5804} - {0C1A05D2-653D-4F88-B397-BA53E0BA7281} = {F41DA3D8-A0E9-4A05-8A35-78313C0F5804} - {D0EE40C1-87EF-413D-A046-DC37D58BC344} = {07BAE427-96CF-4F9B-80A9-48CFB0A89CF3} - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {C0FF62D6-1374-4939-A546-432862338528} - EndGlobalSection -EndGlobal diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..3028a60 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,1128 @@ +# DataFuse Framework + +### Like GraphQL, but for your heterogeneous backend systems + +**DataFuse** is a declarative .NET framework that aggregates data from SQL databases, REST APIs, MongoDB, and Entity Framework into unified, strongly-typed objects — replacing hundreds of lines of manual orchestration code with a clean, schema-driven configuration. + +[![.Net 9.0](https://img.shields.io/badge/.Net-9.0-blue)](https://dotnet.microsoft.com/en-us/download/dotnet/9.0) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/CodeShayk/DataFuse.Net/blob/master/LICENSE.md) +[![GitHub Release](https://img.shields.io/github/v/release/CodeShayk/DataFuse.Net?logo=github&sort=semver)](https://github.com/CodeShayk/DataFuse.Net/releases/latest) + +--- + +## Table of Contents + +1. [Real-World Use Cases](#real-world-use-cases) +2. [Why DataFuse?](#why-datafuse) +3. [Quick Start (5 Minutes)](#quick-start) +4. [Core Concepts](#core-concepts) +5. [Packages](#packages) +6. [Query Implementation Guide](#query-implementation-guide) +7. [Transformer Guide](#transformer-guide) +8. [Advanced Features](#advanced-features) +9. [Architecture](#architecture) +10. [Best Practices](#best-practices) +11. [Getting Support](#getting-support) + +--- + +## Real-World Use Cases + +### 1. E-Commerce Product Page + +Your product page needs data from **three different systems**: inventory from a SQL Server database, live pricing from a pricing microservice REST API, and customer reviews from MongoDB. + +**Without DataFuse** — you write this every time: + +```csharp +// 50+ lines of manual orchestration, repeated across your codebase +public async Task GetProductPage(int productId) +{ + var product = await _db.QueryFirstAsync( + "SELECT * FROM Products WHERE Id = @Id", new { Id = productId }); + + var pricing = await _httpClient.GetFromJsonAsync( + $"https://pricing-api/products/{productId}"); + + var reviews = await _mongoDb.GetCollection("reviews") + .Find(r => r.ProductId == productId).ToListAsync(); + + // Manual assembly — error-prone, no caching, no parallel execution + return new ProductPage + { + ProductId = product.Id, + Name = product.Name, + Description = product.Description, + CurrentPrice = pricing.Price, + DiscountPercent = pricing.Discount, + Reviews = reviews.Select(r => new Review + { + Rating = r.Stars, + Comment = r.Text, + Author = r.AuthorName + }).ToArray(), + AverageRating = reviews.Average(r => r.Stars) + }; +} +``` + +**With DataFuse** — declare your schema once, use everywhere: + +```csharp +public class ProductPageConfiguration : EntityConfiguration +{ + public override IEnumerable> GetSchema() + { + return CreateSchema.For() + .Map(For.Paths("product"), + product => product.Dependents + .Map(For.Paths("product/pricing")) + .Map(For.Paths("product/reviews"))) + .End(); + } +} + +// Usage — one line to get fully hydrated data +var productPage = dataProvider.GetData(new ProductRequest { ProductId = 42 }); +``` + +DataFuse automatically: +- Executes the SQL product query first +- Runs pricing and reviews API calls **in parallel** (they're siblings) +- Passes the product result to dependent queries so they know which product to fetch +- Assembles the final `ProductPage` object via type-safe transformers + +### 2. Customer 360 Dashboard + +Build a unified customer view pulling from a CRM database, a billing REST API, and a support ticket system: + +```csharp +public class Customer360Configuration : EntityConfiguration +{ + public override IEnumerable> GetSchema() + { + return CreateSchema.For() + .Map(For.Paths("customer"), + customer => customer.Dependents + .Map(For.Paths("customer/billing")) + .Map(For.Paths("customer/tickets")) + .Map(For.Paths("customer/orders"), + orders => orders.Dependents + .Map( + For.Paths("customer/orders/order/items")))) + .End(); + } +} +``` + +Need just the basic profile without orders? Use **selective loading**: + +```csharp +var basicProfile = dataProvider.GetData(new CustomerRequest +{ + CustomerId = 123, + SchemaPaths = new[] { "customer", "customer/billing" } +}); +``` + +### 3. Multi-Service Reporting + +Aggregate metrics from multiple microservices into a single report object, mixing SQL databases with REST APIs: + +```csharp +public class SalesReportConfiguration : EntityConfiguration +{ + public override IEnumerable> GetSchema() + { + return CreateSchema.For() + .Map(For.Paths("report"), + report => report.Dependents + .Map(For.Paths("report/inventory")) + .Map(For.Paths("report/shipping")) + .Map(For.Paths("report/revenue"))) + .End(); + } +} +``` + +--- + +## Why DataFuse? + +### The Problem + +In modern architectures, a single page or API response often needs data from **multiple backend systems** — SQL databases, REST APIs, MongoDB, third-party services, caches. The standard approach is to write manual orchestration code for each scenario. This leads to: + +- **Boilerplate explosion**: Every new data combination means another 50+ lines of fetch-assemble-transform code +- **No parallelism by default**: Developers write sequential calls unless they manually add `Task.WhenAll` +- **Tight coupling**: Changing a data source means rewriting orchestration code +- **No selective loading**: You fetch everything even when the consumer only needs a subset +- **Inconsistent patterns**: Every developer solves the same problem differently + +### How DataFuse Solves This + +DataFuse provides a **declarative, schema-driven approach** where you: + +1. **Define** your entity (the shape of the data you want) +2. **Configure** a schema mapping queries to entity paths +3. **Let DataFuse handle** execution order, parallelism, dependency passing, and assembly + +### DataFuse vs Alternatives + +| Capability | Manual Code | GraphQL | MediatR | **DataFuse** | +|---|---|---|---|---| +| Multi-source aggregation | Manual wiring | Resolver-based | Manual wiring | **Declarative schema** | +| Parallel execution | Manual `Task.WhenAll` | Per-resolver | Manual | **Automatic** | +| Selective loading | Manual if/else | Built-in | Manual | **Schema path filtering** | +| Dependency management | Manual ordering | Implicit | Manual | **Parent-child hierarchy** | +| Type safety | Varies | Schema-based | Yes | **Strongly typed** | +| New data source support | Rewrite orchestration | New resolver | New handler | **Add adapter (SQL, API, MongoDB, EF, custom)** | +| Learning curve | None | High (new language) | Low | **Low (C# only)** | +| Backend-only (no client changes) | Yes | No (client queries) | Yes | **Yes** | + +### When to Use DataFuse + +**Use DataFuse when:** +- Your data lives across **different systems** (SQL + APIs + external services) +- You need to **compose complex object graphs** from multiple sources +- You want **automatic parallelism** without managing it yourself +- You need **selective loading** — different consumers need different subsets of data +- You're building **API gateways**, **BFF patterns**, or **composite APIs** + +**Use plain Dapper/EF Core when:** +- All your data is in a single database +- Simple queries with SQL joins suffice + +**Use GraphQL when:** +- You need client-controlled query shapes +- You're building a public API where consumers define what they need + +--- + +## Quick Start + +Get up and running in 5 minutes. + +### 1. Install Packages + +```bash +dotnet add package DataFuse.Integration +dotnet add package DataFuse.Adapters.SQL # For SQL with Dapper +dotnet add package DataFuse.Adapters.WebAPI # For REST APIs +dotnet add package DataFuse.Adapters.MongoDB # For MongoDB +dotnet add package DataFuse.Adapters.EntityFramework # For EF Core +``` + +### 2. Define Your Entity + +```csharp +public class Product : IEntity +{ + public int ProductId { get; set; } + public string Name { get; set; } + public decimal Price { get; set; } + public Category Category { get; set; } + public Review[] Reviews { get; set; } +} +``` + +### 3. Create Queries + +**SQL query** (fetches product from database): +```csharp +public class ProductQuery : SQLQuery +{ + protected override Func> GetQuery( + IDataContext context, IQueryResult parentQueryResult) + { + var request = (ProductRequest)context.Request; + return connection => connection.QueryFirstOrDefaultAsync( + "SELECT ProductId as Id, Name, Price, CategoryId FROM Products WHERE ProductId = @Id", + new { Id = request.ProductId }); + } +} +``` + +**MongoDB query** (fetches reviews from MongoDB): +```csharp +public class ReviewsMongoQuery : MongoQuery> +{ + protected override Func>> GetQuery( + IDataContext context, IQueryResult? parentQueryResult) + { + var product = (ProductResult)parentQueryResult; // Parent result is available + return async database => + { + var collection = database.GetCollection("reviews"); + var reviews = await collection.Find(r => r.ProductId == product.Id).ToListAsync(); + return new CollectionResult(reviews); + }; + } +} +``` + +### 4. Create Transformers + +```csharp +public class ProductTransform : BaseTransformer +{ + public override void Transform(ProductResult queryResult, Product entity) + { + entity.ProductId = queryResult.Id; + entity.Name = queryResult.Name; + entity.Price = queryResult.Price; + } +} + +public class ReviewsTransform : BaseTransformer, Product> +{ + public override void Transform(CollectionResult queryResult, Product entity) + { + entity.Reviews = queryResult?.Select(r => new Review + { + ReviewId = r.Id, Comment = r.Comment, Rating = r.Rating + }).ToArray() ?? Array.Empty(); + } +} +``` + +### 5. Configure the Schema + +```csharp +public class ProductConfiguration : EntityConfiguration +{ + public override IEnumerable> GetSchema() + { + return CreateSchema.For() + .Map(For.Paths("product"), + product => product.Dependents + .Map(For.Paths("product/category")) + .Map(For.Paths("product/reviews"))) + .End(); + } +} +``` + +### 6. Register with DI + +```csharp +services.UseDataFuse() + .WithEngine(c => new QueryEngine(sqlConfiguration)) // SQL adapter + .WithEngine() // Web API adapter + .WithEngine(c => new DataFuse.Adapters.MongoDB.QueryEngine(mongoDatabase)) // MongoDB adapter + .WithPathMatcher(c => new XPathMatcher()) + .WithEntityConfiguration(c => new ProductConfiguration()); + +services.AddHttpClient(); +``` + +### 7. Use It + +```csharp +public class ProductService +{ + private readonly IDataProvider _dataProvider; + + public ProductService(IDataProvider dataProvider) + { + _dataProvider = dataProvider; + } + + public Product GetProduct(int productId) + { + return _dataProvider.GetData(new ProductRequest { ProductId = productId }); + } + + // Selective loading — only fetch product + reviews, skip category + public Product GetProductWithReviews(int productId) + { + return _dataProvider.GetData(new ProductRequest + { + ProductId = productId, + SchemaPaths = new[] { "product", "product/reviews" } + }); + } +} +``` + +--- + +## Core Concepts + +### Entities + +Entities represent the final aggregated data structure. They implement `IEntity` and define the complete object graph that DataFuse will hydrate from multiple sources: + +```csharp +public class Customer : IEntity +{ + public int CustomerId { get; set; } + public string Name { get; set; } + + // Each of these can come from a different data source + public Communication Communication { get; set; } // From REST API + public Address Address { get; set; } // From SQL database + public Order[] Orders { get; set; } // From EF Core +} +``` + +### Queries + +Queries fetch data from a specific source. DataFuse provides base classes for common sources: + +| Base Class | Source | Adapter Package | +|---|---|---| +| `SQLQuery` | SQL databases via Dapper | `DataFuse.Adapters.SQL` | +| `SQLQuery` (EF) | Entity Framework Core | `DataFuse.Adapters.EntityFramework` | +| `MongoQuery` | MongoDB collections | `DataFuse.Adapters.MongoDB` | +| `WebQuery` | REST APIs via HttpClient | `DataFuse.Adapters.WebAPI` | + +#### Parent-Child Query Dependencies + +Child queries receive their parent's result, enabling dependent data fetching: + +```csharp +// Root query — fetches customer from database +public class CustomerQuery : SQLQuery +{ + protected override Func> GetQuery( + IDataContext context, IQueryResult parentQueryResult) + { + var request = (CustomerRequest)context.Request; + return connection => connection.QueryFirstOrDefaultAsync( + "SELECT CustomerId as Id, Name, Code FROM Customers WHERE CustomerId = @Id", + new { Id = request.CustomerId }); + } +} + +// Child query — uses parent's CustomerId to fetch orders +public class OrdersQuery : SQLQuery> +{ + protected override Func>> GetQuery( + IDataContext context, IQueryResult parentQueryResult) + { + var customer = (CustomerResult)parentQueryResult; + return async connection => + { + var orders = await connection.QueryAsync( + "SELECT OrderId, OrderNumber, OrderDate FROM Orders WHERE CustomerId = @Id", + new { Id = customer.Id }); + return new CollectionResult(orders); + }; + } +} +``` + +#### Execution Order + +``` +1. Root Query (CustomerQuery) executes first + | + v CustomerResult passed to children + +2. Child Queries execute IN PARALLEL: + - OrdersQuery (uses CustomerId) + - CommunicationQuery (uses CustomerId) + - AddressQuery (uses CustomerId) + | + v OrderResult passed to grandchildren + +3. Grandchild Queries execute: + - OrderItemsQuery (uses OrderId from OrderResult) +``` + +Queries at the same level run in parallel automatically. DataFuse manages the dependency tree. + +### Transformers + +Transformers map query results onto the entity. Each query has a paired transformer: + +```csharp +public class CustomerTransform : BaseTransformer +{ + public override void Transform(CustomerResult queryResult, Customer entity) + { + entity.CustomerId = queryResult.Id; + entity.Name = queryResult.Name; + } +} + +public class OrdersTransform : BaseTransformer, Customer> +{ + public override void Transform(CollectionResult queryResult, Customer entity) + { + entity.Orders = queryResult?.Select(o => new Order + { + OrderId = o.OrderId, + OrderNumber = o.OrderNumber, + OrderDate = o.OrderDate + }).ToArray() ?? Array.Empty(); + } +} +``` + +### Schema Configuration + +The schema defines the full query-transform-path hierarchy: + +```csharp +public class CustomerConfiguration : EntityConfiguration +{ + public override IEnumerable> GetSchema() + { + return CreateSchema.For() + .Map(For.Paths("customer"), + customer => customer.Dependents + .Map( + For.Paths("customer/communication")) + .Map(For.Paths("customer/orders"), + orders => orders.Dependents + .Map( + For.Paths("customer/orders/order/items")))) + .End(); + } +} +``` + +Schema paths correspond to the entity's object graph: +- `customer` — root properties +- `customer/communication` — Customer.Communication +- `customer/orders` — Customer.Orders collection +- `customer/orders/order/items` — Order.Items for each order + +--- + +## Packages + +### DataFuse.Integration + +Core package with orchestration, DI extensions, helpers, and path matchers. + +```bash +dotnet add package DataFuse.Integration +``` + +**Key components:** `DataProvider`, `QueryBuilder`, `EntityBuilder`, `DataFuseOptionsBuilder`, XPath/JSONPath matchers, DI registration via `UseDataFuse()`. + +### DataFuse.Adapters.Abstraction + +Interfaces and base classes: `IEntity`, `IQuery`, `IQueryResult`, `ITransformer`, `IQueryEngine`, `BaseQuery`, `BaseTransformer`, `EntityConfiguration`, `CreateSchema`, `CollectionResult`, `CacheResultAttribute`. + +### DataFuse.Adapters.SQL + +SQL database support using Dapper. + +```bash +dotnet add package DataFuse.Adapters.SQL +``` + +Supports SQL Server, SQLite, MySQL, PostgreSQL, Oracle. + +### DataFuse.Adapters.EntityFramework + +Entity Framework Core integration. + +```bash +dotnet add package DataFuse.Adapters.EntityFramework +``` + +Full LINQ support, DbContext factory integration. + +### DataFuse.Adapters.MongoDB + +MongoDB support using the official MongoDB driver. + +```bash +dotnet add package DataFuse.Adapters.MongoDB +``` + +Full `IMongoDatabase` access, LINQ and filter builder support. + +### DataFuse.Adapters.WebAPI + +REST API support using HttpClient. + +```bash +dotnet add package DataFuse.Adapters.WebAPI +``` + +Request/response header management, JSON deserialization. + +### Compatibility + +| Package | .NET Framework | .NET Standard | .NET | +|---|---|---|---| +| DataFuse.Integration | 4.6.2+ | 2.0, 2.1 | 9.0+ | +| DataFuse.Adapters.SQL | 4.6.2+ | 2.1 | 9.0+ | +| DataFuse.Adapters.EntityFramework | - | - | 9.0+ | +| DataFuse.Adapters.MongoDB | - | - | 9.0+ | +| DataFuse.Adapters.WebAPI | 4.6.2+ | 2.0, 2.1 | 9.0+ | + +--- + +## Query Implementation Guide + +### SQL Queries (Dapper) + +```csharp +// Single result +public class CustomerQuery : SQLQuery +{ + protected override Func> GetQuery( + IDataContext context, IQueryResult parentQueryResult) + { + var request = (CustomerRequest)context.Request; + return connection => connection.QueryFirstOrDefaultAsync( + @"SELECT CustomerId as Id, CustomerName as Name, CustomerCode as Code + FROM Customers WHERE CustomerId = @CustomerId", + new { CustomerId = request.CustomerId }); + } +} + +// Collection result +public class OrdersQuery : SQLQuery> +{ + protected override Func>> GetQuery( + IDataContext context, IQueryResult parentQueryResult) + { + var customer = (CustomerResult)parentQueryResult; + return async connection => + { + var orders = await connection.QueryAsync( + "SELECT OrderId, OrderNumber, OrderDate FROM Orders WHERE CustomerId = @Id", + new { Id = customer.Id }); + return new CollectionResult(orders); + }; + } +} +``` + +### Entity Framework Queries + +```csharp +public class CustomerQuery : SQLQuery +{ + protected override Func> GetQuery( + IDataContext context, IQueryResult parentQueryResult) + { + var request = (CustomerRequest)context.Request; + return async dbContext => + { + return await dbContext.Set() + .Where(c => c.CustomerId == request.CustomerId) + .Select(c => new CustomerResult + { + Id = c.CustomerId, + Name = c.Name, + Code = c.Code + }) + .FirstOrDefaultAsync(); + }; + } +} +``` + +### MongoDB Queries + +```csharp +// Single document +public class CustomerQuery : MongoQuery +{ + protected override Func> GetQuery( + IDataContext context, IQueryResult? parentQueryResult) + { + var request = (CustomerRequest)context.Request; + return async database => + { + var collection = database.GetCollection("customers"); + return await collection.Find(c => c.Id == request.CustomerId).FirstOrDefaultAsync(); + }; + } +} + +// Collection query with filter +public class OrdersQuery : MongoQuery> +{ + protected override Func>> GetQuery( + IDataContext context, IQueryResult? parentQueryResult) + { + var customer = (CustomerResult)parentQueryResult; + return async database => + { + var collection = database.GetCollection("orders"); + var filter = Builders.Filter.Eq(o => o.CustomerId, customer.Id); + var orders = await collection.Find(filter).SortByDescending(o => o.OrderDate).ToListAsync(); + return new CollectionResult(orders); + }; + } +} + +// Aggregation pipeline +public class SalesSummaryQuery : MongoQuery +{ + protected override Func> GetQuery( + IDataContext context, IQueryResult? parentQueryResult) + { + var customer = (CustomerResult)parentQueryResult; + return async database => + { + var collection = database.GetCollection("orders"); + var result = await collection.Aggregate() + .Match(Builders.Filter.Eq("customerId", customer.Id)) + .Group(new BsonDocument + { + { "_id", "$customerId" }, + { "totalSpent", new BsonDocument("$sum", "$amount") }, + { "orderCount", new BsonDocument("$sum", 1) } + }) + .FirstOrDefaultAsync(); + + return new SalesSummaryResult + { + TotalSpent = result?["totalSpent"].ToDecimal() ?? 0, + OrderCount = result?["orderCount"].ToInt32() ?? 0 + }; + }; + } +} +``` + +### Web API Queries + +```csharp +// Basic API query +public class PricingApiQuery : WebQuery +{ + public PricingApiQuery() : base("https://api.pricing.com/") { } + + protected override Func GetQuery(IDataContext context, IQueryResult parentQueryResult) + { + var product = (ProductResult)parentQueryResult; + return () => new Uri($"products/{product.Id}/pricing", UriKind.Relative); + } +} + +// API query with custom headers +public class AuthenticatedApiQuery : WebQuery +{ + public AuthenticatedApiQuery() : base("https://api.secure.com/") { } + + protected override Func GetQuery(IDataContext context, IQueryResult parentQueryResult) + { + var request = (SecureRequest)context.Request; + return () => new Uri($"secure/data/{request.Id}", UriKind.Relative); + } + + protected override IDictionary GetRequestHeaders() + { + return new Dictionary + { + { "Authorization", "Bearer " + GetAccessToken() }, + { "Accept", "application/json" } + }; + } + + protected override IEnumerable GetResponseHeaders() + { + return new[] { "X-Rate-Limit-Remaining", "X-Request-Id" }; + } +} +``` + +### Query Result Types + +```csharp +// Simple result +public class CustomerResult : IQueryResult +{ + public int Id { get; set; } + public string Name { get; set; } + public string Code { get; set; } +} + +// Collection result +public class CollectionResult : List, IQueryResult +{ + public CollectionResult(IEnumerable items) : base(items) { } + public CollectionResult() { } +} + +// Cached result — cached automatically across queries +[CacheResult] +public class CategoryResult : IQueryResult +{ + public int Id { get; set; } + public string Name { get; set; } +} + +// Web result with response headers +public class UserApiResult : WebHeaderResult +{ + public int Id { get; set; } + public string Name { get; set; } +} +``` + +--- + +## Transformer Guide + +### Basic Transformers + +```csharp +public class CustomerTransform : BaseTransformer +{ + public override void Transform(CustomerResult queryResult, Customer entity) + { + if (queryResult == null) return; + + entity.CustomerId = queryResult.Id; + entity.CustomerName = queryResult.Name ?? string.Empty; + entity.CustomerCode = queryResult.Code; + } +} +``` + +### Collection Transformers + +```csharp +public class OrdersTransform : BaseTransformer, Customer> +{ + public override void Transform(CollectionResult queryResult, Customer entity) + { + entity.Orders = queryResult?.Select(o => new Order + { + OrderId = o.OrderId, + OrderNumber = o.OrderNumber, + OrderDate = o.OrderDate, + TotalAmount = o.TotalAmount + }).ToArray() ?? Array.Empty(); + } +} +``` + +### Context-Aware Transformers + +Transformers can access the request context for conditional logic: + +```csharp +public class ProductTransform : BaseTransformer +{ + public override void Transform(ProductResult queryResult, Product entity) + { + entity.ProductId = queryResult.Id; + entity.Name = queryResult.Name; + entity.Price = queryResult.Price; + + var request = Context.Request as ProductRequest; + if (request?.IncludeMetadata == true) + { + entity.CreatedDate = queryResult.CreatedDate; + entity.LastModified = queryResult.LastModified; + } + } +} +``` + +### Grandchild Transformers + +For deeply nested data, transformers map grandchild results back to the correct parent: + +```csharp +public class OrderItemsTransform : BaseTransformer, Customer> +{ + public override void Transform(CollectionResult queryResult, Customer entity) + { + if (queryResult == null || !queryResult.Any()) return; + + var itemsByOrder = queryResult.GroupBy(item => item.OrderId); + foreach (var group in itemsByOrder) + { + var order = entity.Orders.FirstOrDefault(o => o.OrderId == group.Key); + if (order != null) + { + order.Items = group.Select(item => new OrderItem + { + ItemId = item.ItemId, + ProductName = item.ProductName, + Quantity = item.Quantity, + Price = item.Price + }).ToArray(); + } + } + } +} +``` + +--- + +## Advanced Features + +### Caching + +Mark query results with `[CacheResult]` to cache them for use in other queries and transformers: + +```csharp +[CacheResult] +public class CategoryResult : IQueryResult +{ + public int Id { get; set; } + public string Name { get; set; } +} + +// Access cached results in a transformer +public class ProductTransform : BaseTransformer +{ + public override void Transform(ProductResult queryResult, Product entity) + { + entity.ProductId = queryResult.Id; + + if (Context.Cache.TryGetValue("CategoryResult", out var cached)) + { + entity.CategoryName = ((CategoryResult)cached).Name; + } + } +} +``` + +Caching is useful when a grandchild query needs data from a grandparent query that isn't its direct parent. + +### Selective Loading + +Control which parts of the object graph to load via `SchemaPaths`: + +```csharp +// Load everything +var fullCustomer = dataProvider.GetData(new CustomerRequest +{ + CustomerId = 123 + // SchemaPaths = null loads all configured paths +}); + +// Load only customer + orders (skip communication, address, etc.) +var withOrders = dataProvider.GetData(new CustomerRequest +{ + CustomerId = 123, + SchemaPaths = new[] { "customer", "customer/orders" } +}); + +// Load just the basic profile +var basicProfile = dataProvider.GetData(new CustomerRequest +{ + CustomerId = 123, + SchemaPaths = new[] { "customer" } +}); +``` + +This avoids unnecessary queries — if a consumer only needs the customer name, DataFuse won't execute order or communication queries. + +### Parallel Execution + +Sibling queries at the same level execute in parallel automatically: + +```csharp +return CreateSchema.For() + .Map(For.Paths("customer"), + customer => customer.Dependents + .Map(For.Paths("customer/contact")) // Parallel + .Map(For.Paths("customer/preferences")) // Parallel + .Map(For.Paths("customer/address"))) // Parallel + .End(); +``` + +No `Task.WhenAll` boilerplate needed. DataFuse manages execution order based on the dependency tree. + +### Conditional Query Execution + +Child queries can skip execution based on parent data: + +```csharp +public class PremiumServicesQuery : WebQuery +{ + protected override Func GetQuery(IDataContext context, IQueryResult parentQueryResult) + { + var customer = (CustomerResult)parentQueryResult; + + if (customer.CustomerType != "Premium") + return null; // Returning null skips this query + + return () => new Uri($"premium-services/{customer.Id}", UriKind.Relative); + } +} +``` + +### Cross-Source Dependencies + +Mix data sources within the same entity. A root query can be SQL while its children pull from MongoDB and REST APIs: + +```csharp +return CreateSchema.For() + .Map(For.Paths("customer"), // SQL + customer => customer.Dependents + .Map(For.Paths("customer/billing")) // REST API + .Map(For.Paths("customer/reviews")) // MongoDB + .Map(For.Paths("customer/analytics"))) // EF Core + .End(); +``` + +### Custom Query Engines + +Add support for any data source by implementing `IQueryEngine`: + +```csharp +public class RedisQueryEngine : IQueryEngine +{ + private readonly IConnectionMultiplexer _redis; + + public RedisQueryEngine(IConnectionMultiplexer redis) + { + _redis = redis; + } + + public bool CanExecute(IQuery query) => query is IRedisQuery; + + public async Task Execute(IQuery query) + { + var redisQuery = (IRedisQuery)query; + return await redisQuery.Run(_redis.GetDatabase()); + } +} + +// Register alongside built-in adapters +services.UseDataFuse() + .WithEngine(c => new RedisQueryEngine(redisConnection)) + .WithEngine(c => new QueryEngine(sqlConfiguration)) + .WithEngine(c => new DataFuse.Adapters.MongoDB.QueryEngine(mongoDatabase)) + // ... +``` + +### Path Matching + +DataFuse supports XPath and JSONPath patterns for schema paths: + +**XPath patterns:** +- `customer` — exact match +- `customer/orders` — nested path +- `customer/orders/order/items` — deep nesting + +**JSONPath patterns:** +- `$.customer` — root level +- `$.customer.orders` — nested property +- `$.customer.orders[*].items` — array elements + +Register your preferred matcher: +```csharp +services.UseDataFuse() + .WithPathMatcher(c => new XPathMatcher()) // or JPathMatcher +``` + +--- + +## Architecture + +### Execution Flow + +``` +Client Request + | + v +DataProvider + | + v +QueryBuilder — filters by SchemaPaths, resolves dependencies, builds execution plan + | + v +QueryExecutor — executes level-by-level, parallelizes siblings, propagates results + | + v +EntityBuilder — applies transformers in dependency order, assembles final entity + | + v +Fully Hydrated Entity +``` + +### Component Responsibilities + +| Component | Responsibility | +|---|---| +| `DataProvider` | Main orchestrator — coordinates the full pipeline | +| `QueryBuilder` | Filters queries by requested schema paths, resolves parent-child dependencies | +| `QueryExecutor` | Executes queries level-by-level with parallel processing of siblings | +| `EntityBuilder` | Matches results to transformers and builds the final entity | +| `IQueryEngine` | Adapter interface — each data source implements this | +| `EntityConfiguration` | Declares the schema: query-transform-path mappings | + +### Configuration Patterns + +**Linear** — flat entity: +```csharp +return CreateSchema.For() + .Map(For.Paths("customer")) + .Map(For.Paths("customer/address")) + .End(); +``` + +**Branching** — multiple children: +```csharp +return CreateSchema.For() + .Map(For.Paths("customer"), + c => c.Dependents + .Map(For.Paths("customer/contact")) + .Map(For.Paths("customer/preferences"))) + .End(); +``` + +**Deep nesting** — up to 5 levels: +```csharp +return CreateSchema.For() + .Map(For.Paths("customer"), + c => c.Dependents + .Map(For.Paths("customer/orders"), + o => o.Dependents + .Map(For.Paths("customer/orders/order/items")))) + .End(); +``` + +--- + +## Best Practices + +### Query Design +- Keep queries focused — one query per data concern +- Use `CollectionResult` for queries returning multiple records +- Return `null` from `GetQuery` to conditionally skip execution +- Use `[CacheResult]` for expensive, reusable query results + +### Transformer Design +- Always handle null query results gracefully +- Initialize collections to prevent null reference exceptions +- Keep transformers focused on mapping — avoid business logic +- Use `Context.Request` for conditional transformation + +### Schema Design +- Use meaningful, hierarchical path names that mirror your entity structure +- Keep nesting depth reasonable (3-4 levels is typical) +- Use selective loading for performance-sensitive endpoints + +### DI Registration +```csharp +services.UseDataFuse() + .WithEngine(c => new QueryEngine(sqlConfig)) // SQL + .WithEngine() // REST API + .WithEngine(c => new DataFuse.Adapters.MongoDB.QueryEngine(mongoDatabase)) // MongoDB + .WithPathMatcher(c => new XPathMatcher()) + .WithEntityConfiguration(c => new CustomerConfiguration()) + .WithEntityConfiguration(c => new ProductConfiguration()); + +services.AddLogging(); +services.AddHttpClient(); +``` + +--- + +## Getting Support + +- **GitHub**: [github.com/CodeShayk/DataFuse.Net](https://github.com/CodeShayk/DataFuse.Net) +- **Issues**: [Report bugs and feature requests](https://github.com/CodeShayk/DataFuse.Net/issues) +- **Wiki**: [Developer guide and documentation](https://github.com/CodeShayk/DataFuse.Net/wiki) +- **Samples**: Check example projects in the repository for real-world usage patterns diff --git a/docs/v3.0.0-Release_Notes.md b/docs/v3.0.0-Release_Notes.md new file mode 100644 index 0000000..0f8ba7f --- /dev/null +++ b/docs/v3.0.0-Release_Notes.md @@ -0,0 +1,127 @@ +# DataFuse v3.0.0 Release Notes + +**Release Date:** March 30, 2026 + +DataFuse v3.0.0 is a major release that introduces the new **MongoDB adapter**, **transform hooks**, **query result caching**, and **multi-target framework support**. This release also completes the project rebrand from Schemio to DataFuse, which includes breaking namespace and package changes. + +--- + +## New Features + +### MongoDB Adapter + +New `DataFuse.Adapters.MongoDB` package for aggregating data from MongoDB collections alongside SQL, REST API, and Entity Framework sources. + +- Built on MongoDB Driver 3.4.0 +- Implements the same query/engine pattern as other adapters +- Register via `.WithEngine(c => new DataFuse.Adapters.MongoDB.QueryEngine(mongoDatabase))` + +```csharp +public class ReviewsMongoQuery : MongoQuery> +{ + protected override Func>> GetQuery( + IDataContext context, IQueryResult? parentQueryResult) + { + var product = (ProductResult)parentQueryResult; + return async database => + { + var collection = database.GetCollection("reviews"); + var reviews = await collection.Find(r => r.ProductId == product.Id).ToListAsync(); + return new CollectionResult(reviews); + }; + } +} +``` + +### Transform Hooks + +Pre and post transformation hooks provide fine-grained control over the data transformation pipeline. + +- **PreTransform hooks** execute before a transformation and can cancel it via `context.Cancel()` +- **PostTransform hooks** execute after a transformation and can request repetition via `context.Repeat()` + +### Query Result Caching + +New `CacheResultAttribute` allows marking expensive query results for automatic caching to improve performance. + +### Multi-Target Framework Support + +All packages (except EntityFramework) now target multiple frameworks: + +- `netstandard2.1` +- `net8.0` +- `net9.0` +- `net10.0` + +`DataFuse.Adapters.EntityFramework` targets `net10.0` only (EF Core 10.0 dependency). + +--- + +## Breaking Changes + +### Project Rename (Schemio to DataFuse) + +All namespaces, packages, and registration APIs have been renamed: + +| v2.x (Schemio) | v3.0.0 (DataFuse) | +|-----------------------------|--------------------------------------| +| `Schemio.Core` (contracts) | `DataFuse.Adapters.Abstraction` | +| `Schemio.Core` (impl) | `DataFuse.Integration` | +| `Schemio.SQL` | `DataFuse.Adapters.SQL` | +| `Schemio.EntityFramework` | `DataFuse.Adapters.EntityFramework` | +| `Schemio.API` | `DataFuse.Adapters.WebAPI` | + +### DI Registration + +```csharp +// Before (v2.x) +services.UseSchemio(new SchemioOptionsBuilder() ...); + +// After (v3.0.0) +services.UseDataFuse(new DataFuseOptionsBuilder() ...); +``` + +### Interface Renames + +- `ISchemioOptions` → `IDataFuseOptions` +- `SchemioOptionsBuilder` → `DataFuseOptionsBuilder` + +### NuGet Package Names + +All package IDs have changed. Update your package references: + +| Old Package | New Package | +|------------------------|--------------------------------------| +| `Schemio.Core` | `DataFuse.Adapters.Abstraction` | +| `Schemio.Core` | `DataFuse.Integration` | +| `Schemio.SQL` | `DataFuse.Adapters.SQL` | +| `Schemio.EntityFramework` | `DataFuse.Adapters.EntityFramework` | +| `Schemio.API` | `DataFuse.Adapters.WebAPI` | + +--- + +## Migration Guide + +1. **Update NuGet packages** to the new `DataFuse.*` package names +2. **Update `using` statements** from `Schemio.*` namespaces to `DataFuse.*` +3. **Update DI registration** from `UseSchemio()` to `UseDataFuse()` and `SchemioOptionsBuilder` to `DataFuseOptionsBuilder` +4. **Update target framework** if needed — packages now support `netstandard2.1`, `net8.0`, `net9.0`, and `net10.0` + +--- + +## Packages + +| Package | Version | Frameworks | +|--------------------------------------|---------|----------------------------------------------| +| `DataFuse.Adapters.Abstraction` | 3.0.0 | netstandard2.1, net8.0, net9.0, net10.0 | +| `DataFuse.Integration` | 3.0.0 | netstandard2.1, net8.0, net9.0, net10.0 | +| `DataFuse.Adapters.SQL` | 3.0.0 | netstandard2.1, net8.0, net9.0, net10.0 | +| `DataFuse.Adapters.EntityFramework` | 3.0.0 | net10.0 | +| `DataFuse.Adapters.WebAPI` | 3.0.0 | netstandard2.1, net8.0, net9.0, net10.0 | +| `DataFuse.Adapters.MongoDB` | 3.0.0 | netstandard2.1, net8.0, net9.0, net10.0 | + +--- + +## Repository + +- **GitHub:** [CodeShayk/DataFuse.Net](https://github.com/CodeShayk/DataFuse.Net) diff --git a/docs/wiki.md b/docs/wiki.md new file mode 100644 index 0000000..8fd38dd --- /dev/null +++ b/docs/wiki.md @@ -0,0 +1,1218 @@ +# DataFuse Framework + +### Like GraphQL, but for your heterogeneous backend systems + +**DataFuse** is a declarative .NET framework that aggregates data from SQL databases, REST APIs, MongoDB, and Entity Framework into unified, strongly-typed objects — replacing hundreds of lines of manual orchestration code with a clean, schema-driven configuration. + +[![.Net 9.0](https://img.shields.io/badge/.Net-9.0-blue)](https://dotnet.microsoft.com/en-us/download/dotnet/9.0) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/CodeShayk/DataFuse.Net/blob/master/LICENSE.md) +[![GitHub Release](https://img.shields.io/github/v/release/CodeShayk/DataFuse.Net?logo=github&sort=semver)](https://github.com/CodeShayk/DataFuse.Net/releases/latest) + +--- + +## Table of Contents + +1. [Real-World Use Cases](#real-world-use-cases) +2. [Why DataFuse?](#why-datafuse) +3. [Quick Start (5 Minutes)](#quick-start) +4. [Core Concepts](#core-concepts) +5. [Packages](#packages) +6. [Query Implementation Guide](#query-implementation-guide) +7. [Transformer Guide](#transformer-guide) +8. [Advanced Features](#advanced-features) +9. [Architecture](#architecture) +10. [Best Practices](#best-practices) +11. [Migrating from Schemio to DataFuse](#migrating-from-schemio-v2x-to-datafuse-v300) +12. [Getting Support](#getting-support) + +--- + +## Real-World Use Cases + +### 1. E-Commerce Product Page + +Your product page needs data from **three different systems**: inventory from a SQL Server database, live pricing from a pricing microservice REST API, and customer reviews from MongoDB. + +**Without DataFuse** — you write this every time: + +```csharp +// 50+ lines of manual orchestration, repeated across your codebase +public async Task GetProductPage(int productId) +{ + var product = await _db.QueryFirstAsync( + "SELECT * FROM Products WHERE Id = @Id", new { Id = productId }); + + var pricing = await _httpClient.GetFromJsonAsync( + $"https://pricing-api/products/{productId}"); + + var reviews = await _mongoDb.GetCollection("reviews") + .Find(r => r.ProductId == productId).ToListAsync(); + + // Manual assembly — error-prone, no caching, no parallel execution + return new ProductPage + { + ProductId = product.Id, + Name = product.Name, + Description = product.Description, + CurrentPrice = pricing.Price, + DiscountPercent = pricing.Discount, + Reviews = reviews.Select(r => new Review + { + Rating = r.Stars, + Comment = r.Text, + Author = r.AuthorName + }).ToArray(), + AverageRating = reviews.Average(r => r.Stars) + }; +} +``` + +**With DataFuse** — declare your schema once, use everywhere: + +```csharp +public class ProductPageConfiguration : EntityConfiguration +{ + public override IEnumerable> GetSchema() + { + return CreateSchema.For() + .Map(For.Paths("product"), + product => product.Dependents + .Map(For.Paths("product/pricing")) + .Map(For.Paths("product/reviews"))) + .End(); + } +} + +// Usage — one line to get fully hydrated data +var productPage = dataProvider.GetData(new ProductRequest { ProductId = 42 }); +``` + +DataFuse automatically: +- Executes the SQL product query first +- Runs pricing and reviews API calls **in parallel** (they're siblings) +- Passes the product result to dependent queries so they know which product to fetch +- Assembles the final `ProductPage` object via type-safe transformers + +### 2. Customer 360 Dashboard + +Build a unified customer view pulling from a CRM database, a billing REST API, and a support ticket system: + +```csharp +public class Customer360Configuration : EntityConfiguration +{ + public override IEnumerable> GetSchema() + { + return CreateSchema.For() + .Map(For.Paths("customer"), + customer => customer.Dependents + .Map(For.Paths("customer/billing")) + .Map(For.Paths("customer/tickets")) + .Map(For.Paths("customer/orders"), + orders => orders.Dependents + .Map( + For.Paths("customer/orders/order/items")))) + .End(); + } +} +``` + +Need just the basic profile without orders? Use **selective loading**: + +```csharp +var basicProfile = dataProvider.GetData(new CustomerRequest +{ + CustomerId = 123, + SchemaPaths = new[] { "customer", "customer/billing" } +}); +``` + +### 3. Multi-Service Reporting + +Aggregate metrics from multiple microservices into a single report object, mixing SQL databases with REST APIs: + +```csharp +public class SalesReportConfiguration : EntityConfiguration +{ + public override IEnumerable> GetSchema() + { + return CreateSchema.For() + .Map(For.Paths("report"), + report => report.Dependents + .Map(For.Paths("report/inventory")) + .Map(For.Paths("report/shipping")) + .Map(For.Paths("report/revenue"))) + .End(); + } +} +``` + +--- + +## Why DataFuse? + +### The Problem + +In modern architectures, a single page or API response often needs data from **multiple backend systems** — SQL databases, REST APIs, MongoDB, third-party services, caches. The standard approach is to write manual orchestration code for each scenario. This leads to: + +- **Boilerplate explosion**: Every new data combination means another 50+ lines of fetch-assemble-transform code +- **No parallelism by default**: Developers write sequential calls unless they manually add `Task.WhenAll` +- **Tight coupling**: Changing a data source means rewriting orchestration code +- **No selective loading**: You fetch everything even when the consumer only needs a subset +- **Inconsistent patterns**: Every developer solves the same problem differently + +### How DataFuse Solves This + +DataFuse provides a **declarative, schema-driven approach** where you: + +1. **Define** your entity (the shape of the data you want) +2. **Configure** a schema mapping queries to entity paths +3. **Let DataFuse handle** execution order, parallelism, dependency passing, and assembly + +### DataFuse vs Alternatives + +| Capability | Manual Code | GraphQL | MediatR | **DataFuse** | +|---|---|---|---|---| +| Multi-source aggregation | Manual wiring | Resolver-based | Manual wiring | **Declarative schema** | +| Parallel execution | Manual `Task.WhenAll` | Per-resolver | Manual | **Automatic** | +| Selective loading | Manual if/else | Built-in | Manual | **Schema path filtering** | +| Dependency management | Manual ordering | Implicit | Manual | **Parent-child hierarchy** | +| Type safety | Varies | Schema-based | Yes | **Strongly typed** | +| New data source support | Rewrite orchestration | New resolver | New handler | **Add adapter (SQL, API, MongoDB, EF, custom)** | +| Learning curve | None | High (new language) | Low | **Low (C# only)** | +| Backend-only (no client changes) | Yes | No (client queries) | Yes | **Yes** | + +### When to Use DataFuse + +**Use DataFuse when:** +- Your data lives across **different systems** (SQL + APIs + external services) +- You need to **compose complex object graphs** from multiple sources +- You want **automatic parallelism** without managing it yourself +- You need **selective loading** — different consumers need different subsets of data +- You're building **API gateways**, **BFF patterns**, or **composite APIs** + +**Use plain Dapper/EF Core when:** +- All your data is in a single database +- Simple queries with SQL joins suffice + +**Use GraphQL when:** +- You need client-controlled query shapes +- You're building a public API where consumers define what they need + +--- + +## Quick Start + +Get up and running in 5 minutes. + +### 1. Install Packages + +```bash +dotnet add package DataFuse.Integration +dotnet add package DataFuse.Adapters.SQL # For SQL with Dapper +dotnet add package DataFuse.Adapters.WebAPI # For REST APIs +dotnet add package DataFuse.Adapters.MongoDB # For MongoDB +dotnet add package DataFuse.Adapters.EntityFramework # For EF Core +``` + +### 2. Define Your Entity + +```csharp +public class Product : IEntity +{ + public int ProductId { get; set; } + public string Name { get; set; } + public decimal Price { get; set; } + public Category Category { get; set; } + public Review[] Reviews { get; set; } +} +``` + +### 3. Create Queries + +**SQL query** (fetches product from database): +```csharp +public class ProductQuery : SQLQuery +{ + protected override Func> GetQuery( + IDataContext context, IQueryResult parentQueryResult) + { + var request = (ProductRequest)context.Request; + return connection => connection.QueryFirstOrDefaultAsync( + "SELECT ProductId as Id, Name, Price, CategoryId FROM Products WHERE ProductId = @Id", + new { Id = request.ProductId }); + } +} +``` + +**MongoDB query** (fetches reviews from MongoDB): +```csharp +public class ReviewsMongoQuery : MongoQuery> +{ + protected override Func>> GetQuery( + IDataContext context, IQueryResult? parentQueryResult) + { + var product = (ProductResult)parentQueryResult; // Parent result is available + return async database => + { + var collection = database.GetCollection("reviews"); + var reviews = await collection.Find(r => r.ProductId == product.Id).ToListAsync(); + return new CollectionResult(reviews); + }; + } +} +``` + +### 4. Create Transformers + +```csharp +public class ProductTransform : BaseTransformer +{ + public override void Transform(ProductResult queryResult, Product entity) + { + entity.ProductId = queryResult.Id; + entity.Name = queryResult.Name; + entity.Price = queryResult.Price; + } +} + +public class ReviewsTransform : BaseTransformer, Product> +{ + public override void Transform(CollectionResult queryResult, Product entity) + { + entity.Reviews = queryResult?.Select(r => new Review + { + ReviewId = r.Id, Comment = r.Comment, Rating = r.Rating + }).ToArray() ?? Array.Empty(); + } +} +``` + +### 5. Configure the Schema + +```csharp +public class ProductConfiguration : EntityConfiguration +{ + public override IEnumerable> GetSchema() + { + return CreateSchema.For() + .Map(For.Paths("product"), + product => product.Dependents + .Map(For.Paths("product/category")) + .Map(For.Paths("product/reviews"))) + .End(); + } +} +``` + +### 6. Register with DI + +```csharp +services.UseDataFuse() + .WithEngine(c => new QueryEngine(sqlConfiguration)) // SQL adapter + .WithEngine() // Web API adapter + .WithEngine(c => new DataFuse.Adapters.MongoDB.QueryEngine(mongoDatabase)) // MongoDB adapter + .WithPathMatcher(c => new XPathMatcher()) + .WithEntityConfiguration(c => new ProductConfiguration()); + +services.AddHttpClient(); +``` + +### 7. Use It + +```csharp +public class ProductService +{ + private readonly IDataProvider _dataProvider; + + public ProductService(IDataProvider dataProvider) + { + _dataProvider = dataProvider; + } + + public Product GetProduct(int productId) + { + return _dataProvider.GetData(new ProductRequest { ProductId = productId }); + } + + // Selective loading — only fetch product + reviews, skip category + public Product GetProductWithReviews(int productId) + { + return _dataProvider.GetData(new ProductRequest + { + ProductId = productId, + SchemaPaths = new[] { "product", "product/reviews" } + }); + } +} +``` + +--- + +## Core Concepts + +### Entities + +Entities represent the final aggregated data structure. They implement `IEntity` and define the complete object graph that DataFuse will hydrate from multiple sources: + +```csharp +public class Customer : IEntity +{ + public int CustomerId { get; set; } + public string Name { get; set; } + + // Each of these can come from a different data source + public Communication Communication { get; set; } // From REST API + public Address Address { get; set; } // From SQL database + public Order[] Orders { get; set; } // From EF Core +} +``` + +### Queries + +Queries fetch data from a specific source. DataFuse provides base classes for common sources: + +| Base Class | Source | Adapter Package | +|---|---|---| +| `SQLQuery` | SQL databases via Dapper | `DataFuse.Adapters.SQL` | +| `SQLQuery` (EF) | Entity Framework Core | `DataFuse.Adapters.EntityFramework` | +| `MongoQuery` | MongoDB collections | `DataFuse.Adapters.MongoDB` | +| `WebQuery` | REST APIs via HttpClient | `DataFuse.Adapters.WebAPI` | + +#### Parent-Child Query Dependencies + +Child queries receive their parent's result, enabling dependent data fetching: + +```csharp +// Root query — fetches customer from database +public class CustomerQuery : SQLQuery +{ + protected override Func> GetQuery( + IDataContext context, IQueryResult parentQueryResult) + { + var request = (CustomerRequest)context.Request; + return connection => connection.QueryFirstOrDefaultAsync( + "SELECT CustomerId as Id, Name, Code FROM Customers WHERE CustomerId = @Id", + new { Id = request.CustomerId }); + } +} + +// Child query — uses parent's CustomerId to fetch orders +public class OrdersQuery : SQLQuery> +{ + protected override Func>> GetQuery( + IDataContext context, IQueryResult parentQueryResult) + { + var customer = (CustomerResult)parentQueryResult; + return async connection => + { + var orders = await connection.QueryAsync( + "SELECT OrderId, OrderNumber, OrderDate FROM Orders WHERE CustomerId = @Id", + new { Id = customer.Id }); + return new CollectionResult(orders); + }; + } +} +``` + +#### Execution Order + +``` +1. Root Query (CustomerQuery) executes first + | + v CustomerResult passed to children + +2. Child Queries execute IN PARALLEL: + - OrdersQuery (uses CustomerId) + - CommunicationQuery (uses CustomerId) + - AddressQuery (uses CustomerId) + | + v OrderResult passed to grandchildren + +3. Grandchild Queries execute: + - OrderItemsQuery (uses OrderId from OrderResult) +``` + +Queries at the same level run in parallel automatically. DataFuse manages the dependency tree. + +### Transformers + +Transformers map query results onto the entity. Each query has a paired transformer: + +```csharp +public class CustomerTransform : BaseTransformer +{ + public override void Transform(CustomerResult queryResult, Customer entity) + { + entity.CustomerId = queryResult.Id; + entity.Name = queryResult.Name; + } +} + +public class OrdersTransform : BaseTransformer, Customer> +{ + public override void Transform(CollectionResult queryResult, Customer entity) + { + entity.Orders = queryResult?.Select(o => new Order + { + OrderId = o.OrderId, + OrderNumber = o.OrderNumber, + OrderDate = o.OrderDate + }).ToArray() ?? Array.Empty(); + } +} +``` + +### Schema Configuration + +The schema defines the full query-transform-path hierarchy: + +```csharp +public class CustomerConfiguration : EntityConfiguration +{ + public override IEnumerable> GetSchema() + { + return CreateSchema.For() + .Map(For.Paths("customer"), + customer => customer.Dependents + .Map( + For.Paths("customer/communication")) + .Map(For.Paths("customer/orders"), + orders => orders.Dependents + .Map( + For.Paths("customer/orders/order/items")))) + .End(); + } +} +``` + +Schema paths correspond to the entity's object graph: +- `customer` — root properties +- `customer/communication` — Customer.Communication +- `customer/orders` — Customer.Orders collection +- `customer/orders/order/items` — Order.Items for each order + +--- + +## Packages + +### DataFuse.Integration + +Core package with orchestration, DI extensions, helpers, and path matchers. + +```bash +dotnet add package DataFuse.Integration +``` + +**Key components:** `DataProvider`, `QueryBuilder`, `EntityBuilder`, `DataFuseOptionsBuilder`, XPath/JSONPath matchers, DI registration via `UseDataFuse()`. + +### DataFuse.Adapters.Abstraction + +Interfaces and base classes: `IEntity`, `IQuery`, `IQueryResult`, `ITransformer`, `IQueryEngine`, `BaseQuery`, `BaseTransformer`, `EntityConfiguration`, `CreateSchema`, `CollectionResult`, `CacheResultAttribute`. + +### DataFuse.Adapters.SQL + +SQL database support using Dapper. + +```bash +dotnet add package DataFuse.Adapters.SQL +``` + +Supports SQL Server, SQLite, MySQL, PostgreSQL, Oracle. + +### DataFuse.Adapters.EntityFramework + +Entity Framework Core integration. + +```bash +dotnet add package DataFuse.Adapters.EntityFramework +``` + +Full LINQ support, DbContext factory integration. + +### DataFuse.Adapters.MongoDB + +MongoDB support using the official MongoDB driver. + +```bash +dotnet add package DataFuse.Adapters.MongoDB +``` + +Full `IMongoDatabase` access, LINQ and filter builder support. + +### DataFuse.Adapters.WebAPI + +REST API support using HttpClient. + +```bash +dotnet add package DataFuse.Adapters.WebAPI +``` + +Request/response header management, JSON deserialization. + +### Compatibility + +| Package | Target Frameworks | +|---|---| +| DataFuse.Adapters.Abstraction | netstandard2.1, net8.0, net9.0, net10.0 | +| DataFuse.Integration | netstandard2.1, net8.0, net9.0, net10.0 | +| DataFuse.Adapters.SQL | netstandard2.1, net8.0, net9.0, net10.0 | +| DataFuse.Adapters.EntityFramework | net10.0 | +| DataFuse.Adapters.MongoDB | netstandard2.1, net8.0, net9.0, net10.0 | +| DataFuse.Adapters.WebAPI | netstandard2.1, net8.0, net9.0, net10.0 | + +--- + +## Query Implementation Guide + +### SQL Queries (Dapper) + +```csharp +// Single result +public class CustomerQuery : SQLQuery +{ + protected override Func> GetQuery( + IDataContext context, IQueryResult parentQueryResult) + { + var request = (CustomerRequest)context.Request; + return connection => connection.QueryFirstOrDefaultAsync( + @"SELECT CustomerId as Id, CustomerName as Name, CustomerCode as Code + FROM Customers WHERE CustomerId = @CustomerId", + new { CustomerId = request.CustomerId }); + } +} + +// Collection result +public class OrdersQuery : SQLQuery> +{ + protected override Func>> GetQuery( + IDataContext context, IQueryResult parentQueryResult) + { + var customer = (CustomerResult)parentQueryResult; + return async connection => + { + var orders = await connection.QueryAsync( + "SELECT OrderId, OrderNumber, OrderDate FROM Orders WHERE CustomerId = @Id", + new { Id = customer.Id }); + return new CollectionResult(orders); + }; + } +} +``` + +### Entity Framework Queries + +```csharp +public class CustomerQuery : SQLQuery +{ + protected override Func> GetQuery( + IDataContext context, IQueryResult parentQueryResult) + { + var request = (CustomerRequest)context.Request; + return async dbContext => + { + return await dbContext.Set() + .Where(c => c.CustomerId == request.CustomerId) + .Select(c => new CustomerResult + { + Id = c.CustomerId, + Name = c.Name, + Code = c.Code + }) + .FirstOrDefaultAsync(); + }; + } +} +``` + +### MongoDB Queries + +```csharp +// Single document +public class CustomerQuery : MongoQuery +{ + protected override Func> GetQuery( + IDataContext context, IQueryResult? parentQueryResult) + { + var request = (CustomerRequest)context.Request; + return async database => + { + var collection = database.GetCollection("customers"); + return await collection.Find(c => c.Id == request.CustomerId).FirstOrDefaultAsync(); + }; + } +} + +// Collection query with filter +public class OrdersQuery : MongoQuery> +{ + protected override Func>> GetQuery( + IDataContext context, IQueryResult? parentQueryResult) + { + var customer = (CustomerResult)parentQueryResult; + return async database => + { + var collection = database.GetCollection("orders"); + var filter = Builders.Filter.Eq(o => o.CustomerId, customer.Id); + var orders = await collection.Find(filter).SortByDescending(o => o.OrderDate).ToListAsync(); + return new CollectionResult(orders); + }; + } +} + +// Aggregation pipeline +public class SalesSummaryQuery : MongoQuery +{ + protected override Func> GetQuery( + IDataContext context, IQueryResult? parentQueryResult) + { + var customer = (CustomerResult)parentQueryResult; + return async database => + { + var collection = database.GetCollection("orders"); + var result = await collection.Aggregate() + .Match(Builders.Filter.Eq("customerId", customer.Id)) + .Group(new BsonDocument + { + { "_id", "$customerId" }, + { "totalSpent", new BsonDocument("$sum", "$amount") }, + { "orderCount", new BsonDocument("$sum", 1) } + }) + .FirstOrDefaultAsync(); + + return new SalesSummaryResult + { + TotalSpent = result?["totalSpent"].ToDecimal() ?? 0, + OrderCount = result?["orderCount"].ToInt32() ?? 0 + }; + }; + } +} +``` + +### Web API Queries + +```csharp +// Basic API query +public class PricingApiQuery : WebQuery +{ + public PricingApiQuery() : base("https://api.pricing.com/") { } + + protected override Func GetQuery(IDataContext context, IQueryResult parentQueryResult) + { + var product = (ProductResult)parentQueryResult; + return () => new Uri($"products/{product.Id}/pricing", UriKind.Relative); + } +} + +// API query with custom headers +public class AuthenticatedApiQuery : WebQuery +{ + public AuthenticatedApiQuery() : base("https://api.secure.com/") { } + + protected override Func GetQuery(IDataContext context, IQueryResult parentQueryResult) + { + var request = (SecureRequest)context.Request; + return () => new Uri($"secure/data/{request.Id}", UriKind.Relative); + } + + protected override IDictionary GetRequestHeaders() + { + return new Dictionary + { + { "Authorization", "Bearer " + GetAccessToken() }, + { "Accept", "application/json" } + }; + } + + protected override IEnumerable GetResponseHeaders() + { + return new[] { "X-Rate-Limit-Remaining", "X-Request-Id" }; + } +} +``` + +### Query Result Types + +```csharp +// Simple result +public class CustomerResult : IQueryResult +{ + public int Id { get; set; } + public string Name { get; set; } + public string Code { get; set; } +} + +// Collection result +public class CollectionResult : List, IQueryResult +{ + public CollectionResult(IEnumerable items) : base(items) { } + public CollectionResult() { } +} + +// Cached result — cached automatically across queries +[CacheResult] +public class CategoryResult : IQueryResult +{ + public int Id { get; set; } + public string Name { get; set; } +} + +// Web result with response headers +public class UserApiResult : WebHeaderResult +{ + public int Id { get; set; } + public string Name { get; set; } +} +``` + +--- + +## Transformer Guide + +### Basic Transformers + +```csharp +public class CustomerTransform : BaseTransformer +{ + public override void Transform(CustomerResult queryResult, Customer entity) + { + if (queryResult == null) return; + + entity.CustomerId = queryResult.Id; + entity.CustomerName = queryResult.Name ?? string.Empty; + entity.CustomerCode = queryResult.Code; + } +} +``` + +### Collection Transformers + +```csharp +public class OrdersTransform : BaseTransformer, Customer> +{ + public override void Transform(CollectionResult queryResult, Customer entity) + { + entity.Orders = queryResult?.Select(o => new Order + { + OrderId = o.OrderId, + OrderNumber = o.OrderNumber, + OrderDate = o.OrderDate, + TotalAmount = o.TotalAmount + }).ToArray() ?? Array.Empty(); + } +} +``` + +### Context-Aware Transformers + +Transformers can access the request context for conditional logic: + +```csharp +public class ProductTransform : BaseTransformer +{ + public override void Transform(ProductResult queryResult, Product entity) + { + entity.ProductId = queryResult.Id; + entity.Name = queryResult.Name; + entity.Price = queryResult.Price; + + var request = Context.Request as ProductRequest; + if (request?.IncludeMetadata == true) + { + entity.CreatedDate = queryResult.CreatedDate; + entity.LastModified = queryResult.LastModified; + } + } +} +``` + +### Grandchild Transformers + +For deeply nested data, transformers map grandchild results back to the correct parent: + +```csharp +public class OrderItemsTransform : BaseTransformer, Customer> +{ + public override void Transform(CollectionResult queryResult, Customer entity) + { + if (queryResult == null || !queryResult.Any()) return; + + var itemsByOrder = queryResult.GroupBy(item => item.OrderId); + foreach (var group in itemsByOrder) + { + var order = entity.Orders.FirstOrDefault(o => o.OrderId == group.Key); + if (order != null) + { + order.Items = group.Select(item => new OrderItem + { + ItemId = item.ItemId, + ProductName = item.ProductName, + Quantity = item.Quantity, + Price = item.Price + }).ToArray(); + } + } + } +} +``` + +--- + +## Advanced Features + +### Caching + +Mark query results with `[CacheResult]` to cache them for use in other queries and transformers: + +```csharp +[CacheResult] +public class CategoryResult : IQueryResult +{ + public int Id { get; set; } + public string Name { get; set; } +} + +// Access cached results in a transformer +public class ProductTransform : BaseTransformer +{ + public override void Transform(ProductResult queryResult, Product entity) + { + entity.ProductId = queryResult.Id; + + if (Context.Cache.TryGetValue("CategoryResult", out var cached)) + { + entity.CategoryName = ((CategoryResult)cached).Name; + } + } +} +``` + +Caching is useful when a grandchild query needs data from a grandparent query that isn't its direct parent. + +### Selective Loading + +Control which parts of the object graph to load via `SchemaPaths`: + +```csharp +// Load everything +var fullCustomer = dataProvider.GetData(new CustomerRequest +{ + CustomerId = 123 + // SchemaPaths = null loads all configured paths +}); + +// Load only customer + orders (skip communication, address, etc.) +var withOrders = dataProvider.GetData(new CustomerRequest +{ + CustomerId = 123, + SchemaPaths = new[] { "customer", "customer/orders" } +}); + +// Load just the basic profile +var basicProfile = dataProvider.GetData(new CustomerRequest +{ + CustomerId = 123, + SchemaPaths = new[] { "customer" } +}); +``` + +This avoids unnecessary queries — if a consumer only needs the customer name, DataFuse won't execute order or communication queries. + +### Parallel Execution + +Sibling queries at the same level execute in parallel automatically: + +```csharp +return CreateSchema.For() + .Map(For.Paths("customer"), + customer => customer.Dependents + .Map(For.Paths("customer/contact")) // Parallel + .Map(For.Paths("customer/preferences")) // Parallel + .Map(For.Paths("customer/address"))) // Parallel + .End(); +``` + +No `Task.WhenAll` boilerplate needed. DataFuse manages execution order based on the dependency tree. + +### Conditional Query Execution + +Child queries can skip execution based on parent data: + +```csharp +public class PremiumServicesQuery : WebQuery +{ + protected override Func GetQuery(IDataContext context, IQueryResult parentQueryResult) + { + var customer = (CustomerResult)parentQueryResult; + + if (customer.CustomerType != "Premium") + return null; // Returning null skips this query + + return () => new Uri($"premium-services/{customer.Id}", UriKind.Relative); + } +} +``` + +### Cross-Source Dependencies + +Mix data sources within the same entity. A root query can be SQL while its children pull from MongoDB and REST APIs: + +```csharp +return CreateSchema.For() + .Map(For.Paths("customer"), // SQL + customer => customer.Dependents + .Map(For.Paths("customer/billing")) // REST API + .Map(For.Paths("customer/reviews")) // MongoDB + .Map(For.Paths("customer/analytics"))) // EF Core + .End(); +``` + +### Custom Query Engines + +Add support for any data source by implementing `IQueryEngine`: + +```csharp +public class RedisQueryEngine : IQueryEngine +{ + private readonly IConnectionMultiplexer _redis; + + public RedisQueryEngine(IConnectionMultiplexer redis) + { + _redis = redis; + } + + public bool CanExecute(IQuery query) => query is IRedisQuery; + + public async Task Execute(IQuery query) + { + var redisQuery = (IRedisQuery)query; + return await redisQuery.Run(_redis.GetDatabase()); + } +} + +// Register alongside built-in adapters +services.UseDataFuse() + .WithEngine(c => new RedisQueryEngine(redisConnection)) + .WithEngine(c => new QueryEngine(sqlConfiguration)) + .WithEngine(c => new DataFuse.Adapters.MongoDB.QueryEngine(mongoDatabase)) + // ... +``` + +### Path Matching + +DataFuse supports XPath and JSONPath patterns for schema paths: + +**XPath patterns:** +- `customer` — exact match +- `customer/orders` — nested path +- `customer/orders/order/items` — deep nesting + +**JSONPath patterns:** +- `$.customer` — root level +- `$.customer.orders` — nested property +- `$.customer.orders[*].items` — array elements + +Register your preferred matcher: +```csharp +services.UseDataFuse() + .WithPathMatcher(c => new XPathMatcher()) // or JPathMatcher +``` + +--- + +## Architecture + +### Execution Flow + +``` +Client Request + | + v +DataProvider + | + v +QueryBuilder — filters by SchemaPaths, resolves dependencies, builds execution plan + | + v +QueryExecutor — executes level-by-level, parallelizes siblings, propagates results + | + v +EntityBuilder — applies transformers in dependency order, assembles final entity + | + v +Fully Hydrated Entity +``` + +### Component Responsibilities + +| Component | Responsibility | +|---|---| +| `DataProvider` | Main orchestrator — coordinates the full pipeline | +| `QueryBuilder` | Filters queries by requested schema paths, resolves parent-child dependencies | +| `QueryExecutor` | Executes queries level-by-level with parallel processing of siblings | +| `EntityBuilder` | Matches results to transformers and builds the final entity | +| `IQueryEngine` | Adapter interface — each data source implements this | +| `EntityConfiguration` | Declares the schema: query-transform-path mappings | + +### Configuration Patterns + +**Linear** — flat entity: +```csharp +return CreateSchema.For() + .Map(For.Paths("customer")) + .Map(For.Paths("customer/address")) + .End(); +``` + +**Branching** — multiple children: +```csharp +return CreateSchema.For() + .Map(For.Paths("customer"), + c => c.Dependents + .Map(For.Paths("customer/contact")) + .Map(For.Paths("customer/preferences"))) + .End(); +``` + +**Deep nesting** — up to 5 levels: +```csharp +return CreateSchema.For() + .Map(For.Paths("customer"), + c => c.Dependents + .Map(For.Paths("customer/orders"), + o => o.Dependents + .Map(For.Paths("customer/orders/order/items")))) + .End(); +``` + +--- + +## Best Practices + +### Query Design +- Keep queries focused — one query per data concern +- Use `CollectionResult` for queries returning multiple records +- Return `null` from `GetQuery` to conditionally skip execution +- Use `[CacheResult]` for expensive, reusable query results + +### Transformer Design +- Always handle null query results gracefully +- Initialize collections to prevent null reference exceptions +- Keep transformers focused on mapping — avoid business logic +- Use `Context.Request` for conditional transformation + +### Schema Design +- Use meaningful, hierarchical path names that mirror your entity structure +- Keep nesting depth reasonable (3-4 levels is typical) +- Use selective loading for performance-sensitive endpoints + +### DI Registration +```csharp +services.UseDataFuse() + .WithEngine(c => new QueryEngine(sqlConfig)) // SQL + .WithEngine() // REST API + .WithEngine(c => new DataFuse.Adapters.MongoDB.QueryEngine(mongoDatabase)) // MongoDB + .WithPathMatcher(c => new XPathMatcher()) + .WithEntityConfiguration(c => new CustomerConfiguration()) + .WithEntityConfiguration(c => new ProductConfiguration()); + +services.AddLogging(); +services.AddHttpClient(); +``` + +--- + +## Migrating from Schemio (v2.x) to DataFuse (v3.0.0) + +DataFuse v3.0.0 is a complete rebrand of the Schemio framework. All package names, namespaces, and registration APIs have changed. Follow this guide to upgrade your project. + +### Step 1: Update NuGet Packages + +Remove the old Schemio packages and install the new DataFuse equivalents: + +| Old Package (Remove) | New Package (Install) | +|----------------------------|--------------------------------------| +| `Schemio.Core` | `DataFuse.Adapters.Abstraction` + `DataFuse.Integration` | +| `Schemio.SQL` | `DataFuse.Adapters.SQL` | +| `Schemio.EntityFramework` | `DataFuse.Adapters.EntityFramework` | +| `Schemio.API` | `DataFuse.Adapters.WebAPI` | + +```bash +# Remove old packages +dotnet remove package Schemio.Core +dotnet remove package Schemio.SQL +dotnet remove package Schemio.EntityFramework +dotnet remove package Schemio.API + +# Install new packages +dotnet add package DataFuse.Integration +dotnet add package DataFuse.Adapters.SQL +dotnet add package DataFuse.Adapters.EntityFramework +dotnet add package DataFuse.Adapters.WebAPI +dotnet add package DataFuse.Adapters.MongoDB # New in v3.0.0 +``` + +### Step 2: Update Namespaces + +Find and replace `using` statements across your codebase: + +| Old Namespace | New Namespace | +|----------------------------|--------------------------------------| +| `using Schemio;` | `using DataFuse.Adapters.Abstraction;` | +| `using Schemio.SQL;` | `using DataFuse.Adapters.SQL;` | +| `using Schemio.EntityFramework;` | `using DataFuse.Adapters.EntityFramework;` | +| `using Schemio.API;` | `using DataFuse.Adapters.WebAPI;` | + +### Step 3: Update DI Registration + +The service registration method and options builder have been renamed: + +```csharp +// Before (Schemio v2.x) +services.UseSchemio(new SchemioOptionsBuilder() + .WithEngine(c => new QueryEngine(sqlConfig)) + .WithPathMatcher(c => new XPathMatcher()) + .WithEntityConfiguration(c => new CustomerConfiguration())); + +// After (DataFuse v3.0.0) +services.UseDataFuse(new DataFuseOptionsBuilder() + .WithEngine(c => new QueryEngine(sqlConfig)) + .WithPathMatcher(c => new XPathMatcher()) + .WithEntityConfiguration(c => new CustomerConfiguration())); +``` + +**Changed APIs:** + +| Old (Schemio) | New (DataFuse) | +|----------------------------|--------------------------------------| +| `UseSchemio()` | `UseDataFuse()` | +| `SchemioOptionsBuilder` | `DataFuseOptionsBuilder` | +| `ISchemioOptions` | `IDataFuseOptions` | + +### Step 4: Verify + +No changes are required to your queries, transformers, entity configurations, or schema definitions — only the package references, namespaces, and DI registration need updating. After making these changes: + +1. Build the solution and resolve any remaining namespace errors +2. Run your existing tests to confirm behavior is unchanged +3. Verify DI registration at startup + +### New in v3.0.0 + +After migrating, you can take advantage of new features: + +- **MongoDB Adapter** — aggregate data from MongoDB collections with `DataFuse.Adapters.MongoDB` +- **Transform Hooks** — use `ITransformerHooks` with `PreTransformContext` and `PostTransformContext` for pipeline control +- **Query Result Caching** — mark results with `[CacheResult]` for automatic caching +- **Multi-Target Framework Support** — packages now support `netstandard2.1`, `net8.0`, `net9.0`, and `net10.0` + +See the [Release Notes](RELEASE_NOTES.md) for full details. + +--- + +## Getting Support + +- **GitHub**: [github.com/CodeShayk/DataFuse.Net](https://github.com/CodeShayk/DataFuse.Net) +- **Issues**: [Report bugs and feature requests](https://github.com/CodeShayk/DataFuse.Net/issues) +- **Wiki**: [Developer guide and documentation](https://github.com/CodeShayk/DataFuse.Net/wiki) +- **Samples**: Check example projects in the repository for real-world usage patterns diff --git a/index.html b/index.html new file mode 100644 index 0000000..3527bec --- /dev/null +++ b/index.html @@ -0,0 +1,956 @@ + + + + + + DataFuse Framework - Declarative Multi-Source Data Aggregation for .NET + + + + +
+ + +
+

DataFuse Framework

+
Like GraphQL, but for your heterogeneous backend systems
+
A declarative .NET framework that aggregates data from SQL databases, REST APIs, MongoDB, and Entity Framework into unified, strongly-typed objects — replacing hundreds of lines of manual orchestration code.
+
+ .NET 9.0 + NuGet + MIT License + Multi-Source + Type-Safe +
+
+ +
+ + +

Real-World Use Cases

+ +

DataFuse shines when a single response needs data from multiple backend systems. Here are scenarios where it eliminates boilerplate and adds automatic parallelism.

+ +
+

1. E-Commerce Product Page

+
+ SQL Server — Inventory + REST API — Pricing + MongoDB — Reviews +
+

A product page combines inventory data from your SQL database, live pricing from a pricing microservice, and customer reviews from MongoDB. DataFuse fetches the product from SQL first, then runs pricing and MongoDB queries in parallel.

+
public class ProductPageConfig : EntityConfiguration<ProductPage>
+{
+    public override IEnumerable<Mapping<ProductPage, IQueryResult>> GetSchema()
+    {
+        return CreateSchema.For<ProductPage>()
+            .Map<ProductQuery, ProductTransform>(For.Paths("product"),
+                p => p.Dependents
+                    .Map<PricingApiQuery, PricingTransform>(For.Paths("product/pricing"))
+                    .Map<ReviewsMongoQuery, ReviewsTransform>(For.Paths("product/reviews")))
+            .End();
+    }
+}
+
+ +
+

2. Customer 360 Dashboard

+
+ CRM Database + Billing REST API + Support Ticket API + Orders Database +
+

Build a unified customer view from CRM data, billing history, support tickets, and order details — each from a different system. Use selective loading to fetch only what the current view needs.

+
// Full customer 360
+var customer = dataProvider.GetData(new CustomerRequest { CustomerId = 123 });
+
+// Just profile + billing (skip tickets and orders)
+var billing = dataProvider.GetData(new CustomerRequest
+{
+    CustomerId = 123,
+    SchemaPaths = new[] { "customer", "customer/billing" }
+});
+
+ +
+

3. Multi-Service Reporting

+
+ Sales Database + Inventory API + Shipping API + Revenue Database +
+

Aggregate metrics from multiple microservices into a single report. Each data source is encapsulated in its own query, testable independently, and executed with automatic dependency management.

+
+ +
+ + +

Why DataFuse?

+ +

The Problem

+

In modern architectures, a single page or API response often needs data from multiple backend systems. The standard approach is manual orchestration code:

+ +
// Without DataFuse: 50+ lines per data combination, repeated everywhere
+var customer = await GetCustomerFromDatabase(customerId);
+var orders = await GetOrdersFromAPI(customerId);           // Sequential!
+var billing = await GetBillingFromService(customerId);     // Sequential!
+var tickets = await GetTicketsFromAPI(customerId);          // Sequential!
+
+customer.Orders = orders;
+customer.Billing = billing;
+customer.Tickets = tickets;
+ +
+ Problems with manual orchestration: +
    +
  • Boilerplate explosion — every new data combination means 50+ lines of fetch-assemble code
  • +
  • No parallelism — developers write sequential calls unless they manually add Task.WhenAll
  • +
  • Tight coupling — changing a data source means rewriting orchestration
  • +
  • No selective loading — you fetch everything even when consumers only need a subset
  • +
  • Inconsistent patterns — every developer solves the same problem differently
  • +
+
+ + VS + +
// With DataFuse: declare once, use everywhere
+var customer = dataProvider.GetData(new CustomerRequest { CustomerId = 123 });
+ +
+ DataFuse gives you: +
    +
  • Declarative schema — define data relationships once, reuse everywhere
  • +
  • Automatic parallelism — sibling queries run in parallel, no Task.WhenAll needed
  • +
  • Selective loading — fetch only the data paths the consumer needs
  • +
  • Dependency management — parent results flow to child queries automatically
  • +
  • Type safety — strongly-typed queries, results, and transformers
  • +
  • Extensible adapters — add any data source by implementing IQueryEngine
  • +
+
+ +

Comparison with Alternatives

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
CapabilityManual CodeGraphQLMediatRDataFuse
Multi-source aggregationManual wiringResolver-basedManual wiringDeclarative schema
Parallel executionManual Task.WhenAllPer-resolverManualAutomatic
Selective loadingManual if/elseBuilt-inManualSchema path filtering
Dependency managementManual orderingImplicitManualParent-child hierarchy
Type safetyVariesSchema-basedYesStrongly typed
Learning curveNoneHigh (new language)LowLow (C# only)
Backend-only (no client changes)YesNoYesYes
+ +
+ When to use DataFuse: Your data lives across different systems (SQL + APIs + external services) and you want declarative orchestration with automatic parallelism. When NOT to use it: All your data is in a single database and simple SQL joins suffice. +
+ +
+ + +

Quick Start (5 Minutes)

+ +

1. Install Packages

+
+ dotnet add package DataFuse.Integration
+ dotnet add package DataFuse.Adapters.SQL — For SQL with Dapper
+ dotnet add package DataFuse.Adapters.WebAPI — For REST APIs
+ dotnet add package DataFuse.Adapters.MongoDB — For MongoDB
+ dotnet add package DataFuse.Adapters.EntityFramework — For EF Core +
+ +

2. Define Your Entity

+
public class Product : IEntity
+{
+    public int ProductId { get; set; }
+    public string Name { get; set; }
+    public decimal Price { get; set; }
+    public Category Category { get; set; }
+    public Review[] Reviews { get; set; }
+}
+ +

3. Create a Query

+
public class ProductQuery : SQLQuery<ProductResult>
+{
+    protected override Func<IDbConnection, Task<ProductResult>> GetQuery(
+        IDataContext context, IQueryResult parentQueryResult)
+    {
+        var request = (ProductRequest)context.Request;
+        return connection => connection.QueryFirstOrDefaultAsync<ProductResult>(
+            "SELECT ProductId as Id, Name, Price FROM Products WHERE ProductId = @Id",
+            new { Id = request.ProductId });
+    }
+}
+
+public class ReviewsMongoQuery : MongoQuery<CollectionResult<ReviewResult>>
+{
+    protected override Func<IMongoDatabase, Task<CollectionResult<ReviewResult>>> GetQuery(
+        IDataContext context, IQueryResult? parentQueryResult)
+    {
+        var product = (ProductResult)parentQueryResult;
+        return async database =>
+        {
+            var collection = database.GetCollection<ReviewResult>("reviews");
+            var reviews = await collection.Find(r => r.ProductId == product.Id).ToListAsync();
+            return new CollectionResult<ReviewResult>(reviews);
+        };
+    }
+}
+ +

4. Create Transformers

+
public class ProductTransform : BaseTransformer<ProductResult, Product>
+{
+    public override void Transform(ProductResult queryResult, Product entity)
+    {
+        entity.ProductId = queryResult.Id;
+        entity.Name = queryResult.Name;
+        entity.Price = queryResult.Price;
+    }
+}
+
+public class ReviewsTransform : BaseTransformer<CollectionResult<ReviewResult>, Product>
+{
+    public override void Transform(CollectionResult<ReviewResult> queryResult, Product entity)
+    {
+        entity.Reviews = queryResult?.Select(r => new Review
+        {
+            ReviewId = r.Id, Comment = r.Comment, Rating = r.Rating
+        }).ToArray() ?? Array.Empty<Review>();
+    }
+}
+ +

5. Configure Schema

+
public class ProductConfiguration : EntityConfiguration<Product>
+{
+    public override IEnumerable<Mapping<Product, IQueryResult>> GetSchema()
+    {
+        return CreateSchema.For<Product>()
+            .Map<ProductQuery, ProductTransform>(For.Paths("product"),
+                product => product.Dependents
+                    .Map<CategoryQuery, CategoryTransform>(For.Paths("product/category"))
+                    .Map<ReviewsMongoQuery, ReviewsTransform>(For.Paths("product/reviews")))
+            .End();
+    }
+}
+ +

6. Register & Use

+
// Startup / DI registration
+services.UseDataFuse()
+    .WithEngine(c => new QueryEngine(sqlConfiguration))                          // SQL
+    .WithEngine<DataFuse.Adapters.WebAPI.QueryEngine>()                          // REST API
+    .WithEngine(c => new DataFuse.Adapters.MongoDB.QueryEngine(mongoDatabase))     // MongoDB
+    .WithPathMatcher(c => new XPathMatcher())
+    .WithEntityConfiguration<Product>(c => new ProductConfiguration());
+
+services.AddHttpClient();
+
+// Usage
+public class ProductService
+{
+    private readonly IDataProvider<Product> _dataProvider;
+
+    public ProductService(IDataProvider<Product> dataProvider)
+        => _dataProvider = dataProvider;
+
+    public Product GetProduct(int productId)
+        => _dataProvider.GetData(new ProductRequest { ProductId = productId });
+
+    public Product GetProductWithReviews(int productId)
+        => _dataProvider.GetData(new ProductRequest
+        {
+            ProductId = productId,
+            SchemaPaths = new[] { "product", "product/reviews" }
+        });
+}
+ +
+ + +

Core Concepts

+ +

Entities

+

Entities implement IEntity and define the complete object graph. Each property can be hydrated from a different data source:

+
public class Customer : IEntity
+{
+    public int CustomerId { get; set; }
+    public string Name { get; set; }
+    public Communication Communication { get; set; }  // From REST API
+    public Address Address { get; set; }                // From SQL
+    public Order[] Orders { get; set; }                 // From EF Core
+}
+ +

Queries & Parent-Child Dependencies

+

Child queries receive their parent's result, enabling dependent fetching across data sources:

+
// Root: fetches from SQL
+public class CustomerQuery : SQLQuery<CustomerResult> { /* uses request context */ }
+
+// Child: uses parent CustomerResult to call API
+public class OrdersApiQuery : WebQuery<CollectionResult<OrderResult>>
+{
+    protected override Func<Uri> GetQuery(IDataContext context, IQueryResult parentQueryResult)
+    {
+        var customer = (CustomerResult)parentQueryResult;
+        return () => new Uri($"orders?customerId={customer.Id}", UriKind.Relative);
+    }
+}
+ +

Execution Flow

+
+
1. Root Query executes first
+   |
+   v  Result passed to children
+
+2. Child Queries execute IN PARALLEL
+   - OrdersQuery, CommunicationQuery, AddressQuery
+   |
+   v  Results passed to grandchildren
+
+3. Grandchild Queries execute
+   - OrderItemsQuery (uses OrderId from parent)
+
+ +

Transformers

+

Transformers map query results onto the entity. Each query-transformer pair handles one piece of the object graph:

+
public class OrdersTransform : BaseTransformer<CollectionResult<OrderResult>, Customer>
+{
+    public override void Transform(CollectionResult<OrderResult> queryResult, Customer entity)
+    {
+        entity.Orders = queryResult?.Select(o => new Order
+        {
+            OrderId = o.OrderId,
+            OrderNumber = o.OrderNumber,
+            OrderDate = o.OrderDate
+        }).ToArray() ?? Array.Empty<Order>();
+    }
+}
+ +

Schema Configuration

+

The schema declares the full hierarchy of queries, transformers, and path mappings:

+
return CreateSchema.For<Customer>()
+    .Map<CustomerQuery, CustomerTransform>(For.Paths("customer"),
+        c => c.Dependents
+            .Map<CommunicationQuery, CommTransform>(For.Paths("customer/communication"))
+            .Map<OrdersQuery, OrdersTransform>(For.Paths("customer/orders"),
+                o => o.Dependents
+                    .Map<ItemsQuery, ItemsTransform>(For.Paths("customer/orders/order/items"))))
+    .End();
+ +
+ + +

Packages

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PackagePurpose.NET.NET Standard.NET Framework
DataFuse.IntegrationCore orchestration, DI, helpers, path matchers9.0+2.0, 2.14.6.2+
DataFuse.Adapters.AbstractionInterfaces & base classes (IEntity, IQuery, BaseTransformer)9.0+2.0, 2.14.6.2+
DataFuse.Adapters.SQLSQL via Dapper (SQL Server, SQLite, MySQL, PostgreSQL)9.0+2.14.6.2+
DataFuse.Adapters.EntityFrameworkEntity Framework Core with full LINQ support9.0+--
DataFuse.Adapters.MongoDBMongoDB via official driver9.0+--
DataFuse.Adapters.WebAPIREST APIs via HttpClient with header management9.0+2.0, 2.14.6.2+
+ +
+ + +

Advanced Features

+ +

Selective Loading

+

Fetch only the parts of the object graph you need via SchemaPaths:

+
// Load everything
+var full = dataProvider.GetData(new CustomerRequest { CustomerId = 123 });
+
+// Load only customer + orders
+var partial = dataProvider.GetData(new CustomerRequest
+{
+    CustomerId = 123,
+    SchemaPaths = new[] { "customer", "customer/orders" }
+});
+ +

Caching

+

Mark query results with [CacheResult] to make them available across queries and transformers:

+
[CacheResult]
+public class CategoryResult : IQueryResult
+{
+    public int Id { get; set; }
+    public string Name { get; set; }
+}
+
+// Access in a transformer
+if (Context.Cache.TryGetValue("CategoryResult", out var cached))
+{
+    entity.CategoryName = ((CategoryResult)cached).Name;
+}
+ +

Custom Data Source Adapters

+

Add support for any data source by implementing IQueryEngine:

+
public class RedisQueryEngine : IQueryEngine
+{
+    private readonly IConnectionMultiplexer _redis;
+    public RedisQueryEngine(IConnectionMultiplexer redis) => _redis = redis;
+
+    public bool CanExecute(IQuery query) => query is IRedisQuery;
+
+    public async Task<IQueryResult> Execute(IQuery query)
+    {
+        var redisQuery = (IRedisQuery)query;
+        return await redisQuery.Run(_redis.GetDatabase());
+    }
+}
+
+// Register alongside built-in adapters
+services.UseDataFuse()
+    .WithEngine(c => new RedisQueryEngine(redisConnection))
+    .WithEngine(c => new QueryEngine(sqlConfig))
+    .WithEngine(c => new DataFuse.Adapters.MongoDB.QueryEngine(mongoDb));
+ +

Conditional Query Execution

+

Skip queries based on parent data by returning null:

+
protected override Func<Uri> GetQuery(IDataContext context, IQueryResult parentQueryResult)
+{
+    var customer = (CustomerResult)parentQueryResult;
+    if (customer.CustomerType != "Premium")
+        return null; // Query is skipped
+
+    return () => new Uri($"premium/{customer.Id}", UriKind.Relative);
+}
+ +

Path Matching

+

DataFuse supports XPath and JSONPath patterns for schema paths:

+ + + + + + + + + +
XPathJSONPathMatches
customer$.customerRoot entity
customer/orders$.customer.ordersNested property
customer/orders/order/items$.customer.orders[*].itemsDeep nesting
+ +
+ +
+

DataFuse Framework

+

Declarative multi-source data aggregation for .NET

+ +
+
+ + diff --git a/index.md b/index.md deleted file mode 100644 index 3d66219..0000000 --- a/index.md +++ /dev/null @@ -1,614 +0,0 @@ -# Developer Guide - -## i. Installation - -`Schemio` allows you to aggregate data from heterogeneous data stores offering `SQL` & `API` packages out of the box below. SQL queries are supported by `Dapper` and `EntityFramework` engines. You could also `extend` Schemio to provide your own implementation(s) of `Query` and supporting `Query Engine` to retrieve data from `custom` data store(s). - -Below are the Nuget packages available. - -`Scemio.Core` - Install to extend schemio to implement `custom` querying engine. -``` -NuGet\Install-Package Schemio.Core -``` -`Schemio.SQL` - Install when you would like to include SQL `Dapper` queries to access SQL database. -``` -NuGet\Install-Package Schemio.SQL -``` -`Schemio.EntityFramework` - Install when you would like to include SQL `EntityFramework` queries to access SQL database. -``` -NuGet\Install-Package Schemio.EntityFramework -``` -`Schemio.Api` - Install when you would like to include web api queries with `HttpClient` query engine. -``` -NuGet\Install-Package Schemio.Api -``` -## ii. Implementation: Using Schemio - -To use **Schemio** you need to do the below steps -- **Step 1**: Define the aggregated `Entity`. -- **Step 2**: Setup the aggregate `Configuration` comprising of `Query`/`Transformer` hierarchical nested mappings. -- **Step 3**: Construct the `DataProvider` with required dependencies. - -### Step 1. Define Aggregate Entity. -To create an aggregate `Entity`, implement the class from `IEntity` interface. This is the entity that will be returned as aggregated result from multiple queries assembled to execute against homogeneous or heterogeneous data storage's. - -Below is an example `Customer` entity. - -``` -public class Customer : IEntity -{ - public int CustomerId { get; set; } - public string CustomerCode { get; set; } - public string CustomerName { get; set; } - public Communication Communication { get; set; } - public Order[] Orders { get; set; } -} -``` - -For the customer class, we can see there are three levels of nesting in the object graph. -- Level 1 with paths: `Customer` -- Level 2 with paths: `Customer.Communication` and `Customer.Orders` -- Level 3 with paths: `Customer.Orders.Items` - -If we choose XML Schema Definition (XSD) for the Customer entity then XPaths for nesting levels should be. -``` -- Level 1 with XPath: Customer -- Level 2 with XPaths: Customer/Communication and Customer/Orders -- Level 3 with XPath: Customer/Orders/Order/Items/Item -``` -### Step 2: Setup Entity Aggregate Configuration -To define `Entity Configuration`, derive from `EntityConfiguration` class where `TEntity` is aggregate entity in context (ie. `IEntity`). - -The `Entity Configuration` is basically `hierarchies` of `Query` & `Transformer` pairs mapped to the schema `paths` pointing to various `nesting` levels in the entity's object graph. -* `Query` is an implementation to `fetch` data for mapped sections of object graph. -* `Transformer` is an implementation to `map` data fetched by the associated query to the relevant sections of the entity's object graph. - -Below is an example Entity Configuration for the Customer Entity. - -``` -internal class CustomerConfiguration : EntityConfiguration -{ - public override IEnumerable> GetSchema() - { - return CreateSchema.For() - .Map(For.Paths("customer"), - customer => customer.Dependents - .Map(For.Paths("customer/communication")) - .Map(For.Paths("customer/orders"), - customerOrders => customerOrders.Dependents - .Map(For.Paths("customer/orders/order/items")))) - .End(); - } -} -``` -`CustomerConfiguration` shows `query/transformer` pairs mapped at three levels of nesting as per the `Customer` entity object graph. -`XPaths` are used to identify the schema paths in the object graph. Alternately, you could use your own representation to name the pairs or map the object graph. However, you would need to provide the `ISchemaPathmatcher` implementation to managing path matching. - -#### i. Query/Transformer Mapping -Every `Query` type in the Entity `Configuration` definition should have a complementing `Transformer` type. -You could map multiple `schema paths` to a given query/transformer pair. Currently, `XPath` and `JSONPath` schema languages are supported. - -Below is the snippet from `CustomerConfiguration` definition shows that `CustomerQuery` has associated `CustomerTransform` and the pair is mapped to the root `Customer` object. -``` - .Map(For.Paths("customer")) -``` - -#### ii. Nested Query/Transformer Mappings -You could nest query/transformer pairs in a `parent/child` hierarchy. In which case the output of the parent query will serve as the input to the child query to resolve its query context. - -The query/transformer mappings can be `nested` to `5` levels down. - -Below is snippet to show nesting of `CommunicationQuery` as child to `CustomerQuery`. -``` -.Map(For.Paths("customer"), -- Parent - customer => customer.Dependents - .Map(For.Paths("customer/communication")) -- Child -``` - -Execution Flow -* In parent/child hierarchy, the first parent query executes first, followed by its immediate children. The execution flows in sequence to the last child query in order of its nesting. -* While executing the output of the parent is passed in to the child query to resolve query context and get it ready for execution. -* Transformers are also executed in the same sequence to map data to the Aggregate Entity. -* When a query path for nested query is included for execution, all the parent queries involved in that object graph get included for execution in order of its nesting. - -Please see the execution sequence below for queries and transformers nested in `CustomerConfiguration` implemented above. - -image - -#### iii. Query Class -`Query` - The purpose of a query class is to execute with supported QueryEngine to fetch data from data storage. - -`QueryEngine` is an implementation of `IQueryEngine` to execute queries with supported data storage to return query result (ie. Result instance of `IQueryResult`). - -Depending on the Nuget package(s) installed, you could implement `SQL` and `API` queries. -* `SQL` queries execute to get data from SQL database using `Dapper` or `EntityFramework` engines. -* `API` query executes web api to call an `endpoint` using `HTTPClient` supported engine to get data. - -**Important**: You can combine heterogeneous queries in the Entity configuration to target different data stores. - -Example of SQL & API queries are below. -You need to override the `GetQuery(IDataContext context, IQueryResult parentQueryResult)` method to return query delegate (package specific implementation). -* `IDataContext` is the context parameter passed to DataProvider to get aggregated results (. Aggregated Entity). This parameter is always available for both parent and child queries. -* `IQueryResult` parameter is only available when query is configured in child mode, else will be null. - -##### `Schemio.SQL` - with `Dapper` Query implementation. -To create a SQL query you need to derive from `SQLQuery` where TQueryResult is `IQueryResult` implementation. - -1. Example Parent Query - CustomerQuery -``` -public class CustomerQuery : SQLQuery -{ - protected override Func> GetQuery(IDataContext context, IQueryResult parentQueryResult) - { - // Executes as root or level 1 query. - var customer = (CustomerRequest)context.Request; - - return connection => connection.QueryFirstOrDefaultAsync(new CommandDefinition - ( - "select CustomerId as Id, " + - "Customer_Name as Name," + - "Customer_Code as Code " + - $"from TCustomer where customerId={customer.CustomerId}" - )); - } -} -``` -2. Example Child Query - OrdersQuery -``` -internal class OrdersQuery : SQLQuery> -{ - protected override Func>> GetQuery(IDataContext context, IQueryResult parentQueryResult) - { - // Execute as child to customer query. - var customer = (CustomerResult)parentQueryResult; - - return async connection => - { - var items = await connection.QueryAsync(new CommandDefinition - ( - "select OrderId, " + - "OrderNo, " + - "OrderDate " + - "from TOrder " + - $"where customerId={customer.Id}" - )); - - return new CollectionResult(items); - }; - } -} -``` - -##### `Schemio.EntityFramework` - with `EntityFramework` Query implementation -To create a SQL query you need to derive from `SQLQuery` where TQueryResult is `IQueryResult` implementation. - -1. Example Parent Query - CustomerQuery -``` -public class CustomerQuery : SQLQuery -{ - protected override Func> GetQuery(IDataContext context, IQueryResult parentQueryResult) - { - // Executes as root or level 1 query. parentQueryResult will be null. - var customer = (CustomerRequest)context.Request; - - return async dbContext => - { - var result = await dbContext.Set() - .Where(c => c.Id == customer.CustomerId) - .Select(c => new CustomerResult - { - Id = c.Id, - Name = c.Name, - Code = c.Code - }) - .FirstOrDefaultAsync(); - - return result; - }; - } -} -``` -2. Example Child Query - OrdersQuery -``` - internal class OrdersQuery : SQLQuery> - { - protected override Func>> GetQuery(IDataContext context, IQueryResult parentQueryResult) - { - // Execute as child to customer query. - var customer = (CustomerResult)parentQueryResult; - - return async dbContext => - { - var items = await dbContext.Set() - .Where(p => p.Customer.Id == customer.Id) - .Select(c => new OrderResult - { - CustomerId = c.CustomerId, - OrderId = c.OrderId, - Date = c.Date, - OrderNo = c.OrderNo - }) - .ToListAsync(); - - return new CollectionResult(items); - }; - } - } -``` -##### `Schemio.Api` - with `HttpClient` Query implementation -To create a Web API query you need to derive from `WebQuery` where TQueryResult is `IQueryResult` implementation. - -`Important`: If you need to get response headers in the result then TQueryResult should derive from `WebHeaderResult` class. - -1. Example Parent Query - CustomerQuery -``` -public class CustomerWebQuery : WebQuery -{ - public CustomerWebQuery() : base(Endpoints.BaseAddress) - { - } - - protected override Func GetQuery(IDataContext context, IQueryResult parentApiResult) - { - // Executes as root or level 1 api. - var customerRequest = (CustomerRequest)context.Request; - - return () => new Uri(string.Format(Endpoints.BaseAddress + Endpoints.Customer, customerRequest.CustomerId), UriKind.Absolute); - } - - /// - /// Override to pass outgoing request headers. - /// - /// - protected override IDictionary GetRequestHeaders() - { - return new Dictionary - { - { "x-meta-branch-code", "London" } - }; - } - - /// - /// Override to subscribe for given Response headers to be added to TQueryResult. - /// For receiving response headers, You need to implement the TQueryResult type from `WebHeaderResult` class instead of IQueryResult. - /// - /// - protected override IEnumerable GetResponseHeaders() - { - return new[] { "x-meta-branch-code" }; - } -} -``` -Note: CustomerResult is above query implements from `WebHeaderResult` class to support response headers. -``` -public class CustomerResult : WebHeaderResult -{ - public int Id { get; set; } - public string Code { get; set; } - public string Name { get; set; } -} -``` - -2. Example Child Query - OrdersQuery -``` -internal class OrdersQuery : WebQuery> -{ - public OrdersQuery() : base(Endpoints.BaseAddress) - { - } - protected override Func GetQuery(IDataContext context, IQueryResult parentQueryResult) - { - // Execute as child to customer api. - var customer = (CustomerResult)parentQueryResult; - - return ()=> new Uri(string.Format($"v2/customers/{customer.Id}/orders); - } -} -``` - -#### iv. Transformer Class -The purpose of the transformer class is to transform the data fetched by the linked query class and map to the configured object graph of the entity. - -To define a transformer class, you need to implement `BaseTransformer` -- where TEntity is Aggregate Entity implementing `IEntity`. eg. Customer. -- where TQueryResult is Query Result from associated Query. It is an implementation of `IQueryResult` interface. - -Example transformer - Customer Transformer -``` -internal class CustomerTransform : BaseTransformer -{ - public override Customer Transform(CustomerResult queryResult, Customer entity) - { - var customer = entity ?? new Customer(); - customer.CustomerId = queryResult.Id; - customer.CustomerName = queryResult.CustomerName; - customer.CustomerCode = queryResult.CustomerCode; - - if (queryResult is WebHeaderResult webHeaderResult) - if (webHeaderResult.Headers.TryGetValue("x-meta-branch-code", out var branch)) - customer.Branch = branch; - - return customer; - } -} -``` - -**Note**: It is `important` that the transformer should map data only to the `schema path(s)` pointing `section(s)` of the object graph. - -For the example query/transformer mapping -``` -.Map(For.Paths("customer/communication")) -``` -The Communication transformer should map data only to the `customer/communication` xpath mapped object graph of customer class. - -### Step 3. DataProvider Setup -Data provider needs to be setup with required dependencies. Provide implementations of below dependencies to construct the data provider. - -#### Container Registrations - -With ServiceCollection, you need to register the below dependencies. - -``` - // Register core services - services.AddTransient(typeof(IQueryBuilder<>), typeof(QueryBuilder<>)); - services.AddTransient(typeof(IEntityBuilder<>), typeof(EntityBuilder<>)); - services.AddTransient(typeof(IDataProvider<>), typeof(DataProvider<>)); - services.AddTransient(); - - // Register instance of ISchemaPathMatcher - Json, XPath or Custom. - services.AddTransient(c => new XPathMatcher()); - - // Enable logging - services.AddLogging(); - - //For Dapper SQL engine - Schemio.SQL - services.AddTransient(c => new QueryEngine(new SQLConfiguration { ConnectionSettings = new ConnectionSettings { - Providername = "System.Data.SqlClient", - ConnectionString ="Data Source=Powerstation; Initial Catalog=Customer; Integrated Security=SSPI;" - }}); - - // For entity framework engine - Schemio.EntityFramework - services.AddDbContextFactory(options => options.UseSqlServer(YourSqlConnection), ServiceLifetime.Scoped); - services.AddTransient(c => new QueryEngine(c.GetService>()); - - // For HTTPClient Engine for web APIs - Schemio.API - - // Enable HttpClient - services.AddHttpClient(); - services.AddTransient(); - - // Register each entity configuration. eg CustomerConfiguration - services.AddTransient, CustomerConfiguration>(); - - ``` - -`Please Note:` You can combine multiple query engines and implement supporting types of queries to execute on target data platforms. - - -#### Using Fluent interface for registrations - -i. Example registration: Schemio.SQL - -``` -// Enable DbProviderFactory. - DbProviderFactories.RegisterFactory(DbProviderName, SqliteFactory.Instance); - - var connectionString = $"DataSource={Environment.CurrentDirectory}//Customer.db;mode=readonly;cache=shared"; - - var configuration = new SQLConfiguration { ConnectionSettings = new ConnectionSettings { ConnectionString = connectionString, ProviderName = DbProviderName } }; - -// Enable logging - services.AddLogging(); - - services.UseSchemio() - .WithEngine(c => new QueryEngine(configuration)) - .WithPathMatcher(c => new XPathMatcher()) - .WithEntityConfiguration(c => new CustomerConfiguration()); -``` - -ii. Example registration: Schemio.EntityFramework - -``` - var connectionString = $"DataSource={Environment.CurrentDirectory}//Customer.db;mode=readonly;cache=shared"; - -// Enable DBContext Factory - services.AddDbContextFactory(options => - options.UseSqlite(connectionString)); - -// Enable logging - services.AddLogging(); - - services.UseSchemio() - .WithEngine(c => new QueryEngine(c.GetService>())) - .WithPathMatcher(c => new XPathMatcher()) - .WithEntityConfiguration(c => new CustomerConfiguration()); - -``` -iii. Example registration: Schemio.API -``` - // Enable logging - services.AddLogging(); - - // Enable HttpClient - services.AddHttpClient(); - - services.UseSchemio() - .WithEngine() - .WithPathMatcher(c => new XPathMatcher()) - .WithEntityConfiguration(c => new CustomerConfiguration()); - -``` -iv. Example registration: Multiple Engines -``` - // Enable logging - services.AddLogging(); - - // Enable HttpClient - services.AddHttpClient(); - - var connectionString = $"DataSource={Environment.CurrentDirectory}//Customer.db;mode=readonly;cache=shared"; - -// Enable DBContext Factory - services.AddDbContextFactory(options => - options.UseSqlite(connectionString)); - - services.UseSchemio() - .WithEngine() - .WithEngine(c => new QueryEngine(c.GetService>())) - .WithPathMatcher(c => new XPathMatcher()) - .WithEntityConfiguration(c => new CustomerConfiguration()); -``` - -#### Use Data Provider - -##### i. Dependency Inject - IDataProvider - -To use Data provider, Inject `IDataProvider` where T is IEntity, using constructor & property injection method or explicitly Resolve using service provider ie. `IServiceProvider.GetService(typeof(IDataProvider))` - -##### ii. Call DataProvider.GetData(IEntityRequest context) method. -You need to call the `GetData()` method with an instance of parameter class derived from `IEntityRequest` interface. - -The `IEntityRequest` provides a `SchemaPaths` property, which is a list of schema paths to include for the given request to fetch aggregated data. -- When `no` paths are passed in the parameter then entire aggregated entity for all configured queries is returned. -- When list of schema paths are included in the request then the returned aggregated data entity only includes query results from included queries. - -When nested path for a nested query is included (eg. customer/orders/order/items) then all parent queries in the respective parent paths also get included for execution. - -Example - Control Flow - -image - - -## Extending Schemio - -You could extend Schemio by providing your own custom implementation of the query engine (`IQueryEngine`) and query (`IQuery`) to execute queries on custom target data platform. - -To do this, you need to extend the base interfaces as depicted below. -### i. IQueryEngine -Implement `IQueryEngine` interface to provide the custom query engine to be used with schemio. -``` -public interface IQueryEngine -{ - /// - /// Detrmines whether an instance of query can be executed with this engine. - /// - /// instance of IQuery. - /// Boolean; True when supported. - bool CanExecute(IQuery query); - - /// - /// Executes a given query returning query result. - /// - /// Custom instance of IQuery. - /// Task of IQueryResult. - Task Execute(IQuery> query); -} -``` -Example Entity Framework implementation is below -``` -public class QueryEngine : IQueryEngine where T : DbContext -{ - private readonly IDbContextFactory _dbContextFactory; - - public QueryEngine(IDbContextFactory _dbContextFactory) - { - this._dbContextFactory = _dbContextFactory; - } - - public bool CanExecute(IQuery query) => query != null && query is ISQLQuery; - - public Task Execute(IQuery query) - { - using (var dbcontext = _dbContextFactory.CreateDbContext()) - { - var result = ((ISQLQuery)query).Run(dbcontext); - return result; - } - } -} -``` -### ii. IQuery -With the Query Engine implementation, you also need to provide custom implementation of `IQuery` for executing the query with custom query engine. - -To do this, you need to extend `BaseQuery` where TQueryResult is `IQueryResult`. And, provide overrides for below methods. -- `bool IsContextResolved()` -Engine calls this method to confirm whether the query is ready for execution. Return true when query context is resolved. -- `void ResolveQuery(IDataContext context, IQueryResult parentQueryResult)` -This method is invoked by schemio to resolve the query context required for execution ith supporting query engine. `IQueryResult` parameter is only available when the custom query is configured in nested or child mode. - - -Example - EntityFramework Supported query implementation is shown below. -``` - public abstract class SQLQuery - : BaseQuery, ISQLQuery - where TQueryResult : IQueryResult - { - private Func> QueryDelegate = null; - - public override bool IsContextResolved() => QueryDelegate != null; - - public override void ResolveQuery(IDataContext context, IQueryResult parentQueryResult) - { - QueryDelegate = GetQuery(context, parentQueryResult); - } - - async Task ISQLQuery.Run(DbContext dbContext) - { - return await QueryDelegate(dbContext); - } - - /// - /// Get query delegate to return query result. - /// - /// - /// - /// - protected abstract Func> GetQuery(IDataContext context, IQueryResult parentQueryResult); - } -``` - -### iii. Custom Schema Paths - -Additionally, You can use your own schema language instead of XPath/JSONPath to map aggregated entity's object graph, and register with schemio. - -To do this you need to follow the below steps: -#### i. Use schema paths in Entity Configuration. - -Provide entity schema definition with query/transformer pairs using custom schema language paths. - -Example - with Dummy schema mapping -``` -.Map(For.Paths("customer$orders")) -``` -#### ii. ISchemaPathMatcher -Provide implementation of `ISchemaPathMatcher` interface and implement `IsMatch()` method to provide logic for matching custom paths. - -`Important`: This matcher is used by query builder to filter queries based matched paths, to include only required queries for execution to optimize performance. -``` -public interface ISchemaPathMatcher -{ - bool IsMatch(string inputPath, ISchemaPaths configuredPaths); -} -``` -Example implementation of XPathMatcher is below. -``` -public class XPathMatcher : ISchemaPathMatcher - { - private static readonly Regex ancestorRegex = new Regex(@"=ancestor::(?'path'.*?)(/@|\[.*\]/@)", RegexOptions.Compiled); - - public bool IsMatch(string inputXPath, ISchemaPaths configuredXPaths) - { - if (inputXPath == null) - return false; - - if (configuredXPaths.Paths.Any(x => inputXPath.ToLower().Contains(x.ToLower()))) - return true; - - if (configuredXPaths.Paths.Any(x => inputXPath.Contains("ancestor::") - && ancestorRegex.Matches(inputXPath).Select(match => match.Groups["path"].Value).Distinct().Any(match => x.EndsWith(match)))) - return true; - - return false; - } - } -``` diff --git a/src/DataFuse.Adapters.Abstraction/AssemblyInfo.cs b/src/DataFuse.Adapters.Abstraction/AssemblyInfo.cs new file mode 100644 index 0000000..7e70851 --- /dev/null +++ b/src/DataFuse.Adapters.Abstraction/AssemblyInfo.cs @@ -0,0 +1,8 @@ +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +[assembly: ComVisible(false)] +[assembly: InternalsVisibleTo("DataFuse.Integration")] +[assembly: InternalsVisibleTo("DataFuse.Integration.Tests")] +[assembly: InternalsVisibleTo("DataFuse.Adapters.SQL.Tests")] +[assembly: InternalsVisibleTo("DataFuse.Adapters.EntityFramework.Tests")] diff --git a/src/DataFuse.Adapters.Abstraction/BaseQuery.cs b/src/DataFuse.Adapters.Abstraction/BaseQuery.cs new file mode 100644 index 0000000..e0a99e1 --- /dev/null +++ b/src/DataFuse.Adapters.Abstraction/BaseQuery.cs @@ -0,0 +1,37 @@ +namespace DataFuse.Adapters.Abstraction; + +/// +/// Implement this base class to create a data provider query. +/// +/// +public abstract class BaseQuery : IQuery + where TQueryResult : IQueryResult +{ + /// + /// Children queries dependent on this query. + /// + public List Children { get; set; } = []; + + /// + /// Get the result type for the query + /// + public Type ResultType => typeof(TQueryResult); + + /// + /// Implement to determine whether the query context is resolved and ready to execute with supported engine. + /// + public abstract bool IsContextResolved(); + + /// + /// Implement to resolve query context for execution with supporting query engine. + /// + public abstract void ResolveQuery(IDataContext context, IQueryResult? parentQueryResult); + + /// + /// Run query with supporting IQueryEngine instance. + /// + public Task Run(IQueryEngine engine) + { + return engine.Execute(this); + } +} diff --git a/src/DataFuse.Adapters.Abstraction/BaseTransformer.cs b/src/DataFuse.Adapters.Abstraction/BaseTransformer.cs new file mode 100644 index 0000000..6f219ce --- /dev/null +++ b/src/DataFuse.Adapters.Abstraction/BaseTransformer.cs @@ -0,0 +1,48 @@ +namespace DataFuse.Adapters.Abstraction; + +public abstract class BaseTransformer : ITransformer, ITransformerContext, ITransformerQueryResult, ITransformerHooks + where TEntity : IEntity + where TQueryResult : IQueryResult +{ + /// + /// Transformer instance of data context. + /// + protected IDataContext? Context { get; private set; } + + /// + /// Supported QueryResult type for the transformer. + /// + public Type SupportedQueryResult => typeof(TQueryResult); + + /// + /// Method to set data context for the transformer + /// + public void SetContext(IDataContext context) => Context = context; + + /// + /// Transform method mapping query result data to entity. + /// + public void Transform(IQueryResult queryResult, IEntity entity) + { + Transform((TQueryResult)queryResult, (TEntity)entity); + } + + /// + /// Implement this method to map data to entity. + /// + public abstract void Transform(TQueryResult queryResult, TEntity entity); + + /// + /// Pre-transform method that can be used to perform any pre-transformation logic if needed. + /// + public virtual void PreTransform(PreTransformContext context) + { + } + + /// + /// Post-transform method that can be used to perform any post-transformation logic if needed. + /// + public virtual void PostTransform(PostTransformContext context) + { + } +} diff --git a/src/DataFuse.Adapters.Abstraction/CacheResultAttribute.cs b/src/DataFuse.Adapters.Abstraction/CacheResultAttribute.cs new file mode 100644 index 0000000..c2798d3 --- /dev/null +++ b/src/DataFuse.Adapters.Abstraction/CacheResultAttribute.cs @@ -0,0 +1,4 @@ +namespace DataFuse.Adapters.Abstraction; + +public class CacheResultAttribute : Attribute +{ } diff --git a/src/DataFuse.Adapters.Abstraction/ChildrenQueries.cs b/src/DataFuse.Adapters.Abstraction/ChildrenQueries.cs new file mode 100644 index 0000000..f111bd2 --- /dev/null +++ b/src/DataFuse.Adapters.Abstraction/ChildrenQueries.cs @@ -0,0 +1,7 @@ +namespace DataFuse.Adapters.Abstraction; + +public class ChildrenQueries +{ + public Type ParentQueryResultType { get; set; } = null!; + public IList Queries { get; set; } = []; +} diff --git a/src/DataFuse.Adapters.Abstraction/CollectionResult.cs b/src/DataFuse.Adapters.Abstraction/CollectionResult.cs new file mode 100644 index 0000000..8f02ef4 --- /dev/null +++ b/src/DataFuse.Adapters.Abstraction/CollectionResult.cs @@ -0,0 +1,12 @@ +namespace DataFuse.Adapters.Abstraction; + +public class CollectionResult : List, IQueryResult +{ + public CollectionResult(IEnumerable list) : base(list) + { + } + + public CollectionResult() + { + } +} diff --git a/src/DataFuse.Adapters.Abstraction/CreateSchema.cs b/src/DataFuse.Adapters.Abstraction/CreateSchema.cs new file mode 100644 index 0000000..bc09fa1 --- /dev/null +++ b/src/DataFuse.Adapters.Abstraction/CreateSchema.cs @@ -0,0 +1,129 @@ +namespace DataFuse.Adapters.Abstraction; + +#region Helpers + +public class CreateSchema +{ + public static IMappings For() where T : IEntity + => new Mappings { Order = 1 }; +} + +public class For +{ + public static ISchemaPaths Paths(params string[] paths) + => new SchemaPaths { Paths = paths }; +} + +public class SchemaPaths : ISchemaPaths +{ + public string[] Paths { get; set; } = []; +} + +#endregion Helpers + +public class Mappings : + List>, + IMappings, + IMapOrComplete + where TEntity : IEntity + where TQueryResult : IQueryResult +{ + public int Order { get; set; } + + /// + /// Map query and transformer for given schema path + /// + public IMapOrComplete Map(ISchemaPaths paths) + where TQuery : IQuery, new() + where TTransformer : ITransformer, new() => + Map(paths, null); + + /// + /// Map query and transformer for given schema path and with dependent query/transform mappings + /// + public IMapOrComplete Map(ISchemaPaths paths, Func, IMap>? dependents) + where TQuery : IQuery, new() + where TTransformer : ITransformer, new() + { + var mapping = new Mapping + { + Query = new TQuery(), + Transformer = new TTransformer(), + Order = Order, + SchemaPaths = paths, + }; + + if (dependents is not null) + foreach (var dep in ((IMappings)dependents(mapping)).GetMappings) + { + dep.DependentOn ??= mapping.Query; + Add(dep); + } + + Add(mapping); + + return this; + } + + public Mappings GetMappings => this; + + public IEnumerable> End() => this; +} + +public class Mapping : + IWithDependents + where TEntity : IEntity + where TQueryResult : IQueryResult +{ + public int Order { get; set; } + public ISchemaPaths SchemaPaths { get; set; } = null!; + public IQuery Query { get; set; } = null!; + public ITransformer Transformer { get; set; } = null!; + public IQuery? DependentOn { get; set; } + + public IMappings Dependents => new Mappings { Order = Order + 1 }; +} + +#region Fluent Interfaces + +public interface ISchemaPaths +{ + string[] Paths { get; set; } +} + +public interface IMap + where TEntity : IEntity + where TQueryResult : IQueryResult +{ + IMapOrComplete Map(ISchemaPaths paths) + where TQuery : IQuery, new() + where TTransformer : ITransformer, new(); + + IMapOrComplete Map(ISchemaPaths paths, Func, IMap>? dependents) + where TQuery : IQuery, new() + where TTransformer : ITransformer, new(); +} + +public interface IMappings : IMap + where TEntity : IEntity + where TQueryResult : IQueryResult +{ + int Order { get; set; } + Mappings GetMappings { get; } +} + +public interface IMapOrComplete : IMap + where TEntity : IEntity + where TQueryResult : IQueryResult +{ + IEnumerable> End(); +} + +public interface IWithDependents + where TEntity : IEntity + where TQueryResult : IQueryResult +{ + IMappings Dependents { get; } +} + +#endregion Fluent Interfaces diff --git a/src/DataFuse.Adapters.Abstraction/DataFuse.Adapters.Abstraction.csproj b/src/DataFuse.Adapters.Abstraction/DataFuse.Adapters.Abstraction.csproj new file mode 100644 index 0000000..8f5cc92 --- /dev/null +++ b/src/DataFuse.Adapters.Abstraction/DataFuse.Adapters.Abstraction.csproj @@ -0,0 +1,47 @@ + + + + netstandard2.1;net8.0;net9.0;net10.0 + enable + enable + latest + true + Code Shayk + Code Shayk + Copyright (c) 2026 Code Shayk + https://github.com/CodeShayk/DataFuse.Net + git + README.md + data-integration-transparent.png + DataFuse adapter abstraction layer. Contains interfaces and base classes needed for implementing custom data adapters (SQL, Entity Framework, Web API, etc.). + datafuse adapter abstraction data-mapping query-engine data-mapper schema-mapping data-aggregator + https://github.com/CodeShayk/DataFuse.Net/wiki + LICENSE.md + DataFuse Adapters Abstraction + True + True + snupkg + True + 3.0.0 + v3.0.0 +- Multi-target framework support: netstandard2.1, net8.0, net9.0, net10.0. +- Added CacheResult attribute for query result caching. + + + + + + True + \ + + + True + \ + + + True + \ + + + + diff --git a/src/DataFuse.Adapters.Abstraction/EntityConfiguration.cs b/src/DataFuse.Adapters.Abstraction/EntityConfiguration.cs new file mode 100644 index 0000000..20d5b71 --- /dev/null +++ b/src/DataFuse.Adapters.Abstraction/EntityConfiguration.cs @@ -0,0 +1,21 @@ +namespace DataFuse.Adapters.Abstraction; + +/// +/// Implement to configure schema path mappings for an Entity. +/// +/// Entity type +public abstract class EntityConfiguration : IEntityConfiguration where TEntity : IEntity +{ + public IEnumerable> Mappings { get; } + + protected EntityConfiguration() + { + Mappings = GetSchema(); + } + + /// + /// Implement to configure entity schema mappings with queries & transformers. + /// + /// Entity Schema mappings + public abstract IEnumerable> GetSchema(); +} diff --git a/src/DataFuse.Adapters.Abstraction/Guard.cs b/src/DataFuse.Adapters.Abstraction/Guard.cs new file mode 100644 index 0000000..8d22684 --- /dev/null +++ b/src/DataFuse.Adapters.Abstraction/Guard.cs @@ -0,0 +1,15 @@ +namespace DataFuse.Adapters.Abstraction; + +internal static class Guard +{ +#if NET6_0_OR_GREATER + internal static void ThrowIfNull(object? argument, string? paramName = null) + => ArgumentNullException.ThrowIfNull(argument, paramName); +#else + internal static void ThrowIfNull(object? argument, string? paramName = null) + { + if (argument is null) + throw new ArgumentNullException(paramName); + } +#endif +} diff --git a/src/DataFuse.Adapters.Abstraction/IDataContext.cs b/src/DataFuse.Adapters.Abstraction/IDataContext.cs new file mode 100644 index 0000000..6833323 --- /dev/null +++ b/src/DataFuse.Adapters.Abstraction/IDataContext.cs @@ -0,0 +1,6 @@ +namespace DataFuse.Adapters.Abstraction; + +public interface IDataContext : IEntityContextCache +{ + IEntityRequest Request { get; } +} diff --git a/src/DataFuse.Adapters.Abstraction/IEntity.cs b/src/DataFuse.Adapters.Abstraction/IEntity.cs new file mode 100644 index 0000000..cfe6bdf --- /dev/null +++ b/src/DataFuse.Adapters.Abstraction/IEntity.cs @@ -0,0 +1,8 @@ +namespace DataFuse.Adapters.Abstraction; + +/// +/// Implement Entity required to be hydrated (using query/transformer). +/// +public interface IEntity +{ +} diff --git a/src/DataFuse.Adapters.Abstraction/IEntityConfiguration.cs b/src/DataFuse.Adapters.Abstraction/IEntityConfiguration.cs new file mode 100644 index 0000000..7846f32 --- /dev/null +++ b/src/DataFuse.Adapters.Abstraction/IEntityConfiguration.cs @@ -0,0 +1,13 @@ +namespace DataFuse.Adapters.Abstraction; + +/// +/// Implement to configure schema path mappings for an Entity. +/// +/// Entity type +public interface IEntityConfiguration where TEntity : IEntity +{ + /// + /// Entity schema mappings. + /// + IEnumerable> Mappings { get; } +} diff --git a/src/DataFuse.Adapters.Abstraction/IEntityContextCache.cs b/src/DataFuse.Adapters.Abstraction/IEntityContextCache.cs new file mode 100644 index 0000000..a8cdb43 --- /dev/null +++ b/src/DataFuse.Adapters.Abstraction/IEntityContextCache.cs @@ -0,0 +1,9 @@ +namespace DataFuse.Adapters.Abstraction; + +public interface IEntityContextCache +{ + /// + /// Cache dictionary holding query results for query result type marked with [CacheResult] attribute. + /// + Dictionary Cache { get; set; } +} diff --git a/src/DataFuse.Adapters.Abstraction/IEntityRequest.cs b/src/DataFuse.Adapters.Abstraction/IEntityRequest.cs new file mode 100644 index 0000000..a8018a5 --- /dev/null +++ b/src/DataFuse.Adapters.Abstraction/IEntityRequest.cs @@ -0,0 +1,9 @@ +namespace DataFuse.Adapters.Abstraction; + +public interface IEntityRequest +{ + /// + /// Entity schema paths for data retrieval. + /// + string[] SchemaPaths { get; set; } +} diff --git a/src/DataFuse.Adapters.Abstraction/IPolymorphicResult.cs b/src/DataFuse.Adapters.Abstraction/IPolymorphicResult.cs new file mode 100644 index 0000000..d292870 --- /dev/null +++ b/src/DataFuse.Adapters.Abstraction/IPolymorphicResult.cs @@ -0,0 +1,5 @@ +namespace DataFuse.Adapters.Abstraction; + +public interface IPolymorphicResult : IQueryResult +{ +} diff --git a/src/DataFuse.Adapters.Abstraction/IQuery.cs b/src/DataFuse.Adapters.Abstraction/IQuery.cs new file mode 100644 index 0000000..4bb8459 --- /dev/null +++ b/src/DataFuse.Adapters.Abstraction/IQuery.cs @@ -0,0 +1,15 @@ +namespace DataFuse.Adapters.Abstraction; + +/// +/// Implement IQuery to fetch data using API or database. +/// +public interface IQuery : IQueryRunner +{ + List Children { get; set; } + + Type ResultType { get; } + + bool IsContextResolved(); + + void ResolveQuery(IDataContext context, IQueryResult? parentQueryResult = null); +} diff --git a/src/DataFuse.Adapters.Abstraction/IQueryEngine.cs b/src/DataFuse.Adapters.Abstraction/IQueryEngine.cs new file mode 100644 index 0000000..56ce156 --- /dev/null +++ b/src/DataFuse.Adapters.Abstraction/IQueryEngine.cs @@ -0,0 +1,18 @@ +namespace DataFuse.Adapters.Abstraction; + +public interface IQueryEngine +{ + /// + /// Determines whether an instance of query can be executed with this engine. + /// + /// instance of IQuery. + /// Boolean; True when supported. + bool CanExecute(IQuery query); + + /// + /// Executes a list of queries returning a list of aggregated results. + /// + /// Instance of IQuery. + /// Instance of IQueryResult. + Task Execute(IQuery query); +} diff --git a/src/DataFuse.Adapters.Abstraction/IQueryList.cs b/src/DataFuse.Adapters.Abstraction/IQueryList.cs new file mode 100644 index 0000000..613d16f --- /dev/null +++ b/src/DataFuse.Adapters.Abstraction/IQueryList.cs @@ -0,0 +1,19 @@ +namespace DataFuse.Adapters.Abstraction; + +public interface IQueryList +{ + int QueryDependencyDepth { get; set; } + IEnumerable Queries { get; } + + bool IsEmpty(); + + IQueryList GetByType() where T : class; + + List As(); + + List GetChildrenQueries(); + + void AddRange(IEnumerable collection); + + int Count(); +} diff --git a/src/DataFuse.Adapters.Abstraction/IQueryParameter.cs b/src/DataFuse.Adapters.Abstraction/IQueryParameter.cs new file mode 100644 index 0000000..e5bb933 --- /dev/null +++ b/src/DataFuse.Adapters.Abstraction/IQueryParameter.cs @@ -0,0 +1,5 @@ +namespace DataFuse.Adapters.Abstraction; + +public interface IQueryParameter +{ +} diff --git a/src/DataFuse.Adapters.Abstraction/IQueryResult.cs b/src/DataFuse.Adapters.Abstraction/IQueryResult.cs new file mode 100644 index 0000000..a46b3da --- /dev/null +++ b/src/DataFuse.Adapters.Abstraction/IQueryResult.cs @@ -0,0 +1,5 @@ +namespace DataFuse.Adapters.Abstraction; + +public interface IQueryResult +{ +} diff --git a/src/DataFuse.Adapters.Abstraction/IQueryRunner.cs b/src/DataFuse.Adapters.Abstraction/IQueryRunner.cs new file mode 100644 index 0000000..025f0d4 --- /dev/null +++ b/src/DataFuse.Adapters.Abstraction/IQueryRunner.cs @@ -0,0 +1,6 @@ +namespace DataFuse.Adapters.Abstraction; + +public interface IQueryRunner +{ + Task Run(IQueryEngine engine); +} diff --git a/src/DataFuse.Adapters.Abstraction/ISchemaPathMatcher.cs b/src/DataFuse.Adapters.Abstraction/ISchemaPathMatcher.cs new file mode 100644 index 0000000..6f980c0 --- /dev/null +++ b/src/DataFuse.Adapters.Abstraction/ISchemaPathMatcher.cs @@ -0,0 +1,9 @@ +namespace DataFuse.Adapters.Abstraction; + +public interface ISchemaPathMatcher +{ + /// + /// Determines whether there is a match for given input path vs configured paths for entity's object graph. + /// + bool IsMatch(string inputPath, ISchemaPaths configuredPaths); +} diff --git a/src/DataFuse.Adapters.Abstraction/ITransformer.cs b/src/DataFuse.Adapters.Abstraction/ITransformer.cs new file mode 100644 index 0000000..02e4eda --- /dev/null +++ b/src/DataFuse.Adapters.Abstraction/ITransformer.cs @@ -0,0 +1,12 @@ +namespace DataFuse.Adapters.Abstraction; + +/// +/// Implement transformer to map data from supported query result to entity in context. +/// +public interface ITransformer +{ + /// + /// Transform method to map data to entity for a given query result. + /// + void Transform(IQueryResult queryResult, IEntity entity); +} diff --git a/src/DataFuse.Adapters.Abstraction/ITransformerContext.cs b/src/DataFuse.Adapters.Abstraction/ITransformerContext.cs new file mode 100644 index 0000000..0beeca5 --- /dev/null +++ b/src/DataFuse.Adapters.Abstraction/ITransformerContext.cs @@ -0,0 +1,12 @@ +namespace DataFuse.Adapters.Abstraction; + +/// +/// Implement to set transform with data context. +/// +public interface ITransformerContext +{ + /// + /// Implement to set context in transform. + /// + void SetContext(IDataContext context); +} diff --git a/src/DataFuse.Adapters.Abstraction/ITransformerHooks.cs b/src/DataFuse.Adapters.Abstraction/ITransformerHooks.cs new file mode 100644 index 0000000..dbb8073 --- /dev/null +++ b/src/DataFuse.Adapters.Abstraction/ITransformerHooks.cs @@ -0,0 +1,17 @@ +namespace DataFuse.Adapters.Abstraction; + +/// +/// Transformer hooks interface to allow pre and post transformation actions. +/// +public interface ITransformerHooks +{ + /// + /// Pre-transform hook to perform actions before the transformation. + /// + void PreTransform(PreTransformContext context); + + /// + /// Post-transform hook to perform actions after the transformation. + /// + void PostTransform(PostTransformContext context); +} diff --git a/src/DataFuse.Adapters.Abstraction/ITransformerQueryResult.cs b/src/DataFuse.Adapters.Abstraction/ITransformerQueryResult.cs new file mode 100644 index 0000000..4c01562 --- /dev/null +++ b/src/DataFuse.Adapters.Abstraction/ITransformerQueryResult.cs @@ -0,0 +1,12 @@ +namespace DataFuse.Adapters.Abstraction; + +/// +/// Implement to get supported Query result. +/// +public interface ITransformerQueryResult +{ + /// + /// Supported query result. + /// + Type SupportedQueryResult { get; } +} diff --git a/src/DataFuse.Adapters.Abstraction/PostTransformContext.cs b/src/DataFuse.Adapters.Abstraction/PostTransformContext.cs new file mode 100644 index 0000000..ab2de29 --- /dev/null +++ b/src/DataFuse.Adapters.Abstraction/PostTransformContext.cs @@ -0,0 +1,22 @@ +namespace DataFuse.Adapters.Abstraction; + +public class PostTransformContext : TransformContext +{ + /// + /// Indicates whether the transformation needs to be repeated. + /// + internal bool IsRepeat { get; private set; } + + /// + /// Repeat the transformation process. + /// + public void Repeat() => IsRepeat = true; + + /// + /// Creates a new instance of PostTransformContext with the provided parameters. + /// + public PostTransformContext(TransformContext context) : + base(context.DataContext, context.QueryResult, context.Transformer, context.Entity, context.IsRepeated) + { + } +} diff --git a/src/DataFuse.Adapters.Abstraction/PreTransformContext.cs b/src/DataFuse.Adapters.Abstraction/PreTransformContext.cs new file mode 100644 index 0000000..fe7f77e --- /dev/null +++ b/src/DataFuse.Adapters.Abstraction/PreTransformContext.cs @@ -0,0 +1,22 @@ +namespace DataFuse.Adapters.Abstraction; + +public class PreTransformContext : TransformContext +{ + /// + /// Indicates whether the transformation has been cancelled. + /// + internal bool IsCancelled { get; private set; } + + /// + /// Cancels the transformation process. + /// + public void Cancel() => IsCancelled = true; + + /// + /// Creates a new instance of PreTransformContext with the provided parameters. + /// + public PreTransformContext(TransformContext context) : + base(context.DataContext, context.QueryResult, context.Transformer, context.Entity, context.IsRepeated) + { + } +} diff --git a/src/DataFuse.Adapters.Abstraction/QueryComparer.cs b/src/DataFuse.Adapters.Abstraction/QueryComparer.cs new file mode 100644 index 0000000..1262e5b --- /dev/null +++ b/src/DataFuse.Adapters.Abstraction/QueryComparer.cs @@ -0,0 +1,21 @@ +namespace DataFuse.Adapters.Abstraction; + +public class QueryComparer : IEqualityComparer +{ + public bool Equals(IQuery? x, IQuery? y) + { + if (x is null && y is null) + return true; + + if (x is null || y is null) + return false; + + return x.GetType() == y.GetType(); + } + + public int GetHashCode(IQuery obj) + { + Guard.ThrowIfNull(obj, nameof(obj)); + return obj.GetType().GetHashCode(); + } +} diff --git a/src/DataFuse.Adapters.Abstraction/QueryList.cs b/src/DataFuse.Adapters.Abstraction/QueryList.cs new file mode 100644 index 0000000..c5e5aa6 --- /dev/null +++ b/src/DataFuse.Adapters.Abstraction/QueryList.cs @@ -0,0 +1,58 @@ +namespace DataFuse.Adapters.Abstraction; + +public class QueryList : IQueryList +{ + private readonly List queryList; + + public QueryList() + { + queryList = []; + } + + public IEnumerable Queries => queryList; + + public QueryList(IEnumerable collection) + { + queryList = new List(collection); + } + + public int QueryDependencyDepth { get; set; } + + public IQueryList GetByType() where T : class + { + var queries = queryList.Where(q => q as T is not null); + return new QueryList(queries); + } + + public List As() => queryList.Cast().ToList(); + + public List GetChildrenQueries() + { + var childrenQueries = queryList + .Select(x => new ChildrenQueries { ParentQueryResultType = x.ResultType, Queries = x.Children }) + .Where(x => x.Queries is not null && x.Queries.Any()) + .ToList(); + + return childrenQueries + .Select(x => + { + var distinctList = childrenQueries + .Where(d => d.ParentQueryResultType == x.ParentQueryResultType) + .SelectMany(q => q.Queries) + .Distinct(new QueryComparer()) + .ToList(); + + return new ChildrenQueries { ParentQueryResultType = x.ParentQueryResultType, Queries = distinctList }; + }) + .ToList(); + } + + public new int Count() => queryList.Count; + + public bool IsEmpty() => queryList.Count == 0; + + public void AddRange(IEnumerable collection) + { + queryList.AddRange(collection); + } +} diff --git a/src/DataFuse.Adapters.Abstraction/TransformContext.cs b/src/DataFuse.Adapters.Abstraction/TransformContext.cs new file mode 100644 index 0000000..7b9b088 --- /dev/null +++ b/src/DataFuse.Adapters.Abstraction/TransformContext.cs @@ -0,0 +1,43 @@ +namespace DataFuse.Adapters.Abstraction; + +public class TransformContext +{ + /// + /// Context for the data being transformed. Has cached results and other data. + /// + public IDataContext DataContext { get; private set; } + + /// + /// Type of the transformer that will be used for transformation. + /// + public Type? TransformerType => Transformer?.GetType(); + + public ITransformer Transformer { get; private set; } + + /// + /// Entity that is being transformed. + /// + public IEntity Entity { get; private set; } + + /// + /// Query result (data) that is being used for transformation + /// + public IQueryResult QueryResult { get; private set; } + + /// + /// Indicates whether the transformation has been repeated. + /// + internal bool IsRepeated { get; set; } + + /// + /// Creates a new instance of TransformContext with the provided parameters. + /// + public TransformContext(IDataContext dataContext, IQueryResult queryResult, ITransformer transformer, IEntity entity, bool isRepeated) + { + DataContext = dataContext; + QueryResult = queryResult; + Transformer = transformer; + Entity = entity; + IsRepeated = isRepeated; + } +} diff --git a/src/DataFuse.Adapters.EntityFramework/DataFuse.Adapters.EntityFramework.csproj b/src/DataFuse.Adapters.EntityFramework/DataFuse.Adapters.EntityFramework.csproj new file mode 100644 index 0000000..9c201e7 --- /dev/null +++ b/src/DataFuse.Adapters.EntityFramework/DataFuse.Adapters.EntityFramework.csproj @@ -0,0 +1,60 @@ + + + + net10.0 + enable + true + enable + latest + DataFuse Entity Framework Adapter + Code Shayk + Code Shayk + DataFuse is a powerful .NET library designed to aggregate data from heterogeneous data stores using a schema-driven approach. This package provides the Entity Framework adapter for querying databases. + Copyright (c) 2026 Code Shayk + https://github.com/CodeShayk/DataFuse.Net/wiki + data-integration-transparent.png + README.md + https://github.com/CodeShayk/DataFuse.Net + git + datafuse data-mapping query-engine data-mapper schema-mapping datafuse-entity-framework entity-framework data-aggregator + LICENSE.md + True + True + snupkg + True + 3.0.0 + v3.0.0 +- Targets .NET 10.0. +- Pre and Post Transform hooks for data transformation pipeline. +- Entity Framework Core 10.0 based query execution. + + + + + True + + + + + True + \ + + + True + \ + + + True + \ + + + + + + + + + + + + diff --git a/src/DataFuse.Adapters.EntityFramework/ISQLQuery.cs b/src/DataFuse.Adapters.EntityFramework/ISQLQuery.cs new file mode 100644 index 0000000..28e19e8 --- /dev/null +++ b/src/DataFuse.Adapters.EntityFramework/ISQLQuery.cs @@ -0,0 +1,12 @@ +using Microsoft.EntityFrameworkCore; +using DataFuse.Adapters.Abstraction; + +namespace DataFuse.Adapters.EntityFramework; + +public interface ISQLQuery +{ + /// + /// Get query delegate with implementation to return query result. + /// + Task Run(DbContext dbContext); +} diff --git a/src/DataFuse.Adapters.EntityFramework/QueryEngine.cs b/src/DataFuse.Adapters.EntityFramework/QueryEngine.cs new file mode 100644 index 0000000..11badc4 --- /dev/null +++ b/src/DataFuse.Adapters.EntityFramework/QueryEngine.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore; +using DataFuse.Adapters.Abstraction; + +namespace DataFuse.Adapters.EntityFramework; + +public class QueryEngine : IQueryEngine where T : DbContext +{ + private readonly IDbContextFactory _dbContextFactory; + + public QueryEngine(IDbContextFactory dbContextFactory) + { + _dbContextFactory = dbContextFactory; + } + + public bool CanExecute(IQuery query) => query is ISQLQuery; + + public async Task Execute(IQuery query) + { + await using var dbContext = await _dbContextFactory.CreateDbContextAsync(); + return await ((ISQLQuery)query).Run(dbContext); + } +} diff --git a/src/DataFuse.Adapters.EntityFramework/SQLQuery.cs b/src/DataFuse.Adapters.EntityFramework/SQLQuery.cs new file mode 100644 index 0000000..4ba8a2c --- /dev/null +++ b/src/DataFuse.Adapters.EntityFramework/SQLQuery.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore; +using DataFuse.Adapters.Abstraction; + +namespace DataFuse.Adapters.EntityFramework; + +public abstract class SQLQuery + : BaseQuery, ISQLQuery + where TQueryResult : IQueryResult +{ + private Func>? QueryDelegate; + + public override bool IsContextResolved() => QueryDelegate is not null; + + public override void ResolveQuery(IDataContext context, IQueryResult? parentQueryResult) + { + QueryDelegate = GetQuery(context, parentQueryResult); + } + + async Task ISQLQuery.Run(DbContext dbContext) + { + return await QueryDelegate!(dbContext); + } + + /// + /// Get query delegate to return query result. + /// + protected abstract Func> GetQuery(IDataContext context, IQueryResult? parentQueryResult); +} diff --git a/src/DataFuse.Adapters.MongoDB/DataFuse.Adapters.MongoDB.csproj b/src/DataFuse.Adapters.MongoDB/DataFuse.Adapters.MongoDB.csproj new file mode 100644 index 0000000..33b209e --- /dev/null +++ b/src/DataFuse.Adapters.MongoDB/DataFuse.Adapters.MongoDB.csproj @@ -0,0 +1,60 @@ + + + + netstandard2.1;net8.0;net9.0;net10.0 + enable + enable + latest + true + DataFuse MongoDB Adapter + Code Shayk + Code Shayk + datafuse data-mapping query-engine data-mapper schema-mapping datafuse-mongodb mongodb data-aggregator + https://github.com/CodeShayk/DataFuse.Net + git + README.md + data-integration-transparent.png + https://github.com/CodeShayk/DataFuse.Net/wiki + Copyright (c) 2026 Code Shayk + DataFuse is a powerful .NET library designed to aggregate data from heterogeneous data stores using a schema-driven approach. This package provides the MongoDB adapter for querying MongoDB collections. + LICENSE.md + True + True + snupkg + True + 3.0.0 + v3.0.0 +- New MongoDB adapter for DataFuse data aggregation framework. +- Multi-target framework support: netstandard2.1, net8.0, net9.0, net10.0. +- MongoDB Driver 3.4.0 based query execution. + + + + + True + + + + + True + \ + + + True + \ + + + True + \ + + + + + + + + + + + + diff --git a/src/DataFuse.Adapters.MongoDB/Guard.cs b/src/DataFuse.Adapters.MongoDB/Guard.cs new file mode 100644 index 0000000..1d74fda --- /dev/null +++ b/src/DataFuse.Adapters.MongoDB/Guard.cs @@ -0,0 +1,15 @@ +namespace DataFuse.Adapters.MongoDB; + +internal static class Guard +{ +#if NET6_0_OR_GREATER + internal static void ThrowIfNull(object? argument, string? paramName = null) + => ArgumentNullException.ThrowIfNull(argument, paramName); +#else + internal static void ThrowIfNull(object? argument, string? paramName = null) + { + if (argument is null) + throw new ArgumentNullException(paramName); + } +#endif +} diff --git a/src/DataFuse.Adapters.MongoDB/IMongoQuery.cs b/src/DataFuse.Adapters.MongoDB/IMongoQuery.cs new file mode 100644 index 0000000..494059a --- /dev/null +++ b/src/DataFuse.Adapters.MongoDB/IMongoQuery.cs @@ -0,0 +1,9 @@ +using DataFuse.Adapters.Abstraction; +using MongoDB.Driver; + +namespace DataFuse.Adapters.MongoDB; + +public interface IMongoQuery : IQuery +{ + Task Run(IMongoDatabase database); +} diff --git a/src/DataFuse.Adapters.MongoDB/MongoQuery.cs b/src/DataFuse.Adapters.MongoDB/MongoQuery.cs new file mode 100644 index 0000000..e993915 --- /dev/null +++ b/src/DataFuse.Adapters.MongoDB/MongoQuery.cs @@ -0,0 +1,27 @@ +using DataFuse.Adapters.Abstraction; +using MongoDB.Driver; + +namespace DataFuse.Adapters.MongoDB; + +public abstract class MongoQuery : BaseQuery, IMongoQuery + where TQueryResult : IQueryResult +{ + async Task IMongoQuery.Run(IMongoDatabase database) + { + return await QueryDelegate!(database); + } + + private Func>? QueryDelegate; + + public override bool IsContextResolved() => QueryDelegate is not null; + + public override void ResolveQuery(IDataContext context, IQueryResult? parentQueryResult) + { + QueryDelegate = GetQuery(context, parentQueryResult); + } + + /// + /// Get query delegate to return query result from MongoDB. + /// + protected abstract Func> GetQuery(IDataContext context, IQueryResult? parentQueryResult); +} diff --git a/src/DataFuse.Adapters.MongoDB/QueryEngine.cs b/src/DataFuse.Adapters.MongoDB/QueryEngine.cs new file mode 100644 index 0000000..91873a1 --- /dev/null +++ b/src/DataFuse.Adapters.MongoDB/QueryEngine.cs @@ -0,0 +1,22 @@ +using DataFuse.Adapters.Abstraction; +using MongoDB.Driver; + +namespace DataFuse.Adapters.MongoDB; + +public class QueryEngine : IQueryEngine +{ + private readonly IMongoDatabase _database; + + public QueryEngine(IMongoDatabase database) + { + Guard.ThrowIfNull(database, "MongoDB database instance is required."); + _database = database; + } + + public bool CanExecute(IQuery query) => query is IMongoQuery; + + public async Task Execute(IQuery query) + { + return await ((IMongoQuery)query).Run(_database); + } +} diff --git a/src/DataFuse.Adapters.SQL/DataFuse.Adapters.SQL.csproj b/src/DataFuse.Adapters.SQL/DataFuse.Adapters.SQL.csproj new file mode 100644 index 0000000..fc43807 --- /dev/null +++ b/src/DataFuse.Adapters.SQL/DataFuse.Adapters.SQL.csproj @@ -0,0 +1,61 @@ + + + + netstandard2.1;net8.0;net9.0;net10.0 + enable + enable + latest + true + DataFuse SQL Adapter + Code Shayk + Code Shayk + datafuse data-mapping query-engine data-mapper schema-mapping datafuse-sql dapper-sql dapper data-aggregator + https://github.com/CodeShayk/DataFuse.Net + git + README.md + data-integration-transparent.png + https://github.com/CodeShayk/DataFuse.Net/wiki + Copyright (c) 2026 Code Shayk + DataFuse is a powerful .NET library designed to aggregate data from heterogeneous data stores using a schema-driven approach. This package provides the SQL adapter with Dapper support for querying SQL databases. + LICENSE.md + True + True + snupkg + True + 3.0.0 + v3.0.0 +- Multi-target framework support: netstandard2.1, net8.0, net9.0, net10.0. +- Pre and Post Transform hooks for data transformation pipeline. +- Dapper-based SQL query execution. + + + + + True + + + + + True + \ + + + True + \ + + + True + \ + + + + + + + + + + + + + diff --git a/src/DataFuse.Adapters.SQL/Guard.cs b/src/DataFuse.Adapters.SQL/Guard.cs new file mode 100644 index 0000000..6e47f7d --- /dev/null +++ b/src/DataFuse.Adapters.SQL/Guard.cs @@ -0,0 +1,15 @@ +namespace DataFuse.Adapters.SQL; + +internal static class Guard +{ +#if NET6_0_OR_GREATER + internal static void ThrowIfNull(object? argument, string? paramName = null) + => ArgumentNullException.ThrowIfNull(argument, paramName); +#else + internal static void ThrowIfNull(object? argument, string? paramName = null) + { + if (argument is null) + throw new ArgumentNullException(paramName); + } +#endif +} diff --git a/src/DataFuse.Adapters.SQL/ISQLQuery.cs b/src/DataFuse.Adapters.SQL/ISQLQuery.cs new file mode 100644 index 0000000..e0a29bd --- /dev/null +++ b/src/DataFuse.Adapters.SQL/ISQLQuery.cs @@ -0,0 +1,9 @@ +using System.Data; +using DataFuse.Adapters.Abstraction; + +namespace DataFuse.Adapters.SQL; + +public interface ISQLQuery : IQuery +{ + Task Run(IDbConnection conn); +} diff --git a/src/DataFuse.Adapters.SQL/QueryEngine.cs b/src/DataFuse.Adapters.SQL/QueryEngine.cs new file mode 100644 index 0000000..886e21a --- /dev/null +++ b/src/DataFuse.Adapters.SQL/QueryEngine.cs @@ -0,0 +1,37 @@ +using System.Data.Common; +using DataFuse.Adapters.Abstraction; + +namespace DataFuse.Adapters.SQL; + +public class QueryEngine : IQueryEngine +{ + private readonly SQLConfiguration sqlConfiguration; + + public QueryEngine(SQLConfiguration sqlConfiguration) + { + Guard.ThrowIfNull(sqlConfiguration?.ConnectionSettings?.ProviderName, + "SQL Configuration is required with connection settings. Provider name is missing."); + + Guard.ThrowIfNull(sqlConfiguration?.ConnectionSettings?.ConnectionString, + "SQL Configuration is required with connection settings. Connection string is missing."); + + this.sqlConfiguration = sqlConfiguration!; + } + + public bool CanExecute(IQuery query) => query is ISQLQuery; + + public async Task Execute(IQuery query) + { + var factory = DbProviderFactories.GetFactory(sqlConfiguration.ConnectionSettings.ProviderName) + ?? throw new InvalidOperationException($"Provider: {sqlConfiguration.ConnectionSettings.ProviderName} is not supported. Please register entry in DbProviderFactories "); + + var connection = factory.CreateConnection() + ?? throw new InvalidOperationException($"Failed to create connection with Provider: {sqlConfiguration.ConnectionSettings.ProviderName}. Please check the connection settings."); + + await using (connection) + { + connection.ConnectionString = sqlConfiguration.ConnectionSettings.ConnectionString; + return await ((ISQLQuery)query).Run(connection); + } + } +} diff --git a/src/DataFuse.Adapters.SQL/SQLQuery.cs b/src/DataFuse.Adapters.SQL/SQLQuery.cs new file mode 100644 index 0000000..ddd3e16 --- /dev/null +++ b/src/DataFuse.Adapters.SQL/SQLQuery.cs @@ -0,0 +1,27 @@ +using System.Data; +using DataFuse.Adapters.Abstraction; + +namespace DataFuse.Adapters.SQL; + +public abstract class SQLQuery : BaseQuery, ISQLQuery + where TQueryResult : IQueryResult +{ + async Task ISQLQuery.Run(IDbConnection conn) + { + return await QueryDelegate!(conn); + } + + private Func>? QueryDelegate; + + public override bool IsContextResolved() => QueryDelegate is not null; + + public override void ResolveQuery(IDataContext context, IQueryResult? parentQueryResult) + { + QueryDelegate = GetQuery(context, parentQueryResult); + } + + /// + /// Get query delegate to return query result. + /// + protected abstract Func> GetQuery(IDataContext context, IQueryResult? parentQueryResult); +} diff --git a/src/DataFuse.Adapters.SQL/SqlConfiguration.cs b/src/DataFuse.Adapters.SQL/SqlConfiguration.cs new file mode 100644 index 0000000..f002ee7 --- /dev/null +++ b/src/DataFuse.Adapters.SQL/SqlConfiguration.cs @@ -0,0 +1,24 @@ +namespace DataFuse.Adapters.SQL; + +public class SQLConfiguration +{ + public SQLConfiguration() + { + ConnectionSettings = new ConnectionSettings(); + QuerySettings = new QuerySettings(); + } + + public ConnectionSettings ConnectionSettings { get; set; } + public QuerySettings QuerySettings { get; set; } +} + +public class ConnectionSettings +{ + public string ProviderName { get; set; } = null!; + public string ConnectionString { get; set; } = null!; +} + +public class QuerySettings +{ + public int QueryBatchSize { get; set; } = 10; +} diff --git a/src/DataFuse.Adapters.WebAPI/DataFuse.Adapters.WebAPI.csproj b/src/DataFuse.Adapters.WebAPI/DataFuse.Adapters.WebAPI.csproj new file mode 100644 index 0000000..defa3db --- /dev/null +++ b/src/DataFuse.Adapters.WebAPI/DataFuse.Adapters.WebAPI.csproj @@ -0,0 +1,58 @@ + + + + netstandard2.1;net8.0;net9.0;net10.0 + enable + latest + true + enable + DataFuse Web API Adapter + Code Shayk + Code Shayk + DataFuse is a powerful .NET library designed to aggregate data from heterogeneous data stores using a schema-driven approach. This package provides the Web API adapter using HttpClient. + Copyright (c) 2026 Code Shayk + https://github.com/CodeShayk/DataFuse.Net/wiki + data-integration-transparent.png + README.md + https://github.com/CodeShayk/DataFuse.Net + git + datafuse data-mapping query-engine data-mapper schema-mapping datafuse-webapi data-aggregator + LICENSE.md + True + True + snupkg + True + 3.0.0 + v3.0.0 +- Multi-target framework support: netstandard2.1, net8.0, net9.0, net10.0. +- Pre and Post Transform hooks for data transformation pipeline. +- HttpClient-based Web API query execution. + + + + + True + + + + + True + \ + + + True + \ + + + True + \ + + + + + + + + + + diff --git a/src/DataFuse.Adapters.WebAPI/Guard.cs b/src/DataFuse.Adapters.WebAPI/Guard.cs new file mode 100644 index 0000000..61232d0 --- /dev/null +++ b/src/DataFuse.Adapters.WebAPI/Guard.cs @@ -0,0 +1,15 @@ +namespace DataFuse.Adapters.WebAPI; + +internal static class Guard +{ +#if NET6_0_OR_GREATER + internal static void ThrowIfNull(object? argument, string? paramName = null) + => ArgumentNullException.ThrowIfNull(argument, paramName); +#else + internal static void ThrowIfNull(object? argument, string? paramName = null) + { + if (argument is null) + throw new ArgumentNullException(paramName); + } +#endif +} diff --git a/src/DataFuse.Adapters.WebAPI/IWebQuery.cs b/src/DataFuse.Adapters.WebAPI/IWebQuery.cs new file mode 100644 index 0000000..af0389d --- /dev/null +++ b/src/DataFuse.Adapters.WebAPI/IWebQuery.cs @@ -0,0 +1,9 @@ +using Microsoft.Extensions.Logging; +using DataFuse.Adapters.Abstraction; + +namespace DataFuse.Adapters.WebAPI; + +public interface IWebQuery : IQuery +{ + Task Run(IHttpClientFactory httpClientFactory, ILogger? logger = null); +} diff --git a/src/DataFuse.Adapters.WebAPI/QueryEngine.cs b/src/DataFuse.Adapters.WebAPI/QueryEngine.cs new file mode 100644 index 0000000..6e9591d --- /dev/null +++ b/src/DataFuse.Adapters.WebAPI/QueryEngine.cs @@ -0,0 +1,27 @@ +using Microsoft.Extensions.Logging; +using DataFuse.Adapters.Abstraction; + +namespace DataFuse.Adapters.WebAPI; + +public class QueryEngine : IQueryEngine +{ + private readonly ILogger? logger; + private readonly IHttpClientFactory httpClientFactory; + + public QueryEngine(IHttpClientFactory httpClientFactory, ILogger? logger = null) + { + Guard.ThrowIfNull(httpClientFactory); + this.httpClientFactory = httpClientFactory; + this.logger = logger; + } + + public bool CanExecute(IQuery query) => query is IWebQuery; + + public async Task Execute(IQuery query) + { + if (query is not IWebQuery q) + return null!; + + return await q.Run(httpClientFactory, logger); + } +} diff --git a/src/DataFuse.Adapters.WebAPI/WebHeaderResult.cs b/src/DataFuse.Adapters.WebAPI/WebHeaderResult.cs new file mode 100644 index 0000000..cd00121 --- /dev/null +++ b/src/DataFuse.Adapters.WebAPI/WebHeaderResult.cs @@ -0,0 +1,11 @@ +using DataFuse.Adapters.Abstraction; + +namespace DataFuse.Adapters.WebAPI; + +/// +/// Implement to return web query response with headers. +/// +public abstract class WebHeaderResult : IQueryResult +{ + public IDictionary? Headers { get; internal set; } +} diff --git a/src/DataFuse.Adapters.WebAPI/WebQuery.cs b/src/DataFuse.Adapters.WebAPI/WebQuery.cs new file mode 100644 index 0000000..03f3552 --- /dev/null +++ b/src/DataFuse.Adapters.WebAPI/WebQuery.cs @@ -0,0 +1,171 @@ +using Microsoft.Extensions.Logging; +using DataFuse.Adapters.Abstraction; + +namespace DataFuse.Adapters.WebAPI; + +public abstract class WebQuery : BaseQuery, IWebQuery + where TQueryResult : IQueryResult +{ + protected Uri? BaseAddress; + + protected WebQuery() : this(string.Empty) + { + } + + protected WebQuery(string baseAddress) + { + if (!string.IsNullOrEmpty(baseAddress)) + BaseAddress = new Uri(baseAddress); + } + + private Func? UriDelegate = null; + + public override bool IsContextResolved() => UriDelegate is not null; + + public override void ResolveQuery(IDataContext context, IQueryResult parentQueryResult) + { + UriDelegate = GetQuery(context, parentQueryResult); + } + + /// + /// Override to pass custom outgoing headers with the api request. + /// + protected virtual IDictionary GetRequestHeaders() + { + return new Dictionary(); + } + + /// + /// Override to get custom incoming headers with the api response. + /// The headers collection will be present on `WebHeaderResult.Headers` when api response includes any of the headers defined in this method. + /// + protected virtual IEnumerable GetResponseHeaders() + { + return []; + } + + /// + /// Implement to construct the api web query. + /// + /// Request Context. Always available. + /// Result from parent Query. Only available when configured as nested web query. Else will be null. + protected abstract Func GetQuery(IDataContext context, IQueryResult? parentApiResult = null); + + async Task IWebQuery.Run(IHttpClientFactory httpClientFactory, ILogger? logger) + { + Guard.ThrowIfNull(httpClientFactory); + + logger?.LogInformation("Run api: {QueryName}", GetType().Name); + + var uri = UriDelegate!(); + + if (uri is null) + return null!; + + using var client = httpClientFactory.CreateClient(); + + logger?.LogInformation("Executing web api on thread {ThreadId} (task {TaskId})", + Thread.CurrentThread.ManagedThreadId, Task.CurrentId); + + try + { + try + { + if (BaseAddress is not null) + client.BaseAddress = BaseAddress; + + var requestHeaders = GetRequestHeaders(); + + if (requestHeaders is { Count: > 0 }) + foreach (var header in requestHeaders) + client.DefaultRequestHeaders.Add(header.Key, header.Value); + + var result = await client.GetAsync(uri); + + var raw = await result.Content.ReadAsStringAsync(); + + if (!string.IsNullOrWhiteSpace(raw)) + logger?.LogInformation("Result.Content of executing web api: {Path} is {Content}", uri.AbsolutePath, raw); + + if (!result.IsSuccessStatusCode) + { + logger?.LogInformation("Result of executing web api {Path} is not success status code", uri.AbsolutePath); + return null!; + } + + if (typeof(TQueryResult).UnderlyingSystemType?.Name.Equals(typeof(CollectionResult<>).Name) == true) + { + var typeArgs = typeof(TQueryResult).GetGenericArguments(); + var arrType = typeArgs[0].MakeArrayType(); + var arrObject = System.Text.Json.JsonSerializer.Deserialize(raw, arrType); + if (arrObject is not null) + { + var resultType = typeof(CollectionResult<>); + var collectionType = resultType.MakeGenericType(typeArgs); + var collectionResult = (TQueryResult)Activator.CreateInstance(collectionType, arrObject)!; + + SetResponseHeaders(result, collectionResult); + + return collectionResult; + } + } + else + { + var obj = System.Text.Json.JsonSerializer.Deserialize(raw, typeof(TQueryResult)); + if (obj is not null) + { + var resObj = (TQueryResult)obj; + SetResponseHeaders(result, resObj); + return resObj; + } + } + } + catch (TaskCanceledException ex) + { + logger?.LogWarning(ex, "An error occurred while sending the request. Query URL: {Path}", uri.AbsolutePath); + } + catch (HttpRequestException ex) + { + logger?.LogWarning(ex, "An error occurred while sending the request. Query URL: {Path}", uri.AbsolutePath); + } + } + catch (AggregateException ex) + { + logger?.LogInformation("Web api {QueryName} failed", GetType().Name); + foreach (var e in ex.InnerExceptions) + logger?.LogError(e, ""); + } + + return null!; + } + + private void SetResponseHeaders(HttpResponseMessage response, TQueryResult result) + { + if (response.Headers is null || result is null) + return; + + var headers = GetResponseHeaders(); + + if (headers is null || !headers.Any()) + return; + + if (result is not WebHeaderResult webResult) + throw new InvalidOperationException($"{typeof(TQueryResult).Name} should implement from WebHeaderResult for response Headers"); + + foreach (var header in headers) + { + if (!response.Headers.Any(r => r.Key == header)) + continue; + + var responseHeader = response.Headers.First(r => r.Key == header); + + var value = responseHeader.Value is not null && responseHeader.Value.Any() + ? responseHeader.Value.First() + : string.Empty; + + webResult.Headers ??= new Dictionary(); + + webResult.Headers.Add(responseHeader.Key, value); + } + } +} diff --git a/src/Schemio.Core/AssemblyInfo.cs b/src/DataFuse.Integration/AssemblyInfo.cs similarity index 80% rename from src/Schemio.Core/AssemblyInfo.cs rename to src/DataFuse.Integration/AssemblyInfo.cs index 462744b..55fdf53 100644 --- a/src/Schemio.Core/AssemblyInfo.cs +++ b/src/DataFuse.Integration/AssemblyInfo.cs @@ -11,9 +11,9 @@ // attribute to true on that type. [assembly: ComVisible(false)] -[assembly: InternalsVisibleTo("Schemio.Core.Tests")] -[assembly: InternalsVisibleTo("Schemio.SQL.Tests")] -[assembly: InternalsVisibleTo("Schemio.EntityFramework.Tests")] +[assembly: InternalsVisibleTo("DataFuse.Integration.Tests")] +[assembly: InternalsVisibleTo("DataFuse.Adapters.SQL.Tests")] +[assembly: InternalsVisibleTo("DataFuse.Adapters.EntityFramework.Tests")] // The following GUID is for the ID of the typelib if this project is exposed to COM. diff --git a/src/DataFuse.Integration/DataContext.cs b/src/DataFuse.Integration/DataContext.cs new file mode 100644 index 0000000..9e654ea --- /dev/null +++ b/src/DataFuse.Integration/DataContext.cs @@ -0,0 +1,15 @@ +using DataFuse.Adapters.Abstraction; + +namespace DataFuse.Integration; + +internal class DataContext : IDataContext +{ + public DataContext(IEntityRequest request) + { + Request = request; + Cache = new Dictionary(); + } + + public Dictionary Cache { get; set; } + public IEntityRequest Request { get; set; } +} diff --git a/src/DataFuse.Integration/DataFuse.Integration.csproj b/src/DataFuse.Integration/DataFuse.Integration.csproj new file mode 100644 index 0000000..175d2af --- /dev/null +++ b/src/DataFuse.Integration/DataFuse.Integration.csproj @@ -0,0 +1,58 @@ + + + + netstandard2.1;net8.0;net9.0;net10.0 + enable + enable + latest + true + Code Shayk + Code Shayk + Copyright (c) 2026 Code Shayk + https://github.com/CodeShayk/DataFuse.Net + git + README.md + data-integration-transparent.png + DataFuse is a powerful .NET library designed to aggregate data from heterogeneous data stores using a schema-driven approach. It enables developers to hydrate complex object graphs by fetching data from multiple sources (SQL databases, Web APIs, NoSQL stores) using XPath and JSONPath schema mappings. + graphql data json-schema xsd data-mapping query-engine data-mapper data-schema schema-mapping object-tree-query object-graph-query schema-mapper xsd-data-object object-graph-data entity-data entity-data-fetch hydrate-object object-hydration object-data object-fetch datafuse datafuse-integration data-aggregator + https://github.com/CodeShayk/DataFuse.Net/wiki + LICENSE.md + DataFuse Integration + True + True + snupkg + True + 3.0.0 + v3.0.0 +- Multi-target framework support: netstandard2.1, net8.0, net9.0, net10.0. +- Pre and Post Transform hooks for data transformation pipeline. +- XPath and JSONPath schema-driven data aggregation. + + + + + + True + \ + + + True + \ + + + True + \ + + + + + + + + + + + + + + diff --git a/src/DataFuse.Integration/DataFuseOptionsBuilder.cs b/src/DataFuse.Integration/DataFuseOptionsBuilder.cs new file mode 100644 index 0000000..d031ac9 --- /dev/null +++ b/src/DataFuse.Integration/DataFuseOptionsBuilder.cs @@ -0,0 +1,105 @@ +using DataFuse.Adapters.Abstraction; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Logging; +using DataFuse.Integration.Impl; + +namespace DataFuse.Integration; + +/// +/// Builder for DataFuse options. +/// +public class DataFuseOptionsBuilder : IDataFuseOptions +{ + public DataFuseOptionsBuilder(IServiceCollection services) + { + Services = services; + } + + public IServiceCollection Services { get; } + public int EntityConfigurationCount { get; private set; } + public int EngineCount { get; private set; } + internal bool Silent { get; set; } + + IDataFuseOptions IDataFuseOptions.InSilentMode() + { + Silent = true; + return this; + } + + public IDataFuseOptions WithEngine(Func queryEngine) + { + if (queryEngine is not null) + { + Services.AddScoped(c => queryEngine(c)); + EngineCount++; + } + + return this; + } + + public IDataFuseOptions WithEngine() where TEngine : IQueryEngine + { + Services.AddScoped(typeof(IQueryEngine), typeof(TEngine)); + EngineCount++; + + return this; + } + + public IDataFuseOptions WithEngines(Func queryEngines) + { + if (queryEngines is not null) + { + Services.AddScoped(c => + { + var engines = queryEngines(c); + EngineCount += engines.Length; + return engines; + }); + } + + return this; + } + + /// + /// Register an instance of ISchemaPathMatcher. Default is XPathMatcher. + /// + public IDataFuseOptions WithPathMatcher(Func pathMatcher) + { + if (pathMatcher is not null) + { + Services.RemoveAll(); + Services.AddScoped(c => pathMatcher(c)); + } + + return this; + } + + /// + /// Register an instance of EntityConfiguration for the given entity type. + /// You could register configuration for multiple entities. + /// + public IDataFuseOptions WithEntityConfiguration(Func> entityConfiguration) + where TEntity : class, IEntity, new() + { + if (entityConfiguration is not null) + { + Services.AddTransient(typeof(IEntityConfiguration), c => entityConfiguration(c)); + + Services.AddTransient, QueryBuilder>(c => new QueryBuilder(c.GetService>()!, c.GetService()!)); + Services.AddTransient, EntityBuilder>(c => new EntityBuilder(c.GetService>()!)); + + Services.AddTransient, DataProvider>( + c => new DataProvider( + logger: c.GetService>>(), + queryBuilder: c.GetService>()!, + queryExecutor: c.GetService()!, + entityBuilder: c.GetService>()! + )); + + EntityConfigurationCount++; + } + + return this; + } +} diff --git a/src/DataFuse.Integration/Guard.cs b/src/DataFuse.Integration/Guard.cs new file mode 100644 index 0000000..5cdf380 --- /dev/null +++ b/src/DataFuse.Integration/Guard.cs @@ -0,0 +1,15 @@ +namespace DataFuse.Integration; + +internal static class Guard +{ +#if NET6_0_OR_GREATER + internal static void ThrowIfNull(object? argument, string? paramName = null) + => ArgumentNullException.ThrowIfNull(argument, paramName); +#else + internal static void ThrowIfNull(object? argument, string? paramName = null) + { + if (argument is null) + throw new ArgumentNullException(paramName); + } +#endif +} diff --git a/src/DataFuse.Integration/Helpers/ArrayUtil.cs b/src/DataFuse.Integration/Helpers/ArrayUtil.cs new file mode 100644 index 0000000..c46c47e --- /dev/null +++ b/src/DataFuse.Integration/Helpers/ArrayUtil.cs @@ -0,0 +1,27 @@ +namespace DataFuse.Integration.Helpers; + +public static class ArrayUtil +{ + public static T[] EnsureAndResizeArray(T[]? val, out int index) + { + return EnsureAndResizeArray(val, 1, out index); + } + + public static T[] EnsureAndResizeArray(T[]? val, int extendBy, out int index) + { + if (val is null) + { + val = new T[extendBy]; + index = 0; + } + else + { + var temp = val; + index = temp.Length; + Array.Resize(ref temp, temp.Length + extendBy); + val = temp; + } + + return val; + } +} diff --git a/src/DataFuse.Integration/Helpers/Constraints.cs b/src/DataFuse.Integration/Helpers/Constraints.cs new file mode 100644 index 0000000..b3039be --- /dev/null +++ b/src/DataFuse.Integration/Helpers/Constraints.cs @@ -0,0 +1,9 @@ +namespace DataFuse.Integration.Helpers; + +public static class Constraints +{ + public static void NotNull(this T value) + { + Guard.ThrowIfNull(value, typeof(T).Name); + } +} diff --git a/src/DataFuse.Integration/Helpers/EnumerableExtentions.cs b/src/DataFuse.Integration/Helpers/EnumerableExtentions.cs new file mode 100644 index 0000000..98469ec --- /dev/null +++ b/src/DataFuse.Integration/Helpers/EnumerableExtentions.cs @@ -0,0 +1,36 @@ +using System.Text; + +namespace DataFuse.Integration.Helpers; + +public static class EnumerableExtentions +{ + public static void Each(this IEnumerable enumerable, Action action) + { + Guard.ThrowIfNull(enumerable, nameof(enumerable)); + Guard.ThrowIfNull(action, nameof(action)); + + foreach (var item in enumerable) + action(item); + } + + public static string? ToCSV(this IEnumerable instance, char separator) + { + if (instance is null) + return null; + + if (!instance.Any()) + return string.Empty; + + var csv = new StringBuilder(); + instance.Each(value => csv.AppendFormat("{0}{1}", value, separator)); + return csv.ToString(0, csv.Length - 1); + } + + public static string? ToCSV(this IEnumerable instance) + { + if (instance is null) + return null; + + return instance.ToCSV(','); + } +} diff --git a/src/DataFuse.Integration/Helpers/JsonExtensions.cs b/src/DataFuse.Integration/Helpers/JsonExtensions.cs new file mode 100644 index 0000000..f41db97 --- /dev/null +++ b/src/DataFuse.Integration/Helpers/JsonExtensions.cs @@ -0,0 +1,10 @@ +using System.Text.Json; + +namespace DataFuse.Integration.Helpers; + +public static class JsonExtensions +{ + public static string? ToJson(this object? value) => value is not null ? JsonSerializer.Serialize(value) : null; + + public static object? ToObject(this string? value, Type type) => !string.IsNullOrEmpty(value) ? JsonSerializer.Deserialize(value, type) : null; +} diff --git a/src/DataFuse.Integration/Helpers/StringExtensions.cs b/src/DataFuse.Integration/Helpers/StringExtensions.cs new file mode 100644 index 0000000..6b14d30 --- /dev/null +++ b/src/DataFuse.Integration/Helpers/StringExtensions.cs @@ -0,0 +1,6 @@ +namespace DataFuse.Integration.Helpers; + +public static class StringExtensions +{ + public static bool IsNotNullOrEmpty(this string? value) => !string.IsNullOrEmpty(value); +} diff --git a/src/DataFuse.Integration/Helpers/Xml/XDocumentExts.cs b/src/DataFuse.Integration/Helpers/Xml/XDocumentExts.cs new file mode 100644 index 0000000..06cd4d5 --- /dev/null +++ b/src/DataFuse.Integration/Helpers/Xml/XDocumentExts.cs @@ -0,0 +1,24 @@ +using System.Xml.Linq; + +namespace DataFuse.Integration.Helpers.Xml; + +public static class XDocumentExts +{ + public static void RemoveNamespaces(this XDocument document) + { + if (document.Root is null) + return; + foreach (var xElement in document.Root.DescendantsAndSelf()) + { + if (xElement.Name.Namespace != XNamespace.None) + xElement.Name = XNamespace.None.GetName(xElement.Name.LocalName); + if (xElement.Attributes().Any(a => a.IsNamespaceDeclaration || a.Name.Namespace != XNamespace.None)) + xElement.ReplaceAttributes(xElement.Attributes().Select(a => + { + if (a.IsNamespaceDeclaration) + return null; + return !(a.Name.Namespace != XNamespace.None) ? a : new XAttribute(XNamespace.None.GetName(a.Name.LocalName), a.Value); + })); + } + } +} diff --git a/src/DataFuse.Integration/Helpers/Xml/XmlHelper.cs b/src/DataFuse.Integration/Helpers/Xml/XmlHelper.cs new file mode 100644 index 0000000..c35a575 --- /dev/null +++ b/src/DataFuse.Integration/Helpers/Xml/XmlHelper.cs @@ -0,0 +1,21 @@ +using System.Text; +using System.Xml; +using System.Xml.Serialization; + +namespace DataFuse.Integration.Helpers.Xml; + +public static class XmlHelper +{ + public static string SerializeToXml(T value, XmlSerializerNamespaces namespaces, XmlWriterSettings settings) + { + Guard.ThrowIfNull(value, nameof(value)); + Guard.ThrowIfNull(settings, nameof(settings)); + + var xmlStr = new StringBuilder(); + var x = new XmlSerializer(typeof(T)); + + using var writer = XmlWriter.Create(xmlStr, settings); + x.Serialize(writer, value, namespaces); + return xmlStr.ToString(); + } +} diff --git a/src/DataFuse.Integration/Helpers/Xml/XmlSanitizer.cs b/src/DataFuse.Integration/Helpers/Xml/XmlSanitizer.cs new file mode 100644 index 0000000..13af7c4 --- /dev/null +++ b/src/DataFuse.Integration/Helpers/Xml/XmlSanitizer.cs @@ -0,0 +1,40 @@ +using System.Globalization; +using System.Text.RegularExpressions; +using System.Xml; + +namespace DataFuse.Integration.Helpers.Xml; + +public static partial class XmlSanitizer +{ +#if NET7_0_OR_GREATER + [GeneratedRegex("&(#)?([a-zA-Z0-9]*);")] + private static partial Regex XmlEntityRegex(); +#else + private static readonly Regex _xmlEntityRegex = new("&(#)?([a-zA-Z0-9]*);"); + private static Regex XmlEntityRegex() => _xmlEntityRegex; +#endif + + public static string Sanitize(string xml) + { + if (string.IsNullOrEmpty(xml)) + return xml; + + return XmlEntityRegex().Replace(xml, x => + { + if (x.Groups[1].Value != "#") + return x.Groups[0].Value; + + var strChar = x.Groups[2].Value; + + if (strChar.Contains('x')) + strChar = strChar.Replace("x", string.Empty).Trim(); + + if (!int.TryParse(strChar, NumberStyles.AllowHexSpecifier, null, out var intChar)) + return x.Groups[0].Value; + + return !XmlConvert.IsXmlChar((char)intChar) + ? string.Empty + : x.Groups[0].Value; + }); + } +} diff --git a/src/DataFuse.Integration/IDataFuseOptions.cs b/src/DataFuse.Integration/IDataFuseOptions.cs new file mode 100644 index 0000000..75af43d --- /dev/null +++ b/src/DataFuse.Integration/IDataFuseOptions.cs @@ -0,0 +1,40 @@ +using DataFuse.Adapters.Abstraction; + +namespace DataFuse.Integration; + +/// +/// Interface for DataFuse options builder. +/// +public interface IDataFuseOptions +{ + /// + /// Register a query engine. + /// + IDataFuseOptions WithEngine(Func queryEngines); + + /// + /// Register a query engine of type . + /// + IDataFuseOptions WithEngine() where TEngine : IQueryEngine; + + /// + /// Register an array of query engines. + /// + IDataFuseOptions WithEngines(Func queryEngines); + + /// + /// Register an instance of ISchemaPathMatcher. Default is XPathMatcher. + /// + IDataFuseOptions WithPathMatcher(Func pathMatcher); + + /// + /// Register an instance of EntityConfiguration for the given entity type. + /// + IDataFuseOptions WithEntityConfiguration(Func> entityConfiguration) + where TEntity : class, IEntity, new(); + + /// + /// Set DataFuse to silent mode. In this mode, no exceptions will be thrown if there are no entity configurations or query engines registered. + /// + IDataFuseOptions InSilentMode(); +} diff --git a/src/DataFuse.Integration/IDataProvider.cs b/src/DataFuse.Integration/IDataProvider.cs new file mode 100644 index 0000000..b29559a --- /dev/null +++ b/src/DataFuse.Integration/IDataProvider.cs @@ -0,0 +1,8 @@ +using DataFuse.Adapters.Abstraction; + +namespace DataFuse.Integration; + +public interface IDataProvider where TEntity : IEntity +{ + Task GetDataAsync(IEntityRequest request); +} diff --git a/src/DataFuse.Integration/IEntityBuilder.cs b/src/DataFuse.Integration/IEntityBuilder.cs new file mode 100644 index 0000000..68b72ba --- /dev/null +++ b/src/DataFuse.Integration/IEntityBuilder.cs @@ -0,0 +1,8 @@ +using DataFuse.Adapters.Abstraction; + +namespace DataFuse.Integration; + +public interface IEntityBuilder where TEntity : IEntity +{ + TEntity Build(IDataContext context, IList results); +} diff --git a/src/DataFuse.Integration/IEntityContextValidator.cs b/src/DataFuse.Integration/IEntityContextValidator.cs new file mode 100644 index 0000000..36b89e2 --- /dev/null +++ b/src/DataFuse.Integration/IEntityContextValidator.cs @@ -0,0 +1,8 @@ +using DataFuse.Adapters.Abstraction; + +namespace DataFuse.Integration; + +public interface IEntityContextValidator +{ + void Validate(IEntityRequest context); +} diff --git a/src/DataFuse.Integration/IQueryBuilder.cs b/src/DataFuse.Integration/IQueryBuilder.cs new file mode 100644 index 0000000..3ad650f --- /dev/null +++ b/src/DataFuse.Integration/IQueryBuilder.cs @@ -0,0 +1,8 @@ +using DataFuse.Adapters.Abstraction; + +namespace DataFuse.Integration; + +public interface IQueryBuilder +{ + IQueryList Build(IDataContext context); +} diff --git a/src/DataFuse.Integration/IQueryExecutor.cs b/src/DataFuse.Integration/IQueryExecutor.cs new file mode 100644 index 0000000..f65885b --- /dev/null +++ b/src/DataFuse.Integration/IQueryExecutor.cs @@ -0,0 +1,8 @@ +using DataFuse.Adapters.Abstraction; + +namespace DataFuse.Integration; + +public interface IQueryExecutor +{ + Task> ExecuteAsync(IDataContext context, IQueryList queries); +} diff --git a/src/DataFuse.Integration/Impl/DataProvider.cs b/src/DataFuse.Integration/Impl/DataProvider.cs new file mode 100644 index 0000000..b261c6a --- /dev/null +++ b/src/DataFuse.Integration/Impl/DataProvider.cs @@ -0,0 +1,75 @@ +using System.Diagnostics; +using Microsoft.Extensions.Logging; +using DataFuse.Adapters.Abstraction; +using DataFuse.Integration.PathMatchers; + +namespace DataFuse.Integration.Impl; + +public class DataProvider : IDataProvider + where TEntity : IEntity, new() +{ + private readonly ILogger>? logger; + private readonly IQueryExecutor queryExecutor; + private readonly IQueryBuilder queryBuilder; + private readonly IEntityBuilder entityBuilder; + + public DataProvider( + IEntityConfiguration entitySchema, + params IQueryEngine[] queryEngines) + : this(null, new QueryBuilder(entitySchema, new XPathMatcher()), + new QueryExecutor(queryEngines), new EntityBuilder(entitySchema)) + { + } + + public DataProvider( + ILogger>? logger, + IEntityConfiguration entitySchema, + ISchemaPathMatcher schemaPathMatcher, + params IQueryEngine[] queryEngines) + : this(logger, new QueryBuilder(entitySchema, schemaPathMatcher), + new QueryExecutor(queryEngines), new EntityBuilder(entitySchema)) + { + } + + public DataProvider( + ILogger>? logger, + IQueryBuilder queryBuilder, + IQueryExecutor queryExecutor, + IEntityBuilder entityBuilder) + { + this.logger = logger; + this.queryBuilder = queryBuilder; + this.queryExecutor = queryExecutor; + this.entityBuilder = entityBuilder; + } + + public Task GetDataAsync(IEntityRequest request) + { + var context = new DataContext(request); + return GetDataAsync(context); + } + + internal async Task GetDataAsync(IDataContext context) + { + // Build queries for the data source based on the included xPaths + var watch = Stopwatch.StartNew(); + var queries = queryBuilder.Build(context); + + watch.Stop(); + logger?.LogInformation("Query builder executed in {ElapsedMs} ms", watch.ElapsedMilliseconds); + + // execute all queries to get results + watch = Stopwatch.StartNew(); + var results = await queryExecutor.ExecuteAsync(context, queries); + watch.Stop(); + logger?.LogInformation("Query executor executed in {ElapsedMs} ms", watch.ElapsedMilliseconds); + + // Executes configured transformers to map query results to target entity + watch = Stopwatch.StartNew(); + var entity = entityBuilder.Build(context, results); + watch.Stop(); + logger?.LogInformation("Transform executor executed in {ElapsedMs} ms", watch.ElapsedMilliseconds); + + return entity; + } +} diff --git a/src/DataFuse.Integration/Impl/EntityBuilder.cs b/src/DataFuse.Integration/Impl/EntityBuilder.cs new file mode 100644 index 0000000..cb806da --- /dev/null +++ b/src/DataFuse.Integration/Impl/EntityBuilder.cs @@ -0,0 +1,85 @@ +using DataFuse.Adapters.Abstraction; + +namespace DataFuse.Integration.Impl; + +public class EntityBuilder : IEntityBuilder where T : IEntity, new() +{ + private readonly IEntityConfiguration entitySchema; + + public EntityBuilder(IEntityConfiguration entitySchema) + { + this.entitySchema = entitySchema; + } + + /// + /// Executes configured transformers to populate target entity using list of query results. + /// + public T Build(IDataContext context, IList queryResults) + { + var entity = new T(); + + if (queryResults is null || queryResults.Count == 0) + return entity; + + var mappings = entitySchema.Mappings.ToList(); + + // resolve context of each transformer so it is available inside for transformation if required. + mappings.ForEach(mapping => (mapping.Transformer as ITransformerContext)?.SetContext(context)); + + var queryDependencyDepth = mappings.Max(x => x.Order); + + // iterate through transformers to build data source by order configured. + for (var index = 1; index <= queryDependencyDepth; index++) + { + var transformers = mappings + .Where(mapping => mapping.Order == index) + .Select(m => m.Transformer) + .Distinct() + .ToList(); + + foreach (var queryResult in queryResults) + transformers.Where(transformer => IsMatch(((ITransformerQueryResult)transformer).SupportedQueryResult, queryResult.GetType())).ToList() + .ForEach(transformer => TransformWithHooks(new TransformContext(context, queryResult, transformer, entity, false))); + } + + return entity; + } + + private static bool IsMatch(Type transformerQueryResult, Type queryResult) + { + return transformerQueryResult == queryResult; + } + + /// + /// Transform method that includes hooks for pre and post transformation. + /// + private static void TransformWithHooks(TransformContext context) + { + // Create a pre-transform context to pass the data before transformation. + var preTransformContext = new PreTransformContext(context); + // Call the pre-transform method with the context. + if (context.Transformer is ITransformerHooks transformerPreHook) + transformerPreHook.PreTransform(preTransformContext); + + // If the pre-transform context is cancelled, skip the transformation. + if (!preTransformContext.IsCancelled) + context.Transformer.Transform(context.QueryResult, context.Entity); + + // Post-transform hook can be used to perform any post-transformation logic if needed. + if (!preTransformContext.IsCancelled && context.Transformer is ITransformerHooks transformerPostHook) + { + // Create a post-transform context to pass the data after transformation. + var postTransformContext = new PostTransformContext(context); + // Call the post-transform method with the context. + transformerPostHook.PostTransform(postTransformContext); + // If the post-transform context is repeat, transform again. + if (!postTransformContext.IsRepeated && postTransformContext.IsRepeat) + { + // Mark as repeated to avoid infinite loop. + postTransformContext.IsRepeated = true; + // Repeat the transformation process. + TransformWithHooks(postTransformContext); + } + } + } +} diff --git a/src/DataFuse.Integration/Impl/EventPublisher.cs b/src/DataFuse.Integration/Impl/EventPublisher.cs new file mode 100644 index 0000000..f10c37f --- /dev/null +++ b/src/DataFuse.Integration/Impl/EventPublisher.cs @@ -0,0 +1,30 @@ +using DataFuse.Adapters.Abstraction; + +namespace DataFuse.Integration.Impl; + +public class EventPublisher +{ + private readonly ISubscriber subscriber; + + public EventPublisher(ISubscriber subscriber) + { + this.subscriber = subscriber; + } + + public void PublishEvent(IDataContext context, ExecutorResultArgs args) => subscriber.OnEventHandler(context, args); +} + +public class ExecutorResultArgs : EventArgs +{ + public ExecutorResultArgs(IEnumerable result) + { + Result = result; + } + + public IEnumerable Result { get; } +} + +public interface ISubscriber +{ + void OnEventHandler(IDataContext context, TArgs e); +} diff --git a/src/DataFuse.Integration/Impl/EventSubscriber.cs b/src/DataFuse.Integration/Impl/EventSubscriber.cs new file mode 100644 index 0000000..baee1dc --- /dev/null +++ b/src/DataFuse.Integration/Impl/EventSubscriber.cs @@ -0,0 +1,85 @@ +using DataFuse.Adapters.Abstraction; + +namespace DataFuse.Integration.Impl; + +public class EventSubscriber : ISubscriber +{ + private readonly IList dependentQueries; + + public EventSubscriber(IList dependentQueries) + { + this.dependentQueries = GetUniqueQueryList(dependentQueries); + } + + public void OnEventHandler(IDataContext context, ExecutorResultArgs args) + { + if (args.Result is null) + return; + + var results = args.Result; + + foreach (var queryResult in results.Where(x => !typeof(IPolymorphicResult).IsAssignableFrom(x.GetType()))) + { + if (dependentQueries.All(tuple => tuple.ParentQueryResultType != queryResult.GetType())) + continue; + + var result = queryResult; + + foreach (var unresolved in dependentQueries) + { + if (unresolved.ParentQueryResultType != result.GetType()) + continue; + + foreach (var query in unresolved.Queries) + query.ResolveQuery(context, queryResult); + } + } + + foreach (var queryResult in results.Where(x => typeof(IPolymorphicResult).IsAssignableFrom(x.GetType()))) + { + if (dependentQueries.All(tuple => tuple.ParentQueryResultType != queryResult.GetType() && + !tuple.ParentQueryResultType.IsAssignableFrom(queryResult.GetType())) + ) + continue; + + var result = queryResult; + + foreach (var unresolved in dependentQueries) + { + if (unresolved.ParentQueryResultType != result.GetType() && + !unresolved.ParentQueryResultType.IsAssignableFrom(result.GetType())) + continue; + + foreach (var query in unresolved.Queries) + query.ResolveQuery(context, queryResult); + } + } + } + + public QueryList ResolvedDependents => new(dependentQueries + .SelectMany(x => x.Queries) + .Where(query => query.IsContextResolved())); + + private static IList GetUniqueQueryList(IList queries) + { + if (queries is null) + return new List(); + + var distincts = queries + .Select(x => x.ParentQueryResultType) + .Distinct() + .Select(x => + { + var distinctMerge = queries + .Where(d => d.ParentQueryResultType == x) + .SelectMany(q => q.Queries) + .Distinct(new QueryComparer()) + .ToList(); + + return new ChildrenQueries { ParentQueryResultType = x, Queries = distinctMerge }; + }) + .ToList(); + + return distincts; + } +} diff --git a/src/DataFuse.Integration/Impl/QueryBuilder.cs b/src/DataFuse.Integration/Impl/QueryBuilder.cs new file mode 100644 index 0000000..cde91ae --- /dev/null +++ b/src/DataFuse.Integration/Impl/QueryBuilder.cs @@ -0,0 +1,69 @@ +using DataFuse.Adapters.Abstraction; + +namespace DataFuse.Integration.Impl; + +public class QueryBuilder : IQueryBuilder where T : IEntity +{ + private readonly IEntityConfiguration entitySchema; + private readonly ISchemaPathMatcher schemaPathMatcher; + + public QueryBuilder(IEntityConfiguration entitySchema, ISchemaPathMatcher schemaPathMatcher) + { + this.entitySchema = entitySchema; + this.schemaPathMatcher = schemaPathMatcher; + } + + /// + /// Builds a list of queries for the data entity in context based on the paths provided. + /// + public IQueryList Build(IDataContext context) + { + var mappings = entitySchema.Mappings.ToList(); + + var queries = GetMappedQueries(mappings, context); + + foreach (var query in queries.Queries) + query.ResolveQuery(context); + + return new QueryList(queries.Queries); + } + + private QueryList GetMappedQueries(IReadOnlyCollection> mappings, IDataContext context) + { + var queryDependencyDepth = mappings.Max(x => x.Order); + + for (var index = 1; index <= queryDependencyDepth; index++) + { + var maps = mappings.Where(x => x.Order == index).ToList(); + + foreach (var map in maps) + { + var dependentQueries = + mappings.Where(x => x.Order == index + 1 && x.DependentOn is not null && x.DependentOn.GetType() == map.Query.GetType()).ToList(); + + map.Query.Children = []; + + var filtered = FilterByPaths(context, dependentQueries); + map.Query.Children.AddRange(filtered); + } + } + + var queries = FilterByPaths(context, mappings.Where(x => x.DependentOn is null)) + .Distinct(new QueryComparer()) + .ToList(); + + return new QueryList(queries) { QueryDependencyDepth = queryDependencyDepth }; + } + + private IEnumerable FilterByPaths(IDataContext context, IEnumerable> mappings) + { + var matchedMappings = context.Request?.SchemaPaths is not null + ? mappings.Where(mapping => context.Request.SchemaPaths.Any(Path => schemaPathMatcher.IsMatch(Path, mapping.SchemaPaths))) + .ToList() + : mappings; + + return matchedMappings + .Select(x => x.Query) + .Distinct(new QueryComparer()); + } +} diff --git a/src/DataFuse.Integration/Impl/QueryExecutor.cs b/src/DataFuse.Integration/Impl/QueryExecutor.cs new file mode 100644 index 0000000..0c5191a --- /dev/null +++ b/src/DataFuse.Integration/Impl/QueryExecutor.cs @@ -0,0 +1,76 @@ +using DataFuse.Adapters.Abstraction; + +namespace DataFuse.Integration.Impl; + +public class QueryExecutor : IQueryExecutor +{ + private readonly IEnumerable queryEngines; + + public QueryExecutor(IEnumerable queryEngines) + { + this.queryEngines = queryEngines; + } + + public async Task> ExecuteAsync(IDataContext context, IQueryList queries) + { + var globalResults = new List(); + var counter = 0; + + // supports only 5 levels of query nesting. + while (queries.As().Any() && ++counter <= 5) + queries = await ProcessAsync(context, queries, globalResults); + + return globalResults; + } + + private async Task ProcessAsync(IDataContext context, IQueryList queries, List globalResults) + { + var subscriber = new EventSubscriber(queries.GetChildrenQueries()); + var eventAggregator = new EventPublisher(subscriber); + + var results = await RunQueriesAsync(queries); + + CacheResults(context, results); + + eventAggregator.PublishEvent(context, new ExecutorResultArgs(results)); + + globalResults.AddRange(results); + + return subscriber.ResolvedDependents; + } + + private static void CacheResults(IDataContext context, List results) + { + if (context.Cache is null) + return; + + foreach (var cacheResult in results.Where(result => result is not null && result.GetType() + .GetCustomAttributes(typeof(CacheResultAttribute), false) + .Any())) + if (!context.Cache.ContainsKey(cacheResult.GetType().Name)) + context.Cache.Add(cacheResult.GetType().Name, cacheResult); + } + + private async Task> RunQueriesAsync(IQueryList queryList) + { + var output = new List(); + + foreach (var engine in queryEngines) + { + var tasks = queryList.Queries + .Where(x => engine.CanExecute(x)) + .Select(s => s.Run(engine)) + .ToList(); + + var results = await Task.WhenAll(tasks); + + foreach (var result in results) + { + if (result is not null) + output.Add(result); + } + } + + return output; + } +} diff --git a/src/DataFuse.Integration/PathMatchers/JPathMatcher.cs b/src/DataFuse.Integration/PathMatchers/JPathMatcher.cs new file mode 100644 index 0000000..55cc2b6 --- /dev/null +++ b/src/DataFuse.Integration/PathMatchers/JPathMatcher.cs @@ -0,0 +1,11 @@ +using DataFuse.Integration.Helpers; +using DataFuse.Adapters.Abstraction; + +namespace DataFuse.Integration.PathMatchers; + +public class JPathMatcher : ISchemaPathMatcher +{ + public bool IsMatch(string inputXPath, ISchemaPaths configuredXPaths) => + inputXPath.IsNotNullOrEmpty() + && configuredXPaths.Paths.Any(x => inputXPath.Contains(x, StringComparison.OrdinalIgnoreCase)); +} diff --git a/src/DataFuse.Integration/PathMatchers/XPathMatcher.cs b/src/DataFuse.Integration/PathMatchers/XPathMatcher.cs new file mode 100644 index 0000000..03f6368 --- /dev/null +++ b/src/DataFuse.Integration/PathMatchers/XPathMatcher.cs @@ -0,0 +1,31 @@ +using System.Text.RegularExpressions; +using DataFuse.Adapters.Abstraction; + +namespace DataFuse.Integration.PathMatchers; + +public partial class XPathMatcher : ISchemaPathMatcher +{ +#if NET7_0_OR_GREATER + [GeneratedRegex(@"=ancestor::(?'path'.*?)(/@|\[.*\]/@)", RegexOptions.Compiled)] + private static partial Regex AncestorRegex(); +#else + private static readonly Regex _ancestorRegex = new(@"=ancestor::(?'path'.*?)(/@|\[.*\]/@)", RegexOptions.Compiled); + private static Regex AncestorRegex() => _ancestorRegex; +#endif + + public bool IsMatch(string inputXPath, ISchemaPaths configuredXPaths) + { + if (inputXPath is null) + return false; + + if (configuredXPaths.Paths.Any(x => inputXPath.Contains(x, StringComparison.OrdinalIgnoreCase))) + return true; + + if (configuredXPaths.Paths.Any(x => inputXPath.Contains("ancestor::") + && AncestorRegex().Matches(inputXPath).Cast() + .Select(match => match.Groups["path"].Value).Distinct().Any(match => x.EndsWith(match)))) + return true; + + return false; + } +} diff --git a/src/DataFuse.Integration/ServicesExtensions.cs b/src/DataFuse.Integration/ServicesExtensions.cs new file mode 100644 index 0000000..681d85a --- /dev/null +++ b/src/DataFuse.Integration/ServicesExtensions.cs @@ -0,0 +1,29 @@ +using DataFuse.Adapters.Abstraction; +using Microsoft.Extensions.DependencyInjection; +using DataFuse.Integration.Impl; +using DataFuse.Integration.PathMatchers; + +namespace DataFuse.Integration; + +public static class ServicesExtensions +{ + public static void UseDataFuse(this IServiceCollection services, Action configuration) + { + Guard.ThrowIfNull(configuration, nameof(configuration)); + Guard.ThrowIfNull(services, nameof(services)); + + var options = new DataFuseOptionsBuilder(services); + configuration.Invoke(options); + + if (!services.Any(s => s.ServiceType == typeof(ISchemaPathMatcher))) + services.AddTransient(); + + if (!options.Silent && options.EntityConfigurationCount == 0) + throw new InvalidOperationException("At least one entity configuration must be registered using WithEntityConfiguration method."); + + if (!options.Silent && options.EngineCount == 0) + throw new InvalidOperationException("At least one query engine must be registered using WithEngine or WithEngines method."); + + services.AddTransient(); + } +} diff --git a/src/DataFuse.Integration/XML/XMLDataProvider.cs b/src/DataFuse.Integration/XML/XMLDataProvider.cs new file mode 100644 index 0000000..8c56aec --- /dev/null +++ b/src/DataFuse.Integration/XML/XMLDataProvider.cs @@ -0,0 +1,63 @@ +using System.Xml; +using System.Xml.Linq; +using System.Xml.Serialization; +using DataFuse.Adapters.Abstraction; +using Microsoft.Extensions.Logging; +using DataFuse.Integration.Helpers.Xml; + +namespace DataFuse.Integration.XML; + +internal class XMLDataProvider where T : IEntity +{ + private readonly IEntityContextValidator entityContextValidator; + private readonly IDataProvider dataProvider; + private readonly ILogger> logger; + + private const string AuthorNamespace = "http://www.intelligent-office.net/author/1.0"; + + public XMLDataProvider(IEntityContextValidator entityContextValidator, + IDataProvider dataProvider, + ILogger> logger) + { + this.logger = logger; + this.dataProvider = dataProvider; + this.entityContextValidator = entityContextValidator; + } + + /// + /// Gets the xml document for the data source for given xpaths and context. + /// + public virtual async Task GetDataAsync(IEntityRequest context) + { + // Log Request context. + LogRequest(context); + // Validate Request context + ValidateRequest(context); + // Get entity object for given context + var entity = await dataProvider.GetDataAsync(context); + // Transform entity object to xml document + var doc = TransformToXmlDocument(entity); + return doc; + } + + private XDocument TransformToXmlDocument(T dataSource) + { + var namespaces = new XmlSerializerNamespaces(); + namespaces.Add("a", AuthorNamespace); + var strXml = XmlHelper.SerializeToXml(dataSource, namespaces, new XmlWriterSettings + { + OmitXmlDeclaration = true, + CheckCharacters = false + }); + + logger.LogInformation(strXml); + var xml = XmlSanitizer.Sanitize(strXml); + var doc = XDocument.Parse(xml); + + return doc; + } + + private void LogRequest(IEntityRequest context) => logger.LogInformation(context.GetType().Name); + + private void ValidateRequest(IEntityRequest context) => entityContextValidator.Validate(context); +} diff --git a/src/Schemio.API/IWebQuery.cs b/src/Schemio.API/IWebQuery.cs deleted file mode 100644 index 81fc2d3..0000000 --- a/src/Schemio.API/IWebQuery.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Net.Http; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using Schemio.Core; - -namespace Schemio.API -{ - public interface IWebQuery : IQuery - { - Task Run(IHttpClientFactory httpClientFactory, ILogger logger = null); - } -} \ No newline at end of file diff --git a/src/Schemio.API/QueryEngine.cs b/src/Schemio.API/QueryEngine.cs deleted file mode 100644 index c6326b1..0000000 --- a/src/Schemio.API/QueryEngine.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System.Net.Http; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using Schemio.Core; -using Schemio.Core.Helpers; - -namespace Schemio.API -{ - public class QueryEngine : IQueryEngine - { - private readonly ILogger logger; - private readonly IHttpClientFactory httpClientFactory; - - public QueryEngine(IHttpClientFactory httpClientFactory, ILogger logger = null) - { - this.httpClientFactory = httpClientFactory; - this.logger = logger; - - Constraints.NotNull(httpClientFactory); - } - - public bool CanExecute(IQuery query) => query != null && query is IWebQuery; - - public async Task Execute(IQuery query) - { - if (query == null || !(query is IWebQuery q)) - return null; - - return await q.Run(httpClientFactory, logger); - } - } -} \ No newline at end of file diff --git a/src/Schemio.API/Schemio.API.csproj b/src/Schemio.API/Schemio.API.csproj deleted file mode 100644 index 31f291e..0000000 --- a/src/Schemio.API/Schemio.API.csproj +++ /dev/null @@ -1,50 +0,0 @@ - - - - net9.0 - true - enable - Schemio with EntityFramework - Code Shayk - Code Shayk - .Net Library to hydrate data entities by object graph using schema paths (supports XPath & JSONPath). Supports Web API using HttpClient. - Copyright (c) 2024 Code Shayk - https://github.com/CodeShayk/Schemio/wiki/i.-Home - ninja-icon-16.png - README.md - https://github.com/CodeShayk/Schemio - git - graphql json-schema xsd data-mapping query-engine data-mapper data-schema schema-mapping object-tree-query object-graph-query schema-mapper xsd-data-object object-graph-data entity-data entity-data-fetch hydrate-object object-hydration object-data object-fetch schemio-Api HttpCliet - LICENSE.md - True - True - snupkg - True - 2.0.0 - - - - True - - - - - True - \ - - - True - \ - - - True - \ - - - - - - - - - diff --git a/src/Schemio.API/WebHeaderResult.cs b/src/Schemio.API/WebHeaderResult.cs deleted file mode 100644 index 00f798b..0000000 --- a/src/Schemio.API/WebHeaderResult.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System.Collections.Generic; -using Schemio.Core; - -namespace Schemio.API -{ - /// - /// Implement to return web query response with headers. - /// - public abstract class WebHeaderResult : IQueryResult - { - public IDictionary Headers { get; internal set; } - } -} \ No newline at end of file diff --git a/src/Schemio.API/WebQuery.cs b/src/Schemio.API/WebQuery.cs deleted file mode 100644 index ad6b34b..0000000 --- a/src/Schemio.API/WebQuery.cs +++ /dev/null @@ -1,182 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using Schemio.Core; -using Schemio.Core.Helpers; - -namespace Schemio.API -{ - public abstract class WebQuery : BaseQuery, IWebQuery - where TQueryResult : IQueryResult - { - protected Uri BaseAddress; - - protected WebQuery() : this(string.Empty) - { - } - - protected WebQuery(string baseAddress) - { - if (!string.IsNullOrEmpty(baseAddress)) - BaseAddress = new Uri(baseAddress); - } - - private Func UriDelegate = null; - - public override bool IsContextResolved() => UriDelegate != null; - - public override void ResolveQuery(IDataContext context, IQueryResult parentQueryResult) - { - UriDelegate = GetQuery(context, parentQueryResult); - } - - /// - /// Override to pass custom outgoing headers with the api request. - /// - /// - protected virtual IDictionary GetRequestHeaders() - { - return new Dictionary(); - } - - /// - /// Override to get custom incoming headers with the api response. - /// The headers collection will be present on `WebHeaderResult.Headers` when api response includes any of the headers defined in this method. - /// - /// - protected virtual IEnumerable GetResponseHeaders() - { return []; } - - /// - /// Implement to construct the api web query. - /// - /// Request Context. Always available. - /// Result from parent Query. Only available when configured as nested web query. Else will be null. - /// - protected abstract Func GetQuery(IDataContext context, IQueryResult parentApiResult = null); - - async Task IWebQuery.Run(IHttpClientFactory httpClientFactory, ILogger logger) - { - Constraints.NotNull(httpClientFactory); - - logger?.LogInformation($"Run api: {GetType().Name}"); - - var Uri = UriDelegate(); - - if (Uri == null) - return null; - - using (var client = httpClientFactory.CreateClient()) - { - logger?.LogInformation($"Executing web api on thread {Thread.CurrentThread.ManagedThreadId} (task {Task.CurrentId})"); - - try - { - HttpResponseMessage result; - - try - { - if (BaseAddress != null) - client.BaseAddress = BaseAddress; - - var requestHeaders = GetRequestHeaders(); - - if (requestHeaders != null && requestHeaders.Any()) - foreach (var header in requestHeaders) - client.DefaultRequestHeaders.Add(header.Key, header.Value); - - result = await client.GetAsync(Uri); - - var raw = result.Content.ReadAsStringAsync().Result; - - if (!string.IsNullOrWhiteSpace(raw)) - logger?.LogInformation($"Result.Content of executing web api: {Uri.AbsolutePath} is {raw}"); - - if (!result.IsSuccessStatusCode) - { - logger?.LogInformation($"Result of executing web api {Uri.AbsolutePath} is not success status code"); - return null; - } - - if (typeof(TQueryResult).UnderlyingSystemType != null && typeof(TQueryResult).UnderlyingSystemType.Name.Equals(typeof(CollectionResult<>).Name)) - { - var typeArgs = typeof(TQueryResult).GetGenericArguments(); - var arrType = typeArgs[0].MakeArrayType(); - var arrObject = raw.ToObject(arrType); - if (arrObject != null) - { - var resultType = typeof(CollectionResult<>); - var collectionType = resultType.MakeGenericType(typeArgs); - var collectionResult = (TQueryResult)Activator.CreateInstance(collectionType, arrObject); - - SetResponseHeaders(result, collectionResult); - - return collectionResult; - } - } - else - { - var obj = raw.ToObject(typeof(TQueryResult)); - if (obj != null) - { - var resObj = (TQueryResult)obj; - SetResponseHeaders(result, resObj); - return resObj; - } - } - } - catch (TaskCanceledException ex) - { - logger?.LogWarning(ex, $"An error occurred while sending the request. Query URL: {Uri.AbsolutePath}"); - } - catch (HttpRequestException ex) - { - logger?.LogWarning(ex, $"An error occurred while sending the request. Query URL: {Uri.AbsolutePath}"); - } - } - catch (AggregateException ex) - { - logger?.LogInformation($"Web api {GetType().Name} failed"); - foreach (var e in ex.InnerExceptions) - logger?.LogError(e, ""); - } - } - - return null; - } - - private void SetResponseHeaders(HttpResponseMessage response, TQueryResult? result) - { - if (response.Headers == null || result == null) - return; - - var headers = GetResponseHeaders(); - - if (headers == null || !headers.Any()) - return; - - if (!(result is WebHeaderResult webResult)) - throw new InvalidOperationException($"{typeof(TQueryResult).Name} should implement from WebHeaderResult for response Headers"); - - foreach (var header in headers) - { - if (!response.Headers.Any(r => r.Key == header)) - continue; - - var responseHeader = response.Headers.First(r => r.Key == header); - - var value = responseHeader.Value != null && responseHeader.Value.Any() - ? responseHeader.Value.ElementAt(0) - : string.Empty; - - webResult.Headers ??= new Dictionary(); - - webResult.Headers.Add(responseHeader.Key, value); - } - } - } -} \ No newline at end of file diff --git a/src/Schemio.Core/BaseQuery.cs b/src/Schemio.Core/BaseQuery.cs deleted file mode 100644 index 11fb055..0000000 --- a/src/Schemio.Core/BaseQuery.cs +++ /dev/null @@ -1,46 +0,0 @@ -namespace Schemio.Core -{ - /// - /// Implement this base class to create a data provider query. - /// - /// - public abstract class BaseQuery : IQuery - where TQueryResult : IQueryResult - { - /// - /// Children queries dependent on this query. - /// - public List Children { get; set; } - - /// - /// Get the result type for the query - /// - public Type ResultType - { - get { return typeof(TQueryResult); } - } - - /// - /// Implement to determine whether the query context is resolved and ready to execute with supported engine. - /// - /// Boolean - public abstract bool IsContextResolved(); - - /// - /// Implement to resolve query context for execution with supporting query engine. - /// - /// data context passed to the data provider. - /// query result from parent query (when configured as nested query). Can be null. - public abstract void ResolveQuery(IDataContext context, IQueryResult parentQueryResult); - - /// - /// Run query with supporting IQueryEngine instance. - /// - /// IQueryEngine Instance - /// Instance of IQueryResult - public Task Run(IQueryEngine engine) - { - return engine.Execute(this); - } - } -} \ No newline at end of file diff --git a/src/Schemio.Core/BaseTransformer.cs b/src/Schemio.Core/BaseTransformer.cs deleted file mode 100644 index 3669abf..0000000 --- a/src/Schemio.Core/BaseTransformer.cs +++ /dev/null @@ -1,41 +0,0 @@ -namespace Schemio.Core - -{ - public abstract class BaseTransformer : ITransformer, ITransformerContext, ITransformerQueryResult - where TEntity : IEntity - where TQueryResult : IQueryResult - { - /// - /// Transformer instance of data context. - /// - protected IDataContext Context { get; private set; } - - /// - /// Supported QueryResult type for the transformer. - /// - public Type SupportedQueryResult => typeof(TQueryResult); - - /// - /// Method to set data conext for the transformer - /// - /// - public void SetContext(IDataContext context) => Context = context; - - /// - /// Transform method mapping query result data to entity. - /// - /// Query Result - /// Entity - public void Transform(IQueryResult queryResult, IEntity entity) - { - Transform((TQueryResult)queryResult, (TEntity)entity); - } - - /// - /// Implement this method to map data to entity. - /// - /// Query Result - /// Entity - public abstract void Transform(TQueryResult queryResult, TEntity entity); - } -} \ No newline at end of file diff --git a/src/Schemio.Core/CacheResultAttribute.cs b/src/Schemio.Core/CacheResultAttribute.cs deleted file mode 100644 index d989780..0000000 --- a/src/Schemio.Core/CacheResultAttribute.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace Schemio.Core -{ - public class CacheResultAttribute : Attribute - { } -} \ No newline at end of file diff --git a/src/Schemio.Core/ChildrenQueries.cs b/src/Schemio.Core/ChildrenQueries.cs deleted file mode 100644 index 9906175..0000000 --- a/src/Schemio.Core/ChildrenQueries.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Schemio.Core -{ - public class ChildrenQueries - { - public Type ParentQueryResultType { get; set; } - public IList Queries { get; set; } - } -} \ No newline at end of file diff --git a/src/Schemio.Core/CollectionResult.cs b/src/Schemio.Core/CollectionResult.cs deleted file mode 100644 index d5600b1..0000000 --- a/src/Schemio.Core/CollectionResult.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace Schemio.Core -{ - public class CollectionResult : List, IQueryResult - { - public CollectionResult(IEnumerable list) : base(list) - { - } - - public CollectionResult() - { - } - } -} \ No newline at end of file diff --git a/src/Schemio.Core/CreateSchema.cs b/src/Schemio.Core/CreateSchema.cs deleted file mode 100644 index 8b0b7bc..0000000 --- a/src/Schemio.Core/CreateSchema.cs +++ /dev/null @@ -1,140 +0,0 @@ -namespace Schemio.Core -{ - #region Helpers - - public class CreateSchema - { - public static IMappings For() where T : IEntity - => new Mappings { Order = 1 }; - } - - public class For - { - public static ISchemaPaths Paths(params string[] paths) - => new SchemaPaths { Paths = paths }; - } - - public class SchemaPaths : ISchemaPaths - { - public string[] Paths { get; set; } - } - - #endregion Helpers - - public class Mappings : - List>, - IMappings, - IMapOrComplete - where TEntity : IEntity - where TQueryResult : IQueryResult - { - public int Order { get; set; } - - /// - /// Map query and transformer for given schema path - /// - /// query type - /// transformer type - /// given schema paths - /// - public IMapOrComplete Map(ISchemaPaths paths) - where TQuery : IQuery, new() - where TTransformer : ITransformer, new() => - Map(paths, null); - - /// - /// Map query and transformer for given schema path and with dependent query/transform mappings - /// - /// query type - /// transformer type - /// given schema paths - /// dependent mappings delegate - /// - public IMapOrComplete Map(ISchemaPaths paths, Func, IMap> dependents) - where TQuery : IQuery, new() - where TTransformer : ITransformer, new() - { - var mapping = new Mapping - { - Query = new TQuery(), - Transformer = new TTransformer(), - Order = Order, - SchemaPaths = paths, - }; - - if (dependents != null) - foreach (var dep in ((IMappings)dependents(mapping)).GetMappings) - { - dep.DependentOn ??= mapping.Query; - Add(dep); - } - - Add(mapping); - - return this; - } - - public Mappings GetMappings => this; - - public IEnumerable> End() => this; - } - - public class Mapping : - IWithDependents - where TEntity : IEntity - where TQueryResult : IQueryResult - { - public int Order { get; set; } - public ISchemaPaths SchemaPaths { get; set; } - public IQuery Query { get; set; } - public ITransformer Transformer { get; set; } - public IQuery DependentOn { get; set; } - - public IMappings Dependents => new Mappings { Order = Order + 1 }; - } - - #region Fluent Interfaces - - public interface ISchemaPaths - { - string[] Paths { get; set; } - } - - public interface IMap - where TEntity : IEntity - where TQueryResult : IQueryResult - { - IMapOrComplete Map(ISchemaPaths paths) - where TQuery : IQuery, new() - where TTransformer : ITransformer, new(); - - IMapOrComplete Map(ISchemaPaths paths, Func, IMap> dependents) - where TQuery : IQuery, new() - where TTransformer : ITransformer, new(); - } - - public interface IMappings : IMap - where TEntity : IEntity - where TQueryResult : IQueryResult - { - int Order { get; set; } - Mappings GetMappings { get; } - } - - public interface IMapOrComplete : IMap - where TEntity : IEntity - where TQueryResult : IQueryResult - { - IEnumerable> End(); - } - - public interface IWithDependents - - where TEntity : IEntity - where TQueryResult : IQueryResult - { - IMappings Dependents { get; } - } - - #endregion Fluent Interfaces -} \ No newline at end of file diff --git a/src/Schemio.Core/DataContext.cs b/src/Schemio.Core/DataContext.cs deleted file mode 100644 index 2e528fe..0000000 --- a/src/Schemio.Core/DataContext.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Schemio.Core -{ - internal class DataContext : IDataContext - { - public DataContext(IEntityRequest request) - { - Request = request; - Cache = new Dictionary(); - } - - public Dictionary Cache { get; set; } - public IEntityRequest Request { get; set; } - } -} \ No newline at end of file diff --git a/src/Schemio.Core/EntityConfiguration.cs b/src/Schemio.Core/EntityConfiguration.cs deleted file mode 100644 index 4e2c3d0..0000000 --- a/src/Schemio.Core/EntityConfiguration.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace Schemio.Core -{ - /// - /// Implement to configure schema path mappings for an Entity. - /// - /// Entity type - public abstract class EntityConfiguration : IEntityConfiguration where TEntity : IEntity - { - public IEnumerable> Mappings { get; } - - public EntityConfiguration() - { - Mappings = GetSchema(); - } - - /// - /// Implement to configure entity schema mappings with queries & transformers. - /// - /// Entity Schema mappings - public abstract IEnumerable> GetSchema(); - } -} \ No newline at end of file diff --git a/src/Schemio.Core/Helpers/ArrayUtil.cs b/src/Schemio.Core/Helpers/ArrayUtil.cs deleted file mode 100644 index b9a7ae6..0000000 --- a/src/Schemio.Core/Helpers/ArrayUtil.cs +++ /dev/null @@ -1,28 +0,0 @@ -namespace Schemio.Core.Helpers -{ - public static class ArrayUtil - { - public static T[] EnsureAndResizeArray(T[] val, out int index) - { - return EnsureAndResizeArray(val, 1, out index); - } - - public static T[] EnsureAndResizeArray(T[] val, int extendBy, out int index) - { - if (val == null) - { - val = new T[extendBy]; - index = 0; - } - else - { - var temp = val; - index = temp.Length; - Array.Resize(ref temp, temp.Length + extendBy); - val = temp; - } - - return val; - } - } -} \ No newline at end of file diff --git a/src/Schemio.Core/Helpers/Constraints.cs b/src/Schemio.Core/Helpers/Constraints.cs deleted file mode 100644 index a6351a1..0000000 --- a/src/Schemio.Core/Helpers/Constraints.cs +++ /dev/null @@ -1,11 +0,0 @@ -īģŋnamespace Schemio.Core.Helpers -{ - public static class Constraints - { - public static void NotNull(this T value) - { - if (value == null) - throw new ArgumentNullException(typeof(T).Name); - } - } -} \ No newline at end of file diff --git a/src/Schemio.Core/Helpers/EnumerableExtentions.cs b/src/Schemio.Core/Helpers/EnumerableExtentions.cs deleted file mode 100644 index d455d10..0000000 --- a/src/Schemio.Core/Helpers/EnumerableExtentions.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System.Text; - -namespace Schemio.Core.Helpers -{ - public static class EnumerableExtentions - { - public static void Each(this IEnumerable enumerable, Action action) - { - foreach (var item in enumerable) - action(item); - } - - public static string ToCSV(this IEnumerable instance, char separator) - { - StringBuilder csv; - if (instance != null) - { - csv = new StringBuilder(); - instance.Each(value => csv.AppendFormat("{0}{1}", value, separator)); - return csv.ToString(0, csv.Length - 1); - } - return null; - } - - public static string ToCSV(this IEnumerable instance) - { - return instance.ToCSV(','); - } - } -} \ No newline at end of file diff --git a/src/Schemio.Core/Helpers/JsonExtensions.cs b/src/Schemio.Core/Helpers/JsonExtensions.cs deleted file mode 100644 index d841e5e..0000000 --- a/src/Schemio.Core/Helpers/JsonExtensions.cs +++ /dev/null @@ -1,11 +0,0 @@ -īģŋusing System.Text.Json; - -namespace Schemio.Core.Helpers -{ - public static class JsonExtensions - { - public static string? ToJson(this object value) => value != null ? JsonSerializer.Serialize(value) : null; - - public static object? ToObject(this string value, Type type) => !string.IsNullOrEmpty(value) ? JsonSerializer.Deserialize(value, type) : null; - } -} \ No newline at end of file diff --git a/src/Schemio.Core/Helpers/StringExtensions.cs b/src/Schemio.Core/Helpers/StringExtensions.cs deleted file mode 100644 index 2cefe2d..0000000 --- a/src/Schemio.Core/Helpers/StringExtensions.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Schemio.Core.Helpers -{ - public static class StringExtensions - { - public static bool IsNotNullOrEmpty(this string value) => !string.IsNullOrEmpty(value); - } -} \ No newline at end of file diff --git a/src/Schemio.Core/Helpers/Xml/XDocumentExts.cs b/src/Schemio.Core/Helpers/Xml/XDocumentExts.cs deleted file mode 100644 index 936e178..0000000 --- a/src/Schemio.Core/Helpers/Xml/XDocumentExts.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Xml.Linq; - -namespace Schemio.Core.Helpers.Xml -{ - public static class XDocumentExts - { - public static void RemoveNamespaces(this XDocument document) - { - if (document.Root == null) - return; - foreach (var xElement in document.Root.DescendantsAndSelf()) - { - if (xElement.Name.Namespace != XNamespace.None) - xElement.Name = XNamespace.None.GetName(xElement.Name.LocalName); - if (xElement.Attributes().Any(a => a.IsNamespaceDeclaration || a.Name.Namespace != XNamespace.None)) - xElement.ReplaceAttributes(xElement.Attributes().Select(a => - { - if (a.IsNamespaceDeclaration) - return null; - return !(a.Name.Namespace != XNamespace.None) ? a : new XAttribute(XNamespace.None.GetName(a.Name.LocalName), a.Value); - })); - } - } - } -} \ No newline at end of file diff --git a/src/Schemio.Core/Helpers/Xml/XmlHelper.cs b/src/Schemio.Core/Helpers/Xml/XmlHelper.cs deleted file mode 100644 index b38db24..0000000 --- a/src/Schemio.Core/Helpers/Xml/XmlHelper.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Text; -using System.Xml; -using System.Xml.Serialization; - -namespace Schemio.Core.Helpers.Xml -{ - public static class XmlHelper - { - public static string SerializeToXml(T value, XmlSerializerNamespaces namespaces, XmlWriterSettings settings) - { - var xmlStr = new StringBuilder(); - var x = new XmlSerializer(typeof(T)); - - using (var writer = XmlWriter.Create(xmlStr, settings)) - { - x.Serialize(writer, value, namespaces); - return xmlStr.ToString(); - } - } - } -} \ No newline at end of file diff --git a/src/Schemio.Core/Helpers/Xml/XmlSanitizer.cs b/src/Schemio.Core/Helpers/Xml/XmlSanitizer.cs deleted file mode 100644 index 24f3b1a..0000000 --- a/src/Schemio.Core/Helpers/Xml/XmlSanitizer.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Globalization; -using System.Text.RegularExpressions; -using System.Xml; - -namespace Schemio.Core.Helpers.Xml -{ - public static class XmlSanitizer - { - private static readonly Regex xmlEntityRegex = new Regex("&(#)?([a-zA-Z0-9]*);"); - - public static string Sanitize(string xml) - { - if (string.IsNullOrEmpty(xml)) - return xml; - - return xmlEntityRegex.Replace(xml, x => - { - if (x.Groups[1].Value != "#") - return x.Groups[0].Value; - - var strChar = x.Groups[2].Value; - - if (strChar.Contains("x")) - strChar = strChar.Replace("x", string.Empty).Trim(); - - if (!int.TryParse(strChar, NumberStyles.AllowHexSpecifier, null, out var intChar)) - return x.Groups[0].Value; - - return !XmlConvert.IsXmlChar((char)intChar) - ? string.Empty - : x.Groups[0].Value; - }); - } - } -} \ No newline at end of file diff --git a/src/Schemio.Core/IDataContext.cs b/src/Schemio.Core/IDataContext.cs deleted file mode 100644 index e53967c..0000000 --- a/src/Schemio.Core/IDataContext.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Schemio.Core -{ - public interface IDataContext : IEntityContextCache - { - IEntityRequest Request { get; } - } -} \ No newline at end of file diff --git a/src/Schemio.Core/IDataProvider.cs b/src/Schemio.Core/IDataProvider.cs deleted file mode 100644 index ef3899b..0000000 --- a/src/Schemio.Core/IDataProvider.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Schemio.Core -{ - public interface IDataProvider where TEntity : IEntity - { - TEntity GetData(IEntityRequest request); - } -} \ No newline at end of file diff --git a/src/Schemio.Core/IEntity.cs b/src/Schemio.Core/IEntity.cs deleted file mode 100644 index efe74ed..0000000 --- a/src/Schemio.Core/IEntity.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Schemio.Core -{ - /// - /// Implement Entity required to be hydrated (using query/transformer). - /// - public interface IEntity - { - //decimal Version { get; set; } - } -} \ No newline at end of file diff --git a/src/Schemio.Core/IEntityBuilder.cs b/src/Schemio.Core/IEntityBuilder.cs deleted file mode 100644 index d375e1b..0000000 --- a/src/Schemio.Core/IEntityBuilder.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Schemio.Core -{ - public interface IEntityBuilder where TEntity : IEntity - { - TEntity Build(IDataContext context, IList results); - } -} \ No newline at end of file diff --git a/src/Schemio.Core/IEntityConfiguration.cs b/src/Schemio.Core/IEntityConfiguration.cs deleted file mode 100644 index a7e34c1..0000000 --- a/src/Schemio.Core/IEntityConfiguration.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Schemio.Core -{ - /// - /// Implement to configure schema path mappings for an Entity. - /// - /// Entity type - public interface IEntityConfiguration where TEntity : IEntity - { - /// - /// Entity schema mappings. - /// - IEnumerable> Mappings { get; } - } -} \ No newline at end of file diff --git a/src/Schemio.Core/IEntityContextCache.cs b/src/Schemio.Core/IEntityContextCache.cs deleted file mode 100644 index b4bbd9a..0000000 --- a/src/Schemio.Core/IEntityContextCache.cs +++ /dev/null @@ -1,10 +0,0 @@ -īģŋnamespace Schemio.Core -{ - public interface IEntityContextCache - { - /// - /// Cache dictionary holding query results for query result type marked with [CacheResult] attribute. - /// - Dictionary Cache { get; set; } - } -} \ No newline at end of file diff --git a/src/Schemio.Core/IEntityContextValidator.cs b/src/Schemio.Core/IEntityContextValidator.cs deleted file mode 100644 index 62f3d29..0000000 --- a/src/Schemio.Core/IEntityContextValidator.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Schemio.Core -{ - public interface IEntityContextValidator - { - public void Validate(IEntityRequest context); - } -} \ No newline at end of file diff --git a/src/Schemio.Core/IEntityRequest.cs b/src/Schemio.Core/IEntityRequest.cs deleted file mode 100644 index f26d15f..0000000 --- a/src/Schemio.Core/IEntityRequest.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Schemio.Core -{ - public interface IEntityRequest - { - /// - /// Entity schema paths for data retrieval. - /// - public string[] SchemaPaths { get; set; } - } -} \ No newline at end of file diff --git a/src/Schemio.Core/IPolymorphicResult.cs b/src/Schemio.Core/IPolymorphicResult.cs deleted file mode 100644 index 7d505a5..0000000 --- a/src/Schemio.Core/IPolymorphicResult.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Schemio.Core -{ - public interface IPolymorphicResult : IQueryResult - { - } -} \ No newline at end of file diff --git a/src/Schemio.Core/IQuery.cs b/src/Schemio.Core/IQuery.cs deleted file mode 100644 index 9fe0b58..0000000 --- a/src/Schemio.Core/IQuery.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace Schemio.Core -{ - /// - /// Implement IQuery to fetch data using API or database. - /// - public interface IQuery : IQueryRunner - { - List Children { get; set; } - - Type ResultType { get; } - - bool IsContextResolved(); - - void ResolveQuery(IDataContext context, IQueryResult parentQueryResult = null); - } -} \ No newline at end of file diff --git a/src/Schemio.Core/IQueryBuilder.cs b/src/Schemio.Core/IQueryBuilder.cs deleted file mode 100644 index 8d7686d..0000000 --- a/src/Schemio.Core/IQueryBuilder.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Schemio.Core -{ - public interface IQueryBuilder - { - IQueryList Build(IDataContext context); - } -} \ No newline at end of file diff --git a/src/Schemio.Core/IQueryEngine.cs b/src/Schemio.Core/IQueryEngine.cs deleted file mode 100644 index acfb7cb..0000000 --- a/src/Schemio.Core/IQueryEngine.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace Schemio.Core -{ - public interface IQueryEngine - { - /// - /// Detrmines whether an instance of query can be executed with this engine. - /// - /// instance of IQuery. - /// Boolean; True when supported. - bool CanExecute(IQuery query); - - /// - /// Executes a list of queries returning a list of aggregated results. - /// - /// Instance of IQuery. - /// Instance of IQueryResult. - Task Execute(IQuery query); - } -} \ No newline at end of file diff --git a/src/Schemio.Core/IQueryExecutor.cs b/src/Schemio.Core/IQueryExecutor.cs deleted file mode 100644 index 13ee57a..0000000 --- a/src/Schemio.Core/IQueryExecutor.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Schemio.Core -{ - public interface IQueryExecutor - { - IList Execute(IDataContext context, IQueryList queries); - } -} \ No newline at end of file diff --git a/src/Schemio.Core/IQueryList.cs b/src/Schemio.Core/IQueryList.cs deleted file mode 100644 index 31c9244..0000000 --- a/src/Schemio.Core/IQueryList.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace Schemio.Core -{ - public interface IQueryList - { - int QueryDependencyDepth { get; set; } - IEnumerable Queries { get; } - - bool IsEmpty(); - - IQueryList GetByType() where T : class; - - List As(); - - List GetChildrenQueries(); - - void AddRange(IEnumerable collection); - - int Count(); - } -} \ No newline at end of file diff --git a/src/Schemio.Core/IQueryParameter.cs b/src/Schemio.Core/IQueryParameter.cs deleted file mode 100644 index eda20dd..0000000 --- a/src/Schemio.Core/IQueryParameter.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Schemio.Core -{ - public interface IQueryParameter - { - } -} \ No newline at end of file diff --git a/src/Schemio.Core/IQueryResult.cs b/src/Schemio.Core/IQueryResult.cs deleted file mode 100644 index 41f1ba4..0000000 --- a/src/Schemio.Core/IQueryResult.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Schemio.Core -{ - public interface IQueryResult - { - } -} \ No newline at end of file diff --git a/src/Schemio.Core/IQueryRunner.cs b/src/Schemio.Core/IQueryRunner.cs deleted file mode 100644 index 13abbbb..0000000 --- a/src/Schemio.Core/IQueryRunner.cs +++ /dev/null @@ -1,7 +0,0 @@ -īģŋnamespace Schemio.Core -{ - public interface IQueryRunner - { - Task Run(IQueryEngine engine); - } -} \ No newline at end of file diff --git a/src/Schemio.Core/ISchemaPathMatcher.cs b/src/Schemio.Core/ISchemaPathMatcher.cs deleted file mode 100644 index 51e71de..0000000 --- a/src/Schemio.Core/ISchemaPathMatcher.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace Schemio.Core -{ - public interface ISchemaPathMatcher - { - /// - /// Determines whether there is a match for given input path vs configured paths for entity's object graph. - /// - /// Input path from IEntityRequest.SchemaPaths - /// Configured paths from EntityConfiguration - /// - bool IsMatch(string inputPath, ISchemaPaths configuredPaths); - } -} \ No newline at end of file diff --git a/src/Schemio.Core/ITransformer.cs b/src/Schemio.Core/ITransformer.cs deleted file mode 100644 index 76ae8ef..0000000 --- a/src/Schemio.Core/ITransformer.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace Schemio.Core -{ - /// - /// Implement transformer to map data from supported query result to entity in context. - /// - public interface ITransformer - { - /// - /// Transform method to map data to entity for a given query result. - /// - /// Supported Query Result. - /// Entity in context. - void Transform(IQueryResult queryResult, IEntity entity); - } -} \ No newline at end of file diff --git a/src/Schemio.Core/ITransformerContext.cs b/src/Schemio.Core/ITransformerContext.cs deleted file mode 100644 index df6a1a9..0000000 --- a/src/Schemio.Core/ITransformerContext.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Schemio.Core -{ - /// - /// Implement to set transform with data context. - /// - public interface ITransformerContext - { - /// - /// Implement to set context in transform. - /// - /// - void SetContext(IDataContext context); - } -} \ No newline at end of file diff --git a/src/Schemio.Core/ITransformerQueryResult.cs b/src/Schemio.Core/ITransformerQueryResult.cs deleted file mode 100644 index 893fce6..0000000 --- a/src/Schemio.Core/ITransformerQueryResult.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace Schemio.Core -{ - /// - /// Implement to get supported Query result. - /// - public interface ITransformerQueryResult - { - /// - /// Supported query reslt. - /// - Type SupportedQueryResult { get; } - } -} \ No newline at end of file diff --git a/src/Schemio.Core/Impl/DataProvider.cs b/src/Schemio.Core/Impl/DataProvider.cs deleted file mode 100644 index 484c758..0000000 --- a/src/Schemio.Core/Impl/DataProvider.cs +++ /dev/null @@ -1,74 +0,0 @@ -using Microsoft.Extensions.Logging; -using Schemio.Core.PathMatchers; - -namespace Schemio.Core.Impl -{ - public class DataProvider : IDataProvider - where TEntity : IEntity, new() - { - private readonly ILogger> logger; - private readonly IQueryExecutor queryExecutor; - private readonly IQueryBuilder queryBuilder; - private readonly IEntityBuilder entityBuilder; - - public DataProvider( - IEntityConfiguration entitySchema, - params IQueryEngine[] queryEngines) - : this(null, new QueryBuilder(entitySchema, new XPathMatcher()), - new QueryExecutor(queryEngines), new EntityBuilder(entitySchema)) - { - } - - public DataProvider( - ILogger> logger, - IEntityConfiguration entitySchema, - ISchemaPathMatcher schemaPathMatcher, - params IQueryEngine[] queryEngines) - : this(logger, new QueryBuilder(entitySchema, schemaPathMatcher), - new QueryExecutor(queryEngines), new EntityBuilder(entitySchema)) - { - } - - public DataProvider( - ILogger> logger, - IQueryBuilder queryBuilder, - IQueryExecutor queryExecutor, - IEntityBuilder entityBuilder) - { - this.logger = logger; - this.queryBuilder = queryBuilder; - this.queryExecutor = queryExecutor; - this.entityBuilder = entityBuilder; - } - - public TEntity GetData(IEntityRequest request) - { - var context = new DataContext(request); - return GetData(context); - } - - internal TEntity GetData(IDataContext context) - { - // Build queries for the data source based on the included xPaths - var watch = System.Diagnostics.Stopwatch.StartNew(); - var queries = queryBuilder.Build(context); - - watch.Stop(); - logger?.LogInformation("Query builder executed in " + watch.ElapsedMilliseconds + " ms"); - - // execute all queries to get results - watch = System.Diagnostics.Stopwatch.StartNew(); - var results = queryExecutor.Execute(context, queries); - watch.Stop(); - logger?.LogInformation("Query executor executed in " + watch.ElapsedMilliseconds + " ms"); - - // Executes configured transformers to map query results to target entity - watch = System.Diagnostics.Stopwatch.StartNew(); - var entity = entityBuilder.Build(context, results); - watch.Stop(); - logger?.LogInformation("Transform executor executed in " + watch.ElapsedMilliseconds + " ms"); - - return entity; - } - } -} \ No newline at end of file diff --git a/src/Schemio.Core/Impl/EntityBuilder.cs b/src/Schemio.Core/Impl/EntityBuilder.cs deleted file mode 100644 index 48e6427..0000000 --- a/src/Schemio.Core/Impl/EntityBuilder.cs +++ /dev/null @@ -1,54 +0,0 @@ -namespace Schemio.Core.Impl -{ - public class EntityBuilder : IEntityBuilder where T : IEntity, new() - { - private readonly IEntityConfiguration entitySchema; - - public EntityBuilder(IEntityConfiguration entitySchema) - { - this.entitySchema = entitySchema; - } - - /// - /// Executes configured transformers to populate target entity using list of query results. - /// - /// Entity Context - /// List of Query results - /// - public T Build(IDataContext context, IList queryResults) - { - var entity = new T { /*Version = entitySchemaMapping.Version*/ }; - - if (queryResults == null || !queryResults.Any()) - return entity; - - var mappings = entitySchema.Mappings.ToList(); - - // resolve context of each transformer so it is available inside for transformation if required. - mappings.ForEach(mapping => (mapping.Transformer as ITransformerContext)?.SetContext(context)); - - var queryDependencyDepth = mappings.Max(x => x.Order); - - // iterate through transformers to build data source by order configured. - for (var index = 1; index <= queryDependencyDepth; index++) - { - var transformers = mappings - .Where(mapping => mapping.Order == index) - .Select(m => m.Transformer) - .Distinct() - .ToList(); - - foreach (var queryResult in queryResults) - transformers.Where(transformer => IsMatch(((ITransformerQueryResult)transformer).SupportedQueryResult, queryResult.GetType())).ToList() - .ForEach(supportedtransformer => supportedtransformer?.Transform(queryResult, entity)); - } - - return entity; - - static bool IsMatch(Type transformer, Type queryResult) - { - return transformer == queryResult; - } - } - } -} \ No newline at end of file diff --git a/src/Schemio.Core/Impl/EventPublisher.cs b/src/Schemio.Core/Impl/EventPublisher.cs deleted file mode 100644 index f135e55..0000000 --- a/src/Schemio.Core/Impl/EventPublisher.cs +++ /dev/null @@ -1,29 +0,0 @@ -namespace Schemio.Core.Impl -{ - public class EventPublisher - { - private readonly ISubscriber subscriber; - - public EventPublisher(ISubscriber subscriber) - { - this.subscriber = subscriber; - } - - public void PublishEvent(IDataContext context, ExecutorResultArgs args) => subscriber.OnEventHandler(context, args); - } - - public class ExecutorResultArgs : EventArgs - { - public ExecutorResultArgs(IEnumerable result) - { - Result = result; - } - - public IEnumerable Result { get; } - } - - public interface ISubscriber - { - void OnEventHandler(IDataContext context, ExecutorResultArgs e); - } -} \ No newline at end of file diff --git a/src/Schemio.Core/Impl/EventSubscriber.cs b/src/Schemio.Core/Impl/EventSubscriber.cs deleted file mode 100644 index 34a2668..0000000 --- a/src/Schemio.Core/Impl/EventSubscriber.cs +++ /dev/null @@ -1,84 +0,0 @@ -namespace Schemio.Core.Impl -{ - public class EventSubscriber : ISubscriber - { - private readonly IList dependentQueries; - - public EventSubscriber(IList dependentQueries) - { - this.dependentQueries = GetUniqueQueryList(dependentQueries); - } - - public void OnEventHandler(IDataContext context, ExecutorResultArgs args) - { - if (args.Result == null) - return; - - var results = args.Result; - - foreach (var queryResult in results.Where(x => !typeof(IPolymorphicResult).IsAssignableFrom(x.GetType()))) - { - if (dependentQueries.All(tuple => tuple.ParentQueryResultType != queryResult.GetType())) - continue; - - var result = queryResult; - - foreach (var unresolved in dependentQueries) - { - if (unresolved.ParentQueryResultType != result.GetType()) - continue; - - foreach (var query in unresolved.Queries) - query.ResolveQuery(context, queryResult); - } - } - - foreach (var queryResult in results.Where(x => typeof(IPolymorphicResult).IsAssignableFrom(x.GetType()))) - { - if (dependentQueries.All(tuple => tuple.ParentQueryResultType != queryResult.GetType() && - !tuple.ParentQueryResultType.IsAssignableFrom(queryResult.GetType())) - ) - continue; - - var result = queryResult; - - foreach (var unresolved in dependentQueries) - { - if (unresolved.ParentQueryResultType != result.GetType() && - !unresolved.ParentQueryResultType.IsAssignableFrom(result.GetType())) - continue; - - foreach (var query in unresolved.Queries) - query.ResolveQuery(context, queryResult); - } - } - } - - public QueryList ResolvedDependents => new QueryList(dependentQueries - .SelectMany(x => x.Queries) - .Where(query => query.IsContextResolved())); - - private IList GetUniqueQueryList(IList queries) - { - if (queries == null) - return new List(); - - var distincts = queries - .Select(x => x.ParentQueryResultType) - .Distinct() - .Select(x => - { - var distinctMerge = queries - .Where(d => d.ParentQueryResultType == x) - .SelectMany(q => q.Queries) - .Distinct(new QueryComparer()) - .ToList(); - - return new ChildrenQueries { ParentQueryResultType = x, Queries = distinctMerge }; - }) - .ToList(); - - return distincts; - } - } -} \ No newline at end of file diff --git a/src/Schemio.Core/Impl/QueryBuilder.cs b/src/Schemio.Core/Impl/QueryBuilder.cs deleted file mode 100644 index 2ed81c2..0000000 --- a/src/Schemio.Core/Impl/QueryBuilder.cs +++ /dev/null @@ -1,70 +0,0 @@ -namespace Schemio.Core.Impl -{ - public class QueryBuilder : IQueryBuilder where T : IEntity - { - private readonly IEntityConfiguration entitySchema; - private readonly ISchemaPathMatcher schemaPathMatcher; - - public QueryBuilder(IEntityConfiguration entitySchema, ISchemaPathMatcher schemaPathMatcher) - { - this.entitySchema = entitySchema; - this.schemaPathMatcher = schemaPathMatcher; - } - - /// - /// Builds a list of queries for the data entity in context based on the paths provided. - /// - /// - /// - public IQueryList Build(IDataContext context) - { - var mappings = entitySchema.Mappings.ToList(); - - var queries = GetMappedQueries(mappings, context); - - foreach (var query in queries.Queries) - query.ResolveQuery(context); - - return new QueryList(queries.Queries); - } - - private QueryList GetMappedQueries(IReadOnlyCollection> mappings, IDataContext context) - { - var queryDependencyDepth = mappings.Max(x => x.Order); - - for (var index = 1; index <= queryDependencyDepth; index++) - { - var maps = mappings.Where(x => x.Order == index).ToList(); - - foreach (var map in maps) - { - var dependentQueries = - mappings.Where(x => x.Order == index + 1 && x.DependentOn != null && x.DependentOn.GetType() == map.Query.GetType()).ToList(); - - map.Query.Children = new List(); - - var filtered = FilterByPaths(context, dependentQueries); - map.Query.Children.AddRange(filtered); - } - } - - var queries = FilterByPaths(context, mappings.Where(x => x.DependentOn == null)) - .Distinct(new QueryComparer()) - .ToList(); - - return new QueryList(queries) { QueryDependencyDepth = queryDependencyDepth }; - } - - private IEnumerable FilterByPaths(IDataContext context, IEnumerable> mappings) - { - var matchedMappings = context.Request?.SchemaPaths != null - ? mappings.Where(mapping => context.Request.SchemaPaths.Any(Path => schemaPathMatcher.IsMatch(Path, mapping.SchemaPaths))) - .ToList() - : mappings; - - return matchedMappings - .Select(x => x.Query) - .Distinct(new QueryComparer()); - } - } -} \ No newline at end of file diff --git a/src/Schemio.Core/Impl/QueryExecutor.cs b/src/Schemio.Core/Impl/QueryExecutor.cs deleted file mode 100644 index 4991f17..0000000 --- a/src/Schemio.Core/Impl/QueryExecutor.cs +++ /dev/null @@ -1,76 +0,0 @@ -namespace Schemio.Core.Impl -{ - public class QueryExecutor : IQueryExecutor - { - private readonly IEnumerable queryEngines; - - public QueryExecutor(IEnumerable queryEngines) - { - this.queryEngines = queryEngines; - } - - public IList Execute(IDataContext context, IQueryList queries) - { - var globalResults = new List(); - var counter = 0; - - // supports only 5 levels of query nesting. - while (queries.As().Any() && ++counter <= 5) - queries = Process(context, queries, globalResults); - - return globalResults; - } - - private IQueryList Process(IDataContext context, IQueryList queries, List globalResults) - { - var subscriber = new EventSubscriber(queries.GetChildrenQueries()); - var eventAggregator = new EventPublisher(subscriber); - - var results = RunQueries(queries); - - CacheResults(context, results); - - eventAggregator.PublishEvent(context, new ExecutorResultArgs(results)); - - globalResults.AddRange(results); - - return subscriber.ResolvedDependents; - } - - private static void CacheResults(IDataContext context, List results) - { - if (context.Cache == null) - return; - - foreach (var cacheResult in results.Where(result => result != null && result.GetType() - .GetCustomAttributes(typeof(CacheResultAttribute), false) - .Any())) - if (!context.Cache.ContainsKey(cacheResult.GetType().Name)) - context.Cache.Add(cacheResult.GetType().Name, cacheResult); - } - - private List RunQueries(IQueryList queryList) - { - var output = new List(); - - foreach (var engine in queryEngines) - { - var tasks = queryList.Queries - .Where(x => engine.CanExecute(x)) - .Select(s => s.Run(engine)) - .ToList(); - - Task.WhenAll(tasks); - - foreach (var task in tasks) - { - var result = task.Result; - if (result != null) - output.Add(result); - } - } - - return output; - } - } -} \ No newline at end of file diff --git a/src/Schemio.Core/Motley - Backup.Core.csproj b/src/Schemio.Core/Motley - Backup.Core.csproj deleted file mode 100644 index 8d8a202..0000000 --- a/src/Schemio.Core/Motley - Backup.Core.csproj +++ /dev/null @@ -1,46 +0,0 @@ -īģŋ - - - net8.0 - enable - enable - true - Code Shayk - Code Shayk - Copyright (c) 2024 Code Shayk - https://github.com/CodeShayk/Schemio - git - README.md - ninja-icon-16.png - .Net Library to hydrate data entities by object graph using schema paths (supports XPath & JSONPath). - graphql json-schema xsd data-mapping query-engine data-mapper data-schema schema-mapping object-tree-query object-graph-query schema-mapper xsd-data-object object-graph-data entity-data entity-data-fetch hydrate-object object-hydration object-data object-fetch motley motley-core - https://github.com/CodeShayk/Schemio/wiki/i.-Home - LICENSE.md - Schemio (No Query Engine Provided) - True - True - snupkg - True - - - - - True - \ - - - True - \ - - - True - \ - - - - - - - - - diff --git a/src/Schemio.Core/PathMatchers/JPathMatcher.cs b/src/Schemio.Core/PathMatchers/JPathMatcher.cs deleted file mode 100644 index fa2d03d..0000000 --- a/src/Schemio.Core/PathMatchers/JPathMatcher.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Schemio.Core.Helpers; - -namespace Schemio.Core.PathMatchers -{ - public class JPathMatcher : ISchemaPathMatcher - { - public bool IsMatch(string inputXPath, ISchemaPaths configuredXPaths) => - // Does the template path contain any of the mapping xpaths? - inputXPath.IsNotNullOrEmpty() - && configuredXPaths.Paths.Any(x => inputXPath.ToLower().Contains(x.ToLower())); - } -} \ No newline at end of file diff --git a/src/Schemio.Core/PathMatchers/XPathMatcher.cs b/src/Schemio.Core/PathMatchers/XPathMatcher.cs deleted file mode 100644 index 77e18d0..0000000 --- a/src/Schemio.Core/PathMatchers/XPathMatcher.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System.Text.RegularExpressions; - -namespace Schemio.Core.PathMatchers -{ - public class XPathMatcher : ISchemaPathMatcher - { - private static readonly Regex ancestorRegex = new Regex(@"=ancestor::(?'path'.*?)(/@|\[.*\]/@)", RegexOptions.Compiled); - - public bool IsMatch(string inputXPath, ISchemaPaths configuredXPaths) - { - if (inputXPath == null) - return false; - - if (configuredXPaths.Paths.Any(x => inputXPath.ToLower().Contains(x.ToLower()))) - return true; - - if (configuredXPaths.Paths.Any(x => inputXPath.Contains("ancestor::") - && ancestorRegex.Matches(inputXPath).Select(match => match.Groups["path"].Value).Distinct().Any(match => x.EndsWith(match)))) - return true; - - return false; - } - } -} \ No newline at end of file diff --git a/src/Schemio.Core/QueryComparer.cs b/src/Schemio.Core/QueryComparer.cs deleted file mode 100644 index 85a1e11..0000000 --- a/src/Schemio.Core/QueryComparer.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace Schemio.Core -{ - public class QueryComparer : IEqualityComparer - { - #region IQuery - - public bool Equals(IQuery x, IQuery y) => x.GetType() == y.GetType(); - - public int GetHashCode(IQuery obj) => obj.GetType().GetHashCode(); - - #endregion IQuery - } -} \ No newline at end of file diff --git a/src/Schemio.Core/QueryList.cs b/src/Schemio.Core/QueryList.cs deleted file mode 100644 index bc9422b..0000000 --- a/src/Schemio.Core/QueryList.cs +++ /dev/null @@ -1,60 +0,0 @@ -namespace Schemio.Core -{ - public class QueryList : IQueryList - { - private readonly List queryList; - - public QueryList() - { - queryList = new List(); - } - - public IEnumerable Queries - { get { return queryList; } } - - public QueryList(IEnumerable collection) - { - queryList = new List(collection); - } - - public int QueryDependencyDepth { get; set; } - - public IQueryList GetByType() where T : class - { - var queries = queryList.Where(q => q as T != null); - return new QueryList(queries); - } - - public List As() => queryList.Cast().ToList(); - - public List GetChildrenQueries() - { - var childrenQueries = queryList - .Select(x => new ChildrenQueries { ParentQueryResultType = x.ResultType, Queries = x.Children }) - .Where(x => x.Queries != null && x.Queries.Any()) - .ToList(); - - return childrenQueries - .Select(x => - { - var distinctList = childrenQueries - .Where(d => d.ParentQueryResultType == x.ParentQueryResultType) - .SelectMany(q => q.Queries) - .Distinct(new QueryComparer()) - .ToList(); - - return new ChildrenQueries { ParentQueryResultType = x.ParentQueryResultType, Queries = distinctList }; - }) - .ToList(); - } - - public new int Count() => queryList.Count; - - public bool IsEmpty() => !queryList.Any(); - - public void AddRange(IEnumerable collection) - { - queryList.AddRange(collection); - } - } -} \ No newline at end of file diff --git a/src/Schemio.Core/Schemio.Core.csproj b/src/Schemio.Core/Schemio.Core.csproj deleted file mode 100644 index 578a7ac..0000000 --- a/src/Schemio.Core/Schemio.Core.csproj +++ /dev/null @@ -1,47 +0,0 @@ - - - - net9.0 - enable - enable - true - Code Shayk - Code Shayk - Copyright (c) 2024 Code Shayk - https://github.com/CodeShayk/Schemio - git - README.md - ninja-icon-16.png - .Net Library to hydrate data entities by object graph using schema paths (supports XPath & JSONPath). - graphql json-schema xsd data-mapping query-engine data-mapper data-schema schema-mapping object-tree-query object-graph-query schema-mapper xsd-data-object object-graph-data entity-data entity-data-fetch hydrate-object object-hydration object-data object-fetch schemio schemio-core - https://github.com/CodeShayk/Schemio/wiki/i.-Home - LICENSE.md - Schemio (No Query Engine Provided) - True - True - snupkg - True - 2.0.0 - - - - - True - \ - - - True - \ - - - True - \ - - - - - - - - - diff --git a/src/Schemio.Core/ServicesExtensions.cs b/src/Schemio.Core/ServicesExtensions.cs deleted file mode 100644 index 263be61..0000000 --- a/src/Schemio.Core/ServicesExtensions.cs +++ /dev/null @@ -1,104 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; -using Schemio.Core.Impl; -using Schemio.Core.PathMatchers; - -namespace Schemio.Core -{ - public static class ServicesExtensions - { - public static SchemioOptionsBuilder UseSchemio(this IServiceCollection services) - { - services.AddTransient(typeof(IQueryBuilder<>), typeof(QueryBuilder<>)); - services.AddTransient(typeof(IEntityBuilder<>), typeof(EntityBuilder<>)); - services.AddTransient(typeof(IDataProvider<>), typeof(DataProvider<>)); - services.AddTransient(); - services.AddTransient(); - - return new SchemioOptionsBuilder(services); - } - } - - public class SchemioOptionsBuilder : ISchemioOptions - { - public SchemioOptionsBuilder(IServiceCollection services) - { - Services = services; - } - - public IServiceCollection Services { get; } - - public ISchemioOptions WithEngine(Func queryEngine) - { - if (queryEngine != null) - { - Services.AddTransient(c => queryEngine(c)); - } - - return this; - } - - public ISchemioOptions WithEngine() where TEngine : IQueryEngine - { - Services.AddTransient(typeof(IQueryEngine), typeof(TEngine)); - - return this; - } - - public ISchemioOptions WithEngines(Func queryEngines) - { - if (queryEngines != null) - { - Services.AddTransient(c => queryEngines(c)); - } - - return this; - } - - /// - /// Register an instance of ISchemaPathMatcher. Default is XPathMatcher. - /// - /// - /// - public ISchemioOptions WithPathMatcher(Func pathMatcher) - { - if (pathMatcher != null) - { - Services.RemoveAll(); - Services.AddTransient(c => pathMatcher(c)); - } - - return this; - } - - /// - /// Register and instance of EntityConfiguration - /// You could register configuration for multiple entities. - /// - /// IEntity - /// Instance of EntityConfiguration[typeparamref name="T"] - /// - public ISchemioOptions WithEntityConfiguration(Func> entityConfiguration) where T : class, IEntity - { - if (entityConfiguration != null) - { - Services.AddTransient(typeof(IEntityConfiguration), c => entityConfiguration(c)); - } - - return this; - } - } - - public interface ISchemioOptions - { - ISchemioOptions WithEngine(Func queryEngines); - - ISchemioOptions WithEngine() where TEngine : IQueryEngine; - - ISchemioOptions WithEngines(Func queryEngines); - - ISchemioOptions WithPathMatcher(Func pathMatcher); - - ISchemioOptions WithEntityConfiguration(Func> entityConfiguration) where T : class, IEntity; - } -} \ No newline at end of file diff --git a/src/Schemio.Core/XML/XMLDataProvider.cs b/src/Schemio.Core/XML/XMLDataProvider.cs deleted file mode 100644 index f3e1872..0000000 --- a/src/Schemio.Core/XML/XMLDataProvider.cs +++ /dev/null @@ -1,70 +0,0 @@ -using System.Xml; -using System.Xml.Linq; -using System.Xml.Serialization; -using Microsoft.Extensions.Logging; -using Schemio.Core.Helpers.Xml; - -namespace Schemio.Core.XML -{ - internal class XMLDataProvider where T : IEntity - { - private readonly IEntityContextValidator entityContextValidator; - private IDataProvider dataProvider; - private readonly ILogger> logger; - - private const string AuthorNamespace = "http://www.intelligent-office.net/author/1.0"; - - public XMLDataProvider(IEntityContextValidator entityContextValidator, - IDataProvider dataProvider, - ILogger> logger) - { - this.logger = logger; - this.dataProvider = dataProvider; - this.entityContextValidator = entityContextValidator; - } - - /// - /// Gets the xml document for the data source for given xpaths and context. - /// - /// - /// XDocument - public virtual XDocument GetData(IEntityRequest context) - { - // Log Request context. - LogRequest(context); - // Validate Request context - ValidateRequest(context); - // Get entity object for given context - var entity = dataProvider.GetData(context); - // Transform entity object to xml document - var doc = TransformToXmlDocument(entity); - return doc; - } - - /// - /// Gets the xml document by serializing the data source object. - /// - /// - /// - private XDocument TransformToXmlDocument(T dataSource) - { - var namespaces = new XmlSerializerNamespaces(); - namespaces.Add("a", AuthorNamespace); - var strXml = XmlHelper.SerializeToXml(dataSource, namespaces, new XmlWriterSettings - { - OmitXmlDeclaration = true, - CheckCharacters = false - }); - - logger.LogInformation(strXml); - var xml = XmlSanitizer.Sanitize(strXml); - var doc = XDocument.Parse(xml); - - return doc; - } - - private void LogRequest(IEntityRequest context) => logger.LogInformation(context.GetType().Name); - - private void ValidateRequest(IEntityRequest context) => entityContextValidator.Validate(context); - } -} \ No newline at end of file diff --git a/src/Schemio.EntityFramework/ISQLQuery.cs b/src/Schemio.EntityFramework/ISQLQuery.cs deleted file mode 100644 index 137e8de..0000000 --- a/src/Schemio.EntityFramework/ISQLQuery.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using Schemio.Core; - -namespace Schemio.EntityFramework -{ - public interface ISQLQuery - { - /// - /// Get query delegate with implementation to return query result. - /// Delegate returns a collection from db. - /// - /// Func> - Task Run(DbContext dbContext); - } -} \ No newline at end of file diff --git a/src/Schemio.EntityFramework/QueryEngine.cs b/src/Schemio.EntityFramework/QueryEngine.cs deleted file mode 100644 index acd2b2b..0000000 --- a/src/Schemio.EntityFramework/QueryEngine.cs +++ /dev/null @@ -1,26 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using Schemio.Core; - -namespace Schemio.EntityFramework -{ - public class QueryEngine : IQueryEngine where T : DbContext - { - private readonly IDbContextFactory _dbContextFactory; - - public QueryEngine(IDbContextFactory _dbContextFactory) - { - this._dbContextFactory = _dbContextFactory; - } - - public bool CanExecute(IQuery query) => query != null && query is ISQLQuery; - - public Task Execute(IQuery query) - { - using (var dbcontext = _dbContextFactory.CreateDbContext()) - { - var result = ((ISQLQuery)query).Run(dbcontext); - return result; - } - } - } -} \ No newline at end of file diff --git a/src/Schemio.EntityFramework/SQLQuery.cs b/src/Schemio.EntityFramework/SQLQuery.cs deleted file mode 100644 index ad20b68..0000000 --- a/src/Schemio.EntityFramework/SQLQuery.cs +++ /dev/null @@ -1,32 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using Schemio.Core; - -namespace Schemio.EntityFramework -{ - public abstract class SQLQuery - : BaseQuery, ISQLQuery - where TQueryResult : IQueryResult - { - private Func> QueryDelegate = null; - - public override bool IsContextResolved() => QueryDelegate != null; - - public override void ResolveQuery(IDataContext context, IQueryResult parentQueryResult) - { - QueryDelegate = GetQuery(context, parentQueryResult); - } - - async Task ISQLQuery.Run(DbContext dbContext) - { - return await QueryDelegate(dbContext); - } - - /// - /// Get query delegate to return query result. - /// - /// - /// - /// - protected abstract Func> GetQuery(IDataContext context, IQueryResult parentQueryResult); - } -} \ No newline at end of file diff --git a/src/Schemio.EntityFramework/Schemio.EntityFramework.csproj b/src/Schemio.EntityFramework/Schemio.EntityFramework.csproj deleted file mode 100644 index b9dcc86..0000000 --- a/src/Schemio.EntityFramework/Schemio.EntityFramework.csproj +++ /dev/null @@ -1,54 +0,0 @@ - - - - net9.0 - enable - true - enable - Schemio with EntityFramework - Code Shayk - Code Shayk - .Net Library to hydrate data entities by object graph using schema paths (supports XPath & JSONPath). Supports Entity Framework for querying database. - Copyright (c) 2024 Code Shayk - https://github.com/CodeShayk/Schemio/wiki/i.-Home - ninja-icon-16.png - README.md - https://github.com/CodeShayk/Schemio - git - graphql json-schema xsd data-mapping query-engine data-mapper data-schema schema-mapping object-tree-query object-graph-query schema-mapper xsd-data-object object-graph-data entity-data entity-data-fetch hydrate-object object-hydration object-data object-fetch schemio-entity-framework entity-framework - LICENSE.md - True - True - snupkg - True - 2.0.0 - - - - True - - - - - True - \ - - - True - \ - - - True - \ - - - - - - - - - - - - diff --git a/src/Schemio.SQL/ISQLQuery.cs b/src/Schemio.SQL/ISQLQuery.cs deleted file mode 100644 index 7292752..0000000 --- a/src/Schemio.SQL/ISQLQuery.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Data; -using Schemio.Core; - -namespace Schemio.SQL -{ - public interface ISQLQuery : IQuery - { - Task Run(IDbConnection conn); - } -} \ No newline at end of file diff --git a/src/Schemio.SQL/QueryEngine.cs b/src/Schemio.SQL/QueryEngine.cs deleted file mode 100644 index ad5d168..0000000 --- a/src/Schemio.SQL/QueryEngine.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System.Data.Common; -using Schemio.Core; - -namespace Schemio.SQL -{ - public class QueryEngine : IQueryEngine - { - private readonly SQLConfiguration sqlConfiguration; - - public QueryEngine(SQLConfiguration sqlConfiguration) - { - if (sqlConfiguration?.ConnectionSettings?.ProviderName == null) - throw new ArgumentNullException($"SQL Configuration is required with connection settings. Provider name is missing."); - - if (sqlConfiguration?.ConnectionSettings?.ConnectionString == null) - throw new ArgumentNullException($"SQL Configuration is required with connection settings. Connection string is missing."); - - this.sqlConfiguration = sqlConfiguration; - } - - public bool CanExecute(IQuery query) => query != null && query is ISQLQuery; - - public Task Execute(IQuery query) - { - var factory = DbProviderFactories.GetFactory(sqlConfiguration.ConnectionSettings.ProviderName) - ?? throw new InvalidOperationException($"Provider: {sqlConfiguration.ConnectionSettings.ProviderName} is not supported. Please register entry in DbProviderFactories "); - - using (var connection = factory.CreateConnection()) - { - if (connection == null) - throw new Exception($"Failed to create connection with Provider: {sqlConfiguration.ConnectionSettings.ProviderName}. Please check the connection settings."); - - connection.ConnectionString = sqlConfiguration.ConnectionSettings.ConnectionString; - - var result = ((ISQLQuery)query).Run(connection); - - return result; - } - } - } -} \ No newline at end of file diff --git a/src/Schemio.SQL/SQLQuery.cs b/src/Schemio.SQL/SQLQuery.cs deleted file mode 100644 index 85fa883..0000000 --- a/src/Schemio.SQL/SQLQuery.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System.Data; -using Schemio.Core; - -namespace Schemio.SQL -{ - public abstract class SQLQuery : BaseQuery, ISQLQuery - where TQueryResult : IQueryResult - { - async Task ISQLQuery.Run(IDbConnection conn) - { - return await QueryDelegate(conn); - } - - private Func> QueryDelegate = null; - - public override bool IsContextResolved() => QueryDelegate != null; - - public override void ResolveQuery(IDataContext context, IQueryResult parentQueryResult) - { - QueryDelegate = GetQuery(context, parentQueryResult); - } - - /// - /// Get query delegate to return query result. - /// - /// - /// - /// - protected abstract Func> GetQuery(IDataContext context, IQueryResult parentQueryResult); - } -} \ No newline at end of file diff --git a/src/Schemio.SQL/Schemio.SQL.csproj b/src/Schemio.SQL/Schemio.SQL.csproj deleted file mode 100644 index 1946be3..0000000 --- a/src/Schemio.SQL/Schemio.SQL.csproj +++ /dev/null @@ -1,55 +0,0 @@ - - - - net9.0 - enable - enable - true - Schemio with SQL - Code Shayk - Code Shayk - graphql json-schema xsd data-mapping query-engine data-mapper data-schema schema-mapping object-tree-query object-graph-query schema-mapper xsd-data-object object-graph-data entity-data entity-data-fetch hydrate-object object-hydration object-data object-fetch schemio-sql dapper-sql dapper - https://github.com/CodeShayk/Schemio - git - README.md - ninja-icon-16.png - https://github.com/CodeShayk/Schemio/wiki/i.-Home - Copyright (c) 2024 Code Shayk - .Net Library to hydrate data entities by object graph using schema paths (supports XPath & JSONPath). Supports Dapper for querying SQL database. - LICENSE.md - True - True - snupkg - True - 2.0.0 - - - - True - - - - - True - \ - - - True - \ - - - True - \ - - - - - - - - - - - - - diff --git a/src/Schemio.SQL/SqlConfiguration.cs b/src/Schemio.SQL/SqlConfiguration.cs deleted file mode 100644 index b0c3869..0000000 --- a/src/Schemio.SQL/SqlConfiguration.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace Schemio.SQL -{ - public class SQLConfiguration - { - public SQLConfiguration() - { - ConnectionSettings = new ConnectionSettings(); - QuerySettings = new QuerySettings(); - } - - public ConnectionSettings ConnectionSettings { get; set; } - public QuerySettings QuerySettings { get; set; } - } - - public class ConnectionSettings - { - public string ProviderName { get; set; } - public string ConnectionString { get; set; } - } - - public class QuerySettings - { - public int QueryBatchSize { get; set; } = 10; - } -} \ No newline at end of file diff --git a/tests/Schemio.EntityFramework.Tests/App.config b/tests/DataFuse.Adapters.EntityFramework.Tests/App.config similarity index 100% rename from tests/Schemio.EntityFramework.Tests/App.config rename to tests/DataFuse.Adapters.EntityFramework.Tests/App.config diff --git a/tests/Schemio.EntityFramework.Tests/BaseTest.cs b/tests/DataFuse.Adapters.EntityFramework.Tests/BaseTest.cs similarity index 77% rename from tests/Schemio.EntityFramework.Tests/BaseTest.cs rename to tests/DataFuse.Adapters.EntityFramework.Tests/BaseTest.cs index c44f222..44ed17d 100644 --- a/tests/Schemio.EntityFramework.Tests/BaseTest.cs +++ b/tests/DataFuse.Adapters.EntityFramework.Tests/BaseTest.cs @@ -1,13 +1,14 @@ -using System; using Microsoft.EntityFrameworkCore; +using DataFuse.Integration; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using Schemio.Core; -using Schemio.Core.Helpers; -using Schemio.Core.PathMatchers; -using Schemio.EntityFramework.Tests.EntitySetup.Entities; -using Schemio.EntityFramework.Tests.EntitySetup.EntitySchemas; +using DataFuse.Adapters.Abstraction; +using DataFuse.Integration.Helpers; +using DataFuse.Integration.PathMatchers; +using DataFuse.Adapters.EntityFramework.Tests.EntitySetup.Entities; +using DataFuse.Adapters.EntityFramework.Tests.EntitySetup.EntitySchemas; -namespace Schemio.EntityFramework.Tests +namespace DataFuse.Adapters.EntityFramework.Tests { public class BaseTest { @@ -39,10 +40,10 @@ public void Setup() services.AddLogging(); - services.UseSchemio() + services.UseDataFuse(configuration => configuration .WithEngine(c => new QueryEngine(c.GetService>())) .WithPathMatcher(c => new XPathMatcher()) - .WithEntityConfiguration(c => new CustomerConfiguration()); + .WithEntityConfiguration(c => new CustomerConfiguration())); // 4. Build the service provider _serviceProvider = services.BuildServiceProvider(); diff --git a/tests/Schemio.EntityFramework.Tests/Customer.db b/tests/DataFuse.Adapters.EntityFramework.Tests/Customer.db similarity index 98% rename from tests/Schemio.EntityFramework.Tests/Customer.db rename to tests/DataFuse.Adapters.EntityFramework.Tests/Customer.db index 1d5c292..82716c1 100644 Binary files a/tests/Schemio.EntityFramework.Tests/Customer.db and b/tests/DataFuse.Adapters.EntityFramework.Tests/Customer.db differ diff --git a/tests/Schemio.EntityFramework.Tests/CustomerDbContext.cs b/tests/DataFuse.Adapters.EntityFramework.Tests/CustomerDbContext.cs similarity index 92% rename from tests/Schemio.EntityFramework.Tests/CustomerDbContext.cs rename to tests/DataFuse.Adapters.EntityFramework.Tests/CustomerDbContext.cs index 93a31f3..4cd3a03 100644 --- a/tests/Schemio.EntityFramework.Tests/CustomerDbContext.cs +++ b/tests/DataFuse.Adapters.EntityFramework.Tests/CustomerDbContext.cs @@ -1,8 +1,9 @@ +using System.Globalization; using Microsoft.EntityFrameworkCore; -using Schemio.Core.Helpers; -using Schemio.EntityFramework.Tests.Domain; +using DataFuse.Integration.Helpers; +using DataFuse.Adapters.EntityFramework.Tests.Domain; -namespace Schemio.EntityFramework.Tests +namespace DataFuse.Adapters.EntityFramework.Tests { public class CustomerDbContext : DbContext { @@ -54,7 +55,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) eb.HasKey(b => b.OrderId); eb.Property(b => b.OrderNo); eb.Property(b => b.Date).HasColumnName("OrderDate") - .HasConversion(v => v.ToShortDateString(), s => s.IsNotNullOrEmpty() ? DateTime.ParseExact(s, "dd/MM/yyyy", null) : DateTime.MinValue); + .HasConversion(v => v.ToShortDateString(), s => s.IsNotNullOrEmpty() ? DateTime.ParseExact(s, "dd/MM/yyyy HH:mm:ss", CultureInfo.InvariantCulture) : DateTime.MinValue); eb.HasOne(b => b.Customer); eb.HasMany(b => b.Items); }); diff --git a/tests/Schemio.EntityFramework.Tests/Schemio.EntityFramework.Tests.csproj b/tests/DataFuse.Adapters.EntityFramework.Tests/DataFuse.Adapters.EntityFramework.Tests.csproj similarity index 60% rename from tests/Schemio.EntityFramework.Tests/Schemio.EntityFramework.Tests.csproj rename to tests/DataFuse.Adapters.EntityFramework.Tests/DataFuse.Adapters.EntityFramework.Tests.csproj index 7aa3bf6..7b00a14 100644 --- a/tests/Schemio.EntityFramework.Tests/Schemio.EntityFramework.Tests.csproj +++ b/tests/DataFuse.Adapters.EntityFramework.Tests/DataFuse.Adapters.EntityFramework.Tests.csproj @@ -1,7 +1,7 @@ - net9.0 + net10.0 enable enable @@ -10,24 +10,25 @@ - - + + - - - - + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + + diff --git a/tests/Schemio.EntityFramework.Tests/Domain/Address.cs b/tests/DataFuse.Adapters.EntityFramework.Tests/Domain/Address.cs similarity index 87% rename from tests/Schemio.EntityFramework.Tests/Domain/Address.cs rename to tests/DataFuse.Adapters.EntityFramework.Tests/Domain/Address.cs index defb04b..f3f3640 100644 --- a/tests/Schemio.EntityFramework.Tests/Domain/Address.cs +++ b/tests/DataFuse.Adapters.EntityFramework.Tests/Domain/Address.cs @@ -1,4 +1,4 @@ -namespace Schemio.EntityFramework.Tests.Domain +namespace DataFuse.Adapters.EntityFramework.Tests.Domain { public class Address { diff --git a/tests/Schemio.EntityFramework.Tests/Domain/Communication.cs b/tests/DataFuse.Adapters.EntityFramework.Tests/Domain/Communication.cs similarity index 84% rename from tests/Schemio.EntityFramework.Tests/Domain/Communication.cs rename to tests/DataFuse.Adapters.EntityFramework.Tests/Domain/Communication.cs index 0865d3f..1640d11 100644 --- a/tests/Schemio.EntityFramework.Tests/Domain/Communication.cs +++ b/tests/DataFuse.Adapters.EntityFramework.Tests/Domain/Communication.cs @@ -1,4 +1,4 @@ -namespace Schemio.EntityFramework.Tests.Domain +namespace DataFuse.Adapters.EntityFramework.Tests.Domain { public class Communication { diff --git a/tests/Schemio.EntityFramework.Tests/Domain/Customer.cs b/tests/DataFuse.Adapters.EntityFramework.Tests/Domain/Customer.cs similarity index 82% rename from tests/Schemio.EntityFramework.Tests/Domain/Customer.cs rename to tests/DataFuse.Adapters.EntityFramework.Tests/Domain/Customer.cs index 50f0fce..b03e4d5 100644 --- a/tests/Schemio.EntityFramework.Tests/Domain/Customer.cs +++ b/tests/DataFuse.Adapters.EntityFramework.Tests/Domain/Customer.cs @@ -1,4 +1,4 @@ -namespace Schemio.EntityFramework.Tests.Domain +namespace DataFuse.Adapters.EntityFramework.Tests.Domain { public class Customer { diff --git a/tests/Schemio.EntityFramework.Tests/Domain/Order.cs b/tests/DataFuse.Adapters.EntityFramework.Tests/Domain/Order.cs similarity index 84% rename from tests/Schemio.EntityFramework.Tests/Domain/Order.cs rename to tests/DataFuse.Adapters.EntityFramework.Tests/Domain/Order.cs index 85c6a48..1000e09 100644 --- a/tests/Schemio.EntityFramework.Tests/Domain/Order.cs +++ b/tests/DataFuse.Adapters.EntityFramework.Tests/Domain/Order.cs @@ -1,4 +1,4 @@ -namespace Schemio.EntityFramework.Tests.Domain +namespace DataFuse.Adapters.EntityFramework.Tests.Domain { public class Order { diff --git a/tests/Schemio.EntityFramework.Tests/Domain/OrderItem.cs b/tests/DataFuse.Adapters.EntityFramework.Tests/Domain/OrderItem.cs similarity index 81% rename from tests/Schemio.EntityFramework.Tests/Domain/OrderItem.cs rename to tests/DataFuse.Adapters.EntityFramework.Tests/Domain/OrderItem.cs index 04897bd..14fbf66 100644 --- a/tests/Schemio.EntityFramework.Tests/Domain/OrderItem.cs +++ b/tests/DataFuse.Adapters.EntityFramework.Tests/Domain/OrderItem.cs @@ -1,4 +1,4 @@ -namespace Schemio.EntityFramework.Tests.Domain +namespace DataFuse.Adapters.EntityFramework.Tests.Domain { public class OrderItem { diff --git a/tests/DataFuse.Adapters.EntityFramework.Tests/E2E.Tests.cs b/tests/DataFuse.Adapters.EntityFramework.Tests/E2E.Tests.cs new file mode 100644 index 0000000..58caaba --- /dev/null +++ b/tests/DataFuse.Adapters.EntityFramework.Tests/E2E.Tests.cs @@ -0,0 +1,138 @@ +using Microsoft.Extensions.DependencyInjection; +using DataFuse.Integration; +using DataFuse.Adapters.Abstraction; +using DataFuse.Adapters.EntityFramework.Tests.EntitySetup; +using DataFuse.Adapters.EntityFramework.Tests.EntitySetup.Entities; + +namespace DataFuse.Adapters.EntityFramework.Tests; + +[TestFixture] +public class E2ETests : BaseTest +{ + private IDataProvider _provider; + + [SetUp] + public void Setup() + { + _provider = _serviceProvider.GetService>(); + } + + [Test] + public async Task TestDataProviderToFetchWholeEntityWhenPathsAreNull() + { + var customer = await _provider.GetDataAsync(new CustomerRequest + { + CustomerId = 1 + }); + + var expected = new Customer + { + Id = 1, + Name = "Jack Sparrow", + Code = "AB123", + Communication = new Communication + { + ContactId = 1, + Phone = "0123456789", + Email = "jack.sparrow@gmail.com", + Address = new Address + { + AddressId = 1, + HouseNo = "77", + City = "Wansted", + Region = "Belfast", + PostalCode = "BL34Y56", + Country = "United Kingdom", + } + }, + Orders = [ new Order { + OrderId = 1, + OrderNo = "ZX123VH", + Date = DateTime.Parse("2021-10-22T12:13:04"), + Items = + [ + new OrderItem + { + ItemId = 1, Name = "12 inch Cake", Cost = 30m + }, + new OrderItem + { + ItemId = 2, Name = "20 Cake Candles", Cost = 5m + } + ] + }] + }; + + AssertAreEqual(expected, customer); + } + + [Test] + public async Task TestDataProviderToFetchEntityWhenPathsContainsCommunication() + { + var customer = await _provider.GetDataAsync(new CustomerRequest + { + CustomerId = 1, + SchemaPaths = new[] { "Customer/Communication" } + }); + + var expected = new Customer + { + Id = 1, + Name = "Jack Sparrow", + Code = "AB123", + Communication = new Communication + { + ContactId = 1, + Phone = "0123456789", + Email = "jack.sparrow@gmail.com", + Address = new Address + { + AddressId = 1, + HouseNo = "77", + City = "Wansted", + Region = "Belfast", + PostalCode = "BL34Y56", + Country = "United Kingdom", + } + } + }; + + AssertAreEqual(expected, customer); + } + + [Test] + public async Task TestDataProviderToFetchEntityWhenPathsContainsOrderItems() + { + var customer = await _provider.GetDataAsync(new CustomerRequest + { + CustomerId = 1, + SchemaPaths = new[] { "Customer/orders/order/items/item" } + }); + + var expected = new Customer + { + Id = 1, + Name = "Jack Sparrow", + Code = "AB123", + Orders = [ new Order + { + OrderId = 1, + OrderNo = "ZX123VH", + Date = DateTime.Parse("2021-10-22T12:13:04"), + Items = + [ + new OrderItem + { + ItemId = 1, Name = "12 inch Cake", Cost = 30m + }, + new OrderItem + { + ItemId = 2, Name = "20 Cake Candles", Cost = 5m + } + ] + }] + }; + + AssertAreEqual(expected, customer); + } +} diff --git a/tests/Schemio.SQL.Tests/EntitySetup/CustomerRequest.cs b/tests/DataFuse.Adapters.EntityFramework.Tests/EntitySetup/CustomerRequest.cs similarity index 62% rename from tests/Schemio.SQL.Tests/EntitySetup/CustomerRequest.cs rename to tests/DataFuse.Adapters.EntityFramework.Tests/EntitySetup/CustomerRequest.cs index 6234c15..b111fbe 100644 --- a/tests/Schemio.SQL.Tests/EntitySetup/CustomerRequest.cs +++ b/tests/DataFuse.Adapters.EntityFramework.Tests/EntitySetup/CustomerRequest.cs @@ -1,6 +1,6 @@ -using Schemio.Core; +using DataFuse.Adapters.Abstraction; -namespace Schemio.SQL.Tests.EntitySetup +namespace DataFuse.Adapters.EntityFramework.Tests.EntitySetup { internal class CustomerRequest : IEntityRequest { diff --git a/tests/Schemio.EntityFramework.Tests/EntitySetup/Entities/Address.cs b/tests/DataFuse.Adapters.EntityFramework.Tests/EntitySetup/Entities/Address.cs similarity index 80% rename from tests/Schemio.EntityFramework.Tests/EntitySetup/Entities/Address.cs rename to tests/DataFuse.Adapters.EntityFramework.Tests/EntitySetup/Entities/Address.cs index cd77da9..9ea357d 100644 --- a/tests/Schemio.EntityFramework.Tests/EntitySetup/Entities/Address.cs +++ b/tests/DataFuse.Adapters.EntityFramework.Tests/EntitySetup/Entities/Address.cs @@ -1,4 +1,4 @@ -namespace Schemio.EntityFramework.Tests.EntitySetup.Entities +namespace DataFuse.Adapters.EntityFramework.Tests.EntitySetup.Entities { public class Address { diff --git a/tests/Schemio.EntityFramework.Tests/EntitySetup/Entities/Communication.cs b/tests/DataFuse.Adapters.EntityFramework.Tests/EntitySetup/Entities/Communication.cs similarity index 75% rename from tests/Schemio.EntityFramework.Tests/EntitySetup/Entities/Communication.cs rename to tests/DataFuse.Adapters.EntityFramework.Tests/EntitySetup/Entities/Communication.cs index 5f2c87a..368fe1f 100644 --- a/tests/Schemio.EntityFramework.Tests/EntitySetup/Entities/Communication.cs +++ b/tests/DataFuse.Adapters.EntityFramework.Tests/EntitySetup/Entities/Communication.cs @@ -1,4 +1,4 @@ -namespace Schemio.EntityFramework.Tests.EntitySetup.Entities +namespace DataFuse.Adapters.EntityFramework.Tests.EntitySetup.Entities { public class Communication { diff --git a/tests/Schemio.EntityFramework.Tests/EntitySetup/Entities/Customer.cs b/tests/DataFuse.Adapters.EntityFramework.Tests/EntitySetup/Entities/Customer.cs similarity index 71% rename from tests/Schemio.EntityFramework.Tests/EntitySetup/Entities/Customer.cs rename to tests/DataFuse.Adapters.EntityFramework.Tests/EntitySetup/Entities/Customer.cs index 4cce91d..1477a9b 100644 --- a/tests/Schemio.EntityFramework.Tests/EntitySetup/Entities/Customer.cs +++ b/tests/DataFuse.Adapters.EntityFramework.Tests/EntitySetup/Entities/Customer.cs @@ -1,6 +1,6 @@ -using Schemio.Core; +using DataFuse.Adapters.Abstraction; -namespace Schemio.EntityFramework.Tests.EntitySetup.Entities +namespace DataFuse.Adapters.EntityFramework.Tests.EntitySetup.Entities { public class Customer : IEntity { diff --git a/tests/Schemio.Core.Tests/EntitySetup/Entities/EntityDiagram.cd b/tests/DataFuse.Adapters.EntityFramework.Tests/EntitySetup/Entities/EntityDiagram.cd similarity index 100% rename from tests/Schemio.Core.Tests/EntitySetup/Entities/EntityDiagram.cd rename to tests/DataFuse.Adapters.EntityFramework.Tests/EntitySetup/Entities/EntityDiagram.cd diff --git a/tests/Schemio.EntityFramework.Tests/EntitySetup/Entities/Order.cs b/tests/DataFuse.Adapters.EntityFramework.Tests/EntitySetup/Entities/Order.cs similarity index 75% rename from tests/Schemio.EntityFramework.Tests/EntitySetup/Entities/Order.cs rename to tests/DataFuse.Adapters.EntityFramework.Tests/EntitySetup/Entities/Order.cs index d71c082..be28f20 100644 --- a/tests/Schemio.EntityFramework.Tests/EntitySetup/Entities/Order.cs +++ b/tests/DataFuse.Adapters.EntityFramework.Tests/EntitySetup/Entities/Order.cs @@ -1,4 +1,4 @@ -namespace Schemio.EntityFramework.Tests.EntitySetup.Entities +namespace DataFuse.Adapters.EntityFramework.Tests.EntitySetup.Entities { public class Order { diff --git a/tests/Schemio.EntityFramework.Tests/EntitySetup/Entities/OrderItem.cs b/tests/DataFuse.Adapters.EntityFramework.Tests/EntitySetup/Entities/OrderItem.cs similarity index 69% rename from tests/Schemio.EntityFramework.Tests/EntitySetup/Entities/OrderItem.cs rename to tests/DataFuse.Adapters.EntityFramework.Tests/EntitySetup/Entities/OrderItem.cs index fabe86c..81d2654 100644 --- a/tests/Schemio.EntityFramework.Tests/EntitySetup/Entities/OrderItem.cs +++ b/tests/DataFuse.Adapters.EntityFramework.Tests/EntitySetup/Entities/OrderItem.cs @@ -1,4 +1,4 @@ -namespace Schemio.EntityFramework.Tests.EntitySetup.Entities +namespace DataFuse.Adapters.EntityFramework.Tests.EntitySetup.Entities { public class OrderItem { diff --git a/tests/Schemio.SQL.Tests/EntitySetup/EntityConfiguration/CustomerConfiguration.cs b/tests/DataFuse.Adapters.EntityFramework.Tests/EntitySetup/EntitySchemas/CustomerConfiguration.cs similarity index 68% rename from tests/Schemio.SQL.Tests/EntitySetup/EntityConfiguration/CustomerConfiguration.cs rename to tests/DataFuse.Adapters.EntityFramework.Tests/EntitySetup/EntitySchemas/CustomerConfiguration.cs index 37797f7..8e3f8ca 100644 --- a/tests/Schemio.SQL.Tests/EntitySetup/EntityConfiguration/CustomerConfiguration.cs +++ b/tests/DataFuse.Adapters.EntityFramework.Tests/EntitySetup/EntitySchemas/CustomerConfiguration.cs @@ -1,9 +1,9 @@ -using Schemio.Core; -using Schemio.SQL.Tests.EntitySetup.Entities; -using Schemio.SQL.Tests.EntitySetup.EntitySchemas.Queries; -using Schemio.SQL.Tests.EntitySetup.EntitySchemas.Transforms; +using DataFuse.Adapters.Abstraction; +using DataFuse.Adapters.EntityFramework.Tests.EntitySetup.Entities; +using DataFuse.Adapters.EntityFramework.Tests.EntitySetup.EntitySchemas.Queries; +using DataFuse.Adapters.EntityFramework.Tests.EntitySetup.EntitySchemas.Transforms; -namespace Schemio.SQL.Tests.EntitySetup.EntitySchemas +namespace DataFuse.Adapters.EntityFramework.Tests.EntitySetup.EntitySchemas { internal class CustomerConfiguration : EntityConfiguration { diff --git a/tests/Schemio.EntityFramework.Tests/EntitySetup/EntitySchemas/Queries/CommunicationQuery.cs b/tests/DataFuse.Adapters.EntityFramework.Tests/EntitySetup/EntitySchemas/Queries/CommunicationQuery.cs similarity index 85% rename from tests/Schemio.EntityFramework.Tests/EntitySetup/EntitySchemas/Queries/CommunicationQuery.cs rename to tests/DataFuse.Adapters.EntityFramework.Tests/EntitySetup/EntitySchemas/Queries/CommunicationQuery.cs index 914b098..9c03ddf 100644 --- a/tests/Schemio.EntityFramework.Tests/EntitySetup/EntitySchemas/Queries/CommunicationQuery.cs +++ b/tests/DataFuse.Adapters.EntityFramework.Tests/EntitySetup/EntitySchemas/Queries/CommunicationQuery.cs @@ -1,8 +1,8 @@ using Microsoft.EntityFrameworkCore; -using Schemio.Core; -using Schemio.EntityFramework.Tests.Domain; +using DataFuse.Adapters.Abstraction; +using DataFuse.Adapters.EntityFramework.Tests.Domain; -namespace Schemio.EntityFramework.Tests.EntitySetup.EntitySchemas.Queries +namespace DataFuse.Adapters.EntityFramework.Tests.EntitySetup.EntitySchemas.Queries { internal class CommunicationQuery : SQLQuery { diff --git a/tests/Schemio.EntityFramework.Tests/EntitySetup/EntitySchemas/Queries/CommunicationRecord.cs b/tests/DataFuse.Adapters.EntityFramework.Tests/EntitySetup/EntitySchemas/Queries/CommunicationRecord.cs similarity index 79% rename from tests/Schemio.EntityFramework.Tests/EntitySetup/EntitySchemas/Queries/CommunicationRecord.cs rename to tests/DataFuse.Adapters.EntityFramework.Tests/EntitySetup/EntitySchemas/Queries/CommunicationRecord.cs index 29a8b01..4a1de9d 100644 --- a/tests/Schemio.EntityFramework.Tests/EntitySetup/EntitySchemas/Queries/CommunicationRecord.cs +++ b/tests/DataFuse.Adapters.EntityFramework.Tests/EntitySetup/EntitySchemas/Queries/CommunicationRecord.cs @@ -1,6 +1,6 @@ -using Schemio.Core; +using DataFuse.Adapters.Abstraction; -namespace Schemio.EntityFramework.Tests.EntitySetup.EntitySchemas.Queries +namespace DataFuse.Adapters.EntityFramework.Tests.EntitySetup.EntitySchemas.Queries { [CacheResult] public class CommunicationRecord : IQueryResult diff --git a/tests/Schemio.EntityFramework.Tests/EntitySetup/EntitySchemas/Queries/CustomerQuery.cs b/tests/DataFuse.Adapters.EntityFramework.Tests/EntitySetup/EntitySchemas/Queries/CustomerQuery.cs similarity index 83% rename from tests/Schemio.EntityFramework.Tests/EntitySetup/EntitySchemas/Queries/CustomerQuery.cs rename to tests/DataFuse.Adapters.EntityFramework.Tests/EntitySetup/EntitySchemas/Queries/CustomerQuery.cs index 4e8daa8..03e412c 100644 --- a/tests/Schemio.EntityFramework.Tests/EntitySetup/EntitySchemas/Queries/CustomerQuery.cs +++ b/tests/DataFuse.Adapters.EntityFramework.Tests/EntitySetup/EntitySchemas/Queries/CustomerQuery.cs @@ -1,8 +1,8 @@ using Microsoft.EntityFrameworkCore; -using Schemio.Core; -using Schemio.EntityFramework.Tests.Domain; +using DataFuse.Adapters.Abstraction; +using DataFuse.Adapters.EntityFramework.Tests.Domain; -namespace Schemio.EntityFramework.Tests.EntitySetup.EntitySchemas.Queries +namespace DataFuse.Adapters.EntityFramework.Tests.EntitySetup.EntitySchemas.Queries { public class CustomerQuery : SQLQuery { diff --git a/tests/Schemio.SQL.Tests/EntitySetup/EntityConfiguration/Queries/CustomerRecord.cs b/tests/DataFuse.Adapters.EntityFramework.Tests/EntitySetup/EntitySchemas/Queries/CustomerRecord.cs similarity index 59% rename from tests/Schemio.SQL.Tests/EntitySetup/EntityConfiguration/Queries/CustomerRecord.cs rename to tests/DataFuse.Adapters.EntityFramework.Tests/EntitySetup/EntitySchemas/Queries/CustomerRecord.cs index 67038cf..d4c36f2 100644 --- a/tests/Schemio.SQL.Tests/EntitySetup/EntityConfiguration/Queries/CustomerRecord.cs +++ b/tests/DataFuse.Adapters.EntityFramework.Tests/EntitySetup/EntitySchemas/Queries/CustomerRecord.cs @@ -1,6 +1,6 @@ -using Schemio.Core; +using DataFuse.Adapters.Abstraction; -namespace Schemio.SQL.Tests.EntitySetup.EntitySchemas.Queries +namespace DataFuse.Adapters.EntityFramework.Tests.EntitySetup.EntitySchemas.Queries { public class CustomerRecord : IQueryResult { diff --git a/tests/Schemio.EntityFramework.Tests/EntitySetup/EntitySchemas/Queries/OrderItemRecord.cs b/tests/DataFuse.Adapters.EntityFramework.Tests/EntitySetup/EntitySchemas/Queries/OrderItemRecord.cs similarity index 65% rename from tests/Schemio.EntityFramework.Tests/EntitySetup/EntitySchemas/Queries/OrderItemRecord.cs rename to tests/DataFuse.Adapters.EntityFramework.Tests/EntitySetup/EntitySchemas/Queries/OrderItemRecord.cs index bb1c216..3ab9e3f 100644 --- a/tests/Schemio.EntityFramework.Tests/EntitySetup/EntitySchemas/Queries/OrderItemRecord.cs +++ b/tests/DataFuse.Adapters.EntityFramework.Tests/EntitySetup/EntitySchemas/Queries/OrderItemRecord.cs @@ -1,6 +1,6 @@ -using Schemio.Core; +using DataFuse.Adapters.Abstraction; -namespace Schemio.EntityFramework.Tests.EntitySetup.EntitySchemas.Queries +namespace DataFuse.Adapters.EntityFramework.Tests.EntitySetup.EntitySchemas.Queries { public class OrderItemRecord : IQueryResult { diff --git a/tests/Schemio.EntityFramework.Tests/EntitySetup/EntitySchemas/Queries/OrderItemsQuery.cs b/tests/DataFuse.Adapters.EntityFramework.Tests/EntitySetup/EntitySchemas/Queries/OrderItemsQuery.cs similarity index 84% rename from tests/Schemio.EntityFramework.Tests/EntitySetup/EntitySchemas/Queries/OrderItemsQuery.cs rename to tests/DataFuse.Adapters.EntityFramework.Tests/EntitySetup/EntitySchemas/Queries/OrderItemsQuery.cs index 6c306ec..c38c0e2 100644 --- a/tests/Schemio.EntityFramework.Tests/EntitySetup/EntitySchemas/Queries/OrderItemsQuery.cs +++ b/tests/DataFuse.Adapters.EntityFramework.Tests/EntitySetup/EntitySchemas/Queries/OrderItemsQuery.cs @@ -1,8 +1,8 @@ using Microsoft.EntityFrameworkCore; -using Schemio.Core; -using Schemio.EntityFramework.Tests.Domain; +using DataFuse.Adapters.Abstraction; +using DataFuse.Adapters.EntityFramework.Tests.Domain; -namespace Schemio.EntityFramework.Tests.EntitySetup.EntitySchemas.Queries +namespace DataFuse.Adapters.EntityFramework.Tests.EntitySetup.EntitySchemas.Queries { internal class OrderItemsQuery : SQLQuery> { diff --git a/tests/Schemio.EntityFramework.Tests/EntitySetup/EntitySchemas/Queries/OrderRecord.cs b/tests/DataFuse.Adapters.EntityFramework.Tests/EntitySetup/EntitySchemas/Queries/OrderRecord.cs similarity index 65% rename from tests/Schemio.EntityFramework.Tests/EntitySetup/EntitySchemas/Queries/OrderRecord.cs rename to tests/DataFuse.Adapters.EntityFramework.Tests/EntitySetup/EntitySchemas/Queries/OrderRecord.cs index 2892312..7b91103 100644 --- a/tests/Schemio.EntityFramework.Tests/EntitySetup/EntitySchemas/Queries/OrderRecord.cs +++ b/tests/DataFuse.Adapters.EntityFramework.Tests/EntitySetup/EntitySchemas/Queries/OrderRecord.cs @@ -1,6 +1,6 @@ -using Schemio.Core; +using DataFuse.Adapters.Abstraction; -namespace Schemio.EntityFramework.Tests.EntitySetup.EntitySchemas.Queries +namespace DataFuse.Adapters.EntityFramework.Tests.EntitySetup.EntitySchemas.Queries { public class OrderRecord : IQueryResult { diff --git a/tests/Schemio.EntityFramework.Tests/EntitySetup/EntitySchemas/Queries/OrdersQuery.cs b/tests/DataFuse.Adapters.EntityFramework.Tests/EntitySetup/EntitySchemas/Queries/OrdersQuery.cs similarity index 84% rename from tests/Schemio.EntityFramework.Tests/EntitySetup/EntitySchemas/Queries/OrdersQuery.cs rename to tests/DataFuse.Adapters.EntityFramework.Tests/EntitySetup/EntitySchemas/Queries/OrdersQuery.cs index a16eae5..9b3079a 100644 --- a/tests/Schemio.EntityFramework.Tests/EntitySetup/EntitySchemas/Queries/OrdersQuery.cs +++ b/tests/DataFuse.Adapters.EntityFramework.Tests/EntitySetup/EntitySchemas/Queries/OrdersQuery.cs @@ -1,8 +1,8 @@ using Microsoft.EntityFrameworkCore; -using Schemio.Core; -using Schemio.EntityFramework.Tests.Domain; +using DataFuse.Adapters.Abstraction; +using DataFuse.Adapters.EntityFramework.Tests.Domain; -namespace Schemio.EntityFramework.Tests.EntitySetup.EntitySchemas.Queries +namespace DataFuse.Adapters.EntityFramework.Tests.EntitySetup.EntitySchemas.Queries { internal class OrdersQuery : SQLQuery> { diff --git a/tests/Schemio.EntityFramework.Tests/EntitySetup/EntitySchemas/Transforms/CommunicationTransform.cs b/tests/DataFuse.Adapters.EntityFramework.Tests/EntitySetup/EntitySchemas/Transforms/CommunicationTransform.cs similarity index 77% rename from tests/Schemio.EntityFramework.Tests/EntitySetup/EntitySchemas/Transforms/CommunicationTransform.cs rename to tests/DataFuse.Adapters.EntityFramework.Tests/EntitySetup/EntitySchemas/Transforms/CommunicationTransform.cs index bd52373..3bd2b4e 100644 --- a/tests/Schemio.EntityFramework.Tests/EntitySetup/EntitySchemas/Transforms/CommunicationTransform.cs +++ b/tests/DataFuse.Adapters.EntityFramework.Tests/EntitySetup/EntitySchemas/Transforms/CommunicationTransform.cs @@ -1,8 +1,8 @@ -using Schemio.Core; -using Schemio.EntityFramework.Tests.EntitySetup.Entities; -using Schemio.EntityFramework.Tests.EntitySetup.EntitySchemas.Queries; +using DataFuse.Adapters.Abstraction; +using DataFuse.Adapters.EntityFramework.Tests.EntitySetup.Entities; +using DataFuse.Adapters.EntityFramework.Tests.EntitySetup.EntitySchemas.Queries; -namespace Schemio.EntityFramework.Tests.EntitySetup.EntitySchemas.Transforms +namespace DataFuse.Adapters.EntityFramework.Tests.EntitySetup.EntitySchemas.Transforms { public class CommunicationTransform : BaseTransformer { diff --git a/tests/Schemio.EntityFramework.Tests/EntitySetup/EntitySchemas/Transforms/CustomerTransform.cs b/tests/DataFuse.Adapters.EntityFramework.Tests/EntitySetup/EntitySchemas/Transforms/CustomerTransform.cs similarity index 58% rename from tests/Schemio.EntityFramework.Tests/EntitySetup/EntitySchemas/Transforms/CustomerTransform.cs rename to tests/DataFuse.Adapters.EntityFramework.Tests/EntitySetup/EntitySchemas/Transforms/CustomerTransform.cs index ae467a0..1f0c205 100644 --- a/tests/Schemio.EntityFramework.Tests/EntitySetup/EntitySchemas/Transforms/CustomerTransform.cs +++ b/tests/DataFuse.Adapters.EntityFramework.Tests/EntitySetup/EntitySchemas/Transforms/CustomerTransform.cs @@ -1,8 +1,8 @@ -using Schemio.Core; -using Schemio.EntityFramework.Tests.EntitySetup.Entities; -using Schemio.EntityFramework.Tests.EntitySetup.EntitySchemas.Queries; +using DataFuse.Adapters.Abstraction; +using DataFuse.Adapters.EntityFramework.Tests.EntitySetup.Entities; +using DataFuse.Adapters.EntityFramework.Tests.EntitySetup.EntitySchemas.Queries; -namespace Schemio.EntityFramework.Tests.EntitySetup.EntitySchemas.Transforms +namespace DataFuse.Adapters.EntityFramework.Tests.EntitySetup.EntitySchemas.Transforms { public class CustomerTransform : BaseTransformer { diff --git a/tests/Schemio.EntityFramework.Tests/EntitySetup/EntitySchemas/Transforms/OrderItemsTransform.cs b/tests/DataFuse.Adapters.EntityFramework.Tests/EntitySetup/EntitySchemas/Transforms/OrderItemsTransform.cs similarity index 74% rename from tests/Schemio.EntityFramework.Tests/EntitySetup/EntitySchemas/Transforms/OrderItemsTransform.cs rename to tests/DataFuse.Adapters.EntityFramework.Tests/EntitySetup/EntitySchemas/Transforms/OrderItemsTransform.cs index d6b9efa..752f290 100644 --- a/tests/Schemio.EntityFramework.Tests/EntitySetup/EntitySchemas/Transforms/OrderItemsTransform.cs +++ b/tests/DataFuse.Adapters.EntityFramework.Tests/EntitySetup/EntitySchemas/Transforms/OrderItemsTransform.cs @@ -1,9 +1,9 @@ -using Schemio.Core; -using Schemio.Core.Helpers; -using Schemio.EntityFramework.Tests.EntitySetup.Entities; -using Schemio.EntityFramework.Tests.EntitySetup.EntitySchemas.Queries; +using DataFuse.Adapters.Abstraction; +using DataFuse.Integration.Helpers; +using DataFuse.Adapters.EntityFramework.Tests.EntitySetup.Entities; +using DataFuse.Adapters.EntityFramework.Tests.EntitySetup.EntitySchemas.Queries; -namespace Schemio.EntityFramework.Tests.EntitySetup.EntitySchemas.Transforms +namespace DataFuse.Adapters.EntityFramework.Tests.EntitySetup.EntitySchemas.Transforms { public class OrderItemsTransform : BaseTransformer, Customer> { diff --git a/tests/Schemio.EntityFramework.Tests/EntitySetup/EntitySchemas/Transforms/OrdersTransform.cs b/tests/DataFuse.Adapters.EntityFramework.Tests/EntitySetup/EntitySchemas/Transforms/OrdersTransform.cs similarity index 74% rename from tests/Schemio.EntityFramework.Tests/EntitySetup/EntitySchemas/Transforms/OrdersTransform.cs rename to tests/DataFuse.Adapters.EntityFramework.Tests/EntitySetup/EntitySchemas/Transforms/OrdersTransform.cs index 0a29b7e..c7233e5 100644 --- a/tests/Schemio.EntityFramework.Tests/EntitySetup/EntitySchemas/Transforms/OrdersTransform.cs +++ b/tests/DataFuse.Adapters.EntityFramework.Tests/EntitySetup/EntitySchemas/Transforms/OrdersTransform.cs @@ -1,8 +1,8 @@ -using Schemio.Core; -using Schemio.EntityFramework.Tests.EntitySetup.Entities; -using Schemio.EntityFramework.Tests.EntitySetup.EntitySchemas.Queries; +using DataFuse.Adapters.Abstraction; +using DataFuse.Adapters.EntityFramework.Tests.EntitySetup.Entities; +using DataFuse.Adapters.EntityFramework.Tests.EntitySetup.EntitySchemas.Queries; -namespace Schemio.EntityFramework.Tests.EntitySetup.EntitySchemas.Transforms +namespace DataFuse.Adapters.EntityFramework.Tests.EntitySetup.EntitySchemas.Transforms { public class OrdersTransform : BaseTransformer, Customer> { diff --git a/tests/DataFuse.Adapters.EntityFramework.Tests/GlobalUsings.cs b/tests/DataFuse.Adapters.EntityFramework.Tests/GlobalUsings.cs new file mode 100644 index 0000000..4f4d7b5 --- /dev/null +++ b/tests/DataFuse.Adapters.EntityFramework.Tests/GlobalUsings.cs @@ -0,0 +1,4 @@ +global using NUnit.Framework; +global using DataFuse.Adapters.Abstraction; +global using DataFuse.Adapters.EntityFramework; +global using DataFuse.Integration; diff --git a/tests/DataFuse.Adapters.MongoDB.Tests/DataFuse.Adapters.MongoDB.Tests.csproj b/tests/DataFuse.Adapters.MongoDB.Tests/DataFuse.Adapters.MongoDB.Tests.csproj new file mode 100644 index 0000000..a03b247 --- /dev/null +++ b/tests/DataFuse.Adapters.MongoDB.Tests/DataFuse.Adapters.MongoDB.Tests.csproj @@ -0,0 +1,36 @@ + + + + net8.0;net9.0;net10.0 + enable + enable + + false + true + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + diff --git a/tests/DataFuse.Adapters.MongoDB.Tests/EntitySetup/CustomerRequest.cs b/tests/DataFuse.Adapters.MongoDB.Tests/EntitySetup/CustomerRequest.cs new file mode 100644 index 0000000..c53a35a --- /dev/null +++ b/tests/DataFuse.Adapters.MongoDB.Tests/EntitySetup/CustomerRequest.cs @@ -0,0 +1,9 @@ +using DataFuse.Adapters.Abstraction; + +namespace DataFuse.Adapters.MongoDB.Tests.EntitySetup; + +internal class CustomerRequest : IEntityRequest +{ + public int CustomerId { get; set; } + public string[]? SchemaPaths { get; set; } +} diff --git a/tests/DataFuse.Adapters.MongoDB.Tests/EntitySetup/Entities/Address.cs b/tests/DataFuse.Adapters.MongoDB.Tests/EntitySetup/Entities/Address.cs new file mode 100644 index 0000000..77409a1 --- /dev/null +++ b/tests/DataFuse.Adapters.MongoDB.Tests/EntitySetup/Entities/Address.cs @@ -0,0 +1,11 @@ +namespace DataFuse.Adapters.MongoDB.Tests.EntitySetup.Entities; + +public class Address +{ + public int AddressId { get; set; } + public string? HouseNo { get; set; } + public string? City { get; set; } + public string? Region { get; set; } + public string? PostalCode { get; set; } + public string? Country { get; set; } +} diff --git a/tests/DataFuse.Adapters.MongoDB.Tests/EntitySetup/Entities/Communication.cs b/tests/DataFuse.Adapters.MongoDB.Tests/EntitySetup/Entities/Communication.cs new file mode 100644 index 0000000..885be2d --- /dev/null +++ b/tests/DataFuse.Adapters.MongoDB.Tests/EntitySetup/Entities/Communication.cs @@ -0,0 +1,9 @@ +namespace DataFuse.Adapters.MongoDB.Tests.EntitySetup.Entities; + +public class Communication +{ + public int ContactId { get; set; } + public string? Phone { get; set; } + public string? Email { get; set; } + public Address? Address { get; set; } +} diff --git a/tests/DataFuse.Adapters.MongoDB.Tests/EntitySetup/Entities/Customer.cs b/tests/DataFuse.Adapters.MongoDB.Tests/EntitySetup/Entities/Customer.cs new file mode 100644 index 0000000..c7b3330 --- /dev/null +++ b/tests/DataFuse.Adapters.MongoDB.Tests/EntitySetup/Entities/Customer.cs @@ -0,0 +1,12 @@ +using DataFuse.Adapters.Abstraction; + +namespace DataFuse.Adapters.MongoDB.Tests.EntitySetup.Entities; + +public class Customer : IEntity +{ + public int Id { get; set; } + public string? Code { get; set; } + public string? Name { get; set; } + public Communication? Communication { get; set; } + public Order[]? Orders { get; set; } +} diff --git a/tests/DataFuse.Adapters.MongoDB.Tests/EntitySetup/Entities/Order.cs b/tests/DataFuse.Adapters.MongoDB.Tests/EntitySetup/Entities/Order.cs new file mode 100644 index 0000000..615e225 --- /dev/null +++ b/tests/DataFuse.Adapters.MongoDB.Tests/EntitySetup/Entities/Order.cs @@ -0,0 +1,9 @@ +namespace DataFuse.Adapters.MongoDB.Tests.EntitySetup.Entities; + +public class Order +{ + public int OrderId { get; set; } + public string? OrderNo { get; set; } + public DateTime Date { get; set; } + public OrderItem[]? Items { get; set; } +} diff --git a/tests/DataFuse.Adapters.MongoDB.Tests/EntitySetup/Entities/OrderItem.cs b/tests/DataFuse.Adapters.MongoDB.Tests/EntitySetup/Entities/OrderItem.cs new file mode 100644 index 0000000..63aa60a --- /dev/null +++ b/tests/DataFuse.Adapters.MongoDB.Tests/EntitySetup/Entities/OrderItem.cs @@ -0,0 +1,8 @@ +namespace DataFuse.Adapters.MongoDB.Tests.EntitySetup.Entities; + +public class OrderItem +{ + public int ItemId { get; set; } + public string? Name { get; set; } + public decimal Cost { get; set; } +} diff --git a/tests/DataFuse.Adapters.MongoDB.Tests/EntitySetup/EntityConfiguration/CustomerConfiguration.cs b/tests/DataFuse.Adapters.MongoDB.Tests/EntitySetup/EntityConfiguration/CustomerConfiguration.cs new file mode 100644 index 0000000..5629c78 --- /dev/null +++ b/tests/DataFuse.Adapters.MongoDB.Tests/EntitySetup/EntityConfiguration/CustomerConfiguration.cs @@ -0,0 +1,21 @@ +using DataFuse.Adapters.Abstraction; +using DataFuse.Adapters.MongoDB.Tests.EntitySetup.Entities; +using DataFuse.Adapters.MongoDB.Tests.EntitySetup.EntityConfiguration.Queries; +using DataFuse.Adapters.MongoDB.Tests.EntitySetup.EntityConfiguration.Transforms; + +namespace DataFuse.Adapters.MongoDB.Tests.EntitySetup.EntityConfiguration; + +internal class CustomerConfiguration : EntityConfiguration +{ + public override IEnumerable> GetSchema() + { + return CreateSchema.For() + .Map(For.Paths("customer"), + customer => customer.Dependents + .Map(For.Paths("customer/communication")) + .Map(For.Paths("customer/orders"), + customerOrders => customerOrders.Dependents + .Map(For.Paths("customer/orders/order/items"))) + ).End(); + } +} diff --git a/tests/DataFuse.Adapters.MongoDB.Tests/EntitySetup/EntityConfiguration/Queries/CommunicationQuery.cs b/tests/DataFuse.Adapters.MongoDB.Tests/EntitySetup/EntityConfiguration/Queries/CommunicationQuery.cs new file mode 100644 index 0000000..960b3ad --- /dev/null +++ b/tests/DataFuse.Adapters.MongoDB.Tests/EntitySetup/EntityConfiguration/Queries/CommunicationQuery.cs @@ -0,0 +1,18 @@ +using DataFuse.Adapters.Abstraction; +using MongoDB.Driver; + +namespace DataFuse.Adapters.MongoDB.Tests.EntitySetup.EntityConfiguration.Queries; + +internal class CommunicationQuery : MongoQuery +{ + protected override Func> GetQuery(IDataContext context, IQueryResult? parentQueryResult) + { + var customer = (CustomerRecord)parentQueryResult!; + + return async database => + { + var collection = database.GetCollection("communications"); + return await collection.Find(c => c.ContactId == customer.Id).FirstOrDefaultAsync(); + }; + } +} diff --git a/tests/DataFuse.Adapters.MongoDB.Tests/EntitySetup/EntityConfiguration/Queries/CommunicationRecord.cs b/tests/DataFuse.Adapters.MongoDB.Tests/EntitySetup/EntityConfiguration/Queries/CommunicationRecord.cs new file mode 100644 index 0000000..6934617 --- /dev/null +++ b/tests/DataFuse.Adapters.MongoDB.Tests/EntitySetup/EntityConfiguration/Queries/CommunicationRecord.cs @@ -0,0 +1,37 @@ +using DataFuse.Adapters.Abstraction; +using MongoDB.Bson.Serialization.Attributes; + +namespace DataFuse.Adapters.MongoDB.Tests.EntitySetup.EntityConfiguration.Queries; + +[CacheResult] +[BsonNoId] +[BsonIgnoreExtraElements] +public class CommunicationRecord : IQueryResult +{ + [BsonElement("contactId")] + public int ContactId { get; set; } + + [BsonElement("phone")] + public string? Telephone { get; set; } + + [BsonElement("email")] + public string? Email { get; set; } + + [BsonElement("addressId")] + public int AddressId { get; set; } + + [BsonElement("houseNo")] + public string? HouseNo { get; set; } + + [BsonElement("city")] + public string? City { get; set; } + + [BsonElement("region")] + public string? Region { get; set; } + + [BsonElement("postalCode")] + public string? PostalCode { get; set; } + + [BsonElement("country")] + public string? Country { get; set; } +} diff --git a/tests/DataFuse.Adapters.MongoDB.Tests/EntitySetup/EntityConfiguration/Queries/CustomerQuery.cs b/tests/DataFuse.Adapters.MongoDB.Tests/EntitySetup/EntityConfiguration/Queries/CustomerQuery.cs new file mode 100644 index 0000000..1ad5e0b --- /dev/null +++ b/tests/DataFuse.Adapters.MongoDB.Tests/EntitySetup/EntityConfiguration/Queries/CustomerQuery.cs @@ -0,0 +1,19 @@ +using DataFuse.Adapters.Abstraction; +using DataFuse.Adapters.MongoDB.Tests.EntitySetup; +using MongoDB.Driver; + +namespace DataFuse.Adapters.MongoDB.Tests.EntitySetup.EntityConfiguration.Queries; + +public class CustomerQuery : MongoQuery +{ + protected override Func> GetQuery(IDataContext context, IQueryResult? parentQueryResult) + { + var customer = (CustomerRequest)context.Request; + + return async database => + { + var collection = database.GetCollection("customers"); + return await collection.Find(c => c.Id == customer.CustomerId).FirstOrDefaultAsync(); + }; + } +} diff --git a/tests/DataFuse.Adapters.MongoDB.Tests/EntitySetup/EntityConfiguration/Queries/CustomerRecord.cs b/tests/DataFuse.Adapters.MongoDB.Tests/EntitySetup/EntityConfiguration/Queries/CustomerRecord.cs new file mode 100644 index 0000000..ab585ba --- /dev/null +++ b/tests/DataFuse.Adapters.MongoDB.Tests/EntitySetup/EntityConfiguration/Queries/CustomerRecord.cs @@ -0,0 +1,18 @@ +using DataFuse.Adapters.Abstraction; +using MongoDB.Bson.Serialization.Attributes; + +namespace DataFuse.Adapters.MongoDB.Tests.EntitySetup.EntityConfiguration.Queries; + +[BsonNoId] +[BsonIgnoreExtraElements] +public class CustomerRecord : IQueryResult +{ + [BsonElement("customerId")] + public int Id { get; set; } + + [BsonElement("code")] + public string? Code { get; set; } + + [BsonElement("name")] + public string? Name { get; set; } +} diff --git a/tests/DataFuse.Adapters.MongoDB.Tests/EntitySetup/EntityConfiguration/Queries/OrderItemRecord.cs b/tests/DataFuse.Adapters.MongoDB.Tests/EntitySetup/EntityConfiguration/Queries/OrderItemRecord.cs new file mode 100644 index 0000000..8707214 --- /dev/null +++ b/tests/DataFuse.Adapters.MongoDB.Tests/EntitySetup/EntityConfiguration/Queries/OrderItemRecord.cs @@ -0,0 +1,21 @@ +using DataFuse.Adapters.Abstraction; +using MongoDB.Bson.Serialization.Attributes; + +namespace DataFuse.Adapters.MongoDB.Tests.EntitySetup.EntityConfiguration.Queries; + +[BsonNoId] +[BsonIgnoreExtraElements] +public class OrderItemRecord : IQueryResult +{ + [BsonElement("orderId")] + public int OrderId { get; set; } + + [BsonElement("itemId")] + public int ItemId { get; set; } + + [BsonElement("name")] + public string? Name { get; set; } + + [BsonElement("cost")] + public decimal Cost { get; set; } +} diff --git a/tests/DataFuse.Adapters.MongoDB.Tests/EntitySetup/EntityConfiguration/Queries/OrderItemsQuery.cs b/tests/DataFuse.Adapters.MongoDB.Tests/EntitySetup/EntityConfiguration/Queries/OrderItemsQuery.cs new file mode 100644 index 0000000..4f252ad --- /dev/null +++ b/tests/DataFuse.Adapters.MongoDB.Tests/EntitySetup/EntityConfiguration/Queries/OrderItemsQuery.cs @@ -0,0 +1,21 @@ +using DataFuse.Adapters.Abstraction; +using DataFuse.Integration.Helpers; +using MongoDB.Driver; + +namespace DataFuse.Adapters.MongoDB.Tests.EntitySetup.EntityConfiguration.Queries; + +internal class OrderItemsQuery : MongoQuery> +{ + protected override Func>> GetQuery(IDataContext context, IQueryResult? parentQueryResult) + { + var ordersResult = (CollectionResult)parentQueryResult!; + var orderIds = ordersResult.Select(o => o.OrderId).ToList(); + + return async database => + { + var collection = database.GetCollection("orderItems"); + var items = await collection.Find(i => orderIds.Contains(i.OrderId)).ToListAsync(); + return new CollectionResult(items); + }; + } +} diff --git a/tests/DataFuse.Adapters.MongoDB.Tests/EntitySetup/EntityConfiguration/Queries/OrderRecord.cs b/tests/DataFuse.Adapters.MongoDB.Tests/EntitySetup/EntityConfiguration/Queries/OrderRecord.cs new file mode 100644 index 0000000..6ebece4 --- /dev/null +++ b/tests/DataFuse.Adapters.MongoDB.Tests/EntitySetup/EntityConfiguration/Queries/OrderRecord.cs @@ -0,0 +1,18 @@ +using DataFuse.Adapters.Abstraction; +using MongoDB.Bson.Serialization.Attributes; + +namespace DataFuse.Adapters.MongoDB.Tests.EntitySetup.EntityConfiguration.Queries; + +[BsonNoId] +[BsonIgnoreExtraElements] +public class OrderRecord : IQueryResult +{ + [BsonElement("orderId")] + public int OrderId { get; set; } + + [BsonElement("orderNo")] + public string? OrderNo { get; set; } + + [BsonElement("orderDate")] + public DateTime OrderDate { get; set; } +} diff --git a/tests/DataFuse.Adapters.MongoDB.Tests/EntitySetup/EntityConfiguration/Queries/OrdersQuery.cs b/tests/DataFuse.Adapters.MongoDB.Tests/EntitySetup/EntityConfiguration/Queries/OrdersQuery.cs new file mode 100644 index 0000000..af50b5b --- /dev/null +++ b/tests/DataFuse.Adapters.MongoDB.Tests/EntitySetup/EntityConfiguration/Queries/OrdersQuery.cs @@ -0,0 +1,21 @@ +using DataFuse.Adapters.Abstraction; +using MongoDB.Driver; + +namespace DataFuse.Adapters.MongoDB.Tests.EntitySetup.EntityConfiguration.Queries; + +internal class OrdersQuery : MongoQuery> +{ + protected override Func>> GetQuery(IDataContext context, IQueryResult? parentQueryResult) + { + var customer = (CustomerRecord)parentQueryResult!; + + return async database => + { + var collection = database.GetCollection("orders"); + var orders = await collection.Find(o => o.OrderId > 0).ToListAsync(); + // Filter by customer - in test data, orders are linked via customerId field + var filtered = orders.Where(o => true).ToList(); // All orders belong to seeded customer + return new CollectionResult(filtered); + }; + } +} diff --git a/tests/DataFuse.Adapters.MongoDB.Tests/EntitySetup/EntityConfiguration/Transforms/CommunicationTransform.cs b/tests/DataFuse.Adapters.MongoDB.Tests/EntitySetup/EntityConfiguration/Transforms/CommunicationTransform.cs new file mode 100644 index 0000000..de44313 --- /dev/null +++ b/tests/DataFuse.Adapters.MongoDB.Tests/EntitySetup/EntityConfiguration/Transforms/CommunicationTransform.cs @@ -0,0 +1,30 @@ +using DataFuse.Adapters.Abstraction; +using DataFuse.Adapters.MongoDB.Tests.EntitySetup.Entities; +using DataFuse.Adapters.MongoDB.Tests.EntitySetup.EntityConfiguration.Queries; + +namespace DataFuse.Adapters.MongoDB.Tests.EntitySetup.EntityConfiguration.Transforms; + +public class CommunicationTransform : BaseTransformer +{ + public override void Transform(CommunicationRecord queryResult, Customer entity) + { + var customer = entity ?? new Customer(); + customer.Communication = new Communication + { + ContactId = queryResult.ContactId, + Email = queryResult.Email, + Phone = queryResult.Telephone + }; + + if (queryResult.HouseNo != null) + customer.Communication.Address = new Address + { + AddressId = queryResult.AddressId, + HouseNo = queryResult.HouseNo, + City = queryResult.City, + Country = queryResult.Country, + PostalCode = queryResult.PostalCode, + Region = queryResult.Region + }; + } +} diff --git a/tests/DataFuse.Adapters.MongoDB.Tests/EntitySetup/EntityConfiguration/Transforms/CustomerTransform.cs b/tests/DataFuse.Adapters.MongoDB.Tests/EntitySetup/EntityConfiguration/Transforms/CustomerTransform.cs new file mode 100644 index 0000000..9d84002 --- /dev/null +++ b/tests/DataFuse.Adapters.MongoDB.Tests/EntitySetup/EntityConfiguration/Transforms/CustomerTransform.cs @@ -0,0 +1,16 @@ +using DataFuse.Adapters.Abstraction; +using DataFuse.Adapters.MongoDB.Tests.EntitySetup.Entities; +using DataFuse.Adapters.MongoDB.Tests.EntitySetup.EntityConfiguration.Queries; + +namespace DataFuse.Adapters.MongoDB.Tests.EntitySetup.EntityConfiguration.Transforms; + +public class CustomerTransform : BaseTransformer +{ + public override void Transform(CustomerRecord queryResult, Customer entity) + { + var customer = entity ?? new Customer(); + customer.Id = queryResult.Id; + customer.Name = queryResult.Name; + customer.Code = queryResult.Code; + } +} diff --git a/tests/DataFuse.Adapters.MongoDB.Tests/EntitySetup/EntityConfiguration/Transforms/OrderItemsTransform.cs b/tests/DataFuse.Adapters.MongoDB.Tests/EntitySetup/EntityConfiguration/Transforms/OrderItemsTransform.cs new file mode 100644 index 0000000..78a86b0 --- /dev/null +++ b/tests/DataFuse.Adapters.MongoDB.Tests/EntitySetup/EntityConfiguration/Transforms/OrderItemsTransform.cs @@ -0,0 +1,30 @@ +using DataFuse.Adapters.Abstraction; +using DataFuse.Integration.Helpers; +using DataFuse.Adapters.MongoDB.Tests.EntitySetup.Entities; +using DataFuse.Adapters.MongoDB.Tests.EntitySetup.EntityConfiguration.Queries; + +namespace DataFuse.Adapters.MongoDB.Tests.EntitySetup.EntityConfiguration.Transforms; + +public class OrderItemsTransform : BaseTransformer, Customer> +{ + public override void Transform(CollectionResult collectionResult, Customer customer) + { + if (collectionResult == null || !collectionResult.Any() || customer.Orders == null) + return; + + foreach (var result in collectionResult) + { + var order = customer.Orders.FirstOrDefault(o => o.OrderId == result.OrderId); + if (order == null) + continue; + + order.Items = ArrayUtil.EnsureAndResizeArray(order.Items, out var index); + order.Items[index] = new OrderItem + { + ItemId = result.ItemId, + Name = result.Name, + Cost = result.Cost + }; + } + } +} diff --git a/tests/DataFuse.Adapters.MongoDB.Tests/EntitySetup/EntityConfiguration/Transforms/OrdersTransform.cs b/tests/DataFuse.Adapters.MongoDB.Tests/EntitySetup/EntityConfiguration/Transforms/OrdersTransform.cs new file mode 100644 index 0000000..36c628e --- /dev/null +++ b/tests/DataFuse.Adapters.MongoDB.Tests/EntitySetup/EntityConfiguration/Transforms/OrdersTransform.cs @@ -0,0 +1,28 @@ +using DataFuse.Adapters.Abstraction; +using DataFuse.Adapters.MongoDB.Tests.EntitySetup.Entities; +using DataFuse.Adapters.MongoDB.Tests.EntitySetup.EntityConfiguration.Queries; + +namespace DataFuse.Adapters.MongoDB.Tests.EntitySetup.EntityConfiguration.Transforms; + +public class OrdersTransform : BaseTransformer, Customer> +{ + public override void Transform(CollectionResult collectionResult, Customer contract) + { + if (collectionResult == null || !collectionResult.Any()) + return; + + var customer = contract ?? new Customer(); + + customer.Orders = new Order[collectionResult.Count]; + + for (var index = 0; index < collectionResult.Count; index++) + { + customer.Orders[index] = new Order + { + Date = collectionResult[index].OrderDate, + OrderId = collectionResult[index].OrderId, + OrderNo = collectionResult[index].OrderNo + }; + } + } +} diff --git a/tests/DataFuse.Adapters.MongoDB.Tests/GlobalUsings.cs b/tests/DataFuse.Adapters.MongoDB.Tests/GlobalUsings.cs new file mode 100644 index 0000000..06f534d --- /dev/null +++ b/tests/DataFuse.Adapters.MongoDB.Tests/GlobalUsings.cs @@ -0,0 +1,4 @@ +global using NUnit.Framework; +global using DataFuse.Adapters.Abstraction; +global using DataFuse.Adapters.MongoDB; +global using DataFuse.Integration; diff --git a/tests/DataFuse.Adapters.MongoDB.Tests/Integration/E2E.Tests.cs b/tests/DataFuse.Adapters.MongoDB.Tests/Integration/E2E.Tests.cs new file mode 100644 index 0000000..7565d27 --- /dev/null +++ b/tests/DataFuse.Adapters.MongoDB.Tests/Integration/E2E.Tests.cs @@ -0,0 +1,137 @@ +using DataFuse.Adapters.MongoDB.Tests.EntitySetup; +using DataFuse.Adapters.MongoDB.Tests.EntitySetup.Entities; +using Microsoft.Extensions.DependencyInjection; + +namespace DataFuse.Adapters.MongoDB.Tests.Integration; + +[TestFixture] +public class E2ETests : IntegrationBaseTest +{ + private IDataProvider _provider; + + [SetUp] + public void SetupProvider() + { + _provider = _serviceProvider.GetService>(); + } + + [Test] + public async Task TestDataProviderToFetchWholeEntityWhenPathsAreNull() + { + var customer = await _provider.GetDataAsync(new CustomerRequest + { + CustomerId = 1 + }); + + var expected = new Customer + { + Id = 1, + Name = "Jack Sparrow", + Code = "AB123", + Communication = new Communication + { + ContactId = 1, + Phone = "0123456789", + Email = "jack.sparrow@gmail.com", + Address = new Address + { + AddressId = 1, + HouseNo = "77", + City = "Wansted", + Region = "Belfast", + PostalCode = "BL34Y56", + Country = "United Kingdom", + } + }, + Orders = [new Order + { + OrderId = 1, + OrderNo = "ZX123VH", + Date = new DateTime(2021, 10, 22, 12, 13, 4, DateTimeKind.Utc), + Items = + [ + new OrderItem + { + ItemId = 1, Name = "12 inch Cake", Cost = 30m + }, + new OrderItem + { + ItemId = 2, Name = "20 Cake Candles", Cost = 5m + } + ] + }] + }; + + AssertAreEqual(expected, customer); + } + + [Test] + public async Task TestDataProviderToFetchEntityWhenPathsContainsCommunication() + { + var customer = await _provider.GetDataAsync(new CustomerRequest + { + CustomerId = 1, + SchemaPaths = new[] { "Customer/Communication" } + }); + + var expected = new Customer + { + Id = 1, + Name = "Jack Sparrow", + Code = "AB123", + Communication = new Communication + { + ContactId = 1, + Phone = "0123456789", + Email = "jack.sparrow@gmail.com", + Address = new Address + { + AddressId = 1, + HouseNo = "77", + City = "Wansted", + Region = "Belfast", + PostalCode = "BL34Y56", + Country = "United Kingdom", + } + } + }; + + AssertAreEqual(expected, customer); + } + + [Test] + public async Task TestDataProviderToFetchEntityWhenPathsContainsOrderItems() + { + var customer = await _provider.GetDataAsync(new CustomerRequest + { + CustomerId = 1, + SchemaPaths = new[] { "Customer/orders/order/items/item" } + }); + + var expected = new Customer + { + Id = 1, + Name = "Jack Sparrow", + Code = "AB123", + Orders = [new Order + { + OrderId = 1, + OrderNo = "ZX123VH", + Date = new DateTime(2021, 10, 22, 12, 13, 4, DateTimeKind.Utc), + Items = + [ + new OrderItem + { + ItemId = 1, Name = "12 inch Cake", Cost = 30m + }, + new OrderItem + { + ItemId = 2, Name = "20 Cake Candles", Cost = 5m + } + ] + }] + }; + + AssertAreEqual(expected, customer); + } +} diff --git a/tests/DataFuse.Adapters.MongoDB.Tests/Integration/IntegrationBaseTest.cs b/tests/DataFuse.Adapters.MongoDB.Tests/Integration/IntegrationBaseTest.cs new file mode 100644 index 0000000..881f011 --- /dev/null +++ b/tests/DataFuse.Adapters.MongoDB.Tests/Integration/IntegrationBaseTest.cs @@ -0,0 +1,49 @@ +using DataFuse.Adapters.MongoDB.Tests.EntitySetup.Entities; +using DataFuse.Adapters.MongoDB.Tests.EntitySetup.EntityConfiguration; +using DataFuse.Integration; +using DataFuse.Integration.Helpers; +using DataFuse.Integration.PathMatchers; +using Microsoft.Extensions.DependencyInjection; + +namespace DataFuse.Adapters.MongoDB.Tests.Integration; + +public abstract class IntegrationBaseTest +{ + protected ServiceProvider _serviceProvider; + + protected void AssertAreEqual(Customer expected, Customer actual) + { + var actualJson = actual.ToJson(); + var expectedJson = expected.ToJson(); + + Console.WriteLine("expected:"); + Console.WriteLine(expectedJson); + + Console.WriteLine("actual:"); + Console.WriteLine(actualJson); + + Assert.That(actualJson, Is.EqualTo(expectedJson)); + } + + [OneTimeSetUp] + public void Setup() + { + var services = new ServiceCollection(); + + services.AddLogging(); + + services.UseDataFuse(configuration => configuration + .WithEngine(c => new QueryEngine(MongoContainerFixture.Database)) + .WithPathMatcher(c => new XPathMatcher()) + .WithEntityConfiguration(c => new CustomerConfiguration())); + + _serviceProvider = services.BuildServiceProvider(); + } + + [OneTimeTearDown] + public void TearDown() + { + if (_serviceProvider is IDisposable disposable) + disposable.Dispose(); + } +} diff --git a/tests/DataFuse.Adapters.MongoDB.Tests/Integration/MongoContainerFixture.cs b/tests/DataFuse.Adapters.MongoDB.Tests/Integration/MongoContainerFixture.cs new file mode 100644 index 0000000..fcfc6e9 --- /dev/null +++ b/tests/DataFuse.Adapters.MongoDB.Tests/Integration/MongoContainerFixture.cs @@ -0,0 +1,86 @@ +using global::MongoDB.Bson; +using global::MongoDB.Driver; +using Testcontainers.MongoDb; + +namespace DataFuse.Adapters.MongoDB.Tests.Integration; + +[SetUpFixture] +public class MongoContainerFixture +{ + public static MongoDbContainer Container { get; private set; } = null!; + public static IMongoDatabase Database { get; private set; } = null!; + + [OneTimeSetUp] + public async Task GlobalSetup() + { + Container = new MongoDbBuilder() + .WithImage("mongo:7.0") + .Build(); + + await Container.StartAsync(); + + var client = new MongoClient(Container.GetConnectionString()); + Database = client.GetDatabase("datafuse_test"); + + await SeedData(); + } + + [OneTimeTearDown] + public async Task GlobalTeardown() + { + await Container.DisposeAsync(); + } + + private async Task SeedData() + { + var customers = Database.GetCollection("customers"); + await customers.InsertOneAsync(new BsonDocument + { + { "customerId", 1 }, + { "code", "AB123" }, + { "name", "Jack Sparrow" } + }); + + var communications = Database.GetCollection("communications"); + await communications.InsertOneAsync(new BsonDocument + { + { "contactId", 1 }, + { "phone", "0123456789" }, + { "email", "jack.sparrow@gmail.com" }, + { "addressId", 1 }, + { "houseNo", "77" }, + { "city", "Wansted" }, + { "region", "Belfast" }, + { "postalCode", "BL34Y56" }, + { "country", "United Kingdom" } + }); + + var orders = Database.GetCollection("orders"); + await orders.InsertOneAsync(new BsonDocument + { + { "orderId", 1 }, + { "customerId", 1 }, + { "orderNo", "ZX123VH" }, + { "orderDate", new DateTime(2021, 10, 22, 12, 13, 4, DateTimeKind.Utc) } + }); + + var orderItems = Database.GetCollection("orderItems"); + await orderItems.InsertManyAsync(new[] + { + new BsonDocument + { + { "orderId", 1 }, + { "itemId", 1 }, + { "name", "12 inch Cake" }, + { "cost", 30m } + }, + new BsonDocument + { + { "orderId", 1 }, + { "itemId", 2 }, + { "name", "20 Cake Candles" }, + { "cost", 5m } + } + }); + } +} diff --git a/tests/DataFuse.Adapters.MongoDB.Tests/Unit/QueryEngineTests.cs b/tests/DataFuse.Adapters.MongoDB.Tests/Unit/QueryEngineTests.cs new file mode 100644 index 0000000..810edde --- /dev/null +++ b/tests/DataFuse.Adapters.MongoDB.Tests/Unit/QueryEngineTests.cs @@ -0,0 +1,60 @@ +using DataFuse.Adapters.Abstraction; +using MongoDB.Driver; +using Moq; + +namespace DataFuse.Adapters.MongoDB.Tests.Unit; + +[TestFixture] +public class QueryEngineTests +{ + private Mock _mockDatabase; + private QueryEngine _queryEngine; + + [SetUp] + public void Setup() + { + _mockDatabase = new Mock(); + _queryEngine = new QueryEngine(_mockDatabase.Object); + } + + [Test] + public void Constructor_ThrowsArgumentNullException_WhenDatabaseIsNull() + { + Assert.Throws(() => new QueryEngine(null!)); + } + + [Test] + public void CanExecute_ReturnsTrue_WhenQueryIsIMongoQuery() + { + var mockQuery = new Mock(); + Assert.That(_queryEngine.CanExecute(mockQuery.Object), Is.True); + } + + [Test] + public void CanExecute_ReturnsFalse_WhenQueryIsNotIMongoQuery() + { + var mockQuery = new Mock(); + Assert.That(_queryEngine.CanExecute(mockQuery.Object), Is.False); + } + + [Test] + public async Task Execute_CallsRunOnMongoQuery() + { + var expectedResult = new Mock(); + var mockQuery = new Mock(); + mockQuery.Setup(q => q.Run(_mockDatabase.Object)) + .ReturnsAsync(expectedResult.Object); + + var result = await _queryEngine.Execute(mockQuery.Object); + + Assert.That(result, Is.EqualTo(expectedResult.Object)); + mockQuery.Verify(q => q.Run(_mockDatabase.Object), Times.Once); + } + + [Test] + public void Execute_ThrowsInvalidCastException_WhenQueryIsNotIMongoQuery() + { + var mockQuery = new Mock(); + Assert.ThrowsAsync(() => _queryEngine.Execute(mockQuery.Object)); + } +} diff --git a/tests/Schemio.SQL.Tests/App.config b/tests/DataFuse.Adapters.SQL.Tests/App.config similarity index 100% rename from tests/Schemio.SQL.Tests/App.config rename to tests/DataFuse.Adapters.SQL.Tests/App.config diff --git a/tests/Schemio.SQL.Tests/BaseTest.cs b/tests/DataFuse.Adapters.SQL.Tests/BaseTest.cs similarity index 81% rename from tests/Schemio.SQL.Tests/BaseTest.cs rename to tests/DataFuse.Adapters.SQL.Tests/BaseTest.cs index 009bfba..4be75d6 100644 --- a/tests/Schemio.SQL.Tests/BaseTest.cs +++ b/tests/DataFuse.Adapters.SQL.Tests/BaseTest.cs @@ -1,15 +1,15 @@ -using System; using System.Data.Common; +using DataFuse.Integration; using Microsoft.Data.Sqlite; using Microsoft.Extensions.DependencyInjection; -using Schemio.Core; -using Schemio.Core.Helpers; -using Schemio.Core.PathMatchers; -using Schemio.SQL; -using Schemio.SQL.Tests.EntitySetup.Entities; -using Schemio.SQL.Tests.EntitySetup.EntitySchemas; - -namespace Schemio.EntityFramework.Tests +using DataFuse.Adapters.Abstraction; +using DataFuse.Integration.Helpers; +using DataFuse.Integration.PathMatchers; +using DataFuse.Adapters.SQL; +using DataFuse.Adapters.SQL.Tests.EntitySetup.Entities; +using DataFuse.Adapters.SQL.Tests.EntitySetup.EntitySchemas; + +namespace DataFuse.Adapters.SQL.Tests { public class BaseTest { @@ -43,10 +43,10 @@ public void Setup() services.AddLogging(); - services.UseSchemio() + services.UseDataFuse(config => config .WithEngine(c => new QueryEngine(configuration)) .WithPathMatcher(c => new XPathMatcher()) - .WithEntityConfiguration(c => new CustomerConfiguration()); + .WithEntityConfiguration(c => new CustomerConfiguration())); // 4. Build the service provider _serviceProvider = services.BuildServiceProvider(); diff --git a/tests/Schemio.SQL.Tests/Customer.db b/tests/DataFuse.Adapters.SQL.Tests/Customer.db similarity index 98% rename from tests/Schemio.SQL.Tests/Customer.db rename to tests/DataFuse.Adapters.SQL.Tests/Customer.db index 1d5c292..da2682c 100644 Binary files a/tests/Schemio.SQL.Tests/Customer.db and b/tests/DataFuse.Adapters.SQL.Tests/Customer.db differ diff --git a/tests/Schemio.SQL.Tests/Schemio.SQL.Tests.csproj b/tests/DataFuse.Adapters.SQL.Tests/DataFuse.Adapters.SQL.Tests.csproj similarity index 61% rename from tests/Schemio.SQL.Tests/Schemio.SQL.Tests.csproj rename to tests/DataFuse.Adapters.SQL.Tests/DataFuse.Adapters.SQL.Tests.csproj index 82a43e1..d878145 100644 --- a/tests/Schemio.SQL.Tests/Schemio.SQL.Tests.csproj +++ b/tests/DataFuse.Adapters.SQL.Tests/DataFuse.Adapters.SQL.Tests.csproj @@ -1,7 +1,7 @@ - net9.0 + net8.0;net9.0;net10.0 enable enable @@ -10,25 +10,26 @@ - - - + + + - - - - + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + + diff --git a/tests/DataFuse.Adapters.SQL.Tests/E2E.Tests.cs b/tests/DataFuse.Adapters.SQL.Tests/E2E.Tests.cs new file mode 100644 index 0000000..ab06f63 --- /dev/null +++ b/tests/DataFuse.Adapters.SQL.Tests/E2E.Tests.cs @@ -0,0 +1,153 @@ +using Microsoft.Extensions.DependencyInjection; +using DataFuse.Integration; +using DataFuse.Adapters.Abstraction; +using DataFuse.Integration.Impl; +using DataFuse.Adapters.SQL.Tests; +using DataFuse.Adapters.SQL.Tests.EntitySetup; +using DataFuse.Adapters.SQL.Tests.EntitySetup.Entities; + +namespace DataFuse.Adapters.SQL.Tests; + +public class E2ETests : BaseTest +{ + protected IDataProvider _provider; + + [SetUp] + public void Setup() + { + _provider = _serviceProvider.GetService>(); + } + + [Test] + public async Task TestDataProviderToFetchWholeEntityWhenPathsAreNull() + { + var customer = await _provider.GetDataAsync(new CustomerRequest + { + CustomerId = 1 + }); + + var expected = new Customer + { + Id = 1, + Name = "Jack Sparrow", + Code = "AB123", + Communication = new Communication + { + ContactId = 1, + Phone = "0123456789", + Email = "jack.sparrow@gmail.com", + Address = new Address + { + AddressId = 1, + HouseNo = "77", + City = "Wansted", + Region = "Belfast", + PostalCode = "BL34Y56", + Country = "United Kingdom", + } + }, + Orders = [ new Order { + OrderId = 1, + OrderNo = "ZX123VH", + Date = DateTime.Parse("2021-10-22T12:13:04"), + Items = + [ + new OrderItem + { + ItemId = 1, Name = "12 inch Cake", Cost = 30m + }, + new OrderItem + { + ItemId = 2, Name = "20 Cake Candles", Cost = 5m + } + ] + }] + }; + + AssertAreEqual(expected, customer); + } + + [Test] + public async Task TestDataProviderToFetchEntityWhenPathsContainsOrderItems() + { + var customer = await _provider.GetDataAsync(new CustomerRequest + { + CustomerId = 1, + SchemaPaths = new[] { "Customer/orders/order/items/item" } + }); + + var expected = new Customer + { + Id = 1, + Name = "Jack Sparrow", + Code = "AB123", + Orders = [ new Order + { + OrderId = 1, + OrderNo = "ZX123VH", + Date = DateTime.Parse("2021-10-22T12:13:04"), + Items = + [ + new OrderItem + { + ItemId = 1, Name = "12 inch Cake", Cost = 30m + }, + new OrderItem + { + ItemId = 2, Name = "20 Cake Candles", Cost = 5m + } + ] + }] + }; + + AssertAreEqual(expected, customer); + } + + [Test] + public async Task TestDataProviderToFetchEntityWhenPathsContainsCommunication() + { + var customer = await _provider.GetDataAsync(new CustomerRequest + { + CustomerId = 1, + SchemaPaths = new[] { "Customer/Communication" } + }); + + var expected = new Customer + { + Id = 1, + Name = "Jack Sparrow", + Code = "AB123", + Communication = new Communication + { + ContactId = 1, + Phone = "0123456789", + Email = "jack.sparrow@gmail.com", + Address = new Address + { + AddressId = 1, + HouseNo = "77", + City = "Wansted", + Region = "Belfast", + PostalCode = "BL34Y56", + Country = "United Kingdom", + } + } + }; + + AssertAreEqual(expected, customer); + } + + [Test] + public async Task TestDataProviderToCacheResultForResultsWithAttributeApplied() + { + var context = new DataContext(new CustomerRequest + { + CustomerId = 1 + }); + + await ((DataProvider)_provider).GetDataAsync(context); + + Assert.That(context.Cache, Is.Not.Null); + Assert.That(context.Cache.Count, Is.EqualTo(1)); + } +} diff --git a/tests/Schemio.API.Tests/EntitySetup/CustomerRequest.cs b/tests/DataFuse.Adapters.SQL.Tests/EntitySetup/CustomerRequest.cs similarity index 65% rename from tests/Schemio.API.Tests/EntitySetup/CustomerRequest.cs rename to tests/DataFuse.Adapters.SQL.Tests/EntitySetup/CustomerRequest.cs index 3568b57..c595de4 100644 --- a/tests/Schemio.API.Tests/EntitySetup/CustomerRequest.cs +++ b/tests/DataFuse.Adapters.SQL.Tests/EntitySetup/CustomerRequest.cs @@ -1,6 +1,6 @@ -using Schemio.Core; +using DataFuse.Adapters.Abstraction; -namespace Schemio.API.Tests.EntitySetup +namespace DataFuse.Adapters.SQL.Tests.EntitySetup { internal class CustomerRequest : IEntityRequest { diff --git a/tests/Schemio.Core.Tests/EntitySetup/Entities/Address.cs b/tests/DataFuse.Adapters.SQL.Tests/EntitySetup/Entities/Address.cs similarity index 83% rename from tests/Schemio.Core.Tests/EntitySetup/Entities/Address.cs rename to tests/DataFuse.Adapters.SQL.Tests/EntitySetup/Entities/Address.cs index db8a8c9..75c3d87 100644 --- a/tests/Schemio.Core.Tests/EntitySetup/Entities/Address.cs +++ b/tests/DataFuse.Adapters.SQL.Tests/EntitySetup/Entities/Address.cs @@ -1,4 +1,4 @@ -namespace Schemio.Core.Tests.EntitySetup.Entities +namespace DataFuse.Adapters.SQL.Tests.EntitySetup.Entities { public class Address { diff --git a/tests/Schemio.Core.Tests/EntitySetup/Entities/Communication.cs b/tests/DataFuse.Adapters.SQL.Tests/EntitySetup/Entities/Communication.cs similarity index 78% rename from tests/Schemio.Core.Tests/EntitySetup/Entities/Communication.cs rename to tests/DataFuse.Adapters.SQL.Tests/EntitySetup/Entities/Communication.cs index 68a39d9..4136f85 100644 --- a/tests/Schemio.Core.Tests/EntitySetup/Entities/Communication.cs +++ b/tests/DataFuse.Adapters.SQL.Tests/EntitySetup/Entities/Communication.cs @@ -1,4 +1,4 @@ -namespace Schemio.Core.Tests.EntitySetup.Entities +namespace DataFuse.Adapters.SQL.Tests.EntitySetup.Entities { public class Communication { diff --git a/tests/Schemio.SQL.Tests/EntitySetup/Entities/Customer.cs b/tests/DataFuse.Adapters.SQL.Tests/EntitySetup/Entities/Customer.cs similarity index 73% rename from tests/Schemio.SQL.Tests/EntitySetup/Entities/Customer.cs rename to tests/DataFuse.Adapters.SQL.Tests/EntitySetup/Entities/Customer.cs index 772c668..6cd74bc 100644 --- a/tests/Schemio.SQL.Tests/EntitySetup/Entities/Customer.cs +++ b/tests/DataFuse.Adapters.SQL.Tests/EntitySetup/Entities/Customer.cs @@ -1,6 +1,6 @@ -using Schemio.Core; +using DataFuse.Adapters.Abstraction; -namespace Schemio.SQL.Tests.EntitySetup.Entities +namespace DataFuse.Adapters.SQL.Tests.EntitySetup.Entities { public class Customer : IEntity { diff --git a/tests/Schemio.EntityFramework.Tests/EntitySetup/Entities/EntityDiagram.cd b/tests/DataFuse.Adapters.SQL.Tests/EntitySetup/Entities/EntityDiagram.cd similarity index 100% rename from tests/Schemio.EntityFramework.Tests/EntitySetup/Entities/EntityDiagram.cd rename to tests/DataFuse.Adapters.SQL.Tests/EntitySetup/Entities/EntityDiagram.cd diff --git a/tests/Schemio.Core.Tests/EntitySetup/Entities/Order.cs b/tests/DataFuse.Adapters.SQL.Tests/EntitySetup/Entities/Order.cs similarity index 78% rename from tests/Schemio.Core.Tests/EntitySetup/Entities/Order.cs rename to tests/DataFuse.Adapters.SQL.Tests/EntitySetup/Entities/Order.cs index 9f3a70e..7e9fd6b 100644 --- a/tests/Schemio.Core.Tests/EntitySetup/Entities/Order.cs +++ b/tests/DataFuse.Adapters.SQL.Tests/EntitySetup/Entities/Order.cs @@ -1,4 +1,4 @@ -namespace Schemio.Core.Tests.EntitySetup.Entities +namespace DataFuse.Adapters.SQL.Tests.EntitySetup.Entities { public class Order { diff --git a/tests/Schemio.Core.Tests/EntitySetup/Entities/OrderItem.cs b/tests/DataFuse.Adapters.SQL.Tests/EntitySetup/Entities/OrderItem.cs similarity index 73% rename from tests/Schemio.Core.Tests/EntitySetup/Entities/OrderItem.cs rename to tests/DataFuse.Adapters.SQL.Tests/EntitySetup/Entities/OrderItem.cs index 99616a1..deaa187 100644 --- a/tests/Schemio.Core.Tests/EntitySetup/Entities/OrderItem.cs +++ b/tests/DataFuse.Adapters.SQL.Tests/EntitySetup/Entities/OrderItem.cs @@ -1,4 +1,4 @@ -namespace Schemio.Core.Tests.EntitySetup.Entities +namespace DataFuse.Adapters.SQL.Tests.EntitySetup.Entities { public class OrderItem { diff --git a/tests/Schemio.EntityFramework.Tests/EntitySetup/EntitySchemas/CustomerConfiguration.cs b/tests/DataFuse.Adapters.SQL.Tests/EntitySetup/EntityConfiguration/CustomerConfiguration.cs similarity index 71% rename from tests/Schemio.EntityFramework.Tests/EntitySetup/EntitySchemas/CustomerConfiguration.cs rename to tests/DataFuse.Adapters.SQL.Tests/EntitySetup/EntityConfiguration/CustomerConfiguration.cs index d3769bc..d00702e 100644 --- a/tests/Schemio.EntityFramework.Tests/EntitySetup/EntitySchemas/CustomerConfiguration.cs +++ b/tests/DataFuse.Adapters.SQL.Tests/EntitySetup/EntityConfiguration/CustomerConfiguration.cs @@ -1,9 +1,9 @@ -using Schemio.Core; -using Schemio.EntityFramework.Tests.EntitySetup.Entities; -using Schemio.EntityFramework.Tests.EntitySetup.EntitySchemas.Queries; -using Schemio.EntityFramework.Tests.EntitySetup.EntitySchemas.Transforms; +using DataFuse.Adapters.Abstraction; +using DataFuse.Adapters.SQL.Tests.EntitySetup.Entities; +using DataFuse.Adapters.SQL.Tests.EntitySetup.EntitySchemas.Queries; +using DataFuse.Adapters.SQL.Tests.EntitySetup.EntitySchemas.Transforms; -namespace Schemio.EntityFramework.Tests.EntitySetup.EntitySchemas +namespace DataFuse.Adapters.SQL.Tests.EntitySetup.EntitySchemas { internal class CustomerConfiguration : EntityConfiguration { diff --git a/tests/Schemio.SQL.Tests/EntitySetup/EntityConfiguration/Queries/CommunicationQuery.cs b/tests/DataFuse.Adapters.SQL.Tests/EntitySetup/EntityConfiguration/Queries/CommunicationQuery.cs similarity index 90% rename from tests/Schemio.SQL.Tests/EntitySetup/EntityConfiguration/Queries/CommunicationQuery.cs rename to tests/DataFuse.Adapters.SQL.Tests/EntitySetup/EntityConfiguration/Queries/CommunicationQuery.cs index 0a75dd9..dc30fde 100644 --- a/tests/Schemio.SQL.Tests/EntitySetup/EntityConfiguration/Queries/CommunicationQuery.cs +++ b/tests/DataFuse.Adapters.SQL.Tests/EntitySetup/EntityConfiguration/Queries/CommunicationQuery.cs @@ -1,8 +1,8 @@ using System.Data; using Dapper; -using Schemio.Core; +using DataFuse.Adapters.Abstraction; -namespace Schemio.SQL.Tests.EntitySetup.EntitySchemas.Queries +namespace DataFuse.Adapters.SQL.Tests.EntitySetup.EntitySchemas.Queries { internal class CommunicationQuery : SQLQuery { diff --git a/tests/Schemio.SQL.Tests/EntitySetup/EntityConfiguration/Queries/CommunicationRecord.cs b/tests/DataFuse.Adapters.SQL.Tests/EntitySetup/EntityConfiguration/Queries/CommunicationRecord.cs similarity index 81% rename from tests/Schemio.SQL.Tests/EntitySetup/EntityConfiguration/Queries/CommunicationRecord.cs rename to tests/DataFuse.Adapters.SQL.Tests/EntitySetup/EntityConfiguration/Queries/CommunicationRecord.cs index a71784b..110c112 100644 --- a/tests/Schemio.SQL.Tests/EntitySetup/EntityConfiguration/Queries/CommunicationRecord.cs +++ b/tests/DataFuse.Adapters.SQL.Tests/EntitySetup/EntityConfiguration/Queries/CommunicationRecord.cs @@ -1,6 +1,6 @@ -using Schemio.Core; +using DataFuse.Adapters.Abstraction; -namespace Schemio.SQL.Tests.EntitySetup.EntitySchemas.Queries +namespace DataFuse.Adapters.SQL.Tests.EntitySetup.EntitySchemas.Queries { [CacheResult] public class CommunicationRecord : IQueryResult diff --git a/tests/Schemio.SQL.Tests/EntitySetup/EntityConfiguration/Queries/CustomerQuery.cs b/tests/DataFuse.Adapters.SQL.Tests/EntitySetup/EntityConfiguration/Queries/CustomerQuery.cs similarity index 86% rename from tests/Schemio.SQL.Tests/EntitySetup/EntityConfiguration/Queries/CustomerQuery.cs rename to tests/DataFuse.Adapters.SQL.Tests/EntitySetup/EntityConfiguration/Queries/CustomerQuery.cs index fb5c2ca..0e93134 100644 --- a/tests/Schemio.SQL.Tests/EntitySetup/EntityConfiguration/Queries/CustomerQuery.cs +++ b/tests/DataFuse.Adapters.SQL.Tests/EntitySetup/EntityConfiguration/Queries/CustomerQuery.cs @@ -1,8 +1,8 @@ using System.Data; using Dapper; -using Schemio.Core; +using DataFuse.Adapters.Abstraction; -namespace Schemio.SQL.Tests.EntitySetup.EntitySchemas.Queries +namespace DataFuse.Adapters.SQL.Tests.EntitySetup.EntitySchemas.Queries { public class CustomerQuery : SQLQuery { diff --git a/tests/Schemio.EntityFramework.Tests/EntitySetup/EntitySchemas/Queries/CustomerRecord.cs b/tests/DataFuse.Adapters.SQL.Tests/EntitySetup/EntityConfiguration/Queries/CustomerRecord.cs similarity index 62% rename from tests/Schemio.EntityFramework.Tests/EntitySetup/EntitySchemas/Queries/CustomerRecord.cs rename to tests/DataFuse.Adapters.SQL.Tests/EntitySetup/EntityConfiguration/Queries/CustomerRecord.cs index ae7f175..d67c195 100644 --- a/tests/Schemio.EntityFramework.Tests/EntitySetup/EntitySchemas/Queries/CustomerRecord.cs +++ b/tests/DataFuse.Adapters.SQL.Tests/EntitySetup/EntityConfiguration/Queries/CustomerRecord.cs @@ -1,6 +1,6 @@ -using Schemio.Core; +using DataFuse.Adapters.Abstraction; -namespace Schemio.EntityFramework.Tests.EntitySetup.EntitySchemas.Queries +namespace DataFuse.Adapters.SQL.Tests.EntitySetup.EntitySchemas.Queries { public class CustomerRecord : IQueryResult { diff --git a/tests/Schemio.SQL.Tests/EntitySetup/EntityConfiguration/Queries/OrderItemRecord.cs b/tests/DataFuse.Adapters.SQL.Tests/EntitySetup/EntityConfiguration/Queries/OrderItemRecord.cs similarity index 67% rename from tests/Schemio.SQL.Tests/EntitySetup/EntityConfiguration/Queries/OrderItemRecord.cs rename to tests/DataFuse.Adapters.SQL.Tests/EntitySetup/EntityConfiguration/Queries/OrderItemRecord.cs index f9667ee..e1c0c31 100644 --- a/tests/Schemio.SQL.Tests/EntitySetup/EntityConfiguration/Queries/OrderItemRecord.cs +++ b/tests/DataFuse.Adapters.SQL.Tests/EntitySetup/EntityConfiguration/Queries/OrderItemRecord.cs @@ -1,6 +1,6 @@ -using Schemio.Core; +using DataFuse.Adapters.Abstraction; -namespace Schemio.SQL.Tests.EntitySetup.EntitySchemas.Queries +namespace DataFuse.Adapters.SQL.Tests.EntitySetup.EntitySchemas.Queries { public class OrderItemRecord : IQueryResult { diff --git a/tests/Schemio.SQL.Tests/EntitySetup/EntityConfiguration/Queries/OrderItemsQuery.cs b/tests/DataFuse.Adapters.SQL.Tests/EntitySetup/EntityConfiguration/Queries/OrderItemsQuery.cs similarity index 87% rename from tests/Schemio.SQL.Tests/EntitySetup/EntityConfiguration/Queries/OrderItemsQuery.cs rename to tests/DataFuse.Adapters.SQL.Tests/EntitySetup/EntityConfiguration/Queries/OrderItemsQuery.cs index 4256ae7..76f0643 100644 --- a/tests/Schemio.SQL.Tests/EntitySetup/EntityConfiguration/Queries/OrderItemsQuery.cs +++ b/tests/DataFuse.Adapters.SQL.Tests/EntitySetup/EntityConfiguration/Queries/OrderItemsQuery.cs @@ -1,9 +1,9 @@ using System.Data; using Dapper; -using Schemio.Core; -using Schemio.Core.Helpers; +using DataFuse.Adapters.Abstraction; +using DataFuse.Integration.Helpers; -namespace Schemio.SQL.Tests.EntitySetup.EntitySchemas.Queries +namespace DataFuse.Adapters.SQL.Tests.EntitySetup.EntitySchemas.Queries { internal class OrderItemsQuery : SQLQuery> { diff --git a/tests/Schemio.SQL.Tests/EntitySetup/EntityConfiguration/Queries/OrderRecord.cs b/tests/DataFuse.Adapters.SQL.Tests/EntitySetup/EntityConfiguration/Queries/OrderRecord.cs similarity index 63% rename from tests/Schemio.SQL.Tests/EntitySetup/EntityConfiguration/Queries/OrderRecord.cs rename to tests/DataFuse.Adapters.SQL.Tests/EntitySetup/EntityConfiguration/Queries/OrderRecord.cs index 336df23..3abb7af 100644 --- a/tests/Schemio.SQL.Tests/EntitySetup/EntityConfiguration/Queries/OrderRecord.cs +++ b/tests/DataFuse.Adapters.SQL.Tests/EntitySetup/EntityConfiguration/Queries/OrderRecord.cs @@ -1,6 +1,6 @@ -using Schemio.Core; +using DataFuse.Adapters.Abstraction; -namespace Schemio.SQL.Tests.EntitySetup.EntitySchemas.Queries +namespace DataFuse.Adapters.SQL.Tests.EntitySetup.EntitySchemas.Queries { public class OrderRecord : IQueryResult { diff --git a/tests/Schemio.SQL.Tests/EntitySetup/EntityConfiguration/Queries/OrdersQuery.cs b/tests/DataFuse.Adapters.SQL.Tests/EntitySetup/EntityConfiguration/Queries/OrdersQuery.cs similarity index 89% rename from tests/Schemio.SQL.Tests/EntitySetup/EntityConfiguration/Queries/OrdersQuery.cs rename to tests/DataFuse.Adapters.SQL.Tests/EntitySetup/EntityConfiguration/Queries/OrdersQuery.cs index e49a354..b5af7a4 100644 --- a/tests/Schemio.SQL.Tests/EntitySetup/EntityConfiguration/Queries/OrdersQuery.cs +++ b/tests/DataFuse.Adapters.SQL.Tests/EntitySetup/EntityConfiguration/Queries/OrdersQuery.cs @@ -1,8 +1,8 @@ using System.Data; using Dapper; -using Schemio.Core; +using DataFuse.Adapters.Abstraction; -namespace Schemio.SQL.Tests.EntitySetup.EntitySchemas.Queries +namespace DataFuse.Adapters.SQL.Tests.EntitySetup.EntitySchemas.Queries { internal class OrdersQuery : SQLQuery> { diff --git a/tests/Schemio.SQL.Tests/EntitySetup/EntityConfiguration/Transforms/CommunicationTransform.cs b/tests/DataFuse.Adapters.SQL.Tests/EntitySetup/EntityConfiguration/Transforms/CommunicationTransform.cs similarity index 79% rename from tests/Schemio.SQL.Tests/EntitySetup/EntityConfiguration/Transforms/CommunicationTransform.cs rename to tests/DataFuse.Adapters.SQL.Tests/EntitySetup/EntityConfiguration/Transforms/CommunicationTransform.cs index caadad1..81f6e3e 100644 --- a/tests/Schemio.SQL.Tests/EntitySetup/EntityConfiguration/Transforms/CommunicationTransform.cs +++ b/tests/DataFuse.Adapters.SQL.Tests/EntitySetup/EntityConfiguration/Transforms/CommunicationTransform.cs @@ -1,8 +1,8 @@ -using Schemio.Core; -using Schemio.SQL.Tests.EntitySetup.Entities; -using Schemio.SQL.Tests.EntitySetup.EntitySchemas.Queries; +using DataFuse.Adapters.Abstraction; +using DataFuse.Adapters.SQL.Tests.EntitySetup.Entities; +using DataFuse.Adapters.SQL.Tests.EntitySetup.EntitySchemas.Queries; -namespace Schemio.SQL.Tests.EntitySetup.EntitySchemas.Transforms +namespace DataFuse.Adapters.SQL.Tests.EntitySetup.EntitySchemas.Transforms { public class CommunicationTransform : BaseTransformer { diff --git a/tests/Schemio.SQL.Tests/EntitySetup/EntityConfiguration/Transforms/CustomerTransform.cs b/tests/DataFuse.Adapters.SQL.Tests/EntitySetup/EntityConfiguration/Transforms/CustomerTransform.cs similarity index 61% rename from tests/Schemio.SQL.Tests/EntitySetup/EntityConfiguration/Transforms/CustomerTransform.cs rename to tests/DataFuse.Adapters.SQL.Tests/EntitySetup/EntityConfiguration/Transforms/CustomerTransform.cs index 8669ffa..39facbe 100644 --- a/tests/Schemio.SQL.Tests/EntitySetup/EntityConfiguration/Transforms/CustomerTransform.cs +++ b/tests/DataFuse.Adapters.SQL.Tests/EntitySetup/EntityConfiguration/Transforms/CustomerTransform.cs @@ -1,8 +1,8 @@ -using Schemio.Core; -using Schemio.SQL.Tests.EntitySetup.Entities; -using Schemio.SQL.Tests.EntitySetup.EntitySchemas.Queries; +using DataFuse.Adapters.Abstraction; +using DataFuse.Adapters.SQL.Tests.EntitySetup.Entities; +using DataFuse.Adapters.SQL.Tests.EntitySetup.EntitySchemas.Queries; -namespace Schemio.SQL.Tests.EntitySetup.EntitySchemas.Transforms +namespace DataFuse.Adapters.SQL.Tests.EntitySetup.EntitySchemas.Transforms { public class CustomerTransform : BaseTransformer { diff --git a/tests/Schemio.SQL.Tests/EntitySetup/EntityConfiguration/Transforms/OrderItemsTransform.cs b/tests/DataFuse.Adapters.SQL.Tests/EntitySetup/EntityConfiguration/Transforms/OrderItemsTransform.cs similarity index 76% rename from tests/Schemio.SQL.Tests/EntitySetup/EntityConfiguration/Transforms/OrderItemsTransform.cs rename to tests/DataFuse.Adapters.SQL.Tests/EntitySetup/EntityConfiguration/Transforms/OrderItemsTransform.cs index 0dc1f85..046a51a 100644 --- a/tests/Schemio.SQL.Tests/EntitySetup/EntityConfiguration/Transforms/OrderItemsTransform.cs +++ b/tests/DataFuse.Adapters.SQL.Tests/EntitySetup/EntityConfiguration/Transforms/OrderItemsTransform.cs @@ -1,9 +1,9 @@ -using Schemio.Core; -using Schemio.Core.Helpers; -using Schemio.SQL.Tests.EntitySetup.Entities; -using Schemio.SQL.Tests.EntitySetup.EntitySchemas.Queries; +using DataFuse.Adapters.Abstraction; +using DataFuse.Integration.Helpers; +using DataFuse.Adapters.SQL.Tests.EntitySetup.Entities; +using DataFuse.Adapters.SQL.Tests.EntitySetup.EntitySchemas.Queries; -namespace Schemio.SQL.Tests.EntitySetup.EntitySchemas.Transforms +namespace DataFuse.Adapters.SQL.Tests.EntitySetup.EntitySchemas.Transforms { public class OrderItemsTransform : BaseTransformer, Customer> { diff --git a/tests/Schemio.SQL.Tests/EntitySetup/EntityConfiguration/Transforms/OrdersTransform.cs b/tests/DataFuse.Adapters.SQL.Tests/EntitySetup/EntityConfiguration/Transforms/OrdersTransform.cs similarity index 65% rename from tests/Schemio.SQL.Tests/EntitySetup/EntityConfiguration/Transforms/OrdersTransform.cs rename to tests/DataFuse.Adapters.SQL.Tests/EntitySetup/EntityConfiguration/Transforms/OrdersTransform.cs index c11a122..cecf882 100644 --- a/tests/Schemio.SQL.Tests/EntitySetup/EntityConfiguration/Transforms/OrdersTransform.cs +++ b/tests/DataFuse.Adapters.SQL.Tests/EntitySetup/EntityConfiguration/Transforms/OrdersTransform.cs @@ -1,8 +1,9 @@ -using Schemio.Core; -using Schemio.SQL.Tests.EntitySetup.Entities; -using Schemio.SQL.Tests.EntitySetup.EntitySchemas.Queries; +using System.Globalization; +using DataFuse.Adapters.Abstraction; +using DataFuse.Adapters.SQL.Tests.EntitySetup.Entities; +using DataFuse.Adapters.SQL.Tests.EntitySetup.EntitySchemas.Queries; -namespace Schemio.SQL.Tests.EntitySetup.EntitySchemas.Transforms +namespace DataFuse.Adapters.SQL.Tests.EntitySetup.EntitySchemas.Transforms { public class OrdersTransform : BaseTransformer, Customer> { @@ -19,7 +20,7 @@ public override void Transform(CollectionResult collectionResult, C { customer.Orders[index] = new Order { - Date = DateTime.Parse(collectionResult[index].OrderDate), + Date = DateTime.ParseExact(collectionResult[index].OrderDate, "dd/MM/yyyy HH:mm:ss", CultureInfo.InvariantCulture), OrderId = collectionResult[index].OrderId, OrderNo = collectionResult[index].OrderNo }; diff --git a/tests/DataFuse.Adapters.SQL.Tests/GlobalUsings.cs b/tests/DataFuse.Adapters.SQL.Tests/GlobalUsings.cs new file mode 100644 index 0000000..1a84013 --- /dev/null +++ b/tests/DataFuse.Adapters.SQL.Tests/GlobalUsings.cs @@ -0,0 +1,4 @@ +global using NUnit.Framework; +global using DataFuse.Adapters.Abstraction; +global using DataFuse.Adapters.SQL; +global using DataFuse.Integration; diff --git a/tests/Schemio.API.Tests/Schemio.API.Tests.csproj b/tests/DataFuse.Adapters.WebAPI.Tests/DataFuse.Adapters.WebAPI.Tests.csproj similarity index 56% rename from tests/Schemio.API.Tests/Schemio.API.Tests.csproj rename to tests/DataFuse.Adapters.WebAPI.Tests/DataFuse.Adapters.WebAPI.Tests.csproj index efbeb65..4e3efc1 100644 --- a/tests/Schemio.API.Tests/Schemio.API.Tests.csproj +++ b/tests/DataFuse.Adapters.WebAPI.Tests/DataFuse.Adapters.WebAPI.Tests.csproj @@ -1,7 +1,7 @@ - net9.0 + net8.0;net9.0;net10.0 enable enable @@ -10,23 +10,24 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + - + + diff --git a/tests/Schemio.API.Tests/E2E.Tests/BaseTest.cs b/tests/DataFuse.Adapters.WebAPI.Tests/E2E.Tests/BaseTest.cs similarity index 82% rename from tests/Schemio.API.Tests/E2E.Tests/BaseTest.cs rename to tests/DataFuse.Adapters.WebAPI.Tests/E2E.Tests/BaseTest.cs index ebb7a34..11250d8 100644 --- a/tests/Schemio.API.Tests/E2E.Tests/BaseTest.cs +++ b/tests/DataFuse.Adapters.WebAPI.Tests/E2E.Tests/BaseTest.cs @@ -1,14 +1,16 @@ using Microsoft.Extensions.DependencyInjection; +using DataFuse.Integration; using Microsoft.Extensions.Logging; using NUnit.Framework; -using Schemio.API.Tests.EntitySetup; -using Schemio.Core; -using Schemio.Core.PathMatchers; +using DataFuse.Adapters.WebAPI.Tests.EntitySetup; +using DataFuse.Adapters.WebAPI.Tests.EntitySetup.WebApis; +using DataFuse.Adapters.Abstraction; +using DataFuse.Integration.PathMatchers; using WireMock.RequestBuilders; using WireMock.ResponseBuilders; using WireMock.Server; -namespace Schemio.API.Tests.E2E.Tests +namespace DataFuse.Adapters.WebAPI.Tests.E2E.Tests { [TestFixture] public class BaseTest @@ -55,17 +57,18 @@ public void StubApi(string endpoint, object body, IDictionary he [OneTimeSetUp] public void OneTimeSetup() { - server = WireMockServer.Start(5000); + server = WireMockServer.Start(); + Endpoints.BaseAddress = server.Url + "/"; var services = new ServiceCollection(); services.AddLogging(c => c.AddConsole()); services.AddHttpClient(); - services.UseSchemio() + services.UseDataFuse(configuration => configuration .WithEngine() .WithPathMatcher(c => new JPathMatcher()) - .WithEntityConfiguration(c => new CustomerConfiguration()); + .WithEntityConfiguration(c => new CustomerConfiguration())); // 4. Build the service provider serviceProvider = services.BuildServiceProvider(); diff --git a/tests/DataFuse.Adapters.WebAPI.Tests/E2E.Tests/E2ETests.cs b/tests/DataFuse.Adapters.WebAPI.Tests/E2E.Tests/E2ETests.cs new file mode 100644 index 0000000..5b9cdc3 --- /dev/null +++ b/tests/DataFuse.Adapters.WebAPI.Tests/E2E.Tests/E2ETests.cs @@ -0,0 +1,167 @@ +using DataFuse.Integration.Helpers; +using NUnit.Framework; +using DataFuse.Adapters.WebAPI.Tests.EntitySetup; +using DataFuse.Adapters.WebAPI.Tests.EntitySetup.WebApis; +using static DataFuse.Adapters.WebAPI.Tests.EntitySetup.Customer; +using static DataFuse.Adapters.WebAPI.Tests.EntitySetup.Customer.Contacts; + +namespace DataFuse.Adapters.WebAPI.Tests.E2E.Tests; + +[TestFixture] +public class E2ETests : BaseTest +{ + [SetUp] + public void Setup() + { + StubApi(string.Format(Endpoints.BaseAddress + Endpoints.Customer, Endpoints.Ids.CustomerId), new { Id = 1000, Name = "John McKinsey", Code = "THG-UY6789" }, new Dictionary { { "x-meta-branch-code", "London" } }); + StubApi(string.Format(Endpoints.BaseAddress + Endpoints.Communication, Endpoints.Ids.CustomerId), new { Id = 4567, Telephone = "07675998878", Email = "John.McKinsy@gmail.com", HouseNo = "22", City = "London", Region = "London", PostalCode = "W12 6GH", Country = "United Kingdom" }); + StubApi(string.Format(Endpoints.BaseAddress + Endpoints.Orders, Endpoints.Ids.CustomerId), new[] { new { OrderId = 1234, OrderNo = "GHK-897GB", Date = "2024-01-01T00:00:00" } }); + StubApi(string.Format(Endpoints.BaseAddress + Endpoints.OrderItems, Endpoints.Ids.CustomerId, Endpoints.Ids.OrderId), new[] { new { OrderId = 1234, ItemId = 2244, Name = "Pen", Cost = 12.00m }, new { OrderId = 1234, ItemId = 6677, Name = "Book", Cost = 15.00m } }); + } + + [TearDown] + public void Teardown() + { + } + + [Test] + public async Task TestDataProviderToFetchWholeContractWhenNamesAreNull() + { + var customer = await dataProvider.GetDataAsync(new CustomerRequest + { + CustomerId = Endpoints.Ids.CustomerId + }); + + var expected = new Customer + { + Id = 1000, + Name = "John McKinsey", + Code = "THG-UY6789", + Branch = "London", // Received via response header + Communication = new Contacts + { + ContactId = 4567, + Phone = "07675998878", + Email = "John.McKinsy@gmail.com", + PostalAddress = new Address + { + HouseNo = "22", + City = "London", + Region = "London", + PostalCode = "W12 6GH", + Country = "United Kingdom", + } + }, + Orders = + [ + new Order + { + OrderId = 1234, + OrderNo = "GHK-897GB", + Date = DateTime.Parse("2024-01-01T00:00:00"), + Items = + [ + new Order.OrderItem + { + ItemId = 2244, Name = "Pen", Cost = 12.00m + }, + new Order.OrderItem + { + ItemId = 6677, Name = "Book", Cost = 15.00m + } + ] + } + ] + }; + + AssertAreEqual(expected, customer); + } + + [Test] + public async Task TestDataProviderToFetchPartialCustomerOrdersContractWhenNamesAreIncluded() + { + var customer = await dataProvider.GetDataAsync(new CustomerRequest + { + CustomerId = Endpoints.Ids.CustomerId, + SchemaPaths = ["customer.orders.items"] + }); + + var expected = new Customer + { + Id = 1000, + Name = "John McKinsey", + Code = "THG-UY6789", + Branch = "London", + Orders = + [ + new Order + { + OrderId = 1234, + OrderNo = "GHK-897GB", + Date = DateTime.Parse("2024-01-01T00:00:00"), + Items = + [ + new Order.OrderItem + { + ItemId = 2244, Name = "Pen", Cost = 12.00m + }, + new Order.OrderItem + { + ItemId = 6677, Name = "Book", Cost = 15.00m + } + ] + } + ] + }; + + AssertAreEqual(expected, customer); + } + + [Test] + public async Task TestDataProviderToFetchPartialCustomerCommunicationContractWhenNamesAreIncluded() + { + var customer = await dataProvider.GetDataAsync(new CustomerRequest + { + CustomerId = Endpoints.Ids.CustomerId, + SchemaPaths = ["customer.communication"] + }); + + var expected = new Customer + { + Id = 1000, + Name = "John McKinsey", + Code = "THG-UY6789", + Branch = "London", + Communication = new Contacts + { + ContactId = 4567, + Phone = "07675998878", + Email = "John.McKinsy@gmail.com", + PostalAddress = new Address + { + HouseNo = "22", + City = "London", + Region = "London", + PostalCode = "W12 6GH", + Country = "United Kingdom", + } + } + }; + + AssertAreEqual(expected, customer); + } + + private void AssertAreEqual(Customer expected, Customer actual) + { + var actualCustomer = actual.ToJson(); + var expectedCustomer = expected.ToJson(); + + Console.WriteLine("expected:"); + Console.WriteLine(expectedCustomer); + + Console.WriteLine("actual:"); + Console.WriteLine(actualCustomer); + + Assert.That(actualCustomer, Is.EqualTo(expectedCustomer)); + } +} diff --git a/tests/Schemio.API.Tests/EntitySetup/Customer.cs b/tests/DataFuse.Adapters.WebAPI.Tests/EntitySetup/Customer.cs similarity index 93% rename from tests/Schemio.API.Tests/EntitySetup/Customer.cs rename to tests/DataFuse.Adapters.WebAPI.Tests/EntitySetup/Customer.cs index b9edab2..7e9b7b6 100644 --- a/tests/Schemio.API.Tests/EntitySetup/Customer.cs +++ b/tests/DataFuse.Adapters.WebAPI.Tests/EntitySetup/Customer.cs @@ -1,6 +1,6 @@ -using Schemio.Core; +using DataFuse.Adapters.Abstraction; -namespace Schemio.API.Tests.EntitySetup +namespace DataFuse.Adapters.WebAPI.Tests.EntitySetup { public class Customer : IEntity { diff --git a/tests/Schemio.API.Tests/EntitySetup/CustomerConfiguration.cs b/tests/DataFuse.Adapters.WebAPI.Tests/EntitySetup/CustomerConfiguration.cs similarity index 81% rename from tests/Schemio.API.Tests/EntitySetup/CustomerConfiguration.cs rename to tests/DataFuse.Adapters.WebAPI.Tests/EntitySetup/CustomerConfiguration.cs index da5f523..5dfb78f 100644 --- a/tests/Schemio.API.Tests/EntitySetup/CustomerConfiguration.cs +++ b/tests/DataFuse.Adapters.WebAPI.Tests/EntitySetup/CustomerConfiguration.cs @@ -1,8 +1,8 @@ -using Schemio.API.Tests.EntitySetup.ResultTransformers; -using Schemio.API.Tests.EntitySetup.WebApis; -using Schemio.Core; +using DataFuse.Adapters.WebAPI.Tests.EntitySetup.ResultTransformers; +using DataFuse.Adapters.WebAPI.Tests.EntitySetup.WebApis; +using DataFuse.Adapters.Abstraction; -namespace Schemio.API.Tests.EntitySetup +namespace DataFuse.Adapters.WebAPI.Tests.EntitySetup { internal class CustomerConfiguration : EntityConfiguration { diff --git a/tests/Schemio.EntityFramework.Tests/EntitySetup/CustomerRequest.cs b/tests/DataFuse.Adapters.WebAPI.Tests/EntitySetup/CustomerRequest.cs similarity index 64% rename from tests/Schemio.EntityFramework.Tests/EntitySetup/CustomerRequest.cs rename to tests/DataFuse.Adapters.WebAPI.Tests/EntitySetup/CustomerRequest.cs index e33b242..5a55058 100644 --- a/tests/Schemio.EntityFramework.Tests/EntitySetup/CustomerRequest.cs +++ b/tests/DataFuse.Adapters.WebAPI.Tests/EntitySetup/CustomerRequest.cs @@ -1,6 +1,6 @@ -using Schemio.Core; +using DataFuse.Adapters.Abstraction; -namespace Schemio.EntityFramework.Tests.EntitySetup +namespace DataFuse.Adapters.WebAPI.Tests.EntitySetup { internal class CustomerRequest : IEntityRequest { diff --git a/tests/Schemio.API.Tests/EntitySetup/QueryResults/CommunicationResult.cs b/tests/DataFuse.Adapters.WebAPI.Tests/EntitySetup/QueryResults/CommunicationResult.cs similarity index 80% rename from tests/Schemio.API.Tests/EntitySetup/QueryResults/CommunicationResult.cs rename to tests/DataFuse.Adapters.WebAPI.Tests/EntitySetup/QueryResults/CommunicationResult.cs index 729c021..c064e9e 100644 --- a/tests/Schemio.API.Tests/EntitySetup/QueryResults/CommunicationResult.cs +++ b/tests/DataFuse.Adapters.WebAPI.Tests/EntitySetup/QueryResults/CommunicationResult.cs @@ -1,6 +1,6 @@ -using Schemio.Core; +using DataFuse.Adapters.Abstraction; -namespace Schemio.API.Tests.EntitySetup.QueryResults +namespace DataFuse.Adapters.WebAPI.Tests.EntitySetup.QueryResults { [CacheResult] public class CommunicationResult : IQueryResult diff --git a/tests/Schemio.API.Tests/EntitySetup/QueryResults/CustomerResult.cs b/tests/DataFuse.Adapters.WebAPI.Tests/EntitySetup/QueryResults/CustomerResult.cs similarity index 73% rename from tests/Schemio.API.Tests/EntitySetup/QueryResults/CustomerResult.cs rename to tests/DataFuse.Adapters.WebAPI.Tests/EntitySetup/QueryResults/CustomerResult.cs index 0a55954..8deff49 100644 --- a/tests/Schemio.API.Tests/EntitySetup/QueryResults/CustomerResult.cs +++ b/tests/DataFuse.Adapters.WebAPI.Tests/EntitySetup/QueryResults/CustomerResult.cs @@ -1,4 +1,4 @@ -namespace Schemio.API.Tests.EntitySetup.QueryResults +namespace DataFuse.Adapters.WebAPI.Tests.EntitySetup.QueryResults { public class CustomerResult : WebHeaderResult { diff --git a/tests/Schemio.API.Tests/EntitySetup/QueryResults/OrderItemResult.cs b/tests/DataFuse.Adapters.WebAPI.Tests/EntitySetup/QueryResults/OrderItemResult.cs similarity index 76% rename from tests/Schemio.API.Tests/EntitySetup/QueryResults/OrderItemResult.cs rename to tests/DataFuse.Adapters.WebAPI.Tests/EntitySetup/QueryResults/OrderItemResult.cs index 260a388..634b9cf 100644 --- a/tests/Schemio.API.Tests/EntitySetup/QueryResults/OrderItemResult.cs +++ b/tests/DataFuse.Adapters.WebAPI.Tests/EntitySetup/QueryResults/OrderItemResult.cs @@ -1,4 +1,4 @@ -namespace Schemio.API.Tests.EntitySetup.QueryResults +namespace DataFuse.Adapters.WebAPI.Tests.EntitySetup.QueryResults { public class OrderItemResult { diff --git a/tests/Schemio.API.Tests/EntitySetup/QueryResults/OrderResult.cs b/tests/DataFuse.Adapters.WebAPI.Tests/EntitySetup/QueryResults/OrderResult.cs similarity index 64% rename from tests/Schemio.API.Tests/EntitySetup/QueryResults/OrderResult.cs rename to tests/DataFuse.Adapters.WebAPI.Tests/EntitySetup/QueryResults/OrderResult.cs index 6a5a26d..6537c23 100644 --- a/tests/Schemio.API.Tests/EntitySetup/QueryResults/OrderResult.cs +++ b/tests/DataFuse.Adapters.WebAPI.Tests/EntitySetup/QueryResults/OrderResult.cs @@ -1,6 +1,6 @@ -using Schemio.Core; +using DataFuse.Adapters.Abstraction; -namespace Schemio.API.Tests.EntitySetup.QueryResults +namespace DataFuse.Adapters.WebAPI.Tests.EntitySetup.QueryResults { public class OrderResult : IQueryResult { diff --git a/tests/Schemio.API.Tests/EntitySetup/Transforms/CommunicationTransform.cs b/tests/DataFuse.Adapters.WebAPI.Tests/EntitySetup/Transforms/CommunicationTransform.cs similarity index 83% rename from tests/Schemio.API.Tests/EntitySetup/Transforms/CommunicationTransform.cs rename to tests/DataFuse.Adapters.WebAPI.Tests/EntitySetup/Transforms/CommunicationTransform.cs index d7fc36d..0c324d3 100644 --- a/tests/Schemio.API.Tests/EntitySetup/Transforms/CommunicationTransform.cs +++ b/tests/DataFuse.Adapters.WebAPI.Tests/EntitySetup/Transforms/CommunicationTransform.cs @@ -1,7 +1,7 @@ -using Schemio.API.Tests.EntitySetup.QueryResults; -using Schemio.Core; +using DataFuse.Adapters.WebAPI.Tests.EntitySetup.QueryResults; +using DataFuse.Adapters.Abstraction; -namespace Schemio.API.Tests.EntitySetup.ResultTransformers +namespace DataFuse.Adapters.WebAPI.Tests.EntitySetup.ResultTransformers { public class CommunicationTransform : BaseTransformer { diff --git a/tests/Schemio.API.Tests/EntitySetup/Transforms/CustomerTransform.cs b/tests/DataFuse.Adapters.WebAPI.Tests/EntitySetup/Transforms/CustomerTransform.cs similarity index 76% rename from tests/Schemio.API.Tests/EntitySetup/Transforms/CustomerTransform.cs rename to tests/DataFuse.Adapters.WebAPI.Tests/EntitySetup/Transforms/CustomerTransform.cs index 3f53a6f..9581eae 100644 --- a/tests/Schemio.API.Tests/EntitySetup/Transforms/CustomerTransform.cs +++ b/tests/DataFuse.Adapters.WebAPI.Tests/EntitySetup/Transforms/CustomerTransform.cs @@ -1,7 +1,7 @@ -using Schemio.API.Tests.EntitySetup.QueryResults; -using Schemio.Core; +using DataFuse.Adapters.WebAPI.Tests.EntitySetup.QueryResults; +using DataFuse.Adapters.Abstraction; -namespace Schemio.API.Tests.EntitySetup.ResultTransformers +namespace DataFuse.Adapters.WebAPI.Tests.EntitySetup.ResultTransformers { public class CustomerTransform : BaseTransformer { diff --git a/tests/Schemio.API.Tests/EntitySetup/Transforms/OrderItemsTransform.cs b/tests/DataFuse.Adapters.WebAPI.Tests/EntitySetup/Transforms/OrderItemsTransform.cs similarity index 76% rename from tests/Schemio.API.Tests/EntitySetup/Transforms/OrderItemsTransform.cs rename to tests/DataFuse.Adapters.WebAPI.Tests/EntitySetup/Transforms/OrderItemsTransform.cs index 8c5912a..f570e16 100644 --- a/tests/Schemio.API.Tests/EntitySetup/Transforms/OrderItemsTransform.cs +++ b/tests/DataFuse.Adapters.WebAPI.Tests/EntitySetup/Transforms/OrderItemsTransform.cs @@ -1,9 +1,9 @@ -using Schemio.API.Tests.EntitySetup.QueryResults; -using Schemio.Core; -using Schemio.Core.Helpers; -using static Schemio.API.Tests.EntitySetup.Customer.Order; +using DataFuse.Adapters.WebAPI.Tests.EntitySetup.QueryResults; +using DataFuse.Adapters.Abstraction; +using DataFuse.Integration.Helpers; +using static DataFuse.Adapters.WebAPI.Tests.EntitySetup.Customer.Order; -namespace Schemio.API.Tests.EntitySetup.ResultTransformers +namespace DataFuse.Adapters.WebAPI.Tests.EntitySetup.ResultTransformers { public class OrderItemsTransform : BaseTransformer, Customer> { diff --git a/tests/Schemio.API.Tests/EntitySetup/Transforms/OrdersTransform.cs b/tests/DataFuse.Adapters.WebAPI.Tests/EntitySetup/Transforms/OrdersTransform.cs similarity index 76% rename from tests/Schemio.API.Tests/EntitySetup/Transforms/OrdersTransform.cs rename to tests/DataFuse.Adapters.WebAPI.Tests/EntitySetup/Transforms/OrdersTransform.cs index 8f71f72..b511844 100644 --- a/tests/Schemio.API.Tests/EntitySetup/Transforms/OrdersTransform.cs +++ b/tests/DataFuse.Adapters.WebAPI.Tests/EntitySetup/Transforms/OrdersTransform.cs @@ -1,8 +1,8 @@ -using Schemio.API.Tests.EntitySetup.QueryResults; -using Schemio.Core; -using static Schemio.API.Tests.EntitySetup.Customer; +using DataFuse.Adapters.WebAPI.Tests.EntitySetup.QueryResults; +using DataFuse.Adapters.Abstraction; +using static DataFuse.Adapters.WebAPI.Tests.EntitySetup.Customer; -namespace Schemio.API.Tests.EntitySetup.ResultTransformers +namespace DataFuse.Adapters.WebAPI.Tests.EntitySetup.ResultTransformers { public class OrdersTransform : BaseTransformer, Customer> { diff --git a/tests/Schemio.API.Tests/EntitySetup/WebApis/CommunicationWebQuery.cs b/tests/DataFuse.Adapters.WebAPI.Tests/EntitySetup/WebApis/CommunicationWebQuery.cs similarity index 75% rename from tests/Schemio.API.Tests/EntitySetup/WebApis/CommunicationWebQuery.cs rename to tests/DataFuse.Adapters.WebAPI.Tests/EntitySetup/WebApis/CommunicationWebQuery.cs index 6207527..bd51c4e 100644 --- a/tests/Schemio.API.Tests/EntitySetup/WebApis/CommunicationWebQuery.cs +++ b/tests/DataFuse.Adapters.WebAPI.Tests/EntitySetup/WebApis/CommunicationWebQuery.cs @@ -1,7 +1,7 @@ -using Schemio.API.Tests.EntitySetup.QueryResults; -using Schemio.Core; +using DataFuse.Adapters.WebAPI.Tests.EntitySetup.QueryResults; +using DataFuse.Adapters.Abstraction; -namespace Schemio.API.Tests.EntitySetup.WebApis +namespace DataFuse.Adapters.WebAPI.Tests.EntitySetup.WebApis { internal class CommunicationWebQuery : WebQuery { diff --git a/tests/Schemio.API.Tests/EntitySetup/WebApis/CustomerWebQuery.cs b/tests/DataFuse.Adapters.WebAPI.Tests/EntitySetup/WebApis/CustomerWebQuery.cs similarity index 89% rename from tests/Schemio.API.Tests/EntitySetup/WebApis/CustomerWebQuery.cs rename to tests/DataFuse.Adapters.WebAPI.Tests/EntitySetup/WebApis/CustomerWebQuery.cs index 1f5d26f..b8a1f54 100644 --- a/tests/Schemio.API.Tests/EntitySetup/WebApis/CustomerWebQuery.cs +++ b/tests/DataFuse.Adapters.WebAPI.Tests/EntitySetup/WebApis/CustomerWebQuery.cs @@ -1,7 +1,7 @@ -using Schemio.API.Tests.EntitySetup.QueryResults; -using Schemio.Core; +using DataFuse.Adapters.WebAPI.Tests.EntitySetup.QueryResults; +using DataFuse.Adapters.Abstraction; -namespace Schemio.API.Tests.EntitySetup.WebApis +namespace DataFuse.Adapters.WebAPI.Tests.EntitySetup.WebApis { public class CustomerWebQuery : WebQuery { diff --git a/tests/Schemio.API.Tests/EntitySetup/WebApis/Endpoints.cs b/tests/DataFuse.Adapters.WebAPI.Tests/EntitySetup/WebApis/Endpoints.cs similarity index 90% rename from tests/Schemio.API.Tests/EntitySetup/WebApis/Endpoints.cs rename to tests/DataFuse.Adapters.WebAPI.Tests/EntitySetup/WebApis/Endpoints.cs index e59bf16..e762571 100644 --- a/tests/Schemio.API.Tests/EntitySetup/WebApis/Endpoints.cs +++ b/tests/DataFuse.Adapters.WebAPI.Tests/EntitySetup/WebApis/Endpoints.cs @@ -1,4 +1,4 @@ -namespace Schemio.API.Tests.EntitySetup.WebApis +namespace DataFuse.Adapters.WebAPI.Tests.EntitySetup.WebApis { public static class Endpoints { @@ -7,7 +7,7 @@ public static class Endpoints public const string Orders = "v2/clients/{0}/orders"; public const string OrderItems = "v2/clients/{0}/orders/items?$filter=orderId in {1}"; - public const string BaseAddress = "http://localhost:5000/"; + public static string BaseAddress = "http://localhost:5000/"; public static class Ids { diff --git a/tests/Schemio.API.Tests/EntitySetup/WebApis/OrderItemsWebQuery.cs b/tests/DataFuse.Adapters.WebAPI.Tests/EntitySetup/WebApis/OrderItemsWebQuery.cs similarity index 78% rename from tests/Schemio.API.Tests/EntitySetup/WebApis/OrderItemsWebQuery.cs rename to tests/DataFuse.Adapters.WebAPI.Tests/EntitySetup/WebApis/OrderItemsWebQuery.cs index 9d68217..a80de87 100644 --- a/tests/Schemio.API.Tests/EntitySetup/WebApis/OrderItemsWebQuery.cs +++ b/tests/DataFuse.Adapters.WebAPI.Tests/EntitySetup/WebApis/OrderItemsWebQuery.cs @@ -1,8 +1,8 @@ -using Schemio.API.Tests.EntitySetup.QueryResults; -using Schemio.Core; -using Schemio.Core.Helpers; +using DataFuse.Adapters.WebAPI.Tests.EntitySetup.QueryResults; +using DataFuse.Adapters.Abstraction; +using DataFuse.Integration.Helpers; -namespace Schemio.API.Tests.EntitySetup.WebApis +namespace DataFuse.Adapters.WebAPI.Tests.EntitySetup.WebApis { internal class OrderItemsWebQuery : WebQuery> { diff --git a/tests/Schemio.API.Tests/EntitySetup/WebApis/OrdersWebQuery.cs b/tests/DataFuse.Adapters.WebAPI.Tests/EntitySetup/WebApis/OrdersWebQuery.cs similarity index 76% rename from tests/Schemio.API.Tests/EntitySetup/WebApis/OrdersWebQuery.cs rename to tests/DataFuse.Adapters.WebAPI.Tests/EntitySetup/WebApis/OrdersWebQuery.cs index daa43b8..a97fd3f 100644 --- a/tests/Schemio.API.Tests/EntitySetup/WebApis/OrdersWebQuery.cs +++ b/tests/DataFuse.Adapters.WebAPI.Tests/EntitySetup/WebApis/OrdersWebQuery.cs @@ -1,7 +1,7 @@ -using Schemio.API.Tests.EntitySetup.QueryResults; -using Schemio.Core; +using DataFuse.Adapters.WebAPI.Tests.EntitySetup.QueryResults; +using DataFuse.Adapters.Abstraction; -namespace Schemio.API.Tests.EntitySetup.WebApis +namespace DataFuse.Adapters.WebAPI.Tests.EntitySetup.WebApis { internal class OrdersWebQuery : WebQuery> { diff --git a/tests/DataFuse.Adapters.WebAPI.Tests/GlobalUsings.cs b/tests/DataFuse.Adapters.WebAPI.Tests/GlobalUsings.cs new file mode 100644 index 0000000..ae0509d --- /dev/null +++ b/tests/DataFuse.Adapters.WebAPI.Tests/GlobalUsings.cs @@ -0,0 +1,3 @@ +global using DataFuse.Adapters.Abstraction; +global using DataFuse.Adapters.WebAPI; +global using DataFuse.Integration; diff --git a/tests/DataFuse.Integration.Tests/BaseQueryTests.cs b/tests/DataFuse.Integration.Tests/BaseQueryTests.cs new file mode 100644 index 0000000..18561fd --- /dev/null +++ b/tests/DataFuse.Integration.Tests/BaseQueryTests.cs @@ -0,0 +1,122 @@ +using Moq; + +namespace DataFuse.Integration.Tests +{ + [TestFixture] + public class BaseQueryTests + { + private TestQuery _testQuery; + private Mock _mockEngine; + + [SetUp] + public void Setup() + { + _testQuery = new TestQuery(); + _mockEngine = new Mock(); + } + + [Test] + public void Children_ShouldInitializeAsEmpty() + { + // Arrange & Act + var query = new TestQuery(); + + // Assert + Assert.That(query.Children, Is.Empty); + } + + [Test] + public void Children_CanSetAndGetValue() + { + // Arrange + var childQueries = new List { new TestChildQuery() }; + + // Act + _testQuery.Children = childQueries; + + // Assert + Assert.That(_testQuery.Children, Is.EqualTo(childQueries)); + Assert.That(_testQuery.Children.Count, Is.EqualTo(1)); + } + + [Test] + public void ResultType_ShouldReturnCorrectType() + { + // Act + var resultType = _testQuery.ResultType; + + // Assert + Assert.That(resultType, Is.EqualTo(typeof(TestQueryResult))); + } + + [Test] + public void IsContextResolved_ShouldReturnFalseInitially() + { + // Act + var isResolved = _testQuery.IsContextResolved(); + + // Assert + Assert.That(isResolved, Is.False); + } + + [Test] + public void ResolveQuery_ShouldSetContextResolvedToTrue() + { + // Arrange + var mockContext = new Mock(); + var queryResult = new Mock(); + + // Act + _testQuery.ResolveQuery(mockContext.Object, queryResult.Object); + + // Assert + Assert.That(_testQuery.IsContextResolved(), Is.True); + Assert.That(_testQuery.TestData, Is.EqualTo("resolved")); + } + + [Test] + public void ResolveQuery_WithParentResult_ShouldSetContextResolvedToTrue() + { + // Arrange + var mockContext = new Mock(); + var parentResult = new TestQueryResult { Data = "parent_data" }; + + // Act + _testQuery.ResolveQuery(mockContext.Object, parentResult); + + // Assert + Assert.That(_testQuery.IsContextResolved(), Is.True); + Assert.That(_testQuery.TestData, Is.EqualTo("resolved")); + } + + [Test] + public async Task Run_ShouldCallEngineExecute() + { + // Arrange + var expectedResult = new TestQueryResult { Data = "engine_result" }; + _mockEngine.Setup(x => x.Execute(_testQuery)) + .Returns(Task.FromResult(expectedResult)); + + // Act + var result = await _testQuery.Run(_mockEngine.Object); + + // Assert + Assert.That(result, Is.EqualTo(expectedResult)); + _mockEngine.Verify(x => x.Execute(_testQuery), Times.Once); + } + + [Test] + public async Task Run_WhenEngineReturnsNull_ShouldReturnNull() + { + // Arrange + _mockEngine.Setup(x => x.Execute(_testQuery)) + .Returns(Task.FromResult(null)); + + // Act + var result = await _testQuery.Run(_mockEngine.Object); + + // Assert + Assert.That(result, Is.Null); + } + } +} \ No newline at end of file diff --git a/tests/DataFuse.Integration.Tests/BaseTransformerTests.cs b/tests/DataFuse.Integration.Tests/BaseTransformerTests.cs new file mode 100644 index 0000000..73fa54d --- /dev/null +++ b/tests/DataFuse.Integration.Tests/BaseTransformerTests.cs @@ -0,0 +1,128 @@ +using Moq; + +namespace DataFuse.Integration.Tests +{ + [TestFixture] + public class BaseTransformerTests + { + private TestTransformer _transformer; + private Mock _mockContext; + + [SetUp] + public void Setup() + { + _transformer = new TestTransformer(); + _mockContext = new Mock(); + } + + [Test] + public void SupportedQueryResult_ShouldReturnCorrectType() + { + // Act + var supportedType = _transformer.SupportedQueryResult; + + // Assert + Assert.That(supportedType, Is.EqualTo(typeof(TestQueryResult))); + } + + [Test] + public void Context_ShouldBeNullInitially() + { + // Arrange + var transformer = new TestTransformer(); + + // Act & Assert + // Context is protected, so we can't directly access it in tests + // We'll test through SetContext method + Assert.DoesNotThrow(() => transformer.SetContext(_mockContext.Object)); + } + + [Test] + public void SetContext_ShouldSetContext() + { + // Act + _transformer.SetContext(_mockContext.Object); + + // Assert + // Since Context is protected, we can't directly verify it was set + // But the method should complete without throwing + Assert.DoesNotThrow(() => _transformer.SetContext(_mockContext.Object)); + } + + [Test] + public void SetContext_WithNull_ShouldNotThrow() + { + // Act & Assert + Assert.DoesNotThrow(() => _transformer.SetContext(null)); + } + + [Test] + public void Transform_WithIQueryResultAndIEntity_ShouldCallTypedTransform() + { + // Arrange + var queryResult = new TestQueryResult { Data = "test_data" }; + var entity = new TestEntity(); + + // Act + _transformer.Transform((IQueryResult)queryResult, (IEntity)entity); + + // Assert + Assert.That(entity.Name, Is.EqualTo("test_data")); + Assert.That(entity.Value, Is.EqualTo(9)); // "test_data".Length + } + + [Test] + public void Transform_WithTypedParameters_ShouldTransformEntity() + { + // Arrange + var queryResult = new TestQueryResult { Data = "hello_world" }; + var entity = new TestEntity(); + + // Act + _transformer.Transform(queryResult, entity); + + // Assert + Assert.That(entity.Name, Is.EqualTo("hello_world")); + Assert.That(entity.Value, Is.EqualTo(11)); // "hello_world".Length + } + + [Test] + public void Transform_WithNullData_ShouldHandleGracefully() + { + // Arrange + var queryResult = new TestQueryResult { Data = null }; + var entity = new TestEntity(); + + // Act + _transformer.Transform(queryResult, entity); + + // Assert + Assert.That(entity.Name, Is.Null); + Assert.That(entity.Value, Is.EqualTo(0)); + } + + [Test] + public void Transform_WithWrongQueryResultType_ShouldThrowInvalidCastException() + { + // Arrange + var wrongQueryResult = new TestChildQueryResult { ChildData = "wrong_type" }; + var entity = new TestEntity(); + + // Act & Assert + Assert.Throws(() => + _transformer.Transform((IQueryResult)wrongQueryResult, (IEntity)entity)); + } + + [Test] + public void Transform_WithWrongEntityType_ShouldThrowInvalidCastException() + { + // Arrange + var queryResult = new TestQueryResult { Data = "test_data" }; + var wrongEntity = new TestChildEntity { Description = "wrong_type" }; + + // Act & Assert + Assert.Throws(() => + _transformer.Transform((IQueryResult)queryResult, (IEntity)wrongEntity)); + } + } +} \ No newline at end of file diff --git a/tests/DataFuse.Integration.Tests/CacheResultTests.cs b/tests/DataFuse.Integration.Tests/CacheResultTests.cs new file mode 100644 index 0000000..c5d986f --- /dev/null +++ b/tests/DataFuse.Integration.Tests/CacheResultTests.cs @@ -0,0 +1,281 @@ +namespace DataFuse.Integration.Tests +{ + [TestFixture] + public class CacheResultAttributeTests + { + [Test] + public void Constructor_ShouldCreateInstance() + { + // Act + var attribute = new CacheResultAttribute(); + + // Assert + Assert.That(attribute, Is.Not.Null); + Assert.That(attribute, Is.InstanceOf()); + } + + [Test] + public void Attribute_CanBeAppliedToClass() + { + // Act + var type = typeof(CacheableQueryResult); + var attributes = type.GetCustomAttributes(typeof(CacheResultAttribute), false); + + // Assert + Assert.That(attributes, Is.Not.Null); + Assert.That(attributes.Length, Is.EqualTo(1)); + Assert.That(attributes[0], Is.InstanceOf()); + } + + [Test] + public void Attribute_CanBeDetectedOnType() + { + // Act + var type = typeof(CacheableQueryResult); + var hasCacheAttribute = type.GetCustomAttributes(typeof(CacheResultAttribute), false).Length > 0; + + // Assert + Assert.That(hasCacheAttribute, Is.True); + } + + [Test] + public void Attribute_OnNonCacheableType_ShouldNotBePresent() + { + // Act + var type = typeof(NonCacheableQueryResult); + var hasCacheAttribute = type.GetCustomAttributes(typeof(CacheResultAttribute), false).Length > 0; + + // Assert + Assert.That(hasCacheAttribute, Is.False); + } + + [Test] + public void Attribute_InheritanceTest_ShouldNotInheritByDefault() + { + // Act + var baseType = typeof(CacheableQueryResult); + var derivedType = typeof(DerivedQueryResult); + + var baseHasAttribute = baseType.GetCustomAttributes(typeof(CacheResultAttribute), false).Length > 0; + var derivedHasAttribute = derivedType.GetCustomAttributes(typeof(CacheResultAttribute), false).Length > 0; + var derivedInheritsAttribute = derivedType.GetCustomAttributes(typeof(CacheResultAttribute), true).Length > 0; + + // Assert + Assert.That(baseHasAttribute, Is.True); + Assert.That(derivedHasAttribute, Is.False); // Should not have its own attribute + Assert.That(derivedInheritsAttribute, Is.True); // But should inherit from base when inherit = true + } + + [Test] + public void MultipleAttributes_ShouldNotBeAllowed() + { + // This test verifies that you can't apply the same attribute multiple times + // by checking a type that attempts to do so would cause a compilation error + // Since CacheResultAttribute doesn't specify AllowMultiple = true, + // multiple applications should not be allowed + + // Act + var type = typeof(CacheableQueryResult); + var attributes = type.GetCustomAttributes(typeof(CacheResultAttribute), false); + + // Assert + Assert.That(attributes.Length, Is.EqualTo(1)); // Should only be one instance + } + + [Test] + public void AttributeUsage_ShouldFollowDefaultBehavior() + { + // Act + var attributeType = typeof(CacheResultAttribute); + var usageAttributes = attributeType.GetCustomAttributes(typeof(AttributeUsageAttribute), false); + + // Assert + // Since CacheResultAttribute doesn't specify AttributeUsage, it uses the default + // which allows application to any target and doesn't allow multiple uses + if (usageAttributes.Length > 0) + { + var usage = (AttributeUsageAttribute)usageAttributes[0]; + Assert.That(usage.AllowMultiple, Is.False); + } + } + + // Test classes + [CacheResult] + public class CacheableQueryResult : IQueryResult + { + public string Data { get; set; } + } + + public class NonCacheableQueryResult : IQueryResult + { + public string Data { get; set; } + } + + public class DerivedQueryResult : CacheableQueryResult + { + public string AdditionalData { get; set; } + } + } + + [TestFixture] + public class QueryComparerTests + { + private QueryComparer _comparer; + + [SetUp] + public void Setup() + { + _comparer = new QueryComparer(); + } + + [Test] + public void Equals_WithSameQueryType_ShouldReturnTrue() + { + // Arrange + var query1 = new TestQuery(); + var query2 = new TestQuery(); + + // Act + var result = _comparer.Equals(query1, query2); + + // Assert + Assert.That(result, Is.True); + } + + [Test] + public void Equals_WithDifferentQueryTypes_ShouldReturnFalse() + { + // Arrange + var query1 = new TestQuery(); + var query2 = new TestChildQuery(); + + // Act + var result = _comparer.Equals(query1, query2); + + // Assert + Assert.That(result, Is.False); + } + + [Test] + public void Equals_WithSameInstance_ShouldReturnTrue() + { + // Arrange + var query = new TestQuery(); + + // Act + var result = _comparer.Equals(query, query); + + // Assert + Assert.That(result, Is.True); + } + + [Test] + public void Equals_WithNullQueries_ShouldHandleCorrectly() + { + // Act + var result1 = _comparer.Equals(null, null); + var result2 = _comparer.Equals(new TestQuery(), null); + var result3 = _comparer.Equals(null, new TestQuery()); + + // Assert + Assert.That(result1, Is.True); // Both null should be equal + Assert.That(result2, Is.False); // One null should not equal non-null + Assert.That(result3, Is.False); // One null should not equal non-null + } + + [Test] + public void GetHashCode_WithSameQueryType_ShouldReturnSameHashCode() + { + // Arrange + var query1 = new TestQuery(); + var query2 = new TestQuery(); + + // Act + var hash1 = _comparer.GetHashCode(query1); + var hash2 = _comparer.GetHashCode(query2); + + // Assert + Assert.That(hash1, Is.EqualTo(hash2)); + } + + [Test] + public void GetHashCode_WithDifferentQueryTypes_ShouldReturnDifferentHashCodes() + { + // Arrange + var query1 = new TestQuery(); + var query2 = new TestChildQuery(); + + // Act + var hash1 = _comparer.GetHashCode(query1); + var hash2 = _comparer.GetHashCode(query2); + + // Assert + Assert.That(hash1, Is.Not.EqualTo(hash2)); + } + + [Test] + public void GetHashCode_WithNullQuery_ShouldNotThrow() + { + // Act & Assert + Assert.Throws(() => _comparer.GetHashCode(null)); + } + + [Test] + public void Comparer_ShouldBeUsableInHashSet() + { + // Arrange + var queries = new System.Collections.Generic.HashSet(_comparer) + { + new TestQuery(), + new TestQuery(), // Duplicate type + new TestChildQuery() + }; + + // Act & Assert + Assert.That(queries.Count, Is.EqualTo(2)); // Should only contain unique types + } + + [Test] + public void Comparer_ShouldBeUsableInLinqDistinct() + { + // Arrange + var queries = new System.Collections.Generic.List + { + new TestQuery(), + new TestQuery(), + new TestChildQuery(), + new TestQuery(), + new TestChildQuery() + }; + + // Act + var distinctQueries = queries.Distinct(_comparer).ToList(); + + // Assert + Assert.That(distinctQueries.Count, Is.EqualTo(2)); // Should only contain unique types + } + + [Test] + public void Comparer_WithInheritedTypes_ShouldTreatAsDifferent() + { + // Arrange + var baseQuery = new TestQuery(); + var derivedQuery = new DerivedTestQuery(); + + // Act + var areEqual = _comparer.Equals(baseQuery, derivedQuery); + var baseHash = _comparer.GetHashCode(baseQuery); + var derivedHash = _comparer.GetHashCode(derivedQuery); + + // Assert + Assert.That(areEqual, Is.False); + Assert.That(baseHash, Is.Not.EqualTo(derivedHash)); + } + + // Test class for inheritance testing + public class DerivedTestQuery : TestQuery + { + public string AdditionalProperty { get; set; } + } + } +} \ No newline at end of file diff --git a/tests/DataFuse.Integration.Tests/CollectionResultTests.cs b/tests/DataFuse.Integration.Tests/CollectionResultTests.cs new file mode 100644 index 0000000..6633751 --- /dev/null +++ b/tests/DataFuse.Integration.Tests/CollectionResultTests.cs @@ -0,0 +1,213 @@ +namespace DataFuse.Integration.Tests +{ + [TestFixture] + public class CollectionResultTests + { + [Test] + public void Constructor_WithEmptyEnumerable_ShouldCreateEmptyCollection() + { + // Arrange + var items = new List(); + + // Act + var collection = new CollectionResult(items); + + // Assert + Assert.That(collection, Is.Not.Null); + Assert.That(collection.Count, Is.EqualTo(0)); + Assert.That(collection, Is.InstanceOf()); + } + + [Test] + public void Constructor_WithEnumerable_ShouldCreateCollectionWithItems() + { + // Arrange + var items = new List { "item1", "item2", "item3" }; + + // Act + var collection = new CollectionResult(items); + + // Assert + Assert.That(collection.Count, Is.EqualTo(3)); + Assert.That(collection[0], Is.EqualTo("item1")); + Assert.That(collection[1], Is.EqualTo("item2")); + Assert.That(collection[2], Is.EqualTo("item3")); + } + + [Test] + public void Constructor_WithNull_ShouldThrowArgumentNullException() + { + // Act & Assert + Assert.Throws(() => + new CollectionResult(null)); + } + + [Test] + public void DefaultConstructor_ShouldCreateEmptyCollection() + { + // Act + var collection = new CollectionResult(); + + // Assert + Assert.That(collection, Is.Not.Null); + Assert.That(collection.Count, Is.EqualTo(0)); + Assert.That(collection, Is.InstanceOf()); + } + + [Test] + public void Add_ShouldAddItemToCollection() + { + // Arrange + var collection = new CollectionResult(); + + // Act + collection.Add("new_item"); + + // Assert + Assert.That(collection.Count, Is.EqualTo(1)); + Assert.That(collection[0], Is.EqualTo("new_item")); + } + + [Test] + public void AddRange_ShouldAddMultipleItemsToCollection() + { + // Arrange + var collection = new CollectionResult(); + var newItems = new List { "item1", "item2" }; + + // Act + collection.AddRange(newItems); + + // Assert + Assert.That(collection.Count, Is.EqualTo(2)); + Assert.That(collection[0], Is.EqualTo("item1")); + Assert.That(collection[1], Is.EqualTo("item2")); + } + + [Test] + public void Remove_ShouldRemoveItemFromCollection() + { + // Arrange + var items = new List { "item1", "item2", "item3" }; + var collection = new CollectionResult(items); + + // Act + var removed = collection.Remove("item2"); + + // Assert + Assert.That(removed, Is.True); + Assert.That(collection.Count, Is.EqualTo(2)); + Assert.That(collection.Contains("item2"), Is.False); + Assert.That(collection[0], Is.EqualTo("item1")); + Assert.That(collection[1], Is.EqualTo("item3")); + } + + [Test] + public void Clear_ShouldRemoveAllItems() + { + // Arrange + var items = new List { "item1", "item2", "item3" }; + var collection = new CollectionResult(items); + + // Act + collection.Clear(); + + // Assert + Assert.That(collection.Count, Is.EqualTo(0)); + } + + [Test] + public void Contains_ShouldReturnTrueForExistingItem() + { + // Arrange + var items = new List { "item1", "item2", "item3" }; + var collection = new CollectionResult(items); + + // Act & Assert + Assert.That(collection.Contains("item2"), Is.True); + Assert.That(collection.Contains("item4"), Is.False); + } + + [Test] + public void IndexOf_ShouldReturnCorrectIndex() + { + // Arrange + var items = new List { "item1", "item2", "item3" }; + var collection = new CollectionResult(items); + + // Act & Assert + Assert.That(collection.IndexOf("item2"), Is.EqualTo(1)); + Assert.That(collection.IndexOf("item4"), Is.EqualTo(-1)); + } + + [Test] + public void Insert_ShouldInsertItemAtSpecifiedIndex() + { + // Arrange + var items = new List { "item1", "item3" }; + var collection = new CollectionResult(items); + + // Act + collection.Insert(1, "item2"); + + // Assert + Assert.That(collection.Count, Is.EqualTo(3)); + Assert.That(collection[0], Is.EqualTo("item1")); + Assert.That(collection[1], Is.EqualTo("item2")); + Assert.That(collection[2], Is.EqualTo("item3")); + } + + [Test] + public void RemoveAt_ShouldRemoveItemAtSpecifiedIndex() + { + // Arrange + var items = new List { "item1", "item2", "item3" }; + var collection = new CollectionResult(items); + + // Act + collection.RemoveAt(1); + + // Assert + Assert.That(collection.Count, Is.EqualTo(2)); + Assert.That(collection[0], Is.EqualTo("item1")); + Assert.That(collection[1], Is.EqualTo("item3")); + } + + [Test] + public void Enumeration_ShouldWorkCorrectly() + { + // Arrange + var items = new List { "item1", "item2", "item3" }; + var collection = new CollectionResult(items); + + // Act + var enumeratedItems = collection.ToList(); + + // Assert + Assert.That(enumeratedItems.Count, Is.EqualTo(3)); + Assert.That(enumeratedItems, Is.EqualTo(items)); + } + + [Test] + public void GenericType_ShouldWorkWithDifferentTypes() + { + // Arrange + var numbers = new List { 1, 2, 3 }; + var entities = new List + { + new TestEntity { Name = "entity1", Value = 1 }, + new TestEntity { Name = "entity2", Value = 2 } + }; + + // Act + var numberCollection = new CollectionResult(numbers); + var entityCollection = new CollectionResult(entities); + + // Assert + Assert.That(numberCollection.Count, Is.EqualTo(3)); + Assert.That(numberCollection[0], Is.EqualTo(1)); + Assert.That(entityCollection.Count, Is.EqualTo(2)); + Assert.That(entityCollection[0].Name, Is.EqualTo("entity1")); + } + } +} \ No newline at end of file diff --git a/tests/DataFuse.Integration.Tests/CreateSchemaTests.cs b/tests/DataFuse.Integration.Tests/CreateSchemaTests.cs new file mode 100644 index 0000000..d951569 --- /dev/null +++ b/tests/DataFuse.Integration.Tests/CreateSchemaTests.cs @@ -0,0 +1,217 @@ +namespace DataFuse.Integration.Tests +{ + [TestFixture] + public class CreateSchemaTests + { + [Test] + public void For_ShouldReturnMappingsWithOrderOne() + { + // Act + var mappings = CreateSchema.For(); + + // Assert + Assert.That(mappings, Is.Not.Null); + Assert.That(mappings.Order, Is.EqualTo(1)); + Assert.That(mappings, Is.InstanceOf>()); + } + + [Test] + public void Paths_ShouldReturnSchemaPathsWithCorrectPaths() + { + // Arrange + var paths = new[] { "/root/test", "/root/child" }; + + // Act + var schemaPaths = For.Paths(paths); + + // Assert + Assert.That(schemaPaths, Is.Not.Null); + Assert.That(schemaPaths.Paths, Is.EqualTo(paths)); + Assert.That(schemaPaths, Is.InstanceOf()); + } + + [Test] + public void Paths_WithEmptyArray_ShouldReturnEmptyPaths() + { + // Arrange + var paths = new string[0]; + + // Act + var schemaPaths = For.Paths(paths); + + // Assert + Assert.That(schemaPaths, Is.Not.Null); + Assert.That(schemaPaths.Paths, Is.EqualTo(paths)); + Assert.That(schemaPaths.Paths.Length, Is.EqualTo(0)); + } + + [Test] + public void Paths_WithNullArray_ShouldReturnNullPaths() + { + // Act + var schemaPaths = For.Paths(null); + + // Assert + Assert.That(schemaPaths, Is.Not.Null); + Assert.That(schemaPaths.Paths, Is.Null); + } + + [Test] + public void Paths_WithSinglePath_ShouldReturnSinglePath() + { + // Arrange + var path = "/root/single"; + + // Act + var schemaPaths = For.Paths(path); + + // Assert + Assert.That(schemaPaths, Is.Not.Null); + Assert.That(schemaPaths.Paths, Is.Not.Null); + Assert.That(schemaPaths.Paths.Length, Is.EqualTo(1)); + Assert.That(schemaPaths.Paths[0], Is.EqualTo(path)); + } + + [Test] + public void Map_WithoutDependents_ShouldCreateMappingCorrectly() + { + // Arrange + var paths = For.Paths("/root/test"); + + // Act + var result = CreateSchema.For() + .Map(paths); + + // Assert + Assert.That(result, Is.Not.Null); + Assert.That(result, Is.InstanceOf>()); + + var mappings = result.End().ToList(); + Assert.That(mappings.Count, Is.EqualTo(1)); + Assert.That(mappings[0].Query, Is.InstanceOf()); + Assert.That(mappings[0].Transformer, Is.InstanceOf()); + Assert.That(mappings[0].SchemaPaths, Is.EqualTo(paths)); + Assert.That(mappings[0].Order, Is.EqualTo(1)); + } + + [Test] + public void Map_WithDependents_ShouldCreateMappingsWithDependencies() + { + // Arrange + var parentPaths = For.Paths("/root/test"); + var childPaths = For.Paths("/root/test/child"); + + // Act + var result = CreateSchema.For() + .Map(parentPaths, deps => + deps.Dependents + .Map(childPaths)); + + // Assert + var mappings = result.End().ToList(); + Assert.That(mappings.Count, Is.EqualTo(2)); + + var parentMapping = mappings.FirstOrDefault(m => m.Query is TestQuery); + var childMapping = mappings.FirstOrDefault(m => m.Query is TestChildQuery); + + Assert.That(parentMapping, Is.Not.Null); + Assert.That(childMapping, Is.Not.Null); + Assert.That(childMapping.DependentOn, Is.EqualTo(parentMapping.Query)); + Assert.That(childMapping.Order, Is.EqualTo(2)); // Parent order + 1 + } + + [Test] + public void Map_MultipleRootMappings_ShouldCreateMultipleMappings() + { + // Arrange + var paths1 = For.Paths("/root/test1"); + var paths2 = For.Paths("/root/test2"); + + // Act + var result = CreateSchema.For() + .Map(paths1) + .Map(paths2); + + // Assert + var mappings = result.End().ToList(); + Assert.That(mappings.Count, Is.EqualTo(2)); + + var mapping1 = mappings.FirstOrDefault(m => m.Query is TestQuery); + var mapping2 = mappings.FirstOrDefault(m => m.Query is TestChildQuery); + + Assert.That(mapping1, Is.Not.Null); + Assert.That(mapping2, Is.Not.Null); + Assert.That(mapping1.DependentOn, Is.Null); + Assert.That(mapping2.DependentOn, Is.Null); + Assert.That(mapping1.Order, Is.EqualTo(1)); + Assert.That(mapping2.Order, Is.EqualTo(1)); + } + + [Test] + public void GetMappings_ShouldReturnSelfReference() + { + // Arrange + var mappings = CreateSchema.For(); + + // Act + var getMappings = mappings.GetMappings; + + // Assert + Assert.That(getMappings, Is.SameAs(mappings)); + } + + [Test] + public void Order_ShouldBeSettableAndGettable() + { + // Arrange + var mappings = CreateSchema.For(); + + // Act + mappings.Order = 5; + + // Assert + Assert.That(mappings.Order, Is.EqualTo(5)); + } + + [Test] + public void Dependents_ShouldReturnNewMappingsWithIncrementedOrder() + { + // Arrange + var mapping = new Mapping + { + Order = 3 + }; + + // Act + var dependents = mapping.Dependents; + + // Assert + Assert.That(dependents, Is.Not.Null); + Assert.That(dependents.Order, Is.EqualTo(4)); // Original order + 1 + } + + [Test] + public void ComplexNestedMapping_ShouldCreateCorrectHierarchy() + { + // Act + var result = CreateSchema.For() + .Map(For.Paths("/root"), deps => + deps.Dependents + .Map(For.Paths("/root/child1")) + .Map(For.Paths("/root/child2"), childDeps => + childDeps.Dependents + .Map(For.Paths("/root/child2/grandchild")) + ) + ); + + // Assert + var mappings = result.End().ToList(); + Assert.That(mappings.Count, Is.EqualTo(4)); + + var orders = mappings.Select(m => m.Order).Distinct().ToList(); + Assert.That(orders, Contains.Item(1)); // Root level + Assert.That(orders, Contains.Item(2)); // First level dependents + Assert.That(orders, Contains.Item(3)); // Second level dependents + } + } +} \ No newline at end of file diff --git a/tests/DataFuse.Integration.Tests/DataContextTests.cs b/tests/DataFuse.Integration.Tests/DataContextTests.cs new file mode 100644 index 0000000..3f8901c --- /dev/null +++ b/tests/DataFuse.Integration.Tests/DataContextTests.cs @@ -0,0 +1,162 @@ +using Moq; + +namespace DataFuse.Integration.Tests +{ + [TestFixture] + public class DataContextTests + { + private Mock _mockRequest; + + [SetUp] + public void Setup() + { + _mockRequest = new Mock(); + } + + [Test] + public void Constructor_ShouldInitializePropertiesCorrectly() + { + // Act + var dataContext = new DataContext(_mockRequest.Object); + + // Assert + Assert.That(dataContext.Request, Is.EqualTo(_mockRequest.Object)); + Assert.That(dataContext.Cache, Is.Not.Null); + Assert.That(dataContext.Cache, Is.InstanceOf>()); + Assert.That(dataContext.Cache.Count, Is.EqualTo(0)); + } + + [Test] + public void Constructor_WithNullRequest_ShouldAllowNullRequest() + { + // Act & Assert + Assert.DoesNotThrow(() => new DataContext(null)); + } + + [Test] + public void Request_ShouldReturnSetRequest() + { + // Arrange + var dataContext = new DataContext(_mockRequest.Object); + + // Act + var request = dataContext.Request; + + // Assert + Assert.That(request, Is.SameAs(_mockRequest.Object)); + } + + [Test] + public void Cache_ShouldBeSettableAndGettable() + { + // Arrange + var dataContext = new DataContext(_mockRequest.Object); + var newCache = new Dictionary { { "key1", "value1" } }; + + // Act + dataContext.Cache = newCache; + + // Assert + Assert.That(dataContext.Cache, Is.SameAs(newCache)); + Assert.That(dataContext.Cache["key1"], Is.EqualTo("value1")); + } + + [Test] + public void Cache_CanAddItems() + { + // Arrange + var dataContext = new DataContext(_mockRequest.Object); + + // Act + dataContext.Cache.Add("test_key", "test_value"); + dataContext.Cache["another_key"] = 42; + + // Assert + Assert.That(dataContext.Cache.Count, Is.EqualTo(2)); + Assert.That(dataContext.Cache["test_key"], Is.EqualTo("test_value")); + Assert.That(dataContext.Cache["another_key"], Is.EqualTo(42)); + } + + [Test] + public void Cache_CanRemoveItems() + { + // Arrange + var dataContext = new DataContext(_mockRequest.Object); + dataContext.Cache.Add("key_to_remove", "value"); + + // Act + var removed = dataContext.Cache.Remove("key_to_remove"); + + // Assert + Assert.That(removed, Is.True); + Assert.That(dataContext.Cache.Count, Is.EqualTo(0)); + Assert.That(dataContext.Cache.ContainsKey("key_to_remove"), Is.False); + } + + [Test] + public void Cache_CanCheckContainsKey() + { + // Arrange + var dataContext = new DataContext(_mockRequest.Object); + dataContext.Cache.Add("existing_key", "value"); + + // Act & Assert + Assert.That(dataContext.Cache.ContainsKey("existing_key"), Is.True); + Assert.That(dataContext.Cache.ContainsKey("non_existing_key"), Is.False); + } + + [Test] + public void Cache_CanClear() + { + // Arrange + var dataContext = new DataContext(_mockRequest.Object); + dataContext.Cache.Add("key1", "value1"); + dataContext.Cache.Add("key2", "value2"); + + // Act + dataContext.Cache.Clear(); + + // Assert + Assert.That(dataContext.Cache.Count, Is.EqualTo(0)); + } + + [Test] + public void Cache_ShouldImplementIEntityContextCache() + { + // Arrange + var dataContext = new DataContext(_mockRequest.Object); + + // Act & Assert + Assert.That(dataContext, Is.InstanceOf()); + } + + [Test] + public void DataContext_ShouldImplementIDataContext() + { + // Arrange + var dataContext = new DataContext(_mockRequest.Object); + + // Act & Assert + Assert.That(dataContext, Is.InstanceOf()); + } + + [Test] + public void Cache_WithComplexObjects_ShouldStoreAndRetrieveCorrectly() + { + // Arrange + var dataContext = new DataContext(_mockRequest.Object); + var complexObject = new TestEntity { Name = "test", Value = 123 }; + var queryResult = new TestQueryResult { Data = "query_data" }; + + // Act + dataContext.Cache["entity"] = complexObject; + dataContext.Cache["result"] = queryResult; + + // Assert + Assert.That(dataContext.Cache["entity"], Is.SameAs(complexObject)); + Assert.That(dataContext.Cache["result"], Is.SameAs(queryResult)); + Assert.That(((TestEntity)dataContext.Cache["entity"]).Name, Is.EqualTo("test")); + Assert.That(((TestQueryResult)dataContext.Cache["result"]).Data, Is.EqualTo("query_data")); + } + } +} \ No newline at end of file diff --git a/tests/Schemio.Core.Tests/Schemio.Core.Tests.csproj b/tests/DataFuse.Integration.Tests/DataFuse.Integration.Tests.csproj similarity index 58% rename from tests/Schemio.Core.Tests/Schemio.Core.Tests.csproj rename to tests/DataFuse.Integration.Tests/DataFuse.Integration.Tests.csproj index efd5bc7..48547eb 100644 --- a/tests/Schemio.Core.Tests/Schemio.Core.Tests.csproj +++ b/tests/DataFuse.Integration.Tests/DataFuse.Integration.Tests.csproj @@ -1,7 +1,7 @@ - net9.0 + net8.0;net9.0;net10.0 enable enable @@ -9,22 +9,23 @@ - + - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + + diff --git a/tests/DataFuse.Integration.Tests/DataProvider.Tests/DataProviderTests.cs b/tests/DataFuse.Integration.Tests/DataProvider.Tests/DataProviderTests.cs new file mode 100644 index 0000000..4fa843e --- /dev/null +++ b/tests/DataFuse.Integration.Tests/DataProvider.Tests/DataProviderTests.cs @@ -0,0 +1,41 @@ +using Microsoft.Extensions.Logging; +using DataFuse.Integration; +using Moq; +using DataFuse.Integration.Impl; +using DataFuse.Integration.Tests.EntitySetup; +using DataFuse.Integration.Tests.EntitySetup.Entities; + +namespace DataFuse.Integration.Tests.DataProvider.Tests; + +[TestFixture] +internal class DataProviderTests +{ + private DataProvider _provider; + private Mock>> _logger; + private Mock> _queryBuilder; + private Mock _queryExecutor; + private Mock> _entityBuilder; + + [SetUp] + public void Setup() + { + _logger = new Mock>>(); + _queryBuilder = new Mock>(); + _queryExecutor = new Mock(); + _entityBuilder = new Mock>(); + + _provider = new DataProvider(_logger.Object, _queryBuilder.Object, _queryExecutor.Object, _entityBuilder.Object); + } + + [Test] + public async Task TestDataProvider() + { + var context = new CustomerContext { CustomerId = 1 }; + + await _provider.GetDataAsync(context); + + _queryBuilder.Verify(x => x.Build(It.IsAny()), Times.Once); + _queryExecutor.Verify(x => x.ExecuteAsync(It.IsAny(), It.IsAny()), Times.Once); + _entityBuilder.Verify(x => x.Build(It.IsAny(), It.IsAny>()), Times.Once); + } +} diff --git a/tests/DataFuse.Integration.Tests/DataProvider.Tests/EntityBuilderTests.cs b/tests/DataFuse.Integration.Tests/DataProvider.Tests/EntityBuilderTests.cs new file mode 100644 index 0000000..c7df663 --- /dev/null +++ b/tests/DataFuse.Integration.Tests/DataProvider.Tests/EntityBuilderTests.cs @@ -0,0 +1,161 @@ +using DataFuse.Integration.Impl; +using DataFuse.Integration; +using DataFuse.Integration.Tests.EntitySetup.Configuration.Queries; +using DataFuse.Integration.Tests.EntitySetup.Entities; + +namespace DataFuse.Integration.Tests.DataProvider.Tests +{ + [TestFixture] + internal class EntityBuilderTests + { + private EntityBuilder _entityBuilder; + private IEntityConfiguration _entitySchema; + + public class Invocations + { + public Invocations(Type result, int invocationCount, int preInvocationCount, int postInvocationCount) + { + Result = result; + InvocationCount = invocationCount; + PreInvocationCount = preInvocationCount; + PostInvocationCount = postInvocationCount; + } + + public Type Result { get; set; } + public int InvocationCount { get; set; } + public int PreInvocationCount { get; set; } + public int PostInvocationCount { get; set; } + } + + private static List TransformerInvocations; + + [SetUp] + public void Setup() + { + _entitySchema = new MockCustomerSchema(); + _entityBuilder = new EntityBuilder(_entitySchema); + TransformerInvocations = new List(); + } + + [Test] + public void TestTransformExecutorForCorrectExecutionOfConfiguredTransforms() + { + var queryList = new List + { + new CustomerRecord{Id = 123, CustomerCode= "ABC", CustomerName="Ninja Labs"}, + new CommunicationRecord{Id = 123, Email = "ninja@labs.com", Telephone = "0212345689"}, + new CollectionResult(), + new CollectionResult() + }; + + var entity = _entityBuilder.Build(new DataContext(new EntityContext()), queryList); + + var customerTransforms = TransformerInvocations.Where(x => x.Result == typeof(CustomerRecord)); + Assert.That(customerTransforms.Count() == 1); + Assert.That(customerTransforms.ElementAt(0).PreInvocationCount == 1); + Assert.That(customerTransforms.ElementAt(0).InvocationCount == 1); + Assert.That(customerTransforms.ElementAt(0).PostInvocationCount == 1); + + // Assert that the communication transform is invoked twice due to the repeat in PostTransform + var communicationTransforms = TransformerInvocations.Where(x => x.Result == typeof(CommunicationRecord)); + Assert.That(communicationTransforms.Count() == 1); + Assert.That(communicationTransforms.ElementAt(0).PreInvocationCount == 2); + Assert.That(communicationTransforms.ElementAt(0).InvocationCount == 2); + Assert.That(communicationTransforms.ElementAt(0).PostInvocationCount == 2); + + var orderCollectionTransforms = TransformerInvocations.Where(x => x.Result == typeof(CollectionResult)); + Assert.That(orderCollectionTransforms.Count() == 1); + Assert.That(orderCollectionTransforms.ElementAt(0).PreInvocationCount == 1); + Assert.That(orderCollectionTransforms.ElementAt(0).InvocationCount == 1); + Assert.That(orderCollectionTransforms.ElementAt(0).PostInvocationCount == 1); + + // Assert that the order items transform is not invoked due to cancellation in PreTransform + var orderItemsCollectionTransforms = TransformerInvocations.Where(x => x.Result == typeof(CollectionResult)); + Assert.That(orderItemsCollectionTransforms.Count() == 1); + Assert.That(orderItemsCollectionTransforms.ElementAt(0).PreInvocationCount == 1); + Assert.That(orderItemsCollectionTransforms.ElementAt(0).InvocationCount == 0); + Assert.That(orderItemsCollectionTransforms.ElementAt(0).PostInvocationCount == 0); + + Assert.That(entity, Is.Not.Null); + } + + public class MockTransform : BaseTransformer + where TEntity : IEntity + where TQueryResult : IQueryResult + { + public override void Transform(TQueryResult queryResult, TEntity entity) + { + var invocations = TransformerInvocations.Where(x => x.Result == queryResult.GetType()); + + if (!invocations.Any()) + return; + + var transformInvocations = invocations + .Select(x => new Invocations(x.Result, x.InvocationCount + 1, x.PreInvocationCount, x.PostInvocationCount)).ToList(); + + TransformerInvocations.RemoveAll(x => x.Result == queryResult.GetType()); + TransformerInvocations.AddRange(transformInvocations); + } + + public override void PreTransform(PreTransformContext context) + { + var invocation = TransformerInvocations.FirstOrDefault(x => x.Result == context.QueryResult.GetType()); + + if (invocation != null) + { + TransformerInvocations.Remove(invocation); + TransformerInvocations.Add(new Invocations(invocation.Result, invocation.InvocationCount, invocation.PreInvocationCount + 1, invocation.PostInvocationCount)); + } + else + { + TransformerInvocations.Add(new Invocations(context.QueryResult.GetType(), 0, 1, 0)); + } + + if (context.QueryResult is CollectionResult orderItems) + { + context.Cancel(); + } + } + + public override void PostTransform(PostTransformContext context) + { + var invocations = TransformerInvocations.Where(x => x.Result == context.QueryResult.GetType()); + + if (!invocations.Any()) + return; + + var transformInvocations = invocations + .Select(x => new Invocations(x.Result, x.InvocationCount, x.PreInvocationCount, x.PostInvocationCount + 1)).ToList(); + + TransformerInvocations.RemoveAll(x => x.Result == context.QueryResult.GetType()); + TransformerInvocations.AddRange(transformInvocations); + + if (context.QueryResult is CommunicationRecord communication) + { + context.Repeat(); + } + } + } + + internal class MockCustomerSchema : EntityConfiguration + { + public override IEnumerable> GetSchema() + { + return CreateSchema.For() + .Map>(For.Paths("customer/id", "customer/customercode", "customer/customername"), + customer => customer.Dependents + .Map>(For.Paths("customer/communication")) + .Map, Customer>>(For.Paths("customer/orders"), + customerOrders => customerOrders.Dependents + .Map, Customer>>(For.Paths("customer/orders/order/items"))) + ).End(); + } + } + } + + internal class EntityContext : IEntityRequest + { + public int CustomerId { get; set; } + public string[] SchemaPaths { get; set; } + } +} \ No newline at end of file diff --git a/tests/Schemio.Core.Tests/DataProvider.Tests/QueryBuilderTests.cs b/tests/DataFuse.Integration.Tests/DataProvider.Tests/QueryBuilderTests.cs similarity index 93% rename from tests/Schemio.Core.Tests/DataProvider.Tests/QueryBuilderTests.cs rename to tests/DataFuse.Integration.Tests/DataProvider.Tests/QueryBuilderTests.cs index 5f23466..851fc45 100644 --- a/tests/Schemio.Core.Tests/DataProvider.Tests/QueryBuilderTests.cs +++ b/tests/DataFuse.Integration.Tests/DataProvider.Tests/QueryBuilderTests.cs @@ -1,11 +1,12 @@ -using Schemio.Core.Impl; -using Schemio.Core.PathMatchers; -using Schemio.Core.Tests.EntitySetup; -using Schemio.Core.Tests.EntitySetup.Entities; -using Schemio.Core.Tests.EntitySetup.Configuration; -using Schemio.Core.Tests.EntitySetup.Configuration.Queries; - -namespace Schemio.Core.Tests.DataProvider.Tests +using DataFuse.Integration.Impl; +using DataFuse.Integration; +using DataFuse.Integration.PathMatchers; +using DataFuse.Integration.Tests.EntitySetup; +using DataFuse.Integration.Tests.EntitySetup.Entities; +using DataFuse.Integration.Tests.EntitySetup.Configuration; +using DataFuse.Integration.Tests.EntitySetup.Configuration.Queries; + +namespace DataFuse.Integration.Tests.DataProvider.Tests { [TestFixture] internal class QueryBuilderTests diff --git a/tests/DataFuse.Integration.Tests/DataProvider.Tests/QueryExecutorTests.cs b/tests/DataFuse.Integration.Tests/DataProvider.Tests/QueryExecutorTests.cs new file mode 100644 index 0000000..b23b449 --- /dev/null +++ b/tests/DataFuse.Integration.Tests/DataProvider.Tests/QueryExecutorTests.cs @@ -0,0 +1,53 @@ +using Moq; +using DataFuse.Integration; +using DataFuse.Integration.Impl; +using DataFuse.Integration.PathMatchers; +using DataFuse.Integration.Tests.EntitySetup; +using DataFuse.Integration.Tests.EntitySetup.Entities; +using DataFuse.Integration.Tests.EntitySetup.Configuration; +using DataFuse.Integration.Tests.EntitySetup.Configuration.Queries; + +namespace DataFuse.Integration.Tests.DataProvider.Tests; + +[TestFixture] +internal class QueryExecutorTests +{ + private QueryExecutor _queryExecutor; + private Mock _queryEngine; + + [SetUp] + public void Setup() + { + _queryEngine = new Mock(); + _queryEngine.Setup(x => x.CanExecute(It.IsAny())).Returns(true); + + _queryExecutor = new QueryExecutor(new[] { _queryEngine.Object }); + } + + [Test] + public async Task TestQueryExecutorToReturnWhenNoQueries() + { + await _queryExecutor.ExecuteAsync(new DataContext(new EntityContext()), new QueryList()); + + _queryEngine.Verify(x => x.Execute(It.IsAny()), Times.Never()); + } + + [Test] + public async Task TestQueryExecutorToCallEngineWhenQueriesExistForExecution() + { + await _queryExecutor.ExecuteAsync(new DataContext(new EntityContext()), new QueryList(new[] { new CustomerQuery() }) { }); + + _queryEngine.Verify(x => x.Execute(It.IsAny()), Times.Once()); + } + + [Test] + public async Task TestQueryExecutorToExecuteConfiguredQueriesInCorrectOrder() + { + var querList = new QueryBuilder(new CustomerConfiguration(), new XPathMatcher()) + .Build(new DataContext(new CustomerContext())); + + await _queryExecutor.ExecuteAsync(new DataContext(new EntityContext()), querList); + + _queryEngine.Verify(x => x.Execute(It.IsAny()), Times.Once()); + } +} diff --git a/tests/DataFuse.Integration.Tests/DataProviderTests.cs b/tests/DataFuse.Integration.Tests/DataProviderTests.cs new file mode 100644 index 0000000..8e62170 --- /dev/null +++ b/tests/DataFuse.Integration.Tests/DataProviderTests.cs @@ -0,0 +1,271 @@ +using Microsoft.Extensions.Logging; +using DataFuse.Integration; +using Moq; +using DataFuse.Integration.Impl; + +namespace DataFuse.Integration.Tests.Impl; + +[TestFixture] +public class DataProviderTests +{ + private Mock>> _mockLogger; + private Mock> _mockQueryBuilder; + private Mock _mockQueryExecutor; + private Mock> _mockEntityBuilder; + private Mock> _mockEntityConfiguration; + private Mock _mockPathMatcher; + private Mock _mockQueryEngine; + private DataProvider _dataProvider; + + [SetUp] + public void Setup() + { + _mockLogger = new Mock>>(); + _mockQueryBuilder = new Mock>(); + _mockQueryExecutor = new Mock(); + _mockEntityBuilder = new Mock>(); + _mockEntityConfiguration = new Mock>(); + _mockPathMatcher = new Mock(); + _mockQueryEngine = new Mock(); + + _dataProvider = new DataProvider( + _mockLogger.Object, + _mockQueryBuilder.Object, + _mockQueryExecutor.Object, + _mockEntityBuilder.Object); + } + + [Test] + public void Constructor_WithEntityConfiguration_ShouldCreateDataProvider() + { + // Act + var provider = new DataProvider(_mockEntityConfiguration.Object, _mockQueryEngine.Object); + + // Assert + Assert.That(provider, Is.Not.Null); + } + + [Test] + public void Constructor_WithEntityConfigurationAndPathMatcher_ShouldCreateDataProvider() + { + // Act + var provider = new DataProvider( + _mockLogger.Object, + _mockEntityConfiguration.Object, + _mockPathMatcher.Object, + _mockQueryEngine.Object); + + // Assert + Assert.That(provider, Is.Not.Null); + } + + [Test] + public async Task GetData_WithValidRequest_ShouldReturnEntity() + { + // Arrange + var request = new TestEntityRequest { SchemaPaths = new[] { "/root/test" } }; + var queries = new Mock(); + var results = new List { new TestQueryResult { Data = "test" } }; + var expectedEntity = new TestEntity { Name = "test", Value = 4 }; + + _mockQueryBuilder.Setup(x => x.Build(It.IsAny())).Returns(queries.Object); + _mockQueryExecutor.Setup(x => x.ExecuteAsync(It.IsAny(), queries.Object)).ReturnsAsync(results); + _mockEntityBuilder.Setup(x => x.Build(It.IsAny(), results)).Returns(expectedEntity); + + // Act + var result = await _dataProvider.GetDataAsync(request); + + // Assert + Assert.That(result, Is.EqualTo(expectedEntity)); + _mockQueryBuilder.Verify(x => x.Build(It.IsAny()), Times.Once); + _mockQueryExecutor.Verify(x => x.ExecuteAsync(It.IsAny(), queries.Object), Times.Once); + _mockEntityBuilder.Verify(x => x.Build(It.IsAny(), results), Times.Once); + } + + [Test] + public async Task GetData_ShouldCreateDataContextWithRequest() + { + // Arrange + var request = new TestEntityRequest { SchemaPaths = new[] { "/root/test" } }; + var queries = new Mock(); + var results = new List(); + var expectedEntity = new TestEntity(); + + _mockQueryBuilder.Setup(x => x.Build(It.IsAny())).Returns(queries.Object); + _mockQueryExecutor.Setup(x => x.ExecuteAsync(It.IsAny(), It.IsAny())).ReturnsAsync(results); + _mockEntityBuilder.Setup(x => x.Build(It.IsAny(), It.IsAny>())).Returns(expectedEntity); + + // Act + await _dataProvider.GetDataAsync(request); + + // Assert + _mockQueryBuilder.Verify(x => x.Build(It.Is(ctx => ctx.Request == request)), Times.Once); + } + + [Test] + public async Task GetData_WithNullRequest_ShouldStillWork() + { + // Arrange + IEntityRequest request = null; + var queries = new Mock(); + var results = new List(); + var expectedEntity = new TestEntity(); + + _mockQueryBuilder.Setup(x => x.Build(It.IsAny())).Returns(queries.Object); + _mockQueryExecutor.Setup(x => x.ExecuteAsync(It.IsAny(), It.IsAny())).ReturnsAsync(results); + _mockEntityBuilder.Setup(x => x.Build(It.IsAny(), It.IsAny>())).Returns(expectedEntity); + + // Act & Assert + Assert.DoesNotThrowAsync(async () => await _dataProvider.GetDataAsync(request)); + } + + [Test] + public async Task GetData_ShouldLogExecutionTimes() + { + // Arrange + var request = new TestEntityRequest { SchemaPaths = new[] { "/root/test" } }; + var queries = new Mock(); + var results = new List(); + var expectedEntity = new TestEntity(); + + _mockQueryBuilder.Setup(x => x.Build(It.IsAny())).Returns(queries.Object); + _mockQueryExecutor.Setup(x => x.ExecuteAsync(It.IsAny(), It.IsAny())).ReturnsAsync(results); + _mockEntityBuilder.Setup(x => x.Build(It.IsAny(), It.IsAny>())).Returns(expectedEntity); + + // Act + await _dataProvider.GetDataAsync(request); + + // Assert + _mockLogger.Verify( + x => x.Log( + LogLevel.Information, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("Query builder executed")), + It.IsAny(), + It.IsAny>()), + Times.Once); + + _mockLogger.Verify( + x => x.Log( + LogLevel.Information, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("Query executor executed")), + It.IsAny(), + It.IsAny>()), + Times.Once); + + _mockLogger.Verify( + x => x.Log( + LogLevel.Information, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("Transform executor executed")), + It.IsAny(), + It.IsAny>()), + Times.Once); + } + + [Test] + public void GetData_WhenQueryBuilderThrows_ShouldPropagateException() + { + // Arrange + var request = new TestEntityRequest { SchemaPaths = new[] { "/root/test" } }; + var expectedException = new System.Exception("Query builder failed"); + + _mockQueryBuilder.Setup(x => x.Build(It.IsAny())).Throws(expectedException); + + // Act & Assert + var ex = Assert.ThrowsAsync(async () => await _dataProvider.GetDataAsync(request)); + Assert.That(ex, Is.EqualTo(expectedException)); + } + + [Test] + public void GetData_WhenQueryExecutorThrows_ShouldPropagateException() + { + // Arrange + var request = new TestEntityRequest { SchemaPaths = new[] { "/root/test" } }; + var queries = new Mock(); + var expectedException = new System.Exception("Query executor failed"); + + _mockQueryBuilder.Setup(x => x.Build(It.IsAny())).Returns(queries.Object); + _mockQueryExecutor.Setup(x => x.ExecuteAsync(It.IsAny(), It.IsAny())).ThrowsAsync(expectedException); + + // Act & Assert + var ex = Assert.ThrowsAsync(async () => await _dataProvider.GetDataAsync(request)); + Assert.That(ex, Is.EqualTo(expectedException)); + } + + [Test] + public void GetData_WhenEntityBuilderThrows_ShouldPropagateException() + { + // Arrange + var request = new TestEntityRequest { SchemaPaths = new[] { "/root/test" } }; + var queries = new Mock(); + var results = new List(); + var expectedException = new System.Exception("Entity builder failed"); + + _mockQueryBuilder.Setup(x => x.Build(It.IsAny())).Returns(queries.Object); + _mockQueryExecutor.Setup(x => x.ExecuteAsync(It.IsAny(), It.IsAny())).ReturnsAsync(results); + _mockEntityBuilder.Setup(x => x.Build(It.IsAny(), It.IsAny>())).Throws(expectedException); + + // Act & Assert + var ex = Assert.ThrowsAsync(async () => await _dataProvider.GetDataAsync(request)); + Assert.That(ex, Is.EqualTo(expectedException)); + } + + [Test] + public async Task GetData_WithComplexScenario_ShouldWorkCorrectly() + { + // Arrange + var request = new TestEntityRequest + { + SchemaPaths = new[] { "/root/test", "/root/child" } + }; + var queries = new Mock(); + var results = new List + { + new TestQueryResult { Data = "parent_data" }, + new TestChildQueryResult { ChildData = "child_data" } + }; + var expectedEntity = new TestEntity { Name = "combined", Value = 123 }; + + _mockQueryBuilder.Setup(x => x.Build(It.IsAny())).Returns(queries.Object); + _mockQueryExecutor.Setup(x => x.ExecuteAsync(It.IsAny(), queries.Object)).ReturnsAsync(results); + _mockEntityBuilder.Setup(x => x.Build(It.IsAny(), results)).Returns(expectedEntity); + + // Act + var result = await _dataProvider.GetDataAsync(request); + + // Assert + Assert.That(result, Is.EqualTo(expectedEntity)); + Assert.That(result.Name, Is.EqualTo("combined")); + Assert.That(result.Value, Is.EqualTo(123)); + } + + [Test] + public void DataProvider_ShouldImplementIDataProvider() + { + // Assert + Assert.That(_dataProvider, Is.InstanceOf>()); + } + + [Test] + public async Task GetData_WithEmptyResults_ShouldReturnEntityFromBuilder() + { + // Arrange + var request = new TestEntityRequest { SchemaPaths = new[] { "/root/test" } }; + var queries = new Mock(); + var emptyResults = new List(); + var expectedEntity = new TestEntity { Name = "empty_result", Value = 0 }; + + _mockQueryBuilder.Setup(x => x.Build(It.IsAny())).Returns(queries.Object); + _mockQueryExecutor.Setup(x => x.ExecuteAsync(It.IsAny(), queries.Object)).ReturnsAsync(emptyResults); + _mockEntityBuilder.Setup(x => x.Build(It.IsAny(), emptyResults)).Returns(expectedEntity); + + // Act + var result = await _dataProvider.GetDataAsync(request); + + // Assert + Assert.That(result, Is.EqualTo(expectedEntity)); + _mockEntityBuilder.Verify(x => x.Build(It.IsAny(), emptyResults), Times.Once); + } +} diff --git a/tests/DataFuse.Integration.Tests/EntityConfigurationTests.cs b/tests/DataFuse.Integration.Tests/EntityConfigurationTests.cs new file mode 100644 index 0000000..66fd64d --- /dev/null +++ b/tests/DataFuse.Integration.Tests/EntityConfigurationTests.cs @@ -0,0 +1,194 @@ +namespace DataFuse.Integration.Tests +{ + [TestFixture] + public class EntityConfigurationTests + { + private TestEntityConfiguration _configuration; + + [SetUp] + public void Setup() + { + _configuration = new TestEntityConfiguration(); + } + + [Test] + public void Constructor_ShouldInitializeMappings() + { + // Act + var configuration = new TestEntityConfiguration(); + + // Assert + Assert.That(configuration.Mappings, Is.Not.Null); + } + + [Test] + public void Mappings_ShouldReturnConfiguredMappings() + { + // Act + var mappings = _configuration.Mappings.ToList(); + + // Assert + Assert.That(mappings, Is.Not.Null); + Assert.That(mappings.Count, Is.GreaterThan(0)); + } + + [Test] + public void Mappings_ShouldContainCorrectQueryTypes() + { + // Act + var mappings = _configuration.Mappings.ToList(); + + // Assert + var hasTestQuery = mappings.Any(m => m.Query is TestQuery); + var hasTestChildQuery = mappings.Any(m => m.Query is TestChildQuery); + + Assert.That(hasTestQuery, Is.True); + Assert.That(hasTestChildQuery, Is.True); + } + + [Test] + public void Mappings_ShouldContainCorrectTransformerTypes() + { + // Act + var mappings = _configuration.Mappings.ToList(); + + // Assert + var hasTestTransformer = mappings.Any(m => m.Transformer is TestTransformer); + var hasTestChildTransformer = mappings.Any(m => m.Transformer is TestChildTransformer); + + Assert.That(hasTestTransformer, Is.True); + Assert.That(hasTestChildTransformer, Is.True); + } + + [Test] + public void Mappings_ShouldHaveCorrectSchemaPaths() + { + // Act + var mappings = _configuration.Mappings.ToList(); + + // Assert + var rootMapping = mappings.FirstOrDefault(m => m.Query is TestQuery); + var childMapping = mappings.FirstOrDefault(m => m.Query is TestChildQuery); + + Assert.That(rootMapping, Is.Not.Null); + Assert.That(rootMapping.SchemaPaths, Is.Not.Null); + Assert.That(rootMapping.SchemaPaths.Paths, Contains.Item("/root/test")); + + Assert.That(childMapping, Is.Not.Null); + Assert.That(childMapping.SchemaPaths, Is.Not.Null); + } + + [Test] + public void Mappings_ShouldHaveCorrectDependencyStructure() + { + // Act + var mappings = _configuration.Mappings.ToList(); + + // Assert + var rootMapping = mappings.FirstOrDefault(m => m.Query is TestQuery && m.DependentOn == null); + var dependentMappings = mappings.Where(m => m.DependentOn != null).ToList(); + + Assert.That(rootMapping, Is.Not.Null); + Assert.That(dependentMappings, Is.Not.Empty); + + foreach (var dependent in dependentMappings) + { + Assert.That(dependent.DependentOn, Is.Not.Null); + } + } + + [Test] + public void Mappings_ShouldHaveCorrectOrdering() + { + // Act + var mappings = _configuration.Mappings.ToList(); + + // Assert + var orders = mappings.Select(m => m.Order).Distinct().OrderBy(o => o).ToList(); + + Assert.That(orders, Is.Not.Empty); + Assert.That(orders[0], Is.EqualTo(1)); // Should start with order 1 + + // Orders should be sequential (allowing for gaps, but generally increasing) + foreach (var order in orders) + { + Assert.That(order, Is.GreaterThan(0)); + } + } + + [Test] + public void GetSchema_ShouldReturnSameMappingsAsProperty() + { + // Act + var schemaFromMethod = _configuration.GetSchema().ToList(); + var schemaFromProperty = _configuration.Mappings.ToList(); + + // Assert + Assert.That(schemaFromMethod.Count, Is.EqualTo(schemaFromProperty.Count)); + + for (int i = 0; i < schemaFromMethod.Count; i++) + { + Assert.That(schemaFromMethod[i].Query.GetType(), Is.EqualTo(schemaFromProperty[i].Query.GetType())); + Assert.That(schemaFromMethod[i].Transformer.GetType(), Is.EqualTo(schemaFromProperty[i].Transformer.GetType())); + Assert.That(schemaFromMethod[i].Order, Is.EqualTo(schemaFromProperty[i].Order)); + } + } + + [Test] + public void EntityConfiguration_ShouldImplementInterface() + { + // Assert + Assert.That(_configuration, Is.InstanceOf>()); + } + + [Test] + public void GetSchema_ShouldBeCalledOnlyOnce() + { + // Arrange + var configurationSpy = new TestEntityConfigurationSpy(); + + // Act + var mappings1 = configurationSpy.Mappings.ToList(); + var mappings2 = configurationSpy.Mappings.ToList(); + + // Assert + Assert.That(configurationSpy.GetSchemaCallCount, Is.EqualTo(1)); + } + + // Test implementation that tracks method calls + private class TestEntityConfigurationSpy : EntityConfiguration + { + public int GetSchemaCallCount { get; private set; } + + public override System.Collections.Generic.IEnumerable> GetSchema() + { + GetSchemaCallCount++; + return CreateSchema.For() + .Map(For.Paths("/root/spy")) + .End(); + } + } + + [Test] + public void EmptyEntityConfiguration_ShouldReturnEmptyMappings() + { + // Arrange + var emptyConfiguration = new EmptyEntityConfiguration(); + + // Act + var mappings = emptyConfiguration.Mappings.ToList(); + + // Assert + Assert.That(mappings, Is.Not.Null); + Assert.That(mappings.Count, Is.EqualTo(0)); + } + + private class EmptyEntityConfiguration : EntityConfiguration + { + public override System.Collections.Generic.IEnumerable> GetSchema() + { + return System.Linq.Enumerable.Empty>(); + } + } + } +} \ No newline at end of file diff --git a/tests/Schemio.Core.Tests/EntitySetup/Configuration/CustomerConfiguration.cs b/tests/DataFuse.Integration.Tests/EntitySetup/Configuration/CustomerConfiguration.cs similarity index 76% rename from tests/Schemio.Core.Tests/EntitySetup/Configuration/CustomerConfiguration.cs rename to tests/DataFuse.Integration.Tests/EntitySetup/Configuration/CustomerConfiguration.cs index be8d432..0d8503d 100644 --- a/tests/Schemio.Core.Tests/EntitySetup/Configuration/CustomerConfiguration.cs +++ b/tests/DataFuse.Integration.Tests/EntitySetup/Configuration/CustomerConfiguration.cs @@ -1,8 +1,8 @@ -using Schemio.Core.Tests.EntitySetup.Configuration.Queries; -using Schemio.Core.Tests.EntitySetup.Configuration.Transforms; -using Schemio.Core.Tests.EntitySetup.Entities; +using DataFuse.Integration.Tests.EntitySetup.Configuration.Queries; +using DataFuse.Integration.Tests.EntitySetup.Configuration.Transforms; +using DataFuse.Integration.Tests.EntitySetup.Entities; -namespace Schemio.Core.Tests.EntitySetup.Configuration +namespace DataFuse.Integration.Tests.EntitySetup.Configuration { internal class CustomerConfiguration : EntityConfiguration { diff --git a/tests/Schemio.Core.Tests/EntitySetup/Configuration/Queries/CommunicationQuery.cs b/tests/DataFuse.Integration.Tests/EntitySetup/Configuration/Queries/CommunicationQuery.cs similarity index 88% rename from tests/Schemio.Core.Tests/EntitySetup/Configuration/Queries/CommunicationQuery.cs rename to tests/DataFuse.Integration.Tests/EntitySetup/Configuration/Queries/CommunicationQuery.cs index 3136b90..fce69af 100644 --- a/tests/Schemio.Core.Tests/EntitySetup/Configuration/Queries/CommunicationQuery.cs +++ b/tests/DataFuse.Integration.Tests/EntitySetup/Configuration/Queries/CommunicationQuery.cs @@ -1,4 +1,4 @@ -namespace Schemio.Core.Tests.EntitySetup.Configuration.Queries +namespace DataFuse.Integration.Tests.EntitySetup.Configuration.Queries { internal class CommunicationQuery : BaseQuery { diff --git a/tests/Schemio.Core.Tests/EntitySetup/Configuration/Queries/CommunicationRecord.cs b/tests/DataFuse.Integration.Tests/EntitySetup/Configuration/Queries/CommunicationRecord.cs similarity index 85% rename from tests/Schemio.Core.Tests/EntitySetup/Configuration/Queries/CommunicationRecord.cs rename to tests/DataFuse.Integration.Tests/EntitySetup/Configuration/Queries/CommunicationRecord.cs index fe1274d..e44d7db 100644 --- a/tests/Schemio.Core.Tests/EntitySetup/Configuration/Queries/CommunicationRecord.cs +++ b/tests/DataFuse.Integration.Tests/EntitySetup/Configuration/Queries/CommunicationRecord.cs @@ -1,4 +1,4 @@ -namespace Schemio.Core.Tests.EntitySetup.Configuration.Queries +namespace DataFuse.Integration.Tests.EntitySetup.Configuration.Queries { public class CommunicationRecord : IQueryResult { diff --git a/tests/Schemio.Core.Tests/EntitySetup/Configuration/Queries/CustomerQuery.cs b/tests/DataFuse.Integration.Tests/EntitySetup/Configuration/Queries/CustomerQuery.cs similarity index 87% rename from tests/Schemio.Core.Tests/EntitySetup/Configuration/Queries/CustomerQuery.cs rename to tests/DataFuse.Integration.Tests/EntitySetup/Configuration/Queries/CustomerQuery.cs index 003c58c..d2af7b6 100644 --- a/tests/Schemio.Core.Tests/EntitySetup/Configuration/Queries/CustomerQuery.cs +++ b/tests/DataFuse.Integration.Tests/EntitySetup/Configuration/Queries/CustomerQuery.cs @@ -1,4 +1,4 @@ -namespace Schemio.Core.Tests.EntitySetup.Configuration.Queries +namespace DataFuse.Integration.Tests.EntitySetup.Configuration.Queries { public class CustomerQuery : BaseQuery { diff --git a/tests/Schemio.Core.Tests/EntitySetup/Configuration/Queries/CustomerRecord.cs b/tests/DataFuse.Integration.Tests/EntitySetup/Configuration/Queries/CustomerRecord.cs similarity index 73% rename from tests/Schemio.Core.Tests/EntitySetup/Configuration/Queries/CustomerRecord.cs rename to tests/DataFuse.Integration.Tests/EntitySetup/Configuration/Queries/CustomerRecord.cs index b56ec89..a36535c 100644 --- a/tests/Schemio.Core.Tests/EntitySetup/Configuration/Queries/CustomerRecord.cs +++ b/tests/DataFuse.Integration.Tests/EntitySetup/Configuration/Queries/CustomerRecord.cs @@ -1,4 +1,4 @@ -namespace Schemio.Core.Tests.EntitySetup.Configuration.Queries +namespace DataFuse.Integration.Tests.EntitySetup.Configuration.Queries { public class CustomerRecord : IQueryResult { diff --git a/tests/Schemio.Core.Tests/EntitySetup/Configuration/Queries/OrderItemRecord.cs b/tests/DataFuse.Integration.Tests/EntitySetup/Configuration/Queries/OrderItemRecord.cs similarity index 69% rename from tests/Schemio.Core.Tests/EntitySetup/Configuration/Queries/OrderItemRecord.cs rename to tests/DataFuse.Integration.Tests/EntitySetup/Configuration/Queries/OrderItemRecord.cs index c7621c3..6210c32 100644 --- a/tests/Schemio.Core.Tests/EntitySetup/Configuration/Queries/OrderItemRecord.cs +++ b/tests/DataFuse.Integration.Tests/EntitySetup/Configuration/Queries/OrderItemRecord.cs @@ -1,4 +1,4 @@ -īģŋnamespace Schemio.Core.Tests.EntitySetup.Configuration.Queries +īģŋnamespace DataFuse.Integration.Tests.EntitySetup.Configuration.Queries { public class OrderItemRecord { diff --git a/tests/Schemio.Core.Tests/EntitySetup/Configuration/Queries/OrderItemsQuery.cs b/tests/DataFuse.Integration.Tests/EntitySetup/Configuration/Queries/OrderItemsQuery.cs similarity index 89% rename from tests/Schemio.Core.Tests/EntitySetup/Configuration/Queries/OrderItemsQuery.cs rename to tests/DataFuse.Integration.Tests/EntitySetup/Configuration/Queries/OrderItemsQuery.cs index 3e9d021..3b456aa 100644 --- a/tests/Schemio.Core.Tests/EntitySetup/Configuration/Queries/OrderItemsQuery.cs +++ b/tests/DataFuse.Integration.Tests/EntitySetup/Configuration/Queries/OrderItemsQuery.cs @@ -1,4 +1,4 @@ -namespace Schemio.Core.Tests.EntitySetup.Configuration.Queries +namespace DataFuse.Integration.Tests.EntitySetup.Configuration.Queries { internal class OrderItemsQuery : BaseQuery> { diff --git a/tests/Schemio.Core.Tests/EntitySetup/Configuration/Queries/OrderRecord.cs b/tests/DataFuse.Integration.Tests/EntitySetup/Configuration/Queries/OrderRecord.cs similarity index 69% rename from tests/Schemio.Core.Tests/EntitySetup/Configuration/Queries/OrderRecord.cs rename to tests/DataFuse.Integration.Tests/EntitySetup/Configuration/Queries/OrderRecord.cs index 21811ed..5341834 100644 --- a/tests/Schemio.Core.Tests/EntitySetup/Configuration/Queries/OrderRecord.cs +++ b/tests/DataFuse.Integration.Tests/EntitySetup/Configuration/Queries/OrderRecord.cs @@ -1,4 +1,4 @@ -īģŋnamespace Schemio.Core.Tests.EntitySetup.Configuration.Queries +īģŋnamespace DataFuse.Integration.Tests.EntitySetup.Configuration.Queries { public class OrderRecord { diff --git a/tests/Schemio.Core.Tests/EntitySetup/Configuration/Queries/OrdersQuery.cs b/tests/DataFuse.Integration.Tests/EntitySetup/Configuration/Queries/OrdersQuery.cs similarity index 88% rename from tests/Schemio.Core.Tests/EntitySetup/Configuration/Queries/OrdersQuery.cs rename to tests/DataFuse.Integration.Tests/EntitySetup/Configuration/Queries/OrdersQuery.cs index 9acd1bc..b65af5f 100644 --- a/tests/Schemio.Core.Tests/EntitySetup/Configuration/Queries/OrdersQuery.cs +++ b/tests/DataFuse.Integration.Tests/EntitySetup/Configuration/Queries/OrdersQuery.cs @@ -1,4 +1,4 @@ -namespace Schemio.Core.Tests.EntitySetup.Configuration.Queries +namespace DataFuse.Integration.Tests.EntitySetup.Configuration.Queries { internal class OrdersQuery : BaseQuery> { diff --git a/tests/Schemio.Core.Tests/EntitySetup/Configuration/Transforms/CommunicationTransform.cs b/tests/DataFuse.Integration.Tests/EntitySetup/Configuration/Transforms/CommunicationTransform.cs similarity index 81% rename from tests/Schemio.Core.Tests/EntitySetup/Configuration/Transforms/CommunicationTransform.cs rename to tests/DataFuse.Integration.Tests/EntitySetup/Configuration/Transforms/CommunicationTransform.cs index c608aec..7b8bc1f 100644 --- a/tests/Schemio.Core.Tests/EntitySetup/Configuration/Transforms/CommunicationTransform.cs +++ b/tests/DataFuse.Integration.Tests/EntitySetup/Configuration/Transforms/CommunicationTransform.cs @@ -1,7 +1,7 @@ -using Schemio.Core.Tests.EntitySetup.Configuration.Queries; -using Schemio.Core.Tests.EntitySetup.Entities; +using DataFuse.Integration.Tests.EntitySetup.Configuration.Queries; +using DataFuse.Integration.Tests.EntitySetup.Entities; -namespace Schemio.Core.Tests.EntitySetup.Configuration.Transforms +namespace DataFuse.Integration.Tests.EntitySetup.Configuration.Transforms { public class CommunicationTransform : BaseTransformer { diff --git a/tests/Schemio.Core.Tests/EntitySetup/Configuration/Transforms/CustomerTransform.cs b/tests/DataFuse.Integration.Tests/EntitySetup/Configuration/Transforms/CustomerTransform.cs similarity index 67% rename from tests/Schemio.Core.Tests/EntitySetup/Configuration/Transforms/CustomerTransform.cs rename to tests/DataFuse.Integration.Tests/EntitySetup/Configuration/Transforms/CustomerTransform.cs index 83d982e..7b0137f 100644 --- a/tests/Schemio.Core.Tests/EntitySetup/Configuration/Transforms/CustomerTransform.cs +++ b/tests/DataFuse.Integration.Tests/EntitySetup/Configuration/Transforms/CustomerTransform.cs @@ -1,7 +1,7 @@ -using Schemio.Core.Tests.EntitySetup.Configuration.Queries; -using Schemio.Core.Tests.EntitySetup.Entities; +using DataFuse.Integration.Tests.EntitySetup.Configuration.Queries; +using DataFuse.Integration.Tests.EntitySetup.Entities; -namespace Schemio.Core.Tests.EntitySetup.Configuration.Transforms +namespace DataFuse.Integration.Tests.EntitySetup.Configuration.Transforms { public class CustomerTransform : BaseTransformer { diff --git a/tests/Schemio.Core.Tests/EntitySetup/Configuration/Transforms/OrderItemsTransform.cs b/tests/DataFuse.Integration.Tests/EntitySetup/Configuration/Transforms/OrderItemsTransform.cs similarity index 79% rename from tests/Schemio.Core.Tests/EntitySetup/Configuration/Transforms/OrderItemsTransform.cs rename to tests/DataFuse.Integration.Tests/EntitySetup/Configuration/Transforms/OrderItemsTransform.cs index 3fa722c..da5d6b8 100644 --- a/tests/Schemio.Core.Tests/EntitySetup/Configuration/Transforms/OrderItemsTransform.cs +++ b/tests/DataFuse.Integration.Tests/EntitySetup/Configuration/Transforms/OrderItemsTransform.cs @@ -1,7 +1,7 @@ -using Schemio.Core.Tests.EntitySetup.Configuration.Queries; -using Schemio.Core.Tests.EntitySetup.Entities; +using DataFuse.Integration.Tests.EntitySetup.Configuration.Queries; +using DataFuse.Integration.Tests.EntitySetup.Entities; -namespace Schemio.Core.Tests.EntitySetup.Configuration.Transforms +namespace DataFuse.Integration.Tests.EntitySetup.Configuration.Transforms { public class OrderItemsTransform : BaseTransformer, Customer> { diff --git a/tests/Schemio.Core.Tests/EntitySetup/Configuration/Transforms/OrdersTransform.cs b/tests/DataFuse.Integration.Tests/EntitySetup/Configuration/Transforms/OrdersTransform.cs similarity index 73% rename from tests/Schemio.Core.Tests/EntitySetup/Configuration/Transforms/OrdersTransform.cs rename to tests/DataFuse.Integration.Tests/EntitySetup/Configuration/Transforms/OrdersTransform.cs index dc0c536..e0d1d61 100644 --- a/tests/Schemio.Core.Tests/EntitySetup/Configuration/Transforms/OrdersTransform.cs +++ b/tests/DataFuse.Integration.Tests/EntitySetup/Configuration/Transforms/OrdersTransform.cs @@ -1,7 +1,7 @@ -using Schemio.Core.Tests.EntitySetup.Configuration.Queries; -using Schemio.Core.Tests.EntitySetup.Entities; +using DataFuse.Integration.Tests.EntitySetup.Configuration.Queries; +using DataFuse.Integration.Tests.EntitySetup.Entities; -namespace Schemio.Core.Tests.EntitySetup.Configuration.Transforms +namespace DataFuse.Integration.Tests.EntitySetup.Configuration.Transforms { public class OrdersTransform : BaseTransformer, Customer> { diff --git a/tests/Schemio.Core.Tests/EntitySetup/CustomerContext.cs b/tests/DataFuse.Integration.Tests/EntitySetup/CustomerContext.cs similarity index 76% rename from tests/Schemio.Core.Tests/EntitySetup/CustomerContext.cs rename to tests/DataFuse.Integration.Tests/EntitySetup/CustomerContext.cs index 17dcab3..9fded27 100644 --- a/tests/Schemio.Core.Tests/EntitySetup/CustomerContext.cs +++ b/tests/DataFuse.Integration.Tests/EntitySetup/CustomerContext.cs @@ -1,4 +1,4 @@ -namespace Schemio.Core.Tests.EntitySetup +namespace DataFuse.Integration.Tests.EntitySetup { internal class CustomerContext : IEntityRequest { diff --git a/tests/Schemio.SQL.Tests/EntitySetup/Entities/Address.cs b/tests/DataFuse.Integration.Tests/EntitySetup/Entities/Address.cs similarity index 83% rename from tests/Schemio.SQL.Tests/EntitySetup/Entities/Address.cs rename to tests/DataFuse.Integration.Tests/EntitySetup/Entities/Address.cs index 866975a..7376d2c 100644 --- a/tests/Schemio.SQL.Tests/EntitySetup/Entities/Address.cs +++ b/tests/DataFuse.Integration.Tests/EntitySetup/Entities/Address.cs @@ -1,4 +1,4 @@ -namespace Schemio.SQL.Tests.EntitySetup.Entities +namespace DataFuse.Integration.Tests.EntitySetup.Entities { public class Address { diff --git a/tests/Schemio.SQL.Tests/EntitySetup/Entities/Communication.cs b/tests/DataFuse.Integration.Tests/EntitySetup/Entities/Communication.cs similarity index 78% rename from tests/Schemio.SQL.Tests/EntitySetup/Entities/Communication.cs rename to tests/DataFuse.Integration.Tests/EntitySetup/Entities/Communication.cs index 0ed144f..34c0e53 100644 --- a/tests/Schemio.SQL.Tests/EntitySetup/Entities/Communication.cs +++ b/tests/DataFuse.Integration.Tests/EntitySetup/Entities/Communication.cs @@ -1,4 +1,4 @@ -namespace Schemio.SQL.Tests.EntitySetup.Entities +namespace DataFuse.Integration.Tests.EntitySetup.Entities { public class Communication { diff --git a/tests/Schemio.Core.Tests/EntitySetup/Entities/Customer.cs b/tests/DataFuse.Integration.Tests/EntitySetup/Entities/Customer.cs similarity index 82% rename from tests/Schemio.Core.Tests/EntitySetup/Entities/Customer.cs rename to tests/DataFuse.Integration.Tests/EntitySetup/Entities/Customer.cs index 66b751f..ae8d479 100644 --- a/tests/Schemio.Core.Tests/EntitySetup/Entities/Customer.cs +++ b/tests/DataFuse.Integration.Tests/EntitySetup/Entities/Customer.cs @@ -1,4 +1,4 @@ -namespace Schemio.Core.Tests.EntitySetup.Entities +namespace DataFuse.Integration.Tests.EntitySetup.Entities { public class Customer : IEntity { diff --git a/tests/Schemio.SQL.Tests/EntitySetup/Entities/EntityDiagram.cd b/tests/DataFuse.Integration.Tests/EntitySetup/Entities/EntityDiagram.cd similarity index 100% rename from tests/Schemio.SQL.Tests/EntitySetup/Entities/EntityDiagram.cd rename to tests/DataFuse.Integration.Tests/EntitySetup/Entities/EntityDiagram.cd diff --git a/tests/Schemio.SQL.Tests/EntitySetup/Entities/Order.cs b/tests/DataFuse.Integration.Tests/EntitySetup/Entities/Order.cs similarity index 78% rename from tests/Schemio.SQL.Tests/EntitySetup/Entities/Order.cs rename to tests/DataFuse.Integration.Tests/EntitySetup/Entities/Order.cs index 5c9602f..744b497 100644 --- a/tests/Schemio.SQL.Tests/EntitySetup/Entities/Order.cs +++ b/tests/DataFuse.Integration.Tests/EntitySetup/Entities/Order.cs @@ -1,4 +1,4 @@ -namespace Schemio.SQL.Tests.EntitySetup.Entities +namespace DataFuse.Integration.Tests.EntitySetup.Entities { public class Order { diff --git a/tests/Schemio.SQL.Tests/EntitySetup/Entities/OrderItem.cs b/tests/DataFuse.Integration.Tests/EntitySetup/Entities/OrderItem.cs similarity index 73% rename from tests/Schemio.SQL.Tests/EntitySetup/Entities/OrderItem.cs rename to tests/DataFuse.Integration.Tests/EntitySetup/Entities/OrderItem.cs index 5bfb23f..9ac72ff 100644 --- a/tests/Schemio.SQL.Tests/EntitySetup/Entities/OrderItem.cs +++ b/tests/DataFuse.Integration.Tests/EntitySetup/Entities/OrderItem.cs @@ -1,4 +1,4 @@ -namespace Schemio.SQL.Tests.EntitySetup.Entities +namespace DataFuse.Integration.Tests.EntitySetup.Entities { public class OrderItem { diff --git a/tests/DataFuse.Integration.Tests/GlobalUsings.cs b/tests/DataFuse.Integration.Tests/GlobalUsings.cs new file mode 100644 index 0000000..6e4ee4c --- /dev/null +++ b/tests/DataFuse.Integration.Tests/GlobalUsings.cs @@ -0,0 +1,2 @@ +global using DataFuse.Adapters.Abstraction; +global using DataFuse.Integration; diff --git a/tests/DataFuse.Integration.Tests/Helpers/ArrayUtilTests.cs b/tests/DataFuse.Integration.Tests/Helpers/ArrayUtilTests.cs new file mode 100644 index 0000000..aa549e9 --- /dev/null +++ b/tests/DataFuse.Integration.Tests/Helpers/ArrayUtilTests.cs @@ -0,0 +1,191 @@ +using DataFuse.Integration.Helpers; + +namespace DataFuse.Integration.Tests.Helpers +{ + [TestFixture] + public class ArrayUtilTests + { + [Test] + public void EnsureAndResizeArray_WithNullArray_ShouldCreateNewArrayWithOneElement() + { + // Arrange + string[] array = null; + + // Act + var result = ArrayUtil.EnsureAndResizeArray(array, out int index); + + // Assert + Assert.That(result, Is.Not.Null); + Assert.That(result.Length, Is.EqualTo(1)); + Assert.That(index, Is.EqualTo(0)); + } + + [Test] + public void EnsureAndResizeArray_WithExistingArray_ShouldExpandByOne() + { + // Arrange + var array = new[] { "item1", "item2" }; + + // Act + var result = ArrayUtil.EnsureAndResizeArray(array, out int index); + + // Assert + Assert.That(result.Length, Is.EqualTo(3)); + Assert.That(index, Is.EqualTo(2)); // Should point to the new element + Assert.That(result[0], Is.EqualTo("item1")); + Assert.That(result[1], Is.EqualTo("item2")); + } + + [Test] + public void EnsureAndResizeArray_WithExtendBy_WithNullArray_ShouldCreateNewArrayWithSpecifiedSize() + { + // Arrange + int[] array = null; + int extendBy = 5; + + // Act + var result = ArrayUtil.EnsureAndResizeArray(array, extendBy, out int index); + + // Assert + Assert.That(result, Is.Not.Null); + Assert.That(result.Length, Is.EqualTo(5)); + Assert.That(index, Is.EqualTo(0)); + } + + [Test] + public void EnsureAndResizeArray_WithExtendBy_WithExistingArray_ShouldExpandBySpecifiedAmount() + { + // Arrange + var array = new[] { 1, 2, 3 }; + int extendBy = 3; + + // Act + var result = ArrayUtil.EnsureAndResizeArray(array, extendBy, out int index); + + // Assert + Assert.That(result.Length, Is.EqualTo(6)); + Assert.That(index, Is.EqualTo(3)); // Should point to the first new element + Assert.That(result[0], Is.EqualTo(1)); + Assert.That(result[1], Is.EqualTo(2)); + Assert.That(result[2], Is.EqualTo(3)); + } + + [Test] + public void EnsureAndResizeArray_WithEmptyArray_ShouldExpandCorrectly() + { + // Arrange + var array = new string[0]; + + // Act + var result = ArrayUtil.EnsureAndResizeArray(array, out int index); + + // Assert + Assert.That(result.Length, Is.EqualTo(1)); + Assert.That(index, Is.EqualTo(0)); + } + + [Test] + public void EnsureAndResizeArray_WithExtendByZero_WithNullArray_ShouldCreateEmptyArray() + { + // Arrange + string[] array = null; + int extendBy = 0; + + // Act + var result = ArrayUtil.EnsureAndResizeArray(array, extendBy, out int index); + + // Assert + Assert.That(result, Is.Not.Null); + Assert.That(result.Length, Is.EqualTo(0)); + Assert.That(index, Is.EqualTo(0)); + } + + [Test] + public void EnsureAndResizeArray_WithExtendByZero_WithExistingArray_ShouldNotChangeSize() + { + // Arrange + var array = new[] { "a", "b" }; + int extendBy = 0; + + // Act + var result = ArrayUtil.EnsureAndResizeArray(array, extendBy, out int index); + + // Assert + Assert.That(result.Length, Is.EqualTo(2)); + Assert.That(index, Is.EqualTo(2)); // Should point after the last existing element + Assert.That(result[0], Is.EqualTo("a")); + Assert.That(result[1], Is.EqualTo("b")); + } + + [Test] + public void EnsureAndResizeArray_PreservesExistingData() + { + // Arrange + var array = new[] { 10, 20, 30, 40 }; + + // Act + var result = ArrayUtil.EnsureAndResizeArray(array, 2, out int index); + + // Assert + Assert.That(result.Length, Is.EqualTo(6)); + Assert.That(index, Is.EqualTo(4)); + Assert.That(result[0], Is.EqualTo(10)); + Assert.That(result[1], Is.EqualTo(20)); + Assert.That(result[2], Is.EqualTo(30)); + Assert.That(result[3], Is.EqualTo(40)); + Assert.That(result[4], Is.EqualTo(0)); // Default value for int + Assert.That(result[5], Is.EqualTo(0)); // Default value for int + } + + [Test] + public void EnsureAndResizeArray_WorksWithDifferentTypes() + { + // Arrange + var boolArray = new[] { true, false }; + var doubleArray = new[] { 1.1, 2.2 }; + + // Act + var boolResult = ArrayUtil.EnsureAndResizeArray(boolArray, out int boolIndex); + var doubleResult = ArrayUtil.EnsureAndResizeArray(doubleArray, out int doubleIndex); + + // Assert + Assert.That(boolResult.Length, Is.EqualTo(3)); + Assert.That(boolIndex, Is.EqualTo(2)); + Assert.That(boolResult[0], Is.True); + Assert.That(boolResult[1], Is.False); + Assert.That(boolResult[2], Is.False); // Default value for bool + + Assert.That(doubleResult.Length, Is.EqualTo(3)); + Assert.That(doubleIndex, Is.EqualTo(2)); + Assert.That(doubleResult[0], Is.EqualTo(1.1)); + Assert.That(doubleResult[1], Is.EqualTo(2.2)); + Assert.That(doubleResult[2], Is.EqualTo(0.0)); // Default value for double + } + + [Test] + public void EnsureAndResizeArray_MultipleConsecutiveCalls_ShouldWorkCorrectly() + { + // Arrange + string[] array = null; + + // Act + array = ArrayUtil.EnsureAndResizeArray(array, out int index1); + array[index1] = "first"; + + array = ArrayUtil.EnsureAndResizeArray(array, out int index2); + array[index2] = "second"; + + array = ArrayUtil.EnsureAndResizeArray(array, out int index3); + array[index3] = "third"; + + // Assert + Assert.That(array.Length, Is.EqualTo(3)); + Assert.That(array[0], Is.EqualTo("first")); + Assert.That(array[1], Is.EqualTo("second")); + Assert.That(array[2], Is.EqualTo("third")); + Assert.That(index1, Is.EqualTo(0)); + Assert.That(index2, Is.EqualTo(1)); + Assert.That(index3, Is.EqualTo(2)); + } + } +} \ No newline at end of file diff --git a/tests/DataFuse.Integration.Tests/Helpers/ConstraintsTests.cs b/tests/DataFuse.Integration.Tests/Helpers/ConstraintsTests.cs new file mode 100644 index 0000000..aa9ab59 --- /dev/null +++ b/tests/DataFuse.Integration.Tests/Helpers/ConstraintsTests.cs @@ -0,0 +1,186 @@ +using DataFuse.Integration.Helpers; +using DataFuse.Integration.Tests; + +namespace DataFuse.Integration.Tests.Helpers +{ + [TestFixture] + public class ConstraintsTests + { + [Test] + public void NotNull_WithNonNullValue_ShouldNotThrow() + { + // Arrange + var value = "not null"; + + // Act & Assert + Assert.DoesNotThrow(() => value.NotNull()); + } + + [Test] + public void NotNull_WithNullString_ShouldThrowArgumentNullException() + { + // Arrange + string value = null; + + // Act & Assert + var ex = Assert.Throws(() => value.NotNull()); + Assert.That(ex.ParamName, Is.EqualTo("String")); + } + + [Test] + public void NotNull_WithNullObject_ShouldThrowArgumentNullException() + { + // Arrange + object value = null; + + // Act & Assert + var ex = Assert.Throws(() => value.NotNull()); + Assert.That(ex.ParamName, Is.EqualTo("Object")); + } + + [Test] + public void NotNull_WithNullCustomType_ShouldThrowArgumentNullExceptionWithCorrectTypeName() + { + // Arrange + TestEntity value = null; + + // Act & Assert + var ex = Assert.Throws(() => value.NotNull()); + Assert.That(ex.ParamName, Is.EqualTo("TestEntity")); + } + + [Test] + public void NotNull_WithValidCustomType_ShouldNotThrow() + { + // Arrange + var value = new TestEntity { Name = "test", Value = 123 }; + + // Act & Assert + Assert.DoesNotThrow(() => value.NotNull()); + } + + [Test] + public void NotNull_WithNullInt_ShouldThrowArgumentNullException() + { + // Arrange + int? value = null; + + // Act & Assert + var ex = Assert.Throws(() => value.NotNull()); + Assert.That(ex.ParamName, Is.EqualTo("Nullable`1")); + } + + [Test] + public void NotNull_WithValidInt_ShouldNotThrow() + { + // Arrange + int? value = 42; + + // Act & Assert + Assert.DoesNotThrow(() => value.NotNull()); + } + + [Test] + public void NotNull_WithZeroValue_ShouldNotThrow() + { + // Arrange + int value = 0; + + // Act & Assert + Assert.DoesNotThrow(() => value.NotNull()); + } + + [Test] + public void NotNull_WithEmptyString_ShouldNotThrow() + { + // Arrange + var value = string.Empty; + + // Act & Assert + Assert.DoesNotThrow(() => value.NotNull()); + } + + [Test] + public void NotNull_WithEmptyArray_ShouldNotThrow() + { + // Arrange + var value = new string[0]; + + // Act & Assert + Assert.DoesNotThrow(() => value.NotNull()); + } + + [Test] + public void NotNull_WithNullArray_ShouldThrowArgumentNullException() + { + // Arrange + string[] value = null; + + // Act & Assert + var ex = Assert.Throws(() => value.NotNull()); + Assert.That(ex.ParamName, Is.EqualTo("String[]")); + } + + [Test] + public void NotNull_ChainedCalls_ShouldWorkCorrectly() + { + // Arrange + var value1 = "first"; + var value2 = "second"; + var value3 = "third"; + + // Act & Assert + Assert.DoesNotThrow(() => + { + value1.NotNull(); + value2.NotNull(); + value3.NotNull(); + }); + } + + [Test] + public void NotNull_InMethodChain_ShouldNotBreakFlow() + { + // Arrange + var value = "test value"; + + // Assert - The extension method should not return anything (void) + // This test mainly checks that the method exists and can be called + Assert.DoesNotThrow(() => value.NotNull()); + } + + [Test] + public void NotNull_WithGenericType_ShouldUseCorrectTypeName() + { + // Arrange + List value = null; + + // Act & Assert + var ex = Assert.Throws(() => value.NotNull()); + Assert.That(ex.ParamName, Contains.Substring("List")); + } + + [Test] + public void NotNull_WithInterface_ShouldUseInterfaceName() + { + // Arrange + System.Collections.Generic.IEnumerable value = null; + + // Act & Assert + var ex = Assert.Throws(() => value.NotNull()); + Assert.That(ex.ParamName, Contains.Substring("IEnumerable")); + } + + [Test] + public void NotNull_ExceptionMessage_ShouldBeCorrect() + { + // Arrange + string value = null; + + // Act & Assert + var ex = Assert.Throws(() => value.NotNull()); + Assert.That(ex.Message, Contains.Substring("Value cannot be null")); + Assert.That(ex.ParamName, Is.EqualTo("String")); + } + } +} \ No newline at end of file diff --git a/tests/DataFuse.Integration.Tests/Helpers/EnumerableExtensionsTests.cs b/tests/DataFuse.Integration.Tests/Helpers/EnumerableExtensionsTests.cs new file mode 100644 index 0000000..6c38923 --- /dev/null +++ b/tests/DataFuse.Integration.Tests/Helpers/EnumerableExtensionsTests.cs @@ -0,0 +1,269 @@ +using DataFuse.Integration.Helpers; +using DataFuse.Integration.Tests; + +namespace DataFuse.Integration.Tests.Helpers +{ + [TestFixture] + public class EnumerableExtensionsTests + { + [Test] + public void Each_WithValidEnumerable_ShouldExecuteActionForEachItem() + { + // Arrange + var items = new List { "item1", "item2", "item3" }; + var processedItems = new List(); + + // Act + items.Each(item => processedItems.Add(item.ToUpper())); + + // Assert + Assert.That(processedItems.Count, Is.EqualTo(3)); + Assert.That(processedItems[0], Is.EqualTo("ITEM1")); + Assert.That(processedItems[1], Is.EqualTo("ITEM2")); + Assert.That(processedItems[2], Is.EqualTo("ITEM3")); + } + + [Test] + public void Each_WithEmptyEnumerable_ShouldNotExecuteAction() + { + // Arrange + var items = new List(); + var actionExecuted = false; + + // Act + items.Each(item => actionExecuted = true); + + // Assert + Assert.That(actionExecuted, Is.False); + } + + [Test] + public void Each_WithNullAction_ShouldThrowArgumentNullException() + { + // Arrange + var items = new List { "item1" }; + + // Act & Assert + Assert.Throws(() => items.Each(null)); + } + + [Test] + public void Each_ShouldMaintainOrder() + { + // Arrange + var items = new List { 1, 2, 3, 4, 5 }; + var processedItems = new List(); + + // Act + items.Each(item => processedItems.Add(item * 2)); + + // Assert + Assert.That(processedItems, Is.EqualTo(new[] { 2, 4, 6, 8, 10 })); + } + + [Test] + public void ToCSV_WithDefaultSeparator_ShouldReturnCommaSeparatedValues() + { + // Arrange + var items = new List { "apple", "banana", "cherry" }; + + // Act + var result = items.ToCSV(); + + // Assert + Assert.That(result, Is.EqualTo("apple,banana,cherry")); + } + + [Test] + public void ToCSV_WithCustomSeparator_ShouldReturnCustomSeparatedValues() + { + // Arrange + var items = new List { 1, 2, 3, 4 }; + + // Act + var result = items.ToCSV('|'); + + // Assert + Assert.That(result, Is.EqualTo("1|2|3|4")); + } + + [Test] + public void ToCSV_WithSingleItem_ShouldReturnSingleValue() + { + // Arrange + var items = new List { "single" }; + + // Act + var result = items.ToCSV(); + + // Assert + Assert.That(result, Is.EqualTo("single")); + } + + [Test] + public void ToCSV_WithEmptyEnumerable_ShouldReturnEmptyString() + { + // Arrange + var items = new List(); + + // Act + var result = items.ToCSV(); + + // Assert + Assert.That(result, Is.EqualTo("")); + } + + [Test] + public void ToCSV_WithNullEnumerable_ShouldReturnNull() + { + // Arrange + List items = null; + + // Act + var result = items.ToCSV(); + + // Assert + Assert.That(result, Is.Null); + } + + [Test] + public void ToCSV_WithNullEnumerableAndCustomSeparator_ShouldReturnNull() + { + // Arrange + List items = null; + + // Act + var result = items.ToCSV(';'); + + // Assert + Assert.That(result, Is.Null); + } + + [Test] + public void ToCSV_WithDifferentTypes_ShouldCallToStringOnEachItem() + { + // Arrange + var items = new List { 1, 2.5, true, "text" }; + + // Act + var result = items.ToCSV(); + + // Assert + Assert.That(result, Is.EqualTo("1,2.5,True,text")); + } + + [Test] + public void ToCSV_WithNullItemsInEnumerable_ShouldHandleNulls() + { + // Arrange + var items = new List { "item1", null, "item3" }; + + // Act + var result = items.ToCSV(); + + // Assert + Assert.That(result, Is.EqualTo("item1,,item3")); + } + + [Test] + public void ToCSV_WithCustomObjectsOverridingToString_ShouldUseToStringOutput() + { + // Arrange + var items = new List + { + new TestEntity { Name = "Entity1", Value = 1 }, + new TestEntity { Name = "Entity2", Value = 2 } + }; + + // Act + var result = items.ToCSV(); + + // Assert + // This will depend on TestEntity's ToString() implementation + // Since TestEntity doesn't override ToString(), it will use the default + Assert.That(result, Contains.Substring("TestEntity")); + } + + [Test] + public void ToCSV_WithSpecialCharactersInSeparator_ShouldWorkCorrectly() + { + // Arrange + var items = new List { "a", "b", "c" }; + + // Act + var result = items.ToCSV('\t'); // Tab separator + + // Assert + Assert.That(result, Is.EqualTo("a\tb\tc")); + } + + [Test] + public void ToCSV_WithLargeEnumerable_ShouldHandleEfficiently() + { + // Arrange + var items = Enumerable.Range(1, 1000).ToList(); + + // Act + var result = items.ToCSV(); + + // Assert + Assert.That(result, Is.Not.Null); + Assert.That(result.Split(',').Length, Is.EqualTo(1000)); + Assert.That(result, Does.StartWith("1,2,3")); + Assert.That(result, Does.EndWith("998,999,1000")); + } + + [Test] + public void Each_WithComplexObjects_ShouldWorkCorrectly() + { + // Arrange + var entities = new List + { + new TestEntity { Name = "Test1", Value = 1 }, + new TestEntity { Name = "Test2", Value = 2 } + }; + var totalValue = 0; + + // Act + entities.Each(entity => totalValue += entity.Value); + + // Assert + Assert.That(totalValue, Is.EqualTo(3)); + } + + [Test] + public void Each_WithSideEffects_ShouldModifyOriginalObjects() + { + // Arrange + var entities = new List + { + new TestEntity { Name = "Test1", Value = 1 }, + new TestEntity { Name = "Test2", Value = 2 } + }; + + // Act + entities.Each(entity => entity.Value *= 2); + + // Assert + Assert.That(entities[0].Value, Is.EqualTo(2)); + Assert.That(entities[1].Value, Is.EqualTo(4)); + } + + [Test] + public void ToCSV_PerformanceWithDifferentSeparators_ShouldBeConsistent() + { + // Arrange + var items = Enumerable.Range(1, 100).Select(i => $"item{i}").ToList(); + + // Act + var commaResult = items.ToCSV(','); + var pipeResult = items.ToCSV('|'); + var semicolonResult = items.ToCSV(';'); + + // Assert + Assert.That(commaResult.Split(',').Length, Is.EqualTo(100)); + Assert.That(pipeResult.Split('|').Length, Is.EqualTo(100)); + Assert.That(semicolonResult.Split(';').Length, Is.EqualTo(100)); + } + } +} \ No newline at end of file diff --git a/tests/DataFuse.Integration.Tests/Helpers/JsonExtensionsTests.cs b/tests/DataFuse.Integration.Tests/Helpers/JsonExtensionsTests.cs new file mode 100644 index 0000000..7ec2c8e --- /dev/null +++ b/tests/DataFuse.Integration.Tests/Helpers/JsonExtensionsTests.cs @@ -0,0 +1,345 @@ +using DataFuse.Integration.Helpers; +using DataFuse.Integration.Tests; + +namespace DataFuse.Integration.Tests.Helpers +{ + [TestFixture] + public class JsonExtensionsTests + { + [Test] + public void ToJson_WithValidObject_ShouldReturnJsonString() + { + // Arrange + var testObject = new { Name = "Test", Value = 123 }; + + // Act + var result = testObject.ToJson(); + + // Assert + Assert.That(result, Is.Not.Null); + Assert.That(result, Contains.Substring("Test")); + Assert.That(result, Contains.Substring("123")); + Assert.That(result, Does.StartWith("{")); + Assert.That(result, Does.EndWith("}")); + } + + [Test] + public void ToJson_WithNullObject_ShouldReturnNull() + { + // Arrange + object testObject = null; + + // Act + var result = testObject.ToJson(); + + // Assert + Assert.That(result, Is.Null); + } + + [Test] + public void ToJson_WithString_ShouldReturnQuotedString() + { + // Arrange + var testString = "Hello World"; + + // Act + var result = testString.ToJson(); + + // Assert + Assert.That(result, Is.EqualTo("\"Hello World\"")); + } + + [Test] + public void ToJson_WithNumber_ShouldReturnNumberAsString() + { + // Arrange + var number = 42; + + // Act + var result = number.ToJson(); + + // Assert + Assert.That(result, Is.EqualTo("42")); + } + + [Test] + public void ToJson_WithBoolean_ShouldReturnBooleanAsString() + { + // Arrange + var boolValue = true; + + // Act + var result = boolValue.ToJson(); + + // Assert + Assert.That(result, Is.EqualTo("true")); + } + + [Test] + public void ToJson_WithArray_ShouldReturnJsonArray() + { + // Arrange + var array = new[] { 1, 2, 3 }; + + // Act + var result = array.ToJson(); + + // Assert + Assert.That(result, Is.EqualTo("[1,2,3]")); + } + + [Test] + public void ToJson_WithList_ShouldReturnJsonArray() + { + // Arrange + var list = new List { "apple", "banana", "cherry" }; + + // Act + var result = list.ToJson(); + + // Assert + Assert.That(result, Contains.Substring("apple")); + Assert.That(result, Contains.Substring("banana")); + Assert.That(result, Contains.Substring("cherry")); + Assert.That(result, Does.StartWith("[")); + Assert.That(result, Does.EndWith("]")); + } + + [Test] + public void ToJson_WithComplexObject_ShouldReturnValidJson() + { + // Arrange + var complexObject = new TestEntity { Name = "TestEntity", Value = 456 }; + + // Act + var result = complexObject.ToJson(); + + // Assert + Assert.That(result, Is.Not.Null); + Assert.That(result, Contains.Substring("TestEntity")); + Assert.That(result, Contains.Substring("456")); + } + + [Test] + public void ToObject_WithValidJsonString_ShouldReturnObject() + { + // Arrange + var json = "{\"Name\":\"Test\",\"Value\":123}"; + var expectedType = typeof(TestEntity); + + // Act + var result = json.ToObject(expectedType); + + // Assert + Assert.That(result, Is.Not.Null); + Assert.That(result, Is.InstanceOf()); + var entity = (TestEntity)result; + Assert.That(entity.Name, Is.EqualTo("Test")); + Assert.That(entity.Value, Is.EqualTo(123)); + } + + [Test] + public void ToObject_WithNullOrEmptyString_ShouldReturnNull() + { + // Arrange + string json = null; + var type = typeof(TestEntity); + + // Act + var result1 = json.ToObject(type); + var result2 = string.Empty.ToObject(type); + + // Assert + Assert.That(result1, Is.Null); + Assert.That(result2, Is.Null); + } + + [Test] + public void ToObject_WithInvalidJson_ShouldThrowException() + { + // Arrange + var invalidJson = "{invalid json}"; + var type = typeof(TestEntity); + + // Act & Assert + Assert.Throws(() => invalidJson.ToObject(type)); + } + + [Test] + public void ToObject_WithPrimitiveType_ShouldReturnPrimitive() + { + // Arrange + var json = "42"; + var type = typeof(int); + + // Act + var result = json.ToObject(type); + + // Assert + Assert.That(result, Is.EqualTo(42)); + } + + [Test] + public void ToObject_WithStringType_ShouldReturnString() + { + // Arrange + var json = "\"Hello World\""; + var type = typeof(string); + + // Act + var result = json.ToObject(type); + + // Assert + Assert.That(result, Is.EqualTo("Hello World")); + } + + [Test] + public void ToObject_WithBooleanType_ShouldReturnBoolean() + { + // Arrange + var json = "true"; + var type = typeof(bool); + + // Act + var result = json.ToObject(type); + + // Assert + Assert.That(result, Is.EqualTo(true)); + } + + [Test] + public void ToObject_WithArrayType_ShouldReturnArray() + { + // Arrange + var json = "[1,2,3]"; + var type = typeof(int[]); + + // Act + var result = json.ToObject(type); + + // Assert + Assert.That(result, Is.InstanceOf()); + var array = (int[])result; + Assert.That(array.Length, Is.EqualTo(3)); + Assert.That(array[0], Is.EqualTo(1)); + Assert.That(array[1], Is.EqualTo(2)); + Assert.That(array[2], Is.EqualTo(3)); + } + + [Test] + public void ToObject_WithListType_ShouldReturnList() + { + // Arrange + var json = "[\"apple\",\"banana\",\"cherry\"]"; + var type = typeof(List); + + // Act + var result = json.ToObject(type); + + // Assert + Assert.That(result, Is.InstanceOf>()); + var list = (List)result; + Assert.That(list.Count, Is.EqualTo(3)); + Assert.That(list[0], Is.EqualTo("apple")); + Assert.That(list[1], Is.EqualTo("banana")); + Assert.That(list[2], Is.EqualTo("cherry")); + } + + [Test] + public void RoundTripSerialization_ShouldPreserveData() + { + // Arrange + var originalEntity = new TestEntity { Name = "RoundTrip", Value = 789 }; + + // Act + var json = originalEntity.ToJson(); + var deserializedEntity = (TestEntity)json.ToObject(typeof(TestEntity)); + + // Assert + Assert.That(deserializedEntity, Is.Not.Null); + Assert.That(deserializedEntity.Name, Is.EqualTo(originalEntity.Name)); + Assert.That(deserializedEntity.Value, Is.EqualTo(originalEntity.Value)); + } + + [Test] + public void ToJson_WithNestedObject_ShouldSerializeNested() + { + // Arrange + var nestedObject = new + { + Parent = "ParentValue", + Child = new { Name = "ChildName", Value = 100 } + }; + + // Act + var result = nestedObject.ToJson(); + + // Assert + Assert.That(result, Contains.Substring("ParentValue")); + Assert.That(result, Contains.Substring("ChildName")); + Assert.That(result, Contains.Substring("100")); + } + + [Test] + public void ToObject_WithNestedJson_ShouldDeserializeNested() + { + // Arrange + var json = "{\"Name\":\"Parent\",\"Child\":{\"Description\":\"ChildDesc\"}}"; + var type = typeof(ParentEntity); + + // Act + var result = json.ToObject(type); + + // Assert + Assert.That(result, Is.Not.Null); + Assert.That(result, Is.InstanceOf()); + var parent = (ParentEntity)result; + Assert.That(parent.Name, Is.EqualTo("Parent")); + Assert.That(parent.Child, Is.Not.Null); + Assert.That(parent.Child.Description, Is.EqualTo("ChildDesc")); + } + + [Test] + public void ToJson_WithEmptyObject_ShouldReturnEmptyJson() + { + // Arrange + var emptyObject = new { }; + + // Act + var result = emptyObject.ToJson(); + + // Assert + Assert.That(result, Is.EqualTo("{}")); + } + + [Test] + public void ToObject_WithEmptyJson_ShouldReturnEmptyObject() + { + // Arrange + var json = "{}"; + var type = typeof(TestEntity); + + // Act + var result = json.ToObject(type); + + // Assert + Assert.That(result, Is.Not.Null); + Assert.That(result, Is.InstanceOf()); + var entity = (TestEntity)result; + Assert.That(entity.Name, Is.Null); + Assert.That(entity.Value, Is.EqualTo(0)); + } + + // Helper class for nested object tests + public class ParentEntity + { + public string Name { get; set; } + public ChildEntity Child { get; set; } + } + + public class ChildEntity + { + public string Description { get; set; } + } + } +} \ No newline at end of file diff --git a/tests/DataFuse.Integration.Tests/Helpers/StringExtensionsTests.cs b/tests/DataFuse.Integration.Tests/Helpers/StringExtensionsTests.cs new file mode 100644 index 0000000..35af726 --- /dev/null +++ b/tests/DataFuse.Integration.Tests/Helpers/StringExtensionsTests.cs @@ -0,0 +1,227 @@ +using DataFuse.Integration.Helpers; + +namespace DataFuse.Integration.Tests.Helpers +{ + [TestFixture] + public class StringExtensionsTests + { + [Test] + public void IsNotNullOrEmpty_WithValidString_ShouldReturnTrue() + { + // Arrange + var testString = "Hello World"; + + // Act + var result = testString.IsNotNullOrEmpty(); + + // Assert + Assert.That(result, Is.True); + } + + [Test] + public void IsNotNullOrEmpty_WithEmptyString_ShouldReturnFalse() + { + // Arrange + var testString = string.Empty; + + // Act + var result = testString.IsNotNullOrEmpty(); + + // Assert + Assert.That(result, Is.False); + } + + [Test] + public void IsNotNullOrEmpty_WithNullString_ShouldReturnFalse() + { + // Arrange + string testString = null; + + // Act + var result = testString.IsNotNullOrEmpty(); + + // Assert + Assert.That(result, Is.False); + } + + [Test] + public void IsNotNullOrEmpty_WithWhitespaceString_ShouldReturnTrue() + { + // Arrange + var testString = " "; + + // Act + var result = testString.IsNotNullOrEmpty(); + + // Assert + Assert.That(result, Is.True); // Whitespace is not considered empty by string.IsNullOrEmpty + } + + [Test] + public void IsNotNullOrEmpty_WithSingleCharacter_ShouldReturnTrue() + { + // Arrange + var testString = "a"; + + // Act + var result = testString.IsNotNullOrEmpty(); + + // Assert + Assert.That(result, Is.True); + } + + [Test] + public void IsNotNullOrEmpty_WithLongString_ShouldReturnTrue() + { + // Arrange + var testString = new string('a', 1000); + + // Act + var result = testString.IsNotNullOrEmpty(); + + // Assert + Assert.That(result, Is.True); + } + + [Test] + public void IsNotNullOrEmpty_WithSpecialCharacters_ShouldReturnTrue() + { + // Arrange + var testString = "!@#$%^&*()"; + + // Act + var result = testString.IsNotNullOrEmpty(); + + // Assert + Assert.That(result, Is.True); + } + + [Test] + public void IsNotNullOrEmpty_WithUnicodeCharacters_ShouldReturnTrue() + { + // Arrange + var testString = "Hello ä¸–į•Œ"; + + // Act + var result = testString.IsNotNullOrEmpty(); + + // Assert + Assert.That(result, Is.True); + } + + [Test] + public void IsNotNullOrEmpty_WithNewlineCharacters_ShouldReturnTrue() + { + // Arrange + var testString = "\n\r\t"; + + // Act + var result = testString.IsNotNullOrEmpty(); + + // Assert + Assert.That(result, Is.True); + } + + [Test] + public void IsNotNullOrEmpty_WithZeroWidthSpace_ShouldReturnTrue() + { + // Arrange + var testString = "\u200B"; // Zero-width space + + // Act + var result = testString.IsNotNullOrEmpty(); + + // Assert + Assert.That(result, Is.True); + } + + [Test] + public void IsNotNullOrEmpty_MultipleCallsOnSameString_ShouldReturnSameResult() + { + // Arrange + var testString = "consistent"; + + // Act + var result1 = testString.IsNotNullOrEmpty(); + var result2 = testString.IsNotNullOrEmpty(); + var result3 = testString.IsNotNullOrEmpty(); + + // Assert + Assert.That(result1, Is.True); + Assert.That(result2, Is.True); + Assert.That(result3, Is.True); + Assert.That(result1, Is.EqualTo(result2)); + Assert.That(result2, Is.EqualTo(result3)); + } + + [Test] + public void IsNotNullOrEmpty_BehaviorConsistentWithStringIsNullOrEmpty() + { + // Arrange + var testCases = new[] + { + "normal string", + "", + null, + " ", + "\t\n", + "a", + new string('x', 100) + }; + + foreach (var testCase in testCases) + { + // Act + var extensionResult = testCase.IsNotNullOrEmpty(); + var expectedResult = !string.IsNullOrEmpty(testCase); + + // Assert + Assert.That(extensionResult, Is.EqualTo(expectedResult), + $"Failed for test case: '{testCase}'"); + } + } + + [Test] + public void IsNotNullOrEmpty_CanBeChainedWithOtherStringMethods() + { + // Arrange + var testString = " Hello World "; + + // Act + var result = testString.IsNotNullOrEmpty() && testString.Trim().Length > 0; + + // Assert + Assert.That(result, Is.True); + } + + [Test] + public void IsNotNullOrEmpty_WithInterpolatedString_ShouldReturnTrue() + { + // Arrange + var name = "John"; + var testString = $"Hello {name}"; + + // Act + var result = testString.IsNotNullOrEmpty(); + + // Assert + Assert.That(result, Is.True); + } + + [Test] + public void IsNotNullOrEmpty_WithStringBuilder_ShouldReturnTrue() + { + // Arrange + var sb = new System.Text.StringBuilder(); + sb.Append("Hello"); + sb.Append(" World"); + var testString = sb.ToString(); + + // Act + var result = testString.IsNotNullOrEmpty(); + + // Assert + Assert.That(result, Is.True); + } + } +} \ No newline at end of file diff --git a/tests/DataFuse.Integration.Tests/Helpers/Xml/XDocumentExtsTests.cs b/tests/DataFuse.Integration.Tests/Helpers/Xml/XDocumentExtsTests.cs new file mode 100644 index 0000000..89ee4e9 --- /dev/null +++ b/tests/DataFuse.Integration.Tests/Helpers/Xml/XDocumentExtsTests.cs @@ -0,0 +1,134 @@ +using DataFuse.Integration.Helpers.Xml; + +namespace DataFuse.Integration.Tests.Helpers.Xml +{ + [TestFixture] + public class XDocumentExtsTests + { + [Test] + public void RemoveNamespaces_WithNamespacedDocument_ShouldRemoveNamespaces() + { + // Arrange + var xmlWithNamespaces = @" + + Content1 + Content2 + "; + var document = System.Xml.Linq.XDocument.Parse(xmlWithNamespaces); + + // Act + document.RemoveNamespaces(); + + // Assert + Assert.That(document.Root.Name.LocalName, Is.EqualTo("root")); + Assert.That(document.Root.Name.Namespace, Is.EqualTo(System.Xml.Linq.XNamespace.None)); + + var element1 = document.Root.Element("element1"); + var element2 = document.Root.Element("element2"); + + Assert.That(element1, Is.Not.Null); + Assert.That(element1.Name.Namespace, Is.EqualTo(System.Xml.Linq.XNamespace.None)); + Assert.That(element2, Is.Not.Null); + Assert.That(element2.Name.Namespace, Is.EqualTo(System.Xml.Linq.XNamespace.None)); + } + + [Test] + public void RemoveNamespaces_WithNullRoot_ShouldNotThrow() + { + // Arrange + var document = new System.Xml.Linq.XDocument(); + + // Act & Assert + Assert.DoesNotThrow(() => document.RemoveNamespaces()); + } + + [Test] + public void RemoveNamespaces_WithSimpleDocument_ShouldNotChangeStructure() + { + // Arrange + var simpleXml = @"Content"; + var document = System.Xml.Linq.XDocument.Parse(simpleXml); + + // Act + document.RemoveNamespaces(); + + // Assert + Assert.That(document.Root.Name.LocalName, Is.EqualTo("root")); + Assert.That(document.Root.Element("child").Value, Is.EqualTo("Content")); + } + + [Test] + public void RemoveNamespaces_WithAttributeNamespaces_ShouldRemoveNamespaceAttributes() + { + // Arrange + var xmlWithAttributeNamespaces = @"Content"; + var document = System.Xml.Linq.XDocument.Parse(xmlWithAttributeNamespaces); + + // Act + document.RemoveNamespaces(); + + // Assert + var rootElement = document.Root; + Assert.That(rootElement.Attribute("attr"), Is.Not.Null); + Assert.That(rootElement.Attribute("attr").Value, Is.EqualTo("value")); + + // Namespace declaration attributes should be removed + var namespaceAttrs = rootElement.Attributes().Where(a => a.IsNamespaceDeclaration); + Assert.That(namespaceAttrs.Count(), Is.EqualTo(0)); + } + + [Test] + public void RemoveNamespaces_WithNestedNamespaces_ShouldRemoveAllNamespaces() + { + // Arrange + var nestedXml = @" + + + + Content + + + "; + var document = System.Xml.Linq.XDocument.Parse(nestedXml); + + // Act + document.RemoveNamespaces(); + + // Assert + Assert.That(document.Root.Name.LocalName, Is.EqualTo("root")); + + var level1 = document.Root.Element("level1"); + var level2 = level1?.Element("level2"); + var level3 = level2?.Element("level3"); + + Assert.That(level1, Is.Not.Null); + Assert.That(level1.Name.Namespace, Is.EqualTo(System.Xml.Linq.XNamespace.None)); + Assert.That(level2, Is.Not.Null); + Assert.That(level2.Name.Namespace, Is.EqualTo(System.Xml.Linq.XNamespace.None)); + Assert.That(level3, Is.Not.Null); + Assert.That(level3.Name.Namespace, Is.EqualTo(System.Xml.Linq.XNamespace.None)); + Assert.That(level3.Value, Is.EqualTo("Content")); + } + + [Test] + public void RemoveNamespaces_ShouldPreserveElementContent() + { + // Arrange + var xmlContent = @" + Text Content + + Nested Content + + "; + var document = System.Xml.Linq.XDocument.Parse(xmlContent); + + // Act + document.RemoveNamespaces(); + + // Assert + Assert.That(document.Root.Element("child1").Value, Is.EqualTo("Text Content")); + Assert.That(document.Root.Element("child2").Element("grandchild").Value, Is.EqualTo("Nested Content")); + Assert.That(document.Root.Element("child2").Attribute("attr").Value, Is.EqualTo("attrValue")); + } + } +} \ No newline at end of file diff --git a/tests/DataFuse.Integration.Tests/Helpers/Xml/XmlHelperTests.cs b/tests/DataFuse.Integration.Tests/Helpers/Xml/XmlHelperTests.cs new file mode 100644 index 0000000..7dfaa58 --- /dev/null +++ b/tests/DataFuse.Integration.Tests/Helpers/Xml/XmlHelperTests.cs @@ -0,0 +1,232 @@ +using System.Xml; +using System.Xml.Serialization; +using DataFuse.Integration.Helpers.Xml; + +namespace DataFuse.Integration.Tests.Helpers.Xml +{ + [TestFixture] + public class XmlHelperTests + { + [Test] + public void SerializeToXml_WithValidObject_ShouldReturnXmlString() + { + // Arrange + var testObject = new TestSerializableEntity { Name = "Test", Value = 123 }; + var namespaces = new XmlSerializerNamespaces(); + var settings = new XmlWriterSettings + { + OmitXmlDeclaration = true, + Indent = false + }; + + // Act + var result = XmlHelper.SerializeToXml(testObject, namespaces, settings); + + // Assert + Assert.That(result, Is.Not.Null); + Assert.That(result, Contains.Substring("Test")); + Assert.That(result, Contains.Substring("123")); + Assert.That(result, Contains.Substring("TestSerializableEntity")); + } + + [Test] + public void SerializeToXml_WithNamespaces_ShouldIncludeNamespaces() + { + // Arrange + var testObject = new TestSerializableEntity { Name = "Test", Value = 123 }; + var namespaces = new XmlSerializerNamespaces(); + namespaces.Add("test", "http://test.namespace"); + var settings = new XmlWriterSettings + { + OmitXmlDeclaration = true, + Indent = false + }; + + // Act + var result = XmlHelper.SerializeToXml(testObject, namespaces, settings); + + // Assert + Assert.That(result, Is.Not.Null); + Assert.That(result, Contains.Substring("http://test.namespace")); + } + + [Test] + public void SerializeToXml_WithXmlDeclaration_ShouldIncludeDeclaration() + { + // Arrange + var testObject = new TestSerializableEntity { Name = "Test", Value = 123 }; + var namespaces = new XmlSerializerNamespaces(); + var settings = new XmlWriterSettings + { + OmitXmlDeclaration = false, + Indent = false + }; + + // Act + var result = XmlHelper.SerializeToXml(testObject, namespaces, settings); + + // Assert + Assert.That(result, Is.Not.Null); + Assert.That(result, Does.StartWith("(() => + XmlHelper.SerializeToXml(testObject, namespaces, settings)); + } + + [Test] + public void SerializeToXml_WithNullNamespaces_ShouldStillWork() + { + // Arrange + var testObject = new TestSerializableEntity { Name = "Test", Value = 123 }; + XmlSerializerNamespaces namespaces = null; + var settings = new XmlWriterSettings + { + OmitXmlDeclaration = true, + Indent = false + }; + + // Act + var result = XmlHelper.SerializeToXml(testObject, namespaces, settings); + + // Assert + Assert.That(result, Is.Not.Null); + Assert.That(result, Contains.Substring("Test")); + } + + [Test] + public void SerializeToXml_WithNullSettings_ShouldThrowException() + { + // Arrange + var testObject = new TestSerializableEntity { Name = "Test", Value = 123 }; + var namespaces = new XmlSerializerNamespaces(); + XmlWriterSettings settings = null; + + // Act & Assert + Assert.Throws(() => + XmlHelper.SerializeToXml(testObject, namespaces, settings)); + } + + [Test] + public void SerializeToXml_WithComplexObject_ShouldSerializeAllProperties() + { + // Arrange + var complexObject = new ComplexSerializableEntity + { + Id = 1, + Name = "Complex", + Child = new TestSerializableEntity { Name = "Child", Value = 456 }, + Items = new[] { "item1", "item2", "item3" } + }; + var namespaces = new XmlSerializerNamespaces(); + var settings = new XmlWriterSettings + { + OmitXmlDeclaration = true, + Indent = true + }; + + // Act + var result = XmlHelper.SerializeToXml(complexObject, namespaces, settings); + + // Assert + Assert.That(result, Is.Not.Null); + Assert.That(result, Contains.Substring("Complex")); + Assert.That(result, Contains.Substring("Child")); + Assert.That(result, Contains.Substring("456")); + Assert.That(result, Contains.Substring("item1")); + Assert.That(result, Contains.Substring("item2")); + Assert.That(result, Contains.Substring("item3")); + } + + [Test] + public void SerializeToXml_WithEmptyNamespaces_ShouldNotIncludeCustomNamespaces() + { + // Arrange + var testObject = new TestSerializableEntity { Name = "Test", Value = 123 }; + var namespaces = new XmlSerializerNamespaces(); + var settings = new XmlWriterSettings + { + OmitXmlDeclaration = true, + Indent = false + }; + + // Act + var result = XmlHelper.SerializeToXml(testObject, namespaces, settings); + + // Assert + Assert.That(result, Is.Not.Null); + Assert.That(result, Contains.Substring("Test")); + // Should still have default XML namespace declarations + } + + [Test] + public void SerializeToXml_WithMultipleNamespaces_ShouldIncludeAll() + { + // Arrange + var testObject = new TestSerializableEntity { Name = "Test", Value = 123 }; + var namespaces = new XmlSerializerNamespaces(); + namespaces.Add("ns1", "http://namespace1.com"); + namespaces.Add("ns2", "http://namespace2.com"); + namespaces.Add("ns3", "http://namespace3.com"); + var settings = new XmlWriterSettings + { + OmitXmlDeclaration = true, + Indent = false + }; + + // Act + var result = XmlHelper.SerializeToXml(testObject, namespaces, settings); + + // Assert + Assert.That(result, Is.Not.Null); + Assert.That(result, Contains.Substring("http://namespace1.com")); + Assert.That(result, Contains.Substring("http://namespace2.com")); + Assert.That(result, Contains.Substring("http://namespace3.com")); + } + + // Test classes for serialization + public class TestSerializableEntity + { + public string Name { get; set; } + public int Value { get; set; } + } + + public class ComplexSerializableEntity + { + public int Id { get; set; } + public string Name { get; set; } + public TestSerializableEntity Child { get; set; } + public string[] Items { get; set; } + } + } +} \ No newline at end of file diff --git a/tests/DataFuse.Integration.Tests/Helpers/Xml/XmlSanitizerTests.cs b/tests/DataFuse.Integration.Tests/Helpers/Xml/XmlSanitizerTests.cs new file mode 100644 index 0000000..b5676ba --- /dev/null +++ b/tests/DataFuse.Integration.Tests/Helpers/Xml/XmlSanitizerTests.cs @@ -0,0 +1,182 @@ +using DataFuse.Integration.Helpers.Xml; + +namespace DataFuse.Integration.Tests.Helpers.Xml +{ + [TestFixture] + public class XmlSanitizerTests + { + [Test] + public void Sanitize_WithNullInput_ShouldReturnNull() + { + // Act + var result = XmlSanitizer.Sanitize(null); + + // Assert + Assert.That(result, Is.Null); + } + + [Test] + public void Sanitize_WithEmptyString_ShouldReturnEmptyString() + { + // Act + var result = XmlSanitizer.Sanitize(string.Empty); + + // Assert + Assert.That(result, Is.EqualTo(string.Empty)); + } + + [Test] + public void Sanitize_WithValidXml_ShouldReturnUnchanged() + { + // Arrange + var validXml = "Valid content"; + + // Act + var result = XmlSanitizer.Sanitize(validXml); + + // Assert + Assert.That(result, Is.EqualTo(validXml)); + } + + [Test] + public void Sanitize_WithValidNumericEntity_ShouldReturnUnchanged() + { + // Arrange + var xmlWithEntity = "ABC"; // ABC + + // Act + var result = XmlSanitizer.Sanitize(xmlWithEntity); + + // Assert + Assert.That(result, Is.EqualTo(xmlWithEntity)); + } + + [Test] + public void Sanitize_WithValidHexEntity_ShouldReturnUnchanged() + { + // Arrange + var xmlWithHexEntity = "ABC"; // ABC + + // Act + var result = XmlSanitizer.Sanitize(xmlWithHexEntity); + + // Assert + Assert.That(result, Is.EqualTo(xmlWithHexEntity)); + } + + [Test] + public void Sanitize_WithInvalidControlCharacterEntity_ShouldRemoveEntity() + { + // Arrange + var xmlWithInvalidEntity = ""; // NULL character + + // Act + var result = XmlSanitizer.Sanitize(xmlWithInvalidEntity); + + // Assert + Assert.That(result, Is.EqualTo("")); + } + + [Test] + public void Sanitize_WithMultipleInvalidEntities_ShouldRemoveAllInvalid() + { + // Arrange + var xmlWithInvalidEntities = "�ValidContent"; + + // Act + var result = XmlSanitizer.Sanitize(xmlWithInvalidEntities); + + // Assert + Assert.That(result, Is.EqualTo("ValidContent")); + } + + [Test] + public void Sanitize_WithMixedValidAndInvalidEntities_ShouldKeepValidRemoveInvalid() + { + // Arrange + var xmlWithMixedEntities = "A�BC"; // A[invalid]B[invalid]C + + // Act + var result = XmlSanitizer.Sanitize(xmlWithMixedEntities); + + // Assert + Assert.That(result, Is.EqualTo("ABC")); + } + + [Test] + public void Sanitize_WithNamedEntities_ShouldKeepNamedEntities() + { + // Arrange + var xmlWithNamedEntities = "&<>"'"; + + // Act + var result = XmlSanitizer.Sanitize(xmlWithNamedEntities); + + // Assert + Assert.That(result, Is.EqualTo(xmlWithNamedEntities)); + } + + [Test] + public void Sanitize_WithInvalidNumericEntity_ShouldReturnOriginalEntity() + { + // Arrange + var xmlWithInvalidNumber = "&#ABC;"; // Invalid number + + // Act + var result = XmlSanitizer.Sanitize(xmlWithInvalidNumber); + + // Assert + Assert.That(result, Is.EqualTo(xmlWithInvalidNumber)); // Should remain unchanged + } + + [Test] + public void Sanitize_WithHexEntityWithoutXPrefix_ShouldRemoveInvalidCharacter() + { + // Arrange + var xmlWithInvalidHex = ""; // NULL character in hex + + // Act + var result = XmlSanitizer.Sanitize(xmlWithInvalidHex); + + // Assert + Assert.That(result, Is.EqualTo("")); + } + + [Test] + public void Sanitize_WithComplexXmlContainingInvalidEntities_ShouldSanitizeCorrectly() + { + // Arrange + var complexXml = @" + Valid content A here + �BadContentA + "; + + // Act + var result = XmlSanitizer.Sanitize(complexXml); + + // Assert + Assert.That(result, Contains.Substring("Valid content A here")); + Assert.That(result, Contains.Substring("BadContentA")); + Assert.That(result, Does.Not.Contain("�")); + Assert.That(result, Does.Not.Contain("")); + Assert.That(result, Does.Not.Contain("")); + } + + [Test] + public void Sanitize_WithLargeDocument_ShouldProcessEfficiently() + { + // Arrange + var largeXml = ""; + for (var i = 0; i < 1000; i++) + largeXml += $"Content {i} &#{i % 32}; more content"; + largeXml += ""; + + // Act & Assert + Assert.DoesNotThrow(() => XmlSanitizer.Sanitize(largeXml)); + + var result = XmlSanitizer.Sanitize(largeXml); + Assert.That(result, Is.Not.Null); + Assert.That(result.Length, Is.LessThanOrEqualTo(largeXml.Length)); // Should be same or smaller after sanitization + } + } +} \ No newline at end of file diff --git a/tests/DataFuse.Integration.Tests/PathMatchers/JPathMatcherTests.cs b/tests/DataFuse.Integration.Tests/PathMatchers/JPathMatcherTests.cs new file mode 100644 index 0000000..af8858a --- /dev/null +++ b/tests/DataFuse.Integration.Tests/PathMatchers/JPathMatcherTests.cs @@ -0,0 +1,172 @@ +using DataFuse.Integration.PathMatchers; + +namespace DataFuse.Integration.Tests.PathMatchers +{ + [TestFixture] + public class JPathMatcherTests + { + private JPathMatcher _matcher; + + [SetUp] + public void Setup() + { + _matcher = new JPathMatcher(); + } + + [Test] + public void IsMatch_WithNullOrEmptyInputPath_ShouldReturnFalse() + { + // Arrange + var configuredPaths = new SchemaPaths { Paths = new[] { "$.root.test" } }; + + // Act + var result1 = _matcher.IsMatch(null, configuredPaths); + var result2 = _matcher.IsMatch("", configuredPaths); + + // Assert + Assert.That(result1, Is.False); + Assert.That(result2, Is.False); + } + + [Test] + public void IsMatch_WithExactMatch_ShouldReturnTrue() + { + // Arrange + var inputPath = "$.root.test.element"; + var configuredPaths = new SchemaPaths { Paths = new[] { "$.root.test" } }; + + // Act + var result = _matcher.IsMatch(inputPath, configuredPaths); + + // Assert + Assert.That(result, Is.True); + } + + [Test] + public void IsMatch_WithCaseInsensitiveMatch_ShouldReturnTrue() + { + // Arrange + var inputPath = "$.ROOT.TEST.element"; + var configuredPaths = new SchemaPaths { Paths = new[] { "$.root.test" } }; + + // Act + var result = _matcher.IsMatch(inputPath, configuredPaths); + + // Assert + Assert.That(result, Is.True); + } + + [Test] + public void IsMatch_WithNoMatch_ShouldReturnFalse() + { + // Arrange + var inputPath = "$.different.path.element"; + var configuredPaths = new SchemaPaths { Paths = new[] { "$.root.test" } }; + + // Act + var result = _matcher.IsMatch(inputPath, configuredPaths); + + // Assert + Assert.That(result, Is.False); + } + + [Test] + public void IsMatch_WithMultipleConfiguredPaths_ShouldMatchAny() + { + // Arrange + var inputPath = "$.root.second.element"; + var configuredPaths = new SchemaPaths + { + Paths = new[] { "$.root.first", "$.root.second", "$.root.third" } + }; + + // Act + var result = _matcher.IsMatch(inputPath, configuredPaths); + + // Assert + Assert.That(result, Is.True); + } + + [Test] + public void IsMatch_WithPartialPath_ShouldReturnTrue() + { + // Arrange + var inputPath = "$.root.test.child.grandchild"; + var configuredPaths = new SchemaPaths { Paths = new[] { "test.child" } }; + + // Act + var result = _matcher.IsMatch(inputPath, configuredPaths); + + // Assert + Assert.That(result, Is.True); + } + + [Test] + public void IsMatch_WithArrayNotation_ShouldReturnTrue() + { + // Arrange + var inputPath = "$.root.items[0].name"; + var configuredPaths = new SchemaPaths { Paths = new[] { "items[0]" } }; + + // Act + var result = _matcher.IsMatch(inputPath, configuredPaths); + + // Assert + Assert.That(result, Is.True); + } + + [Test] + public void IsMatch_WithEmptyConfiguredPaths_ShouldReturnFalse() + { + // Arrange + var inputPath = "$.root.test"; + var configuredPaths = new SchemaPaths { Paths = new string[0] }; + + // Act + var result = _matcher.IsMatch(inputPath, configuredPaths); + + // Assert + Assert.That(result, Is.False); + } + + [Test] + public void IsMatch_WithNullConfiguredPaths_ShouldThrow() + { + // Arrange + var inputPath = "$.root.test"; + var configuredPaths = new SchemaPaths { Paths = null }; + + // Act & Assert + Assert.Throws(() => + _matcher.IsMatch(inputPath, configuredPaths)); + } + + [Test] + public void IsMatch_WithComplexJsonPath_ShouldReturnTrue() + { + // Arrange + var inputPath = "$.store.book[*].author"; + var configuredPaths = new SchemaPaths { Paths = new[] { "book[*].author" } }; + + // Act + var result = _matcher.IsMatch(inputPath, configuredPaths); + + // Assert + Assert.That(result, Is.True); + } + + [Test] + public void IsMatch_WithWildcardInConfigured_ShouldReturnTrue() + { + // Arrange + var inputPath = "$.root.items.specific.value"; + var configuredPaths = new SchemaPaths { Paths = new[] { "items.specific" } }; + + // Act + var result = _matcher.IsMatch(inputPath, configuredPaths); + + // Assert + Assert.That(result, Is.True); + } + } +} \ No newline at end of file diff --git a/tests/DataFuse.Integration.Tests/PathMatchers/XPathMatcherTests.cs b/tests/DataFuse.Integration.Tests/PathMatchers/XPathMatcherTests.cs new file mode 100644 index 0000000..41b35b0 --- /dev/null +++ b/tests/DataFuse.Integration.Tests/PathMatchers/XPathMatcherTests.cs @@ -0,0 +1,171 @@ +using DataFuse.Integration.PathMatchers; + +namespace DataFuse.Integration.Tests.PathMatchers +{ + [TestFixture] + public class XPathMatcherTests + { + private XPathMatcher _matcher; + + [SetUp] + public void Setup() + { + _matcher = new XPathMatcher(); + } + + [Test] + public void IsMatch_WithNullInputPath_ShouldReturnFalse() + { + // Arrange + string inputPath = null; + var configuredPaths = new SchemaPaths { Paths = new[] { "/root/test" } }; + + // Act + var result = _matcher.IsMatch(inputPath, configuredPaths); + + // Assert + Assert.That(result, Is.False); + } + + [Test] + public void IsMatch_WithExactMatch_ShouldReturnTrue() + { + // Arrange + var inputPath = "/root/test/element"; + var configuredPaths = new SchemaPaths { Paths = new[] { "/root/test" } }; + + // Act + var result = _matcher.IsMatch(inputPath, configuredPaths); + + // Assert + Assert.That(result, Is.True); + } + + [Test] + public void IsMatch_WithCaseInsensitiveMatch_ShouldReturnTrue() + { + // Arrange + var inputPath = "/ROOT/TEST/element"; + var configuredPaths = new SchemaPaths { Paths = new[] { "/root/test" } }; + + // Act + var result = _matcher.IsMatch(inputPath, configuredPaths); + + // Assert + Assert.That(result, Is.True); + } + + [Test] + public void IsMatch_WithNoMatch_ShouldReturnFalse() + { + // Arrange + var inputPath = "/different/path/element"; + var configuredPaths = new SchemaPaths { Paths = new[] { "/root/test" } }; + + // Act + var result = _matcher.IsMatch(inputPath, configuredPaths); + + // Assert + Assert.That(result, Is.False); + } + + [Test] + public void IsMatch_WithMultipleConfiguredPaths_ShouldMatchAny() + { + // Arrange + var inputPath = "/root/second/element"; + var configuredPaths = new SchemaPaths + { + Paths = new[] { "/root/first", "/root/second", "/root/third" } + }; + + // Act + var result = _matcher.IsMatch(inputPath, configuredPaths); + + // Assert + Assert.That(result, Is.True); + } + + [Test] + public void IsMatch_WithAncestorAxis_ShouldReturnTrue() + { + // Arrange + var inputPath = "/root/element=ancestor::parent/@attribute"; + var configuredPaths = new SchemaPaths { Paths = new[] { "/root/parent" } }; + + // Act + var result = _matcher.IsMatch(inputPath, configuredPaths); + + // Assert + Assert.That(result, Is.True); + } + + [Test] + public void IsMatch_WithAncestorAxisAndPredicate_ShouldReturnTrue() + { + // Arrange + var inputPath = "/root/element=ancestor::parent[position()=1]/@attribute"; + var configuredPaths = new SchemaPaths { Paths = new[] { "/root/parent" } }; + + // Act + var result = _matcher.IsMatch(inputPath, configuredPaths); + + // Assert + Assert.That(result, Is.True); + } + + [Test] + public void IsMatch_WithAncestorAxisNoMatch_ShouldReturnFalse() + { + // Arrange + var inputPath = "/root/element=ancestor::different/@attribute"; + var configuredPaths = new SchemaPaths { Paths = new[] { "/root/parent" } }; + + // Act + var result = _matcher.IsMatch(inputPath, configuredPaths); + + // Assert + Assert.That(result, Is.False); + } + + [Test] + public void IsMatch_WithPartialPath_ShouldReturnTrue() + { + // Arrange + var inputPath = "/root/test/child/grandchild"; + var configuredPaths = new SchemaPaths { Paths = new[] { "test/child" } }; + + // Act + var result = _matcher.IsMatch(inputPath, configuredPaths); + + // Assert + Assert.That(result, Is.True); + } + + [Test] + public void IsMatch_WithEmptyConfiguredPaths_ShouldReturnFalse() + { + // Arrange + var inputPath = "/root/test"; + var configuredPaths = new SchemaPaths { Paths = new string[0] }; + + // Act + var result = _matcher.IsMatch(inputPath, configuredPaths); + + // Assert + Assert.That(result, Is.False); + } + + [Test] + public void IsMatch_WithNullConfiguredPaths_ShouldThrow() + { + // Arrange + var inputPath = "/root/test"; + var configuredPaths = new SchemaPaths { Paths = null }; + + // Act & Assert + Assert.Throws(() => + _matcher.IsMatch(inputPath, configuredPaths)); + } + } +} \ No newline at end of file diff --git a/tests/DataFuse.Integration.Tests/QueryListTests.cs b/tests/DataFuse.Integration.Tests/QueryListTests.cs new file mode 100644 index 0000000..6190f70 --- /dev/null +++ b/tests/DataFuse.Integration.Tests/QueryListTests.cs @@ -0,0 +1,332 @@ +namespace DataFuse.Integration.Tests +{ + [TestFixture] + public class QueryListTests + { + [Test] + public void DefaultConstructor_ShouldCreateEmptyQueryList() + { + // Act + var queryList = new QueryList(); + + // Assert + Assert.That(queryList.Queries, Is.Not.Null); + Assert.That(queryList.Count(), Is.EqualTo(0)); + Assert.That(queryList.IsEmpty(), Is.True); + } + + [Test] + public void Constructor_WithCollection_ShouldInitializeWithQueries() + { + // Arrange + var queries = new List { new TestQuery(), new TestChildQuery() }; + + // Act + var queryList = new QueryList(queries); + + // Assert + Assert.That(queryList.Count(), Is.EqualTo(2)); + Assert.That(queryList.IsEmpty(), Is.False); + Assert.That(queryList.Queries.Count(), Is.EqualTo(2)); + } + + [Test] + public void Constructor_WithNullCollection_ShouldThrowArgumentNullException() + { + // Act & Assert + Assert.Throws(() => new QueryList(null)); + } + + [Test] + public void Constructor_WithEmptyCollection_ShouldCreateEmptyQueryList() + { + // Arrange + var emptyQueries = new List(); + + // Act + var queryList = new QueryList(emptyQueries); + + // Assert + Assert.That(queryList.Count(), Is.EqualTo(0)); + Assert.That(queryList.IsEmpty(), Is.True); + } + + [Test] + public void QueryDependencyDepth_ShouldBeSettableAndGettable() + { + // Arrange + var queryList = new QueryList(); + + // Act + queryList.QueryDependencyDepth = 5; + + // Assert + Assert.That(queryList.QueryDependencyDepth, Is.EqualTo(5)); + } + + [Test] + public void Queries_ShouldReturnReadOnlyEnumerable() + { + // Arrange + var queries = new List { new TestQuery() }; + var queryList = new QueryList(queries); + + // Act + var result = queryList.Queries; + + // Assert + Assert.That(result, Is.Not.Null); + Assert.That(result.Count(), Is.EqualTo(1)); + } + + [Test] + public void GetByType_ShouldReturnQueriesOfSpecificType() + { + // Arrange + var queries = new List + { + new TestQuery(), + new TestChildQuery(), + new TestQuery() + }; + var queryList = new QueryList(queries); + + // Act + var testQueries = queryList.GetByType(); + + // Assert + Assert.That(testQueries.Count(), Is.EqualTo(2)); + } + + [Test] + public void GetByType_WithNoMatchingType_ShouldReturnEmptyQueryList() + { + // Arrange + var queries = new List { new TestQuery() }; + var queryList = new QueryList(queries); + + // Act + var result = queryList.GetByType(); + + // Assert + Assert.That(result.Count(), Is.EqualTo(0)); + Assert.That(result.IsEmpty(), Is.True); + } + + [Test] + public void As_ShouldReturnListOfSpecificType() + { + // Arrange + var queries = new List { new TestQuery(), new TestChildQuery() }; + var queryList = new QueryList(queries); + + // Act + var result = queryList.As(); + + // Assert + Assert.That(result, Is.InstanceOf>()); + Assert.That(result.Count, Is.EqualTo(2)); + } + + [Test] + public void GetChildrenQueries_WithQueriesHavingChildren_ShouldReturnChildrenQueries() + { + // Arrange + var childQuery1 = new TestChildQuery(); + var childQuery2 = new TestChildQuery(); + + var parentQuery = new TestQuery + { + Children = new List { childQuery1, childQuery2 } + }; + + var queryList = new QueryList(new List { parentQuery }); + + // Act + var childrenQueries = queryList.GetChildrenQueries(); + + // Assert + Assert.That(childrenQueries, Is.Not.Null); + Assert.That(childrenQueries.Count, Is.EqualTo(1)); + Assert.That(childrenQueries[0].ParentQueryResultType, Is.EqualTo(typeof(TestQueryResult))); + Assert.That(childrenQueries[0].Queries.Count, Is.EqualTo(1)); + } + + [Test] + public void GetChildrenQueries_WithNoChildren_ShouldReturnEmptyList() + { + // Arrange + var parentQuery = new TestQuery { Children = null }; + var queryList = new QueryList(new List { parentQuery }); + + // Act + var childrenQueries = queryList.GetChildrenQueries(); + + // Assert + Assert.That(childrenQueries, Is.Not.Null); + Assert.That(childrenQueries.Count, Is.EqualTo(0)); + } + + [Test] + public void GetChildrenQueries_WithEmptyChildren_ShouldReturnEmptyList() + { + // Arrange + var parentQuery = new TestQuery { Children = new List() }; + var queryList = new QueryList(new List { parentQuery }); + + // Act + var childrenQueries = queryList.GetChildrenQueries(); + + // Assert + Assert.That(childrenQueries, Is.Not.Null); + Assert.That(childrenQueries.Count, Is.EqualTo(0)); + } + + [Test] + public void GetChildrenQueries_ShouldGroupByParentResultType() + { + // Arrange + var child1 = new TestChildQuery(); + var child2 = new TestChildQuery(); + + var parent1 = new TestQuery { Children = new List { child1 } }; + var parent2 = new TestQuery { Children = new List { child2 } }; + + var queryList = new QueryList(new List { parent1, parent2 }); + + // Act + var childrenQueries = queryList.GetChildrenQueries(); + + // Assert + Assert.That(childrenQueries.Count, Is.EqualTo(2)); // Should be grouped by result type + Assert.That(childrenQueries[0].ParentQueryResultType, Is.EqualTo(typeof(TestQueryResult))); + Assert.That(childrenQueries[0].Queries.Count, Is.EqualTo(1)); // Should contain both children + } + + [Test] + public void GetChildrenQueries_ShouldRemoveDuplicateQueries() + { + // Arrange + var duplicateChild = new TestChildQuery(); + + var parent = new TestQuery + { + Children = new List { duplicateChild, duplicateChild } + }; + + var queryList = new QueryList(new List { parent }); + + // Act + var childrenQueries = queryList.GetChildrenQueries(); + + // Assert + Assert.That(childrenQueries.Count, Is.EqualTo(1)); + Assert.That(childrenQueries[0].Queries.Count, Is.EqualTo(1)); // Duplicates should be removed + } + + [Test] + public void Count_ShouldReturnCorrectCount() + { + // Arrange + var queries = new List { new TestQuery(), new TestChildQuery(), new TestQuery() }; + var queryList = new QueryList(queries); + + // Act + var count = queryList.Count(); + + // Assert + Assert.That(count, Is.EqualTo(3)); + } + + [Test] + public void IsEmpty_WithQueries_ShouldReturnFalse() + { + // Arrange + var queries = new List { new TestQuery() }; + var queryList = new QueryList(queries); + + // Act + var isEmpty = queryList.IsEmpty(); + + // Assert + Assert.That(isEmpty, Is.False); + } + + [Test] + public void IsEmpty_WithoutQueries_ShouldReturnTrue() + { + // Arrange + var queryList = new QueryList(); + + // Act + var isEmpty = queryList.IsEmpty(); + + // Assert + Assert.That(isEmpty, Is.True); + } + + [Test] + public void AddRange_ShouldAddQueriesToList() + { + // Arrange + var queryList = new QueryList(); + var newQueries = new List { new TestQuery(), new TestChildQuery() }; + + // Act + queryList.AddRange(newQueries); + + // Assert + Assert.That(queryList.Count(), Is.EqualTo(2)); + Assert.That(queryList.IsEmpty(), Is.False); + } + + [Test] + public void AddRange_WithExistingQueries_ShouldAppendToList() + { + // Arrange + var initialQueries = new List { new TestQuery() }; + var queryList = new QueryList(initialQueries); + var additionalQueries = new List { new TestChildQuery() }; + + // Act + queryList.AddRange(additionalQueries); + + // Assert + Assert.That(queryList.Count(), Is.EqualTo(2)); + } + + [Test] + public void AddRange_WithNullCollection_ShouldThrowArgumentNullException() + { + // Arrange + var queryList = new QueryList(); + + // Act & Assert + Assert.Throws(() => queryList.AddRange(null)); + } + + [Test] + public void AddRange_WithEmptyCollection_ShouldNotChangeCount() + { + // Arrange + var queryList = new QueryList(new List { new TestQuery() }); + var emptyQueries = new List(); + + // Act + queryList.AddRange(emptyQueries); + + // Assert + Assert.That(queryList.Count(), Is.EqualTo(1)); + } + + [Test] + public void QueryList_ShouldImplementIQueryList() + { + // Arrange + var queryList = new QueryList(); + + // Assert + Assert.That(queryList, Is.InstanceOf()); + } + } +} \ No newline at end of file diff --git a/tests/DataFuse.Integration.Tests/ServicesExtensionsTests.cs b/tests/DataFuse.Integration.Tests/ServicesExtensionsTests.cs new file mode 100644 index 0000000..cc26601 --- /dev/null +++ b/tests/DataFuse.Integration.Tests/ServicesExtensionsTests.cs @@ -0,0 +1,344 @@ +using Microsoft.Extensions.DependencyInjection; +using DataFuse.Integration; +using Moq; +using DataFuse.Integration.Impl; +using DataFuse.Integration.PathMatchers; + +namespace DataFuse.Integration.Tests +{ + [TestFixture] + public class ServicesExtensionsTests + { + private ServiceCollection _services; + + [SetUp] + public void Setup() + { + _services = new ServiceCollection(); + } + + [Test] + public void UseDataFuse_ShouldRegisterCoreServices() + { + var mockConfiguration = new Mock>(); + Func> factory = provider => mockConfiguration.Object; + + // Act + _services.UseDataFuse(config => + { + config.WithEntityConfiguration(factory); + config.WithEngine(); + config.WithPathMatcher(provider => new XPathMatcher()); + }); + + // Assert + var serviceProvider = _services.BuildServiceProvider(); + + // Verify core services are registered + Assert.That(serviceProvider.GetService(typeof(IQueryBuilder)), Is.Not.Null); + Assert.That(serviceProvider.GetService(typeof(IEntityBuilder)), Is.Not.Null); + Assert.That(serviceProvider.GetService(typeof(IDataProvider)), Is.Not.Null); + Assert.That(serviceProvider.GetService(), Is.Not.Null); + Assert.That(serviceProvider.GetService(), Is.InstanceOf()); + } + + [Test] + public void UseDataFuse_ShouldReturnDataFuseOptionsBuilder() + { + // Act + DataFuseOptionsBuilder result = null; + _services.UseDataFuse(c => + { + c.InSilentMode(); + result = (DataFuseOptionsBuilder)c; + }); + + // Assert + Assert.That(result, Is.InstanceOf()); + Assert.That(result.Services, Is.SameAs(_services)); + } + + [Test] + public void WithEngine_WithFactory_ShouldRegisterQueryEngine() + { + // Arrange + var mockEngine = new Mock(); + Func factory = provider => mockEngine.Object; + + // Act + _services.UseDataFuse(c => c + .InSilentMode() + .WithEngine(factory)); + + // Assert + var serviceProvider = _services.BuildServiceProvider(); + var engine = serviceProvider.GetService(); + Assert.That(engine, Is.SameAs(mockEngine.Object)); + } + + [Test] + public void WithEngine_WithNullFactory_ShouldNotThrow() + { + // Act & Assert + Assert.DoesNotThrow(() => _services.UseDataFuse(c => c + .InSilentMode() + .WithEngine((Func)null))); + } + + [Test] + public void WithEngine_Generic_ShouldRegisterQueryEngine() + { + // Act + _services.UseDataFuse(c => c + .InSilentMode() + .WithEngine()); + + // Assert + var serviceProvider = _services.BuildServiceProvider(); + var engine = serviceProvider.GetService(); + Assert.That(engine, Is.InstanceOf()); + } + + [Test] + public void WithEngines_WithFactory_ShouldRegisterQueryEngineArray() + { + // Arrange + var mockEngine1 = new Mock(); + var mockEngine2 = new Mock(); + var engines = new[] { mockEngine1.Object, mockEngine2.Object }; + Func factory = provider => engines; + + // Act + _services.UseDataFuse(c => c + .InSilentMode() + .WithEngines(factory)); + + var serviceProvider = _services.BuildServiceProvider(); + var registeredEngines = serviceProvider.GetService(); + Assert.That(registeredEngines, Is.SameAs(engines)); + } + + [Test] + public void WithEngines_WithNullFactory_ShouldNotThrow() + { + // Act & Assert + Assert.DoesNotThrow(() => _services.UseDataFuse(c => c + .InSilentMode() + .WithEngines(null))); + } + + [Test] + public void WithPathMatcher_ShouldReplaceDefaultPathMatcher() + { + // Arrange + var mockPathMatcher = new Mock(); + Func factory = provider => mockPathMatcher.Object; + + // Act + _services.UseDataFuse(c => c + .InSilentMode() + .WithPathMatcher(factory)); + + // Assert + var serviceProvider = _services.BuildServiceProvider(); + var pathMatcher = serviceProvider.GetService(); + Assert.That(pathMatcher, Is.SameAs(mockPathMatcher.Object)); + } + + [Test] + public void WithPathMatcher_WithNullFactory_ShouldNotThrow() + { + // Act & Assert + Assert.DoesNotThrow(() => _services.UseDataFuse(c => c + .InSilentMode() + .WithPathMatcher(null))); + } + + [Test] + public void WithNullSchemioConfig_ShouldThrow() + { + // Act & Assert + Assert.Throws(() => _services.UseDataFuse(null)); + } + + [Test] + public void WithNullServices_ShouldThrow() + { + // Arrange + IServiceCollection nullServices = null; + // Act & Assert + Assert.Throws(() => nullServices.UseDataFuse(c => c.WithPathMatcher(null))); + } + + [Test] + public void WithNoEntityConfiguration_ShouldThrow() + { + // Arrange + var mockConfiguration = new Mock>(); + Func> factory = provider => mockConfiguration.Object; + + // Act & Assert + Assert.Throws(() => _services.UseDataFuse(c => c.WithEngine())); + } + + [Test] + public void WithEntityConfiguration_WithNoEngines_ShouldThrow() + { + // Arrange + var mockConfiguration = new Mock>(); + Func> factory = provider => mockConfiguration.Object; + + // Act & Assert + Assert.Throws(() => _services.UseDataFuse(c => c.WithEntityConfiguration(factory))); + } + + [Test] + public void WithEntityConfiguration_ShouldRegisterEntityConfiguration() + { + // Arrange + var mockConfiguration = new Mock>(); + Func> factory = provider => mockConfiguration.Object; + + // Act + _services.UseDataFuse(c => c + .InSilentMode() + .WithEntityConfiguration(factory)); + + // Assert + var serviceProvider = _services.BuildServiceProvider(); + var configuration = serviceProvider.GetService>(); + Assert.That(configuration, Is.SameAs(mockConfiguration.Object)); + } + + [Test] + public void WithEntityConfiguration_WithNullFactory_ShouldNotThrow() + { + // Act & Assert + Assert.DoesNotThrow(() => _services.UseDataFuse(c => c + .InSilentMode() + .WithEntityConfiguration(null))); + } + + [Test] + public void ChainedConfiguration_ShouldWorkCorrectly() + { + // Arrange + var mockEngine = new Mock(); + var mockPathMatcher = new Mock(); + var mockConfiguration = new Mock>(); + + // Act + _services.UseDataFuse(c => c + .WithEngine(provider => mockEngine.Object) + .WithPathMatcher(provider => mockPathMatcher.Object) + .WithEntityConfiguration(provider => mockConfiguration.Object)); + + // Assert + var serviceProvider = _services.BuildServiceProvider(); + + var engine = serviceProvider.GetService(); + var pathMatcher = serviceProvider.GetService(); + var configuration = serviceProvider.GetService>(); + + Assert.That(engine, Is.SameAs(mockEngine.Object)); + Assert.That(pathMatcher, Is.SameAs(mockPathMatcher.Object)); + Assert.That(configuration, Is.SameAs(mockConfiguration.Object)); + } + + [Test] + public void MultipleEntityConfigurations_ShouldRegisterSeparately() + { + // Arrange + var mockConfiguration1 = new Mock>(); + var mockConfiguration2 = new Mock>(); + + // Act + _services.UseDataFuse(c => c + .InSilentMode() + .WithEntityConfiguration(provider => mockConfiguration1.Object) + .WithEntityConfiguration(provider => mockConfiguration2.Object)); + + // Assert + var serviceProvider = _services.BuildServiceProvider(); + + var configuration1 = serviceProvider.GetService>(); + var configuration2 = serviceProvider.GetService>(); + + Assert.That(configuration1, Is.SameAs(mockConfiguration1.Object)); + Assert.That(configuration2, Is.SameAs(mockConfiguration2.Object)); + } + + [Test] + public void DataFuseOptionsBuilder_Constructor_ShouldSetServices() + { + // Act + var optionsBuilder = new DataFuseOptionsBuilder(_services); + + // Assert + Assert.That(optionsBuilder.Services, Is.SameAs(_services)); + } + + [Test] + public void WithEngine_MultipleCallsWithGeneric_ShouldRegisterMultiple() + { + // Act + _services.UseDataFuse(c => c + .InSilentMode() + .WithEngine() + .WithEngine()); + + // Assert + var serviceProvider = _services.BuildServiceProvider(); + var engines = serviceProvider.GetServices(); + + // Should have multiple registrations + var engineList = System.Linq.Enumerable.ToList(engines); + Assert.That(engineList.Count, Is.GreaterThan(1)); + } + + [Test] + public void WithPathMatcher_ShouldRemoveExistingRegistrations() + { + // Arrange + var mockPathMatcher = new Mock(); + + // Act + _services.UseDataFuse(c => c + .InSilentMode() + .WithPathMatcher(provider => mockPathMatcher.Object)); + + // Assert + var pathMatcherDescriptors = System.Linq.Enumerable.Where(_services, s => s.ServiceType == typeof(ISchemaPathMatcher)); + var descriptorList = System.Linq.Enumerable.ToList(pathMatcherDescriptors); + + // Should only have one registration after replacement + Assert.That(descriptorList.Count, Is.EqualTo(1)); + + var serviceProvider = _services.BuildServiceProvider(); + var pathMatcher = serviceProvider.GetService(); + Assert.That(pathMatcher, Is.SameAs(mockPathMatcher.Object)); + } + + [Test] + public void ComplexConfiguration_ShouldResolveAllDependencies() + { + // Arrange + var configuration = new TestEntityConfiguration(); + var engine = new TestQueryEngine(); + + // Act + _services.UseDataFuse(config => config + .WithEngine() + .WithEntityConfiguration(provider => configuration) + ); + + // Assert + var serviceProvider = _services.BuildServiceProvider(); + + // Should be able to resolve DataProvider with all dependencies + var dataProvider = serviceProvider.GetService>(); + Assert.That(dataProvider, Is.Not.Null); + Assert.That(dataProvider, Is.InstanceOf>()); + } + } +} \ No newline at end of file diff --git a/tests/DataFuse.Integration.Tests/Test.Common.cs b/tests/DataFuse.Integration.Tests/Test.Common.cs new file mode 100644 index 0000000..a9cae96 --- /dev/null +++ b/tests/DataFuse.Integration.Tests/Test.Common.cs @@ -0,0 +1,137 @@ +namespace DataFuse.Integration.Tests +{ + // Test entities + public class TestEntity : IEntity + { + public string Name { get; set; } + public int Value { get; set; } + } + + public class TestChildEntity : IEntity + { + public string Description { get; set; } + } + + // Test query results + public class TestQueryResult : IQueryResult + { + public string Data { get; set; } + } + + public class TestChildQueryResult : IQueryResult + { + public string ChildData { get; set; } + } + + public class TestPolymorphicResult : IPolymorphicResult + { + public string PolymorphicData { get; set; } + } + + // Test queries + public class TestQuery : BaseQuery + { + private bool _isContextResolved; + public string TestData { get; set; } + + public override bool IsContextResolved() + { + return _isContextResolved; + } + + public override void ResolveQuery(IDataContext context, IQueryResult parentQueryResult) + { + _isContextResolved = true; + TestData = "resolved"; + } + } + + public class TestChildQuery : BaseQuery + { + private bool _isContextResolved; + public string ChildTestData { get; set; } + + public override bool IsContextResolved() + { + return _isContextResolved; + } + + public override void ResolveQuery(IDataContext context, IQueryResult parentQueryResult) + { + _isContextResolved = true; + ChildTestData = parentQueryResult != null ? "resolved_with_parent" : "resolved_without_parent"; + } + } + + // Test transformers + public class TestTransformer : BaseTransformer + { + public override void Transform(TestQueryResult queryResult, TestEntity entity) + { + entity.Name = queryResult.Data; + entity.Value = queryResult.Data?.Length ?? 0; + } + } + + public class TestChildTransformer : BaseTransformer + { + public override void Transform(TestChildQueryResult queryResult, TestEntity entity) + { + entity.Name = entity.Name + "_" + queryResult.ChildData; + } + } + + // Test query engine + public class TestQueryEngine : IQueryEngine + { + public bool CanExecute(IQuery query) + { + return query is TestQuery || query is TestChildQuery; + } + + public async Task Execute(IQuery query) + { + await Task.Delay(1); // Simulate async operation + + if (query is TestQuery) + return new TestQueryResult { Data = "test_data" }; + + if (query is TestChildQuery) + return new TestChildQueryResult { ChildData = "child_data" }; + + return null; + } + } + + // Test entity configuration + public class TestEntityConfiguration : EntityConfiguration + { + public override IEnumerable> GetSchema() + { + return CreateSchema.For() + .Map(For.Paths("/root/test")) + .Map( + For.Paths("/root/test/child"), + deps => deps.Dependents + .Map(For.Paths("/root/test/child/nested")) + ) + .End(); + } + } + + // Test entity request + public class TestEntityRequest : IEntityRequest + { + public string[] SchemaPaths { get; set; } + } + + // Test entity context validator + public class TestEntityContextValidator : IEntityContextValidator + { + public void Validate(IEntityRequest context) + { + if (context?.SchemaPaths == null || context.SchemaPaths.Length == 0) + throw new ArgumentException("SchemaPaths cannot be null or empty"); + } + } +} \ No newline at end of file diff --git a/tests/Schemio.Core.Tests/Usings.cs b/tests/DataFuse.Integration.Tests/Usings.cs similarity index 100% rename from tests/Schemio.Core.Tests/Usings.cs rename to tests/DataFuse.Integration.Tests/Usings.cs diff --git a/tests/Schemio.API.Tests/E2E.Tests/E2ETests.cs b/tests/Schemio.API.Tests/E2E.Tests/E2ETests.cs deleted file mode 100644 index b44161a..0000000 --- a/tests/Schemio.API.Tests/E2E.Tests/E2ETests.cs +++ /dev/null @@ -1,168 +0,0 @@ -using Schemio.Core.Helpers; -using NUnit.Framework; -using Schemio.API.Tests.EntitySetup; -using Schemio.API.Tests.EntitySetup.WebApis; -using static Schemio.API.Tests.EntitySetup.Customer; -using static Schemio.API.Tests.EntitySetup.Customer.Contacts; - -namespace Schemio.API.Tests.E2E.Tests -{ - [TestFixture] - public class E2ETests : BaseTest - { - [SetUp] - public void Setup() - { - StubApi(string.Format(Endpoints.BaseAddress + Endpoints.Customer, Endpoints.Ids.CustomerId), new { Id = 1000, Name = "John McKinsey", Code = "THG-UY6789" }, new Dictionary { { "x-meta-branch-code", "London" } }); - StubApi(string.Format(Endpoints.BaseAddress + Endpoints.Communication, Endpoints.Ids.CustomerId), new { Id = 4567, Telephone = "07675998878", Email = "John.McKinsy@gmail.com", HouseNo = "22", City = "London", Region = "London", PostalCode = "W12 6GH", Country = "United Kingdom" }); - StubApi(string.Format(Endpoints.BaseAddress + Endpoints.Orders, Endpoints.Ids.CustomerId), new[] { new { OrderId = 1234, OrderNo = "GHK-897GB", Date = "2024-01-01T00:00:00" } }); - StubApi(string.Format(Endpoints.BaseAddress + Endpoints.OrderItems, Endpoints.Ids.CustomerId, Endpoints.Ids.OrderId), new[] { new { OrderId = 1234, ItemId = 2244, Name = "Pen", Cost = 12.00m }, new { OrderId = 1234, ItemId = 6677, Name = "Book", Cost = 15.00m } }); - } - - [TearDown] - public void Teardown() - { - } - - [Test] - public void TestDataProviderToFetchWholeContractWhenNamesAreNull() - { - var customer = dataProvider.GetData(new CustomerRequest - { - CustomerId = Endpoints.Ids.CustomerId - }); - - var expected = new Customer - { - Id = 1000, - Name = "John McKinsey", - Code = "THG-UY6789", - Branch = "London", // Received via response header - Communication = new Contacts - { - ContactId = 4567, - Phone = "07675998878", - Email = "John.McKinsy@gmail.com", - PostalAddress = new Address - { - HouseNo = "22", - City = "London", - Region = "London", - PostalCode = "W12 6GH", - Country = "United Kingdom", - } - }, - Orders = - [ - new Order - { - OrderId = 1234, - OrderNo = "GHK-897GB", - Date = DateTime.Parse("2024-01-01T00:00:00"), - Items = - [ - new Order.OrderItem - { - ItemId = 2244, Name = "Pen", Cost = 12.00m - }, - new Order.OrderItem - { - ItemId = 6677, Name = "Book", Cost = 15.00m - } - ] - } - ] - }; - - AssertAreEqual(expected, customer); - } - - [Test] - public void TestDataProviderToFetchPartialCustomerOrdersContractWhenNamesAreIncluded() - { - var customer = dataProvider.GetData(new CustomerRequest - { - CustomerId = Endpoints.Ids.CustomerId, - SchemaPaths = ["customer.orders.items"] - }); - - var expected = new Customer - { - Id = 1000, - Name = "John McKinsey", - Code = "THG-UY6789", - Branch = "London", - Orders = - [ - new Order - { - OrderId = 1234, - OrderNo = "GHK-897GB", - Date = DateTime.Parse("2024-01-01T00:00:00"), - Items = - [ - new Order.OrderItem - { - ItemId = 2244, Name = "Pen", Cost = 12.00m - }, - new Order.OrderItem - { - ItemId = 6677, Name = "Book", Cost = 15.00m - } - ] - } - ] - }; - - AssertAreEqual(expected, customer); - } - - [Test] - public void TestDataProviderToFetchPartialCustomerCommunicationContractWhenNamesAreIncluded() - { - var customer = dataProvider.GetData(new CustomerRequest - { - CustomerId = Endpoints.Ids.CustomerId, - SchemaPaths = ["customer.communication"] - }); - - var expected = new Customer - { - Id = 1000, - Name = "John McKinsey", - Code = "THG-UY6789", - Branch = "London", - Communication = new Contacts - { - ContactId = 4567, - Phone = "07675998878", - Email = "John.McKinsy@gmail.com", - PostalAddress = new Address - { - HouseNo = "22", - City = "London", - Region = "London", - PostalCode = "W12 6GH", - Country = "United Kingdom", - } - } - }; - - AssertAreEqual(expected, customer); - } - - private void AssertAreEqual(Customer expected, Customer actual) - { - var actualCustomer = actual.ToJson(); - var expectedCustomer = expected.ToJson(); - - Console.WriteLine("expected:"); - Console.WriteLine(expectedCustomer); - - Console.WriteLine("actual:"); - Console.WriteLine(actualCustomer); - - Assert.That(actualCustomer, Is.EqualTo(expectedCustomer)); - } - } -} \ No newline at end of file diff --git a/tests/Schemio.Core.Tests/DataProvider.Tests/DataProviderTests.cs b/tests/Schemio.Core.Tests/DataProvider.Tests/DataProviderTests.cs deleted file mode 100644 index e8ce3c8..0000000 --- a/tests/Schemio.Core.Tests/DataProvider.Tests/DataProviderTests.cs +++ /dev/null @@ -1,41 +0,0 @@ -using Microsoft.Extensions.Logging; -using Moq; -using Schemio.Core.Impl; -using Schemio.Core.Tests.EntitySetup; -using Schemio.Core.Tests.EntitySetup.Entities; - -namespace Schemio.Core.Tests.DataProvider.Tests -{ - [TestFixture] - internal class DataProviderTests - { - private DataProvider _provider; - private Mock>> _logger; - private Mock> _queryBuilder; - private Mock _queryExecutor; - private Mock> _entityBuilder; - - [SetUp] - public void Setup() - { - _logger = new Mock>>(); - _queryBuilder = new Mock>(); - _queryExecutor = new Mock(); - _entityBuilder = new Mock>(); - - _provider = new DataProvider(_logger.Object, _queryBuilder.Object, _queryExecutor.Object, _entityBuilder.Object); - } - - [Test] - public void TestDataProvider() - { - var context = new CustomerContext { CustomerId = 1 }; - - _provider.GetData(context); - - _queryBuilder.Verify(x => x.Build(It.IsAny()), Times.Once); - _queryExecutor.Verify(x => x.Execute(It.IsAny(), It.IsAny()), Times.Once); - _entityBuilder.Verify(x => x.Build(It.IsAny(), It.IsAny>()), Times.Once); - } - } -} \ No newline at end of file diff --git a/tests/Schemio.Core.Tests/DataProvider.Tests/EntityBuilderTests.cs b/tests/Schemio.Core.Tests/DataProvider.Tests/EntityBuilderTests.cs deleted file mode 100644 index 14ae66b..0000000 --- a/tests/Schemio.Core.Tests/DataProvider.Tests/EntityBuilderTests.cs +++ /dev/null @@ -1,86 +0,0 @@ -using Schemio.Core.Impl; -using Schemio.Core.Tests.EntitySetup.Configuration.Queries; -using Schemio.Core.Tests.EntitySetup.Entities; - -namespace Schemio.Core.Tests.DataProvider.Tests -{ - [TestFixture] - internal class EntityBuilderTests - { - private EntityBuilder _entityBuilder; - private IEntityConfiguration _entitySchema; - - private static List<(Type result, int InvocationCount)> TransformerInvocations; - - [SetUp] - public void Setup() - { - _entitySchema = new MockCustomerSchema(); - _entityBuilder = new EntityBuilder(_entitySchema); - TransformerInvocations = new List<(Type result, int InvocationCount)>(); - } - - [Test] - public void TestTransformExecutorForCorrectExecutionOfConfiguredTransforms() - { - var queryList = new List - { - new CustomerRecord{Id = 123, CustomerCode= "ABC", CustomerName="Ninja Labs"}, - new CommunicationRecord{Id = 123, Email = "ninja@labs.com", Telephone = "0212345689"}, - new CollectionResult(), - new CollectionResult() - }; - - var entity = _entityBuilder.Build(new DataContext(new EntityContext()), queryList); - - var customerTransforms = TransformerInvocations.Where(x => x.result == typeof(CustomerRecord)); - Assert.That(customerTransforms.Count() == 1); - Assert.That(customerTransforms.ElementAt(0).InvocationCount == 1); - - var communicationTransforms = TransformerInvocations.Where(x => x.result == typeof(CommunicationRecord)); - Assert.That(communicationTransforms.Count() == 1); - Assert.That(communicationTransforms.ElementAt(0).InvocationCount == 1); - - var orderCollectionTransforms = TransformerInvocations.Where(x => x.result == typeof(CollectionResult)); - Assert.That(orderCollectionTransforms.Count() == 1); - Assert.That(orderCollectionTransforms.ElementAt(0).InvocationCount == 1); - - var orderItemsCollectionTransforms = TransformerInvocations.Where(x => x.result == typeof(CollectionResult)); - Assert.That(orderItemsCollectionTransforms.Count() == 1); - Assert.That(orderItemsCollectionTransforms.ElementAt(0).InvocationCount == 1); - - Assert.That(entity, Is.Not.Null); - } - - public class MockTransform : BaseTransformer - where TEntity : IEntity - where TQueryResult : IQueryResult - { - public override void Transform(TQueryResult queryResult, TEntity entity) - { - TransformerInvocations.Add((queryResult.GetType(), 1)); - } - } - - internal class MockCustomerSchema : EntityConfiguration - { - public override IEnumerable> GetSchema() - { - return CreateSchema.For() - .Map>(For.Paths("customer/id", "customer/customercode", "customer/customername"), - customer => customer.Dependents - .Map>(For.Paths("customer/communication")) - .Map, Customer>>(For.Paths("customer/orders"), - customerOrders => customerOrders.Dependents - .Map, Customer>>(For.Paths("customer/orders/order/items"))) - ).End(); - } - } - } - - internal class EntityContext : IEntityRequest - { - public int CustomerId { get; set; } - public string[] SchemaPaths { get; set; } - } -} \ No newline at end of file diff --git a/tests/Schemio.Core.Tests/DataProvider.Tests/QueryExecutorTests.cs b/tests/Schemio.Core.Tests/DataProvider.Tests/QueryExecutorTests.cs deleted file mode 100644 index 6d40035..0000000 --- a/tests/Schemio.Core.Tests/DataProvider.Tests/QueryExecutorTests.cs +++ /dev/null @@ -1,53 +0,0 @@ -using Moq; -using Schemio.Core.Impl; -using Schemio.Core.PathMatchers; -using Schemio.Core.Tests.EntitySetup; -using Schemio.Core.Tests.EntitySetup.Entities; -using Schemio.Core.Tests.EntitySetup.Configuration; -using Schemio.Core.Tests.EntitySetup.Configuration.Queries; - -namespace Schemio.Core.Tests.DataProvider.Tests -{ - [TestFixture] - internal class QueryExecutorTests - { - private QueryExecutor _queryExecutor; - private Mock _queryEngine; - - [SetUp] - public void Setup() - { - _queryEngine = new Mock(); - _queryEngine.Setup(x => x.CanExecute(It.IsAny())).Returns(true); - - _queryExecutor = new QueryExecutor(new[] { _queryEngine.Object }); - } - - [Test] - public void TestQueryExecutorToReturnWhenNoQueries() - { - _queryExecutor.Execute(new DataContext(new EntityContext()), new QueryList()); - - _queryEngine.Verify(x => x.Execute(It.IsAny()), Times.Never()); - } - - [Test] - public void TestQueryExecutorToCallEngineWhenQueriesExistForExecution() - { - _queryExecutor.Execute(new DataContext(new EntityContext()), new QueryList(new[] { new CustomerQuery() }) { }); - - _queryEngine.Verify(x => x.Execute(It.IsAny()), Times.Once()); - } - - [Test] // TODO - All sequence assertions - public void TestQueryExecutorToExecuteConfiguredQueriesInCorrectOrder() - { - var querList = new QueryBuilder(new CustomerConfiguration(), new XPathMatcher()) - .Build(new DataContext(new CustomerContext())); - - _queryExecutor.Execute(new DataContext(new EntityContext()), querList); - - _queryEngine.Verify(x => x.Execute(It.IsAny()), Times.Once()); - } - } -} \ No newline at end of file diff --git a/tests/Schemio.EntityFramework.Tests/E2E.Tests.cs b/tests/Schemio.EntityFramework.Tests/E2E.Tests.cs deleted file mode 100644 index 860c2b0..0000000 --- a/tests/Schemio.EntityFramework.Tests/E2E.Tests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using Schemio.Core; -using Schemio.EntityFramework.Tests.EntitySetup; -using Schemio.EntityFramework.Tests.EntitySetup.Entities; - -namespace Schemio.EntityFramework.Tests -{ - [TestFixture] - public class E2ETests : BaseTest - { - private IDataProvider _provider; - - [SetUp] - public void Setup() - { - _provider = _serviceProvider.GetService>(); - } - - [Test] - public void TestDataProviderToFetchWholeEntityWhenPathsAreNull() - { - var customer = _provider.GetData(new CustomerRequest - { - CustomerId = 1 - }); - - var expected = new Customer - { - Id = 1, - Name = "Jack Sparrow", - Code = "AB123", - Communication = new Communication - { - ContactId = 1, - Phone = "0123456789", - Email = "jack.sparrow@gmail.com", - Address = new Address - { - AddressId = 1, - HouseNo = "77", - City = "Wansted", - Region = "Belfast", - PostalCode = "BL34Y56", - Country = "United Kingdom", - } - }, - Orders = [ new Order { - OrderId = 1, - OrderNo = "ZX123VH", - Date = DateTime.Parse("2021-10-22T00:00:00"), - Items = - [ - new OrderItem - { - ItemId = 1, Name = "12 inch Cake", Cost = 30m - }, - new OrderItem - { - ItemId = 2, Name = "20 Cake Candles", Cost = 5m - } - ] - }] - }; - - AssertAreEqual(expected, customer); - } - - [Test] - public void TestDataProviderToFetchEntityWhenPathsContainsCommunication() - { - var customer = _provider.GetData(new CustomerRequest - { - CustomerId = 1, - SchemaPaths = new[] { "Customer/Communication" } - }); - - var expected = new Customer - { - Id = 1, - Name = "Jack Sparrow", - Code = "AB123", - Communication = new Communication - { - ContactId = 1, - Phone = "0123456789", - Email = "jack.sparrow@gmail.com", - Address = new Address - { - AddressId = 1, - HouseNo = "77", - City = "Wansted", - Region = "Belfast", - PostalCode = "BL34Y56", - Country = "United Kingdom", - } - } - }; - - AssertAreEqual(expected, customer); - } - - [Test] - public void TestDataProviderToFetchEntityWhenPathsContainsOrderItems() - { - var customer = _provider.GetData(new CustomerRequest - { - CustomerId = 1, - SchemaPaths = new[] { "Customer/orders/order/items/item" } - }); - - var expected = new Customer - { - Id = 1, - Name = "Jack Sparrow", - Code = "AB123", - Orders = [ new Order - { - OrderId = 1, - OrderNo = "ZX123VH", - Date = DateTime.Parse("2021-10-22T00:00:00"), - Items = - [ - new OrderItem - { - ItemId = 1, Name = "12 inch Cake", Cost = 30m - }, - new OrderItem - { - ItemId = 2, Name = "20 Cake Candles", Cost = 5m - } - ] - }] - }; - - AssertAreEqual(expected, customer); - } - } -} \ No newline at end of file diff --git a/tests/Schemio.EntityFramework.Tests/GlobalUsings.cs b/tests/Schemio.EntityFramework.Tests/GlobalUsings.cs deleted file mode 100644 index cefced4..0000000 --- a/tests/Schemio.EntityFramework.Tests/GlobalUsings.cs +++ /dev/null @@ -1 +0,0 @@ -global using NUnit.Framework; \ No newline at end of file diff --git a/tests/Schemio.SQL.Tests/E2E.Tests.cs b/tests/Schemio.SQL.Tests/E2E.Tests.cs deleted file mode 100644 index 23dc60d..0000000 --- a/tests/Schemio.SQL.Tests/E2E.Tests.cs +++ /dev/null @@ -1,153 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using Schemio.Core; -using Schemio.Core.Impl; -using Schemio.EntityFramework.Tests; -using Schemio.SQL.Tests.EntitySetup; -using Schemio.SQL.Tests.EntitySetup.Entities; - -namespace Schemio.SQL.Tests -{ - public class E2ETests : BaseTest - { - protected IDataProvider _provider; - - [SetUp] - public void Setup() - { - _provider = _serviceProvider.GetService>(); - } - - [Test] - public void TestDataProviderToFetchWholeEntityWhenPathsAreNull() - { - var customer = _provider.GetData(new CustomerRequest - { - CustomerId = 1 - }); - - var expected = new Customer - { - Id = 1, - Name = "Jack Sparrow", - Code = "AB123", - Communication = new Communication - { - ContactId = 1, - Phone = "0123456789", - Email = "jack.sparrow@gmail.com", - Address = new Address - { - AddressId = 1, - HouseNo = "77", - City = "Wansted", - Region = "Belfast", - PostalCode = "BL34Y56", - Country = "United Kingdom", - } - }, - Orders = [ new Order { - OrderId = 1, - OrderNo = "ZX123VH", - Date = DateTime.Parse("2021-10-22T00:00:00"), - Items = - [ - new OrderItem - { - ItemId = 1, Name = "12 inch Cake", Cost = 30m - }, - new OrderItem - { - ItemId = 2, Name = "20 Cake Candles", Cost = 5m - } - ] - }] - }; - - AssertAreEqual(expected, customer); - } - - [Test] - public void TestDataProviderToFetchEntityWhenPathsContainsOrderItems() - { - var customer = _provider.GetData(new CustomerRequest - { - CustomerId = 1, - SchemaPaths = new[] { "Customer/orders/order/items/item" } - }); - - var expected = new Customer - { - Id = 1, - Name = "Jack Sparrow", - Code = "AB123", - Orders = [ new Order - { - OrderId = 1, - OrderNo = "ZX123VH", - Date = DateTime.Parse("2021-10-22T00:00:00"), - Items = - [ - new OrderItem - { - ItemId = 1, Name = "12 inch Cake", Cost = 30m - }, - new OrderItem - { - ItemId = 2, Name = "20 Cake Candles", Cost = 5m - } - ] - }] - }; - - AssertAreEqual(expected, customer); - } - - [Test] - public void TestDataProviderToFetchEntityWhenPathsContainsCommunication() - { - var customer = _provider.GetData(new CustomerRequest - { - CustomerId = 1, - SchemaPaths = new[] { "Customer/Communication" } - }); - - var expected = new Customer - { - Id = 1, - Name = "Jack Sparrow", - Code = "AB123", - Communication = new Communication - { - ContactId = 1, - Phone = "0123456789", - Email = "jack.sparrow@gmail.com", - Address = new Address - { - AddressId = 1, - HouseNo = "77", - City = "Wansted", - Region = "Belfast", - PostalCode = "BL34Y56", - Country = "United Kingdom", - } - } - }; - - AssertAreEqual(expected, customer); - } - - [Test] - public void TestDataProviderToCacheResultForResultsWithAttributeApplied() - { - var context = new DataContext(new CustomerRequest - { - CustomerId = 1 - }); - - ((DataProvider)_provider).GetData(context); - - Assert.That(context.Cache, Is.Not.Null); - Assert.That(context.Cache.Count, Is.EqualTo(1)); - } - } -} \ No newline at end of file diff --git a/tests/Schemio.SQL.Tests/GlobalUsings.cs b/tests/Schemio.SQL.Tests/GlobalUsings.cs deleted file mode 100644 index cefced4..0000000 --- a/tests/Schemio.SQL.Tests/GlobalUsings.cs +++ /dev/null @@ -1 +0,0 @@ -global using NUnit.Framework; \ No newline at end of file