Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 32 commits into from Mar 29, 2019
Merged
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
15c0684
Ported HeaderPropagation from aspnet/Extensions
alefranz Feb 25, 2019
3729951
Introduced Middleware
alefranz Feb 25, 2019
91a4970
Refactored middleware logic
alefranz Mar 10, 2019
eef7598
Refactored builder extensions
alefranz Mar 10, 2019
27bdc5c
Copyright notice
alefranz Mar 10, 2019
3262f6d
Test for friendly exception on Builder
alefranz Mar 10, 2019
d455d66
Fixed header name selection when no output name specified
alefranz Mar 10, 2019
cb6e356
Set comparer for the dictionary of headers
alefranz Mar 10, 2019
8a8b005
Merge branch 'master' into header-propagation
alefranz Mar 13, 2019
b0b67ef
Refactored configuration as Dictionary
alefranz Mar 13, 2019
3edbc95
Renamed state objects
alefranz Mar 13, 2019
0582f29
renamed OutboundHeaderName in configuration
alefranz Mar 13, 2019
f883f68
Changed DefaultValuesGenerator to ValueFactory
alefranz Mar 14, 2019
5af6d22
Missing docs
alefranz Mar 14, 2019
1d4c2f0
Removed AlwaysAdd and added tests for null entry in configuration
alefranz Mar 14, 2019
3e51de0
Improved docs
alefranz Mar 17, 2019
559491f
Merge branch 'master' into header-propagation
alefranz Mar 27, 2019
1619bb4
Update src/Middleware/HeaderPropagation/src/DependencyInjection/Heade…
rynowak Mar 27, 2019
4b414b0
Moved dependency injection extensions
alefranz Mar 27, 2019
ae94fe0
DI: reused ServiceCollection extension in the HttpClientBuilder one
alefranz Mar 27, 2019
78d4eec
Moved service registration
alefranz Mar 27, 2019
d06b743
Update src/Middleware/HeaderPropagation/src/HeaderPropagationEntry.cs
rynowak Mar 27, 2019
adc5962
more docs
alefranz Mar 27, 2019
158faf6
Improved docs
alefranz Mar 27, 2019
aeee1fe
Update src/Middleware/HeaderPropagation/src/HeaderPropagationValues.cs
rynowak Mar 27, 2019
9706db1
Fixed build
alefranz Mar 27, 2019
c4de7f7
Update eng/SharedFramework.Local.props
anurse Mar 27, 2019
6e40837
Updated tests for null config
alefranz Mar 27, 2019
85d67e0
Reversed condition on HeaderPropagationMessageHandler as suggested
alefranz Mar 27, 2019
adab2b9
Added docs for HeaderPropagationMessageHandler
alefranz Mar 27, 2019
61df695
Changed proj to ship package to NuGet
alefranz Mar 28, 2019
689f594
Merge branch 'master' into header-propagation
alefranz Mar 29, 2019
File filter...
Filter file types
Jump to…
Jump to file or symbol
Failed to load files and symbols.

Always

Just for now

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

This conversation was marked as resolved by rynowak

This comment has been minimized.

Copy link
@rynowak

rynowak Mar 21, 2019

Member

I think somewhere in the docs it would be nice to have a comment like:

If ValueFactory is null and DefaultValue is null then....
If ValueFactory is non-null then...
If ValueFactory is null and DefaultValues is non-null then...

Basically, somewhere we should document how users should think about the precedence of these options.

/// <summary>
/// Gets or sets the default value to be used when the header in the incoming request is missing or empty.
/// </summary>
This conversation was marked as resolved by rynowak

This comment has been minimized.

Copy link
@rynowak

rynowak Mar 21, 2019

Member

It would be nice if the docs explain how this interacts with ValueFactory.

/// <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>
This conversation was marked as resolved by rynowak

This comment has been minimized.

Copy link
@rynowak

rynowak Mar 21, 2019

Member

docs should cover what happens when don't have a value factory.

public Func<string, HttpContext, StringValues> ValueFactory { get; set; }
This conversation was marked as resolved by rynowak

This comment has been minimized.

Copy link
@alefranz

alefranz Mar 17, 2019

Author Contributor

@rynowak In changing this to a ValueFactory, I've also added the inbound header name passed to the value factory, what do you think?
It simplify reusing the same value factory for multiple headers, but it's not really a strong argument.

This comment has been minimized.

Copy link
@rynowak

rynowak Mar 21, 2019

Member

I think this is fine.

}
}
@@ -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)
This conversation was marked as resolved by rynowak

