-
Notifications
You must be signed in to change notification settings - Fork 52
Add header propagation functionality #143
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
base: main
Are you sure you want to change the base?
Changes from all commits
ee3fb28
2633ddb
d2b31c0
8470656
36fa388
31b7404
cda0688
1142392
cd0cdcf
c92d60d
bd2e70b
799b893
6e18584
0efe9d7
8bee968
323a3f9
2cea55c
dfba641
766ef2c
03c53f2
99b3217
50ae6f9
9df31eb
559ba2c
d372b3e
3db053b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||
---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,78 @@ | ||||||||
// Copyright (c) Microsoft Corporation. All rights reserved. | ||||||||
// Licensed under the MIT License. | ||||||||
|
||||||||
using System; | ||||||||
using System.Collections.Generic; | ||||||||
using System.Reflection; | ||||||||
|
||||||||
namespace Microsoft.Agents.Core.HeaderPropagation; | ||||||||
|
||||||||
/// <summary> | ||||||||
/// Attribute to load headers for header propagation. | ||||||||
/// This attribute should be applied to classes that implement the <see cref="IHeaderPropagationAttribute"/> interface. | ||||||||
/// </summary> | ||||||||
[AttributeUsage(AttributeTargets.Class)] | ||||||||
public class HeaderPropagationAttribute : Attribute | ||||||||
{ | ||||||||
internal static void LoadHeaders() | ||||||||
{ | ||||||||
// Init newly loaded assemblies | ||||||||
AppDomain.CurrentDomain.AssemblyLoad += (s, o) => LoadHeadersAssembly(o.LoadedAssembly); | ||||||||
|
||||||||
// And all the ones we currently have loaded | ||||||||
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) | ||||||||
{ | ||||||||
LoadHeadersAssembly(assembly); | ||||||||
} | ||||||||
} | ||||||||
|
||||||||
private static void LoadHeadersAssembly(Assembly assembly) | ||||||||
{ | ||||||||
foreach (var type in GetLoadHeadersTypes(assembly)) | ||||||||
{ | ||||||||
#if !NETSTANDARD | ||||||||
if (!typeof(IHeaderPropagationAttribute).IsAssignableFrom(type)) | ||||||||
{ | ||||||||
throw new InvalidOperationException( | ||||||||
$"Type '{type.FullName}' is marked with [HeaderPropagation] but does not implement IHeaderPropagationAttribute."); | ||||||||
} | ||||||||
#endif | ||||||||
|
||||||||
var loadHeaders = type.GetMethod(nameof(LoadHeaders), BindingFlags.Static | BindingFlags.Public); | ||||||||
|
||||||||
if (loadHeaders == null) | ||||||||
{ | ||||||||
continue; | ||||||||
} | ||||||||
|
||||||||
loadHeaders.Invoke(assembly, [HeaderPropagationContext.HeadersToPropagate]); | ||||||||
} | ||||||||
Comment on lines
+48
to
+49
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When invoking a static method via reflection, the instance parameter should be 'null' rather than 'assembly'. Use 'loadHeaders.Invoke(null, new object[]{ HeaderPropagationContext.HeadersToPropagate })'.
Suggested change
Copilot uses AI. Check for mistakes. Positive FeedbackNegative Feedback |
||||||||
} | ||||||||
|
||||||||
private static IEnumerable<Type> GetLoadHeadersTypes(Assembly assembly) | ||||||||
{ | ||||||||
IList<Type> result = []; | ||||||||
|
||||||||
Type[] types; | ||||||||
|
||||||||
try | ||||||||
{ | ||||||||
types = assembly.GetTypes(); | ||||||||
} | ||||||||
catch (Exception ex) | ||||||||
{ | ||||||||
System.Diagnostics.Trace.WriteLine($"HeaderPropagationAttribute.GetLoadHeadersTypes: {ex.Message}"); | ||||||||
return result; | ||||||||
} | ||||||||
|
||||||||
foreach (Type type in types) | ||||||||
{ | ||||||||
if (type.GetCustomAttributes(typeof(HeaderPropagationAttribute), true).Length > 0) | ||||||||
{ | ||||||||
result.Add(type); | ||||||||
} | ||||||||
} | ||||||||
|
||||||||
return result; | ||||||||
} | ||||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
// Copyright (c) Microsoft Corporation. All rights reserved. | ||
// Licensed under the MIT License. | ||
|
||
using System; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using System.Threading; | ||
using Microsoft.Extensions.Primitives; | ||
|
||
namespace Microsoft.Agents.Core.HeaderPropagation; | ||
|
||
/// <summary> | ||
/// Shared context to manage request headers that will be used to propagate them in the <see cref="HeaderPropagationExtensions.AddHeaderPropagation"/>. | ||
/// </summary> | ||
public class HeaderPropagationContext() | ||
{ | ||
private static readonly AsyncLocal<IDictionary<string, StringValues>> _headersFromRequest = new(); | ||
private static HeaderPropagationEntryCollection _headersToPropagate = new(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I believe this is populated by virtue of the attribute handling, yes? There is a question about concurrency. We do something similar with the serializer options init (via attributes). I added a lock there because I wasn't sure about the exact module loading thread safety. But I'm not 100% that is required. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We added a lock to the Add operations in HeaderPropagationEntryCollection. Thanks! |
||
|
||
static HeaderPropagationContext() | ||
{ | ||
HeaderPropagationAttribute.LoadHeaders(); | ||
} | ||
|
||
/// <summary> | ||
/// Gets or sets the request headers that will be propagated based on what's inside the <see cref="HeadersToPropagate"/> property. | ||
/// </summary> | ||
public static IDictionary<string, StringValues> HeadersFromRequest | ||
{ | ||
get | ||
{ | ||
return _headersFromRequest.Value; | ||
} | ||
set | ||
{ | ||
// Create a copy to ensure headers are not modified by the original request. | ||
#if !NETSTANDARD | ||
var headers = value?.ToDictionary(StringComparer.InvariantCultureIgnoreCase); | ||
#else | ||
var headers = value?.ToDictionary(x => x.Key, x => x.Value, StringComparer.InvariantCultureIgnoreCase); | ||
#endif | ||
_headersFromRequest.Value = FilterHeaders(headers); | ||
} | ||
} | ||
|
||
/// <summary> | ||
/// Gets or sets the headers to allow during the propagation. | ||
/// </summary> | ||
public static HeaderPropagationEntryCollection HeadersToPropagate | ||
{ | ||
get | ||
{ | ||
return _headersToPropagate; | ||
} | ||
set | ||
{ | ||
_headersToPropagate = value ?? new(); | ||
} | ||
} | ||
|
||
/// <summary> | ||
/// Filters the request headers based on the keys provided in <see cref="HeadersToPropagate"/>. | ||
/// </summary> | ||
/// <param name="requestHeaders">Headers collection from an Http request.</param> | ||
/// <returns>Filtered headers.</returns> | ||
private static Dictionary<string, StringValues> FilterHeaders(Dictionary<string, StringValues> requestHeaders) | ||
{ | ||
var result = new Dictionary<string, StringValues>(); | ||
|
||
if (requestHeaders == null || requestHeaders.Count == 0) | ||
{ | ||
return result; | ||
} | ||
|
||
// Ensure the default headers are always set by overriding the LoadHeaders configuration. | ||
_headersToPropagate.Propagate("x-ms-correlation-id"); | ||
|
||
foreach (var header in HeadersToPropagate.Entries) | ||
{ | ||
var headerExists = requestHeaders.TryGetValue(header.Key, out var requestHeader); | ||
|
||
switch (header.Action) | ||
{ | ||
case HeaderPropagationEntryAction.Add: | ||
#if !NETSTANDARD | ||
result.TryAdd(header.Key, header.Value); | ||
#else | ||
result.Add(header.Key, header.Value); | ||
#endif | ||
break; | ||
case HeaderPropagationEntryAction.Append when headerExists: | ||
StringValues newValue = requestHeader.Concat(header.Value).ToArray(); | ||
result[header.Key] = newValue; | ||
break; | ||
case HeaderPropagationEntryAction.Propagate when headerExists: | ||
result[header.Key] = requestHeader; | ||
break; | ||
case HeaderPropagationEntryAction.Override: | ||
result[header.Key] = header.Value; | ||
break; | ||
} | ||
} | ||
|
||
return result; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
// Copyright (c) Microsoft Corporation. All rights reserved. | ||
// Licensed under the MIT License. | ||
|
||
using Microsoft.Extensions.Primitives; | ||
|
||
namespace Microsoft.Agents.Core.HeaderPropagation; | ||
|
||
/// <summary> | ||
/// Represents a single header entry used for header propagation. | ||
/// </summary> | ||
public class HeaderPropagationEntry | ||
{ | ||
/// <summary> | ||
/// Key of the header entry. | ||
/// </summary> | ||
public string Key { get; set; } = string.Empty; | ||
|
||
/// <summary> | ||
/// Value of the header entry. | ||
/// </summary> | ||
public StringValues Value { get; set; } = new StringValues(string.Empty); | ||
|
||
/// <summary> | ||
/// Action of the header entry (Add, Append, etc.). | ||
/// </summary> | ||
public HeaderPropagationEntryAction Action; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
// Copyright (c) Microsoft Corporation. All rights reserved. | ||
// Licensed under the MIT License. | ||
|
||
using System.Collections.Generic; | ||
using System.Linq; | ||
using Microsoft.Extensions.Primitives; | ||
|
||
namespace Microsoft.Agents.Core.HeaderPropagation; | ||
|
||
/// <summary> | ||
/// Represents a collection of all the header entries that are to be propagated to the outgoing request. | ||
/// </summary> | ||
public class HeaderPropagationEntryCollection | ||
{ | ||
private readonly Dictionary<string, HeaderPropagationEntry> _entries = []; | ||
|
||
private static readonly object _optionsLock = new object(); | ||
|
||
/// <summary> | ||
/// Gets the collection of header entries to be propagated to the outgoing request. | ||
/// </summary> | ||
public List<HeaderPropagationEntry> Entries | ||
{ | ||
get => [.. _entries.Select(x => x.Value)]; | ||
} | ||
|
||
/// <summary> | ||
/// Attempts to add a new header entry to the collection. | ||
/// </summary> | ||
/// <remarks> | ||
/// If the key already exists in the incoming request headers collection, it will be ignored. | ||
/// </remarks> | ||
/// <param name="key">The key of the element to add.</param> | ||
/// <param name="value">The value to add for the specified key.</param> | ||
public void Add(string key, StringValues value) | ||
{ | ||
lock (_optionsLock) | ||
{ | ||
_entries[key] = new HeaderPropagationEntry | ||
{ | ||
Key = key, | ||
Value = value, | ||
Action = HeaderPropagationEntryAction.Add | ||
}; | ||
} | ||
} | ||
|
||
/// <summary> | ||
/// Appends a new header value to an existing key. | ||
/// </summary> | ||
/// <remarks> | ||
/// If the key does not exist in the incoming request headers collection, it will be ignored. | ||
/// </remarks> | ||
/// <param name="key">The key of the element to add.</param> | ||
/// <param name="value">The value to add for the specified key.</param> | ||
public void Append(string key, StringValues value) | ||
{ | ||
lock (_optionsLock) | ||
{ | ||
StringValues newValue; | ||
|
||
if (_entries.TryGetValue(key, out var entry)) | ||
{ | ||
// If the key already exists, append the new value to the existing one. | ||
newValue = StringValues.Concat(entry.Value, value); | ||
} | ||
|
||
_entries[key] = new HeaderPropagationEntry | ||
{ | ||
Key = key, | ||
Value = !StringValues.IsNullOrEmpty(newValue) ? newValue : value, | ||
Action = HeaderPropagationEntryAction.Append | ||
}; | ||
} | ||
} | ||
|
||
/// <summary> | ||
/// Propagates the incoming request header value to the outgoing request. | ||
/// </summary> | ||
/// <remarks> | ||
/// If the key does not exist in the incoming request headers collection, it will be ignored. | ||
/// </remarks> | ||
/// <param name="key">The key of the element to add.</param> | ||
public void Propagate(string key) | ||
{ | ||
lock (_optionsLock) | ||
{ | ||
_entries[key] = new HeaderPropagationEntry | ||
{ | ||
Key = key, | ||
Action = HeaderPropagationEntryAction.Propagate | ||
}; | ||
} | ||
} | ||
|
||
/// <summary> | ||
/// Overrides the header value of an existing key. | ||
/// </summary> | ||
/// <remarks> | ||
/// If the key does not exist in the incoming request headers collection, it will add it. | ||
/// </remarks> | ||
/// <param name="key">The key of the element to add.</param> | ||
/// <param name="value">The value to add for the specified key.</param> | ||
public void Override(string key, StringValues value) | ||
{ | ||
lock (_optionsLock) | ||
{ | ||
_entries[key] = new HeaderPropagationEntry | ||
{ | ||
Key = key, | ||
Value = value, | ||
Action = HeaderPropagationEntryAction.Override | ||
}; | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Take a look at
SerializationInitAttribute.GetLoadOnInitTypes
. We had to add exception handling because some modules failed on this, and the app would crash. See if this is relevant here too.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks Tracy, we applied the changes.