diff --git a/extensions/Worker.Extensions.Http.AspNetCore/src/FunctionsHostBuilderExtensions.cs b/extensions/Worker.Extensions.Http.AspNetCore/src/FunctionsHostBuilderExtensions.cs index e0ca72ea8..9b5be87ad 100644 --- a/extensions/Worker.Extensions.Http.AspNetCore/src/FunctionsHostBuilderExtensions.cs +++ b/extensions/Worker.Extensions.Http.AspNetCore/src/FunctionsHostBuilderExtensions.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT License. See License.txt in the project root for license information. using System; @@ -18,6 +18,8 @@ namespace Microsoft.Extensions.Hosting /// public static class FunctionsHostBuilderExtensions { + private const string AspNetCoreIntegrationConfiguredKey = "__FunctionsAspNetCoreConfigured"; + /// /// Configures the worker to use the ASP.NET Core integration, enabling advanced HTTP features. /// @@ -63,6 +65,16 @@ public static IHostBuilder ConfigureFunctionsWebApplication(this IHostBuilder bu internal static IHostBuilder ConfigureAspNetCoreIntegration(this IHostBuilder builder) { + if (builder.Properties.TryGetValue(AspNetCoreIntegrationConfiguredKey, out var alreadyConfiguredObj) && + alreadyConfiguredObj is bool alreadyConfigured && + alreadyConfigured) + { + // Already configured, don't do it twice + return builder; + } + + builder.Properties[AspNetCoreIntegrationConfiguredKey] = true; + builder.ConfigureServices(services => { services.AddSingleton(); diff --git a/extensions/Worker.Extensions.Http.AspNetCore/src/WorkerBuilderExtensions.cs b/extensions/Worker.Extensions.Http.AspNetCore/src/WorkerBuilderExtensions.cs index 7422e7634..3a97a77f7 100644 --- a/extensions/Worker.Extensions.Http.AspNetCore/src/WorkerBuilderExtensions.cs +++ b/extensions/Worker.Extensions.Http.AspNetCore/src/WorkerBuilderExtensions.cs @@ -1,7 +1,8 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT License. See License.txt in the project root for license information. using System; +using System.Linq; using Microsoft.AspNetCore.Builder; using Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore; using Microsoft.Extensions.DependencyInjection; @@ -22,9 +23,12 @@ internal static class WorkerBuilderExtensions /// internal static IFunctionsWorkerApplicationBuilder UseAspNetCoreIntegration(this IFunctionsWorkerApplicationBuilder builder) { - if (builder is null) + ArgumentNullException.ThrowIfNull(builder, nameof(builder)); + + // Check if already configured by looking for our middleware + if (builder.Services.Any(d => d.ImplementationType == typeof(FunctionsHttpProxyingMiddleware))) { - throw new ArgumentNullException(nameof(builder)); + return builder; } builder.UseMiddleware(); @@ -36,7 +40,7 @@ internal static IFunctionsWorkerApplicationBuilder UseAspNetCoreIntegration(this builder.Services.Configure((workerOption) => { workerOption.InputConverters.RegisterAt(0); - workerOption.Capabilities.Add(Constants.HttpUriCapability, HttpUriProvider.HttpUriString); + workerOption.Capabilities[Constants.HttpUriCapability] = HttpUriProvider.HttpUriString; }); return builder; diff --git a/src/DotNetWorker.ApplicationInsights/FunctionsApplicationInsightsExtensions.cs b/src/DotNetWorker.ApplicationInsights/FunctionsApplicationInsightsExtensions.cs index c45e8b6a1..41157b24c 100644 --- a/src/DotNetWorker.ApplicationInsights/FunctionsApplicationInsightsExtensions.cs +++ b/src/DotNetWorker.ApplicationInsights/FunctionsApplicationInsightsExtensions.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT License. See License.txt in the project root for license information. using System; @@ -26,6 +26,12 @@ public static IServiceCollection ConfigureFunctionsApplicationInsights(this ISer throw new ArgumentNullException(nameof(services)); } + // Check if already configured by looking for our validation service + if (services.Any(d => d.ImplementationType == typeof(ApplicationInsightsValidationService))) + { + return services; + } + services.ConfigureOptions(); services.AddSingleton, AppServiceOptionsInitializer>(); services.AddSingleton(); diff --git a/src/DotNetWorker.ApplicationInsights/release_notes.md b/src/DotNetWorker.ApplicationInsights/release_notes.md index 756698416..21b737490 100644 --- a/src/DotNetWorker.ApplicationInsights/release_notes.md +++ b/src/DotNetWorker.ApplicationInsights/release_notes.md @@ -3,4 +3,5 @@ ### Microsoft.Azure.Functions.Worker.ApplicationInsights - Updating `Azure.Identity` from 1.12.0 to 1.17.0 -- Updating `Microsoft.ApplicationInsights.PerfCounterCollector` from 2.22.0 to 2.23.0 \ No newline at end of file +- Updating `Microsoft.ApplicationInsights.PerfCounterCollector` from 2.22.0 to 2.23.0 +- Improved idempotency of service registration calls (#3273) \ No newline at end of file diff --git a/test/DotNetWorker.Tests/ApplicationInsights/ApplicationInsightsConfigurationTests.cs b/test/DotNetWorker.Tests/ApplicationInsights/ApplicationInsightsConfigurationTests.cs index 46f6f11c6..f354fe2c8 100644 --- a/test/DotNetWorker.Tests/ApplicationInsights/ApplicationInsightsConfigurationTests.cs +++ b/test/DotNetWorker.Tests/ApplicationInsights/ApplicationInsightsConfigurationTests.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using Microsoft.ApplicationInsights.Extensibility; @@ -62,6 +62,8 @@ private static void Verify(IHostBuilder builder) builder.Build(); Assert.Contains(typeof(FunctionsTelemetryInitializer), initializers); + Assert.Equal(6, initializers.Count()); Assert.Contains(typeof(FunctionsTelemetryModule), modules); + Assert.Equal(8, modules.Count()); } } diff --git a/test/extensions/Worker.Extensions.Http.AspNetCore.Tests/FunctionsHostBuilderExtensionsTests.cs b/test/extensions/Worker.Extensions.Http.AspNetCore.Tests/FunctionsHostBuilderExtensionsTests.cs index bc5556666..c97ef2ab4 100644 --- a/test/extensions/Worker.Extensions.Http.AspNetCore.Tests/FunctionsHostBuilderExtensionsTests.cs +++ b/test/extensions/Worker.Extensions.Http.AspNetCore.Tests/FunctionsHostBuilderExtensionsTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT License. See License.txt in the project root for license information. using Microsoft.Azure.Functions.Worker; @@ -58,6 +58,44 @@ public void ConfigureFunctionsWebApplication_ShouldConfigureFunctionsWebApplicat VerifyRegistrationOfAspNetCoreIntegrationServices(host); } + [Fact] + public void ConfigureFunctionsWebApplication_CalledTwice_ShouldBeIdempotent() + { + var builder = new HostBuilder(); + + int callbackCount = 0; + + // Call ConfigureFunctionsWebApplication twice but should call callbacks each time + builder.ConfigureFunctionsWebApplication((_, _) => callbackCount++); + builder.ConfigureFunctionsWebApplication((_, _) => callbackCount++); + + IServiceCollection serviceCollection = default!; + builder.ConfigureServices(services => + { + serviceCollection = services; + }); + + var host = builder.Build(); + + // Verify services are registered + VerifyRegistrationOfAspNetCoreIntegrationServices(host); + + Assert.Equal(2, callbackCount); + + ValidateSingleServiceType(serviceCollection, typeof(FunctionsHttpProxyingMiddleware)); + ValidateSingleServiceType(serviceCollection, typeof(IHttpCoordinator)); + ValidateSingleServiceType(serviceCollection, typeof(FunctionsEndpointDataSource)); + + static void ValidateSingleServiceType(IServiceCollection services, Type type) + { + Assert.NotNull(services); + var coordinatorDescriptors = services + .Where(d => d.ServiceType == type) + .ToList(); + Assert.Single(coordinatorDescriptors); + } + } + private static void VerifyRegistrationOfCustomMiddleware(IHost host) { Assert.NotNull(host.Services.GetService());