This comment has been minimized.

Copy link
@rynowak

rynowak Mar 21, 2019

Member

docs here (even if it doesn't explain much)

This comment has been minimized.

Copy link
@alefranz

alefranz Mar 28, 2019

Author Contributor

done

{
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)
This conversation was marked as resolved by rynowak

This comment has been minimized.

Copy link
@rynowak

rynowak Mar 21, 2019

Member

docs here (even if it doesn't explain much)

This comment has been minimized.

Copy link
@alefranz

alefranz Mar 28, 2019

Author Contributor

done

{
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);

This comment has been minimized.

Copy link
@benaadams

benaadams Apr 10, 2019

Contributor

The cast (string[])values will cause an array allocation for most headers (which are only one)

Better would be to check the count and then switch on overload e.g.

var count = values.Count;
if (count == 1)
{
    request.Headers.TryAddWithoutValidation(outputName, (string)values);
}
else if (count > 1)
{
    request.Headers.TryAddWithoutValidation(outputName, (string[])values);
}

This comment has been minimized.

Copy link
@benaadams

benaadams Apr 10, 2019

Contributor

Also HttpClient has an odd behaviour where some headers are rejected from the Headers collection and instead need to be added to the Content.Headers collection (if there is a content); so might need to be more like:

var hasContent = request.Content != null;

// ...

var count = values.Count;
if (count == 1)
{
    var value = (string)values;
    if (!request.Headers.TryAddWithoutValidation(outputName, value) && hasContent)
    {
        request.Content.Headers.TryAddWithoutValidation(outputName, value);
    }
} 
else ...

from: https://github.com/aspnet/Benchmarks/blob/7b1a5e986f06ae79270b13866e701cc6cb5bb395/src/Proxy/ProxyExtensions.cs#L31-L34

This comment has been minimized.

Copy link
@alefranz

alefranz Apr 11, 2019

Author Contributor

Thanks @benaadams !
I'll raise a PR to address this by the end of the week.

}
}

return base.SendAsync(request, cancellationToken);
}
}
}
@@ -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)
This conversation was marked as resolved by rynowak

This comment has been minimized.

Copy link
@rynowak

This comment has been minimized.

Copy link
@alefranz

alefranz Mar 27, 2019

Author Contributor

I haven't added documentation as I haven't seen it documented in other middlewares and I think it makes sense given it should never been instantiated directly or called Invoke, even if they are both public.

{
_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)
This conversation was marked as resolved by rynowak

This comment has been minimized.

Copy link
@rynowak
{
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)
This conversation was marked as resolved by rynowak

This comment has been minimized.

Copy link
@rynowak

rynowak Mar 21, 2019

Member

What are the cases where entry could be null?

Are you just hedging against someone adding a null entry to _options.Headers? I think it would be better to let that crash with an nullref.

This comment has been minimized.

Copy link
@alefranz

alefranz Mar 27, 2019

Author Contributor

I think this could be a valid scenario, now that the header inbound name is the key of the dictionary and not in the entry.
A null configuration entry will correspond to using the same name outbound and having no factory nor default value, see last 2 tests: https://github.com/aspnet/AspNetCore/pull/7921/files#diff-e52877730c5607887b50f212d490c111R189

I guess I'll have to document this, unless you think an entry should be mandatory (eventually with all fields as null)

This comment has been minimized.

Copy link
@rynowak

rynowak Mar 28, 2019

Member

I'm happy to leave this for now. When this gets merged I'm going to spend a little time playing with it.

{
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;
}
}
}
@@ -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>();
}
}
This conversation was marked as resolved by rynowak

This comment has been minimized.

Copy link
@rynowak

rynowak Mar 1, 2019

Member

The drawback of using a list is that you can express duplicates (same outbound name). I'm not sure that it's really a problem that needs to be solved.

This comment has been minimized.

Copy link
@alefranz

alefranz Mar 17, 2019

Author Contributor

I had decided to change it to a dictionary, as overriding arrays in config is always a bit messy. Also the ordering doesn't add much value imo. However I used the input name as key so it doesn't address the potential issue you raised.
The reason why I used the input name is that the input name is the one relevant in the middleware. The output name is only used by the Handler, is optional and I believe it should be moved to be moved to the handler configuration as part as handling the ability to specify which handler to include per client (in a following PR).

@@ -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));
}
}
}
}
@@ -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>
ProTip! Use n and p to navigate between commits in a pull request.
You can’t perform that action at this time.