Skip to content

Commit

Permalink
Ported AspNetCore #9793 (#2)
Browse files Browse the repository at this point in the history
Improved usability
  • Loading branch information
alefranz authored May 8, 2019
1 parent ce666cf commit 895d14d
Show file tree
Hide file tree
Showing 17 changed files with 397 additions and 174 deletions.
18 changes: 11 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,17 @@ app.UseHeaderPropagation();

In `Startup.ConfigureServices` add the required services, eventually specifying a configuration action:
```csharp
services.AddHeaderPropagation(o => o.Headers.Add("User-Agent", new HeaderPropagationEntry
services.AddHeaderPropagation(o =>
{
DefaultValue = "Mozilla/5.0 (trust me, I'm really Mozilla!)",
}));
// propagate the header if present
o.Headers.Add("User-Agent");

// if still missing, set it with a value factory
o.Headers.Add("User-Agent", context => "Mozilla/5.0 (trust me, I'm really Mozilla!)");

// propagate the header if present, using a different name in the outbound request
o.Headers.Add("Accept-Language", "Lang");
});
```
If you are using the `HttpClientFactory`, add the `DelegatingHandler` to the client configuration using the `AddHeaderPropagation` extension method.

Expand All @@ -49,10 +56,7 @@ Each entry define the behaviour to propagate that header as follows:

