diff --git a/DurableTask.Netherite.sln b/DurableTask.Netherite.sln index 42971069..70774eae 100644 --- a/DurableTask.Netherite.sln +++ b/DurableTask.Netherite.sln @@ -41,7 +41,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TokenCredentialDF", "sample EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TokenCredentialDTFx", "samples\TokenCredentialDTFx\TokenCredentialDTFx.csproj", "{FBFF0814-E6C0-489A-ACCF-9D0699219621}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Functions.Worker.Extensions.DurableTask.Netherite", "src\Functions.Worker.Extensions.DurableTask.Netherite\Functions.Worker.Extensions.DurableTask.Netherite.csproj", "{3E17402B-3F65-4E5B-B752-48AD56B81208}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Functions.Worker.Extensions.DurableTask.Netherite", "src\Functions.Worker.Extensions.DurableTask.Netherite\Functions.Worker.Extensions.DurableTask.Netherite.csproj", "{3E17402B-3F65-4E5B-B752-48AD56B81208}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StandaloneClient", "samples\StandaloneClient\StandaloneClient.csproj", "{CC543A91-1815-4192-88EB-89C892652ACB}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -97,6 +99,10 @@ Global {3E17402B-3F65-4E5B-B752-48AD56B81208}.Debug|Any CPU.Build.0 = Debug|Any CPU {3E17402B-3F65-4E5B-B752-48AD56B81208}.Release|Any CPU.ActiveCfg = Release|Any CPU {3E17402B-3F65-4E5B-B752-48AD56B81208}.Release|Any CPU.Build.0 = Release|Any CPU + {CC543A91-1815-4192-88EB-89C892652ACB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CC543A91-1815-4192-88EB-89C892652ACB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CC543A91-1815-4192-88EB-89C892652ACB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CC543A91-1815-4192-88EB-89C892652ACB}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -114,6 +120,7 @@ Global {B99AB043-47DC-467C-93CB-D6C69D3B1AD4} = {AB958467-9236-402E-833C-B8DE4841AB9F} {FBFF0814-E6C0-489A-ACCF-9D0699219621} = {AB958467-9236-402E-833C-B8DE4841AB9F} {3E17402B-3F65-4E5B-B752-48AD56B81208} = {D33AB157-04B9-4BAD-B580-C3C87C17828C} + {CC543A91-1815-4192-88EB-89C892652ACB} = {AB958467-9236-402E-833C-B8DE4841AB9F} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {238A9613-5411-41CF-BDEC-168CCD5C03FB} diff --git a/samples/StandaloneClient/FunctionsStartup.cs b/samples/StandaloneClient/FunctionsStartup.cs new file mode 100644 index 00000000..0cf095d2 --- /dev/null +++ b/samples/StandaloneClient/FunctionsStartup.cs @@ -0,0 +1,90 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// Reference: https://docs.microsoft.com/en-us/azure/azure-functions/functions-dotnet-dependency-injection +[assembly: Microsoft.Azure.Functions.Extensions.DependencyInjection.FunctionsStartup(typeof(StandaloneClient.Startup))] +namespace StandaloneClient +{ + using System; + using System.Collections.Generic; + using Azure.Identity; + using DurableTask.Netherite; + using DurableTask.Netherite.AzureFunctions; + using Microsoft.Azure.Functions.Extensions.DependencyInjection; + using Microsoft.Azure.WebJobs; + using Microsoft.Azure.WebJobs.Extensions.DurableTask.ContextImplementations; + using Microsoft.Azure.WebJobs.Extensions.DurableTask.Options; + using Microsoft.Azure.WebJobs.Extensions.DurableTask; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.DependencyInjection.Extensions; + using Microsoft.Extensions.Logging; + using Microsoft.Extensions.Options; + using System.Linq; + using Castle.Core.Logging; + + public class Startup : FunctionsStartup + { + public override void Configure(IFunctionsHostBuilder builder) + { + builder.Services.AddSingleton(); + } + + public class ExternalClientFactory + { + readonly DurableClientFactory configuredClientFactory; + readonly string defaultHubName; + readonly string defaultConnectionName; + readonly bool isEmulation; + + const string DefaultStorageProviderName = "AzureStorage"; + const string DefaultStorageConnectionName = "AzureWebJobsStorage"; + + public ExternalClientFactory( + IOptions durableClientOptions, + IOptions durableTaskOptions, + Microsoft.Extensions.Logging.ILoggerFactory loggerFactory, + IServiceProvider serviceProvider) + { + // determine the name of the configured storage provider + bool storageTypeIsConfigured = durableTaskOptions.Value.StorageProvider.TryGetValue("type", out object storageType); + string storageProviderName = "AzureStorage"; // default storage provider name + if (storageTypeIsConfigured) + { + storageProviderName = storageType.ToString(); + } + + // find the provider factory for the configured storage provider + IEnumerable providerFactories = serviceProvider.GetServices(); + IDurabilityProviderFactory providerFactory = providerFactories.First(f => string.Equals(f.Name, storageProviderName, StringComparison.OrdinalIgnoreCase)); + + // create the client factory + this.configuredClientFactory = new DurableClientFactory(durableClientOptions, durableTaskOptions, providerFactory, loggerFactory); + + // determine the default hub name based on the configuration + this.defaultHubName = durableTaskOptions.Value.HubName; + + // determine the default connection name based on the configuration + if (durableTaskOptions.Value.StorageProvider.TryGetValue("StorageConnectionName", out object value)) + { + this.defaultConnectionName = value.ToString(); + } + else + { + this.defaultConnectionName = DefaultStorageConnectionName; + } + } + + public IDurableClient GetClient(string connectionName = null, string hubName = null) + { + var clientOptions = new DurableClientOptions() { + ConnectionName = connectionName ?? this.defaultConnectionName, + TaskHub = hubName ?? this.defaultHubName, + IsExternalClient = true, + }; + + var client = this.configuredClientFactory.CreateClient(clientOptions); + return client; + } + } + } +} \ No newline at end of file diff --git a/samples/StandaloneClient/Properties/launchSettings.json b/samples/StandaloneClient/Properties/launchSettings.json new file mode 100644 index 00000000..45608342 --- /dev/null +++ b/samples/StandaloneClient/Properties/launchSettings.json @@ -0,0 +1,9 @@ +{ + "profiles": { + "ExternalClient": { + "commandName": "Project", + "commandLineArgs": "--port 7135", + "launchBrowser": false + } + } +} \ No newline at end of file diff --git a/samples/StandaloneClient/README.md b/samples/StandaloneClient/README.md new file mode 100644 index 00000000..78957316 --- /dev/null +++ b/samples/StandaloneClient/README.md @@ -0,0 +1,17 @@ +# Standalone DF client + +This sample demonstrates how to create a standalone durable client. + +This client is constructed in a function app that is separate from the function app that runs the task hub. + +It is meant to be used in conjunction with the HelloDF sample which defines the orchestration that is being executed. + +## How to run it + +1. Set the environment variable EventHubsConnection to contain a connection string for an event hubs namespace. You cannot use emulation for creating a standalone client. + +2. Start the HelloDF sample app so it runs the task hub. Keep this running (and watch for error messages in the log). + +3. In a second window, start this app (StandaloneDFClient). Keep it running (and watch for error messages in the log). + +4. Issue an HTTP request to the standalone client, like `curl http://localhost:7135/test`. You should receive (possibly after a few seconds) a return message saying 'client successfully started the instance'. \ No newline at end of file diff --git a/samples/StandaloneClient/StandaloneClient.csproj b/samples/StandaloneClient/StandaloneClient.csproj new file mode 100644 index 00000000..e94e8836 --- /dev/null +++ b/samples/StandaloneClient/StandaloneClient.csproj @@ -0,0 +1,20 @@ + + + net6.0 + v4 + + + + + + + + + PreserveNewest + + + PreserveNewest + Never + + + diff --git a/samples/StandaloneClient/Test.cs b/samples/StandaloneClient/Test.cs new file mode 100644 index 00000000..a054aa7d --- /dev/null +++ b/samples/StandaloneClient/Test.cs @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace StandaloneClient +{ + using System; + using System.Threading.Tasks; + using System.Net; + using Microsoft.AspNetCore.Mvc; + using Microsoft.Azure.WebJobs; + using Microsoft.Azure.WebJobs.Extensions.Http; + using Microsoft.AspNetCore.Http; + using Microsoft.Extensions.Options; + using Microsoft.Azure.WebJobs.Extensions.DurableTask; + using Microsoft.Azure.WebJobs.Extensions.DurableTask.ContextImplementations; + using Microsoft.Azure.WebJobs.Extensions.DurableTask.Options; + using static StandaloneClient.Startup; + + public class Test + { + readonly ExternalClientFactory externalClientFactory; + + public Test(ExternalClientFactory externalClientFactory) + { + this.externalClientFactory = externalClientFactory; + } + + [FunctionName(nameof(Test))] + public async Task Run( + [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = null)] HttpRequest req) + { + try + { + var client = this.externalClientFactory.GetClient(); + string orchestrationInstanceId = await client.StartNewAsync("HelloSequence"); + return new OkObjectResult($"client successfully started the instance {orchestrationInstanceId}.\n"); + } + catch (Exception e) + { + return new ObjectResult($"exception: {e}") { StatusCode = (int)HttpStatusCode.InternalServerError }; + } + } + } +} diff --git a/samples/StandaloneClient/host.json b/samples/StandaloneClient/host.json new file mode 100644 index 00000000..58addc8b --- /dev/null +++ b/samples/StandaloneClient/host.json @@ -0,0 +1,14 @@ +{ + "version": "2.0", + "extensions": { + "http": { + "routePrefix": "" + }, + "durableTask": { + "hubName": "hello", + "storageProvider": { + "type": "Netherite" + } + } + } +} diff --git a/samples/StandaloneClient/local.settings.json b/samples/StandaloneClient/local.settings.json new file mode 100644 index 00000000..ec1826b4 --- /dev/null +++ b/samples/StandaloneClient/local.settings.json @@ -0,0 +1,7 @@ +{ + "IsEncrypted": false, + "Values": { + "AzureWebJobsStorage": "UseDevelopmentStorage=true", + "FUNCTIONS_WORKER_RUNTIME": "dotnet" + } +} \ No newline at end of file