Skip to content

Commit 8cca794

Browse files
committed
HeaderPropagation: add support for hosted services
1 parent 7d050e8 commit 8cca794

12 files changed

+387
-211
lines changed

src/Middleware/HeaderPropagation/ref/Microsoft.AspNetCore.HeaderPropagation.netcoreapp3.0.cs

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@ namespace Microsoft.AspNetCore.HeaderPropagation
1414
public readonly partial struct HeaderPropagationContext
1515
{
1616
private readonly object _dummy;
17-
public HeaderPropagationContext(Microsoft.AspNetCore.Http.HttpContext httpContext, string headerName, Microsoft.Extensions.Primitives.StringValues headerValue) { throw null; }
17+
public HeaderPropagationContext(System.Collections.Generic.IDictionary<string, Microsoft.Extensions.Primitives.StringValues> requestHeaders, string headerName, Microsoft.Extensions.Primitives.StringValues headerValue) { throw null; }
1818
public string HeaderName { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
1919
public Microsoft.Extensions.Primitives.StringValues HeaderValue { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
20-
public Microsoft.AspNetCore.Http.HttpContext HttpContext { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
20+
public System.Collections.Generic.IDictionary<string, Microsoft.Extensions.Primitives.StringValues> RequestHeaders { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
2121
}
2222
public partial class HeaderPropagationEntry
2323
{
@@ -58,19 +58,28 @@ public HeaderPropagationMessageHandlerOptions() { }
5858
}
5959
public partial class HeaderPropagationMiddleware
6060
{
61-
public HeaderPropagationMiddleware(Microsoft.AspNetCore.Http.RequestDelegate next, Microsoft.Extensions.Options.IOptions<Microsoft.AspNetCore.HeaderPropagation.HeaderPropagationOptions> options, Microsoft.AspNetCore.HeaderPropagation.HeaderPropagationValues values) { }
61+
public HeaderPropagationMiddleware(Microsoft.AspNetCore.Http.RequestDelegate next, Microsoft.AspNetCore.HeaderPropagation.IHeaderPropagationProcessor processor) { }
6262
public System.Threading.Tasks.Task Invoke(Microsoft.AspNetCore.Http.HttpContext context) { throw null; }
6363
}
6464
public partial class HeaderPropagationOptions
6565
{
6666
public HeaderPropagationOptions() { }
6767
public Microsoft.AspNetCore.HeaderPropagation.HeaderPropagationEntryCollection Headers { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
6868
}
69+
public partial class HeaderPropagationProcessor : Microsoft.AspNetCore.HeaderPropagation.IHeaderPropagationProcessor
70+
{
71+
public HeaderPropagationProcessor(Microsoft.Extensions.Options.IOptions<Microsoft.AspNetCore.HeaderPropagation.HeaderPropagationOptions> options, Microsoft.AspNetCore.HeaderPropagation.HeaderPropagationValues values) { }
72+
public void ProcessRequest(System.Collections.Generic.IDictionary<string, Microsoft.Extensions.Primitives.StringValues> requestHeaders) { }
73+
}
6974
public partial class HeaderPropagationValues
7075
{
7176
public HeaderPropagationValues() { }
7277
public System.Collections.Generic.IDictionary<string, Microsoft.Extensions.Primitives.StringValues> Headers { get { throw null; } set { } }
7378
}
79+
public partial interface IHeaderPropagationProcessor
80+
{
81+
void ProcessRequest(System.Collections.Generic.IDictionary<string, Microsoft.Extensions.Primitives.StringValues> requestHeaders);
82+
}
7483
}
7584
namespace Microsoft.Extensions.DependencyInjection
7685
{
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Net.Http;
4+
using System.Threading;
5+
using System.Threading.Tasks;
6+
using Microsoft.AspNetCore.HeaderPropagation;
7+
using Microsoft.Extensions.Hosting;
8+
using Microsoft.Extensions.Logging;
9+
using Microsoft.Extensions.Primitives;
10+
11+
namespace HeaderPropagationSample
12+
{
13+
public class SampleHostedService : IHostedService
14+
{
15+
private readonly IHttpClientFactory _httpClientFactory;
16+
private readonly HeaderPropagationProcessor _headerPropagationProcessor;
17+
private readonly ILogger _logger;
18+
19+
public SampleHostedService(IHttpClientFactory httpClientFactory, HeaderPropagationProcessor headerPropagationProcessor, ILogger<SampleHostedService> logger)
20+
{
21+
_httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory));
22+
_headerPropagationProcessor = headerPropagationProcessor ?? throw new ArgumentNullException(nameof(headerPropagationProcessor));
23+
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
24+
}
25+
26+
public Task StartAsync(CancellationToken cancellationToken)
27+
{
28+
return DoWorkAsync();
29+
}
30+
31+
private async Task DoWorkAsync()
32+
{
33+
_logger.LogInformation("Background Service is working.");
34+
35+
_headerPropagationProcessor.ProcessRequest(new Dictionary<string, StringValues>());
36+
var client = _httpClientFactory.CreateClient("test");
37+
var result = await client.GetAsync("http://localhost:62013/forwarded");
38+
39+
_logger.LogInformation("Background Service:\n{result}", result);
40+
}
41+
42+
public Task StopAsync(CancellationToken cancellationToken)
43+
{
44+
return Task.CompletedTask;
45+
}
46+
}
47+
}

src/Middleware/HeaderPropagation/samples/HeaderPropagationSample/Startup.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ public void ConfigureServices(IServiceCollection services)
4949
services
5050
.AddHttpClient("another")
5151
.AddHeaderPropagation(options => options.Headers.Add("X-BetaFeatures", "X-Experiments"));
52+
53+
services.AddHostedService<SampleHostedService>();
5254
}
5355

5456
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IHttpClientFactory clientFactory)

src/Middleware/HeaderPropagation/src/DependencyInjection/HeaderPropagationServiceCollectionExtensions.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ public static IServiceCollection AddHeaderPropagation(this IServiceCollection se
2323
}
2424

2525
services.TryAddSingleton<HeaderPropagationValues>();
26+
services.TryAddSingleton<IHeaderPropagationProcessor, HeaderPropagationProcessor>();
2627

2728
return services;
2829
}

