HeaderPropagation: propagate incoming request headers to outgoing HTTP requests (#7921)

* Ported HeaderPropagation from aspnet/Extensions

* Introduced Middleware

* Refactored middleware logic

* Refactored builder extensions

* Copyright notice

* Test for friendly exception on Builder

* Fixed header name selection when no output name specified

* Set comparer for the dictionary of headers

* Refactored configuration as Dictionary

* Renamed state objects

* renamed OutboundHeaderName in configuration

* Changed DefaultValuesGenerator to ValueFactory

* Missing docs

* Removed AlwaysAdd and added tests for null entry in configuration

* Improved docs

* Update src/Middleware/HeaderPropagation/src/DependencyInjection/HeaderPropagationExtensions.cs

Co-Authored-By: alefranz <alessio@franceschelli.me>

* Moved dependency injection extensions

* DI: reused ServiceCollection extension in the HttpClientBuilder one

* Moved service registration

* Update src/Middleware/HeaderPropagation/src/HeaderPropagationEntry.cs

Co-Authored-By: alefranz <alessio@franceschelli.me>

* more docs

* Improved docs

* Update src/Middleware/HeaderPropagation/src/HeaderPropagationValues.cs

Co-Authored-By: alefranz <alessio@franceschelli.me>

* Fixed build

* Update eng/SharedFramework.Local.props

Co-Authored-By: alefranz <alessio@franceschelli.me>

* Updated tests for null config

* Reversed condition on HeaderPropagationMessageHandler as suggested

* Added docs for HeaderPropagationMessageHandler

* Changed proj to ship package to NuGet
This commit is contained in:
Alessio Franceschelli 2019-03-29 19:00:46 +00:00 committed by Ryan Nowak
parent f6130e8430
commit f28cf2bbc8
15 changed files with 938 additions and 0 deletions

View File

@ -73,6 +73,7 @@
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Diagnostics.Abstractions" ProjectPath="$(RepositoryRoot)src\Middleware\Diagnostics.Abstractions\src\Microsoft.AspNetCore.Diagnostics.Abstractions.csproj" RefProjectPath="$(RepositoryRoot)src\Middleware\Diagnostics.Abstractions\ref\Microsoft.AspNetCore.Diagnostics.Abstractions.csproj" />
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" ProjectPath="$(RepositoryRoot)src\Middleware\Diagnostics.EntityFrameworkCore\src\Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.csproj" RefProjectPath="$(RepositoryRoot)src\Middleware\Diagnostics.EntityFrameworkCore\ref\Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.csproj" />
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Diagnostics" ProjectPath="$(RepositoryRoot)src\Middleware\Diagnostics\src\Microsoft.AspNetCore.Diagnostics.csproj" RefProjectPath="$(RepositoryRoot)src\Middleware\Diagnostics\ref\Microsoft.AspNetCore.Diagnostics.csproj" />
<ProjectReferenceProvider Include="Microsoft.AspNetCore.HeaderPropagation" ProjectPath="$(RepositoryRoot)src\Middleware\HeaderPropagation\src\Microsoft.AspNetCore.HeaderPropagation.csproj" RefProjectPath="$(RepositoryRoot)src\Middleware\HeaderPropagation\ref\Microsoft.AspNetCore.HeaderPropagation.csproj" />
<ProjectReferenceProvider Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" ProjectPath="$(RepositoryRoot)src\Middleware\HealthChecks.EntityFrameworkCore\src\Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore.csproj" RefProjectPath="$(RepositoryRoot)src\Middleware\HealthChecks.EntityFrameworkCore\ref\Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore.csproj" />
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Diagnostics.HealthChecks" ProjectPath="$(RepositoryRoot)src\Middleware\HealthChecks\src\Microsoft.AspNetCore.Diagnostics.HealthChecks.csproj" RefProjectPath="$(RepositoryRoot)src\Middleware\HealthChecks\ref\Microsoft.AspNetCore.Diagnostics.HealthChecks.csproj" />
<ProjectReferenceProvider Include="Microsoft.AspNetCore.HostFiltering" ProjectPath="$(RepositoryRoot)src\Middleware\HostFiltering\src\Microsoft.AspNetCore.HostFiltering.csproj" RefProjectPath="$(RepositoryRoot)src\Middleware\HostFiltering\ref\Microsoft.AspNetCore.HostFiltering.csproj" />

View File

@ -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));
/// <summary>
/// Adds a middleware that collect headers to be propagated to a <see cref="HttpClient"/>.
/// </summary>
/// <param name="app">The <see cref="IApplicationBuilder"/> to add the middleware to.</param>
/// <returns>A reference to the <paramref name="app"/> after the operation has completed.</returns>
public static IApplicationBuilder UseHeaderPropagation(this IApplicationBuilder app)
{
if (app == null)
{
throw new ArgumentNullException(nameof(app));
}
if (app.ApplicationServices.GetService<HeaderPropagationValues>() == null)
{
throw new InvalidOperationException(_unableToFindServices);
}
return app.UseMiddleware<HeaderPropagationMiddleware>();
}
}
}