- When present, the `ValueFactory` is the only method used to set the value. The factory should return `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 `HeaderPropagationOptions.Headers` or, if missing or empty, it will be the values
specified in `DefaultValue` or, if the `DefaultValue` is empty, the header will not
be added to the outbound calls.
- If multiple configurations for the same header are present, the first which returns a value wins.

Please note the factory is called only once per incoming request and the same value will be used by all the
outbound calls.
2 changes: 2 additions & 0 deletions azure-pipelines.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,7 @@ variables:
buildConfiguration: 'Release'

steps:
- script: dotnet build --configuration $(buildConfiguration)
displayName: 'dotnet build $(buildConfiguration)'
- script: dotnet test --configuration $(buildConfiguration)
displayName: 'dotnet test $(buildConfiguration)'
13 changes: 10 additions & 3 deletions samples/WebApplication/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,17 @@ public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

services.AddHeaderPropagation(o => o.Headers.Add("User-Agent", new HeaderPropagationEntry
services.AddHeaderPropagation(o =>
{
DefaultValue = "Mozilla/5.0 (trust me, I'm really Mozilla!)",
}));
// propagate the header if present
o.Headers.Add("User-Agent");

// if still missing, set it with a value factory
o.Headers.Add("User-Agent", context => "Mozilla/5.0 (trust me, I'm really Mozilla!)");

// propagate the header if present, using a different name in the outbound request
o.Headers.Add("Accept-Language", "Lang");
});

services.AddHttpClient<GitHubClient>(c =>
{
Expand Down
2 changes: 1 addition & 1 deletion samples/WebApplication/WebApplication.csproj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>netcoreapp2.2</TargetFramework>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@

using System;
using System.Net.Http;
using HeaderPropagation;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;

namespace Microsoft.AspNetCore.Builder
namespace HeaderPropagation
{
public static class HeaderPropagationApplicationBuilderExtensions
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using HeaderPropagation;
using Microsoft.Extensions.DependencyInjection;

namespace Microsoft.Extensions.DependencyInjection
namespace HeaderPropagation
{
public static class HeaderPropagationHttpClientBuilderExtensions
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@

using System;
using System.Net.Http;
using HeaderPropagation;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;

namespace Microsoft.Extensions.DependencyInjection
namespace HeaderPropagation
{
public static class HeaderPropagationServiceCollectionExtensions
{
Expand Down
54 changes: 54 additions & 0 deletions src/HeaderPropagationContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// 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 HeaderPropagation
{
/// <summary>
/// A context object for <see cref="HeaderPropagationEntry.ValueFilter"/> delegates.
/// </summary>
public struct HeaderPropagationContext
{
/// <summary>
/// Initializes a new instance of <see cref="HeaderPropagationContext"/> with the provided
/// <paramref name="httpContext"/>, <paramref name="headerName"/> and <paramref name="headerValue"/>.
/// </summary>
/// <param name="httpContext">The <see cref="Microsoft.AspNetCore.Http.HttpContext"/> associated with the current request.</param>
/// <param name="headerName">The header name.</param>
/// <param name="headerValue">The header value present in the current request.</param>
public HeaderPropagationContext(HttpContext httpContext, string headerName, StringValues headerValue)
{
if (httpContext == null)
{
throw new ArgumentNullException(nameof(httpContext));
}

if (headerName == null)
{
throw new ArgumentNullException(nameof(headerName));
}

HttpContext = httpContext;
HeaderName = headerName;
HeaderValue = headerValue;
}

/// <summary>
/// Gets the <see cref="Microsoft.AspNetCore.Http.HttpContext"/> associated with the current request.
/// </summary>
public HttpContext HttpContext { get; }

/// <summary>
/// Gets the header name.
/// </summary>
public string HeaderName { get; }

/// <summary>
/// Gets the header value from the current request.
/// </summary>
public StringValues HeaderValue { get; }
}
}
78 changes: 47 additions & 31 deletions src/HeaderPropagationEntry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
// 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 HeaderPropagation
Expand All @@ -13,44 +12,61 @@ namespace HeaderPropagation
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.
/// Creates a new <see cref="HeaderPropagationEntry"/> with the provided <paramref name="inboundHeaderName"/>,
/// <paramref name="outboundHeaderName"/>, and
/// </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; }
/// <param name="inboundHeaderName">
/// The name of the header to be captured by <see cref="HeaderPropagationMiddleware"/>.
/// </param>
/// <param name="outboundHeaderName">
/// The name of the header to be added by <see cref="HeaderPropagationMessageHandler"/>.
/// </param>
/// <param name="valueFilter">
/// A filter delegate that can be used to transform the header value. May be null.
/// </param>
public HeaderPropagationEntry(
string inboundHeaderName,
string outboundHeaderName,
Func<HeaderPropagationContext, StringValues> valueFilter)
{
if (inboundHeaderName == null)
{
throw new ArgumentNullException(nameof(inboundHeaderName));
}

if (outboundHeaderName == null)
{
throw new ArgumentNullException(nameof(outboundHeaderName));
}

InboundHeaderName = inboundHeaderName;
OutboundHeaderName = outboundHeaderName;
ValueFilter = valueFilter; // May be null
}

/// <summary>
/// Gets or sets the default value to be used when the header in the incoming request is missing or empty.
/// Gets the name of the header that will be captured by the <see cref="HeaderPropagationMiddleware"/>.
/// </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; }
public string InboundHeaderName { get; }

/// <summary>
/// Gets the name of the header to be used by the <see cref="HeaderPropagationMessageHandler"/> for the
/// outbound http requests.
/// </summary>
public string OutboundHeaderName { get; }

/// <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.
/// Gets or sets a filter delegate that can be used to transform the header value.
/// </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.
/// <para>
/// When present, the delegate will be evaluated once per request to provide the transformed
/// header value. The delegate will be called regardless of whether a header with the name
/// corresponding to <see cref="InboundHeaderName"/> is present in the request. If the result
/// of evaluating <see cref="ValueFilter"/> is null or empty, it will not be added to the propagated
/// values.
/// </para>
/// </remarks>
public Func<string, HttpContext, StringValues> ValueFactory { get; set; }
public Func<HeaderPropagationContext, StringValues> ValueFilter { get; }
}
}
108 changes: 108 additions & 0 deletions src/HeaderPropagationEntryCollection.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
// 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;
using Microsoft.Extensions.Primitives;

namespace HeaderPropagation
{
/// <summary>
/// A collection of <see cref="HeaderPropagationEntry"/> items.
/// </summary>
public sealed class HeaderPropagationEntryCollection : Collection<HeaderPropagationEntry>
{
/// <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"/>.
/// </summary>
/// <param name="headerName">The header name to be propagated.</param>
public void Add(string headerName)
{
if (headerName == null)
{
throw new ArgumentNullException(nameof(headerName));
}

Add(new HeaderPropagationEntry(headerName, headerName, valueFilter: null));
}

/// <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"/>.
/// </summary>
/// <param name="headerName">The header name to be propagated.</param>
/// <param name="valueFilter">
/// A filter delegate that can be used to transform the header value.
/// <see cref="HeaderPropagationEntry.ValueFilter"/>.
/// </param>
public void Add(string headerName, Func<HeaderPropagationContext, StringValues> valueFilter)
{
if (headerName == null)
{
throw new ArgumentNullException(nameof(headerName));
}

Add(new HeaderPropagationEntry(headerName, headerName, valueFilter));
}

/// <summary>
/// Adds an <see cref="HeaderPropagationEntry"/> that will use the provided <paramref name="inboundHeaderName"/>
/// and <paramref name="outboundHeaderName"/>.
/// </summary>
/// <param name="inboundHeaderName">
/// The name of the header to be captured by <see cref="HeaderPropagationMiddleware"/>.
/// </param>
/// <param name="outboundHeaderName">
/// The name of the header to be added by <see cref="HeaderPropagationMessageHandler"/>.
/// </param>
public void Add(string inboundHeaderName, string outboundHeaderName)
{
if (inboundHeaderName == null)
{
throw new ArgumentNullException(nameof(inboundHeaderName));
}

if (outboundHeaderName == null)
{
throw new ArgumentNullException(nameof(outboundHeaderName));
}

Add(new HeaderPropagationEntry(inboundHeaderName, outboundHeaderName, valueFilter: null));
}

/// <summary>
/// Adds an <see cref="HeaderPropagationEntry"/> that will use the provided <paramref name="inboundHeaderName"/>,
/// <paramref name="outboundHeaderName"/>, and <paramref name="valueFilter"/>.
/// </summary>
/// <param name="inboundHeaderName">
/// The name of the header to be captured by <see cref="HeaderPropagationMiddleware"/>.
/// </param>
/// <param name="outboundHeaderName">
/// The name of the header to be added by <see cref="HeaderPropagationMessageHandler"/>.
/// </param>
/// <param name="valueFilter">
/// A filter delegate that can be used to transform the header value.
/// <see cref="HeaderPropagationEntry.ValueFilter"/>.
/// </param>
public void Add(
string inboundHeaderName,
string outboundHeaderName,
Func<HeaderPropagationContext, StringValues> valueFilter)
{
if (inboundHeaderName == null)
{
throw new ArgumentNullException(nameof(inboundHeaderName));
}

if (outboundHeaderName == null)
{
throw new ArgumentNullException(nameof(outboundHeaderName));
}

Add(new HeaderPropagationEntry(inboundHeaderName, outboundHeaderName, valueFilter));
}
}
}
Loading

0 comments on commit 895d14d

Please sign in to comment.