src/Middleware/HeaderPropagation/src/HeaderPropagationContext.cs

Lines changed: 8 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System;
5-
using Microsoft.AspNetCore.Http;
5+
using System.Collections.Generic;
66
using Microsoft.Extensions.Primitives;
77

88
namespace Microsoft.AspNetCore.HeaderPropagation
@@ -14,32 +14,22 @@ public readonly struct HeaderPropagationContext
1414
{
1515
/// <summary>
1616
/// Initializes a new instance of <see cref="HeaderPropagationContext"/> with the provided
17-
/// <paramref name="httpContext"/>, <paramref name="headerName"/> and <paramref name="headerValue"/>.
17+
/// <paramref name="requestHeaders"/>, <paramref name="headerName"/> and <paramref name="headerValue"/>.
1818
/// </summary>
19-
/// <param name="httpContext">The <see cref="Http.HttpContext"/> associated with the current request.</param>
19+
/// <param name="requestHeaders">The headers associated with the current request.</param>
2020
/// <param name="headerName">The header name.</param>
2121
/// <param name="headerValue">The header value present in the current request.</param>
22-
public HeaderPropagationContext(HttpContext httpContext, string headerName, StringValues headerValue)
22+
public HeaderPropagationContext(IDictionary<string, StringValues> requestHeaders, string headerName, StringValues headerValue)
2323
{
24-
if (httpContext == null)
25-
{
26-
throw new ArgumentNullException(nameof(httpContext));
27-
}
28-
29-
if (headerName == null)
30-
{
31-
throw new ArgumentNullException(nameof(headerName));
32-
}
33-
34-
HttpContext = httpContext;
35-
HeaderName = headerName;
24+
RequestHeaders = requestHeaders ?? throw new ArgumentNullException(nameof(requestHeaders));
25+
HeaderName = headerName ?? throw new ArgumentNullException(nameof(headerName));
3626
HeaderValue = headerValue;
3727
}
3828

3929
/// <summary>
40-
/// Gets the <see cref="Http.HttpContext"/> associated with the current request.
30+
/// Gets the headers associated with the current request.
4131
/// </summary>
42-
public HttpContext HttpContext { get; }
32+
public IDictionary<string, StringValues> RequestHeaders { get; }
4333

4434
/// <summary>
4535
/// Gets the header name.

src/Middleware/HeaderPropagation/src/HeaderPropagationMessageHandler.cs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ namespace Microsoft.AspNetCore.HeaderPropagation
1515
/// </summary>
1616
public class HeaderPropagationMessageHandler : DelegatingHandler
1717
{
18-
private readonly HeaderPropagationValues _values;
1918
private readonly HeaderPropagationMessageHandlerOptions _options;
19+
private readonly HeaderPropagationValues _values;
2020

2121
/// <summary>
2222
/// Creates a new instance of the <see cref="HeaderPropagationMessageHandler"/>.
@@ -47,9 +47,10 @@ protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage reques
4747
if (captured == null)
4848
{
4949
var message =
50-
$"The {nameof(HeaderPropagationValues)}.{nameof(HeaderPropagationValues.Headers)} property has not been " +
51-
$"initialized. Register the header propagation middleware by adding 'app.{nameof(HeaderPropagationApplicationBuilderExtensions.UseHeaderPropagation)}() " +
52-
$"in the 'Configure(...)' method.";
50+
$"The {nameof(HeaderPropagationValues)}.{nameof(HeaderPropagationValues.Headers)} property has not been initialized. " +
51+
$"If using this {nameof(HttpClient)} as part of an http request, register the header propagation middleware by adding " +
52+
$"'app.{nameof(HeaderPropagationApplicationBuilderExtensions.UseHeaderPropagation)}() in the 'Configure(...)' method. " +
53+
$"Otherwise, use {nameof(HeaderPropagationProcessor)}.{nameof(HeaderPropagationProcessor.ProcessRequest)}() before using the {nameof(HttpClient)}.";
5354
throw new InvalidOperationException(message);
5455
}
5556

src/Middleware/HeaderPropagation/src/HeaderPropagationMiddleware.cs

Lines changed: 4 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,9 @@
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System;
5-
using System.Collections.Generic;
65
using System.Net.Http;
76
using System.Threading.Tasks;
87
using Microsoft.AspNetCore.Http;
9-
using Microsoft.Extensions.Options;
10-
using Microsoft.Extensions.Primitives;
118

129
namespace Microsoft.AspNetCore.HeaderPropagation
1310
{
@@ -17,61 +14,19 @@ namespace Microsoft.AspNetCore.HeaderPropagation
1714
public class HeaderPropagationMiddleware
1815
{
1916
private readonly RequestDelegate _next;
20-
private readonly HeaderPropagationOptions _options;
21-
private readonly HeaderPropagationValues _values;
17+
private readonly IHeaderPropagationProcessor _processor;
2218

23-
public HeaderPropagationMiddleware(RequestDelegate next, IOptions<HeaderPropagationOptions> options, HeaderPropagationValues values)
19+
public HeaderPropagationMiddleware(RequestDelegate next, IHeaderPropagationProcessor processor)
2420
{
2521
_next = next ?? throw new ArgumentNullException(nameof(next));
26-
27-
if (options == null)
28-
{
29-
throw new ArgumentNullException(nameof(options));
30-
}
31-
_options = options.Value;
32-
33-
_values = values ?? throw new ArgumentNullException(nameof(values));
22+
_processor = processor ?? throw new ArgumentNullException(nameof(processor));
3423
}
3524

3625
public Task Invoke(HttpContext context)
3726
{
38-
// We need to intialize the headers because the message handler will use this to detect misconfiguration.
39-
var headers = _values.Headers ??= new Dictionary<string, StringValues>(StringComparer.OrdinalIgnoreCase);
40-
41-
// Perf: avoid foreach since we don't define a struct enumerator.
42-
var entries = _options.Headers;
43-
for (var i = 0; i < entries.Count; i++)
44-
{
45-
var entry = entries[i];
46-
47-
// We intentionally process entries in order, and allow earlier entries to
48-
// take precedence over later entries when they have the same output name.
49-
if (!headers.ContainsKey(entry.CapturedHeaderName))
50-
{
51-
var value = GetValue(context, entry);
52-
if (!StringValues.IsNullOrEmpty(value))
53-
{
54-
headers.Add(entry.CapturedHeaderName, value);
55-
}
56-
}
57-
}
27+
_processor.ProcessRequest(context.Request.Headers);
5828

5929
return _next.Invoke(context);
6030
}
61-
62-
private static StringValues GetValue(HttpContext context, HeaderPropagationEntry entry)
63-
{
64-
context.Request.Headers.TryGetValue(entry.InboundHeaderName, out var value);
65-
if (entry.ValueFilter != null)
66-
{
67-
var filtered = entry.ValueFilter(new HeaderPropagationContext(context, entry.InboundHeaderName, value));
68-
if (!StringValues.IsNullOrEmpty(filtered))
69-
{
70-
value = filtered;
71-
}
72-
}
73-
74-
return value;
75-
}
7631
}
7732
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using Microsoft.Extensions.Options;
7+
using Microsoft.Extensions.Primitives;
8+
9+
namespace Microsoft.AspNetCore.HeaderPropagation
10+
{
11+
public class HeaderPropagationProcessor : IHeaderPropagationProcessor
12+
{
13+
private readonly HeaderPropagationOptions _options;
14+
private readonly HeaderPropagationValues _values;
15+
16+
public HeaderPropagationProcessor(IOptions<HeaderPropagationOptions> options, HeaderPropagationValues values)
17+
{
18+
if (options == null)
19+
{
20+
throw new ArgumentNullException(nameof(options));
21+
}
22+
_options = options.Value;
23+
24+
_values = values;
25+
}
26+
27+
public void ProcessRequest(IDictionary<string, StringValues> requestHeaders)
28+
{
29+
if (requestHeaders == null)
30+
{
31+
throw new ArgumentNullException(nameof(requestHeaders));
32+
}
33+
34+
if (_values.Headers != null)
35+
{
36+
var message =
37+
$"The {nameof(HeaderPropagationValues)}.{nameof(HeaderPropagationValues.Headers)} was already initialized. "
38+
+ $"Each invocation of {nameof(HeaderPropagationProcessor)}.{nameof(HeaderPropagationProcessor.ProcessRequest)}() must be in a separate async context.";
39+
throw new InvalidOperationException(message);
40+
}
41+
42+
// We need to intialize the headers because the message handler will use this to detect misconfiguration.
43+
var headers = _values.Headers = new Dictionary<string, StringValues>(StringComparer.OrdinalIgnoreCase);
44+
45+
// Perf: avoid foreach since we don't define a struct enumerator.
46+
var entries = _options.Headers;
47+
for (var i = 0; i < entries.Count; i++)
48+
{
49+
var entry = entries[i];
50+
51+
// We intentionally process entries in order, and allow earlier entries to
52+
// take precedence over later entries when they have the same output name.
53+
if (!headers.ContainsKey(entry.CapturedHeaderName))
54+
{
55+
var value = GetValue(requestHeaders, entry);
56+
if (!StringValues.IsNullOrEmpty(value))
57+
{
58+
headers.Add(entry.CapturedHeaderName, value);
59+
}
60+
}
61+
}
62+
}
63+
64+
private static StringValues GetValue(IDictionary<string, StringValues> requestHeaders, HeaderPropagationEntry entry)
65+
{
66+
requestHeaders.TryGetValue(entry.InboundHeaderName, out var value);
67+
if (entry.ValueFilter != null)
68+
{
69+
var filtered = entry.ValueFilter(new HeaderPropagationContext(requestHeaders, entry.InboundHeaderName, value));
70+
if (!StringValues.IsNullOrEmpty(filtered))
71+
{
72+
value = filtered;
73+
}
74+
}
75+
76+
return value;
77+
}
78+
}
79+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
namespace Microsoft.AspNetCore.HeaderPropagation
5+
{
6+
public interface IHeaderPropagationProcessor
7+
{
8+
void ProcessRequest(System.Collections.Generic.IDictionary<string, Extensions.Primitives.StringValues> requestHeaders);
9+
}
10+
}

src/Middleware/HeaderPropagation/test/HeaderPropagationIntegrationTest.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,9 @@ public async Task HeaderPropagation_WithoutMiddleware_Throws()
6363
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
6464
Assert.IsType<InvalidOperationException>(captured);
6565
Assert.Equal(
66-
"The HeaderPropagationValues.Headers property has not been initialized. Register the header propagation middleware " +
67-
"by adding 'app.UseHeaderPropagation() in the 'Configure(...)' method.",
66+
"The HeaderPropagationValues.Headers property has not been initialized. If using this HttpClient as part of an http request, " +
67+
"register the header propagation middleware by adding 'app.UseHeaderPropagation() in the 'Configure(...)' method. " +
68+
"Otherwise, use HeaderPropagationProcessor.ProcessRequest() before using the HttpClient.",
6869
captured.Message);
6970
}
7071

0 commit comments

Comments
 (0)