diff --git a/.azure-pipelines/ultimate-pipeline.yml b/.azure-pipelines/ultimate-pipeline.yml
index 3b2d1d38a18b..e5ca0be96622 100644
--- a/.azure-pipelines/ultimate-pipeline.yml
+++ b/.azure-pipelines/ultimate-pipeline.yml
@@ -2037,6 +2037,7 @@ stages:
- powershell: |
mkdir -Force ./artifacts/build_data/snapshots
mkdir -Force ./artifacts/build_data/logs/LoaderOptimizationStartup
+ mkdir -Force ./artifacts/build_data/logs/MultipleAppsInDomain
docker compose -f docker-compose.windows.yml run --rm start-test-agent.windows
docker compose -f docker-compose.windows.yml build `
@@ -2044,7 +2045,12 @@ stages:
--build-arg ENABLE_32_BIT=$(enable32bit) `
IntegrationTests.IIS.LoaderOptimizationStartup
- docker compose -f docker-compose.windows.yml up -d IntegrationTests.IIS.LoaderOptimizationStartup
+ docker compose -f docker-compose.windows.yml build `
+ --build-arg DOTNET_TRACER_MSI=.$(relativeMsiOutputDirectory)/*.msi `
+ --build-arg ENABLE_32_BIT=$(enable32bit) `
+ IntegrationTests.IIS.MultipleAppsInDomain
+
+ docker compose -f docker-compose.windows.yml up -d IntegrationTests.IIS.LoaderOptimizationStartup IntegrationTests.IIS.MultipleAppsInDomain
displayName: docker-compose start IntegrationTests.IIS
retryCountOnTaskFailure: 5
env:
diff --git a/.dockerignore b/.dockerignore
index b3cc4e29f439..86eef56b5d1f 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -11,6 +11,7 @@ packages/
**/obj/
!tracer/test/test-applications/aspnet/Samples.AspNet472.LoaderOptimizationRegKey/bin/
+!tracer/test/test-applications/aspnet/Samples.AspNet.MultipleAppsInDomain/bin/
!artifacts/msi/
!tracer/test/Datadog.Trace.TestHelpers/
diff --git a/Datadog.Trace.sln b/Datadog.Trace.sln
index a633fc0664d2..f5fd2b4c01f0 100644
--- a/Datadog.Trace.sln
+++ b/Datadog.Trace.sln
@@ -593,6 +593,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Samples.AWS.EventBridge", "
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AssemblyLoadContextResolve", "tracer\test\test-applications\regression\AssemblyLoadContextResolve\AssemblyLoadContextResolve.csproj", "{8B1AF6A7-DD41-4347-B637-90C23D69B50E}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Samples.AspNet.MultipleAppsInDomain", "tracer\test\test-applications\aspnet\Samples.AspNet.MultipleAppsInDomain\Samples.AspNet.MultipleAppsInDomain.csproj", "{A82EB6F8-D8D0-4763-B252-08CA3F39D153}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -1421,6 +1423,10 @@ Global
{8B1AF6A7-DD41-4347-B637-90C23D69B50E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8B1AF6A7-DD41-4347-B637-90C23D69B50E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8B1AF6A7-DD41-4347-B637-90C23D69B50E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A82EB6F8-D8D0-4763-B252-08CA3F39D153}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A82EB6F8-D8D0-4763-B252-08CA3F39D153}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A82EB6F8-D8D0-4763-B252-08CA3F39D153}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A82EB6F8-D8D0-4763-B252-08CA3F39D153}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -1652,6 +1658,7 @@ Global
{E1B0F72C-991A-409D-9266-DE5ED1BD940E} = {A0C5FBBB-CFB2-4FB9-B8F0-55676E9DCF06}
{D6155F26-8245-4B66-8944-79C3DF9F9DA3} = {BAF8F246-3645-42AD-B1D0-0F7EAFBAB34A}
{8B1AF6A7-DD41-4347-B637-90C23D69B50E} = {498A300E-D036-49B7-A43D-821D1CAF11A5}
+ {A82EB6F8-D8D0-4763-B252-08CA3F39D153} = {AFA0AB23-64F0-4AC1-9050-6CE8FE06F580}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {160A1D00-1F5B-40F8-A155-621B4459D78F}
diff --git a/docker-compose.windows.yml b/docker-compose.windows.yml
index be33331dd937..613bdf0759c8 100644
--- a/docker-compose.windows.yml
+++ b/docker-compose.windows.yml
@@ -71,6 +71,51 @@ services:
- DD_LOGGER_DD_TAGS
- IS_SSI_RUN
+ IntegrationTests.IIS.MultipleAppsInDomain:
+ build:
+ context: ./
+ args:
+ - ENABLE_32_BIT
+ - DOTNET_TRACER_MSI
+ dockerfile: ./tracer/build/_build/docker/iis.multipleappsindomain.dockerfile
+ image: datadog-iis-multipleappsindomain
+ volumes:
+ - ./artifacts/build_data/logs/MultipleAppsInDomain:c:/ProgramData/Datadog .NET Tracer/logs
+ ports:
+ - "8081:8081"
+ - "8082:8082"
+ depends_on:
+ - test-agent.windows
+ environment:
+ - DD_TRACE_AGENT_URL=http://test-agent.windows:8126
+ - DD_CLR_ENABLE_NGEN=${DD_CLR_ENABLE_NGEN:-1}
+ - DD_LOGGER_DD_API_KEY
+ - DD_LOGGER_DD_SERVICE
+ - DD_LOGGER_DD_ENV
+ - DD_LOGGER_ENABLED
+ - DD_LOGGER_TF_BUILD=${TF_BUILD:-}
+ - DD_LOGGER_BUILD_BUILDID
+ - DD_LOGGER_BUILD_DEFINITIONNAME
+ - DD_LOGGER_BUILD_SOURCESDIRECTORY
+ - DD_LOGGER_BUILD_REPOSITORY_URI
+ - DD_LOGGER_BUILD_SOURCEVERSION
+ - DD_LOGGER_BUILD_SOURCEBRANCH
+ - DD_LOGGER_BUILD_SOURCEBRANCHNAME
+ - DD_LOGGER_BUILD_SOURCEVERSIONMESSAGE
+ - DD_LOGGER_BUILD_REQUESTEDFORID
+ - DD_LOGGER_BUILD_REQUESTEDFOREMAIL
+ - DD_LOGGER_SYSTEM_TEAMFOUNDATIONSERVERURI
+ - DD_LOGGER_SYSTEM_TEAMPROJECTID
+ - DD_LOGGER_SYSTEM_STAGEDISPLAYNAME
+ - DD_LOGGER_SYSTEM_JOBDISPLAYNAME
+ - DD_LOGGER_SYSTEM_JOBID
+ - DD_LOGGER_SYSTEM_TASKINSTANCEID=${SYSTEM_TASKINSTANCEID:-}
+ - DD_LOGGER_SYSTEM_PULLREQUEST_SOURCEREPOSITORYURI
+ - DD_LOGGER_SYSTEM_PULLREQUEST_SOURCECOMMITID
+ - DD_LOGGER_SYSTEM_PULLREQUEST_SOURCEBRANCH
+ - DD_LOGGER_DD_TAGS
+ - IS_SSI_RUN
+
smoke-tests.windows:
build:
context: ./tracer/ # have to use this as the context, as Dockercompose requires dockerfile to be inside context dir
diff --git a/tracer/build/_build/docker/iis.multipleappsindomain.dockerfile b/tracer/build/_build/docker/iis.multipleappsindomain.dockerfile
new file mode 100644
index 000000000000..049203e7d9f2
--- /dev/null
+++ b/tracer/build/_build/docker/iis.multipleappsindomain.dockerfile
@@ -0,0 +1,34 @@
+FROM mcr.microsoft.com/dotnet/framework/aspnet:4.8-windowsservercore-ltsc2022
+SHELL ["powershell", "-Command", "$ErrorActionPreference = 'Stop'; $ProgressPreference = 'SilentlyContinue';"]
+
+# Copy IIS websites
+ADD tracer/test/test-applications/aspnet/Samples.AspNet.MultipleAppsInDomain/bin/Release/publish MultipleAppsInDomain
+
+# Set up multiple apps in single domain with custom config IIS websites
+ARG ENABLE_32_BIT
+ENV ENABLE_32_BIT=${ENABLE_32_BIT:-false}
+
+RUN c:\Windows\System32\inetsrv\appcmd add apppool /name:mutliAppPool /managedRuntimeVersion:"v4.0" /managedPipelineMode:"Integrated" /enable32bitapponwin64:$env:ENABLE_32_BIT
+
+# The SetEnvironmentVariable() calls shouldn't _need_ to be here - we should be able to do it just
+# using docker-compose, but I can't get that to work for some inexplicable reason, no matter
+# what I do, so here we are.
+RUN Remove-WebSite -Name 'Default Web Site'; \
+ Write-Host "Created app pool with 32 bit reg key: $env:ENABLE_32_BIT"; \
+ Write-Host "Creating multi app domain sites"; \
+ [System.Environment]::SetEnvironmentVariable('DD_TRACE_AGENT_URL', 'http://test-agent.windows:8126', [System.EnvironmentVariableTarget]::Machine); \
+ [System.Environment]::SetEnvironmentVariable('DD_TRACE_AGENT_URL', 'http://test-agent.windows:8126', [System.EnvironmentVariableTarget]::Process); \
+ [System.Environment]::SetEnvironmentVariable('DD_TRACE_AGENT_URL', 'http://test-agent.windows:8126', [System.EnvironmentVariableTarget]::User); \
+ New-Website -Name 'MultiAppPoolWithCustomConfig1' -ApplicationPool mutliAppPool -Port 8081 -PhysicalPath 'c:\MultipleAppsInDomain'; \
+ New-Website -Name 'MultiAppPoolWithCustomConfig2' -ApplicationPool mutliAppPool -Port 8082 -PhysicalPath 'c:\MultipleAppsInDomain';
+
+# Install the .NET Tracer MSI
+ARG DOTNET_TRACER_MSI
+ADD $DOTNET_TRACER_MSI ./datadog-apm.msi
+RUN Start-Process -Wait msiexec -ArgumentList '/qn /i datadog-apm.msi'
+
+# Restart IIS
+RUN net stop /y was; \
+ net start w3svc
+
+EXPOSE 80
\ No newline at end of file
diff --git a/tracer/test/Datadog.Trace.ClrProfiler.IntegrationTests/IIS/MultipleAppsInDomainWithCustomConfigBuilder.cs b/tracer/test/Datadog.Trace.ClrProfiler.IntegrationTests/IIS/MultipleAppsInDomainWithCustomConfigBuilder.cs
new file mode 100644
index 000000000000..3b41cdc2a00a
--- /dev/null
+++ b/tracer/test/Datadog.Trace.ClrProfiler.IntegrationTests/IIS/MultipleAppsInDomainWithCustomConfigBuilder.cs
@@ -0,0 +1,98 @@
+//
+// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License.
+// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc.
+//
+
+#if NETFRAMEWORK
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Net;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using Datadog.Trace.Configuration.Telemetry;
+using Datadog.Trace.Logging;
+using FluentAssertions;
+using Newtonsoft.Json;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace Datadog.Trace.ClrProfiler.IntegrationTests.IIS;
+
+public class MultipleAppsInDomainWithCustomConfigBuilder(ITestOutputHelper output)
+{
+ [SkippableFact]
+ [Trait("RunOnWindows", "True")]
+ [Trait("IIS", "True")]
+ [Trait("MSI", "True")]
+ public async Task ApplicationDoesNotReturnErrors()
+ {
+ const string App1Url = "http://localhost:8081";
+ const string App2Url = "http://localhost:8082";
+
+ var intervalMilliseconds = 500;
+ var intervals = 5;
+ var serverReady = false;
+ var client = new HttpClient()
+ {
+ Timeout = TimeSpan.FromSeconds(30), // yes, this is a long time, but we're running this in CI, in windows containers...
+ };
+
+ // wait for server to be ready to receive requests
+ while (intervals-- > 0)
+ {
+ try
+ {
+ output.WriteLine($"Sending warmup request to App 1 {App1Url}");
+ var serverReadyResponse = await client.GetAsync(App1Url);
+ serverReady = serverReadyResponse.StatusCode == HttpStatusCode.OK;
+ }
+ catch
+ {
+ // ignore
+ }
+
+ if (serverReady)
+ {
+ output.WriteLine("The server is ready.");
+ break;
+ }
+
+ Thread.Sleep(intervalMilliseconds);
+ }
+
+ // Send request to app 1
+ var responseMessage = await client.GetAsync(App1Url);
+ var response = await responseMessage.Content.ReadAsStringAsync();
+ output.WriteLine($"Received response from app1 at {App1Url}: {response}");
+ responseMessage.StatusCode.Should().Be(HttpStatusCode.OK);
+
+ var app1Result = JsonConvert.DeserializeObject(response);
+ app1Result.Pid.Should().NotBe(0);
+ app1Result.AppConfig.Should().ContainKey("DummyKey1").WhoseValue.Should().Be("DummyValue1 - from custom config");
+
+ // Send request to app 2
+ responseMessage = await client.GetAsync(App2Url);
+ response = await responseMessage.Content.ReadAsStringAsync();
+ output.WriteLine($"Received response from app2 at {App2Url}: {response}");
+ responseMessage.StatusCode.Should().Be(HttpStatusCode.OK);
+
+ var app2Result = JsonConvert.DeserializeObject(response);
+ app2Result.Pid.Should().Be(app1Result.Pid);
+ app2Result.AppConfig.Should().ContainKey("DummyKey1").WhoseValue.Should().Be("DummyValue1 - from custom config");
+
+ // verify we have some logs, so we know instrumentation happened
+ var logDirectory = Path.Combine(DatadogLoggingFactory.GetLogDirectory(NullConfigurationTelemetry.Instance), "MultipleAppsInDomain");
+ output.WriteLine($"Reading files from {logDirectory}");
+ Directory.GetFiles(logDirectory).Should().NotBeEmpty();
+ }
+
+ public class Results
+ {
+ public int Pid { get; set; }
+
+ public Dictionary AppConfig { get; set; }
+ }
+}
+#endif
diff --git a/tracer/test/test-applications/aspnet/Samples.AspNet.MultipleAppsInDomain/Controllers/HomeController.cs b/tracer/test/test-applications/aspnet/Samples.AspNet.MultipleAppsInDomain/Controllers/HomeController.cs
new file mode 100644
index 000000000000..160ae6f22543
--- /dev/null
+++ b/tracer/test/test-applications/aspnet/Samples.AspNet.MultipleAppsInDomain/Controllers/HomeController.cs
@@ -0,0 +1,28 @@
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Web.Mvc;
+
+namespace Samples.AspNet.MultipleAppsInDomain.Controllers
+{
+ public class HomeController : Controller
+ {
+ public ActionResult Index()
+ {
+ var results = new Results();
+ results.Pid = Process.GetCurrentProcess().Id;
+ results.AppConfig = new Dictionary();
+ foreach(var key in System.Configuration.ConfigurationManager.AppSettings.AllKeys)
+ {
+ results.AppConfig[key] = System.Configuration.ConfigurationManager.AppSettings[key];
+ }
+
+ return Json(results, JsonRequestBehavior.AllowGet);
+ }
+
+ public class Results
+ {
+ public int Pid { get; set; }
+ public Dictionary AppConfig { get; set; }
+ }
+ }
+}
\ No newline at end of file
diff --git a/tracer/test/test-applications/aspnet/Samples.AspNet.MultipleAppsInDomain/CustomConfigBuilder/CustomConfigBuilder.cs b/tracer/test/test-applications/aspnet/Samples.AspNet.MultipleAppsInDomain/CustomConfigBuilder/CustomConfigBuilder.cs
new file mode 100644
index 000000000000..e62051b219dc
--- /dev/null
+++ b/tracer/test/test-applications/aspnet/Samples.AspNet.MultipleAppsInDomain/CustomConfigBuilder/CustomConfigBuilder.cs
@@ -0,0 +1,245 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT license. See the License.txt file in the project root for full license information.
+
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.IO;
+using System.Linq;
+using System.Net.Http;
+using System.Text.RegularExpressions;
+using System.Threading.Tasks;
+
+
+namespace Microsoft.Configuration.ConfigurationBuilders
+{
+ ///
+ /// A ConfigurationProvider that (theoretically) pulls secrets from a remove api
+ /// Modelled on:
+ /// https://github.com/aspnet/MicrosoftConfigurationBuilders/blob/main/src/Azure/AzureKeyVaultConfigBuilder.cs
+ /// Doesn't _actually_ do anything useful with the data, just to get something that's instrumented _before_ we're initialized
+ ///
+ public class CustomConfigBuilder : KeyValueConfigBuilder
+ {
+ #pragma warning disable CS1591 // No xml comments for tag literals.
+ #pragma warning restore CS1591 // No xml comments for tag literals.
+ public const string vaultNameTag = "vaultName";
+ public const string connectionStringTag = "connectionString"; // obsolete
+ public const string uriTag = "uri";
+ public const string versionTag = "version";
+ public const string preloadTag = "preloadSecretNames";
+
+ ///
+ /// Gets or sets a property indicating whether the builder should request a list of all keys from the vault before
+ /// looking up secrets. (This knowledge may reduce the number of requests made to KeyVault, but could also bring
+ /// large amounts of data into memory that may be unwanted.)
+ ///
+ public bool Preload { get; protected set; }
+
+ private HttpClient _kvClient;
+ private Lazy> _allKeys;
+
+ public static List Secrets =
+ [
+ new("DummyKey1", "DummyValue1 - from custom config"),
+ new("DummyKey2", "DummyValue2 - from custom config"),
+ new("DummyKey3", "DummyValue3 - from custom config"),
+ new("DummyKey4", "DummyValue4 - from custom config"),
+ new("DummyKey5", "DummyValue5 - from custom config"),
+ ];
+
+ ///
+ /// Initializes the configuration builder lazily.
+ ///
+ /// The friendly name of the provider.
+ /// A collection of the name/value pairs representing builder-specific attributes specified in the configuration for this provider.
+ protected override void LazyInitialize(string name, NameValueCollection config)
+ {
+ // Default to 'Enabled'. base.LazyInitialize() will override if specified in config.
+ Enabled = KeyValueEnabled.Enabled;
+
+ // Key Vault names can only contain [a-zA-Z0-9] and '-'.
+ // https://docs.microsoft.com/en-us/azure/key-vault/general/about-keys-secrets-certificates
+ // That's a lot of disallowed characters to map away. Fortunately, 'charMap' allows users
+ // to do this on a per-case basis. But let's cover some common cases by default.
+ // Don't add '/' to the map though, as that will mess up versioned keys.
+ CharacterMap.Add(":", "-");
+ CharacterMap.Add("_", "-");
+ CharacterMap.Add(".", "-");
+ CharacterMap.Add("+", "-");
+ CharacterMap.Add(@"\", "-");
+
+ base.LazyInitialize(name, config);
+
+ // At this point, we have our 'Enabled' choice. If we are disabled, we can stop right here.
+ if (Enabled == KeyValueEnabled.Disabled) return;
+
+ // It's lazy, but if something goes off-track before we do this... well, we'd at least like to
+ // work with an empty list rather than a null list. So do this up front.
+ _allKeys = new Lazy>(() => GetAllKeys(), System.Threading.LazyThreadSafetyMode.ExecutionAndPublication);
+
+ Preload = true;
+
+ // Connect to http
+ try
+ {
+ _kvClient = new HttpClient();
+ }
+ catch (Exception)
+ {
+ if (!IsOptional)
+ throw;
+ _kvClient = null;
+ }
+
+ }
+
+ ///
+ /// Looks up a single 'value' for the given 'key.'
+ ///
+ /// The 'key' for the secret to look up in the configured Key Vault. (Prefix handling is not needed here.)
+ /// The value corresponding to the given 'key' or null if no value is found.
+ public override string GetValue(string key)
+ {
+ // hit the network!
+ // Only hit the network if we didn't preload, or if we know the key exists after preloading.
+ if (!Preload || _allKeys.Value.Contains(key, StringComparer.OrdinalIgnoreCase))
+ {
+ // Azure Key Vault keys are case-insensitive, so this should be fine.
+ // vKey.Version here is either the same as this.Version or this.Version is null
+ // Also, this is a synchronous method. And in single-threaded contexts like ASP.Net
+ // it can be bad/dangerous to block on async calls. So lets work some TPL voodoo
+ // to avoid potential deadlocks.
+ return Task.Run(async () => { return await GetValueAsync(key, string.Empty); }).Result?.Value;
+ }
+
+ return null;
+ }
+
+ ///
+ /// Returns a Boolean value indicating whether the given exception is should be considered an optional issue that
+ /// should be ignored or whether the exception should bubble up. This should consult .
+ ///
+ ///
+ /// A Boolean to indicate whether the exception should be ignored.
+ // TODO: This should be considered for moving into KeyValueConfigBuilder as a virtual method in a major update.
+ // But for now, leave it here since we don't want to force a hard tie between minor versions of these packages.
+ protected bool ExceptionIsOptional(Exception e)
+ {
+ // Failed Azure requests have different meanings
+ if (e is HttpRequestException rfex)
+ {
+ return IsOptional;
+ }
+
+ // Even when 'optional', don't catch things unless we're certain we know what it is.
+ return false;
+ }
+
+
+ ///
+ /// Retrieves all known key/value pairs from the Key Vault where the key begins with with .
+ ///
+ /// A prefix string to filter the list of potential keys retrieved from the source.
+ /// A collection of key/value pairs.
+ public override ICollection> GetAllValues(string prefix)
+ {
+ ConcurrentDictionary d = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase);
+ List tasks = new List();
+
+ foreach (string key in _allKeys.Value)
+ {
+ if (key.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
+ tasks.Add(Task.Run(() => GetValueAsync(key, string.Empty).ContinueWith(t =>
+ {
+ // Azure Key Vault keys are case-insensitive, so there shouldn't be any races here.
+ // Include version information. It will get filtered out later before updating config.
+ KeyVaultSecret secret = t.Result;
+ if (secret != null)
+ {
+ d[key] = secret.Value;
+ }
+ })));
+ }
+ Task.WhenAll(tasks).Wait();
+
+ return d;
+ }
+
+ ///
+ /// Makes a determination about whether the input key is valid for this builder and backing store.
+ ///
+ /// The string to be validated. May be partial.
+ /// True if the string is valid. False if the string is not a valid key.
+ public override bool ValidateKey(string key)
+ {
+ // Key Vault only allows alphanumerics and '-'. This builder also allows for one '/' for versioning.
+ return Regex.IsMatch(key, "^[a-zA-Z0-9-]+(/?[a-zA-Z0-9-]+)?$");
+ }
+
+ ///
+ /// Transforms the raw key to a new string just before updating items in Strict and Greedy modes.
+ ///
+ /// The key as read from the incoming config section.
+ /// The key string that will be left in the processed config section.
+ public override string UpdateKey(string rawKey)
+ {
+ // Remove the version segment if it's there.
+ return rawKey;
+ }
+
+ private async Task GetValueAsync(string key, string version)
+ {
+ if (_kvClient == null)
+ return null;
+
+ try
+ {
+ using var result = await _kvClient.GetAsync("https://raw.githubusercontent.com/DataDog/dd-trace-dotnet/refs/heads/master/.env");
+
+ // we don't actually use the value
+ return Secrets.FirstOrDefault(s => s.Key == key);
+ }
+ catch (AggregateException ae)
+ {
+ ae.Handle((ex) => ExceptionIsOptional(ex)); // Re-throws if not optional
+ }
+ catch (Exception e) when (ExceptionIsOptional(e)) { }
+
+ return null;
+ }
+
+ private List GetAllKeys()
+ {
+ List keys = new List(); // KeyVault keys are case-insensitive. There won't be case-duplicates. List<> should be fine.
+
+ // Don't go loading all the keys if we can't, or if we were told not to
+ if (_kvClient == null || !Preload)
+ return keys;
+
+ try
+ {
+ // make HTTP Request (? )
+ var result = GetValueAsync(string.Empty, string.Empty).Result;
+
+ return Secrets.Select(c => c.Key).ToList();
+ }
+ catch (AggregateException ae)
+ {
+ ae.Handle((ex) => ExceptionIsOptional(ex)); // Re-throws if not optional
+ }
+ catch (Exception e) when (ExceptionIsOptional(e)) { }
+
+ return keys;
+ }
+ }
+
+ public record KeyVaultSecret(string Key, string Value)
+ {
+ public string Key { get; } = Key;
+ public string Value { get; } = Value;
+ }
+}
+
+
diff --git a/tracer/test/test-applications/aspnet/Samples.AspNet.MultipleAppsInDomain/CustomConfigBuilder/KeyValueConfigBuilder.cs b/tracer/test/test-applications/aspnet/Samples.AspNet.MultipleAppsInDomain/CustomConfigBuilder/KeyValueConfigBuilder.cs
new file mode 100644
index 000000000000..86146ee0747c
--- /dev/null
+++ b/tracer/test/test-applications/aspnet/Samples.AspNet.MultipleAppsInDomain/CustomConfigBuilder/KeyValueConfigBuilder.cs
@@ -0,0 +1,462 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT license. See the License.txt file in the project root for full license information.
+
+using System;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.Configuration;
+using System.Security;
+using System.Text.RegularExpressions;
+
+namespace Microsoft.Configuration.ConfigurationBuilders
+{
+ ///
+ /// Base class for a set of ConfigurationBuilders that follow a basic key/value pair substitution model. This base
+ /// class handles substitution modes and most prefix concerns, so implementing classes only need to be a basic
+ /// source of key/value pairs through the and methods.
+ ///
+ public abstract class KeyValueConfigBuilder : ConfigurationBuilder
+ {
+ #pragma warning disable CS1591 // No xml comments for tag literals.
+ public const string modeTag = "mode";
+ public const string prefixTag = "prefix";
+ public const string stripPrefixTag = "stripPrefix";
+ public const string tokenPatternTag = "tokenPattern";
+ public const string optionalTag = "optional";
+ public const string enabledTag = "enabled";
+ public const string escapeTag = "escapeExpandedValues";
+ public const string charMapTag = "charMap";
+ public const string recursionGuardTag = "recur";
+ #pragma warning restore CS1591 // No xml comments for tag literals.
+
+ private NameValueCollection _config = null;
+ private IDictionary _cachedValues;
+ private bool _lazyInitializeStarted = false;
+ private bool _lazyInitialized = false;
+ private bool _greedyInitialized = false;
+
+ ///
+ /// Gets or sets the substitution pattern to be used by the KeyValueConfigBuilder.
+ ///
+ public KeyValueMode Mode { get { EnsureInitialized(); return _mode; } }
+ private KeyValueMode _mode = KeyValueMode.Strict;
+
+ ///
+ /// Gets or sets a prefix string that must be matched by keys to be considered for value substitution.
+ ///
+ public string KeyPrefix { get { EnsureInitialized(); return _keyPrefix; } }
+ private string _keyPrefix = "";
+
+ private bool StripPrefix { get { EnsureInitialized(); return _stripPrefix; } }
+ private bool _stripPrefix = false; // Prefix-stripping is all handled in this base class; this is private so it doesn't confuse sub-classes.
+
+ ///
+ /// Specifies whether the config builder should cause errors if the backing source cannot be found.
+ ///
+ [Obsolete("Please use the 'Enabled' flag instead to specify optional builders.")]
+ public bool Optional { get { return Enabled != KeyValueEnabled.Enabled; } protected set { _enabled = value ? KeyValueEnabled.Optional : KeyValueEnabled.Enabled; } }
+ ///
+ /// Specifies whether the config builder should cause errors if the backing source cannot be found.
+ ///
+ public bool IsOptional { get { return Enabled != KeyValueEnabled.Enabled; } }
+
+ ///
+ /// Specifies whether the config builder should cause errors if the backing source cannot be found, or even run at all.
+ ///
+ public KeyValueEnabled Enabled { get { EnsureInitialized(); return _enabled; } protected set { _enabled = value; } }
+ private KeyValueEnabled _enabled = KeyValueEnabled.Optional;
+
+ ///
+ /// Specifies whether the config builder should cause errors if the backing source cannot be found.
+ ///
+ public bool EscapeValues { get { EnsureInitialized(); return _escapeValues; } protected set { _escapeValues = value; } }
+ private bool _escapeValues = false;
+
+ ///
+ /// Gets or sets a regular expression used for matching tokens during 'Token' substitution.
+ ///
+ public string TokenPattern { get { EnsureInitialized(); return _tokenPattern; } protected set { _tokenPattern = value; } }
+ //private string _tokenPattern = @"\$\{(\w+)\}";
+ private string _tokenPattern = @"\$\{(\w[\w-_$@#+,.:~]*)\}"; // Updated to be more reasonable for V2
+ //private string _tokenPattern = @"\$\{(\w[\w-_$@#+,.~]*)(?:\:([^}]*))?\}"; // Something like this to allow default values
+
+ ///
+ /// Gets or sets the behavior to use when recursion is detected.
+ ///
+ public RecursionGuardValues Recursion { get { return _recur; } private set { _recur = value; } }
+ private RecursionGuardValues _recur = RecursionGuardValues.Throw;
+
+ ///
+ /// Gets or sets a string-represented mapping of characters to apply when mapping keys. Escape with doubles. Ex "@=a,$=S" or "a-z=a,,z,0-9=0,,9"
+ ///
+ public Dictionary CharacterMap { get { EnsureInitialized(); return _characterMap; } protected set { _characterMap = value; } }
+ private Dictionary _characterMap = new Dictionary();
+
+ ///
+ /// Gets the ConfigurationSection object that is currently being processed by this builder.
+ ///
+ protected ConfigurationSection CurrentSection { get { return _currentSection; } }
+ private ConfigurationSection _currentSection = null;
+
+ ///
+ /// Looks up a single 'value' for the given 'key.'
+ ///
+ /// The 'key' to look up in the config source. (Prefix handling is not needed here.)
+ /// The value corresponding to the given 'key' or null if no value is found.
+ public abstract string GetValue(string key);
+
+ ///
+ /// Retrieves all known key/value pairs for the configuration source where the key begins with with .
+ ///
+ /// A prefix string to filter the list of potential keys retrieved from the source.
+ /// A collection of key/value pairs.
+ public abstract ICollection> GetAllValues(string prefix);
+
+ ///
+ /// Transform the given key to an intermediate format that will be used to look up values in backing store.
+ ///
+ /// The string to be mapped.
+ /// The key string to be used while looking up config values..
+ public virtual string MapKey(string key)
+ {
+ if (String.IsNullOrEmpty(key))
+ return key;
+
+ foreach (var mapping in CharacterMap)
+ key = key.Replace(mapping.Key, mapping.Value);
+
+ return key;
+ }
+
+ ///
+ /// Makes a determination about whether the input key is valid for this builder and backing store.
+ ///
+ /// The string to be validated. May be partial.
+ /// True if the string is valid. False if the string is not a valid key.
+ public virtual bool ValidateKey(string key) { return true; }
+
+ ///
+ /// Transforms the raw key to a new string just before updating items in Strict and Greedy modes.
+ ///
+ /// The key as read from the incoming config section.
+ /// The key string that will be left in the processed config section.
+ public virtual string UpdateKey(string rawKey) { return rawKey; }
+
+ ///
+ /// Initializes the configuration builder.
+ ///
+ /// The friendly name of the provider.
+ /// A collection of the name/value pairs representing builder-specific attributes specified in the configuration for this provider.
+ public override void Initialize(string name, NameValueCollection config)
+ {
+ base.Initialize(name, config);
+ _config = config ?? new NameValueCollection();
+
+ if (_config[recursionGuardTag] != null)
+ {
+ // We want an exception here if 'recursionCheck' is specified but unrecognized.
+ Recursion = (RecursionGuardValues)Enum.Parse(typeof(RecursionGuardValues), config[recursionGuardTag], true);
+ }
+ }
+
+ ///
+ /// Initializes the configuration builder lazily.
+ ///
+ /// The friendly name of the provider.
+ /// A collection of the name/value pairs representing builder-specific attributes specified in the configuration for this provider.
+ protected virtual void LazyInitialize(string name, NameValueCollection config)
+ {
+ // We need this first so we can look for tokens to replace with AppSettings
+ _tokenPattern = config[tokenPatternTag] ?? _tokenPattern;
+
+ // Next, check 'enabled' to see if we even need to do anything.
+ // 'optional' is obsolete, but we'll still honor it only if it is set explicitly and does not conflict
+ // with an explicit 'enabled' attribute.
+ _enabled = (UpdateConfigSettingWithAppSettings(enabledTag) != null) ? (KeyValueEnabled)Enum.Parse(typeof(KeyValueEnabled), config[enabledTag], true) : _enabled;
+ if (config[enabledTag] == null)
+ {
+ // There was no explicit 'enabled' attribute, but we have our default. Only change if we find an explicit 'optional'.
+ if (UpdateConfigSettingWithAppSettings(optionalTag) != null)
+ _enabled = Boolean.Parse(config[optionalTag]) ? KeyValueEnabled.Optional : KeyValueEnabled.Enabled;
+ }
+
+ // At this point, we have our 'Enabled' choice. If we are disabled, we can stop right here.
+ if (_enabled == KeyValueEnabled.Disabled) return;
+
+ // Use pre-assigned defaults if not specified. Non-freeform options should throw on unrecognized values.
+ _mode = (UpdateConfigSettingWithAppSettings(modeTag) != null) ? (KeyValueMode)Enum.Parse(typeof(KeyValueMode), config[modeTag], true) : _mode;
+ _keyPrefix = UpdateConfigSettingWithAppSettings(prefixTag) ?? _keyPrefix;
+ _stripPrefix = (UpdateConfigSettingWithAppSettings(stripPrefixTag) != null) ? Boolean.Parse(config[stripPrefixTag]) : _stripPrefix;
+ _escapeValues = (UpdateConfigSettingWithAppSettings(escapeTag) != null) ? Boolean.Parse(config[escapeTag]) : _escapeValues;
+ _characterMap = (UpdateConfigSettingWithAppSettings(charMapTag) != null) ? ParseCharacterMap(config[charMapTag]) : _characterMap;
+
+ _cachedValues = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ }
+
+ ///
+ /// Perform token substitution on a config parameter passed through builder initialization using token values from appSettings.
+ ///
+ /// The name of the parameter to be retrieved.
+ /// The updated parameter value if it exists. Null otherwise.
+ protected string UpdateConfigSettingWithAppSettings(string configName)
+ {
+ string configValue = _config[configName];
+
+ if (!_lazyInitializeStarted || String.IsNullOrWhiteSpace(configValue))
+ return configValue;
+
+ configValue = Regex.Replace(configValue, _tokenPattern, (m) =>
+ {
+ string settingName = m.Groups[1].Value;
+ string defaultValue = (m.Groups[2].Success) ? m.Groups[2].Value : m.Groups[0].Value;
+
+ // If we are processing appSettings in ProcessConfigurationSection(), then we can use that. Other config builders in
+ // the chain before us have already finished, so this is a relatively consistent and logical state to draw from.
+ if (CurrentSection is AppSettingsSection appSettings && CurrentSection.SectionInformation?.SectionName == "appSettings")
+ return (appSettings.Settings[settingName]?.Value ?? defaultValue);
+
+ // Try to use CurrentConfiguration before falling back to ConfigurationManager. Otherwise OpenConfiguration()
+ // scenarios won't work because we're looking in the wrong processes AppSettings.
+ else if (CurrentSection?.CurrentConfiguration?.AppSettings is AppSettingsSection currentAppSettings)
+ return (currentAppSettings.Settings[settingName]?.Value ?? defaultValue);
+
+ // All other config sections can just go through ConfigurationManager to get app settings though. :)
+ return (ConfigurationManager.AppSettings[settingName] ?? defaultValue);
+ });
+
+ _config[configName] = configValue;
+ return configValue;
+ }
+
+ ///
+ /// Use to populate a cache of possible key/value pairs and avoid
+ /// querying the config source multiple times. Always called by base in 'Greedy' mode. May also be called by
+ /// individual builders in some other cases.
+ ///
+ protected void EnsureGreedyInitialized()
+ {
+ try
+ {
+ // In Greedy mode, we need to know all the key/value pairs from this config source. So we
+ // can't 'cache' them as we go along. Slurp them all up now. But only once. ;)
+ if (!_greedyInitialized)
+ {
+ string prefix = MapKey(KeyPrefix); // Do this outside the lock. It ensures _cachedValues is initialized.
+ lock (_cachedValues)
+ {
+ if (!_greedyInitialized && (String.IsNullOrEmpty(prefix) || ValidateKey(prefix)))
+ {
+ foreach (KeyValuePair kvp in GetAllValues(prefix))
+ {
+ _cachedValues.Add(kvp.Key, kvp.Value);
+ }
+ _greedyInitialized = true;
+ }
+ }
+ }
+ }
+ catch (Exception ex) when (!KeyValueExceptionHelper.IsKeyValueConfigException(ex))
+ {
+ throw KeyValueExceptionHelper.CreateKVCException("GetAllValues() Error", ex, this);
+ }
+ }
+
+ //=========================================================================================================================
+ #region "Private" stuff
+ // Sub-classes need not worry about this stuff, even though some of it is "public" because it comes from the framework.
+
+ ///
+ /// (Warning: Overriding may interfere with recursion detection.)
+ ///
+ public override ConfigurationSection ProcessConfigurationSection(ConfigurationSection configSection)
+ {
+ _currentSection = configSection;
+
+ using (var rg = new RecursionGuard(this, configSection.SectionInformation?.Name, Recursion))
+ {
+ // Don't do anything more if we are disabled or getting caught in recursion.
+ if (rg.ShouldStop || Enabled == KeyValueEnabled.Disabled)
+ return configSection;
+
+ // See if we know how to process this section
+ ISectionHandler handler = SectionHandlersSection.GetSectionHandler(configSection);
+ if (handler == null)
+ return configSection;
+
+
+ // Strict Mode. Only replace existing key/values.
+ if (Mode == KeyValueMode.Strict)
+ {
+ foreach (var configItem in handler.KeysValuesAndState())
+ {
+ // Presumably, UpdateKey will preserve casing appropriately, so newKey is cased as expected.
+ string newKey = UpdateKey(configItem.Item1);
+ string newValue = GetValueInternal(configItem.Item1);
+
+ if (newValue != null)
+ handler.InsertOrUpdate(newKey, newValue, configItem.Item1, configItem.Item3);
+ }
+ }
+
+ // Token Mode. Replace tokens in existing key/values.
+ else if (Mode == KeyValueMode.Token)
+ {
+ foreach (var configItem in handler.KeysValuesAndState())
+ {
+ string newKey = ExpandTokens(configItem.Item1);
+ string newValue = ExpandTokens(configItem.Item2);
+
+ if (newValue != null)
+ handler.InsertOrUpdate(newKey, newValue, configItem.Item1, configItem.Item3);
+ }
+ }
+
+ // Greedy Mode. Insert all key/values.
+ else if (Mode == KeyValueMode.Greedy)
+ {
+ EnsureGreedyInitialized();
+
+ // Cached keys have already been 'mapped', but the prefix property we're about to use for trimming them
+ // hasn't. Do that here so we are sure to correctly trim prefixes according to the way they are mapped.
+ string prefix = MapKey(KeyPrefix);
+ foreach (KeyValuePair kvp in _cachedValues)
+ {
+ if (kvp.Value != null)
+ {
+ // Here, kvp.Key is not from the config file, so it might not be correctly cased. Get the correct casing for UpdateKey.
+ string oldKey = TrimPrefix(handler.TryGetOriginalCase(kvp.Key), prefix);
+ string newKey = UpdateKey(oldKey);
+ handler.InsertOrUpdate(newKey, kvp.Value, oldKey);
+ }
+ }
+ }
+ }
+
+ _currentSection = null;
+ return configSection;
+ }
+
+ private void EnsureInitialized()
+ {
+ if (!_lazyInitialized)
+ {
+ lock (this)
+ {
+ if (!_lazyInitialized && !_lazyInitializeStarted)
+ {
+ try
+ {
+ _lazyInitializeStarted = true;
+ LazyInitialize(Name, _config);
+ _lazyInitialized = true;
+ }
+ catch (Exception ex) when (!KeyValueExceptionHelper.IsKeyValueConfigException(ex))
+ {
+ throw KeyValueExceptionHelper.CreateKVCException("Initialization Error", ex, this);
+ }
+ }
+ }
+ }
+ }
+
+ private string ExpandTokens(string rawString)
+ {
+ string updatedString = Regex.Replace(rawString, TokenPattern, (m) =>
+ {
+ string key = m.Groups[1].Value;
+ string defaultValue = (m.Groups[2].Success) ? m.Groups[2].Value : m.Groups[0].Value;
+
+ // Same prefix-handling rules apply in token mode as in strict mode.
+ // Since the key is being completely replaced by the value, we don't need to call UpdateKey().
+ return EscapeValue(GetValueInternal(key)) ?? defaultValue;
+ });
+
+ return updatedString;
+ }
+
+ private string GetValueInternal(string key)
+ {
+ if (String.IsNullOrEmpty(key))
+ return null;
+
+ try
+ {
+ // Make sure the key we are looking up begins with the correct prefix... if we are not stripping prefixes.
+ if (!StripPrefix && !key.StartsWith(KeyPrefix, StringComparison.OrdinalIgnoreCase))
+ return null;
+
+ // Stripping Prefix in strict mode means from the source key. The static config file will have a prefix-less key to match.
+ // ie should only match the key/value (KeyPrefix + "MySetting") from the source.
+ string sourceKey = MapKey((StripPrefix) ? KeyPrefix + key : key);
+
+ if (!ValidateKey(sourceKey))
+ return null;
+
+ return (_cachedValues.ContainsKey(sourceKey)) ? _cachedValues[sourceKey] : _cachedValues[sourceKey] = GetValue(sourceKey);
+ }
+ catch (Exception ex) when (!KeyValueExceptionHelper.IsKeyValueConfigException(ex))
+ {
+ throw KeyValueExceptionHelper.CreateKVCException("GetValue() Error", ex, this);
+ }
+ }
+
+ private string TrimPrefix(string fullString, string prefix = null)
+ {
+ prefix = prefix ?? KeyPrefix;
+
+ if (!StripPrefix || !fullString.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
+ return fullString;
+
+ return fullString.Substring(prefix.Length);
+ }
+
+ // Maybe this could be virtual? Simple xml escaping should be enough for most folks.
+ private string EscapeValue(string original)
+ {
+ return (_escapeValues && original != null) ? SecurityElement.Escape(original) : original;
+ }
+
+ private Dictionary ParseCharacterMap(string stringMap)
+ {
+ // The format here is string=string,string=string.
+ // To use separators in your maps, escape them by doubling.
+ Dictionary charmap = new Dictionary();
+ char[] coupler = { '=' };
+ char[] delimiter = { ',' };
+
+ if (String.IsNullOrWhiteSpace(stringMap))
+ return charmap;
+
+ try
+ {
+ // Break the string into pairs - Account for escaped ','s
+ var pairs = stringMap.Replace(",,", "\x30").Split(delimiter, StringSplitOptions.RemoveEmptyEntries);
+
+ foreach (string pairing in pairs)
+ {
+ // Remember to un-escape any ','s first, and do then escape escaped '='
+ var mapping = pairing.Replace("\x30", ",").Replace("==", "\x30").Split(coupler, 2, StringSplitOptions.RemoveEmptyEntries);
+
+ // If we have a 'mapping' that does not have two parts, this is an error
+ if (mapping.Length < 2)
+ throw new ArgumentException("Mapping should be a ',' delimited list of strings paired with '='. Use double characters to escape ',' and '='.", charMapTag);
+
+ // Remember to un-escape any '='s first
+ mapping[0] = mapping[0].Replace("\x30", "=");
+ mapping[1] = mapping[1].Replace("\x30", "=");
+
+ charmap.Add(mapping[0], mapping[1]);
+ }
+ }
+ catch (Exception ex)
+ {
+ throw new Exception($"Error in Configuration Builder '{Name}' while parsing '{charMapTag}'", ex);
+ }
+
+ return charmap;
+ }
+
+ #endregion
+ //=========================================================================================================================
+ }
+}
\ No newline at end of file
diff --git a/tracer/test/test-applications/aspnet/Samples.AspNet.MultipleAppsInDomain/CustomConfigBuilder/KeyValueEnabled.cs b/tracer/test/test-applications/aspnet/Samples.AspNet.MultipleAppsInDomain/CustomConfigBuilder/KeyValueEnabled.cs
new file mode 100644
index 000000000000..00e7522d4b2f
--- /dev/null
+++ b/tracer/test/test-applications/aspnet/Samples.AspNet.MultipleAppsInDomain/CustomConfigBuilder/KeyValueEnabled.cs
@@ -0,0 +1,34 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT license. See the License.txt file in the project root for full license information.
+
+namespace Microsoft.Configuration.ConfigurationBuilders
+{
+ ///
+ /// Possible modes (or behaviors) for key/value substitution.
+ ///
+ public enum KeyValueEnabled
+ {
+ ///
+ /// Will execute KeyValueConfigurationBuilder and throw on error.
+ ///
+ Enabled,
+ ///
+ /// Will not execute KeyValueConfigurationBuilder.
+ ///
+ Disabled,
+ ///
+ /// Will execute KeyValueConfigurationBuilder but not report errors.
+ ///
+ Optional,
+
+ // For convenience, allow true/false in the builder attribute as well.
+ ///
+ /// Same as Enabled.
+ ///
+ True = Enabled,
+ ///
+ /// Same as Disabled.
+ ///
+ False = Disabled
+ }
+}
\ No newline at end of file
diff --git a/tracer/test/test-applications/aspnet/Samples.AspNet.MultipleAppsInDomain/CustomConfigBuilder/KeyValueExceptions.cs b/tracer/test/test-applications/aspnet/Samples.AspNet.MultipleAppsInDomain/CustomConfigBuilder/KeyValueExceptions.cs
new file mode 100644
index 000000000000..0a4abc295a9f
--- /dev/null
+++ b/tracer/test/test-applications/aspnet/Samples.AspNet.MultipleAppsInDomain/CustomConfigBuilder/KeyValueExceptions.cs
@@ -0,0 +1,67 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT license. See the License.txt file in the project root for full license information.
+
+using System;
+using System.Configuration;
+
+namespace Microsoft.Configuration.ConfigurationBuilders
+{
+ internal class KeyValueExceptionHelper
+ {
+ public static Exception CreateKVCException(string msg, Exception ex, ConfigurationBuilder cb)
+ {
+
+ // If it's a ConfigurationErrorsException though, that means its coming from a re-entry to the
+ // config system. That's where the root issue is, and that's the "Error Message" we want on
+ // the top of the exception chain. So wrap it in another ConfigurationErrorsException of
+ // sorts so the config system will use it instead of rolling it's own at this secondary
+ // level.
+ if (ex is ConfigurationErrorsException ceex)
+ {
+ var inner = new KeyValueConfigBuilderException($"'{cb.Name}' {msg} ==> {ceex.InnerException?.Message ?? ceex.Message}", ex.InnerException);
+ return new KeyValueConfigurationErrorsException(ceex.Message, inner);
+ }
+
+ var ff = new KeyValueConfigBuilderException();
+ return new KeyValueConfigBuilderException($"'{cb.Name}' {msg}: {ex.Message}", ex);
+ }
+
+ // We only want to wrap the original exception. Once we wrap it, just keep raising the wrapped
+ // exception so we don't create an endless chain of exception wrappings that are not helpful when
+ // being surfaced in a YSOD or similar. Use this helper to determine if wrapping is needed.
+ public static bool IsKeyValueConfigException(Exception ex) => (ex is KeyValueConfigBuilderException) || (ex is KeyValueConfigurationErrorsException);
+ }
+
+ // There are two different exception types here because the .Net config system treats
+ // ConfigurationErrorsExceptions differently. It considers it to be a pre-wrapped and ready for
+ // presentation exception. Other exceptions get wrapped by the config system. We don't want
+ // to lose that "pre-wrapped-ness" if the exception has already been through .Net config.
+
+ ///
+ /// An exception that wraps the root failure due to non-config exceptions while processing Key Value Config Builders.
+ ///
+ [Serializable]
+ public class KeyValueConfigBuilderException : Exception
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public KeyValueConfigBuilderException() : base() { }
+
+ internal KeyValueConfigBuilderException(string msg, Exception inner) : base(msg, inner) { }
+ }
+
+ ///
+ /// An exception that wraps the root failure due to config exceptions while processing Key Value Config Builders.
+ ///
+ [Serializable]
+ public class KeyValueConfigurationErrorsException : ConfigurationErrorsException
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public KeyValueConfigurationErrorsException() : base() { }
+
+ internal KeyValueConfigurationErrorsException(string msg, Exception inner) : base(msg, inner) { }
+ }
+}
\ No newline at end of file
diff --git a/tracer/test/test-applications/aspnet/Samples.AspNet.MultipleAppsInDomain/CustomConfigBuilder/KeyValueMode.cs b/tracer/test/test-applications/aspnet/Samples.AspNet.MultipleAppsInDomain/CustomConfigBuilder/KeyValueMode.cs
new file mode 100644
index 000000000000..44e834e24219
--- /dev/null
+++ b/tracer/test/test-applications/aspnet/Samples.AspNet.MultipleAppsInDomain/CustomConfigBuilder/KeyValueMode.cs
@@ -0,0 +1,24 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT license. See the License.txt file in the project root for full license information.
+
+namespace Microsoft.Configuration.ConfigurationBuilders
+{
+ ///
+ /// Possible modes (or behaviors) for key/value substitution.
+ ///
+ public enum KeyValueMode
+ {
+ ///
+ /// Replaces 'value' if 'key' is matched. Only operates on known key/value config sections.
+ ///
+ Strict,
+ ///
+ /// Inserts all 'values' regardless of the previous existence of the 'key.' Only operates on known key/value config sections.
+ ///
+ Greedy,
+ ///
+ /// Replace 'key'-specifying tokens in the 'key' or 'value' parts of a config entry.
+ ///
+ Token = 3
+ }
+}
\ No newline at end of file
diff --git a/tracer/test/test-applications/aspnet/Samples.AspNet.MultipleAppsInDomain/CustomConfigBuilder/RecursionGuard.cs b/tracer/test/test-applications/aspnet/Samples.AspNet.MultipleAppsInDomain/CustomConfigBuilder/RecursionGuard.cs
new file mode 100644
index 000000000000..85fa7828cff8
--- /dev/null
+++ b/tracer/test/test-applications/aspnet/Samples.AspNet.MultipleAppsInDomain/CustomConfigBuilder/RecursionGuard.cs
@@ -0,0 +1,108 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT license. See the License.txt file in the project root for full license information.
+
+using System;
+using System.Collections.Generic;
+using System.Configuration;
+using System.Threading;
+
+namespace Microsoft.Configuration.ConfigurationBuilders
+{
+ ///
+ /// Possible behaviors when dealing with config builder recursion.
+ ///
+ public enum RecursionGuardValues
+ {
+ ///
+ /// [Default] Throw when reprocessing the same section.
+ ///
+ Throw,
+ ///
+ /// [!!May yield unexpected results in recursive conditions!!] Stop recursing, return without processing.
+ ///
+ Stop,
+ ///
+ /// [!!May result in deadlocks or stack overflow in recursive conditions!!] Do nothing. Continue recursing.
+ ///
+ Allow
+ }
+
+ ///
+ /// Disposable object to help detect and handle problems with recursion.
+ ///
+ internal class RecursionGuard : IDisposable
+ {
+ private static readonly AsyncLocal> sectionsEntered = new AsyncLocal>();
+
+ internal bool ShouldStop = false;
+ private readonly string sectionName;
+ private readonly string builderName;
+
+ internal RecursionGuard(ConfigurationBuilder configBuilder, string sectionName, RecursionGuardValues behavior)
+ {
+ // If behavior says "do nothing"... then let's do nothing.
+ if (behavior == RecursionGuardValues.Allow)
+ return;
+
+ sectionsEntered.Value = sectionsEntered.Value ?? new Stack<(string, string)>();
+ builderName = $"{configBuilder.Name}[{configBuilder.GetType()}]";
+
+ // The idea here is that in this "thread" of execution, a config section is logged on this stack
+ // while being processed by a builder. Ie, in ProcessRawXml() or ProcessConfigurationSection().
+ // Once the builder is done in each phase, it pops the section off the stack. [And the next builder
+ // might pop it back on to do it's work.] The goal is to prevent an endless loop between sections
+ // in one thread, not to prevent concurrent processing by multiple threads.
+ // If we ever re-enter the same config section recursively, it will already be on the stack.
+ // Since we push/pop for each builder, it does not cause problems with multiple builders in one chain.
+ // Also, we can still enter a chain of different config sections so long as we don't re-enter any.
+ // Unfortunately, we can't rely on much more than a simple section name since more information is
+ // not available to us, especially in ProcessRawXml(). Fortunately, config sections are generally
+ // top-level enough that there shouldn't really be any collisions here.
+ var prev = FindSection(sectionsEntered.Value, sectionName);
+ if (prev != null)
+ {
+ // Should we throw an exception?
+ if (behavior == RecursionGuardValues.Throw)
+ {
+ // Don't touch sectionsEntered. We did not add to it. We should not take from it or
+ // throw it away in case somebody wants to handle this exception.
+ //System.IO.File.WriteAllText(@"C:\ProgramData\Datadog .NET Tracer\logs\RecursionGuard.log", $"The ConfigurationBuilder '{prev.Value.builder}' has recursively re-entered processing of the '{sectionName}' section: " + new System.Diagnostics.StackFrame());
+ throw new InvalidOperationException($"The ConfigurationBuilder '{prev.Value.builder}' has recursively re-entered processing of the '{sectionName}' section.");
+ }
+
+ // If we don't throw, should we at least stop going down the rabbit hole?
+ ShouldStop = (behavior == RecursionGuardValues.Stop);
+ }
+
+ // If we get here, then we will allow the section to be processed. Or at least we are returning
+ // a guard instance and letting the caller decide what to do. We will be popping when we dispose,
+ // so regardless of what our caller does, we should add the section name to the list of entered
+ // sections, and also remember it so we know to remove it from the list when we are disposed.
+ this.sectionName = sectionName;
+ sectionsEntered.Value.Push((sectionName, builderName));
+ }
+
+ public void Dispose()
+ {
+ // If we tracked the entering of a section... stop tracking now.
+ if (sectionName != null)
+ {
+ var (section, builder) = sectionsEntered.Value.Pop();
+
+ // Sanity check to make sure we are un-tracking what we expect to un-track.
+ if ((section != sectionName) || (builder != builderName)) {
+ throw new InvalidOperationException($"The ConfigurationBuilder {builderName} has detected a mix up while processing of the '{sectionName}' section. ({builder},{section})");
+ }
+ }
+ }
+
+ private static (string section, string builder)? FindSection(Stack<(string s, string b)> stack, string section)
+ {
+ foreach (var record in stack)
+ if (record.s == section)
+ return record;
+
+ return null;
+ }
+ }
+}
\ No newline at end of file
diff --git a/tracer/test/test-applications/aspnet/Samples.AspNet.MultipleAppsInDomain/CustomConfigBuilder/SectionHandler.cs b/tracer/test/test-applications/aspnet/Samples.AspNet.MultipleAppsInDomain/CustomConfigBuilder/SectionHandler.cs
new file mode 100644
index 000000000000..7acecbee51d6
--- /dev/null
+++ b/tracer/test/test-applications/aspnet/Samples.AspNet.MultipleAppsInDomain/CustomConfigBuilder/SectionHandler.cs
@@ -0,0 +1,331 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT license. See the License.txt file in the project root for full license information.
+
+using System;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.Configuration;
+using System.Configuration.Provider;
+
+namespace Microsoft.Configuration.ConfigurationBuilders
+{
+ // Covariance is not allowed with generic classes. Lets use this trick instead.
+ internal interface ISectionHandler
+ {
+ void InsertOrUpdate(string newKey, string newValue, string oldKey = null, object oldItem = null);
+ IEnumerable> KeysValuesAndState();
+ string TryGetOriginalCase(string requestedKey);
+ }
+
+ ///
+ /// A class to be used by s to apply key/value config pairs to .Net configuration sections.
+ ///
+ /// The type of that the implementing class can process.
+ public abstract class SectionHandler : ProviderBase, ISectionHandler where T : ConfigurationSection
+ {
+ ///
+ /// The instance being processed by this .
+ ///
+ public T ConfigSection { get; private set; }
+
+ ///
+ /// Obsolete: Please implement to provide enumeration compatible with
+ ///
+ /// An enumerator over pairs consisting of the existing key for a config value in the config section, and an object reference
+ /// for the key/value pair to be passed in to while processing the config section.
+ [Obsolete]
+ public virtual IEnumerator> GetEnumerator()
+ {
+ return null;
+ }
+
+ ///
+ /// Gets an for iterating over the key/value pairs contained in the assigned . />
+ ///
+ /// An of for iterating over each key/value pair contained in
+ /// the config section. Each Tuple contains the 'key', 'value', and a 'state' object which is passed back to this SectionHandler
+ /// when updating records with .
+ public virtual IEnumerable> KeysValuesAndState()
+ {
+#pragma warning disable CS0612 // Type or member is obsolete
+ foreach (KeyValuePair kvp in this)
+#pragma warning restore CS0612 // Type or member is obsolete
+ yield return Tuple.Create(kvp.Key, (string)null, kvp.Value);
+ }
+
+ ///
+ /// Updates an existing config value in the assigned with a new key and a new value. The old config value
+ /// can be located using the or parameters. If an old config value is not
+ /// found, a new config value should be inserted.
+ ///
+ /// The updated key name for the config item.
+ /// The updated value for the config item.
+ /// The old key name for the config item, or null.
+ /// A reference to the old key/value pair obtained by , or null.
+ public abstract void InsertOrUpdate(string newKey, string newValue, string oldKey = null, object oldItem = null);
+
+ ///
+ /// Attempt to lookup the original key casing so it can be preserved during greedy updates which would otherwise lose
+ /// the original casing in favor of the casing used in the config source.
+ ///
+ /// The key to find original casing for.
+ /// Unless overridden, returns the string passed in.
+ public virtual string TryGetOriginalCase(string requestedKey)
+ {
+ return requestedKey;
+ }
+
+
+#pragma warning disable IDE0051 // Remove unused private members
+ // We call this depending on section type via reflection in SectionHandlerSection.GetSectionHandler()
+ private void Initialize(string name, T configSection, NameValueCollection config)
+#pragma warning restore IDE0051 // Remove unused private members
+ {
+ ConfigSection = configSection;
+ Initialize(name, config);
+ }
+ }
+
+ ///
+ /// A class that can be used by s to apply key/value config pairs to .
+ ///
+ public class AppSettingsSectionHandler : SectionHandler
+ {
+ ///
+ /// Updates an existing app setting in the assigned with a new key and a new value. The old setting
+ /// can be located using the parameter. If an old setting is not found, a new setting should be inserted.
+ ///
+ /// The updated key name for the app setting.
+ /// The updated value for the app setting.
+ /// The old key name for the app setting, or null.
+ /// The old key name for the app setting, or null., or null.
+ public override void InsertOrUpdate(string newKey, string newValue, string oldKey = null, object oldItem = null)
+ {
+ if (newValue != null)
+ {
+ if (oldKey != null)
+ ConfigSection.Settings.Remove(oldKey);
+
+ ConfigSection.Settings.Remove(newKey);
+ ConfigSection.Settings.Add(newKey, newValue);
+ }
+ }
+
+ ///
+ /// Gets an for iterating over the key/value pairs contained in the assigned . />
+ ///
+ /// An enumerator over tuples where the values of the tuple are the existing key for each setting, the old value for the
+ /// setting, and the existing key for the setting again as the state which will be returned unmodified when updating.
+ public override IEnumerable> KeysValuesAndState()
+ {
+ // Grab a copy of the keys array since we are using 'yield' and the Settings collection may change on us.
+ ConfigurationElement[] allSettings = new ConfigurationElement[ConfigSection.Settings.Count];
+ ConfigSection.Settings.CopyTo(allSettings, 0);
+ foreach (KeyValueConfigurationElement setting in allSettings)
+ yield return Tuple.Create(setting.Key, setting.Value, (object)setting.Key);
+ }
+
+ ///
+ /// Attempt to lookup the original key casing so it can be preserved during greedy updates which would otherwise lose
+ /// the original casing in favor of the casing used in the config source.
+ ///
+ /// The key to find original casing for.
+ /// A string containing the key with original casing from the config section, or the key as passed in if no match
+ /// can be found.
+ public override string TryGetOriginalCase(string requestedKey)
+ {
+ if (!String.IsNullOrWhiteSpace(requestedKey))
+ {
+ var keyval = ConfigSection.Settings[requestedKey];
+ if (keyval != null)
+ return keyval.Key;
+ }
+
+ return base.TryGetOriginalCase(requestedKey);
+ }
+ }
+
+ ///
+ /// A class that can be used by s to apply key/value config pairs to .
+ ///
+ public class ConnectionStringsSectionHandler : SectionHandler
+ {
+ ///
+ /// Updates an existing connection string in the assigned with a new name and a new value. The old
+ /// connection string can be located using the parameter. If an old connection string is not found, a new connection
+ /// string should be inserted.
+ ///
+ /// The updated key name for the connection string.
+ /// The updated value for the connection string.
+ /// The old key name for the connection string, or null.
+ /// A reference to the old object obtained by , or null.
+ public override void InsertOrUpdate(string newKey, string newValue, string oldKey = null, object oldItem = null)
+ {
+ if (newValue != null)
+ {
+ // Preserve the old entry if it exists, as it might have more than just name/connectionString attributes.
+ ConnectionStringSettings cs = (oldItem as ConnectionStringSettings) ?? new ConnectionStringSettings();
+
+ // Make sure there are no entries using the old or new name other than this one
+ ConfigSection.ConnectionStrings.Remove(oldKey);
+ ConfigSection.ConnectionStrings.Remove(newKey);
+
+ // Update values and re-add to the collection
+ cs.Name = newKey;
+ cs.ConnectionString = newValue;
+ ConfigSection.ConnectionStrings.Add(cs);
+ }
+ }
+
+ ///
+ /// Gets an for iterating over the key/value pairs contained in the assigned . />
+ ///
+ /// An enumerator over tuples where the values of the tuple are the existing name for each connection string, the value of
+ /// the connection string, and a reference to the object itself which will be returned to
+ /// us as a reference state object when updating the config record.
+ public override IEnumerable> KeysValuesAndState()
+ {
+ // The ConnectionStrings collection may change on us while we enumerate. :/
+ ConnectionStringSettings[] connStrs = new ConnectionStringSettings[ConfigSection.ConnectionStrings.Count];
+ ConfigSection.ConnectionStrings.CopyTo(connStrs, 0);
+
+ foreach (ConnectionStringSettings cs in connStrs)
+ yield return Tuple.Create(cs.Name, cs.ConnectionString, (object)cs);
+ }
+
+ ///
+ /// Attempt to lookup the original key casing so it can be preserved during greedy updates which would otherwise lose
+ /// the original casing in favor of the casing used in the config source.
+ ///
+ /// The key to find original casing for.
+ /// A string containing the key with original casing from the config section, or the key as passed in if no match
+ /// can be found.
+ public override string TryGetOriginalCase(string requestedKey)
+ {
+ if (!String.IsNullOrWhiteSpace(requestedKey))
+ {
+ var connStr = ConfigSection.ConnectionStrings[requestedKey];
+ if (connStr != null)
+ return connStr.Name;
+ }
+
+ return base.TryGetOriginalCase(requestedKey);
+ }
+ }
+
+ ///
+ /// A class that can be used by s to apply key/value config pairs to
+ /// with special 'tagging' to allow updating both the 'connectionString' attribute as well as the 'providerName' attribute.
+ ///
+ public class ConnectionStringsSectionHandler2 : SectionHandler
+ {
+ private const string connStrNameTag = ":connectionString";
+ private const string providerNameTag = ":providerName";
+
+ private class CSSH2State { public bool UpdateName; public ConnectionStringSettings CS; }
+
+ ///
+ /// Updates an existing connection string attribute in the assigned with a new name and a new value. The old
+ /// connection string setting can be located using the parameter. If an old connection string is not found, a new connection
+ /// string should be inserted.
+ ///
+ /// The updated key name for the connection string. May be post-fixed with attribute tag.
+ /// The updated value for the connection string.
+ /// The old key name for the connection string, or null. May be post-fixed with attribute tag.
+ /// A reference to the old object obtained by , or null.
+ public override void InsertOrUpdate(string newKey, string newValue, string oldKey = null, object oldItem = null)
+ {
+ string tag;
+ (oldKey, tag) = SplitTag(oldKey);
+ (newKey, _) = SplitTag(newKey);
+
+ CSSH2State state = oldItem as CSSH2State;
+ ConnectionStringSettings cs = state?.CS ?? ConfigSection.ConnectionStrings[oldKey] ?? new ConnectionStringSettings();
+
+ // Make sure there are no entries using the old or new name other than this one
+ ConfigSection.ConnectionStrings.Remove(oldKey);
+ ConfigSection.ConnectionStrings.Remove(newKey);
+
+ // Update values and re-add to the collection (no state means 'Greedy' mode where we do want to update)
+ if (state == null || state.UpdateName)
+ cs.Name = newKey;
+ if (tag == providerNameTag)
+ cs.ProviderName = newValue;
+ else
+ cs.ConnectionString = newValue;
+ ConfigSection.ConnectionStrings.Add(cs);
+ }
+
+ ///
+ /// Gets an for iterating over the key/value pairs contained in the assigned . />
+ ///
+ /// An enumerator over tuples where the values of the tuple are the existing name for each connection string, the value of
+ /// the connection string or the value of the provider name, and a reference to the object itself
+ /// which will be returned to us as a reference state object when updating the config record.
+ public override IEnumerable> KeysValuesAndState()
+ {
+ // The ConnectionStrings collection may change on us while we enumerate. :/
+ ConnectionStringSettings[] connStrs = new ConnectionStringSettings[ConfigSection.ConnectionStrings.Count];
+ ConfigSection.ConnectionStrings.CopyTo(connStrs, 0);
+
+ foreach (ConnectionStringSettings cs in connStrs)
+ {
+ // Greedy mode doesn't enumerate here. It just goes direct to 'InsertOrUpdate', which preserves non-tagged
+ // behavior via null-tag awareness.
+ // Strict mode will need us to lookup a non-tagged value in addition to tagged values in order to
+ // remain as compatible as possible with the simple old model.
+ // Token mode is trickier. See step-by-step notes.
+
+ string originalName = cs.Name;
+ string originalCS = cs.ConnectionString;
+
+ // In 'Token' mode, this will replace tokens in 'name' and 'connectionString'.
+ yield return Tuple.Create(originalName, originalCS, (object)new CSSH2State() { UpdateName = true, CS = cs }) ;
+
+ // In 'Token' mode, this will re-replace tokens in 'connectionString' only. Conceptually a no-op, except we
+ // don't know which mode we're in so we can't technically skip this re-replacement. We also can't skip this step because
+ // it is required for 'Strict' mode. (It will also re-lookup tokens in 'name', but we are able to skip replacing those
+ // here, since using _this_ tagged 'name' string might not be faithful to the original non-tagged 'name'.)
+ // Also, re-lookups for tokens should be cached and free, since the tokens inside the 'name' didn't change when tagged.
+ yield return Tuple.Create(originalName + connStrNameTag, originalCS, (object)new CSSH2State() { UpdateName = false, CS = cs });
+
+ // In 'Token' mode, this will replace tokens in 'providerName' only. Same deal with 'name' as the previous step. However,
+ // the tag on the original name is important, as that is the only way we will know to work on 'providerName' instead of 'name'
+ // in 'InsertOrUpdate'.
+ yield return Tuple.Create(originalName + providerNameTag, cs.ProviderName, (object)new CSSH2State() { UpdateName = false, CS = cs });
+ }
+ }
+
+ ///
+ /// Attempt to lookup the original key casing so it can be preserved during greedy updates which would otherwise lose
+ /// the original casing in favor of the casing used in the config source.
+ ///
+ /// The key to find original casing for.
+ /// A string containing the key with original casing from the config section, or the key as passed in if no match
+ /// can be found.
+ public override string TryGetOriginalCase(string requestedKey)
+ {
+ if (!String.IsNullOrWhiteSpace(requestedKey))
+ {
+ var connStr = ConfigSection.ConnectionStrings[requestedKey];
+ if (connStr != null)
+ return connStr.Name;
+ }
+
+ return base.TryGetOriginalCase(requestedKey);
+ }
+
+ private (string, string) SplitTag(string key)
+ {
+ if (key != null)
+ {
+ if (key.EndsWith(connStrNameTag))
+ return (key.Remove(key.Length - connStrNameTag.Length), connStrNameTag);
+ else if (key.EndsWith(providerNameTag))
+ return (key.Remove(key.Length - providerNameTag.Length), providerNameTag);
+ }
+
+ return (key, null);
+ }
+ }
+}
\ No newline at end of file
diff --git a/tracer/test/test-applications/aspnet/Samples.AspNet.MultipleAppsInDomain/CustomConfigBuilder/SectionHandlerSection.cs b/tracer/test/test-applications/aspnet/Samples.AspNet.MultipleAppsInDomain/CustomConfigBuilder/SectionHandlerSection.cs
new file mode 100644
index 000000000000..0df242ce277f
--- /dev/null
+++ b/tracer/test/test-applications/aspnet/Samples.AspNet.MultipleAppsInDomain/CustomConfigBuilder/SectionHandlerSection.cs
@@ -0,0 +1,93 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT license. See the License.txt file in the project root for full license information.
+
+using System;
+using System.Collections.Specialized;
+using System.Configuration;
+using System.Reflection;
+
+namespace Microsoft.Configuration.ConfigurationBuilders
+{
+ ///
+ /// Provides programmatic access to the 'sectionHandlers' config section. This class can't be inherited.
+ ///
+ public sealed class SectionHandlersSection : ConfigurationSection
+ {
+ private static readonly string handlerSectionName = "Microsoft.Configuration.ConfigurationBuilders.SectionHandlers";
+
+ ///
+ /// Gets the collection of s defined for processing config sections with s./>
+ ///
+ [ConfigurationProperty("handlers", IsDefaultCollection = true, Options = ConfigurationPropertyOptions.IsDefaultCollection)]
+ public ProviderSettingsCollection Handlers
+ {
+ get { return (ProviderSettingsCollection)base["handlers"]; }
+ }
+
+ ///
+ /// Used to initialize a default set of section handlers. (For the appSettings and connectionStrings sections.)
+ ///
+ protected override void InitializeDefault()
+ {
+ // This only runs once at the top "parent" level of the config stack. If there is already an
+ // existing parent in the stack to inherit, then this does not get called.
+ base.InitializeDefault();
+ if (Handlers != null)
+ {
+ Handlers.Add(new ProviderSettings("DefaultAppSettingsHandler", "Microsoft.Configuration.ConfigurationBuilders.AppSettingsSectionHandler"));
+ Handlers.Add(new ProviderSettings("DefaultConnectionStringsHandler", "Microsoft.Configuration.ConfigurationBuilders.ConnectionStringsSectionHandler"));
+ }
+ }
+
+ internal static ISectionHandler GetSectionHandler(T configSection) where T : ConfigurationSection
+ {
+ if (configSection == null)
+ return null;
+
+ SectionHandlersSection handlerSection = GetSectionHandlersSection(configSection);
+
+ if (handlerSection != null)
+ {
+ // Look at each handler to see if it works on this section. Reverse order so last match wins.
+ // .IsSubclassOf() requires an exact type match. So SectionHandler won't work.
+ Type sectionHandlerGenericTemplate = typeof(SectionHandler<>);
+ Type sectionHandlerDesiredType = sectionHandlerGenericTemplate.MakeGenericType(configSection.GetType());
+ for (int i = handlerSection.Handlers.Count; i-- > 0;)
+ {
+ Type handlerType = Type.GetType(handlerSection.Handlers[i].Type);
+ if (handlerType != null && handlerType.IsSubclassOf(sectionHandlerDesiredType))
+ {
+ if (Activator.CreateInstance(handlerType) is ISectionHandler handler)
+ {
+ ProviderSettings settings = handlerSection.Handlers[i];
+ NameValueCollection clonedParams = new NameValueCollection(settings.Parameters.Count);
+ foreach (string key in settings.Parameters)
+ clonedParams[key] = settings.Parameters[key];
+
+ MethodInfo init = sectionHandlerDesiredType.GetMethod("Initialize", BindingFlags.NonPublic | BindingFlags.Instance);
+ init.Invoke(handler, new object[] { settings.Name, configSection, clonedParams });
+
+ return handler;
+ }
+ }
+ }
+ }
+
+ throw new Exception($"Error in Configuration: Cannot find ISectionHandler for '{configSection.SectionInformation.Name}' section.");
+ }
+
+ private static SectionHandlersSection GetSectionHandlersSection(ConfigurationSection currentSection)
+ {
+ SectionHandlersSection handlersSection = (currentSection?.CurrentConfiguration?.GetSection(handlerSectionName) as SectionHandlersSection)
+ ?? (ConfigurationManager.GetSection(handlerSectionName) as SectionHandlersSection);
+
+ if (handlersSection == null)
+ {
+ handlersSection = new SectionHandlersSection();
+ handlersSection.InitializeDefault();
+ }
+
+ return handlersSection;
+ }
+ }
+}
\ No newline at end of file
diff --git a/tracer/test/test-applications/aspnet/Samples.AspNet.MultipleAppsInDomain/Global.asax b/tracer/test/test-applications/aspnet/Samples.AspNet.MultipleAppsInDomain/Global.asax
new file mode 100644
index 000000000000..f9688d589561
--- /dev/null
+++ b/tracer/test/test-applications/aspnet/Samples.AspNet.MultipleAppsInDomain/Global.asax
@@ -0,0 +1 @@
+<%@ Application Codebehind="Global.asax.cs" Inherits="Samples.AspNet.MultipleAppsInDomain.MvcApplication" Language="C#" %>
diff --git a/tracer/test/test-applications/aspnet/Samples.AspNet.MultipleAppsInDomain/Global.asax.cs b/tracer/test/test-applications/aspnet/Samples.AspNet.MultipleAppsInDomain/Global.asax.cs
new file mode 100644
index 000000000000..01f512204075
--- /dev/null
+++ b/tracer/test/test-applications/aspnet/Samples.AspNet.MultipleAppsInDomain/Global.asax.cs
@@ -0,0 +1,20 @@
+using System.Web.Mvc;
+using System.Web.Routing;
+
+namespace Samples.AspNet.MultipleAppsInDomain
+{
+ public class MvcApplication : System.Web.HttpApplication
+ {
+ protected void Application_Start()
+ {
+ GlobalFilters.Filters.Add(new HandleErrorAttribute());
+ RouteTable.Routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
+
+ RouteTable.Routes.MapRoute(
+ name: "Default",
+ url: "{controller}/{action}/{id}",
+ defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
+ );
+ }
+ }
+}
diff --git a/tracer/test/test-applications/aspnet/Samples.AspNet.MultipleAppsInDomain/Properties/AssemblyInfo.cs b/tracer/test/test-applications/aspnet/Samples.AspNet.MultipleAppsInDomain/Properties/AssemblyInfo.cs
new file mode 100644
index 000000000000..5199db5c4bf0
--- /dev/null
+++ b/tracer/test/test-applications/aspnet/Samples.AspNet.MultipleAppsInDomain/Properties/AssemblyInfo.cs
@@ -0,0 +1,35 @@
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle("Samples.AspNet.MultipleAppsInDomain")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("Samples.AspNet.MultipleAppsInDomain")]
+[assembly: AssemblyCopyright("Copyright © 2024")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// Setting ComVisible to false makes the types in this assembly not visible
+// to COM components. If you need to access a type in this assembly from
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
+
+// The following GUID is for the ID of the typelib if this project is exposed to COM
+[assembly: Guid("5c947d5f-aa18-4f76-82cd-2decb7f92b19")]
+
+// Version information for an assembly consists of the following four values:
+//
+// Major Version
+// Minor Version
+// Build Number
+// Revision
+//
+// You can specify all the values or you can default the Revision and Build Numbers
+// by using the '*' as shown below:
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]
diff --git a/tracer/test/test-applications/aspnet/Samples.AspNet.MultipleAppsInDomain/Properties/PublishProfiles/FolderProfile.pubxml b/tracer/test/test-applications/aspnet/Samples.AspNet.MultipleAppsInDomain/Properties/PublishProfiles/FolderProfile.pubxml
new file mode 100644
index 000000000000..59dc41bc5209
--- /dev/null
+++ b/tracer/test/test-applications/aspnet/Samples.AspNet.MultipleAppsInDomain/Properties/PublishProfiles/FolderProfile.pubxml
@@ -0,0 +1,17 @@
+
+
+
+
+ false
+ false
+ true
+ Release
+ Any CPU
+ FileSystem
+ bin\app.publish\
+ FileSystem
+ <_TargetId>Folder
+
+
\ No newline at end of file
diff --git a/tracer/test/test-applications/aspnet/Samples.AspNet.MultipleAppsInDomain/Samples.AspNet.MultipleAppsInDomain.csproj b/tracer/test/test-applications/aspnet/Samples.AspNet.MultipleAppsInDomain/Samples.AspNet.MultipleAppsInDomain.csproj
new file mode 100644
index 000000000000..01e5dafde7ee
--- /dev/null
+++ b/tracer/test/test-applications/aspnet/Samples.AspNet.MultipleAppsInDomain/Samples.AspNet.MultipleAppsInDomain.csproj
@@ -0,0 +1,184 @@
+
+
+
+
+
+ Debug
+ AnyCPU
+
+
+ 2.0
+ {A82EB6F8-D8D0-4763-B252-08CA3F39D153}
+ {349c5851-65df-11da-9384-00065b846f21};{fae04ec0-301f-11d3-bf4b-00c04f79efbc}
+ Library
+ Properties
+ Samples.AspNet.MultipleAppsInDomain
+ Samples.AspNet.MultipleAppsInDomain
+ v4.7.2
+ false
+ true
+
+ 44344
+
+
+
+
+
+
+ latest
+
+
+ true
+ full
+ false
+ bin\
+ DEBUG;TRACE
+ prompt
+ 4
+
+
+ true
+ pdbonly
+ true
+ bin\
+ TRACE
+ prompt
+ 4
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ True
+ ..\..\..\..\..\packages\Microsoft.Web.Infrastructure.2.0.1\lib\net40\Microsoft.Web.Infrastructure.dll
+
+
+
+
+
+
+ True
+ ..\..\..\..\..\packages\Microsoft.AspNet.WebPages.3.2.9\lib\net45\System.Web.Helpers.dll
+
+
+ True
+ ..\..\..\..\..\packages\Microsoft.AspNet.Mvc.5.2.9\lib\net45\System.Web.Mvc.dll
+
+
+ True
+ ..\..\..\..\..\packages\Microsoft.AspNet.Razor.3.2.9\lib\net45\System.Web.Razor.dll
+
+
+ True
+ ..\..\..\..\..\packages\Microsoft.AspNet.WebPages.3.2.9\lib\net45\System.Web.WebPages.dll
+
+
+ True
+ ..\..\..\..\..\packages\Microsoft.AspNet.WebPages.3.2.9\lib\net45\System.Web.WebPages.Deployment.dll
+
+
+ True
+ ..\..\..\..\..\packages\Microsoft.AspNet.WebPages.3.2.9\lib\net45\System.Web.WebPages.Razor.dll
+
+
+ ..\..\..\..\..\packages\Newtonsoft.Json.12.0.2\lib\net45\Newtonsoft.Json.dll
+
+
+ True
+ ..\..\..\..\..\packages\Antlr.3.5.0.2\lib\Antlr3.Runtime.dll
+
+
+
+
+ ..\..\..\..\..\packages\Microsoft.CodeDom.Providers.DotNetCompilerPlatform.2.0.1\lib\net45\Microsoft.CodeDom.Providers.DotNetCompilerPlatform.dll
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Global.asax
+
+
+
+
+
+
+
+
+ Web.config
+
+
+ Web.config
+
+
+
+
+
+
+
+
+ 10.0
+ $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)
+
+
+
+
+
+
+
+
+
+
+
+ True
+ True
+ 60592
+ /
+ https://localhost:44344/
+ False
+ False
+
+
+ False
+
+
+
+
+
+
+ This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.
+
+
+
+
+
\ No newline at end of file
diff --git a/tracer/test/test-applications/aspnet/Samples.AspNet.MultipleAppsInDomain/Views/Web.config b/tracer/test/test-applications/aspnet/Samples.AspNet.MultipleAppsInDomain/Views/Web.config
new file mode 100644
index 000000000000..1b3772a5fc34
--- /dev/null
+++ b/tracer/test/test-applications/aspnet/Samples.AspNet.MultipleAppsInDomain/Views/Web.config
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tracer/test/test-applications/aspnet/Samples.AspNet.MultipleAppsInDomain/Views/_ViewStart.cshtml b/tracer/test/test-applications/aspnet/Samples.AspNet.MultipleAppsInDomain/Views/_ViewStart.cshtml
new file mode 100644
index 000000000000..be2a4f37a62e
--- /dev/null
+++ b/tracer/test/test-applications/aspnet/Samples.AspNet.MultipleAppsInDomain/Views/_ViewStart.cshtml
@@ -0,0 +1,3 @@
+@{
+ Layout = null;
+}
diff --git a/tracer/test/test-applications/aspnet/Samples.AspNet.MultipleAppsInDomain/Web.Debug.config b/tracer/test/test-applications/aspnet/Samples.AspNet.MultipleAppsInDomain/Web.Debug.config
new file mode 100644
index 000000000000..d7712aaf1783
--- /dev/null
+++ b/tracer/test/test-applications/aspnet/Samples.AspNet.MultipleAppsInDomain/Web.Debug.config
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/tracer/test/test-applications/aspnet/Samples.AspNet.MultipleAppsInDomain/Web.Release.config b/tracer/test/test-applications/aspnet/Samples.AspNet.MultipleAppsInDomain/Web.Release.config
new file mode 100644
index 000000000000..28a4d5fcc327
--- /dev/null
+++ b/tracer/test/test-applications/aspnet/Samples.AspNet.MultipleAppsInDomain/Web.Release.config
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tracer/test/test-applications/aspnet/Samples.AspNet.MultipleAppsInDomain/Web.config b/tracer/test/test-applications/aspnet/Samples.AspNet.MultipleAppsInDomain/Web.config
new file mode 100644
index 000000000000..ae3dc03c57a6
--- /dev/null
+++ b/tracer/test/test-applications/aspnet/Samples.AspNet.MultipleAppsInDomain/Web.config
@@ -0,0 +1,68 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tracer/test/test-applications/aspnet/Samples.AspNet.MultipleAppsInDomain/favicon.ico b/tracer/test/test-applications/aspnet/Samples.AspNet.MultipleAppsInDomain/favicon.ico
new file mode 100644
index 000000000000..a3a799985c43
Binary files /dev/null and b/tracer/test/test-applications/aspnet/Samples.AspNet.MultipleAppsInDomain/favicon.ico differ
diff --git a/tracer/test/test-applications/aspnet/Samples.AspNet.MultipleAppsInDomain/packages.config b/tracer/test/test-applications/aspnet/Samples.AspNet.MultipleAppsInDomain/packages.config
new file mode 100644
index 000000000000..0f4aa45bf0a8
--- /dev/null
+++ b/tracer/test/test-applications/aspnet/Samples.AspNet.MultipleAppsInDomain/packages.config
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file