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