From 8dc0b49ea727cb8b10e265adedd90d75cd7dc6f8 Mon Sep 17 00:00:00 2001 From: "dotnet-maestro[bot]" <42748379+dotnet-maestro[bot]@users.noreply.github.com> Date: Tue, 14 Apr 2020 19:58:09 +0000 Subject: [PATCH 1/5] [release/3.1] Update dependencies from dotnet/arcade (#20815) * Update dependencies from https://github.com/dotnet/arcade build 20200413.4 - Microsoft.DotNet.Arcade.Sdk: 1.0.0-beta.20113.5 -> 1.0.0-beta.20213.4 - Microsoft.DotNet.GenAPI: 1.0.0-beta.20113.5 -> 1.0.0-beta.20213.4 - Microsoft.DotNet.Helix.Sdk: 2.0.0-beta.20113.5 -> 2.0.0-beta.20213.4 Dependency coherency updates - Microsoft.AspNetCore.Analyzer.Testing: 3.1.4-servicing.20181.5 -> 3.1.4-servicing.20202.2 (parent: Microsoft.EntityFrameworkCore) - Microsoft.AspNetCore.BenchmarkRunner.Sources: 3.1.4-servicing.20181.5 -> 3.1.4-servicing.20202.2 (parent: Microsoft.EntityFrameworkCore) - Microsoft.Extensions.ActivatorUtilities.Sources: 3.1.4-servicing.20181.5 -> 3.1.4-servicing.20202.2 (parent: Microsoft.EntityFrameworkCore) - Microsoft.Extensions.CommandLineUtils.Sources: 3.1.4-servicing.20181.5 -> 3.1.4-servicing.20202.2 (parent: Microsoft.EntityFrameworkCore) - Microsoft.Extensions.HashCodeCombiner.Sources: 3.1.4-servicing.20181.5 -> 3.1.4-servicing.20202.2 (parent: Microsoft.EntityFrameworkCore) - Microsoft.Extensions.HostFactoryResolver.Sources: 3.1.4-servicing.20181.5 -> 3.1.4-servicing.20202.2 (parent: Microsoft.EntityFrameworkCore) - Microsoft.Extensions.Logging.Testing: 3.1.4-servicing.20181.5 -> 3.1.4-servicing.20202.2 (parent: Microsoft.EntityFrameworkCore) - Microsoft.Extensions.ParameterDefaultValue.Sources: 3.1.4-servicing.20181.5 -> 3.1.4-servicing.20202.2 (parent: Microsoft.EntityFrameworkCore) - Microsoft.Extensions.TypeNameHelper.Sources: 3.1.4-servicing.20181.5 -> 3.1.4-servicing.20202.2 (parent: Microsoft.EntityFrameworkCore) - Microsoft.Extensions.ValueStopwatch.Sources: 3.1.4-servicing.20181.5 -> 3.1.4-servicing.20202.2 (parent: Microsoft.EntityFrameworkCore) - Microsoft.NETCore.App.Internal: 3.1.4-servicing.20181.2 -> 3.1.4-servicing.20202.1 (parent: Microsoft.Extensions.Logging) - Internal.AspNetCore.Analyzers: 3.1.4-servicing.20181.5 -> 3.1.4-servicing.20202.2 (parent: Microsoft.EntityFrameworkCore) - Microsoft.AspNetCore.Testing: 3.1.4-servicing.20181.5 -> 3.1.4-servicing.20202.2 (parent: Microsoft.EntityFrameworkCore) * Fixup nuget.config * Only update from Arcade Co-authored-by: dotnet-maestro[bot] Co-authored-by: William Godbe --- eng/Version.Details.xml | 14 +++++++------- eng/Versions.props | 4 ++-- eng/common/tools.sh | 9 ++++++++- global.json | 4 ++-- 4 files changed, 19 insertions(+), 12 deletions(-) diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index ea493936af..94b3b13f14 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -413,17 +413,17 @@ https://github.com/dotnet/extensions cf044102f01a3402a680fa58cabea8a9ca53aa3d - + https://github.com/dotnet/arcade - 15f00efd583eab4372b2e9ca25bd80ace5b119ad + 1a55276ab9d16792cec595ba870df39a9d97d5ca - + https://github.com/dotnet/arcade - 15f00efd583eab4372b2e9ca25bd80ace5b119ad + 1a55276ab9d16792cec595ba870df39a9d97d5ca - + https://github.com/dotnet/arcade - 15f00efd583eab4372b2e9ca25bd80ace5b119ad + 1a55276ab9d16792cec595ba870df39a9d97d5ca https://github.com/dotnet/extensions @@ -434,4 +434,4 @@ d8180a5ecafb92adcfbfe8cf9199eb23be1a1ccf - + \ No newline at end of file diff --git a/eng/Versions.props b/eng/Versions.props index e10323db1b..082dc20102 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -62,7 +62,7 @@ --> - 1.0.0-beta.20113.5 + 1.0.0-beta.20213.4 3.4.1-beta4-20127-10 @@ -271,4 +271,4 @@ https://dotnetcli.blob.core.windows.net/dotnet/ https://dotnetclimsrc.blob.core.windows.net/dotnet/ - + \ No newline at end of file diff --git a/eng/common/tools.sh b/eng/common/tools.sh index 94965a8fd2..acbb0c5b3f 100755 --- a/eng/common/tools.sh +++ b/eng/common/tools.sh @@ -210,7 +210,14 @@ function InstallDotNet { local runtimeSourceFeedKey='' if [[ -n "${7:-}" ]]; then - decodedFeedKey=`echo $7 | base64 --decode` + # The 'base64' binary on alpine uses '-d' and doesn't support '--decode' + # '-d'. To work around this, do a simple detection and switch the parameter + # accordingly. + decodeArg="--decode" + if base64 --help 2>&1 | grep -q "BusyBox"; then + decodeArg="-d" + fi + decodedFeedKey=`echo $7 | base64 $decodeArg` runtimeSourceFeedKey="--feed-credential $decodedFeedKey" fi diff --git a/global.json b/global.json index 67ec79203b..974708a611 100644 --- a/global.json +++ b/global.json @@ -25,7 +25,7 @@ }, "msbuild-sdks": { "Yarn.MSBuild": "1.15.2", - "Microsoft.DotNet.Arcade.Sdk": "1.0.0-beta.20113.5", - "Microsoft.DotNet.Helix.Sdk": "2.0.0-beta.20113.5" + "Microsoft.DotNet.Arcade.Sdk": "1.0.0-beta.20213.4", + "Microsoft.DotNet.Helix.Sdk": "2.0.0-beta.20213.4" } } From 01429973eed3fc2c6cd3d799c2bae0d7af50b854 Mon Sep 17 00:00:00 2001 From: William Godbe Date: Tue, 14 Apr 2020 12:59:06 -0700 Subject: [PATCH 2/5] [release/3.1] Pass access token as query string when running SignalR in the browser (#20466) * Pass access token as query string when running SignalR in the browser * WEBASSEMBLY -> BROWSER --- .../src/HttpConnection.Log.cs | 8 ++++++++ .../src/HttpConnection.cs | 16 ++++++++++++++++ .../src/Internal/Utils.cs | 6 ++++++ .../src/Internal/WebSocketsTransport.cs | 16 +++++++++++++++- 4 files changed, 45 insertions(+), 1 deletion(-) diff --git a/src/SignalR/clients/csharp/Http.Connections.Client/src/HttpConnection.Log.cs b/src/SignalR/clients/csharp/Http.Connections.Client/src/HttpConnection.Log.cs index 0836ea8708..74b3de2e71 100644 --- a/src/SignalR/clients/csharp/Http.Connections.Client/src/HttpConnection.Log.cs +++ b/src/SignalR/clients/csharp/Http.Connections.Client/src/HttpConnection.Log.cs @@ -66,6 +66,9 @@ namespace Microsoft.AspNetCore.Http.Connections.Client private static readonly Action _transportStarted = LoggerMessage.Define(LogLevel.Debug, new EventId(18, "TransportStarted"), "Transport '{Transport}' started."); + private static readonly Action _serverSentEventsNotSupportedByBrowser = + LoggerMessage.Define(LogLevel.Debug, new EventId(19, "ServerSentEventsNotSupportedByBrowser"), "Skipping ServerSentEvents because they are not supported by the browser."); + public static void Starting(ILogger logger) { _starting(logger, null); @@ -167,6 +170,11 @@ namespace Microsoft.AspNetCore.Http.Connections.Client { _transportStarted(logger, transportType, null); } + + public static void ServerSentEventsNotSupportedByBrowser(ILogger logger) + { + _serverSentEventsNotSupportedByBrowser(logger, null); + } } } } diff --git a/src/SignalR/clients/csharp/Http.Connections.Client/src/HttpConnection.cs b/src/SignalR/clients/csharp/Http.Connections.Client/src/HttpConnection.cs index 4375d11285..4089360c01 100644 --- a/src/SignalR/clients/csharp/Http.Connections.Client/src/HttpConnection.cs +++ b/src/SignalR/clients/csharp/Http.Connections.Client/src/HttpConnection.cs @@ -37,6 +37,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Client private bool _started; private bool _disposed; private bool _hasInherentKeepAlive; + private bool _isRunningInBrowser; private readonly HttpClient _httpClient; private readonly HttpConnectionOptions _httpConnectionOptions; @@ -150,6 +151,14 @@ namespace Microsoft.AspNetCore.Http.Connections.Client _httpClient = CreateHttpClient(); } + _isRunningInBrowser = Utils.IsRunningInBrowser(); + + + if (httpConnectionOptions.Transports == HttpTransportType.ServerSentEvents && _isRunningInBrowser) + { + throw new ArgumentException("ServerSentEvents can not be the only transport specified when running in the browser.", nameof(httpConnectionOptions)); + } + _transportFactory = new DefaultTransportFactory(httpConnectionOptions.Transports, _loggerFactory, _httpClient, httpConnectionOptions, GetAccessTokenAsync); _logScope = new ConnectionLogScope(); @@ -365,6 +374,13 @@ namespace Microsoft.AspNetCore.Http.Connections.Client continue; } + if (transportType == HttpTransportType.ServerSentEvents && _isRunningInBrowser) + { + Log.ServerSentEventsNotSupportedByBrowser(_logger); + transportExceptions.Add(new TransportFailedException("ServerSentEvents", "The transport is not supported in the browser.")); + continue; + } + try { if ((transportType & _httpConnectionOptions.Transports) == 0) diff --git a/src/SignalR/clients/csharp/Http.Connections.Client/src/Internal/Utils.cs b/src/SignalR/clients/csharp/Http.Connections.Client/src/Internal/Utils.cs index f92f89a3a0..8071a054a3 100644 --- a/src/SignalR/clients/csharp/Http.Connections.Client/src/Internal/Utils.cs +++ b/src/SignalR/clients/csharp/Http.Connections.Client/src/Internal/Utils.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Runtime.InteropServices; namespace Microsoft.AspNetCore.Http.Connections.Client.Internal { @@ -41,5 +42,10 @@ namespace Microsoft.AspNetCore.Http.Connections.Client.Internal builder.Query = newQueryString; return builder.Uri; } + + internal static bool IsRunningInBrowser() + { + return RuntimeInformation.IsOSPlatform(OSPlatform.Create("BROWSER")); + } } } diff --git a/src/SignalR/clients/csharp/Http.Connections.Client/src/Internal/WebSocketsTransport.cs b/src/SignalR/clients/csharp/Http.Connections.Client/src/Internal/WebSocketsTransport.cs index 905a965841..8f201f6dbf 100644 --- a/src/SignalR/clients/csharp/Http.Connections.Client/src/Internal/WebSocketsTransport.cs +++ b/src/SignalR/clients/csharp/Http.Connections.Client/src/Internal/WebSocketsTransport.cs @@ -6,6 +6,7 @@ using System.Diagnostics; using System.IO.Pipelines; using System.Net.WebSockets; using System.Runtime.InteropServices; +using System.Text.Encodings.Web; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Connections; @@ -23,6 +24,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Client.Internal private readonly ILogger _logger; private readonly TimeSpan _closeTimeout; private volatile bool _aborted; + private bool _isRunningInBrowser; private IDuplexPipe _transport; @@ -87,6 +89,8 @@ namespace Microsoft.AspNetCore.Http.Connections.Client.Internal // Ignore the HttpConnectionOptions access token provider. We were given an updated delegate from the HttpConnection. _accessTokenProvider = accessTokenProvider; + + _isRunningInBrowser = Utils.IsRunningInBrowser(); } public async Task StartAsync(Uri url, TransferFormat transferFormat, CancellationToken cancellationToken = default) @@ -113,7 +117,17 @@ namespace Microsoft.AspNetCore.Http.Connections.Client.Internal var accessToken = await _accessTokenProvider(); if (!string.IsNullOrEmpty(accessToken)) { - _webSocket.Options.SetRequestHeader("Authorization", $"Bearer {accessToken}"); + // We can't use request headers in the browser, so instead append the token as a query string in that case + if (_isRunningInBrowser) + { + var accessTokenEncoded = UrlEncoder.Default.Encode(accessToken); + accessTokenEncoded = "access_token=" + accessTokenEncoded; + resolvedUrl = Utils.AppendQueryString(resolvedUrl, accessTokenEncoded); + } + else + { + _webSocket.Options.SetRequestHeader("Authorization", $"Bearer {accessToken}"); + } } } From 260de2c10ba21f0693127d54c09156b4a81ee0cf Mon Sep 17 00:00:00 2001 From: BrennanConroy Date: Wed, 8 Apr 2020 14:52:13 -0700 Subject: [PATCH 3/5] Add BuildNumber to SiteExtension build --- ...Microsoft.AspNetCore.AzureAppServices.SiteExtension.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/SiteExtensions/LoggingAggregate/src/Microsoft.AspNetCore.AzureAppServices.SiteExtension/Microsoft.AspNetCore.AzureAppServices.SiteExtension.csproj b/src/SiteExtensions/LoggingAggregate/src/Microsoft.AspNetCore.AzureAppServices.SiteExtension/Microsoft.AspNetCore.AzureAppServices.SiteExtension.csproj index 18a0870512..34e276e2dd 100644 --- a/src/SiteExtensions/LoggingAggregate/src/Microsoft.AspNetCore.AzureAppServices.SiteExtension/Microsoft.AspNetCore.AzureAppServices.SiteExtension.csproj +++ b/src/SiteExtensions/LoggingAggregate/src/Microsoft.AspNetCore.AzureAppServices.SiteExtension/Microsoft.AspNetCore.AzureAppServices.SiteExtension.csproj @@ -27,8 +27,8 @@ - - + + From 5353ce95094a3e36d582c2cc6c662e83965ac83b Mon Sep 17 00:00:00 2001 From: Brennan Date: Wed, 15 Apr 2020 12:26:21 -0700 Subject: [PATCH 4/5] Use SemVer1 for SiteExtension (#20839) --- ...Microsoft.AspNetCore.AzureAppServices.SiteExtension.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/SiteExtensions/LoggingAggregate/src/Microsoft.AspNetCore.AzureAppServices.SiteExtension/Microsoft.AspNetCore.AzureAppServices.SiteExtension.csproj b/src/SiteExtensions/LoggingAggregate/src/Microsoft.AspNetCore.AzureAppServices.SiteExtension/Microsoft.AspNetCore.AzureAppServices.SiteExtension.csproj index 34e276e2dd..cd00643432 100644 --- a/src/SiteExtensions/LoggingAggregate/src/Microsoft.AspNetCore.AzureAppServices.SiteExtension/Microsoft.AspNetCore.AzureAppServices.SiteExtension.csproj +++ b/src/SiteExtensions/LoggingAggregate/src/Microsoft.AspNetCore.AzureAppServices.SiteExtension/Microsoft.AspNetCore.AzureAppServices.SiteExtension.csproj @@ -27,8 +27,8 @@ - - + + From 1b99352ec7e0ee0b743d6709b063b54b9330c8e1 Mon Sep 17 00:00:00 2001 From: David Fowler Date: Wed, 15 Apr 2020 12:41:59 -0700 Subject: [PATCH 5/5] Don't re-use DefaultHttpContext if IHttpContextAccessor is in use (#15049) (#20844) * Don't re-use DefaultHttpContext if IHttpContextAccessor is in use - Consumers may still get null or an ODE but will never end up with data from a different request. - Make sure an ODE is thrown from all properties on HttpContext after the request is over. --- .../src/Http/DefaultHttpContextFactory.cs | 2 + .../src/Internal/HostingApplication.cs | 8 + .../HostingApplicationDiagnosticsTests.cs | 532 ++++++++++++++++ .../Hosting/test/HostingApplicationTests.cs | 574 +++--------------- src/Http/Http/src/DefaultHttpContext.cs | 4 +- 5 files changed, 633 insertions(+), 487 deletions(-) create mode 100644 src/Hosting/Hosting/test/HostingApplicationDiagnosticsTests.cs diff --git a/src/Hosting/Hosting/src/Http/DefaultHttpContextFactory.cs b/src/Hosting/Hosting/src/Http/DefaultHttpContextFactory.cs index 08ecd9b057..2637a8b3a6 100644 --- a/src/Hosting/Hosting/src/Http/DefaultHttpContextFactory.cs +++ b/src/Hosting/Hosting/src/Http/DefaultHttpContextFactory.cs @@ -26,6 +26,8 @@ namespace Microsoft.AspNetCore.Http _serviceScopeFactory = serviceProvider.GetRequiredService(); } + internal IHttpContextAccessor HttpContextAccessor => _httpContextAccessor; + public HttpContext Create(IFeatureCollection featureCollection) { if (featureCollection is null) diff --git a/src/Hosting/Hosting/src/Internal/HostingApplication.cs b/src/Hosting/Hosting/src/Internal/HostingApplication.cs index c64426c0db..979469ace8 100644 --- a/src/Hosting/Hosting/src/Internal/HostingApplication.cs +++ b/src/Hosting/Hosting/src/Internal/HostingApplication.cs @@ -96,6 +96,14 @@ namespace Microsoft.AspNetCore.Hosting if (_defaultHttpContextFactory != null) { _defaultHttpContextFactory.Dispose((DefaultHttpContext)httpContext); + + if (_defaultHttpContextFactory.HttpContextAccessor != null) + { + // Clear the HttpContext if the accessor was used. It's likely that the lifetime extends + // past the end of the http request and we want to avoid changing the reference from under + // consumers. + context.HttpContext = null; + } } else { diff --git a/src/Hosting/Hosting/test/HostingApplicationDiagnosticsTests.cs b/src/Hosting/Hosting/test/HostingApplicationDiagnosticsTests.cs new file mode 100644 index 0000000000..385424fa67 --- /dev/null +++ b/src/Hosting/Hosting/test/HostingApplicationDiagnosticsTests.cs @@ -0,0 +1,532 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Extensions.Logging; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Hosting.Tests +{ + public class HostingApplicationDiagnosticsTests + { + [Fact] + public void DisposeContextDoesNotThrowWhenContextScopeIsNull() + { + // Arrange + var hostingApplication = CreateApplication(out var features); + var context = hostingApplication.CreateContext(features); + + // Act/Assert + hostingApplication.DisposeContext(context, null); + } + + [Fact] + public void CreateContextWithDisabledLoggerDoesNotCreateActivity() + { + // Arrange + var hostingApplication = CreateApplication(out var features); + + // Act + hostingApplication.CreateContext(features); + + Assert.Null(Activity.Current); + } + + [Fact] + public void CreateContextWithEnabledLoggerCreatesActivityAndSetsActivityInScope() + { + // Arrange + var logger = new LoggerWithScopes(isEnabled: true); + var hostingApplication = CreateApplication(out var features, logger: logger); + + // Act + var context = hostingApplication.CreateContext(features); + + Assert.Single(logger.Scopes); + var pairs = ((IReadOnlyList>)logger.Scopes[0]).ToDictionary(p => p.Key, p => p.Value); + Assert.Equal(Activity.Current.Id, pairs["SpanId"].ToString()); + Assert.Equal(Activity.Current.RootId, pairs["TraceId"].ToString()); + Assert.Equal(string.Empty, pairs["ParentId"]?.ToString()); + } + + [Fact] + public void CreateContextWithEnabledLoggerAndRequestIdCreatesActivityAndSetsActivityInScope() + { + // Arrange + + // Generate an id we can use for the request id header (in the correct format) + var activity = new Activity("IncomingRequest"); + activity.Start(); + var id = activity.Id; + activity.Stop(); + + var logger = new LoggerWithScopes(isEnabled: true); + var hostingApplication = CreateApplication(out var features, logger: logger, configure: context => + { + context.Request.Headers["Request-Id"] = id; + }); + + // Act + var context = hostingApplication.CreateContext(features); + + Assert.Single(logger.Scopes); + var pairs = ((IReadOnlyList>)logger.Scopes[0]).ToDictionary(p => p.Key, p => p.Value); + Assert.Equal(Activity.Current.Id, pairs["SpanId"].ToString()); + Assert.Equal(Activity.Current.RootId, pairs["TraceId"].ToString()); + Assert.Equal(id, pairs["ParentId"].ToString()); + } + + [Fact] + public void ActivityStopDoesNotFireIfNoListenerAttachedForStart() + { + // Arrange + var diagnosticListener = new DiagnosticListener("DummySource"); + var logger = new LoggerWithScopes(isEnabled: true); + var hostingApplication = CreateApplication(out var features, diagnosticListener: diagnosticListener, logger: logger); + var startFired = false; + var stopFired = false; + + diagnosticListener.Subscribe(new CallbackDiagnosticListener(pair => + { + // This should not fire + if (pair.Key == "Microsoft.AspNetCore.Hosting.HttpRequestIn.Start") + { + startFired = true; + } + + // This should not fire + if (pair.Key == "Microsoft.AspNetCore.Hosting.HttpRequestIn.Stop") + { + stopFired = true; + } + }), + (s, o, arg3) => + { + // The events are off + return false; + }); + + + // Act + var context = hostingApplication.CreateContext(features); + + hostingApplication.DisposeContext(context, exception: null); + + Assert.False(startFired); + Assert.False(stopFired); + Assert.Null(Activity.Current); + } + + [Fact] + public void ActivityIsNotCreatedWhenIsEnabledForActivityIsFalse() + { + var diagnosticListener = new DiagnosticListener("DummySource"); + var hostingApplication = CreateApplication(out var features, diagnosticListener: diagnosticListener); + + bool eventsFired = false; + bool isEnabledActivityFired = false; + bool isEnabledStartFired = false; + + diagnosticListener.Subscribe(new CallbackDiagnosticListener(pair => + { + eventsFired |= pair.Key.StartsWith("Microsoft.AspNetCore.Hosting.HttpRequestIn"); + }), (s, o, arg3) => + { + if (s == "Microsoft.AspNetCore.Hosting.HttpRequestIn") + { + Assert.IsAssignableFrom(o); + isEnabledActivityFired = true; + } + if (s == "Microsoft.AspNetCore.Hosting.HttpRequestIn.Start") + { + isEnabledStartFired = true; + } + return false; + }); + + hostingApplication.CreateContext(features); + Assert.Null(Activity.Current); + Assert.True(isEnabledActivityFired); + Assert.False(isEnabledStartFired); + Assert.False(eventsFired); + } + + [Fact] + public void ActivityIsCreatedButNotLoggedWhenIsEnabledForActivityStartIsFalse() + { + var diagnosticListener = new DiagnosticListener("DummySource"); + var hostingApplication = CreateApplication(out var features, diagnosticListener: diagnosticListener); + + bool eventsFired = false; + bool isEnabledStartFired = false; + bool isEnabledActivityFired = false; + + diagnosticListener.Subscribe(new CallbackDiagnosticListener(pair => + { + eventsFired |= pair.Key.StartsWith("Microsoft.AspNetCore.Hosting.HttpRequestIn"); + }), (s, o, arg3) => + { + if (s == "Microsoft.AspNetCore.Hosting.HttpRequestIn") + { + Assert.IsAssignableFrom(o); + isEnabledActivityFired = true; + return true; + } + + if (s == "Microsoft.AspNetCore.Hosting.HttpRequestIn.Start") + { + isEnabledStartFired = true; + return false; + } + return true; + }); + + hostingApplication.CreateContext(features); + Assert.NotNull(Activity.Current); + Assert.True(isEnabledActivityFired); + Assert.True(isEnabledStartFired); + Assert.False(eventsFired); + } + + [Fact] + public void ActivityIsCreatedAndLogged() + { + var diagnosticListener = new DiagnosticListener("DummySource"); + var hostingApplication = CreateApplication(out var features, diagnosticListener: diagnosticListener); + + bool startCalled = false; + + diagnosticListener.Subscribe(new CallbackDiagnosticListener(pair => + { + if (pair.Key == "Microsoft.AspNetCore.Hosting.HttpRequestIn.Start") + { + startCalled = true; + Assert.NotNull(pair.Value); + Assert.NotNull(Activity.Current); + Assert.Equal("Microsoft.AspNetCore.Hosting.HttpRequestIn", Activity.Current.OperationName); + AssertProperty(pair.Value, "HttpContext"); + } + })); + + hostingApplication.CreateContext(features); + Assert.NotNull(Activity.Current); + Assert.True(startCalled); + } + + [Fact] + public void ActivityIsStoppedDuringStopCall() + { + var diagnosticListener = new DiagnosticListener("DummySource"); + var hostingApplication = CreateApplication(out var features, diagnosticListener: diagnosticListener); + + bool endCalled = false; + diagnosticListener.Subscribe(new CallbackDiagnosticListener(pair => + { + if (pair.Key == "Microsoft.AspNetCore.Hosting.HttpRequestIn.Stop") + { + endCalled = true; + + Assert.NotNull(Activity.Current); + Assert.True(Activity.Current.Duration > TimeSpan.Zero); + Assert.Equal("Microsoft.AspNetCore.Hosting.HttpRequestIn", Activity.Current.OperationName); + AssertProperty(pair.Value, "HttpContext"); + } + })); + + var context = hostingApplication.CreateContext(features); + hostingApplication.DisposeContext(context, null); + Assert.True(endCalled); + } + + [Fact] + public void ActivityIsStoppedDuringUnhandledExceptionCall() + { + var diagnosticListener = new DiagnosticListener("DummySource"); + var hostingApplication = CreateApplication(out var features, diagnosticListener: diagnosticListener); + + bool endCalled = false; + diagnosticListener.Subscribe(new CallbackDiagnosticListener(pair => + { + if (pair.Key == "Microsoft.AspNetCore.Hosting.HttpRequestIn.Stop") + { + endCalled = true; + Assert.NotNull(Activity.Current); + Assert.True(Activity.Current.Duration > TimeSpan.Zero); + Assert.Equal("Microsoft.AspNetCore.Hosting.HttpRequestIn", Activity.Current.OperationName); + AssertProperty(pair.Value, "HttpContext"); + } + })); + + var context = hostingApplication.CreateContext(features); + hostingApplication.DisposeContext(context, new Exception()); + Assert.True(endCalled); + } + + [Fact] + public void ActivityIsAvailableDuringUnhandledExceptionCall() + { + var diagnosticListener = new DiagnosticListener("DummySource"); + var hostingApplication = CreateApplication(out var features, diagnosticListener: diagnosticListener); + + bool endCalled = false; + diagnosticListener.Subscribe(new CallbackDiagnosticListener(pair => + { + if (pair.Key == "Microsoft.AspNetCore.Hosting.UnhandledException") + { + endCalled = true; + Assert.NotNull(Activity.Current); + Assert.Equal("Microsoft.AspNetCore.Hosting.HttpRequestIn", Activity.Current.OperationName); + } + })); + + var context = hostingApplication.CreateContext(features); + hostingApplication.DisposeContext(context, new Exception()); + Assert.True(endCalled); + } + + [Fact] + public void ActivityIsAvailibleDuringRequest() + { + var diagnosticListener = new DiagnosticListener("DummySource"); + var hostingApplication = CreateApplication(out var features, diagnosticListener: diagnosticListener); + + diagnosticListener.Subscribe(new CallbackDiagnosticListener(pair => { }), + s => + { + if (s.StartsWith("Microsoft.AspNetCore.Hosting.HttpRequestIn")) + { + return true; + } + return false; + }); + + hostingApplication.CreateContext(features); + + Assert.NotNull(Activity.Current); + Assert.Equal("Microsoft.AspNetCore.Hosting.HttpRequestIn", Activity.Current.OperationName); + } + + [Fact] + public void ActivityParentIdAndBaggeReadFromHeaders() + { + var diagnosticListener = new DiagnosticListener("DummySource"); + var hostingApplication = CreateApplication(out var features, diagnosticListener: diagnosticListener); + + diagnosticListener.Subscribe(new CallbackDiagnosticListener(pair => { }), + s => + { + if (s.StartsWith("Microsoft.AspNetCore.Hosting.HttpRequestIn")) + { + return true; + } + return false; + }); + + features.Set(new HttpRequestFeature() + { + Headers = new HeaderDictionary() + { + {"Request-Id", "ParentId1"}, + {"Correlation-Context", "Key1=value1, Key2=value2"} + } + }); + hostingApplication.CreateContext(features); + Assert.Equal("Microsoft.AspNetCore.Hosting.HttpRequestIn", Activity.Current.OperationName); + Assert.Equal("ParentId1", Activity.Current.ParentId); + Assert.Contains(Activity.Current.Baggage, pair => pair.Key == "Key1" && pair.Value == "value1"); + Assert.Contains(Activity.Current.Baggage, pair => pair.Key == "Key2" && pair.Value == "value2"); + } + + + [Fact] + public void ActivityTraceParentAndTraceStateFromHeaders() + { + var diagnosticListener = new DiagnosticListener("DummySource"); + var hostingApplication = CreateApplication(out var features, diagnosticListener: diagnosticListener); + + diagnosticListener.Subscribe(new CallbackDiagnosticListener(pair => { }), + s => + { + if (s.StartsWith("Microsoft.AspNetCore.Hosting.HttpRequestIn")) + { + return true; + } + return false; + }); + + features.Set(new HttpRequestFeature() + { + Headers = new HeaderDictionary() + { + {"traceparent", "00-0123456789abcdef0123456789abcdef-0123456789abcdef-01"}, + {"tracestate", "TraceState1"}, + {"Correlation-Context", "Key1=value1, Key2=value2"} + } + }); + hostingApplication.CreateContext(features); + Assert.Equal("Microsoft.AspNetCore.Hosting.HttpRequestIn", Activity.Current.OperationName); + Assert.Equal(ActivityIdFormat.W3C, Activity.Current.IdFormat); + Assert.Equal("0123456789abcdef0123456789abcdef", Activity.Current.TraceId.ToHexString()); + Assert.Equal("0123456789abcdef", Activity.Current.ParentSpanId.ToHexString()); + Assert.Equal("TraceState1", Activity.Current.TraceStateString); + + Assert.Contains(Activity.Current.Baggage, pair => pair.Key == "Key1" && pair.Value == "value1"); + Assert.Contains(Activity.Current.Baggage, pair => pair.Key == "Key2" && pair.Value == "value2"); + } + + [Fact] + public void ActivityOnExportHookIsCalled() + { + var diagnosticListener = new DiagnosticListener("DummySource"); + var hostingApplication = CreateApplication(out var features, diagnosticListener: diagnosticListener); + + bool onActivityImportCalled = false; + diagnosticListener.Subscribe( + observer: new CallbackDiagnosticListener(pair => { }), + isEnabled: (s, o, _) => true, + onActivityImport: (activity, context) => + { + onActivityImportCalled = true; + Assert.Null(Activity.Current); + Assert.Equal("Microsoft.AspNetCore.Hosting.HttpRequestIn", activity.OperationName); + Assert.NotNull(context); + Assert.IsAssignableFrom(context); + + activity.ActivityTraceFlags = ActivityTraceFlags.Recorded; + }); + + hostingApplication.CreateContext(features); + + Assert.True(onActivityImportCalled); + Assert.NotNull(Activity.Current); + Assert.True(Activity.Current.Recorded); + } + + + private static void AssertProperty(object o, string name) + { + Assert.NotNull(o); + var property = o.GetType().GetTypeInfo().GetProperty(name, BindingFlags.Instance | BindingFlags.Public); + Assert.NotNull(property); + var value = property.GetValue(o); + Assert.NotNull(value); + Assert.IsAssignableFrom(value); + } + + private static HostingApplication CreateApplication(out FeatureCollection features, + DiagnosticListener diagnosticListener = null, ILogger logger = null, Action configure = null) + { + var httpContextFactory = new Mock(); + + features = new FeatureCollection(); + features.Set(new HttpRequestFeature()); + var context = new DefaultHttpContext(features); + configure?.Invoke(context); + httpContextFactory.Setup(s => s.Create(It.IsAny())).Returns(context); + httpContextFactory.Setup(s => s.Dispose(It.IsAny())); + + var hostingApplication = new HostingApplication( + ctx => Task.CompletedTask, + logger ?? new NullScopeLogger(), + diagnosticListener ?? new NoopDiagnosticListener(), + httpContextFactory.Object); + + return hostingApplication; + } + + private class NullScopeLogger : ILogger + { + private readonly bool _isEnabled; + public NullScopeLogger(bool isEnabled = false) + { + _isEnabled = isEnabled; + } + + public IDisposable BeginScope(TState state) => null; + + public bool IsEnabled(LogLevel logLevel) => _isEnabled; + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) + { + } + } + + private class LoggerWithScopes : ILogger + { + private readonly bool _isEnabled; + public LoggerWithScopes(bool isEnabled = false) + { + _isEnabled = isEnabled; + } + + public IDisposable BeginScope(TState state) + { + Scopes.Add(state); + return new Scope(); + } + + public List Scopes { get; set; } = new List(); + + public bool IsEnabled(LogLevel logLevel) => _isEnabled; + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) + { + + } + + private class Scope : IDisposable + { + public void Dispose() + { + } + } + } + + private class NoopDiagnosticListener : DiagnosticListener + { + private readonly bool _isEnabled; + + public NoopDiagnosticListener(bool isEnabled = false) : base("DummyListener") + { + _isEnabled = isEnabled; + } + + public override bool IsEnabled(string name) => _isEnabled; + + public override void Write(string name, object value) + { + } + } + + private class CallbackDiagnosticListener : IObserver> + { + private readonly Action> _callback; + + public CallbackDiagnosticListener(Action> callback) + { + _callback = callback; + } + + public void OnNext(KeyValuePair value) + { + _callback(value); + } + + public void OnError(Exception error) + { + } + + public void OnCompleted() + { + } + } + } +} diff --git a/src/Hosting/Hosting/test/HostingApplicationTests.cs b/src/Hosting/Hosting/test/HostingApplicationTests.cs index 64d832eb0c..4dd9863718 100644 --- a/src/Hosting/Hosting/test/HostingApplicationTests.cs +++ b/src/Hosting/Hosting/test/HostingApplicationTests.cs @@ -1,532 +1,136 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - using System; +using System.Collections; using System.Collections.Generic; using System.Diagnostics; -using System.Linq; -using System.Reflection; using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting.Server.Abstractions; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging.Abstractions; using Moq; using Xunit; +using static Microsoft.AspNetCore.Hosting.HostingApplication; namespace Microsoft.AspNetCore.Hosting.Tests { public class HostingApplicationTests { [Fact] - public void DisposeContextDoesNotThrowWhenContextScopeIsNull() + public void DisposeContextDoesNotClearHttpContextIfDefaultHttpContextFactoryUsed() { // Arrange - var hostingApplication = CreateApplication(out var features); + var hostingApplication = CreateApplication(); + var httpContext = new DefaultHttpContext(); + + var context = hostingApplication.CreateContext(httpContext.Features); + Assert.NotNull(context.HttpContext); + + // Act/Assert + hostingApplication.DisposeContext(context, null); + Assert.NotNull(context.HttpContext); + } + + [Fact] + public void DisposeContextClearsHttpContextIfIHttpContextAccessorIsActive() + { + // Arrange + var hostingApplication = CreateApplication(useHttpContextAccessor: true); + var httpContext = new DefaultHttpContext(); + + var context = hostingApplication.CreateContext(httpContext.Features); + Assert.NotNull(context.HttpContext); + + // Act/Assert + hostingApplication.DisposeContext(context, null); + Assert.Null(context.HttpContext); + } + + [Fact] + public void CreateContextReinitializesPreviouslyStoredDefaultHttpContext() + { + // Arrange + var hostingApplication = CreateApplication(); + var features = new FeaturesWithContext(new DefaultHttpContext().Features); + var previousContext = new DefaultHttpContext(); + // Pretend like we had previous HttpContext + features.HostContext = new Context(); + features.HostContext.HttpContext = previousContext; + var context = hostingApplication.CreateContext(features); + Assert.Same(previousContext, context.HttpContext); + + // Act/Assert + hostingApplication.DisposeContext(context, null); + Assert.Same(previousContext, context.HttpContext); + } + + [Fact] + public void CreateContextCreatesNewContextIfNotUsingDefaultHttpContextFactory() + { + // Arrange + var factory = new Mock(); + factory.Setup(m => m.Create(It.IsAny())).Returns(f => new DefaultHttpContext(f)); + factory.Setup(m => m.Dispose(It.IsAny())).Callback(() => { }); + + var hostingApplication = CreateApplication(factory.Object); + var features = new FeaturesWithContext(new DefaultHttpContext().Features); + var previousContext = new DefaultHttpContext(); + // Pretend like we had previous HttpContext + features.HostContext = new Context(); + features.HostContext.HttpContext = previousContext; + + var context = hostingApplication.CreateContext(features); + Assert.NotSame(previousContext, context.HttpContext); // Act/Assert hostingApplication.DisposeContext(context, null); } - [Fact] - public void CreateContextWithDisabledLoggerDoesNotCreateActivity() + private static HostingApplication CreateApplication(IHttpContextFactory httpContextFactory = null, bool useHttpContextAccessor = false) { - // Arrange - var hostingApplication = CreateApplication(out var features); - - // Act - hostingApplication.CreateContext(features); - - Assert.Null(Activity.Current); - } - - [Fact] - public void CreateContextWithEnabledLoggerCreatesActivityAndSetsActivityInScope() - { - // Arrange - var logger = new LoggerWithScopes(isEnabled: true); - var hostingApplication = CreateApplication(out var features, logger: logger); - - // Act - var context = hostingApplication.CreateContext(features); - - Assert.Single(logger.Scopes); - var pairs = ((IReadOnlyList>)logger.Scopes[0]).ToDictionary(p => p.Key, p => p.Value); - Assert.Equal(Activity.Current.Id, pairs["SpanId"].ToString()); - Assert.Equal(Activity.Current.RootId, pairs["TraceId"].ToString()); - Assert.Equal(string.Empty, pairs["ParentId"]?.ToString()); - } - - [Fact] - public void CreateContextWithEnabledLoggerAndRequestIdCreatesActivityAndSetsActivityInScope() - { - // Arrange - - // Generate an id we can use for the request id header (in the correct format) - var activity = new Activity("IncomingRequest"); - activity.Start(); - var id = activity.Id; - activity.Stop(); - - var logger = new LoggerWithScopes(isEnabled: true); - var hostingApplication = CreateApplication(out var features, logger: logger, configure: context => + var services = new ServiceCollection(); + services.AddOptions(); + if (useHttpContextAccessor) { - context.Request.Headers["Request-Id"] = id; - }); + services.AddHttpContextAccessor(); + } - // Act - var context = hostingApplication.CreateContext(features); - - Assert.Single(logger.Scopes); - var pairs = ((IReadOnlyList>)logger.Scopes[0]).ToDictionary(p => p.Key, p => p.Value); - Assert.Equal(Activity.Current.Id, pairs["SpanId"].ToString()); - Assert.Equal(Activity.Current.RootId, pairs["TraceId"].ToString()); - Assert.Equal(id, pairs["ParentId"].ToString()); - } - - [Fact] - public void ActivityStopDoesNotFireIfNoListenerAttachedForStart() - { - // Arrange - var diagnosticListener = new DiagnosticListener("DummySource"); - var logger = new LoggerWithScopes(isEnabled: true); - var hostingApplication = CreateApplication(out var features, diagnosticListener: diagnosticListener, logger: logger); - var startFired = false; - var stopFired = false; - - diagnosticListener.Subscribe(new CallbackDiagnosticListener(pair => - { - // This should not fire - if (pair.Key == "Microsoft.AspNetCore.Hosting.HttpRequestIn.Start") - { - startFired = true; - } - - // This should not fire - if (pair.Key == "Microsoft.AspNetCore.Hosting.HttpRequestIn.Stop") - { - stopFired = true; - } - }), - (s, o, arg3) => - { - // The events are off - return false; - }); - - - // Act - var context = hostingApplication.CreateContext(features); - - hostingApplication.DisposeContext(context, exception: null); - - Assert.False(startFired); - Assert.False(stopFired); - Assert.Null(Activity.Current); - } - - [Fact] - public void ActivityIsNotCreatedWhenIsEnabledForActivityIsFalse() - { - var diagnosticListener = new DiagnosticListener("DummySource"); - var hostingApplication = CreateApplication(out var features, diagnosticListener: diagnosticListener); - - bool eventsFired = false; - bool isEnabledActivityFired = false; - bool isEnabledStartFired = false; - - diagnosticListener.Subscribe(new CallbackDiagnosticListener(pair => - { - eventsFired |= pair.Key.StartsWith("Microsoft.AspNetCore.Hosting.HttpRequestIn"); - }), (s, o, arg3) => - { - if (s == "Microsoft.AspNetCore.Hosting.HttpRequestIn") - { - Assert.IsAssignableFrom(o); - isEnabledActivityFired = true; - } - if (s == "Microsoft.AspNetCore.Hosting.HttpRequestIn.Start") - { - isEnabledStartFired = true; - } - return false; - }); - - hostingApplication.CreateContext(features); - Assert.Null(Activity.Current); - Assert.True(isEnabledActivityFired); - Assert.False(isEnabledStartFired); - Assert.False(eventsFired); - } - - [Fact] - public void ActivityIsCreatedButNotLoggedWhenIsEnabledForActivityStartIsFalse() - { - var diagnosticListener = new DiagnosticListener("DummySource"); - var hostingApplication = CreateApplication(out var features, diagnosticListener: diagnosticListener); - - bool eventsFired = false; - bool isEnabledStartFired = false; - bool isEnabledActivityFired = false; - - diagnosticListener.Subscribe(new CallbackDiagnosticListener(pair => - { - eventsFired |= pair.Key.StartsWith("Microsoft.AspNetCore.Hosting.HttpRequestIn"); - }), (s, o, arg3) => - { - if (s == "Microsoft.AspNetCore.Hosting.HttpRequestIn") - { - Assert.IsAssignableFrom(o); - isEnabledActivityFired = true; - return true; - } - - if (s == "Microsoft.AspNetCore.Hosting.HttpRequestIn.Start") - { - isEnabledStartFired = true; - return false; - } - return true; - }); - - hostingApplication.CreateContext(features); - Assert.NotNull(Activity.Current); - Assert.True(isEnabledActivityFired); - Assert.True(isEnabledStartFired); - Assert.False(eventsFired); - } - - [Fact] - public void ActivityIsCreatedAndLogged() - { - var diagnosticListener = new DiagnosticListener("DummySource"); - var hostingApplication = CreateApplication(out var features, diagnosticListener: diagnosticListener); - - bool startCalled = false; - - diagnosticListener.Subscribe(new CallbackDiagnosticListener(pair => - { - if (pair.Key == "Microsoft.AspNetCore.Hosting.HttpRequestIn.Start") - { - startCalled = true; - Assert.NotNull(pair.Value); - Assert.NotNull(Activity.Current); - Assert.Equal("Microsoft.AspNetCore.Hosting.HttpRequestIn", Activity.Current.OperationName); - AssertProperty(pair.Value, "HttpContext"); - } - })); - - hostingApplication.CreateContext(features); - Assert.NotNull(Activity.Current); - Assert.True(startCalled); - } - - [Fact] - public void ActivityIsStoppedDuringStopCall() - { - var diagnosticListener = new DiagnosticListener("DummySource"); - var hostingApplication = CreateApplication(out var features, diagnosticListener: diagnosticListener); - - bool endCalled = false; - diagnosticListener.Subscribe(new CallbackDiagnosticListener(pair => - { - if (pair.Key == "Microsoft.AspNetCore.Hosting.HttpRequestIn.Stop") - { - endCalled = true; - - Assert.NotNull(Activity.Current); - Assert.True(Activity.Current.Duration > TimeSpan.Zero); - Assert.Equal("Microsoft.AspNetCore.Hosting.HttpRequestIn", Activity.Current.OperationName); - AssertProperty(pair.Value, "HttpContext"); - } - })); - - var context = hostingApplication.CreateContext(features); - hostingApplication.DisposeContext(context, null); - Assert.True(endCalled); - } - - [Fact] - public void ActivityIsStoppedDuringUnhandledExceptionCall() - { - var diagnosticListener = new DiagnosticListener("DummySource"); - var hostingApplication = CreateApplication(out var features, diagnosticListener: diagnosticListener); - - bool endCalled = false; - diagnosticListener.Subscribe(new CallbackDiagnosticListener(pair => - { - if (pair.Key == "Microsoft.AspNetCore.Hosting.HttpRequestIn.Stop") - { - endCalled = true; - Assert.NotNull(Activity.Current); - Assert.True(Activity.Current.Duration > TimeSpan.Zero); - Assert.Equal("Microsoft.AspNetCore.Hosting.HttpRequestIn", Activity.Current.OperationName); - AssertProperty(pair.Value, "HttpContext"); - } - })); - - var context = hostingApplication.CreateContext(features); - hostingApplication.DisposeContext(context, new Exception()); - Assert.True(endCalled); - } - - [Fact] - public void ActivityIsAvailableDuringUnhandledExceptionCall() - { - var diagnosticListener = new DiagnosticListener("DummySource"); - var hostingApplication = CreateApplication(out var features, diagnosticListener: diagnosticListener); - - bool endCalled = false; - diagnosticListener.Subscribe(new CallbackDiagnosticListener(pair => - { - if (pair.Key == "Microsoft.AspNetCore.Hosting.UnhandledException") - { - endCalled = true; - Assert.NotNull(Activity.Current); - Assert.Equal("Microsoft.AspNetCore.Hosting.HttpRequestIn", Activity.Current.OperationName); - } - })); - - var context = hostingApplication.CreateContext(features); - hostingApplication.DisposeContext(context, new Exception()); - Assert.True(endCalled); - } - - [Fact] - public void ActivityIsAvailibleDuringRequest() - { - var diagnosticListener = new DiagnosticListener("DummySource"); - var hostingApplication = CreateApplication(out var features, diagnosticListener: diagnosticListener); - - diagnosticListener.Subscribe(new CallbackDiagnosticListener(pair => { }), - s => - { - if (s.StartsWith("Microsoft.AspNetCore.Hosting.HttpRequestIn")) - { - return true; - } - return false; - }); - - hostingApplication.CreateContext(features); - - Assert.NotNull(Activity.Current); - Assert.Equal("Microsoft.AspNetCore.Hosting.HttpRequestIn", Activity.Current.OperationName); - } - - [Fact] - public void ActivityParentIdAndBaggeReadFromHeaders() - { - var diagnosticListener = new DiagnosticListener("DummySource"); - var hostingApplication = CreateApplication(out var features, diagnosticListener: diagnosticListener); - - diagnosticListener.Subscribe(new CallbackDiagnosticListener(pair => { }), - s => - { - if (s.StartsWith("Microsoft.AspNetCore.Hosting.HttpRequestIn")) - { - return true; - } - return false; - }); - - features.Set(new HttpRequestFeature() - { - Headers = new HeaderDictionary() - { - {"Request-Id", "ParentId1"}, - {"Correlation-Context", "Key1=value1, Key2=value2"} - } - }); - hostingApplication.CreateContext(features); - Assert.Equal("Microsoft.AspNetCore.Hosting.HttpRequestIn", Activity.Current.OperationName); - Assert.Equal("ParentId1", Activity.Current.ParentId); - Assert.Contains(Activity.Current.Baggage, pair => pair.Key == "Key1" && pair.Value == "value1"); - Assert.Contains(Activity.Current.Baggage, pair => pair.Key == "Key2" && pair.Value == "value2"); - } - - - [Fact] - public void ActivityTraceParentAndTraceStateFromHeaders() - { - var diagnosticListener = new DiagnosticListener("DummySource"); - var hostingApplication = CreateApplication(out var features, diagnosticListener: diagnosticListener); - - diagnosticListener.Subscribe(new CallbackDiagnosticListener(pair => { }), - s => - { - if (s.StartsWith("Microsoft.AspNetCore.Hosting.HttpRequestIn")) - { - return true; - } - return false; - }); - - features.Set(new HttpRequestFeature() - { - Headers = new HeaderDictionary() - { - {"traceparent", "00-0123456789abcdef0123456789abcdef-0123456789abcdef-01"}, - {"tracestate", "TraceState1"}, - {"Correlation-Context", "Key1=value1, Key2=value2"} - } - }); - hostingApplication.CreateContext(features); - Assert.Equal("Microsoft.AspNetCore.Hosting.HttpRequestIn", Activity.Current.OperationName); - Assert.Equal(ActivityIdFormat.W3C, Activity.Current.IdFormat); - Assert.Equal("0123456789abcdef0123456789abcdef", Activity.Current.TraceId.ToHexString()); - Assert.Equal("0123456789abcdef", Activity.Current.ParentSpanId.ToHexString()); - Assert.Equal("TraceState1", Activity.Current.TraceStateString); - - Assert.Contains(Activity.Current.Baggage, pair => pair.Key == "Key1" && pair.Value == "value1"); - Assert.Contains(Activity.Current.Baggage, pair => pair.Key == "Key2" && pair.Value == "value2"); - } - - [Fact] - public void ActivityOnExportHookIsCalled() - { - var diagnosticListener = new DiagnosticListener("DummySource"); - var hostingApplication = CreateApplication(out var features, diagnosticListener: diagnosticListener); - - bool onActivityImportCalled = false; - diagnosticListener.Subscribe( - observer: new CallbackDiagnosticListener(pair => { }), - isEnabled: (s, o, _) => true, - onActivityImport: (activity, context) => - { - onActivityImportCalled = true; - Assert.Null(Activity.Current); - Assert.Equal("Microsoft.AspNetCore.Hosting.HttpRequestIn", activity.OperationName); - Assert.NotNull(context); - Assert.IsAssignableFrom(context); - - activity.ActivityTraceFlags = ActivityTraceFlags.Recorded; - }); - - hostingApplication.CreateContext(features); - - Assert.True(onActivityImportCalled); - Assert.NotNull(Activity.Current); - Assert.True(Activity.Current.Recorded); - } - - - private static void AssertProperty(object o, string name) - { - Assert.NotNull(o); - var property = o.GetType().GetTypeInfo().GetProperty(name, BindingFlags.Instance | BindingFlags.Public); - Assert.NotNull(property); - var value = property.GetValue(o); - Assert.NotNull(value); - Assert.IsAssignableFrom(value); - } - - private static HostingApplication CreateApplication(out FeatureCollection features, - DiagnosticListener diagnosticListener = null, ILogger logger = null, Action configure = null) - { - var httpContextFactory = new Mock(); - - features = new FeatureCollection(); - features.Set(new HttpRequestFeature()); - var context = new DefaultHttpContext(features); - configure?.Invoke(context); - httpContextFactory.Setup(s => s.Create(It.IsAny())).Returns(context); - httpContextFactory.Setup(s => s.Dispose(It.IsAny())); + httpContextFactory ??= new DefaultHttpContextFactory(services.BuildServiceProvider()); var hostingApplication = new HostingApplication( ctx => Task.CompletedTask, - logger ?? new NullScopeLogger(), - diagnosticListener ?? new NoopDiagnosticListener(), - httpContextFactory.Object); + NullLogger.Instance, + new DiagnosticListener("Microsoft.AspNetCore"), + httpContextFactory); return hostingApplication; } - private class NullScopeLogger : ILogger + private class FeaturesWithContext : IHostContextContainer, IFeatureCollection { - private readonly bool _isEnabled; - public NullScopeLogger(bool isEnabled = false) + public FeaturesWithContext(IFeatureCollection features) { - _isEnabled = isEnabled; + Features = features; } - public IDisposable BeginScope(TState state) => null; + public IFeatureCollection Features { get; } - public bool IsEnabled(LogLevel logLevel) => _isEnabled; + public object this[Type key] { get => Features[key]; set => Features[key] = value; } - public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) - { - } - } + public T HostContext { get; set; } - private class LoggerWithScopes : ILogger - { - private readonly bool _isEnabled; - public LoggerWithScopes(bool isEnabled = false) - { - _isEnabled = isEnabled; - } + public bool IsReadOnly => Features.IsReadOnly; - public IDisposable BeginScope(TState state) - { - Scopes.Add(state); - return new Scope(); - } + public int Revision => Features.Revision; - public List Scopes { get; set; } = new List(); + public TFeature Get() => Features.Get(); - public bool IsEnabled(LogLevel logLevel) => _isEnabled; + public IEnumerator> GetEnumerator() => Features.GetEnumerator(); - public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) - { + public void Set(TFeature instance) => Features.Set(instance); - } - - private class Scope : IDisposable - { - public void Dispose() - { - } - } - } - - private class NoopDiagnosticListener : DiagnosticListener - { - private readonly bool _isEnabled; - - public NoopDiagnosticListener(bool isEnabled = false) : base("DummyListener") - { - _isEnabled = isEnabled; - } - - public override bool IsEnabled(string name) => _isEnabled; - - public override void Write(string name, object value) - { - } - } - - private class CallbackDiagnosticListener : IObserver> - { - private readonly Action> _callback; - - public CallbackDiagnosticListener(Action> callback) - { - _callback = callback; - } - - public void OnNext(KeyValuePair value) - { - _callback(value); - } - - public void OnError(Exception error) - { - } - - public void OnCompleted() - { - } + IEnumerator IEnumerable.GetEnumerator() => Features.GetEnumerator(); } } } diff --git a/src/Http/Http/src/DefaultHttpContext.cs b/src/Http/Http/src/DefaultHttpContext.cs index 102e96947d..7d096f186a 100644 --- a/src/Http/Http/src/DefaultHttpContext.cs +++ b/src/Http/Http/src/DefaultHttpContext.cs @@ -98,9 +98,9 @@ namespace Microsoft.AspNetCore.Http public override HttpResponse Response => _response; - public override ConnectionInfo Connection => _connection ?? (_connection = new DefaultConnectionInfo(_features.Collection)); + public override ConnectionInfo Connection => _connection ?? (_connection = new DefaultConnectionInfo(Features)); - public override WebSocketManager WebSockets => _websockets ?? (_websockets = new DefaultWebSocketManager(_features.Collection)); + public override WebSocketManager WebSockets => _websockets ?? (_websockets = new DefaultWebSocketManager(Features)); public override ClaimsPrincipal User {