diff --git a/eng/ProjectReferences.props b/eng/ProjectReferences.props
index 56e3e873c2b1..ba4bf03987ad 100644
--- a/eng/ProjectReferences.props
+++ b/eng/ProjectReferences.props
@@ -73,6 +73,7 @@
+
diff --git a/src/Middleware/HeaderPropagation/src/DependencyInjection/HeaderPropagationApplicationBuilderExtensions.cs b/src/Middleware/HeaderPropagation/src/DependencyInjection/HeaderPropagationApplicationBuilderExtensions.cs
new file mode 100644
index 000000000000..7297b1de7ec9
--- /dev/null
+++ b/src/Middleware/HeaderPropagation/src/DependencyInjection/HeaderPropagationApplicationBuilderExtensions.cs
@@ -0,0 +1,38 @@
+// 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.Net.Http;
+using Microsoft.AspNetCore.HeaderPropagation;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Microsoft.AspNetCore.Builder
+{
+ public static class HeaderPropagationApplicationBuilderExtensions
+ {
+ private static readonly string _unableToFindServices = string.Format(
+ "Unable to find the required services. Please add all the required services by calling '{0}.{1}' inside the call to 'ConfigureServices(...)' in the application startup code.",
+ nameof(IServiceCollection),
+ nameof(HeaderPropagationServiceCollectionExtensions.AddHeaderPropagation));
+
+ ///
+ /// Adds a middleware that collect headers to be propagated to a .
+ ///
+ /// The to add the middleware to.
+ /// A reference to the after the operation has completed.
+ public static IApplicationBuilder UseHeaderPropagation(this IApplicationBuilder app)
+ {
+ if (app == null)
+ {
+ throw new ArgumentNullException(nameof(app));
+ }
+
+ if (app.ApplicationServices.GetService() == null)
+ {
+ throw new InvalidOperationException(_unableToFindServices);
+ }
+
+ return app.UseMiddleware();
+ }
+ }
+}
diff --git a/src/Middleware/HeaderPropagation/src/DependencyInjection/HeaderPropagationHttpClientBuilderExtensions.cs b/src/Middleware/HeaderPropagation/src/DependencyInjection/HeaderPropagationHttpClientBuilderExtensions.cs
new file mode 100644
index 000000000000..f71614392f10
--- /dev/null
+++ b/src/Middleware/HeaderPropagation/src/DependencyInjection/HeaderPropagationHttpClientBuilderExtensions.cs
@@ -0,0 +1,30 @@
+// 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 Microsoft.AspNetCore.HeaderPropagation;
+
+namespace Microsoft.Extensions.DependencyInjection
+{
+ public static class HeaderPropagationHttpClientBuilderExtensions
+ {
+ ///
+ /// Adds a message handler for propagating headers collected by the to a outgoing request.
+ ///
+ /// The to add the message handler to.
+ /// The so that additional calls can be chained.
+ public static IHttpClientBuilder AddHeaderPropagation(this IHttpClientBuilder builder)
+ {
+ if (builder == null)
+ {
+ throw new ArgumentNullException(nameof(builder));
+ }
+
+ builder.Services.AddHeaderPropagation();
+
+ builder.AddHttpMessageHandler();
+
+ return builder;
+ }
+ }
+}
diff --git a/src/Middleware/HeaderPropagation/src/DependencyInjection/HeaderPropagationServiceCollectionExtensions.cs b/src/Middleware/HeaderPropagation/src/DependencyInjection/HeaderPropagationServiceCollectionExtensions.cs
new file mode 100644
index 000000000000..b175cd36e90b
--- /dev/null
+++ b/src/Middleware/HeaderPropagation/src/DependencyInjection/HeaderPropagationServiceCollectionExtensions.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.Net.Http;
+using Microsoft.AspNetCore.HeaderPropagation;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+
+namespace Microsoft.Extensions.DependencyInjection
+{
+ public static class HeaderPropagationServiceCollectionExtensions
+ {
+ ///
+ /// Adds services required for propagating headers to a .
+ ///
+ /// The to add the services to.
+ /// The so that additional calls can be chained.
+ public static IServiceCollection AddHeaderPropagation(this IServiceCollection services)
+ {
+ if (services == null)
+ {
+ throw new ArgumentNullException(nameof(services));
+ }
+
+ services.TryAddSingleton();
+ services.TryAddTransient();
+
+ return services;
+ }
+
+ ///
+ /// Adds services required for propagating headers to a .
+ ///
+ /// The to add the services to.
+ /// A delegate used to configure the .
+ /// The so that additional calls can be chained.
+ public static IServiceCollection AddHeaderPropagation(this IServiceCollection services, Action configureOptions)
+ {
+ if (services == null)
+ {
+ throw new ArgumentNullException(nameof(services));
+ }
+
+ if (configureOptions == null)
+ {
+ throw new ArgumentNullException(nameof(configureOptions));
+ }
+
+ services.Configure(configureOptions);
+ services.AddHeaderPropagation();
+
+ return services;
+ }
+ }
+}
diff --git a/src/Middleware/HeaderPropagation/src/HeaderPropagationEntry.cs b/src/Middleware/HeaderPropagation/src/HeaderPropagationEntry.cs
new file mode 100644
index 000000000000..5c9257400d2d
--- /dev/null
+++ b/src/Middleware/HeaderPropagation/src/HeaderPropagationEntry.cs
@@ -0,0 +1,56 @@
+// 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 Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Primitives;
+
+namespace Microsoft.AspNetCore.HeaderPropagation
+{
+ ///
+ /// Define the configuration of a header for the .
+ ///
+ public class HeaderPropagationEntry
+ {
+ ///
+ /// Gets or sets the name of the header to be used by the for the
+ /// outbound http requests.
+ ///
+ ///
+ /// If is present, the value of the header in the outbound calls will be the one
+ /// returned by the factory or, if the factory returns an empty value, the header will be omitted.
+ /// Otherwise, it will be the value of the header in the incoming request named as the key of this entry in
+ /// or, if missing or empty, the value specified in
+ /// or, if the is empty, it will not be
+ /// added to the outbound calls.
+ ///
+ public string OutboundHeaderName { get; set; }
+
+ ///
+ /// Gets or sets the default value to be used when the header in the incoming request is missing or empty.
+ ///
+ ///
+ /// This value is ignored when is set.
+ /// When it is it has no effect and, if the header is missing or empty in the
+ /// incoming request, it will not be added to outbound calls.
+ ///
+ public StringValues DefaultValue { get; set; }
+
+ ///
+ /// Gets or sets the value factory to be used.
+ /// It gets as input the inbound header name for this entry as defined in
+ /// and the of the current request.
+ ///
+ ///
+ /// When present, the factory is the only method used to set the value.
+ /// The factory should return to not add the header.
+ /// When not present, the value will be taken from the header in the incoming request named as the key of this
+ /// entry in or, if missing or empty, it will be the values
+ /// specified in or, if the is empty, the header will not
+ /// be added to the outbound calls.
+ /// Please note the factory is called only once per incoming request and the same value will be used by all the
+ /// outbound calls.
+ ///
+ public Func ValueFactory { get; set; }
+ }
+}
diff --git a/src/Middleware/HeaderPropagation/src/HeaderPropagationMessageHandler.cs b/src/Middleware/HeaderPropagation/src/HeaderPropagationMessageHandler.cs
new file mode 100644
index 000000000000..ff47b5ba48d7
--- /dev/null
+++ b/src/Middleware/HeaderPropagation/src/HeaderPropagationMessageHandler.cs
@@ -0,0 +1,67 @@
+// 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.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Options;
+using Microsoft.Extensions.Primitives;
+
+namespace Microsoft.AspNetCore.HeaderPropagation
+{
+ ///
+ /// A message handler for propagating headers collected by the to a outgoing request.
+ ///
+ public class HeaderPropagationMessageHandler : DelegatingHandler
+ {
+ private readonly HeaderPropagationValues _values;
+ private readonly HeaderPropagationOptions _options;
+
+ ///
+ /// 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 HeaderPropagationMessageHandler(IOptions options, HeaderPropagationValues values)
+ {
+ if (options == null)
+ {
+ throw new ArgumentNullException(nameof(options));
+ }
+
+ _options = options.Value;
+
+ _values = values ?? throw new ArgumentNullException(nameof(values));
+ }
+
+ ///
+ /// Sends an HTTP request to the inner handler to send to the server as an asynchronous operation, after adding
+ /// the propagated headers.
+ ///
+ ///
+ /// If an header with the same name is already present in the request, even if empty, the corresponding
+ /// propagated header will not be added.
+ ///
+ /// The HTTP request message to send to the server.
+ /// A cancellation token to cancel operation.
+ /// The task object representing the asynchronous operation.
+ protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
+ {
+ foreach ((var headerName, var entry) in _options.Headers)
+ {
+ var outputName = string.IsNullOrEmpty(entry?.OutboundHeaderName) ? headerName : entry.OutboundHeaderName;
+
+ if (!request.Headers.Contains(outputName) &&
+ _values.Headers.TryGetValue(headerName, out var values) &&
+ !StringValues.IsNullOrEmpty(values))
+ {
+ request.Headers.TryAddWithoutValidation(outputName, (string[])values);
+ }
+ }
+
+ return base.SendAsync(request, cancellationToken);
+ }
+ }
+}
diff --git a/src/Middleware/HeaderPropagation/src/HeaderPropagationMiddleware.cs b/src/Middleware/HeaderPropagation/src/HeaderPropagationMiddleware.cs
new file mode 100644
index 000000000000..4183faa0fa3d
--- /dev/null
+++ b/src/Middleware/HeaderPropagation/src/HeaderPropagationMiddleware.cs
@@ -0,0 +1,67 @@
+// 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.Net.Http;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Options;
+using Microsoft.Extensions.Primitives;
+
+namespace Microsoft.AspNetCore.HeaderPropagation
+{
+ ///
+ /// A Middleware for propagating headers to a .
+ ///
+ public class HeaderPropagationMiddleware
+ {
+ private readonly RequestDelegate _next;
+ private readonly HeaderPropagationOptions _options;
+ private readonly HeaderPropagationValues _values;
+
+ public HeaderPropagationMiddleware(RequestDelegate next, IOptions options, HeaderPropagationValues values)
+ {
+ _next = next ?? throw new ArgumentNullException(nameof(next));
+
+ if (options == null)
+ {
+ throw new ArgumentNullException(nameof(options));
+ }
+ _options = options.Value;
+
+ _values = values ?? throw new ArgumentNullException(nameof(values));
+ }
+
+ public Task Invoke(HttpContext context)
+ {
+ foreach ((var headerName, var entry) in _options.Headers)
+ {
+ var values = GetValues(headerName, entry, context);
+
+ if (!StringValues.IsNullOrEmpty(values))
+ {
+ _values.Headers.TryAdd(headerName, values);
+ }
+ }
+
+ return _next.Invoke(context);
+ }
+
+ private static StringValues GetValues(string headerName, HeaderPropagationEntry entry, HttpContext context)
+ {
+ if (entry?.ValueFactory != null)
+ {
+ return entry.ValueFactory(headerName, context);
+ }
+
+ if (context.Request.Headers.TryGetValue(headerName, out var values)
+ && !StringValues.IsNullOrEmpty(values))
+ {
+ return values;
+ }
+
+ return entry != null ? entry.DefaultValue : StringValues.Empty;
+ }
+ }
+}
diff --git a/src/Middleware/HeaderPropagation/src/HeaderPropagationOptions.cs b/src/Middleware/HeaderPropagation/src/HeaderPropagationOptions.cs
new file mode 100644
index 000000000000..ffd04583f0c9
--- /dev/null
+++ b/src/Middleware/HeaderPropagation/src/HeaderPropagationOptions.cs
@@ -0,0 +1,19 @@
+// 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;
+
+namespace Microsoft.AspNetCore.HeaderPropagation
+{
+ ///
+ /// Provides configuration for the .
+ ///
+ public class HeaderPropagationOptions
+ {
+ ///
+ /// Gets or sets the headers to be collected by the
+ /// and to be propagated by the .
+ ///
+ public IDictionary Headers { get; set; } = new Dictionary();
+ }
+}
diff --git a/src/Middleware/HeaderPropagation/src/HeaderPropagationValues.cs b/src/Middleware/HeaderPropagation/src/HeaderPropagationValues.cs
new file mode 100644
index 000000000000..8c3d9ab26d0a
--- /dev/null
+++ b/src/Middleware/HeaderPropagation/src/HeaderPropagationValues.cs
@@ -0,0 +1,29 @@
+// 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.Extensions.Primitives;
+
+namespace Microsoft.AspNetCore.HeaderPropagation
+{
+ ///
+ /// Contains the headers values for the .
+ ///
+ public class HeaderPropagationValues
+ {
+ private readonly static AsyncLocal> _headers = new AsyncLocal>();
+
+ ///
+ /// Gets the headers values collected by the from the current request that can be propagated.
+ ///
+ public IDictionary Headers
+ {
+ get
+ {
+ return _headers.Value ?? (_headers.Value = new Dictionary(StringComparer.OrdinalIgnoreCase));
+ }
+ }
+ }
+}
diff --git a/src/Middleware/HeaderPropagation/src/Microsoft.AspNetCore.HeaderPropagation.csproj b/src/Middleware/HeaderPropagation/src/Microsoft.AspNetCore.HeaderPropagation.csproj
new file mode 100644
index 000000000000..5ae4e64cc2f3
--- /dev/null
+++ b/src/Middleware/HeaderPropagation/src/Microsoft.AspNetCore.HeaderPropagation.csproj
@@ -0,0 +1,22 @@
+
+
+
+ ASP.NET Core middleware to propagate HTTP headers from the incoming request to the outgoing HTTP Client requests
+ netcoreapp3.0
+ true
+ $(NoWarn);CS1591
+ true
+ aspnetcore;httpclient
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Middleware/HeaderPropagation/test/HeaderPropagationIntegrationTest.cs b/src/Middleware/HeaderPropagation/test/HeaderPropagationIntegrationTest.cs
new file mode 100644
index 000000000000..207ececc6338
--- /dev/null
+++ b/src/Middleware/HeaderPropagation/test/HeaderPropagationIntegrationTest.cs
@@ -0,0 +1,109 @@
+// 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.Net;
+using System.Net.Http;
+using System.Net.Http.Headers;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.TestHost;
+using Microsoft.Extensions.DependencyInjection;
+using Xunit;
+
+namespace Microsoft.AspNetCore.HeaderPropagation.Tests
+{
+ public class HeaderPropagationIntegrationTest
+ {
+ [Fact]
+ public async Task HeaderInRequest_AddCorrectValue()
+ {
+ // Arrange
+ var handler = new SimpleHandler();
+ var builder = CreateBuilder(c =>
+ c.Headers.Add("in", new HeaderPropagationEntry
+ {
+ OutboundHeaderName = "out",
+ }),
+ handler);
+ var server = new TestServer(builder);
+ var client = server.CreateClient();
+
+ var request = new HttpRequestMessage();
+ request.Headers.Add("in", "test");
+
+ // Act
+ var response = await client.SendAsync(request);
+
+ // Assert
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ Assert.True(handler.Headers.Contains("out"));
+ Assert.Equal(new[] { "test" }, handler.Headers.GetValues("out"));
+ }
+
+ [Fact]
+ public void Builder_UseHeaderPropagation_Without_AddHeaderPropagation_Throws()
+ {
+ var builder = new WebHostBuilder()
+ .Configure(app =>
+ {
+ app.UseHeaderPropagation();
+ });
+
+ var exception = Assert.Throws(() => new TestServer(builder));
+ Assert.Equal(
+ "Unable to find the required services. Please add all the required services by calling 'IServiceCollection.AddHeaderPropagation' inside the call to 'ConfigureServices(...)' in the application startup code.",
+ exception.Message);
+ }
+
+ private IWebHostBuilder CreateBuilder(Action configure, HttpMessageHandler primaryHandler)
+ {
+ return new WebHostBuilder()
+ .Configure(app =>
+ {
+ app.UseHeaderPropagation();
+ app.UseMiddleware();
+ })
+ .ConfigureServices(services =>
+ {
+ services.AddHttpClient("example.com", c => c.BaseAddress = new Uri("http://example.com"))
+ .ConfigureHttpMessageHandlerBuilder(b =>
+ {
+ b.PrimaryHandler = primaryHandler;
+ })
+ .AddHeaderPropagation();
+ services.AddHeaderPropagation(configure);
+ });
+ }
+
+ private class SimpleHandler : DelegatingHandler
+ {
+ public HttpHeaders Headers { get; private set; }
+
+ protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
+ {
+ Headers = request.Headers;
+ return Task.FromResult(new HttpResponseMessage());
+ }
+ }
+
+ private class SimpleMiddleware
+ {
+ private readonly IHttpClientFactory _httpClientFactory;
+
+ public SimpleMiddleware(RequestDelegate next, IHttpClientFactory httpClientFactory)
+ {
+ _httpClientFactory = httpClientFactory;
+ }
+
+ public Task InvokeAsync(HttpContext _)
+ {
+ var client = _httpClientFactory.CreateClient("example.com");
+ return client.GetAsync("");
+ }
+ }
+ }
+}
diff --git a/src/Middleware/HeaderPropagation/test/HeaderPropagationMessageHandlerTest.cs b/src/Middleware/HeaderPropagation/test/HeaderPropagationMessageHandlerTest.cs
new file mode 100644
index 000000000000..320cc108f1a7
--- /dev/null
+++ b/src/Middleware/HeaderPropagation/test/HeaderPropagationMessageHandlerTest.cs
@@ -0,0 +1,180 @@
+// 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.Net.Http;
+using System.Net.Http.Headers;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Options;
+using Xunit;
+
+namespace Microsoft.AspNetCore.HeaderPropagation.Tests
+{
+ public class HeaderPropagationMessageHandlerTest
+ {
+ public HeaderPropagationMessageHandlerTest()
+ {
+ Handler = new SimpleHandler();
+
+ State = new HeaderPropagationValues();
+ Configuration = new HeaderPropagationOptions();
+
+ var headerPropagationMessageHandler =
+ new HeaderPropagationMessageHandler(Options.Create(Configuration), State)
+ {
+ InnerHandler = Handler
+ };
+
+ Client = new HttpClient(headerPropagationMessageHandler)
+ {
+ BaseAddress = new Uri("http://example.com")
+ };
+ }
+
+ private SimpleHandler Handler { get; }
+ public HeaderPropagationValues State { get; set; }
+ public HeaderPropagationOptions Configuration { get; set; }
+ public HttpClient Client { get; set; }
+
+ [Fact]
+ public async Task HeaderInState_AddCorrectValue()
+ {
+ // Arrange
+ Configuration.Headers.Add("in", new HeaderPropagationEntry { OutboundHeaderName = "out" });
+ State.Headers.Add("in", "test");
+
+ // Act
+ await Client.SendAsync(new HttpRequestMessage());
+
+ // Assert
+ Assert.True(Handler.Headers.Contains("out"));
+ Assert.Equal(new[] { "test" }, Handler.Headers.GetValues("out"));
+ }
+
+ [Fact]
+ public async Task HeaderInState_NoOutputName_UseInputName()
+ {
+ // Arrange
+ Configuration.Headers.Add("in", new HeaderPropagationEntry());
+ State.Headers.Add("in", "test");
+
+ // Act
+ await Client.SendAsync(new HttpRequestMessage());
+
+ // Assert
+ Assert.True(Handler.Headers.Contains("in"));
+ Assert.Equal(new[] { "test" }, Handler.Headers.GetValues("in"));
+ }
+
+ [Fact]
+ public async Task NoHeaderInState_DoesNotAddIt()
+ {
+ // Arrange
+ Configuration.Headers.Add("inout", new HeaderPropagationEntry());
+
+ // Act
+ await Client.SendAsync(new HttpRequestMessage());
+
+ // Assert
+ Assert.Empty(Handler.Headers);
+ }
+
+ [Fact]
+ public async Task HeaderInState_NotInOptions_DoesNotAddIt()
+ {
+ // Arrange
+ State.Headers.Add("inout", "test");
+
+ // Act
+ await Client.SendAsync(new HttpRequestMessage());
+
+ // Assert
+ Assert.Empty(Handler.Headers);
+ }
+
+ [Fact]
+ public async Task MultipleHeadersInState_AddsAll()
+ {
+ // Arrange
+ Configuration.Headers.Add("inout", new HeaderPropagationEntry());
+ Configuration.Headers.Add("another", new HeaderPropagationEntry());
+ State.Headers.Add("inout", "test");
+ State.Headers.Add("another", "test2");
+
+ // Act
+ await Client.SendAsync(new HttpRequestMessage());
+
+ // Assert
+ Assert.True(Handler.Headers.Contains("inout"));
+ Assert.True(Handler.Headers.Contains("another"));
+ Assert.Equal(new[] { "test" }, Handler.Headers.GetValues("inout"));
+ Assert.Equal(new[] { "test2" }, Handler.Headers.GetValues("another"));
+ }
+
+ [Theory]
+ [InlineData(null)]
+ [InlineData("")]
+ public async Task HeaderEmptyInState_DoNotAddIt(string headerValue)
+ {
+ // Arrange
+ Configuration.Headers.Add("inout", new HeaderPropagationEntry());
+ State.Headers.Add("inout", headerValue);
+
+ // Act
+ await Client.SendAsync(new HttpRequestMessage());
+
+ // Assert
+ Assert.False(Handler.Headers.Contains("inout"));
+ }
+
+ [Theory]
+ [InlineData("", new[] { "" })]
+ [InlineData(null, new[] { "" })]
+ [InlineData("42", new[] { "42" })]
+ public async Task HeaderInState_HeaderAlreadyInOutgoingRequest(string outgoingValue,
+ string[] expectedValues)
+ {
+ // Arrange
+ State.Headers.Add("inout", "test");
+ Configuration.Headers.Add("inout", new HeaderPropagationEntry());
+
+ var request = new HttpRequestMessage();
+ request.Headers.Add("inout", outgoingValue);
+
+ // Act
+ await Client.SendAsync(request);
+
+ // Assert
+ Assert.True(Handler.Headers.Contains("inout"));
+ Assert.Equal(expectedValues, Handler.Headers.GetValues("inout"));
+ }
+
+ [Fact]
+ public async Task NullEntryInConfiguration_AddCorrectValue()
+ {
+ // Arrange
+ Configuration.Headers.Add("in", null);
+ State.Headers.Add("in", "test");
+
+ // Act
+ await Client.SendAsync(new HttpRequestMessage());
+
+ // Assert
+ Assert.True(Handler.Headers.Contains("in"));
+ Assert.Equal(new[] { "test" }, Handler.Headers.GetValues("in"));
+ }
+
+ private class SimpleHandler : DelegatingHandler
+ {
+ public HttpHeaders Headers { get; private set; }
+
+ protected override Task SendAsync(HttpRequestMessage request,
+ CancellationToken cancellationToken)
+ {
+ Headers = request.Headers;
+ return Task.FromResult(new HttpResponseMessage());
+ }
+ }
+ }
+}
diff --git a/src/Middleware/HeaderPropagation/test/HeaderPropagationMiddlewareTest.cs b/src/Middleware/HeaderPropagation/test/HeaderPropagationMiddlewareTest.cs
new file mode 100644
index 000000000000..ad9e8e10d2ee
--- /dev/null
+++ b/src/Middleware/HeaderPropagation/test/HeaderPropagationMiddlewareTest.cs
@@ -0,0 +1,217 @@
+// 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.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Options;
+using Microsoft.Extensions.Primitives;
+using Xunit;
+
+namespace Microsoft.AspNetCore.HeaderPropagation.Tests
+{
+ public class HeaderPropagationMiddlewareTest
+ {
+ public HeaderPropagationMiddlewareTest()
+ {
+ Context = new DefaultHttpContext();
+ Next = ctx => Task.CompletedTask;
+ Configuration = new HeaderPropagationOptions();
+ State = new HeaderPropagationValues();
+ Middleware = new HeaderPropagationMiddleware(Next,
+ new OptionsWrapper(Configuration),
+ State);
+ }
+
+ public DefaultHttpContext Context { get; set; }
+ public RequestDelegate Next { get; set; }
+ public HeaderPropagationOptions Configuration { get; set; }
+ public HeaderPropagationValues State { get; set; }
+ public HeaderPropagationMiddleware Middleware { get; set; }
+
+ [Fact]
+ public async Task HeaderInRequest_AddCorrectValue()
+ {
+ // Arrange
+ Configuration.Headers.Add("in", new HeaderPropagationEntry());
+ Context.Request.Headers.Add("in", "test");
+
+ // Act
+ await Middleware.Invoke(Context);
+
+ // Assert
+ Assert.Contains("in", State.Headers.Keys);
+ Assert.Equal(new[] { "test" }, State.Headers["in"]);
+ }
+
+ [Fact]
+ public async Task NoHeaderInRequest_DoesNotAddIt()
+ {
+ // Arrange
+ Configuration.Headers.Add("in", new HeaderPropagationEntry());
+
+ // Act
+ await Middleware.Invoke(Context);
+
+ // Assert
+ Assert.Empty(State.Headers);
+ }
+
+ [Fact]
+ public async Task HeaderInRequest_NotInOptions_DoesNotAddIt()
+ {
+ // Arrange
+ Context.Request.Headers.Add("in", "test");
+
+ // Act
+ await Middleware.Invoke(Context);
+
+ // Assert
+ Assert.Empty(State.Headers);
+ }
+
+ [Fact]
+ public async Task MultipleHeadersInRequest_AddAllHeaders()
+ {
+ // Arrange
+ Configuration.Headers.Add("in", new HeaderPropagationEntry());
+ Configuration.Headers.Add("another", new HeaderPropagationEntry());
+ Context.Request.Headers.Add("in", "test");
+ Context.Request.Headers.Add("another", "test2");
+
+ // Act
+ await Middleware.Invoke(Context);
+
+ // Assert
+ Assert.Contains("in", State.Headers.Keys);
+ Assert.Equal(new[] { "test" }, State.Headers["in"]);
+ Assert.Contains("another", State.Headers.Keys);
+ Assert.Equal(new[] { "test2" }, State.Headers["another"]);
+ }
+
+ [Theory]
+ [InlineData(null)]
+ [InlineData("")]
+ public async Task HeaderEmptyInRequest_DoesNotAddIt(string headerValue)
+ {
+ // Arrange
+ Configuration.Headers.Add("in", new HeaderPropagationEntry());
+ Context.Request.Headers.Add("in", headerValue);
+
+ // Act
+ await Middleware.Invoke(Context);
+
+ // Assert
+ Assert.DoesNotContain("in", State.Headers.Keys);
+ }
+
+ [Theory]
+ [InlineData(new[] { "default" }, new[] { "default" })]
+ [InlineData(new[] { "default", "other" }, new[] { "default", "other" })]
+ public async Task NoHeaderInRequest_AddsDefaultValue(string[] defaultValues,
+ string[] expectedValues)
+ {
+ // Arrange
+ Configuration.Headers.Add("in", new HeaderPropagationEntry { DefaultValue = defaultValues });
+
+ // Act
+ await Middleware.Invoke(Context);
+
+ // Assert
+ Assert.Contains("in", State.Headers.Keys);
+ Assert.Equal(expectedValues, State.Headers["in"]);
+ }
+
+ [Theory]
+ [InlineData(new[] { "default" }, new[] { "default" })]
+ [InlineData(new[] { "default", "other" }, new[] { "default", "other" })]
+ public async Task UsesValueFactory(string[] factoryValues,
+ string[] expectedValues)
+ {
+ // Arrange
+ string receivedName = null;
+ HttpContext receivedContext = null;
+ Configuration.Headers.Add("in", new HeaderPropagationEntry
+ {
+ DefaultValue = "no",
+ ValueFactory = (name, ctx) =>
+ {
+ receivedName = name;
+ receivedContext = ctx;
+ return factoryValues;
+ }
+ });
+
+ // Act
+ await Middleware.Invoke(Context);
+
+ // Assert
+ Assert.Contains("in", State.Headers.Keys);
+ Assert.Equal(expectedValues, State.Headers["in"]);
+ Assert.Equal("in", receivedName);
+ Assert.Same(Context, receivedContext);
+ }
+
+ [Fact]
+ public async Task PreferValueFactory_OverDefaultValuesAndRequestHeader()
+ {
+ // Arrange
+ Configuration.Headers.Add("in", new HeaderPropagationEntry
+ {
+ DefaultValue = "no",
+ ValueFactory = (name, ctx) => "test"
+ });
+ Context.Request.Headers.Add("in", "no");
+
+ // Act
+ await Middleware.Invoke(Context);
+
+ // Assert
+ Assert.Contains("in", State.Headers.Keys);
+ Assert.Equal("test", State.Headers["in"]);
+ }
+
+ [Fact]
+ public async Task EmptyValuesFromValueFactory_DoesNotAddIt()
+ {
+ // Arrange
+ Configuration.Headers.Add("in", new HeaderPropagationEntry
+ {
+ ValueFactory = (name, ctx) => StringValues.Empty
+ });
+
+ // Act
+ await Middleware.Invoke(Context);
+
+ // Assert
+ Assert.DoesNotContain("in", State.Headers.Keys);
+ }
+
+ [Fact]
+ public async Task NullEntryInConfiguration_HeaderInRequest_AddsCorrectValue()
+ {
+ // Arrange
+ Configuration.Headers.Add("in", null);
+ Context.Request.Headers.Add("in", "test");
+
+ // Act
+ await Middleware.Invoke(Context);
+
+ // Assert
+ Assert.Contains("in", State.Headers.Keys);
+ Assert.Equal(new[] { "test" }, State.Headers["in"]);
+ }
+
+ [Fact]
+ public async Task NullEntryInConfiguration_NoHeaderInRequest_DoesNotAddHeader()
+ {
+ // Arrange
+ Configuration.Headers.Add("in", null);
+
+ // Act
+ await Middleware.Invoke(Context);
+
+ // Assert
+ Assert.DoesNotContain("in", State.Headers.Keys);
+ }
+ }
+}
diff --git a/src/Middleware/HeaderPropagation/test/Microsoft.AspNetCore.HeaderPropagation.Tests.csproj b/src/Middleware/HeaderPropagation/test/Microsoft.AspNetCore.HeaderPropagation.Tests.csproj
new file mode 100644
index 000000000000..39ce2903ea39
--- /dev/null
+++ b/src/Middleware/HeaderPropagation/test/Microsoft.AspNetCore.HeaderPropagation.Tests.csproj
@@ -0,0 +1,13 @@
+
+
+
+ netcoreapp3.0
+
+
+
+
+
+
+
+
+
diff --git a/src/Middleware/Middleware.sln b/src/Middleware/Middleware.sln
index 4eefacd361e5..27500743f636 100644
--- a/src/Middleware/Middleware.sln
+++ b/src/Middleware/Middleware.sln
@@ -269,6 +269,14 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.SpaSer
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.NodeServices.Tests", "NodeServices\test\Microsoft.AspNetCore.NodeServices.Tests.csproj", "{B04E9CB6-0D1C-4C21-B626-89B6926A491F}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "HeaderPropagation", "HeaderPropagation", "{0437D207-864E-429C-92B4-9D08D290188C}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.HeaderPropagation", "HeaderPropagation\src\Microsoft.AspNetCore.HeaderPropagation.csproj", "{D66BD4A3-DA19-413B-8FC5-4BCCFB03E084}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.HeaderPropagation.Tests", "HeaderPropagation\test\Microsoft.AspNetCore.HeaderPropagation.Tests.csproj", "{7E18FA09-5E08-4E41-836F-25C94B60C608}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{8CDBD9C6-96D8-4987-AFCD-D248FBC7F02D}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -1479,6 +1487,30 @@ Global
{B04E9CB6-0D1C-4C21-B626-89B6926A491F}.Release|x64.Build.0 = Release|Any CPU
{B04E9CB6-0D1C-4C21-B626-89B6926A491F}.Release|x86.ActiveCfg = Release|Any CPU
{B04E9CB6-0D1C-4C21-B626-89B6926A491F}.Release|x86.Build.0 = Release|Any CPU
+ {D66BD4A3-DA19-413B-8FC5-4BCCFB03E084}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {D66BD4A3-DA19-413B-8FC5-4BCCFB03E084}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {D66BD4A3-DA19-413B-8FC5-4BCCFB03E084}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {D66BD4A3-DA19-413B-8FC5-4BCCFB03E084}.Debug|x64.Build.0 = Debug|Any CPU
+ {D66BD4A3-DA19-413B-8FC5-4BCCFB03E084}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {D66BD4A3-DA19-413B-8FC5-4BCCFB03E084}.Debug|x86.Build.0 = Debug|Any CPU
+ {D66BD4A3-DA19-413B-8FC5-4BCCFB03E084}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {D66BD4A3-DA19-413B-8FC5-4BCCFB03E084}.Release|Any CPU.Build.0 = Release|Any CPU
+ {D66BD4A3-DA19-413B-8FC5-4BCCFB03E084}.Release|x64.ActiveCfg = Release|Any CPU
+ {D66BD4A3-DA19-413B-8FC5-4BCCFB03E084}.Release|x64.Build.0 = Release|Any CPU
+ {D66BD4A3-DA19-413B-8FC5-4BCCFB03E084}.Release|x86.ActiveCfg = Release|Any CPU
+ {D66BD4A3-DA19-413B-8FC5-4BCCFB03E084}.Release|x86.Build.0 = Release|Any CPU
+ {7E18FA09-5E08-4E41-836F-25C94B60C608}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {7E18FA09-5E08-4E41-836F-25C94B60C608}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {7E18FA09-5E08-4E41-836F-25C94B60C608}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {7E18FA09-5E08-4E41-836F-25C94B60C608}.Debug|x64.Build.0 = Debug|Any CPU
+ {7E18FA09-5E08-4E41-836F-25C94B60C608}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {7E18FA09-5E08-4E41-836F-25C94B60C608}.Debug|x86.Build.0 = Debug|Any CPU
+ {7E18FA09-5E08-4E41-836F-25C94B60C608}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {7E18FA09-5E08-4E41-836F-25C94B60C608}.Release|Any CPU.Build.0 = Release|Any CPU
+ {7E18FA09-5E08-4E41-836F-25C94B60C608}.Release|x64.ActiveCfg = Release|Any CPU
+ {7E18FA09-5E08-4E41-836F-25C94B60C608}.Release|x64.Build.0 = Release|Any CPU
+ {7E18FA09-5E08-4E41-836F-25C94B60C608}.Release|x86.ActiveCfg = Release|Any CPU
+ {7E18FA09-5E08-4E41-836F-25C94B60C608}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -1595,6 +1627,9 @@ Global
{D9D02772-1D53-45C3-B2CC-888F9978958C} = {D6FA4ABE-E685-4EDD-8B06-D8777E76B472}
{5D5B7E54-9323-498A-8983-E9BDFA3B2D07} = {D6FA4ABE-E685-4EDD-8B06-D8777E76B472}
{B04E9CB6-0D1C-4C21-B626-89B6926A491F} = {17B409B3-7EC6-49D8-847E-CFAA319E01B5}
+ {D66BD4A3-DA19-413B-8FC5-4BCCFB03E084} = {0437D207-864E-429C-92B4-9D08D290188C}
+ {7E18FA09-5E08-4E41-836F-25C94B60C608} = {8CDBD9C6-96D8-4987-AFCD-D248FBC7F02D}
+ {8CDBD9C6-96D8-4987-AFCD-D248FBC7F02D} = {0437D207-864E-429C-92B4-9D08D290188C}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {83786312-A93B-4BB4-AB06-7C6913A59AFA}