Skip to content

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

Open
wants to merge 26 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
ee3fb28
Add header propagation functionality
sw-joelmut Mar 19, 2025
2633ddb
Improve overall implementation
sw-joelmut Mar 20, 2025
d2b31c0
fix possibly null reference
sw-joelmut Mar 20, 2025
8470656
Apply nit changes
sw-joelmut Mar 20, 2025
36fa388
Add attributes
sw-joelmut Apr 7, 2025
31b7404
Merge branch 'main' into southworks/add/header-propagation
sw-joelmut Apr 7, 2025
cda0688
fix merge main issues
sw-joelmut Apr 7, 2025
1142392
Improve comments
sw-joelmut Apr 9, 2025
cd0cdcf
Merge branch 'main' into southworks/add/header-propagation
sw-joelmut Apr 9, 2025
c92d60d
Merge remote-tracking branch 'origin/main' into southworks/add/header…
sw-joelmut Apr 10, 2025
bd2e70b
Merge branch 'main' into southworks/add/header-propagation
sw-joelmut Apr 24, 2025
799b893
Add Attribute interface to customize headers to propagate
sw-joelmut Apr 24, 2025
6e18584
Merge branch 'main' into southworks/add/header-propagation
May 5, 2025
0efe9d7
Fix issues with netstandard
sw-joelmut May 6, 2025
8bee968
Merge remote-tracking branch 'origin/main' into southworks/add/header…
sw-joelmut May 12, 2025
323a3f9
Merge branch 'main' into southworks/add/header-propagation
sw-joelmut May 14, 2025
2cea55c
Merge branch 'main' into southworks/add/header-propagation
MattB-msft May 22, 2025
dfba641
Merge branch 'main' into southworks/add/header-propagation
tracyboehrer May 28, 2025
766ef2c
Merge branch 'main' into southworks/add/header-propagation
ceciliaavila May 28, 2025
03c53f2
Add lock and exception handling in header propagation
ceciliaavila May 29, 2025
99b3217
Merge branch 'main' into southworks/add/header-propagation
ceciliaavila May 29, 2025
50ae6f9
Add header propagation tests
ceciliaavila May 30, 2025
9df31eb
Merge branch 'main' into southworks/add/header-propagation
MattB-msft Jun 3, 2025
559ba2c
Merge branch 'main' into southworks/add/header-propagation
Jun 8, 2025
d372b3e
Add unit test to check static property maintains isolation
sw-joelmut Jul 2, 2025
3db053b
Merge branch 'main' into southworks/add/header-propagation
sw-joelmut Jul 2, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions src/Microsoft.Agents.SDK.sln
Original file line number Diff line number Diff line change
@@ -129,6 +129,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Agents.Extensions
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Agents.Extensions.Teams.AI.Tests", "tests\Microsoft.Agents.Extensions.Teams.AI.Tests\Microsoft.Agents.Extensions.Teams.AI.Tests.csproj", "{3D6B6E6F-483B-45F3-B53C-93A5A72EE0BF}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Agents.Core.HeaderPropagation.Tests", "tests\Microsoft.Agents.Core.Tests\Microsoft.Agents.Core.HeaderPropagation.Tests.csproj", "{112E7BB8-2368-4061-90DC-3A8D6C7A43E6}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TeamsAgent", "samples\Teams\TeamsAgent\TeamsAgent.csproj", "{D6410977-B795-C315-CC94-C2482E84BB4A}"
EndProject
Global
@@ -705,6 +706,18 @@ Global
{3D6B6E6F-483B-45F3-B53C-93A5A72EE0BF}.Release|x64.Build.0 = Release|Any CPU
{3D6B6E6F-483B-45F3-B53C-93A5A72EE0BF}.Release|x86.ActiveCfg = Release|Any CPU
{3D6B6E6F-483B-45F3-B53C-93A5A72EE0BF}.Release|x86.Build.0 = Release|Any CPU
{112E7BB8-2368-4061-90DC-3A8D6C7A43E6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{112E7BB8-2368-4061-90DC-3A8D6C7A43E6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{112E7BB8-2368-4061-90DC-3A8D6C7A43E6}.Debug|x64.ActiveCfg = Debug|Any CPU
{112E7BB8-2368-4061-90DC-3A8D6C7A43E6}.Debug|x64.Build.0 = Debug|Any CPU
{112E7BB8-2368-4061-90DC-3A8D6C7A43E6}.Debug|x86.ActiveCfg = Debug|Any CPU
{112E7BB8-2368-4061-90DC-3A8D6C7A43E6}.Debug|x86.Build.0 = Debug|Any CPU
{112E7BB8-2368-4061-90DC-3A8D6C7A43E6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{112E7BB8-2368-4061-90DC-3A8D6C7A43E6}.Release|Any CPU.Build.0 = Release|Any CPU
{112E7BB8-2368-4061-90DC-3A8D6C7A43E6}.Release|x64.ActiveCfg = Release|Any CPU
{112E7BB8-2368-4061-90DC-3A8D6C7A43E6}.Release|x64.Build.0 = Release|Any CPU
{112E7BB8-2368-4061-90DC-3A8D6C7A43E6}.Release|x86.ActiveCfg = Release|Any CPU
{112E7BB8-2368-4061-90DC-3A8D6C7A43E6}.Release|x86.Build.0 = Release|Any CPU
{D6410977-B795-C315-CC94-C2482E84BB4A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D6410977-B795-C315-CC94-C2482E84BB4A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D6410977-B795-C315-CC94-C2482E84BB4A}.Debug|x64.ActiveCfg = Debug|Any CPU
@@ -782,6 +795,7 @@ Global
{D5202D4A-2F15-CE1B-F82C-2405C040EB14} = {674A812C-7287-4883-97F9-697D83750648}
{A44FB40F-1383-4AAB-A7F0-261CCFF172D8} = {927E4F54-6FBC-4390-BF64-BF3C1874C1AB}
{3D6B6E6F-483B-45F3-B53C-93A5A72EE0BF} = {AD743B78-D61F-4FBF-B620-FA83CE599A50}
{112E7BB8-2368-4061-90DC-3A8D6C7A43E6} = {AD743B78-D61F-4FBF-B620-FA83CE599A50}
{D6410977-B795-C315-CC94-C2482E84BB4A} = {674A812C-7287-4883-97F9-697D83750648}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
Original file line number Diff line number Diff line change
@@ -15,6 +15,7 @@
using Microsoft.Agents.Builder;
using Microsoft.Agents.Client.Errors;
using Microsoft.Agents.Core;
using Microsoft.Agents.Core.HeaderPropagation;
using Microsoft.Agents.Core.Models;
using Microsoft.Agents.Core.Serialization;
using Microsoft.Extensions.Logging;
@@ -255,6 +256,8 @@ private async Task<HttpResponseMessage> SendRequest(IActivity activity, bool use

using var httpClient = _httpClientFactory.CreateClient(nameof(HttpAgentClient));

httpClient.AddHeaderPropagation();

// Add the auth header to the HTTP request.
if (!useAnonymous)
{
Original file line number Diff line number Diff line change
@@ -5,6 +5,7 @@
using System.Net.Http;
using System.Threading.Tasks;
using System;
using Microsoft.Agents.Core.HeaderPropagation;

namespace Microsoft.Agents.Connector.RestClients
{
@@ -28,6 +29,7 @@ public async Task<HttpClient> GetHttpClientAsync()
}

httpClient.AddDefaultUserAgent();
httpClient.AddHeaderPropagation();

return httpClient;
}
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))
Copy link
Member

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.

Copy link
Collaborator

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.

{
#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
Copy link
Preview

Copilot AI Apr 25, 2025

Choose a reason for hiding this comment

The 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
loadHeaders.Invoke(assembly, [HeaderPropagationContext.HeadersToPropagate]);
}
loadHeaders.Invoke(null, new object[] { HeaderPropagationContext.HeadersToPropagate });

Copilot uses AI. Check for mistakes.

}

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();
Copy link
Member

Choose a reason for hiding this comment

The 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.

Copy link
Collaborator

Choose a reason for hiding this comment

The 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
};
}
}
}
Loading
Oops, something went wrong.
Loading
Oops, something went wrong.