View File

@ -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
{
/// <summary>
/// Adds a message handler for propagating headers collected by the <see cref="HeaderPropagationMiddleware"/> to a outgoing request.
/// </summary>
/// <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)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
builder.Services.AddHeaderPropagation();
builder.AddHttpMessageHandler<HeaderPropagationMessageHandler>();
return builder;
}
}
}

View File

@ -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
{
/// <summary>
/// Adds services required for propagating headers to a <see cref="HttpClient"/>.
/// </summary>
/// <param name="services">The <see cref="IServiceCollection"/> to add the services to.</param>
/// <returns>The <see cref="IServiceCollection"/> so that additional calls can be chained.</returns>
public static IServiceCollection AddHeaderPropagation(this IServiceCollection services)
{
if (services == null)
{
throw new ArgumentNullException(nameof(services));
}
services.TryAddSingleton<HeaderPropagationValues>();
services.TryAddTransient<HeaderPropagationMessageHandler>();
return services;
}
/// <summary>
/// Adds services required for propagating headers to a <see cref="HttpClient"/>.
/// </summary>
/// <param name="services">The <see cref="IServiceCollection"/> to add the services to.</param>
/// <param name="configureOptions">A delegate used to configure the <see cref="HeaderPropagationOptions"/>.</param>
/// <returns>The <see cref="IServiceCollection"/> so that additional calls can be chained.</returns>
public static IServiceCollection AddHeaderPropagation(this IServiceCollection services, Action<HeaderPropagationOptions> configureOptions)
{
if (services == null)
{
throw new ArgumentNullException(nameof(services));
}
if (configureOptions == null)
{
throw new ArgumentNullException(nameof(configureOptions));
}
services.Configure(configureOptions);
services.AddHeaderPropagation();
return services;
}
}
}

View File

@ -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
{
/// <summary>
/// Define the configuration of a header for the <see cref="HeaderPropagationMiddleware"/>.
/// </summary>
public class HeaderPropagationEntry
{
/// <summary>
/// Gets or sets the name of the header to be used by the <see cref="HeaderPropagationMessageHandler"/> for the
/// outbound http requests.
/// </summary>
/// <remarks>
/// If <see cref="ValueFactory"/> 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
/// <see cref="HeaderPropagationOptions.Headers"/> or, if missing or empty, the value specified in
/// <see cref="DefaultValue"/> or, if the <see cref="DefaultValue"/> is empty, it will not be
/// added to the outbound calls.
/// </remarks>
public string OutboundHeaderName { get; set; }
/// <summary>
/// Gets or sets the default value to be used when the header in the incoming request is missing or empty.
/// </summary>
/// <remarks>
/// This value is ignored when <see cref="ValueFactory"/> is set.
/// When it is <see cref="StringValues.Empty"/> it has no effect and, if the header is missing or empty in the
/// incoming request, it will not be added to outbound calls.
/// </remarks>
public StringValues DefaultValue { get; set; }
/// <summary>
/// Gets or sets the value factory to be used.
/// It gets as input the inbound header name for this entry as defined in
/// <see cref="HeaderPropagationOptions.Headers"/> and the <see cref="HttpContext"/> of the current request.
/// </summary>
/// <remarks>
/// When present, the factory is the only method used to set the value.
/// The factory should return <see cref="StringValues.Empty"/> 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 <see cref="HeaderPropagationOptions.Headers"/> or, if missing or empty, it will be the values
/// specified in <see cref="DefaultValue"/> or, if the <see cref="DefaultValue"/> 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.
/// </remarks>
public Func<string, HttpContext, StringValues> ValueFactory { get; set; }
}
}

View File

