diff --git a/release_notes.md b/release_notes.md index 87ce7dbb7..68b06ba95 100644 --- a/release_notes.md +++ b/release_notes.md @@ -3,5 +3,6 @@ ## Bug fixes * Fix message loss bug in ContinueAsNew scenarios ([Azure/durabletask#544](https://github.com/Azure/durabletask/pull/544)) +* Allow custom connection string names when creating a DurableClient in an ASP.NET Core app (external app) (#1895) ## Breaking Changes \ No newline at end of file diff --git a/src/WebJobs.Extensions.DurableTask/AzureStorageDurabilityProviderFactory.cs b/src/WebJobs.Extensions.DurableTask/AzureStorageDurabilityProviderFactory.cs index a6f03cc48..d4be166e9 100644 --- a/src/WebJobs.Extensions.DurableTask/AzureStorageDurabilityProviderFactory.cs +++ b/src/WebJobs.Extensions.DurableTask/AzureStorageDurabilityProviderFactory.cs @@ -83,7 +83,7 @@ internal string GetDefaultStorageConnectionString() // This method should not be called before the app settings are resolved into the options. // Because of this, we wait to validate the options until right before building a durability provider, rather // than in the Factory constructor. - private void EnsureInitialized() + private void EnsureDefaultClientSettingsInitialized() { if (!this.hasValidatedOptions) { @@ -103,7 +103,7 @@ private void EnsureInitialized() public virtual DurabilityProvider GetDurabilityProvider() { - this.EnsureInitialized(); + this.EnsureDefaultClientSettingsInitialized(); if (this.defaultStorageProvider == null) { var defaultService = new AzureStorageOrchestrationService(this.defaultSettings); @@ -120,7 +120,11 @@ public virtual DurabilityProvider GetDurabilityProvider() public virtual DurabilityProvider GetDurabilityProvider(DurableClientAttribute attribute) { - this.EnsureInitialized(); + if (!attribute.ExternalClient) + { + this.EnsureDefaultClientSettingsInitialized(); + } + return this.GetAzureStorageStorageProvider(attribute); } @@ -130,8 +134,12 @@ private AzureStorageDurabilityProvider GetAzureStorageStorageProvider(DurableCli AzureStorageOrchestrationServiceSettings settings = this.GetAzureStorageOrchestrationServiceSettings(connectionName, attribute.TaskHub); AzureStorageDurabilityProvider innerClient; - if (string.Equals(this.defaultSettings.TaskHubName, settings.TaskHubName, StringComparison.OrdinalIgnoreCase) && - string.Equals(this.defaultSettings.StorageConnectionString, settings.StorageConnectionString, StringComparison.OrdinalIgnoreCase)) + + // Need to check this.defaultStorageProvider != null for external clients that call GetDurabilityProvider(attribute) + // which never initializes the defaultStorageProvider. + if (string.Equals(this.defaultSettings?.TaskHubName, settings.TaskHubName, StringComparison.OrdinalIgnoreCase) && + string.Equals(this.defaultSettings?.StorageConnectionString, settings.StorageConnectionString, StringComparison.OrdinalIgnoreCase) && + this.defaultStorageProvider != null) { // It's important that clients use the same AzureStorageOrchestrationService instance // as the host when possible to ensure we any send operations can be picked up diff --git a/src/WebJobs.Extensions.DurableTask/Microsoft.Azure.WebJobs.Extensions.DurableTask.xml b/src/WebJobs.Extensions.DurableTask/Microsoft.Azure.WebJobs.Extensions.DurableTask.xml index acee3d77b..842705265 100644 --- a/src/WebJobs.Extensions.DurableTask/Microsoft.Azure.WebJobs.Extensions.DurableTask.xml +++ b/src/WebJobs.Extensions.DurableTask/Microsoft.Azure.WebJobs.Extensions.DurableTask.xml @@ -2508,6 +2508,14 @@ Populate default configurations of to create Durable Clients. Returns the provided . + + + Adds the Durable Task extension to the provided . + + The to configure. + Populate configurations of to create Durable Clients. + Returns the provided . + Adds the Durable Task extension to the provided . diff --git a/test/Common/DurableTaskEndToEndTests.cs b/test/Common/DurableTaskEndToEndTests.cs index bc6bb2d35..f3659472f 100644 --- a/test/Common/DurableTaskEndToEndTests.cs +++ b/test/Common/DurableTaskEndToEndTests.cs @@ -15,9 +15,15 @@ using System.Threading.Tasks; using DurableTask.AzureStorage; using DurableTask.Core; +using Microsoft.Azure.WebJobs.Extensions.DurableTask.ContextImplementations; +using Microsoft.Azure.WebJobs.Extensions.DurableTask.Options; using Microsoft.Azure.WebJobs.Host; using Microsoft.Azure.WebJobs.Host.TestCommon; using Microsoft.Diagnostics.Tracing; +#if !FUNCTIONS_V1 +using Microsoft.Extensions.Hosting; +using WebJobs.Extensions.DurableTask.Tests.V2; +#endif using Microsoft.WindowsAzure.Storage; using Microsoft.WindowsAzure.Storage.Blob; using Moq; @@ -219,6 +225,48 @@ public async Task DurableClient_AzureStorage_SuccessfulSetup() } } +#if !FUNCTIONS_V1 + /// + /// End to end test that ensures that customers can configure custom connection string names + /// using DurableClientOptions when they create a DurableClient from an external app (e.g. ASP.NET Core app). + /// The appSettings dictionary acts like appsettings.json and durableClientOptions are the + /// settings passed in during a call to DurableClient (IDurableClientFactory.CreateClient(durableClientOptions)). + /// + [Fact] + [Trait("Category", PlatformSpecificHelpers.TestCategory)] + public async Task DurableClient_AzureStorage__ReadsCustomStorageConnString() + { + string taskHubName = TestHelpers.GetTaskHubNameFromTestName( + nameof(this.DurableClient_AzureStorage__ReadsCustomStorageConnString), + enableExtendedSessions: false); + + Dictionary appSettings = new Dictionary + { + { "CustomStorageAccountName", TestHelpers.GetStorageConnectionString() }, + { "TestTaskHub", taskHubName }, + }; + + // ConnectionName is used to look up the storage connection string in appsettings + DurableClientOptions durableClientOptions = new DurableClientOptions + { + ConnectionName = "CustomStorageAccountName", + TaskHub = taskHubName, + }; + + var connectionStringResolver = new TestCustomConnectionsStringResolver(appSettings); + + using (IHost clientHost = TestHelpers.GetJobHostExternalEnvironment( + connectionStringResolver: connectionStringResolver)) + { + await clientHost.StartAsync(); + IDurableClientFactory durableClientFactory = clientHost.Services.GetService(typeof(IDurableClientFactory)) as DurableClientFactory; + IDurableClient durableClient = durableClientFactory.CreateClient(durableClientOptions); + Assert.Equal(taskHubName, durableClient.TaskHubName); + await clientHost.StopAsync(); + } + } +#endif + /// /// End-to-end test which validates a simple orchestrator function does not have assigned value for . /// diff --git a/test/Common/DurableTaskHostExtensions.cs b/test/Common/DurableTaskHostExtensions.cs index 40a4fdc63..969f02e71 100644 --- a/test/Common/DurableTaskHostExtensions.cs +++ b/test/Common/DurableTaskHostExtensions.cs @@ -20,6 +20,7 @@ public static async Task StartOrchestratorAsync( var startFunction = useTaskHubFromAppSettings ? typeof(ClientFunctions).GetMethod(nameof(ClientFunctions.StartFunctionWithTaskHub)) : typeof(ClientFunctions).GetMethod(nameof(ClientFunctions.StartFunction)); + var clientRef = new TestDurableClient[1]; var args = new Dictionary { diff --git a/test/Common/TestHelpers.cs b/test/Common/TestHelpers.cs index a2fc1e757..75c0769f5 100644 --- a/test/Common/TestHelpers.cs +++ b/test/Common/TestHelpers.cs @@ -12,8 +12,10 @@ using DurableTask.AzureStorage; using Microsoft.ApplicationInsights.Channel; #if !FUNCTIONS_V1 -using Microsoft.Azure.WebJobs.Extensions.DurableTask.Correlation; +using Microsoft.Extensions.Hosting; #endif +using Microsoft.Azure.WebJobs.Extensions.DurableTask.ContextImplementations; +using Microsoft.Azure.WebJobs.Extensions.DurableTask.Options; using Microsoft.Azure.WebJobs.Host.TestCommon; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -200,6 +202,21 @@ public static ITestHost GetJobHostWithOptions( } #if !FUNCTIONS_V1 + public static IHost GetJobHostExternalEnvironment(IConnectionStringResolver connectionStringResolver = null) + { + if (connectionStringResolver == null) + { + connectionStringResolver = new TestConnectionStringResolver(); + } + + return GetJobHostWithOptionsForDurableClientFactoryExternal(connectionStringResolver); + } + + public static IHost GetJobHostWithOptionsForDurableClientFactoryExternal(IConnectionStringResolver connectionStringResolver) + { + return PlatformSpecificHelpers.CreateJobHostExternalEnvironment(connectionStringResolver); + } + public static ITestHost GetJobHostWithMultipleDurabilityProviders( DurableTaskOptions options = null, IEnumerable durabilityProviderFactories = null) diff --git a/test/FunctionsV2/PlatformSpecificHelpers.FunctionsV2.cs b/test/FunctionsV2/PlatformSpecificHelpers.FunctionsV2.cs index a67e5f354..e31bb2069 100644 --- a/test/FunctionsV2/PlatformSpecificHelpers.FunctionsV2.cs +++ b/test/FunctionsV2/PlatformSpecificHelpers.FunctionsV2.cs @@ -8,6 +8,7 @@ using System.Threading.Tasks; using Microsoft.ApplicationInsights.Channel; using Microsoft.Azure.WebJobs.Extensions.DurableTask.Correlation; +using Microsoft.Azure.WebJobs.Extensions.DurableTask.Options; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Hosting; @@ -102,6 +103,20 @@ public static ITestHost CreateJobHost( return new FunctionsV2HostWrapper(host, options, nameResolver); } + public static IHost CreateJobHostExternalEnvironment(IConnectionStringResolver connectionStringResolver) + { + IHost host = new HostBuilder() + .ConfigureServices( + serviceCollection => + { + serviceCollection.AddSingleton(connectionStringResolver); + serviceCollection.AddDurableClientFactory(); + }) + .Build(); + + return host; + } + public static ITestHost CreateJobHostWithMultipleDurabilityProviders( IOptions options, IEnumerable durabilityProviderFactories) diff --git a/test/FunctionsV2/TestCustomConnectionsStringResolver.cs b/test/FunctionsV2/TestCustomConnectionsStringResolver.cs new file mode 100644 index 000000000..49aa0dfc7 --- /dev/null +++ b/test/FunctionsV2/TestCustomConnectionsStringResolver.cs @@ -0,0 +1,30 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Text; +using Microsoft.Azure.WebJobs.Extensions.DurableTask; + +namespace WebJobs.Extensions.DurableTask.Tests.V2 +{ + internal class TestCustomConnectionsStringResolver : IConnectionStringResolver + { + private readonly Dictionary connectionStrings; + + public TestCustomConnectionsStringResolver(Dictionary connectionStrings) + { + this.connectionStrings = connectionStrings; + } + + public string Resolve(string connectionStringName) + { + if (this.connectionStrings.TryGetValue(connectionStringName, out string value)) + { + return value; + } + + return null; + } + } +}