HeaderPropagation Middleware: configuration per client (#10096)

* HeaderPropagation Middleware: configuration per client

* Renamed fields

* Renamed fields

* Fix sample

* Addressed feedback, cleaned up and added tests

* Addressed feedback on HeaderPropagationHttpClientBuilderExtensions

* Updated reference assemblies
This commit is contained in:
Alessio Franceschelli 2019-05-17 22:16:44 +01:00 committed by Ryan Nowak
parent c43d713fd4
commit 320fadb756
17 changed files with 403 additions and 62 deletions

View File

@ -21,9 +21,9 @@ namespace Microsoft.AspNetCore.HeaderPropagation
}
public partial class HeaderPropagationEntry
{
public HeaderPropagationEntry(string inboundHeaderName, string outboundHeaderName, System.Func<Microsoft.AspNetCore.HeaderPropagation.HeaderPropagationContext, Microsoft.Extensions.Primitives.StringValues> valueFilter) { }
public HeaderPropagationEntry(string inboundHeaderName, string capturedHeaderName, System.Func<Microsoft.AspNetCore.HeaderPropagation.HeaderPropagationContext, Microsoft.Extensions.Primitives.StringValues> valueFilter) { }
public string CapturedHeaderName { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
public string InboundHeaderName { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
public string OutboundHeaderName { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
public System.Func<Microsoft.AspNetCore.HeaderPropagation.HeaderPropagationContext, Microsoft.Extensions.Primitives.StringValues> ValueFilter { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
}
public sealed partial class HeaderPropagationEntryCollection : System.Collections.ObjectModel.Collection<Microsoft.AspNetCore.HeaderPropagation.HeaderPropagationEntry>
@ -36,9 +36,26 @@ namespace Microsoft.AspNetCore.HeaderPropagation
}
public partial class HeaderPropagationMessageHandler : System.Net.Http.DelegatingHandler
{
public HeaderPropagationMessageHandler(Microsoft.Extensions.Options.IOptions<Microsoft.AspNetCore.HeaderPropagation.HeaderPropagationOptions> options, Microsoft.AspNetCore.HeaderPropagation.HeaderPropagationValues values) { }
public HeaderPropagationMessageHandler(Microsoft.AspNetCore.HeaderPropagation.HeaderPropagationMessageHandlerOptions options, Microsoft.AspNetCore.HeaderPropagation.HeaderPropagationValues values) { }
protected override System.Threading.Tasks.Task<System.Net.Http.HttpResponseMessage> SendAsync(System.Net.Http.HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) { throw null; }
}
public partial class HeaderPropagationMessageHandlerEntry
{
public HeaderPropagationMessageHandlerEntry(string capturedHeaderName, string outboundHeaderName) { }
public string CapturedHeaderName { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
public string OutboundHeaderName { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
}
public sealed partial class HeaderPropagationMessageHandlerEntryCollection : System.Collections.ObjectModel.Collection<Microsoft.AspNetCore.HeaderPropagation.HeaderPropagationMessageHandlerEntry>
{
public HeaderPropagationMessageHandlerEntryCollection() { }
public void Add(string headerName) { }
public void Add(string capturedHeaderName, string outboundHeaderName) { }
}
public partial class HeaderPropagationMessageHandlerOptions
{
public HeaderPropagationMessageHandlerOptions() { }
public Microsoft.AspNetCore.HeaderPropagation.HeaderPropagationMessageHandlerEntryCollection Headers { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
}
public partial class HeaderPropagationMiddleware
{
public HeaderPropagationMiddleware(Microsoft.AspNetCore.Http.RequestDelegate next, Microsoft.Extensions.Options.IOptions<Microsoft.AspNetCore.HeaderPropagation.HeaderPropagationOptions> options, Microsoft.AspNetCore.HeaderPropagation.HeaderPropagationValues values) { }
@ -60,6 +77,7 @@ namespace Microsoft.Extensions.DependencyInjection
public static partial class HeaderPropagationHttpClientBuilderExtensions
{
public static Microsoft.Extensions.DependencyInjection.IHttpClientBuilder AddHeaderPropagation(this Microsoft.Extensions.DependencyInjection.IHttpClientBuilder builder) { throw null; }
public static Microsoft.Extensions.DependencyInjection.IHttpClientBuilder AddHeaderPropagation(this Microsoft.Extensions.DependencyInjection.IHttpClientBuilder builder, System.Action<Microsoft.AspNetCore.HeaderPropagation.HeaderPropagationMessageHandlerOptions> configure) { throw null; }
}
public static partial class HeaderPropagationServiceCollectionExtensions
{

View File

@ -8,6 +8,7 @@
<Reference Include="Microsoft.AspNetCore.Diagnostics" />
<Reference Include="Microsoft.AspNetCore.HeaderPropagation" />
<Reference Include="Microsoft.AspNetCore.Hosting" />
<Reference Include="Microsoft.AspNetCore.Server.Kestrel" />
<Reference Include="Microsoft.AspNetCore.StaticFiles" />
<Reference Include="Microsoft.Extensions.Hosting" />
</ItemGroup>

View File

@ -15,6 +15,7 @@ namespace HeaderPropagationSample
Host.CreateDefaultBuilder(args)
.ConfigureWebHost(webBuilder =>
{
webBuilder.UseKestrel();
webBuilder.UseStartup<Startup>();
});
}

View File

@ -49,6 +49,9 @@ namespace HeaderPropagationSample
.AddHttpClient("test")
.AddHeaderPropagation();
services
.AddHttpClient("another")
.AddHeaderPropagation(options => options.Headers.Add("X-BetaFeatures", "X-Experiments"));
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IHttpClientFactory clientFactory)
@ -71,19 +74,23 @@ namespace HeaderPropagationSample
await context.Response.WriteAsync($"'/' Got Header '{header.Key}': {string.Join(", ", header.Value)}\r\n");
}
await context.Response.WriteAsync("Sending request to /forwarded\r\n");
var uri = UriHelper.BuildAbsolute(context.Request.Scheme, context.Request.Host, context.Request.PathBase, "/forwarded");
var client = clientFactory.CreateClient("test");
var response = await client.GetAsync(uri);
foreach (var header in response.RequestMessage.Headers)
var clientNames = new[] { "test", "another" };
foreach (var clientName in clientNames)
{
await context.Response.WriteAsync($"Sent Header '{header.Key}': {string.Join(", ", header.Value)}\r\n");
}
await context.Response.WriteAsync("Sending request to /forwarded\r\n");
await context.Response.WriteAsync("Got response\r\n");
await context.Response.WriteAsync(await response.Content.ReadAsStringAsync());
var uri = UriHelper.BuildAbsolute(context.Request.Scheme, context.Request.Host, context.Request.PathBase, "/forwarded");
var client = clientFactory.CreateClient(clientName);
var response = await client.GetAsync(uri);
foreach (var header in response.RequestMessage.Headers)
{
await context.Response.WriteAsync($"Sent Header '{header.Key}': {string.Join(", ", header.Value)}\r\n");
}
await context.Response.WriteAsync("Got response\r\n");
await context.Response.WriteAsync(await response.Content.ReadAsStringAsync());
}
});
endpoints.MapGet("/forwarded", async context =>

View File

@ -3,6 +3,7 @@
using System;
using Microsoft.AspNetCore.HeaderPropagation;
using Microsoft.Extensions.Options;
namespace Microsoft.Extensions.DependencyInjection
{
@ -11,6 +12,9 @@ namespace Microsoft.Extensions.DependencyInjection
/// <summary>
/// Adds a message handler for propagating headers collected by the <see cref="HeaderPropagationMiddleware"/> to a outgoing request.
/// </summary>
/// <remarks>
/// When using this method, all the configured headers will be applied to the outgoing HTTP requests.
/// </remarks>
/// <param name="builder">The <see cref="IHttpClientBuilder"/> to add the message handler to.</param>
/// <returns>The <see cref="IHttpClientBuilder"/> so that additional calls can be chained.</returns>
public static IHttpClientBuilder AddHeaderPropagation(this IHttpClientBuilder builder)
@ -22,7 +26,49 @@ namespace Microsoft.Extensions.DependencyInjection
builder.Services.AddHeaderPropagation();
builder.AddHttpMessageHandler<HeaderPropagationMessageHandler>();
builder.AddHttpMessageHandler(services =>
{
var options = new HeaderPropagationMessageHandlerOptions();
var middlewareOptions = services.GetRequiredService<IOptions<HeaderPropagationOptions>>();
for (var i = 0; i < middlewareOptions.Value.Headers.Count; i++)
{
var header = middlewareOptions.Value.Headers[i];
options.Headers.Add(header.CapturedHeaderName, header.CapturedHeaderName);
}
return new HeaderPropagationMessageHandler(options, services.GetRequiredService<HeaderPropagationValues>());
});
return builder;
}
/// <summary>
/// Adds a message handler for propagating headers collected by the <see cref="HeaderPropagationMiddleware"/> to a outgoing request,
/// explicitly specifying which headers to propagate.
/// </summary>
/// <remarks>This also allows to redefine the name to use for a header in the outgoing request.</remarks>
/// <param name="builder">The <see cref="IHttpClientBuilder"/> to add the message handler to.</param>
/// <param name="configure">A delegate used to configure the <see cref="HeaderPropagationMessageHandlerOptions"/>.</param>
/// <returns>The <see cref="IHttpClientBuilder"/> so that additional calls can be chained.</returns>
public static IHttpClientBuilder AddHeaderPropagation(this IHttpClientBuilder builder, Action<HeaderPropagationMessageHandlerOptions> configure)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
if (configure == null)
{
throw new ArgumentNullException(nameof(configure));
}
builder.Services.AddHeaderPropagation();
builder.AddHttpMessageHandler(services =>
{
var options = new HeaderPropagationMessageHandlerOptions();
configure(options);
return new HeaderPropagationMessageHandler(options, services.GetRequiredService<HeaderPropagationValues>());
});
return builder;
}

View File

@ -13,12 +13,12 @@ namespace Microsoft.AspNetCore.HeaderPropagation
{
/// <summary>
/// Creates a new <see cref="HeaderPropagationEntry"/> with the provided <paramref name="inboundHeaderName"/>,
/// <paramref name="outboundHeaderName"/>, and
/// <paramref name="capturedHeaderName"/> and <paramref name="valueFilter"/>.
/// </summary>
/// <param name="inboundHeaderName">
/// The name of the header to be captured by <see cref="HeaderPropagationMiddleware"/>.
/// </param>
/// <param name="outboundHeaderName">
/// <param name="capturedHeaderName">
/// The name of the header to be added by <see cref="HeaderPropagationMessageHandler"/>.
/// </param>
/// <param name="valueFilter">
@ -26,7 +26,7 @@ namespace Microsoft.AspNetCore.HeaderPropagation
/// </param>
public HeaderPropagationEntry(
string inboundHeaderName,
string outboundHeaderName,
string capturedHeaderName,
Func<HeaderPropagationContext, StringValues> valueFilter)
{
if (inboundHeaderName == null)
@ -34,13 +34,13 @@ namespace Microsoft.AspNetCore.HeaderPropagation
throw new ArgumentNullException(nameof(inboundHeaderName));
}
if (outboundHeaderName == null)
if (capturedHeaderName == null)
{
throw new ArgumentNullException(nameof(outboundHeaderName));
throw new ArgumentNullException(nameof(capturedHeaderName));
}
InboundHeaderName = inboundHeaderName;
OutboundHeaderName = outboundHeaderName;
CapturedHeaderName = capturedHeaderName;
ValueFilter = valueFilter; // May be null
}
@ -50,10 +50,10 @@ namespace Microsoft.AspNetCore.HeaderPropagation
public string InboundHeaderName { get; }
/// <summary>
/// Gets the name of the header to be used by the <see cref="HeaderPropagationMessageHandler"/> for the
/// Gets the name of the header to be used by default by the <see cref="HeaderPropagationMessageHandler"/> for the
/// outbound http requests.
/// </summary>
public string OutboundHeaderName { get; }
public string CapturedHeaderName { get; }
/// <summary>
/// Gets or sets a filter delegate that can be used to transform the header value.

View File

@ -15,7 +15,7 @@ namespace Microsoft.AspNetCore.HeaderPropagation
/// <summary>
/// Adds an <see cref="HeaderPropagationEntry"/> that will use <paramref name="headerName"/> as
/// the value of <see cref="HeaderPropagationEntry.InboundHeaderName"/> and
/// <see cref="HeaderPropagationEntry.OutboundHeaderName"/>.
/// <see cref="HeaderPropagationEntry.CapturedHeaderName"/>.
/// </summary>
/// <param name="headerName">The header name to be propagated.</param>
public void Add(string headerName)
@ -31,7 +31,7 @@ namespace Microsoft.AspNetCore.HeaderPropagation
/// <summary>
/// Adds an <see cref="HeaderPropagationEntry"/> that will use <paramref name="headerName"/> as
/// the value of <see cref="HeaderPropagationEntry.InboundHeaderName"/> and
/// <see cref="HeaderPropagationEntry.OutboundHeaderName"/>.
/// <see cref="HeaderPropagationEntry.CapturedHeaderName"/>.
/// </summary>
/// <param name="headerName">The header name to be propagated.</param>
/// <param name="valueFilter">

View File

@ -6,7 +6,6 @@ using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;
namespace Microsoft.AspNetCore.HeaderPropagation
@ -17,7 +16,7 @@ namespace Microsoft.AspNetCore.HeaderPropagation
public class HeaderPropagationMessageHandler : DelegatingHandler
{
private readonly HeaderPropagationValues _values;
private readonly HeaderPropagationOptions _options;
private readonly HeaderPropagationMessageHandlerOptions _options;
/// <summary>
/// Creates a new instance of the <see cref="HeaderPropagationMessageHandler"/>.
@ -25,15 +24,9 @@ namespace Microsoft.AspNetCore.HeaderPropagation
/// <param name="options">The options that define which headers are propagated.</param>
/// <param name="values">The values of the headers to be propagated populated by the
/// <see cref="HeaderPropagationMiddleware"/>.</param>
public HeaderPropagationMessageHandler(IOptions<HeaderPropagationOptions> options, HeaderPropagationValues values)
public HeaderPropagationMessageHandler(HeaderPropagationMessageHandlerOptions options, HeaderPropagationValues values)
{
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}
_options = options.Value;
_options = options ?? throw new ArgumentNullException(nameof(options));
_values = values ?? throw new ArgumentNullException(nameof(values));
}
@ -71,7 +64,7 @@ namespace Microsoft.AspNetCore.HeaderPropagation
if (!request.Headers.TryGetValues(entry.OutboundHeaderName, out var _) &&
!(hasContent && request.Content.Headers.TryGetValues(entry.OutboundHeaderName, out var _)))
{
if (captured.TryGetValue(entry.OutboundHeaderName, out var stringValues) &&
if (captured.TryGetValue(entry.CapturedHeaderName, out var stringValues) &&
!StringValues.IsNullOrEmpty(stringValues))
{
if (stringValues.Count == 1)

View File

@ -0,0 +1,51 @@
// 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;
namespace Microsoft.AspNetCore.HeaderPropagation
{
/// <summary>
/// Define the configuration of an header for the <see cref="HeaderPropagationMessageHandler"/>.
/// </summary>
public class HeaderPropagationMessageHandlerEntry
{
/// <summary>
/// Creates a new <see cref="HeaderPropagationMessageHandlerEntry"/> with the provided <paramref name="capturedHeaderName"/>
/// and <paramref name="outboundHeaderName"/>.
/// </summary>
/// <param name="capturedHeaderName">
/// The name of the header to be used to lookup the headers captured by the <see cref="HeaderPropagationMiddleware"/>.
/// </param>
/// <param name="outboundHeaderName">
/// The name of the header to be added to the outgoing http requests by the <see cref="HeaderPropagationMessageHandler"/>.
/// </param>
public HeaderPropagationMessageHandlerEntry(
string capturedHeaderName,
string outboundHeaderName)
{
if (capturedHeaderName == null)
{
throw new ArgumentNullException(nameof(capturedHeaderName));
}
if (outboundHeaderName == null)
{
throw new ArgumentNullException(nameof(outboundHeaderName));
}
CapturedHeaderName = capturedHeaderName;
OutboundHeaderName = outboundHeaderName;
}
/// <summary>
/// Gets the name of the header to be used to lookup the headers captured by the <see cref="HeaderPropagationMiddleware"/>.
/// </summary>
public string CapturedHeaderName { get; }
/// <summary>
/// Gets the name of the header to be added to the outgoing http requests by the <see cref="HeaderPropagationMessageHandler"/>.
/// </summary>
public string OutboundHeaderName { get; }
}
}

View File

@ -0,0 +1,57 @@
// 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.ObjectModel;
namespace Microsoft.AspNetCore.HeaderPropagation
{
/// <summary>
/// A collection of <see cref="HeaderPropagationMessageHandlerEntry"/> items.
/// </summary>
public sealed class HeaderPropagationMessageHandlerEntryCollection : Collection<HeaderPropagationMessageHandlerEntry>
{
/// <summary>
/// Adds an <see cref="HeaderPropagationMessageHandlerEntry"/> that will use <paramref name="headerName"/> as
/// the value of <see cref="HeaderPropagationMessageHandlerEntry.CapturedHeaderName"/> and
/// <see cref="HeaderPropagationMessageHandlerEntry.OutboundHeaderName"/>.
/// </summary>
/// <param name="headerName">
/// The name of the header to be added by the <see cref="HeaderPropagationMessageHandler"/>.
/// </param>
public void Add(string headerName)
{
if (headerName == null)
{
throw new ArgumentNullException(nameof(headerName));
}
Add(new HeaderPropagationMessageHandlerEntry(headerName, headerName));
}
/// <summary>
/// Adds an <see cref="HeaderPropagationMessageHandlerEntry"/> that will use the provided <paramref name="capturedHeaderName"/>
/// and <paramref name="outboundHeaderName"/>.
/// </summary>
/// <param name="capturedHeaderName">
/// The name of the header captured by the <see cref="HeaderPropagationMiddleware"/>.
/// </param>
/// <param name="outboundHeaderName">
/// The name of the header to be added by the <see cref="HeaderPropagationMessageHandler"/>.
/// </param>
public void Add(string capturedHeaderName, string outboundHeaderName)
{
if (capturedHeaderName == null)
{
throw new ArgumentNullException(nameof(capturedHeaderName));
}
if (outboundHeaderName == null)
{
throw new ArgumentNullException(nameof(outboundHeaderName));
}
Add(new HeaderPropagationMessageHandlerEntry(capturedHeaderName, outboundHeaderName));
}
}
}

View File

@ -0,0 +1,22 @@
// 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
{
/// <summary>
/// Provides configuration for the <see cref="HeaderPropagationMessageHandler"/>.
/// </summary>
public class HeaderPropagationMessageHandlerOptions
{
/// <summary>
/// Gets or sets the headers to be propagated by the <see cref="HeaderPropagationMessageHandler"/>.
/// </summary>
/// <remarks>
/// If <see cref="Headers"/> is empty, all the headers captured by the <see cref="HeaderPropagationMiddleware"/> are propagated.
/// Entries in <see cref="Headers"/> are processed in order while adding headers inside
/// <see cref="HeaderPropagationMessageHandler"/>. This can cause an earlier entry to take precedence
/// over a later entry if they have the same <see cref="HeaderPropagationMessageHandlerEntry.OutboundHeaderName"/>.
/// </remarks>
public HeaderPropagationMessageHandlerEntryCollection Headers { get; set; } = new HeaderPropagationMessageHandlerEntryCollection();
}
}

View File

@ -46,12 +46,12 @@ namespace Microsoft.AspNetCore.HeaderPropagation
// We intentionally process entries in order, and allow earlier entries to
// take precedence over later entries when they have the same output name.
if (!headers.ContainsKey(entry.OutboundHeaderName))
if (!headers.ContainsKey(entry.CapturedHeaderName))
{
var value = GetValue(context, entry);
if (!StringValues.IsNullOrEmpty(value))
{
headers.Add(entry.OutboundHeaderName, value);
headers.Add(entry.CapturedHeaderName, value);
}
}
}

View File

@ -13,9 +13,9 @@ namespace Microsoft.AspNetCore.HeaderPropagation
/// and to be propagated by the <see cref="HeaderPropagationMessageHandler"/>.
/// </summary>
/// <remarks>
/// Entries in <see cref="Headers"/> are processes in order while capturing headers inside
/// Entries in <see cref="Headers"/> are processed in order while capturing headers inside
/// <see cref="HeaderPropagationMiddleware"/>. This can cause an earlier entry to take precedence
/// over a later entry if they have the same <see cref="HeaderPropagationEntry.OutboundHeaderName"/>.
/// over a later entry if they have the same <see cref="HeaderPropagationEntry.CapturedHeaderName"/>.
/// </remarks>
public HeaderPropagationEntryCollection Headers { get; set; } = new HeaderPropagationEntryCollection();
}

View File

@ -21,7 +21,7 @@ namespace Microsoft.AspNetCore.HeaderPropagation
/// that can be propagated.
/// </summary>
/// <remarks>
/// The keys of <see cref="Headers"/> correspond to <see cref="HeaderPropagationEntry.OutboundHeaderName"/>.
/// The keys of <see cref="Headers"/> correspond to <see cref="HeaderPropagationEntry.CapturedHeaderName"/>.
/// </remarks>
public IDictionary<string, StringValues> Headers
{

View File

@ -91,6 +91,35 @@ namespace Microsoft.AspNetCore.HeaderPropagation.Tests
Assert.Equal(new[] { "test" }, handler.Headers.GetValues("out"));
}
[Fact]
public async Task MultipleHeaders_HeadersInRequest_AddAllHeaders()
{
// Arrange
var handler = new SimpleHandler();
var builder = CreateBuilder(c =>
{
c.Headers.Add("first");
c.Headers.Add("second");
},
handler);
var server = new TestServer(builder);
var client = server.CreateClient();
var request = new HttpRequestMessage();
request.Headers.Add("first", "value");
request.Headers.Add("second", "other");
// Act
var response = await client.SendAsync(request);
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.True(handler.Headers.Contains("first"));
Assert.Equal(new[] { "value" }, handler.Headers.GetValues("first"));
Assert.True(handler.Headers.Contains("second"));
Assert.Equal(new[] { "other" }, handler.Headers.GetValues("second"));
}
[Fact]
public void Builder_UseHeaderPropagation_Without_AddHeaderPropagation_Throws()
{
@ -106,7 +135,31 @@ namespace Microsoft.AspNetCore.HeaderPropagation.Tests
exception.Message);
}
private IWebHostBuilder CreateBuilder(Action<HeaderPropagationOptions> configure, HttpMessageHandler primaryHandler)
[Fact]
public async Task HeaderInRequest_OverrideHeaderPerClient_AddCorrectValue()
{
// Arrange
var handler = new SimpleHandler();
var builder = CreateBuilder(
c => c.Headers.Add("in", "out"),
handler,
c => c.Headers.Add("out", "different"));
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("different"));
Assert.Equal(new[] { "test" }, handler.Headers.GetValues("different"));
}
private IWebHostBuilder CreateBuilder(Action<HeaderPropagationOptions> configure, HttpMessageHandler primaryHandler, Action<HeaderPropagationMessageHandlerOptions> configureClient = null)
{
return new WebHostBuilder()
.Configure(app =>
@ -116,13 +169,21 @@ namespace Microsoft.AspNetCore.HeaderPropagation.Tests
})
.ConfigureServices(services =>
{
services.AddHttpClient("example.com", c => c.BaseAddress = new Uri("http://example.com"))
services.AddHeaderPropagation(configure);
var client = services.AddHttpClient("example.com", c => c.BaseAddress = new Uri("http://example.com"))
.ConfigureHttpMessageHandlerBuilder(b =>
{
b.PrimaryHandler = primaryHandler;
})
.AddHeaderPropagation();
services.AddHeaderPropagation(configure);
});
if (configureClient != null)
{
client.AddHeaderPropagation(configureClient);
}
else
{
client.AddHeaderPropagation();
}
});
}

