diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json
new file mode 100644
index 0000000000..0b395bfd86
--- /dev/null
+++ b/.config/dotnet-tools.json
@@ -0,0 +1,20 @@
+{
+ "version": 1,
+ "isRoot": true,
+ "tools": {
+ "jetbrains.resharper.globaltools": {
+ "version": "2025.1.3",
+ "commands": [
+ "jb"
+ ],
+ "rollForward": false
+ },
+ "regitlint": {
+ "version": "6.3.13",
+ "commands": [
+ "regitlint"
+ ],
+ "rollForward": false
+ }
+ }
+}
\ No newline at end of file
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000000..adf8ab64ba
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,164 @@
+# EditorConfig is awesome: http://EditorConfig.org
+
+# top-most EditorConfig file
+root = true
+
+[*]
+indent_style = space
+indent_size = 4
+tab-width = 4
+charset = utf-8
+trim_trailing_whitespace = true
+insert_final_newline = true
+
+[*.{build,config,csproj,js,json,proj,props,targets,xml,ruleset,xsd,yml,yaml}]
+indent_size = 2
+tab-width = 2
+max_line_length = 160
+
+[*.{cs,cshtml,ascx,aspx}]
+
+#### C#/.NET Code Style ####
+
+# Default severity for IDE* analyzers with category 'Style'
+# Note: specific rules below use severity silent, because Resharper code cleanup auto-fixes them.
+dotnet_analyzer_diagnostic.category-Style.severity = warning
+
+# 'using' directive preferences
+dotnet_sort_system_directives_first = true
+csharp_using_directive_placement = outside_namespace:silent
+# IDE0005: Remove unnecessary import
+dotnet_diagnostic.IDE0005.severity = silent
+
+# Namespace declarations
+csharp_style_namespace_declarations = file_scoped:silent
+# IDE0160: Use block-scoped namespace
+dotnet_diagnostic.IDE0160.severity = silent
+# IDE0161: Use file-scoped namespace
+dotnet_diagnostic.IDE0161.severity = silent
+
+# this. preferences
+dotnet_style_qualification_for_field = false:silent
+dotnet_style_qualification_for_property = false:silent
+dotnet_style_qualification_for_method = false:silent
+dotnet_style_qualification_for_event = false:silent
+# IDE0003: Remove this or Me qualification
+dotnet_diagnostic.IDE0003.severity = silent
+# IDE0009: Add this or Me qualification
+dotnet_diagnostic.IDE0009.severity = silent
+
+# Language keywords vs BCL types preferences
+dotnet_style_predefined_type_for_locals_parameters_members = true:silent
+dotnet_style_predefined_type_for_member_access = true:silent
+# IDE0049: Use language keywords instead of framework type names for type references
+dotnet_diagnostic.IDE0049.severity = silent
+
+# Modifier preferences
+dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent
+# IDE0040: Add accessibility modifiers
+dotnet_diagnostic.IDE0040.severity = silent
+csharp_preferred_modifier_order = public, private, protected, internal, new, static, abstract, virtual, sealed, readonly, override, extern, unsafe, volatile, async:silent
+# IDE0036: Order modifiers
+dotnet_diagnostic.IDE0036.severity = silent
+
+# Expression-level preferences
+dotnet_style_operator_placement_when_wrapping = end_of_line
+dotnet_style_prefer_auto_properties = true:silent
+# IDE0032: Use auto property
+dotnet_diagnostic.IDE0032.severity = silent
+dotnet_style_prefer_conditional_expression_over_assignment = true:silent
+# IDE0045: Use conditional expression for assignment
+dotnet_diagnostic.IDE0045.severity = silent
+dotnet_style_prefer_conditional_expression_over_return = true:silent
+# IDE0046: Use conditional expression for return
+dotnet_diagnostic.IDE0046.severity = silent
+csharp_style_unused_value_expression_statement_preference = discard_variable:silent
+# IDE0058: Remove unused expression value
+dotnet_diagnostic.IDE0058.severity = silent
+
+# Collection expression preferences (note: turned off in shared-package.props)
+dotnet_style_prefer_collection_expression = when_types_exactly_match
+dotnet_diagnostic.IDE0306.severity = silent # Workaround for https://github.com/dotnet/roslyn/issues/77177
+
+# Parameter preferences
+dotnet_code_quality_unused_parameters = non_public
+
+# Local functions vs lambdas
+csharp_style_prefer_local_over_anonymous_function = false:silent
+# IDE0039: Use local function instead of lambda
+dotnet_diagnostic.IDE0039.severity = silent
+
+# Expression-bodied members
+csharp_style_expression_bodied_accessors = true:silent
+# IDE0027: Use expression body for accessors
+dotnet_diagnostic.IDE0027.severity = silent
+csharp_style_expression_bodied_constructors = false:silent
+# IDE0021: Use expression body for constructors
+dotnet_diagnostic.IDE0021.severity = silent
+csharp_style_expression_bodied_indexers = true:silent
+# IDE0026: Use expression body for indexers
+dotnet_diagnostic.IDE0026.severity = silent
+csharp_style_expression_bodied_lambdas = true:silent
+# IDE0053: Use expression body for lambdas
+dotnet_diagnostic.IDE0053.severity = silent
+csharp_style_expression_bodied_local_functions = false:silent
+# IDE0061: Use expression body for local functions
+dotnet_diagnostic.IDE0061.severity = silent
+csharp_style_expression_bodied_methods = false:silent
+# IDE0022: Use expression body for methods
+dotnet_diagnostic.IDE0022.severity = silent
+csharp_style_expression_bodied_operators = false:silent
+# IDE0023: Use expression body for conversion operators
+dotnet_diagnostic.IDE0023.severity = silent
+# IDE0024: Use expression body for operators
+dotnet_diagnostic.IDE0024.severity = silent
+csharp_style_expression_bodied_properties = true:silent
+# IDE0025: Use expression body for properties
+dotnet_diagnostic.IDE0025.severity = silent
+
+# Code-block preferences
+csharp_prefer_braces = true:silent
+# IDE0011: Add braces
+dotnet_diagnostic.IDE0011.severity = silent
+
+# Indentation preferences
+csharp_indent_case_contents_when_block = false
+
+# Wrapping preferences
+csharp_preserve_single_line_statements = false
+
+# 'var' usage preferences
+csharp_style_var_for_built_in_types = false:silent
+csharp_style_var_when_type_is_apparent = true:silent
+csharp_style_var_elsewhere = false:silent
+# IDE0007: Use var instead of explicit type
+dotnet_diagnostic.IDE0007.severity = silent
+# IDE0008: Use explicit type instead of var
+dotnet_diagnostic.IDE0008.severity = silent
+
+# Parentheses preferences
+dotnet_style_parentheses_in_arithmetic_binary_operators = never_if_unnecessary:silent
+dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent
+dotnet_style_parentheses_in_relational_binary_operators = never_if_unnecessary:silent
+# IDE0047: Remove unnecessary parentheses
+dotnet_diagnostic.IDE0047.severity = silent
+# IDE0048: Add parentheses for clarity
+dotnet_diagnostic.IDE0048.severity = silent
+
+# IDE0010: Add missing cases to switch statement
+dotnet_diagnostic.IDE0010.severity = silent
+# IDE0072: Add missing cases to switch expression
+dotnet_diagnostic.IDE0072.severity = silent
+
+# IDE0029: Null check can be simplified
+dotnet_diagnostic.IDE0029.severity = silent
+# IDE0030: Null check can be simplified
+dotnet_diagnostic.IDE0030.severity = silent
+# IDE0270: Null check can be simplified
+dotnet_diagnostic.IDE0270.severity = silent
+
+# JSON002: Probable JSON string detected
+dotnet_diagnostic.JSON002.severity = silent
+
+# CA1848: Use the LoggerMessage delegates (depends on https://github.com/SteeltoeOSS/Steeltoe/issues/969)
+dotnet_diagnostic.CA1848.severity = silent
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000000..67e9d51adf
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,2 @@
+# Enable running shell scripts from bash on Windows
+*.git.properties eol=lf
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 0000000000..ec1d87a7df
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,32 @@
+---
+name: Bug report
+about: Create a report to help us improve
+title: ''
+labels: Type/bug
+assignees: ''
+
+---
+
+## Describe the bug
+A clear and concise description of the bug.
+
+## Steps to reproduce
+Steps to reproduce the behavior:
+1.
+2.
+
+## Expected behavior
+A clear and concise description of what you expected to happen.
+
+## Environment (please complete the following information):
+ - Steeltoe Version [e.g. 2.3.0]
+ - Platform: [e.g. Cloud Foundry, Azure, etc.)
+ - OS: [e.g. Windows, Linux, Mac OSX]
+ - .NET Version [e.g. .NET Core 2.1.0, .NET Framework 4.7.1, etc.]
+ - Any other library versions to note
+
+## Screenshots
+If applicable, add screenshots to help explain your problem.
+
+## Additional context or links
+Add any other context about the problem here.
diff --git a/.github/ISSUE_TEMPLATE/feature-or-enhancement-request.md b/.github/ISSUE_TEMPLATE/feature-or-enhancement-request.md
new file mode 100644
index 0000000000..ee07587d82
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature-or-enhancement-request.md
@@ -0,0 +1,20 @@
+---
+name: Feature or enhancement request
+about: Suggest an idea for this project
+title: ''
+labels: Type/enhancement
+assignees: ''
+
+---
+
+## Is your feature request related to a problem? Please describe.
+A clear and concise description of what the problem you would like solved.
+
+## Describe the solution you'd like
+A clear and concise description of what you want to happen.
+
+## Describe alternatives you've considered
+A clear and concise description of any alternative solutions or features you've considered.
+
+## Additional context
+Add any other context, code snippets, or examples of the feature here.
diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md
new file mode 100644
index 0000000000..d10452a60b
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/question.md
@@ -0,0 +1,18 @@
+---
+name: Question
+about: Select if you have a question or feedback - Also check out slack.steeltoe.io for assistance.
+title: "[QUESTION] "
+labels: Type/question
+assignees: ''
+
+---
+
+## Question
+_Enter your question or feedback here_
+
+## Environment (please complete the following information):
+ - .NET Version [e.g. .NET Core 3.1.4, .NET Framework 4.7.1, etc.]
+ - Steeltoe Version [e.g. 3.0.1]
+
+## Additional context or links
+Add any other context about the problem here.
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 0000000000..c1e9dd5bd9
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,20 @@
+
+
+
+
+## Description
+
+{A detailed description}
+
+Fixes #{bug number}
+
+## Quality checklist
+
+
+
+- [ ] Your code complies with our [Coding Style](https://github.com/SteeltoeOSS/.github/blob/main/contributing-docs/contributing-code-style.md).
+- [ ] You've updated unit and/or integration tests for your change, where applicable.
+- [ ] You've updated documentation for your change, where applicable.
+ If your change affects other repositories, such as [Documentation](https://github.com/SteeltoeOSS/Documentation), [Samples](https://github.com/SteeltoeOSS/Samples) and/or [MainSite](https://github.com/SteeltoeOSS/MainSite), add linked PRs here.
+- [ ] There's an open issue for the PR that you are making. If you'd like to propose a new feature or change, please open an issue to discuss the change or find an existing issue.
+- [ ] You've added required license files and/or file headers (explaining where the code came from with proper attribution), where code is copied from StackOverflow, a blog, or OSS.
diff --git a/.github/workflows/component-common.yml b/.github/workflows/component-common.yml
new file mode 100644
index 0000000000..80f6ea8e7b
--- /dev/null
+++ b/.github/workflows/component-common.yml
@@ -0,0 +1,32 @@
+name: Steeltoe.Common
+
+on:
+ workflow_dispatch:
+ pull_request:
+ paths:
+ - .editorconfig
+ - stylecop.json
+ - '*.props'
+ - '*.ruleset'
+ - .config/dotnet-tools.json
+ - .github/workflows/component-shared-workflow.yml
+ - .github/workflows/component-common.yml
+ - src/Common/**
+ - src/Steeltoe.Common.slnf
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ linux:
+ uses: ./.github/workflows/component-shared-workflow.yml
+ with:
+ component: Common
+ OS: ubuntu
+
+ windows:
+ uses: ./.github/workflows/component-shared-workflow.yml
+ with:
+ component: Common
+ OS: windows
diff --git a/.github/workflows/component-configuration.yml b/.github/workflows/component-configuration.yml
new file mode 100644
index 0000000000..f0cae5d49c
--- /dev/null
+++ b/.github/workflows/component-configuration.yml
@@ -0,0 +1,27 @@
+name: Steeltoe.Configuration
+
+on:
+ workflow_dispatch:
+ pull_request:
+ paths:
+ - .editorconfig
+ - stylecop.json
+ - '*.props'
+ - '*.ruleset'
+ - .config/dotnet-tools.json
+ - .github/workflows/component-shared-workflow.yml
+ - .github/workflows/component-configuration.yml
+ - src/Configuration/**
+ - src/Steeltoe.Configuration.slnf
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ linux:
+ uses: ./.github/workflows/component-shared-workflow.yml
+ with:
+ component: Configuration
+ OS: ubuntu
+ runConfigServer: true
diff --git a/.github/workflows/component-connectors.yml b/.github/workflows/component-connectors.yml
new file mode 100644
index 0000000000..375c7677f9
--- /dev/null
+++ b/.github/workflows/component-connectors.yml
@@ -0,0 +1,26 @@
+name: Steeltoe.Connectors
+
+on:
+ workflow_dispatch:
+ pull_request:
+ paths:
+ - .editorconfig
+ - stylecop.json
+ - '*.props'
+ - '*.ruleset'
+ - .config/dotnet-tools.json
+ - .github/workflows/component-shared-workflow.yml
+ - .github/workflows/component-connectors.yml
+ - src/Connectors/**
+ - src/Steeltoe.Connectors.slnf
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ linux:
+ uses: ./.github/workflows/component-shared-workflow.yml
+ with:
+ component: Connectors
+ OS: ubuntu
diff --git a/.github/workflows/component-discovery.yml b/.github/workflows/component-discovery.yml
new file mode 100644
index 0000000000..12bf4088a7
--- /dev/null
+++ b/.github/workflows/component-discovery.yml
@@ -0,0 +1,26 @@
+name: Steeltoe.Discovery
+
+on:
+ workflow_dispatch:
+ pull_request:
+ paths:
+ - .editorconfig
+ - stylecop.json
+ - '*.props'
+ - '*.ruleset'
+ - .config/dotnet-tools.json
+ - .github/workflows/component-shared-workflow.yml
+ - .github/workflows/component-discovery.yml
+ - src/Discovery/**
+ - src/Steeltoe.Discovery.slnf
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ linux:
+ uses: ./.github/workflows/component-shared-workflow.yml
+ with:
+ component: Discovery
+ OS: ubuntu
diff --git a/.github/workflows/component-logging.yml b/.github/workflows/component-logging.yml
new file mode 100644
index 0000000000..1a453d26a8
--- /dev/null
+++ b/.github/workflows/component-logging.yml
@@ -0,0 +1,26 @@
+name: Steeltoe.Logging
+
+on:
+ workflow_dispatch:
+ pull_request:
+ paths:
+ - .editorconfig
+ - stylecop.json
+ - '*.props'
+ - '*.ruleset'
+ - .config/dotnet-tools.json
+ - .github/workflows/component-shared-workflow.yml
+ - .github/workflows/component-logging.yml
+ - src/Logging/**
+ - src/Steeltoe.Logging.slnf
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ linux:
+ uses: ./.github/workflows/component-shared-workflow.yml
+ with:
+ component: Logging
+ OS: ubuntu
diff --git a/.github/workflows/component-management.yml b/.github/workflows/component-management.yml
new file mode 100644
index 0000000000..e3a01e34f8
--- /dev/null
+++ b/.github/workflows/component-management.yml
@@ -0,0 +1,39 @@
+name: Steeltoe.Management
+
+on:
+ workflow_dispatch:
+ pull_request:
+ paths:
+ - .editorconfig
+ - stylecop.json
+ - '*.props'
+ - '*.ruleset'
+ - .config/dotnet-tools.json
+ - .github/workflows/component-shared-workflow.yml
+ - .github/workflows/component-management.yml
+ - src/Management/**
+ - src/Steeltoe.Management.slnf
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ linux:
+ uses: ./.github/workflows/component-shared-workflow.yml
+ with:
+ component: Management
+ OS: ubuntu
+
+ macos:
+ uses: ./.github/workflows/component-shared-workflow.yml
+ with:
+ component: Management
+ OS: macos
+ skipFilter: Category!=SkipOnMacOS
+
+ windows:
+ uses: ./.github/workflows/component-shared-workflow.yml
+ with:
+ component: Management
+ OS: windows
diff --git a/.github/workflows/component-security.yml b/.github/workflows/component-security.yml
new file mode 100644
index 0000000000..cfe3a805f5
--- /dev/null
+++ b/.github/workflows/component-security.yml
@@ -0,0 +1,31 @@
+name: Steeltoe.Security
+
+on:
+ workflow_dispatch:
+ pull_request:
+ paths:
+ - .editorconfig
+ - stylecop.json
+ - '*.props'
+ - '*.ruleset'
+ - .config/dotnet-tools.json
+ - .github/workflows/component-shared-workflow.yml
+ - .github/workflows/component-security.yml
+ - src/Security/**
+ - src/Steeltoe.Security.slnf
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ linux:
+ uses: ./.github/workflows/component-shared-workflow.yml
+ with:
+ component: Security
+ OS: ubuntu
+ windows:
+ uses: ./.github/workflows/component-shared-workflow.yml
+ with:
+ component: Security
+ OS: windows
diff --git a/.github/workflows/component-shared-workflow.yml b/.github/workflows/component-shared-workflow.yml
new file mode 100644
index 0000000000..00dec350c0
--- /dev/null
+++ b/.github/workflows/component-shared-workflow.yml
@@ -0,0 +1,114 @@
+name: Component Build
+
+on:
+ workflow_call:
+ inputs:
+ component:
+ required: true
+ type: string
+ OS:
+ required: true
+ type: string
+ skipFilter:
+ required: false
+ type: string
+ runConfigServer:
+ required: false
+ type: boolean
+ default: false
+
+permissions:
+ contents: read
+ pull-requests: write
+
+env:
+ DOTNET_CLI_TELEMETRY_OPTOUT: 1
+ DOTNET_NOLOGO: true
+ DOTNET_GENERATE_ASPNET_CERTIFICATE: ${{ inputs.OS == 'macOS' && 'false' || '' }}
+ SOLUTION_FILE: src/Steeltoe.${{ inputs.component }}.slnf
+ COMMON_TEST_ARGS: >-
+ --no-build --configuration Release --collect "XPlat Code Coverage" --logger trx --results-directory ${{ github.workspace }}
+ --settings coverlet.runsettings --blame-crash --blame-hang-timeout 3m
+ SKIP_FILTER_NO_MEMORY_DUMPS: >-
+ ${{ inputs.skipFilter && format('--filter "{0}&Category!=MemoryDumps"', inputs.skipFilter) || '--filter "Category!=MemoryDumps"' }}
+ SKIP_FILTER_WITH_MEMORY_DUMPS: >-
+ ${{ inputs.skipFilter && format('--filter "{0}&Category=MemoryDumps"', inputs.skipFilter) || '--filter "Category=MemoryDumps"' }}
+
+jobs:
+ build:
+ name: Build and Test
+ timeout-minutes: 15
+ runs-on: ${{ inputs.OS }}-latest
+
+ services:
+ eurekaServer:
+ image: ${{ inputs.runConfigServer && 'steeltoe.azurecr.io/eureka-server' || null }}
+ ports:
+ - 8761:8761
+ configServer:
+ image: ${{ inputs.runConfigServer && 'steeltoe.azurecr.io/config-server' || null }}
+ env:
+ eureka.client.enabled: true
+ eureka.client.serviceUrl.defaultZone: http://eurekaServer:8761/eureka
+ eureka.instance.hostname: localhost
+ eureka.instance.instanceId: localhost:configServer:8888
+ ports:
+ - 8888:8888
+
+ steps:
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: |
+ 8.0.*
+ 9.0.*
+
+ - name: Install Nerdbank.GitVersioning (macOS only)
+ if: ${{ inputs.OS == 'macos' }}
+ run: dotnet tool install --global nbgv
+
+ - name: Git checkout
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Restore packages
+ run: dotnet restore ${{ env.SOLUTION_FILE }} --verbosity minimal
+
+ - name: Set package version
+ run: nbgv cloud
+
+ - name: Build solution
+ run: dotnet build ${{ env.SOLUTION_FILE }} --no-restore --configuration Release --verbosity minimal
+
+ - name: Test (net8.0)
+ run: dotnet test ${{ env.SOLUTION_FILE }} --framework net8.0 ${{ env.SKIP_FILTER_NO_MEMORY_DUMPS }} ${{ env.COMMON_TEST_ARGS }}
+
+ - name: Test (net8.0) (memory dumps)
+ if: ${{ inputs.component == 'Management' }}
+ run: dotnet test ${{ env.SOLUTION_FILE }} --framework net8.0 ${{ env.SKIP_FILTER_WITH_MEMORY_DUMPS }} ${{ env.COMMON_TEST_ARGS }}
+
+ - name: Test (net9.0)
+ run: dotnet test ${{ env.SOLUTION_FILE }} --framework net9.0 ${{ env.SKIP_FILTER_NO_MEMORY_DUMPS }} ${{ env.COMMON_TEST_ARGS }}
+
+ - name: Test (net9.0) (memory dumps)
+ if: ${{ inputs.component == 'Management' }}
+ run: dotnet test ${{ env.SOLUTION_FILE }} --framework net9.0 ${{ env.SKIP_FILTER_WITH_MEMORY_DUMPS }} ${{ env.COMMON_TEST_ARGS }}
+
+ - name: Upload hang dumps (on failure)
+ if: ${{ failure() }}
+ uses: actions/upload-artifact@v4
+ with:
+ name: FailedTestOutput-${{ inputs.OS }}
+ path: '**/*.dmp'
+ if-no-files-found: ignore
+
+ - name: Report test results
+ if: ${{ !cancelled() }}
+ uses: dorny/test-reporter@v2
+ with:
+ name: ${{ inputs.OS }} test results
+ reporter: dotnet-trx
+ path: '**/*.trx'
+ fail-on-empty: 'true'
+ fail-on-error: 'false'
diff --git a/.github/workflows/sonarcube.yml b/.github/workflows/sonarcube.yml
new file mode 100644
index 0000000000..1c593dcd80
--- /dev/null
+++ b/.github/workflows/sonarcube.yml
@@ -0,0 +1,100 @@
+name: SonarQube
+
+on:
+ workflow_dispatch:
+ push:
+ branches:
+ - main
+ - '[0-9]+.x'
+ - 'release/*'
+ pull_request:
+ types: [opened, synchronize, reopened]
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+permissions:
+ contents: read
+ pull-requests: write
+
+env:
+ DOTNET_CLI_TELEMETRY_OPTOUT: 1
+ DOTNET_NOLOGO: true
+ SOLUTION_FILE: 'src/Steeltoe.All.sln'
+ SONAR_TEST_ARGS: >-
+ --no-build --configuration Release --collect "XPlat Code Coverage" --logger trx --results-directory ${{ github.workspace }}
+ --settings coverlet.runsettings -- DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.UseSourceLink=false
+
+jobs:
+ analyze:
+ name: Analyze
+ timeout-minutes: 30
+ runs-on: ubuntu-latest
+
+ services:
+ eurekaServer:
+ image: 'steeltoe.azurecr.io/eureka-server'
+ ports:
+ - 8761:8761
+ configServer:
+ image: 'steeltoe.azurecr.io/config-server'
+ env:
+ eureka.client.enabled: true
+ eureka.client.serviceUrl.defaultZone: http://eurekaServer:8761/eureka
+ eureka.instance.hostname: localhost
+ eureka.instance.instanceId: localhost:configServer:8888
+ ports:
+ - 8888:8888
+
+ steps:
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: |
+ 8.0.*
+ 9.0.*
+
+ - name: Install Sonar .NET Scanner
+ run: dotnet tool install --global dotnet-sonarscanner
+
+ - name: Git checkout
+ uses: actions/checkout@v4
+ with:
+ # Sonar: Shallow clones should be disabled for a better relevancy of analysis.
+ fetch-depth: 0
+
+ - name: Restore packages
+ run: dotnet restore ${{ env.SOLUTION_FILE }} --verbosity minimal
+
+ - name: Set package version
+ run: nbgv cloud
+
+ - name: Begin Sonar .NET scanner
+ id: sonar_begin
+ env:
+ SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
+ run: >-
+ dotnet sonarscanner begin /k:"SteeltoeOSS_steeltoe" /o:"steeltoeoss" /d:sonar.token="${{ secrets.SONAR_TOKEN }}"
+ /d:sonar.host.url="https://sonarcloud.io" /d:sonar.cs.opencover.reportsPaths=**/coverage.opencover.xml
+
+ - name: Build solution
+ run: dotnet build ${{ env.SOLUTION_FILE }} --no-restore --configuration Release --verbosity minimal
+
+ - name: Test (net8.0)
+ run: dotnet test ${{ env.SOLUTION_FILE }} --filter "Category!=MemoryDumps" --framework net8.0 ${{ env.SONAR_TEST_ARGS }}
+
+ - name: Test (net8.0) (memory dumps)
+ run: dotnet test ${{ env.SOLUTION_FILE }} --filter "Category=MemoryDumps" --framework net8.0 ${{ env.SONAR_TEST_ARGS }}
+
+ - name: Test (net9.0)
+ run: dotnet test ${{ env.SOLUTION_FILE }} --filter "Category!=MemoryDumps" --framework net9.0 ${{ env.SONAR_TEST_ARGS }}
+
+ - name: Test (net9.0) (memory dumps)
+ run: dotnet test ${{ env.SOLUTION_FILE }} --filter "Category=MemoryDumps" --framework net9.0 ${{ env.SONAR_TEST_ARGS }}
+
+ - name: End Sonar .NET scanner
+ if: ${{ !cancelled() && steps.sonar_begin.outcome == 'success' }}
+ env:
+ SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
+ run: dotnet sonarscanner end /d:sonar.token="${{ secrets.SONAR_TOKEN }}"
diff --git a/.github/workflows/verify-code-style.yml b/.github/workflows/verify-code-style.yml
new file mode 100644
index 0000000000..d4c21da811
--- /dev/null
+++ b/.github/workflows/verify-code-style.yml
@@ -0,0 +1,68 @@
+name: Cleanup Code
+
+on:
+ workflow_dispatch:
+ push:
+ branches:
+ - main
+ - '[0-9]+.x'
+ - 'release/*'
+ pull_request:
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+permissions:
+ contents: read
+
+env:
+ DOTNET_CLI_TELEMETRY_OPTOUT: 1
+ DOTNET_NOLOGO: true
+ SOLUTION_FILE: 'src/Steeltoe.All.sln'
+
+jobs:
+ verify:
+ name: Verify Code Style
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: |
+ 8.0.*
+ 9.0.*
+
+ - name: Git checkout
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Restore tools
+ run: dotnet tool restore --verbosity minimal
+
+ - name: Restore packages
+ run: dotnet restore ${{ env.SOLUTION_FILE }} --verbosity minimal
+
+ - name: Set package version
+ run: nbgv cloud
+
+ - name: CleanupCode (on PR diff)
+ if: ${{ github.event_name == 'pull_request' }}
+ shell: pwsh
+ run: |
+ # Not using the environment variables for SHAs, because they may be outdated. This may happen on force-push after the build is queued, but before it starts.
+ # The below works because HEAD is detached (at the merge commit), so HEAD~1 is at the base branch. When a PR contains no commits, this job will not run.
+ $headCommitHash = git rev-parse HEAD
+ $baseCommitHash = git rev-parse HEAD~1
+
+ Write-Output "Running code cleanup on commit range $baseCommitHash..$headCommitHash in pull request."
+ dotnet regitlint -s ${{ env.SOLUTION_FILE }} --print-command --skip-tool-check --max-runs=5 --jb --dotnetcoresdk=$(dotnet --version) --jb-profile="Steeltoe Full Cleanup" --jb --properties:Configuration=Release --jb --properties:RunAnalyzers=false --jb --properties:NuGetAudit=false --jb --verbosity=WARN -f commits -a $headCommitHash -b $baseCommitHash --fail-on-diff --print-diff
+
+ - name: CleanupCode (on branch)
+ if: ${{ github.event_name == 'push' || github.event_name == 'release' }}
+ shell: pwsh
+ run: |
+ Write-Output "Running code cleanup on all files."
+ dotnet regitlint -s ${{ env.SOLUTION_FILE }} --print-command --skip-tool-check --jb --dotnetcoresdk=$(dotnet --version) --jb-profile="Steeltoe Full Cleanup" --jb --properties:Configuration=Release --jb --properties:RunAnalyzers=false --jb --properties:NuGetAudit=false --jb --verbosity=WARN --fail-on-diff --print-diff
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000000..1cb051b459
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,432 @@
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+##
+## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
+
+# User-specific files
+*.rsuser
+*.suo
+*.user
+*.userosscache
+*.sln.docstates
+
+# User-specific files (MonoDevelop/Xamarin Studio)
+*.userprefs
+
+# Mono auto generated files
+mono_crash.*
+
+# Build results
+[Dd]ebug/
+[Dd]ebugPublic/
+[Rr]elease/
+[Rr]eleases/
+x64/
+x86/
+[Ww][Ii][Nn]32/
+[Aa][Rr][Mm]/
+[Aa][Rr][Mm]64/
+bld/
+[Bb]in/
+[Oo]bj/
+[Ll]og/
+[Ll]ogs/
+
+# Visual Studio 2015/2017 cache/options directory
+.vs/
+# Uncomment if you have tasks that create the project's static files in wwwroot
+#wwwroot/
+
+# Visual Studio 2017 auto generated files
+Generated\ Files/
+
+# MSTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+
+# NUnit
+*.VisualState.xml
+TestResult.xml
+nunit-*.xml
+
+# Build Results of an ATL Project
+[Dd]ebugPS/
+[Rr]eleasePS/
+dlldata.c
+
+# Benchmark Results
+BenchmarkDotNet.Artifacts/
+
+# .NET Core
+project.lock.json
+project.fragment.lock.json
+artifacts/
+
+# ASP.NET Scaffolding
+ScaffoldingReadMe.txt
+
+# StyleCop
+StyleCopReport.xml
+
+# Files built by Visual Studio
+*_i.c
+*_p.c
+*_h.h
+*.ilk
+*.meta
+*.obj
+*.iobj
+*.pch
+*.pdb
+*.ipdb
+*.pgc
+*.pgd
+*.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.tmp_proj
+*_wpftmp.csproj
+*.log
+*.tlog
+*.vspscc
+*.vssscc
+.builds
+*.pidb
+*.svclog
+*.scc
+
+# Chutzpah Test files
+_Chutzpah*
+
+# Visual C++ cache files
+ipch/
+*.aps
+*.ncb
+*.opendb
+*.opensdf
+*.sdf
+*.cachefile
+*.VC.db
+*.VC.VC.opendb
+
+# Visual Studio profiler
+*.psess
+*.vsp
+*.vspx
+*.sap
+
+# Visual Studio Trace Files
+*.e2e
+
+# TFS 2012 Local Workspace
+$tf/
+
+# Guidance Automation Toolkit
+*.gpState
+
+# ReSharper is a .NET coding add-in
+_ReSharper*/
+*.[Rr]e[Ss]harper
+*.DotSettings.user
+
+# TeamCity is a build add-in
+_TeamCity*
+
+# DotCover is a Code Coverage Tool
+*.dotCover
+
+# AxoCover is a Code Coverage Tool
+.axoCover/*
+!.axoCover/settings.json
+
+# Coverlet is a free, cross platform Code Coverage Tool
+coverage*.json
+coverage*.xml
+coverage*.info
+
+# Visual Studio code coverage results
+*.coverage
+*.coveragexml
+
+# NCrunch
+_NCrunch_*
+.*crunch*.local.xml
+nCrunchTemp_*
+
+# MightyMoose
+*.mm.*
+AutoTest.Net/
+
+# Web workbench (sass)
+.sass-cache/
+
+# Installshield output folder
+[Ee]xpress/
+
+# DocProject is a documentation generator add-in
+DocProject/buildhelp/
+DocProject/Help/*.HxT
+DocProject/Help/*.HxC
+DocProject/Help/*.hhc
+DocProject/Help/*.hhk
+DocProject/Help/*.hhp
+DocProject/Help/Html2
+DocProject/Help/html
+
+# Click-Once directory
+publish/
+
+# Publish Web Output
+*.[Pp]ublish.xml
+*.azurePubxml
+# Note: Comment the next line if you want to checkin your web deploy settings,
+# but database connection strings (with potential passwords) will be unencrypted
+*.pubxml
+*.publishproj
+
+# Microsoft Azure Web App publish settings. Comment the next line if you want to
+# checkin your Azure Web App publish settings, but sensitive information contained
+# in these scripts will be unencrypted
+PublishScripts/
+
+# NuGet Packages
+*.nupkg
+# NuGet Symbol Packages
+*.snupkg
+# The packages folder can be ignored because of Package Restore
+**/[Pp]ackages/*
+# except build/, which is used as an MSBuild target.
+!**/[Pp]ackages/build/
+# Uncomment if necessary however generally it will be regenerated when needed
+#!**/[Pp]ackages/repositories.config
+# NuGet v3's project.json files produces more ignorable files
+*.nuget.props
+*.nuget.targets
+
+# Microsoft Azure Build Output
+csx/
+*.build.csdef
+
+# Microsoft Azure Emulator
+ecf/
+rcf/
+
+# Windows Store app package directories and files
+AppPackages/
+BundleArtifacts/
+Package.StoreAssociation.xml
+_pkginfo.txt
+*.appx
+*.appxbundle
+*.appxupload
+
+# Visual Studio cache files
+# files ending in .cache can be ignored
+*.[Cc]ache
+# but keep track of directories ending in .cache
+!?*.[Cc]ache/
+
+# Others
+ClientBin/
+~$*
+*~
+*.dbmdl
+*.dbproj.schemaview
+*.jfm
+*.pfx
+*.publishsettings
+orleans.codegen.cs
+
+# Including strong name files can present a security risk
+# (https://github.com/github/gitignore/pull/2483#issue-259490424)
+#*.snk
+
+# Since there are multiple workflows, uncomment next line to ignore bower_components
+# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
+#bower_components/
+
+# RIA/Silverlight projects
+Generated_Code/
+
+# Backup & report files from converting an old project file
+# to a newer Visual Studio version. Backup files are not needed,
+# because we have git ;-)
+_UpgradeReport_Files/
+Backup*/
+UpgradeLog*.XML
+UpgradeLog*.htm
+ServiceFabricBackup/
+*.rptproj.bak
+
+# SQL Server files
+*.mdf
+*.ldf
+*.ndf
+
+# Business Intelligence projects
+*.rdl.data
+*.bim.layout
+*.bim_*.settings
+*.rptproj.rsuser
+*- [Bb]ackup.rdl
+*- [Bb]ackup ([0-9]).rdl
+*- [Bb]ackup ([0-9][0-9]).rdl
+
+# Microsoft Fakes
+FakesAssemblies/
+
+# GhostDoc plugin setting file
+*.GhostDoc.xml
+
+# Node.js Tools for Visual Studio
+.ntvs_analysis.dat
+node_modules/
+
+# Visual Studio 6 build log
+*.plg
+
+# Visual Studio 6 workspace options file
+*.opt
+
+# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
+*.vbw
+
+# Visual Studio 6 auto-generated project file (contains which files were open etc.)
+*.vbp
+
+# Visual Studio 6 workspace and project file (working project files containing files to include in project)
+*.dsw
+*.dsp
+
+# Visual Studio 6 technical files
+*.ncb
+*.aps
+
+# Visual Studio LightSwitch build output
+**/*.HTMLClient/GeneratedArtifacts
+**/*.DesktopClient/GeneratedArtifacts
+**/*.DesktopClient/ModelManifest.xml
+**/*.Server/GeneratedArtifacts
+**/*.Server/ModelManifest.xml
+_Pvt_Extensions
+
+# Paket dependency manager
+.paket/paket.exe
+paket-files/
+
+# FAKE - F# Make
+.fake/
+
+# CodeRush personal settings
+.cr/personal
+
+# Python Tools for Visual Studio (PTVS)
+__pycache__/
+*.pyc
+
+# Cake - Uncomment if you are using it
+# tools/**
+# !tools/packages.config
+
+# Tabs Studio
+*.tss
+
+# Telerik's JustMock configuration file
+*.jmconfig
+
+# BizTalk build output
+*.btp.cs
+*.btm.cs
+*.odx.cs
+*.xsd.cs
+
+# OpenCover UI analysis results
+OpenCover/
+
+# Azure Stream Analytics local run output
+ASALocalRun/
+
+# MSBuild Binary and Structured Log
+*.binlog
+
+# NVidia Nsight GPU debugger configuration file
+*.nvuser
+
+# MFractors (Xamarin productivity tool) working folder
+.mfractor/
+
+# Local History for Visual Studio
+.localhistory/
+
+# Visual Studio History (VSHistory) files
+.vshistory/
+
+# BeatPulse healthcheck temp database
+healthchecksdb
+
+# Backup folder for Package Reference Convert tool in Visual Studio 2017
+MigrationBackup/
+
+# Ionide (cross platform F# VS Code tools) working folder
+.ionide/
+
+# Fody - auto-generated XML schema
+FodyWeavers.xsd
+
+# VS Code files for those working on multiple tools
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+*.code-workspace
+/.vscode/settings.json
+
+# Local History for Visual Studio Code
+.history/
+
+# Windows Installer files from build outputs
+*.cab
+*.msi
+*.msix
+*.msm
+*.msp
+
+# JetBrains Rider
+*.sln.iml
+
+#############################################
+### Additions specific to this repository ###
+#############################################
+
+# Pivotal Software
+Pivotal.GemFire.dll
+pivnet*
+pivotal*.zip
+GeneratedCertificates/
+
+# Mac DS_Store files
+.DS_Store
+
+# SonarQube
+.sonarqube/
+
+# JetBrains IDEs Rider/IntelliJ (based on https://intellij-support.jetbrains.com/hc/en-us/articles/206544839)
+**/.idea/**/*.xml
+**/.idea/**/*.iml
+**/.idea/**/*.ids
+**/.idea/**/*.ipr
+**/.idea/**/*.iws
+**/.idea/**/*.name
+**/.idea/**/*.properties
+**/.idea/**/*.ser
+**/.idea/**/shelf/
+**/.idea/**/dictionaries/
+**/.idea/**/libraries/
+**/.idea/**/artifacts/
+**/.idea/**/httpRequests/
+**/.idea/**/dataSources/
+!**/.idea/**/codeStyles/*
diff --git a/Directory.Build.targets b/Directory.Build.targets
new file mode 100644
index 0000000000..b59583d8cd
--- /dev/null
+++ b/Directory.Build.targets
@@ -0,0 +1,129 @@
+
+
+
+
+
+ $(MSBuildProjectDirectory)\ConfigurationSchema.json
+ true
+
+
+
+
+
+
+
+ $(TargetsForTfmSpecificContentInPackage);AddPackageTargetsInPackage
+
+
+
+
+
+
+
+
+
+
+ $(TargetsTriggeredByCompilation);GenerateConfigurationSchema
+
+ $(MSBuildThisFileDirectory)src\Tools\src\ConfigurationSchemaGenerator\ConfigurationSchemaGenerator.csproj
+ $(IntermediateOutputPath)$(AsemblyName).configschema.rsp
+ $(IntermediateOutputPath)ConfigurationSchema.json
+
+
+
+
+
+
+
+
+
+
+
+
+ "@(IntermediateAssembly)"
+ "@(ReferencePath, '" "')"
+ "$(GeneratedConfigurationSchemaOutputPath)"
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ dotnet exec "$(ConfigurationSchemaGeneratorPath)"
+ $(GeneratorCommandLine) @"$(ConfigurationSchemaGeneratorRspPath)"
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ $([System.IO.File]::ReadAllText('$(ConfigurationSchemaPath)'))
+ $([System.IO.File]::ReadAllText('$(GeneratedConfigurationSchemaOutputPath)'))
+
+
+
+
+
+
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000000..261eeb9e9f
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/PackageReadme.md b/PackageReadme.md
new file mode 100644
index 0000000000..939dc38d8c
--- /dev/null
+++ b/PackageReadme.md
@@ -0,0 +1,13 @@
+# Steeltoe
+
+[Steeltoe](https://steeltoe.io/) provides building blocks for development of .NET applications that integrate with [Spring](https://spring.io/) and [Spring Boot](https://spring.io/projects/spring-boot) environments, as well as [Cloud Foundry](https://www.cloudfoundry.org/) and [Kubernetes](https://kubernetes.io/) with first-party support for [Tanzu](https://tanzu.vmware.com/tanzu).
+
+Key features include:
+
+- External (optionally encrypted) configuration using [Spring Cloud Config Server](https://docs.spring.io/spring-cloud-config/docs/current/reference/html/)
+- Service discovery with [Netflix Eureka](https://spring.io/projects/spring-cloud-netflix) and [HashiCorp Consul](https://www.consul.io/)
+- Management endpoints (compatible with [actuators](https://docs.spring.io/spring-boot/docs/current/reference/html/actuator.html)), providing system info (such as versions, configuration, service container contents, mapped routes and HTTP traffic), heap/thread dumps, health checks, exporting metrics to [Prometheus](https://prometheus.io/), and changing log levels at runtime.
+- Connectivity to databases (such as [SQL Server](https://www.microsoft.com/sql-server)/[Azure SQL](https://azure.microsoft.com/products/azure-sql), [Cosmos DB](https://azure.microsoft.com/products/cosmos-db/), [MongoDB](https://www.mongodb.com/), [Redis](https://redis.io/), [RabbitMQ](https://www.rabbitmq.com/), [PostgreSQL](https://www.postgresql.org/), and [MySQL](https://www.mysql.com/)), including support for [Entity Framework Core](https://learn.microsoft.com/ef/core/)
+- Single sign-on, JWT and Certificate auth with [Cloud Foundry](https://www.cloudfoundry.org/)
+
+For more information and to get started, please visit [Steeltoe on GitHub](https://github.com/SteeltoeOSS/Steeltoe) or the [documentation](https://docs.steeltoe.io).
diff --git a/README.md b/README.md
new file mode 100644
index 0000000000..c4f38e30bc
--- /dev/null
+++ b/README.md
@@ -0,0 +1,65 @@
+# Steeltoe .NET Open Source Software
+
+[](https://dev.azure.com/SteeltoeOSS/Steeltoe/_build/latest?definitionId=4&branchName=main)
+ [](https://sonarcloud.io/component_measures?id=SteeltoeOSS_steeltoe&branch=main)
+ [](https://sonarcloud.io/component_measures?id=SteeltoeOSS_steeltoe&branch=main&metric=coverage&view=list)
+ [](https://www.nuget.org/profiles/SteeltoeOSS)
+ [](LICENSE)
+ [](http://stackoverflow.com/questions/tagged/steeltoe)
+
+## Why Steeltoe?
+
+[Steeltoe](https://steeltoe.io/) provides building blocks for development of .NET applications that integrate with [Spring](https://spring.io/) and [Spring Boot](https://spring.io/projects/spring-boot) environments, as well as [Cloud Foundry](https://www.cloudfoundry.org/) and [Kubernetes](https://kubernetes.io/) with first-party support for [Tanzu](https://tanzu.vmware.com/tanzu).
+
+Key features include:
+
+- External (optionally encrypted) configuration using [Spring Cloud Config Server](https://docs.spring.io/spring-cloud-config/docs/current/reference/html/)
+- Service discovery with [Netflix Eureka](https://spring.io/projects/spring-cloud-netflix) and [HashiCorp Consul](https://www.consul.io/)
+- Management endpoints (compatible with [actuators](https://docs.spring.io/spring-boot/docs/current/reference/html/actuator.html)), providing system info (such as versions, configuration, service container contents, mapped routes and HTTP traffic), heap/thread dumps, health checks, exporting metrics to [Prometheus](https://prometheus.io/), and changing log levels at runtime.
+- Connectivity to databases (such as [SQL Server](https://www.microsoft.com/sql-server)/[Azure SQL](https://azure.microsoft.com/products/azure-sql), [Cosmos DB](https://azure.microsoft.com/products/cosmos-db/), [MongoDB](https://www.mongodb.com/), [Redis](https://redis.io/), [RabbitMQ](https://www.rabbitmq.com/), [PostgreSQL](https://www.postgresql.org/), and [MySQL](https://www.mysql.com/)), including support for [Entity Framework Core](https://learn.microsoft.com/ef/core/)
+- Single sign-on, JWT and Certificate auth with [Cloud Foundry](https://www.cloudfoundry.org/)
+
+## Getting Started
+
+In addition to the [feature documentation](https://docs.steeltoe.io/api), we have built several tools to help you get started:
+
+- [Steeltoe Initializr](https://start.steeltoe.io) - Pick and choose what type of application you would like to build and let us generate the initial project for you
+ - Initializr uses [.NET templates](https://github.com/SteeltoeOSS/NetCoreToolTemplates) that can also be used from the `dotnet` CLI and inside of Visual Studio
+- [Steeltoe Samples](https://github.com/SteeltoeOSS/Samples) - Here we have working samples for trying out features and to use as code references
+
+### Prerequisites
+
+| Steeltoe Version | .NET Version |
+| --- | --- |
+| 4.x | .NET 8 - 9 |
+| 3.x | .NET Core 3.1 - .NET 6 |
+| 2.x | .NET Framework 4.6.1+ |
+
+## Support and Feedback
+
+For community support, we recommend [Steeltoe OSS Slack](https://slack.steeltoe.io), [StackOverflow](https://stackoverflow.com/questions/tagged/steeltoe), or [open an issue](https://github.com/SteeltoeOSS/Steeltoe/issues/new/choose).
+
+For production support, we recommend that you contact [Broadcom Support](https://support.broadcom.com/).
+
+## Pre-release packages
+
+If you want to try the latest bits from the `main` branch, use the Steeltoe development feed by adding a reference to your `nuget.config` file:
+
+```xml
+
+
+
+
+
+
+
+```
+
+## Contributing
+
+For more information on contributing to the project or other project information, please see the [Steeltoe Wiki](https://github.com/SteeltoeOSS/Steeltoe/wiki).
+
+### Code of Conduct
+
+This project has adopted the code of conduct defined by the Contributor Covenant to clarify expected behavior in our community.
+For more information see the [.NET Foundation Code of Conduct](https://dotnetfoundation.org/code-of-conduct).
diff --git a/Steeltoe.Debug.ruleset b/Steeltoe.Debug.ruleset
new file mode 100644
index 0000000000..b56f27157f
--- /dev/null
+++ b/Steeltoe.Debug.ruleset
@@ -0,0 +1,241 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Steeltoe.Release.ruleset b/Steeltoe.Release.ruleset
new file mode 100644
index 0000000000..aa918acc4b
--- /dev/null
+++ b/Steeltoe.Release.ruleset
@@ -0,0 +1,215 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/azure-pipelines.yml b/azure-pipelines.yml
new file mode 100644
index 0000000000..0bbf96362d
--- /dev/null
+++ b/azure-pipelines.yml
@@ -0,0 +1,188 @@
+trigger:
+ branches:
+ include:
+ - main
+ - release/*
+ paths:
+ exclude:
+ - README.md
+ - roadmaps/*
+
+jobs:
+- job: Steeltoe_CI
+ timeoutInMinutes: 60
+ variables:
+ DOTNET_NOLOGO: true
+ DOTNET_CLI_TELEMETRY_OPTOUT: 1
+ strategy:
+ matrix:
+ Linux:
+ imageName: ubuntu-latest
+ skipFilter: '--filter "Category!=SkipOnLinux"'
+ sonarAnalyze: true
+ integrationTests: true
+ # MacOS is turned off because it causes flaky builds.
+ #MacOS:
+ # imageName: macOS-latest
+ # skipFilter: '--filter "Category!=Integration&Category!=SkipOnMacOS"'
+ Windows:
+ imageName: windows-latest
+ skipFilter: '--filter "Category!=Integration"'
+ pool:
+ vmImage: $(imageName)
+ steps:
+ - task: PowerShell@2
+ displayName: Turn off certificates on macOS
+ condition: eq(variables['imageName'], 'macOS-latest')
+ inputs:
+ targetType: 'inline'
+ script: |
+ # Setting DOTNET_GENERATE_ASPNET_CERTIFICATE to "false" makes it easier to determine which test failed on macOS when it tried to start a web server with https enabled.
+ # Without setting this, the following message appears in the logs:
+ # The application is trying to access the ASP.NET Core developer certificate key. A prompt might appear to ask for permission to access the key.
+ # When that happens, select 'Always Allow' to grant 'dotnet' access to the certificate key in the future.
+ # and the testrun fails, but without indicating which test caused it. By setting this, the causing test fails with the next message:
+ # Unable to configure HTTPS endpoint. No server certificate was specified, and the default developer certificate could not be found or is out of date.
+ # To prevent the causing test from failing the testrun, disable it on macOS by adding [Trait("Category", "SkipOnMacOS")].
+ echo "##vso[task.setvariable variable=DOTNET_GENERATE_ASPNET_CERTIFICATE]false"
+ - checkout: self
+ fetchDepth: 0
+ - task: UseDotNet@2
+ displayName: Install .NET 8
+ inputs:
+ version: 8.0.x
+ - task: UseDotNet@2
+ displayName: Install .NET 9
+ inputs:
+ version: 9.0.x
+ - task: DotNetCoreCLI@2
+ displayName: Install Nerdbank.GitVersioning tool
+ condition: eq(variables['imageName'], 'macOS-latest')
+ inputs:
+ command: custom
+ custom: tool
+ arguments: install --global nbgv
+ - task: PowerShell@2
+ displayName: Set package version
+ inputs:
+ targetType: 'inline'
+ script: |
+ nbgv cloud
+ - task: DotNetCoreCLI@2
+ displayName: dotnet restore
+ inputs:
+ command: restore
+ verbosityRestore: Minimal
+ projects: src/Steeltoe.All.sln
+ feedsToUse: config
+ nugetConfigPath: nuget.config
+ - task: DotNetCoreCLI@2
+ displayName: dotnet build
+ inputs:
+ command: build
+ projects: src/Steeltoe.All.sln
+ arguments: --no-restore -c $(buildConfiguration) -v minimal
+ - script: |
+ docker run -d --name eurekaserver -p 8761:8761 steeltoe.azurecr.io/eureka-server
+ sleep 10s
+ docker run -d --name configserver -p 8888:8888 --add-host=host.docker.internal:host-gateway steeltoe.azurecr.io/config-server --eureka.client.enabled=true --eureka.instance.hostname=localhost --eureka.instance.instanceId="localhost:configserver:8888"
+ sleep 10s
+ condition: eq(variables['integrationTests'], 'true')
+ displayName: Start Docker services
+ # - script: |
+ # echo "Fetching logs from Config Server container..."
+ # docker logs $(docker ps --filter "name=configserver" --format "{{.ID}}")
+ # echo "Fetching logs from Eureka Server container..."
+ # docker logs $(docker ps --filter "name=eurekaserver" --format "{{.ID}}")
+ # displayName: "Get Config Server Logs"
+ - task: DotNetCoreCLI@2
+ displayName: dotnet test net8.0
+ inputs:
+ command: test
+ projects: '**/*.csproj'
+ arguments: '--framework net8.0 --blame-hang-timeout 3m --no-build -c $(buildConfiguration) $(skipFilter)&Category!=MemoryDumps --collect "XPlat Code Coverage" --settings coverlet.runsettings --logger trx --results-directory $(Build.SourcesDirectory)'
+ publishTestResults: false
+ - task: DotNetCoreCLI@2
+ displayName: dotnet test net8.0 (memory dumps)
+ inputs:
+ command: test
+ projects: '**/Steeltoe.Management.Endpoint.Test.csproj'
+ arguments: '--framework net8.0 --blame-hang-timeout 3m --no-build -c $(buildConfiguration) $(skipFilter)&Category=MemoryDumps --collect "XPlat Code Coverage" --settings coverlet.runsettings --logger trx --results-directory $(Build.SourcesDirectory)'
+ publishTestResults: false
+ - task: DotNetCoreCLI@2
+ displayName: dotnet test net9.0
+ inputs:
+ command: test
+ projects: '**/*.csproj'
+ arguments: '--framework net9.0 --blame-hang-timeout 3m --no-build -c $(buildConfiguration) $(skipFilter)&Category!=MemoryDumps --collect "XPlat Code Coverage" --settings coverlet.runsettings --logger trx --results-directory $(Build.SourcesDirectory)'
+ publishTestResults: false
+ - task: DotNetCoreCLI@2
+ displayName: dotnet test net9.0 (memory dumps)
+ inputs:
+ command: test
+ projects: '**/Steeltoe.Management.Endpoint.Test.csproj'
+ arguments: '--framework net9.0 --blame-hang-timeout 3m --no-build -c $(buildConfiguration) $(skipFilter)&Category=MemoryDumps --collect "XPlat Code Coverage" --settings coverlet.runsettings --logger trx --results-directory $(Build.SourcesDirectory)'
+ publishTestResults: false
+ - task: CopyFiles@2
+ condition: failed()
+ inputs:
+ contents: $(Build.SourcesDirectory)/**/*.dmp
+ targetFolder: $(Build.ArtifactStagingDirectory)/hangdumps
+ - publish: $(Build.ArtifactStagingDirectory)/hangdumps
+ condition: failed()
+ displayName: Publish test result files if tests fail
+ artifact: FailedTestOutput-$(Agent.JobName)
+ - script: |
+ docker kill configserver
+ docker rm configserver
+ docker kill eurekaserver
+ docker rm eurekaserver
+ condition: and(succeededOrFailed(), eq(variables['integrationTests'], 'true'))
+ displayName: Stop Docker services
+ - task: PublishTestResults@2
+ condition: succeededOrFailed()
+ displayName: Publish test results
+ inputs:
+ testResultsFormat: VSTest
+ testResultsFiles: '*.trx'
+ mergeTestResults: true
+ - task: Palmmedia.reportgenerator.reportgenerator-build-release-task.reportgenerator@5
+ condition: and(succeededOrFailed(), or(eq(variables['Agent.OS'], 'Windows_NT'), eq(variables['integrationTests'], 'true')))
+ displayName: Consolidate coverage for this job
+ inputs:
+ reports: $(Build.SourcesDirectory)/**/*opencover.xml
+ targetdir: $(Build.ArtifactStagingDirectory)/CodeCoverage/$(Agent.JobName)
+ reporttypes: Cobertura
+ - publish: $(Build.ArtifactStagingDirectory)/CodeCoverage/$(Agent.JobName)
+ condition: and(succeeded(), or(eq(variables['Agent.OS'], 'Windows_NT'), eq(variables['integrationTests'], 'true')))
+ displayName: Publish code coverage artifacts
+ artifact: coverageResults-$(Agent.JobName)
+- job: Wrap_up
+ dependsOn:
+ - Steeltoe_CI
+ pool:
+ vmImage: ubuntu-latest
+ steps:
+ - download: current
+ artifact: coverageResults-Steeltoe_CI Linux
+ condition: succeededOrFailed()
+ displayName: Download test coverage results from Linux
+ continueOnError: true
+ - download: current
+ artifact: coverageResults-Steeltoe_CI Windows
+ condition: succeededOrFailed()
+ displayName: Download test coverage results from Windows
+ continueOnError: true
+ - task: Palmmedia.reportgenerator.reportgenerator-build-release-task.reportgenerator@5
+ condition: succeededOrFailed()
+ displayName: Consolidate code coverage results
+ inputs:
+ reports: $(Pipeline.Workspace)/**/Cobertura.xml
+ targetdir: $(Build.ArtifactStagingDirectory)/CodeCoverage
+ reporttypes: Cobertura
+ - task: PublishCodeCoverageResults@1
+ condition: succeededOrFailed()
+ displayName: Publish code coverage to Azure DevOps
+ inputs:
+ codeCoverageTool: Cobertura
+ summaryFileLocation: $(Build.ArtifactStagingDirectory)/CodeCoverage/Cobertura.xml
diff --git a/build/icon.png b/build/icon.png
new file mode 100644
index 0000000000..8fc8dff2c5
Binary files /dev/null and b/build/icon.png differ
diff --git a/build/package.yml b/build/package.yml
new file mode 100644
index 0000000000..56b1c3e9cd
--- /dev/null
+++ b/build/package.yml
@@ -0,0 +1,96 @@
+trigger:
+ branches:
+ include:
+ - main
+ - release/*
+ paths:
+ exclude:
+ - README.md
+ - roadmaps/*
+
+jobs:
+- job: Steeltoe_Package
+ pool:
+ vmImage: windows-latest
+ variables:
+ DOTNET_NOLOGO: true
+ DOTNET_CLI_TELEMETRY_OPTOUT: 1
+ steps:
+ - checkout: self
+ fetchDepth: 0
+ - task: UseDotNet@2
+ displayName: Install .NET 8
+ inputs:
+ version: 8.0.x
+ - task: UseDotNet@2
+ displayName: Install .NET 9
+ inputs:
+ version: 9.0.x
+ - task: PowerShell@2
+ displayName: Set package version
+ env:
+ PackageVersionOverride: $(PackageVersionOverride)
+ inputs:
+ targetType: 'inline'
+ script: |
+ if ($env:PackageVersionOverride) {
+ Write-Host "Overriding package version with: $env:PackageVersionOverride"
+ Write-Warning "Always provide a 4-segment version (such as 1.2.3.0 or 1.2.3.0-rc1), to prevent an increment in patch number."
+ Write-Warning "The commit hash may still be added to the version, depending on the source branch or PR being built."
+ nbgv set-version $env:PackageVersionOverride
+
+ Write-Host "Contents of version.json after update:"
+ get-content version.json
+
+ git config --global user.email "cibuild@steeltoe.io"
+ git config --global user.name "steeltoe-cibuild"
+ git commit --allow-empty -m "Activating version override by locally committing changes to version.json."
+ }
+
+ nbgv cloud
+ - task: DotNetCoreCLI@2
+ displayName: dotnet restore
+ inputs:
+ command: restore
+ verbosityRestore: Minimal
+ projects: src/Steeltoe.All.sln
+ feedsToUse: config
+ nugetConfigPath: nuget.config
+ - task: DotNetCoreCLI@2
+ displayName: dotnet build
+ inputs:
+ command: build
+ projects: src/Steeltoe.All.sln
+ arguments: --no-restore -c Release -v minimal
+ - task: DotNetCoreCLI@2
+ displayName: dotnet pack
+ inputs:
+ command: pack
+ verbosityPack: Minimal
+ packagesToPack: src/Steeltoe.All.sln
+ configuration: Release
+ packDirectory: $(Build.ArtifactStagingDirectory)/packages
+ nobuild: true
+ - task: DotNetCoreCLI@2
+ condition: and(succeeded(), not(eq(variables['build.reason'], 'PullRequest')))
+ inputs:
+ command: custom
+ custom: tool
+ arguments: install --tool-path . sign --prerelease
+ displayName: Install code signing tool
+ - pwsh: |
+ .\sign code azure-key-vault "**/*.nupkg" `
+ --base-directory "$(Build.ArtifactStagingDirectory)/packages" `
+ --azure-key-vault-url "$(SignKeyVaultUrl)" `
+ --azure-key-vault-tenant-id "$(SignTenantId)" `
+ --azure-key-vault-client-id "$(SignClientId)" `
+ --azure-key-vault-client-secret "$(SignClientSecret)" `
+ --azure-key-vault-certificate "$(SignKeyVaultCertificate)" `
+ --description "Steeltoe" `
+ --description-url "https://github.com/SteeltoeOSS"
+ condition: and(succeeded(), not(eq(variables['build.reason'], 'PullRequest')))
+ displayName: Sign packages
+ - publish: $(Build.ArtifactStagingDirectory)/packages
+ condition: succeeded()
+ displayName: Publish build artifacts
+ artifact: Packages
diff --git a/build/pr-code-cleanup.yml b/build/pr-code-cleanup.yml
new file mode 100644
index 0000000000..31dd8dcd31
--- /dev/null
+++ b/build/pr-code-cleanup.yml
@@ -0,0 +1,134 @@
+trigger: none
+
+jobs:
+- job: PR_Code_Cleanup
+ variables:
+ DOTNET_NOLOGO: true
+ DOTNET_CLI_TELEMETRY_OPTOUT: 1
+ pool:
+ vmImage: ubuntu-latest
+ steps:
+ - task: PowerShell@2
+ displayName: Validate preconditions
+ inputs:
+ targetType: 'inline'
+ script: |
+ if (-not $env:SYSTEM_PULLREQUEST_TARGETBRANCH) {
+ throw 'This pipeline can only be run from pull requests. Use "/azp run cleanup-code" in a GitHub PR comment.'
+ }
+ - task: UseDotNet@2
+ displayName: Install .NET 8
+ inputs:
+ version: 8.0.x
+ - task: UseDotNet@2
+ displayName: Install .NET 9
+ inputs:
+ version: 9.0.x
+ - checkout: self
+ fetchDepth: 0
+ persistCredentials: true
+ - task: PowerShell@2
+ displayName: Set package version
+ inputs:
+ targetType: 'inline'
+ script: |
+ nbgv cloud
+ - task: PowerShell@2
+ displayName: Checkout PR branch
+ inputs:
+ targetType: 'inline'
+ script: |
+ # We're going to push changes, so we need to be on the PR branch
+ # instead of the detached head that contains the merge result.
+ Write-Output "Switching to branch $env:SYSTEM_PULLREQUEST_SOURCEBRANCH"
+ git checkout $env:SYSTEM_PULLREQUEST_SOURCEBRANCH
+ - task: PowerShell@2
+ displayName: Restore tools
+ inputs:
+ targetType: 'inline'
+ script: |
+ dotnet tool restore
+ - task: PowerShell@2
+ displayName: Restore packages
+ inputs:
+ targetType: 'inline'
+ script: |
+ dotnet restore src
+ - task: PowerShell@2
+ displayName: Code cleanup
+ inputs:
+ targetType: 'inline'
+ script: |
+ # Find the most-recent common ancestor to diff against. Using the target branch name is incorrect because this PR may be
+ # out-of-date (which means the target branch has newer commits, and we should ignore those).
+ $baseCommitHash = git merge-base origin/$env:SYSTEM_PULLREQUEST_SOURCEBRANCH origin/$env:SYSTEM_PULLREQUEST_TARGETBRANCH
+ if ($LastExitCode -ne 0) { throw "Command 'git merge-base' failed with exit code $LastExitCode." }
+
+ $headCommitHash = git rev-parse HEAD
+ if ($LastExitCode -ne 0) { throw "Command 'git rev-parse' failed with exit code $LastExitCode." }
+
+ if ($baseCommitHash -ne $headCommitHash) {
+ Write-Output "Running code cleanup on commit range $baseCommitHash..$headCommitHash in pull request."
+ dotnet regitlint -s src/Steeltoe.All.sln --print-command --skip-tool-check --jb --dotnetcoresdk=$(dotnet --version) --jb-profile="Steeltoe Full Cleanup" --jb --properties:Configuration=Release --jb --verbosity=WARN --jb --properties:NuGetAudit=false -f commits -a $headCommitHash -b $baseCommitHash
+ }
+ - task: PowerShell@2
+ displayName: Detect changes
+ inputs:
+ targetType: 'inline'
+ script: |
+ git add -A
+ git diff-index --quiet HEAD --
+ $hasChangesToCommit = $LastExitCode -ne 0
+ Write-Output "##vso[task.setvariable variable=hasChangesToCommit]$hasChangesToCommit"
+ exit 0
+ - task: PowerShell@2
+ displayName: Push changes
+ condition: and(succeeded(), eq(variables['hasChangesToCommit'], 'True'))
+ env:
+ SYSTEM_ACCESSTOKEN: $(System.AccessToken)
+ inputs:
+ targetType: 'inline'
+ script: |
+ git config --global user.email "cibuild@steeltoe.io"
+ git config --global user.name "steeltoe-cibuild"
+
+ Write-Output "Committing changes."
+ git commit -m "Automated code cleanup from Steeltoe cibuild"
+ if ($LastExitCode -ne 0) { throw "Command 'git commit' failed with exit code $LastExitCode." }
+
+ Write-Output "Pushing changes."
+ git push origin
+ if ($LastExitCode -ne 0) { throw "Command 'git push' failed with exit code $LastExitCode." }
+
+ Write-Output "##vso[task.setvariable variable=hasCommitted]True"
+ exit 0
+ - task: PowerShell@2
+ displayName: Compose status message
+ condition: always()
+ inputs:
+ targetType: 'inline'
+ script: |
+ if ($env:HASCOMMITTED -eq 'True') {
+ $statusMessage = "Code cleanup successfully reformatted files and pushed changes."
+ }
+ elseif ($env:AGENT_JOBSTATUS -eq "Canceled") {
+ $statusMessage = "Code cleanup was canceled, no changes were pushed."
+ }
+ elseif ($env:AGENT_JOBSTATUS -eq "Failed") {
+ $url = "$(System.TeamFoundationCollectionUri)$(System.TeamProject)/_build/results?buildId=$(Build.BuildId)"
+ $statusMessage = "Code cleanup failed to reformat and push changes.
View details [here]($url).
"
+ }
+ else {
+ $statusMessage = "Code cleanup successfully reformatted files, but there were no changes to push."
+ }
+
+ Write-Output "Status: $statusMessage"
+ Write-Output "##vso[task.setvariable variable=statusMessage]$statusMessage"
+ exit 0
+ - task: GitHubComment@0
+ displayName: Notify status in PR comment
+ condition: and(always(), not(eq(variables['statusMessage'], '')))
+ inputs:
+ gitHubConnection: 'SteeltoeOSS (1)'
+ repositoryName: '$(Build.Repository.Name)'
+ comment: $(statusMessage)
diff --git a/cleanupcode.ps1 b/cleanupcode.ps1
new file mode 100644
index 0000000000..4f36c58367
--- /dev/null
+++ b/cleanupcode.ps1
@@ -0,0 +1,44 @@
+#Requires -Version 7.0
+
+# This script reformats (part of) the codebase to make it compliant with our coding guidelines.
+
+param(
+ # Git branch name or base commit hash to reformat only the subset of changed files. Omit for all files.
+ [string] $revision
+)
+
+function VerifySuccessExitCode {
+ if ($LastExitCode -ne 0) {
+ throw "Command failed with exit code $LastExitCode."
+ }
+}
+
+dotnet tool restore
+VerifySuccessExitCode
+
+dotnet restore src
+VerifySuccessExitCode
+
+if ($revision) {
+ $headCommitHash = git rev-parse HEAD
+ VerifySuccessExitCode
+
+ $baseCommitHash = git rev-parse $revision
+ VerifySuccessExitCode
+
+ if ($baseCommitHash -eq $headCommitHash) {
+ Write-Output "Running code cleanup on staged/unstaged files."
+ dotnet regitlint -s src/Steeltoe.All.sln --print-command --skip-tool-check --max-runs=5 --jb --dotnetcoresdk=$(dotnet --version) --jb-profile="Steeltoe Full Cleanup" --jb --properties:Configuration=Release --jb --properties:NuGetAudit=false --jb --verbosity=WARN -f staged,modified
+ VerifySuccessExitCode
+ }
+ else {
+ Write-Output "Running code cleanup on commit range $baseCommitHash..$headCommitHash, including staged/unstaged files."
+ dotnet regitlint -s src/Steeltoe.All.sln --print-command --skip-tool-check --max-runs=5 --jb --dotnetcoresdk=$(dotnet --version) --jb-profile="Steeltoe Full Cleanup" --jb --properties:Configuration=Release --jb --properties:NuGetAudit=false --jb --verbosity=WARN -f staged,modified,commits -a $headCommitHash -b $baseCommitHash
+ VerifySuccessExitCode
+ }
+}
+else {
+ Write-Output "Running code cleanup on all files."
+ dotnet regitlint -s src/Steeltoe.All.sln --print-command --skip-tool-check --jb --dotnetcoresdk=$(dotnet --version) --jb-profile="Steeltoe Full Cleanup" --jb --properties:Configuration=Release --jb --properties:NuGetAudit=false --jb --verbosity=WARN
+ VerifySuccessExitCode
+}
diff --git a/coverlet.runsettings b/coverlet.runsettings
new file mode 100644
index 0000000000..89c6bc25f5
--- /dev/null
+++ b/coverlet.runsettings
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+ opencover
+ [*.Test*]*,[*]Microsoft.Diagnostics*
+ [Steeltoe.*]*
+ ObsoleteAttribute,GeneratedCodeAttribute,CompilerGeneratedAttribute
+ true
+ false
+ false
+
+
+
+
+
diff --git a/nuget.config b/nuget.config
new file mode 100644
index 0000000000..f5253c498c
--- /dev/null
+++ b/nuget.config
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/roadmaps/2.0.0.md b/roadmaps/2.0.0.md
new file mode 100644
index 0000000000..ec7f860602
--- /dev/null
+++ b/roadmaps/2.0.0.md
@@ -0,0 +1,23 @@
+# Release Notes: 2.0.0
+Release Date: February 15th, 2018
+#### Features
+* Full ASP.NET Core 2.0 support
+* CredHub Support
+* SQLServer Connector support
+* Actuators for .NET Framework
+ * Thread dump endpoint - Windows only (#150011855)
+ * Supports .NET Framework and .NET Core on Windows only
+ * Heap dump endpoint - Windows only (#150011867)
+ * Supports .NET Framework and .NET Core on Windows only
+* Enhancements
+ * Refactoring of code and cleanup
+ * Autofac work
+ * Discovery, Circuit Breaker, Config Server, Configuration
+* Sample Updates
+ * CredHub
+ * SQLServer Connector (#151373297)
+ * New Actuator endpoints (Thread and Heap dump)
+* Documentation
+ * Changes required for ASP.NET 2.0 support
+ * New features
+ * Restructure of documentation site
diff --git a/roadmaps/2.1.0.md b/roadmaps/2.1.0.md
new file mode 100644
index 0000000000..55c87b754f
--- /dev/null
+++ b/roadmaps/2.1.0.md
@@ -0,0 +1,36 @@
+# Release Notes: 2.1.0
+Release Date: August 17th, 2018
+#### Features
+ * Support for ASP.NET 2.1
+ * Management endpoints
+ * Added /env, /refresh, /mappings, /metrics
+ * Disable Cloud Foundry security checks when running locally
+ * Added default management health contributors
+ * Redis, Rabbit, Relational (mysql, mssql, postgres)
+ * Management endpoint support for ASP.NET 4.x apps
+ * PCF Apps Manager integration for ASP.NET 4.x apps
+ * Both Owin and HTTP Module (SysWeb) are supported
+ * Open Census Metrics (#151738121)
+ * Provide Spring Boot compatible Metrics Endpoint
+ * Automatic instrumentation of common ingress and egress points
+ * Provide an exporter for Cloud Foundry Metrics Forwarder
+ * Metrics visible in PCF Metrics
+ * Open Census Distributed Tracing = ASP.NET Core only
+ * Log correlation support like what Spring Cloud Sleuth enables
+ * Trace correlation with PCF Metrics
+ * Automatic instrumentation of common ingress and egress points
+ * Automatic trace context propagation (Zipkin headers)
+ * Provide a Zipkin Exporter
+ * Discovery
+ * URL style Basic Auth support
+ * Connectors
+ * Create an out-of-the-box collection of IHealthContributors for connectors
+ * Autofac provider for EF6
+ * Security support for ASP.NET 4.x apps
+ * PCF SSO and/or UAA integration
+ * HttpClientFactory support
+ * Steeltoe Discovery handler
+ * Sample Updates
+ * ASP.NET 4.x samples with Actuators and Security
+ * ASP.NET Core Distributed Tracing Sample
+
diff --git a/roadmaps/2.2.0.md b/roadmaps/2.2.0.md
new file mode 100644
index 0000000000..d89108e5bd
--- /dev/null
+++ b/roadmaps/2.2.0.md
@@ -0,0 +1,47 @@
+# Release 2.2.0 GA
+Anticipated Release Date: Q1/19
+Note: Listed features are subject to change
+
+#### Features, Enhancements
+* Service Discovery
+ * Eureka
+ * Support binding multiple urls for eureka servers
+ * Hashicorp Consul supported added
+ * Supports common Steeltoe IDiscoveryClient abstraction
+ * Contributed by @majian159
+* Add Health contributors
+ * Config Server
+ * Discovery Client
+* Connectors
+ * Add MongoDB connector
+* Actuator endpoints
+ * Align supported actuators with Spring Boot 2.0 actuators
+ * Naming and functionality
+ * Discuss standardizing these
+* Steeltoe Security libraries:
+ * Refactor and clean up code
+ * Add support for OpenID with Core
+* Enhanced client-side Load Balancer support
+ * Currently we have a randomized load balancer, this will likely add a round-robin load balancer
+ * Also considered is a pluggable load balancer (need to investigate)
+* Property placeholder support throughout Steeltoe projects
+
+##### Possible Feature Addition
+* Spring Cloud Stream (Messaging Abstraction)
+ * Allow to use RabbitMQ and Kafka
+ * Update Hystrix to use Spring Cloud Stream
+
+
+#### Other
+* Sign the NuGet packages
+
+#### Pushed to next release
+* Add Gemfire/Geode connector (.NET Framework only)
+* Eureka
+ * Add support for other enhancements/features that have been added to Netflix Eureka and Spring Cloud Eureka
+* Metrics Enhancements
+ * Instrumentation for Hystrix to add tracing and stats
+ * Instrumentation for EFCore to add tracing and stats
+ * Instrumentation for Connectors to add tracing and stats
+* Add Health contributors
+ * Circuit Breaker
diff --git a/roadmaps/2.3.0.md b/roadmaps/2.3.0.md
new file mode 100644
index 0000000000..12a31dad88
--- /dev/null
+++ b/roadmaps/2.3.0.md
@@ -0,0 +1,50 @@
+# Release 2.3.0 GA
+Anticipated Release Date: August 2019
+
+Complete listing of tickets:
+* RC1
+ * [Open](https://github.com/SteeltoeOSS/steeltoe/milestone/1)
+ * [Closed](https://github.com/SteeltoeOSS/steeltoe/milestone/1?closed=1)
+* RC2
+ * [Open](https://github.com/SteeltoeOSS/steeltoe/milestone/3)
+ * [Closed](https://github.com/SteeltoeOSS/steeltoe/milestone/3?closed=1)
+* GA
+ * [Open](https://github.com/SteeltoeOSS/steeltoe/milestone/4)
+ * [Closed](https://github.com/SteeltoeOSS/steeltoe/milestone/4?closed=1)
+
+>Note: Listed features are subject to change
+
+#### Features, Enhancements
+* Logging
+ * Serilog logging support
+* Management
+ * Support for using [Health checks in ASP .NET Core](https://docs.microsoft.com/en-us/aspnet/core/host-and-deploy/health-checks?view=aspnetcore-2.2)
+ * Support for launching CloudFoundry tasks bundled with applications
+* Connectors
+ * Added a GemFire/Geode/PCC connector
+ * Added ability to apply EF migrations using `cf task` (Contributed by @macsux)
+ * Added additional property support for Microsoft SQL Server Connection strings.
+ * Added Apache Geode/GemFire/Pivotal Cloud Cache connector
+
+#### Notable issues resolved
+* [Configuration/48](https://github.com/SteeltoeOSS/Configuration/issues/48) - Configuration - fixed `enabled` setting
+* [Steeltoe/#33](https://github.com/SteeltoeOSS/steeltoe/issues/33) - Fixed 404 errors when using open source config server
+* [Security/18](https://github.com/SteeltoeOSS/Security/issues/18) - Allow customizable claim definitions
+* [Steeltoe/34](https://github.com/SteeltoeOSS/steeltoe/issues/34) - Fixed application name in manifest.yml overriding spring:application:name in appsettings.json
+* [Steeltoe/40](https://github.com/SteeltoeOSS/steeltoe/issues/40) - Updated Redis library version
+* [Steeltoe/33](https://github.com/SteeltoeOSS/steeltoe/issues/41) - Added option to disable vault renewal for OSS config server
+* [Steeltoe/30](https://github.com/SteeltoeOSS/steeltoe/issues/30) - Add Search Path option to PostgresSQL connector (Contributed by @jpmorin)
+* [Steeltoe/63](https://github.com/SteeltoeOSS/steeltoe/pull/63) - Redis TLS ports are picked as default if specified in creds (Contributed by @jplebre)
+
+
+
+
+#### Other
+* Steeltoe Repository Restructure
+ * Mono repo for Steeltoe core components
+ * Move to Azure Devops
+ * Move the build pipelines (CI/CD)
+ * Move testing coverage
+ * Enhance code coverage
+* Create [SteeltoeOSS-Incubator](https://github.com/steeltoeoss-incubator) org for new projects
+
diff --git a/roadmaps/2.4.0.md b/roadmaps/2.4.0.md
new file mode 100644
index 0000000000..3357d6100a
--- /dev/null
+++ b/roadmaps/2.4.0.md
@@ -0,0 +1,37 @@
+# Release 2.4.0 GA
+Release Candidate (RC1) Date: November 1st, 2019
+
+GA Release Date: November 13th, 2019
+
+
+Complete listing of tickets:
+* RC1
+ * [Open](https://github.com/SteeltoeOSS/steeltoe/milestone/6)
+ * [Closed](https://github.com/SteeltoeOSS/steeltoe/milestone/6?closed=1)
+* GA
+ * [Open](https://github.com/SteeltoeOSS/steeltoe/milestone/8)
+ * [Closed](https://github.com/SteeltoeOSS/steeltoe/milestone/8?closed=1)
+
+
+>Note: Listed features are subject to change
+
+#### Features, Enhancements
+* .NET Core 3.0 support
+ * [Steeltoe/90](https://github.com/SteeltoeOSS/steeltoe/issues/90): DynamicLogger does not work with Microsoft.Extensions.Logging 3.0
+ * [Steeltoe/139](https://github.com/SteeltoeOSS/steeltoe/issues/139): TypeLoadException thrown when using CloudFoundryOAuth
+ * [Steeltoe/116](https://github.com/SteeltoeOSS/steeltoe/issues/116): Fix mappings endpoint
+* Connectors
+ * Enhanced documentation around the GemFire/Geode/PCC connector
+* HostBuilder extensions added
+ * [Steeltoe/122](https://github.com/SteeltoeOSS/steeltoe/issues/122): Logging hostbuilder extensions
+ * [Steeltoe/157](https://github.com/SteeltoeOSS/steeltoe/issues/157): Management hostbuilder extensions
+
+#### Notable issues resolved
+* [Steeltoe/135](https://github.com/SteeltoeOSS/steeltoe/issues/135): Serilog DynamicConsole breaks after changing logging levels
+* [Steeltoe/123](https://github.com/SteeltoeOSS/steeltoe/issues/123): Connectors are configuration case sensitive
+* [Steeltoe/108](https://github.com/SteeltoeOSS/steeltoe/issues/108): For IIS deployment with VirtualPath, the basePath is not set on redirect_uri - Contributed by @rabadiw
+* [Steeltoe/19](https://github.com/SteeltoeOSS/steeltoe/issues/19): Specify InvariantCulture for ToString conversion when publishing ConfigServerClientSettings - Contributed by @akovalov0
+* [Steeltoe/166](https://github.com/SteeltoeOSS/steeltoe/issues/166): Allow better control over CORS policies
+* [Steeltoe/152](https://github.com/SteeltoeOSS/steeltoe/issues/152): Updated CredHub API calls due to latest update
+* [Steeltoe/150](https://github.com/SteeltoeOSS/steeltoe/issues/150): Show all registered configuration providers in the /env endpoint including placeholder provider
+
diff --git a/roadmaps/2.5.0.md b/roadmaps/2.5.0.md
new file mode 100644
index 0000000000..6b0567c99f
--- /dev/null
+++ b/roadmaps/2.5.0.md
@@ -0,0 +1,11 @@
+# Release 2.5.0
+Anticipated Release Date: TBD
+
+Complete listing of tickets:
+* RC1
+ * [Open](https://github.com/SteeltoeOSS/steeltoe/milestone/7)
+ * [Closed](https://github.com/SteeltoeOSS/steeltoe/milestone/7?closed=1)
+
+>Note: Listed features are subject to change
+
+#### Features, Enhancements - TBD
diff --git a/roadmaps/3.0.0.md b/roadmaps/3.0.0.md
new file mode 100644
index 0000000000..b57473aa8a
--- /dev/null
+++ b/roadmaps/3.0.0.md
@@ -0,0 +1,109 @@
+# Release 3.0.0 GA
+Release Date: August 21, 2020
+
+Release Notes: https://github.com/SteeltoeOSS/Steeltoe/releases/tag/3.0.0
+
+### Milestone 1
+Release Notes: https://github.com/SteeltoeOSS/steeltoe/releases/tag/3.0.0-m1
+
+Release Status: Released on 02/21/2020
+
+### Milestone 2
+Issue listing: https://github.com/SteeltoeOSS/steeltoe/releases/tag/3.0.0-m2
+
+Release Status: Released on 04/09/2020
+
+### Milestone 3
+Issue listing: https://github.com/SteeltoeOSS/steeltoe/releases/tag/3.0.0-m3
+
+Release Status: Released on 07/20/2020
+
+### Release Candidate 1
+Issue listing: https://github.com/SteeltoeOSS/Steeltoe/milestone/16
+
+
+## General Availability Enhancements and Features
+
+#### Features, Enhancements
+* Streaming Support (Messaging Abstraction)
+ * Steeltoe Messaging
+ * Easy auto-wiring of Steeltoe Messaging APIs and a RabbitMQ broker (*Completed in Milestone 2*)
+ * Steeltoe Streams (Experimental library only)
+* Additional Platform Support and Integrations
+ * Azure Spring Cloud
+ * Kubernetes
+ * Discovery
+ * Configuration
+ * Readiness/Liveness endpoints
+* Discovery
+ * Pluggable discovery clients (options include: Eureka, Consul, Kubernetes, No-op fall back to infrastructure/container)
+ * Add support for other enhancements/features that have been added to Netflix Eureka and Spring Cloud Eureka
+* Connectors
+ * New abstraction layer for connectors
+ * Allow for easier extensibility
+ * Separate out CF specific components
+ * Add CosmosDB connector (*Completed in Milestone 2*)
+* Configuration
+ * Kubernetes configuration and extensions
+ * Configuration providers for ConfigMap and Secrets
+ * Extensions for Host and WebHost
+* Distributed Tracing
+ * Move from OpenCensus Tracing to OpenTelemetry Tracing packages (*Completed in Milestone 1*)
+ * New Exporters for tracing
+* Management
+ * Metrics move from OpenCensus to OpenTelemetry Stats/Metrics packages
+ * New prometheus exporter
+ * Add support for collecting core dumps on Linux
+ * Actuator endpoints easier configuration and defaults
+ * Better support for standalone (non-CF) management via Spring Boot Admin application
+ * Health Groups added
+ * Readiness/Liveness endpoints under /health endpoint
+* Circuit Breaker
+ * Work on alternative to Hystrix Dashboard
+ * Using prometheus endpoint
+* Configuration Server
+ * mTLS support (*Completed in Milestone 2*)
+* Tooling (Components released separately from Steeltoe)
+ * Enhanced Cloud Native .NET Development Tools
+ * [Steeltoe Local (CLI)](https://github.com/SteeltoeOSS/Tooling)
+ * Service creation
+ * Local developer environment
+ * Local Debugging
+ * Easy setup and running of services
+ * [Steeltoe Initializr](https://github.com/SteeltoeOSS/initializr) -- Currently in Beta at [https://start.steeltoe.io](https://start.steeltoe.io)
+ * Getting Started
+ * Dynamic Templating
+ * Project creation
+ * Utilize `dotnet new` capabilities
+
+#### Other
+* Create abstractions and split out platform specific code (CloudFoundry) that builds off of our core components into own components
+ * This provides better path for other platform providers to build off of Steeltoe core components
+* Review and identify areas for refactoring and improvement across all components
+
+#### Optional (if we have time)
+* Add Health contributors
+ * Circuit Breaker
+* Streaming Support
+ * Steeltoe Streams
+ * RabbitMQ Binder (Moved to 3.1 Release)
+ * Kafka Binder
+ * Steeltoe Streams and Spring Cloud Data Flow integration with RabbitMQ (Moved to 3.1 release)
+ * Steeltoe Bus
+ * Ability to link nodes of a distributed system with a message broker
+ * Dependent on Steeltoe Streams project
+ * Provide auto-update of configuration properties across microservice applications
+ * Dependendent on Steeltoe Streams and Steeltoe Bus implementation
+* Discovery
+ * Blue/Green deployments through endpoint
+ * Use endpoint to set registered instances to `offline`
+* Circuit Breaker
+ * Investigate how we can integrate Polly into our current implementation
+* Connectors
+ * Add Kafka connector
+* Metrics Enhancements
+ * Instrumentation for Hystrix to add tracing and stats
+ * Instrumentation for EFCore to add tracing and stats
+ * Instrumentation for Connectors to add tracing and stats
+* Other
+ * Performance benchmarking
diff --git a/roadmaps/3.1.0.md b/roadmaps/3.1.0.md
new file mode 100644
index 0000000000..c7b5196d39
--- /dev/null
+++ b/roadmaps/3.1.0.md
@@ -0,0 +1,47 @@
+# Release 3.1.0 GA
+
+Anticipated Release Date: End of 2020
+
+## General Availability Enhancements and Features
+
+*Note: Listed features are subject to change*
+
+### Features, Enhancements
+
+* Streaming Support
+ * Steeltoe Streams
+ * RabbitMQ Binder
+ * Steeltoe Streams and Spring Cloud Data Flow integration with RabbitMQ
+* Discovery
+ * Add support for other enhancements/features that have been added to Netflix Eureka and Spring Cloud Eureka
+ * Blue/Green deployments through endpoints
+ * Use endpoint to set registered instances to `offline`
+* Circuit Breaker
+ * Investigate how we can integrate Polly into our current implementation
+* Other
+ * Performance benchmarking
+* Initializr (released separately from Steeltoe codebase)
+ * [Steeltoe Initializr](https://github.com/SteeltoeOSS/Initializr) -- Currently in Beta at [https://start.steeltoe.io](https://start.steeltoe.io)
+ * Create and maintainable and extensible application
+ * Update UI with new features
+
+### Other
+
+* Review and identify areas for refactoring and improvement across all components
+
+### Optional (if we have time)
+
+* Streaming Support
+ * Steeltoe Streams
+ * Kafka Binder
+ * Steeltoe Bus
+ * Ability to link nodes of a distributed system with a message broker
+ * Dependent on Steeltoe Streams project
+ * Provide auto-update of configuration properties across microservice applications
+ * Dependent on Steeltoe Streams and Steeltoe Bus implementation
+* Metrics Enhancements
+ * Instrumentation for Hystrix to add tracing and stats
+ * Instrumentation for EFCore to add tracing and stats
+ * Instrumentation for Connectors to add tracing and stats
+* Connectors
+ * Add Kafka connector
diff --git a/roadmaps/3.2.0.md b/roadmaps/3.2.0.md
new file mode 100644
index 0000000000..0c30471a22
--- /dev/null
+++ b/roadmaps/3.2.0.md
@@ -0,0 +1,23 @@
+# Release 3.2.0 GA
+
+Anticipated Release Date: First half of 2022
+
+## General Availability Enhancements and Features
+
+>*Note: Listed features are subject to change*
+
+### Features, Enhancements
+
+* Support for .NET 6
+ * WebApplicationBuilder extensions
+ * ConfigurationManager support
+* Support polling Spring Cloud Config Server for updates
+* Management
+ * Update Thread and Heap Dump implementations
+ * Depend on a GA release of OpenTelemetry Metrics
+ * Support exporting metrics and traces to Wavefront
+* Enhance discovery-first config server feature to work with servers other than Eureka
+
+### Other
+
+* Review and identify areas for refactoring and improvement across all components
diff --git a/roadmaps/4.0.0.md b/roadmaps/4.0.0.md
new file mode 100644
index 0000000000..7ecc0cbf51
--- /dev/null
+++ b/roadmaps/4.0.0.md
@@ -0,0 +1,41 @@
+# Release 4.0.0 GA
+
+Anticipated Release Date: Late 2023
+
+## General Availability Enhancements and Features
+
+>*Note: Listed features are subject to change*
+
+### Features, Enhancements
+
+* Public API Surface Area Review
+* Configuration
+ * [possibly research-only] Support for application configuration service on TAP & ASA
+ * Support for Spring Cloud Kubernetes Config Server
+* Connectors
+ * Support for the [Kubernetes Service Binding Specification](https://github.com/servicebinding/spec)
+ * Refactor to simpler implementation for easier maintenance
+* Management
+ * Actuators available on an alternate port
+ * Heap and thread dumps available from a sidecar
+* Service Discovery
+ * Blue/Green deployments through endpoints
+ * Use endpoint to set registered instances to `offline`
+ * Support for Spring Cloud Kubernetes Discovery Server
+
+### Other
+
+* Refactoring and improvement across all components
+
+### Optional (if we have time)
+
+* Performance benchmarking
+* Enhanced compatibility with runtime configuration, trimming, hot reload, R2R
+* Streaming Support
+ * Steeltoe Bus
+ * Ability to link nodes of a distributed system with a message broker
+ * Dependent on Steeltoe Stream project
+ * Provide auto-update of configuration properties across microservice applications
+ * Dependent on Steeltoe Stream and Steeltoe Bus implementation
+* Connectors
+ * Add Kafka connector
diff --git a/shared-package.props b/shared-package.props
new file mode 100644
index 0000000000..92994674f0
--- /dev/null
+++ b/shared-package.props
@@ -0,0 +1,61 @@
+
+
+
+ $(NoWarn);CS1591;IDE0028;IDE0300;IDE0301;IDE0302;IDE0303;IDE0304;IDE0305;IDE0306
+ true
+
+
+
+ Broadcom
+ https://steeltoe.io/images/transparent.png
+ icon.png
+ https://steeltoe.io
+ Apache-2.0
+ false
+ See https://github.com/SteeltoeOSS/Steeltoe/releases.
+ PackageReadme.md
+
+
+
+ true
+ embedded
+ true
+
+
+
+ True
+
+
+
+
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/shared-project.props b/shared-project.props
new file mode 100644
index 0000000000..2dfa2fa8de
--- /dev/null
+++ b/shared-project.props
@@ -0,0 +1,8 @@
+
+
+ $(NoWarn);SA0001
+ false
+
+
+
+
diff --git a/shared-test.props b/shared-test.props
new file mode 100644
index 0000000000..f3c9188d2c
--- /dev/null
+++ b/shared-test.props
@@ -0,0 +1,37 @@
+
+
+ $(NoWarn);S2094;SA1602;CA1062;CA1707;NU5104
+
+
+
+
+ false
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ PreserveNewest
+
+
+
diff --git a/shared.props b/shared.props
new file mode 100644
index 0000000000..faa0a95dcb
--- /dev/null
+++ b/shared.props
@@ -0,0 +1,55 @@
+
+
+ latest
+ enable
+ enable
+ true
+ Recommended
+ false
+ false
+ false
+ false
+ false
+
+
+
+
+ $(NoWarn)IDE0051;IDE0052
+
+
+
+ $(MSBuildThisFileDirectory)\Steeltoe.Debug.ruleset
+
+
+
+ true
+ true
+ $(MSBuildThisFileDirectory)\Steeltoe.Release.ruleset
+
+
+
+
+
+
+
+
+ <_Parameter1>false
+
+
+ <_Parameter1>false
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/.idea/.idea.Steeltoe.All/.idea/.gitignore b/src/.idea/.idea.Steeltoe.All/.idea/.gitignore
new file mode 100644
index 0000000000..1f3c1ae240
--- /dev/null
+++ b/src/.idea/.idea.Steeltoe.All/.idea/.gitignore
@@ -0,0 +1 @@
+# Empty .gitignore file to prevent Rider from adding one
diff --git a/src/.idea/.idea.Steeltoe.All/.idea/codeStyles/Project.xml b/src/.idea/.idea.Steeltoe.All/.idea/codeStyles/Project.xml
new file mode 100644
index 0000000000..0ef40e2e39
--- /dev/null
+++ b/src/.idea/.idea.Steeltoe.All/.idea/codeStyles/Project.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/.idea/.idea.Steeltoe.All/.idea/codeStyles/codeStyleConfig.xml b/src/.idea/.idea.Steeltoe.All/.idea/codeStyles/codeStyleConfig.xml
new file mode 100644
index 0000000000..a36471a052
--- /dev/null
+++ b/src/.idea/.idea.Steeltoe.All/.idea/codeStyles/codeStyleConfig.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Bootstrap/src/AutoConfiguration/BootstrapScanner.cs b/src/Bootstrap/src/AutoConfiguration/BootstrapScanner.cs
new file mode 100644
index 0000000000..2c1b85a862
--- /dev/null
+++ b/src/Bootstrap/src/AutoConfiguration/BootstrapScanner.cs
@@ -0,0 +1,284 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the Apache 2.0 License.
+// See the LICENSE file in the project root for more information.
+
+using Microsoft.Extensions.Logging;
+using Steeltoe.Common;
+using Steeltoe.Common.DynamicTypeAccess;
+using Steeltoe.Common.Hosting;
+using Steeltoe.Common.Logging;
+using Steeltoe.Configuration.CloudFoundry;
+using Steeltoe.Configuration.ConfigServer;
+using Steeltoe.Configuration.Encryption;
+using Steeltoe.Configuration.Placeholder;
+using Steeltoe.Configuration.RandomValue;
+using Steeltoe.Configuration.SpringBoot;
+using Steeltoe.Connectors.CosmosDb;
+using Steeltoe.Connectors.CosmosDb.DynamicTypeAccess;
+using Steeltoe.Connectors.MongoDb;
+using Steeltoe.Connectors.MongoDb.DynamicTypeAccess;
+using Steeltoe.Connectors.MySql;
+using Steeltoe.Connectors.MySql.DynamicTypeAccess;
+using Steeltoe.Connectors.PostgreSql;
+using Steeltoe.Connectors.PostgreSql.DynamicTypeAccess;
+using Steeltoe.Connectors.RabbitMQ;
+using Steeltoe.Connectors.RabbitMQ.DynamicTypeAccess;
+using Steeltoe.Connectors.Redis;
+using Steeltoe.Connectors.Redis.DynamicTypeAccess;
+using Steeltoe.Connectors.SqlServer;
+using Steeltoe.Connectors.SqlServer.RuntimeTypeAccess;
+using Steeltoe.Discovery.Configuration;
+using Steeltoe.Discovery.Consul;
+using Steeltoe.Discovery.Eureka;
+using Steeltoe.Logging.DynamicConsole;
+using Steeltoe.Logging.DynamicSerilog;
+using Steeltoe.Management.Endpoint.Actuators.All;
+using Steeltoe.Management.Prometheus;
+using Steeltoe.Management.Tracing;
+
+namespace Steeltoe.Bootstrap.AutoConfiguration;
+
+internal sealed class BootstrapScanner
+{
+ private readonly HostBuilderWrapper _wrapper;
+ private readonly AssemblyLoader _loader;
+ private readonly ILoggerFactory _loggerFactory;
+ private readonly ILogger _logger;
+
+ public BootstrapScanner(HostBuilderWrapper wrapper, IReadOnlySet assemblyNamesToExclude, ILoggerFactory loggerFactory)
+ {
+ ArgumentNullException.ThrowIfNull(wrapper);
+ ArgumentNullException.ThrowIfNull(assemblyNamesToExclude);
+ ArgumentGuard.ElementsNotNullOrWhiteSpace(assemblyNamesToExclude);
+ ArgumentNullException.ThrowIfNull(loggerFactory);
+
+ _wrapper = wrapper;
+ _loader = new AssemblyLoader(assemblyNamesToExclude);
+ _loggerFactory = loggerFactory;
+ _logger = loggerFactory.CreateLogger("Steeltoe.Bootstrap.AutoConfiguration");
+ }
+
+ public void ConfigureSteeltoe()
+ {
+ if (_loggerFactory is BootstrapLoggerFactory bootstrapLoggerFactory)
+ {
+ _wrapper.ConfigureServices(services => services.UpgradeBootstrapLoggerFactory(bootstrapLoggerFactory));
+ }
+
+ if (!WireIfLoaded(WireConfigServer, SteeltoeAssemblyNames.ConfigurationConfigServer))
+ {
+ WireIfLoaded(WireCloudFoundryConfiguration, SteeltoeAssemblyNames.ConfigurationCloudFoundry);
+ }
+
+ WireIfLoaded(WireRandomValueProvider, SteeltoeAssemblyNames.ConfigurationRandomValue);
+ WireIfLoaded(WireSpringBootProvider, SteeltoeAssemblyNames.ConfigurationSpringBoot);
+ WireIfLoaded(WireDecryptionProvider, SteeltoeAssemblyNames.ConfigurationEncryption);
+ WireIfLoaded(WirePlaceholderResolver, SteeltoeAssemblyNames.ConfigurationPlaceholder);
+ WireIfLoaded(WireConnectors, SteeltoeAssemblyNames.Connectors);
+
+ if (!WireIfLoaded(WireDynamicSerilog, SteeltoeAssemblyNames.LoggingDynamicSerilog))
+ {
+ WireIfLoaded(WireDynamicConsole, SteeltoeAssemblyNames.LoggingDynamicConsole);
+ }
+
+ WireIfLoaded(WireDiscoveryConfiguration, SteeltoeAssemblyNames.DiscoveryConfiguration);
+ WireIfLoaded(WireDiscoveryConsul, SteeltoeAssemblyNames.DiscoveryConsul);
+ WireIfLoaded(WireDiscoveryEureka, SteeltoeAssemblyNames.DiscoveryEureka);
+ WireIfLoaded(WireAllActuators, SteeltoeAssemblyNames.ManagementEndpoint);
+ WireIfLoaded(WirePrometheus, SteeltoeAssemblyNames.ManagementPrometheus);
+ WireIfLoaded(WireDistributedTracingLogProcessor, SteeltoeAssemblyNames.ManagementTracing);
+ }
+
+ private void WireConfigServer()
+ {
+ _wrapper.AddConfigServer(_loggerFactory);
+
+ _logger.LogInformation("Configured Config Server configuration provider");
+ }
+
+ private void WireCloudFoundryConfiguration()
+ {
+ _wrapper.AddCloudFoundryConfiguration(_loggerFactory);
+
+ _logger.LogInformation("Configured Cloud Foundry configuration provider");
+ }
+
+ private void WireRandomValueProvider()
+ {
+ _wrapper.ConfigureAppConfiguration(configurationBuilder => configurationBuilder.AddRandomValueSource(_loggerFactory));
+
+ _logger.LogInformation("Configured random value configuration provider");
+ }
+
+ private void WireSpringBootProvider()
+ {
+ _wrapper.ConfigureAppConfiguration(configurationBuilder => configurationBuilder.AddSpringBootFromEnvironmentVariable(_loggerFactory));
+
+ string[] args = Environment.GetCommandLineArgs().Skip(1).ToArray();
+ _wrapper.ConfigureAppConfiguration(configurationBuilder => configurationBuilder.AddSpringBootFromCommandLine(args, _loggerFactory));
+
+ _logger.LogInformation("Configured Spring Boot configuration provider");
+ }
+
+ private void WireDecryptionProvider()
+ {
+ _wrapper.ConfigureAppConfiguration(configurationBuilder => configurationBuilder.AddDecryption(_loggerFactory));
+
+ _logger.LogInformation("Configured decryption configuration provider");
+ }
+
+ private void WirePlaceholderResolver()
+ {
+ _wrapper.ConfigureAppConfiguration(configurationBuilder => configurationBuilder.AddPlaceholderResolver(_loggerFactory));
+
+ _logger.LogInformation("Configured placeholder configuration provider");
+ }
+
+ private void WireConnectors()
+ {
+ WireIfAnyLoaded(WireCosmosDbConnector, CosmosDbPackageResolver.Default);
+ WireIfAnyLoaded(WireMongoDbConnector, MongoDbPackageResolver.Default);
+ WireIfAnyLoaded(WireMySqlConnector, MySqlPackageResolver.Default);
+ WireIfAnyLoaded(WirePostgreSqlConnector, PostgreSqlPackageResolver.Default);
+ WireIfAnyLoaded(WireRabbitMQConnector, RabbitMQPackageResolver.Default);
+ WireIfAnyLoaded(WireRedisConnector, StackExchangeRedisPackageResolver.Default, MicrosoftRedisPackageResolver.Default);
+ WireIfAnyLoaded(WireSqlServerConnector, SqlServerPackageResolver.Default);
+ }
+
+ private void WireCosmosDbConnector()
+ {
+ _wrapper.ConfigureAppConfiguration(configurationBuilder => configurationBuilder.ConfigureCosmosDb());
+ _wrapper.ConfigureServices((host, services) => services.AddCosmosDb(host.Configuration));
+
+ _logger.LogInformation("Configured CosmosDB connector");
+ }
+
+ private void WireMongoDbConnector()
+ {
+ _wrapper.ConfigureAppConfiguration(configurationBuilder => configurationBuilder.ConfigureMongoDb());
+ _wrapper.ConfigureServices((host, services) => services.AddMongoDb(host.Configuration));
+
+ _logger.LogInformation("Configured MongoDB connector");
+ }
+
+ private void WireMySqlConnector()
+ {
+ _wrapper.ConfigureAppConfiguration(configurationBuilder => configurationBuilder.ConfigureMySql());
+ _wrapper.ConfigureServices((host, services) => services.AddMySql(host.Configuration));
+
+ _logger.LogInformation("Configured MySQL connector");
+ }
+
+ private void WirePostgreSqlConnector()
+ {
+ _wrapper.ConfigureAppConfiguration(configurationBuilder => configurationBuilder.ConfigurePostgreSql());
+ _wrapper.ConfigureServices((host, services) => services.AddPostgreSql(host.Configuration));
+
+ _logger.LogInformation("Configured PostgreSQL connector");
+ }
+
+ private void WireRabbitMQConnector()
+ {
+ _wrapper.ConfigureAppConfiguration(configurationBuilder => configurationBuilder.ConfigureRabbitMQ());
+ _wrapper.ConfigureServices((host, services) => services.AddRabbitMQ(host.Configuration));
+
+ _logger.LogInformation("Configured RabbitMQ connector");
+ }
+
+ private void WireRedisConnector()
+ {
+ _wrapper.ConfigureAppConfiguration(configurationBuilder => configurationBuilder.ConfigureRedis());
+ _wrapper.ConfigureServices((host, services) => services.AddRedis(host.Configuration));
+
+ _logger.LogInformation("Configured StackExchange Redis connector");
+
+ // Intentionally ignoring excluded assemblies here.
+ if (MicrosoftRedisPackageResolver.Default.IsAvailable())
+ {
+ _logger.LogInformation("Configured Redis distributed cache connector");
+ }
+ }
+
+ private void WireSqlServerConnector()
+ {
+ _wrapper.ConfigureAppConfiguration(configurationBuilder => configurationBuilder.ConfigureSqlServer());
+ _wrapper.ConfigureServices((host, services) => services.AddSqlServer(host.Configuration));
+
+ _logger.LogInformation("Configured SQL Server connector");
+ }
+
+ private void WireDynamicSerilog()
+ {
+ _wrapper.ConfigureLogging(loggingBuilder => loggingBuilder.AddDynamicSerilog());
+
+ _logger.LogInformation("Configured dynamic console logger for Serilog");
+ }
+
+ private void WireDynamicConsole()
+ {
+ _wrapper.ConfigureLogging(loggingBuilder => loggingBuilder.AddDynamicConsole());
+
+ _logger.LogInformation("Configured dynamic console logger");
+ }
+
+ private void WireDiscoveryConfiguration()
+ {
+ _wrapper.ConfigureServices(services => services.AddConfigurationDiscoveryClient());
+
+ _logger.LogInformation("Configured configuration discovery client");
+ }
+
+ private void WireDiscoveryConsul()
+ {
+ _wrapper.ConfigureServices(services => services.AddConsulDiscoveryClient());
+
+ _logger.LogInformation("Configured Consul discovery client");
+ }
+
+ private void WireDiscoveryEureka()
+ {
+ _wrapper.ConfigureServices(services => services.AddEurekaDiscoveryClient());
+
+ _logger.LogInformation("Configured Eureka discovery client");
+ }
+
+ private void WireAllActuators()
+ {
+ _wrapper.ConfigureServices(services => services.AddAllActuators());
+
+ _logger.LogInformation("Configured actuators");
+ }
+
+ private void WirePrometheus()
+ {
+ _wrapper.ConfigureServices(services => services.AddPrometheusActuator());
+
+ _logger.LogInformation("Configured Prometheus");
+ }
+
+ private void WireDistributedTracingLogProcessor()
+ {
+ _wrapper.ConfigureServices(services => services.AddTracingLogProcessor());
+
+ _logger.LogInformation("Configured distributed tracing log processor");
+ }
+
+ private bool WireIfLoaded(Action wireAction, string assemblyName)
+ {
+ if (!_loader.IsAssemblyLoaded(assemblyName))
+ {
+ return false;
+ }
+
+ wireAction();
+ return true;
+ }
+
+ private void WireIfAnyLoaded(Action wireAction, params PackageResolver[] packageResolvers)
+ {
+ if (Array.Exists(packageResolvers, packageResolver => packageResolver.IsAvailable(_loader.AssemblyNamesToExclude)))
+ {
+ wireAction();
+ }
+ }
+}
diff --git a/src/Bootstrap/src/AutoConfiguration/HostApplicationBuilderExtensions.cs b/src/Bootstrap/src/AutoConfiguration/HostApplicationBuilderExtensions.cs
new file mode 100644
index 0000000000..5231ea3e27
--- /dev/null
+++ b/src/Bootstrap/src/AutoConfiguration/HostApplicationBuilderExtensions.cs
@@ -0,0 +1,99 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the Apache 2.0 License.
+// See the LICENSE file in the project root for more information.
+
+using System.Collections.Immutable;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+using Steeltoe.Common;
+using Steeltoe.Common.Hosting;
+using Steeltoe.Common.Logging;
+
+namespace Steeltoe.Bootstrap.AutoConfiguration;
+
+public static class HostApplicationBuilderExtensions
+{
+ private static readonly IReadOnlySet EmptySet = ImmutableHashSet.Empty;
+
+ ///
+ /// Automatically configures Steeltoe packages that have been added to your project as NuGet references.
+ ///
+ ///
+ /// The to configure.
+ ///
+ ///
+ /// The incoming so that additional calls can be chained.
+ ///
+ public static IHostApplicationBuilder AddSteeltoe(this IHostApplicationBuilder builder)
+ {
+ return AddSteeltoe(builder, EmptySet, BootstrapLoggerFactory.CreateConsole());
+ }
+
+ ///
+ /// Automatically configures Steeltoe packages that have been added to your project as NuGet references.
+ ///
+ ///
+ /// The to configure.
+ ///
+ ///
+ /// The set of assembly names to exclude from autoconfiguration. For ease of use, select from the constants in .
+ ///
+ ///
+ /// The incoming so that additional calls can be chained.
+ ///
+ public static IHostApplicationBuilder AddSteeltoe(this IHostApplicationBuilder builder, IReadOnlySet assemblyNamesToExclude)
+ {
+ return AddSteeltoe(builder, assemblyNamesToExclude, BootstrapLoggerFactory.CreateConsole());
+ }
+
+ ///
+ /// Automatically configures Steeltoe packages that have been added to your project as NuGet references.
+ ///
+ ///
+ /// The to configure.
+ ///
+ ///
+ /// Used for internal logging. Pass to disable logging, or use to write
+ /// only to the console until logging is fully initialized.
+ ///
+ ///
+ /// The incoming so that additional calls can be chained.
+ ///
+ public static IHostApplicationBuilder AddSteeltoe(this IHostApplicationBuilder builder, ILoggerFactory loggerFactory)
+ {
+ return AddSteeltoe(builder, EmptySet, loggerFactory);
+ }
+
+ ///
+ /// Automatically configures Steeltoe packages that have been added to your project as NuGet references.
+ ///
+ ///
+ /// The to configure.
+ ///
+ ///
+ /// The set of assembly names to exclude from autoconfiguration. For ease of use, select from the constants in .
+ ///
+ ///
+ /// Used for internal logging. Pass to disable logging, or use to write
+ /// only to the console until logging is fully initialized.
+ ///
+ ///
+ /// The incoming so that additional calls can be chained.
+ ///
+ public static IHostApplicationBuilder AddSteeltoe(this IHostApplicationBuilder builder, IReadOnlySet assemblyNamesToExclude,
+ ILoggerFactory loggerFactory)
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+ ArgumentNullException.ThrowIfNull(assemblyNamesToExclude);
+ ArgumentGuard.ElementsNotNullOrWhiteSpace(assemblyNamesToExclude);
+ ArgumentNullException.ThrowIfNull(loggerFactory);
+
+ HostBuilderWrapper wrapper = HostBuilderWrapper.Wrap(builder);
+
+ var scanner = new BootstrapScanner(wrapper, assemblyNamesToExclude, loggerFactory);
+ scanner.ConfigureSteeltoe();
+
+ return builder;
+ }
+}
diff --git a/src/Bootstrap/src/AutoConfiguration/HostBuilderExtensions.cs b/src/Bootstrap/src/AutoConfiguration/HostBuilderExtensions.cs
new file mode 100644
index 0000000000..f0311ca709
--- /dev/null
+++ b/src/Bootstrap/src/AutoConfiguration/HostBuilderExtensions.cs
@@ -0,0 +1,98 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the Apache 2.0 License.
+// See the LICENSE file in the project root for more information.
+
+using System.Collections.Immutable;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+using Steeltoe.Common;
+using Steeltoe.Common.Hosting;
+using Steeltoe.Common.Logging;
+
+namespace Steeltoe.Bootstrap.AutoConfiguration;
+
+public static class HostBuilderExtensions
+{
+ private static readonly IReadOnlySet EmptySet = ImmutableHashSet.Empty;
+
+ ///
+ /// Automatically configures Steeltoe packages that have been added to your project as NuGet references.
+ ///
+ ///
+ /// The to configure.
+ ///
+ ///
+ /// The incoming so that additional calls can be chained.
+ ///
+ public static IHostBuilder AddSteeltoe(this IHostBuilder builder)
+ {
+ return AddSteeltoe(builder, EmptySet, BootstrapLoggerFactory.CreateConsole());
+ }
+
+ ///
+ /// Automatically configures Steeltoe packages that have been added to your project as NuGet references.
+ ///
+ ///
+ /// The to configure.
+ ///
+ ///
+ /// The set of assembly names to exclude from autoconfiguration. For ease of use, select from the constants in .
+ ///
+ ///
+ /// The incoming so that additional calls can be chained.
+ ///
+ public static IHostBuilder AddSteeltoe(this IHostBuilder builder, IReadOnlySet assemblyNamesToExclude)
+ {
+ return AddSteeltoe(builder, assemblyNamesToExclude, BootstrapLoggerFactory.CreateConsole());
+ }
+
+ ///
+ /// Automatically configures Steeltoe packages that have been added to your project as NuGet references.
+ ///
+ ///
+ /// The to configure.
+ ///
+ ///
+ /// Used for internal logging. Pass to disable logging, or use to write
+ /// only to the console until logging is fully initialized.
+ ///
+ ///
+ /// The incoming so that additional calls can be chained.
+ ///
+ public static IHostBuilder AddSteeltoe(this IHostBuilder builder, ILoggerFactory loggerFactory)
+ {
+ return AddSteeltoe(builder, EmptySet, loggerFactory);
+ }
+
+ ///
+ /// Automatically configures Steeltoe packages that have been added to your project as NuGet references.
+ ///
+ ///
+ /// The to configure.
+ ///
+ ///
+ /// The set of assembly names to exclude from autoconfiguration. For ease of use, select from the constants in .
+ ///
+ ///
+ /// Used for internal logging. Pass to disable logging, or use to write
+ /// only to the console until logging is fully initialized.
+ ///
+ ///
+ /// The incoming so that additional calls can be chained.
+ ///
+ public static IHostBuilder AddSteeltoe(this IHostBuilder builder, IReadOnlySet assemblyNamesToExclude, ILoggerFactory loggerFactory)
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+ ArgumentNullException.ThrowIfNull(assemblyNamesToExclude);
+ ArgumentGuard.ElementsNotNullOrWhiteSpace(assemblyNamesToExclude);
+ ArgumentNullException.ThrowIfNull(loggerFactory);
+
+ HostBuilderWrapper wrapper = HostBuilderWrapper.Wrap(builder);
+
+ var scanner = new BootstrapScanner(wrapper, assemblyNamesToExclude, loggerFactory);
+ scanner.ConfigureSteeltoe();
+
+ return builder;
+ }
+}
diff --git a/src/Bootstrap/src/AutoConfiguration/Properties/AssemblyInfo.cs b/src/Bootstrap/src/AutoConfiguration/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000000..84aa78e3ca
--- /dev/null
+++ b/src/Bootstrap/src/AutoConfiguration/Properties/AssemblyInfo.cs
@@ -0,0 +1,7 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the Apache 2.0 License.
+// See the LICENSE file in the project root for more information.
+
+using System.Runtime.CompilerServices;
+
+[assembly: InternalsVisibleTo("Steeltoe.Bootstrap.AutoConfiguration.Test")]
diff --git a/src/Bootstrap/src/AutoConfiguration/PublicAPI.Shipped.txt b/src/Bootstrap/src/AutoConfiguration/PublicAPI.Shipped.txt
new file mode 100644
index 0000000000..5f282702bb
--- /dev/null
+++ b/src/Bootstrap/src/AutoConfiguration/PublicAPI.Shipped.txt
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/Bootstrap/src/AutoConfiguration/PublicAPI.Unshipped.txt b/src/Bootstrap/src/AutoConfiguration/PublicAPI.Unshipped.txt
new file mode 100644
index 0000000000..e7e163c397
--- /dev/null
+++ b/src/Bootstrap/src/AutoConfiguration/PublicAPI.Unshipped.txt
@@ -0,0 +1,32 @@
+#nullable enable
+const Steeltoe.Bootstrap.AutoConfiguration.SteeltoeAssemblyNames.ConfigurationCloudFoundry = "Steeltoe.Configuration.CloudFoundry" -> string!
+const Steeltoe.Bootstrap.AutoConfiguration.SteeltoeAssemblyNames.ConfigurationConfigServer = "Steeltoe.Configuration.ConfigServer" -> string!
+const Steeltoe.Bootstrap.AutoConfiguration.SteeltoeAssemblyNames.ConfigurationEncryption = "Steeltoe.Configuration.Encryption" -> string!
+const Steeltoe.Bootstrap.AutoConfiguration.SteeltoeAssemblyNames.ConfigurationPlaceholder = "Steeltoe.Configuration.Placeholder" -> string!
+const Steeltoe.Bootstrap.AutoConfiguration.SteeltoeAssemblyNames.ConfigurationRandomValue = "Steeltoe.Configuration.RandomValue" -> string!
+const Steeltoe.Bootstrap.AutoConfiguration.SteeltoeAssemblyNames.ConfigurationSpringBoot = "Steeltoe.Configuration.SpringBoot" -> string!
+const Steeltoe.Bootstrap.AutoConfiguration.SteeltoeAssemblyNames.Connectors = "Steeltoe.Connectors" -> string!
+const Steeltoe.Bootstrap.AutoConfiguration.SteeltoeAssemblyNames.DiscoveryConfiguration = "Steeltoe.Discovery.Configuration" -> string!
+const Steeltoe.Bootstrap.AutoConfiguration.SteeltoeAssemblyNames.DiscoveryConsul = "Steeltoe.Discovery.Consul" -> string!
+const Steeltoe.Bootstrap.AutoConfiguration.SteeltoeAssemblyNames.DiscoveryEureka = "Steeltoe.Discovery.Eureka" -> string!
+const Steeltoe.Bootstrap.AutoConfiguration.SteeltoeAssemblyNames.LoggingDynamicConsole = "Steeltoe.Logging.DynamicConsole" -> string!
+const Steeltoe.Bootstrap.AutoConfiguration.SteeltoeAssemblyNames.LoggingDynamicSerilog = "Steeltoe.Logging.DynamicSerilog" -> string!
+const Steeltoe.Bootstrap.AutoConfiguration.SteeltoeAssemblyNames.ManagementEndpoint = "Steeltoe.Management.Endpoint" -> string!
+const Steeltoe.Bootstrap.AutoConfiguration.SteeltoeAssemblyNames.ManagementPrometheus = "Steeltoe.Management.Prometheus" -> string!
+const Steeltoe.Bootstrap.AutoConfiguration.SteeltoeAssemblyNames.ManagementTracing = "Steeltoe.Management.Tracing" -> string!
+static Steeltoe.Bootstrap.AutoConfiguration.HostApplicationBuilderExtensions.AddSteeltoe(this Microsoft.Extensions.Hosting.IHostApplicationBuilder! builder) -> Microsoft.Extensions.Hosting.IHostApplicationBuilder!
+static Steeltoe.Bootstrap.AutoConfiguration.HostApplicationBuilderExtensions.AddSteeltoe(this Microsoft.Extensions.Hosting.IHostApplicationBuilder! builder, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory) -> Microsoft.Extensions.Hosting.IHostApplicationBuilder!
+static Steeltoe.Bootstrap.AutoConfiguration.HostApplicationBuilderExtensions.AddSteeltoe(this Microsoft.Extensions.Hosting.IHostApplicationBuilder! builder, System.Collections.Generic.IReadOnlySet! assemblyNamesToExclude) -> Microsoft.Extensions.Hosting.IHostApplicationBuilder!
+static Steeltoe.Bootstrap.AutoConfiguration.HostApplicationBuilderExtensions.AddSteeltoe(this Microsoft.Extensions.Hosting.IHostApplicationBuilder! builder, System.Collections.Generic.IReadOnlySet! assemblyNamesToExclude, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory) -> Microsoft.Extensions.Hosting.IHostApplicationBuilder!
+static Steeltoe.Bootstrap.AutoConfiguration.HostBuilderExtensions.AddSteeltoe(this Microsoft.Extensions.Hosting.IHostBuilder! builder) -> Microsoft.Extensions.Hosting.IHostBuilder!
+static Steeltoe.Bootstrap.AutoConfiguration.HostBuilderExtensions.AddSteeltoe(this Microsoft.Extensions.Hosting.IHostBuilder! builder, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory) -> Microsoft.Extensions.Hosting.IHostBuilder!
+static Steeltoe.Bootstrap.AutoConfiguration.HostBuilderExtensions.AddSteeltoe(this Microsoft.Extensions.Hosting.IHostBuilder! builder, System.Collections.Generic.IReadOnlySet! assemblyNamesToExclude) -> Microsoft.Extensions.Hosting.IHostBuilder!
+static Steeltoe.Bootstrap.AutoConfiguration.HostBuilderExtensions.AddSteeltoe(this Microsoft.Extensions.Hosting.IHostBuilder! builder, System.Collections.Generic.IReadOnlySet! assemblyNamesToExclude, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory) -> Microsoft.Extensions.Hosting.IHostBuilder!
+static Steeltoe.Bootstrap.AutoConfiguration.WebHostBuilderExtensions.AddSteeltoe(this Microsoft.AspNetCore.Hosting.IWebHostBuilder! builder) -> Microsoft.AspNetCore.Hosting.IWebHostBuilder!
+static Steeltoe.Bootstrap.AutoConfiguration.WebHostBuilderExtensions.AddSteeltoe(this Microsoft.AspNetCore.Hosting.IWebHostBuilder! builder, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory) -> Microsoft.AspNetCore.Hosting.IWebHostBuilder!
+static Steeltoe.Bootstrap.AutoConfiguration.WebHostBuilderExtensions.AddSteeltoe(this Microsoft.AspNetCore.Hosting.IWebHostBuilder! builder, System.Collections.Generic.IReadOnlySet! assemblyNamesToExclude) -> Microsoft.AspNetCore.Hosting.IWebHostBuilder!
+static Steeltoe.Bootstrap.AutoConfiguration.WebHostBuilderExtensions.AddSteeltoe(this Microsoft.AspNetCore.Hosting.IWebHostBuilder! builder, System.Collections.Generic.IReadOnlySet! assemblyNamesToExclude, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory) -> Microsoft.AspNetCore.Hosting.IWebHostBuilder!
+Steeltoe.Bootstrap.AutoConfiguration.HostApplicationBuilderExtensions
+Steeltoe.Bootstrap.AutoConfiguration.HostBuilderExtensions
+Steeltoe.Bootstrap.AutoConfiguration.SteeltoeAssemblyNames
+Steeltoe.Bootstrap.AutoConfiguration.WebHostBuilderExtensions
diff --git a/src/Bootstrap/src/AutoConfiguration/Steeltoe.Bootstrap.AutoConfiguration.csproj b/src/Bootstrap/src/AutoConfiguration/Steeltoe.Bootstrap.AutoConfiguration.csproj
new file mode 100644
index 0000000000..106df9355a
--- /dev/null
+++ b/src/Bootstrap/src/AutoConfiguration/Steeltoe.Bootstrap.AutoConfiguration.csproj
@@ -0,0 +1,42 @@
+
+
+ net8.0
+ Automatically configure Steeltoe packages that are referenced by a project. This is not a meta package, other packages must be added separately.
+ autoconfiguration;automatic;configuration;application;bootstrapping;starter
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ False
+
+
+ False
+
+
+
diff --git a/src/Bootstrap/src/AutoConfiguration/SteeltoeAssemblyNames.cs b/src/Bootstrap/src/AutoConfiguration/SteeltoeAssemblyNames.cs
new file mode 100644
index 0000000000..1a70029b83
--- /dev/null
+++ b/src/Bootstrap/src/AutoConfiguration/SteeltoeAssemblyNames.cs
@@ -0,0 +1,35 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the Apache 2.0 License.
+// See the LICENSE file in the project root for more information.
+
+namespace Steeltoe.Bootstrap.AutoConfiguration;
+
+///
+/// Lists the names of Steeltoe assemblies that are used in autoconfiguration.
+///
+public static class SteeltoeAssemblyNames
+{
+ public const string ConfigurationCloudFoundry = "Steeltoe.Configuration.CloudFoundry";
+ public const string ConfigurationConfigServer = "Steeltoe.Configuration.ConfigServer";
+ public const string ConfigurationRandomValue = "Steeltoe.Configuration.RandomValue";
+ public const string ConfigurationSpringBoot = "Steeltoe.Configuration.SpringBoot";
+ public const string ConfigurationPlaceholder = "Steeltoe.Configuration.Placeholder";
+ public const string ConfigurationEncryption = "Steeltoe.Configuration.Encryption";
+ public const string Connectors = "Steeltoe.Connectors";
+ public const string DiscoveryConfiguration = "Steeltoe.Discovery.Configuration";
+ public const string DiscoveryConsul = "Steeltoe.Discovery.Consul";
+ public const string DiscoveryEureka = "Steeltoe.Discovery.Eureka";
+ public const string LoggingDynamicConsole = "Steeltoe.Logging.DynamicConsole";
+ public const string LoggingDynamicSerilog = "Steeltoe.Logging.DynamicSerilog";
+ public const string ManagementEndpoint = "Steeltoe.Management.Endpoint";
+ public const string ManagementPrometheus = "Steeltoe.Management.Prometheus";
+ public const string ManagementTracing = "Steeltoe.Management.Tracing";
+
+ internal static readonly IReadOnlySet All = typeof(SteeltoeAssemblyNames).GetFields().Where(field => field.FieldType == typeof(string))
+ .Select(field => field.GetValue(null)).Cast().ToHashSet();
+
+ internal static IReadOnlySet Only(string assemblyName)
+ {
+ return All.Except([assemblyName]).ToHashSet();
+ }
+}
diff --git a/src/Bootstrap/src/AutoConfiguration/WebHostBuilderExtensions.cs b/src/Bootstrap/src/AutoConfiguration/WebHostBuilderExtensions.cs
new file mode 100644
index 0000000000..398f36bb72
--- /dev/null
+++ b/src/Bootstrap/src/AutoConfiguration/WebHostBuilderExtensions.cs
@@ -0,0 +1,98 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the Apache 2.0 License.
+// See the LICENSE file in the project root for more information.
+
+using System.Collections.Immutable;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+using Steeltoe.Common;
+using Steeltoe.Common.Hosting;
+using Steeltoe.Common.Logging;
+
+namespace Steeltoe.Bootstrap.AutoConfiguration;
+
+public static class WebHostBuilderExtensions
+{
+ private static readonly IReadOnlySet EmptySet = ImmutableHashSet.Empty;
+
+ ///
+ /// Automatically configures Steeltoe packages that have been added to your project as NuGet references.
+ ///
+ ///
+ /// The to configure.
+ ///
+ ///
+ /// The incoming so that additional calls can be chained.
+ ///
+ public static IWebHostBuilder AddSteeltoe(this IWebHostBuilder builder)
+ {
+ return AddSteeltoe(builder, EmptySet, BootstrapLoggerFactory.CreateConsole());
+ }
+
+ ///
+ /// Automatically configures Steeltoe packages that have been added to your project as NuGet references.
+ ///
+ ///
+ /// The to configure.
+ ///
+ ///
+ /// The set of assembly names to exclude from autoconfiguration. For ease of use, select from the constants in .
+ ///
+ ///
+ /// The incoming so that additional calls can be chained.
+ ///
+ public static IWebHostBuilder AddSteeltoe(this IWebHostBuilder builder, IReadOnlySet assemblyNamesToExclude)
+ {
+ return AddSteeltoe(builder, assemblyNamesToExclude, BootstrapLoggerFactory.CreateConsole());
+ }
+
+ ///
+ /// Automatically configures Steeltoe packages that have been added to your project as NuGet references.
+ ///
+ ///
+ /// The to configure.
+ ///
+ ///
+ /// Used for internal logging. Pass to disable logging, or use to write
+ /// only to the console until logging is fully initialized.
+ ///
+ ///
+ /// The incoming so that additional calls can be chained.
+ ///
+ public static IWebHostBuilder AddSteeltoe(this IWebHostBuilder builder, ILoggerFactory loggerFactory)
+ {
+ return AddSteeltoe(builder, EmptySet, loggerFactory);
+ }
+
+ ///
+ /// Automatically configures Steeltoe packages that have been added to your project as NuGet references.
+ ///
+ ///
+ /// The to configure.
+ ///
+ ///
+ /// The set of assembly names to exclude from autoconfiguration. For ease of use, select from the constants in .
+ ///
+ ///
+ /// Used for internal logging. Pass to disable logging, or use to write
+ /// only to the console until logging is fully initialized.
+ ///
+ ///
+ /// The incoming so that additional calls can be chained.
+ ///
+ public static IWebHostBuilder AddSteeltoe(this IWebHostBuilder builder, IReadOnlySet assemblyNamesToExclude, ILoggerFactory loggerFactory)
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+ ArgumentNullException.ThrowIfNull(assemblyNamesToExclude);
+ ArgumentGuard.ElementsNotNullOrWhiteSpace(assemblyNamesToExclude);
+ ArgumentNullException.ThrowIfNull(loggerFactory);
+
+ HostBuilderWrapper wrapper = HostBuilderWrapper.Wrap(builder);
+
+ var scanner = new BootstrapScanner(wrapper, assemblyNamesToExclude, loggerFactory);
+ scanner.ConfigureSteeltoe();
+
+ return builder;
+ }
+}
diff --git a/src/Bootstrap/test/AutoConfiguration.Test/HostBuilderExtensionsTest.cs b/src/Bootstrap/test/AutoConfiguration.Test/HostBuilderExtensionsTest.cs
new file mode 100644
index 0000000000..773f54350d
--- /dev/null
+++ b/src/Bootstrap/test/AutoConfiguration.Test/HostBuilderExtensionsTest.cs
@@ -0,0 +1,455 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the Apache 2.0 License.
+// See the LICENSE file in the project root for more information.
+
+using System.Net;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.Azure.Cosmos;
+using Microsoft.Data.SqlClient;
+using Microsoft.Extensions.Caching.Distributed;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using MongoDB.Driver;
+using MySqlConnector;
+using Npgsql;
+using OpenTelemetry.Metrics;
+using RabbitMQ.Client;
+using StackExchange.Redis;
+using Steeltoe.Common;
+using Steeltoe.Common.Discovery;
+using Steeltoe.Common.TestResources;
+using Steeltoe.Configuration;
+using Steeltoe.Configuration.CloudFoundry;
+using Steeltoe.Configuration.CloudFoundry.ServiceBindings;
+using Steeltoe.Configuration.ConfigServer;
+using Steeltoe.Configuration.Encryption;
+using Steeltoe.Configuration.Kubernetes.ServiceBindings;
+using Steeltoe.Configuration.Placeholder;
+using Steeltoe.Configuration.RandomValue;
+using Steeltoe.Configuration.SpringBoot;
+using Steeltoe.Connectors;
+using Steeltoe.Connectors.CosmosDb;
+using Steeltoe.Connectors.MongoDb;
+using Steeltoe.Connectors.MySql;
+using Steeltoe.Connectors.PostgreSql;
+using Steeltoe.Connectors.RabbitMQ;
+using Steeltoe.Connectors.Redis;
+using Steeltoe.Connectors.SqlServer;
+using Steeltoe.Discovery.Configuration;
+using Steeltoe.Discovery.Consul;
+using Steeltoe.Discovery.Eureka;
+using Steeltoe.Logging;
+using Steeltoe.Logging.DynamicConsole;
+using Steeltoe.Logging.DynamicSerilog;
+using Steeltoe.Management.Endpoint;
+using Steeltoe.Management.Endpoint.Actuators.Hypermedia;
+using Steeltoe.Management.Tracing;
+
+namespace Steeltoe.Bootstrap.AutoConfiguration.Test;
+
+public sealed class HostBuilderExtensionsTest
+{
+ [Theory]
+ [InlineData(HostBuilderType.Host)]
+ [InlineData(HostBuilderType.WebHost)]
+ [InlineData(HostBuilderType.WebApplication)]
+ [InlineData(HostBuilderType.HostApplication)]
+ public async Task ConfigServerConfiguration_IsAutowired(HostBuilderType hostBuilderType)
+ {
+ await using HostWrapper hostWrapper = HostWrapperFactory.GetForOnly(SteeltoeAssemblyNames.ConfigurationConfigServer, hostBuilderType);
+
+ AssertConfigServerConfigurationIsAutowired(hostWrapper);
+ }
+
+ [Theory]
+ [InlineData(HostBuilderType.Host)]
+ [InlineData(HostBuilderType.WebHost)]
+ [InlineData(HostBuilderType.WebApplication)]
+ [InlineData(HostBuilderType.HostApplication)]
+ public async Task CloudFoundryConfiguration_IsAutowired(HostBuilderType hostBuilderType)
+ {
+ await using HostWrapper hostWrapper = HostWrapperFactory.GetForOnly(SteeltoeAssemblyNames.ConfigurationCloudFoundry, hostBuilderType);
+
+ AssertCloudFoundryConfigurationIsAutowired(hostWrapper);
+ }
+
+ [Theory]
+ [InlineData(HostBuilderType.Host)]
+ [InlineData(HostBuilderType.WebHost)]
+ [InlineData(HostBuilderType.WebApplication)]
+ [InlineData(HostBuilderType.HostApplication)]
+ public async Task RandomValueConfiguration_IsAutowired(HostBuilderType hostBuilderType)
+ {
+ await using HostWrapper hostWrapper = HostWrapperFactory.GetForOnly(SteeltoeAssemblyNames.ConfigurationRandomValue, hostBuilderType);
+
+ AssertRandomValueConfigurationIsAutowired(hostWrapper);
+ }
+
+ [Theory]
+ [InlineData(HostBuilderType.Host)]
+ [InlineData(HostBuilderType.WebHost)]
+ [InlineData(HostBuilderType.WebApplication)]
+ [InlineData(HostBuilderType.HostApplication)]
+ public async Task SpringBootConfiguration_IsAutowired(HostBuilderType hostBuilderType)
+ {
+ await using HostWrapper hostWrapper = HostWrapperFactory.GetForOnly(SteeltoeAssemblyNames.ConfigurationSpringBoot, hostBuilderType);
+
+ AssertSpringBootConfigurationIsAutowired(hostWrapper);
+ }
+
+ [Theory]
+ [InlineData(HostBuilderType.Host)]
+ [InlineData(HostBuilderType.WebHost)]
+ [InlineData(HostBuilderType.WebApplication)]
+ [InlineData(HostBuilderType.HostApplication)]
+ public async Task EncryptionConfiguration_IsAutowired(HostBuilderType hostBuilderType)
+ {
+ await using HostWrapper hostWrapper = HostWrapperFactory.GetForOnly(SteeltoeAssemblyNames.ConfigurationEncryption, hostBuilderType);
+
+ AssertEncryptionConfigurationIsAutowired(hostWrapper);
+ }
+
+ [Theory]
+ [InlineData(HostBuilderType.Host)]
+ [InlineData(HostBuilderType.WebHost)]
+ [InlineData(HostBuilderType.WebApplication)]
+ [InlineData(HostBuilderType.HostApplication)]
+ public async Task PlaceholderResolver_IsAutowired(HostBuilderType hostBuilderType)
+ {
+ await using HostWrapper hostWrapper = HostWrapperFactory.GetForOnly(SteeltoeAssemblyNames.ConfigurationPlaceholder, hostBuilderType);
+
+ AssertPlaceholderResolverIsAutowired(hostWrapper);
+ }
+
+ [Theory]
+ [InlineData(HostBuilderType.Host)]
+ [InlineData(HostBuilderType.WebHost)]
+ [InlineData(HostBuilderType.WebApplication)]
+ [InlineData(HostBuilderType.HostApplication)]
+ public async Task Connectors_AreAutowired(HostBuilderType hostBuilderType)
+ {
+ await using HostWrapper hostWrapper = HostWrapperFactory.GetForOnly(SteeltoeAssemblyNames.Connectors, hostBuilderType);
+
+ AssertConnectorsAreAutowired(hostWrapper);
+ }
+
+ [Theory]
+ [InlineData(HostBuilderType.Host)]
+ [InlineData(HostBuilderType.WebHost)]
+ [InlineData(HostBuilderType.WebApplication)]
+ [InlineData(HostBuilderType.HostApplication)]
+ public async Task SqlServerConnector_NotAutowiredIfDependenciesExcluded(HostBuilderType hostBuilderType)
+ {
+ var exclusions = new HashSet(SteeltoeAssemblyNames.All);
+ exclusions.Remove(SteeltoeAssemblyNames.Connectors);
+ exclusions.Add("Microsoft.Data.SqlClient");
+ exclusions.Add("System.Data.SqlClient");
+
+ await using HostWrapper hostWrapper = HostWrapperFactory.GetExcluding(exclusions, hostBuilderType);
+
+ hostWrapper.Services.GetService>().Should().BeNull();
+ }
+
+ [Theory]
+ [InlineData(HostBuilderType.Host)]
+ [InlineData(HostBuilderType.WebHost)]
+ [InlineData(HostBuilderType.WebApplication)]
+ [InlineData(HostBuilderType.HostApplication)]
+ public async Task DynamicSerilog_IsAutowired(HostBuilderType hostBuilderType)
+ {
+ await using HostWrapper hostWrapper = HostWrapperFactory.GetForOnly(SteeltoeAssemblyNames.LoggingDynamicSerilog, hostBuilderType);
+
+ AssertDynamicSerilogIsAutowired(hostWrapper);
+ }
+
+ [Theory]
+ [InlineData(HostBuilderType.Host)]
+ [InlineData(HostBuilderType.WebHost)]
+ [InlineData(HostBuilderType.WebApplication)]
+ [InlineData(HostBuilderType.HostApplication)]
+ public async Task DynamicConsoleLogger_IsAutowired(HostBuilderType hostBuilderType)
+ {
+ await using HostWrapper hostWrapper = HostWrapperFactory.GetForOnly(SteeltoeAssemblyNames.LoggingDynamicConsole, hostBuilderType);
+
+ AssertDynamicConsoleIsAutowired(hostWrapper);
+ }
+
+ [Theory]
+ [InlineData(HostBuilderType.Host)]
+ [InlineData(HostBuilderType.WebHost)]
+ [InlineData(HostBuilderType.WebApplication)]
+ [InlineData(HostBuilderType.HostApplication)]
+ public async Task ServiceDiscoveryClients_AreAutowired(HostBuilderType hostBuilderType)
+ {
+ var assembliesToInclude = new HashSet
+ {
+ SteeltoeAssemblyNames.DiscoveryConfiguration,
+ SteeltoeAssemblyNames.DiscoveryConsul,
+ SteeltoeAssemblyNames.DiscoveryEureka
+ };
+
+ await using HostWrapper hostWrapper =
+ HostWrapperFactory.GetExcluding(SteeltoeAssemblyNames.All.Except(assembliesToInclude).ToHashSet(), hostBuilderType);
+
+ AssertServiceDiscoveryClientsAreAutowired(hostWrapper);
+ }
+
+ [Theory]
+ [InlineData(HostBuilderType.Host)]
+ [InlineData(HostBuilderType.WebHost)]
+ [InlineData(HostBuilderType.WebApplication)]
+ [InlineData(HostBuilderType.HostApplication)]
+ public async Task Prometheus_IsAutowired(HostBuilderType hostBuilderType)
+ {
+ await using HostWrapper hostWrapper = HostWrapperFactory.GetForOnly(SteeltoeAssemblyNames.ManagementPrometheus, hostBuilderType);
+
+ AssertPrometheusIsAutowired(hostWrapper);
+ }
+
+ [Theory]
+ [InlineData(HostBuilderType.Host)]
+ [InlineData(HostBuilderType.WebHost)]
+ [InlineData(HostBuilderType.WebApplication)]
+ [InlineData(HostBuilderType.HostApplication)]
+ public async Task AllActuators_AreAutowired(HostBuilderType hostBuilderType)
+ {
+ await using HostWrapper hostWrapper = HostWrapperFactory.GetForOnly(SteeltoeAssemblyNames.ManagementEndpoint, hostBuilderType);
+ await hostWrapper.StartAsync(TestContext.Current.CancellationToken);
+
+ await AssertAllActuatorsAreAutowiredAsync(hostWrapper, true, TestContext.Current.CancellationToken);
+ }
+
+ [Theory]
+ [InlineData(HostBuilderType.Host)]
+ [InlineData(HostBuilderType.WebHost)]
+ [InlineData(HostBuilderType.WebApplication)]
+ [InlineData(HostBuilderType.HostApplication)]
+ public async Task Tracing_IsAutowired(HostBuilderType hostBuilderType)
+ {
+ await using HostWrapper hostWrapper = HostWrapperFactory.GetForOnly(SteeltoeAssemblyNames.ManagementTracing, hostBuilderType);
+
+ AssertTracingIsAutowired(hostWrapper);
+ }
+
+ [Theory]
+ [InlineData(HostBuilderType.Host)]
+ [InlineData(HostBuilderType.WebHost)]
+ [InlineData(HostBuilderType.WebApplication)]
+ [InlineData(HostBuilderType.HostApplication)]
+ public async Task Everything_IsAutowired(HostBuilderType hostBuilderType)
+ {
+ await using HostWrapper hostWrapper = HostWrapperFactory.GetExcluding(new HashSet(), hostBuilderType);
+
+ AssertConfigServerConfigurationIsAutowired(hostWrapper);
+ AssertCloudFoundryConfigurationIsAutowired(hostWrapper);
+ AssertRandomValueConfigurationIsAutowired(hostWrapper);
+ AssertSpringBootConfigurationIsAutowired(hostWrapper);
+ AssertEncryptionConfigurationIsAutowired(hostWrapper);
+ AssertPlaceholderResolverIsAutowired(hostWrapper);
+ AssertConnectorsAreAutowired(hostWrapper);
+ AssertDynamicSerilogIsAutowired(hostWrapper);
+ AssertServiceDiscoveryClientsAreAutowired(hostWrapper);
+ AssertPrometheusIsAutowired(hostWrapper);
+ AssertTracingIsAutowired(hostWrapper);
+
+ await hostWrapper.StartAsync(TestContext.Current.CancellationToken);
+
+ await AssertAllActuatorsAreAutowiredAsync(hostWrapper, false, TestContext.Current.CancellationToken);
+ }
+
+ private static void AssertConfigServerConfigurationIsAutowired(HostWrapper hostWrapper)
+ {
+ var configuration = hostWrapper.Services.GetRequiredService();
+
+ configuration.EnumerateProviders().Should().ContainSingle();
+ configuration.EnumerateProviders().Should().ContainSingle();
+ }
+
+ private static void AssertCloudFoundryConfigurationIsAutowired(HostWrapper hostWrapper)
+ {
+ var configuration = hostWrapper.Services.GetRequiredService();
+
+ configuration.EnumerateProviders().Should().ContainSingle();
+ }
+
+ private static void AssertRandomValueConfigurationIsAutowired(HostWrapper hostWrapper)
+ {
+ var configuration = hostWrapper.Services.GetRequiredService();
+
+ configuration.EnumerateProviders().Should().ContainSingle();
+ }
+
+ private static void AssertSpringBootConfigurationIsAutowired(HostWrapper hostWrapper)
+ {
+ var configuration = hostWrapper.Services.GetRequiredService();
+
+ configuration.EnumerateProviders().Should().ContainSingle();
+ configuration.EnumerateProviders().Should().ContainSingle();
+ }
+
+ private static void AssertEncryptionConfigurationIsAutowired(HostWrapper hostWrapper)
+ {
+ var configurationRoot = (IConfigurationRoot)hostWrapper.Services.GetRequiredService();
+
+ configurationRoot.EnumerateProviders().Should().ContainSingle();
+ }
+
+ private static void AssertPlaceholderResolverIsAutowired(HostWrapper hostWrapper)
+ {
+ var configurationRoot = (IConfigurationRoot)hostWrapper.Services.GetRequiredService();
+
+ configurationRoot.EnumerateProviders().Should().ContainSingle();
+ }
+
+ private static void AssertConnectorsAreAutowired(HostWrapper hostWrapper)
+ {
+ var configuration = hostWrapper.Services.GetRequiredService();
+
+ configuration.EnumerateProviders().Should().NotBeEmpty();
+ configuration.EnumerateProviders().Should().ContainSingle();
+
+ hostWrapper.Services.GetService>().Should().NotBeNull();
+ hostWrapper.Services.GetService>().Should().NotBeNull();
+ hostWrapper.Services.GetService>().Should().NotBeNull();
+ hostWrapper.Services.GetService>().Should().NotBeNull();
+ hostWrapper.Services.GetService>().Should().NotBeNull();
+ hostWrapper.Services.GetService>().Should().NotBeNull();
+ hostWrapper.Services.GetService>().Should().NotBeNull();
+ hostWrapper.Services.GetService>().Should().NotBeNull();
+ }
+
+ private static void AssertDynamicSerilogIsAutowired(HostWrapper hostWrapper)
+ {
+ var loggerProvider = hostWrapper.Services.GetRequiredService();
+
+ loggerProvider.Should().BeOfType();
+ }
+
+ private static void AssertDynamicConsoleIsAutowired(HostWrapper hostWrapper)
+ {
+ var loggerProvider = hostWrapper.Services.GetRequiredService();
+
+ loggerProvider.Should().BeOfType();
+ }
+
+ private static void AssertServiceDiscoveryClientsAreAutowired(HostWrapper hostWrapper)
+ {
+ IDiscoveryClient[] discoveryClients = [.. hostWrapper.Services.GetServices()];
+
+ discoveryClients.Should().HaveCount(3);
+ discoveryClients.Should().ContainSingle(discoveryClient => discoveryClient is ConfigurationDiscoveryClient);
+ discoveryClients.Should().ContainSingle(discoveryClient => discoveryClient is ConsulDiscoveryClient);
+ discoveryClients.Should().ContainSingle(discoveryClient => discoveryClient is EurekaDiscoveryClient);
+ }
+
+ private static void AssertPrometheusIsAutowired(HostWrapper hostWrapper)
+ {
+ hostWrapper.Services.GetService().Should().NotBeNull();
+ }
+
+ private static async Task AssertAllActuatorsAreAutowiredAsync(HostWrapper hostWrapper, bool expectHealthy, CancellationToken cancellationToken)
+ {
+ hostWrapper.Services.GetServices().Should().ContainSingle();
+ hostWrapper.Services.GetServices().OfType().Should().ContainSingle();
+
+ if (hostWrapper.Services.GetService() != null)
+ {
+ using HttpClient httpClient = hostWrapper.GetTestClient();
+
+ HttpResponseMessage response = await httpClient.GetAsync(new Uri("/actuator", UriKind.Relative), cancellationToken);
+ response.StatusCode.Should().Be(HttpStatusCode.OK);
+
+ response = await httpClient.GetAsync(new Uri("/actuator/info", UriKind.Relative), cancellationToken);
+ response.StatusCode.Should().Be(HttpStatusCode.OK);
+
+ response = await httpClient.GetAsync(new Uri("/actuator/health", UriKind.Relative), cancellationToken);
+ response.StatusCode.Should().Be(expectHealthy ? HttpStatusCode.OK : HttpStatusCode.ServiceUnavailable);
+
+ response = await httpClient.GetAsync(new Uri("/actuator/health/liveness", UriKind.Relative), cancellationToken);
+ response.StatusCode.Should().Be(HttpStatusCode.OK);
+ string responseContent = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken);
+ responseContent.Should().Contain("""LivenessState":"CORRECT""");
+
+ response = await httpClient.GetAsync(new Uri("/actuator/health/readiness", UriKind.Relative), cancellationToken);
+ response.StatusCode.Should().Be(HttpStatusCode.OK);
+ responseContent = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken);
+ responseContent.Should().Contain("""ReadinessState":"ACCEPTING_TRAFFIC""");
+ }
+ }
+
+ private static void AssertTracingIsAutowired(HostWrapper hostWrapper)
+ {
+ var applicationInstanceInfo = hostWrapper.Services.GetRequiredService();
+ applicationInstanceInfo.ApplicationName.Should().NotBeNull();
+
+ hostWrapper.Services.GetServices().OfType().Should().ContainSingle();
+ }
+
+ private static class HostWrapperFactory
+ {
+ public static HostWrapper GetForOnly(string assemblyNameToInclude, HostBuilderType hostBuilderType)
+ {
+ IReadOnlySet exclusions = SteeltoeAssemblyNames.Only(assemblyNameToInclude);
+ return GetExcluding(exclusions, hostBuilderType);
+ }
+
+ public static HostWrapper GetExcluding(IReadOnlySet assemblyNamesToExclude, HostBuilderType hostBuilderType)
+ {
+ return hostBuilderType switch
+ {
+ HostBuilderType.Host => HostWrapper.Wrap(GetExcludingFromHostBuilder(assemblyNamesToExclude)),
+ HostBuilderType.WebHost => HostWrapper.Wrap(GetExcludingFromWebHostBuilder(assemblyNamesToExclude)),
+ HostBuilderType.WebApplication => HostWrapper.Wrap(GetExcludingFromWebApplicationBuilder(assemblyNamesToExclude)),
+ HostBuilderType.HostApplication => HostWrapper.Wrap(GetExcludingFromHostApplicationBuilder(assemblyNamesToExclude)),
+ _ => throw new NotSupportedException()
+ };
+ }
+
+ private static IHost GetExcludingFromHostBuilder(IReadOnlySet assemblyNamesToExclude)
+ {
+ HostBuilder hostBuilder = TestHostBuilderFactory.CreateWeb();
+
+ hostBuilder.ConfigureWebHost(builder =>
+ {
+ builder.ConfigureAppConfiguration(configurationBuilder => configurationBuilder.Add(FastTestConfigurations.All));
+ builder.Configure(applicationBuilder => applicationBuilder.UseRouting());
+ builder.AddSteeltoe(assemblyNamesToExclude);
+ });
+
+ return hostBuilder.Build();
+ }
+
+ private static IWebHost GetExcludingFromWebHostBuilder(IReadOnlySet assemblyNamesToExclude)
+ {
+ WebHostBuilder builder = TestWebHostBuilderFactory.Create();
+ builder.ConfigureAppConfiguration(configurationBuilder => configurationBuilder.Add(FastTestConfigurations.All));
+ builder.Configure(applicationBuilder => applicationBuilder.UseRouting());
+ builder.AddSteeltoe(assemblyNamesToExclude);
+
+ return builder.Build();
+ }
+
+ private static WebApplication GetExcludingFromWebApplicationBuilder(IReadOnlySet assemblyNamesToExclude)
+ {
+ WebApplicationBuilder builder = TestWebApplicationBuilderFactory.CreateDefault();
+ builder.Configuration.Add(FastTestConfigurations.All);
+ builder.AddSteeltoe(assemblyNamesToExclude);
+
+ WebApplication host = builder.Build();
+ host.UseRouting();
+
+ return host;
+ }
+
+ private static IHost GetExcludingFromHostApplicationBuilder(IReadOnlySet assemblyNamesToExclude)
+ {
+ HostApplicationBuilder builder = TestHostApplicationBuilderFactory.Create();
+ builder.Configuration.Add(FastTestConfigurations.All);
+ builder.AddSteeltoe(assemblyNamesToExclude);
+
+ return builder.Build();
+ }
+ }
+}
diff --git a/src/Bootstrap/test/AutoConfiguration.Test/Steeltoe.Bootstrap.AutoConfiguration.Test.csproj b/src/Bootstrap/test/AutoConfiguration.Test/Steeltoe.Bootstrap.AutoConfiguration.Test.csproj
new file mode 100644
index 0000000000..16e099632f
--- /dev/null
+++ b/src/Bootstrap/test/AutoConfiguration.Test/Steeltoe.Bootstrap.AutoConfiguration.Test.csproj
@@ -0,0 +1,35 @@
+
+
+ net9.0;net8.0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Bootstrap/test/AutoConfiguration.Test/xunit.runner.json b/src/Bootstrap/test/AutoConfiguration.Test/xunit.runner.json
new file mode 100644
index 0000000000..fdeefaa456
--- /dev/null
+++ b/src/Bootstrap/test/AutoConfiguration.Test/xunit.runner.json
@@ -0,0 +1,4 @@
+{
+ "maxParallelThreads": 1,
+ "parallelizeTestCollections": false
+}
diff --git a/src/Bootstrap/test/EmptyAutoConfiguration.Test/EmptyAutoConfigurationTest.cs b/src/Bootstrap/test/EmptyAutoConfiguration.Test/EmptyAutoConfigurationTest.cs
new file mode 100644
index 0000000000..884cf35a40
--- /dev/null
+++ b/src/Bootstrap/test/EmptyAutoConfiguration.Test/EmptyAutoConfigurationTest.cs
@@ -0,0 +1,78 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the Apache 2.0 License.
+// See the LICENSE file in the project root for more information.
+
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.Extensions.Hosting;
+using Steeltoe.Bootstrap.AutoConfiguration;
+using Steeltoe.Common.TestResources;
+
+namespace Steeltoe.Bootstrap.EmptyAutoConfiguration.Test;
+
+public sealed class EmptyAutoConfigurationTest
+{
+ [Fact]
+ public void Bootstrap_does_not_depend_on_other_Steeltoe_packages()
+ {
+ // Assemblies from referenced projects without PrivateAssets="All" are copied into the test output directory during build.
+ var whitelist = new HashSet(StringComparer.OrdinalIgnoreCase)
+ {
+ "Steeltoe.Bootstrap.AutoConfiguration.dll",
+ "Steeltoe.Common.dll",
+ "Steeltoe.Common.Hosting.dll",
+ "Steeltoe.Common.Logging.dll",
+
+ "Steeltoe.Bootstrap.EmptyAutoConfiguration.Test.dll",
+ "Steeltoe.Common.TestResources.dll"
+ };
+
+ string[] files =
+ [
+ .. Directory.GetFiles(AppDomain.CurrentDomain.BaseDirectory, "Steeltoe*.dll").Select(Path.GetFileName).Select(fileName => fileName!)
+ .Where(fileName => !whitelist.Contains(fileName))
+ ];
+
+ files.Should().BeEmpty();
+ }
+
+ [Fact]
+ public void Loads_without_any_Steeltoe_references_using_WebApplicationBuilder()
+ {
+ WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create();
+
+ Action action = () => builder.AddSteeltoe();
+
+ action.Should().NotThrow();
+ }
+
+ [Fact]
+ public void Loads_without_any_Steeltoe_references_using_HostApplicationBuilder()
+ {
+ HostApplicationBuilder builder = TestHostApplicationBuilderFactory.Create();
+
+ Action action = () => builder.AddSteeltoe();
+
+ action.Should().NotThrow();
+ }
+
+ [Fact]
+ public void Loads_without_any_Steeltoe_references_using_WebHostBuilder()
+ {
+ WebHostBuilder builder = TestWebHostBuilderFactory.Create();
+
+ Action action = () => builder.AddSteeltoe();
+
+ action.Should().NotThrow();
+ }
+
+ [Fact]
+ public void Loads_without_any_Steeltoe_references_using_HostBuilder()
+ {
+ IHostBuilder builder = TestHostBuilderFactory.Create();
+
+ Action action = () => builder.AddSteeltoe();
+
+ action.Should().NotThrow();
+ }
+}
diff --git a/src/Bootstrap/test/EmptyAutoConfiguration.Test/Steeltoe.Bootstrap.EmptyAutoConfiguration.Test.csproj b/src/Bootstrap/test/EmptyAutoConfiguration.Test/Steeltoe.Bootstrap.EmptyAutoConfiguration.Test.csproj
new file mode 100644
index 0000000000..19e169e9b3
--- /dev/null
+++ b/src/Bootstrap/test/EmptyAutoConfiguration.Test/Steeltoe.Bootstrap.EmptyAutoConfiguration.Test.csproj
@@ -0,0 +1,11 @@
+
+
+ net9.0;net8.0
+
+
+
+
+
+
+
+
diff --git a/src/Common/README.md b/src/Common/README.md
new file mode 100644
index 0000000000..22da6b628c
--- /dev/null
+++ b/src/Common/README.md
@@ -0,0 +1,3 @@
+# Steeltoe Common Packages
+
+This repository contains several packages that are common to other Steeltoe components.
diff --git a/src/Common/src/Certificates/CertificateConfigurationExtensions.cs b/src/Common/src/Certificates/CertificateConfigurationExtensions.cs
new file mode 100644
index 0000000000..5b429703cd
--- /dev/null
+++ b/src/Common/src/Certificates/CertificateConfigurationExtensions.cs
@@ -0,0 +1,160 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the Apache 2.0 License.
+// See the LICENSE file in the project root for more information.
+
+using Microsoft.Extensions.Configuration;
+
+namespace Steeltoe.Common.Certificates;
+
+public static class CertificateConfigurationExtensions
+{
+ internal const string AppInstanceIdentityCertificateName = "AppInstanceIdentity";
+
+ ///
+ /// Adds file path information for a certificate and (optional) private key to configuration, for use with .
+ ///
+ ///
+ /// The to add configuration to.
+ ///
+ ///
+ /// Name of the certificate, or for an unnamed certificate.
+ ///
+ ///
+ /// The path on disk to locate a valid certificate file.
+ ///
+ ///
+ /// The path on disk to locate a valid PEM-encoded RSA key file.
+ ///
+ ///
+ /// The incoming so that additional calls can be chained.
+ ///
+ internal static IConfigurationBuilder AddCertificate(this IConfigurationBuilder builder, string certificateName, string certificateFilePath,
+ string? privateKeyFilePath = null)
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+ ArgumentNullException.ThrowIfNull(certificateName);
+ ArgumentException.ThrowIfNullOrEmpty(certificateFilePath);
+
+ string keyPrefix = certificateName.Length == 0
+ ? $"{CertificateOptions.ConfigurationKeyPrefix}{ConfigurationPath.KeyDelimiter}"
+ : $"{CertificateOptions.ConfigurationKeyPrefix}{ConfigurationPath.KeyDelimiter}{certificateName}{ConfigurationPath.KeyDelimiter}";
+
+ var keys = new Dictionary
+ {
+ [$"{keyPrefix}CertificateFilePath"] = certificateFilePath
+ };
+
+ if (!string.IsNullOrEmpty(privateKeyFilePath))
+ {
+ keys[$"{keyPrefix}PrivateKeyFilePath"] = privateKeyFilePath;
+ }
+
+ builder.AddInMemoryCollection(keys);
+ return builder;
+ }
+
+ ///
+ /// Adds PEM certificate files representing application identity to the application configuration. When running outside of Cloud Foundry-based platforms,
+ /// this method will create certificates resembling those found on the platform.
+ ///
+ ///
+ /// The to add configuration to.
+ ///
+ ///
+ /// When running outside of Cloud Foundry, the CA and Intermediate certificates will be created in a directory above the current project, so that they
+ /// can be shared between different projects in the same solution.
+ ///
+ ///
+ /// The incoming so that additional calls can be chained.
+ ///
+ public static IConfigurationBuilder AddAppInstanceIdentityCertificate(this IConfigurationBuilder builder)
+ {
+ return AddAppInstanceIdentityCertificate(builder, TimeProvider.System, null, null);
+ }
+
+ ///
+ /// Adds PEM certificate files representing application identity to the application configuration. When running outside of Cloud Foundry-based platforms,
+ /// this method will create certificates resembling those found on the platform.
+ ///
+ ///
+ /// The to add configuration to.
+ ///
+ ///
+ /// (Optional) A GUID representing an organization (org), for use with Cloud Foundry certificate-based authorization policy.
+ ///
+ ///
+ /// (Optional) A GUID representing a space, for use with Cloud Foundry certificate-based authorization policy.
+ ///
+ ///
+ /// When running outside of Cloud Foundry, the CA and Intermediate certificates will be created in a directory above the current project, so that they
+ /// can be shared between different projects in the same solution.
+ ///
+ ///
+ /// The incoming so that additional calls can be chained.
+ ///
+ public static IConfigurationBuilder AddAppInstanceIdentityCertificate(this IConfigurationBuilder builder, Guid? orgId, Guid? spaceId)
+ {
+ return AddAppInstanceIdentityCertificate(builder, TimeProvider.System, orgId, spaceId);
+ }
+
+ ///
+ /// Adds PEM certificate files representing application identity to the application configuration. When running outside of Cloud Foundry-based platforms,
+ /// this method will create certificates resembling those found on the platform.
+ ///
+ ///
+ /// The to add configuration to.
+ ///
+ ///
+ /// Provides access to the system time.
+ ///
+ ///
+ /// (Optional) A GUID representing an organization (org), for use with Cloud Foundry certificate-based authorization policy.
+ ///
+ ///
+ /// (Optional) A GUID representing a space, for use with Cloud Foundry certificate-based authorization policy.
+ ///
+ ///
+ /// When running outside of Cloud Foundry, the CA and Intermediate certificates will be created in a directory above the current project, so that they
+ /// can be shared between different projects in the same solution.
+ ///
+ ///
+ /// The incoming so that additional calls can be chained.
+ ///
+ public static IConfigurationBuilder AddAppInstanceIdentityCertificate(this IConfigurationBuilder builder, TimeProvider timeProvider, Guid? orgId,
+ Guid? spaceId)
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+ ArgumentNullException.ThrowIfNull(timeProvider);
+
+ if (!Platform.IsCloudFoundry)
+ {
+ orgId ??= Guid.NewGuid();
+ spaceId ??= Guid.NewGuid();
+
+ var writer = new LocalCertificateWriter(timeProvider);
+ writer.Write(orgId.Value, spaceId.Value);
+
+ Environment.SetEnvironmentVariable("CF_SYSTEM_CERT_PATH",
+ Path.Combine(Directory.GetParent(LocalCertificateWriter.AppBasePath)?.FullName ?? string.Empty,
+ LocalCertificateWriter.CertificateDirectoryName));
+
+ Environment.SetEnvironmentVariable("CF_INSTANCE_CERT",
+ Path.Combine(LocalCertificateWriter.AppBasePath, LocalCertificateWriter.CertificateDirectoryName,
+ $"{LocalCertificateWriter.CertificateFilenamePrefix}Cert.pem"));
+
+ Environment.SetEnvironmentVariable("CF_INSTANCE_KEY",
+ Path.Combine(LocalCertificateWriter.AppBasePath, LocalCertificateWriter.CertificateDirectoryName,
+ $"{LocalCertificateWriter.CertificateFilenamePrefix}Key.pem"));
+ }
+
+ string? certificateFile = Environment.GetEnvironmentVariable("CF_INSTANCE_CERT");
+ string? keyFile = Environment.GetEnvironmentVariable("CF_INSTANCE_KEY");
+
+ if (certificateFile != null && keyFile != null)
+ {
+ builder.AddCertificate(AppInstanceIdentityCertificateName, certificateFile, keyFile);
+ }
+
+ return builder;
+ }
+}
diff --git a/src/Common/src/Certificates/CertificateOptions.cs b/src/Common/src/Certificates/CertificateOptions.cs
new file mode 100644
index 0000000000..3b062e0f38
--- /dev/null
+++ b/src/Common/src/Certificates/CertificateOptions.cs
@@ -0,0 +1,19 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the Apache 2.0 License.
+// See the LICENSE file in the project root for more information.
+
+using System.Security.Cryptography.X509Certificates;
+
+namespace Steeltoe.Common.Certificates;
+
+///
+/// Options for use with platform-provided certificates.
+///
+public sealed class CertificateOptions
+{
+ internal const string ConfigurationKeyPrefix = "Certificates";
+
+ public X509Certificate2? Certificate { get; set; }
+
+ public IList IssuerChain { get; } = [];
+}
diff --git a/src/Common/src/Certificates/CertificateServiceCollectionExtensions.cs b/src/Common/src/Certificates/CertificateServiceCollectionExtensions.cs
new file mode 100644
index 0000000000..84264a7071
--- /dev/null
+++ b/src/Common/src/Certificates/CertificateServiceCollectionExtensions.cs
@@ -0,0 +1,42 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the Apache 2.0 License.
+// See the LICENSE file in the project root for more information.
+
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using Microsoft.Extensions.Options;
+
+namespace Steeltoe.Common.Certificates;
+
+public static class CertificateServiceCollectionExtensions
+{
+ ///
+ /// Binds certificate paths in configuration to for use with client certificates.
+ ///
+ ///
+ /// The to add services to.
+ ///
+ ///
+ /// Name of the certificate used in configuration and IOptions, or for an unnamed certificate.
+ ///
+ ///
+ /// The incoming so that additional calls can be chained.
+ ///
+ public static IServiceCollection ConfigureCertificateOptions(this IServiceCollection services, string certificateName)
+ {
+ ArgumentNullException.ThrowIfNull(services);
+ ArgumentNullException.ThrowIfNull(certificateName);
+
+ string configurationKey = certificateName.Length == 0
+ ? CertificateOptions.ConfigurationKeyPrefix
+ : ConfigurationPath.Combine(CertificateOptions.ConfigurationKeyPrefix, certificateName);
+
+ services.AddOptions().BindConfiguration(configurationKey);
+ services.WatchFilePathInOptions(configurationKey, certificateName, "CertificateFileName");
+ services.WatchFilePathInOptions(configurationKey, certificateName, "PrivateKeyFileName");
+
+ services.TryAddEnumerable(ServiceDescriptor.Singleton, ConfigureCertificateOptions>());
+ return services;
+ }
+}
diff --git a/src/Common/src/Certificates/CertificateSettings.cs b/src/Common/src/Certificates/CertificateSettings.cs
new file mode 100644
index 0000000000..fcbee8e2f3
--- /dev/null
+++ b/src/Common/src/Certificates/CertificateSettings.cs
@@ -0,0 +1,25 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the Apache 2.0 License.
+// See the LICENSE file in the project root for more information.
+
+using System.Security.Cryptography.X509Certificates;
+
+namespace Steeltoe.Common.Certificates;
+
+///
+/// Configuration settings for certificate access. Indicates where to load a from.
+///
+internal sealed class CertificateSettings
+{
+ // This type only exists to enable JSON schema documentation via ConfigurationSchemaAttribute.
+
+ ///
+ /// Gets or sets the local path to a certificate file on disk. Use if the private key is stored in another file.
+ ///
+ public string? CertificateFilePath { get; set; }
+
+ ///
+ /// Gets or sets the local path to a private key file on disk (optional).
+ ///
+ public string? PrivateKeyFilePath { get; set; }
+}
diff --git a/src/Common/src/Certificates/ConfigurationSchema.json b/src/Common/src/Certificates/ConfigurationSchema.json
new file mode 100644
index 0000000000..0ac68091c0
--- /dev/null
+++ b/src/Common/src/Certificates/ConfigurationSchema.json
@@ -0,0 +1,48 @@
+{
+ "definitions": {
+ "logLevel": {
+ "properties": {
+ "Steeltoe": {
+ "$ref": "#/definitions/logLevelThreshold"
+ },
+ "Steeltoe.Common": {
+ "$ref": "#/definitions/logLevelThreshold"
+ },
+ "Steeltoe.Common.Certificates": {
+ "$ref": "#/definitions/logLevelThreshold"
+ }
+ }
+ }
+ },
+ "type": "object",
+ "properties": {
+ "Certificates": {
+ "type": "object",
+ "properties": {
+ "CertificateFilePath": {
+ "type": "string",
+ "description": "Gets or sets the local path to a certificate file on disk. Use 'Steeltoe.Common.Certificates.CertificateSettings.PrivateKeyFilePath' if the private key is stored in another file."
+ },
+ "PrivateKeyFilePath": {
+ "type": "string",
+ "description": "Gets or sets the local path to a private key file on disk (optional)."
+ }
+ },
+ "description": "Configuration settings for certificate access. Indicates where to load a 'System.Security.Cryptography.X509Certificates.X509Certificate2' from.",
+ "additionalProperties": {
+ "type": "object",
+ "properties": {
+ "CertificateFilePath": {
+ "type": "string",
+ "description": "Gets or sets the local path to a certificate file on disk. Use 'Steeltoe.Common.Certificates.CertificateSettings.PrivateKeyFilePath' if the private key is stored in another file."
+ },
+ "PrivateKeyFilePath": {
+ "type": "string",
+ "description": "Gets or sets the local path to a private key file on disk (optional)."
+ }
+ },
+ "description": "Configuration settings for certificate access. Indicates where to load a 'System.Security.Cryptography.X509Certificates.X509Certificate2' from."
+ }
+ }
+ }
+}
diff --git a/src/Common/src/Certificates/ConfigureCertificateOptions.cs b/src/Common/src/Certificates/ConfigureCertificateOptions.cs
new file mode 100644
index 0000000000..e70acc06e6
--- /dev/null
+++ b/src/Common/src/Certificates/ConfigureCertificateOptions.cs
@@ -0,0 +1,64 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the Apache 2.0 License.
+// See the LICENSE file in the project root for more information.
+
+using System.Security.Cryptography.X509Certificates;
+using System.Text;
+using System.Text.RegularExpressions;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Options;
+
+namespace Steeltoe.Common.Certificates;
+
+internal sealed class ConfigureCertificateOptions : IConfigureNamedOptions
+{
+ private static readonly Regex CertificateRegex = new("-+BEGIN CERTIFICATE-+.+?-+END CERTIFICATE-+", RegexOptions.Compiled | RegexOptions.Singleline,
+ TimeSpan.FromSeconds(1));
+
+ private readonly IConfiguration _configuration;
+
+ public ConfigureCertificateOptions(IConfiguration configuration)
+ {
+ ArgumentNullException.ThrowIfNull(configuration);
+
+ _configuration = configuration;
+ }
+
+ public void Configure(CertificateOptions options)
+ {
+ Configure(Options.DefaultName, options);
+ }
+
+ public void Configure(string? name, CertificateOptions options)
+ {
+ ArgumentNullException.ThrowIfNull(options);
+
+ string? certificateFilePath = _configuration.GetValue(GetConfigurationKey(name, "CertificateFilePath"));
+
+ if (options.Certificate != null || certificateFilePath == null || !File.Exists(certificateFilePath))
+ {
+ return;
+ }
+
+ string? privateKeyFilePath = _configuration.GetValue(GetConfigurationKey(name, "PrivateKeyFilePath"));
+
+ options.Certificate = privateKeyFilePath != null && File.Exists(privateKeyFilePath)
+ ? X509Certificate2.CreateFromPemFile(certificateFilePath, privateKeyFilePath)
+ : new X509Certificate2(certificateFilePath);
+
+ X509Certificate2[] certificateChain = CertificateRegex.Matches(File.ReadAllText(certificateFilePath))
+ .Select(x => new X509Certificate2(Encoding.ASCII.GetBytes(x.Value))).ToArray();
+
+ foreach (X509Certificate2 issuer in certificateChain.Skip(1))
+ {
+ options.IssuerChain.Add(issuer);
+ }
+ }
+
+ private static string GetConfigurationKey(string? optionName, string propertyName)
+ {
+ return string.IsNullOrEmpty(optionName)
+ ? string.Join(ConfigurationPath.KeyDelimiter, CertificateOptions.ConfigurationKeyPrefix, propertyName)
+ : string.Join(ConfigurationPath.KeyDelimiter, CertificateOptions.ConfigurationKeyPrefix, optionName, propertyName);
+ }
+}
diff --git a/src/Common/src/Certificates/FilePathInOptionsChangeTokenSource.cs b/src/Common/src/Certificates/FilePathInOptionsChangeTokenSource.cs
new file mode 100644
index 0000000000..ddf7cc1aa7
--- /dev/null
+++ b/src/Common/src/Certificates/FilePathInOptionsChangeTokenSource.cs
@@ -0,0 +1,129 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the Apache 2.0 License.
+// See the LICENSE file in the project root for more information.
+
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.FileProviders;
+using Microsoft.Extensions.FileProviders.Physical;
+using Microsoft.Extensions.Options;
+using Microsoft.Extensions.Primitives;
+
+namespace Steeltoe.Common.Certificates;
+
+internal sealed class FilePathInOptionsChangeTokenSource : IOptionsChangeTokenSource, IDisposable
+{
+ private readonly FileSystemChangeWatcher _fileWatcher = new();
+ private string _filePath;
+ private ConfigurationReloadToken _changeFilePathToken = new();
+
+ public string Name { get; }
+
+ public FilePathInOptionsChangeTokenSource(string? optionName, string filePath)
+ {
+ ArgumentNullException.ThrowIfNull(filePath);
+
+ Name = optionName ?? Options.DefaultName;
+ _filePath = filePath;
+ }
+
+ public void ChangePath(string filePath)
+ {
+ if (filePath != _filePath)
+ {
+ _filePath = filePath;
+
+ // Wait until the file is fully written to disk.
+ Thread.Sleep(500);
+
+ ConfigurationReloadToken previousToken = Interlocked.Exchange(ref _changeFilePathToken, new ConfigurationReloadToken());
+ previousToken.OnReload();
+ }
+ }
+
+ public IChangeToken GetChangeToken()
+ {
+ IChangeToken watcherChangeToken = _fileWatcher.GetChangeToken(_filePath);
+
+ return new CompositeChangeToken([
+ watcherChangeToken,
+ _changeFilePathToken
+ ]);
+ }
+
+ public void Dispose()
+ {
+ _fileWatcher.Dispose();
+ }
+
+ private sealed class FileSystemChangeWatcher : IDisposable
+ {
+ private PhysicalFilesWatcher? _filesWatcher;
+ private string? _previousFilePath;
+
+ public IChangeToken GetChangeToken(string filePath)
+ {
+ if (filePath == _previousFilePath)
+ {
+ if (_filesWatcher == null)
+ {
+ // Determined earlier that this path is not watchable.
+ return NullChangeToken.Singleton;
+ }
+
+ // Return new token for the same file.
+ string absolutePath = Path.GetFullPath(filePath);
+ string fileName = Path.GetFileName(absolutePath);
+ return _filesWatcher.CreateFileChangeToken(fileName);
+ }
+
+ // Path has changed. Stop watching the previous file, if any.
+ _previousFilePath = filePath;
+ Dispose();
+
+ if (filePath.Length > 0)
+ {
+ string absolutePath = Path.GetFullPath(filePath);
+ string? directory = Path.GetDirectoryName(absolutePath);
+
+ if (directory != null)
+ {
+ string root = EnsureTrailingSlash(directory);
+
+ // Because PhysicalFilesWatcher implicitly disposes FileSystemWatcher (despite not owning it),
+ // we can't just update the existing FileSystemWatcher.Path, but instead we must recreate everything.
+ var fileSystemWatcher = new FileSystemWatcher(root);
+
+ try
+ {
+ // This throws on macOS, because it requires polling. But to make polling actually work, we need to
+ // set the internal UseActivePolling property. See https://github.com/dotnet/runtime/issues/48602.
+ _filesWatcher = new PhysicalFilesWatcher(root, fileSystemWatcher, false);
+
+ string fileName = Path.GetFileName(absolutePath);
+ return _filesWatcher.CreateFileChangeToken(fileName);
+ }
+ catch (Exception)
+ {
+ Dispose();
+ fileSystemWatcher.Dispose();
+ throw;
+ }
+ }
+ }
+
+ // New path is not watchable.
+ return NullChangeToken.Singleton;
+ }
+
+ public void Dispose()
+ {
+ _filesWatcher?.Dispose();
+ _filesWatcher = null;
+ }
+
+ private static string EnsureTrailingSlash(string path)
+ {
+ return path.Length > 0 && path[^1] != Path.DirectorySeparatorChar ? $"{path}{Path.DirectorySeparatorChar}" : path;
+ }
+ }
+}
diff --git a/src/Common/src/Certificates/LocalCertificateWriter.cs b/src/Common/src/Certificates/LocalCertificateWriter.cs
new file mode 100644
index 0000000000..8c0ded40c1
--- /dev/null
+++ b/src/Common/src/Certificates/LocalCertificateWriter.cs
@@ -0,0 +1,151 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the Apache 2.0 License.
+// See the LICENSE file in the project root for more information.
+
+using System.Security.Cryptography;
+using System.Security.Cryptography.X509Certificates;
+
+namespace Steeltoe.Common.Certificates;
+
+internal sealed class LocalCertificateWriter
+{
+ internal const string CertificateDirectoryName = "GeneratedCertificates";
+ internal const string CertificateFilenamePrefix = "SteeltoeAppInstance";
+ internal static readonly string AppBasePath = GetAppBasePath();
+ private static readonly string ParentPath = Directory.GetParent(AppBasePath)?.ToString() ?? string.Empty;
+
+ internal static readonly string RootCaPfxPath = Path.Combine(ParentPath, CertificateDirectoryName, "SteeltoeCA.pfx");
+ internal static readonly string IntermediatePfxPath = Path.Combine(ParentPath, CertificateDirectoryName, "SteeltoeIntermediate.pfx");
+
+ private readonly TimeProvider _timeProvider;
+
+ public LocalCertificateWriter(TimeProvider timeProvider)
+ {
+ ArgumentNullException.ThrowIfNull(timeProvider);
+
+ _timeProvider = timeProvider;
+ }
+
+ private static string GetAppBasePath()
+ {
+ if (AppContext.BaseDirectory.Contains($"{Path.DirectorySeparatorChar}bin{Path.DirectorySeparatorChar}", StringComparison.Ordinal))
+ {
+ // Traverse up to the project directory, if running from IDE. Strips off a sub-path like: \bin\Debug\net8.0\win-x64\
+ return AppContext.BaseDirectory[..AppContext.BaseDirectory.LastIndexOf($"{Path.DirectorySeparatorChar}bin", StringComparison.Ordinal)];
+ }
+
+ return AppContext.BaseDirectory[..^1];
+ }
+
+ public void Write(Guid orgId, Guid spaceId)
+ {
+ var appId = Guid.NewGuid();
+ var instanceId = Guid.NewGuid();
+
+ // Certificates provided by Cloud Foundry will have a subject that doesn't comply with standards.
+ // System.Security.Cryptography.X509Certificates.CertificateRequest would re-order these components to comply if we tried to match.
+ // Non-compliant subject looks like this: "CN={instanceId}, OU=organization:{organizationId} + OU=space:{spaceId} + OU=app:{appId}"
+ string subject = $"CN={instanceId}, OU=app:{appId} + OU=space:{spaceId} + OU=organization:{orgId}";
+
+ X509Certificate2 caCertificate;
+
+ // Create a directory a level above the running project to contain the root and intermediate certificates
+ if (!Directory.Exists(Path.Combine(ParentPath, CertificateDirectoryName)))
+ {
+ Directory.CreateDirectory(Path.Combine(ParentPath, CertificateDirectoryName));
+ }
+
+ // Create the root certificate if it doesn't already exist (can be shared by multiple applications)
+ if (!File.Exists(RootCaPfxPath))
+ {
+ caCertificate = CreateRootCertificate("CN=SteeltoeGeneratedCA");
+ File.WriteAllBytes(RootCaPfxPath, caCertificate.Export(X509ContentType.Pfx));
+ }
+ else
+ {
+ caCertificate = new X509Certificate2(RootCaPfxPath);
+ }
+
+ // Create the intermediate certificate if it doesn't already exist (can be shared by multiple applications)
+ X509Certificate2 intermediateCertificate;
+
+ if (!File.Exists(IntermediatePfxPath))
+ {
+ intermediateCertificate = CreateIntermediateCertificate("CN=SteeltoeGeneratedIntermediate", caCertificate);
+ File.WriteAllBytes(IntermediatePfxPath, intermediateCertificate.Export(X509ContentType.Pfx));
+ }
+ else
+ {
+ intermediateCertificate = new X509Certificate2(IntermediatePfxPath);
+ }
+
+ var subjectAlternativeNameBuilder = new SubjectAlternativeNameBuilder();
+
+ X509Certificate2 clientCertificate = CreateClientCertificate(subject, intermediateCertificate, subjectAlternativeNameBuilder);
+
+ // Create a folder inside the project to store generated certificate files
+ if (!Directory.Exists(Path.Combine(AppBasePath, CertificateDirectoryName)))
+ {
+ Directory.CreateDirectory(Path.Combine(AppBasePath, CertificateDirectoryName));
+ }
+
+ string chainedCertificateContents = clientCertificate.ExportCertificatePem() + Environment.NewLine + intermediateCertificate.ExportCertificatePem();
+ string keyContents = clientCertificate.GetRSAPrivateKey()!.ExportRSAPrivateKeyPem();
+
+ File.WriteAllText(Path.Combine(AppBasePath, CertificateDirectoryName, $"{CertificateFilenamePrefix}Cert.pem"), chainedCertificateContents);
+ File.WriteAllText(Path.Combine(AppBasePath, CertificateDirectoryName, $"{CertificateFilenamePrefix}Key.pem"), keyContents);
+ }
+
+ private X509Certificate2 CreateRootCertificate(string distinguishedName)
+ {
+ using var privateKey = RSA.Create();
+
+ var certificateRequest =
+ new CertificateRequest(new X500DistinguishedName(distinguishedName), privateKey, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
+
+ certificateRequest.CertificateExtensions.Add(new X509BasicConstraintsExtension(true, false, 0, true));
+
+ return certificateRequest.CreateSelfSigned(_timeProvider.GetUtcNow(), new DateTimeOffset(2039, 12, 31, 23, 59, 59, TimeSpan.Zero));
+ }
+
+ private X509Certificate2 CreateIntermediateCertificate(string subjectName, X509Certificate2 issuerCertificate)
+ {
+ using var privateKey = RSA.Create();
+ var certificateRequest = new CertificateRequest(subjectName, privateKey, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
+ certificateRequest.CertificateExtensions.Add(new X509BasicConstraintsExtension(true, false, 0, true));
+
+ byte[] serialNumber = new byte[8];
+ using var randomNumberGenerator = RandomNumberGenerator.Create();
+ randomNumberGenerator.GetBytes(serialNumber);
+
+ return certificateRequest.Create(issuerCertificate, _timeProvider.GetUtcNow(), issuerCertificate.NotAfter, serialNumber).CopyWithPrivateKey(privateKey);
+ }
+
+ private X509Certificate2 CreateClientCertificate(string subjectName, X509Certificate2 issuerCertificate, SubjectAlternativeNameBuilder alternativeNames,
+ DateTimeOffset? notAfter = null)
+ {
+ using var privateKey = RSA.Create();
+ var request = new CertificateRequest(subjectName, privateKey, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
+
+ request.CertificateExtensions.Add(new X509BasicConstraintsExtension(false, false, 0, false));
+ request.CertificateExtensions.Add(new X509KeyUsageExtension(X509KeyUsageFlags.DigitalSignature, false));
+
+ request.CertificateExtensions.Add(new X509EnhancedKeyUsageExtension([
+ new Oid("1.3.6.1.5.5.7.3.1"), // serverAuth
+ new Oid("1.3.6.1.5.5.7.3.2") // clientAuth
+ ], false));
+
+ request.CertificateExtensions.Add(alternativeNames.Build());
+
+ byte[] serialNumber = new byte[8];
+
+ using (var randomNumberGenerator = RandomNumberGenerator.Create())
+ {
+ randomNumberGenerator.GetBytes(serialNumber);
+ }
+
+ DateTimeOffset utcNow = _timeProvider.GetUtcNow();
+ X509Certificate2 signedCertificate = request.Create(issuerCertificate, utcNow, notAfter ?? utcNow.AddDays(1), serialNumber);
+ return signedCertificate.CopyWithPrivateKey(privateKey);
+ }
+}
diff --git a/src/Common/src/Certificates/Properties/AssemblyInfo.cs b/src/Common/src/Certificates/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000000..8d7d4413e2
--- /dev/null
+++ b/src/Common/src/Certificates/Properties/AssemblyInfo.cs
@@ -0,0 +1,17 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the Apache 2.0 License.
+// See the LICENSE file in the project root for more information.
+
+using System.Runtime.CompilerServices;
+using Aspire;
+using Steeltoe.Common.Certificates;
+
+[assembly: ConfigurationSchema("Certificates", typeof(CertificateSettings))]
+[assembly: ConfigurationSchema("Certificates:*", typeof(CertificateSettings))]
+[assembly: LoggingCategories("Steeltoe", "Steeltoe.Common", "Steeltoe.Common.Certificates")]
+
+[assembly: InternalsVisibleTo("Steeltoe.Common.Certificates.Test")]
+[assembly: InternalsVisibleTo("Steeltoe.Configuration.ConfigServer")]
+[assembly: InternalsVisibleTo("Steeltoe.Discovery.Eureka")]
+[assembly: InternalsVisibleTo("Steeltoe.Security.Authorization.Certificate")]
+[assembly: InternalsVisibleTo("Steeltoe.Security.Authorization.Certificate.Test")]
diff --git a/src/Common/src/Certificates/PublicAPI.Shipped.txt b/src/Common/src/Certificates/PublicAPI.Shipped.txt
new file mode 100644
index 0000000000..5f282702bb
--- /dev/null
+++ b/src/Common/src/Certificates/PublicAPI.Shipped.txt
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/Common/src/Certificates/PublicAPI.Unshipped.txt b/src/Common/src/Certificates/PublicAPI.Unshipped.txt
new file mode 100644
index 0000000000..f4b9ec725f
--- /dev/null
+++ b/src/Common/src/Certificates/PublicAPI.Unshipped.txt
@@ -0,0 +1,12 @@
+#nullable enable
+static Steeltoe.Common.Certificates.CertificateConfigurationExtensions.AddAppInstanceIdentityCertificate(this Microsoft.Extensions.Configuration.IConfigurationBuilder! builder) -> Microsoft.Extensions.Configuration.IConfigurationBuilder!
+static Steeltoe.Common.Certificates.CertificateConfigurationExtensions.AddAppInstanceIdentityCertificate(this Microsoft.Extensions.Configuration.IConfigurationBuilder! builder, System.Guid? orgId, System.Guid? spaceId) -> Microsoft.Extensions.Configuration.IConfigurationBuilder!
+static Steeltoe.Common.Certificates.CertificateConfigurationExtensions.AddAppInstanceIdentityCertificate(this Microsoft.Extensions.Configuration.IConfigurationBuilder! builder, System.TimeProvider! timeProvider, System.Guid? orgId, System.Guid? spaceId) -> Microsoft.Extensions.Configuration.IConfigurationBuilder!
+static Steeltoe.Common.Certificates.CertificateServiceCollectionExtensions.ConfigureCertificateOptions(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, string! certificateName) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!
+Steeltoe.Common.Certificates.CertificateConfigurationExtensions
+Steeltoe.Common.Certificates.CertificateOptions
+Steeltoe.Common.Certificates.CertificateOptions.Certificate.get -> System.Security.Cryptography.X509Certificates.X509Certificate2?
+Steeltoe.Common.Certificates.CertificateOptions.Certificate.set -> void
+Steeltoe.Common.Certificates.CertificateOptions.CertificateOptions() -> void
+Steeltoe.Common.Certificates.CertificateOptions.IssuerChain.get -> System.Collections.Generic.IList!
+Steeltoe.Common.Certificates.CertificateServiceCollectionExtensions
diff --git a/src/Common/src/Certificates/ServiceCollectionExtensions.cs b/src/Common/src/Certificates/ServiceCollectionExtensions.cs
new file mode 100644
index 0000000000..b99ef14c29
--- /dev/null
+++ b/src/Common/src/Certificates/ServiceCollectionExtensions.cs
@@ -0,0 +1,67 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the Apache 2.0 License.
+// See the LICENSE file in the project root for more information.
+
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Options;
+using Microsoft.Extensions.Primitives;
+
+namespace Steeltoe.Common.Certificates;
+
+internal static class ServiceCollectionExtensions
+{
+ ///
+ /// Refreshes when the file path in the specified property changes on disk, or the property value changes.
+ ///
+ ///
+ /// The options type that contains .
+ ///
+ ///
+ /// The to add services to.
+ ///
+ ///
+ /// The configuration key that is bound to.
+ ///
+ ///
+ /// The named option, which gets added to .
+ ///
+ ///
+ /// The name of the property that contains a file path.
+ ///
+ ///
+ /// The incoming so that additional calls can be chained.
+ ///
+ public static IServiceCollection WatchFilePathInOptions(this IServiceCollection services, string key, string? optionName, string pathPropertyName)
+ {
+ ArgumentNullException.ThrowIfNull(services);
+ ArgumentException.ThrowIfNullOrEmpty(key);
+ ArgumentException.ThrowIfNullOrEmpty(pathPropertyName);
+
+ services.AddSingleton>(serviceProvider =>
+ {
+ var configuration = serviceProvider.GetRequiredService();
+ string filePath = GetFilePath(configuration, key, optionName, pathPropertyName);
+ var watcher = new FilePathInOptionsChangeTokenSource(optionName, filePath);
+
+ _ = ChangeToken.OnChange(configuration.GetReloadToken, () =>
+ {
+ string newFilePath = GetFilePath(configuration, key, optionName, pathPropertyName);
+ watcher.ChangePath(newFilePath);
+ });
+
+ return watcher;
+ });
+
+ return services;
+ }
+
+ private static string GetFilePath(IConfiguration configuration, string key, string? optionName, string propertyName)
+ {
+ string? filePath = configuration.GetValue(string.IsNullOrEmpty(optionName)
+ ? $"{key}{ConfigurationPath.KeyDelimiter}{propertyName}"
+ : $"{key}{ConfigurationPath.KeyDelimiter}{optionName}{ConfigurationPath.KeyDelimiter}{propertyName}");
+
+ return filePath ?? string.Empty;
+ }
+}
diff --git a/src/Common/src/Certificates/Steeltoe.Common.Certificates.csproj b/src/Common/src/Certificates/Steeltoe.Common.Certificates.csproj
new file mode 100644
index 0000000000..85bf2ab00d
--- /dev/null
+++ b/src/Common/src/Certificates/Steeltoe.Common.Certificates.csproj
@@ -0,0 +1,18 @@
+
+
+ net8.0
+ Steeltoe shared library for working with certificates.
+ security;pem;certificate
+ true
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Common/src/Common/ApplicationInstanceInfo.cs b/src/Common/src/Common/ApplicationInstanceInfo.cs
new file mode 100644
index 0000000000..8951c7a409
--- /dev/null
+++ b/src/Common/src/Common/ApplicationInstanceInfo.cs
@@ -0,0 +1,12 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the Apache 2.0 License.
+// See the LICENSE file in the project root for more information.
+
+namespace Steeltoe.Common;
+
+///
+public class ApplicationInstanceInfo : IApplicationInstanceInfo
+{
+ ///
+ public virtual string? ApplicationName { get; set; }
+}
diff --git a/src/Common/src/Common/ArgumentGuard.cs b/src/Common/src/Common/ArgumentGuard.cs
new file mode 100644
index 0000000000..cb94f02bb0
--- /dev/null
+++ b/src/Common/src/Common/ArgumentGuard.cs
@@ -0,0 +1,51 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the Apache 2.0 License.
+// See the LICENSE file in the project root for more information.
+
+using System.Diagnostics;
+using System.Runtime.CompilerServices;
+
+namespace Steeltoe.Common;
+
+internal static class ArgumentGuard
+{
+ public static void ElementsNotNull(IEnumerable? elements, [CallerArgumentExpression(nameof(elements))] string? parameterName = null)
+ where T : class
+ {
+ AssertNoDotInParameterName(parameterName);
+
+ if (elements != null && elements.Any(element => element == null))
+ {
+ throw new ArgumentException("Collection element cannot be null.", parameterName);
+ }
+ }
+
+ public static void ElementsNotNullOrEmpty(IEnumerable? elements, [CallerArgumentExpression(nameof(elements))] string? parameterName = null)
+ {
+ AssertNoDotInParameterName(parameterName);
+
+ if (elements != null && elements.Any(string.IsNullOrEmpty))
+ {
+ throw new ArgumentException("Collection element cannot be null or an empty string.", parameterName);
+ }
+ }
+
+ public static void ElementsNotNullOrWhiteSpace(IEnumerable? elements, [CallerArgumentExpression(nameof(elements))] string? parameterName = null)
+ {
+ AssertNoDotInParameterName(parameterName);
+
+ if (elements != null && elements.Any(string.IsNullOrWhiteSpace))
+ {
+ throw new ArgumentException("Collection element cannot be null, an empty string, or composed entirely of whitespace.", parameterName);
+ }
+ }
+
+ [Conditional("DEBUG")]
+ private static void AssertNoDotInParameterName(string? parameterName)
+ {
+ if (parameterName != null && parameterName.Contains('.'))
+ {
+ throw new InvalidOperationException("Only use this method to verify the parameter itself, not its members.");
+ }
+ }
+}
diff --git a/src/Common/src/Common/AssemblyLoader.cs b/src/Common/src/Common/AssemblyLoader.cs
new file mode 100644
index 0000000000..d602ea6475
--- /dev/null
+++ b/src/Common/src/Common/AssemblyLoader.cs
@@ -0,0 +1,96 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the Apache 2.0 License.
+// See the LICENSE file in the project root for more information.
+
+using System.Reflection;
+
+namespace Steeltoe.Common;
+
+internal sealed class AssemblyLoader
+{
+ private static readonly IReadOnlySet EmptySet = new HashSet();
+
+ public IReadOnlySet AssemblyNamesToExclude { get; }
+
+ static AssemblyLoader()
+ {
+ AppDomain.CurrentDomain.AssemblyResolve += AssemblyResolver.LoadAnyVersion;
+ }
+
+ public AssemblyLoader()
+ : this(EmptySet)
+ {
+ }
+
+ public AssemblyLoader(IReadOnlySet assemblyNamesToExclude)
+ {
+ ArgumentNullException.ThrowIfNull(assemblyNamesToExclude);
+ ArgumentGuard.ElementsNotNullOrWhiteSpace(assemblyNamesToExclude);
+
+ // Take a copy to ensure comparisons are case-insensitive.
+ AssemblyNamesToExclude = assemblyNamesToExclude.ToHashSet(StringComparer.OrdinalIgnoreCase);
+ }
+
+ public bool IsAssemblyLoaded(string assemblyName)
+ {
+ ArgumentException.ThrowIfNullOrWhiteSpace(assemblyName);
+
+ if (AssemblyNamesToExclude.Contains(assemblyName))
+ {
+ return false;
+ }
+
+ return TryLoadAssembly(assemblyName);
+ }
+
+ private static bool TryLoadAssembly(string assemblyName)
+ {
+ try
+ {
+ _ = Assembly.Load(assemblyName);
+ return true;
+ }
+ catch (Exception exception) when (exception is ArgumentException or IOException or BadImageFormatException)
+ {
+ return false;
+ }
+ }
+
+ private static class AssemblyResolver
+ {
+ private static readonly HashSet FailedAssemblyNames = new(StringComparer.OrdinalIgnoreCase);
+
+ public static Assembly? LoadAnyVersion(object? sender, ResolveEventArgs args)
+ {
+ // Load whatever version is available (ignore Version, Culture and PublicKeyToken).
+ string assemblySimpleName = new AssemblyName(args.Name).Name!;
+
+ if (FailedAssemblyNames.Contains(assemblySimpleName))
+ {
+ return null;
+ }
+
+ // AssemblyName.Equals() returns false when path and full name are identical, so the code below
+ // avoids a crash caused by inserting duplicate keys in the dictionary.
+ Dictionary assembliesBySimpleName = AppDomain.CurrentDomain.GetAssemblies().GroupBy(nextAssembly => nextAssembly.GetName().Name!)
+ .ToDictionary(grouping => grouping.Key, grouping => grouping.First(), StringComparer.OrdinalIgnoreCase);
+
+ if (assembliesBySimpleName.TryGetValue(assemblySimpleName, out Assembly? assembly))
+ {
+ return assembly;
+ }
+
+ if (args.Name.Contains(".resources", StringComparison.Ordinal))
+ {
+ return args.RequestingAssembly;
+ }
+
+ // Prevent recursive attempts to resolve.
+ FailedAssemblyNames.Add(assemblySimpleName);
+ assembly = Assembly.Load(assemblySimpleName);
+ FailedAssemblyNames.Remove(assemblySimpleName);
+
+ return assembly;
+ }
+ }
+}
diff --git a/src/Common/src/Common/CasingConventions/EnumExtensions.cs b/src/Common/src/Common/CasingConventions/EnumExtensions.cs
new file mode 100644
index 0000000000..a1323d3ac8
--- /dev/null
+++ b/src/Common/src/Common/CasingConventions/EnumExtensions.cs
@@ -0,0 +1,29 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the Apache 2.0 License.
+// See the LICENSE file in the project root for more information.
+
+namespace Steeltoe.Common.CasingConventions;
+
+public static class EnumExtensions
+{
+ ///
+ /// Converts a pascal-cased enum member to its snake-case string representation.
+ ///
+ ///
+ /// The enumeration type.
+ ///
+ ///
+ /// The enumeration member to convert.
+ ///
+ ///
+ /// The output style.
+ ///
+ public static string ToSnakeCaseString(this TEnum value, SnakeCaseStyle style)
+ where TEnum : struct, Enum
+ {
+ string pascalCaseText = value.ToString();
+
+ var converter = new SnakeCaseEnumConverter(style);
+ return converter.ToSnakeCase(pascalCaseText);
+ }
+}
diff --git a/src/Common/src/Common/CasingConventions/SnakeCaseAllCapsEnumMemberJsonConverter.cs b/src/Common/src/Common/CasingConventions/SnakeCaseAllCapsEnumMemberJsonConverter.cs
new file mode 100644
index 0000000000..618148a1ee
--- /dev/null
+++ b/src/Common/src/Common/CasingConventions/SnakeCaseAllCapsEnumMemberJsonConverter.cs
@@ -0,0 +1,34 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the Apache 2.0 License.
+// See the LICENSE file in the project root for more information.
+
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace Steeltoe.Common.CasingConventions;
+
+///
+/// Converts between pascal-cased enum members and their snake-case-all-caps string representation.
+///
+/// OUT_OF_SERVICE
+/// ]]>
+///
+///
+public sealed class SnakeCaseAllCapsEnumMemberJsonConverter : JsonConverterFactory
+{
+ ///
+ public override bool CanConvert(Type typeToConvert)
+ {
+ ArgumentNullException.ThrowIfNull(typeToConvert);
+
+ return typeToConvert.IsEnum;
+ }
+
+ ///
+ public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
+ {
+ Type converterType = typeof(SnakeCaseEnumConverter<>).MakeGenericType(typeToConvert);
+ return (JsonConverter)Activator.CreateInstance(converterType, SnakeCaseStyle.AllCaps)!;
+ }
+}
diff --git a/src/Common/src/Common/CasingConventions/SnakeCaseEnumConverter.cs b/src/Common/src/Common/CasingConventions/SnakeCaseEnumConverter.cs
new file mode 100644
index 0000000000..d57ccbc69c
--- /dev/null
+++ b/src/Common/src/Common/CasingConventions/SnakeCaseEnumConverter.cs
@@ -0,0 +1,110 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the Apache 2.0 License.
+// See the LICENSE file in the project root for more information.
+
+using System.Text;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+#pragma warning disable S4040 // Strings should be normalized to uppercase
+
+namespace Steeltoe.Common.CasingConventions;
+
+///
+/// Converts between pascal-cased enum members and their snake-case string representation.
+///
+///
+/// The enumeration type.
+///
+internal sealed class SnakeCaseEnumConverter(SnakeCaseStyle style) : JsonConverter
+ where TEnum : struct, Enum
+{
+ private readonly SnakeCaseStyle _style = style;
+
+ ///
+ public override bool CanConvert(Type typeToConvert)
+ {
+ return typeToConvert.IsEnum;
+ }
+
+ ///
+ public override TEnum Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ JsonTokenType token = reader.TokenType;
+
+ if (token == JsonTokenType.String)
+ {
+ string enumText = reader.GetString()!;
+ string pascalCaseText = ToPascalCase(enumText);
+
+ if (Enum.TryParse(pascalCaseText, out TEnum value) || Enum.TryParse(pascalCaseText, true, out value))
+ {
+ return value;
+ }
+ }
+
+ throw new JsonException();
+ }
+
+ private string ToPascalCase(string snakeCaseText)
+ {
+ var builder = new StringBuilder();
+ bool nextCharToUpper = true;
+
+ foreach (char ch in snakeCaseText)
+ {
+ if (ch == '_')
+ {
+ nextCharToUpper = true;
+ continue;
+ }
+
+ if (nextCharToUpper)
+ {
+ builder.Append(char.ToUpperInvariant(ch));
+ nextCharToUpper = false;
+ }
+ else
+ {
+ builder.Append(char.ToLowerInvariant(ch));
+ }
+ }
+
+ return builder.ToString();
+ }
+
+ ///
+ public override void Write(Utf8JsonWriter writer, TEnum value, JsonSerializerOptions options)
+ {
+ string pascalCaseText = value.ToString();
+ string snakeCaseText = ToSnakeCase(pascalCaseText);
+
+ writer.WriteStringValue(snakeCaseText);
+ }
+
+ public string ToSnakeCase(string pascalCaseText)
+ {
+ var builder = new StringBuilder();
+
+ for (int index = 0; index < pascalCaseText.Length; index++)
+ {
+ char ch = pascalCaseText[index];
+
+ if (char.IsUpper(ch))
+ {
+ if (index > 0)
+ {
+ builder.Append('_');
+ }
+
+ builder.Append(_style == SnakeCaseStyle.AllCaps ? ch : char.ToLowerInvariant(ch));
+ }
+ else
+ {
+ builder.Append(_style == SnakeCaseStyle.NoCaps ? ch : char.ToUpperInvariant(ch));
+ }
+ }
+
+ return builder.ToString();
+ }
+}
diff --git a/src/Common/src/Common/CasingConventions/SnakeCaseStyle.cs b/src/Common/src/Common/CasingConventions/SnakeCaseStyle.cs
new file mode 100644
index 0000000000..85d0e11ff7
--- /dev/null
+++ b/src/Common/src/Common/CasingConventions/SnakeCaseStyle.cs
@@ -0,0 +1,21 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the Apache 2.0 License.
+// See the LICENSE file in the project root for more information.
+
+namespace Steeltoe.Common.CasingConventions;
+
+///
+/// Defines styles for text conversion to snake-case naming convention.
+///
+public enum SnakeCaseStyle
+{
+ ///
+ /// Indicates all characters uppercase, for example: OUT_OF_SERVICE.
+ ///
+ AllCaps,
+
+ ///
+ /// Indicates all characters lowercase, for example: out_of_service.
+ ///
+ NoCaps
+}
diff --git a/src/Common/src/Common/Configuration/ConfigurationKeyConverter.cs b/src/Common/src/Common/Configuration/ConfigurationKeyConverter.cs
new file mode 100644
index 0000000000..a0787b014c
--- /dev/null
+++ b/src/Common/src/Common/Configuration/ConfigurationKeyConverter.cs
@@ -0,0 +1,87 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the Apache 2.0 License.
+// See the LICENSE file in the project root for more information.
+
+using System.Text;
+using System.Text.RegularExpressions;
+using Microsoft.Extensions.Configuration;
+
+namespace Steeltoe.Common.Configuration;
+
+internal static class ConfigurationKeyConverter
+{
+ private const string DotDelimiterString = ".";
+ private const char DotDelimiterChar = '.';
+ private const char UnderscoreDelimiterChar = '_';
+ private const char EscapeChar = '\\';
+ private const string EscapeString = "\\";
+
+ private static readonly Regex ArrayRegex = new(@"\[(?\d+)\]", RegexOptions.Compiled | RegexOptions.Singleline, TimeSpan.FromSeconds(1));
+
+ public static string AsDotNetConfigurationKey(string key)
+ {
+ if (string.IsNullOrEmpty(key))
+ {
+ return key;
+ }
+
+ IEnumerable split = UniversalHierarchySplit(key);
+ var sb = new StringBuilder();
+
+ foreach (string keyPart in split.Select(ConvertArrayKey))
+ {
+ sb.Append(keyPart);
+ sb.Append(ConfigurationPath.KeyDelimiter);
+ }
+
+ return sb.ToString(0, sb.Length - 1);
+ }
+
+ private static IEnumerable UniversalHierarchySplit(string source)
+ {
+ List result = [];
+
+ int segmentStart = 0;
+
+ for (int i = 0; i < source.Length; i++)
+ {
+ bool readEscapeChar = false;
+
+ if (source[i] == EscapeChar)
+ {
+ readEscapeChar = true;
+ i++;
+ }
+
+ if (!readEscapeChar && source[i] == DotDelimiterChar)
+ {
+ result.Add(UnEscapeString(source[segmentStart..i]));
+ segmentStart = i + 1;
+ }
+
+ if (!readEscapeChar && source[i] == UnderscoreDelimiterChar && i < source.Length - 1 && source[i + 1] == UnderscoreDelimiterChar)
+ {
+ result.Add(UnEscapeString(source[segmentStart..i]));
+ segmentStart = i + 2;
+ }
+
+ if (i == source.Length - 1)
+ {
+ result.Add(UnEscapeString(source[segmentStart..]));
+ }
+ }
+
+ return [.. result];
+
+ static string UnEscapeString(string src)
+ {
+ return src.Replace(EscapeString + DotDelimiterString, DotDelimiterString, StringComparison.Ordinal)
+ .Replace(EscapeString + EscapeString, EscapeString, StringComparison.Ordinal);
+ }
+ }
+
+ private static string ConvertArrayKey(string key)
+ {
+ return ArrayRegex.Replace(key, ":${digits}");
+ }
+}
diff --git a/src/Common/src/Common/Configuration/SpringApplicationSettings.cs b/src/Common/src/Common/Configuration/SpringApplicationSettings.cs
new file mode 100644
index 0000000000..3f25cb7115
--- /dev/null
+++ b/src/Common/src/Common/Configuration/SpringApplicationSettings.cs
@@ -0,0 +1,18 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the Apache 2.0 License.
+// See the LICENSE file in the project root for more information.
+
+namespace Steeltoe.Common.Configuration;
+
+///
+/// Fallback configuration settings that describe this application.
+///
+internal sealed class SpringApplicationSettings
+{
+ // This type only exists to enable JSON schema documentation via ConfigurationSchemaAttribute.
+
+ ///
+ /// Gets or sets the name of this application.
+ ///
+ public string? Name { get; set; }
+}
diff --git a/src/Common/src/Common/ConfigurationSchemaAttributes.cs b/src/Common/src/Common/ConfigurationSchemaAttributes.cs
new file mode 100644
index 0000000000..8872dabe80
--- /dev/null
+++ b/src/Common/src/Common/ConfigurationSchemaAttributes.cs
@@ -0,0 +1,58 @@
+#pragma warning disable
+
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+// Steeltoe: This file was copied from the .NET Aspire Configuration Schema generator
+// at https://github.com/dotnet/aspire/tree/cb7cc4d78f8dd2b4df1053a229493cdbf88f50df/src/Tools/ConfigurationSchemaGenerator.
+
+using System.Diagnostics.CodeAnalysis;
+
+namespace Aspire;
+
+///
+/// Attribute used to automatically generate a JSON schema for a component's configuration.
+///
+[ExcludeFromCodeCoverage(Justification = "Used while compiling Steeltoe, does not ship with Steeltoe.")]
+[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
+internal sealed class ConfigurationSchemaAttribute : Attribute
+{
+ public ConfigurationSchemaAttribute(string path, Type type, string[]? exclusionPaths = null)
+ {
+ Path = path;
+ Type = type;
+ ExclusionPaths = exclusionPaths;
+ }
+
+ ///
+ /// The path corresponding to which config section binds to.
+ ///
+ public string Path { get; }
+
+ ///
+ /// The type that is bound to the configuration. This type will be walked and generate a JSON schema for all the properties.
+ ///
+ public Type Type { get; }
+
+ ///
+ /// (optional) The config sections to exclude from the ConfigurationSchema. This is useful if there are properties you don't want to publicize in the config schema.
+ ///
+ public string[]? ExclusionPaths { get; }
+}
+
+///
+/// Provides information to describe the logging categories produced by a component.
+///
+[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = false)]
+internal sealed class LoggingCategoriesAttribute : Attribute
+{
+ public LoggingCategoriesAttribute(params string[] categories)
+ {
+ Categories = categories;
+ }
+
+ ///
+ /// The list of log categories produced by the component. These categories will show up under the Logging:LogLevel section in appsettings.json.
+ ///
+ public string[] Categories { get; set; }
+}
diff --git a/src/Common/src/Common/ConfigureApplicationInstanceInfo.cs b/src/Common/src/Common/ConfigureApplicationInstanceInfo.cs
new file mode 100644
index 0000000000..d59e9c0715
--- /dev/null
+++ b/src/Common/src/Common/ConfigureApplicationInstanceInfo.cs
@@ -0,0 +1,35 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the Apache 2.0 License.
+// See the LICENSE file in the project root for more information.
+
+using System.Reflection;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Options;
+
+namespace Steeltoe.Common;
+
+internal sealed class ConfigureApplicationInstanceInfo : IConfigureOptions
+{
+ private readonly IConfiguration _configuration;
+
+ public ConfigureApplicationInstanceInfo(IConfiguration configuration)
+ {
+ ArgumentNullException.ThrowIfNull(configuration);
+
+ _configuration = configuration;
+ }
+
+ public void Configure(ApplicationInstanceInfo options)
+ {
+ ArgumentNullException.ThrowIfNull(options);
+
+ options.ApplicationName ??= _configuration.GetValue("spring:application:name") ??
+ GetAspNetApplicationName() ?? Assembly.GetEntryAssembly()!.GetName().Name;
+ }
+
+ private string? GetAspNetApplicationName()
+ {
+ // When using UseStartup() on the host builder, ASP.NET Core sets the below key to point to the assembly containing T.
+ return _configuration.GetValue("applicationName");
+ }
+}
diff --git a/src/Common/src/Common/Discovery/DiscoveryClientHostedService.cs b/src/Common/src/Common/Discovery/DiscoveryClientHostedService.cs
new file mode 100644
index 0000000000..aed725cd82
--- /dev/null
+++ b/src/Common/src/Common/Discovery/DiscoveryClientHostedService.cs
@@ -0,0 +1,38 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the Apache 2.0 License.
+// See the LICENSE file in the project root for more information.
+
+using Microsoft.Extensions.Hosting;
+
+namespace Steeltoe.Common.Discovery;
+
+///
+/// Calls when the app is being stopped.
+///
+internal sealed class DiscoveryClientHostedService : IHostedService
+{
+ private readonly IDiscoveryClient[] _discoveryClients;
+
+ public DiscoveryClientHostedService(IEnumerable discoveryClients)
+ {
+ ArgumentNullException.ThrowIfNull(discoveryClients);
+
+ IDiscoveryClient[] discoveryClientArray = discoveryClients.ToArray();
+ ArgumentGuard.ElementsNotNull(discoveryClientArray);
+
+ _discoveryClients = discoveryClientArray;
+ }
+
+ public Task StartAsync(CancellationToken cancellationToken)
+ {
+ return Task.CompletedTask;
+ }
+
+ public async Task StopAsync(CancellationToken cancellationToken)
+ {
+ foreach (IDiscoveryClient discoveryClient in _discoveryClients)
+ {
+ await discoveryClient.ShutdownAsync(cancellationToken);
+ }
+ }
+}
diff --git a/src/Common/src/Common/Discovery/IDiscoveryClient.cs b/src/Common/src/Common/Discovery/IDiscoveryClient.cs
new file mode 100644
index 0000000000..598095b2a9
--- /dev/null
+++ b/src/Common/src/Common/Discovery/IDiscoveryClient.cs
@@ -0,0 +1,65 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the Apache 2.0 License.
+// See the LICENSE file in the project root for more information.
+
+namespace Steeltoe.Common.Discovery;
+
+///
+/// Provides access to remote service instances using a discovery server.
+///
+public interface IDiscoveryClient
+{
+ ///
+ /// Gets a human-readable description of this discovery client.
+ ///
+ string Description { get; }
+
+ ///
+ /// Gets information used to register the local service instance (this app) to the discovery server.
+ ///
+ ///
+ /// The service instance that represents this app, or null when unavailable.
+ ///
+ IServiceInstance? GetLocalServiceInstance();
+
+ ///
+ /// Gets all registered service IDs from the discovery server.
+ ///
+ ///
+ /// The token to monitor for cancellation requests.
+ ///
+ ///
+ /// The list of service IDs.
+ ///
+ Task> GetServiceIdsAsync(CancellationToken cancellationToken);
+
+ ///
+ /// Gets all service instances associated with the specified service ID from the discovery server.
+ ///
+ ///
+ /// The ID of the service to lookup.
+ ///
+ ///
+ /// The token to monitor for cancellation requests.
+ ///
+ ///
+ /// The list of remote service instances.
+ ///
+ Task> GetInstancesAsync(string serviceId, CancellationToken cancellationToken);
+
+ ///
+ /// Deregisters the local service (this app) from the discovery server.
+ ///
+ ///
+ /// This method exists for two reasons, instead of just reusing . The first reason is that this method enables
+ /// cancellation. The second reason is more complicated. Deregistration typically requires to send an HTTP request to the discovery server. When using
+ /// `IHttpClientFactory`, it requires the IoC container to obtain an . But that fails when the container is being disposed.
+ /// Deregistration must be performed earlier. That's why implementations should register `DiscoveryClientHostedService` in the IoC container, which is a
+ /// hosted service that performs deregistration (by calling this method) when the app is terminating. At that time, the IoC container is still
+ /// accessible.
+ ///
+ ///
+ /// The token to monitor for cancellation requests.
+ ///
+ Task ShutdownAsync(CancellationToken cancellationToken);
+}
diff --git a/src/Common/src/Common/Discovery/IServiceInstance.cs b/src/Common/src/Common/Discovery/IServiceInstance.cs
new file mode 100644
index 0000000000..8f23b8ef3d
--- /dev/null
+++ b/src/Common/src/Common/Discovery/IServiceInstance.cs
@@ -0,0 +1,38 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the Apache 2.0 License.
+// See the LICENSE file in the project root for more information.
+
+namespace Steeltoe.Common.Discovery;
+
+public interface IServiceInstance
+{
+ ///
+ /// Gets the service ID as registered by the discovery client.
+ ///
+ string ServiceId { get; }
+
+ ///
+ /// Gets the hostname of the registered service instance.
+ ///
+ string Host { get; }
+
+ ///
+ /// Gets the port of the registered service instance.
+ ///
+ int Port { get; }
+
+ ///
+ /// Gets a value indicating whether the scheme of the registered service instance is https.
+ ///
+ bool IsSecure { get; }
+
+ ///
+ /// Gets the resolved address of the registered service instance.
+ ///
+ Uri Uri { get; }
+
+ ///
+ /// Gets the key/value metadata associated with this service instance.
+ ///
+ IReadOnlyDictionary Metadata { get; }
+}
diff --git a/src/Common/src/Common/DynamicTypeAccess/InstanceAccessor.cs b/src/Common/src/Common/DynamicTypeAccess/InstanceAccessor.cs
new file mode 100644
index 0000000000..cb46cb4ed4
--- /dev/null
+++ b/src/Common/src/Common/DynamicTypeAccess/InstanceAccessor.cs
@@ -0,0 +1,70 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the Apache 2.0 License.
+// See the LICENSE file in the project root for more information.
+
+namespace Steeltoe.Common.DynamicTypeAccess;
+
+///
+/// Provides reflection-based access to the members of an object instance.
+///
+internal sealed class InstanceAccessor : ReflectionAccessor
+{
+ public TypeAccessor DeclaredTypeAccessor { get; }
+ public object Instance { get; }
+
+ public InstanceAccessor(TypeAccessor declaredTypeAccessor, object instance)
+ : base(AssertNotNull(declaredTypeAccessor))
+ {
+ ArgumentNullException.ThrowIfNull(instance);
+
+ if (!declaredTypeAccessor.Type.IsInstanceOfType(instance))
+ {
+ throw new InvalidOperationException($"Object of type '{instance.GetType()}' is not assignable to type '{declaredTypeAccessor.Type}'.");
+ }
+
+ DeclaredTypeAccessor = declaredTypeAccessor;
+ Instance = instance;
+ }
+
+ private static Type AssertNotNull(TypeAccessor declaredTypeAccessor)
+ {
+ ArgumentNullException.ThrowIfNull(declaredTypeAccessor);
+
+ return declaredTypeAccessor.Type;
+ }
+
+ public InstanceAccessor AsRuntimeType()
+ {
+ // Interface members that are defined in a base interface aren't found by reflection.
+ // To work around that, reflect against the actual implementation.
+
+ Type runtimeType = Instance.GetType();
+ var typeAccessor = new TypeAccessor(runtimeType);
+ return new InstanceAccessor(typeAccessor, Instance);
+ }
+
+ public T GetPrivateFieldValue(string name)
+ {
+ return GetPrivateFieldValue(name, Instance);
+ }
+
+ public T GetPropertyValue(string name)
+ {
+ return GetPropertyValue(name, Instance);
+ }
+
+ public void SetPropertyValue(string name, object? value)
+ {
+ SetPropertyValue(name, Instance, value);
+ }
+
+ public object? InvokeMethod(string name, bool isPublic, params object?[] arguments)
+ {
+ return base.InvokeMethod(name, isPublic, Instance, arguments);
+ }
+
+ public object? InvokeMethodOverload(string name, bool isPublic, Type[] parameterTypes, params object?[] arguments)
+ {
+ return base.InvokeMethodOverload(name, isPublic, parameterTypes, Instance, arguments);
+ }
+}
diff --git a/src/Common/src/Common/DynamicTypeAccess/PackageResolver.cs b/src/Common/src/Common/DynamicTypeAccess/PackageResolver.cs
new file mode 100644
index 0000000000..98293e61bf
--- /dev/null
+++ b/src/Common/src/Common/DynamicTypeAccess/PackageResolver.cs
@@ -0,0 +1,106 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the Apache 2.0 License.
+// See the LICENSE file in the project root for more information.
+
+using System.Collections.Immutable;
+using System.Reflection;
+
+namespace Steeltoe.Common.DynamicTypeAccess;
+
+///
+/// Dynamically loads s at runtime from a list of candidates.
+///
+internal abstract class PackageResolver
+{
+ private static readonly IReadOnlySet EmptySet = ImmutableHashSet.Empty;
+
+ private readonly IReadOnlyList _assemblyNames;
+ private readonly IReadOnlyList _packageNames;
+
+ protected PackageResolver(string assemblyName, string packageName)
+ : this([assemblyName], [packageName])
+ {
+ }
+
+ protected PackageResolver(IReadOnlyList assemblyNames, IReadOnlyList packageNames)
+ {
+ ArgumentNullException.ThrowIfNull(assemblyNames);
+ ArgumentGuard.ElementsNotNullOrWhiteSpace(assemblyNames);
+ ArgumentNullException.ThrowIfNull(packageNames);
+ ArgumentGuard.ElementsNotNullOrWhiteSpace(packageNames);
+
+ _assemblyNames = assemblyNames;
+ _packageNames = packageNames;
+ }
+
+ public bool IsAvailable()
+ {
+ return IsAvailable(EmptySet);
+ }
+
+ public bool IsAvailable(IReadOnlySet assemblyNamesToExclude)
+ {
+ return IsAssemblyAvailable(assemblyNamesToExclude);
+ }
+
+ protected virtual bool IsAssemblyAvailable(IReadOnlySet assemblyNamesToExclude)
+ {
+ foreach (string assemblyName in _assemblyNames)
+ {
+ if (!assemblyNamesToExclude.Contains(assemblyName))
+ {
+ try
+ {
+ Assembly.Load(new AssemblyName(assemblyName));
+ return true;
+ }
+ catch (Exception exception) when (exception is ArgumentException or IOException or BadImageFormatException)
+ {
+ // Intentionally left empty.
+ }
+ }
+ }
+
+ return false;
+ }
+
+ protected TypeAccessor ResolveType(params string[] typeNames)
+ {
+ ArgumentNullException.ThrowIfNull(typeNames);
+ ArgumentGuard.ElementsNotNullOrWhiteSpace(typeNames);
+
+ List exceptions = [];
+
+ // A type can be moved to a different assembly in a future NuGet version, so probe all combinations to be resilient against that.
+ foreach (string assemblyName in _assemblyNames)
+ {
+ try
+ {
+ Assembly assembly = Assembly.Load(new AssemblyName(assemblyName));
+
+ foreach (string typeName in typeNames)
+ {
+ try
+ {
+ Type type = assembly.GetType(typeName, true)!;
+ return new TypeAccessor(type);
+ }
+ catch (Exception exception) when (exception is ArgumentException or IOException or BadImageFormatException or TypeLoadException)
+ {
+ exceptions.Add(exception);
+ }
+ }
+ }
+ catch (Exception exception) when (exception is ArgumentException or IOException or BadImageFormatException)
+ {
+ exceptions.Add(exception);
+ }
+ }
+
+ throw new AggregateException(
+ _packageNames.Count == 1
+ ? $"Unable to load a required type. Please add the '{_packageNames[0]}' NuGet package to your project."
+ : $"Unable to load a required type. Please add one of these NuGet packages to your project: '{string.Join("', '", _packageNames)}'.",
+ exceptions);
+ }
+}
diff --git a/src/Common/src/Common/DynamicTypeAccess/ReflectionAccessor.cs b/src/Common/src/Common/DynamicTypeAccess/ReflectionAccessor.cs
new file mode 100644
index 0000000000..33f4b3ee46
--- /dev/null
+++ b/src/Common/src/Common/DynamicTypeAccess/ReflectionAccessor.cs
@@ -0,0 +1,150 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the Apache 2.0 License.
+// See the LICENSE file in the project root for more information.
+
+using System.Reflection;
+
+namespace Steeltoe.Common.DynamicTypeAccess;
+
+///
+/// Base type for reflection-based type/member access.
+///
+internal abstract class ReflectionAccessor
+{
+ private readonly Type _type;
+
+ protected ReflectionAccessor(Type type)
+ {
+ ArgumentNullException.ThrowIfNull(type);
+
+ _type = type;
+ }
+
+ protected T GetPrivateFieldValue(string name, object? instance)
+ {
+ ArgumentException.ThrowIfNullOrWhiteSpace(name);
+
+ object? value = GetPrivateFieldValue(name, instance);
+ return (T)value!;
+ }
+
+ private object? GetPrivateFieldValue(string name, object? instance)
+ {
+ FieldInfo fieldInfo = GetField(name, false, instance == null);
+ return fieldInfo.GetValue(instance);
+ }
+
+ private FieldInfo GetField(string name, bool isPublic, bool isStatic)
+ {
+ BindingFlags bindingFlags = CreateBindingFlags(isPublic, isStatic);
+ FieldInfo? fieldInfo = _type.GetField(name, bindingFlags);
+
+ if (fieldInfo == null)
+ {
+ throw new InvalidOperationException($"Field '{_type}.{name}' does not exist.");
+ }
+
+ return fieldInfo;
+ }
+
+ protected T GetPropertyValue(string name, object? instance)
+ {
+ ArgumentException.ThrowIfNullOrWhiteSpace(name);
+
+ object? value = GetPropertyValue(name, instance);
+ return (T)value!;
+ }
+
+ private object? GetPropertyValue(string name, object? instance)
+ {
+ MethodInfo propertySetter = GetPropertyGetter(name);
+ return propertySetter.Invoke(instance, []);
+ }
+
+ private MethodInfo GetPropertyGetter(string name)
+ {
+ PropertyInfo propertyInfo = GetProperty(name);
+
+ if (propertyInfo.GetMethod == null)
+ {
+ throw new InvalidOperationException($"Property '{_type}.{name}' is write-only.");
+ }
+
+ return propertyInfo.GetMethod;
+ }
+
+ protected void SetPropertyValue(string name, object? instance, object? value)
+ {
+ ArgumentException.ThrowIfNullOrWhiteSpace(name);
+
+ MethodInfo propertySetter = GetPropertySetter(name);
+
+ propertySetter.Invoke(instance, [value]);
+ }
+
+ private MethodInfo GetPropertySetter(string name)
+ {
+ PropertyInfo propertyInfo = GetProperty(name);
+
+ if (propertyInfo.SetMethod == null)
+ {
+ throw new InvalidOperationException($"Property '{_type}.{name}' is read-only.");
+ }
+
+ return propertyInfo.SetMethod;
+ }
+
+ private PropertyInfo GetProperty(string name)
+ {
+ PropertyInfo? propertyInfo = _type.GetProperty(name);
+
+ if (propertyInfo == null)
+ {
+ throw new InvalidOperationException($"Property '{_type}.{name}' does not exist.");
+ }
+
+ return propertyInfo;
+ }
+
+ protected object? InvokeMethod(string name, bool isPublic, object? instance, object?[] arguments)
+ {
+ ArgumentException.ThrowIfNullOrWhiteSpace(name);
+ ArgumentNullException.ThrowIfNull(arguments);
+
+ MethodInfo methodInfo = GetMethod(name, isPublic, instance == null, null);
+ return methodInfo.Invoke(instance, arguments);
+ }
+
+ protected object? InvokeMethodOverload(string name, bool isPublic, Type[] parameterTypes, object? instance, object?[] arguments)
+ {
+ ArgumentException.ThrowIfNullOrWhiteSpace(name);
+ ArgumentNullException.ThrowIfNull(parameterTypes);
+ ArgumentGuard.ElementsNotNull(parameterTypes);
+ ArgumentNullException.ThrowIfNull(arguments);
+
+ MethodInfo methodInfo = GetMethod(name, isPublic, instance == null, parameterTypes);
+ return methodInfo.Invoke(instance, arguments);
+ }
+
+ private MethodInfo GetMethod(string name, bool isPublic, bool isStatic, Type[]? parameterTypes)
+ {
+ BindingFlags bindingFlags = CreateBindingFlags(isPublic, isStatic);
+
+ MethodInfo? methodInfo = parameterTypes == null ? _type.GetMethod(name, bindingFlags) : _type.GetMethod(name, bindingFlags, parameterTypes);
+
+ if (methodInfo == null)
+ {
+ throw new InvalidOperationException($"Method '{_type}.{name}' does not exist.");
+ }
+
+ return methodInfo;
+ }
+
+ private static BindingFlags CreateBindingFlags(bool isPublic, bool isStatic)
+ {
+ BindingFlags bindingFlags = default;
+ bindingFlags |= isPublic ? BindingFlags.Public : BindingFlags.NonPublic;
+ bindingFlags |= isStatic ? BindingFlags.Static : BindingFlags.Instance;
+ return bindingFlags;
+ }
+}
diff --git a/src/Common/src/Common/DynamicTypeAccess/Shim.cs b/src/Common/src/Common/DynamicTypeAccess/Shim.cs
new file mode 100644
index 0000000000..e1bcac4704
--- /dev/null
+++ b/src/Common/src/Common/DynamicTypeAccess/Shim.cs
@@ -0,0 +1,28 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the Apache 2.0 License.
+// See the LICENSE file in the project root for more information.
+
+namespace Steeltoe.Common.DynamicTypeAccess;
+
+///
+/// Building block to provide statically-typed access to a that is dynamically loaded at runtime.
+///
+internal abstract class Shim
+{
+ protected InstanceAccessor InstanceAccessor { get; }
+
+ public Type DeclaredType => InstanceAccessor.DeclaredTypeAccessor.Type;
+ public virtual object Instance => InstanceAccessor.Instance;
+
+ protected Shim(InstanceAccessor instanceAccessor)
+ {
+ ArgumentNullException.ThrowIfNull(instanceAccessor);
+
+ InstanceAccessor = instanceAccessor;
+ }
+
+ public override string? ToString()
+ {
+ return InstanceAccessor.Instance.ToString();
+ }
+}
diff --git a/src/Common/src/Common/DynamicTypeAccess/TaskShim.cs b/src/Common/src/Common/DynamicTypeAccess/TaskShim.cs
new file mode 100644
index 0000000000..561bf2fd96
--- /dev/null
+++ b/src/Common/src/Common/DynamicTypeAccess/TaskShim.cs
@@ -0,0 +1,38 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the Apache 2.0 License.
+// See the LICENSE file in the project root for more information.
+
+namespace Steeltoe.Common.DynamicTypeAccess;
+
+///
+/// Provides a shim for a compiler-generated type that behaves like a , but is not assignable to it. For example:
+/// .AsyncStateMachineBox]]>.
+///
+///
+/// The type of the result produced by the underlying task.
+///
+internal sealed class TaskShim(object instance)
+ : Shim(Wrap(instance)), IDisposable
+{
+ public override Task Instance => (Task)base.Instance;
+
+ private static InstanceAccessor Wrap(object instance)
+ {
+ Type taskLikeType = instance.GetType();
+ var typeAccessor = new TypeAccessor(taskLikeType);
+ return new InstanceAccessor(typeAccessor, instance);
+ }
+
+ public TResult GetResult()
+ {
+ object awaiter = InstanceAccessor.InvokeMethod("GetAwaiter", true)!;
+ var awaiterAccessor = new InstanceAccessor(new TypeAccessor(awaiter.GetType()), awaiter);
+ object result = awaiterAccessor.InvokeMethod("GetResult", true)!;
+ return (TResult)result;
+ }
+
+ public void Dispose()
+ {
+ Instance.Dispose();
+ }
+}
diff --git a/src/Common/src/Common/DynamicTypeAccess/TypeAccessor.cs b/src/Common/src/Common/DynamicTypeAccess/TypeAccessor.cs
new file mode 100644
index 0000000000..005288588b
--- /dev/null
+++ b/src/Common/src/Common/DynamicTypeAccess/TypeAccessor.cs
@@ -0,0 +1,55 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the Apache 2.0 License.
+// See the LICENSE file in the project root for more information.
+
+namespace Steeltoe.Common.DynamicTypeAccess;
+
+///
+/// Provides reflection-based access to static members of a .
+///
+internal sealed class TypeAccessor(Type type)
+ : ReflectionAccessor(type)
+{
+ public Type Type { get; } = type;
+
+ public static TypeAccessor MakeGenericAccessor(Type openType, params Type[] typeArguments)
+ {
+ ArgumentNullException.ThrowIfNull(openType);
+ ArgumentNullException.ThrowIfNull(typeArguments);
+ ArgumentGuard.ElementsNotNull(typeArguments);
+
+ Type constructedType = openType.MakeGenericType(typeArguments);
+ return new TypeAccessor(constructedType);
+ }
+
+ public InstanceAccessor CreateInstance(params object?[]? arguments)
+ {
+ object instance = arguments is { Length: > 0 } ? Activator.CreateInstance(Type, arguments)! : Activator.CreateInstance(Type)!;
+ return new InstanceAccessor(this, instance);
+ }
+
+ public T GetPrivateFieldValue(string name)
+ {
+ return GetPrivateFieldValue(name, null);
+ }
+
+ public T GetPropertyValue(string name)
+ {
+ return GetPropertyValue(name, null);
+ }
+
+ public void SetPropertyValue(string name, object? value)
+ {
+ SetPropertyValue(name, null, value);
+ }
+
+ public object? InvokeMethod(string name, bool isPublic, params object?[] arguments)
+ {
+ return base.InvokeMethod(name, isPublic, null, arguments);
+ }
+
+ public object? InvokeMethodOverload(string name, bool isPublic, Type[] parameterTypes, params object?[] arguments)
+ {
+ return base.InvokeMethodOverload(name, isPublic, parameterTypes, null, arguments);
+ }
+}
diff --git a/src/Common/src/Common/Extensions/ExceptionExtensions.cs b/src/Common/src/Common/Extensions/ExceptionExtensions.cs
new file mode 100644
index 0000000000..9d3cf1919d
--- /dev/null
+++ b/src/Common/src/Common/Extensions/ExceptionExtensions.cs
@@ -0,0 +1,79 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the Apache 2.0 License.
+// See the LICENSE file in the project root for more information.
+
+using System.Reflection;
+
+namespace Steeltoe.Common.Extensions;
+
+internal static class ExceptionExtensions
+{
+ ///
+ /// Unwraps a potential tree of nested exceptions, returning the deepest one. Exceptions of type and
+ /// with a single inner exception are unwrapped.
+ ///
+ ///
+ /// The exception to unwrap.
+ ///
+ ///
+ /// The deepest exception, or the original is there's nothing to unwrap.
+ ///
+ public static Exception UnwrapAll(this Exception exception)
+ {
+ ArgumentNullException.ThrowIfNull(exception);
+
+ bool hasChanges = true;
+
+ while (hasChanges)
+ {
+ hasChanges = false;
+
+ if (exception is TargetInvocationException && exception.InnerException != null)
+ {
+ exception = exception.InnerException;
+ hasChanges = true;
+ }
+ else if (exception is AggregateException { InnerExceptions.Count: 1 } aggregateException)
+ {
+ exception = aggregateException.InnerExceptions[0];
+ hasChanges = true;
+ }
+ }
+
+ return exception;
+ }
+
+ ///
+ /// Determines whether the thrown exception results from incoming request cancellation.
+ ///
+ ///
+ /// The caught exception to inspect.
+ ///
+ public static bool IsCancellation(this Exception? exception)
+ {
+ // See note in remarks at https://learn.microsoft.com/en-us/dotnet/api/system.net.http.httpclient.sendasync.
+ if (exception is OperationCanceledException && exception.InnerException?.GetType() != typeof(TimeoutException))
+ {
+ return true;
+ }
+
+ return false;
+ }
+
+ ///
+ /// Determines whether the thrown exception results from an HTTP request timeout.
+ ///
+ ///
+ /// The caught exception to inspect.
+ ///
+ public static bool IsHttpClientTimeout(this Exception? exception)
+ {
+ // See note in remarks at https://learn.microsoft.com/en-us/dotnet/api/system.net.http.httpclient.sendasync.
+ if (exception is OperationCanceledException && exception.InnerException?.GetType() == typeof(TimeoutException))
+ {
+ return true;
+ }
+
+ return false;
+ }
+}
diff --git a/src/Common/src/Common/Extensions/ServiceCollectionExtensions.cs b/src/Common/src/Common/Extensions/ServiceCollectionExtensions.cs
new file mode 100644
index 0000000000..f47eb0da18
--- /dev/null
+++ b/src/Common/src/Common/Extensions/ServiceCollectionExtensions.cs
@@ -0,0 +1,38 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the Apache 2.0 License.
+// See the LICENSE file in the project root for more information.
+
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using Microsoft.Extensions.Options;
+
+namespace Steeltoe.Common.Extensions;
+
+public static class ServiceCollectionExtensions
+{
+ ///
+ /// Registers for use with the options pattern. It is also registered as
+ /// in the IoC container for easy access.
+ ///
+ ///
+ /// The to add services to.
+ ///
+ ///
+ /// The incoming so that additional calls can be chained.
+ ///
+ public static IServiceCollection AddApplicationInstanceInfo(this IServiceCollection services)
+ {
+ ArgumentNullException.ThrowIfNull(services);
+
+ services.AddOptions();
+ services.TryAddEnumerable(ServiceDescriptor.Singleton, ConfigureApplicationInstanceInfo>());
+
+ services.TryAddSingleton(serviceProvider =>
+ {
+ var optionsMonitor = serviceProvider.GetRequiredService>();
+ return optionsMonitor.CurrentValue;
+ });
+
+ return services;
+ }
+}
diff --git a/src/Common/src/Common/Extensions/UriExtensions.cs b/src/Common/src/Common/Extensions/UriExtensions.cs
new file mode 100644
index 0000000000..cbcf5f95fe
--- /dev/null
+++ b/src/Common/src/Common/Extensions/UriExtensions.cs
@@ -0,0 +1,64 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the Apache 2.0 License.
+// See the LICENSE file in the project root for more information.
+
+using System.Diagnostics.CodeAnalysis;
+using System.Net;
+
+namespace Steeltoe.Common.Extensions;
+
+internal static class UriExtensions
+{
+ public static string ToMaskedString(this Uri source)
+ {
+ ArgumentNullException.ThrowIfNull(source);
+
+ string uris = source.ToString();
+
+ if (uris.Contains(','))
+ {
+ return string.Join(',',
+ uris.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).Select(uri => ToMaskedUri(new Uri(uri)).ToString()));
+ }
+
+ return ToMaskedUri(source).ToString();
+ }
+
+ private static Uri ToMaskedUri(Uri source)
+ {
+ if (string.IsNullOrEmpty(source.UserInfo))
+ {
+ return source;
+ }
+
+ var builder = new UriBuilder(source)
+ {
+ UserName = "****",
+#pragma warning disable S2068 // Hard-coded credentials are security-sensitive
+ Password = "****"
+#pragma warning restore S2068 // Hard-coded credentials are security-sensitive
+ };
+
+ return builder.Uri;
+ }
+
+ public static bool TryGetUsernamePassword(this Uri uri, [NotNullWhen(true)] out string? username, [NotNullWhen(true)] out string? password)
+ {
+ ArgumentNullException.ThrowIfNull(uri);
+
+ string userInfo = uri.GetComponents(UriComponents.UserInfo, UriFormat.UriEscaped);
+
+ string[] parts = userInfo.Split(':');
+
+ if (parts.Length == 2)
+ {
+ username = WebUtility.UrlDecode(parts[0]);
+ password = WebUtility.UrlDecode(parts[1]);
+ return true;
+ }
+
+ username = null;
+ password = null;
+ return false;
+ }
+}
diff --git a/src/Common/src/Common/HealthChecks/HealthAggregator.cs b/src/Common/src/Common/HealthChecks/HealthAggregator.cs
new file mode 100644
index 0000000000..92c1e7d88a
--- /dev/null
+++ b/src/Common/src/Common/HealthChecks/HealthAggregator.cs
@@ -0,0 +1,223 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the Apache 2.0 License.
+// See the LICENSE file in the project root for more information.
+
+using System.Collections.Concurrent;
+using Microsoft.Extensions.Diagnostics.HealthChecks;
+using Steeltoe.Common.Extensions;
+using MicrosoftHealthCheckResult = Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckResult;
+using MicrosoftHealthStatus = Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus;
+using SteeltoeHealthCheckResult = Steeltoe.Common.HealthChecks.HealthCheckResult;
+using SteeltoeHealthStatus = Steeltoe.Common.HealthChecks.HealthStatus;
+
+namespace Steeltoe.Common.HealthChecks;
+
+internal sealed class HealthAggregator : IHealthAggregator
+{
+ public async Task AggregateAsync(ICollection contributors,
+ ICollection healthCheckRegistrations, IServiceProvider serviceProvider, CancellationToken cancellationToken)
+ {
+ ArgumentNullException.ThrowIfNull(contributors);
+ ArgumentNullException.ThrowIfNull(healthCheckRegistrations);
+ ArgumentNullException.ThrowIfNull(serviceProvider);
+
+ SteeltoeHealthCheckResult contributorsResult = await AggregateHealthContributorsAsync(contributors, cancellationToken);
+
+ IDictionary healthCheckResults =
+ await AggregateMicrosoftHealthChecksAsync(contributors, healthCheckRegistrations, serviceProvider, cancellationToken);
+
+ return AddChecksSetStatus(contributorsResult, healthCheckResults);
+ }
+
+ private async Task AggregateHealthContributorsAsync(ICollection contributors,
+ CancellationToken cancellationToken)
+ {
+ if (contributors.Count == 0)
+ {
+ return new SteeltoeHealthCheckResult();
+ }
+
+ var aggregatorResult = new SteeltoeHealthCheckResult();
+ var healthChecks = new ConcurrentDictionary();
+ var keys = new ConcurrentBag();
+
+ await Parallel.ForEachAsync(contributors, cancellationToken, async (contributor, _) =>
+ {
+ string contributorId = GetKey(keys, contributor.Id);
+ SteeltoeHealthCheckResult? healthCheckResult;
+
+ try
+ {
+ healthCheckResult = await contributor.CheckHealthAsync(cancellationToken);
+ }
+ catch (Exception exception) when (!exception.IsCancellation())
+ {
+ healthCheckResult = new SteeltoeHealthCheckResult();
+ }
+
+ if (healthCheckResult != null)
+ {
+ healthChecks.TryAdd(contributorId, healthCheckResult);
+ }
+ });
+
+ return AddChecksSetStatus(aggregatorResult, healthChecks);
+ }
+
+ private static async Task> AggregateMicrosoftHealthChecksAsync(ICollection contributors,
+ ICollection healthCheckRegistrations, IServiceProvider serviceProvider, CancellationToken cancellationToken)
+ {
+ if (healthCheckRegistrations.Count == 0)
+ {
+ return new Dictionary();
+ }
+
+ var healthChecks = new ConcurrentDictionary();
+ var keys = new ConcurrentBag(contributors.Select(contributor => contributor.Id));
+
+ // run all HealthCheckRegistration checks in parallel
+ await Parallel.ForEachAsync(healthCheckRegistrations, cancellationToken, async (registration, _) =>
+ {
+ string contributorName = GetKey(keys, registration.Name);
+ SteeltoeHealthCheckResult healthCheckResult;
+
+ try
+ {
+ healthCheckResult = await RunMicrosoftHealthCheckAsync(serviceProvider, registration, cancellationToken);
+ }
+ catch (Exception exception) when (!exception.IsCancellation())
+ {
+ healthCheckResult = new SteeltoeHealthCheckResult();
+ }
+
+ healthChecks.TryAdd(contributorName, healthCheckResult);
+ });
+
+ return healthChecks;
+ }
+
+ private static async Task RunMicrosoftHealthCheckAsync(IServiceProvider serviceProvider, HealthCheckRegistration registration,
+ CancellationToken cancellationToken)
+ {
+ var context = new HealthCheckContext
+ {
+ Registration = registration
+ };
+
+ var healthCheckResult = new SteeltoeHealthCheckResult();
+
+ try
+ {
+ IHealthCheck check = registration.Factory(serviceProvider);
+ MicrosoftHealthCheckResult result = await check.CheckHealthAsync(context, cancellationToken);
+
+ healthCheckResult.Status = ToHealthStatus(result.Status);
+ healthCheckResult.Description = result.Description;
+
+ foreach ((string key, object value) in result.Data)
+ {
+ healthCheckResult.Details[key] = value;
+ }
+
+ if (result.Exception != null && !string.IsNullOrEmpty(result.Exception.Message))
+ {
+ healthCheckResult.Details.Add("error", result.Exception.Message);
+ }
+ }
+ catch (Exception exception) when (!exception.IsCancellation())
+ {
+ // Catch all exceptions so that a status can always be returned
+ healthCheckResult.Details.Add("exception", exception.Message);
+ }
+
+ return healthCheckResult;
+ }
+
+ private static SteeltoeHealthStatus ToHealthStatus(MicrosoftHealthStatus status)
+ {
+ return status switch
+ {
+ MicrosoftHealthStatus.Healthy => SteeltoeHealthStatus.Up,
+ MicrosoftHealthStatus.Degraded => SteeltoeHealthStatus.Warning,
+ MicrosoftHealthStatus.Unhealthy => SteeltoeHealthStatus.Down,
+ _ => SteeltoeHealthStatus.Unknown
+ };
+ }
+
+ private static string GetKey(ConcurrentBag keys, string key)
+ {
+ lock (keys)
+ {
+ // add the contributor with a -n appended to the id
+ if (keys.Any(value => value == key))
+ {
+ string newKey = $"{key}-{keys.Count(value => value.StartsWith(key, StringComparison.Ordinal))}";
+ keys.Add(newKey);
+ return newKey;
+ }
+
+ keys.Add(key);
+ return key;
+ }
+ }
+
+ private static SteeltoeHealthCheckResult AddChecksSetStatus(SteeltoeHealthCheckResult result, IDictionary healthChecks)
+ {
+ var orderedCheckResults = new SortedDictionary(CheckResultOrderingKeyComparer.Instance);
+
+ foreach ((string name, SteeltoeHealthCheckResult checkResult) in healthChecks)
+ {
+ if (checkResult.Status > result.Status)
+ {
+ result.Status = checkResult.Status;
+ }
+
+ var orderingKey = new CheckResultOrderingKey(checkResult.Status, name);
+ orderedCheckResults.Add(orderingKey, checkResult);
+ }
+
+ foreach ((CheckResultOrderingKey orderingKey, SteeltoeHealthCheckResult checkResult) in orderedCheckResults)
+ {
+ result.Details.Add(orderingKey.Name, checkResult);
+ }
+
+ return result;
+ }
+
+ private sealed record CheckResultOrderingKey(SteeltoeHealthStatus Status, string Name);
+
+ private sealed class CheckResultOrderingKeyComparer : IComparer
+ {
+ private static readonly List HealthStatusOrder =
+ [
+ SteeltoeHealthStatus.Down,
+ SteeltoeHealthStatus.OutOfService,
+ SteeltoeHealthStatus.Warning,
+ SteeltoeHealthStatus.Unknown,
+ SteeltoeHealthStatus.Up
+ ];
+
+ public static CheckResultOrderingKeyComparer Instance { get; } = new();
+
+ private CheckResultOrderingKeyComparer()
+ {
+ }
+
+ public int Compare(CheckResultOrderingKey? x, CheckResultOrderingKey? y)
+ {
+ if (x == null || y == null)
+ {
+ return Comparer.Default.Compare(x, y);
+ }
+
+ int result = HealthStatusOrder.IndexOf(x.Status).CompareTo(HealthStatusOrder.IndexOf(y.Status));
+
+ if (result == 0)
+ {
+ result = string.Compare(x.Name, y.Name, StringComparison.Ordinal);
+ }
+
+ return result;
+ }
+ }
+}
diff --git a/src/Common/src/Common/HealthChecks/HealthCheckResult.cs b/src/Common/src/Common/HealthChecks/HealthCheckResult.cs
new file mode 100644
index 0000000000..473c5e9936
--- /dev/null
+++ b/src/Common/src/Common/HealthChecks/HealthCheckResult.cs
@@ -0,0 +1,38 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the Apache 2.0 License.
+// See the LICENSE file in the project root for more information.
+
+using System.Text.Json.Serialization;
+using Steeltoe.Common.CasingConventions;
+using Steeltoe.Common.Json;
+
+namespace Steeltoe.Common.HealthChecks;
+
+///
+/// The result of a health check.
+///
+public sealed class HealthCheckResult
+{
+ ///
+ /// Gets or sets the status of the health check.
+ ///
+ ///
+ /// Used by the health middleware to determine the HTTP Status code.
+ ///
+ [JsonConverter(typeof(SnakeCaseAllCapsEnumMemberJsonConverter))]
+ public HealthStatus Status { get; set; } = HealthStatus.Unknown;
+
+ ///
+ /// Gets or sets a description of the health check result.
+ ///
+ ///
+ /// Currently only used on check failures.
+ ///
+ public string? Description { get; set; }
+
+ ///
+ /// Gets details of the health check.
+ ///
+ [JsonIgnoreEmptyCollection]
+ public IDictionary Details { get; } = new Dictionary();
+}
diff --git a/src/Common/src/Common/HealthChecks/HealthStatus.cs b/src/Common/src/Common/HealthChecks/HealthStatus.cs
new file mode 100644
index 0000000000..8730ba48d2
--- /dev/null
+++ b/src/Common/src/Common/HealthChecks/HealthStatus.cs
@@ -0,0 +1,40 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the Apache 2.0 License.
+// See the LICENSE file in the project root for more information.
+
+using System.Text.Json.Serialization;
+using Steeltoe.Common.CasingConventions;
+
+namespace Steeltoe.Common.HealthChecks;
+
+///
+/// Represents the reported status of a health check.
+///
+[JsonConverter(typeof(SnakeCaseAllCapsEnumMemberJsonConverter))]
+public enum HealthStatus
+{
+ ///
+ /// Indicates that the component is in an unknown state.
+ ///
+ Unknown,
+
+ ///
+ /// Indicates that the component is healthy.
+ ///
+ Up,
+
+ ///
+ /// Indicates that the component is in a warning state.
+ ///
+ Warning,
+
+ ///
+ /// Indicates that the component is under maintenance (planned downtime).
+ ///
+ OutOfService,
+
+ ///
+ /// Indicates that the component is unhealthy, or an unhandled exception was thrown while executing the health check.
+ ///
+ Down
+}
diff --git a/src/Common/src/Common/HealthChecks/IHealthAggregator.cs b/src/Common/src/Common/HealthChecks/IHealthAggregator.cs
new file mode 100644
index 0000000000..a9259ba474
--- /dev/null
+++ b/src/Common/src/Common/HealthChecks/IHealthAggregator.cs
@@ -0,0 +1,13 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the Apache 2.0 License.
+// See the LICENSE file in the project root for more information.
+
+using Microsoft.Extensions.Diagnostics.HealthChecks;
+
+namespace Steeltoe.Common.HealthChecks;
+
+public interface IHealthAggregator
+{
+ Task AggregateAsync(ICollection contributors, ICollection healthCheckRegistrations,
+ IServiceProvider serviceProvider, CancellationToken cancellationToken);
+}
diff --git a/src/Common/src/Common/HealthChecks/IHealthContributor.cs b/src/Common/src/Common/HealthChecks/IHealthContributor.cs
new file mode 100644
index 0000000000..42766d6956
--- /dev/null
+++ b/src/Common/src/Common/HealthChecks/IHealthContributor.cs
@@ -0,0 +1,27 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the Apache 2.0 License.
+// See the LICENSE file in the project root for more information.
+
+namespace Steeltoe.Common.HealthChecks;
+
+///
+/// Implement this interface and add to DI to be included in health checks.
+///
+public interface IHealthContributor
+{
+ ///
+ /// Gets an identifier for the type of check being performed.
+ ///
+ string Id { get; }
+
+ ///
+ /// Performs a health check.
+ ///
+ ///
+ /// The token to monitor for cancellation requests.
+ ///
+ ///
+ /// The result of the health check, or null if this health check is currently disabled.
+ ///
+ Task CheckHealthAsync(CancellationToken cancellationToken);
+}
diff --git a/src/Common/src/Common/IApplicationInstanceInfo.cs b/src/Common/src/Common/IApplicationInstanceInfo.cs
new file mode 100644
index 0000000000..4cfc8a688f
--- /dev/null
+++ b/src/Common/src/Common/IApplicationInstanceInfo.cs
@@ -0,0 +1,16 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the Apache 2.0 License.
+// See the LICENSE file in the project root for more information.
+
+namespace Steeltoe.Common;
+
+///
+/// Provides information about the currently running application instance.
+///
+public interface IApplicationInstanceInfo
+{
+ ///
+ /// Gets the name of this application.
+ ///
+ string? ApplicationName { get; }
+}
diff --git a/src/Common/src/Common/IApplicationTask.cs b/src/Common/src/Common/IApplicationTask.cs
new file mode 100644
index 0000000000..b8348ec56f
--- /dev/null
+++ b/src/Common/src/Common/IApplicationTask.cs
@@ -0,0 +1,19 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the Apache 2.0 License.
+// See the LICENSE file in the project root for more information.
+
+namespace Steeltoe.Common;
+
+///
+/// A runnable asynchronous task bundled with the assembly that can be executed on-demand.
+///
+public interface IApplicationTask
+{
+ ///
+ /// Executes this task asynchronously.
+ ///
+ ///
+ /// The token to monitor for cancellation requests.
+ ///
+ Task RunAsync(CancellationToken cancellationToken);
+}
diff --git a/src/Common/src/Common/Json/JsonIgnoreEmptyCollectionAttribute.cs b/src/Common/src/Common/Json/JsonIgnoreEmptyCollectionAttribute.cs
new file mode 100644
index 0000000000..2010ac54d9
--- /dev/null
+++ b/src/Common/src/Common/Json/JsonIgnoreEmptyCollectionAttribute.cs
@@ -0,0 +1,14 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the Apache 2.0 License.
+// See the LICENSE file in the project root for more information.
+
+using System.Text.Json;
+
+namespace Steeltoe.Common.Json;
+
+///
+/// Indicates that a collection property should not be serialized if the collection is empty. Use
+/// to register in .
+///
+[AttributeUsage(AttributeTargets.Property)]
+public sealed class JsonIgnoreEmptyCollectionAttribute : Attribute;
diff --git a/src/Common/src/Common/Json/JsonSerializerOptionsExtensions.cs b/src/Common/src/Common/Json/JsonSerializerOptionsExtensions.cs
new file mode 100644
index 0000000000..6502ff09e0
--- /dev/null
+++ b/src/Common/src/Common/Json/JsonSerializerOptionsExtensions.cs
@@ -0,0 +1,48 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the Apache 2.0 License.
+// See the LICENSE file in the project root for more information.
+
+using System.Collections;
+using System.Text.Json;
+using System.Text.Json.Serialization.Metadata;
+
+namespace Steeltoe.Common.Json;
+
+public static class JsonSerializerOptionsExtensions
+{
+ ///
+ /// Registers a contract modifier in the specified that skips serialization of empty collections marked with
+ /// .
+ ///
+ ///
+ /// The JSON serializer options to configure.
+ ///
+ ///
+ /// The incoming so that additional calls can be chained.
+ ///
+ public static JsonSerializerOptions AddJsonIgnoreEmptyCollection(this JsonSerializerOptions options)
+ {
+ ArgumentNullException.ThrowIfNull(options);
+
+ options.TypeInfoResolverChain.Insert(0, new DefaultJsonTypeInfoResolver
+ {
+ Modifiers =
+ {
+ SkipEmptyCollection
+ }
+ });
+
+ return options;
+ }
+
+ private static void SkipEmptyCollection(JsonTypeInfo info)
+ {
+ foreach (JsonPropertyInfo property in info.Properties)
+ {
+ if (property.AttributeProvider != null && property.AttributeProvider.IsDefined(typeof(JsonIgnoreEmptyCollectionAttribute), true))
+ {
+ property.ShouldSerialize = (_, value) => value is not IEnumerable enumerable || enumerable.Cast