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/.github/actions/build/action.yaml b/.github/actions/build/action.yaml
new file mode 100644
index 0000000..2aaee52
--- /dev/null
+++ b/.github/actions/build/action.yaml
@@ -0,0 +1,57 @@
+name: Build
+description: 'Build steps'
+inputs:
+ useVersioning:
+ description: 'Apply versioning to the build'
+ required: true
+ default: 'false'
+ configuration:
+ description: 'Build configuration'
+ required: true
+ default: 'Debug'
+runs:
+ using: 'composite'
+ steps:
+
+
+ - name: tools - dotnet - install
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: '8.x'
+
+ - name: tools - gitversion - install
+ if: ${{ inputs.useVersioning == 'true' }}
+ uses: gittools/actions/gitversion/setup@v1.1.1
+ with:
+ versionSpec: '5.x'
+ preferLatestVersion: true
+
+ - name: tools - gitversion - execute
+ if: ${{ inputs.useVersioning == 'true' }}
+ uses: gittools/actions/gitversion/execute@v1.1.1
+ # with:
+ # useConfigFile: true
+ # configFilePath: GitVersion.yaml
+
+ - name: cache - nuget
+ uses: actions/cache@v4
+ with:
+ path: ~/.nuget/packages
+ key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj') }}
+ restore-keys: |
+ ${{ runner.os }}-nuget-
+
+ - name: dotnet restore
+ shell: bash
+ run: dotnet restore
+
+
+ - name: dotnet build
+ if: ${{ inputs.useVersioning != 'true' }}
+ shell: bash
+ run: dotnet build --no-restore --configuration ${{ inputs.configuration }}
+
+ - name: dotnet build
+ if: ${{ inputs.useVersioning == 'true' }}
+ shell: bash
+ run: dotnet build --no-restore --configuration ${{ inputs.configuration }} /p:Version=${{env.GitVersion_SemVer}} /p:AssemblyVersion=${{env.GitVersion_AssemblySemFileVer}} /p:NuGetVersion=${{env.GitVersion_SemVer}}
\ No newline at end of file
diff --git a/.github/actions/test/action.yaml b/.github/actions/test/action.yaml
new file mode 100644
index 0000000..36d2541
--- /dev/null
+++ b/.github/actions/test/action.yaml
@@ -0,0 +1,16 @@
+name: Test
+description: 'Test steps'
+runs:
+ using: 'composite'
+ steps:
+ - name: dotnet test
+ shell: bash
+ run: dotnet test --no-build --verbosity normal
+
+ - name: test-reporter
+ uses: dorny/test-reporter@v1
+ if: always()
+ with:
+ name: Test Results
+ path: .artifacts/TestResults/*.trx
+ reporter: dotnet-trx
\ No newline at end of file
diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml
new file mode 100644
index 0000000..4960b60
--- /dev/null
+++ b/.github/workflows/main.yaml
@@ -0,0 +1,74 @@
+name: Main
+
+on:
+ push:
+ branches:
+ - '**' # Matches all branches
+
+jobs:
+ ci:
+ name: CI
+ runs-on: ubuntu-latest
+ steps:
+
+ - name: checkout
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 1
+
+ - name: build
+ uses: ./.github/actions/build
+ with:
+ configuration: Debug
+ useVersioning: false
+
+ - name: test
+ uses: ./.github/actions/test
+
+ cd:
+ name: CD
+ needs: ci
+ runs-on: ubuntu-latest
+ if: >
+ github.ref == 'refs/heads/main' ||
+ github.ref == 'refs/heads/develop' ||
+ startsWith(github.ref, 'refs/heads/feature/') ||
+ startsWith(github.ref, 'refs/heads/release/') ||
+ startsWith(github.ref, 'refs/heads/hotfix/')
+
+ steps:
+ - name: checkout
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: build
+ uses: ./.github/actions/build
+ with:
+ configuration: Release
+ useVersioning: true
+
+ - name: artifacts - nuget - gather
+ run: |
+ mkdir -p .artifacts/nuget
+ find . -name "*.nupkg" -exec cp {} .artifacts/nuget/ \;
+
+ - name: artifacts - nuget - upload
+ uses: actions/upload-artifact@v4
+ with:
+ name: artifacts-nuget
+ path: .artifacts/nuget/*.nupkg
+
+ - name: git - tag
+ if: >
+ github.ref == 'refs/heads/main' ||
+ startsWith(github.ref, 'refs/heads/release/') ||
+ startsWith(github.ref, 'refs/heads/hotfix/')
+ run: |
+ git config --global user.name 'github-actions'
+ git config --global user.email 'github-actions@github.com'
+ git tag ${{env.GitVersion_SemVer}}
+ git push origin ${{env.GitVersion_SemVer}}
+
+
+
diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml
new file mode 100644
index 0000000..0f0c116
--- /dev/null
+++ b/.github/workflows/pr.yaml
@@ -0,0 +1,24 @@
+name: Pull Request
+
+on:
+ pull_request:
+
+jobs:
+ ci:
+ name: CI - Build and Test
+ runs-on: ubuntu-latest
+ steps:
+
+ - name: checkout
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 1
+
+ - name: build
+ uses: ./.github/actions/build
+ with:
+ configuration: Debug
+ useVersioning: false
+
+ - name: test
+ uses: ./.github/actions/test
\ No newline at end of file
diff --git a/Directory.Build.props b/Directory.Build.props
new file mode 100644
index 0000000..54c3ce5
--- /dev/null
+++ b/Directory.Build.props
@@ -0,0 +1,43 @@
+
+
+ true
+ enable
+ enable
+
+ embedded
+ true
+
+ $(MSBuildThisFileDirectory)..\
+
+
+ $(SolutionDir).artifacts/nuget
+ emberstack
+ EmberStack
+ https://github.com/emberstack/ES.FX
+ MIT
+
+
+
+
+ $(NoWarn);NU5104
+
+
+
+
+ false
+
+
+
+ true
+
+
+
+
+
+
+
+ trx%3bLogFileName=$(MSBuildProjectName).trx
+ $(MSBuildThisFileDirectory)/.artifacts/TestResults
+
+
+
diff --git a/ES.FX.sln b/ES.FX.sln
new file mode 100644
index 0000000..b8298fe
--- /dev/null
+++ b/ES.FX.sln
@@ -0,0 +1,194 @@
+
+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("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ES.FX.Ignite.Spark.Tests", "tests\ES.FX.Ignite.Spark.Tests\ES.FX.Ignite.Spark.Tests.csproj", "{E0F4DFC2-46E3-4EF6-AA9D-71F0B73911D3}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ES.FX.Shared.SqlServer.Tests", "tests\ES.FX.Shared.SqlServer.Tests\ES.FX.Shared.SqlServer.Tests.csproj", "{938AE7EF-E227-4EED-912D-43A9AEF614B5}"
+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
+ 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}
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {388473D0-FE20-4821-BFE4-C3CD3E184C8F}
+ EndGlobalSection
+EndGlobal
diff --git a/GitVersion.yaml b/GitVersion.yaml
new file mode 100644
index 0000000..6bb0929
--- /dev/null
+++ b/GitVersion.yaml
@@ -0,0 +1,35 @@
+mode: ContinuousDeployment
+branches:
+ main:
+ tag: ''
+ increment: Patch
+ regex: ^main$
+ source-branches: ['develop', 'release']
+ is-mainline: true
+ develop:
+ tag: 'dev'
+ increment: Minor
+ regex: ^develop$
+ source-branches: ['main']
+ is-mainline: false
+ feature:
+ tag: 'feature'
+ increment: Inherit
+ regex: ^feature[/-]
+ source-branches: ['develop']
+ is-mainline: false
+ release:
+ tag: 'rc'
+ increment: Patch
+ regex: ^release[/-]
+ source-branches: ['develop']
+ is-mainline: false
+ hotfix:
+ tag: 'hotfix'
+ increment: Patch
+ regex: ^hotfix[/-]
+ source-branches: ['main', 'develop']
+ is-mainline: false
+ignore:
+ sha: []
+merge-message-formats: {}
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..6bf02cf
--- /dev/null
+++ b/playground/Playground.Microservice.Api.Host/HostedServices/TestHostedService.cs
@@ -0,0 +1,36 @@
+#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..3767283
--- /dev/null
+++ b/playground/Playground.Microservice.Api.Host/Playground.Microservice.Api.Host.csproj
@@ -0,0 +1,28 @@
+
+
+
+ net8.0
+ enable
+ enable
+ Linux
+ ..\..
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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..8edf39a
--- /dev/null
+++ b/playground/Playground.Microservice.Api.Host/Program.cs
@@ -0,0 +1,40 @@
+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 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();
+
+ var app = builder.Build();
+ app.UseIgnite();
+
+
+
+
+ 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..c4263c4
--- /dev/null
+++ b/playground/Playground.Shared.Data.Simple.EntityFrameworkCore.SqlServer/DummyDbContextDesignTimeFactory.cs
@@ -0,0 +1,30 @@
+using JetBrains.Annotations;
+using Microsoft.Data.SqlClient;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Design;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+
+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..d64fba2
--- /dev/null
+++ b/playground/Playground.Shared.Data.Simple.EntityFrameworkCore/SimpleDbContext.cs
@@ -0,0 +1,20 @@
+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..d32c6df
--- /dev/null
+++ b/playground/Playground.Shared.Data.Simple.EntityFrameworkCore/SimpleReadOnlyDbContext.cs
@@ -0,0 +1,7 @@
+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..602d479
--- /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() { 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..1037aa4
--- /dev/null
+++ b/src/ES.FX.Hosting/Lifetime/ProgramEntryBuilder.cs
@@ -0,0 +1,54 @@
+using Microsoft.Extensions.Logging;
+
+namespace ES.FX.Hosting.Lifetime;
+
+
+///
+/// Builder for creating a instance
+///
+public class ProgramEntryBuilder
+{
+ private ILogger _logger;
+ private readonly List> _exitActions = [];
+ private readonly ProgramEntryOptions _options;
+
+ 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()
+ {
+ return new ProgramEntry(_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..ccd2feb
--- /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..33a7795
--- /dev/null
+++ b/src/ES.FX.Ignite.Hosting/IgniteHostingExtensions.cs
@@ -0,0 +1,31 @@
+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)
+ {
+ return 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",
+ optional: true, reloadOnChange: true);
+ }
+
+}
\ 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..0c2a956
--- /dev/null
+++ b/src/ES.FX.Ignite.Microsoft.EntityFrameworkCore.SqlServer/Configuration/SqlServerDbContextSparkOptions.cs
@@ -0,0 +1,25 @@
+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..c277fd8
--- /dev/null
+++ b/src/ES.FX.Ignite.Microsoft.EntityFrameworkCore.SqlServer/Configuration/SqlServerDbContextSparkSettings.cs
@@ -0,0 +1,20 @@
+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..8f15ae1
--- /dev/null
+++ b/src/ES.FX.Ignite.Microsoft.EntityFrameworkCore.SqlServer/Hosting/SqlServerDbContextHostingExtensions.cs
@@ -0,0 +1,176 @@
+using EntityFramework.Exceptions.SqlServer;
+using ES.FX.Ignite.Microsoft.EntityFrameworkCore.SqlServer.Configuration;
+using ES.FX.Ignite.Spark.Configuration;
+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 ES.FX.Ignite.Microsoft.EntityFrameworkCore.Spark;
+using ES.FX.Ignite.Spark.HealthChecks;
+using OpenTelemetry.Trace;
+
+namespace ES.FX.Ignite.Microsoft.EntityFrameworkCore.SqlServer.Hosting;
+
+[PublicAPI]
+public static class SqlServerDbContextHostingExtensions
+{
+ ///
+ /// Registers the given 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 key. Default is .
+ public static void AddSqlServerDbContext(this IHostApplicationBuilder builder,
+ string? key = null,
+ Action>? configureSettings = null,
+ Action>? configureOptions = null,
+ Action? configureDbContextOptionsBuilder = null,
+ Action? configureSqlServerDbContextOptionsBuilder = null,
+ ServiceLifetime lifetime = ServiceLifetime.Transient,
+ string configurationSectionKey = DbContextSpark.ConfigurationSectionKey) where TDbContext : DbContext
+ => builder.RegisterDbContext(key, configureSettings, configureOptions,
+ configureDbContextOptionsBuilder, configureSqlServerDbContextOptionsBuilder, lifetime,
+ configurationSectionKey);
+
+ ///
+ /// 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 key. 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? key = null,
+ Action>? configureSettings = null,
+ Action>? configureOptions = null,
+ Action? configureDbContextOptionsBuilder = null,
+ Action? configureSqlServerDbContextOptionsBuilder = null,
+ ServiceLifetime lifetime = ServiceLifetime.Transient,
+ string configurationSectionKey = DbContextSpark.ConfigurationSectionKey) where TDbContext : DbContext
+ => builder.RegisterDbContext(key, configureSettings, configureOptions,
+ configureDbContextOptionsBuilder, configureSqlServerDbContextOptionsBuilder, lifetime,
+ configurationSectionKey, useDbContextFactory: true);
+
+
+
+ private static void RegisterDbContext(this IHostApplicationBuilder builder,
+ string? key = null,
+ Action>? configureSettings = null,
+ Action>? configureOptions = null,
+ Action? configureDbContextOptionsBuilder = null,
+ Action? configureSqlServerDbContextOptionsBuilder = null,
+ ServiceLifetime lifetime = ServiceLifetime.Transient,
+ string configurationSectionKey = DbContextSpark.ConfigurationSectionKey,
+ bool useDbContextFactory = false) where TDbContext : DbContext
+ {
+ key = SparkConfig.Key(key, typeof(TDbContext).Name);
+ var configurationKey = SparkConfig.ConfigurationKey(key, configurationSectionKey);
+
+ ConfigureOptions(builder, configurationKey, configureOptions);
+
+ var settings = SparkConfig.GetSettings(builder.Configuration, configurationKey, configureSettings);
+ builder.Services.AddSingleton(settings);
+
+ if (useDbContextFactory)
+ {
+ builder.Services.AddDbContextFactory(ConfigureBuilder, lifetime);
+ }
+ else
+ {
+ builder.Services.AddDbContext(ConfigureBuilder, lifetime);
+ }
+
+ ConfigureInstrumentation(builder, key, settings);
+
+
+ return;
+
+ void ConfigureBuilder(IServiceProvider sp, DbContextOptionsBuilder dbContextOptionsBuilder) =>
+ ConfigureDbContextOptionsBuilder(sp, dbContextOptionsBuilder,
+ configureDbContextOptionsBuilder,
+ configureSqlServerDbContextOptionsBuilder);
+ }
+
+
+ private static void ConfigureOptions(
+ IHostApplicationBuilder builder,
+ string configurationKey,
+ Action>? configureOptions = null) where T : DbContext
+ {
+ var optionsBuilder = builder.Services
+ .AddOptions>()
+ .BindConfiguration(configurationKey);
+
+ if (configureOptions is not null)
+ {
+ optionsBuilder.Configure(configureOptions);
+ }
+ }
+
+ private static void ConfigureDbContextOptionsBuilder(IServiceProvider serviceProvider,
+ DbContextOptionsBuilder dbContextOptionsBuilder,
+ Action? configureDbContextOptionsBuilder,
+ Action? configureSqlServerDbContextOptionsBuilder) where T : 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 key,
+ SqlServerDbContextSparkSettings settings) where TContext : DbContext
+ {
+ if (!settings.DisableTracing)
+ {
+ builder.Services.AddOpenTelemetry().WithTracing(tracerProviderBuilder =>
+ tracerProviderBuilder.AddSqlClientInstrumentation());
+ }
+
+ if (!settings.DisableHealthChecks)
+ {
+ builder.TryAddHealthCheck(
+ name: $"{DbContextSpark.Name}.{key}",
+ 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..bef2fdb
--- /dev/null
+++ b/src/ES.FX.Ignite.Microsoft.EntityFrameworkCore/Migrations/RelationalDbContextMigrationsTask.cs
@@ -0,0 +1,43 @@
+using System.Diagnostics;
+using ES.FX.Migrations.Abstractions;
+using JetBrains.Annotations;
+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..4cc5372
--- /dev/null
+++ b/src/ES.FX.Ignite.Microsoft.EntityFrameworkCore/Migrations/RelationalDbContextMigrationsTaskExtensions.cs
@@ -0,0 +1,22 @@
+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..7f4b64e
--- /dev/null
+++ b/src/ES.FX.Ignite.Microsoft.EntityFrameworkCore/Spark/DbContextSpark.cs
@@ -0,0 +1,20 @@
+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 key.
+ ///
+ public const string ConfigurationSectionKey = $"{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..af12110
--- /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..7686bbb
--- /dev/null
+++ b/src/ES.FX.Ignite.Migrations/Hosting/MigrationsServiceHostingExtensions.cs
@@ -0,0 +1,29 @@
+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 key. Default is .
+ public static void AddMigrationsService(this IHostApplicationBuilder builder,
+ Action? configureSettings = null,
+ string configurationSectionKey = MigrationsServiceSpark.ConfigurationSectionKey)
+ {
+ var settings = SparkConfig.GetSettings(builder.Configuration, configurationSectionKey, 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..2f2924c
--- /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..0cc7030
--- /dev/null
+++ b/src/ES.FX.Ignite.Migrations/Spark/MigrationsServiceSpark.cs
@@ -0,0 +1,20 @@
+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 key.
+ ///
+ public const string ConfigurationSectionKey = $"{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..12dd92c
--- /dev/null
+++ b/src/ES.FX.Ignite.Serilog/Hosting/SerilogHostingExtensions.cs
@@ -0,0 +1,49 @@
+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..6a4e82c
--- /dev/null
+++ b/src/ES.FX.Ignite.Serilog/Hosting/SerilogRequestLoggingHostingExtensions.cs
@@ -0,0 +1,38 @@
+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..0116316
--- /dev/null
+++ b/src/ES.FX.Ignite.Spark/Configuration/IgniteConfigurationSections.cs
@@ -0,0 +1,9 @@
+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..8126edd
--- /dev/null
+++ b/src/ES.FX.Ignite.Spark/Configuration/SparkConfig.cs
@@ -0,0 +1,71 @@
+using Microsoft.Extensions.Configuration;
+
+namespace ES.FX.Ignite.Spark.Configuration
+{
+ public static class SparkConfig
+ {
+
+ ///
+ /// Default configuration key for settings
+ ///
+ public const string SettingsKey = "Settings";
+
+
+ ///
+ /// Gets the key or the default key if the key is null or empty.
+ ///
+ /// The spark key
+ /// The default spark key
+ ///
+ public static string Key(string? key, string defaultKey)
+ {
+ key = key?.Trim();
+ defaultKey = defaultKey.Trim();
+
+ return key ?? defaultKey;
+ }
+
+ public static string ConfigurationKey(string key, string section)
+ {
+ key = key.Trim();
+ section = section.Trim();
+ var configurationKey = section == string.Empty ? key : $"{section}:{key}";
+
+ return configurationKey;
+ }
+
+ ///
+ /// 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 sectionKey,
+ Action? configureSettings = null) where T : new()
+ => GetSettings(new T(), configuration, sectionKey, 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 sectionKey,
+ Action? configureSettings = null)
+ {
+ configuration.GetSection($"{sectionKey}:{SettingsKey}").Bind(settings);
+ configureSettings?.Invoke(settings);
+
+ return settings;
+ }
+
+
+ }
+}
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..311d340
--- /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..2ce8d41
--- /dev/null
+++ b/src/ES.FX.Ignite.Spark/HealthChecks/HealthChecksExtensions.cs
@@ -0,0 +1,30 @@
+using JetBrains.Annotations;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Diagnostics.HealthChecks;
+
+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/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..6e3c4e9
--- /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/SqlConnectionStringBuilderExtensions.cs b/src/ES.FX.Microsoft.Data.SqlClient/SqlConnectionStringBuilderExtensions.cs
new file mode 100644
index 0000000..e759690
--- /dev/null
+++ b/src/ES.FX.Microsoft.Data.SqlClient/SqlConnectionStringBuilderExtensions.cs
@@ -0,0 +1,39 @@
+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)
+ {
+ return 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)
+ {
+ var clone = new SqlConnectionStringBuilder(builder.ConnectionString);
+ return clone.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..7bac074
--- /dev/null
+++ b/src/ES.FX.Microsoft.EntityFrameworkCore/Factories/DelegateDbContextFactory.cs
@@ -0,0 +1,18 @@
+using JetBrains.Annotations;
+using Microsoft.EntityFrameworkCore;
+
+namespace ES.FX.Microsoft.EntityFrameworkCore.Factories;
+
+///
+/// Defines a factory for creating instances of using a delegate.
+///
+/// DbContext type
+/// Service provider used by the factory
+/// Factory function used to create the DbContext
+[PublicAPI]
+public class DelegateDbContextFactory(IServiceProvider serviceProvider, Func factory)
+ : IDbContextFactory
+ where T : DbContext
+{
+ public T 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..cb19896
--- /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..cbc58c5
--- /dev/null
+++ b/src/ES.FX.Serilog/Enrichers/CachedPropertyEnricher.cs
@@ -0,0 +1,22 @@
+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(this.GetLogEventProperty(propertyFactory));
+ }
+
+ private LogEventProperty GetLogEventProperty(ILogEventPropertyFactory propertyFactory)
+ {
+ return _cachedProperty ??= CreateProperty(propertyFactory);
+ }
+
+ protected abstract LogEventProperty CreateProperty(ILogEventPropertyFactory propertyFactory);
+ }
+}
diff --git a/src/ES.FX.Serilog/Enrichers/EntryAssemblyNameEnricher.cs b/src/ES.FX.Serilog/Enrichers/EntryAssemblyNameEnricher.cs
new file mode 100644
index 0000000..e3262c1
--- /dev/null
+++ b/src/ES.FX.Serilog/Enrichers/EntryAssemblyNameEnricher.cs
@@ -0,0 +1,20 @@
+
+
+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)
+ {
+ return 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..faff4a8
--- /dev/null
+++ b/src/ES.FX.Serilog/Lifetime/ProgramEntrySerilogExtensions.cs
@@ -0,0 +1,59 @@
+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..a27fff6
--- /dev/null
+++ b/src/ES.FX.Serilog/Sinks/Console/ConsoleOutputTemplates.cs
@@ -0,0 +1,6 @@
+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..b3bbd86
--- /dev/null
+++ b/src/ES.FX/Collections/ArrayExtensions.cs
@@ -0,0 +1,15 @@
+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)
+ {
+ return 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..61c0b89
--- /dev/null
+++ b/src/ES.FX/Exceptions/ExceptionExtensions.cs
@@ -0,0 +1,42 @@
+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..0093c44
--- /dev/null
+++ b/src/ES.FX/IO/StreamExtensions.cs
@@ -0,0 +1,37 @@
+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..3236d69
--- /dev/null
+++ b/src/ES.FX/Reflection/ManifestResource.cs
@@ -0,0 +1,89 @@
+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()
+ {
+ return 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.EntityFrameworkCore.SqlServer.Tests/Context/TestDbContextDesignTimeFactory.cs b/tests/ES.FX.Ignite.Microsoft.EntityFrameworkCore.SqlServer.Tests/Context/TestDbContextDesignTimeFactory.cs
new file mode 100644
index 0000000..6251293
--- /dev/null
+++ b/tests/ES.FX.Ignite.Microsoft.EntityFrameworkCore.SqlServer.Tests/Context/TestDbContextDesignTimeFactory.cs
@@ -0,0 +1,30 @@
+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..749a740
--- /dev/null
+++ b/tests/ES.FX.Ignite.Microsoft.EntityFrameworkCore.SqlServer.Tests/SqlServerDbContextConnectTests.cs
@@ -0,0 +1,38 @@
+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..bea7f6f
--- /dev/null
+++ b/tests/ES.FX.Ignite.Microsoft.EntityFrameworkCore.SqlServer.Tests/SqlServerDbContextHostingExtensionsTests.cs
@@ -0,0 +1,240 @@
+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.NotEqual(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.Equal(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.Equal(factory1, factory2);
+
+ // Factory created DbContexts should not be scoped if the factory is scoped
+ var createdDbContext1 = factory1.CreateDbContext();
+ var createdDbContext2 = factory1.CreateDbContext();
+ Assert.NotEqual(createdDbContext1, createdDbContext2);
+
+ // Resolved DbContexts should be the same within the same scope
+ var resolvedDbContext1 = app.Services.GetRequiredService();
+ var resolvedDbContext2 = app.Services.GetRequiredService();
+ Assert.Equal(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.Equal(scopedFactory1, scopedFactory2);
+ Assert.NotEqual(scopedFactory1, factory1);
+
+ // Scope resolved DbContexts should be the same within the same scope
+ var scopedDbContext1 = scope.ServiceProvider.GetRequiredService();
+ var scopedDbContext2 = scope.ServiceProvider.GetRequiredService();
+ Assert.Equal(scopedDbContext1, scopedDbContext2);
+ Assert.NotEqual(scopedDbContext1, resolvedDbContext1);
+ }
+
+
+
+ [Fact]
+ public void AddSqlServerDbContext_CanChangeSettingsInCode()
+ {
+ var builder = Host.CreateEmptyApplicationBuilder(null);
+
+ //Configure settings
+ builder.Configuration.AddInMemoryCollection([
+ new(
+ $"{DbContextSpark.ConfigurationSectionKey}:{nameof(TestDbContext)}:{SparkConfig.SettingsKey}:{nameof(SqlServerDbContextSparkSettings.DisableTracing)}",
+ true.ToString()),
+ new(
+ $"{DbContextSpark.ConfigurationSectionKey}:{nameof(TestDbContext)}:{SparkConfig.SettingsKey}:{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(
+ $"{DbContextSpark.ConfigurationSectionKey}:{nameof(TestDbContext)}:{nameof(SqlServerDbContextSparkOptions.ConnectionString)}",
+ initialConnectionString),
+ new(
+ $"{DbContextSpark.ConfigurationSectionKey}:{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..239833e
--- /dev/null
+++ b/tests/ES.FX.Ignite.Microsoft.EntityFrameworkCore.Tests/Context/TestDbContext.cs
@@ -0,0 +1,23 @@
+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..c7201c2
--- /dev/null
+++ b/tests/ES.FX.Ignite.Microsoft.EntityFrameworkCore.Tests/ES.FX.Ignite.Microsoft.EntityFrameworkCore.Tests.csproj
@@ -0,0 +1,30 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+ false
+ true
+
+
+
+
+ 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..0832209
--- /dev/null
+++ b/tests/ES.FX.Ignite.Microsoft.EntityFrameworkCore.Tests/UnitTest1.cs
@@ -0,0 +1,11 @@
+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..62fd6fc
--- /dev/null
+++ b/tests/ES.FX.Ignite.Spark.Tests/Configuration/SparkConfigTests.cs
@@ -0,0 +1,46 @@
+using ES.FX.Ignite.Spark.Configuration;
+
+namespace ES.FX.Ignite.Spark.Tests.Configuration
+{
+ public class SparkConfigTests
+ {
+ [Fact]
+ public void SparkConfig_Key_UsesDefaultIfNull()
+ {
+ const string defaultKey = "default";
+ var key = SparkConfig.Key(null, defaultKey);
+
+ Assert.Equal(defaultKey, key);
+ }
+
+ [Fact]
+ public void SparkConfig_Key_UsesKeyIfNotNull()
+ {
+ const string defaultKey = "default";
+ const string properKey = "key";
+ var key = SparkConfig.Key(properKey, defaultKey);
+
+ Assert.Equal(properKey, key);
+ }
+
+
+ [Fact]
+ public void SparkConfig_ConfigurationKey_UsesKeyIfSectionIsNull()
+ {
+ const string key = "key";
+ var configurationKey = SparkConfig.ConfigurationKey(key, string.Empty);
+
+ Assert.Equal(key, configurationKey);
+ }
+
+ [Fact]
+ public void SparkConfig_ConfigurationKey_UsesKeyAndSection()
+ {
+ const string section = "section";
+ const string key = "key";
+ var configurationKey = SparkConfig.ConfigurationKey(key, section);
+
+ Assert.Equal($"{section}:{key}", configurationKey);
+ }
+ }
+}
\ 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..6ebcc65
--- /dev/null
+++ b/tests/ES.FX.Ignite.Spark.Tests/ES.FX.Ignite.Spark.Tests.csproj
@@ -0,0 +1,26 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+ false
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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..8ee1e02
--- /dev/null
+++ b/tests/ES.FX.Shared.SqlServer.Tests/ES.FX.Shared.SqlServer.Tests.csproj
@@ -0,0 +1,25 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+ false
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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..ff69008
--- /dev/null
+++ b/tests/ES.FX.Shared.SqlServer.Tests/Fixtures/SqlServerContainerFixture.cs
@@ -0,0 +1,31 @@
+using Testcontainers.MsSql;
+
+namespace ES.FX.Shared.SqlServer.Tests.Fixtures;
+
+public sealed class SqlServerContainerFixture : IAsyncLifetime
+{
+ public MsSqlContainer? Container { get; private set; }
+ public const string Registry = "mcr.microsoft.com";
+ public const string Image = "mssql/server";
+ public const string Tag = "2022-latest";
+
+ public string GetConnectionString() => Container?.GetConnectionString() ??
+ throw new InvalidOperationException("The test container was not initialized.");
+
+ 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();
+ }
+ }
+}
\ 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..ba77974
--- /dev/null
+++ b/tests/ES.FX.Shared.SqlServer.Tests/SqlServerFixtureTests.cs
@@ -0,0 +1,21 @@
+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 client = new SqlConnection(connectionString);
+ client.Open();
+ var command = client.CreateCommand();
+ command.CommandText = "SELECT 1";
+ var result = command.ExecuteScalar();
+ Assert.Equal(1, result);
+ client.Close();
+ }
+ }
+}
\ 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..20d7e1b
--- /dev/null
+++ b/tests/ES.FX.Tests/Collections/ExceptionExtensionsTests.cs
@@ -0,0 +1,31 @@
+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..30a8a2a
--- /dev/null
+++ b/tests/ES.FX.Tests/Exceptions/ExceptionExtensionsTests.cs
@@ -0,0 +1,55 @@
+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..89acc13
--- /dev/null
+++ b/tests/ES.FX.Tests/IO/ManifestResourceTests.cs
@@ -0,0 +1,57 @@
+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..eab8c14
--- /dev/null
+++ b/tests/ES.FX.Tests/Linq/EnumerableExtensionsTests.cs
@@ -0,0 +1,28 @@
+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..3a0a92d
--- /dev/null
+++ b/tests/ES.FX.Tests/Reflection/EnumerableExtensionsTests.cs
@@ -0,0 +1,160 @@
+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