View File

@ -0,0 +1,34 @@
// 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 Xunit;
namespace Microsoft.AspNetCore.HeaderPropagation.Tests
{
public class HeaderPropagationMessageHandlerEntryCollectionTest
{
[Fact]
public void Add_SingleValue_UseValueForBothProperties()
{
var collection = new HeaderPropagationMessageHandlerEntryCollection();
collection.Add("foo");
Assert.Single(collection);
var entry = collection[0];
Assert.Equal("foo", entry.CapturedHeaderName);
Assert.Equal("foo", entry.OutboundHeaderName);
}
[Fact]
public void Add_BothValues_UseCorrectValues()
{
var collection = new HeaderPropagationMessageHandlerEntryCollection();
collection.Add("foo", "bar");
Assert.Single(collection);
var entry = collection[0];
Assert.Equal("foo", entry.CapturedHeaderName);
Assert.Equal("bar", entry.OutboundHeaderName);
}
}
}

View File

@ -7,7 +7,6 @@ using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;
using Xunit;
@ -22,10 +21,10 @@ namespace Microsoft.AspNetCore.HeaderPropagation.Tests
State = new HeaderPropagationValues();
State.Headers = new Dictionary<string, StringValues>(StringComparer.OrdinalIgnoreCase);
Configuration = new HeaderPropagationOptions();
Configuration = new HeaderPropagationMessageHandlerOptions();
var headerPropagationMessageHandler =
new HeaderPropagationMessageHandler(Options.Create(Configuration), State)
new HeaderPropagationMessageHandler(Configuration, State)
{
InnerHandler = Handler
};
@ -38,14 +37,14 @@ namespace Microsoft.AspNetCore.HeaderPropagation.Tests
private SimpleHandler Handler { get; }
public HeaderPropagationValues State { get; set; }
public HeaderPropagationOptions Configuration { get; set; }
public HeaderPropagationMessageHandlerOptions Configuration { get; set; }
public HttpClient Client { get; set; }
[Fact]
public async Task HeaderInState_AddCorrectValue()
{
// Arrange
Configuration.Headers.Add("in", "out");
Configuration.Headers.Add("out");
State.Headers.Add("out", "test");
// Act
@ -60,7 +59,7 @@ namespace Microsoft.AspNetCore.HeaderPropagation.Tests
public async Task HeaderInState_WithMultipleValues_AddAllValues()
{
// Arrange
Configuration.Headers.Add("in", "out");
Configuration.Headers.Add("out");
State.Headers.Add("out", new[] { "one", "two" });
// Act
@ -74,8 +73,8 @@ namespace Microsoft.AspNetCore.HeaderPropagation.Tests
[Fact]
public async Task HeaderInState_RequestWithContent_ContentHeaderPresent_DoesNotAddIt()
{
Configuration.Headers.Add("in", "Content-Type");
State.Headers.Add("in", "test");
Configuration.Headers.Add("Content-Type");
State.Headers.Add("Content-Type", "test");
// Act
await Client.SendAsync(new HttpRequestMessage() { Content = new StringContent("test") });
@ -88,7 +87,7 @@ namespace Microsoft.AspNetCore.HeaderPropagation.Tests
[Fact]
public async Task HeaderInState_RequestWithContent_ContentHeaderNotPresent_AddValue()
{
Configuration.Headers.Add("in", "Content-Language");
Configuration.Headers.Add("Content-Language");
State.Headers.Add("Content-Language", "test");
// Act
@ -102,7 +101,7 @@ namespace Microsoft.AspNetCore.HeaderPropagation.Tests
[Fact]
public async Task HeaderInState_WithMultipleValues_RequestWithContent_ContentHeaderNotPresent_AddAllValues()
{
Configuration.Headers.Add("in", "Content-Language");
Configuration.Headers.Add("Content-Language");
State.Headers.Add("Content-Language", new[] { "one", "two" });
// Act
@ -114,18 +113,18 @@ namespace Microsoft.AspNetCore.HeaderPropagation.Tests
}
[Fact]
public async Task HeaderInState_NoOutputName_UseInputName()
public async Task HeaderInState_WithOutboundName_UseOutboundName()
{
// Arrange
Configuration.Headers.Add("in");
State.Headers.Add("in", "test");
Configuration.Headers.Add("state", "out");
State.Headers.Add("state", "test");
// Act
await Client.SendAsync(new HttpRequestMessage());
// Assert
Assert.True(Handler.Headers.Contains("in"));
Assert.Equal(new[] { "test" }, Handler.Headers.GetValues("in"));
Assert.True(Handler.Headers.Contains("out"));
Assert.Equal(new[] { "test" }, Handler.Headers.GetValues("out"));
}
[Fact]
@ -193,7 +192,7 @@ namespace Microsoft.AspNetCore.HeaderPropagation.Tests
[InlineData("", new[] { "" })]
[InlineData(null, new[] { "" })]
[InlineData("42", new[] { "42" })]
public async Task HeaderInState_HeaderAlreadyInOutgoingRequest(string outgoingValue,
public async Task HeaderInState_HeaderAlreadyInOutgoingRequest_DoesNotOverrideIt(string outgoingValue,
string[] expectedValues)
{
// Arrange
@ -211,6 +210,57 @@ namespace Microsoft.AspNetCore.HeaderPropagation.Tests
Assert.Equal(expectedValues, Handler.Headers.GetValues("inout"));
}
[Fact]
public async Task HeaderInState_HeaderTwiceInOptions_DoesNotAddItTwice()
{
// Arrange
State.Headers.Add("name", "value");
Configuration.Headers.Add("name");
Configuration.Headers.Add("name");
// Act
await Client.SendAsync(new HttpRequestMessage());
// Assert
Assert.True(Handler.Headers.Contains("name"));
Assert.Equal(new[] { "value" }, Handler.Headers.GetValues("name"));
}
[Fact]
public async Task HeaderInState_HeaderTwiceInOptionsWithDifferentNames_AddsBoth()
{
// Arrange
State.Headers.Add("name", "value");
Configuration.Headers.Add("name");
Configuration.Headers.Add("name", "other");
// Act
await Client.SendAsync(new HttpRequestMessage());
// Assert
Assert.True(Handler.Headers.Contains("name"));
Assert.Equal(new[] { "value" }, Handler.Headers.GetValues("name"));
Assert.True(Handler.Headers.Contains("other"));
Assert.Equal(new[] { "value" }, Handler.Headers.GetValues("name"));
}
[Fact]
public async Task TwoHeadersInState_BothHeadersInOptionsWithSameName_AddsFirst()
{
// Arrange
State.Headers.Add("name", "value");
State.Headers.Add("other", "override");
Configuration.Headers.Add("name");
Configuration.Headers.Add("other", "name");
// Act
await Client.SendAsync(new HttpRequestMessage());
// Assert
Assert.True(Handler.Headers.Contains("name"));
Assert.Equal(new[] { "value" }, Handler.Headers.GetValues("name"));
}
private class SimpleHandler : DelegatingHandler
{
public HttpHeaders Headers { get; private set; }