diff --git a/src/Middleware/HeaderPropagation/ref/Microsoft.AspNetCore.HeaderPropagation.netcoreapp3.0.cs b/src/Middleware/HeaderPropagation/ref/Microsoft.AspNetCore.HeaderPropagation.netcoreapp3.0.cs index 887719c62484..f0f51b955caa 100644 --- a/src/Middleware/HeaderPropagation/ref/Microsoft.AspNetCore.HeaderPropagation.netcoreapp3.0.cs +++ b/src/Middleware/HeaderPropagation/ref/Microsoft.AspNetCore.HeaderPropagation.netcoreapp3.0.cs @@ -6,6 +6,7 @@ namespace Microsoft.AspNetCore.Builder public static partial class HeaderPropagationApplicationBuilderExtensions { public static Microsoft.AspNetCore.Builder.IApplicationBuilder UseHeaderPropagation(this Microsoft.AspNetCore.Builder.IApplicationBuilder app) { throw null; } + public static Microsoft.AspNetCore.Builder.IApplicationBuilder UseHeaderPropagation(this Microsoft.AspNetCore.Builder.IApplicationBuilder app, bool includeInLoggerScope) { throw null; } } } namespace Microsoft.AspNetCore.HeaderPropagation @@ -34,6 +35,11 @@ public void Add(string headerName, System.Func valueFilter) { } } + public partial class HeaderPropagationLoggerScopeMiddleware + { + public HeaderPropagationLoggerScopeMiddleware(Microsoft.AspNetCore.Http.RequestDelegate next, Microsoft.Extensions.Logging.ILogger logger, Microsoft.AspNetCore.HeaderPropagation.IHeaderPropagationLoggerScopeBuilder loggerScopeBuilder) { } + public System.Threading.Tasks.Task Invoke(Microsoft.AspNetCore.Http.HttpContext context) { throw null; } + } public partial class HeaderPropagationMessageHandler : System.Net.Http.DelegatingHandler { public HeaderPropagationMessageHandler(Microsoft.AspNetCore.HeaderPropagation.HeaderPropagationMessageHandlerOptions options, Microsoft.AspNetCore.HeaderPropagation.HeaderPropagationValues values) { } @@ -69,7 +75,9 @@ public HeaderPropagationOptions() { } public partial class HeaderPropagationValues { public HeaderPropagationValues() { } - public System.Collections.Generic.IDictionary Headers { get { throw null; } set { } } + } + public partial interface IHeaderPropagationLoggerScopeBuilder + { } } namespace Microsoft.Extensions.DependencyInjection diff --git a/src/Middleware/HeaderPropagation/samples/HeaderPropagationSample/HeaderPropagationSample.csproj b/src/Middleware/HeaderPropagation/samples/HeaderPropagationSample/HeaderPropagationSample.csproj index 8410f6d290d2..0a8a3d48f16f 100644 --- a/src/Middleware/HeaderPropagation/samples/HeaderPropagationSample/HeaderPropagationSample.csproj +++ b/src/Middleware/HeaderPropagation/samples/HeaderPropagationSample/HeaderPropagationSample.csproj @@ -1,4 +1,4 @@ - + netcoreapp3.0 @@ -11,5 +11,6 @@ + diff --git a/src/Middleware/HeaderPropagation/samples/HeaderPropagationSample/Program.cs b/src/Middleware/HeaderPropagation/samples/HeaderPropagationSample/Program.cs index 63aca35aa65a..6851d70320c3 100644 --- a/src/Middleware/HeaderPropagation/samples/HeaderPropagationSample/Program.cs +++ b/src/Middleware/HeaderPropagation/samples/HeaderPropagationSample/Program.cs @@ -1,5 +1,6 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; namespace HeaderPropagationSample { @@ -16,6 +17,8 @@ public static IHostBuilder CreateHostBuilder(string[] args) => { webBuilder.UseKestrel(); webBuilder.UseStartup(); - }); + }) + .ConfigureLogging((hostingContext, logging) => + logging.AddConsole(options => options.IncludeScopes = true)); } } diff --git a/src/Middleware/HeaderPropagation/samples/HeaderPropagationSample/Startup.cs b/src/Middleware/HeaderPropagation/samples/HeaderPropagationSample/Startup.cs index 00138f6efe31..8898f511d500 100644 --- a/src/Middleware/HeaderPropagation/samples/HeaderPropagationSample/Startup.cs +++ b/src/Middleware/HeaderPropagation/samples/HeaderPropagationSample/Startup.cs @@ -58,7 +58,9 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IHttpCli app.UseDeveloperExceptionPage(); } - app.UseHeaderPropagation(); + // Add the propagated headers Middleware + // and configure it to include the propagated headers to the logger scope. + app.UseHeaderPropagation(includeInLoggerScope: true); app.UseRouting(); diff --git a/src/Middleware/HeaderPropagation/src/DependencyInjection/HeaderPropagationApplicationBuilderExtensions.cs b/src/Middleware/HeaderPropagation/src/DependencyInjection/HeaderPropagationApplicationBuilderExtensions.cs index 7297b1de7ec9..33cbbdacbf47 100644 --- a/src/Middleware/HeaderPropagation/src/DependencyInjection/HeaderPropagationApplicationBuilderExtensions.cs +++ b/src/Middleware/HeaderPropagation/src/DependencyInjection/HeaderPropagationApplicationBuilderExtensions.cs @@ -34,5 +34,23 @@ public static IApplicationBuilder UseHeaderPropagation(this IApplicationBuilder return app.UseMiddleware(); } + + /// + /// Adds a middleware that collect headers to be propagated to a . + /// + /// The to add the middleware to. + /// Includes the captured headers in the logger scope. + /// A reference to the after the operation has completed. + public static IApplicationBuilder UseHeaderPropagation(this IApplicationBuilder app, bool includeInLoggerScope) + { + app.UseHeaderPropagation(); + + if (includeInLoggerScope) + { + app.UseMiddleware(); + } + + return app; + } } } diff --git a/src/Middleware/HeaderPropagation/src/DependencyInjection/HeaderPropagationServiceCollectionExtensions.cs b/src/Middleware/HeaderPropagation/src/DependencyInjection/HeaderPropagationServiceCollectionExtensions.cs index ab3d60cc0d1e..ff425c7c634c 100644 --- a/src/Middleware/HeaderPropagation/src/DependencyInjection/HeaderPropagationServiceCollectionExtensions.cs +++ b/src/Middleware/HeaderPropagation/src/DependencyInjection/HeaderPropagationServiceCollectionExtensions.cs @@ -23,6 +23,7 @@ public static IServiceCollection AddHeaderPropagation(this IServiceCollection se } services.TryAddSingleton(); + services.TryAddSingleton(); return services; } diff --git a/src/Middleware/HeaderPropagation/src/HeaderPropagationLoggerScope.cs b/src/Middleware/HeaderPropagation/src/HeaderPropagationLoggerScope.cs new file mode 100644 index 000000000000..2d0ea4b0b397 --- /dev/null +++ b/src/Middleware/HeaderPropagation/src/HeaderPropagationLoggerScope.cs @@ -0,0 +1,75 @@ +// 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.Globalization; +using System.Text; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.AspNetCore.HeaderPropagation +{ + internal class HeaderPropagationLoggerScope : IReadOnlyList> + { + private readonly List _headerNames; + private readonly IDictionary _headerValues; + private string _cachedToString; + + public HeaderPropagationLoggerScope(List headerNames, IDictionary headerValues) + { + _headerNames = headerNames ?? throw new ArgumentNullException(nameof(headerNames)); + _headerValues = headerValues ?? throw new ArgumentNullException(nameof(headerValues)); + } + + public int Count => _headerNames.Count; + + public KeyValuePair this[int index] + { + get + { + var headerName = _headerNames[index]; + _headerValues.TryGetValue(headerName, out var value); + return new KeyValuePair(headerName, value); + } + } + + public IEnumerator> GetEnumerator() + { + for (var i = 0; i < Count; i++) + { + yield return this[i]; + } + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public override string ToString() + { + if (_cachedToString == null) + { + var sb = new StringBuilder(); + + for (int i = 0; i < Count; i++) + { + if (i > 0) sb.Append(' '); + + var headerName = _headerNames[i]; + _headerValues.TryGetValue(headerName, out var value); + + sb.Append(string.Format( + CultureInfo.InvariantCulture, + "{0}:{1}", + headerName, value.ToString())); + } + + _cachedToString = sb.ToString(); + } + + return _cachedToString; + } + } +} diff --git a/src/Middleware/HeaderPropagation/src/HeaderPropagationLoggerScopeBuilder.cs b/src/Middleware/HeaderPropagation/src/HeaderPropagationLoggerScopeBuilder.cs new file mode 100644 index 000000000000..5a96195e376d --- /dev/null +++ b/src/Middleware/HeaderPropagation/src/HeaderPropagationLoggerScopeBuilder.cs @@ -0,0 +1,55 @@ +// 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 Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.HeaderPropagation +{ + + /// + /// A builder to build the for the . + /// + internal class HeaderPropagationLoggerScopeBuilder : IHeaderPropagationLoggerScopeBuilder + { + private readonly List _headerNames = new List(); + private readonly HeaderPropagationValues _values; + + /// + /// Creates a new instance of the . + /// + /// The options that define which headers are propagated. + /// The values of the headers to be propagated populated by the + /// . + public HeaderPropagationLoggerScopeBuilder(IOptions options, HeaderPropagationValues values) + { + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + var headers = options.Value.Headers; + + _values = values ?? throw new ArgumentNullException(nameof(values)); + + var uniqueHeaderNames = new HashSet(); + + // Perf: not using directly the HashSet so we can iterate without allocating an enumerator + // and avoiding foreach since we don't define a struct-enumerator. + for (var i = 0; i < headers.Count; i++) + { + var headerName = headers[i].CapturedHeaderName; + if (uniqueHeaderNames.Add(headerName)) + { + _headerNames.Add(headerName); + } + } + } + + /// + /// Build the for the current async context. + /// + HeaderPropagationLoggerScope IHeaderPropagationLoggerScopeBuilder.Build() => + new HeaderPropagationLoggerScope(_headerNames, _values.Headers); + } +} diff --git a/src/Middleware/HeaderPropagation/src/HeaderPropagationLoggerScopeMiddleware.cs b/src/Middleware/HeaderPropagation/src/HeaderPropagationLoggerScopeMiddleware.cs new file mode 100644 index 000000000000..502a0ce32b06 --- /dev/null +++ b/src/Middleware/HeaderPropagation/src/HeaderPropagationLoggerScopeMiddleware.cs @@ -0,0 +1,29 @@ +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNetCore.HeaderPropagation +{ + public class HeaderPropagationLoggerScopeMiddleware + { + private readonly RequestDelegate _next; + private readonly ILogger _logger; + private readonly IHeaderPropagationLoggerScopeBuilder _loggerScopeBuilder; + + public HeaderPropagationLoggerScopeMiddleware(RequestDelegate next, ILogger logger, IHeaderPropagationLoggerScopeBuilder loggerScopeBuilder) + { + _next = next ?? throw new ArgumentNullException(nameof(next)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _loggerScopeBuilder = loggerScopeBuilder ?? throw new ArgumentNullException(nameof(loggerScopeBuilder)); + } + + public Task Invoke(HttpContext context) + { + using (_logger.BeginScope(_loggerScopeBuilder.Build())) + { + return _next.Invoke(context); + } + } + } +} diff --git a/src/Middleware/HeaderPropagation/src/HeaderPropagationValues.cs b/src/Middleware/HeaderPropagation/src/HeaderPropagationValues.cs index 741b02e7d61b..67ca82d04755 100644 --- a/src/Middleware/HeaderPropagation/src/HeaderPropagationValues.cs +++ b/src/Middleware/HeaderPropagation/src/HeaderPropagationValues.cs @@ -1,10 +1,8 @@ // 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.Threading; -using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.Primitives; namespace Microsoft.AspNetCore.HeaderPropagation @@ -23,7 +21,7 @@ public class HeaderPropagationValues /// /// The keys of correspond to . /// - public IDictionary Headers + internal IDictionary Headers { get { diff --git a/src/Middleware/HeaderPropagation/src/IHeaderPropagationLoggerScopeBuilder.cs b/src/Middleware/HeaderPropagation/src/IHeaderPropagationLoggerScopeBuilder.cs new file mode 100644 index 000000000000..1915014d1216 --- /dev/null +++ b/src/Middleware/HeaderPropagation/src/IHeaderPropagationLoggerScopeBuilder.cs @@ -0,0 +1,16 @@ +// 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. + +namespace Microsoft.AspNetCore.HeaderPropagation +{ + /// + /// A builder to build the for the . + /// + public interface IHeaderPropagationLoggerScopeBuilder + { + /// + /// Build the for the current async context. + /// + internal HeaderPropagationLoggerScope Build(); + } +} diff --git a/src/Middleware/HeaderPropagation/src/Microsoft.AspNetCore.HeaderPropagation.csproj b/src/Middleware/HeaderPropagation/src/Microsoft.AspNetCore.HeaderPropagation.csproj index 5ae4e64cc2f3..326e75c6d7e5 100644 --- a/src/Middleware/HeaderPropagation/src/Microsoft.AspNetCore.HeaderPropagation.csproj +++ b/src/Middleware/HeaderPropagation/src/Microsoft.AspNetCore.HeaderPropagation.csproj @@ -1,4 +1,4 @@ - + ASP.NET Core middleware to propagate HTTP headers from the incoming request to the outgoing HTTP Client requests @@ -9,10 +9,6 @@ aspnetcore;httpclient - - - - diff --git a/src/Middleware/HeaderPropagation/src/Properties/AssemblyInfo.cs b/src/Middleware/HeaderPropagation/src/Properties/AssemblyInfo.cs new file mode 100644 index 000000000000..828538ae12cc --- /dev/null +++ b/src/Middleware/HeaderPropagation/src/Properties/AssemblyInfo.cs @@ -0,0 +1,6 @@ +// 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.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Microsoft.AspNetCore.HeaderPropagation.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] diff --git a/src/Middleware/HeaderPropagation/test/HeaderPropagationIntegrationTest.cs b/src/Middleware/HeaderPropagation/test/HeaderPropagationIntegrationTest.cs index 585bb914729a..4fc0beccff95 100644 --- a/src/Middleware/HeaderPropagation/test/HeaderPropagationIntegrationTest.cs +++ b/src/Middleware/HeaderPropagation/test/HeaderPropagationIntegrationTest.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.Collections.Generic; using System.Net; using System.Net.Http; using System.Net.Http.Headers; @@ -12,6 +13,8 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Primitives; using Xunit; namespace Microsoft.AspNetCore.HeaderPropagation.Tests @@ -159,12 +162,60 @@ public async Task HeaderInRequest_OverrideHeaderPerClient_AddCorrectValue() Assert.Equal(new[] { "test" }, handler.Headers.GetValues("different")); } - private IWebHostBuilder CreateBuilder(Action configure, HttpMessageHandler primaryHandler, Action configureClient = null) + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task HeaderInRequest_IncludeInLoggerScope_AddScopeToLogger(bool includeInLoggerScope) + { + // Arrange + var handler = new SimpleHandler(); + var loggingProvider = new LoggerProvider(); + var builder = CreateBuilder(c => + { + c.Headers.Add("foo"); + }, + handler, + includeInLoggerScope: includeInLoggerScope) + .ConfigureLogging((_, logging) => logging.AddProvider(loggingProvider)); + var server = new TestServer(builder); + var client = server.CreateClient(); + + var request = new HttpRequestMessage(); + request.Headers.Add("foo", "bar"); + + // Act + var response = await client.SendAsync(request); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.True(handler.Headers.Contains("foo")); + Assert.Equal(new[] { "bar" }, handler.Headers.GetValues("foo")); + if (includeInLoggerScope) + { + Assert.Single(loggingProvider.Scopes); + var scope = loggingProvider.Scopes[0]; + Assert.Single(scope); + Assert.IsType(scope); + var entry = scope[0]; + Assert.Equal("foo", entry.Key); + Assert.Equal("bar", (StringValues)entry.Value); + } + else + { + Assert.Empty(loggingProvider.Scopes); + } + } + + private IWebHostBuilder CreateBuilder( + Action configure, + HttpMessageHandler primaryHandler, + Action configureClient = null, + bool includeInLoggerScope = false) { return new WebHostBuilder() .Configure(app => { - app.UseHeaderPropagation(); + app.UseHeaderPropagation(includeInLoggerScope); app.UseMiddleware(); }) .ConfigureServices(services => @@ -213,5 +264,30 @@ public Task InvokeAsync(HttpContext _) return client.GetAsync(""); } } + + private class LoggerProvider : ILoggerProvider, ILogger + { + public List Scopes { get; } = new List(); + + public bool IsEnabled(LogLevel logLevel) => true; + + public ILogger CreateLogger(string name) => this; + + public IDisposable BeginScope(TState state) + { + if (state is HeaderPropagationLoggerScope scope) + { + Scopes.Add(scope); + } + return this; + } + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) + { + } + + public void Dispose() + { + } + } } } diff --git a/src/Middleware/HeaderPropagation/test/HeaderPropagationLoggerScopeBuilderTest.cs b/src/Middleware/HeaderPropagation/test/HeaderPropagationLoggerScopeBuilderTest.cs new file mode 100644 index 000000000000..c6682345a82e --- /dev/null +++ b/src/Middleware/HeaderPropagation/test/HeaderPropagationLoggerScopeBuilderTest.cs @@ -0,0 +1,70 @@ +// 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.Collections.Generic; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Primitives; +using Xunit; + +namespace Microsoft.AspNetCore.HeaderPropagation.Tests +{ + public class HeaderPropagationLoggerScopeBuilderTest + { + public HeaderPropagationLoggerScopeBuilderTest() + { + Options = new HeaderPropagationOptions(); + Values = new HeaderPropagationValues() + { + Headers = new Dictionary() + }; + } + + public HeaderPropagationOptions Options { get; set; } + public HeaderPropagationValues Values { get; set; } + + [Fact] + public void NoPropagatedHeaders_EmptyScope() + { + // Arrange + IHeaderPropagationLoggerScopeBuilder builder = CreateBuilder(); + + // Act + var scope = builder.Build(); + + // Assert + Assert.Empty(scope); + Assert.Empty(scope.ToString()); + } + + [Theory] + [InlineData(1)] + [InlineData(2)] + public void PropagatedHeaderHasValue_AddValueInScope_WithoutDuplicates(int times) + { + // Arrange + for (var i = 0; i < times; i++) + { + Options.Headers.Add("foo"); + } + Values.Headers.Add("foo", "bar"); + IHeaderPropagationLoggerScopeBuilder builder = new HeaderPropagationLoggerScopeBuilder( + new OptionsWrapper(Options), + Values); + + // Act + var scope = builder.Build(); + + // Assert + Assert.Single(scope); + var entry = scope[0]; + Assert.Equal("foo", entry.Key); + Assert.IsType(entry.Value); + Assert.Equal("bar", (StringValues)entry.Value); + Assert.Equal("foo:bar", scope.ToString()); + } + + private IHeaderPropagationLoggerScopeBuilder CreateBuilder() => + new HeaderPropagationLoggerScopeBuilder(new OptionsWrapper(Options), Values); + + } +} diff --git a/src/Middleware/HeaderPropagation/test/HeaderPropagationLoggerScopeMiddlewareTest.cs b/src/Middleware/HeaderPropagation/test/HeaderPropagationLoggerScopeMiddlewareTest.cs new file mode 100644 index 000000000000..cd3c2af448b5 --- /dev/null +++ b/src/Middleware/HeaderPropagation/test/HeaderPropagationLoggerScopeMiddlewareTest.cs @@ -0,0 +1,69 @@ +// 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.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Primitives; +using Xunit; + +namespace Microsoft.AspNetCore.HeaderPropagation.Tests +{ + public class HeaderPropagationLoggerScopeMiddlewareTest + { + public HeaderPropagationLoggerScopeMiddlewareTest() + { + Context = new DefaultHttpContext(); + Next = ctx => Task.CompletedTask; + Logger = new TestLogger(); + Builder = new TestHeaderPropagationLoggerScopeBuilder(); + + Middleware = new HeaderPropagationLoggerScopeMiddleware(Next, Logger, Builder); + } + + public DefaultHttpContext Context { get; set; } + public RequestDelegate Next { get; set; } + public TestLogger Logger { get; set; } + public TestHeaderPropagationLoggerScopeBuilder Builder { get; set; } + public HeaderPropagationLoggerScopeMiddleware Middleware { get; set; } + + [Fact] + public async Task GetsScopeFromBuilderAndAddsItToLogger() + { + // Act + await Middleware.Invoke(Context); + + // Assert + Assert.NotNull(Logger.Scope); + Assert.Same(Builder.Scope, Logger.Scope); + } + } + + public class TestHeaderPropagationLoggerScopeBuilder : IHeaderPropagationLoggerScopeBuilder + { + internal HeaderPropagationLoggerScope Scope { get; set; } = + new HeaderPropagationLoggerScope(new List(), new Dictionary()); + + HeaderPropagationLoggerScope IHeaderPropagationLoggerScopeBuilder.Build() => Scope; + } + + public class TestLogger : ILogger + { + public object Scope { get; private set; } + + public IDisposable BeginScope(TState state) + { + Scope = state; + return null; + } + + public bool IsEnabled(LogLevel logLevel) => true; + + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) + { + } + } +} diff --git a/src/Middleware/HeaderPropagation/test/HeaderPropagationLoggerScopeTest.cs b/src/Middleware/HeaderPropagation/test/HeaderPropagationLoggerScopeTest.cs new file mode 100644 index 000000000000..2466d7d07321 --- /dev/null +++ b/src/Middleware/HeaderPropagation/test/HeaderPropagationLoggerScopeTest.cs @@ -0,0 +1,91 @@ +// 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.Collections.Generic; +using Microsoft.Extensions.Primitives; +using Xunit; + +namespace Microsoft.AspNetCore.HeaderPropagation.Tests +{ + public class HeaderPropagationLoggerScopeTest + { + [Fact] + public void NoPropagatedHeaders_EmptyScope() + { + // Arrange + var headerNames = new List(); + var headerValues = new Dictionary(); + + // Act + var scope = new HeaderPropagationLoggerScope(headerNames, headerValues); + + // Assert + Assert.Empty(scope); + Assert.Empty(scope.ToString()); + } + + [Fact] + public void PropagatedHeaderHasValue_AddsValueInScope() + { + // Arrange + var headerNames = new List { "foo" }; + var headerValues = new Dictionary { ["foo"] = "bar" }; + + // Act + var scope = new HeaderPropagationLoggerScope(headerNames, headerValues); + + // Assert + Assert.Single(scope); + var entry = scope[0]; + Assert.Equal("foo", entry.Key); + Assert.IsType(entry.Value); + Assert.Equal("bar", (StringValues)entry.Value); + Assert.Equal("foo:bar", scope.ToString()); + } + + [Fact] + public void PropagatedHeaderHasNoValue_AddsValueInScopeWithoutValue() + { + // Arrange + var headerNames = new List { "foo" }; + var headerValues = new Dictionary(); + + // Act + var scope = new HeaderPropagationLoggerScope(headerNames, headerValues); + + // Assert + Assert.Single(scope); + var entry = scope[0]; + Assert.Equal("foo", entry.Key); + Assert.IsType(entry.Value); + Assert.Empty((StringValues)entry.Value); + Assert.Equal("foo:", scope.ToString()); + } + + [Fact] + public void MultiplePropagatedHeadersHaveValue_AddsAllInScopeInOrder() + { + // Arrange + var headerNames = new List { "foo", "answer" }; + var headerValues = new Dictionary { + ["foo"] = "bar", + ["answer"] = "42" + }; + + // Act + var scope = new HeaderPropagationLoggerScope(headerNames, headerValues); + + // Assert + Assert.Equal(2, scope.Count); + var entry = scope[0]; + Assert.Equal("foo", entry.Key); + Assert.IsType(entry.Value); + Assert.Equal("bar", (StringValues)entry.Value); + entry = scope[1]; + Assert.Equal("answer", entry.Key); + Assert.IsType(entry.Value); + Assert.Equal("42", (StringValues)entry.Value); + Assert.Equal("foo:bar answer:42", scope.ToString()); + } + } +}