o%7nR`5uyUMxIxiE}V*>o4Sqb
zNl8EaO}Tiv=4rfkG0l|KL^30OOiyuThx?#A7PT<}AT4=*ws^_)23~RJ#$q(#GKfKB
z>_y3D|Mj&a|7X~FI$alB=u{rG=8ifOXgq(`2n54G>XRI|FI{&=YJH1Yvnu!H7v_gYS_Kg=KJM
z457UCFmX){=R{u{NS=2d`lptv%*sX
zDmy@d_L8G*667ym2W+VgaVPPLA4(Q^Rr|4hU&d>sS0IK8%RR4B!SmB-eB!0UAfNgR
zfY;QCG@81vjkEN@po}eRcZ^;*s2{
zu|}aI1cy!mW%RxDDme^qK~QALUhLmm?201DJscYVPG*62&q>M!<9I9yS3TGBLo3(T
z+^v#jj)uV4`++%z_iaLy_OtDG&+MDXKN=eVn*Oz(`yr^l%WFzABwpALQtjIS{?_PX
z)&UUs-a=5bi=y-NPy~x@NpR~lN;t8%*a6UNuBrWiW&zDl{)WLWkj}gZV*Q6C8dkB2
z7Es14gdqJu4FV_~bPc2TWkQtBvFzdF!8yj;7nx@)zQg^j$37=#e?f-$^$0ROUHUvQ
zycDpj8IX?GYHkVmC$9uGXEIeM5^0kGWIL4}*6T4u1A?hpg3kx+$SeQi3AQhu_-25n
zvHbKFI{&QdOF7S&mMB@;KwVa3+KlL=A3f+uFa)VH&6-uvwWaX9u97H3zN2sy;qGBU
zdWtraCOsBCno;nYETbxv$r%9YLjckS&3B>F^y#n@Rmxr~`lKa!kkMI=$u@0?6hV9KQnTjbPlDkV%WGe!cLooI
z52dt^cp2<+K5dIvdF>`<2866xSOs-y1HVa)u_4eIc+!mZgw24AX>$iFogGj6iykQm
z4l9{s#)*XTA}ExPir|@iu2MPf8BkojOinKZqkr{7lcf}*O&^@7)evwXfe
zi*_i3Y9@&=Fg6x0sE-+0-~u3WozqrlSo{cW{~d?Tr#>}uAgOc})2R~}VFJQQTOCy*{Ed#Ps_zFc)lvmlz%LayX9EaSpl!-wIfRK1r;oSrMs5t2x$#MdN-dylCHK)kspS-$=k2?-7L@(9ORp7|l2+o{#
z9!;hI2r0}cEG?jQp}h!M5E?VO`esxfVD{X*jQC=w<2xY9+-dS5RiB3g)0$X<(&Q`+a@%v-ZYM2D|lyBWRFD44@|Ilee?c!
z83b`;xU9K8`T$uK;Gm80%#ds5WP5Wb41kbo$K;u}DuDpy2R1)67Jdzm`u^d-grD6R
zKE~Zg+yz!kIcykR$1P*Pm~a3>_H2dI7pOq>0%N_lKkwY_$3V`Yci@~2v4$oqt@FIS
zoD$q@apeF#E1m97_mm8_79t3bcK?4bjM?(p=1i_S0kM(R`Oj&c7bhcl&84KxfUtPs
zdVYYX=tQC&rn=7$zG>_BvO(i#h4bC$YwH}g0uJ1eh;@ELZ|`1800^0NOqwx42`IE}
zvU{1u-s+q40#%Fn^^+z2$%(!*t%1t?|Agd{c}*z*p+G1Code 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..992e13329a
--- /dev/null
+++ b/src/Common/src/Common/ConfigurationSchemaAttributes.cs
@@ -0,0 +1,55 @@
+#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.
+
+namespace Aspire;
+
+///
+/// Attribute used to automatically generate a JSON schema for a component's configuration.
+///
+[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