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.
This commit is contained in:
David Fowler 2020-04-15 12:41:59 -07:00 committed by GitHub
parent 5353ce9509
commit 1b99352ec7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 633 additions and 487 deletions

View File

@ -26,6 +26,8 @@ namespace Microsoft.AspNetCore.Http
_serviceScopeFactory = serviceProvider.GetRequiredService<IServiceScopeFactory>();
}
internal IHttpContextAccessor HttpContextAccessor => _httpContextAccessor;
public HttpContext Create(IFeatureCollection featureCollection)
{
if (featureCollection is null)

View File

@ -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
{

View File

@ -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<KeyValuePair<string, object>>)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<KeyValuePair<string, object>>)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<HttpContext>(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<HttpContext>(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<HttpContext>(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<HttpContext>(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<HttpContext>(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<IHttpRequestFeature>(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<IHttpRequestFeature>(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<HttpContext>(context);
activity.ActivityTraceFlags = ActivityTraceFlags.Recorded;
});
hostingApplication.CreateContext(features);
Assert.True(onActivityImportCalled);
Assert.NotNull(Activity.Current);
Assert.True(Activity.Current.Recorded);
}
private static void AssertProperty<T>(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<T>(value);
}
private static HostingApplication CreateApplication(out FeatureCollection features,
DiagnosticListener diagnosticListener = null, ILogger logger = null, Action<DefaultHttpContext> configure = null)
{
var httpContextFactory = new Mock<IHttpContextFactory>();
features = new FeatureCollection();
features.Set<IHttpRequestFeature>(new HttpRequestFeature());
var context = new DefaultHttpContext(features);
configure?.Invoke(context);
httpContextFactory.Setup(s => s.Create(It.IsAny<IFeatureCollection>())).Returns(context);
httpContextFactory.Setup(s => s.Dispose(It.IsAny<HttpContext>()));
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>(TState state) => null;
public bool IsEnabled(LogLevel logLevel) => _isEnabled;
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
{
}
}
private class LoggerWithScopes : ILogger
{
private readonly bool _isEnabled;
public LoggerWithScopes(bool isEnabled = false)
{
_isEnabled = isEnabled;
}
public IDisposable BeginScope<TState>(TState state)
{
Scopes.Add(state);
return new Scope();
}
public List<object> Scopes { get; set; } = new List<object>();
public bool IsEnabled(LogLevel logLevel) => _isEnabled;
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> 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<KeyValuePair<string, object>>
{
private readonly Action<KeyValuePair<string, object>> _callback;
public CallbackDiagnosticListener(Action<KeyValuePair<string, object>> callback)
{
_callback = callback;
}
public void OnNext(KeyValuePair<string, object> value)
{
_callback(value);
}
public void OnError(Exception error)
{
}
public void OnCompleted()
{
}
}
}
}

View File

@ -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<Context>(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<IHttpContextFactory>();
factory.Setup(m => m.Create(It.IsAny<IFeatureCollection>())).Returns<IFeatureCollection>(f => new DefaultHttpContext(f));
factory.Setup(m => m.Dispose(It.IsAny<HttpContext>())).Callback(() => { });
var hostingApplication = CreateApplication(factory.Object);
var features = new FeaturesWithContext<Context>(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<KeyValuePair<string, object>>)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<KeyValuePair<string, object>>)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<HttpContext>(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<HttpContext>(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<HttpContext>(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<HttpContext>(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<HttpContext>(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<IHttpRequestFeature>(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<IHttpRequestFeature>(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<HttpContext>(context);
activity.ActivityTraceFlags = ActivityTraceFlags.Recorded;
});
hostingApplication.CreateContext(features);
Assert.True(onActivityImportCalled);
Assert.NotNull(Activity.Current);
Assert.True(Activity.Current.Recorded);
}
private static void AssertProperty<T>(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<T>(value);
}
private static HostingApplication CreateApplication(out FeatureCollection features,
DiagnosticListener diagnosticListener = null, ILogger logger = null, Action<DefaultHttpContext> configure = null)
{
var httpContextFactory = new Mock<IHttpContextFactory>();
features = new FeatureCollection();
features.Set<IHttpRequestFeature>(new HttpRequestFeature());
var context = new DefaultHttpContext(features);
configure?.Invoke(context);
httpContextFactory.Setup(s => s.Create(It.IsAny<IFeatureCollection>())).Returns(context);
httpContextFactory.Setup(s => s.Dispose(It.IsAny<HttpContext>()));
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<T> : IHostContextContainer<T>, IFeatureCollection
{
private readonly bool _isEnabled;
public NullScopeLogger(bool isEnabled = false)
public FeaturesWithContext(IFeatureCollection features)
{
_isEnabled = isEnabled;
Features = features;
}
public IDisposable BeginScope<TState>(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<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> 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>(TState state)
{
Scopes.Add(state);
return new Scope();
}
public int Revision => Features.Revision;
public List<object> Scopes { get; set; } = new List<object>();
public TFeature Get<TFeature>() => Features.Get<TFeature>();
public bool IsEnabled(LogLevel logLevel) => _isEnabled;
public IEnumerator<KeyValuePair<Type, object>> GetEnumerator() => Features.GetEnumerator();
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
{
public void Set<TFeature>(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<KeyValuePair<string, object>>
{
private readonly Action<KeyValuePair<string, object>> _callback;
public CallbackDiagnosticListener(Action<KeyValuePair<string, object>> callback)
{
_callback = callback;
}
public void OnNext(KeyValuePair<string, object> value)
{
_callback(value);
}
public void OnError(Exception error)
{
}
public void OnCompleted()
{
}
IEnumerator IEnumerable.GetEnumerator() => Features.GetEnumerator();
}
}
}

View File

@ -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
{