Skip to content

Commit f95755b

Browse files
Add IDurableClientFactory abstraction (#1125)
Use a new dependency injection abstraction to make the creation of `IDurableClient` objects simpler. This has two large benefits: 1. It allows `IDurableClient` instances to be utilized via services that are injected via Dependency Injection. 2. It allows apps that aren't explicitly Durable Functions apps to create instances of `IDurableClient` to manage their Durable Entities and Orchestrations. Co-authored-by: [email protected] <[email protected]>
1 parent 044d234 commit f95755b

13 files changed

+591
-45
lines changed

src/WebJobs.Extensions.DurableTask/ContextImplementations/DurableClient.cs

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -41,25 +41,37 @@ private static readonly OrchestrationRuntimeStatus[] RunningStatus
4141
private readonly DurableTaskExtension config;
4242
private readonly DurableClientAttribute attribute; // for rehydrating a Client after a webhook
4343
private readonly MessagePayloadDataConverter messageDataConverter;
44+
private readonly DurableTaskOptions durableTaskOptions;
4445

4546
internal DurableClient(
4647
DurabilityProvider serviceClient,
47-
DurableTaskExtension config,
4848
HttpApiHandler httpHandler,
49-
DurableClientAttribute attribute)
49+
DurableClientAttribute attribute,
50+
MessagePayloadDataConverter messageDataConverter,
51+
EndToEndTraceHelper traceHelper,
52+
DurableTaskOptions durableTaskOptions)
5053
{
51-
this.config = config ?? throw new ArgumentNullException(nameof(config));
52-
53-
this.messageDataConverter = config.MessageDataConverter;
54+
this.messageDataConverter = messageDataConverter;
5455

5556
this.client = new TaskHubClient(serviceClient, this.messageDataConverter);
5657
this.durabilityProvider = serviceClient;
57-
this.traceHelper = config.TraceHelper;
58+
this.traceHelper = traceHelper;
5859
this.httpApiHandler = httpHandler;
59-
this.hubName = attribute.TaskHub ?? config.Options.HubName;
60+
this.durableTaskOptions = durableTaskOptions;
61+
this.hubName = attribute.TaskHub ?? this.durableTaskOptions.HubName;
6062
this.attribute = attribute;
6163
}
6264

65+
internal DurableClient(
66+
DurabilityProvider serviceClient,
67+
DurableTaskExtension config,
68+
HttpApiHandler httpHandler,
69+
DurableClientAttribute attribute)
70+
: this(serviceClient, httpHandler, attribute, config.MessageDataConverter, config.TraceHelper, config.Options)
71+
{
72+
this.config = config;
73+
}
74+
6375
public string TaskHubName => this.hubName;
6476

