Skip to content

Commit

Permalink
HeaderPropagation: propagate incoming request headers to outgoing HTT…
Browse files Browse the repository at this point in the history
…P 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
  • Loading branch information
alefranz authored and rynowak committed Mar 29, 2019
1 parent f6130e8 commit f28cf2b
Show file tree
Hide file tree
Showing 15 changed files with 938 additions and 0 deletions.
1 change: 1 addition & 0 deletions eng/ProjectReferences.props
Original file line number Diff line number Diff line change
Expand Up @@ -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" />
Expand Down
Original file line number Diff line number Diff line change
@@ -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>();
}
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
56 changes: 56 additions & 0 deletions src/Middleware/HeaderPropagation/src/HeaderPropagationEntry.cs
Original file line number Diff line number Diff line change
@@ -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; }
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
19 changes: 19 additions & 0 deletions src/Middleware/HeaderPropagation/src/HeaderPropagationOptions.cs
Original file line number Diff line number Diff line change
@@ -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>();
}
}
29 changes: 29 additions & 0 deletions src/Middleware/HeaderPropagation/src/HeaderPropagationValues.cs
Original file line number Diff line number Diff line change
@@ -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));
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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>
Loading

0 comments on commit f28cf2b

Please sign in to comment.