diff --git a/release_notes.md b/release_notes.md
index 7333dc06a..3babbb06b 100644
--- a/release_notes.md
+++ b/release_notes.md
@@ -5,10 +5,12 @@
### New Features
- Fail fast if extendedSessionsEnabled set to 'true' for the worker type that doesn't support extended sessions (https://github.com/Azure/azure-functions-durable-extension/pull/2732).
+- Added an `IFunctionsWorkerApplicationBuilder.ConfigureDurableExtension()` extension method for cases where auto-registration does not work (no source gen running). (#2950)
### Bug Fixes
- Fix custom connection name not working when using IDurableClientFactory.CreateClient() - contributed by [@hctan](https://github.com/hctan)
+- Made durable extension for isolated worker configuration idempotent, allowing multiple calls safely. (#2950)
### Breaking Changes
diff --git a/src/Worker.Extensions.DurableTask/DurableTaskExtensionStartup.cs b/src/Worker.Extensions.DurableTask/DurableTaskExtensionStartup.cs
index 626acd6bf..af7bab017 100644
--- a/src/Worker.Extensions.DurableTask/DurableTaskExtensionStartup.cs
+++ b/src/Worker.Extensions.DurableTask/DurableTaskExtensionStartup.cs
@@ -1,20 +1,8 @@
// 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 Azure.Core.Serialization;
using Microsoft.Azure.Functions.Worker.Core;
using Microsoft.Azure.Functions.Worker.Extensions.DurableTask;
-using Microsoft.DurableTask;
-using Microsoft.DurableTask.Client;
-using Microsoft.DurableTask.Converters;
-using Microsoft.DurableTask.Worker;
-using Microsoft.DurableTask.Worker.Shims;
-using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.DependencyInjection.Extensions;
-using Microsoft.Extensions.Hosting;
-using Microsoft.Extensions.Logging;
-using Microsoft.Extensions.Options;
[assembly: WorkerExtensionStartup(typeof(DurableTaskExtensionStartup))]
@@ -28,49 +16,6 @@ public sealed class DurableTaskExtensionStartup : WorkerExtensionStartup
///
public override void Configure(IFunctionsWorkerApplicationBuilder applicationBuilder)
{
- applicationBuilder.Services.AddSingleton();
- applicationBuilder.Services.AddOptions()
- .Configure(options => options.EnableEntitySupport = true)
- .PostConfigure((opt, sp) =>
- {
- if (GetConverter(sp) is DataConverter converter)
- {
- opt.DataConverter = converter;
- }
- });
-
- applicationBuilder.Services.AddOptions()
- .Configure(options => options.EnableEntitySupport = true)
- .PostConfigure((opt, sp) =>
- {
- if (GetConverter(sp) is DataConverter converter)
- {
- opt.DataConverter = converter;
- }
- });
-
- applicationBuilder.Services.TryAddSingleton(sp =>
- {
- DurableTaskWorkerOptions options = sp.GetRequiredService>().Value;
- ILoggerFactory factory = sp.GetRequiredService();
- return new DurableTaskShimFactory(options, factory); // For GrpcOrchestrationRunner
- });
-
- applicationBuilder.Services.Configure(o =>
- {
- o.InputConverters.Register();
- });
-
- applicationBuilder.UseMiddleware();
- }
-
- private static DataConverter? GetConverter(IServiceProvider services)
- {
- // We intentionally do not consider a DataConverter in the DI provider, or if one was already set. This is to
- // ensure serialization is consistent with the rest of Azure Functions. This is particularly important because
- // TaskActivity bindings use ObjectSerializer directly for the time being. Due to this, allowing DataConverter
- // to be set separately from ObjectSerializer would give an inconsistent serialization solution.
- WorkerOptions? worker = services.GetRequiredService>()?.Value;
- return worker?.Serializer is not null ? new ObjectConverterShim(worker.Serializer) : null;
+ applicationBuilder.ConfigureDurableExtension();
}
}
diff --git a/src/Worker.Extensions.DurableTask/FunctionsWorkerApplicationBuilderExtensions.cs b/src/Worker.Extensions.DurableTask/FunctionsWorkerApplicationBuilderExtensions.cs
new file mode 100644
index 000000000..642446dd4
--- /dev/null
+++ b/src/Worker.Extensions.DurableTask/FunctionsWorkerApplicationBuilderExtensions.cs
@@ -0,0 +1,125 @@
+// 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 Azure.Core.Serialization;
+using Microsoft.Azure.Functions.Worker.Core;
+using Microsoft.Azure.Functions.Worker.Extensions.DurableTask;
+using Microsoft.DurableTask;
+using Microsoft.DurableTask.Client;
+using Microsoft.DurableTask.Converters;
+using Microsoft.DurableTask.Worker;
+using Microsoft.DurableTask.Worker.Shims;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.Azure.Functions.Worker;
+
+///
+/// Extensions for .
+///
+public static class FunctionsWorkerApplicationBuilderExtensions
+{
+ ///
+ /// Configures the Durable Functions extension for the worker.
+ ///
+ /// The builder to configure.
+ /// The for call chaining.
+ public static IFunctionsWorkerApplicationBuilder ConfigureDurableExtension(this IFunctionsWorkerApplicationBuilder builder)
+ {
+ if (builder is null)
+ {
+ throw new ArgumentNullException(nameof(builder));
+ }
+
+ builder.Services.TryAddSingleton();
+ builder.Services.TryAddEnumerable(
+ ServiceDescriptor.Singleton, ConfigureClientOptions>());
+ builder.Services.TryAddEnumerable(
+ ServiceDescriptor.Singleton, PostConfigureClientOptions>());
+ builder.Services.TryAddEnumerable(
+ ServiceDescriptor.Singleton, ConfigureWorkerOptions>());
+ builder.Services.TryAddEnumerable(
+ ServiceDescriptor.Singleton, PostConfigureWorkerOptions>());
+
+ builder.Services.TryAddSingleton(sp =>
+ {
+ DurableTaskWorkerOptions options = sp.GetRequiredService>().Value;
+ ILoggerFactory factory = sp.GetRequiredService();
+ return new DurableTaskShimFactory(options, factory); // For GrpcOrchestrationRunner
+ });
+
+ builder.Services.TryAddEnumerable(
+ ServiceDescriptor.Singleton, ConfigureInputConverter>());
+ if (!builder.Services.Any(d => d.ServiceType == typeof(DurableTaskFunctionsMiddleware)))
+ {
+ builder.UseMiddleware();
+ }
+
+ return builder;
+ }
+
+ private class ConfigureInputConverter : IConfigureOptions
+ {
+ public void Configure(WorkerOptions options)
+ {
+ options.InputConverters.Register();
+ }
+ }
+
+ private class ConfigureClientOptions : IConfigureOptions
+ {
+ public void Configure(DurableTaskClientOptions options)
+ {
+ options.EnableEntitySupport = true;
+ }
+ }
+
+ private class PostConfigureClientOptions : IPostConfigureOptions
+ {
+ readonly IOptionsMonitor workerOptions;
+
+ public PostConfigureClientOptions(IOptionsMonitor workerOptions)
+ {
+ this.workerOptions = workerOptions;
+ }
+
+ public void PostConfigure(string name, DurableTaskClientOptions options)
+ {
+ if (this.workerOptions.Get(name).Serializer is { } serializer)
+ {
+ options.DataConverter = new ObjectConverterShim(serializer);
+ }
+ }
+ }
+
+ private class ConfigureWorkerOptions : IConfigureOptions
+ {
+ public void Configure(DurableTaskWorkerOptions options)
+ {
+ options.EnableEntitySupport = true;
+ }
+ }
+
+ private class PostConfigureWorkerOptions : IPostConfigureOptions
+ {
+ readonly IOptionsMonitor workerOptions;
+
+ public PostConfigureWorkerOptions(IOptionsMonitor workerOptions)
+ {
+ this.workerOptions = workerOptions;
+ }
+
+ public void PostConfigure(string name, DurableTaskWorkerOptions options)
+ {
+ if (this.workerOptions.Get(name).Serializer is { } serializer)
+ {
+ options.DataConverter = new ObjectConverterShim(serializer);
+ }
+ }
+ }
+}