@ -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
{
/// <summary>
/// A message handler for propagating headers collected by the <see cref="HeaderPropagationMiddleware"/> to a outgoing request.
/// </summary>
public class HeaderPropagationMessageHandler : DelegatingHandler
{
private readonly HeaderPropagationValues _values;
private readonly HeaderPropagationOptions _options;
/// <summary>
/// Creates a new instance of the <see cref="HeaderPropagationMessageHandler"/>.
/// </summary>
/// <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)
{
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}
_options = options.Value;
_values = values ?? throw new ArgumentNullException(nameof(values));
}
/// <summary>
/// Sends an HTTP request to the inner handler to send to the server as an asynchronous operation, after adding
/// the propagated headers.
/// </summary>
/// <remarks>
/// If an header with the same name is already present in the request, even if empty, the corresponding
/// propagated header will not be added.
/// </remarks>
/// <param name="request">The HTTP request message to send to the server.</param>
/// <param name="cancellationToken">A cancellation token to cancel operation.</param>
/// <returns>The task object representing the asynchronous operation.</returns>
protected override Task<HttpResponseMessage> 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);
}
}
}

View File

@ -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
{
/// <summary>
/// A Middleware for propagating headers to a <see cref="HttpClient"/>.
/// </summary>
public class HeaderPropagationMiddleware
{
private readonly RequestDelegate _next;
private readonly HeaderPropagationOptions _options;
private readonly HeaderPropagationValues _values;
public HeaderPropagationMiddleware(RequestDelegate next, IOptions<HeaderPropagationOptions> 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;
}
}
}

View File

@ -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
{
/// <summary>
/// Provides configuration for the <see cref="HeaderPropagationMiddleware"/>.
/// </summary>
public class HeaderPropagationOptions
{
/// <summary>
/// Gets or sets the headers to be collected by the <see cref="HeaderPropagationMiddleware"/>
/// and to be propagated by the <see cref="HeaderPropagationMessageHandler"/>.
/// </summary>
public IDictionary<string, HeaderPropagationEntry> Headers { get; set; } = new Dictionary<string, HeaderPropagationEntry>();
}
}

View File

@ -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
{
/// <summary>
/// Contains the headers values for the <see cref="HeaderPropagationMiddleware"/>.
/// </summary>
public class HeaderPropagationValues
{
private readonly static AsyncLocal<Dictionary<string, StringValues>> _headers = new AsyncLocal<Dictionary<string, StringValues>>();
/// <summary>
/// Gets the headers values collected by the <see cref="HeaderPropagationMiddleware"/> from the current request that can be propagated.
/// </summary>
public IDictionary<string, StringValues> Headers
{
get
{
return _headers.Value ?? (_headers.Value = new Dictionary<string, StringValues>(StringComparer.OrdinalIgnoreCase));
}
}
}
}

View File

@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Description>ASP.NET Core middleware to propagate HTTP headers from the incoming request to the outgoing HTTP Client requests</Description>
<TargetFramework>netcoreapp3.0</TargetFramework>
<IsShippingPackage>true</IsShippingPackage>
<NoWarn>$(NoWarn);CS1591</NoWarn>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<PackageTags>aspnetcore;httpclient</PackageTags>
</PropertyGroup>
<ItemGroup>
<InternalsVisibleTo Include="Microsoft.AspNetCore.HeaderPropagation.Tests" />
</ItemGroup>
<ItemGroup>
<Reference Include="Microsoft.AspNetCore.Http" />
<Reference Include="Microsoft.Extensions.Http" />
<Reference Include="Microsoft.Extensions.DependencyInjection" />
</ItemGroup>
</Project>

View File

@ -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<InvalidOperationException>(() => 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<HeaderPropagationOptions> configure, HttpMessageHandler primaryHandler)
{
return new WebHostBuilder()
.Configure(app =>
{
app.UseHeaderPropagation();
app.UseMiddleware<SimpleMiddleware>();
})
.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<HttpResponseMessage> 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("");
}
}
}
}

View File

@ -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<HttpResponseMessage> SendAsync(HttpRequestMessage request,
CancellationToken cancellationToken)
{
Headers = request.Headers;
return Task.FromResult(new HttpResponseMessage());
}
}
}
}

View File

@ -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<HeaderPropagationOptions>(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);
}
}
}

View File

@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netcoreapp3.0</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<Reference Include="Microsoft.Extensions.DependencyInjection" />
<Reference Include="Microsoft.AspNetCore.TestHost" />
<Reference Include="Microsoft.AspNetCore.HeaderPropagation" />
</ItemGroup>
</Project>

View File

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