diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..fe1152b
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,30 @@
+**/.classpath
+**/.dockerignore
+**/.env
+**/.git
+**/.gitignore
+**/.project
+**/.settings
+**/.toolstarget
+**/.vs
+**/.vscode
+**/*.*proj.user
+**/*.dbmdl
+**/*.jfm
+**/azds.yaml
+**/bin
+**/charts
+**/docker-compose*
+**/Dockerfile*
+**/node_modules
+**/npm-debug.log
+**/obj
+**/secrets.dev.yaml
+**/values.dev.yaml
+LICENSE
+README.md
+!**/.gitignore
+!.git/HEAD
+!.git/config
+!.git/packed-refs
+!.git/refs/heads/**
\ No newline at end of file
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..362818c
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,60 @@
+# Set default behavior to automatically normalize line endings.
+* text=auto
+
+# Collapse these files in PRs by default
+*.xlf linguist-generated=true
+*.lcl linguist-generated=true
+
+*.jpg binary
+*.png binary
+*.gif binary
+
+# Force bash scripts to always use lf line endings so that if a repo is accessed
+# in Unix via a file share from Windows, the scripts will work.
+*.in text eol=lf
+*.sh text eol=lf
+
+# Likewise, force cmd and batch scripts to always use crlf
+*.cmd text eol=crlf
+*.bat text eol=crlf
+
+*.cs text=auto diff=csharp
+*.vb text=auto
+*.resx text=auto
+*.c text=auto
+*.cpp text=auto
+*.cxx text=auto
+*.h text=auto
+*.hxx text=auto
+*.py text=auto
+*.rb text=auto
+*.java text=auto
+*.html text=auto
+*.htm text=auto
+*.css text=auto
+*.scss text=auto
+*.sass text=auto
+*.less text=auto
+*.js text=auto
+*.lisp text=auto
+*.clj text=auto
+*.sql text=auto
+*.php text=auto
+*.lua text=auto
+*.m text=auto
+*.asm text=auto
+*.erl text=auto
+*.fs text=auto
+*.fsx text=auto
+*.hs text=auto
+
+*.csproj text=auto
+*.vbproj text=auto
+*.fsproj text=auto
+*.dbproj text=auto
+*.sln text=auto eol=crlf
+
+# Set linguist language for .h files explicitly based on
+# https://github.com/github/linguist/issues/1626#issuecomment-401442069
+# this only affects the repo's language statistics
+*.h linguist-language=C
\ No newline at end of file
diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml
new file mode 100644
index 0000000..0ddd010
--- /dev/null
+++ b/.github/dependabot.yaml
@@ -0,0 +1,30 @@
+version: 2
+registries:
+ public-nuget:
+ type: nuget-feed
+ url: https://api.nuget.org/v3/index.json
+updates:
+ - package-ecosystem: nuget
+ directory: "/"
+ registries:
+ - public-nuget
+ schedule:
+ interval: daily
+ open-pull-requests-limit: 15
+ labels:
+ - "area-dependencies"
+ groups:
+ all-dependencies:
+ patterns:
+ - "*"
+ - package-ecosystem: "github-actions"
+ directory: "/"
+ schedule:
+ interval: daily
+ open-pull-requests-limit: 5
+ labels:
+ - "area-dependencies"
+ groups:
+ all-dependencies:
+ patterns:
+ - "*"
\ No newline at end of file
diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml
new file mode 100644
index 0000000..bb5b59d
--- /dev/null
+++ b/.github/workflows/main.yaml
@@ -0,0 +1,158 @@
+name: Main Workflow
+
+on:
+ push:
+ branches:
+ - '**' # Matches all branches
+ pull_request:
+
+jobs:
+ ci:
+ name: CI
+ runs-on: ubuntu-latest
+ permissions:
+ pull-requests: read
+ id-token: write
+ contents: read
+ checks: write
+ env:
+ build: false
+ outputs:
+ change_detection_src: ${{ steps.change_detection.outputs.src }}
+ build: ${{ env.build }}
+ semVer: ${{env.GitVersion_SemVer}}
+ steps:
+
+ - name: checkout
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: tools - dotnet - install
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: '8.x'
+
+ - name: tools - gitversion - install
+ uses: gittools/actions/gitversion/setup@v1.1.1
+ with:
+ versionSpec: '5.x'
+ preferLatestVersion: true
+
+ - name: tools - gitversion - execute
+ uses: gittools/actions/gitversion/execute@v1.1.1
+ with:
+ useConfigFile: true
+ configFilePath: GitVersion.yaml
+
+ - name: tools - detect changes
+ id: change_detection
+ uses: dorny/paths-filter@v3
+ with:
+ base: ${{ github.ref }}
+ filters: |
+ src:
+ - 'src/**'
+ - 'ES.FX.sln'
+ - 'Directory.Build.props'
+ build:
+ - 'src/**'
+ - 'playground/**'
+ - 'tests/**'
+ - 'ES.FX.sln'
+ - 'Directory.Build.props'
+
+ - name: tools - evaluate build flag
+ if: steps.change_detection.outputs.build == 'true' ||
+ github.event_name == 'pull_request'
+ run: echo "build=true" >> $GITHUB_ENV
+
+
+ - name: cache - nuget
+ if: ${{ env.build == 'true' }}
+ uses: actions/cache@v4
+ with:
+ path: ~/.nuget/packages
+ key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj') }}
+ restore-keys: |
+ ${{ runner.os }}-nuget-
+
+ - name: dotnet restore
+ if: ${{ env.build == 'true' }}
+ run: dotnet restore
+
+ - name: dotnet build
+ if: ${{ env.build == 'true' }}
+ run: dotnet build --no-restore --configuration Release /p:Version=${{env.GitVersion_SemVer}} /p:AssemblyVersion=${{env.GitVersion_AssemblySemFileVer}} /p:NuGetVersion=${{env.GitVersion_SemVer}}
+
+ - name: dotnet test
+ if: ${{ env.build == 'true' }}
+ run: dotnet test --no-build --configuration Release --verbosity normal
+
+ - name: test-reporter
+ uses: dorny/test-reporter@v1
+ if: ${{ env.build == 'true' }}
+ with:
+ name: Test Results
+ path: .artifacts/TestResults/*.trx
+ reporter: dotnet-trx
+
+ - name: artifacts - nuget - gather
+ if: ${{ env.build == 'true' }}
+ run: |
+ mkdir -p .artifacts/nuget
+ find . -name "*.nupkg" -exec cp {} .artifacts/nuget/ \;
+
+ - name: artifacts - nuget - upload
+ if: ${{ env.build == 'true' }}
+ uses: actions/upload-artifact@v4
+ with:
+ name: artifacts-nuget-${{env.GitVersion_SemVer}}
+ path: .artifacts/nuget/*.nupkg
+
+ cd:
+ name: CD
+ runs-on: ubuntu-latest
+ needs: ci
+ if: >
+ needs.ci.outputs.change_detection_src == 'true' &&
+ github.event_name == 'push' &&
+ (github.ref == 'refs/heads/main' ||
+ github.ref == 'refs/heads/develop' ||
+ startsWith(github.ref, 'refs/heads/feature/') ||
+ startsWith(github.ref, 'refs/heads/releases/') ||
+ startsWith(github.ref, 'refs/heads/hotfix/'))
+ env:
+ build: ${{ needs.ci.outputs.build }}
+ semVer: ${{ needs.ci.outputs.semVer }}
+ changes_src: ${{ needs.ci.outputs.change_detection_src }}
+ steps:
+ - name: artifacts - nuget - download
+ uses: actions/download-artifact@v4
+ with:
+ name: artifacts-nuget-${{env.semVer}}
+ path: .artifacts/nuget
+
+ - name: dotnet nuget add source
+ if: ${{ env.changes_src == 'true' }}
+ run: dotnet nuget add source --username USERNAME --password ${{ secrets.GITHUB_TOKEN }} --store-password-in-clear-text --name github "https://nuget.pkg.github.com/emberstack/index.json"
+
+ - name: dotnet nuget push
+ if: ${{ env.changes_src == 'true' }}
+ run: |
+ for pkg in .artifacts/nuget/*.nupkg; do
+ dotnet nuget push "$pkg" --source "github" --api-key ${{ secrets.ES_GITHUB_PAT }}
+ done
+
+ - name: checkout
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: git - tag
+ if: ${{ env.changes_src == 'true' }}
+ run: |
+ git config --global user.name 'github-actions'
+ git config --global user.email 'github-actions@github.com'
+ git tag version/v${{env.GitVersion_SemVer}}
+ git push version/origin v${{env.GitVersion_SemVer}}
\ No newline at end of file
diff --git a/Directory.Build.props b/Directory.Build.props
new file mode 100644
index 0000000..0074a85
--- /dev/null
+++ b/Directory.Build.props
@@ -0,0 +1,41 @@
+
+
+ true
+ enable
+ enable
+
+ embedded
+ true
+
+ $(MSBuildThisFileDirectory)..\
+
+
+ $(SolutionDir).artifacts/nuget
+ emberstack
+ EmberStack
+ https://github.com/emberstack/ES.FX
+ MIT
+
+
+
+
+
+ false
+
+
+
+
+ true
+
+ $(NoWarn);NU5104
+
+
+
+
+
+
+
+ trx%3bLogFileName=$(MSBuildProjectName).trx
+ $(MSBuildThisFileDirectory).artifacts/TestResults
+
+
\ No newline at end of file
diff --git a/ES.FX.sln b/ES.FX.sln
new file mode 100644
index 0000000..92ef5fa
--- /dev/null
+++ b/ES.FX.sln
@@ -0,0 +1,215 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.10.34928.147
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "..Solution Items", "..Solution Items", "{C82E6C65-F435-4DA5-BC04-1E29BA2B0376}"
+ ProjectSection(SolutionItems) = preProject
+ Directory.Build.props = Directory.Build.props
+ EndProjectSection
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "FX", "FX", "{F8538BCE-36D3-4317-8C3C-7540117B99A7}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Ignite", "Ignite", "{D8925EFE-9CEE-472F-86E4-24E25B1D7429}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{32548940-0629-46AC-B9BF-E7C41F1C49DE}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Sparks", "Sparks", "{5A5969AF-86CB-489B-B0DE-C0BAADF5B424}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ES.FX", "src\ES.FX\ES.FX.csproj", "{59D82B3D-BB0E-48EE-A794-2FF5ED2A7933}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ES.FX.Hosting", "src\ES.FX.Hosting\ES.FX.Hosting.csproj", "{35BDE65B-418E-4893-93D4-5E4FBD70485B}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ES.FX.Ignite.Microsoft.EntityFrameworkCore", "src\ES.FX.Ignite.Microsoft.EntityFrameworkCore\ES.FX.Ignite.Microsoft.EntityFrameworkCore.csproj", "{18C11FC1-BB81-454B-863E-09E800C92744}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ES.FX.Microsoft.EntityFrameworkCore", "src\ES.FX.Microsoft.EntityFrameworkCore\ES.FX.Microsoft.EntityFrameworkCore.csproj", "{CE5F8B87-AF97-45A8-B756-B1EC829ED2B1}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ES.FX.Migrations", "src\ES.FX.Migrations\ES.FX.Migrations.csproj", "{C02A58D5-D662-422C-BE31-6458902F0096}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ES.FX.Serilog", "src\ES.FX.Serilog\ES.FX.Serilog.csproj", "{83F22077-88DC-409C-8F24-1D4263372F0C}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ES.FX.Ignite.Microsoft.EntityFrameworkCore.SqlServer", "src\ES.FX.Ignite.Microsoft.EntityFrameworkCore.SqlServer\ES.FX.Ignite.Microsoft.EntityFrameworkCore.SqlServer.csproj", "{BBEBE438-6C1E-4415-A194-B9BFA37041FD}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ES.FX.Ignite.Migrations", "src\ES.FX.Ignite.Migrations\ES.FX.Ignite.Migrations.csproj", "{AA84032A-F03E-4F29-A9F8-B6DBA62C8BBA}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ES.FX.Ignite.Serilog", "src\ES.FX.Ignite.Serilog\ES.FX.Ignite.Serilog.csproj", "{C2D1FC07-5A25-499A-B2AB-5FFC38802019}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ES.FX.Ignite.Hosting", "src\ES.FX.Ignite.Hosting\ES.FX.Ignite.Hosting.csproj", "{4105330C-28C4-438B-9B65-04F15DBE9986}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ES.FX.Ignite.Spark", "src\ES.FX.Ignite.Spark\ES.FX.Ignite.Spark.csproj", "{BDD026D8-59C0-4B90-83AA-7C1B75082117}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "playground", "playground", "{9EF9C5BD-97AA-4E59-A84B-9B7FCCE87649}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".src", ".src", "{C08180CD-7BF7-4138-B1CA-DE0DCF339CB5}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ES.FX.Tests", "tests\ES.FX.Tests\ES.FX.Tests.csproj", "{98158873-3DBC-4A0F-A799-71126B838026}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Playground.SimpleConsole", "playground\Playground.SimpleConsole\Playground.SimpleConsole.csproj", "{7E7411A2-EA67-492B-B159-D15F9CB9B2FA}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Playground.Microservice.Api.Host", "playground\Playground.Microservice.Api.Host\Playground.Microservice.Api.Host.csproj", "{1989335E-8FBC-498D-8293-F3816EF36F15}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "shared", "shared", "{BD8EB348-8F5D-41BB-BA2E-6D5C7A71FAB5}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Playground.Shared.Data.Simple.EntityFrameworkCore", "playground\Playground.Shared.Data.Simple.EntityFrameworkCore\Playground.Shared.Data.Simple.EntityFrameworkCore.csproj", "{E561B8AC-C07F-443E-8296-B81EEC5FA5C0}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Playground.Shared.Data.Simple.EntityFrameworkCore.SqlServer", "playground\Playground.Shared.Data.Simple.EntityFrameworkCore.SqlServer\Playground.Shared.Data.Simple.EntityFrameworkCore.SqlServer.csproj", "{8234178C-1342-478B-B702-59032587531F}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Playground.Microservice.Worker.Host", "playground\Playground.Microservice.Worker.Host\Playground.Microservice.Worker.Host.csproj", "{9020A791-C5C4-4D4A-B495-A8DC42559316}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Shared", "Shared", "{A86D1628-AA35-400B-99C9-C59FB7F2AB65}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ES.FX.Ignite.Microsoft.EntityFrameworkCore.SqlServer.Tests", "tests\ES.FX.Ignite.Microsoft.EntityFrameworkCore.SqlServer.Tests\ES.FX.Ignite.Microsoft.EntityFrameworkCore.SqlServer.Tests.csproj", "{525CEE29-DFC4-4497-AE1A-8B267B404201}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ES.FX.Ignite.Microsoft.EntityFrameworkCore.Tests", "tests\ES.FX.Ignite.Microsoft.EntityFrameworkCore.Tests\ES.FX.Ignite.Microsoft.EntityFrameworkCore.Tests.csproj", "{0AAE113B-7D85-477C-A72C-55B8C8BED049}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ES.FX.Ignite.Spark.Tests", "tests\ES.FX.Ignite.Spark.Tests\ES.FX.Ignite.Spark.Tests.csproj", "{E0F4DFC2-46E3-4EF6-AA9D-71F0B73911D3}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ES.FX.Shared.SqlServer.Tests", "tests\ES.FX.Shared.SqlServer.Tests\ES.FX.Shared.SqlServer.Tests.csproj", "{938AE7EF-E227-4EED-912D-43A9AEF614B5}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ES.FX.Ignite.Microsoft.Data.SqlClient", "src\ES.FX.Ignite.Microsoft.Data.SqlClient\ES.FX.Ignite.Microsoft.Data.SqlClient.csproj", "{DB1966F4-AF4B-4E04-97FD-01F0E994EDA0}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ES.FX.Microsoft.Data.SqlClient", "src\ES.FX.Microsoft.Data.SqlClient\ES.FX.Microsoft.Data.SqlClient.csproj", "{EDFC5F9A-F0F6-46E0-A3DC-9B81F5115327}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ES.FX.Ignite.Microsoft.Data.SqlClient.Tests", "tests\ES.FX.Ignite.Microsoft.Data.SqlClient.Tests\ES.FX.Ignite.Microsoft.Data.SqlClient.Tests.csproj", "{F78162A1-433B-4245-99FB-6C0B9E7B05C0}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {59D82B3D-BB0E-48EE-A794-2FF5ED2A7933}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {59D82B3D-BB0E-48EE-A794-2FF5ED2A7933}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {59D82B3D-BB0E-48EE-A794-2FF5ED2A7933}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {59D82B3D-BB0E-48EE-A794-2FF5ED2A7933}.Release|Any CPU.Build.0 = Release|Any CPU
+ {35BDE65B-418E-4893-93D4-5E4FBD70485B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {35BDE65B-418E-4893-93D4-5E4FBD70485B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {35BDE65B-418E-4893-93D4-5E4FBD70485B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {35BDE65B-418E-4893-93D4-5E4FBD70485B}.Release|Any CPU.Build.0 = Release|Any CPU
+ {18C11FC1-BB81-454B-863E-09E800C92744}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {18C11FC1-BB81-454B-863E-09E800C92744}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {18C11FC1-BB81-454B-863E-09E800C92744}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {18C11FC1-BB81-454B-863E-09E800C92744}.Release|Any CPU.Build.0 = Release|Any CPU
+ {CE5F8B87-AF97-45A8-B756-B1EC829ED2B1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {CE5F8B87-AF97-45A8-B756-B1EC829ED2B1}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {CE5F8B87-AF97-45A8-B756-B1EC829ED2B1}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {CE5F8B87-AF97-45A8-B756-B1EC829ED2B1}.Release|Any CPU.Build.0 = Release|Any CPU
+ {C02A58D5-D662-422C-BE31-6458902F0096}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C02A58D5-D662-422C-BE31-6458902F0096}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C02A58D5-D662-422C-BE31-6458902F0096}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C02A58D5-D662-422C-BE31-6458902F0096}.Release|Any CPU.Build.0 = Release|Any CPU
+ {83F22077-88DC-409C-8F24-1D4263372F0C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {83F22077-88DC-409C-8F24-1D4263372F0C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {83F22077-88DC-409C-8F24-1D4263372F0C}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {83F22077-88DC-409C-8F24-1D4263372F0C}.Release|Any CPU.Build.0 = Release|Any CPU
+ {BBEBE438-6C1E-4415-A194-B9BFA37041FD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {BBEBE438-6C1E-4415-A194-B9BFA37041FD}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {BBEBE438-6C1E-4415-A194-B9BFA37041FD}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {BBEBE438-6C1E-4415-A194-B9BFA37041FD}.Release|Any CPU.Build.0 = Release|Any CPU
+ {AA84032A-F03E-4F29-A9F8-B6DBA62C8BBA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {AA84032A-F03E-4F29-A9F8-B6DBA62C8BBA}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {AA84032A-F03E-4F29-A9F8-B6DBA62C8BBA}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {AA84032A-F03E-4F29-A9F8-B6DBA62C8BBA}.Release|Any CPU.Build.0 = Release|Any CPU
+ {C2D1FC07-5A25-499A-B2AB-5FFC38802019}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C2D1FC07-5A25-499A-B2AB-5FFC38802019}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C2D1FC07-5A25-499A-B2AB-5FFC38802019}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C2D1FC07-5A25-499A-B2AB-5FFC38802019}.Release|Any CPU.Build.0 = Release|Any CPU
+ {4105330C-28C4-438B-9B65-04F15DBE9986}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {4105330C-28C4-438B-9B65-04F15DBE9986}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {4105330C-28C4-438B-9B65-04F15DBE9986}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {4105330C-28C4-438B-9B65-04F15DBE9986}.Release|Any CPU.Build.0 = Release|Any CPU
+ {BDD026D8-59C0-4B90-83AA-7C1B75082117}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {BDD026D8-59C0-4B90-83AA-7C1B75082117}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {BDD026D8-59C0-4B90-83AA-7C1B75082117}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {BDD026D8-59C0-4B90-83AA-7C1B75082117}.Release|Any CPU.Build.0 = Release|Any CPU
+ {98158873-3DBC-4A0F-A799-71126B838026}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {98158873-3DBC-4A0F-A799-71126B838026}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {98158873-3DBC-4A0F-A799-71126B838026}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {98158873-3DBC-4A0F-A799-71126B838026}.Release|Any CPU.Build.0 = Release|Any CPU
+ {7E7411A2-EA67-492B-B159-D15F9CB9B2FA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {7E7411A2-EA67-492B-B159-D15F9CB9B2FA}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {7E7411A2-EA67-492B-B159-D15F9CB9B2FA}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {7E7411A2-EA67-492B-B159-D15F9CB9B2FA}.Release|Any CPU.Build.0 = Release|Any CPU
+ {1989335E-8FBC-498D-8293-F3816EF36F15}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {1989335E-8FBC-498D-8293-F3816EF36F15}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {1989335E-8FBC-498D-8293-F3816EF36F15}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {1989335E-8FBC-498D-8293-F3816EF36F15}.Release|Any CPU.Build.0 = Release|Any CPU
+ {E561B8AC-C07F-443E-8296-B81EEC5FA5C0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {E561B8AC-C07F-443E-8296-B81EEC5FA5C0}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {E561B8AC-C07F-443E-8296-B81EEC5FA5C0}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {E561B8AC-C07F-443E-8296-B81EEC5FA5C0}.Release|Any CPU.Build.0 = Release|Any CPU
+ {8234178C-1342-478B-B702-59032587531F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {8234178C-1342-478B-B702-59032587531F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {8234178C-1342-478B-B702-59032587531F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {8234178C-1342-478B-B702-59032587531F}.Release|Any CPU.Build.0 = Release|Any CPU
+ {9020A791-C5C4-4D4A-B495-A8DC42559316}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {9020A791-C5C4-4D4A-B495-A8DC42559316}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {9020A791-C5C4-4D4A-B495-A8DC42559316}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {9020A791-C5C4-4D4A-B495-A8DC42559316}.Release|Any CPU.Build.0 = Release|Any CPU
+ {525CEE29-DFC4-4497-AE1A-8B267B404201}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {525CEE29-DFC4-4497-AE1A-8B267B404201}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {525CEE29-DFC4-4497-AE1A-8B267B404201}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {525CEE29-DFC4-4497-AE1A-8B267B404201}.Release|Any CPU.Build.0 = Release|Any CPU
+ {0AAE113B-7D85-477C-A72C-55B8C8BED049}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {0AAE113B-7D85-477C-A72C-55B8C8BED049}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {0AAE113B-7D85-477C-A72C-55B8C8BED049}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {0AAE113B-7D85-477C-A72C-55B8C8BED049}.Release|Any CPU.Build.0 = Release|Any CPU
+ {E0F4DFC2-46E3-4EF6-AA9D-71F0B73911D3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {E0F4DFC2-46E3-4EF6-AA9D-71F0B73911D3}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {E0F4DFC2-46E3-4EF6-AA9D-71F0B73911D3}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {E0F4DFC2-46E3-4EF6-AA9D-71F0B73911D3}.Release|Any CPU.Build.0 = Release|Any CPU
+ {938AE7EF-E227-4EED-912D-43A9AEF614B5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {938AE7EF-E227-4EED-912D-43A9AEF614B5}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {938AE7EF-E227-4EED-912D-43A9AEF614B5}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {938AE7EF-E227-4EED-912D-43A9AEF614B5}.Release|Any CPU.Build.0 = Release|Any CPU
+ {DB1966F4-AF4B-4E04-97FD-01F0E994EDA0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {DB1966F4-AF4B-4E04-97FD-01F0E994EDA0}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {DB1966F4-AF4B-4E04-97FD-01F0E994EDA0}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {DB1966F4-AF4B-4E04-97FD-01F0E994EDA0}.Release|Any CPU.Build.0 = Release|Any CPU
+ {EDFC5F9A-F0F6-46E0-A3DC-9B81F5115327}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {EDFC5F9A-F0F6-46E0-A3DC-9B81F5115327}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {EDFC5F9A-F0F6-46E0-A3DC-9B81F5115327}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {EDFC5F9A-F0F6-46E0-A3DC-9B81F5115327}.Release|Any CPU.Build.0 = Release|Any CPU
+ {F78162A1-433B-4245-99FB-6C0B9E7B05C0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {F78162A1-433B-4245-99FB-6C0B9E7B05C0}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F78162A1-433B-4245-99FB-6C0B9E7B05C0}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {F78162A1-433B-4245-99FB-6C0B9E7B05C0}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {F8538BCE-36D3-4317-8C3C-7540117B99A7} = {C08180CD-7BF7-4138-B1CA-DE0DCF339CB5}
+ {D8925EFE-9CEE-472F-86E4-24E25B1D7429} = {C08180CD-7BF7-4138-B1CA-DE0DCF339CB5}
+ {5A5969AF-86CB-489B-B0DE-C0BAADF5B424} = {D8925EFE-9CEE-472F-86E4-24E25B1D7429}
+ {59D82B3D-BB0E-48EE-A794-2FF5ED2A7933} = {F8538BCE-36D3-4317-8C3C-7540117B99A7}
+ {35BDE65B-418E-4893-93D4-5E4FBD70485B} = {F8538BCE-36D3-4317-8C3C-7540117B99A7}
+ {18C11FC1-BB81-454B-863E-09E800C92744} = {5A5969AF-86CB-489B-B0DE-C0BAADF5B424}
+ {CE5F8B87-AF97-45A8-B756-B1EC829ED2B1} = {F8538BCE-36D3-4317-8C3C-7540117B99A7}
+ {C02A58D5-D662-422C-BE31-6458902F0096} = {F8538BCE-36D3-4317-8C3C-7540117B99A7}
+ {83F22077-88DC-409C-8F24-1D4263372F0C} = {F8538BCE-36D3-4317-8C3C-7540117B99A7}
+ {BBEBE438-6C1E-4415-A194-B9BFA37041FD} = {5A5969AF-86CB-489B-B0DE-C0BAADF5B424}
+ {AA84032A-F03E-4F29-A9F8-B6DBA62C8BBA} = {5A5969AF-86CB-489B-B0DE-C0BAADF5B424}
+ {C2D1FC07-5A25-499A-B2AB-5FFC38802019} = {5A5969AF-86CB-489B-B0DE-C0BAADF5B424}
+ {4105330C-28C4-438B-9B65-04F15DBE9986} = {D8925EFE-9CEE-472F-86E4-24E25B1D7429}
+ {BDD026D8-59C0-4B90-83AA-7C1B75082117} = {D8925EFE-9CEE-472F-86E4-24E25B1D7429}
+ {98158873-3DBC-4A0F-A799-71126B838026} = {32548940-0629-46AC-B9BF-E7C41F1C49DE}
+ {7E7411A2-EA67-492B-B159-D15F9CB9B2FA} = {9EF9C5BD-97AA-4E59-A84B-9B7FCCE87649}
+ {1989335E-8FBC-498D-8293-F3816EF36F15} = {9EF9C5BD-97AA-4E59-A84B-9B7FCCE87649}
+ {BD8EB348-8F5D-41BB-BA2E-6D5C7A71FAB5} = {32548940-0629-46AC-B9BF-E7C41F1C49DE}
+ {E561B8AC-C07F-443E-8296-B81EEC5FA5C0} = {A86D1628-AA35-400B-99C9-C59FB7F2AB65}
+ {8234178C-1342-478B-B702-59032587531F} = {A86D1628-AA35-400B-99C9-C59FB7F2AB65}
+ {9020A791-C5C4-4D4A-B495-A8DC42559316} = {9EF9C5BD-97AA-4E59-A84B-9B7FCCE87649}
+ {A86D1628-AA35-400B-99C9-C59FB7F2AB65} = {9EF9C5BD-97AA-4E59-A84B-9B7FCCE87649}
+ {525CEE29-DFC4-4497-AE1A-8B267B404201} = {32548940-0629-46AC-B9BF-E7C41F1C49DE}
+ {0AAE113B-7D85-477C-A72C-55B8C8BED049} = {32548940-0629-46AC-B9BF-E7C41F1C49DE}
+ {E0F4DFC2-46E3-4EF6-AA9D-71F0B73911D3} = {32548940-0629-46AC-B9BF-E7C41F1C49DE}
+ {938AE7EF-E227-4EED-912D-43A9AEF614B5} = {BD8EB348-8F5D-41BB-BA2E-6D5C7A71FAB5}
+ {DB1966F4-AF4B-4E04-97FD-01F0E994EDA0} = {5A5969AF-86CB-489B-B0DE-C0BAADF5B424}
+ {EDFC5F9A-F0F6-46E0-A3DC-9B81F5115327} = {F8538BCE-36D3-4317-8C3C-7540117B99A7}
+ {F78162A1-433B-4245-99FB-6C0B9E7B05C0} = {32548940-0629-46AC-B9BF-E7C41F1C49DE}
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {388473D0-FE20-4821-BFE4-C3CD3E184C8F}
+ EndGlobalSection
+EndGlobal
diff --git a/ES.FX.sln.DotSettings b/ES.FX.sln.DotSettings
new file mode 100644
index 0000000..9411ed1
--- /dev/null
+++ b/ES.FX.sln.DotSettings
@@ -0,0 +1,9 @@
+
+ HINT
+ HINT
+ HINT
+ HINT
+ Custom Full Cleanup
+ ExpressionBody
+ ExpressionBody
+ ExpressionBody
\ No newline at end of file
diff --git a/GitVersion.yaml b/GitVersion.yaml
new file mode 100644
index 0000000..2a9b741
--- /dev/null
+++ b/GitVersion.yaml
@@ -0,0 +1,119 @@
+assembly-versioning-scheme: MajorMinorPatch
+assembly-file-versioning-scheme: MajorMinorPatch
+mode: ContinuousDelivery
+tag-prefix: 'version/[vV]'
+continuous-delivery-fallback-tag: ci
+major-version-bump-message: '\+semver:\s?(breaking|major)'
+minor-version-bump-message: '\+semver:\s?(feature|minor)'
+patch-version-bump-message: '\+semver:\s?(fix|patch)'
+no-bump-message: '\+semver:\s?(none|skip)'
+legacy-semver-padding: 4
+build-metadata-padding: 4
+commits-since-version-source-padding: 4
+tag-pre-release-weight: 60000
+commit-message-incrementing: Enabled
+branches:
+ develop:
+ mode: ContinuousDeployment
+ tag: develop
+ increment: Minor
+ prevent-increment-of-merged-branch-version: false
+ track-merge-target: true
+ regex: ^dev(elop)?(ment)?$
+ source-branches: []
+ tracks-release-branches: true
+ is-release-branch: false
+ is-mainline: false
+ pre-release-weight: 0
+ main:
+ mode: ContinuousDelivery
+ tag: ''
+ increment: Patch
+ prevent-increment-of-merged-branch-version: true
+ track-merge-target: false
+ regex: ^master$|^main$
+ source-branches:
+ - develop
+ - release
+ tracks-release-branches: false
+ is-release-branch: false
+ is-mainline: true
+ pre-release-weight: 55000
+ release:
+ mode: ContinuousDelivery
+ tag: rc
+ increment: None
+ prevent-increment-of-merged-branch-version: true
+ track-merge-target: false
+ regex: ^releases?[/-]
+ source-branches:
+ - develop
+ - main
+ - support
+ - release
+ tracks-release-branches: false
+ is-release-branch: true
+ is-mainline: false
+ pre-release-weight: 30000
+ feature:
+ mode: ContinuousDelivery
+ tag: '{BranchName}'
+ increment: Inherit
+ regex: ^features?[/-]
+ source-branches:
+ - develop
+ - main
+ - release
+ - feature
+ - support
+ - hotfix
+ pre-release-weight: 30000
+ pull-request:
+ mode: ContinuousDelivery
+ tag: PullRequest
+ increment: Inherit
+ tag-number-pattern: '[/-](?\d+)'
+ regex: ^(pull|pull\-requests|pr)[/-]
+ source-branches:
+ - develop
+ - main
+ - release
+ - feature
+ - support
+ - hotfix
+ pre-release-weight: 30000
+ hotfix:
+ mode: ContinuousDelivery
+ tag: hotfix
+ increment: Patch
+ prevent-increment-of-merged-branch-version: false
+ track-merge-target: false
+ regex: ^hotfix(es)?[/-]
+ source-branches:
+ - release
+ - main
+ - support
+ - hotfix
+ tracks-release-branches: false
+ is-release-branch: false
+ is-mainline: false
+ pre-release-weight: 30000
+ support:
+ mode: ContinuousDelivery
+ tag: ''
+ increment: Patch
+ prevent-increment-of-merged-branch-version: true
+ track-merge-target: false
+ regex: ^support[/-]
+ source-branches:
+ - main
+ tracks-release-branches: false
+ is-release-branch: false
+ is-mainline: true
+ pre-release-weight: 55000
+ignore:
+ sha: []
+increment: Inherit
+commit-date-format: yyyy-MM-dd
+merge-message-formats: {}
+update-build-number: true
\ No newline at end of file
diff --git a/playground/Playground.Microservice.Api.Host/Dockerfile b/playground/Playground.Microservice.Api.Host/Dockerfile
new file mode 100644
index 0000000..29f73bd
--- /dev/null
+++ b/playground/Playground.Microservice.Api.Host/Dockerfile
@@ -0,0 +1,24 @@
+#See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging.
+
+FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
+USER app
+WORKDIR /app
+EXPOSE 8080
+
+FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
+ARG BUILD_CONFIGURATION=Release
+WORKDIR /src
+COPY ["playground/Playground.Microservice.Api.Host/Playground.Microservice.Api.Host.csproj", "playground/Playground.Microservice.Api.Host/"]
+RUN dotnet restore "./playground/Playground.Microservice.Api.Host/Playground.Microservice.Api.Host.csproj"
+COPY . .
+WORKDIR "/src/playground/Playground.Microservice.Api.Host"
+RUN dotnet build "./Playground.Microservice.Api.Host.csproj" -c $BUILD_CONFIGURATION -o /app/build
+
+FROM build AS publish
+ARG BUILD_CONFIGURATION=Release
+RUN dotnet publish "./Playground.Microservice.Api.Host.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
+
+FROM base AS final
+WORKDIR /app
+COPY --from=publish /app/publish .
+ENTRYPOINT ["dotnet", "Playground.Microservice.Api.Host.dll"]
\ No newline at end of file
diff --git a/playground/Playground.Microservice.Api.Host/HostedServices/TestHostedService.cs b/playground/Playground.Microservice.Api.Host/HostedServices/TestHostedService.cs
new file mode 100644
index 0000000..4d348de
--- /dev/null
+++ b/playground/Playground.Microservice.Api.Host/HostedServices/TestHostedService.cs
@@ -0,0 +1,31 @@
+#pragma warning disable CS9113 // Parameter is unread.
+
+using Microsoft.EntityFrameworkCore;
+using Playground.Shared.Data.Simple.EntityFrameworkCore;
+
+namespace Playground.Microservice.Api.Host.HostedServices;
+
+internal class TestHostedService(
+ ILogger logger,
+ IServiceProvider serviceProvider,
+ IDbContextFactory dbContextFactory,
+ SimpleDbContext context) : BackgroundService
+{
+ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
+ {
+ while (true)
+ {
+ await Task.Delay(2000);
+
+ logger.LogInformation("Running");
+
+ var writeFactory = serviceProvider.GetRequiredService>();
+
+ var writeContext = writeFactory.CreateDbContext();
+
+ var writeUsers = writeContext.SimpleUsers.ToList();
+
+ var contextUsers = context.SimpleUsers.ToList();
+ }
+ }
+}
\ No newline at end of file
diff --git a/playground/Playground.Microservice.Api.Host/Playground.Microservice.Api.Host.csproj b/playground/Playground.Microservice.Api.Host/Playground.Microservice.Api.Host.csproj
new file mode 100644
index 0000000..b3d9d59
--- /dev/null
+++ b/playground/Playground.Microservice.Api.Host/Playground.Microservice.Api.Host.csproj
@@ -0,0 +1,31 @@
+
+
+
+ net8.0
+ enable
+ enable
+ Linux
+ ..\..
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/playground/Playground.Microservice.Api.Host/Playground.Microservice.Api.Host.http b/playground/Playground.Microservice.Api.Host/Playground.Microservice.Api.Host.http
new file mode 100644
index 0000000..9a3e942
--- /dev/null
+++ b/playground/Playground.Microservice.Api.Host/Playground.Microservice.Api.Host.http
@@ -0,0 +1,6 @@
+@Playground.Microservice.Api.Host_HostAddress = http://localhost:5156
+
+GET {{Playground.Microservice.Api.Host_HostAddress}}/weatherforecast/
+Accept: application/json
+
+###
diff --git a/playground/Playground.Microservice.Api.Host/Program.cs b/playground/Playground.Microservice.Api.Host/Program.cs
new file mode 100644
index 0000000..a129572
--- /dev/null
+++ b/playground/Playground.Microservice.Api.Host/Program.cs
@@ -0,0 +1,42 @@
+using ES.FX.Hosting.Lifetime;
+using ES.FX.Ignite.Hosting;
+using ES.FX.Ignite.Microsoft.EntityFrameworkCore.Migrations;
+using ES.FX.Ignite.Microsoft.EntityFrameworkCore.SqlServer.Hosting;
+using ES.FX.Ignite.Migrations.Hosting;
+using ES.FX.Ignite.Serilog.Hosting;
+using ES.FX.Serilog.Lifetime;
+using Microsoft.Data.SqlClient;
+using Playground.Microservice.Api.Host.HostedServices;
+using Playground.Shared.Data.Simple.EntityFrameworkCore;
+using Playground.Shared.Data.Simple.EntityFrameworkCore.SqlServer;
+
+return await ProgramEntry.CreateBuilder(args).UseSerilog().Build().RunAsync(async _ =>
+{
+ var builder = WebApplication.CreateBuilder(args);
+ builder.AddIgnite();
+
+ builder.AddSerilog();
+ builder.AddMigrationsService();
+
+ builder.AddSqlServerDbContextFactory(nameof(SimpleDbContext),
+ configureSqlServerDbContextOptionsBuilder: sqlServerDbContextOptionsBuilder =>
+ {
+ sqlServerDbContextOptionsBuilder.MigrationsAssembly(typeof(DummyDbContextDesignTimeFactory).Assembly
+ .FullName);
+ });
+ builder.AddDbContextMigrationsTask();
+
+
+ builder.Services.AddHostedService();
+
+
+ builder.Services.AddKeyedSingleton(null, (sp, key) => { return new SqlConnection("nothing"); });
+
+ var app = builder.Build();
+ app.UseIgnite();
+
+ app.Services.GetRequiredService();
+
+ await app.RunAsync();
+ return 0;
+});
\ No newline at end of file
diff --git a/playground/Playground.Microservice.Api.Host/Properties/launchSettings.json b/playground/Playground.Microservice.Api.Host/Properties/launchSettings.json
new file mode 100644
index 0000000..794fac4
--- /dev/null
+++ b/playground/Playground.Microservice.Api.Host/Properties/launchSettings.json
@@ -0,0 +1,25 @@
+{
+ "profiles": {
+ "Project": {
+ "commandName": "Project",
+ "launchBrowser": false,
+ "launchUrl": "swagger",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ },
+ "dotnetRunMessages": true,
+ "applicationUrl": "http://0.0.0.0:8080"
+ },
+ "Docker": {
+ "commandName": "Docker",
+ "launchBrowser": false,
+ "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}/swagger",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ },
+ "publishAllPorts": true,
+ "useSSL": false
+ }
+ },
+ "$schema": "http://json.schemastore.org/launchsettings.json"
+}
\ No newline at end of file
diff --git a/playground/Playground.Microservice.Api.Host/appsettings.Development.json b/playground/Playground.Microservice.Api.Host/appsettings.Development.json
new file mode 100644
index 0000000..0e0dcd2
--- /dev/null
+++ b/playground/Playground.Microservice.Api.Host/appsettings.Development.json
@@ -0,0 +1,3 @@
+{
+
+}
\ No newline at end of file
diff --git a/playground/Playground.Microservice.Api.Host/appsettings.json b/playground/Playground.Microservice.Api.Host/appsettings.json
new file mode 100644
index 0000000..2ab7698
--- /dev/null
+++ b/playground/Playground.Microservice.Api.Host/appsettings.json
@@ -0,0 +1,53 @@
+{
+ "Serilog": {
+ "Using": [ "Serilog.Sinks.Seq" ],
+ "LevelSwitches": { "$consoleLevelSwitch": "Verbose" },
+ "MinimumLevel": {
+ "Default": "Verbose",
+ "Override": {
+ //"Microsoft": "Information",
+ //"Microsoft.Hosting.Lifetime": "Information",
+ //"Microsoft.AspNetCore.Hosting": "Warning",
+ //"Microsoft.AspNetCore.Mvc": "Warning",
+ //"Microsoft.AspNetCore.Routing": "Warning",
+ "Microsoft.EntityFrameworkCore": "Information",
+ "Microsoft.EntityFrameworkCore.Database.Command": "Warning"
+ }
+ },
+
+ "WriteTo": [
+ {
+ "Name": "Console",
+ "Args": {
+ "outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] ({SourceContext}) {Message:lj}{NewLine}{Exception}",
+ "levelSwitch": "$consoleLevelSwitch"
+ }
+ }
+ //{
+ // "Name": "Seq",
+ // "Args": {
+ // "serverUrl": "http://seq:5341"
+ // }
+ //}
+ ]
+ },
+ "Ignite": {
+ "Services": {
+ "MigrationsService": {
+ "Settings": {
+ "Enabled": true
+ }
+ }
+ },
+ "DbContext": {
+ "SimpleDbContext": {
+ "ConnectionString": "Data Source=(local);Initial Catalog=SimpleDbContext;User ID=sa;Password=SuperPass#;MultipleActiveResultSets=true;Connect Timeout=1000;TrustServerCertificate=True",
+ "DisableRetry": true,
+ "Settings": {
+ "DisableHealthChecks": false
+ }
+ }
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/playground/Playground.Microservice.Worker.Host/Playground.Microservice.Worker.Host.csproj b/playground/Playground.Microservice.Worker.Host/Playground.Microservice.Worker.Host.csproj
new file mode 100644
index 0000000..707f72d
--- /dev/null
+++ b/playground/Playground.Microservice.Worker.Host/Playground.Microservice.Worker.Host.csproj
@@ -0,0 +1,15 @@
+
+
+
+ Exe
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
+
diff --git a/playground/Playground.Microservice.Worker.Host/Program.cs b/playground/Playground.Microservice.Worker.Host/Program.cs
new file mode 100644
index 0000000..e139ee9
--- /dev/null
+++ b/playground/Playground.Microservice.Worker.Host/Program.cs
@@ -0,0 +1,15 @@
+using ES.FX.Hosting.Lifetime;
+using ES.FX.Ignite.Hosting;
+using Microsoft.Extensions.Hosting;
+
+return await ProgramEntry.CreateBuilder(args).Build().RunAsync(async options =>
+{
+ var builder = Host.CreateApplicationBuilder(args);
+ builder.AddIgnite();
+
+ var app = builder.Build();
+ app.UseIgnite();
+
+ await app.RunAsync();
+ return 0;
+});
\ No newline at end of file
diff --git a/playground/Playground.Shared.Data.Simple.EntityFrameworkCore.SqlServer/DummyDbContextDesignTimeFactory.cs b/playground/Playground.Shared.Data.Simple.EntityFrameworkCore.SqlServer/DummyDbContextDesignTimeFactory.cs
new file mode 100644
index 0000000..a62d639
--- /dev/null
+++ b/playground/Playground.Shared.Data.Simple.EntityFrameworkCore.SqlServer/DummyDbContextDesignTimeFactory.cs
@@ -0,0 +1,31 @@
+using JetBrains.Annotations;
+using Microsoft.Data.SqlClient;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Design;
+
+namespace Playground.Shared.Data.Simple.EntityFrameworkCore.SqlServer;
+
+[PublicAPI]
+public class DummyDbContextDesignTimeFactory : IDesignTimeDbContextFactory
+{
+ public SimpleDbContext CreateDbContext(string[] args)
+ {
+ var optionsBuilder = new DbContextOptionsBuilder();
+ var sqlBuilder = new SqlConnectionStringBuilder
+ {
+ DataSource = "(local)",
+ UserID = "sa",
+ Password = "SuperPass#",
+ InitialCatalog = $"{nameof(SimpleDbContext)}_Design",
+ TrustServerCertificate = true
+ };
+ optionsBuilder.UseSqlServer(sqlBuilder.ConnectionString,
+ sqlServerDbContextOptionsBuilder =>
+ {
+ sqlServerDbContextOptionsBuilder.MigrationsAssembly(typeof(DummyDbContextDesignTimeFactory).Assembly
+ .FullName);
+ });
+
+ return new SimpleDbContext(optionsBuilder.Options);
+ }
+}
\ No newline at end of file
diff --git a/playground/Playground.Shared.Data.Simple.EntityFrameworkCore.SqlServer/Migrations/20240611155142_V1.Designer.cs b/playground/Playground.Shared.Data.Simple.EntityFrameworkCore.SqlServer/Migrations/20240611155142_V1.Designer.cs
new file mode 100644
index 0000000..07393b9
--- /dev/null
+++ b/playground/Playground.Shared.Data.Simple.EntityFrameworkCore.SqlServer/Migrations/20240611155142_V1.Designer.cs
@@ -0,0 +1,41 @@
+//
+using System;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Metadata;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Playground.Shared.Data.Simple.EntityFrameworkCore;
+
+#nullable disable
+
+namespace Playground.Shared.Data.Simple.EntityFrameworkCore.SqlServer.Migrations
+{
+ [DbContext(typeof(SimpleDbContext))]
+ [Migration("20240611155142_V1")]
+ partial class V1
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "8.0.6")
+ .HasAnnotation("Relational:MaxIdentifierLength", 128);
+
+ SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
+
+ modelBuilder.Entity("Playground.Shared.Data.Simple.EntityFrameworkCore.Entities.SimpleUser", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier");
+
+ b.HasKey("Id");
+
+ b.ToTable("SimpleUsers");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/playground/Playground.Shared.Data.Simple.EntityFrameworkCore.SqlServer/Migrations/20240611155142_V1.cs b/playground/Playground.Shared.Data.Simple.EntityFrameworkCore.SqlServer/Migrations/20240611155142_V1.cs
new file mode 100644
index 0000000..c16b2d5
--- /dev/null
+++ b/playground/Playground.Shared.Data.Simple.EntityFrameworkCore.SqlServer/Migrations/20240611155142_V1.cs
@@ -0,0 +1,33 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Playground.Shared.Data.Simple.EntityFrameworkCore.SqlServer.Migrations
+{
+ ///
+ public partial class V1 : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.CreateTable(
+ name: "SimpleUsers",
+ columns: table => new
+ {
+ Id = table.Column(type: "uniqueidentifier", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_SimpleUsers", x => x.Id);
+ });
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(
+ name: "SimpleUsers");
+ }
+ }
+}
diff --git a/playground/Playground.Shared.Data.Simple.EntityFrameworkCore.SqlServer/Migrations/SimpleDbContextModelSnapshot.cs b/playground/Playground.Shared.Data.Simple.EntityFrameworkCore.SqlServer/Migrations/SimpleDbContextModelSnapshot.cs
new file mode 100644
index 0000000..72efec0
--- /dev/null
+++ b/playground/Playground.Shared.Data.Simple.EntityFrameworkCore.SqlServer/Migrations/SimpleDbContextModelSnapshot.cs
@@ -0,0 +1,38 @@
+//
+using System;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Metadata;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Playground.Shared.Data.Simple.EntityFrameworkCore;
+
+#nullable disable
+
+namespace Playground.Shared.Data.Simple.EntityFrameworkCore.SqlServer.Migrations
+{
+ [DbContext(typeof(SimpleDbContext))]
+ partial class SimpleDbContextModelSnapshot : ModelSnapshot
+ {
+ protected override void BuildModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "8.0.6")
+ .HasAnnotation("Relational:MaxIdentifierLength", 128);
+
+ SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
+
+ modelBuilder.Entity("Playground.Shared.Data.Simple.EntityFrameworkCore.Entities.SimpleUser", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier");
+
+ b.HasKey("Id");
+
+ b.ToTable("SimpleUsers");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/playground/Playground.Shared.Data.Simple.EntityFrameworkCore.SqlServer/Playground.Shared.Data.Simple.EntityFrameworkCore.SqlServer.csproj b/playground/Playground.Shared.Data.Simple.EntityFrameworkCore.SqlServer/Playground.Shared.Data.Simple.EntityFrameworkCore.SqlServer.csproj
new file mode 100644
index 0000000..81ec7d6
--- /dev/null
+++ b/playground/Playground.Shared.Data.Simple.EntityFrameworkCore.SqlServer/Playground.Shared.Data.Simple.EntityFrameworkCore.SqlServer.csproj
@@ -0,0 +1,22 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
diff --git a/playground/Playground.Shared.Data.Simple.EntityFrameworkCore/Configurations/SimpleUserEntityConfiguration.cs b/playground/Playground.Shared.Data.Simple.EntityFrameworkCore/Configurations/SimpleUserEntityConfiguration.cs
new file mode 100644
index 0000000..bedcec5
--- /dev/null
+++ b/playground/Playground.Shared.Data.Simple.EntityFrameworkCore/Configurations/SimpleUserEntityConfiguration.cs
@@ -0,0 +1,13 @@
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+using Playground.Shared.Data.Simple.EntityFrameworkCore.Entities;
+
+namespace Playground.Shared.Data.Simple.EntityFrameworkCore.Configurations;
+
+public class SimpleUserEntityConfiguration : IEntityTypeConfiguration
+{
+ public void Configure(EntityTypeBuilder builder)
+ {
+ builder.HasKey(p => p.Id);
+ }
+}
\ No newline at end of file
diff --git a/playground/Playground.Shared.Data.Simple.EntityFrameworkCore/Entities/SimpleUser.cs b/playground/Playground.Shared.Data.Simple.EntityFrameworkCore/Entities/SimpleUser.cs
new file mode 100644
index 0000000..e1e45a7
--- /dev/null
+++ b/playground/Playground.Shared.Data.Simple.EntityFrameworkCore/Entities/SimpleUser.cs
@@ -0,0 +1,6 @@
+namespace Playground.Shared.Data.Simple.EntityFrameworkCore.Entities;
+
+public class SimpleUser
+{
+ public Guid Id { get; set; }
+}
\ No newline at end of file
diff --git a/playground/Playground.Shared.Data.Simple.EntityFrameworkCore/Playground.Shared.Data.Simple.EntityFrameworkCore.csproj b/playground/Playground.Shared.Data.Simple.EntityFrameworkCore/Playground.Shared.Data.Simple.EntityFrameworkCore.csproj
new file mode 100644
index 0000000..7bf21ec
--- /dev/null
+++ b/playground/Playground.Shared.Data.Simple.EntityFrameworkCore/Playground.Shared.Data.Simple.EntityFrameworkCore.csproj
@@ -0,0 +1,13 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
diff --git a/playground/Playground.Shared.Data.Simple.EntityFrameworkCore/SimpleDbContext.cs b/playground/Playground.Shared.Data.Simple.EntityFrameworkCore/SimpleDbContext.cs
new file mode 100644
index 0000000..128707d
--- /dev/null
+++ b/playground/Playground.Shared.Data.Simple.EntityFrameworkCore/SimpleDbContext.cs
@@ -0,0 +1,19 @@
+using Microsoft.EntityFrameworkCore;
+using Playground.Shared.Data.Simple.EntityFrameworkCore.Entities;
+
+namespace Playground.Shared.Data.Simple.EntityFrameworkCore;
+
+public class SimpleDbContext(
+ DbContextOptions dbContextOptions) :
+ DbContext(dbContextOptions)
+{
+ public DbSet SimpleUsers { get; set; }
+
+ protected override void OnModelCreating(ModelBuilder modelBuilder)
+ {
+ modelBuilder
+ .ApplyConfigurationsFromAssembly(typeof(SimpleDbContext).Assembly);
+
+ base.OnModelCreating(modelBuilder);
+ }
+}
\ No newline at end of file
diff --git a/playground/Playground.Shared.Data.Simple.EntityFrameworkCore/SimpleReadOnlyDbContext.cs b/playground/Playground.Shared.Data.Simple.EntityFrameworkCore/SimpleReadOnlyDbContext.cs
new file mode 100644
index 0000000..44bb15a
--- /dev/null
+++ b/playground/Playground.Shared.Data.Simple.EntityFrameworkCore/SimpleReadOnlyDbContext.cs
@@ -0,0 +1,6 @@
+using Microsoft.EntityFrameworkCore;
+
+namespace Playground.Shared.Data.Simple.EntityFrameworkCore;
+
+public class SimpleReadOnlyDbContext(DbContextOptions dbContextOptions) :
+ SimpleDbContext(dbContextOptions);
\ No newline at end of file
diff --git a/playground/Playground.SimpleConsole/Playground.SimpleConsole.csproj b/playground/Playground.SimpleConsole/Playground.SimpleConsole.csproj
new file mode 100644
index 0000000..f0fb694
--- /dev/null
+++ b/playground/Playground.SimpleConsole/Playground.SimpleConsole.csproj
@@ -0,0 +1,15 @@
+
+
+
+ Exe
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
+
diff --git a/playground/Playground.SimpleConsole/Program.cs b/playground/Playground.SimpleConsole/Program.cs
new file mode 100644
index 0000000..3df29c6
--- /dev/null
+++ b/playground/Playground.SimpleConsole/Program.cs
@@ -0,0 +1,9 @@
+using ES.FX.Hosting.Lifetime;
+using ES.FX.Serilog.Lifetime;
+
+return await ProgramEntry.CreateBuilder(args).UseSerilog().Build().RunAsync(options =>
+{
+ throw new Exception("Test exception");
+
+ //return Task.FromResult(1);
+});
\ No newline at end of file
diff --git a/src/ES.FX.Hosting/ES.FX.Hosting.csproj b/src/ES.FX.Hosting/ES.FX.Hosting.csproj
new file mode 100644
index 0000000..94de28c
--- /dev/null
+++ b/src/ES.FX.Hosting/ES.FX.Hosting.csproj
@@ -0,0 +1,14 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
+
diff --git a/src/ES.FX.Hosting/Lifetime/ControlledExitException.cs b/src/ES.FX.Hosting/Lifetime/ControlledExitException.cs
new file mode 100644
index 0000000..251739d
--- /dev/null
+++ b/src/ES.FX.Hosting/Lifetime/ControlledExitException.cs
@@ -0,0 +1,21 @@
+namespace ES.FX.Hosting.Lifetime;
+
+///
+/// Exception used to terminate the application with a specific exit code
+///
+public class ControlledExitException : Exception
+{
+ public ControlledExitException()
+ {
+ }
+
+ public ControlledExitException(string message) : base(message)
+ {
+ }
+
+ public ControlledExitException(string message, Exception innerException) : base(message, innerException)
+ {
+ }
+
+ public int ExitCode { get; set; } = 0;
+}
\ No newline at end of file
diff --git a/src/ES.FX.Hosting/Lifetime/ProgramEntry.cs b/src/ES.FX.Hosting/Lifetime/ProgramEntry.cs
new file mode 100644
index 0000000..db5f9d0
--- /dev/null
+++ b/src/ES.FX.Hosting/Lifetime/ProgramEntry.cs
@@ -0,0 +1,49 @@
+using Microsoft.Extensions.Logging;
+
+namespace ES.FX.Hosting.Lifetime;
+
+///
+/// A wrapper for program entry point with support for logging and graceful shutdown.
+///
+/// Logger used for log messages
+/// List of actions to be executed before the program exits (regardless of the exit reason)
+/// Program entry options
+public sealed class ProgramEntry(
+ ILogger logger,
+ IReadOnlyList> exitActions,
+ ProgramEntryOptions options)
+{
+ public async Task RunAsync(Func> action)
+ {
+ try
+ {
+ logger.LogTrace("Starting Program");
+ var exitCode = await action(options);
+ logger.LogDebug("Program completed with exit code {exitCode}", exitCode);
+ return exitCode;
+ }
+ catch (ControlledExitException ex)
+ {
+ logger.LogDebug("Program exited controlled with message \"{message}\" and exit code {exitCode}",
+ ex.Message, ex.ExitCode);
+ return ex.ExitCode;
+ }
+ catch (Exception ex)
+ {
+ logger.LogCritical(ex, "Program terminated unexpectedly");
+ return 1;
+ }
+ finally
+ {
+ foreach (var exitAction in exitActions) await exitAction(options);
+ }
+ }
+
+
+ ///
+ /// Initializes a new instance of the class with preconfigured defaults.
+ ///
+ /// The command line arguments.
+ /// The .
+ public static ProgramEntryBuilder CreateBuilder(string[] args) => new(new ProgramEntryOptions { Args = args });
+}
\ No newline at end of file
diff --git a/src/ES.FX.Hosting/Lifetime/ProgramEntryBuilder.cs b/src/ES.FX.Hosting/Lifetime/ProgramEntryBuilder.cs
new file mode 100644
index 0000000..f3d1714
--- /dev/null
+++ b/src/ES.FX.Hosting/Lifetime/ProgramEntryBuilder.cs
@@ -0,0 +1,47 @@
+using Microsoft.Extensions.Logging;
+
+namespace ES.FX.Hosting.Lifetime;
+
+///
+/// Builder for creating a instance
+///
+public class ProgramEntryBuilder
+{
+ private readonly List> _exitActions = [];
+ private readonly ProgramEntryOptions _options;
+ private ILogger _logger;
+
+ public ProgramEntryBuilder(ProgramEntryOptions options)
+ {
+ _options = options;
+ _logger = LoggerFactory.Create(builder => { builder.AddConsole(); }).CreateLogger();
+ }
+
+ ///
+ /// Sets the logger to be used by the
+ ///
+ /// The new instance
+ /// The
+ public ProgramEntryBuilder WithLogger(ILogger logger)
+ {
+ _logger = logger;
+ return this;
+ }
+
+ ///
+ /// Adds a new action to be executed before the program exits (regardless of the exit reason)
+ ///
+ /// Func to execute on exit
+ /// The
+ public ProgramEntryBuilder AddExitAction(Func exitAction)
+ {
+ _exitActions.Add(exitAction);
+ return this;
+ }
+
+ ///
+ /// Builds the instance
+ ///
+ /// The instance
+ public ProgramEntry Build() => new(_logger, _exitActions, new ProgramEntryOptions { Args = _options.Args });
+}
\ No newline at end of file
diff --git a/src/ES.FX.Hosting/Lifetime/ProgramEntryOptions.cs b/src/ES.FX.Hosting/Lifetime/ProgramEntryOptions.cs
new file mode 100644
index 0000000..1e1041f
--- /dev/null
+++ b/src/ES.FX.Hosting/Lifetime/ProgramEntryOptions.cs
@@ -0,0 +1,12 @@
+namespace ES.FX.Hosting.Lifetime;
+
+///
+/// Options for the
+///
+public class ProgramEntryOptions
+{
+ ///
+ /// The command line arguments.
+ ///
+ public string[]? Args { get; init; }
+}
\ No newline at end of file
diff --git a/src/ES.FX.Ignite.Hosting/ES.FX.Ignite.Hosting.csproj b/src/ES.FX.Ignite.Hosting/ES.FX.Ignite.Hosting.csproj
new file mode 100644
index 0000000..2f4d929
--- /dev/null
+++ b/src/ES.FX.Ignite.Hosting/ES.FX.Ignite.Hosting.csproj
@@ -0,0 +1,16 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
diff --git a/src/ES.FX.Ignite.Hosting/IgniteHostingExtensions.cs b/src/ES.FX.Ignite.Hosting/IgniteHostingExtensions.cs
new file mode 100644
index 0000000..004c31c
--- /dev/null
+++ b/src/ES.FX.Ignite.Hosting/IgniteHostingExtensions.cs
@@ -0,0 +1,27 @@
+using JetBrains.Annotations;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Hosting;
+
+namespace ES.FX.Ignite.Hosting;
+
+[PublicAPI]
+public static class IgniteHostingExtensions
+{
+ public static void AddIgnite(this IHostApplicationBuilder builder)
+ {
+ AddDefaultConfigurationSlim(builder);
+ }
+
+ public static IHost UseIgnite(this IHost app) => app;
+
+
+ private static void AddDefaultConfigurationSlim(IHostApplicationBuilder builder)
+ {
+ // Add a new environment-specific configuration file to override the default settings.
+ // This allows adding environment-specific settings without changing the default settings.
+ // Useful when mounting a configuration file from a secret manager or a configuration provider.
+ builder.Configuration
+ .AddJsonFile($"appsettings.{builder.Environment}.overrides.json",
+ true, true);
+ }
+}
\ No newline at end of file
diff --git a/src/ES.FX.Ignite.Microsoft.Data.SqlClient/Configuration/SqlServerClientSparkOptions.cs b/src/ES.FX.Ignite.Microsoft.Data.SqlClient/Configuration/SqlServerClientSparkOptions.cs
new file mode 100644
index 0000000..e0da429
--- /dev/null
+++ b/src/ES.FX.Ignite.Microsoft.Data.SqlClient/Configuration/SqlServerClientSparkOptions.cs
@@ -0,0 +1,14 @@
+using Microsoft.Data.SqlClient;
+
+namespace ES.FX.Ignite.Microsoft.Data.SqlClient.Configuration;
+
+///
+/// Provides the options for connecting to a SQL Server database using a
+///
+public class SqlServerClientSparkOptions
+{
+ ///
+ /// The connection string of the SQL server database to connect to.
+ ///
+ public string? ConnectionString { get; set; }
+}
\ No newline at end of file
diff --git a/src/ES.FX.Ignite.Microsoft.Data.SqlClient/Configuration/SqlServerClientSparkSettings.cs b/src/ES.FX.Ignite.Microsoft.Data.SqlClient/Configuration/SqlServerClientSparkSettings.cs
new file mode 100644
index 0000000..89bd419
--- /dev/null
+++ b/src/ES.FX.Ignite.Microsoft.Data.SqlClient/Configuration/SqlServerClientSparkSettings.cs
@@ -0,0 +1,19 @@
+using Microsoft.Data.SqlClient;
+
+namespace ES.FX.Ignite.Microsoft.Data.SqlClient.Configuration;
+
+///
+/// Provides the settings for connecting to a SQL Server database using a
+///
+public class SqlServerClientSparkSettings
+{
+ ///
+ /// Gets or sets a boolean value that indicates whether the database health check is disabled or not.
+ ///
+ public bool DisableHealthChecks { get; set; }
+
+ ///
+ /// Gets or sets a boolean value that indicates whether the OpenTelemetry tracing is disabled or not.
+ ///
+ public bool DisableTracing { get; set; }
+}
\ No newline at end of file
diff --git a/src/ES.FX.Ignite.Microsoft.Data.SqlClient/ES.FX.Ignite.Microsoft.Data.SqlClient.csproj b/src/ES.FX.Ignite.Microsoft.Data.SqlClient/ES.FX.Ignite.Microsoft.Data.SqlClient.csproj
new file mode 100644
index 0000000..f363842
--- /dev/null
+++ b/src/ES.FX.Ignite.Microsoft.Data.SqlClient/ES.FX.Ignite.Microsoft.Data.SqlClient.csproj
@@ -0,0 +1,20 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/ES.FX.Ignite.Microsoft.Data.SqlClient/Hosting/SqlServerClientHostingExtensions.cs b/src/ES.FX.Ignite.Microsoft.Data.SqlClient/Hosting/SqlServerClientHostingExtensions.cs
new file mode 100644
index 0000000..52a55b1
--- /dev/null
+++ b/src/ES.FX.Ignite.Microsoft.Data.SqlClient/Hosting/SqlServerClientHostingExtensions.cs
@@ -0,0 +1,157 @@
+using ES.FX.Ignite.Microsoft.Data.SqlClient.Configuration;
+using ES.FX.Ignite.Microsoft.Data.SqlClient.Spark;
+using ES.FX.Ignite.Spark.Configuration;
+using ES.FX.Ignite.Spark.HealthChecks;
+using ES.FX.Microsoft.Data.SqlClient.Abstractions;
+using ES.FX.Microsoft.Data.SqlClient.Factories;
+using HealthChecks.SqlServer;
+using JetBrains.Annotations;
+using Microsoft.Data.SqlClient;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Diagnostics.HealthChecks;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Options;
+using OpenTelemetry.Trace;
+
+namespace ES.FX.Ignite.Microsoft.Data.SqlClient.Hosting;
+
+[PublicAPI]
+public static class SqlServerClientHostingExtensions
+{
+ ///
+ /// Registers as a service in the services provided by the
+ /// .
+ /// Enables health check, logging and telemetry for the .
+ ///
+ /// The to read config from and add services to.
+ /// A name used to retrieve the settings and options from configuration
+ ///
+ /// If not null, registers a keyed service with the service key. If null, registers a default
+ /// service
+ ///
+ ///
+ /// An optional delegate that can be used for customizing settings. It's invoked after the
+ /// settings are read from the configuration.
+ ///
+ ///
+ /// An optional delegate that can be used for customizing options. It's invoked after the
+ /// options are read from the configuration.
+ ///
+ ///
+ /// The lifetime of the . Default is
+ /// .
+ ///
+ ///
+ /// The configuration section path. Default is
+ /// .
+ ///
+ public static void AddSqlServerClient(this IHostApplicationBuilder builder,
+ string name,
+ string? serviceKey = null,
+ Action? configureSettings = null,
+ Action? configureOptions = null,
+ ServiceLifetime lifetime = ServiceLifetime.Transient,
+ string configurationSectionPath = SqlServerClientSpark.ConfigurationSectionPath) =>
+ RegisterSqlServerClient(builder, name, serviceKey, configureSettings, configureOptions, lifetime,
+ configurationSectionPath);
+
+
+ ///
+ /// Registers as a service in the services provided by the
+ /// .
+ /// Enables health check, logging and telemetry for .
+ ///
+ /// The to read config from and add services to.
+ /// A name used to retrieve the settings and options from configuration
+ ///
+ /// If not null, registers a keyed service with the service key. If null, registers a default
+ /// service
+ ///
+ ///
+ /// An optional delegate that can be used for customizing settings. It's invoked after the
+ /// settings are read from the configuration.
+ ///
+ ///
+ /// An optional delegate that can be used for customizing options. It's invoked after the
+ /// options are read from the configuration.
+ ///
+ ///
+ /// The lifetime of the . Default is
+ /// .
+ ///
+ ///
+ /// The configuration section path. Default is
+ /// .
+ ///
+ /// For convenience, this method also registers as a service.
+ public static void AddSqlServerClientFactory(this IHostApplicationBuilder builder,
+ string name,
+ string? serviceKey = null,
+ Action? configureSettings = null,
+ Action? configureOptions = null,
+ ServiceLifetime lifetime = ServiceLifetime.Transient,
+ string configurationSectionPath = SqlServerClientSpark.ConfigurationSectionPath) =>
+ RegisterSqlServerClient(builder, name, serviceKey, configureSettings, configureOptions, lifetime,
+ configurationSectionPath, true);
+
+
+ private static void RegisterSqlServerClient(this IHostApplicationBuilder builder,
+ string name,
+ string? serviceKey = null,
+ Action? configureSettings = null,
+ Action? configureOptions = null,
+ ServiceLifetime lifetime = ServiceLifetime.Transient,
+ string configurationSectionPath = SqlServerClientSpark.ConfigurationSectionPath,
+ bool useFactory = false)
+ {
+ var configPath = SparkConfig.Path(name, configurationSectionPath);
+
+ var settings = SparkConfig.GetSettings(builder.Configuration, configPath, configureSettings);
+ builder.Services.AddKeyedSingleton(serviceKey, settings);
+
+ var optionsBuilder = builder.Services
+ .AddOptions(serviceKey ?? Options.DefaultName)
+ .BindConfiguration(configPath);
+ if (configureOptions is not null) optionsBuilder.Configure(configureOptions);
+
+
+ if (useFactory)
+ builder.Services.Add(new ServiceDescriptor(typeof(ISqlConnectionFactory), serviceKey,
+ (provider, key) => new DelegateSqlConnectionFactory(provider, sp => ResolveSqlConnection(sp, key)),
+ lifetime));
+
+ builder.Services.Add(new ServiceDescriptor(typeof(SqlConnection), serviceKey, ResolveSqlConnection, lifetime));
+
+
+ if (!settings.DisableTracing)
+ builder.Services.AddOpenTelemetry().WithTracing(tracerProviderBuilder =>
+ {
+ tracerProviderBuilder.AddSqlClientInstrumentation();
+ });
+
+ if (!settings.DisableHealthChecks)
+ builder.TryAddHealthCheck(new HealthCheckRegistration(
+ $"{SqlServerClientSpark.Name}_{name.Trim()}" +
+ (string.IsNullOrWhiteSpace(serviceKey) ? string.Empty : $"_{serviceKey.Trim()}")
+ ,
+ sp =>
+ {
+ var options = sp.GetRequiredService>().Get(serviceKey);
+ return new SqlServerHealthCheck(new SqlServerHealthCheckOptions
+ {
+ ConnectionString = options.ConnectionString ?? string.Empty
+ });
+ },
+ default,
+ default,
+ default));
+
+ return;
+
+ static SqlConnection ResolveSqlConnection(IServiceProvider sp, object? key)
+ {
+ var options = sp.GetRequiredService>().Get(key as string);
+ return new SqlConnection(options.ConnectionString);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/ES.FX.Ignite.Microsoft.Data.SqlClient/Spark/SqlServerClientSpark.cs b/src/ES.FX.Ignite.Microsoft.Data.SqlClient/Spark/SqlServerClientSpark.cs
new file mode 100644
index 0000000..4c66f3e
--- /dev/null
+++ b/src/ES.FX.Ignite.Microsoft.Data.SqlClient/Spark/SqlServerClientSpark.cs
@@ -0,0 +1,19 @@
+using ES.FX.Ignite.Spark.Configuration;
+
+namespace ES.FX.Ignite.Microsoft.Data.SqlClient.Spark;
+
+///
+/// definition
+///
+public static class SqlServerClientSpark
+{
+ ///
+ /// Spark name
+ ///
+ public const string Name = "SqlServerClient";
+
+ ///
+ /// The default configuration section path
+ ///
+ public const string ConfigurationSectionPath = $"{IgniteConfigurationSections.Ignite}:{Name}";
+}
\ No newline at end of file
diff --git a/src/ES.FX.Ignite.Microsoft.EntityFrameworkCore.SqlServer/Configuration/SqlServerDbContextSparkOptions.cs b/src/ES.FX.Ignite.Microsoft.EntityFrameworkCore.SqlServer/Configuration/SqlServerDbContextSparkOptions.cs
new file mode 100644
index 0000000..450a033
--- /dev/null
+++ b/src/ES.FX.Ignite.Microsoft.EntityFrameworkCore.SqlServer/Configuration/SqlServerDbContextSparkOptions.cs
@@ -0,0 +1,26 @@
+using Microsoft.EntityFrameworkCore;
+
+namespace ES.FX.Ignite.Microsoft.EntityFrameworkCore.SqlServer.Configuration;
+
+///
+/// Provides the options for connecting to a SQL Server database using EntityFrameworkCore using the
+///
+///
+/// type
+public class SqlServerDbContextSparkOptions where TDbContext : DbContext
+{
+ ///
+ /// The connection string of the SQL server database to connect to.
+ ///
+ public string? ConnectionString { get; set; }
+
+ ///
+ /// Gets or sets whether retries should be disabled.
+ ///
+ public bool DisableRetry { get; set; }
+
+ ///
+ /// Gets or sets the time in seconds to wait for the command to execute.
+ ///
+ public int? CommandTimeout { get; set; }
+}
\ No newline at end of file
diff --git a/src/ES.FX.Ignite.Microsoft.EntityFrameworkCore.SqlServer/Configuration/SqlServerDbContextSparkSettings.cs b/src/ES.FX.Ignite.Microsoft.EntityFrameworkCore.SqlServer/Configuration/SqlServerDbContextSparkSettings.cs
new file mode 100644
index 0000000..c12380b
--- /dev/null
+++ b/src/ES.FX.Ignite.Microsoft.EntityFrameworkCore.SqlServer/Configuration/SqlServerDbContextSparkSettings.cs
@@ -0,0 +1,21 @@
+using Microsoft.EntityFrameworkCore;
+
+namespace ES.FX.Ignite.Microsoft.EntityFrameworkCore.SqlServer.Configuration;
+
+///
+/// Provides the settings for connecting to a SQL Server database using EntityFrameworkCore using a
+///
+///
+/// type
+public class SqlServerDbContextSparkSettings where TDbContext : DbContext
+{
+ ///
+ /// Gets or sets a boolean value that indicates whether the database health check is disabled or not.
+ ///
+ public bool DisableHealthChecks { get; set; }
+
+ ///
+ /// Gets or sets a boolean value that indicates whether the OpenTelemetry tracing is disabled or not.
+ ///
+ public bool DisableTracing { get; set; }
+}
\ No newline at end of file
diff --git a/src/ES.FX.Ignite.Microsoft.EntityFrameworkCore.SqlServer/ES.FX.Ignite.Microsoft.EntityFrameworkCore.SqlServer.csproj b/src/ES.FX.Ignite.Microsoft.EntityFrameworkCore.SqlServer/ES.FX.Ignite.Microsoft.EntityFrameworkCore.SqlServer.csproj
new file mode 100644
index 0000000..e8a17f8
--- /dev/null
+++ b/src/ES.FX.Ignite.Microsoft.EntityFrameworkCore.SqlServer/ES.FX.Ignite.Microsoft.EntityFrameworkCore.SqlServer.csproj
@@ -0,0 +1,22 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/ES.FX.Ignite.Microsoft.EntityFrameworkCore.SqlServer/Hosting/SqlServerDbContextHostingExtensions.cs b/src/ES.FX.Ignite.Microsoft.EntityFrameworkCore.SqlServer/Hosting/SqlServerDbContextHostingExtensions.cs
new file mode 100644
index 0000000..0471dd2
--- /dev/null
+++ b/src/ES.FX.Ignite.Microsoft.EntityFrameworkCore.SqlServer/Hosting/SqlServerDbContextHostingExtensions.cs
@@ -0,0 +1,195 @@
+using EntityFramework.Exceptions.SqlServer;
+using ES.FX.Ignite.Microsoft.EntityFrameworkCore.Spark;
+using ES.FX.Ignite.Microsoft.EntityFrameworkCore.SqlServer.Configuration;
+using ES.FX.Ignite.Spark.Configuration;
+using ES.FX.Ignite.Spark.HealthChecks;
+using JetBrains.Annotations;
+using Microsoft.Data.SqlClient;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Options;
+using OpenTelemetry.Trace;
+
+namespace ES.FX.Ignite.Microsoft.EntityFrameworkCore.SqlServer.Hosting;
+
+[PublicAPI]
+public static class SqlServerDbContextHostingExtensions
+{
+ ///
+ /// Registers as a service in the services provided by the
+ /// .
+ /// Enables retries, health check, logging and telemetry for the .
+ ///
+ /// The that needs to be registered.
+ /// The to read config from and add services to.
+ /// A name used to retrieve the settings and options from configuration
+ ///
+ /// An optional delegate that can be used for customizing settings. It's invoked after the
+ /// settings are read from the configuration.
+ ///
+ ///
+ /// An optional delegate that can be used for customizing options. It's invoked after the
+ /// options are read from the configuration.
+ ///
+ ///
+ /// An optional delegate that can be used for customizing the
+ /// .
+ ///
+ ///
+ /// An optional delegate that can be used for customizing the
+ /// .
+ ///
+ ///
+ /// The lifetime of the . Default is
+ /// .
+ ///
+ ///
+ /// The configuration section path. Default is
+ /// .
+ ///
+ public static void AddSqlServerDbContext(this IHostApplicationBuilder builder,
+ string? name = null,
+ Action>? configureSettings = null,
+ Action>? configureOptions = null,
+ Action? configureDbContextOptionsBuilder = null,
+ Action? configureSqlServerDbContextOptionsBuilder = null,
+ ServiceLifetime lifetime = ServiceLifetime.Transient,
+ string configurationSectionPath = DbContextSpark.ConfigurationSectionPath) where TDbContext : DbContext =>
+ builder.RegisterDbContext(name, configureSettings, configureOptions,
+ configureDbContextOptionsBuilder, configureSqlServerDbContextOptionsBuilder, lifetime,
+ configurationSectionPath);
+
+ ///
+ /// Registers a as a service in the services provided by the
+ /// .
+ /// Enables retries, health check, logging and telemetry for the .
+ ///
+ ///
+ /// The type used by the
+ /// that needs to be registered.
+ ///
+ /// The to read config from and add services to.
+ /// A name used to retrieve the settings and options from configuration
+ ///
+ /// An optional delegate that can be used for customizing settings. It's invoked after the
+ /// settings are read from the configuration.
+ ///
+ ///
+ /// An optional delegate that can be used for customizing options. It's invoked after the
+ /// options are read from the configuration.
+ ///
+ ///
+ /// An optional delegate that can be used for customizing the
+ /// .
+ ///
+ ///
+ /// An optional delegate that can be used for customizing the
+ /// .
+ ///
+ ///
+ /// The lifetime of the . Default is
+ /// .
+ ///
+ ///
+ /// The configuration section path. Default is
+ /// .
+ ///
+ ///
+ /// This also registers the as a service in the services provided by the
+ /// with the same lifetime specified by .
+ ///
+ public static void AddSqlServerDbContextFactory(this IHostApplicationBuilder builder,
+ string? name = null,
+ Action>? configureSettings = null,
+ Action>? configureOptions = null,
+ Action? configureDbContextOptionsBuilder = null,
+ Action? configureSqlServerDbContextOptionsBuilder = null,
+ ServiceLifetime lifetime = ServiceLifetime.Transient,
+ string configurationSectionPath = DbContextSpark.ConfigurationSectionPath) where TDbContext : DbContext =>
+ builder.RegisterDbContext(name, configureSettings, configureOptions,
+ configureDbContextOptionsBuilder, configureSqlServerDbContextOptionsBuilder, lifetime,
+ configurationSectionPath, true);
+
+
+ private static void RegisterDbContext(this IHostApplicationBuilder builder,
+ string? name = null,
+ Action>? configureSettings = null,
+ Action>? configureOptions = null,
+ Action? configureDbContextOptionsBuilder = null,
+ Action? configureSqlServerDbContextOptionsBuilder = null,
+ ServiceLifetime lifetime = ServiceLifetime.Transient,
+ string configurationSectionPath = DbContextSpark.ConfigurationSectionPath,
+ bool useFactory = false) where TDbContext : DbContext
+ {
+ name = SparkConfig.Name(name, typeof(TDbContext).Name);
+ var configPath = SparkConfig.Path(name, configurationSectionPath);
+
+ var settings = SparkConfig.GetSettings(builder.Configuration, configPath, configureSettings);
+ builder.Services.AddSingleton(settings);
+
+ var optionsBuilder = builder.Services
+ .AddOptions>()
+ .BindConfiguration(configPath);
+ if (configureOptions is not null) optionsBuilder.Configure(configureOptions);
+
+ if (useFactory)
+ builder.Services.AddDbContextFactory(ConfigureBuilder, lifetime);
+ else
+ builder.Services.AddDbContext(ConfigureBuilder, lifetime);
+
+ ConfigureInstrumentation(builder, name, settings);
+
+
+ return;
+
+ void ConfigureBuilder(IServiceProvider sp, DbContextOptionsBuilder dbContextOptionsBuilder) =>
+ ConfigureDbContextOptionsBuilder(sp, dbContextOptionsBuilder,
+ configureDbContextOptionsBuilder,
+ configureSqlServerDbContextOptionsBuilder);
+ }
+
+
+ private static void ConfigureDbContextOptionsBuilder(IServiceProvider serviceProvider,
+ DbContextOptionsBuilder dbContextOptionsBuilder,
+ Action? configureDbContextOptionsBuilder,
+ Action? configureSqlServerDbContextOptionsBuilder)
+ where TDbContext : DbContext
+ {
+ var sqlServerDbContextSparkOptions = serviceProvider
+ .GetRequiredService>>()
+ .CurrentValue;
+
+ var connectionStringBuilder = new SqlConnectionStringBuilder(sqlServerDbContextSparkOptions.ConnectionString);
+
+ dbContextOptionsBuilder
+ .UseSqlServer(connectionStringBuilder.ConnectionString, options =>
+ {
+ if (!sqlServerDbContextSparkOptions.DisableRetry) options.EnableRetryOnFailure();
+
+ if (sqlServerDbContextSparkOptions.CommandTimeout.HasValue)
+ options.CommandTimeout(sqlServerDbContextSparkOptions.CommandTimeout.Value);
+
+ configureSqlServerDbContextOptionsBuilder?.Invoke(options);
+ })
+ .UseExceptionProcessor();
+ configureDbContextOptionsBuilder?.Invoke(dbContextOptionsBuilder);
+ }
+
+
+ private static void ConfigureInstrumentation(
+ IHostApplicationBuilder builder,
+ string serviceName,
+ SqlServerDbContextSparkSettings settings) where TContext : DbContext
+ {
+ if (!settings.DisableTracing)
+ builder.Services.AddOpenTelemetry().WithTracing(tracerProviderBuilder =>
+ tracerProviderBuilder.AddSqlClientInstrumentation());
+
+ if (!settings.DisableHealthChecks)
+ builder.TryAddHealthCheck(
+ $"{DbContextSpark.Name}.{serviceName}",
+ static hcBuilder => hcBuilder.AddDbContextCheck());
+ }
+}
\ No newline at end of file
diff --git a/src/ES.FX.Ignite.Microsoft.EntityFrameworkCore/ES.FX.Ignite.Microsoft.EntityFrameworkCore.csproj b/src/ES.FX.Ignite.Microsoft.EntityFrameworkCore/ES.FX.Ignite.Microsoft.EntityFrameworkCore.csproj
new file mode 100644
index 0000000..8b516ab
--- /dev/null
+++ b/src/ES.FX.Ignite.Microsoft.EntityFrameworkCore/ES.FX.Ignite.Microsoft.EntityFrameworkCore.csproj
@@ -0,0 +1,20 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/ES.FX.Ignite.Microsoft.EntityFrameworkCore/Migrations/RelationalDbContextMigrationsTask.cs b/src/ES.FX.Ignite.Microsoft.EntityFrameworkCore/Migrations/RelationalDbContextMigrationsTask.cs
new file mode 100644
index 0000000..0c37693
--- /dev/null
+++ b/src/ES.FX.Ignite.Microsoft.EntityFrameworkCore/Migrations/RelationalDbContextMigrationsTask.cs
@@ -0,0 +1,42 @@
+using System.Diagnostics;
+using ES.FX.Migrations.Abstractions;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging;
+
+namespace ES.FX.Ignite.Microsoft.EntityFrameworkCore.Migrations;
+
+///
+/// for applying migrations to that uses relational databases.
+///
+/// The type
+/// Logger instance
+/// The instance
+public class RelationalDbContextMigrationsTask(
+ ILogger> logger,
+ TDbContext context) : IMigrationsTask where TDbContext : DbContext
+{
+ public async Task ApplyMigrations(CancellationToken cancellationToken = default)
+ {
+ logger.LogInformation("Applying migrations for {contextType}", typeof(TDbContext).Name);
+
+ var stopWatch = new Stopwatch();
+ stopWatch.Start();
+
+
+ var migrations = (await context.Database.GetPendingMigrationsAsync(cancellationToken)).ToList();
+
+ if (migrations.Count > 0)
+ {
+ logger.LogInformation("Applying {count} migrations", migrations.Count);
+ await context.Database.MigrateAsync(cancellationToken);
+ }
+ else
+ {
+ logger.LogInformation("No migrations to apply");
+ }
+
+ stopWatch.Stop();
+ logger.LogInformation("Migrations for {contextType} completed in {elapsed}", typeof(TDbContext).Name,
+ stopWatch.Elapsed);
+ }
+}
\ No newline at end of file
diff --git a/src/ES.FX.Ignite.Microsoft.EntityFrameworkCore/Migrations/RelationalDbContextMigrationsTaskExtensions.cs b/src/ES.FX.Ignite.Microsoft.EntityFrameworkCore/Migrations/RelationalDbContextMigrationsTaskExtensions.cs
new file mode 100644
index 0000000..169c0fc
--- /dev/null
+++ b/src/ES.FX.Ignite.Microsoft.EntityFrameworkCore/Migrations/RelationalDbContextMigrationsTaskExtensions.cs
@@ -0,0 +1,23 @@
+using ES.FX.Migrations.Abstractions;
+using JetBrains.Annotations;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+
+namespace ES.FX.Ignite.Microsoft.EntityFrameworkCore.Migrations;
+
+[PublicAPI]
+public static class RelationalDbContextMigrationsTaskExtensions
+{
+ ///
+ /// Registers a for applying migrations to that uses
+ /// relational databases.
+ ///
+ ///
+ ///
+ public static void AddDbContextMigrationsTask(this IHostApplicationBuilder builder)
+ where TDbContext : DbContext
+ {
+ builder.Services.AddTransient>();
+ }
+}
\ No newline at end of file
diff --git a/src/ES.FX.Ignite.Microsoft.EntityFrameworkCore/Spark/DbContextSpark.cs b/src/ES.FX.Ignite.Microsoft.EntityFrameworkCore/Spark/DbContextSpark.cs
new file mode 100644
index 0000000..f1f59ba
--- /dev/null
+++ b/src/ES.FX.Ignite.Microsoft.EntityFrameworkCore/Spark/DbContextSpark.cs
@@ -0,0 +1,19 @@
+using ES.FX.Ignite.Spark.Configuration;
+
+namespace ES.FX.Ignite.Microsoft.EntityFrameworkCore.Spark;
+
+///
+/// definition
+///
+public static class DbContextSpark
+{
+ ///
+ /// Spark name
+ ///
+ public const string Name = "DbContext";
+
+ ///
+ /// The default configuration section path
+ ///
+ public const string ConfigurationSectionPath = $"{IgniteConfigurationSections.Ignite}:{Name}";
+}
\ No newline at end of file
diff --git a/src/ES.FX.Ignite.Migrations/Configuration/MigrationsServiceSparkSettings.cs b/src/ES.FX.Ignite.Migrations/Configuration/MigrationsServiceSparkSettings.cs
new file mode 100644
index 0000000..9dcc082
--- /dev/null
+++ b/src/ES.FX.Ignite.Migrations/Configuration/MigrationsServiceSparkSettings.cs
@@ -0,0 +1,12 @@
+using ES.FX.Ignite.Migrations.Service;
+
+namespace ES.FX.Ignite.Migrations.Configuration;
+
+///
+/// Provides the settings for the
+///
+public class MigrationsServiceSparkSettings
+{
+ public bool Enabled { get; set; }
+ public bool ExitOnComplete { get; set; }
+}
\ No newline at end of file
diff --git a/src/ES.FX.Ignite.Migrations/ES.FX.Ignite.Migrations.csproj b/src/ES.FX.Ignite.Migrations/ES.FX.Ignite.Migrations.csproj
new file mode 100644
index 0000000..4399390
--- /dev/null
+++ b/src/ES.FX.Ignite.Migrations/ES.FX.Ignite.Migrations.csproj
@@ -0,0 +1,16 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
diff --git a/src/ES.FX.Ignite.Migrations/Hosting/MigrationsServiceHostingExtensions.cs b/src/ES.FX.Ignite.Migrations/Hosting/MigrationsServiceHostingExtensions.cs
new file mode 100644
index 0000000..da3e8c3
--- /dev/null
+++ b/src/ES.FX.Ignite.Migrations/Hosting/MigrationsServiceHostingExtensions.cs
@@ -0,0 +1,35 @@
+using ES.FX.Ignite.Migrations.Configuration;
+using ES.FX.Ignite.Migrations.Service;
+using ES.FX.Ignite.Migrations.Spark;
+using ES.FX.Ignite.Spark.Configuration;
+using JetBrains.Annotations;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+
+namespace ES.FX.Ignite.Migrations.Hosting;
+
+[PublicAPI]
+public static class MigrationsServiceHostingExtensions
+{
+ ///
+ /// Adds the to the .
+ ///
+ /// The to read config from and add services to.
+ ///
+ /// An optional delegate that can be used for customizing settings. It's invoked after the
+ /// settings are read from the configuration.
+ ///
+ ///
+ /// The configuration section path. Default is
+ /// .
+ ///
+ public static void AddMigrationsService(this IHostApplicationBuilder builder,
+ Action? configureSettings = null,
+ string configurationSectionPath = MigrationsServiceSpark.ConfigurationSectionPath)
+ {
+ var settings = SparkConfig.GetSettings(builder.Configuration, configurationSectionPath, configureSettings);
+ builder.Services.AddSingleton(settings);
+
+ builder.Services.AddHostedService();
+ }
+}
\ No newline at end of file
diff --git a/src/ES.FX.Ignite.Migrations/Service/MigrationsService.cs b/src/ES.FX.Ignite.Migrations/Service/MigrationsService.cs
new file mode 100644
index 0000000..2dcdf7b
--- /dev/null
+++ b/src/ES.FX.Ignite.Migrations/Service/MigrationsService.cs
@@ -0,0 +1,71 @@
+using System.Diagnostics;
+using ES.FX.Ignite.Migrations.Configuration;
+using ES.FX.Migrations.Abstractions;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+
+namespace ES.FX.Ignite.Migrations.Service;
+
+///
+/// Hosted service for running migrations tasks.
+///
+/// The .
+/// The .
+///
+/// The used to look up the
+/// instances.
+///
+public class MigrationsService(
+ ILogger logger,
+ MigrationsServiceSparkSettings settings,
+ IServiceProvider serviceProvider)
+ : IHostedService
+{
+ public async Task StartAsync(CancellationToken cancellationToken)
+ {
+ logger.LogDebug("Service enabled: {enabled}", settings.Enabled);
+
+ if (!settings.Enabled) return;
+
+
+ logger.LogTrace("Running migration tasks");
+
+ var migrationTasks = serviceProvider.GetServices().ToList();
+ logger.LogDebug("Migrations tasks: {migrationTaskCount}", migrationTasks.Count);
+
+ var stopwatch = new Stopwatch();
+ stopwatch.Start();
+
+ for (var index = 0; index < migrationTasks.Count; index++)
+ {
+ var task = migrationTasks[index];
+ logger.LogTrace("Running task {currentMigrationTaskIndex}/{migrationTaskCount}",
+ index + 1, migrationTasks.Count);
+
+ var taskStopwatch = new Stopwatch();
+ taskStopwatch.Start();
+
+ await task.ApplyMigrations(cancellationToken);
+
+ taskStopwatch.Stop();
+ logger.LogDebug("Task {currentMigrationTaskIndex}/{migrationTaskCount} completed in {elapsed}",
+ index + 1, migrationTasks.Count, taskStopwatch.Elapsed);
+ }
+
+ stopwatch.Stop();
+ logger.LogInformation("Migrations tasks completed in {totalElapsed}", stopwatch.Elapsed);
+
+
+ logger.LogDebug("Exit on complete: {exitOnComplete}", settings.ExitOnComplete);
+
+ if (settings.ExitOnComplete)
+ {
+ logger.LogInformation("Exiting application on migrations completed");
+ //Use this instead of hostApplicationLifetime.StopApplication(); to ensure that the application exits immediately and cleanly
+ Environment.Exit(0);
+ }
+ }
+
+ public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
+}
\ No newline at end of file
diff --git a/src/ES.FX.Ignite.Migrations/Spark/MigrationsServiceSpark.cs b/src/ES.FX.Ignite.Migrations/Spark/MigrationsServiceSpark.cs
new file mode 100644
index 0000000..db6f1b7
--- /dev/null
+++ b/src/ES.FX.Ignite.Migrations/Spark/MigrationsServiceSpark.cs
@@ -0,0 +1,19 @@
+using ES.FX.Ignite.Spark.Configuration;
+
+namespace ES.FX.Ignite.Migrations.Spark;
+
+///
+/// definition
+///
+public static class MigrationsServiceSpark
+{
+ ///
+ /// Spark name
+ ///
+ public const string Name = "MigrationsService";
+
+ ///
+ /// The default configuration section path
+ ///
+ public const string ConfigurationSectionPath = $"{IgniteConfigurationSections.Services}:{Name}";
+}
\ No newline at end of file
diff --git a/src/ES.FX.Ignite.Serilog/ES.FX.Ignite.Serilog.csproj b/src/ES.FX.Ignite.Serilog/ES.FX.Ignite.Serilog.csproj
new file mode 100644
index 0000000..2f78c0c
--- /dev/null
+++ b/src/ES.FX.Ignite.Serilog/ES.FX.Ignite.Serilog.csproj
@@ -0,0 +1,20 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/ES.FX.Ignite.Serilog/Hosting/SerilogHostingExtensions.cs b/src/ES.FX.Ignite.Serilog/Hosting/SerilogHostingExtensions.cs
new file mode 100644
index 0000000..5631c9a
--- /dev/null
+++ b/src/ES.FX.Ignite.Serilog/Hosting/SerilogHostingExtensions.cs
@@ -0,0 +1,51 @@
+using ES.FX.Serilog.Enrichers;
+using JetBrains.Annotations;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Serilog;
+using Serilog.Core;
+
+namespace ES.FX.Ignite.Serilog.Hosting;
+
+[PublicAPI]
+public static class SerilogHostingExtensions
+{
+ ///
+ /// Adds Serilog to the host builder with the default enrichers and default configuration
+ ///
+ /// The to read config from and add services to.
+ /// ///
+ ///
+ /// An optional delegate that can be used for customizing the
+ /// that will be used to construct a .
+ ///
+ ///
+ /// Apply the default logger configuration. If enabled the default configuration
+ /// will be applied before the custom configuration.
+ ///
+ public static void AddSerilog(this IHostApplicationBuilder builder,
+ Action? configureLoggerConfiguration = null, bool applyDefaultConfiguration = true)
+ {
+ builder.Services.AddSerilog((services, loggerConfiguration) =>
+ {
+ if (applyDefaultConfiguration)
+ loggerConfiguration
+ .MinimumLevel.Verbose()
+ .Destructure.ToMaximumCollectionCount(64)
+ .Destructure.ToMaximumStringLength(2048)
+ .Destructure.ToMaximumDepth(16)
+ .Enrich.FromLogContext()
+ .Enrich.WithMachineName()
+ .Enrich.WithEnvironmentName()
+ .Enrich.With();
+
+ loggerConfiguration
+ .ReadFrom.Services(services)
+ .ReadFrom.Configuration(builder.Configuration);
+
+ configureLoggerConfiguration?.Invoke(loggerConfiguration);
+ });
+
+ if (applyDefaultConfiguration) builder.Services.AddSingleton();
+ }
+}
\ No newline at end of file
diff --git a/src/ES.FX.Ignite.Serilog/Hosting/SerilogRequestLoggingHostingExtensions.cs b/src/ES.FX.Ignite.Serilog/Hosting/SerilogRequestLoggingHostingExtensions.cs
new file mode 100644
index 0000000..392dfd3
--- /dev/null
+++ b/src/ES.FX.Ignite.Serilog/Hosting/SerilogRequestLoggingHostingExtensions.cs
@@ -0,0 +1,42 @@
+using JetBrains.Annotations;
+using Microsoft.AspNetCore.Builder;
+using Serilog;
+using Serilog.AspNetCore;
+using Serilog.Events;
+
+namespace ES.FX.Ignite.Serilog.Hosting;
+
+[PublicAPI]
+public static class SerilogRequestLoggingHostingExtensions
+{
+ ///
+ /// Adds Serilog Request logging to the application
+ ///
+ /// The .
+ ///
+ /// An optional delegate that can be used for customizing the
+ /// .
+ ///
+ public static void UseSerilogRequestLogging(this IApplicationBuilder app,
+ Action? configureOptions = null)
+ {
+ //Add Serilog Request logging
+ SerilogApplicationBuilderExtensions.UseSerilogRequestLogging(app, options =>
+ {
+ ConfigureDefaultRequestLoggingSlim(options);
+ configureOptions?.Invoke(options);
+ });
+ }
+
+
+ private static void ConfigureDefaultRequestLoggingSlim(RequestLoggingOptions options)
+ {
+ options.GetLevel = (_, _, _) => LogEventLevel.Debug;
+
+ options.EnrichDiagnosticContext = (diagnosticContext, httpContext) =>
+ {
+ diagnosticContext.Set("RequestHost", httpContext.Request.Host.Value);
+ diagnosticContext.Set("RequestScheme", httpContext.Request.Scheme);
+ };
+ }
+}
\ No newline at end of file
diff --git a/src/ES.FX.Ignite.Spark/Configuration/IgniteConfigurationSections.cs b/src/ES.FX.Ignite.Spark/Configuration/IgniteConfigurationSections.cs
new file mode 100644
index 0000000..265068a
--- /dev/null
+++ b/src/ES.FX.Ignite.Spark/Configuration/IgniteConfigurationSections.cs
@@ -0,0 +1,10 @@
+namespace ES.FX.Ignite.Spark.Configuration;
+
+///
+/// Ignite default configuration sections
+///
+public static class IgniteConfigurationSections
+{
+ public const string Ignite = nameof(FX.Ignite);
+ public const string Services = $"{Ignite}:{nameof(Services)}";
+}
\ No newline at end of file
diff --git a/src/ES.FX.Ignite.Spark/Configuration/SparkConfig.cs b/src/ES.FX.Ignite.Spark/Configuration/SparkConfig.cs
new file mode 100644
index 0000000..4a47e48
--- /dev/null
+++ b/src/ES.FX.Ignite.Spark/Configuration/SparkConfig.cs
@@ -0,0 +1,79 @@
+using Microsoft.Extensions.Configuration;
+
+namespace ES.FX.Ignite.Spark.Configuration;
+
+public static class SparkConfig
+{
+ ///
+ /// Default configuration section for settings
+ ///
+ public const string Settings = "Settings";
+
+
+ ///
+ /// Gets the name or the default if the name is null or empty.
+ ///
+ /// The spark name
+ /// The default spark name
+ ///
+ public static string Name(string? name, string defaultName)
+ {
+ name = name?.Trim();
+ defaultName = defaultName.Trim();
+
+ return name ?? defaultName;
+ }
+
+ ///
+ /// Gets the configuration path for the service.
+ ///
+ /// Name of the service
+ /// Section path
+ ///
+ public static string Path(string serviceName, string sectionPath)
+ {
+ serviceName = serviceName.Trim();
+ sectionPath = sectionPath.Trim();
+ var configPath = sectionPath == string.Empty ? serviceName : $"{sectionPath}:{serviceName}";
+
+ return configPath;
+ }
+
+ ///
+ /// Gets the settings from the configuration.
+ ///
+ /// Settings type
+ /// The to get the settings from.
+ /// The section to get the settings from.
+ ///
+ /// An optional delegate to configure the settings. This is called after the settings are
+ /// loaded from the configuration.
+ ///
+ /// The settings instance.
+ public static T GetSettings(IConfiguration configuration,
+ string configurationPath,
+ Action? configureSettings = null) where T : new() =>
+ GetSettings(new T(), configuration, configurationPath, configureSettings);
+
+ ///
+ /// Gets the settings from the configuration.
+ ///
+ /// Settings type
+ /// The settings instance to bind the configuration to.
+ /// The to get the settings from.
+ /// The section to get the settings from.
+ ///
+ /// An optional delegate to configure the settings. This is called after the settings are
+ /// loaded from the configuration.
+ ///
+ /// The settings instance.
+ public static T GetSettings(T settings, IConfiguration configuration,
+ string configurationPath,
+ Action? configureSettings = null)
+ {
+ configuration.GetSection($"{configurationPath}:{Settings}").Bind(settings);
+ configureSettings?.Invoke(settings);
+
+ return settings;
+ }
+}
\ No newline at end of file
diff --git a/src/ES.FX.Ignite.Spark/ES.FX.Ignite.Spark.csproj b/src/ES.FX.Ignite.Spark/ES.FX.Ignite.Spark.csproj
new file mode 100644
index 0000000..502d519
--- /dev/null
+++ b/src/ES.FX.Ignite.Spark/ES.FX.Ignite.Spark.csproj
@@ -0,0 +1,18 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/ES.FX.Ignite.Spark/HealthChecks/HealthChecksExtensions.cs b/src/ES.FX.Ignite.Spark/HealthChecks/HealthChecksExtensions.cs
new file mode 100644
index 0000000..ce136a0
--- /dev/null
+++ b/src/ES.FX.Ignite.Spark/HealthChecks/HealthChecksExtensions.cs
@@ -0,0 +1,33 @@
+using JetBrains.Annotations;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Diagnostics.HealthChecks;
+using Microsoft.Extensions.Hosting;
+
+namespace ES.FX.Ignite.Spark.HealthChecks;
+
+[PublicAPI]
+public static class HealthChecksExtensions
+{
+ ///
+ /// Adds a HealthCheckRegistration if one hasn't already been added to the builder.
+ ///
+ public static void TryAddHealthCheck(this IHostApplicationBuilder builder,
+ HealthCheckRegistration healthCheckRegistration)
+ {
+ builder.TryAddHealthCheck(healthCheckRegistration.Name, hcBuilder => hcBuilder.Add(healthCheckRegistration));
+ }
+
+ ///
+ /// Invokes the action if the given hasn't already been
+ /// added to the builder.
+ ///
+ public static void TryAddHealthCheck(this IHostApplicationBuilder builder, string name,
+ Action addHealthCheck)
+ {
+ var healthCheckKey = $"Ignite.HealthChecks.{name}";
+ if (builder.Properties.ContainsKey(healthCheckKey)) return;
+
+ builder.Properties[healthCheckKey] = true;
+ addHealthCheck(builder.Services.AddHealthChecks());
+ }
+}
\ No newline at end of file
diff --git a/src/ES.FX.Microsoft.Data.SqlClient/Abstractions/ISqlConnectionFactory.cs b/src/ES.FX.Microsoft.Data.SqlClient/Abstractions/ISqlConnectionFactory.cs
new file mode 100644
index 0000000..d4b9e22
--- /dev/null
+++ b/src/ES.FX.Microsoft.Data.SqlClient/Abstractions/ISqlConnectionFactory.cs
@@ -0,0 +1,26 @@
+using JetBrains.Annotations;
+using Microsoft.Data.SqlClient;
+
+namespace ES.FX.Microsoft.Data.SqlClient.Abstractions;
+
+///
+/// Defines a factory for creating instances.
+///
+[PublicAPI]
+public interface ISqlConnectionFactory
+{
+ ///
+ /// Creates a new instance.
+ ///
+ SqlConnection CreateConnection();
+
+
+ ///
+ /// Creates a new instance in an async context.
+ ///
+ /// A to observe while waiting for the task to complete.
+ /// A task containing the created that represents the asynchronous operation.
+ /// If the is canceled.
+ Task CreateConnectionAsync(CancellationToken cancellationToken = default)
+ => Task.FromResult(CreateConnection());
+}
\ No newline at end of file
diff --git a/src/ES.FX.Microsoft.Data.SqlClient/ES.FX.Microsoft.Data.SqlClient.csproj b/src/ES.FX.Microsoft.Data.SqlClient/ES.FX.Microsoft.Data.SqlClient.csproj
new file mode 100644
index 0000000..67a4cb1
--- /dev/null
+++ b/src/ES.FX.Microsoft.Data.SqlClient/ES.FX.Microsoft.Data.SqlClient.csproj
@@ -0,0 +1,17 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/ES.FX.Microsoft.Data.SqlClient/Factories/DelegateSqlConnectionFactory.cs b/src/ES.FX.Microsoft.Data.SqlClient/Factories/DelegateSqlConnectionFactory.cs
new file mode 100644
index 0000000..d4a53a5
--- /dev/null
+++ b/src/ES.FX.Microsoft.Data.SqlClient/Factories/DelegateSqlConnectionFactory.cs
@@ -0,0 +1,19 @@
+using ES.FX.Microsoft.Data.SqlClient.Abstractions;
+using JetBrains.Annotations;
+using Microsoft.Data.SqlClient;
+
+namespace ES.FX.Microsoft.Data.SqlClient.Factories;
+
+///
+/// Defines a factory for creating instances of using a delegate.
+///
+/// Service provider used by the factory
+/// Factory function used to create the
+[PublicAPI]
+public class DelegateSqlConnectionFactory(
+ IServiceProvider serviceProvider,
+ Func factory)
+ : ISqlConnectionFactory
+{
+ public SqlConnection CreateConnection() => factory(serviceProvider);
+}
\ No newline at end of file
diff --git a/src/ES.FX.Microsoft.Data.SqlClient/Queries/SqlServerSafeQuery.cs b/src/ES.FX.Microsoft.Data.SqlClient/Queries/SqlServerSafeQuery.cs
new file mode 100644
index 0000000..5cacd32
--- /dev/null
+++ b/src/ES.FX.Microsoft.Data.SqlClient/Queries/SqlServerSafeQuery.cs
@@ -0,0 +1,59 @@
+using Microsoft.Data.SqlClient;
+
+namespace ES.FX.Microsoft.Data.SqlClient.Queries;
+
+///
+/// Provides a safe query to execute on a SQL Server database to check if the connection is valid
+///
+public static class SqlServerSafeQuery
+{
+ public const string CommandText = "SELECT 1";
+
+ ///
+ /// Executes a safe query to check if the connection is valid
+ ///
+ /// The to execute the query on
+ /// Indicates whether to close the connection after executing the query
+ /// A boolean value indicating whether the connection is valid
+ public static bool ExecuteSafeQuery(this SqlConnection connection, bool close = true)
+ {
+ try
+ {
+ connection.Open();
+ var command = connection.CreateCommand();
+ command.CommandText = CommandText;
+ var result = command.ExecuteScalar();
+ if (close) connection.Close();
+ return result != null && (int)result == 1;
+ }
+ catch
+ {
+ return false;
+ }
+ }
+
+ ///
+ /// Executes a safe query to check if the connection is valid
+ ///
+ /// The to execute the query on
+ /// Indicates whether to close the connection after executing the query
+ /// The to cancel the operation
+ /// A boolean value indicating whether the connection is valid
+ public static async Task ExecuteSafeQueryAsync(this SqlConnection connection, bool close = true,
+ CancellationToken cancellationToken = default)
+ {
+ try
+ {
+ await connection.OpenAsync(cancellationToken);
+ var command = connection.CreateCommand();
+ command.CommandText = CommandText;
+ var result = await command.ExecuteScalarAsync(cancellationToken);
+ if (close) connection.Close();
+ return result != null && (int)result == 1;
+ }
+ catch
+ {
+ return false;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/ES.FX.Microsoft.Data.SqlClient/SqlConnectionStringBuilderExtensions.cs b/src/ES.FX.Microsoft.Data.SqlClient/SqlConnectionStringBuilderExtensions.cs
new file mode 100644
index 0000000..e0511a7
--- /dev/null
+++ b/src/ES.FX.Microsoft.Data.SqlClient/SqlConnectionStringBuilderExtensions.cs
@@ -0,0 +1,36 @@
+using JetBrains.Annotations;
+using Microsoft.Data.SqlClient;
+
+namespace ES.FX.Microsoft.Data.SqlClient;
+
+[PublicAPI]
+public static class SqlConnectionStringBuilderExtensions
+{
+ ///
+ /// Changes the InitialCatalog to the "master" database
+ ///
+ public static SqlConnectionStringBuilder SetInitialCatalogToMaster(this SqlConnectionStringBuilder builder) =>
+ builder.SetInitialCatalog("master");
+
+
+ ///
+ /// Sets the InitialCatalog to
+ ///
+ ///
+ public static SqlConnectionStringBuilder SetInitialCatalog(this SqlConnectionStringBuilder builder,
+ string database)
+ {
+ builder.InitialCatalog = database;
+ return builder;
+ }
+
+
+ ///
+ /// Creates a new instance of with
+ /// set to "master"
+ ///
+ ///
+ /// The cloned
+ public static SqlConnectionStringBuilder CloneForMaster(this SqlConnectionStringBuilder builder) =>
+ new SqlConnectionStringBuilder(builder.ConnectionString).SetInitialCatalogToMaster();
+}
\ No newline at end of file
diff --git a/src/ES.FX.Microsoft.EntityFrameworkCore/ES.FX.Microsoft.EntityFrameworkCore.csproj b/src/ES.FX.Microsoft.EntityFrameworkCore/ES.FX.Microsoft.EntityFrameworkCore.csproj
new file mode 100644
index 0000000..e1d987e
--- /dev/null
+++ b/src/ES.FX.Microsoft.EntityFrameworkCore/ES.FX.Microsoft.EntityFrameworkCore.csproj
@@ -0,0 +1,14 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
+
diff --git a/src/ES.FX.Microsoft.EntityFrameworkCore/Factories/DelegateDbContextFactory.cs b/src/ES.FX.Microsoft.EntityFrameworkCore/Factories/DelegateDbContextFactory.cs
new file mode 100644
index 0000000..6806f93
--- /dev/null
+++ b/src/ES.FX.Microsoft.EntityFrameworkCore/Factories/DelegateDbContextFactory.cs
@@ -0,0 +1,20 @@
+using JetBrains.Annotations;
+using Microsoft.EntityFrameworkCore;
+
+namespace ES.FX.Microsoft.EntityFrameworkCore.Factories;
+
+///
+/// Defines a factory for creating instances of using a delegate.
+///
+/// > type
+/// Service provider used by the factory
+/// Factory function used to create the
+[PublicAPI]
+public class DelegateDbContextFactory(
+ IServiceProvider serviceProvider,
+ Func factory)
+ : IDbContextFactory
+ where TDbContext : DbContext
+{
+ public TDbContext CreateDbContext() => factory(serviceProvider);
+}
\ No newline at end of file
diff --git a/src/ES.FX.Migrations/Abstractions/IMigrationsTask.cs b/src/ES.FX.Migrations/Abstractions/IMigrationsTask.cs
new file mode 100644
index 0000000..8b55002
--- /dev/null
+++ b/src/ES.FX.Migrations/Abstractions/IMigrationsTask.cs
@@ -0,0 +1,6 @@
+namespace ES.FX.Migrations.Abstractions;
+
+public interface IMigrationsTask
+{
+ Task ApplyMigrations(CancellationToken cancellationToken = default);
+}
\ No newline at end of file
diff --git a/src/ES.FX.Migrations/ES.FX.Migrations.csproj b/src/ES.FX.Migrations/ES.FX.Migrations.csproj
new file mode 100644
index 0000000..be26386
--- /dev/null
+++ b/src/ES.FX.Migrations/ES.FX.Migrations.csproj
@@ -0,0 +1,13 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
diff --git a/src/ES.FX.Serilog/ES.FX.Serilog.csproj b/src/ES.FX.Serilog/ES.FX.Serilog.csproj
new file mode 100644
index 0000000..061d1f9
--- /dev/null
+++ b/src/ES.FX.Serilog/ES.FX.Serilog.csproj
@@ -0,0 +1,22 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/ES.FX.Serilog/Enrichers/ApplicationNameEnricher.cs b/src/ES.FX.Serilog/Enrichers/ApplicationNameEnricher.cs
new file mode 100644
index 0000000..3cc60b4
--- /dev/null
+++ b/src/ES.FX.Serilog/Enrichers/ApplicationNameEnricher.cs
@@ -0,0 +1,14 @@
+using JetBrains.Annotations;
+using Microsoft.Extensions.Hosting;
+using Serilog.Core.Enrichers;
+
+namespace ES.FX.Serilog.Enrichers;
+
+///
+/// Enricher for setting the application name on the property ApplicationName
+///
+[PublicAPI]
+public class ApplicationNameEnricher(IHostEnvironment hostEnvironment)
+ : PropertyEnricher(nameof(IHostEnvironment.ApplicationName), hostEnvironment.ApplicationName)
+{
+}
\ No newline at end of file
diff --git a/src/ES.FX.Serilog/Enrichers/CachedPropertyEnricher.cs b/src/ES.FX.Serilog/Enrichers/CachedPropertyEnricher.cs
new file mode 100644
index 0000000..fd44d7b
--- /dev/null
+++ b/src/ES.FX.Serilog/Enrichers/CachedPropertyEnricher.cs
@@ -0,0 +1,21 @@
+using Serilog.Core;
+using Serilog.Events;
+
+namespace ES.FX.Serilog.Enrichers;
+
+public abstract class CachedPropertyEnricher : ILogEventEnricher
+{
+ private LogEventProperty? _cachedProperty;
+
+ public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
+ {
+ logEvent.AddPropertyIfAbsent(GetLogEventProperty(propertyFactory));
+ }
+
+ private LogEventProperty GetLogEventProperty(ILogEventPropertyFactory propertyFactory)
+ {
+ return _cachedProperty ??= CreateProperty(propertyFactory);
+ }
+
+ protected abstract LogEventProperty CreateProperty(ILogEventPropertyFactory propertyFactory);
+}
\ No newline at end of file
diff --git a/src/ES.FX.Serilog/Enrichers/EntryAssemblyNameEnricher.cs b/src/ES.FX.Serilog/Enrichers/EntryAssemblyNameEnricher.cs
new file mode 100644
index 0000000..656eddf
--- /dev/null
+++ b/src/ES.FX.Serilog/Enrichers/EntryAssemblyNameEnricher.cs
@@ -0,0 +1,16 @@
+using System.Reflection;
+using JetBrains.Annotations;
+using Serilog.Core;
+using Serilog.Events;
+
+namespace ES.FX.Serilog.Enrichers;
+
+///
+/// Enricher for setting the application entry assembly on the ApplicationEntryAssembly property
+///
+[PublicAPI]
+public class EntryAssemblyNameEnricher : CachedPropertyEnricher
+{
+ protected override LogEventProperty CreateProperty(ILogEventPropertyFactory propertyFactory) =>
+ propertyFactory.CreateProperty("ApplicationEntryAssembly", Assembly.GetEntryAssembly()?.FullName);
+}
\ No newline at end of file
diff --git a/src/ES.FX.Serilog/Lifetime/ProgramEntrySerilogExtensions.cs b/src/ES.FX.Serilog/Lifetime/ProgramEntrySerilogExtensions.cs
new file mode 100644
index 0000000..4f6ce7e
--- /dev/null
+++ b/src/ES.FX.Serilog/Lifetime/ProgramEntrySerilogExtensions.cs
@@ -0,0 +1,60 @@
+using ES.FX.Hosting.Lifetime;
+using ES.FX.Serilog.Enrichers;
+using ES.FX.Serilog.Sinks.Console;
+using JetBrains.Annotations;
+using Serilog;
+using Serilog.Debugging;
+using Serilog.Events;
+using Serilog.Extensions.Logging;
+
+namespace ES.FX.Serilog.Lifetime;
+
+[PublicAPI]
+public static class ProgramEntrySerilogExtensions
+{
+ ///
+ /// Use Serilog as the logger for the ProgramEntry
+ ///
+ /// The
+ /// The minimum level for logging
+ /// Action to configure the .
+ /// Enables the Serilog SelfLog to console (useful to debug Serilog)
+ /// The
+ public static ProgramEntryBuilder UseSerilog(this ProgramEntryBuilder builder,
+ LogEventLevel minimumLevel = LogEventLevel.Information,
+ Action? configureLoggerConfiguration = null, bool enableConsoleSelfLog = true)
+ {
+ // Enable Serilog SelfLog to console
+ if (enableConsoleSelfLog) SelfLog.Enable(Console.Error);
+
+ // Configure the logger configuration
+ var loggerConfiguration = new LoggerConfiguration();
+
+ loggerConfiguration
+ .MinimumLevel.Is(minimumLevel)
+ .WriteTo.Console(outputTemplate: ConsoleOutputTemplates.Default)
+ .Destructure.ToMaximumCollectionCount(64)
+ .Destructure.ToMaximumStringLength(2048)
+ .Destructure.ToMaximumDepth(16)
+ .Enrich.FromLogContext()
+ .Enrich.WithMachineName()
+ .Enrich.WithEnvironmentName()
+ .Enrich.With();
+
+ configureLoggerConfiguration?.Invoke(loggerConfiguration);
+
+ // Create a Serilog logger from the configuration
+ // This logger will be replaced by the host logger during bootstrapping
+ Log.Logger = loggerConfiguration.CreateBootstrapLogger();
+
+ // Set the logger for the ProgramEntry to the logger created by Serilog
+ builder.WithLogger(
+ new SerilogLoggerFactory(Log.Logger)
+ .CreateLogger(typeof(ProgramEntry).FullName ?? nameof(ProgramEntry)));
+
+ // Handle application exit by closing and flushing the Serilog logger
+ builder.AddExitAction(async _ => await Log.CloseAndFlushAsync());
+
+ return builder;
+ }
+}
\ No newline at end of file
diff --git a/src/ES.FX.Serilog/Sinks/Console/ConsoleOutputTemplates.cs b/src/ES.FX.Serilog/Sinks/Console/ConsoleOutputTemplates.cs
new file mode 100644
index 0000000..a953388
--- /dev/null
+++ b/src/ES.FX.Serilog/Sinks/Console/ConsoleOutputTemplates.cs
@@ -0,0 +1,7 @@
+namespace ES.FX.Serilog.Sinks.Console;
+
+public static class ConsoleOutputTemplates
+{
+ public const string Default =
+ "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] ({SourceContext}) {Message:lj}{NewLine}{Exception}";
+}
\ No newline at end of file
diff --git a/src/ES.FX/Collections/ArrayExtensions.cs b/src/ES.FX/Collections/ArrayExtensions.cs
new file mode 100644
index 0000000..7f861a5
--- /dev/null
+++ b/src/ES.FX/Collections/ArrayExtensions.cs
@@ -0,0 +1,12 @@
+using JetBrains.Annotations;
+
+namespace ES.FX.Collections;
+
+[PublicAPI]
+public static class ArrayExtensions
+{
+ ///
+ /// Checks if the array is null or empty
+ ///
+ public static bool IsNullOrEmpty(this Array? array) => array is null || array.Length == 0;
+}
\ No newline at end of file
diff --git a/src/ES.FX/ES.FX.csproj b/src/ES.FX/ES.FX.csproj
new file mode 100644
index 0000000..73aeb5d
--- /dev/null
+++ b/src/ES.FX/ES.FX.csproj
@@ -0,0 +1,14 @@
+
+
+
+ net8.0
+ enable
+ enable
+ true
+
+
+
+
+
+
+
diff --git a/src/ES.FX/Exceptions/ExceptionExtensions.cs b/src/ES.FX/Exceptions/ExceptionExtensions.cs
new file mode 100644
index 0000000..d049e01
--- /dev/null
+++ b/src/ES.FX/Exceptions/ExceptionExtensions.cs
@@ -0,0 +1,40 @@
+using JetBrains.Annotations;
+
+namespace ES.FX.Exceptions;
+
+///
+/// Extension methods for
+///
+[PublicAPI]
+public static class ExceptionExtensions
+{
+ ///
+ /// Returns the innermost
+ ///
+ public static Exception InnermostException(this Exception exception)
+ {
+ while (true)
+ {
+ if (exception.InnerException == null) return exception;
+ exception = exception.InnerException;
+ }
+ }
+
+
+ ///
+ /// Returns the innermost of type
+ ///
+ public static Exception? InnermostException(this Exception? exception) where T : Exception
+ {
+ if (exception == null) return null;
+
+ T? foundException = null;
+ while (exception != null)
+ {
+ if (exception is T specificException) foundException = specificException;
+ exception = exception.InnerException;
+ }
+
+ return foundException;
+ }
+}
\ No newline at end of file
diff --git a/src/ES.FX/IO/StreamExtensions.cs b/src/ES.FX/IO/StreamExtensions.cs
new file mode 100644
index 0000000..ed5723f
--- /dev/null
+++ b/src/ES.FX/IO/StreamExtensions.cs
@@ -0,0 +1,38 @@
+using JetBrains.Annotations;
+using static System.ArgumentNullException;
+
+namespace ES.FX.IO;
+
+[PublicAPI]
+public static class StreamExtensions
+{
+ ///
+ /// Reads all bytes from the stream and returns them as a byte array
+ ///
+ public static byte[] ToByteArray(this Stream stream)
+ {
+ ThrowIfNull(stream);
+
+ if (stream is MemoryStream directMemoryStream)
+ return directMemoryStream.ToArray();
+
+ using var memoryStream = new MemoryStream();
+ stream.CopyTo(memoryStream);
+ return memoryStream.ToArray();
+ }
+
+ ///
+ /// Reads all bytes from the stream asynchronously and returns them as a byte array
+ ///
+ public static async Task ToByteArrayAsync(this Stream stream, CancellationToken cancellationToken = default)
+ {
+ ThrowIfNull(stream);
+
+ if (stream is MemoryStream directMemoryStream)
+ return directMemoryStream.ToArray();
+
+ using var memoryStream = new MemoryStream();
+ await stream.CopyToAsync(memoryStream, cancellationToken);
+ return memoryStream.ToArray();
+ }
+}
\ No newline at end of file
diff --git a/src/ES.FX/Linq/EnumerableExtensions.cs b/src/ES.FX/Linq/EnumerableExtensions.cs
new file mode 100644
index 0000000..9ff4e22
--- /dev/null
+++ b/src/ES.FX/Linq/EnumerableExtensions.cs
@@ -0,0 +1,20 @@
+using JetBrains.Annotations;
+
+namespace ES.FX.Linq;
+
+///
+/// Linq extensions for collection
+///
+[PublicAPI]
+public static class EnumerableExtensions
+{
+ ///
+ /// Returns a random item in an collection or the default value
+ ///
+ /// The source enumerable
+ public static T? TakeRandomItemOrDefault(this IEnumerable source)
+ {
+ var list = source.ToList();
+ return list.Count == 0 ? default : list[Random.Shared.Next(list.Count)];
+ }
+}
\ No newline at end of file
diff --git a/src/ES.FX/Reflection/ManifestResource.cs b/src/ES.FX/Reflection/ManifestResource.cs
new file mode 100644
index 0000000..138a3e3
--- /dev/null
+++ b/src/ES.FX/Reflection/ManifestResource.cs
@@ -0,0 +1,85 @@
+using System.Reflection;
+using ES.FX.IO;
+using JetBrains.Annotations;
+
+namespace ES.FX.Reflection;
+
+///
+/// Wrapper for a manifest resource embedded in an assembly
+/// Provides quick access to content and resource properties
+///
+///
+/// Creates a new wrapper for the manifest resource
+///
+/// Source assembly
+/// Resource name
+[PublicAPI]
+public class ManifestResource(Assembly Assembly, string Name)
+{
+ ///
+ /// Gets the resource name
+ ///
+ public string Name { get; } = Name;
+
+
+ ///
+ /// Gets the manifest resource info
+ ///
+ public ManifestResourceInfo? Info => Assembly.GetManifestResourceInfo(Name);
+
+ ///
+ /// Returns the stream for the manifest resource
+ ///
+ public Stream? GetStream() => Assembly.GetManifestResourceStream(Name);
+
+
+ ///
+ /// Reads all bytes for the manifest resource
+ ///
+ public byte[]? ReadAllBytes()
+ {
+ using var stream = GetStream();
+ return stream?.ToByteArray();
+ }
+
+
+ ///
+ /// Reads all bytes for the manifest resource
+ ///
+ public async Task ReadAllBytesAsync(CancellationToken cancellation = default)
+ {
+ await using var stream = GetStream();
+ return stream is not null ? await stream.ToByteArrayAsync(cancellation) : null;
+ }
+
+
+ ///
+ /// Returns manifest resource content as text
+ ///
+ public string? ReadText()
+ {
+ using var reader = GetStreamReader();
+ return reader?.ReadToEnd();
+ }
+
+
+ ///
+ /// Returns manifest resource content as text
+ ///
+ public async Task ReadTextAsync(CancellationToken cancellationToken = default)
+ {
+ using var reader = GetStreamReader();
+ if (reader is null) return null;
+ return await reader.ReadToEndAsync(cancellationToken);
+ }
+
+
+ ///
+ /// Returns a initialized with the manifest resource content
+ ///
+ public StreamReader? GetStreamReader()
+ {
+ var stream = GetStream();
+ return stream is not null ? new StreamReader(stream) : null;
+ }
+}
\ No newline at end of file
diff --git a/src/ES.FX/Reflection/ManifestResourceExtensions.cs b/src/ES.FX/Reflection/ManifestResourceExtensions.cs
new file mode 100644
index 0000000..c0cc991
--- /dev/null
+++ b/src/ES.FX/Reflection/ManifestResourceExtensions.cs
@@ -0,0 +1,23 @@
+using System.Reflection;
+using JetBrains.Annotations;
+
+namespace ES.FX.Reflection;
+
+///
+/// Extension methods for
+///
+[PublicAPI]
+public static class ManifestResourceExtensions
+{
+ ///
+ /// Gets the wrappers for embedded assembly resources
+ ///
+ /// Source assembly
+ /// List of wrappers
+ public static ManifestResource[] GetManifestResources(this Assembly assembly)
+ {
+ return assembly.GetManifestResourceNames()
+ .Select(resource => new ManifestResource(assembly, resource))
+ .ToArray();
+ }
+}
\ No newline at end of file
diff --git a/tests/ES.FX.Ignite.Microsoft.Data.SqlClient.Tests/ES.FX.Ignite.Microsoft.Data.SqlClient.Tests.csproj b/tests/ES.FX.Ignite.Microsoft.Data.SqlClient.Tests/ES.FX.Ignite.Microsoft.Data.SqlClient.Tests.csproj
new file mode 100644
index 0000000..4c0f8ec
--- /dev/null
+++ b/tests/ES.FX.Ignite.Microsoft.Data.SqlClient.Tests/ES.FX.Ignite.Microsoft.Data.SqlClient.Tests.csproj
@@ -0,0 +1,35 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+ false
+ true
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/ES.FX.Ignite.Microsoft.Data.SqlClient.Tests/SqlServerClientHostingExtensionsTests.cs b/tests/ES.FX.Ignite.Microsoft.Data.SqlClient.Tests/SqlServerClientHostingExtensionsTests.cs
new file mode 100644
index 0000000..6d2466c
--- /dev/null
+++ b/tests/ES.FX.Ignite.Microsoft.Data.SqlClient.Tests/SqlServerClientHostingExtensionsTests.cs
@@ -0,0 +1,225 @@
+using ES.FX.Ignite.Microsoft.Data.SqlClient.Configuration;
+using ES.FX.Ignite.Microsoft.Data.SqlClient.Hosting;
+using ES.FX.Ignite.Microsoft.Data.SqlClient.Spark;
+using ES.FX.Ignite.Spark.Configuration;
+using ES.FX.Microsoft.Data.SqlClient.Abstractions;
+using Microsoft.Data.SqlClient;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Options;
+
+namespace ES.FX.Ignite.Microsoft.Data.SqlClient.Tests;
+
+public class SqlServerClientHostingExtensionsTests
+{
+ [Fact]
+ public void AddSqlServerClient_CanResolveConnection()
+ {
+ var builder = Host.CreateEmptyApplicationBuilder(null);
+
+ builder.AddSqlServerClient("database");
+
+ var app = builder.Build();
+
+ app.Services.GetRequiredService();
+ }
+
+ [Fact]
+ public void AddSqlServerClientFactory_CanResolveFactoryAndConnection()
+ {
+ var builder = Host.CreateEmptyApplicationBuilder(null);
+
+ builder.AddSqlServerClientFactory("database");
+
+ var app = builder.Build();
+
+ app.Services.GetRequiredService();
+ app.Services.GetRequiredService();
+ }
+
+ [Fact]
+ public void AddSqlServerClientFactory_Lifetime_Transient_FactoryHasCorrectLifetime()
+ {
+ var builder = Host.CreateEmptyApplicationBuilder(null);
+
+ builder.AddSqlServerClientFactory("database", lifetime: ServiceLifetime.Transient);
+
+ var app = builder.Build();
+
+ var factory1 = app.Services.GetRequiredService();
+ var factory2 = app.Services.GetRequiredService();
+ Assert.NotSame(factory1, factory2);
+ }
+
+ [Fact]
+ public void AddSqlServerClientFactory_Lifetime_Singleton_FactoryHasCorrectLifetime()
+ {
+ var builder = Host.CreateEmptyApplicationBuilder(null);
+
+ builder.AddSqlServerClientFactory("database", lifetime: ServiceLifetime.Singleton);
+
+ var app = builder.Build();
+
+ var factory1 = app.Services.GetRequiredService();
+ var factory2 = app.Services.GetRequiredService();
+ Assert.Same(factory1, factory2);
+ }
+
+ [Fact]
+ public void AddSqlServerClientFactory_Lifetime_Scoped_FactoryAndConnectionHaveCorrectLifetime()
+ {
+ var builder = Host.CreateEmptyApplicationBuilder(null);
+
+ builder.AddSqlServerClientFactory("database", lifetime: ServiceLifetime.Scoped);
+
+ var app = builder.Build();
+
+ // Scoped factories should be the same within the same scope
+ var factory1 = app.Services.GetRequiredService();
+ var factory2 = app.Services.GetRequiredService();
+ Assert.Same(factory1, factory2);
+
+ // Factory created SqlConnections should not be scoped if the factory is scoped
+ var createdConnection1 = factory1.CreateConnection();
+ var createdConnection2 = factory1.CreateConnection();
+ Assert.NotSame(createdConnection1, createdConnection2);
+
+ // Resolved SqlConnections should be the same within the same scope
+ var resolvedConnection1 = app.Services.GetRequiredService();
+ var resolvedConnection2 = app.Services.GetRequiredService();
+ Assert.Same(resolvedConnection1, resolvedConnection2);
+
+
+ // Factories should be different in different scopes
+ var scope = app.Services.CreateScope();
+ var scopedFactory1 = scope.ServiceProvider.GetRequiredService();
+ var scopedFactory2 = scope.ServiceProvider.GetRequiredService();
+ Assert.Same(scopedFactory1, scopedFactory2);
+ Assert.NotSame(scopedFactory1, factory1);
+
+ // Scope resolved SqlConnections should be the same within the same scope
+ var scopedConnection1 = scope.ServiceProvider.GetRequiredService();
+ var scopedConnection2 = scope.ServiceProvider.GetRequiredService();
+ Assert.Same(scopedConnection1, scopedConnection2);
+ Assert.NotSame(scopedConnection1, resolvedConnection1);
+ }
+
+
+ [Fact]
+ public void AddSqlServerClient_CanChangeSettingsInCode()
+ {
+ const string name = "database";
+ var builder = Host.CreateEmptyApplicationBuilder(null);
+
+ //Configure settings
+ builder.Configuration.AddInMemoryCollection([
+ new KeyValuePair(
+ $"{SqlServerClientSpark.ConfigurationSectionPath}:{name}:{SparkConfig.Settings}:{nameof(SqlServerClientSparkSettings.DisableTracing)}",
+ true.ToString()),
+ new KeyValuePair(
+ $"{SqlServerClientSpark.ConfigurationSectionPath}:{name}:{SparkConfig.Settings}:{nameof(SqlServerClientSparkSettings.DisableHealthChecks)}",
+ true.ToString())
+ ]);
+ builder.AddSqlServerClient(name,
+ configureSettings: settings =>
+ {
+ //Settings should have correct value from configuration
+ Assert.True(settings.DisableTracing);
+ Assert.True(settings.DisableHealthChecks);
+
+ //Change the settings
+ settings.DisableTracing = false;
+ });
+
+ var app = builder.Build();
+
+ var settings = app.Services.GetRequiredService();
+ Assert.False(settings.DisableTracing);
+ Assert.True(settings.DisableHealthChecks);
+ }
+
+ [Fact]
+ public void AddSqlServerClient_CanChangeOptionsInCode()
+ {
+ const string name = "database";
+ var initialConnectionString = new SqlConnectionStringBuilder
+ {
+ InitialCatalog = "InitialDatabase",
+ DataSource = "InitialServer"
+ }.ConnectionString;
+
+ var changedConnectionString = new SqlConnectionStringBuilder(initialConnectionString)
+ {
+ InitialCatalog = "ChangedDatabase"
+ }.ConnectionString;
+
+
+ var builder = Host.CreateEmptyApplicationBuilder(null);
+
+ //Configure options
+ builder.Configuration.AddInMemoryCollection([
+ new KeyValuePair(
+ $"{SqlServerClientSpark.ConfigurationSectionPath}:{name}:{nameof(SqlServerClientSparkOptions.ConnectionString)}",
+ initialConnectionString)
+ ]);
+ builder.AddSqlServerClient(name,
+ configureOptions: options =>
+ {
+ //Options should have correct value from configuration
+ Assert.Equal(initialConnectionString, options.ConnectionString);
+
+ //Change the options
+ options.ConnectionString = changedConnectionString;
+ });
+
+ var app = builder.Build();
+
+ var options = app.Services.GetRequiredService>();
+ Assert.Equal(changedConnectionString, options.Value.ConnectionString);
+
+
+ var connection = app.Services.GetRequiredService();
+
+ Assert.Equal(changedConnectionString, connection.ConnectionString);
+ }
+
+
+ [Fact]
+ public void AddSqlServerClient_CanHaveMultipleKeyedServices()
+ {
+ var builder = Host.CreateEmptyApplicationBuilder(null);
+
+ //Configure settings
+ builder.Configuration.AddInMemoryCollection([
+ new KeyValuePair(
+ $"{SqlServerClientSpark.ConfigurationSectionPath}:database1:{nameof(SqlServerClientSparkOptions.ConnectionString)}",
+ "Data Source=local;Database=database1"),
+ new KeyValuePair(
+ $"{SqlServerClientSpark.ConfigurationSectionPath}:database2:{nameof(SqlServerClientSparkOptions.ConnectionString)}",
+ "Data Source=local;Database=database2"),
+ new KeyValuePair(
+ $"{SqlServerClientSpark.ConfigurationSectionPath}:database3:{nameof(SqlServerClientSparkOptions.ConnectionString)}",
+ "Data Source=local;Database=database3")
+ ]);
+
+
+ builder.AddSqlServerClient("database1");
+ builder.AddSqlServerClient("database2", "database2");
+ builder.AddSqlServerClient("database3", "database3");
+
+ var app = builder.Build();
+
+ var connection1 = app.Services.GetRequiredService();
+ var connection2 = app.Services.GetRequiredKeyedService("database2");
+ var connection3 = app.Services.GetRequiredKeyedService("database3");
+
+ Assert.NotSame(connection1, connection2);
+ Assert.NotSame(connection1, connection3);
+ Assert.NotSame(connection2, connection3);
+
+ Assert.Equal("database1", connection1.Database);
+ Assert.Equal("database2", connection2.Database);
+ Assert.Equal("database3", connection3.Database);
+ }
+}
\ No newline at end of file
diff --git a/tests/ES.FX.Ignite.Microsoft.Data.SqlClient.Tests/SqlServerDbContextConnectTests.cs b/tests/ES.FX.Ignite.Microsoft.Data.SqlClient.Tests/SqlServerDbContextConnectTests.cs
new file mode 100644
index 0000000..9e3e83e
--- /dev/null
+++ b/tests/ES.FX.Ignite.Microsoft.Data.SqlClient.Tests/SqlServerDbContextConnectTests.cs
@@ -0,0 +1,27 @@
+using ES.FX.Ignite.Microsoft.Data.SqlClient.Hosting;
+using ES.FX.Shared.SqlServer.Tests.Fixtures;
+using Microsoft.Data.SqlClient;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+
+namespace ES.FX.Ignite.Microsoft.Data.SqlClient.Tests;
+
+public class SqlServerDbContextConnectTests(SqlServerContainerFixture sqlServerFixture)
+ : IClassFixture
+{
+ [Fact]
+ public async Task AddSqlServerClient_CanConnect()
+ {
+ var builder = Host.CreateEmptyApplicationBuilder(null);
+
+ builder.AddSqlServerClient("database",
+ configureOptions: options => { options.ConnectionString = sqlServerFixture.GetConnectionString(); }
+ );
+
+ var app = builder.Build();
+
+
+ var connection = app.Services.GetRequiredService();
+ await connection.OpenAsync();
+ }
+}
\ No newline at end of file
diff --git a/tests/ES.FX.Ignite.Microsoft.EntityFrameworkCore.SqlServer.Tests/Context/TestDbContextDesignTimeFactory.cs b/tests/ES.FX.Ignite.Microsoft.EntityFrameworkCore.SqlServer.Tests/Context/TestDbContextDesignTimeFactory.cs
new file mode 100644
index 0000000..172e357
--- /dev/null
+++ b/tests/ES.FX.Ignite.Microsoft.EntityFrameworkCore.SqlServer.Tests/Context/TestDbContextDesignTimeFactory.cs
@@ -0,0 +1,32 @@
+using ES.FX.Ignite.Microsoft.EntityFrameworkCore.Tests.Context;
+using JetBrains.Annotations;
+using Microsoft.Data.SqlClient;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Design;
+
+namespace ES.FX.Ignite.Microsoft.EntityFrameworkCore.SqlServer.Tests.Context;
+
+[PublicAPI]
+public class TestDbContextDesignTimeFactory : IDesignTimeDbContextFactory
+{
+ public TestDbContext CreateDbContext(string[] args)
+ {
+ var optionsBuilder = new DbContextOptionsBuilder();
+ var sqlBuilder = new SqlConnectionStringBuilder
+ {
+ DataSource = "(local)",
+ UserID = "sa",
+ Password = "SuperPass#",
+ InitialCatalog = $"{nameof(TestDbContext)}_Design",
+ TrustServerCertificate = true
+ };
+ optionsBuilder.UseSqlServer(sqlBuilder.ConnectionString,
+ sqlServerDbContextOptionsBuilder =>
+ {
+ sqlServerDbContextOptionsBuilder.MigrationsAssembly(typeof(TestDbContextDesignTimeFactory).Assembly
+ .FullName);
+ });
+
+ return new TestDbContext(optionsBuilder.Options);
+ }
+}
\ No newline at end of file
diff --git a/tests/ES.FX.Ignite.Microsoft.EntityFrameworkCore.SqlServer.Tests/ES.FX.Ignite.Microsoft.EntityFrameworkCore.SqlServer.Tests.csproj b/tests/ES.FX.Ignite.Microsoft.EntityFrameworkCore.SqlServer.Tests/ES.FX.Ignite.Microsoft.EntityFrameworkCore.SqlServer.Tests.csproj
new file mode 100644
index 0000000..83f3faa
--- /dev/null
+++ b/tests/ES.FX.Ignite.Microsoft.EntityFrameworkCore.SqlServer.Tests/ES.FX.Ignite.Microsoft.EntityFrameworkCore.SqlServer.Tests.csproj
@@ -0,0 +1,40 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+ false
+ true
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/ES.FX.Ignite.Microsoft.EntityFrameworkCore.SqlServer.Tests/Migrations/20240611164449_V1.Designer.cs b/tests/ES.FX.Ignite.Microsoft.EntityFrameworkCore.SqlServer.Tests/Migrations/20240611164449_V1.Designer.cs
new file mode 100644
index 0000000..d2efc24
--- /dev/null
+++ b/tests/ES.FX.Ignite.Microsoft.EntityFrameworkCore.SqlServer.Tests/Migrations/20240611164449_V1.Designer.cs
@@ -0,0 +1,41 @@
+//
+using System;
+using ES.FX.Ignite.Microsoft.EntityFrameworkCore.Tests.Context;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Metadata;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace ES.FX.Ignite.Microsoft.EntityFrameworkCore.SqlServer.Tests.Migrations
+{
+ [DbContext(typeof(TestDbContext))]
+ [Migration("20240611164449_V1")]
+ partial class V1
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "8.0.6")
+ .HasAnnotation("Relational:MaxIdentifierLength", 128);
+
+ SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
+
+ modelBuilder.Entity("ES.FX.Ignite.Microsoft.EntityFrameworkCore.Tests.Context.Entities.TestUser", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier");
+
+ b.HasKey("Id");
+
+ b.ToTable("TestUsers");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/tests/ES.FX.Ignite.Microsoft.EntityFrameworkCore.SqlServer.Tests/Migrations/20240611164449_V1.cs b/tests/ES.FX.Ignite.Microsoft.EntityFrameworkCore.SqlServer.Tests/Migrations/20240611164449_V1.cs
new file mode 100644
index 0000000..d4d1fcd
--- /dev/null
+++ b/tests/ES.FX.Ignite.Microsoft.EntityFrameworkCore.SqlServer.Tests/Migrations/20240611164449_V1.cs
@@ -0,0 +1,33 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace ES.FX.Ignite.Microsoft.EntityFrameworkCore.SqlServer.Tests.Migrations
+{
+ ///
+ public partial class V1 : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.CreateTable(
+ name: "TestUsers",
+ columns: table => new
+ {
+ Id = table.Column(type: "uniqueidentifier", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_TestUsers", x => x.Id);
+ });
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(
+ name: "TestUsers");
+ }
+ }
+}
diff --git a/tests/ES.FX.Ignite.Microsoft.EntityFrameworkCore.SqlServer.Tests/Migrations/TestDbContextModelSnapshot.cs b/tests/ES.FX.Ignite.Microsoft.EntityFrameworkCore.SqlServer.Tests/Migrations/TestDbContextModelSnapshot.cs
new file mode 100644
index 0000000..ba64bf9
--- /dev/null
+++ b/tests/ES.FX.Ignite.Microsoft.EntityFrameworkCore.SqlServer.Tests/Migrations/TestDbContextModelSnapshot.cs
@@ -0,0 +1,38 @@
+//
+using System;
+using ES.FX.Ignite.Microsoft.EntityFrameworkCore.Tests.Context;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Metadata;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace ES.FX.Ignite.Microsoft.EntityFrameworkCore.SqlServer.Tests.Migrations
+{
+ [DbContext(typeof(TestDbContext))]
+ partial class TestDbContextModelSnapshot : ModelSnapshot
+ {
+ protected override void BuildModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "8.0.6")
+ .HasAnnotation("Relational:MaxIdentifierLength", 128);
+
+ SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
+
+ modelBuilder.Entity("ES.FX.Ignite.Microsoft.EntityFrameworkCore.Tests.Context.Entities.TestUser", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier");
+
+ b.HasKey("Id");
+
+ b.ToTable("SimpleUsers");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/tests/ES.FX.Ignite.Microsoft.EntityFrameworkCore.SqlServer.Tests/SqlServerDbContextConnectTests.cs b/tests/ES.FX.Ignite.Microsoft.EntityFrameworkCore.SqlServer.Tests/SqlServerDbContextConnectTests.cs
new file mode 100644
index 0000000..b4bf006
--- /dev/null
+++ b/tests/ES.FX.Ignite.Microsoft.EntityFrameworkCore.SqlServer.Tests/SqlServerDbContextConnectTests.cs
@@ -0,0 +1,37 @@
+using ES.FX.Ignite.Microsoft.EntityFrameworkCore.SqlServer.Hosting;
+using ES.FX.Ignite.Microsoft.EntityFrameworkCore.SqlServer.Tests.Context;
+using ES.FX.Ignite.Microsoft.EntityFrameworkCore.Tests.Context;
+using ES.FX.Shared.SqlServer.Tests.Fixtures;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+
+namespace ES.FX.Ignite.Microsoft.EntityFrameworkCore.SqlServer.Tests;
+
+public class SqlServerDbContextConnectTests(SqlServerContainerFixture sqlServerFixture)
+ : IClassFixture
+{
+ [Fact]
+ public async Task AddSqlServerDbContext_CanConnect()
+ {
+ var builder = Host.CreateEmptyApplicationBuilder(null);
+
+ builder.AddSqlServerDbContext(
+ configureOptions: options => { options.ConnectionString = sqlServerFixture.GetConnectionString(); },
+ configureSqlServerDbContextOptionsBuilder: sqlServerDbContextOptionsBuilder =>
+ {
+ sqlServerDbContextOptionsBuilder.MigrationsAssembly(
+ typeof(TestDbContextDesignTimeFactory).Assembly.FullName);
+ });
+
+ var app = builder.Build();
+
+
+ var context = app.Services.GetRequiredService();
+ Assert.True(await context.Database.CanConnectAsync());
+
+ await context.Database.MigrateAsync();
+ var pendingMigrations = await context.Database.GetPendingMigrationsAsync();
+ Assert.Empty(pendingMigrations);
+ }
+}
\ No newline at end of file
diff --git a/tests/ES.FX.Ignite.Microsoft.EntityFrameworkCore.SqlServer.Tests/SqlServerDbContextHostingExtensionsTests.cs b/tests/ES.FX.Ignite.Microsoft.EntityFrameworkCore.SqlServer.Tests/SqlServerDbContextHostingExtensionsTests.cs
new file mode 100644
index 0000000..92c3dd1
--- /dev/null
+++ b/tests/ES.FX.Ignite.Microsoft.EntityFrameworkCore.SqlServer.Tests/SqlServerDbContextHostingExtensionsTests.cs
@@ -0,0 +1,239 @@
+using ES.FX.Ignite.Microsoft.EntityFrameworkCore.Spark;
+using ES.FX.Ignite.Microsoft.EntityFrameworkCore.SqlServer.Configuration;
+using ES.FX.Ignite.Microsoft.EntityFrameworkCore.SqlServer.Hosting;
+using ES.FX.Ignite.Microsoft.EntityFrameworkCore.Tests.Context;
+using ES.FX.Ignite.Spark.Configuration;
+using Microsoft.Data.SqlClient;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.SqlServer.Infrastructure.Internal;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Options;
+
+namespace ES.FX.Ignite.Microsoft.EntityFrameworkCore.SqlServer.Tests;
+
+#pragma warning disable EF1001 // Internal EF Core API usage.
+public class SqlServerDbContextHostingExtensionsTests
+{
+ [Fact]
+ public void AddSqlServerDbContext_CanResolveDbContext()
+ {
+ var builder = Host.CreateEmptyApplicationBuilder(null);
+
+ builder.AddSqlServerDbContext();
+
+ var app = builder.Build();
+
+ app.Services.GetRequiredService();
+ }
+
+ [Fact]
+ public void AddSqlServerDbContextFactory_CanResolveFactoryAndDbContext()
+ {
+ var builder = Host.CreateEmptyApplicationBuilder(null);
+
+ builder.AddSqlServerDbContextFactory();
+
+ var app = builder.Build();
+
+ app.Services.GetRequiredService();
+ app.Services.GetRequiredService>();
+ }
+
+ [Fact]
+ public void AddSqlServerDbContextFactory_Lifetime_Transient_FactoryHasCorrectLifetime()
+ {
+ var builder = Host.CreateEmptyApplicationBuilder(null);
+
+ builder.AddSqlServerDbContextFactory(lifetime: ServiceLifetime.Transient);
+
+ var app = builder.Build();
+
+ var factory1 = app.Services.GetRequiredService>();
+ var factory2 = app.Services.GetRequiredService>();
+ Assert.NotSame(factory1, factory2);
+ }
+
+ [Fact]
+ public void AddSqlServerDbContextFactory_Lifetime_Singleton_FactoryHasCorrectLifetime()
+ {
+ var builder = Host.CreateEmptyApplicationBuilder(null);
+
+ builder.AddSqlServerDbContextFactory(lifetime: ServiceLifetime.Singleton);
+
+ var app = builder.Build();
+
+ var factory1 = app.Services.GetRequiredService>();
+ var factory2 = app.Services.GetRequiredService>();
+ Assert.Same(factory1, factory2);
+ }
+
+ [Fact]
+ public void AddSqlServerDbContextFactory_Lifetime_Scoped_FactoryAndContextHaveCorrectLifetime()
+ {
+ var builder = Host.CreateEmptyApplicationBuilder(null);
+
+ builder.AddSqlServerDbContextFactory(lifetime: ServiceLifetime.Scoped);
+
+ var app = builder.Build();
+
+ // Scoped factories should be the same within the same scope
+ var factory1 = app.Services.GetRequiredService>();
+ var factory2 = app.Services.GetRequiredService>();
+ Assert.Same(factory1, factory2);
+
+ // Factory created DbContexts should not be scoped if the factory is scoped
+ var createdDbContext1 = factory1.CreateDbContext();
+ var createdDbContext2 = factory1.CreateDbContext();
+ Assert.NotSame(createdDbContext1, createdDbContext2);
+
+ // Resolved DbContexts should be the same within the same scope
+ var resolvedDbContext1 = app.Services.GetRequiredService();
+ var resolvedDbContext2 = app.Services.GetRequiredService();
+ Assert.Same(resolvedDbContext1, resolvedDbContext2);
+
+
+ // Factories should be different in different scopes
+ var scope = app.Services.CreateScope();
+ var scopedFactory1 = scope.ServiceProvider.GetRequiredService>();
+ var scopedFactory2 = scope.ServiceProvider.GetRequiredService>();
+ Assert.Same(scopedFactory1, scopedFactory2);
+ Assert.NotSame(scopedFactory1, factory1);
+
+ // Scope resolved DbContexts should be the same within the same scope
+ var scopedDbContext1 = scope.ServiceProvider.GetRequiredService();
+ var scopedDbContext2 = scope.ServiceProvider.GetRequiredService();
+ Assert.Same(scopedDbContext1, scopedDbContext2);
+ Assert.NotSame(scopedDbContext1, resolvedDbContext1);
+ }
+
+
+ [Fact]
+ public void AddSqlServerDbContext_CanChangeSettingsInCode()
+ {
+ var builder = Host.CreateEmptyApplicationBuilder(null);
+
+ //Configure settings
+ builder.Configuration.AddInMemoryCollection([
+ new KeyValuePair(
+ $"{DbContextSpark.ConfigurationSectionPath}:{nameof(TestDbContext)}:{SparkConfig.Settings}:{nameof(SqlServerDbContextSparkSettings.DisableTracing)}",
+ true.ToString()),
+ new KeyValuePair(
+ $"{DbContextSpark.ConfigurationSectionPath}:{nameof(TestDbContext)}:{SparkConfig.Settings}:{nameof(SqlServerDbContextSparkSettings.DisableHealthChecks)}",
+ true.ToString())
+ ]);
+ builder.AddSqlServerDbContext(
+ configureSettings: settings =>
+ {
+ //Settings should have correct value from configuration
+ Assert.True(settings.DisableTracing);
+ Assert.True(settings.DisableHealthChecks);
+
+ //Change the settings
+ settings.DisableTracing = false;
+ });
+
+ var app = builder.Build();
+
+ var settings = app.Services.GetRequiredService>();
+ Assert.False(settings.DisableTracing);
+ Assert.True(settings.DisableHealthChecks);
+ }
+
+ [Fact]
+ public void AddSqlServerDbContext_CanChangeOptionsInCode()
+ {
+ var initialConnectionString = new SqlConnectionStringBuilder
+ {
+ InitialCatalog = "InitialDatabase",
+ DataSource = "InitialServer"
+ }.ConnectionString;
+ const int initialCommandTimeout = 123;
+
+ var changedConnectionString = new SqlConnectionStringBuilder(initialConnectionString)
+ {
+ InitialCatalog = "ChangedDatabase"
+ }.ConnectionString;
+ const int changedCommandTimeout = 500;
+
+
+ var builder = Host.CreateEmptyApplicationBuilder(null);
+
+ //Configure options
+ builder.Configuration.AddInMemoryCollection([
+ new KeyValuePair(
+ $"{DbContextSpark.ConfigurationSectionPath}:{nameof(TestDbContext)}:{nameof(SqlServerDbContextSparkOptions.ConnectionString)}",
+ initialConnectionString),
+ new KeyValuePair(
+ $"{DbContextSpark.ConfigurationSectionPath}:{nameof(TestDbContext)}:{nameof(SqlServerDbContextSparkOptions.CommandTimeout)}",
+ initialCommandTimeout.ToString())
+ ]);
+ builder.AddSqlServerDbContext(
+ configureOptions: options =>
+ {
+ //Options should have correct value from configuration
+ Assert.Equal(initialConnectionString, options.ConnectionString);
+ Assert.Equal(initialCommandTimeout, options.CommandTimeout);
+
+ //Change the options
+ options.ConnectionString = changedConnectionString;
+ options.CommandTimeout = changedCommandTimeout;
+ });
+
+ var app = builder.Build();
+
+ var options = app.Services.GetRequiredService>>();
+ Assert.Equal(changedConnectionString, options.Value.ConnectionString);
+ Assert.Equal(changedCommandTimeout, options.Value.CommandTimeout);
+
+
+ var context = app.Services.GetRequiredService();
+
+ Assert.Equal(changedCommandTimeout, context.Options.FindExtension()?.CommandTimeout);
+ Assert.Equal(changedConnectionString,
+ context.Options.FindExtension()?.ConnectionString);
+ }
+
+ [Fact]
+ public void AddSqlServerDbContext_CanChangeDbContextOptionsInCode()
+ {
+ var builder = Host.CreateEmptyApplicationBuilder(null);
+
+ builder.AddSqlServerDbContext(
+ configureDbContextOptionsBuilder: dbContextOptionsBuilder =>
+ {
+ //Enable sensitive data logging
+ dbContextOptionsBuilder.EnableSensitiveDataLogging();
+ });
+
+ var app = builder.Build();
+
+ var context = app.Services.GetRequiredService();
+
+ //Check if sensitive data logging is enabled
+ Assert.True(context.Options.FindExtension()?.IsSensitiveDataLoggingEnabled);
+ }
+
+
+ [Fact]
+ public void AddSqlServerDbContext_CanChangeSqlServerDbContextOptionsInCode()
+ {
+ const int commandTimeout = 12345;
+
+ var builder = Host.CreateEmptyApplicationBuilder(null);
+
+ builder.AddSqlServerDbContext(
+ configureSqlServerDbContextOptionsBuilder: sqlServerDbContextOptionsBuilder =>
+ {
+ sqlServerDbContextOptionsBuilder.CommandTimeout(commandTimeout);
+ });
+
+ var app = builder.Build();
+
+ var context = app.Services.GetRequiredService();
+
+ Assert.Equal(commandTimeout, context.Database.GetCommandTimeout());
+ }
+}
\ No newline at end of file
diff --git a/tests/ES.FX.Ignite.Microsoft.EntityFrameworkCore.Tests/Context/Configurations/TestUserEntityConfiguration.cs b/tests/ES.FX.Ignite.Microsoft.EntityFrameworkCore.Tests/Context/Configurations/TestUserEntityConfiguration.cs
new file mode 100644
index 0000000..ead570b
--- /dev/null
+++ b/tests/ES.FX.Ignite.Microsoft.EntityFrameworkCore.Tests/Context/Configurations/TestUserEntityConfiguration.cs
@@ -0,0 +1,13 @@
+using ES.FX.Ignite.Microsoft.EntityFrameworkCore.Tests.Context.Entities;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace ES.FX.Ignite.Microsoft.EntityFrameworkCore.Tests.Context.Configurations;
+
+public class TestUserEntityConfiguration : IEntityTypeConfiguration
+{
+ public void Configure(EntityTypeBuilder builder)
+ {
+ builder.HasKey(p => p.Id);
+ }
+}
\ No newline at end of file
diff --git a/tests/ES.FX.Ignite.Microsoft.EntityFrameworkCore.Tests/Context/Entities/TestUser.cs b/tests/ES.FX.Ignite.Microsoft.EntityFrameworkCore.Tests/Context/Entities/TestUser.cs
new file mode 100644
index 0000000..932c31b
--- /dev/null
+++ b/tests/ES.FX.Ignite.Microsoft.EntityFrameworkCore.Tests/Context/Entities/TestUser.cs
@@ -0,0 +1,6 @@
+namespace ES.FX.Ignite.Microsoft.EntityFrameworkCore.Tests.Context.Entities;
+
+public class TestUser
+{
+ public Guid Id { get; set; }
+}
\ No newline at end of file
diff --git a/tests/ES.FX.Ignite.Microsoft.EntityFrameworkCore.Tests/Context/TestDbContext.cs b/tests/ES.FX.Ignite.Microsoft.EntityFrameworkCore.Tests/Context/TestDbContext.cs
new file mode 100644
index 0000000..f6ebdbe
--- /dev/null
+++ b/tests/ES.FX.Ignite.Microsoft.EntityFrameworkCore.Tests/Context/TestDbContext.cs
@@ -0,0 +1,22 @@
+using ES.FX.Ignite.Microsoft.EntityFrameworkCore.Tests.Context.Entities;
+using Microsoft.EntityFrameworkCore;
+
+namespace ES.FX.Ignite.Microsoft.EntityFrameworkCore.Tests.Context;
+
+public class TestDbContext(
+ DbContextOptions dbContextOptions) :
+ DbContext(dbContextOptions)
+{
+ public DbSet TestUsers { get; set; }
+
+
+ public DbContextOptions Options => dbContextOptions;
+
+ protected override void OnModelCreating(ModelBuilder modelBuilder)
+ {
+ modelBuilder
+ .ApplyConfigurationsFromAssembly(typeof(TestDbContext).Assembly);
+
+ base.OnModelCreating(modelBuilder);
+ }
+}
\ No newline at end of file
diff --git a/tests/ES.FX.Ignite.Microsoft.EntityFrameworkCore.Tests/ES.FX.Ignite.Microsoft.EntityFrameworkCore.Tests.csproj b/tests/ES.FX.Ignite.Microsoft.EntityFrameworkCore.Tests/ES.FX.Ignite.Microsoft.EntityFrameworkCore.Tests.csproj
new file mode 100644
index 0000000..8b3abd8
--- /dev/null
+++ b/tests/ES.FX.Ignite.Microsoft.EntityFrameworkCore.Tests/ES.FX.Ignite.Microsoft.EntityFrameworkCore.Tests.csproj
@@ -0,0 +1,36 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+ false
+ true
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/ES.FX.Ignite.Microsoft.EntityFrameworkCore.Tests/UnitTest1.cs b/tests/ES.FX.Ignite.Microsoft.EntityFrameworkCore.Tests/UnitTest1.cs
new file mode 100644
index 0000000..e3b2ea7
--- /dev/null
+++ b/tests/ES.FX.Ignite.Microsoft.EntityFrameworkCore.Tests/UnitTest1.cs
@@ -0,0 +1,9 @@
+namespace ES.FX.Ignite.Microsoft.EntityFrameworkCore.Tests;
+
+public class UnitTest1
+{
+ [Fact]
+ public void Test1()
+ {
+ }
+}
\ No newline at end of file
diff --git a/tests/ES.FX.Ignite.Spark.Tests/Configuration/SparkConfigTests.cs b/tests/ES.FX.Ignite.Spark.Tests/Configuration/SparkConfigTests.cs
new file mode 100644
index 0000000..c188ae3
--- /dev/null
+++ b/tests/ES.FX.Ignite.Spark.Tests/Configuration/SparkConfigTests.cs
@@ -0,0 +1,45 @@
+using ES.FX.Ignite.Spark.Configuration;
+
+namespace ES.FX.Ignite.Spark.Tests.Configuration;
+
+public class SparkConfigTests
+{
+ [Fact]
+ public void SparkConfig_Name_UsesDefaultIfNull()
+ {
+ const string defaultName = "default";
+ var name = SparkConfig.Name(null, defaultName);
+
+ Assert.Equal(defaultName, name);
+ }
+
+ [Fact]
+ public void SparkConfig_Name_UsesNameIfNotNull()
+ {
+ const string defaultName = "default";
+ const string serviceName = "key";
+ var name = SparkConfig.Name(serviceName, defaultName);
+
+ Assert.Equal(serviceName, name);
+ }
+
+
+ [Fact]
+ public void SparkConfig_Path_UsesNameIfSectionIsNull()
+ {
+ const string name = "name";
+ var configPath = SparkConfig.Path(name, string.Empty);
+
+ Assert.Equal(name, configPath);
+ }
+
+ [Fact]
+ public void SparkConfig_Path_UsesNameAndSection()
+ {
+ const string section = "section";
+ const string name = "name";
+ var configPath = SparkConfig.Path(name, section);
+
+ Assert.Equal($"{section}:{name}", configPath);
+ }
+}
\ No newline at end of file
diff --git a/tests/ES.FX.Ignite.Spark.Tests/ES.FX.Ignite.Spark.Tests.csproj b/tests/ES.FX.Ignite.Spark.Tests/ES.FX.Ignite.Spark.Tests.csproj
new file mode 100644
index 0000000..813630b
--- /dev/null
+++ b/tests/ES.FX.Ignite.Spark.Tests/ES.FX.Ignite.Spark.Tests.csproj
@@ -0,0 +1,32 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+ false
+ true
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/ES.FX.Shared.SqlServer.Tests/ES.FX.Shared.SqlServer.Tests.csproj b/tests/ES.FX.Shared.SqlServer.Tests/ES.FX.Shared.SqlServer.Tests.csproj
new file mode 100644
index 0000000..0f93196
--- /dev/null
+++ b/tests/ES.FX.Shared.SqlServer.Tests/ES.FX.Shared.SqlServer.Tests.csproj
@@ -0,0 +1,35 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+ false
+ true
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/ES.FX.Shared.SqlServer.Tests/Fixtures/SqlServerContainerFixture.cs b/tests/ES.FX.Shared.SqlServer.Tests/Fixtures/SqlServerContainerFixture.cs
new file mode 100644
index 0000000..df46209
--- /dev/null
+++ b/tests/ES.FX.Shared.SqlServer.Tests/Fixtures/SqlServerContainerFixture.cs
@@ -0,0 +1,29 @@
+using Testcontainers.MsSql;
+
+namespace ES.FX.Shared.SqlServer.Tests.Fixtures;
+
+public sealed class SqlServerContainerFixture : IAsyncLifetime
+{
+ public const string Registry = "mcr.microsoft.com";
+ public const string Image = "mssql/server";
+ public const string Tag = "2022-latest";
+ public MsSqlContainer? Container { get; private set; }
+
+ public async Task InitializeAsync()
+ {
+ Container = new MsSqlBuilder()
+ .WithName($"{nameof(SqlServerContainerFixture)}-{Guid.NewGuid()}")
+ .WithImage($"{Registry}/{Image}:{Tag}")
+ .Build();
+ await Container.StartAsync();
+ }
+
+ public async Task DisposeAsync()
+ {
+ if (Container is not null) await Container.DisposeAsync();
+ }
+
+ public string GetConnectionString() =>
+ Container?.GetConnectionString() ??
+ throw new InvalidOperationException("The test container was not initialized.");
+}
\ No newline at end of file
diff --git a/tests/ES.FX.Shared.SqlServer.Tests/SqlServerFixtureTests.cs b/tests/ES.FX.Shared.SqlServer.Tests/SqlServerFixtureTests.cs
new file mode 100644
index 0000000..c4f16c4
--- /dev/null
+++ b/tests/ES.FX.Shared.SqlServer.Tests/SqlServerFixtureTests.cs
@@ -0,0 +1,18 @@
+using ES.FX.Microsoft.Data.SqlClient.Queries;
+using ES.FX.Shared.SqlServer.Tests.Fixtures;
+using Microsoft.Data.SqlClient;
+
+namespace ES.FX.Shared.SqlServer.Tests;
+
+public class SqlServerFixtureTests(SqlServerContainerFixture sqlServerContainerFixture)
+ : IClassFixture
+{
+ [Fact]
+ public void SqlServerContainer_CanConnect()
+ {
+ var connectionString = sqlServerContainerFixture.GetConnectionString();
+ var connection = new SqlConnection(connectionString);
+ var result = connection.ExecuteSafeQuery();
+ Assert.True(result);
+ }
+}
\ No newline at end of file
diff --git a/tests/ES.FX.Tests/Collections/ExceptionExtensionsTests.cs b/tests/ES.FX.Tests/Collections/ExceptionExtensionsTests.cs
new file mode 100644
index 0000000..51f2fc9
--- /dev/null
+++ b/tests/ES.FX.Tests/Collections/ExceptionExtensionsTests.cs
@@ -0,0 +1,30 @@
+using ES.FX.Collections;
+
+namespace ES.FX.Tests.Collections;
+
+public class ExceptionExtensionsTests
+{
+ [Fact]
+ public void Array_NullOrEmpty_ReturnsTrueForNull()
+ {
+ Array? array = null;
+ var result = array.IsNullOrEmpty();
+ Assert.True(result);
+ }
+
+ [Fact]
+ public void Array_NullOrEmpty_ReturnsTrueForEmpty()
+ {
+ Array array = Array.Empty();
+ var result = array.IsNullOrEmpty();
+ Assert.True(result);
+ }
+
+ [Fact]
+ public void Array_NullOrEmpty_ReturnsFalseForArrayWithElements()
+ {
+ var array = new[] { 0 };
+ var result = array.IsNullOrEmpty();
+ Assert.False(result);
+ }
+}
\ No newline at end of file
diff --git a/tests/ES.FX.Tests/ES.FX.Tests.csproj b/tests/ES.FX.Tests/ES.FX.Tests.csproj
new file mode 100644
index 0000000..e8be7f6
--- /dev/null
+++ b/tests/ES.FX.Tests/ES.FX.Tests.csproj
@@ -0,0 +1,40 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+ false
+ true
+
+
+
+
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/ES.FX.Tests/Exceptions/ExceptionExtensionsTests.cs b/tests/ES.FX.Tests/Exceptions/ExceptionExtensionsTests.cs
new file mode 100644
index 0000000..72252a6
--- /dev/null
+++ b/tests/ES.FX.Tests/Exceptions/ExceptionExtensionsTests.cs
@@ -0,0 +1,53 @@
+using ES.FX.Exceptions;
+
+namespace ES.FX.Tests.Exceptions;
+
+public class ExceptionExtensionsTests
+{
+ [Fact]
+ public void Exception_InnerMost_ReturnsInnermostException()
+ {
+ var exception = new Exception("Outer", new Exception("Inner", new Exception("Innermost")));
+ var result = exception.InnermostException();
+ Assert.Equal("Innermost", result.Message);
+ }
+
+ [Fact]
+ public void Exception_InnerMost_ReturnsSelfIfNoInnerException()
+ {
+ var exception = new Exception("Outer");
+ var result = exception.InnermostException();
+ Assert.Equal("Outer", result.Message);
+ }
+
+ [Fact]
+ public void Exception_InnerMostOfType_ReturnsInnermostExceptionOfType()
+ {
+ var exception = new Exception("Outer",
+ new ArgumentException("Inner",
+ new ArgumentException("Innermost",
+ new Exception("Last"))));
+ var result = exception.InnermostException();
+ Assert.NotNull(result);
+ Assert.Equal("Innermost", result.Message);
+ }
+
+ [Fact]
+ public void Exception_InnerMostOfType_ReturnsNullIfNoInnermostException()
+ {
+ var exception = new Exception("Outer",
+ new ArgumentException("Inner",
+ new ArgumentException("Innermost",
+ new Exception("Last"))));
+ var result = exception.InnermostException();
+ Assert.Null(result);
+ }
+
+ [Fact]
+ public void Exception_InnerMost_ReturnsNullForANullException()
+ {
+ Exception? exception = null;
+ var result = exception.InnermostException();
+ Assert.Null(result);
+ }
+}
\ No newline at end of file
diff --git a/tests/ES.FX.Tests/IO/ManifestResourceTests.cs b/tests/ES.FX.Tests/IO/ManifestResourceTests.cs
new file mode 100644
index 0000000..c368d4d
--- /dev/null
+++ b/tests/ES.FX.Tests/IO/ManifestResourceTests.cs
@@ -0,0 +1,54 @@
+using ES.FX.IO;
+
+namespace ES.FX.Tests.IO;
+
+public class ManifestResourceTests
+{
+ [Fact]
+ public void Stream_ToByteArray_ReturnsCorrectByteArray()
+ {
+ var sourceArray = new[] { byte.MinValue, byte.MaxValue };
+ var source = new MemoryStream(sourceArray);
+
+ var result = source.ToByteArray();
+ Assert.Equal(2, result.Length);
+ Assert.Equal(sourceArray, result);
+ }
+
+ [Fact]
+ public void Stream_ToByteArray_ReturnsCorrectByteArrayForNonMemoryStream()
+ {
+ var sourceArray = new[] { byte.MinValue, byte.MaxValue };
+ var sourceMemoryStream = new MemoryStream(sourceArray);
+ var source = new BufferedStream(sourceMemoryStream);
+
+ var result = source.ToByteArray();
+ Assert.Equal(2, result.Length);
+ Assert.Equal(sourceArray, result);
+ }
+
+
+ [Fact]
+ public async Task Stream_ToByteArrayAsync_ReturnsCorrectByteArray()
+ {
+ var sourceArray = new[] { byte.MinValue, byte.MaxValue };
+ var source = new MemoryStream(sourceArray);
+
+ var result = await source.ToByteArrayAsync();
+ Assert.Equal(2, result.Length);
+ Assert.Equal(sourceArray, result);
+ }
+
+
+ [Fact]
+ public async Task Stream_ToByteArrayAsync_ReturnsCorrectByteArrayForNonMemoryStream()
+ {
+ var sourceArray = new[] { byte.MinValue, byte.MaxValue };
+ var sourceMemoryStream = new MemoryStream(sourceArray);
+ var source = new BufferedStream(sourceMemoryStream);
+
+ var result = await source.ToByteArrayAsync();
+ Assert.Equal(2, result.Length);
+ Assert.Equal(sourceArray, result);
+ }
+}
\ No newline at end of file
diff --git a/tests/ES.FX.Tests/Linq/EnumerableExtensionsTests.cs b/tests/ES.FX.Tests/Linq/EnumerableExtensionsTests.cs
new file mode 100644
index 0000000..fec27db
--- /dev/null
+++ b/tests/ES.FX.Tests/Linq/EnumerableExtensionsTests.cs
@@ -0,0 +1,23 @@
+using ES.FX.Linq;
+
+namespace ES.FX.Tests.Linq;
+
+public class EnumerableExtensionsTests
+{
+ [Fact]
+ public void Enumerable_GetManifestResources_ReturnsItemIfNotEmpty()
+ {
+ var enumerable = new[] { 1, 2, 3, 4, 5 };
+ var result = enumerable.TakeRandomItemOrDefault();
+ Assert.Contains(result, enumerable);
+ }
+
+
+ [Fact]
+ public void Enumerable_GetManifestResources_ReturnsItemIfEmpty()
+ {
+ var enumerable = Array.Empty();
+ var result = enumerable.TakeRandomItemOrDefault();
+ Assert.Equal(default, result);
+ }
+}
\ No newline at end of file
diff --git a/tests/ES.FX.Tests/Reflection/EnumerableExtensionsTests.cs b/tests/ES.FX.Tests/Reflection/EnumerableExtensionsTests.cs
new file mode 100644
index 0000000..21d5786
--- /dev/null
+++ b/tests/ES.FX.Tests/Reflection/EnumerableExtensionsTests.cs
@@ -0,0 +1,153 @@
+using ES.FX.Reflection;
+
+namespace ES.FX.Tests.Reflection;
+
+public class EnumerableExtensionsTests
+{
+ [Fact]
+ public void Assembly_GetManifestResources_ReturnsManifestResources()
+ {
+ var assembly = typeof(EnumerableExtensionsTests).Assembly;
+ var resources = assembly.GetManifestResources();
+ Assert.NotEmpty(resources);
+ }
+
+
+ [Fact]
+ public void ManifestResource_ReturnsNullInfoForInvalidResourceName()
+ {
+ var assembly = typeof(EnumerableExtensionsTests).Assembly;
+ var resource = new ManifestResource(assembly, Guid.NewGuid().ToString());
+ Assert.Null(resource.Info);
+ }
+
+ [Fact]
+ public void ManifestResource_ReturnsNullStreamForInvalidResourceName()
+ {
+ var assembly = typeof(EnumerableExtensionsTests).Assembly;
+ var resource = new ManifestResource(assembly, Guid.NewGuid().ToString());
+ Assert.Null(resource.GetStream());
+ }
+
+ [Fact]
+ public void ManifestResource_ReturnsNullStreamReaderForInvalidResourceName()
+ {
+ var assembly = typeof(EnumerableExtensionsTests).Assembly;
+ var resource = new ManifestResource(assembly, Guid.NewGuid().ToString());
+ Assert.Null(resource.GetStreamReader());
+ }
+
+ [Fact]
+ public void ManifestResource_ReturnsNullByteArrayForInvalidResourceName()
+ {
+ var assembly = typeof(EnumerableExtensionsTests).Assembly;
+ var resource = new ManifestResource(assembly, Guid.NewGuid().ToString());
+ var result = resource.ReadAllBytes();
+ Assert.Null(result);
+ }
+
+ [Fact]
+ public void ManifestResource_ReturnsNullTextForInvalidResourceName()
+ {
+ var assembly = typeof(EnumerableExtensionsTests).Assembly;
+ var resource = new ManifestResource(assembly, Guid.NewGuid().ToString());
+ var result = resource.ReadText();
+ Assert.Null(result);
+ }
+
+ [Fact]
+ public async Task ManifestResource_ReturnsNullTextAsyncForInvalidResourceName()
+ {
+ var assembly = typeof(EnumerableExtensionsTests).Assembly;
+ var resource = new ManifestResource(assembly, Guid.NewGuid().ToString());
+ var result = await resource.ReadTextAsync();
+ Assert.Null(result);
+ }
+
+
+ [Fact]
+ public void ManifestResource_ReturnsResourceByName()
+ {
+ var assembly = typeof(EnumerableExtensionsTests).Assembly;
+ var resources = assembly.GetManifestResources();
+ var resource = resources.First();
+
+ var result = new ManifestResource(assembly, resource.Name);
+ Assert.NotNull(result);
+ Assert.Equal(resource.Name, result.Name);
+ Assert.Equal(resource.Info?.FileName, result.Info?.FileName);
+ }
+
+ [Fact]
+ public void ManifestResource_CanReadStream()
+ {
+ var assembly = typeof(EnumerableExtensionsTests).Assembly;
+ var resources = assembly.GetManifestResources();
+ var resource = resources.First();
+
+ var result = resource.GetStream();
+ Assert.NotNull(result);
+
+ Assert.True(result.CanRead);
+ }
+
+ [Fact]
+ public void ManifestResource_CanGetStreamReader()
+ {
+ var assembly = typeof(EnumerableExtensionsTests).Assembly;
+ var resources = assembly.GetManifestResources();
+ var resource = resources.First();
+
+ var result = resource.GetStreamReader();
+ Assert.NotNull(result);
+ }
+
+ [Fact]
+ public void ManifestResource_CanReadText()
+ {
+ var assembly = typeof(EnumerableExtensionsTests).Assembly;
+ var resources = assembly.GetManifestResources();
+ var resource = resources.First();
+
+ var result = resource.ReadText();
+ Assert.NotNull(result);
+ Assert.Equal("TestContentDoNotModify", result);
+ }
+
+
+ [Fact]
+ public async Task ManifestResource_CanReadTextAsync()
+ {
+ var assembly = typeof(EnumerableExtensionsTests).Assembly;
+ var resources = assembly.GetManifestResources();
+ var resource = resources.First();
+
+ var result = await resource.ReadTextAsync();
+ Assert.NotNull(result);
+ Assert.Equal("TestContentDoNotModify", result);
+ }
+
+
+ [Fact]
+ public void ManifestResource_CanReadAllBytes()
+ {
+ var assembly = typeof(EnumerableExtensionsTests).Assembly;
+ var resources = assembly.GetManifestResources();
+ var resource = resources.First();
+
+ var result = resource.ReadAllBytes();
+ Assert.NotNull(result);
+ }
+
+
+ [Fact]
+ public async Task ManifestResource_CanReadAllBytesAsync()
+ {
+ var assembly = typeof(EnumerableExtensionsTests).Assembly;
+ var resources = assembly.GetManifestResources();
+ var resource = resources.First();
+
+ var result = await resource.ReadAllBytesAsync();
+ Assert.NotNull(result);
+ }
+}
\ No newline at end of file
diff --git a/tests/ES.FX.Tests/_EmbeddedResources/TestEmbeddedResource.txt b/tests/ES.FX.Tests/_EmbeddedResources/TestEmbeddedResource.txt
new file mode 100644
index 0000000..8c75035
--- /dev/null
+++ b/tests/ES.FX.Tests/_EmbeddedResources/TestEmbeddedResource.txt
@@ -0,0 +1 @@
+TestContentDoNotModify
\ No newline at end of file