From ee3fb28db12e870a15617fa18102aeec61b81efb Mon Sep 17 00:00:00 2001 From: Joel Mut Date: Wed, 19 Mar 2025 15:48:23 +0000 Subject: [PATCH 1/9] Add header propagation functionality --- .../AspNetCore/AgentsHttpClientFactory.cs | 32 ++++++++++ .../ActivityTaskQueue.cs | 5 +- .../ActivityWithClaims.cs | 6 ++ .../HostedActivityService.cs | 4 ++ .../IActivityTaskQueue.cs | 4 +- .../Hosting/AspNetCore/CloudAdapter.cs | 4 +- .../HeaderPropagationContext.cs | 63 ++++++++++++++++++ .../HeaderPropagationMiddleware.cs | 44 +++++++++++++ .../HeaderPropagationOptions.cs | 18 ++++++ .../AspNetCore/ServiceCollectionExtensions.cs | 64 +++++++++++++++++++ 10 files changed, 239 insertions(+), 5 deletions(-) create mode 100644 src/libraries/Hosting/AspNetCore/AgentsHttpClientFactory.cs create mode 100644 src/libraries/Hosting/AspNetCore/HeaderPropagation/HeaderPropagationContext.cs create mode 100644 src/libraries/Hosting/AspNetCore/HeaderPropagation/HeaderPropagationMiddleware.cs create mode 100644 src/libraries/Hosting/AspNetCore/HeaderPropagation/HeaderPropagationOptions.cs diff --git a/src/libraries/Hosting/AspNetCore/AgentsHttpClientFactory.cs b/src/libraries/Hosting/AspNetCore/AgentsHttpClientFactory.cs new file mode 100644 index 00000000..428b1221 --- /dev/null +++ b/src/libraries/Hosting/AspNetCore/AgentsHttpClientFactory.cs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.Agents.Hosting.AspNetCore.HeaderPropagation; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Net.Http; + +namespace Microsoft.Agents.Hosting.AspNetCore +{ + /// + /// Custom implementation of an IHttpClientFactory that allows customizing the settings in the instance. + /// + /// Service provider used to gather DI related classes. + /// Existing HttpClientFactory to use in this implementation. + public class AgentsHttpClientFactory(IServiceProvider provider = null, IHttpClientFactory factory = null) : IHttpClientFactory + { + public HttpClient CreateClient(string name) + { + var client = factory?.CreateClient(name) ?? new HttpClient(); + + var headers = provider.GetService()?.Headers ?? new HeaderDictionary(); + foreach (var header in headers) + { + client.DefaultRequestHeaders.TryAddWithoutValidation(header.Key, [header.Value]); + } + + return client; + } + } +} diff --git a/src/libraries/Hosting/AspNetCore/BackgroundActivityService/ActivityTaskQueue.cs b/src/libraries/Hosting/AspNetCore/BackgroundActivityService/ActivityTaskQueue.cs index 9d2cae99..69579b86 100644 --- a/src/libraries/Hosting/AspNetCore/BackgroundActivityService/ActivityTaskQueue.cs +++ b/src/libraries/Hosting/AspNetCore/BackgroundActivityService/ActivityTaskQueue.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. // using Microsoft.Agents.Core.Models; +using Microsoft.AspNetCore.Http; using System; using System.Collections.Concurrent; using System.Security.Claims; @@ -20,12 +21,12 @@ public class ActivityTaskQueue : IActivityTaskQueue /// - public void QueueBackgroundActivity(ClaimsIdentity claimsIdentity, IActivity activity, bool proactive = false, string proactiveAudience = null, Type bot = null) + public void QueueBackgroundActivity(ClaimsIdentity claimsIdentity, IActivity activity, bool proactive = false, string proactiveAudience = null, Type bot = null, IHeaderDictionary headers = null) { ArgumentNullException.ThrowIfNull(claimsIdentity); ArgumentNullException.ThrowIfNull(activity); - _activities.Enqueue(new ActivityWithClaims { BotType = bot, ClaimsIdentity = claimsIdentity, Activity = activity, IsProactive = proactive, ProactiveAudience = proactiveAudience }); + _activities.Enqueue(new ActivityWithClaims { BotType = bot, ClaimsIdentity = claimsIdentity, Activity = activity, IsProactive = proactive, ProactiveAudience = proactiveAudience, Headers = headers }); _signal.Release(); } diff --git a/src/libraries/Hosting/AspNetCore/BackgroundActivityService/ActivityWithClaims.cs b/src/libraries/Hosting/AspNetCore/BackgroundActivityService/ActivityWithClaims.cs index 4047633b..38b6487c 100644 --- a/src/libraries/Hosting/AspNetCore/BackgroundActivityService/ActivityWithClaims.cs +++ b/src/libraries/Hosting/AspNetCore/BackgroundActivityService/ActivityWithClaims.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. // using Microsoft.Agents.Core.Models; +using Microsoft.AspNetCore.Http; using System; using System.Security.Claims; @@ -30,5 +31,10 @@ public class ActivityWithClaims public bool IsProactive { get; set; } public string ProactiveAudience { get; set; } + /// + /// Headers used for the current request. + /// + public IHeaderDictionary Headers { get; set; } + } } diff --git a/src/libraries/Hosting/AspNetCore/BackgroundActivityService/HostedActivityService.cs b/src/libraries/Hosting/AspNetCore/BackgroundActivityService/HostedActivityService.cs index 38f62bb8..28ffe600 100644 --- a/src/libraries/Hosting/AspNetCore/BackgroundActivityService/HostedActivityService.cs +++ b/src/libraries/Hosting/AspNetCore/BackgroundActivityService/HostedActivityService.cs @@ -8,7 +8,9 @@ using System.Threading.Tasks; using Microsoft.Agents.Authentication; using Microsoft.Agents.BotBuilder; +using Microsoft.Agents.Hosting.AspNetCore.HeaderPropagation; using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; @@ -134,6 +136,8 @@ private Task GetTaskFromWorkItem(ActivityWithClaims activityWithClaims, Cancella // else that is transient as part of the bot, that uses IServiceProvider will encounter error since that is scoped // and disposed before this gets called. var bot = _serviceProvider.GetService(activityWithClaims.BotType ?? typeof(IBot)); + var headerValues = _serviceProvider.GetService(); + headerValues.Headers = activityWithClaims.Headers; if (activityWithClaims.IsProactive) { diff --git a/src/libraries/Hosting/AspNetCore/BackgroundActivityService/IActivityTaskQueue.cs b/src/libraries/Hosting/AspNetCore/BackgroundActivityService/IActivityTaskQueue.cs index df400ce9..dadf3f9d 100644 --- a/src/libraries/Hosting/AspNetCore/BackgroundActivityService/IActivityTaskQueue.cs +++ b/src/libraries/Hosting/AspNetCore/BackgroundActivityService/IActivityTaskQueue.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. // using Microsoft.Agents.Core.Models; +using Microsoft.AspNetCore.Http; using System; using System.Security.Claims; using System.Threading; @@ -25,7 +26,8 @@ public interface IActivityTaskQueue /// /// /// - void QueueBackgroundActivity(ClaimsIdentity claimsIdentity, IActivity activity, bool proactive = false, string proactiveAudience = null, Type bot = null); + /// Headers used for the current request. + void QueueBackgroundActivity(ClaimsIdentity claimsIdentity, IActivity activity, bool proactive = false, string proactiveAudience = null, Type bot = null, IHeaderDictionary headers = null); /// /// Wait for a signal of an enqueued Activity with Claims to be processed. diff --git a/src/libraries/Hosting/AspNetCore/CloudAdapter.cs b/src/libraries/Hosting/AspNetCore/CloudAdapter.cs index 61d71cd9..e9a372a9 100644 --- a/src/libraries/Hosting/AspNetCore/CloudAdapter.cs +++ b/src/libraries/Hosting/AspNetCore/CloudAdapter.cs @@ -11,9 +11,9 @@ using Microsoft.Extensions.Logging; using Microsoft.Agents.Core.Models; using Microsoft.Agents.BotBuilder; -using Microsoft.Agents.Connector.Types; using System.Text; using Microsoft.Agents.Core.Errors; +using System.Linq; namespace Microsoft.Agents.Hosting.AspNetCore { @@ -134,7 +134,7 @@ public async Task ProcessAsync(HttpRequest httpRequest, HttpResponse httpRespons else { // Queue the activity to be processed by the ActivityBackgroundService - _activityTaskQueue.QueueBackgroundActivity(claimsIdentity, activity); + _activityTaskQueue.QueueBackgroundActivity(claimsIdentity, activity, headers: httpRequest.Headers); // Activity has been queued to process, so return immediately httpResponse.StatusCode = (int)HttpStatusCode.Accepted; diff --git a/src/libraries/Hosting/AspNetCore/HeaderPropagation/HeaderPropagationContext.cs b/src/libraries/Hosting/AspNetCore/HeaderPropagation/HeaderPropagationContext.cs new file mode 100644 index 00000000..326d63f6 --- /dev/null +++ b/src/libraries/Hosting/AspNetCore/HeaderPropagation/HeaderPropagationContext.cs @@ -0,0 +1,63 @@ + +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Threading; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.Agents.Hosting.AspNetCore.HeaderPropagation; + +/// +/// Shared context between the and for propagating headers. +/// +public class HeaderPropagationContext(HeaderPropagationOptions options) +{ + private static readonly AsyncLocal _headers = new(); + + /// + /// Gets or sets the headers allowed by the class to be propagated when the gets created. + /// + public IHeaderDictionary Headers + { + get + { + return _headers.Value; + } + set + { + _headers.Value = FilterHeaders(value); + } + } + + /// + /// Filters the request headers based on the keys provided in . + /// + /// Headers collection from an Http request. + /// Filtered headers. + private HeaderDictionary FilterHeaders(IHeaderDictionary requestHeaders) + { + var result = new HeaderDictionary(); + if (requestHeaders == null || requestHeaders.Count == 0 || options.Headers.Count == 0) + { + return result; + } + + // Create a copy to ensure headers are not modified by the original request. + var headers = requestHeaders.ToDictionary(); + + foreach (var key in options.Headers) + { + var header = headers.FirstOrDefault(x => x.Key.Equals(key, StringComparison.InvariantCultureIgnoreCase)); + if (header.Key != null) + { + result.TryAdd(header.Key, header.Value); + } + } + + return result; + } +} \ No newline at end of file diff --git a/src/libraries/Hosting/AspNetCore/HeaderPropagation/HeaderPropagationMiddleware.cs b/src/libraries/Hosting/AspNetCore/HeaderPropagation/HeaderPropagationMiddleware.cs new file mode 100644 index 00000000..88d55da9 --- /dev/null +++ b/src/libraries/Hosting/AspNetCore/HeaderPropagation/HeaderPropagationMiddleware.cs @@ -0,0 +1,44 @@ + +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.Agents.Hosting.AspNetCore.HeaderPropagation; + +/// +/// A Middleware for propagating headers from incoming to outgoing requests using a custom internally. +/// +public class HeaderPropagationMiddleware +{ + private readonly RequestDelegate _next; + private readonly HeaderPropagationContext _context; + + /// + /// Initializes a new instance of . + /// + /// The next middleware in the pipeline. + /// The that stores the request headers to be propagated. + public HeaderPropagationMiddleware(RequestDelegate next, HeaderPropagationContext context) + { + ArgumentNullException.ThrowIfNull(next); + ArgumentNullException.ThrowIfNull(context); + + _next = next; + _context = context; + } + + /// + /// Executes the middleware to set the request headers in . + /// + /// The for the current request. + public Task Invoke(HttpContext context) + { + _context.Headers = context.Request.Headers; + + return _next.Invoke(context); + } +} \ No newline at end of file diff --git a/src/libraries/Hosting/AspNetCore/HeaderPropagation/HeaderPropagationOptions.cs b/src/libraries/Hosting/AspNetCore/HeaderPropagation/HeaderPropagationOptions.cs new file mode 100644 index 00000000..ebc781a1 --- /dev/null +++ b/src/libraries/Hosting/AspNetCore/HeaderPropagation/HeaderPropagationOptions.cs @@ -0,0 +1,18 @@ + +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Collections.Generic; + +namespace Microsoft.Agents.Hosting.AspNetCore.HeaderPropagation; + +/// +/// Provides configuration for the . +/// +public class HeaderPropagationOptions +{ + /// + /// Gets or sets the keys of the headers to be propagated from incoming to outgoing requests. + /// + public List Headers { get; } = []; +} \ No newline at end of file diff --git a/src/libraries/Hosting/AspNetCore/ServiceCollectionExtensions.cs b/src/libraries/Hosting/AspNetCore/ServiceCollectionExtensions.cs index e6a51a20..9664ca8d 100644 --- a/src/libraries/Hosting/AspNetCore/ServiceCollectionExtensions.cs +++ b/src/libraries/Hosting/AspNetCore/ServiceCollectionExtensions.cs @@ -7,13 +7,16 @@ using Microsoft.Agents.BotBuilder.App.UserAuth; using Microsoft.Agents.Client; using Microsoft.Agents.Hosting.AspNetCore.BackgroundQueue; +using Microsoft.Agents.Hosting.AspNetCore.HeaderPropagation; using Microsoft.Agents.Storage; +using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; +using System.Linq; using System.Net.Http; namespace Microsoft.Agents.Hosting.AspNetCore @@ -45,6 +48,8 @@ public static IHostApplicationBuilder AddBot(this IHostApplicati where TBot : class, IBot where TAdapter : CloudAdapter { + AddHeaderPropagation(builder); + AddHttpClientFactory(builder); AddCore(builder, storage); // Add the Bot, this is the primary worker for the bot. @@ -157,6 +162,22 @@ public static void AddCloudAdapter(this IServiceCollection services) where T services.AddSingleton(sp => sp.GetService()); } + /// + /// Adds a middleware that collect headers to be propagated to an instance. + /// + /// The to add the middleware to. + /// The configuration to select which headers to propagate from incoming to outgoing requests. + /// A reference to the after the operation has completed. + public static IApplicationBuilder UseHeaderPropagation(this IApplicationBuilder app, Action configureOptions) + { + ArgumentNullException.ThrowIfNull(app); + ArgumentNullException.ThrowIfNull(configureOptions); + + configureOptions(app.ApplicationServices.GetRequiredService()); + + return app.UseMiddleware(); + } + private static void AddCore(this IHostApplicationBuilder builder, IStorage storage = null) where TAdapter : CloudAdapter { @@ -192,5 +213,48 @@ private static void AddAsyncCloudAdapterSupport(this IServiceCollection services services.AddSingleton(); services.AddSingleton(); } + + /// + /// Adds a custom Http Client factory that wraps the default . + /// + private static void AddHttpClientFactory(this IHostApplicationBuilder builder) + { + var defaultDescriptor = builder.Services.FirstOrDefault(d => d.ServiceType == typeof(IHttpClientFactory)); + if (defaultDescriptor == null) + { + builder.Services.AddSingleton(sp => new AgentsHttpClientFactory(sp)); + return; + } + + // Remove the default registration. + builder.Services.Remove(defaultDescriptor); + + // Capture a factory delegate that can create the default IHttpClientFactory instance. + Func defaultFactory; + if (defaultDescriptor.ImplementationFactory != null) + { + defaultFactory = provider => (IHttpClientFactory)defaultDescriptor.ImplementationFactory(provider); + } + else if (defaultDescriptor.ImplementationInstance != null) + { + defaultFactory = _ => (IHttpClientFactory)defaultDescriptor.ImplementationInstance; + } + else + { + defaultFactory = provider => ActivatorUtilities.CreateInstance(provider, defaultDescriptor.ImplementationType) as IHttpClientFactory; + } + + // Register custom factory that wraps the default. + builder.Services.AddSingleton(sp => new AgentsHttpClientFactory(sp, defaultFactory(sp))); + } + + /// + /// Adds header propagation services. + /// + private static void AddHeaderPropagation(this IHostApplicationBuilder builder) + { + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + } } } From 2633ddb27458ba274233684dd655fbe98a169d09 Mon Sep 17 00:00:00 2001 From: Joel Mut Date: Thu, 20 Mar 2025 11:10:28 +0000 Subject: [PATCH 2/9] Improve overall implementation --- .../AspNetCore/AgentsHttpClientFactory.cs | 16 +++++++++++++++- .../ActivityTaskQueue.cs | 6 +++++- .../HostedActivityService.cs | 13 ++++++++----- .../HeaderPropagationContext.cs | 7 +++---- .../HeaderPropagationOptions.cs | 5 +++-- .../CloudAdapterTests.cs | 4 ++-- .../ServiceCollectionExtensionsTests.cs | 5 +++++ 7 files changed, 41 insertions(+), 15 deletions(-) diff --git a/src/libraries/Hosting/AspNetCore/AgentsHttpClientFactory.cs b/src/libraries/Hosting/AspNetCore/AgentsHttpClientFactory.cs index 428b1221..e66761ed 100644 --- a/src/libraries/Hosting/AspNetCore/AgentsHttpClientFactory.cs +++ b/src/libraries/Hosting/AspNetCore/AgentsHttpClientFactory.cs @@ -4,6 +4,8 @@ using Microsoft.Agents.Hosting.AspNetCore.HeaderPropagation; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; using System; using System.Net.Http; @@ -20,10 +22,22 @@ public HttpClient CreateClient(string name) { var client = factory?.CreateClient(name) ?? new HttpClient(); + var logger = provider?.GetService>() ?? NullLogger.Instance; + logger.LogDebug("Creating HTTP client with name: {ClientName}.", name); + var headers = provider.GetService()?.Headers ?? new HeaderDictionary(); + + logger.LogDebug("Found {HeaderCount} headers to propagate.", headers.Count); foreach (var header in headers) { - client.DefaultRequestHeaders.TryAddWithoutValidation(header.Key, [header.Value]); + if (client.DefaultRequestHeaders.TryAddWithoutValidation(header.Key, [header.Value])) + { + logger.LogWarning("Failed to add header {HeaderName} to HTTP client.", header.Key); + } + else + { + logger.LogTrace("Added header {HeaderName} to HTTP client.", header.Key); + } } return client; diff --git a/src/libraries/Hosting/AspNetCore/BackgroundActivityService/ActivityTaskQueue.cs b/src/libraries/Hosting/AspNetCore/BackgroundActivityService/ActivityTaskQueue.cs index 69579b86..3fbd9417 100644 --- a/src/libraries/Hosting/AspNetCore/BackgroundActivityService/ActivityTaskQueue.cs +++ b/src/libraries/Hosting/AspNetCore/BackgroundActivityService/ActivityTaskQueue.cs @@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Http; using System; using System.Collections.Concurrent; +using System.Linq; using System.Security.Claims; using System.Threading; using System.Threading.Tasks; @@ -25,8 +26,11 @@ public void QueueBackgroundActivity(ClaimsIdentity claimsIdentity, IActivity act { ArgumentNullException.ThrowIfNull(claimsIdentity); ArgumentNullException.ThrowIfNull(activity); + + //Copy to prevent unexpected side effects from later mutations of the original headers. + var copyHeaders = new HeaderDictionary(headers.ToDictionary()); - _activities.Enqueue(new ActivityWithClaims { BotType = bot, ClaimsIdentity = claimsIdentity, Activity = activity, IsProactive = proactive, ProactiveAudience = proactiveAudience, Headers = headers }); + _activities.Enqueue(new ActivityWithClaims { BotType = bot, ClaimsIdentity = claimsIdentity, Activity = activity, IsProactive = proactive, ProactiveAudience = proactiveAudience, Headers = copyHeaders }); _signal.Release(); } diff --git a/src/libraries/Hosting/AspNetCore/BackgroundActivityService/HostedActivityService.cs b/src/libraries/Hosting/AspNetCore/BackgroundActivityService/HostedActivityService.cs index 28ffe600..fcb3d40a 100644 --- a/src/libraries/Hosting/AspNetCore/BackgroundActivityService/HostedActivityService.cs +++ b/src/libraries/Hosting/AspNetCore/BackgroundActivityService/HostedActivityService.cs @@ -137,23 +137,26 @@ private Task GetTaskFromWorkItem(ActivityWithClaims activityWithClaims, Cancella // and disposed before this gets called. var bot = _serviceProvider.GetService(activityWithClaims.BotType ?? typeof(IBot)); var headerValues = _serviceProvider.GetService(); - headerValues.Headers = activityWithClaims.Headers; + if (headerValues != null) + { + headerValues.Headers = activityWithClaims.Headers; + } if (activityWithClaims.IsProactive) { await _adapter.ProcessProactiveAsync( - activityWithClaims.ClaimsIdentity, + activityWithClaims.ClaimsIdentity, activityWithClaims.Activity, activityWithClaims.ProactiveAudience ?? BotClaims.GetTokenAudience(activityWithClaims.ClaimsIdentity), - ((IBot)bot).OnTurnAsync, + ((IBot)bot).OnTurnAsync, stoppingToken).ConfigureAwait(false); } else { await _adapter.ProcessActivityAsync( - activityWithClaims.ClaimsIdentity, + activityWithClaims.ClaimsIdentity, activityWithClaims.Activity, - ((IBot)bot).OnTurnAsync, + ((IBot)bot).OnTurnAsync, stoppingToken).ConfigureAwait(false); } } diff --git a/src/libraries/Hosting/AspNetCore/HeaderPropagation/HeaderPropagationContext.cs b/src/libraries/Hosting/AspNetCore/HeaderPropagation/HeaderPropagationContext.cs index 326d63f6..3acb8633 100644 --- a/src/libraries/Hosting/AspNetCore/HeaderPropagation/HeaderPropagationContext.cs +++ b/src/libraries/Hosting/AspNetCore/HeaderPropagation/HeaderPropagationContext.cs @@ -47,14 +47,13 @@ private HeaderDictionary FilterHeaders(IHeaderDictionary requestHeaders) } // Create a copy to ensure headers are not modified by the original request. - var headers = requestHeaders.ToDictionary(); + var headers = requestHeaders.ToDictionary(StringComparer.InvariantCultureIgnoreCase); foreach (var key in options.Headers) { - var header = headers.FirstOrDefault(x => x.Key.Equals(key, StringComparison.InvariantCultureIgnoreCase)); - if (header.Key != null) + if (headers.TryGetValue(key, out var header)) { - result.TryAdd(header.Key, header.Value); + result.TryAdd(key, header); } } diff --git a/src/libraries/Hosting/AspNetCore/HeaderPropagation/HeaderPropagationOptions.cs b/src/libraries/Hosting/AspNetCore/HeaderPropagation/HeaderPropagationOptions.cs index ebc781a1..a348b40f 100644 --- a/src/libraries/Hosting/AspNetCore/HeaderPropagation/HeaderPropagationOptions.cs +++ b/src/libraries/Hosting/AspNetCore/HeaderPropagation/HeaderPropagationOptions.cs @@ -1,4 +1,3 @@ - // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. @@ -11,8 +10,10 @@ namespace Microsoft.Agents.Hosting.AspNetCore.HeaderPropagation; /// public class HeaderPropagationOptions { + private readonly List _headers = []; + /// /// Gets or sets the keys of the headers to be propagated from incoming to outgoing requests. /// - public List Headers { get; } = []; + public List Headers => _headers; } \ No newline at end of file diff --git a/src/tests/Microsoft.Agents.Hosting.AspNetCore/CloudAdapterTests.cs b/src/tests/Microsoft.Agents.Hosting.AspNetCore/CloudAdapterTests.cs index bf0d96aa..24d42307 100644 --- a/src/tests/Microsoft.Agents.Hosting.AspNetCore/CloudAdapterTests.cs +++ b/src/tests/Microsoft.Agents.Hosting.AspNetCore/CloudAdapterTests.cs @@ -126,7 +126,7 @@ public async Task ProcessAsync_ShouldSetUnauthorized() var context = CreateHttpContext(new(ActivityTypes.Message, conversation: new(id: "test"))); var bot = new ActivityHandler(); - record.Queue.Setup(e => e.QueueBackgroundActivity(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + record.Queue.Setup(e => e.QueueBackgroundActivity(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Throws(new UnauthorizedAccessException()) .Verifiable(Times.Once); @@ -155,7 +155,7 @@ public async Task ProcessAsync_ShouldQueueActivity() var context = CreateHttpContext(new(ActivityTypes.Message, conversation: new(id: "test"))); var bot = new ActivityHandler(); - record.Queue.Setup(e => e.QueueBackgroundActivity(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + record.Queue.Setup(e => e.QueueBackgroundActivity(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Verifiable(Times.Once); await record.Adapter.ProcessAsync(context.Request, context.Response, bot, CancellationToken.None); diff --git a/src/tests/Microsoft.Agents.Hosting.AspNetCore/ServiceCollectionExtensionsTests.cs b/src/tests/Microsoft.Agents.Hosting.AspNetCore/ServiceCollectionExtensionsTests.cs index 09567ab7..4dbba422 100644 --- a/src/tests/Microsoft.Agents.Hosting.AspNetCore/ServiceCollectionExtensionsTests.cs +++ b/src/tests/Microsoft.Agents.Hosting.AspNetCore/ServiceCollectionExtensionsTests.cs @@ -4,12 +4,14 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Net.Http; using Microsoft.Agents.Authentication; using Microsoft.Agents.BotBuilder; using Microsoft.Agents.BotBuilder.Compat; using Microsoft.Agents.Client; using Microsoft.Agents.Core.Models; using Microsoft.Agents.Hosting.AspNetCore.BackgroundQueue; +using Microsoft.Agents.Hosting.AspNetCore.HeaderPropagation; using Microsoft.Agents.Storage; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -52,6 +54,9 @@ public void AddBot_ShouldSetServices() .Select(e => e.ImplementationType ?? e.ServiceType) .ToList(); var expected = new List{ + typeof(HeaderPropagationOptions), + typeof(HeaderPropagationContext), + typeof(IHttpClientFactory), typeof(ConfigurationConnections), typeof(RestChannelServiceClientFactory), typeof(MemoryStorage), From d2b31c09e0fa60fa280c87fef428c41fcd4dee02 Mon Sep 17 00:00:00 2001 From: Joel Mut Date: Thu, 20 Mar 2025 11:23:36 +0000 Subject: [PATCH 3/9] fix possibly null reference --- .../AspNetCore/BackgroundActivityService/ActivityTaskQueue.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/Hosting/AspNetCore/BackgroundActivityService/ActivityTaskQueue.cs b/src/libraries/Hosting/AspNetCore/BackgroundActivityService/ActivityTaskQueue.cs index 3fbd9417..cf9bdbbf 100644 --- a/src/libraries/Hosting/AspNetCore/BackgroundActivityService/ActivityTaskQueue.cs +++ b/src/libraries/Hosting/AspNetCore/BackgroundActivityService/ActivityTaskQueue.cs @@ -28,7 +28,7 @@ public void QueueBackgroundActivity(ClaimsIdentity claimsIdentity, IActivity act ArgumentNullException.ThrowIfNull(activity); //Copy to prevent unexpected side effects from later mutations of the original headers. - var copyHeaders = new HeaderDictionary(headers.ToDictionary()); + var copyHeaders = headers != null ? new HeaderDictionary(headers.ToDictionary()) : []; _activities.Enqueue(new ActivityWithClaims { BotType = bot, ClaimsIdentity = claimsIdentity, Activity = activity, IsProactive = proactive, ProactiveAudience = proactiveAudience, Headers = copyHeaders }); _signal.Release(); From 8470656358e7d4755e7a7dcb7c443ece520035f6 Mon Sep 17 00:00:00 2001 From: Joel Mut Date: Thu, 20 Mar 2025 15:49:37 +0000 Subject: [PATCH 4/9] Apply nit changes --- .../AspNetCore/BackgroundActivityService/ActivityTaskQueue.cs | 2 +- .../AspNetCore/HeaderPropagation/HeaderPropagationContext.cs | 1 - .../HeaderPropagation/HeaderPropagationMiddleware.cs | 3 +-- .../Hosting/AspNetCore/ServiceCollectionExtensions.cs | 2 +- 4 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/libraries/Hosting/AspNetCore/BackgroundActivityService/ActivityTaskQueue.cs b/src/libraries/Hosting/AspNetCore/BackgroundActivityService/ActivityTaskQueue.cs index cf9bdbbf..03cf0bfd 100644 --- a/src/libraries/Hosting/AspNetCore/BackgroundActivityService/ActivityTaskQueue.cs +++ b/src/libraries/Hosting/AspNetCore/BackgroundActivityService/ActivityTaskQueue.cs @@ -27,7 +27,7 @@ public void QueueBackgroundActivity(ClaimsIdentity claimsIdentity, IActivity act ArgumentNullException.ThrowIfNull(claimsIdentity); ArgumentNullException.ThrowIfNull(activity); - //Copy to prevent unexpected side effects from later mutations of the original headers. + // Copy to prevent unexpected side effects from later mutations of the original headers. var copyHeaders = headers != null ? new HeaderDictionary(headers.ToDictionary()) : []; _activities.Enqueue(new ActivityWithClaims { BotType = bot, ClaimsIdentity = claimsIdentity, Activity = activity, IsProactive = proactive, ProactiveAudience = proactiveAudience, Headers = copyHeaders }); diff --git a/src/libraries/Hosting/AspNetCore/HeaderPropagation/HeaderPropagationContext.cs b/src/libraries/Hosting/AspNetCore/HeaderPropagation/HeaderPropagationContext.cs index 3acb8633..f70d2522 100644 --- a/src/libraries/Hosting/AspNetCore/HeaderPropagation/HeaderPropagationContext.cs +++ b/src/libraries/Hosting/AspNetCore/HeaderPropagation/HeaderPropagationContext.cs @@ -1,4 +1,3 @@ - // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. diff --git a/src/libraries/Hosting/AspNetCore/HeaderPropagation/HeaderPropagationMiddleware.cs b/src/libraries/Hosting/AspNetCore/HeaderPropagation/HeaderPropagationMiddleware.cs index 88d55da9..ced71365 100644 --- a/src/libraries/Hosting/AspNetCore/HeaderPropagation/HeaderPropagationMiddleware.cs +++ b/src/libraries/Hosting/AspNetCore/HeaderPropagation/HeaderPropagationMiddleware.cs @@ -1,4 +1,3 @@ - // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. @@ -10,7 +9,7 @@ namespace Microsoft.Agents.Hosting.AspNetCore.HeaderPropagation; /// -/// A Middleware for propagating headers from incoming to outgoing requests using a custom internally. +/// A middleware to propagate incoming request headers to outgoing ones using internally a custom . /// public class HeaderPropagationMiddleware { diff --git a/src/libraries/Hosting/AspNetCore/ServiceCollectionExtensions.cs b/src/libraries/Hosting/AspNetCore/ServiceCollectionExtensions.cs index 9664ca8d..321f9ff0 100644 --- a/src/libraries/Hosting/AspNetCore/ServiceCollectionExtensions.cs +++ b/src/libraries/Hosting/AspNetCore/ServiceCollectionExtensions.cs @@ -163,7 +163,7 @@ public static void AddCloudAdapter(this IServiceCollection services) where T } /// - /// Adds a middleware that collect headers to be propagated to an instance. + /// Adds a middleware that collects headers to be propagated to an instance. /// /// The to add the middleware to. /// The configuration to select which headers to propagate from incoming to outgoing requests. From 36fa388d843fbf514fee21f1d0a3ebfb29d29c5f Mon Sep 17 00:00:00 2001 From: Joel Mut Date: Mon, 7 Apr 2025 18:44:17 +0100 Subject: [PATCH 5/9] Add attributes --- .../Microsoft.Agents.Client/HttpBotChannel.cs | 3 +- .../RestClients/RestClientBase.cs | 2 + .../HeaderPropagationAttribute.cs | 47 ++++++++++ .../HeaderPropagationContext.cs | 86 +++++++++++++++++++ .../HeaderPropagationExtensions.cs | 17 ++++ .../Connector/HeaderPropagation.cs | 25 ++++++ .../AspNetCore/AgentsHttpClientFactory.cs | 46 ---------- .../HostedActivityService.cs | 9 +- .../HeaderPropagationContext.cs | 61 ------------- .../HeaderPropagationOptions.cs | 19 ---- .../HeaderPropagationMiddleware.cs | 14 ++- .../AspNetCore/ServiceCollectionExtensions.cs | 55 +----------- .../ServiceCollectionExtensionsTests.cs | 3 - 13 files changed, 188 insertions(+), 199 deletions(-) create mode 100644 src/libraries/Core/Microsoft.Agents.Core/HeaderPropagation/HeaderPropagationAttribute.cs create mode 100644 src/libraries/Core/Microsoft.Agents.Core/HeaderPropagation/HeaderPropagationContext.cs create mode 100644 src/libraries/Core/Microsoft.Agents.Core/HeaderPropagation/HeaderPropagationExtensions.cs create mode 100644 src/libraries/Extensions/Microsoft.Agents.Extensions.Teams/Connector/HeaderPropagation.cs delete mode 100644 src/libraries/Hosting/AspNetCore/AgentsHttpClientFactory.cs delete mode 100644 src/libraries/Hosting/AspNetCore/HeaderPropagation/HeaderPropagationContext.cs delete mode 100644 src/libraries/Hosting/AspNetCore/HeaderPropagation/HeaderPropagationOptions.cs rename src/libraries/Hosting/AspNetCore/{HeaderPropagation => }/HeaderPropagationMiddleware.cs (62%) diff --git a/src/libraries/Client/Microsoft.Agents.Client/HttpBotChannel.cs b/src/libraries/Client/Microsoft.Agents.Client/HttpBotChannel.cs index 1c7afbb8..e6c74336 100644 --- a/src/libraries/Client/Microsoft.Agents.Client/HttpBotChannel.cs +++ b/src/libraries/Client/Microsoft.Agents.Client/HttpBotChannel.cs @@ -8,9 +8,9 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Agents.Authentication; -using Microsoft.Agents.Core; using Microsoft.Agents.Core.Models; using Microsoft.Agents.Core.Serialization; +using Microsoft.Agents.Core.HeaderPropagation; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; @@ -91,6 +91,7 @@ public async Task> PostActivityAsync(string toBotId, string // Add the auth header to the HTTP request. var tokenResult = await _tokenAccess.GetAccessTokenAsync(toBotResource, [$"{toBotId}/.default"]).ConfigureAwait(false); httpRequestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", tokenResult); + _httpClient.AddHeaderPropagation(); using (var httpResponseMessage = await _httpClient.SendAsync(httpRequestMessage, cancellationToken).ConfigureAwait(false)) { diff --git a/src/libraries/Client/Microsoft.Agents.Connector/RestClients/RestClientBase.cs b/src/libraries/Client/Microsoft.Agents.Connector/RestClients/RestClientBase.cs index 4d4a8437..f31d65e3 100644 --- a/src/libraries/Client/Microsoft.Agents.Connector/RestClients/RestClientBase.cs +++ b/src/libraries/Client/Microsoft.Agents.Connector/RestClients/RestClientBase.cs @@ -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 GetHttpClientAsync() } httpClient.AddDefaultUserAgent(); + httpClient.AddHeaderPropagation(); return httpClient; } diff --git a/src/libraries/Core/Microsoft.Agents.Core/HeaderPropagation/HeaderPropagationAttribute.cs b/src/libraries/Core/Microsoft.Agents.Core/HeaderPropagation/HeaderPropagationAttribute.cs new file mode 100644 index 00000000..16f0caa7 --- /dev/null +++ b/src/libraries/Core/Microsoft.Agents.Core/HeaderPropagation/HeaderPropagationAttribute.cs @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.Extensions.Primitives; +using System; +using System.Collections.Generic; +using System.Reflection; + +namespace Microsoft.Agents.Core.HeaderPropagation; + +public class HeaderPropagationAttribute : Attribute +{ + internal static void SetHeadersSerialization() + { + //init newly loaded assemblies + AppDomain.CurrentDomain.AssemblyLoad += (s, o) => SetHeadersAssembly(o.LoadedAssembly); + + //and all the ones we currently have loaded + foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) + { + SetHeadersAssembly(assembly); + } + } + + private static void SetHeadersAssembly(Assembly assembly) + { + foreach (var type in GetLoadOnsetHeadersTypes(assembly)) + { + var setHeaders = type.GetMethod("SetHeaders", BindingFlags.Static | BindingFlags.Public); + if (setHeaders?.Invoke(assembly, null) is IDictionary headers) + { + HeaderPropagationContext.HeadersToPropagate = headers; + } + } + } + + private static IEnumerable GetLoadOnsetHeadersTypes(Assembly assembly) + { + foreach (Type type in assembly.GetTypes()) + { + if (type.GetCustomAttributes(typeof(HeaderPropagationAttribute), true).Length > 0) + { + yield return type; + } + } + } +} diff --git a/src/libraries/Core/Microsoft.Agents.Core/HeaderPropagation/HeaderPropagationContext.cs b/src/libraries/Core/Microsoft.Agents.Core/HeaderPropagation/HeaderPropagationContext.cs new file mode 100644 index 00000000..3d8406c3 --- /dev/null +++ b/src/libraries/Core/Microsoft.Agents.Core/HeaderPropagation/HeaderPropagationContext.cs @@ -0,0 +1,86 @@ +// 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; + +/// +/// Shared context between the and for propagating headers. +/// +public class HeaderPropagationContext() +{ + private static readonly AsyncLocal> _headersFromRequest = new(); + private static Dictionary _headersToPropagate = new(); + + static HeaderPropagationContext() + { + HeaderPropagationAttribute.SetHeadersSerialization(); + } + + /// + /// Gets or sets the request headers that will be propagated based on what's inside the property. + /// + public static IDictionary HeadersFromRequest + { + get + { + return _headersFromRequest.Value; + } + set + { + // Create a copy to ensure headers are not modified by the original request. + var headers = value?.ToDictionary(StringComparer.InvariantCultureIgnoreCase); + _headersFromRequest.Value = FilterHeaders(headers); + } + } + + /// + /// Gets or sets the headers to allow during the propagation. + /// Providing a header with a value will override the one in the request, otherwise the value from the request will be used. + /// + public static IDictionary HeadersToPropagate + { + get + { + var defaultHeaders = new Dictionary { + { "x-ms-correlation-id", "" } + }; + return defaultHeaders.Concat(_headersToPropagate).ToDictionary(StringComparer.InvariantCultureIgnoreCase); + } + set + { + _headersToPropagate = _headersToPropagate.Concat(value).ToDictionary(StringComparer.InvariantCultureIgnoreCase); + } + } + + /// + /// Filters the request headers based on the keys provided in . + /// + /// Headers collection from an Http request. + /// Filtered headers. + private static Dictionary FilterHeaders(Dictionary requestHeaders) + { + var result = new Dictionary(); + + if (requestHeaders == null || requestHeaders.Count == 0) + { + return result; + } + + foreach (var item in HeadersToPropagate) + { + if (requestHeaders.TryGetValue(item.Key, out var header)) + { + var value = !string.IsNullOrWhiteSpace(item.Value) ? item.Value : header; + result.TryAdd(item.Key, value); + } + } + + return result; + } +} \ No newline at end of file diff --git a/src/libraries/Core/Microsoft.Agents.Core/HeaderPropagation/HeaderPropagationExtensions.cs b/src/libraries/Core/Microsoft.Agents.Core/HeaderPropagation/HeaderPropagationExtensions.cs new file mode 100644 index 00000000..2e9a0e9f --- /dev/null +++ b/src/libraries/Core/Microsoft.Agents.Core/HeaderPropagation/HeaderPropagationExtensions.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Net.Http; + +namespace Microsoft.Agents.Core.HeaderPropagation; + +public static class HeaderPropagationExtensions +{ + public static void AddHeaderPropagation(this HttpClient httpClient) + { + foreach (var header in HeaderPropagationContext.HeadersFromRequest) + { + httpClient.DefaultRequestHeaders.TryAddWithoutValidation(header.Key, [header.Value]); + } + } +} diff --git a/src/libraries/Extensions/Microsoft.Agents.Extensions.Teams/Connector/HeaderPropagation.cs b/src/libraries/Extensions/Microsoft.Agents.Extensions.Teams/Connector/HeaderPropagation.cs new file mode 100644 index 00000000..58358064 --- /dev/null +++ b/src/libraries/Extensions/Microsoft.Agents.Extensions.Teams/Connector/HeaderPropagation.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Collections.Generic; +using Microsoft.Agents.Core.HeaderPropagation; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.Agents.Extensions.Teams.Connector +{ + // This class could implement an interface to guide customers on how to implement the SetHeaders method. + [HeaderPropagation] + internal class HeaderPropagation + { + public static Dictionary SetHeaders() + { + // TODO: this functionality could be improved by providing a helper method to set the header either with a key or a key and value. + // Possibly provide an interface or abstract class to guide customers on how to implement the SetHeaders method. + return new Dictionary() + { + { "User-Agent", "Teams-123" }, + { "Testing-Propagation-Id", "" } + }; + } + } +} diff --git a/src/libraries/Hosting/AspNetCore/AgentsHttpClientFactory.cs b/src/libraries/Hosting/AspNetCore/AgentsHttpClientFactory.cs deleted file mode 100644 index e66761ed..00000000 --- a/src/libraries/Hosting/AspNetCore/AgentsHttpClientFactory.cs +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using Microsoft.Agents.Hosting.AspNetCore.HeaderPropagation; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; -using System; -using System.Net.Http; - -namespace Microsoft.Agents.Hosting.AspNetCore -{ - /// - /// Custom implementation of an IHttpClientFactory that allows customizing the settings in the instance. - /// - /// Service provider used to gather DI related classes. - /// Existing HttpClientFactory to use in this implementation. - public class AgentsHttpClientFactory(IServiceProvider provider = null, IHttpClientFactory factory = null) : IHttpClientFactory - { - public HttpClient CreateClient(string name) - { - var client = factory?.CreateClient(name) ?? new HttpClient(); - - var logger = provider?.GetService>() ?? NullLogger.Instance; - logger.LogDebug("Creating HTTP client with name: {ClientName}.", name); - - var headers = provider.GetService()?.Headers ?? new HeaderDictionary(); - - logger.LogDebug("Found {HeaderCount} headers to propagate.", headers.Count); - foreach (var header in headers) - { - if (client.DefaultRequestHeaders.TryAddWithoutValidation(header.Key, [header.Value])) - { - logger.LogWarning("Failed to add header {HeaderName} to HTTP client.", header.Key); - } - else - { - logger.LogTrace("Added header {HeaderName} to HTTP client.", header.Key); - } - } - - return client; - } - } -} diff --git a/src/libraries/Hosting/AspNetCore/BackgroundActivityService/HostedActivityService.cs b/src/libraries/Hosting/AspNetCore/BackgroundActivityService/HostedActivityService.cs index fcb3d40a..59fb2eae 100644 --- a/src/libraries/Hosting/AspNetCore/BackgroundActivityService/HostedActivityService.cs +++ b/src/libraries/Hosting/AspNetCore/BackgroundActivityService/HostedActivityService.cs @@ -8,9 +8,8 @@ using System.Threading.Tasks; using Microsoft.Agents.Authentication; using Microsoft.Agents.BotBuilder; -using Microsoft.Agents.Hosting.AspNetCore.HeaderPropagation; +using Microsoft.Agents.Core.HeaderPropagation; using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; @@ -136,11 +135,7 @@ private Task GetTaskFromWorkItem(ActivityWithClaims activityWithClaims, Cancella // else that is transient as part of the bot, that uses IServiceProvider will encounter error since that is scoped // and disposed before this gets called. var bot = _serviceProvider.GetService(activityWithClaims.BotType ?? typeof(IBot)); - var headerValues = _serviceProvider.GetService(); - if (headerValues != null) - { - headerValues.Headers = activityWithClaims.Headers; - } + HeaderPropagationContext.HeadersFromRequest = activityWithClaims.Headers; if (activityWithClaims.IsProactive) { diff --git a/src/libraries/Hosting/AspNetCore/HeaderPropagation/HeaderPropagationContext.cs b/src/libraries/Hosting/AspNetCore/HeaderPropagation/HeaderPropagationContext.cs deleted file mode 100644 index f70d2522..00000000 --- a/src/libraries/Hosting/AspNetCore/HeaderPropagation/HeaderPropagationContext.cs +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net.Http; -using System.Threading; -using Microsoft.AspNetCore.Http; - -namespace Microsoft.Agents.Hosting.AspNetCore.HeaderPropagation; - -/// -/// Shared context between the and for propagating headers. -/// -public class HeaderPropagationContext(HeaderPropagationOptions options) -{ - private static readonly AsyncLocal _headers = new(); - - /// - /// Gets or sets the headers allowed by the class to be propagated when the gets created. - /// - public IHeaderDictionary Headers - { - get - { - return _headers.Value; - } - set - { - _headers.Value = FilterHeaders(value); - } - } - - /// - /// Filters the request headers based on the keys provided in . - /// - /// Headers collection from an Http request. - /// Filtered headers. - private HeaderDictionary FilterHeaders(IHeaderDictionary requestHeaders) - { - var result = new HeaderDictionary(); - if (requestHeaders == null || requestHeaders.Count == 0 || options.Headers.Count == 0) - { - return result; - } - - // Create a copy to ensure headers are not modified by the original request. - var headers = requestHeaders.ToDictionary(StringComparer.InvariantCultureIgnoreCase); - - foreach (var key in options.Headers) - { - if (headers.TryGetValue(key, out var header)) - { - result.TryAdd(key, header); - } - } - - return result; - } -} \ No newline at end of file diff --git a/src/libraries/Hosting/AspNetCore/HeaderPropagation/HeaderPropagationOptions.cs b/src/libraries/Hosting/AspNetCore/HeaderPropagation/HeaderPropagationOptions.cs deleted file mode 100644 index a348b40f..00000000 --- a/src/libraries/Hosting/AspNetCore/HeaderPropagation/HeaderPropagationOptions.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System.Collections.Generic; - -namespace Microsoft.Agents.Hosting.AspNetCore.HeaderPropagation; - -/// -/// Provides configuration for the . -/// -public class HeaderPropagationOptions -{ - private readonly List _headers = []; - - /// - /// Gets or sets the keys of the headers to be propagated from incoming to outgoing requests. - /// - public List Headers => _headers; -} \ No newline at end of file diff --git a/src/libraries/Hosting/AspNetCore/HeaderPropagation/HeaderPropagationMiddleware.cs b/src/libraries/Hosting/AspNetCore/HeaderPropagationMiddleware.cs similarity index 62% rename from src/libraries/Hosting/AspNetCore/HeaderPropagation/HeaderPropagationMiddleware.cs rename to src/libraries/Hosting/AspNetCore/HeaderPropagationMiddleware.cs index ced71365..d4543ebf 100644 --- a/src/libraries/Hosting/AspNetCore/HeaderPropagation/HeaderPropagationMiddleware.cs +++ b/src/libraries/Hosting/AspNetCore/HeaderPropagationMiddleware.cs @@ -2,32 +2,28 @@ // Licensed under the MIT License. using System; -using System.Net.Http; using System.Threading.Tasks; +using Microsoft.Agents.Core.HeaderPropagation; using Microsoft.AspNetCore.Http; -namespace Microsoft.Agents.Hosting.AspNetCore.HeaderPropagation; +namespace Microsoft.Agents.Hosting.AspNetCore; /// -/// A middleware to propagate incoming request headers to outgoing ones using internally a custom . +/// A middleware to propagate incoming request headers to outgoing ones by internally using the static class. /// public class HeaderPropagationMiddleware { private readonly RequestDelegate _next; - private readonly HeaderPropagationContext _context; /// /// Initializes a new instance of . /// /// The next middleware in the pipeline. - /// The that stores the request headers to be propagated. - public HeaderPropagationMiddleware(RequestDelegate next, HeaderPropagationContext context) + public HeaderPropagationMiddleware(RequestDelegate next) { ArgumentNullException.ThrowIfNull(next); - ArgumentNullException.ThrowIfNull(context); _next = next; - _context = context; } /// @@ -36,7 +32,7 @@ public HeaderPropagationMiddleware(RequestDelegate next, HeaderPropagationContex /// The for the current request. public Task Invoke(HttpContext context) { - _context.Headers = context.Request.Headers; + HeaderPropagationContext.HeadersFromRequest = context.Request.Headers; return _next.Invoke(context); } diff --git a/src/libraries/Hosting/AspNetCore/ServiceCollectionExtensions.cs b/src/libraries/Hosting/AspNetCore/ServiceCollectionExtensions.cs index 321f9ff0..d12b559e 100644 --- a/src/libraries/Hosting/AspNetCore/ServiceCollectionExtensions.cs +++ b/src/libraries/Hosting/AspNetCore/ServiceCollectionExtensions.cs @@ -7,7 +7,6 @@ using Microsoft.Agents.BotBuilder.App.UserAuth; using Microsoft.Agents.Client; using Microsoft.Agents.Hosting.AspNetCore.BackgroundQueue; -using Microsoft.Agents.Hosting.AspNetCore.HeaderPropagation; using Microsoft.Agents.Storage; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.Configuration; @@ -16,7 +15,6 @@ using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; -using System.Linq; using System.Net.Http; namespace Microsoft.Agents.Hosting.AspNetCore @@ -48,8 +46,6 @@ public static IHostApplicationBuilder AddBot(this IHostApplicati where TBot : class, IBot where TAdapter : CloudAdapter { - AddHeaderPropagation(builder); - AddHttpClientFactory(builder); AddCore(builder, storage); // Add the Bot, this is the primary worker for the bot. @@ -163,17 +159,13 @@ public static void AddCloudAdapter(this IServiceCollection services) where T } /// - /// Adds a middleware that collects headers to be propagated to an instance. + /// Adds a middleware that collects headers to be propagated. /// /// The to add the middleware to. - /// The configuration to select which headers to propagate from incoming to outgoing requests. /// A reference to the after the operation has completed. - public static IApplicationBuilder UseHeaderPropagation(this IApplicationBuilder app, Action configureOptions) + public static IApplicationBuilder UseHeaderPropagation(this IApplicationBuilder app) { ArgumentNullException.ThrowIfNull(app); - ArgumentNullException.ThrowIfNull(configureOptions); - - configureOptions(app.ApplicationServices.GetRequiredService()); return app.UseMiddleware(); } @@ -213,48 +205,5 @@ private static void AddAsyncCloudAdapterSupport(this IServiceCollection services services.AddSingleton(); services.AddSingleton(); } - - /// - /// Adds a custom Http Client factory that wraps the default . - /// - private static void AddHttpClientFactory(this IHostApplicationBuilder builder) - { - var defaultDescriptor = builder.Services.FirstOrDefault(d => d.ServiceType == typeof(IHttpClientFactory)); - if (defaultDescriptor == null) - { - builder.Services.AddSingleton(sp => new AgentsHttpClientFactory(sp)); - return; - } - - // Remove the default registration. - builder.Services.Remove(defaultDescriptor); - - // Capture a factory delegate that can create the default IHttpClientFactory instance. - Func defaultFactory; - if (defaultDescriptor.ImplementationFactory != null) - { - defaultFactory = provider => (IHttpClientFactory)defaultDescriptor.ImplementationFactory(provider); - } - else if (defaultDescriptor.ImplementationInstance != null) - { - defaultFactory = _ => (IHttpClientFactory)defaultDescriptor.ImplementationInstance; - } - else - { - defaultFactory = provider => ActivatorUtilities.CreateInstance(provider, defaultDescriptor.ImplementationType) as IHttpClientFactory; - } - - // Register custom factory that wraps the default. - builder.Services.AddSingleton(sp => new AgentsHttpClientFactory(sp, defaultFactory(sp))); - } - - /// - /// Adds header propagation services. - /// - private static void AddHeaderPropagation(this IHostApplicationBuilder builder) - { - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - } } } diff --git a/src/tests/Microsoft.Agents.Hosting.AspNetCore/ServiceCollectionExtensionsTests.cs b/src/tests/Microsoft.Agents.Hosting.AspNetCore/ServiceCollectionExtensionsTests.cs index 4dbba422..54ef5b81 100644 --- a/src/tests/Microsoft.Agents.Hosting.AspNetCore/ServiceCollectionExtensionsTests.cs +++ b/src/tests/Microsoft.Agents.Hosting.AspNetCore/ServiceCollectionExtensionsTests.cs @@ -11,7 +11,6 @@ using Microsoft.Agents.Client; using Microsoft.Agents.Core.Models; using Microsoft.Agents.Hosting.AspNetCore.BackgroundQueue; -using Microsoft.Agents.Hosting.AspNetCore.HeaderPropagation; using Microsoft.Agents.Storage; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -54,8 +53,6 @@ public void AddBot_ShouldSetServices() .Select(e => e.ImplementationType ?? e.ServiceType) .ToList(); var expected = new List{ - typeof(HeaderPropagationOptions), - typeof(HeaderPropagationContext), typeof(IHttpClientFactory), typeof(ConfigurationConnections), typeof(RestChannelServiceClientFactory), From cda0688451d3e8e535e4e32991a9a766c0eb8106 Mon Sep 17 00:00:00 2001 From: Joel Mut Date: Mon, 7 Apr 2025 19:24:38 +0100 Subject: [PATCH 6/9] fix merge main issues --- .../BackgroundQueue/ActivityWithClaims.cs | 3 ++- .../BackgroundQueue/HostedActivityService.cs | 20 ++----------------- .../BackgroundQueue/IActivityTaskQueue.cs | 1 + .../Hosting/AspNetCore/CloudAdapter.cs | 4 ++-- .../CloudAdapterTests.cs | 6 +----- .../ServiceCollectionExtensionsTests.cs | 2 -- 6 files changed, 8 insertions(+), 28 deletions(-) diff --git a/src/libraries/Hosting/AspNetCore/BackgroundQueue/ActivityWithClaims.cs b/src/libraries/Hosting/AspNetCore/BackgroundQueue/ActivityWithClaims.cs index 1b9e3308..6eefa65e 100644 --- a/src/libraries/Hosting/AspNetCore/BackgroundQueue/ActivityWithClaims.cs +++ b/src/libraries/Hosting/AspNetCore/BackgroundQueue/ActivityWithClaims.cs @@ -35,7 +35,8 @@ public class ActivityWithClaims /// Invoked when ProcessActivity is done. Ignored if IsProactive. /// public Action OnComplete { get; set; } - + + /// /// Headers used for the current request. /// public IHeaderDictionary Headers { get; set; } diff --git a/src/libraries/Hosting/AspNetCore/BackgroundQueue/HostedActivityService.cs b/src/libraries/Hosting/AspNetCore/BackgroundQueue/HostedActivityService.cs index 97cb69d0..9263a579 100644 --- a/src/libraries/Hosting/AspNetCore/BackgroundQueue/HostedActivityService.cs +++ b/src/libraries/Hosting/AspNetCore/BackgroundQueue/HostedActivityService.cs @@ -7,7 +7,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Agents.Authentication; -using Microsoft.Agents.BotBuilder; +using Microsoft.Agents.Builder; using Microsoft.Agents.Core.HeaderPropagation; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; @@ -134,40 +134,24 @@ private Task GetTaskFromWorkItem(ActivityWithClaims activityWithClaims, Cancella // We must go back through DI to get the IAgent. This is because the IAgent is typically transient, and anything // else that is transient as part of the Agent, that uses IServiceProvider will encounter error since that is scoped // and disposed before this gets called. -<<<<<<< HEAD:src/libraries/Hosting/AspNetCore/BackgroundActivityService/HostedActivityService.cs - var bot = _serviceProvider.GetService(activityWithClaims.BotType ?? typeof(IBot)); - HeaderPropagationContext.HeadersFromRequest = activityWithClaims.Headers; -======= var agent = _serviceProvider.GetService(activityWithClaims.AgentType ?? typeof(IAgent)); ->>>>>>> main:src/libraries/Hosting/AspNetCore/BackgroundQueue/HostedActivityService.cs + HeaderPropagationContext.HeadersFromRequest = activityWithClaims.Headers; if (activityWithClaims.IsProactive) { await _adapter.ProcessProactiveAsync( activityWithClaims.ClaimsIdentity, activityWithClaims.Activity, -<<<<<<< HEAD:src/libraries/Hosting/AspNetCore/BackgroundActivityService/HostedActivityService.cs - activityWithClaims.ProactiveAudience ?? BotClaims.GetTokenAudience(activityWithClaims.ClaimsIdentity), - ((IBot)bot).OnTurnAsync, -======= activityWithClaims.ProactiveAudience ?? AgentClaims.GetTokenAudience(activityWithClaims.ClaimsIdentity), ((IAgent)agent).OnTurnAsync, ->>>>>>> main:src/libraries/Hosting/AspNetCore/BackgroundQueue/HostedActivityService.cs stoppingToken).ConfigureAwait(false); } else { -<<<<<<< HEAD:src/libraries/Hosting/AspNetCore/BackgroundActivityService/HostedActivityService.cs - await _adapter.ProcessActivityAsync( - activityWithClaims.ClaimsIdentity, - activityWithClaims.Activity, - ((IBot)bot).OnTurnAsync, -======= var response = await _adapter.ProcessActivityAsync( activityWithClaims.ClaimsIdentity, activityWithClaims.Activity, ((IAgent)agent).OnTurnAsync, ->>>>>>> main:src/libraries/Hosting/AspNetCore/BackgroundQueue/HostedActivityService.cs stoppingToken).ConfigureAwait(false); activityWithClaims.OnComplete?.Invoke(response); diff --git a/src/libraries/Hosting/AspNetCore/BackgroundQueue/IActivityTaskQueue.cs b/src/libraries/Hosting/AspNetCore/BackgroundQueue/IActivityTaskQueue.cs index 39296c60..8be78eb2 100644 --- a/src/libraries/Hosting/AspNetCore/BackgroundQueue/IActivityTaskQueue.cs +++ b/src/libraries/Hosting/AspNetCore/BackgroundQueue/IActivityTaskQueue.cs @@ -27,6 +27,7 @@ public interface IActivityTaskQueue /// /// /// + /// Headers used for the current request. void QueueBackgroundActivity(ClaimsIdentity claimsIdentity, IActivity activity, bool proactive = false, string proactiveAudience = null, Type agent = null, Action onComplete = null, IHeaderDictionary headers = null); /// diff --git a/src/libraries/Hosting/AspNetCore/CloudAdapter.cs b/src/libraries/Hosting/AspNetCore/CloudAdapter.cs index d0bf8f42..dd60586a 100644 --- a/src/libraries/Hosting/AspNetCore/CloudAdapter.cs +++ b/src/libraries/Hosting/AspNetCore/CloudAdapter.cs @@ -9,10 +9,10 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using Microsoft.Agents.Core.Models; -using Microsoft.Agents.BotBuilder; +using Microsoft.Agents.Builder; using System.Text; using Microsoft.Agents.Core.Errors; -using System.Linq; +using Microsoft.Agents.Hosting.AspNetCore.BackgroundQueue; namespace Microsoft.Agents.Hosting.AspNetCore { diff --git a/src/tests/Microsoft.Agents.Hosting.AspNetCore/CloudAdapterTests.cs b/src/tests/Microsoft.Agents.Hosting.AspNetCore/CloudAdapterTests.cs index f8ca73f5..bd2dac86 100644 --- a/src/tests/Microsoft.Agents.Hosting.AspNetCore/CloudAdapterTests.cs +++ b/src/tests/Microsoft.Agents.Hosting.AspNetCore/CloudAdapterTests.cs @@ -125,11 +125,7 @@ public async Task ProcessAsync_ShouldSetUnauthorized() var context = CreateHttpContext(new(ActivityTypes.Message, conversation: new(id: "test"))); var bot = new ActivityHandler(); -<<<<<<< HEAD - record.Queue.Setup(e => e.QueueBackgroundActivity(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) -======= - record.Queue.Setup(e => e.QueueBackgroundActivity(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>())) ->>>>>>> main + record.Queue.Setup(e => e.QueueBackgroundActivity(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny())) .Throws(new UnauthorizedAccessException()) .Verifiable(Times.Once); diff --git a/src/tests/Microsoft.Agents.Hosting.AspNetCore/ServiceCollectionExtensionsTests.cs b/src/tests/Microsoft.Agents.Hosting.AspNetCore/ServiceCollectionExtensionsTests.cs index 6b86ebeb..9f0f4003 100644 --- a/src/tests/Microsoft.Agents.Hosting.AspNetCore/ServiceCollectionExtensionsTests.cs +++ b/src/tests/Microsoft.Agents.Hosting.AspNetCore/ServiceCollectionExtensionsTests.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Net.Http; using Microsoft.Agents.Authentication; using Microsoft.Agents.Builder; using Microsoft.Agents.Builder.Compat; @@ -51,7 +50,6 @@ public void AddBot_ShouldSetServices() .Select(e => e.ImplementationType ?? e.ServiceType) .ToList(); var expected = new List{ - typeof(IHttpClientFactory), typeof(ConfigurationConnections), typeof(RestChannelServiceClientFactory), // CloudAdapter services. From 114239265450e54d0e73d1c4c802f70a075314fa Mon Sep 17 00:00:00 2001 From: Joel Mut Date: Wed, 9 Apr 2025 10:50:32 +0100 Subject: [PATCH 7/9] Improve comments --- .../HeaderPropagation/HeaderPropagationContext.cs | 2 +- .../Connector/HeaderPropagation.cs | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/libraries/Core/Microsoft.Agents.Core/HeaderPropagation/HeaderPropagationContext.cs b/src/libraries/Core/Microsoft.Agents.Core/HeaderPropagation/HeaderPropagationContext.cs index 3d8406c3..a6b3c92d 100644 --- a/src/libraries/Core/Microsoft.Agents.Core/HeaderPropagation/HeaderPropagationContext.cs +++ b/src/libraries/Core/Microsoft.Agents.Core/HeaderPropagation/HeaderPropagationContext.cs @@ -10,7 +10,7 @@ namespace Microsoft.Agents.Core.HeaderPropagation; /// -/// Shared context between the and for propagating headers. +/// Shared context to manage request headers that will be used to propagate them in the . /// public class HeaderPropagationContext() { diff --git a/src/libraries/Extensions/Microsoft.Agents.Extensions.Teams/Connector/HeaderPropagation.cs b/src/libraries/Extensions/Microsoft.Agents.Extensions.Teams/Connector/HeaderPropagation.cs index 58358064..e11e6753 100644 --- a/src/libraries/Extensions/Microsoft.Agents.Extensions.Teams/Connector/HeaderPropagation.cs +++ b/src/libraries/Extensions/Microsoft.Agents.Extensions.Teams/Connector/HeaderPropagation.cs @@ -7,7 +7,6 @@ namespace Microsoft.Agents.Extensions.Teams.Connector { - // This class could implement an interface to guide customers on how to implement the SetHeaders method. [HeaderPropagation] internal class HeaderPropagation { @@ -17,8 +16,11 @@ public static Dictionary SetHeaders() // Possibly provide an interface or abstract class to guide customers on how to implement the SetHeaders method. return new Dictionary() { - { "User-Agent", "Teams-123" }, - { "Testing-Propagation-Id", "" } + // 1. Provide a header without value to propagate the incoming request value to outgoing requests. + // { "User-Agent", "" } + + // 2. Provide a header with value to propagate it to outgoing requests. + //{ "User-Agent", "< VALUE-TO-PROPAGATE >" } }; } } From 799b893bb7f1eef805350239c656a77d19bc32b4 Mon Sep 17 00:00:00 2001 From: Joel Mut Date: Thu, 24 Apr 2025 17:02:19 +0100 Subject: [PATCH 8/9] Add Attribute interface to customize headers to propagate --- .../HttpAgentClient.cs | 3 + .../HeaderPropagationAttribute.cs | 37 +++++--- .../HeaderPropagationContext.cs | 38 +++++--- .../HeaderPropagationEntry.cs | 27 ++++++ .../HeaderPropagationEntryCollection.cs | 94 +++++++++++++++++++ .../HeaderPropagationEntryTypes.cs | 36 +++++++ .../HeaderPropagationExtensions.cs | 9 ++ .../IHeaderPropagationAttribute.cs | 16 ++++ .../Connector/HeaderPropagation.cs | 17 +--- 9 files changed, 238 insertions(+), 39 deletions(-) create mode 100644 src/libraries/Core/Microsoft.Agents.Core/HeaderPropagation/HeaderPropagationEntry.cs create mode 100644 src/libraries/Core/Microsoft.Agents.Core/HeaderPropagation/HeaderPropagationEntryCollection.cs create mode 100644 src/libraries/Core/Microsoft.Agents.Core/HeaderPropagation/HeaderPropagationEntryTypes.cs create mode 100644 src/libraries/Core/Microsoft.Agents.Core/HeaderPropagation/IHeaderPropagationAttribute.cs diff --git a/src/libraries/Client/Microsoft.Agents.Client/HttpAgentClient.cs b/src/libraries/Client/Microsoft.Agents.Client/HttpAgentClient.cs index 33c2f2ba..56961010 100644 --- a/src/libraries/Client/Microsoft.Agents.Client/HttpAgentClient.cs +++ b/src/libraries/Client/Microsoft.Agents.Client/HttpAgentClient.cs @@ -14,6 +14,7 @@ using Microsoft.Agents.Authentication; using Microsoft.Agents.Builder; using Microsoft.Agents.Client.Errors; +using Microsoft.Agents.Core.HeaderPropagation; using Microsoft.Agents.Core.Models; using Microsoft.Agents.Core.Serialization; using Microsoft.Extensions.Logging; @@ -245,6 +246,8 @@ private async Task SendRequest(IActivity activity, Cancella using var httpClient = _httpClientFactory.CreateClient(nameof(HttpAgentClient)); + httpClient.AddHeaderPropagation(); + // Add the auth header to the HTTP request. var tokenResult = await _tokenProvider.GetAccessTokenAsync(_settings.ConnectionSettings.ResourceUrl, [$"{_settings.ConnectionSettings.ClientId}/.default"]).ConfigureAwait(false); httpRequestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", tokenResult); diff --git a/src/libraries/Core/Microsoft.Agents.Core/HeaderPropagation/HeaderPropagationAttribute.cs b/src/libraries/Core/Microsoft.Agents.Core/HeaderPropagation/HeaderPropagationAttribute.cs index 16f0caa7..a4878c76 100644 --- a/src/libraries/Core/Microsoft.Agents.Core/HeaderPropagation/HeaderPropagationAttribute.cs +++ b/src/libraries/Core/Microsoft.Agents.Core/HeaderPropagation/HeaderPropagationAttribute.cs @@ -1,40 +1,53 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -using Microsoft.Extensions.Primitives; using System; using System.Collections.Generic; using System.Reflection; namespace Microsoft.Agents.Core.HeaderPropagation; +/// +/// Attribute to load headers for header propagation. +/// This attribute should be applied to classes that implement the interface. +/// +[AttributeUsage(AttributeTargets.Class)] public class HeaderPropagationAttribute : Attribute { - internal static void SetHeadersSerialization() + internal static void LoadHeaders() { - //init newly loaded assemblies - AppDomain.CurrentDomain.AssemblyLoad += (s, o) => SetHeadersAssembly(o.LoadedAssembly); + // Init newly loaded assemblies + AppDomain.CurrentDomain.AssemblyLoad += (s, o) => LoadHeadersAssembly(o.LoadedAssembly); - //and all the ones we currently have loaded + // And all the ones we currently have loaded foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) { - SetHeadersAssembly(assembly); + LoadHeadersAssembly(assembly); } } - private static void SetHeadersAssembly(Assembly assembly) + private static void LoadHeadersAssembly(Assembly assembly) { - foreach (var type in GetLoadOnsetHeadersTypes(assembly)) + foreach (var type in GetLoadHeadersTypes(assembly)) { - var setHeaders = type.GetMethod("SetHeaders", BindingFlags.Static | BindingFlags.Public); - if (setHeaders?.Invoke(assembly, null) is IDictionary headers) + var loadHeaders = type.GetMethod(nameof(LoadHeaders), BindingFlags.Static | BindingFlags.Public); + + if (loadHeaders == null) { - HeaderPropagationContext.HeadersToPropagate = headers; + continue; } + + if (!typeof(IHeaderPropagationAttribute).IsAssignableFrom(type)) + { + throw new InvalidOperationException( + $"Type '{type.FullName}' is marked with [HeaderPropagation] but does not implement IHeaderPropagationAttribute."); + } + + loadHeaders.Invoke(assembly, [HeaderPropagationContext.HeadersToPropagate]); } } - private static IEnumerable GetLoadOnsetHeadersTypes(Assembly assembly) + private static IEnumerable GetLoadHeadersTypes(Assembly assembly) { foreach (Type type in assembly.GetTypes()) { diff --git a/src/libraries/Core/Microsoft.Agents.Core/HeaderPropagation/HeaderPropagationContext.cs b/src/libraries/Core/Microsoft.Agents.Core/HeaderPropagation/HeaderPropagationContext.cs index a6b3c92d..22ba4746 100644 --- a/src/libraries/Core/Microsoft.Agents.Core/HeaderPropagation/HeaderPropagationContext.cs +++ b/src/libraries/Core/Microsoft.Agents.Core/HeaderPropagation/HeaderPropagationContext.cs @@ -15,11 +15,11 @@ namespace Microsoft.Agents.Core.HeaderPropagation; public class HeaderPropagationContext() { private static readonly AsyncLocal> _headersFromRequest = new(); - private static Dictionary _headersToPropagate = new(); + private static HeaderPropagationEntryCollection _headersToPropagate = new(); static HeaderPropagationContext() { - HeaderPropagationAttribute.SetHeadersSerialization(); + HeaderPropagationAttribute.LoadHeaders(); } /// @@ -41,20 +41,16 @@ public static IDictionary HeadersFromRequest /// /// Gets or sets the headers to allow during the propagation. - /// Providing a header with a value will override the one in the request, otherwise the value from the request will be used. /// - public static IDictionary HeadersToPropagate + public static HeaderPropagationEntryCollection HeadersToPropagate { get { - var defaultHeaders = new Dictionary { - { "x-ms-correlation-id", "" } - }; - return defaultHeaders.Concat(_headersToPropagate).ToDictionary(StringComparer.InvariantCultureIgnoreCase); + return _headersToPropagate; } set { - _headersToPropagate = _headersToPropagate.Concat(value).ToDictionary(StringComparer.InvariantCultureIgnoreCase); + _headersToPropagate = value ?? new(); } } @@ -72,12 +68,28 @@ private static Dictionary FilterHeaders(Dictionary +/// Represents a single header entry used for header propagation. +/// +public class HeaderPropagationEntry +{ + /// + /// Key of the header entry. + /// + public string Key { get; set; } = string.Empty; + + /// + /// Value of the header entry. + /// + public StringValues Value { get; set; } = new StringValues(string.Empty); + + /// + /// Action of the header entry (Add, Append, etc.). + /// + public HeaderPropagationEntryAction Action; +} diff --git a/src/libraries/Core/Microsoft.Agents.Core/HeaderPropagation/HeaderPropagationEntryCollection.cs b/src/libraries/Core/Microsoft.Agents.Core/HeaderPropagation/HeaderPropagationEntryCollection.cs new file mode 100644 index 00000000..4796a7d5 --- /dev/null +++ b/src/libraries/Core/Microsoft.Agents.Core/HeaderPropagation/HeaderPropagationEntryCollection.cs @@ -0,0 +1,94 @@ +// 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; + +/// +/// Represents a collection of all the header entries that are to be propagated to the outgoing request. +/// +public class HeaderPropagationEntryCollection +{ + private readonly Dictionary _entries = []; + + /// + /// Gets the collection of header entries to be propagated to the outgoing request. + /// + public List Entries + { + get => [.. _entries.Select(x => x.Value)]; + } + + /// + /// Attempts to add a new header entry to the collection. + /// + /// + /// If the key already exists in the incoming request headers collection, it will be ignored. + /// + /// The key of the element to add. + /// The value to add for the specified key. + public void Add(string key, StringValues value) + { + _entries[key] = new HeaderPropagationEntry + { + Key = key, + Value = value, + Action = HeaderPropagationEntryAction.Add + }; + } + + /// + /// Appends a new header value to an existing key. + /// + /// + /// If the key does not exist in the incoming request headers collection, it will be ignored. + /// + /// The key of the element to add. + /// The value to add for the specified key. + public void Append(string key, StringValues value) + { + _entries[key] = new HeaderPropagationEntry + { + Key = key, + Value = value, + Action = HeaderPropagationEntryAction.Append + }; + } + + /// + /// Propagates the incoming request header value to the outgoing request. + /// + /// + /// If the key does not exist in the incoming request headers collection, it will be ignored. + /// + /// The key of the element to add. + public void Propagate(string key) + { + _entries[key] = new HeaderPropagationEntry + { + Key = key, + Action = HeaderPropagationEntryAction.Propagate + }; + } + + /// + /// Overrides the header value of an existing key. + /// + /// + /// If the key does not exist in the incoming request headers collection, it will add it. + /// + /// The key of the element to add. + /// The value to add for the specified key. + public void Override(string key, StringValues value) + { + _entries[key] = new HeaderPropagationEntry + { + Key = key, + Value = value, + Action = HeaderPropagationEntryAction.Override + }; + } +} diff --git a/src/libraries/Core/Microsoft.Agents.Core/HeaderPropagation/HeaderPropagationEntryTypes.cs b/src/libraries/Core/Microsoft.Agents.Core/HeaderPropagation/HeaderPropagationEntryTypes.cs new file mode 100644 index 00000000..fa937b8e --- /dev/null +++ b/src/libraries/Core/Microsoft.Agents.Core/HeaderPropagation/HeaderPropagationEntryTypes.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Runtime.Serialization; + +namespace Microsoft.Agents.Core.HeaderPropagation; + +/// +/// Represents the action of the header entry. +/// +public enum HeaderPropagationEntryAction +{ + /// + /// Adds a new header entry to the outgoing request. + /// + [EnumMember(Value = "add")] + Add, + + /// + /// Appends a new header value to an existing key in the outgoing request. + /// + [EnumMember(Value = "append")] + Append, + + /// + /// Propagates the header entry from the incoming request to the outgoing request. + /// + [EnumMember(Value = "propagate")] + Propagate, + + /// + /// Overrides an existing header entry in the outgoing request. + /// + [EnumMember(Value = "override")] + Override +} diff --git a/src/libraries/Core/Microsoft.Agents.Core/HeaderPropagation/HeaderPropagationExtensions.cs b/src/libraries/Core/Microsoft.Agents.Core/HeaderPropagation/HeaderPropagationExtensions.cs index 2e9a0e9f..51500c3d 100644 --- a/src/libraries/Core/Microsoft.Agents.Core/HeaderPropagation/HeaderPropagationExtensions.cs +++ b/src/libraries/Core/Microsoft.Agents.Core/HeaderPropagation/HeaderPropagationExtensions.cs @@ -7,8 +7,17 @@ namespace Microsoft.Agents.Core.HeaderPropagation; public static class HeaderPropagationExtensions { + /// + /// Loads incoming request headers based on a list of headers to propagate into the HttpClient. + /// + /// The . public static void AddHeaderPropagation(this HttpClient httpClient) { + if (HeaderPropagationContext.HeadersFromRequest == null) + { + return; + } + foreach (var header in HeaderPropagationContext.HeadersFromRequest) { httpClient.DefaultRequestHeaders.TryAddWithoutValidation(header.Key, [header.Value]); diff --git a/src/libraries/Core/Microsoft.Agents.Core/HeaderPropagation/IHeaderPropagationAttribute.cs b/src/libraries/Core/Microsoft.Agents.Core/HeaderPropagation/IHeaderPropagationAttribute.cs new file mode 100644 index 00000000..0d3e4831 --- /dev/null +++ b/src/libraries/Core/Microsoft.Agents.Core/HeaderPropagation/IHeaderPropagationAttribute.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace Microsoft.Agents.Core.HeaderPropagation; + +/// +/// Interface to ensure that the header propagation attribute is implemented correctly. +/// +public interface IHeaderPropagationAttribute +{ + /// + /// Loads the header entries into the outgoing request headers collection. + /// + /// A collection to operate over the headers. + abstract static void LoadHeaders(HeaderPropagationEntryCollection collection); +} diff --git a/src/libraries/Extensions/Microsoft.Agents.Extensions.Teams/Connector/HeaderPropagation.cs b/src/libraries/Extensions/Microsoft.Agents.Extensions.Teams/Connector/HeaderPropagation.cs index e11e6753..7b2e0ae8 100644 --- a/src/libraries/Extensions/Microsoft.Agents.Extensions.Teams/Connector/HeaderPropagation.cs +++ b/src/libraries/Extensions/Microsoft.Agents.Extensions.Teams/Connector/HeaderPropagation.cs @@ -1,27 +1,16 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -using System.Collections.Generic; using Microsoft.Agents.Core.HeaderPropagation; -using Microsoft.Extensions.Primitives; namespace Microsoft.Agents.Extensions.Teams.Connector { [HeaderPropagation] - internal class HeaderPropagation + internal class HeaderPropagation : IHeaderPropagationAttribute { - public static Dictionary SetHeaders() + public static void LoadHeaders(HeaderPropagationEntryCollection collection) { - // TODO: this functionality could be improved by providing a helper method to set the header either with a key or a key and value. - // Possibly provide an interface or abstract class to guide customers on how to implement the SetHeaders method. - return new Dictionary() - { - // 1. Provide a header without value to propagate the incoming request value to outgoing requests. - // { "User-Agent", "" } - - // 2. Provide a header with value to propagate it to outgoing requests. - //{ "User-Agent", "< VALUE-TO-PROPAGATE >" } - }; + // Propagate headers to the outgoing request by adding them to the HeaderPropagationEntryCollection. } } } From 0efe9d7decf17b45b128914319a48874988c4a5a Mon Sep 17 00:00:00 2001 From: Joel Mut Date: Tue, 6 May 2025 16:44:14 +0100 Subject: [PATCH 9/9] Fix issues with netstandard --- .../HeaderPropagationAttribute.cs | 14 ++++++++------ .../HeaderPropagation/HeaderPropagationContext.cs | 8 ++++++++ .../IHeaderPropagationAttribute.cs | 2 ++ .../Connector/HeaderPropagation.cs | 5 +++++ 4 files changed, 23 insertions(+), 6 deletions(-) diff --git a/src/libraries/Core/Microsoft.Agents.Core/HeaderPropagation/HeaderPropagationAttribute.cs b/src/libraries/Core/Microsoft.Agents.Core/HeaderPropagation/HeaderPropagationAttribute.cs index a4878c76..126bac73 100644 --- a/src/libraries/Core/Microsoft.Agents.Core/HeaderPropagation/HeaderPropagationAttribute.cs +++ b/src/libraries/Core/Microsoft.Agents.Core/HeaderPropagation/HeaderPropagationAttribute.cs @@ -30,6 +30,14 @@ 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) @@ -37,12 +45,6 @@ private static void LoadHeadersAssembly(Assembly assembly) continue; } - if (!typeof(IHeaderPropagationAttribute).IsAssignableFrom(type)) - { - throw new InvalidOperationException( - $"Type '{type.FullName}' is marked with [HeaderPropagation] but does not implement IHeaderPropagationAttribute."); - } - loadHeaders.Invoke(assembly, [HeaderPropagationContext.HeadersToPropagate]); } } diff --git a/src/libraries/Core/Microsoft.Agents.Core/HeaderPropagation/HeaderPropagationContext.cs b/src/libraries/Core/Microsoft.Agents.Core/HeaderPropagation/HeaderPropagationContext.cs index 22ba4746..c6cd70bc 100644 --- a/src/libraries/Core/Microsoft.Agents.Core/HeaderPropagation/HeaderPropagationContext.cs +++ b/src/libraries/Core/Microsoft.Agents.Core/HeaderPropagation/HeaderPropagationContext.cs @@ -34,7 +34,11 @@ public static IDictionary HeadersFromRequest 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); } } @@ -78,7 +82,11 @@ private static Dictionary FilterHeaders(Dictionary /// Interface to ensure that the header propagation attribute is implemented correctly. /// @@ -14,3 +15,4 @@ public interface IHeaderPropagationAttribute /// A collection to operate over the headers. abstract static void LoadHeaders(HeaderPropagationEntryCollection collection); } +#endif \ No newline at end of file diff --git a/src/libraries/Extensions/Microsoft.Agents.Extensions.Teams/Connector/HeaderPropagation.cs b/src/libraries/Extensions/Microsoft.Agents.Extensions.Teams/Connector/HeaderPropagation.cs index 7b2e0ae8..d7d0ac6c 100644 --- a/src/libraries/Extensions/Microsoft.Agents.Extensions.Teams/Connector/HeaderPropagation.cs +++ b/src/libraries/Extensions/Microsoft.Agents.Extensions.Teams/Connector/HeaderPropagation.cs @@ -6,7 +6,12 @@ namespace Microsoft.Agents.Extensions.Teams.Connector { [HeaderPropagation] + +#if !NETSTANDARD internal class HeaderPropagation : IHeaderPropagationAttribute +#else + internal class HeaderPropagation +#endif { public static void LoadHeaders(HeaderPropagationEntryCollection collection) {