6577
internal DurabilityProvider DurabilityProvider => this.durabilityProvider;
@@ -113,7 +125,7 @@ async Task<IActionResult> IDurableOrchestrationClient.WaitForCompletionOrCreateC
113125
/// <inheritdoc />
114126
async Task<string> IDurableOrchestrationClient.StartNewAsync<T>(string orchestratorFunctionName, string instanceId, T input)
115127
{
116-
if (this.ClientReferencesCurrentApp(this))
128+
if (!this.attribute.ExternalClient && this.ClientReferencesCurrentApp(this))
117129
{
118130
this.config.ThrowIfFunctionDoesNotExist(orchestratorFunctionName, FunctionType.Orchestrator);
119131
}
@@ -154,7 +166,7 @@ async Task<string> IDurableOrchestrationClient.StartNewAsync<T>(string orchestra
154166

155167
private OrchestrationStatus[] GetStatusesNotToOverride()
156168
{
157-
var overridableStates = this.config.Options.OverridableExistingInstanceStates;
169+
var overridableStates = this.durableTaskOptions.OverridableExistingInstanceStates;
158170
if (overridableStates == OverridableStates.NonRunningStates)
159171
{
160172
return new OrchestrationStatus[]
@@ -322,7 +334,7 @@ private bool ClientReferencesCurrentApp(DurableClient client)
322334

323335
private bool TaskHubMatchesCurrentApp(DurableClient client)
324336
{
325-
var taskHubName = this.config.Options.HubName;
337+
var taskHubName = this.durableTaskOptions.HubName;
326338
return client.TaskHubName.Equals(taskHubName);
327339
}
328340

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT License. See LICENSE in the project root for license information.
3+
4+
using System;
5+
using System.Collections.Concurrent;
6+
using Microsoft.Azure.WebJobs.Extensions.DurableTask.Options;
7+
using Microsoft.Extensions.Logging;
8+
using Microsoft.Extensions.Options;
9+
using Newtonsoft.Json;
10+
11+
namespace Microsoft.Azure.WebJobs.Extensions.DurableTask.ContextImplementations
12+
{
13+
/// <summary>
14+
/// Factory class to create Durable Client to start works outside an azure function context.
15+
/// </summary>
16+
public class DurableClientFactory : IDurableClientFactory, IDisposable
17+
{
18+
// Creating client objects is expensive, so we cache them when the attributes match.
19+
// Note that DurableClientAttribute defines a custom equality comparer.
20+
private readonly ConcurrentDictionary<DurableClientAttribute, DurableClient> cachedClients =
21+
new ConcurrentDictionary<DurableClientAttribute, DurableClient>();
22+
23+
private readonly ConcurrentDictionary<DurableClientAttribute, HttpApiHandler> cachedHttpListeners =
24+
new ConcurrentDictionary<DurableClientAttribute, HttpApiHandler>();
25+
26+
private readonly DurableClientOptions defaultDurableClientOptions;
27+
private readonly DurableTaskOptions durableTaskOptions;
28+
private readonly IDurabilityProviderFactory durabilityProviderFactory;
29+
private readonly ILogger logger;
30+
31+
/// <summary>
32+
/// Initializes a new instance of the <see cref="DurableClientFactory"/> class.
33+
/// </summary>
34+
/// <param name="defaultDurableClientOptions">Default Options to Build Durable Clients.</param>
35+
/// <param name="orchestrationServiceFactory">The factory used to create orchestration service based on the configured storage provider.</param>
36+
/// <param name="loggerFactory">The logger factory used for extension-specific logging and orchestration tracking.</param>
37+
/// <param name="durableTaskOptions">The configuration options for this extension.</param>
38+
/// <param name="messageSerializerSettingsFactory">The factory used to create <see cref="JsonSerializerSettings"/> for message settings.</param>
39+
public DurableClientFactory(
40+
IOptions<DurableClientOptions> defaultDurableClientOptions,
41+
IOptions<DurableTaskOptions> durableTaskOptions,
42+
IDurabilityProviderFactory orchestrationServiceFactory,
43+
ILoggerFactory loggerFactory,
44+
IMessageSerializerSettingsFactory messageSerializerSettingsFactory = null)
45+
{
46+
this.logger = loggerFactory.CreateLogger(DurableTaskExtension.LoggerCategoryName);
47+
48+
this.durabilityProviderFactory = orchestrationServiceFactory;
49+
this.defaultDurableClientOptions = defaultDurableClientOptions.Value;
50+
this.durableTaskOptions = durableTaskOptions?.Value ?? new DurableTaskOptions();
51+
52+
this.MessageDataConverter = DurableTaskExtension.CreateMessageDataConverter(messageSerializerSettingsFactory);
53+
this.TraceHelper = new EndToEndTraceHelper(this.logger, this.durableTaskOptions.Tracing.TraceReplayEvents);
54+
}
55+
56+
internal MessagePayloadDataConverter MessageDataConverter { get; private set; }
57+
58+
internal EndToEndTraceHelper TraceHelper { get; private set; }
59+
60+
/// <summary>
61+
/// Gets a <see cref="IDurableClient"/> using configuration from a <see cref="DurableClientOptions"/> instance.
62+
/// </summary>
63+
/// <param name="durableClientOptions">options containing the client configuration parameters.</param>
64+
/// <returns>Returns a <see cref="IDurableClient"/> instance. The returned instance may be a cached instance.</returns>
65+
public IDurableClient CreateClient(DurableClientOptions durableClientOptions)
66+
{
67+
if (durableClientOptions == null)
68+
{
69+
throw new ArgumentException("Please configure 'DurableClientOptions'");
70+
}
71+
72+
if (string.IsNullOrWhiteSpace(durableClientOptions.TaskHub))
73+
{
74+
throw new ArgumentException("Please provide value for 'TaskHub'");
75+
}
76+
77+
DurableClientAttribute attribute = new DurableClientAttribute(durableClientOptions);
78+
79+
HttpApiHandler httpApiHandler = this.cachedHttpListeners.GetOrAdd(
80+
attribute,
81+
attr =>
82+
{
83+
return new HttpApiHandler(null, null, this.durableTaskOptions, this.logger);
84+
});
85+
86+
DurableClient client = this.cachedClients.GetOrAdd(
87+
attribute,
88+
attr =>
89+
{
90+
DurabilityProvider innerClient = this.durabilityProviderFactory.GetDurabilityProvider(attribute);
91+
return new DurableClient(innerClient, httpApiHandler, attribute, this.MessageDataConverter, this.TraceHelper, this.durableTaskOptions);
92+
});
93+
94+
return client;
95+
}
96+
97+
/// <summary>
98+
/// Gets a <see cref="IDurableClient"/> using configuration from a <see cref="DurableClientOptions"/> instance.
99+
/// </summary>
100+
/// <returns>Returns a <see cref="IDurableClient"/> instance. The returned instance may be a cached instance.</returns>
101+
public IDurableClient CreateClient()
102+
{
103+
return this.CreateClient(this.defaultDurableClientOptions);
104+
}
105+
106+
/// <inheritdoc />
107+
public void Dispose()
108+
{
109+
foreach (var cachedHttpListener in this.cachedHttpListeners)
110+
{
111+
cachedHttpListener.Value?.Dispose();
112+
}
113+
}
114+
}
115+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT License. See LICENSE in the project root for license information.
3+
4+
using Microsoft.Azure.WebJobs.Extensions.DurableTask.Options;
5+
6+
namespace Microsoft.Azure.WebJobs.Extensions.DurableTask.ContextImplementations
7+
{
8+
/// <summary>
9+
/// Factory class to create Durable Client to start works outside an azure function context.
10+
/// </summary>
11+
public interface IDurableClientFactory
12+
{
13+
/// <summary>
14+
/// Gets a <see cref="IDurableClient"/> using configuration from a <see cref="DurableClientOptions"/> instance.
15+
/// </summary>
16+
/// <param name="durableClientOptions">options containing the client configuration parameters.</param>
17+
/// <returns>Returns a <see cref="IDurableClient"/> instance. The returned instance may be a cached instance.</returns>
18+
IDurableClient CreateClient(DurableClientOptions durableClientOptions);
19+
20+
/// <summary>
21+
/// Gets a <see cref="IDurableClient"/> using configuration from a <see cref="DurableClientOptions"/> instance.
22+
/// </summary>
23+
/// <returns>Returns a <see cref="IDurableClient"/> instance. The returned instance may be a cached instance.</returns>
24+
IDurableClient CreateClient();
25+
}
26+
}

src/WebJobs.Extensions.DurableTask/DurableClientAttribute.cs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System;
55
using System.Diagnostics;
66
using Microsoft.Azure.WebJobs.Description;
7+
using Microsoft.Azure.WebJobs.Extensions.DurableTask.Options;
78

89
namespace Microsoft.Azure.WebJobs.Extensions.DurableTask
910
{
@@ -15,6 +16,22 @@ namespace Microsoft.Azure.WebJobs.Extensions.DurableTask
1516
[Binding]
1617
public class DurableClientAttribute : Attribute, IEquatable<DurableClientAttribute>
1718
{
19+
/// <summary>
20+
/// Initializes a new instance of the <see cref="DurableClientAttribute"/> class.
21+
/// </summary>
22+
public DurableClientAttribute() { }
23+
24+
/// <summary>
25+
/// Initializes a new instance of the <see cref="DurableClientAttribute"/> class.
26+
/// </summary>
27+
/// <param name="durableClientOptions">durable client options</param>
28+
public DurableClientAttribute(DurableClientOptions durableClientOptions)
29+
{
30+
this.TaskHub = durableClientOptions.TaskHub;
31+
this.ConnectionName = durableClientOptions.ConnectionName;
32+
this.ExternalClient = durableClientOptions.IsExternalClient;
33+
}
34+
1835
/// <summary>
1936
/// Optional. Gets or sets the name of the task hub in which the orchestration data lives.
2037
/// </summary>
@@ -39,6 +56,11 @@ public class DurableClientAttribute : Attribute, IEquatable<DurableClientAttribu
3956
/// </remarks>
4057
public string ConnectionName { get; set; }
4158

59+
/// <summary>
60+
/// Indicate if the client is External from the azure function where orchestrator functions are hosted.
61+
/// </summary>
62+
public bool ExternalClient { get; set; }
63+
4264
/// <summary>
4365
/// Returns a hash code for this attribute.
4466
/// </summary>

src/WebJobs.Extensions.DurableTask/DurableTaskExtension.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ public class DurableTaskExtension :
4343
INameVersionObjectManager<TaskOrchestration>,
4444
INameVersionObjectManager<TaskActivity>
4545
{
46-
private static readonly string LoggerCategoryName = LogCategories.CreateTriggerCategory("DurableTask");
46+
internal static readonly string LoggerCategoryName = LogCategories.CreateTriggerCategory("DurableTask");
4747

4848
// Creating client objects is expensive, so we cache them when the attributes match.
4949
// Note that DurableClientAttribute defines a custom equality comparer.
@@ -133,7 +133,7 @@ public DurableTaskExtension(
133133
DurableHttpClientFactory durableHttpClientFactory = new DurableHttpClientFactory();
134134
this.durableHttpClient = durableHttpClientFactory.GetClient(durableHttpMessageHandlerFactory);
135135

136-
this.MessageDataConverter = this.CreateMessageDataConverter(messageSerializerSettingsFactory);
136+
this.MessageDataConverter = CreateMessageDataConverter(messageSerializerSettingsFactory);
137137
this.ErrorDataConverter = this.CreateErrorDataConverter(errorSerializerSettingsFactory);
138138

139139
this.HttpApiHandler = new HttpApiHandler(this, logger);
@@ -189,7 +189,7 @@ public string HubName
189189

190190
internal MessagePayloadDataConverter ErrorDataConverter { get; private set; }
191191

192-
private MessagePayloadDataConverter CreateMessageDataConverter(IMessageSerializerSettingsFactory messageSerializerSettingsFactory)
192+
internal static MessagePayloadDataConverter CreateMessageDataConverter(IMessageSerializerSettingsFactory messageSerializerSettingsFactory)
193193
{
194194
bool isDefault;
195195
if (messageSerializerSettingsFactory == null)

src/WebJobs.Extensions.DurableTask/DurableTaskJobHostConfigurationExtensions.cs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
using System.Net.Http;
66
using System.Threading;
77
#if !FUNCTIONS_V1
8+
using Microsoft.Azure.WebJobs.Extensions.DurableTask.ContextImplementations;
9+
using Microsoft.Azure.WebJobs.Extensions.DurableTask.Options;
810
using Microsoft.Extensions.DependencyInjection;
911
using Microsoft.Extensions.DependencyInjection.Extensions;
1012
using Microsoft.Extensions.Hosting;
@@ -43,10 +45,45 @@ public static IWebJobsBuilder AddDurableTask(this IWebJobsBuilder builder)
4345
serviceCollection.TryAddSingleton<IMessageSerializerSettingsFactory, MessageSerializerSettingsFactory>();
4446
serviceCollection.TryAddSingleton<IErrorSerializerSettingsFactory, ErrorSerializerSettingsFactory>();
4547
serviceCollection.TryAddSingleton<IApplicationLifetimeWrapper, HostLifecycleService>();
48+
serviceCollection.TryAddSingleton<IDurableClientFactory, DurableClientFactory>();
4649

4750
return builder;
4851
}
4952

53+
/// <summary>
54+
/// Adds the Durable Task extension to the provided <see cref="IServiceCollection"/>.
55+
/// </summary>
56+
/// <param name="serviceCollection">The <see cref="IServiceCollection"/> to configure.</param>
57+
/// <returns>Returns the provided <see cref="IServiceCollection"/>.</returns>
58+
public static IServiceCollection AddDurableTask(this IServiceCollection serviceCollection)
59+
{
60+
if (serviceCollection == null)
61+
{
62+
throw new ArgumentNullException(nameof(serviceCollection));
63+
}
64+
65+
serviceCollection.TryAddSingleton<INameResolver, DefaultNameResolver>();
66+
serviceCollection.TryAddSingleton<IConnectionStringResolver, StandardConnectionStringProvider>();
67+
serviceCollection.TryAddSingleton<IDurabilityProviderFactory, AzureStorageDurabilityProviderFactory>();
68+
serviceCollection.TryAddSingleton<IDurableClientFactory, DurableClientFactory>();
69+
serviceCollection.TryAddSingleton<IMessageSerializerSettingsFactory, MessageSerializerSettingsFactory>();
70+
71+
return serviceCollection;
72+
}
73+
74+
/// <summary>
75+
/// Adds the Durable Task extension to the provided <see cref="IServiceCollection"/>.
76+
/// </summary>
77+
/// <param name="serviceCollection">The <see cref="IServiceCollection"/> to configure.</param>
78+
/// <param name="optionsBuilder">Populate default configurations of <see cref="DurableClientOptions"/> to create Durable Clients.</param>
79+
/// <returns>Returns the provided <see cref="IServiceCollection"/>.</returns>
80+
public static IServiceCollection AddDurableTask(this IServiceCollection serviceCollection, Action<DurableClientOptions> optionsBuilder)
81+
{
82+
AddDurableTask(serviceCollection);
83+
serviceCollection.Configure<DurableClientOptions>(optionsBuilder.Invoke);
84+
return serviceCollection;
85+
}
86+
5087
/// <summary>
5188
/// Adds the Durable Task extension to the provided <see cref="IWebJobsBuilder"/>.
5289
/// </summary>

0 commit comments

Comments
 (0)