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