From fc053ecc196384c58dcefd7419cfbd57bbcf80be Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Tue, 7 Jan 2025 15:55:02 -0800 Subject: [PATCH 01/33] durable task scheduler auth extension save initial --- Microsoft.DurableTask.sln | 10 + src/Extensions/Azure/Azure.csproj | 27 +++ .../DurableTaskSchedulerConnectionString.cs | 82 ++++++++ .../Azure/DurableTaskSchedulerExtensions.cs | 156 +++++++++++++++ .../Azure/DurableTaskSchedulerOptions.cs | 147 +++++++++++++++ ...rableTaskSchedulerConnectionStringTests.cs | 122 ++++++++++++ .../DurableTaskSchedulerExtensionsTests.cs | 178 ++++++++++++++++++ .../DurableTaskSchedulerOptionsTests.cs | 119 ++++++++++++ .../Extensions.Azure.Tests.csproj | 34 ++++ 9 files changed, 875 insertions(+) create mode 100644 src/Extensions/Azure/Azure.csproj create mode 100644 src/Extensions/Azure/DurableTaskSchedulerConnectionString.cs create mode 100644 src/Extensions/Azure/DurableTaskSchedulerExtensions.cs create mode 100644 src/Extensions/Azure/DurableTaskSchedulerOptions.cs create mode 100644 test/Extensions.Azure.Tests/DurableTaskSchedulerConnectionStringTests.cs create mode 100644 test/Extensions.Azure.Tests/DurableTaskSchedulerExtensionsTests.cs create mode 100644 test/Extensions.Azure.Tests/DurableTaskSchedulerOptionsTests.cs create mode 100644 test/Extensions.Azure.Tests/Extensions.Azure.Tests.csproj diff --git a/Microsoft.DurableTask.sln b/Microsoft.DurableTask.sln index 2b8bab8a..96d6355f 100644 --- a/Microsoft.DurableTask.sln +++ b/Microsoft.DurableTask.sln @@ -71,6 +71,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Analyzers.Tests", "test\Ana EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AzureFunctionsApp.Tests", "samples\AzureFunctionsUnitTests\AzureFunctionsApp.Tests.csproj", "{FC2692E7-79AE-400E-A50F-8E0BCC8C9BD9}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Extensions", "Extensions", "{5227C712-2355-403F-90D6-51D0BCAE4D38}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Azure", "src\Extensions\Azure\Azure.csproj", "{662BF73D-A4DD-4910-8625-7C12F1ACDBEC}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -185,6 +189,10 @@ Global {FC2692E7-79AE-400E-A50F-8E0BCC8C9BD9}.Debug|Any CPU.Build.0 = Debug|Any CPU {FC2692E7-79AE-400E-A50F-8E0BCC8C9BD9}.Release|Any CPU.ActiveCfg = Release|Any CPU {FC2692E7-79AE-400E-A50F-8E0BCC8C9BD9}.Release|Any CPU.Build.0 = Release|Any CPU + {662BF73D-A4DD-4910-8625-7C12F1ACDBEC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {662BF73D-A4DD-4910-8625-7C12F1ACDBEC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {662BF73D-A4DD-4910-8625-7C12F1ACDBEC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {662BF73D-A4DD-4910-8625-7C12F1ACDBEC}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -220,6 +228,8 @@ Global {998E9D97-BD36-4A9D-81FC-5DAC1CE40083} = {8AFC9781-F6F1-4696-BB4A-9ED7CA9D612B} {541FCCCE-1059-4691-B027-F761CD80DE92} = {E5637F81-2FB9-4CD7-900D-455363B142A7} {FC2692E7-79AE-400E-A50F-8E0BCC8C9BD9} = {EFF7632B-821E-4CFC-B4A0-ED4B24296B17} + {5227C712-2355-403F-90D6-51D0BCAE4D38} = {8AFC9781-F6F1-4696-BB4A-9ED7CA9D612B} + {662BF73D-A4DD-4910-8625-7C12F1ACDBEC} = {5227C712-2355-403F-90D6-51D0BCAE4D38} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {AB41CB55-35EA-4986-A522-387AB3402E71} diff --git a/src/Extensions/Azure/Azure.csproj b/src/Extensions/Azure/Azure.csproj new file mode 100644 index 00000000..8f1d2e35 --- /dev/null +++ b/src/Extensions/Azure/Azure.csproj @@ -0,0 +1,27 @@ + + + + netstandard2.0;net6.0 + Azure extensions for the Durable Task Framework. + true + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Extensions/Azure/DurableTaskSchedulerConnectionString.cs b/src/Extensions/Azure/DurableTaskSchedulerConnectionString.cs new file mode 100644 index 00000000..ae71cf21 --- /dev/null +++ b/src/Extensions/Azure/DurableTaskSchedulerConnectionString.cs @@ -0,0 +1,82 @@ +// ------------------------------------------------------------ +// Copyright (c) Microsoft Corporation.All rights reserved. +// ------------------------------------------------------------ + +using System.Data.Common; + +namespace DurableTask.Extensions.Azure; + +/// +/// Represents the constituent parts of a connection string for a Durable Task Scheduler service. +/// +public sealed class DurableTaskSchedulerConnectionString +{ + readonly DbConnectionStringBuilder builder; + + /// + /// Initializes a new instance of the class. + /// + /// A connection string for a Durable Task Scheduler service. + public DurableTaskSchedulerConnectionString(string connectionString) + { + this.builder = new() { ConnectionString = connectionString }; + } + + /// + /// Gets the authentication method specified in the connection string (if any). + /// + public string Authentication => this.GetRequiredValue("Authentication"); + + /// + /// Gets the managed identity or workload identity client ID specified in the connection string (if any). + /// + public string? ClientId => this.GetValue("ClientID"); + + /// + /// Gets the "AdditionallyAllowedTenants" property, optionally used by Workload Identity. + /// Multiple values can be separated by a comma. + /// + public IList? AdditionallyAllowedTenants => + string.IsNullOrEmpty(this.AdditionallyAllowedTenantsStr) + ? null + : this.AdditionallyAllowedTenantsStr!.Split(','); + + /// + /// Gets the "TenantId" property, optionally used by Workload Identity. + /// + public string? TenantId => this.GetValue("TenantId"); + + /// + /// Gets the "TokenFilePath" property, optionally used by Workload Identity. + /// + public string? TokenFilePath => this.GetValue("TokenFilePath"); + + /// + /// Gets the endpoint specified in the connection string (if any). + /// + public string Endpoint => this.GetRequiredValue("Endpoint"); + + /// + /// Gets the task hub name specified in the connection string. + /// + public string TaskHubName => this.GetRequiredValue("TaskHub"); + + string? AdditionallyAllowedTenantsStr => this.GetValue("AdditionallyAllowedTenants"); + + string? GetValue(string name) => + this.builder.TryGetValue(name, out object? value) + ? value as string + : null; + + string GetRequiredValue(string name) + { + string? value = this.GetValue(name); + if (string.IsNullOrEmpty(value)) + { + throw new ArgumentNullException( + $"The connection string is missing the required '{name}' property."); + } + + return value!; + } +} diff --git a/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs b/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs new file mode 100644 index 00000000..1ec6b3a7 --- /dev/null +++ b/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs @@ -0,0 +1,156 @@ +using Azure.Core; +using Microsoft.DurableTask.Client; +using Microsoft.DurableTask.Worker; +using System.Diagnostics; + +namespace DurableTask.Extensions.Azure; + +// NOTE: These extension methods will eventually be provided by the Durable Task SDK itself. +public static class DurableTaskSchedulerExtensions +{ + // Configure the Durable Task *Worker* to use the Durable Task Scheduler service with the specified options. + public static void UseDurableTaskScheduler( + this IDurableTaskWorkerBuilder builder, + string endpointAddress, + string taskHubName, + TokenCredential credential, + Action? configure = null) + { + DurableTaskSchedulerOptions options = new(endpointAddress, taskHubName, credential); + + configure?.Invoke(options); + + builder.UseGrpc(GetGrpcChannelForOptions(options)); + } + + public static void UseDurableTaskScheduler( + this IDurableTaskWorkerBuilder builder, + string connectionString, + Action? configure = null) + { + var options = DurableTaskSchedulerOptions.FromConnectionString(connectionString); + configure?.Invoke(options); + builder.UseGrpc(GetGrpcChannelForOptions(options)); + } + + // Configure the Durable Task *Client* to use the Durable Task Scheduler service with the specified options. + public static void UseDurableTaskScheduler( + this IDurableTaskClientBuilder builder, + string endpointAddress, + string taskHubName, + TokenCredential credential, + Action? configure = null) + { + DurableTaskSchedulerOptions options = new(endpointAddress, taskHubName, credential); + + configure?.Invoke(options); + + builder.UseGrpc(GetGrpcChannelForOptions(options)); + } + + public static void UseDurableTaskScheduler( + this IDurableTaskClientBuilder builder, + string connectionString, + Action? configure = null) + { + var options = DurableTaskSchedulerOptions.FromConnectionString(connectionString); + configure?.Invoke(options); + builder.UseGrpc(GetGrpcChannelForOptions(options)); + } + + static GrpcChannel GetGrpcChannelForOptions(DurableTaskSchedulerOptions options) + { + if (string.IsNullOrEmpty(options.EndpointAddress)) + { + throw RequiredOptionMissing(nameof(options.TaskHubName)); + } + + if (string.IsNullOrEmpty(options.TaskHubName)) + { + throw RequiredOptionMissing(nameof(options.TaskHubName)); + } + + TokenCredential credential = options.Credential ?? throw RequiredOptionMissing(nameof(options.Credential)); + + string taskHubName = options.TaskHubName; + string endpoint = options.EndpointAddress; + + if (!endpoint.Contains("://")) + { + endpoint = $"https://{endpoint}"; + } + + string resourceId = options.ResourceId ?? "https://durabletask.io"; +#if NET6_0 + int processId = Environment.ProcessId; +#else + int processId = Process.GetCurrentProcess().Id; +#endif + string workerId = options.WorkerId ?? $"{Environment.MachineName},{processId},{Guid.NewGuid()}"; + + TokenCache? cache = + options.Credential is not null + ? new( + options.Credential, + new(new[] { $"{options.ResourceId}/.default" }), + TimeSpan.FromMinutes(5)) + : null; + + CallCredentials managedBackendCreds = CallCredentials.FromInterceptor( + async (context, metadata) => + { + metadata.Add("taskhub", taskHubName); + metadata.Add("workerid", workerId); + + if (cache is null) + { + return; + } + + AccessToken token = await cache.GetTokenAsync(context.CancellationToken); + + metadata.Add("Authorization", $"Bearer {token.Token}"); + }); + + #if NET6_0 + return GrpcChannel.ForAddress( + endpoint, + new GrpcChannelOptions + { + Credentials = ChannelCredentials.Create(ChannelCredentials.SecureSsl, managedBackendCreds), + }); + #else + return new GrpcChannel( + endpoint, + ChannelCredentials.Create(ChannelCredentials.SecureSsl, managedBackendCreds)); + #endif + } + + static Exception RequiredOptionMissing(string optionName) + { + return new ArgumentException(message: $"Required option '{optionName}' was not provided."); + } + + sealed class TokenCache(TokenCredential credential, TokenRequestContext context, TimeSpan margin) + { + readonly TokenCredential credential = credential; + readonly TokenRequestContext context = context; + readonly TimeSpan margin = margin; + + AccessToken? token; + + public async Task GetTokenAsync(CancellationToken cancellationToken) + { + DateTimeOffset nowWithMargin = DateTimeOffset.UtcNow.Add(this.margin); + + if (this.token is null + || this.token.Value.RefreshOn < nowWithMargin + || this.token.Value.ExpiresOn < nowWithMargin) + { + this.token = await this.credential.GetTokenAsync(this.context, cancellationToken); + } + + return this.token.Value; + } + } +} \ No newline at end of file diff --git a/src/Extensions/Azure/DurableTaskSchedulerOptions.cs b/src/Extensions/Azure/DurableTaskSchedulerOptions.cs new file mode 100644 index 00000000..a089bcc9 --- /dev/null +++ b/src/Extensions/Azure/DurableTaskSchedulerOptions.cs @@ -0,0 +1,147 @@ +using System.Globalization; +using Azure.Core; +using Azure.Identity; + +namespace DurableTask.Extensions.Azure; + +// NOTE: These options definitions will eventually be provided by the Durable Task SDK itself. + +/// +/// Options for configuring the Durable Task Scheduler. +/// +public class DurableTaskSchedulerOptions +{ + internal DurableTaskSchedulerOptions(string endpointAddress, string taskHubName, TokenCredential? credential = null) + { + this.EndpointAddress = endpointAddress ?? throw new ArgumentNullException(nameof(endpointAddress)); + this.TaskHubName = taskHubName ?? throw new ArgumentNullException(nameof(taskHubName)); + this.Credential = credential; + } + + /// + /// The endpoint address of the Durable Task Scheduler resource. + /// Expected to be in the format "https://{scheduler-name}.{region}.durabletask.io". + /// + public string EndpointAddress { get; } + + /// + /// The name of the task hub resource associated with the Durable Task Scheduler resource. + /// + public string TaskHubName { get; } + + /// + /// The credential used to authenticate with the Durable Task Scheduler task hub resource. + /// + public TokenCredential? Credential { get; } + + /// + /// The resource ID of the Durable Task Scheduler resource. + /// The default value is https://durabletask.io. + /// + public string? ResourceId { get; set; } + + /// + /// The worker ID used to identify the worker instance. + /// The default value is a string containing the machine name and the process ID. + /// + public string? WorkerId { get; set; } + + + public static DurableTaskSchedulerOptions FromConnectionString(string connectionString) + { + return FromConnectionString(new DurableTaskSchedulerConnectionString(connectionString)); + } + + public static DurableTaskSchedulerOptions FromConnectionString( + DurableTaskSchedulerConnectionString connectionString) + { + // Example connection strings: + // "Endpoint=https://myaccount.westus3.durabletask.io/;Authentication=ManagedIdentity;ClientID=00000000-0000-0000-0000-000000000000;TaskHubName=th01" + // "Endpoint=https://myaccount.westus3.durabletask.io/;Authentication=DefaultAzure;TaskHubName=th01" + // "Endpoint=https://myaccount.westus3.durabletask.io/;Authentication=None;TaskHubName=th01" (undocumented and only intended for local testing) + + string endpointAddress = connectionString.Endpoint; + + if (!endpointAddress.Contains("://")) + { + // If the protocol is missing, assume HTTPS. + endpointAddress = "https://" + endpointAddress; + } + + string authType = connectionString.Authentication; + + TokenCredential? credential; + + // Parse the supported auth types, in a case-insensitive way and without spaces + switch (authType.ToLower(CultureInfo.InvariantCulture).Replace(" ", string.Empty)) + { + case "defaultazure": + // Default Azure credentials, suitable for a variety of scenarios + // In many cases, users will need to pass additional configuration options via env vars + credential = new DefaultAzureCredential(); + break; + + case "managedidentity": + // Use Managed identity + // Suitable for Azure-hosted scenarios + // Note that ClientId could be null for system-assigned managed identities + credential = new ManagedIdentityCredential(connectionString.ClientId); + break; + + case "workloadidentity": + // Use Workload Identity Federation. + // This is commonly-used in Kubernetes (hosted on Azure or anywhere), or in CI environments like + // Azure Pipelines or GitHub Actions. It can also be used with SPIFFE. + WorkloadIdentityCredentialOptions opts = new() { }; + if (!string.IsNullOrEmpty(connectionString.ClientId)) + { + opts.ClientId = connectionString.ClientId; + } + + if (!string.IsNullOrEmpty(connectionString.TenantId)) + { + opts.TenantId = connectionString.TenantId; + } + + if (connectionString.AdditionallyAllowedTenants is not null) + { + foreach (string tenant in connectionString.AdditionallyAllowedTenants) + { + opts.AdditionallyAllowedTenants.Add(tenant); + } + } + + credential = new WorkloadIdentityCredential(opts); + break; + + case "environment": + // Use credentials from the environment + credential = new EnvironmentCredential(); + break; + + case "azurecli": + // Use credentials from the Azure CLI + credential = new AzureCliCredential(); + break; + + case "azurepowershell": + // Use credentials from the Azure PowerShell modules + credential = new AzurePowerShellCredential(); + break; + + case "none": + // Do not use any authentication/authorization (for testing only) + // This is a no-op + credential = null; + break; + + default: + throw new ArgumentException( + $"The connection string contains an unsupported authentication type '{authType}'.", + nameof(connectionString)); + } + + DurableTaskSchedulerOptions options = new(endpointAddress, connectionString.TaskHubName, credential); + return options; + } +} \ No newline at end of file diff --git a/test/Extensions.Azure.Tests/DurableTaskSchedulerConnectionStringTests.cs b/test/Extensions.Azure.Tests/DurableTaskSchedulerConnectionStringTests.cs new file mode 100644 index 00000000..bb56ad55 --- /dev/null +++ b/test/Extensions.Azure.Tests/DurableTaskSchedulerConnectionStringTests.cs @@ -0,0 +1,122 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using FluentAssertions; +using System.Data.Common; +using Xunit; + +namespace DurableTask.Extensions.Azure.Tests; + +public class DurableTaskSchedulerConnectionStringTests +{ + private const string ValidEndpoint = "myaccount.westus3.durabletask.io"; + private const string ValidTaskHub = "testhub"; + private const string ValidClientId = "00000000-0000-0000-0000-000000000000"; + private const string ValidTenantId = "11111111-1111-1111-1111-111111111111"; + + [Fact] + public void Constructor_WithValidConnectionString_ShouldParseCorrectly() + { + // Arrange + string connectionString = $"Endpoint={ValidEndpoint};Authentication=DefaultAzure;TaskHub={ValidTaskHub}"; + + // Act + var parsedConnectionString = new DurableTaskSchedulerConnectionString(connectionString); + + // Assert + parsedConnectionString.Endpoint.Should().Be(ValidEndpoint); + parsedConnectionString.TaskHubName.Should().Be(ValidTaskHub); + parsedConnectionString.Authentication.Should().Be("DefaultAzure"); + } + + [Fact] + public void Constructor_WithManagedIdentity_ShouldParseClientId() + { + // Arrange + string connectionString = $"Endpoint={ValidEndpoint};Authentication=ManagedIdentity;ClientID={ValidClientId};TaskHub={ValidTaskHub}"; + + // Act + var parsedConnectionString = new DurableTaskSchedulerConnectionString(connectionString); + + // Assert + parsedConnectionString.ClientId.Should().Be(ValidClientId); + } + + [Fact] + public void Constructor_WithWorkloadIdentity_ShouldParseAllProperties() + { + // Arrange + string connectionString = $"Endpoint={ValidEndpoint};Authentication=WorkloadIdentity;ClientID={ValidClientId};TenantId={ValidTenantId};TaskHub={ValidTaskHub}"; + + // Act + var parsedConnectionString = new DurableTaskSchedulerConnectionString(connectionString); + + // Assert + parsedConnectionString.ClientId.Should().Be(ValidClientId); + parsedConnectionString.TenantId.Should().Be(ValidTenantId); + } + + [Fact] + public void Constructor_WithAdditionallyAllowedTenants_ShouldParseTenantList() + { + // Arrange + const string tenants = "tenant1,tenant2,tenant3"; + string connectionString = $"Endpoint={ValidEndpoint};Authentication=WorkloadIdentity;AdditionallyAllowedTenants={tenants};TaskHub={ValidTaskHub}"; + + // Act + var parsedConnectionString = new DurableTaskSchedulerConnectionString(connectionString); + + // Assert + parsedConnectionString.AdditionallyAllowedTenants.Should().NotBeNull(); + parsedConnectionString.AdditionallyAllowedTenants.Should().BeEquivalentTo(new[] { "tenant1", "tenant2", "tenant3" }); + } + + [Fact] + public void Constructor_WithMissingRequiredProperties_ShouldThrowArgumentNullException() + { + // Arrange + string connectionString = $"Authentication=DefaultAzure;TaskHub={ValidTaskHub}"; // Missing Endpoint + + // Act & Assert + var action = () => _ = new DurableTaskSchedulerConnectionString(connectionString).Endpoint; + var exception = action.Should().Throw().Which; + exception.Message.Should().Contain("'Endpoint' property"); + } + + [Fact] + public void Constructor_WithInvalidConnectionString_ShouldThrowArgumentException() + { + // Arrange + string connectionString = "This is not a valid connection string"; + + // Act & Assert + var action = () => new DurableTaskSchedulerConnectionString(connectionString); + action.Should().Throw() + .WithMessage("*Format of the initialization string does not conform to specification*"); + } + + [Theory] + [InlineData("")] + [InlineData(null)] + public void Constructor_WithNullOrEmptyConnectionString_ShouldThrowArgumentException(string? connectionString) + { + // Act & Assert + var action = () => _ = new DurableTaskSchedulerConnectionString(connectionString!).Endpoint; + action.Should().Throw() + .WithMessage("*'Endpoint' property*"); + } + + [Fact] + public void GetValue_WithNonExistentProperty_ShouldReturnNull() + { + // Arrange + string connectionString = $"Endpoint={ValidEndpoint};Authentication=DefaultAzure;TaskHub={ValidTaskHub}"; + var parsedConnectionString = new DurableTaskSchedulerConnectionString(connectionString); + + // Assert + parsedConnectionString.ClientId.Should().BeNull(); + parsedConnectionString.TenantId.Should().BeNull(); + parsedConnectionString.TokenFilePath.Should().BeNull(); + parsedConnectionString.AdditionallyAllowedTenants.Should().BeNull(); + } +} diff --git a/test/Extensions.Azure.Tests/DurableTaskSchedulerExtensionsTests.cs b/test/Extensions.Azure.Tests/DurableTaskSchedulerExtensionsTests.cs new file mode 100644 index 00000000..440cab0f --- /dev/null +++ b/test/Extensions.Azure.Tests/DurableTaskSchedulerExtensionsTests.cs @@ -0,0 +1,178 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Azure.Core; +using Azure.Identity; +using FluentAssertions; +using Grpc.Net.Client; +using Microsoft.DurableTask.Client; +using Microsoft.DurableTask.Client.Grpc; +using Microsoft.DurableTask.Worker; +using Microsoft.DurableTask.Worker.Grpc; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Moq; +using Xunit; + +namespace DurableTask.Extensions.Azure.Tests; + +public class DurableTaskSchedulerExtensionsTests +{ + private const string ValidEndpoint = "myaccount.westus3.durabletask.io"; + private const string ValidTaskHub = "testhub"; + + [Fact] + public void UseDurableTaskScheduler_Worker_WithEndpointAndCredential_ShouldConfigureCorrectly() + { + // Arrange + var services = new ServiceCollection(); + var mockBuilder = new Mock(); + mockBuilder.Setup(b => b.Services).Returns(services); + var credential = new DefaultAzureCredential(); + + // Act + mockBuilder.Object.UseDurableTaskScheduler(ValidEndpoint, ValidTaskHub, credential); + + // Assert - Verify that the options were registered + var provider = services.BuildServiceProvider(); + var options = provider.GetService>(); + options.Should().NotBeNull(); + } + + [Fact] + public void UseDurableTaskScheduler_Worker_WithConnectionString_ShouldConfigureCorrectly() + { + // Arrange + var services = new ServiceCollection(); + var mockBuilder = new Mock(); + mockBuilder.Setup(b => b.Services).Returns(services); + string connectionString = $"Endpoint={ValidEndpoint};Authentication=DefaultAzure;TaskHub={ValidTaskHub}"; + + // Act + mockBuilder.Object.UseDurableTaskScheduler(connectionString); + + // Assert + var provider = services.BuildServiceProvider(); + var options = provider.GetService>(); + options.Should().NotBeNull(); + } + + [Fact] + public void UseDurableTaskScheduler_Client_WithEndpointAndCredential_ShouldConfigureCorrectly() + { + // Arrange + var services = new ServiceCollection(); + var mockBuilder = new Mock(); + mockBuilder.Setup(b => b.Services).Returns(services); + var credential = new DefaultAzureCredential(); + + // Act + mockBuilder.Object.UseDurableTaskScheduler(ValidEndpoint, ValidTaskHub, credential); + + // Assert + var provider = services.BuildServiceProvider(); + var options = provider.GetService>(); + options.Should().NotBeNull(); + } + + [Fact] + public void UseDurableTaskScheduler_Client_WithConnectionString_ShouldConfigureCorrectly() + { + // Arrange + var services = new ServiceCollection(); + var mockBuilder = new Mock(); + mockBuilder.Setup(b => b.Services).Returns(services); + string connectionString = $"Endpoint={ValidEndpoint};Authentication=DefaultAzure;TaskHub={ValidTaskHub}"; + + // Act + mockBuilder.Object.UseDurableTaskScheduler(connectionString); + + // Assert + var provider = services.BuildServiceProvider(); + var options = provider.GetService>(); + options.Should().NotBeNull(); + } + + [Fact] + public void UseDurableTaskScheduler_WithOptions_ShouldApplyConfiguration() + { + // Arrange + var services = new ServiceCollection(); + var mockBuilder = new Mock(); + mockBuilder.Setup(b => b.Services).Returns(services); + var credential = new DefaultAzureCredential(); + string workerId = "customWorker"; + + // Act + mockBuilder.Object.UseDurableTaskScheduler( + ValidEndpoint, + ValidTaskHub, + credential, + options => options.WorkerId = workerId); + + // Assert + var provider = services.BuildServiceProvider(); + var options = provider.GetService>(); + options.Should().NotBeNull(); + } + + [Theory] + [InlineData(null, ValidTaskHub)] + [InlineData(ValidEndpoint, null)] + public void UseDurableTaskScheduler_WithNullParameters_ShouldThrowArgumentNullException(string endpoint, string taskHub) + { + // Arrange + var services = new ServiceCollection(); + var mockBuilder = new Mock(); + mockBuilder.Setup(b => b.Services).Returns(services); + var credential = new DefaultAzureCredential(); + + // Act & Assert + var action = () => mockBuilder.Object.UseDurableTaskScheduler(endpoint, taskHub, credential); + action.Should().Throw(); + } + + [Fact] + public void UseDurableTaskScheduler_WithNullCredential_ShouldThrowArgumentException() + { + // Arrange + var services = new ServiceCollection(); + var mockBuilder = new Mock(); + mockBuilder.Setup(b => b.Services).Returns(services); + TokenCredential? credential = null; + + // Act & Assert + var action = () => mockBuilder.Object.UseDurableTaskScheduler(ValidEndpoint, ValidTaskHub, credential!); + action.Should().Throw() + .WithMessage("*Required option 'Credential' was not provided*"); + } + + [Fact] + public void UseDurableTaskScheduler_WithInvalidConnectionString_ShouldThrowArgumentException() + { + // Arrange + var services = new ServiceCollection(); + var mockBuilder = new Mock(); + mockBuilder.Setup(b => b.Services).Returns(services); + string invalidConnectionString = "This is not a valid connection string"; + + // Act & Assert + var action = () => mockBuilder.Object.UseDurableTaskScheduler(invalidConnectionString); + action.Should().Throw(); + } + + [Theory] + [InlineData("")] + [InlineData(null)] + public void UseDurableTaskScheduler_WithNullOrEmptyConnectionString_ShouldThrowArgumentException(string connectionString) + { + // Arrange + var services = new ServiceCollection(); + var mockBuilder = new Mock(); + mockBuilder.Setup(b => b.Services).Returns(services); + + // Act & Assert + var action = () => mockBuilder.Object.UseDurableTaskScheduler(connectionString); + action.Should().Throw(); + } +} diff --git a/test/Extensions.Azure.Tests/DurableTaskSchedulerOptionsTests.cs b/test/Extensions.Azure.Tests/DurableTaskSchedulerOptionsTests.cs new file mode 100644 index 00000000..c7e8fd0a --- /dev/null +++ b/test/Extensions.Azure.Tests/DurableTaskSchedulerOptionsTests.cs @@ -0,0 +1,119 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Azure.Core; +using Azure.Identity; +using FluentAssertions; +using Xunit; + +namespace DurableTask.Extensions.Azure.Tests; + +public class DurableTaskSchedulerOptionsTests +{ + private const string ValidEndpoint = "myaccount.westus3.durabletask.io"; + private const string ValidTaskHub = "testhub"; + + [Fact] + public void FromConnectionString_WithDefaultAzure_ShouldCreateValidInstance() + { + // Arrange + string connectionString = $"Endpoint={ValidEndpoint};Authentication=DefaultAzure;TaskHub={ValidTaskHub}"; + + // Act + var options = DurableTaskSchedulerOptions.FromConnectionString(connectionString); + + // Assert + options.EndpointAddress.Should().Be($"https://{ValidEndpoint}"); + options.TaskHubName.Should().Be(ValidTaskHub); + options.Credential.Should().BeOfType(); + } + + [Fact] + public void FromConnectionString_WithManagedIdentity_ShouldCreateValidInstance() + { + // Arrange + const string clientId = "00000000-0000-0000-0000-000000000000"; + string connectionString = $"Endpoint={ValidEndpoint};Authentication=ManagedIdentity;ClientID={clientId};TaskHub={ValidTaskHub}"; + + // Act + var options = DurableTaskSchedulerOptions.FromConnectionString(connectionString); + + // Assert + options.EndpointAddress.Should().Be($"https://{ValidEndpoint}"); + options.TaskHubName.Should().Be(ValidTaskHub); + options.Credential.Should().BeOfType(); + } + + [Fact] + public void FromConnectionString_WithWorkloadIdentity_ShouldCreateValidInstance() + { + // Arrange + const string clientId = "00000000-0000-0000-0000-000000000000"; + const string tenantId = "11111111-1111-1111-1111-111111111111"; + string connectionString = $"Endpoint={ValidEndpoint};Authentication=WorkloadIdentity;ClientID={clientId};TenantId={tenantId};TaskHub={ValidTaskHub}"; + + // Act + var options = DurableTaskSchedulerOptions.FromConnectionString(connectionString); + + // Assert + options.EndpointAddress.Should().Be($"https://{ValidEndpoint}"); + options.TaskHubName.Should().Be(ValidTaskHub); + options.Credential.Should().BeOfType(); + } + + [Theory] + [InlineData("Environment")] + [InlineData("AzureCLI")] + [InlineData("AzurePowerShell")] + public void FromConnectionString_WithValidAuthTypes_ShouldCreateValidInstance(string authType) + { + // Arrange + string connectionString = $"Endpoint={ValidEndpoint};Authentication={authType};TaskHub={ValidTaskHub}"; + + // Act + var options = DurableTaskSchedulerOptions.FromConnectionString(connectionString); + + // Assert + options.EndpointAddress.Should().Be($"https://{ValidEndpoint}"); + options.TaskHubName.Should().Be(ValidTaskHub); + options.Credential.Should().NotBeNull(); + } + + [Fact] + public void FromConnectionString_WithInvalidAuthType_ShouldThrowArgumentException() + { + // Arrange + string connectionString = $"Endpoint={ValidEndpoint};Authentication=InvalidAuth;TaskHub={ValidTaskHub}"; + + // Act & Assert + var action = () => DurableTaskSchedulerOptions.FromConnectionString(connectionString); + action.Should().Throw() + .WithMessage("*contains an unsupported authentication type*"); + } + + [Fact] + public void FromConnectionString_WithMissingRequiredProperties_ShouldThrowArgumentNullException() + { + // Arrange + string connectionString = $"Endpoint={ValidEndpoint};Authentication=DefaultAzure"; // Missing TaskHub + + // Act & Assert + var action = () => DurableTaskSchedulerOptions.FromConnectionString(connectionString); + action.Should().Throw(); + } + + [Fact] + public void FromConnectionString_WithNone_ShouldCreateInstanceWithNullCredential() + { + // Arrange + string connectionString = $"Endpoint={ValidEndpoint};Authentication=None;TaskHub={ValidTaskHub}"; + + // Act + var options = DurableTaskSchedulerOptions.FromConnectionString(connectionString); + + // Assert + options.EndpointAddress.Should().Be($"https://{ValidEndpoint}"); + options.TaskHubName.Should().Be(ValidTaskHub); + options.Credential.Should().BeNull(); + } +} diff --git a/test/Extensions.Azure.Tests/Extensions.Azure.Tests.csproj b/test/Extensions.Azure.Tests/Extensions.Azure.Tests.csproj new file mode 100644 index 00000000..8ea58bf8 --- /dev/null +++ b/test/Extensions.Azure.Tests/Extensions.Azure.Tests.csproj @@ -0,0 +1,34 @@ + + + + net6.0 + enable + enable + false + true + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + From 6f1d2bc9111819a941be34501d094dec9ddda2b3 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Wed, 8 Jan 2025 22:46:31 -0800 Subject: [PATCH 02/33] support local conn with no auth and via http --- .../Azure/DurableTaskSchedulerExtensions.cs | 30 ++++++++++++++----- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs b/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs index 1ec6b3a7..71c81877 100644 --- a/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs +++ b/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs @@ -70,8 +70,6 @@ static GrpcChannel GetGrpcChannelForOptions(DurableTaskSchedulerOptions options) throw RequiredOptionMissing(nameof(options.TaskHubName)); } - TokenCredential credential = options.Credential ?? throw RequiredOptionMissing(nameof(options.Credential)); - string taskHubName = options.TaskHubName; string endpoint = options.EndpointAddress; @@ -112,17 +110,35 @@ options.Credential is not null metadata.Add("Authorization", $"Bearer {token.Token}"); }); + // Production will use HTTPS, but local testing will use HTTP + ChannelCredentials channelCreds = endpoint.StartsWith("https://") ? + ChannelCredentials.SecureSsl : + ChannelCredentials.Insecure; #if NET6_0 - return GrpcChannel.ForAddress( - endpoint, - new GrpcChannelOptions + return GrpcChannel.ForAddress(this.options.Address, new GrpcChannelOptions { - Credentials = ChannelCredentials.Create(ChannelCredentials.SecureSsl, managedBackendCreds), + // The same credential is being used for all operations. + // https://learn.microsoft.com/aspnet/core/grpc/authn-and-authz#set-the-bearer-token-with-callcredentials + Credentials = ChannelCredentials.Create(channelCreds, managedBackendCreds), + + // TODO: This is not appropriate for use in production settings. Setting this to true should + // only be done for local testing. We should hide this setting behind some kind of flag. + UnsafeUseInsecureChannelCallCredentials = true, }); #else return new GrpcChannel( endpoint, - ChannelCredentials.Create(ChannelCredentials.SecureSsl, managedBackendCreds)); + ChannelCredentials.Create(channelCreds, managedBackendCreds), + new GrpcChannelOptions + { + // The same credential is being used for all operations. + // https://learn.microsoft.com/aspnet/core/grpc/authn-and-authz#set-the-bearer-token-with-callcredentials + Credentials = ChannelCredentials.Create(channelCreds, managedBackendCreds), + + // TODO: This is not appropriate for use in production settings. Setting this to true should + // only be done for local testing. We should hide this setting behind some kind of flag. + UnsafeUseInsecureChannelCallCredentials = true, + }); #endif } From 51e39bde6e8987b6b9c444fe84ca86cecc1ddf9a Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Wed, 8 Jan 2025 22:55:07 -0800 Subject: [PATCH 03/33] be consistent with azuremanaged targetversion --- src/Extensions/Azure/Azure.csproj | 2 +- .../Azure/DurableTaskSchedulerExtensions.cs | 25 +++---------------- 2 files changed, 4 insertions(+), 23 deletions(-) diff --git a/src/Extensions/Azure/Azure.csproj b/src/Extensions/Azure/Azure.csproj index 8f1d2e35..25704be0 100644 --- a/src/Extensions/Azure/Azure.csproj +++ b/src/Extensions/Azure/Azure.csproj @@ -1,7 +1,7 @@  - netstandard2.0;net6.0 + net6.0 Azure extensions for the Durable Task Framework. true diff --git a/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs b/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs index 71c81877..0933921b 100644 --- a/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs +++ b/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs @@ -1,4 +1,5 @@ using Azure.Core; +using Grpc.Net.Client; using Microsoft.DurableTask.Client; using Microsoft.DurableTask.Worker; using System.Diagnostics; @@ -79,12 +80,8 @@ static GrpcChannel GetGrpcChannelForOptions(DurableTaskSchedulerOptions options) } string resourceId = options.ResourceId ?? "https://durabletask.io"; -#if NET6_0 int processId = Environment.ProcessId; -#else - int processId = Process.GetCurrentProcess().Id; -#endif - string workerId = options.WorkerId ?? $"{Environment.MachineName},{processId},{Guid.NewGuid()}"; + string workerId = options.WorkerId ?? $"{Environment.MachineName},{processId},{Guid.NewGuid():N}"; TokenCache? cache = options.Credential is not null @@ -114,8 +111,7 @@ options.Credential is not null ChannelCredentials channelCreds = endpoint.StartsWith("https://") ? ChannelCredentials.SecureSsl : ChannelCredentials.Insecure; - #if NET6_0 - return GrpcChannel.ForAddress(this.options.Address, new GrpcChannelOptions + return GrpcChannel.ForAddress(options.EndpointAddress, new GrpcChannelOptions { // The same credential is being used for all operations. // https://learn.microsoft.com/aspnet/core/grpc/authn-and-authz#set-the-bearer-token-with-callcredentials @@ -125,21 +121,6 @@ options.Credential is not null // only be done for local testing. We should hide this setting behind some kind of flag. UnsafeUseInsecureChannelCallCredentials = true, }); - #else - return new GrpcChannel( - endpoint, - ChannelCredentials.Create(channelCreds, managedBackendCreds), - new GrpcChannelOptions - { - // The same credential is being used for all operations. - // https://learn.microsoft.com/aspnet/core/grpc/authn-and-authz#set-the-bearer-token-with-callcredentials - Credentials = ChannelCredentials.Create(channelCreds, managedBackendCreds), - - // TODO: This is not appropriate for use in production settings. Setting this to true should - // only be done for local testing. We should hide this setting behind some kind of flag. - UnsafeUseInsecureChannelCallCredentials = true, - }); - #endif } static Exception RequiredOptionMissing(string optionName) From 6987f1bcef3e0a26813c5de33410cf2d4aac9125 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Wed, 8 Jan 2025 22:56:39 -0800 Subject: [PATCH 04/33] clean --- src/Extensions/Azure/DurableTaskSchedulerExtensions.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs b/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs index 0933921b..395e8263 100644 --- a/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs +++ b/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs @@ -1,12 +1,9 @@ using Azure.Core; -using Grpc.Net.Client; using Microsoft.DurableTask.Client; using Microsoft.DurableTask.Worker; -using System.Diagnostics; namespace DurableTask.Extensions.Azure; -// NOTE: These extension methods will eventually be provided by the Durable Task SDK itself. public static class DurableTaskSchedulerExtensions { // Configure the Durable Task *Worker* to use the Durable Task Scheduler service with the specified options. From e1628154aa884420b0de30bab2a07050c7f25dfe Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Wed, 8 Jan 2025 23:01:07 -0800 Subject: [PATCH 05/33] namespace --- src/Extensions/Azure/DurableTaskSchedulerConnectionString.cs | 2 +- src/Extensions/Azure/DurableTaskSchedulerExtensions.cs | 2 +- src/Extensions/Azure/DurableTaskSchedulerOptions.cs | 2 +- .../DurableTaskSchedulerConnectionStringTests.cs | 2 +- .../DurableTaskSchedulerExtensionsTests.cs | 2 +- test/Extensions.Azure.Tests/DurableTaskSchedulerOptionsTests.cs | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Extensions/Azure/DurableTaskSchedulerConnectionString.cs b/src/Extensions/Azure/DurableTaskSchedulerConnectionString.cs index ae71cf21..04a7ad65 100644 --- a/src/Extensions/Azure/DurableTaskSchedulerConnectionString.cs +++ b/src/Extensions/Azure/DurableTaskSchedulerConnectionString.cs @@ -4,7 +4,7 @@ using System.Data.Common; -namespace DurableTask.Extensions.Azure; +namespace Microsoft.DurableTask.Extensions.Azure; /// /// Represents the constituent parts of a connection string for a Durable Task Scheduler service. diff --git a/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs b/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs index 395e8263..39ef3b22 100644 --- a/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs +++ b/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs @@ -2,7 +2,7 @@ using Microsoft.DurableTask.Client; using Microsoft.DurableTask.Worker; -namespace DurableTask.Extensions.Azure; +namespace Microsoft.DurableTask.Extensions.Azure; public static class DurableTaskSchedulerExtensions { diff --git a/src/Extensions/Azure/DurableTaskSchedulerOptions.cs b/src/Extensions/Azure/DurableTaskSchedulerOptions.cs index a089bcc9..4cc5c691 100644 --- a/src/Extensions/Azure/DurableTaskSchedulerOptions.cs +++ b/src/Extensions/Azure/DurableTaskSchedulerOptions.cs @@ -2,7 +2,7 @@ using Azure.Core; using Azure.Identity; -namespace DurableTask.Extensions.Azure; +namespace Microsoft.DurableTask.Extensions.Azure; // NOTE: These options definitions will eventually be provided by the Durable Task SDK itself. diff --git a/test/Extensions.Azure.Tests/DurableTaskSchedulerConnectionStringTests.cs b/test/Extensions.Azure.Tests/DurableTaskSchedulerConnectionStringTests.cs index bb56ad55..aaca0a66 100644 --- a/test/Extensions.Azure.Tests/DurableTaskSchedulerConnectionStringTests.cs +++ b/test/Extensions.Azure.Tests/DurableTaskSchedulerConnectionStringTests.cs @@ -5,7 +5,7 @@ using System.Data.Common; using Xunit; -namespace DurableTask.Extensions.Azure.Tests; +namespace Microsoft.DurableTask.Extensions.Azure.Tests; public class DurableTaskSchedulerConnectionStringTests { diff --git a/test/Extensions.Azure.Tests/DurableTaskSchedulerExtensionsTests.cs b/test/Extensions.Azure.Tests/DurableTaskSchedulerExtensionsTests.cs index 440cab0f..24db69ca 100644 --- a/test/Extensions.Azure.Tests/DurableTaskSchedulerExtensionsTests.cs +++ b/test/Extensions.Azure.Tests/DurableTaskSchedulerExtensionsTests.cs @@ -14,7 +14,7 @@ using Moq; using Xunit; -namespace DurableTask.Extensions.Azure.Tests; +namespace Microsoft.DurableTask.Extensions.Azure.Tests; public class DurableTaskSchedulerExtensionsTests { diff --git a/test/Extensions.Azure.Tests/DurableTaskSchedulerOptionsTests.cs b/test/Extensions.Azure.Tests/DurableTaskSchedulerOptionsTests.cs index c7e8fd0a..b1c903c2 100644 --- a/test/Extensions.Azure.Tests/DurableTaskSchedulerOptionsTests.cs +++ b/test/Extensions.Azure.Tests/DurableTaskSchedulerOptionsTests.cs @@ -6,7 +6,7 @@ using FluentAssertions; using Xunit; -namespace DurableTask.Extensions.Azure.Tests; +namespace Microsoft.DurableTask.Extensions.Azure.Tests; public class DurableTaskSchedulerOptionsTests { From e19de446b996da4965fa537178c646ec79e3a3e9 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Wed, 8 Jan 2025 23:02:42 -0800 Subject: [PATCH 06/33] fix --- src/Extensions/Azure/DurableTaskSchedulerExtensions.cs | 6 +++++- src/Extensions/Azure/DurableTaskSchedulerOptions.cs | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs b/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs index 39ef3b22..1dce80d0 100644 --- a/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs +++ b/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs @@ -1,4 +1,8 @@ -using Azure.Core; +// ------------------------------------------------------------ +// Copyright (c) Microsoft Corporation.All rights reserved. +// ------------------------------------------------------------ + +using Azure.Core; using Microsoft.DurableTask.Client; using Microsoft.DurableTask.Worker; diff --git a/src/Extensions/Azure/DurableTaskSchedulerOptions.cs b/src/Extensions/Azure/DurableTaskSchedulerOptions.cs index 4cc5c691..e58e0c25 100644 --- a/src/Extensions/Azure/DurableTaskSchedulerOptions.cs +++ b/src/Extensions/Azure/DurableTaskSchedulerOptions.cs @@ -1,4 +1,8 @@ -using System.Globalization; +// ------------------------------------------------------------ +// Copyright (c) Microsoft Corporation.All rights reserved. +// ------------------------------------------------------------ + +using System.Globalization; using Azure.Core; using Azure.Identity; From 3a6dc52aaaa636924033c6e57b995837a785582c Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Thu, 9 Jan 2025 00:36:10 -0800 Subject: [PATCH 07/33] fix --- src/Extensions/Azure/DurableTaskSchedulerExtensions.cs | 10 +++------- .../DurableTaskSchedulerExtensionsTests.cs | 5 ++--- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs b/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs index 1dce80d0..4a9638b6 100644 --- a/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs +++ b/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs @@ -19,9 +19,7 @@ public static void UseDurableTaskScheduler( Action? configure = null) { DurableTaskSchedulerOptions options = new(endpointAddress, taskHubName, credential); - configure?.Invoke(options); - builder.UseGrpc(GetGrpcChannelForOptions(options)); } @@ -44,9 +42,7 @@ public static void UseDurableTaskScheduler( Action? configure = null) { DurableTaskSchedulerOptions options = new(endpointAddress, taskHubName, credential); - configure?.Invoke(options); - builder.UseGrpc(GetGrpcChannelForOptions(options)); } @@ -64,7 +60,7 @@ static GrpcChannel GetGrpcChannelForOptions(DurableTaskSchedulerOptions options) { if (string.IsNullOrEmpty(options.EndpointAddress)) { - throw RequiredOptionMissing(nameof(options.TaskHubName)); + throw RequiredOptionMissing(nameof(options.EndpointAddress)); } if (string.IsNullOrEmpty(options.TaskHubName)) @@ -88,7 +84,7 @@ static GrpcChannel GetGrpcChannelForOptions(DurableTaskSchedulerOptions options) options.Credential is not null ? new( options.Credential, - new(new[] { $"{options.ResourceId}/.default" }), + new(new[] { $"{resourceId}/.default" }), TimeSpan.FromMinutes(5)) : null; @@ -112,7 +108,7 @@ options.Credential is not null ChannelCredentials channelCreds = endpoint.StartsWith("https://") ? ChannelCredentials.SecureSsl : ChannelCredentials.Insecure; - return GrpcChannel.ForAddress(options.EndpointAddress, new GrpcChannelOptions + return GrpcChannel.ForAddress(endpoint, new GrpcChannelOptions { // The same credential is being used for all operations. // https://learn.microsoft.com/aspnet/core/grpc/authn-and-authz#set-the-bearer-token-with-callcredentials diff --git a/test/Extensions.Azure.Tests/DurableTaskSchedulerExtensionsTests.cs b/test/Extensions.Azure.Tests/DurableTaskSchedulerExtensionsTests.cs index 24db69ca..518b53c1 100644 --- a/test/Extensions.Azure.Tests/DurableTaskSchedulerExtensionsTests.cs +++ b/test/Extensions.Azure.Tests/DurableTaskSchedulerExtensionsTests.cs @@ -133,7 +133,7 @@ public void UseDurableTaskScheduler_WithNullParameters_ShouldThrowArgumentNullEx } [Fact] - public void UseDurableTaskScheduler_WithNullCredential_ShouldThrowArgumentException() + public void UseDurableTaskScheduler_WithNullCredential_ShouldSucceed() { // Arrange var services = new ServiceCollection(); @@ -143,8 +143,7 @@ public void UseDurableTaskScheduler_WithNullCredential_ShouldThrowArgumentExcept // Act & Assert var action = () => mockBuilder.Object.UseDurableTaskScheduler(ValidEndpoint, ValidTaskHub, credential!); - action.Should().Throw() - .WithMessage("*Required option 'Credential' was not provided*"); + action.Should().NotThrow(); } [Fact] From 65dfb85bcae00e6ee2dec5c58ad707bf3de4297f Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Thu, 9 Jan 2025 00:44:42 -0800 Subject: [PATCH 08/33] doc --- .../Azure/DurableTaskSchedulerExtensions.cs | 37 ++++++++++++++++++- .../Azure/DurableTaskSchedulerOptions.cs | 15 ++++++-- 2 files changed, 47 insertions(+), 5 deletions(-) diff --git a/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs b/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs index 4a9638b6..21ccf76e 100644 --- a/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs +++ b/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs @@ -8,9 +8,19 @@ namespace Microsoft.DurableTask.Extensions.Azure; +/// +/// Extension methods for configuring Durable Task workers and clients to use the Azure Durable Task Scheduler service. +/// public static class DurableTaskSchedulerExtensions { - // Configure the Durable Task *Worker* to use the Durable Task Scheduler service with the specified options. + /// + /// Configures Durable Task worker to use the Azure Durable Task Scheduler service. + /// + /// The worker builder to configure. + /// The endpoint address of the Durable Task Scheduler service. + /// The name of the task hub to connect to. + /// The credential to use for authentication. + /// Optional callback to configure additional options. public static void UseDurableTaskScheduler( this IDurableTaskWorkerBuilder builder, string endpointAddress, @@ -19,10 +29,18 @@ public static void UseDurableTaskScheduler( Action? configure = null) { DurableTaskSchedulerOptions options = new(endpointAddress, taskHubName, credential); + configure?.Invoke(options); + builder.UseGrpc(GetGrpcChannelForOptions(options)); } + /// + /// Configures Durable Task worker to use the Azure Durable Task Scheduler service using a connection string. + /// + /// The worker builder to configure. + /// The connection string for the Durable Task Scheduler service. + /// Optional callback to configure additional options. public static void UseDurableTaskScheduler( this IDurableTaskWorkerBuilder builder, string connectionString, @@ -33,7 +51,14 @@ public static void UseDurableTaskScheduler( builder.UseGrpc(GetGrpcChannelForOptions(options)); } - // Configure the Durable Task *Client* to use the Durable Task Scheduler service with the specified options. + /// + /// Configures Durable Task client to use the Azure Durable Task Scheduler service. + /// + /// The client builder to configure. + /// The endpoint address of the Durable Task Scheduler service. + /// The name of the task hub to connect to. + /// The credential to use for authentication. + /// Optional callback to configure additional options. public static void UseDurableTaskScheduler( this IDurableTaskClientBuilder builder, string endpointAddress, @@ -42,10 +67,18 @@ public static void UseDurableTaskScheduler( Action? configure = null) { DurableTaskSchedulerOptions options = new(endpointAddress, taskHubName, credential); + configure?.Invoke(options); + builder.UseGrpc(GetGrpcChannelForOptions(options)); } + /// + /// Configures Durable Task client to use the Azure Durable Task Scheduler service using a connection string. + /// + /// The client builder to configure. + /// The connection string for the Durable Task Scheduler service. + /// Optional callback to configure additional options. public static void UseDurableTaskScheduler( this IDurableTaskClientBuilder builder, string connectionString, diff --git a/src/Extensions/Azure/DurableTaskSchedulerOptions.cs b/src/Extensions/Azure/DurableTaskSchedulerOptions.cs index e58e0c25..94bc63bc 100644 --- a/src/Extensions/Azure/DurableTaskSchedulerOptions.cs +++ b/src/Extensions/Azure/DurableTaskSchedulerOptions.cs @@ -8,8 +8,6 @@ namespace Microsoft.DurableTask.Extensions.Azure; -// NOTE: These options definitions will eventually be provided by the Durable Task SDK itself. - /// /// Options for configuring the Durable Task Scheduler. /// @@ -50,12 +48,23 @@ internal DurableTaskSchedulerOptions(string endpointAddress, string taskHubName, /// public string? WorkerId { get; set; } - + /// + /// Creates a new instance of from a connection string. + /// + /// The connection string containing the configuration settings. + /// A new instance of configured with the connection string settings. + /// Thrown when the connection string contains an unsupported authentication type. public static DurableTaskSchedulerOptions FromConnectionString(string connectionString) { return FromConnectionString(new DurableTaskSchedulerConnectionString(connectionString)); } + /// + /// Creates a new instance of from a parsed connection string. + /// + /// The parsed connection string containing the configuration settings. + /// A new instance of configured with the connection string settings. + /// Thrown when the connection string contains an unsupported authentication type. public static DurableTaskSchedulerOptions FromConnectionString( DurableTaskSchedulerConnectionString connectionString) { From 29445a04f445a4f65256fa99b22eda10dd8ffe92 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Thu, 9 Jan 2025 01:06:41 -0800 Subject: [PATCH 09/33] ppl --- .github/workflows/validate-build.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/validate-build.yml b/.github/workflows/validate-build.yml index 65153017..5f3694b4 100644 --- a/.github/workflows/validate-build.yml +++ b/.github/workflows/validate-build.yml @@ -25,7 +25,12 @@ jobs: with: submodules: true - - name: Setup .NET + - name: Setup .NET 6.0 + uses: actions/setup-dotnet@v3 + with: + dotnet-version: '6.0.x' + + - name: Setup .NET from global.json uses: actions/setup-dotnet@v3 with: global-json-file: global.json From e63f12a0a8ed38a410aa1346216f7caf2c6219bc Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Thu, 9 Jan 2025 01:15:36 -0800 Subject: [PATCH 10/33] remove --- test/Extensions.Azure.Tests/Extensions.Azure.Tests.csproj | 4 ---- 1 file changed, 4 deletions(-) diff --git a/test/Extensions.Azure.Tests/Extensions.Azure.Tests.csproj b/test/Extensions.Azure.Tests/Extensions.Azure.Tests.csproj index 8ea58bf8..a62bb109 100644 --- a/test/Extensions.Azure.Tests/Extensions.Azure.Tests.csproj +++ b/test/Extensions.Azure.Tests/Extensions.Azure.Tests.csproj @@ -2,10 +2,6 @@ net6.0 - enable - enable - false - true From 5c72ee7137d69384bc1a3560b3ac16c958142258 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Thu, 9 Jan 2025 01:20:13 -0800 Subject: [PATCH 11/33] test proj to sln --- Microsoft.DurableTask.sln | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Microsoft.DurableTask.sln b/Microsoft.DurableTask.sln index 96d6355f..59acc82d 100644 --- a/Microsoft.DurableTask.sln +++ b/Microsoft.DurableTask.sln @@ -75,6 +75,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Extensions", "Extensions", EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Azure", "src\Extensions\Azure\Azure.csproj", "{662BF73D-A4DD-4910-8625-7C12F1ACDBEC}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Extensions.Azure.Tests", "test\Extensions.Azure.Tests\Extensions.Azure.Tests.csproj", "{DBB5DB4E-A1B0-4C86-A233-213789C46929}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -193,6 +195,10 @@ Global {662BF73D-A4DD-4910-8625-7C12F1ACDBEC}.Debug|Any CPU.Build.0 = Debug|Any CPU {662BF73D-A4DD-4910-8625-7C12F1ACDBEC}.Release|Any CPU.ActiveCfg = Release|Any CPU {662BF73D-A4DD-4910-8625-7C12F1ACDBEC}.Release|Any CPU.Build.0 = Release|Any CPU + {DBB5DB4E-A1B0-4C86-A233-213789C46929}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DBB5DB4E-A1B0-4C86-A233-213789C46929}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DBB5DB4E-A1B0-4C86-A233-213789C46929}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DBB5DB4E-A1B0-4C86-A233-213789C46929}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -230,6 +236,7 @@ Global {FC2692E7-79AE-400E-A50F-8E0BCC8C9BD9} = {EFF7632B-821E-4CFC-B4A0-ED4B24296B17} {5227C712-2355-403F-90D6-51D0BCAE4D38} = {8AFC9781-F6F1-4696-BB4A-9ED7CA9D612B} {662BF73D-A4DD-4910-8625-7C12F1ACDBEC} = {5227C712-2355-403F-90D6-51D0BCAE4D38} + {DBB5DB4E-A1B0-4C86-A233-213789C46929} = {E5637F81-2FB9-4CD7-900D-455363B142A7} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {AB41CB55-35EA-4986-A522-387AB3402E71} From c6e42c54c096ff7c1f8c0ddccff88be0fbd81823 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Thu, 9 Jan 2025 01:24:57 -0800 Subject: [PATCH 12/33] fix warning --- .../DurableTaskSchedulerConnectionString.cs | 5 ++--- .../Azure/DurableTaskSchedulerExtensions.cs | 5 ++--- .../Azure/DurableTaskSchedulerOptions.cs | 18 ++++++++++-------- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/Extensions/Azure/DurableTaskSchedulerConnectionString.cs b/src/Extensions/Azure/DurableTaskSchedulerConnectionString.cs index 04a7ad65..4599cd64 100644 --- a/src/Extensions/Azure/DurableTaskSchedulerConnectionString.cs +++ b/src/Extensions/Azure/DurableTaskSchedulerConnectionString.cs @@ -1,6 +1,5 @@ -// ------------------------------------------------------------ -// Copyright (c) Microsoft Corporation.All rights reserved. -// ------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. using System.Data.Common; diff --git a/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs b/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs index 21ccf76e..6e933d41 100644 --- a/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs +++ b/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs @@ -1,6 +1,5 @@ -// ------------------------------------------------------------ -// Copyright (c) Microsoft Corporation.All rights reserved. -// ------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. using Azure.Core; using Microsoft.DurableTask.Client; diff --git a/src/Extensions/Azure/DurableTaskSchedulerOptions.cs b/src/Extensions/Azure/DurableTaskSchedulerOptions.cs index 94bc63bc..5e603515 100644 --- a/src/Extensions/Azure/DurableTaskSchedulerOptions.cs +++ b/src/Extensions/Azure/DurableTaskSchedulerOptions.cs @@ -1,6 +1,5 @@ -// ------------------------------------------------------------ -// Copyright (c) Microsoft Corporation.All rights reserved. -// ------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. using System.Globalization; using Azure.Core; @@ -13,6 +12,9 @@ namespace Microsoft.DurableTask.Extensions.Azure; /// public class DurableTaskSchedulerOptions { + /// + /// Initializes a new instance of the class. + /// internal DurableTaskSchedulerOptions(string endpointAddress, string taskHubName, TokenCredential? credential = null) { this.EndpointAddress = endpointAddress ?? throw new ArgumentNullException(nameof(endpointAddress)); @@ -21,29 +23,29 @@ internal DurableTaskSchedulerOptions(string endpointAddress, string taskHubName, } /// - /// The endpoint address of the Durable Task Scheduler resource. + /// Gets the endpoint address of the Durable Task Scheduler resource. /// Expected to be in the format "https://{scheduler-name}.{region}.durabletask.io". /// public string EndpointAddress { get; } /// - /// The name of the task hub resource associated with the Durable Task Scheduler resource. + /// Gets the name of the task hub resource associated with the Durable Task Scheduler resource. /// public string TaskHubName { get; } /// - /// The credential used to authenticate with the Durable Task Scheduler task hub resource. + /// Gets the credential used to authenticate with the Durable Task Scheduler task hub resource. /// public TokenCredential? Credential { get; } /// - /// The resource ID of the Durable Task Scheduler resource. + /// Gets or sets the resource ID of the Durable Task Scheduler resource. /// The default value is https://durabletask.io. /// public string? ResourceId { get; set; } /// - /// The worker ID used to identify the worker instance. + /// Gets or sets the worker ID used to identify the worker instance. /// The default value is a string containing the machine name and the process ID. /// public string? WorkerId { get; set; } From 6a66aaa1f03bfa84a28e8db4376870a4168ba0ad Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Thu, 9 Jan 2025 01:35:00 -0800 Subject: [PATCH 13/33] remove dup --- .../Extensions.Azure.Tests.csproj | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/test/Extensions.Azure.Tests/Extensions.Azure.Tests.csproj b/test/Extensions.Azure.Tests/Extensions.Azure.Tests.csproj index a62bb109..e9c83d3e 100644 --- a/test/Extensions.Azure.Tests/Extensions.Azure.Tests.csproj +++ b/test/Extensions.Azure.Tests/Extensions.Azure.Tests.csproj @@ -5,19 +5,7 @@ - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - From 131c575bed4a909e27d7ed9aaeb6ea79d9f804bd Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Thu, 9 Jan 2025 01:45:56 -0800 Subject: [PATCH 14/33] fix --- .../Azure/DurableTaskSchedulerExtensions.cs | 24 ++++++++++--------- .../Azure/DurableTaskSchedulerOptions.cs | 9 +++---- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs b/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs index 6e933d41..1f1bfb02 100644 --- a/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs +++ b/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using Azure.Core; @@ -141,18 +141,20 @@ options.Credential is not null ChannelCredentials.SecureSsl : ChannelCredentials.Insecure; return GrpcChannel.ForAddress(endpoint, new GrpcChannelOptions - { - // The same credential is being used for all operations. - // https://learn.microsoft.com/aspnet/core/grpc/authn-and-authz#set-the-bearer-token-with-callcredentials - Credentials = ChannelCredentials.Create(channelCreds, managedBackendCreds), - // TODO: This is not appropriate for use in production settings. Setting this to true should - // only be done for local testing. We should hide this setting behind some kind of flag. - UnsafeUseInsecureChannelCallCredentials = true, - }); + return GrpcChannel.ForAddress(endpoint, new GrpcChannelOptions + { + // The same credential is being used for all operations. + // https://learn.microsoft.com/aspnet/core/grpc/authn-and-authz#set-the-bearer-token-with-callcredentials + Credentials = ChannelCredentials.Create(channelCreds, managedBackendCreds), + + // TODO: This is not appropriate for use in production settings. Setting this to true should + // only be done for local testing. We should hide this setting behind some kind of flag. + UnsafeUseInsecureChannelCallCredentials = true, + }); } - static Exception RequiredOptionMissing(string optionName) + static ArgumentException RequiredOptionMissing(string optionName) { return new ArgumentException(message: $"Required option '{optionName}' was not provided."); } @@ -179,4 +181,4 @@ public async Task GetTokenAsync(CancellationToken cancellationToken return this.token.Value; } } -} \ No newline at end of file +} diff --git a/src/Extensions/Azure/DurableTaskSchedulerOptions.cs b/src/Extensions/Azure/DurableTaskSchedulerOptions.cs index 5e603515..96e7692d 100644 --- a/src/Extensions/Azure/DurableTaskSchedulerOptions.cs +++ b/src/Extensions/Azure/DurableTaskSchedulerOptions.cs @@ -1,5 +1,5 @@ // Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. +// Licensed under the MIT License. using System.Globalization; using Azure.Core; @@ -15,6 +15,9 @@ public class DurableTaskSchedulerOptions /// /// Initializes a new instance of the class. /// + /// The endpoint address of the Durable Task Scheduler service. + /// The name of the task hub to connect to. + /// The credential to use for authentication, or null for no authentication. internal DurableTaskSchedulerOptions(string endpointAddress, string taskHubName, TokenCredential? credential = null) { this.EndpointAddress = endpointAddress ?? throw new ArgumentNullException(nameof(endpointAddress)); @@ -74,7 +77,6 @@ public static DurableTaskSchedulerOptions FromConnectionString( // "Endpoint=https://myaccount.westus3.durabletask.io/;Authentication=ManagedIdentity;ClientID=00000000-0000-0000-0000-000000000000;TaskHubName=th01" // "Endpoint=https://myaccount.westus3.durabletask.io/;Authentication=DefaultAzure;TaskHubName=th01" // "Endpoint=https://myaccount.westus3.durabletask.io/;Authentication=None;TaskHubName=th01" (undocumented and only intended for local testing) - string endpointAddress = connectionString.Endpoint; if (!endpointAddress.Contains("://")) @@ -84,7 +86,6 @@ public static DurableTaskSchedulerOptions FromConnectionString( } string authType = connectionString.Authentication; - TokenCredential? credential; // Parse the supported auth types, in a case-insensitive way and without spaces @@ -159,4 +160,4 @@ public static DurableTaskSchedulerOptions FromConnectionString( DurableTaskSchedulerOptions options = new(endpointAddress, connectionString.TaskHubName, credential); return options; } -} \ No newline at end of file +} From 65fa60729223649af33f52db2ecd3c94c7771197 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Thu, 9 Jan 2025 01:57:27 -0800 Subject: [PATCH 15/33] Revert "fix" This reverts commit 131c575bed4a909e27d7ed9aaeb6ea79d9f804bd. --- .../Azure/DurableTaskSchedulerExtensions.cs | 24 +++++++++---------- .../Azure/DurableTaskSchedulerOptions.cs | 9 ++++--- 2 files changed, 15 insertions(+), 18 deletions(-) diff --git a/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs b/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs index 1f1bfb02..6e933d41 100644 --- a/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs +++ b/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using Azure.Core; @@ -141,20 +141,18 @@ options.Credential is not null ChannelCredentials.SecureSsl : ChannelCredentials.Insecure; return GrpcChannel.ForAddress(endpoint, new GrpcChannelOptions + { + // The same credential is being used for all operations. + // https://learn.microsoft.com/aspnet/core/grpc/authn-and-authz#set-the-bearer-token-with-callcredentials + Credentials = ChannelCredentials.Create(channelCreds, managedBackendCreds), - return GrpcChannel.ForAddress(endpoint, new GrpcChannelOptions - { - // The same credential is being used for all operations. - // https://learn.microsoft.com/aspnet/core/grpc/authn-and-authz#set-the-bearer-token-with-callcredentials - Credentials = ChannelCredentials.Create(channelCreds, managedBackendCreds), - - // TODO: This is not appropriate for use in production settings. Setting this to true should - // only be done for local testing. We should hide this setting behind some kind of flag. - UnsafeUseInsecureChannelCallCredentials = true, - }); + // TODO: This is not appropriate for use in production settings. Setting this to true should + // only be done for local testing. We should hide this setting behind some kind of flag. + UnsafeUseInsecureChannelCallCredentials = true, + }); } - static ArgumentException RequiredOptionMissing(string optionName) + static Exception RequiredOptionMissing(string optionName) { return new ArgumentException(message: $"Required option '{optionName}' was not provided."); } @@ -181,4 +179,4 @@ public async Task GetTokenAsync(CancellationToken cancellationToken return this.token.Value; } } -} +} \ No newline at end of file diff --git a/src/Extensions/Azure/DurableTaskSchedulerOptions.cs b/src/Extensions/Azure/DurableTaskSchedulerOptions.cs index 96e7692d..5e603515 100644 --- a/src/Extensions/Azure/DurableTaskSchedulerOptions.cs +++ b/src/Extensions/Azure/DurableTaskSchedulerOptions.cs @@ -1,5 +1,5 @@ // Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. +// Licensed under the MIT License. using System.Globalization; using Azure.Core; @@ -15,9 +15,6 @@ public class DurableTaskSchedulerOptions /// /// Initializes a new instance of the class. /// - /// The endpoint address of the Durable Task Scheduler service. - /// The name of the task hub to connect to. - /// The credential to use for authentication, or null for no authentication. internal DurableTaskSchedulerOptions(string endpointAddress, string taskHubName, TokenCredential? credential = null) { this.EndpointAddress = endpointAddress ?? throw new ArgumentNullException(nameof(endpointAddress)); @@ -77,6 +74,7 @@ public static DurableTaskSchedulerOptions FromConnectionString( // "Endpoint=https://myaccount.westus3.durabletask.io/;Authentication=ManagedIdentity;ClientID=00000000-0000-0000-0000-000000000000;TaskHubName=th01" // "Endpoint=https://myaccount.westus3.durabletask.io/;Authentication=DefaultAzure;TaskHubName=th01" // "Endpoint=https://myaccount.westus3.durabletask.io/;Authentication=None;TaskHubName=th01" (undocumented and only intended for local testing) + string endpointAddress = connectionString.Endpoint; if (!endpointAddress.Contains("://")) @@ -86,6 +84,7 @@ public static DurableTaskSchedulerOptions FromConnectionString( } string authType = connectionString.Authentication; + TokenCredential? credential; // Parse the supported auth types, in a case-insensitive way and without spaces @@ -160,4 +159,4 @@ public static DurableTaskSchedulerOptions FromConnectionString( DurableTaskSchedulerOptions options = new(endpointAddress, connectionString.TaskHubName, credential); return options; } -} +} \ No newline at end of file From 4f45ec553fdf83eb221cb0fadb576211b4298ddd Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 10 Jan 2025 12:52:46 -0800 Subject: [PATCH 16/33] save --- .../Azure/DurableTaskSchedulerExtensions.cs | 7 +------ src/Extensions/Azure/DurableTaskSchedulerOptions.cs | 12 +++++++++++- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs b/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs index 6e933d41..d3809cfd 100644 --- a/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs +++ b/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using Azure.Core; @@ -103,11 +103,6 @@ static GrpcChannel GetGrpcChannelForOptions(DurableTaskSchedulerOptions options) string taskHubName = options.TaskHubName; string endpoint = options.EndpointAddress; - if (!endpoint.Contains("://")) - { - endpoint = $"https://{endpoint}"; - } - string resourceId = options.ResourceId ?? "https://durabletask.io"; int processId = Environment.ProcessId; string workerId = options.WorkerId ?? $"{Environment.MachineName},{processId},{Guid.NewGuid():N}"; diff --git a/src/Extensions/Azure/DurableTaskSchedulerOptions.cs b/src/Extensions/Azure/DurableTaskSchedulerOptions.cs index 5e603515..aa3d2a1f 100644 --- a/src/Extensions/Azure/DurableTaskSchedulerOptions.cs +++ b/src/Extensions/Azure/DurableTaskSchedulerOptions.cs @@ -26,7 +26,17 @@ internal DurableTaskSchedulerOptions(string endpointAddress, string taskHubName, /// Gets the endpoint address of the Durable Task Scheduler resource. /// Expected to be in the format "https://{scheduler-name}.{region}.durabletask.io". /// - public string EndpointAddress { get; } + public string EndpointAddress + { + get => this.endpointAddress; + set + { + // Add https:// prefix if no protocol is specified + this.endpointAddress = !value.Contains("://") + ? $"https://{value}" + : value; + } + } /// /// Gets the name of the task hub resource associated with the Durable Task Scheduler resource. From d4607e49208ba2e8687daca1efb96e1ace51244a Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 10 Jan 2025 13:03:51 -0800 Subject: [PATCH 17/33] fix --- .../Azure/DurableTaskSchedulerConnectionString.cs | 8 +------- .../Azure/DurableTaskSchedulerExtensions.cs | 11 ++--------- 2 files changed, 3 insertions(+), 16 deletions(-) diff --git a/src/Extensions/Azure/DurableTaskSchedulerConnectionString.cs b/src/Extensions/Azure/DurableTaskSchedulerConnectionString.cs index 4599cd64..9002f77b 100644 --- a/src/Extensions/Azure/DurableTaskSchedulerConnectionString.cs +++ b/src/Extensions/Azure/DurableTaskSchedulerConnectionString.cs @@ -70,12 +70,6 @@ public DurableTaskSchedulerConnectionString(string connectionString) string GetRequiredValue(string name) { string? value = this.GetValue(name); - if (string.IsNullOrEmpty(value)) - { - throw new ArgumentNullException( - $"The connection string is missing the required '{name}' property."); - } - - return value!; + return Check.NotNullOrEmpty(value, $"The connection string is missing the required '{name}' property."); } } diff --git a/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs b/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs index d3809cfd..8fbce95c 100644 --- a/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs +++ b/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs @@ -90,15 +90,8 @@ public static void UseDurableTaskScheduler( static GrpcChannel GetGrpcChannelForOptions(DurableTaskSchedulerOptions options) { - if (string.IsNullOrEmpty(options.EndpointAddress)) - { - throw RequiredOptionMissing(nameof(options.EndpointAddress)); - } - - if (string.IsNullOrEmpty(options.TaskHubName)) - { - throw RequiredOptionMissing(nameof(options.TaskHubName)); - } + Check.NotNullOrEmpty(options.EndpointAddress, nameof(options.EndpointAddress)); + Check.NotNullOrEmpty(options.TaskHubName, nameof(options.TaskHubName)); string taskHubName = options.TaskHubName; string endpoint = options.EndpointAddress; From 552a9c83f136edd391b75fe4fa42f0b9b37b0576 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 10 Jan 2025 13:44:16 -0800 Subject: [PATCH 18/33] save --- .../Azure/DurableTaskSchedulerExtensions.cs | 83 +--------------- .../Azure/DurableTaskSchedulerOptions.cs | 99 ++++++++++++++++--- 2 files changed, 90 insertions(+), 92 deletions(-) diff --git a/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs b/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs index 8fbce95c..4fda6df8 100644 --- a/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs +++ b/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs @@ -31,7 +31,7 @@ public static void UseDurableTaskScheduler( configure?.Invoke(options); - builder.UseGrpc(GetGrpcChannelForOptions(options)); + builder.UseGrpc(options.GetGrpcChannel()); } /// @@ -47,7 +47,7 @@ public static void UseDurableTaskScheduler( { var options = DurableTaskSchedulerOptions.FromConnectionString(connectionString); configure?.Invoke(options); - builder.UseGrpc(GetGrpcChannelForOptions(options)); + builder.UseGrpc(options.GetGrpcChannel()); } /// @@ -69,7 +69,7 @@ public static void UseDurableTaskScheduler( configure?.Invoke(options); - builder.UseGrpc(GetGrpcChannelForOptions(options)); + builder.UseGrpc(options.GetGrpcChannel()); } /// @@ -85,86 +85,11 @@ public static void UseDurableTaskScheduler( { var options = DurableTaskSchedulerOptions.FromConnectionString(connectionString); configure?.Invoke(options); - builder.UseGrpc(GetGrpcChannelForOptions(options)); - } - - static GrpcChannel GetGrpcChannelForOptions(DurableTaskSchedulerOptions options) - { - Check.NotNullOrEmpty(options.EndpointAddress, nameof(options.EndpointAddress)); - Check.NotNullOrEmpty(options.TaskHubName, nameof(options.TaskHubName)); - - string taskHubName = options.TaskHubName; - string endpoint = options.EndpointAddress; - - string resourceId = options.ResourceId ?? "https://durabletask.io"; - int processId = Environment.ProcessId; - string workerId = options.WorkerId ?? $"{Environment.MachineName},{processId},{Guid.NewGuid():N}"; - - TokenCache? cache = - options.Credential is not null - ? new( - options.Credential, - new(new[] { $"{resourceId}/.default" }), - TimeSpan.FromMinutes(5)) - : null; - - CallCredentials managedBackendCreds = CallCredentials.FromInterceptor( - async (context, metadata) => - { - metadata.Add("taskhub", taskHubName); - metadata.Add("workerid", workerId); - - if (cache is null) - { - return; - } - - AccessToken token = await cache.GetTokenAsync(context.CancellationToken); - - metadata.Add("Authorization", $"Bearer {token.Token}"); - }); - - // Production will use HTTPS, but local testing will use HTTP - ChannelCredentials channelCreds = endpoint.StartsWith("https://") ? - ChannelCredentials.SecureSsl : - ChannelCredentials.Insecure; - return GrpcChannel.ForAddress(endpoint, new GrpcChannelOptions - { - // The same credential is being used for all operations. - // https://learn.microsoft.com/aspnet/core/grpc/authn-and-authz#set-the-bearer-token-with-callcredentials - Credentials = ChannelCredentials.Create(channelCreds, managedBackendCreds), - - // TODO: This is not appropriate for use in production settings. Setting this to true should - // only be done for local testing. We should hide this setting behind some kind of flag. - UnsafeUseInsecureChannelCallCredentials = true, - }); + builder.UseGrpc(options.GetGrpcChannel()); } static Exception RequiredOptionMissing(string optionName) { return new ArgumentException(message: $"Required option '{optionName}' was not provided."); } - - sealed class TokenCache(TokenCredential credential, TokenRequestContext context, TimeSpan margin) - { - readonly TokenCredential credential = credential; - readonly TokenRequestContext context = context; - readonly TimeSpan margin = margin; - - AccessToken? token; - - public async Task GetTokenAsync(CancellationToken cancellationToken) - { - DateTimeOffset nowWithMargin = DateTimeOffset.UtcNow.Add(this.margin); - - if (this.token is null - || this.token.Value.RefreshOn < nowWithMargin - || this.token.Value.ExpiresOn < nowWithMargin) - { - this.token = await this.credential.GetTokenAsync(this.context, cancellationToken); - } - - return this.token.Value; - } - } } \ No newline at end of file diff --git a/src/Extensions/Azure/DurableTaskSchedulerOptions.cs b/src/Extensions/Azure/DurableTaskSchedulerOptions.cs index aa3d2a1f..4151cbf2 100644 --- a/src/Extensions/Azure/DurableTaskSchedulerOptions.cs +++ b/src/Extensions/Azure/DurableTaskSchedulerOptions.cs @@ -4,6 +4,7 @@ using System.Globalization; using Azure.Core; using Azure.Identity; +using Grpc.Core; namespace Microsoft.DurableTask.Extensions.Azure; @@ -17,8 +18,15 @@ public class DurableTaskSchedulerOptions /// internal DurableTaskSchedulerOptions(string endpointAddress, string taskHubName, TokenCredential? credential = null) { - this.EndpointAddress = endpointAddress ?? throw new ArgumentNullException(nameof(endpointAddress)); - this.TaskHubName = taskHubName ?? throw new ArgumentNullException(nameof(taskHubName)); + Check.NotNullOrEmpty(endpointAddress, nameof(endpointAddress)); + Check.NotNullOrEmpty(taskHubName, nameof(taskHubName)); + + // Add https:// prefix if no protocol is specified + this.EndpointAddress = !endpointAddress.Contains("://") + ? $"https://{endpointAddress}" + : endpointAddress; + + this.TaskHubName = taskHubName; this.Credential = credential; } @@ -26,17 +34,7 @@ internal DurableTaskSchedulerOptions(string endpointAddress, string taskHubName, /// Gets the endpoint address of the Durable Task Scheduler resource. /// Expected to be in the format "https://{scheduler-name}.{region}.durabletask.io". /// - public string EndpointAddress - { - get => this.endpointAddress; - set - { - // Add https:// prefix if no protocol is specified - this.endpointAddress = !value.Contains("://") - ? $"https://{value}" - : value; - } - } + public string EndpointAddress { get; } /// /// Gets the name of the task hub resource associated with the Durable Task Scheduler resource. @@ -71,6 +69,58 @@ public static DurableTaskSchedulerOptions FromConnectionString(string connection return FromConnectionString(new DurableTaskSchedulerConnectionString(connectionString)); } + internal GrpcChannel GetGrpcChannel() + { + Check.NotNullOrEmpty(this.EndpointAddress, nameof(this.EndpointAddress)); + Check.NotNullOrEmpty(this.TaskHubName, nameof(this.TaskHubName)); + + string taskHubName = this.TaskHubName; + string endpoint = this.EndpointAddress; + + string resourceId = this.ResourceId ?? "https://durabletask.io"; + int processId = Environment.ProcessId; + string workerId = this.WorkerId ?? $"{Environment.MachineName},{processId},{Guid.NewGuid():N}"; + + TokenCache? cache = + this.Credential is not null + ? new( + this.Credential, + new(new[] { $"{resourceId}/.default" }), + TimeSpan.FromMinutes(5)) + : null; + + CallCredentials managedBackendCreds = CallCredentials.FromInterceptor( + async (context, metadata) => + { + metadata.Add("taskhub", taskHubName); + metadata.Add("workerid", workerId); + + if (cache is null) + { + return; + } + + AccessToken token = await cache.GetTokenAsync(context.CancellationToken); + + metadata.Add("Authorization", $"Bearer {token.Token}"); + }); + + // Production will use HTTPS, but local testing will use HTTP + ChannelCredentials channelCreds = endpoint.StartsWith("https://") ? + ChannelCredentials.SecureSsl : + ChannelCredentials.Insecure; + return GrpcChannel.ForAddress(endpoint, new GrpcChannelOptions + { + // The same credential is being used for all operations. + // https://learn.microsoft.com/aspnet/core/grpc/authn-and-authz#set-the-bearer-token-with-callcredentials + Credentials = ChannelCredentials.Create(channelCreds, managedBackendCreds), + + // TODO: This is not appropriate for use in production settings. Setting this to true should + // only be done for local testing. We should hide this setting behind some kind of flag. + UnsafeUseInsecureChannelCallCredentials = true, + }); + } + /// /// Creates a new instance of from a parsed connection string. /// @@ -169,4 +219,27 @@ public static DurableTaskSchedulerOptions FromConnectionString( DurableTaskSchedulerOptions options = new(endpointAddress, connectionString.TaskHubName, credential); return options; } + + sealed class TokenCache(TokenCredential credential, TokenRequestContext context, TimeSpan margin) + { + readonly TokenCredential credential = credential; + readonly TokenRequestContext context = context; + readonly TimeSpan margin = margin; + + AccessToken? token; + + public async Task GetTokenAsync(CancellationToken cancellationToken) + { + DateTimeOffset nowWithMargin = DateTimeOffset.UtcNow.Add(this.margin); + + if (this.token is null + || this.token.Value.RefreshOn < nowWithMargin + || this.token.Value.ExpiresOn < nowWithMargin) + { + this.token = await this.credential.GetTokenAsync(this.context, cancellationToken); + } + + return this.token.Value; + } + } } \ No newline at end of file From 54dba769f9face0ed18a1e4cbadd978e0e2aa9df Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 10 Jan 2025 13:46:24 -0800 Subject: [PATCH 19/33] save --- src/Extensions/Azure/DurableTaskSchedulerOptions.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Extensions/Azure/DurableTaskSchedulerOptions.cs b/src/Extensions/Azure/DurableTaskSchedulerOptions.cs index 4151cbf2..9bf17460 100644 --- a/src/Extensions/Azure/DurableTaskSchedulerOptions.cs +++ b/src/Extensions/Azure/DurableTaskSchedulerOptions.cs @@ -20,12 +20,12 @@ internal DurableTaskSchedulerOptions(string endpointAddress, string taskHubName, { Check.NotNullOrEmpty(endpointAddress, nameof(endpointAddress)); Check.NotNullOrEmpty(taskHubName, nameof(taskHubName)); - + // Add https:// prefix if no protocol is specified - this.EndpointAddress = !endpointAddress.Contains("://") - ? $"https://{endpointAddress}" + this.EndpointAddress = !endpointAddress.Contains("://") + ? $"https://{endpointAddress}" : endpointAddress; - + this.TaskHubName = taskHubName; this.Credential = credential; } From 5e97555a2ac574a0fc350a15a003888227b6a311 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 10 Jan 2025 13:57:32 -0800 Subject: [PATCH 20/33] some fb --- src/Extensions/Azure/AccessTokenCache.cs | 50 +++++++++++++++++++ .../Azure/DurableTaskSchedulerOptions.cs | 34 ++----------- 2 files changed, 55 insertions(+), 29 deletions(-) create mode 100644 src/Extensions/Azure/AccessTokenCache.cs diff --git a/src/Extensions/Azure/AccessTokenCache.cs b/src/Extensions/Azure/AccessTokenCache.cs new file mode 100644 index 00000000..0a273170 --- /dev/null +++ b/src/Extensions/Azure/AccessTokenCache.cs @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Azure.Core; + +namespace Microsoft.DurableTask.Extensions.Azure; + +/// +/// Caches and manages refresh for Azure access tokens. +/// +internal sealed class AccessTokenCache +{ + readonly TokenCredential credential; + readonly TokenRequestContext context; + readonly TimeSpan margin; + + AccessToken? token; + + /// + /// Initializes a new instance of the class. + /// + /// The token credential to use for authentication. + /// The token request context. + /// The time margin to use for token refresh. + public AccessTokenCache(TokenCredential credential, TokenRequestContext context, TimeSpan margin) + { + this.credential = credential; + this.context = context; + this.margin = margin; + } + + /// + /// Gets a token, either from cache or by requesting a new one if needed. + /// + /// A cancellation token. + /// An access token. + public async Task GetTokenAsync(CancellationToken cancellationToken) + { + DateTimeOffset nowWithMargin = DateTimeOffset.UtcNow.Add(this.margin); + + if (this.token is null + || this.token.Value.RefreshOn < nowWithMargin + || this.token.Value.ExpiresOn < nowWithMargin) + { + this.token = await this.credential.GetTokenAsync(this.context, cancellationToken); + } + + return this.token.Value; + } +} diff --git a/src/Extensions/Azure/DurableTaskSchedulerOptions.cs b/src/Extensions/Azure/DurableTaskSchedulerOptions.cs index 9bf17460..c7dc157d 100644 --- a/src/Extensions/Azure/DurableTaskSchedulerOptions.cs +++ b/src/Extensions/Azure/DurableTaskSchedulerOptions.cs @@ -81,11 +81,11 @@ internal GrpcChannel GetGrpcChannel() int processId = Environment.ProcessId; string workerId = this.WorkerId ?? $"{Environment.MachineName},{processId},{Guid.NewGuid():N}"; - TokenCache? cache = + AccessTokenCache? cache = this.Credential is not null - ? new( + ? new AccessTokenCache( this.Credential, - new(new[] { $"{resourceId}/.default" }), + new TokenRequestContext(new[] { $"{resourceId}/.default" }), TimeSpan.FromMinutes(5)) : null; @@ -95,13 +95,12 @@ this.Credential is not null metadata.Add("taskhub", taskHubName); metadata.Add("workerid", workerId); - if (cache is null) + if (cache == null) { return; } - + AccessToken token = await cache.GetTokenAsync(context.CancellationToken); - metadata.Add("Authorization", $"Bearer {token.Token}"); }); @@ -219,27 +218,4 @@ public static DurableTaskSchedulerOptions FromConnectionString( DurableTaskSchedulerOptions options = new(endpointAddress, connectionString.TaskHubName, credential); return options; } - - sealed class TokenCache(TokenCredential credential, TokenRequestContext context, TimeSpan margin) - { - readonly TokenCredential credential = credential; - readonly TokenRequestContext context = context; - readonly TimeSpan margin = margin; - - AccessToken? token; - - public async Task GetTokenAsync(CancellationToken cancellationToken) - { - DateTimeOffset nowWithMargin = DateTimeOffset.UtcNow.Add(this.margin); - - if (this.token is null - || this.token.Value.RefreshOn < nowWithMargin - || this.token.Value.ExpiresOn < nowWithMargin) - { - this.token = await this.credential.GetTokenAsync(this.context, cancellationToken); - } - - return this.token.Value; - } - } } \ No newline at end of file From 9c890c5a2b9d9c83dd7b5381ed649d929a57394a Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 10 Jan 2025 14:05:03 -0800 Subject: [PATCH 21/33] fix --- src/Extensions/Azure/DurableTaskSchedulerOptions.cs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Extensions/Azure/DurableTaskSchedulerOptions.cs b/src/Extensions/Azure/DurableTaskSchedulerOptions.cs index c7dc157d..23d85bea 100644 --- a/src/Extensions/Azure/DurableTaskSchedulerOptions.cs +++ b/src/Extensions/Azure/DurableTaskSchedulerOptions.cs @@ -13,6 +13,8 @@ namespace Microsoft.DurableTask.Extensions.Azure; /// public class DurableTaskSchedulerOptions { + private readonly string defaultWorkerId; + /// /// Initializes a new instance of the class. /// @@ -28,6 +30,9 @@ internal DurableTaskSchedulerOptions(string endpointAddress, string taskHubName, this.TaskHubName = taskHubName; this.Credential = credential; + + // Generate the default worker ID once at construction time + this.defaultWorkerId = $"{Environment.MachineName},{Environment.ProcessId},{Guid.NewGuid():N}"; } /// @@ -54,7 +59,7 @@ internal DurableTaskSchedulerOptions(string endpointAddress, string taskHubName, /// /// Gets or sets the worker ID used to identify the worker instance. - /// The default value is a string containing the machine name and the process ID. + /// The default value is a string containing the machine name, process ID, and a unique identifier. /// public string? WorkerId { get; set; } @@ -78,8 +83,7 @@ internal GrpcChannel GetGrpcChannel() string endpoint = this.EndpointAddress; string resourceId = this.ResourceId ?? "https://durabletask.io"; - int processId = Environment.ProcessId; - string workerId = this.WorkerId ?? $"{Environment.MachineName},{processId},{Guid.NewGuid():N}"; + string workerId = this.WorkerId ?? this.defaultWorkerId; AccessTokenCache? cache = this.Credential is not null From 49c62822ede2637569e67bdc3274a93d626e27bb Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 10 Jan 2025 14:06:15 -0800 Subject: [PATCH 22/33] update --- src/Extensions/Azure/DurableTaskSchedulerOptions.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Extensions/Azure/DurableTaskSchedulerOptions.cs b/src/Extensions/Azure/DurableTaskSchedulerOptions.cs index 23d85bea..3a0c6abd 100644 --- a/src/Extensions/Azure/DurableTaskSchedulerOptions.cs +++ b/src/Extensions/Azure/DurableTaskSchedulerOptions.cs @@ -32,6 +32,7 @@ internal DurableTaskSchedulerOptions(string endpointAddress, string taskHubName, this.Credential = credential; // Generate the default worker ID once at construction time + // TODO: More iteration needed over time https://github.com/microsoft/durabletask-dotnet/pull/362#discussion_r1909547102 this.defaultWorkerId = $"{Environment.MachineName},{Environment.ProcessId},{Guid.NewGuid():N}"; } From 1adf7ccb85f017b246037d033ed950c8cbf26db8 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 10 Jan 2025 14:16:28 -0800 Subject: [PATCH 23/33] update tests --- Microsoft.DurableTask.sln | 2 +- .../Azure/DurableTaskSchedulerExtensions.cs | 5 ----- .../Extensions.Azure.Tests.csproj | 18 ------------------ test/Extensions/Azure/Azure.Tests.csproj | 18 ++++++++++++++++++ ...urableTaskSchedulerConnectionStringTests.cs | 0 .../DurableTaskSchedulerExtensionsTests.cs | 0 .../Azure}/DurableTaskSchedulerOptionsTests.cs | 0 7 files changed, 19 insertions(+), 24 deletions(-) delete mode 100644 test/Extensions.Azure.Tests/Extensions.Azure.Tests.csproj create mode 100644 test/Extensions/Azure/Azure.Tests.csproj rename test/{Extensions.Azure.Tests => Extensions/Azure}/DurableTaskSchedulerConnectionStringTests.cs (100%) rename test/{Extensions.Azure.Tests => Extensions/Azure}/DurableTaskSchedulerExtensionsTests.cs (100%) rename test/{Extensions.Azure.Tests => Extensions/Azure}/DurableTaskSchedulerOptionsTests.cs (100%) diff --git a/Microsoft.DurableTask.sln b/Microsoft.DurableTask.sln index 59acc82d..4e5a4451 100644 --- a/Microsoft.DurableTask.sln +++ b/Microsoft.DurableTask.sln @@ -75,7 +75,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Extensions", "Extensions", EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Azure", "src\Extensions\Azure\Azure.csproj", "{662BF73D-A4DD-4910-8625-7C12F1ACDBEC}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Extensions.Azure.Tests", "test\Extensions.Azure.Tests\Extensions.Azure.Tests.csproj", "{DBB5DB4E-A1B0-4C86-A233-213789C46929}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Azure.Tests", "test\Extensions\Azure\Azure.Tests.csproj", "{DBB5DB4E-A1B0-4C86-A233-213789C46929}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs b/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs index 4fda6df8..ed823d29 100644 --- a/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs +++ b/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs @@ -87,9 +87,4 @@ public static void UseDurableTaskScheduler( configure?.Invoke(options); builder.UseGrpc(options.GetGrpcChannel()); } - - static Exception RequiredOptionMissing(string optionName) - { - return new ArgumentException(message: $"Required option '{optionName}' was not provided."); - } } \ No newline at end of file diff --git a/test/Extensions.Azure.Tests/Extensions.Azure.Tests.csproj b/test/Extensions.Azure.Tests/Extensions.Azure.Tests.csproj deleted file mode 100644 index e9c83d3e..00000000 --- a/test/Extensions.Azure.Tests/Extensions.Azure.Tests.csproj +++ /dev/null @@ -1,18 +0,0 @@ - - - - net6.0 - - - - - - - - - - - - - - diff --git a/test/Extensions/Azure/Azure.Tests.csproj b/test/Extensions/Azure/Azure.Tests.csproj new file mode 100644 index 00000000..53146103 --- /dev/null +++ b/test/Extensions/Azure/Azure.Tests.csproj @@ -0,0 +1,18 @@ + + + + net6.0 + + + + + + + + + + + + + + diff --git a/test/Extensions.Azure.Tests/DurableTaskSchedulerConnectionStringTests.cs b/test/Extensions/Azure/DurableTaskSchedulerConnectionStringTests.cs similarity index 100% rename from test/Extensions.Azure.Tests/DurableTaskSchedulerConnectionStringTests.cs rename to test/Extensions/Azure/DurableTaskSchedulerConnectionStringTests.cs diff --git a/test/Extensions.Azure.Tests/DurableTaskSchedulerExtensionsTests.cs b/test/Extensions/Azure/DurableTaskSchedulerExtensionsTests.cs similarity index 100% rename from test/Extensions.Azure.Tests/DurableTaskSchedulerExtensionsTests.cs rename to test/Extensions/Azure/DurableTaskSchedulerExtensionsTests.cs diff --git a/test/Extensions.Azure.Tests/DurableTaskSchedulerOptionsTests.cs b/test/Extensions/Azure/DurableTaskSchedulerOptionsTests.cs similarity index 100% rename from test/Extensions.Azure.Tests/DurableTaskSchedulerOptionsTests.cs rename to test/Extensions/Azure/DurableTaskSchedulerOptionsTests.cs From 14b94ebdd51119d92110b9547ee221cf3568a565 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 10 Jan 2025 15:24:38 -0800 Subject: [PATCH 24/33] sample sample fix --- Microsoft.DurableTask.sln | 7 + .../dotnet/AspNetWebApp/AspNetWebApp.csproj | 27 ++++ .../dotnet/AspNetWebApp/DockerFile | 22 +++ .../Orchestrations/HelloCities.cs | 30 ++++ .../dotnet/AspNetWebApp/Program.cs | 55 +++++++ .../Properties/launchSettings.json | 23 +++ .../dotnet/AspNetWebApp/README.md | 147 ++++++++++++++++++ .../AspNetWebApp/ScenariosController.cs | 50 ++++++ .../portable-sdk/dotnet/AspNetWebApp/Utils.cs | 38 +++++ .../AspNetWebApp/appsettings.Development.json | 8 + .../AspNetWebApp/appsettings.Production.json | 11 ++ .../dotnet/AspNetWebApp/appsettings.json | 9 ++ 12 files changed, 427 insertions(+) create mode 100644 samples/portable-sdk/dotnet/AspNetWebApp/AspNetWebApp.csproj create mode 100644 samples/portable-sdk/dotnet/AspNetWebApp/DockerFile create mode 100644 samples/portable-sdk/dotnet/AspNetWebApp/Orchestrations/HelloCities.cs create mode 100644 samples/portable-sdk/dotnet/AspNetWebApp/Program.cs create mode 100644 samples/portable-sdk/dotnet/AspNetWebApp/Properties/launchSettings.json create mode 100644 samples/portable-sdk/dotnet/AspNetWebApp/README.md create mode 100644 samples/portable-sdk/dotnet/AspNetWebApp/ScenariosController.cs create mode 100644 samples/portable-sdk/dotnet/AspNetWebApp/Utils.cs create mode 100644 samples/portable-sdk/dotnet/AspNetWebApp/appsettings.Development.json create mode 100644 samples/portable-sdk/dotnet/AspNetWebApp/appsettings.Production.json create mode 100644 samples/portable-sdk/dotnet/AspNetWebApp/appsettings.json diff --git a/Microsoft.DurableTask.sln b/Microsoft.DurableTask.sln index 4e5a4451..b7758f81 100644 --- a/Microsoft.DurableTask.sln +++ b/Microsoft.DurableTask.sln @@ -77,6 +77,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Azure", "src\Extensions\Azu EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Azure.Tests", "test\Extensions\Azure\Azure.Tests.csproj", "{DBB5DB4E-A1B0-4C86-A233-213789C46929}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AspNetWebApp", "samples\portable-sdk\dotnet\AspNetWebApp\AspNetWebApp.csproj", "{869D2D51-9372-4764-B059-C43B6C1180A3}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -199,6 +201,10 @@ Global {DBB5DB4E-A1B0-4C86-A233-213789C46929}.Debug|Any CPU.Build.0 = Debug|Any CPU {DBB5DB4E-A1B0-4C86-A233-213789C46929}.Release|Any CPU.ActiveCfg = Release|Any CPU {DBB5DB4E-A1B0-4C86-A233-213789C46929}.Release|Any CPU.Build.0 = Release|Any CPU + {869D2D51-9372-4764-B059-C43B6C1180A3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {869D2D51-9372-4764-B059-C43B6C1180A3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {869D2D51-9372-4764-B059-C43B6C1180A3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {869D2D51-9372-4764-B059-C43B6C1180A3}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -237,6 +243,7 @@ Global {5227C712-2355-403F-90D6-51D0BCAE4D38} = {8AFC9781-F6F1-4696-BB4A-9ED7CA9D612B} {662BF73D-A4DD-4910-8625-7C12F1ACDBEC} = {5227C712-2355-403F-90D6-51D0BCAE4D38} {DBB5DB4E-A1B0-4C86-A233-213789C46929} = {E5637F81-2FB9-4CD7-900D-455363B142A7} + {869D2D51-9372-4764-B059-C43B6C1180A3} = {EFF7632B-821E-4CFC-B4A0-ED4B24296B17} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {AB41CB55-35EA-4986-A522-387AB3402E71} diff --git a/samples/portable-sdk/dotnet/AspNetWebApp/AspNetWebApp.csproj b/samples/portable-sdk/dotnet/AspNetWebApp/AspNetWebApp.csproj new file mode 100644 index 00000000..14516742 --- /dev/null +++ b/samples/portable-sdk/dotnet/AspNetWebApp/AspNetWebApp.csproj @@ -0,0 +1,27 @@ + + + + net8.0 + enable + enable + true + $(BaseIntermediateOutputPath)Generated + + + false + false + + + + + + + + + + + + + + + diff --git a/samples/portable-sdk/dotnet/AspNetWebApp/DockerFile b/samples/portable-sdk/dotnet/AspNetWebApp/DockerFile new file mode 100644 index 00000000..5ddb3ae9 --- /dev/null +++ b/samples/portable-sdk/dotnet/AspNetWebApp/DockerFile @@ -0,0 +1,22 @@ +#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging. + +FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base +WORKDIR /app +EXPOSE 8080 + +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +WORKDIR /src +COPY ["AspNetWebApp.csproj", "."] +RUN dotnet restore "./AspNetWebApp.csproj" +COPY . . +WORKDIR "/src/." +RUN dotnet build "AspNetWebApp.csproj" -c Release -o /app/build + +FROM build AS publish +RUN dotnet publish "AspNetWebApp.csproj" -c Release -o /app/publish /p:UseAppHost=false + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENV ASPNETCORE_ENVIRONMENT=Production +ENTRYPOINT ["dotnet", "AspNetWebApp.dll"] diff --git a/samples/portable-sdk/dotnet/AspNetWebApp/Orchestrations/HelloCities.cs b/samples/portable-sdk/dotnet/AspNetWebApp/Orchestrations/HelloCities.cs new file mode 100644 index 00000000..5c48dcaa --- /dev/null +++ b/samples/portable-sdk/dotnet/AspNetWebApp/Orchestrations/HelloCities.cs @@ -0,0 +1,30 @@ +using Microsoft.DurableTask; +using Microsoft.DurableTask.Client; + +namespace AspNetWebApp.Scenarios; + +[DurableTask] +class HelloCities : TaskOrchestrator> +{ + public override async Task> RunAsync(TaskOrchestrationContext context, string input) + { + List results = + [ + await context.CallSayHelloAsync("Seattle"), + await context.CallSayHelloAsync("Amsterdam"), + await context.CallSayHelloAsync("Hyderabad"), + await context.CallSayHelloAsync("Shanghai"), + await context.CallSayHelloAsync("Tokyo"), + ]; + return results; + } +} + +[DurableTask] +class SayHello : TaskActivity +{ + public override Task RunAsync(TaskActivityContext context, string cityName) + { + return Task.FromResult($"Hello, {cityName}!"); + } +} diff --git a/samples/portable-sdk/dotnet/AspNetWebApp/Program.cs b/samples/portable-sdk/dotnet/AspNetWebApp/Program.cs new file mode 100644 index 00000000..4c21b7b2 --- /dev/null +++ b/samples/portable-sdk/dotnet/AspNetWebApp/Program.cs @@ -0,0 +1,55 @@ +using System.Text.Json.Serialization; +using Azure.Core; +using Azure.Identity; +using Microsoft.DurableTask; +using Microsoft.DurableTask.Client; +using Microsoft.DurableTask.Worker; +using Microsoft.DurableTask.Extensions.Azure; + +WebApplicationBuilder builder = WebApplication.CreateBuilder(args); + +string endpointAddress = builder.Configuration["DURABLE_TASK_SCHEDULER_ENDPOINT_ADDRESS"] + ?? throw new InvalidOperationException("Missing required configuration 'DURABLE_TASK_SCHEDULER_ENDPOINT_ADDRESS'"); + +string taskHubName = builder.Configuration["DURABLE_TASK_SCHEDULER_TASK_HUB_NAME"] + ?? throw new InvalidOperationException("Missing required configuration 'DURABLE_TASK_SCHEDULER_TASK_HUB_NAME'"); + +TokenCredential credential = builder.Environment.IsProduction() + ? new DefaultAzureCredential(new DefaultAzureCredentialOptions { ManagedIdentityClientId = builder.Configuration["CONTAINER_APP_UMI_CLIENT_ID"] }) + : new DefaultAzureCredential(); + +// Add all the generated orchestrations and activities automatically +builder.Services.AddDurableTaskWorker(builder => +{ + builder.AddTasks(r => r.AddAllGeneratedTasks()); + builder.UseDurableTaskScheduler(endpointAddress, taskHubName, credential); +}); + +// Register the client, which can be used to start orchestrations +builder.Services.AddDurableTaskClient(builder => +{ + builder.UseDurableTaskScheduler(endpointAddress, taskHubName, credential); +}); + +// Configure console logging using the simpler, more compact format +builder.Services.AddLogging(logging => +{ + logging.AddSimpleConsole(options => + { + options.SingleLine = true; + options.UseUtcTimestamp = true; + options.TimestampFormat = "yyyy-MM-ddTHH:mm:ss.fffZ "; + }); +}); + +// Configure the HTTP request pipeline +builder.Services.AddControllers().AddJsonOptions(options => +{ + options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); + options.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; +}); + +// The actual listen URL can be configured in environment variables named "ASPNETCORE_URLS" or "ASPNETCORE_URLS_HTTPS" +WebApplication app = builder.Build(); +app.MapControllers(); +app.Run(); diff --git a/samples/portable-sdk/dotnet/AspNetWebApp/Properties/launchSettings.json b/samples/portable-sdk/dotnet/AspNetWebApp/Properties/launchSettings.json new file mode 100644 index 00000000..4ee2ec75 --- /dev/null +++ b/samples/portable-sdk/dotnet/AspNetWebApp/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:36209", + "sslPort": 0 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "applicationUrl": "http://localhost:5008", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DURABLE_TASK_SCHEDULER_ENDPOINT_ADDRESS": "https://localhost:8082", + "DURABLE_TASK_SCHEDULER_TASK_HUB_NAME": "samples" + } + } + } +} diff --git a/samples/portable-sdk/dotnet/AspNetWebApp/README.md b/samples/portable-sdk/dotnet/AspNetWebApp/README.md new file mode 100644 index 00000000..4875b63e --- /dev/null +++ b/samples/portable-sdk/dotnet/AspNetWebApp/README.md @@ -0,0 +1,147 @@ +# Hello World with the Durable Task SDK for .NET + +In addition to [Durable Functions](https://learn.microsoft.com/azure/azure-functions/durable/durable-functions-overview), the [Durable Task SDK for .NET](https://github.com/microsoft/durabletask-dotnet) can also use the Durable Task Scheduler service for managing orchestration state. + +This directory includes a sample .NET console app that demonstrates how to use the Durable Task Scheduler with the Durable Task SDK for .NET (without any Azure Functions dependency). + +## Prerequisites + +- [.NET 8 SDK](https://dotnet.microsoft.com/download/dotnet/8.0) +- [PowerShell](https://docs.microsoft.com/powershell/scripting/install/installing-powershell) +- [Azure CLI](https://docs.microsoft.com/cli/azure/install-azure-cli) + +## Creating a Durable Task Scheduler task hub + +Before you can run the app, you need to create a Durable Task Scheduler task hub in Azure and produce a connection string that references it. + +> **NOTE**: These are abbreviated instructions for simplicity. For a full set of instructions, see the Azure Durable Functions [QuickStart guide](../../../../quickstarts/HelloCities/README.md#create-a-durable-task-scheduler-namespace-and-task-hub). + +1. Install the Durable Task Scheduler CLI extension: + + ```bash + az upgrade + az extension add --name durabletask --allow-preview true + ``` + +1. Create a resource group: + + ```powershell + az group create --name my-resource-group --location northcentralus + ``` + +1. Create a Durable Task Scheduler namespace: + + ```powershell + az durabletask namespace create -g my-resource-group --name my-namespace + ``` + +1. Create a task hub within the namespace: + + ```powershell + az durabletask taskhub create -g my-resource-group --namespace my-namespace --name "portable-dotnet" + ``` + +1. Grant the current user permission to connect to the `portable-dotnet` task hub: + + ```powershell + $subscriptionId = az account show --query "id" -o tsv + $loggedInUser = az account show --query "user.name" -o tsv + + az role assignment create ` + --assignee $loggedInUser ` + --role "Durable Task Data Contributor" ` + --scope "/subscriptions/$subscriptionId/resourceGroups/my-resource-group/providers/Microsoft.DurableTask/namespaces/my-namespace/taskHubs/portable-dotnet" + ``` + + Note that it may take a minute for the role assignment to take effect. + +1. Get the endpoint for the scheduler resource and save it to the `DURABLE_TASK_SCHEDULER_ENDPOINT_ADDRESS` environment variable: + + ```powershell + $endpoint = az durabletask namespace show ` + -g my-resource-group ` + -n my-namespace ` + --query "properties.url" ` + -o tsv + $env:DURABLE_TASK_SCHEDULER_ENDPOINT_ADDRESS = $endpoint + ``` + + The `DURABLE_TASK_SCHEDULER_ENDPOINT_ADDRESS` environment variable is used by the sample app to connect to the Durable Task Scheduler resource. + +1. Save the task hub name to the `DURABLE_TASK_SCHEDULER_TASK_HUB_NAME` environment variable: + + ```powershell + $env:DURABLE_TASK_SCHEDULER_TASK_HUB_NAME = "portable-dotnet" + ``` + + The `DURABLE_TASK_SCHEDULER_TASK_HUB_NAME` environment variable is to configure the sample app with the correct task hub resource name. + +## Running the sample + +In the same terminal window as above, use the following steps to run the sample on your local machine. + +1. Clone this repository. + +1. Open a terminal window and navigate to the `samples/portable-sdk/dotnet/AspNetWebApp` directory. + +1. Run the following command to build and run the sample: + + ```bash + dotnet run + ``` + +You should see output similar to the following: + +```plaintext +Building... +info: Microsoft.DurableTask[1] + Durable Task gRPC worker starting. +info: Microsoft.Hosting.Lifetime[14] + Now listening on: http://localhost:5008 +info: Microsoft.Hosting.Lifetime[0] + Application started. Press Ctrl+C to shut down. +info: Microsoft.Hosting.Lifetime[0] + Hosting environment: Development +info: Microsoft.Hosting.Lifetime[0] + Content root path: D:\projects\Azure-Functions-Durable-Task-Scheduler-Private-Preview\samples\portable-sdk\dotnet\AspNetWebApp +info: Microsoft.DurableTask[4] + Sidecar work-item streaming connection established. +``` + +## View orchestrations in the dashboard + +You can view the orchestrations in the Durable Task Scheduler dashboard by navigating to the namespace-specific dashboard URL in your browser. + +Use the following PowerShell command to get the dashboard URL: + +```powershell +$baseUrl = az durabletask namespace show ` + -g my-resource-group ` + -n my-namespace ` + --query "properties.dashboardUrl" ` + -o tsv +$dashboardUrl = "$baseUrl/taskHubs/portable-dotnet" +$dashboardUrl +``` + +The URL should look something like the following: + +```plaintext +https://my-namespace-atdngmgxfsh0-db.northcentralus.durabletask.io/taskHubs/portable-dotnet +``` + +Once logged in, you should see the orchestrations that were created by the sample app. Below is an example of what the dashboard might look like (note that some of the details will be different than the screenshot): + +![Durable Task Scheduler dashboard](/media/images/dtfx-sample-dashboard.png) + + +## Optional: Deploy to Azure Container Apps +1. Create an container app following the instructions in the [Azure Container App documentation](https://learn.microsoft.com/azure/container-apps/get-started?tabs=bash). +2. During step 1, specify the deployed container app code folder at samples\portable-sdk\dotnet\AspNetWebApp +3. Follow the instructions to create a user managed identity and assign the `Durable Task Data Contributor` role then attach it to the container app you created in step 1 at [Azure-Functions-Durable-Task-Scheduler-Private-Preview](..\..\..\..\docs\configure-existing-app.md#run-the-app-on-azure-net). Please skip section "Add required environment variables to app" since these environment variables are not required for deploying to container app. +4. Call the container app endpoint at `http://sampleapi-.azurecontainerapps.io/api/orchestrators/HelloCities`, Sample curl command: + + ```bash + curl -X POST "https://sampleapi-.azurecontainerapps.io/api/orchestrators/HelloCities" + ``` +5. You should see the orchestration created in the Durable Task Scheduler dashboard. diff --git a/samples/portable-sdk/dotnet/AspNetWebApp/ScenariosController.cs b/samples/portable-sdk/dotnet/AspNetWebApp/ScenariosController.cs new file mode 100644 index 00000000..d6049129 --- /dev/null +++ b/samples/portable-sdk/dotnet/AspNetWebApp/ScenariosController.cs @@ -0,0 +1,50 @@ +using System.Diagnostics; +using Microsoft.AspNetCore.Mvc; +using Microsoft.DurableTask; +using Microsoft.DurableTask.Client; + +namespace AspNetWebApp; + +[Route("scenarios")] +[ApiController] +public partial class ScenariosController( + DurableTaskClient durableTaskClient, + ILogger logger) : ControllerBase +{ + readonly DurableTaskClient durableTaskClient = durableTaskClient; + readonly ILogger logger = logger; + + [HttpPost("hellocities")] + public async Task RunHelloCities([FromQuery] int? count, [FromQuery] string? prefix) + { + if (count is null || count < 1) + { + return this.BadRequest(new { error = "A 'count' query string parameter is required and it must contain a positive number." }); + } + + // Generate a semi-unique prefix for the instance IDs to simplify tracking + prefix ??= $"hellocities-{count}-"; + prefix += DateTime.UtcNow.ToString("yyyyMMdd-hhmmss"); + + this.logger.LogInformation("Scheduling {count} orchestrations with a prefix of '{prefix}'...", count, prefix); + + Stopwatch sw = Stopwatch.StartNew(); + await Enumerable.Range(0, count.Value).ParallelForEachAsync(1000, i => + { + string instanceId = $"{prefix}-{i:X16}"; + return this.durableTaskClient.ScheduleNewHelloCitiesInstanceAsync( + input: null!, + new StartOrchestrationOptions(instanceId)); + }); + + sw.Stop(); + this.logger.LogInformation( + "All {count} orchestrations were scheduled successfully in {time}ms!", + count, + sw.ElapsedMilliseconds); + return this.Ok(new + { + message = $"Scheduled {count} orchestrations prefixed with '{prefix}' in {sw.ElapsedMilliseconds}." + }); + } +} diff --git a/samples/portable-sdk/dotnet/AspNetWebApp/Utils.cs b/samples/portable-sdk/dotnet/AspNetWebApp/Utils.cs new file mode 100644 index 00000000..6ddabf39 --- /dev/null +++ b/samples/portable-sdk/dotnet/AspNetWebApp/Utils.cs @@ -0,0 +1,38 @@ +namespace AspNetWebApp; + +static class Utils +{ + public static async Task ParallelForEachAsync(this IEnumerable items, int maxConcurrency, Func action) + { + List tasks; + if (items is ICollection itemCollection) + { + tasks = new List(itemCollection.Count); + } + else + { + tasks = []; + } + + using SemaphoreSlim semaphore = new(maxConcurrency); + foreach (T item in items) + { + tasks.Add(InvokeThrottledAction(item, action, semaphore)); + } + + await Task.WhenAll(tasks); + } + + static async Task InvokeThrottledAction(T item, Func action, SemaphoreSlim semaphore) + { + await semaphore.WaitAsync(); + try + { + await action(item); + } + finally + { + semaphore.Release(); + } + } +} diff --git a/samples/portable-sdk/dotnet/AspNetWebApp/appsettings.Development.json b/samples/portable-sdk/dotnet/AspNetWebApp/appsettings.Development.json new file mode 100644 index 00000000..a6e86ace --- /dev/null +++ b/samples/portable-sdk/dotnet/AspNetWebApp/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/samples/portable-sdk/dotnet/AspNetWebApp/appsettings.Production.json b/samples/portable-sdk/dotnet/AspNetWebApp/appsettings.Production.json new file mode 100644 index 00000000..70b7d1d9 --- /dev/null +++ b/samples/portable-sdk/dotnet/AspNetWebApp/appsettings.Production.json @@ -0,0 +1,11 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "DURABLE_TASK_SCHEDULER_ENDPOINT_ADDRESS": "https://{your-durable-task-endpoint}.durabletask.io", + "DURABLE_TASK_SCHEDULER_TASK_HUB_NAME": "{your-task-hub-name}", + "CONTAINER_APP_UMI_CLIENT_ID": "{your-user-managed-identity-client-id}" +} diff --git a/samples/portable-sdk/dotnet/AspNetWebApp/appsettings.json b/samples/portable-sdk/dotnet/AspNetWebApp/appsettings.json new file mode 100644 index 00000000..10f68b8c --- /dev/null +++ b/samples/portable-sdk/dotnet/AspNetWebApp/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} From 5696b2f8116752f86c53ab6f4ef8a655f4111e5c Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 10 Jan 2025 20:43:26 -0800 Subject: [PATCH 25/33] save --- .../Azure/DurableTaskSchedulerExtensions.cs | 28 +++++++++++++------ 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs b/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs index ed823d29..c7102bfb 100644 --- a/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs +++ b/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs @@ -27,10 +27,12 @@ public static void UseDurableTaskScheduler( TokenCredential credential, Action? configure = null) { - DurableTaskSchedulerOptions options = new(endpointAddress, taskHubName, credential); - - configure?.Invoke(options); + if (configure is not null) + { + builder.Services.Configure("DurableTaskSchedulerOptionsForWorker", configure); + } + DurableTaskSchedulerOptions options = new(endpointAddress, taskHubName, credential); builder.UseGrpc(options.GetGrpcChannel()); } @@ -45,8 +47,12 @@ public static void UseDurableTaskScheduler( string connectionString, Action? configure = null) { + if (configure is not null) + { + builder.Services.Configure(builder.Name, configure); + } + var options = DurableTaskSchedulerOptions.FromConnectionString(connectionString); - configure?.Invoke(options); builder.UseGrpc(options.GetGrpcChannel()); } @@ -65,10 +71,12 @@ public static void UseDurableTaskScheduler( TokenCredential credential, Action? configure = null) { - DurableTaskSchedulerOptions options = new(endpointAddress, taskHubName, credential); - - configure?.Invoke(options); + if (configure is not null) + { + builder.Services.Configure(builder.Name, configure); + } + DurableTaskSchedulerOptions options = new(endpointAddress, taskHubName, credential); builder.UseGrpc(options.GetGrpcChannel()); } @@ -83,8 +91,12 @@ public static void UseDurableTaskScheduler( string connectionString, Action? configure = null) { + if (configure is not null) + { + builder.Services.Configure(builder.Name, configure); + } + var options = DurableTaskSchedulerOptions.FromConnectionString(connectionString); - configure?.Invoke(options); builder.UseGrpc(options.GetGrpcChannel()); } } \ No newline at end of file From 62e2b3043c3d1c49f9494ad6d672bbeb740ded01 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 10 Jan 2025 21:02:02 -0800 Subject: [PATCH 26/33] save --- .../Azure/DurableTaskSchedulerExtensions.cs | 44 +++++++------------ 1 file changed, 16 insertions(+), 28 deletions(-) diff --git a/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs b/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs index c7102bfb..8e58071d 100644 --- a/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs +++ b/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs @@ -4,6 +4,8 @@ using Azure.Core; using Microsoft.DurableTask.Client; using Microsoft.DurableTask.Worker; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; namespace Microsoft.DurableTask.Extensions.Azure; @@ -12,6 +14,16 @@ namespace Microsoft.DurableTask.Extensions.Azure; /// public static class DurableTaskSchedulerExtensions { + /// + /// Configures DurableTaskScheduler options. + /// + public static void ConfigureDurableTaskSchedulerOptions( + IServiceCollection services, + Action configure) + { + services.Configure(Options.DefaultName, configure); + } + /// /// Configures Durable Task worker to use the Azure Durable Task Scheduler service. /// @@ -24,14 +36,8 @@ public static void UseDurableTaskScheduler( this IDurableTaskWorkerBuilder builder, string endpointAddress, string taskHubName, - TokenCredential credential, - Action? configure = null) + TokenCredential credential) { - if (configure is not null) - { - builder.Services.Configure("DurableTaskSchedulerOptionsForWorker", configure); - } - DurableTaskSchedulerOptions options = new(endpointAddress, taskHubName, credential); builder.UseGrpc(options.GetGrpcChannel()); } @@ -44,14 +50,8 @@ public static void UseDurableTaskScheduler( /// Optional callback to configure additional options. public static void UseDurableTaskScheduler( this IDurableTaskWorkerBuilder builder, - string connectionString, - Action? configure = null) + string connectionString) { - if (configure is not null) - { - builder.Services.Configure(builder.Name, configure); - } - var options = DurableTaskSchedulerOptions.FromConnectionString(connectionString); builder.UseGrpc(options.GetGrpcChannel()); } @@ -68,14 +68,8 @@ public static void UseDurableTaskScheduler( this IDurableTaskClientBuilder builder, string endpointAddress, string taskHubName, - TokenCredential credential, - Action? configure = null) + TokenCredential credential) { - if (configure is not null) - { - builder.Services.Configure(builder.Name, configure); - } - DurableTaskSchedulerOptions options = new(endpointAddress, taskHubName, credential); builder.UseGrpc(options.GetGrpcChannel()); } @@ -88,14 +82,8 @@ public static void UseDurableTaskScheduler( /// Optional callback to configure additional options. public static void UseDurableTaskScheduler( this IDurableTaskClientBuilder builder, - string connectionString, - Action? configure = null) + string connectionString) { - if (configure is not null) - { - builder.Services.Configure(builder.Name, configure); - } - var options = DurableTaskSchedulerOptions.FromConnectionString(connectionString); builder.UseGrpc(options.GetGrpcChannel()); } From 02b02c733253878d06229357a78feef33fbafee5 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 10 Jan 2025 23:23:41 -0800 Subject: [PATCH 27/33] update --- src/Extensions/Azure/Azure.csproj | 1 + .../Azure/DurableTaskSchedulerExtensions.cs | 107 +++++++++---- .../Azure/DurableTaskSchedulerOptions.cs | 148 ++++++------------ 3 files changed, 121 insertions(+), 135 deletions(-) diff --git a/src/Extensions/Azure/Azure.csproj b/src/Extensions/Azure/Azure.csproj index 25704be0..d82dd02f 100644 --- a/src/Extensions/Azure/Azure.csproj +++ b/src/Extensions/Azure/Azure.csproj @@ -9,6 +9,7 @@ + diff --git a/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs b/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs index 8e58071d..2e8f2e22 100644 --- a/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs +++ b/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs @@ -7,6 +7,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; + namespace Microsoft.DurableTask.Extensions.Azure; /// @@ -14,77 +15,115 @@ namespace Microsoft.DurableTask.Extensions.Azure; /// public static class DurableTaskSchedulerExtensions { - /// - /// Configures DurableTaskScheduler options. - /// - public static void ConfigureDurableTaskSchedulerOptions( - IServiceCollection services, - Action configure) - { - services.Configure(Options.DefaultName, configure); - } - /// /// Configures Durable Task worker to use the Azure Durable Task Scheduler service. /// - /// The worker builder to configure. - /// The endpoint address of the Durable Task Scheduler service. - /// The name of the task hub to connect to. - /// The credential to use for authentication. - /// Optional callback to configure additional options. public static void UseDurableTaskScheduler( this IDurableTaskWorkerBuilder builder, string endpointAddress, string taskHubName, - TokenCredential credential) + TokenCredential credential, + Action? configure = null) { - DurableTaskSchedulerOptions options = new(endpointAddress, taskHubName, credential); + builder.Services.AddOptions(builder.Name) + .Configure(options => + { + options.EndpointAddress = endpointAddress; + options.TaskHubName = taskHubName; + options.Credential = credential; + }) + .Configure(configure ?? (_ => { })) + .ValidateDataAnnotations() + .ValidateOnStart(); + + var options = builder.Services.BuildServiceProvider() + .GetRequiredService>() + .Get(builder.Name); + builder.UseGrpc(options.GetGrpcChannel()); } /// /// Configures Durable Task worker to use the Azure Durable Task Scheduler service using a connection string. /// - /// The worker builder to configure. - /// The connection string for the Durable Task Scheduler service. - /// Optional callback to configure additional options. public static void UseDurableTaskScheduler( this IDurableTaskWorkerBuilder builder, - string connectionString) + string connectionString, + Action? configure = null) { - var options = DurableTaskSchedulerOptions.FromConnectionString(connectionString); + var connectionOptions = DurableTaskSchedulerOptions.FromConnectionString(connectionString); + + builder.Services.AddOptions(builder.Name) + .Configure(options => + { + options.EndpointAddress = connectionOptions.EndpointAddress; + options.TaskHubName = connectionOptions.TaskHubName; + options.Credential = connectionOptions.Credential; + }) + .Configure(configure ?? (_ => { })) + .ValidateDataAnnotations() + .ValidateOnStart(); + + var options = builder.Services.BuildServiceProvider() + .GetRequiredService>() + .Get(builder.Name); + builder.UseGrpc(options.GetGrpcChannel()); } /// /// Configures Durable Task client to use the Azure Durable Task Scheduler service. /// - /// The client builder to configure. - /// The endpoint address of the Durable Task Scheduler service. - /// The name of the task hub to connect to. - /// The credential to use for authentication. - /// Optional callback to configure additional options. public static void UseDurableTaskScheduler( this IDurableTaskClientBuilder builder, string endpointAddress, string taskHubName, - TokenCredential credential) + TokenCredential credential, + Action? configure = null) { - DurableTaskSchedulerOptions options = new(endpointAddress, taskHubName, credential); + builder.Services.AddOptions(Options.DefaultName) + .Configure(options => + { + options.EndpointAddress = endpointAddress; + options.TaskHubName = taskHubName; + options.Credential = credential; + }) + .Configure(configure ?? (_ => { })) + .ValidateDataAnnotations() + .ValidateOnStart(); + + var options = builder.Services.BuildServiceProvider() + .GetRequiredService>() + .Get(Options.DefaultName); + builder.UseGrpc(options.GetGrpcChannel()); } /// /// Configures Durable Task client to use the Azure Durable Task Scheduler service using a connection string. /// - /// The client builder to configure. - /// The connection string for the Durable Task Scheduler service. - /// Optional callback to configure additional options. public static void UseDurableTaskScheduler( this IDurableTaskClientBuilder builder, - string connectionString) + string connectionString, + Action? configure = null) { - var options = DurableTaskSchedulerOptions.FromConnectionString(connectionString); + var connectionOptions = DurableTaskSchedulerOptions.FromConnectionString(connectionString); + + builder.Services.AddOptions(Options.DefaultName) + .Configure(options => + { + options.EndpointAddress = connectionOptions.EndpointAddress; + options.TaskHubName = connectionOptions.TaskHubName; + options.Credential = connectionOptions.Credential; + }) + .Configure(configure ?? (_ => { })) + .ValidateDataAnnotations() + .ValidateOnStart(); + + var options = builder.Services.BuildServiceProvider() + .GetRequiredService>() + .Get(Options.DefaultName); + builder.UseGrpc(options.GetGrpcChannel()); } } \ No newline at end of file diff --git a/src/Extensions/Azure/DurableTaskSchedulerOptions.cs b/src/Extensions/Azure/DurableTaskSchedulerOptions.cs index 3a0c6abd..06fdbf5d 100644 --- a/src/Extensions/Azure/DurableTaskSchedulerOptions.cs +++ b/src/Extensions/Azure/DurableTaskSchedulerOptions.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System.ComponentModel.DataAnnotations; using System.Globalization; using Azure.Core; using Azure.Identity; @@ -13,63 +14,45 @@ namespace Microsoft.DurableTask.Extensions.Azure; /// public class DurableTaskSchedulerOptions { - private readonly string defaultWorkerId; - - /// - /// Initializes a new instance of the class. - /// - internal DurableTaskSchedulerOptions(string endpointAddress, string taskHubName, TokenCredential? credential = null) - { - Check.NotNullOrEmpty(endpointAddress, nameof(endpointAddress)); - Check.NotNullOrEmpty(taskHubName, nameof(taskHubName)); - - // Add https:// prefix if no protocol is specified - this.EndpointAddress = !endpointAddress.Contains("://") - ? $"https://{endpointAddress}" - : endpointAddress; - - this.TaskHubName = taskHubName; - this.Credential = credential; - - // Generate the default worker ID once at construction time - // TODO: More iteration needed over time https://github.com/microsoft/durabletask-dotnet/pull/362#discussion_r1909547102 - this.defaultWorkerId = $"{Environment.MachineName},{Environment.ProcessId},{Guid.NewGuid():N}"; - } - /// - /// Gets the endpoint address of the Durable Task Scheduler resource. + /// Gets or sets the endpoint address of the Durable Task Scheduler resource. /// Expected to be in the format "https://{scheduler-name}.{region}.durabletask.io". /// - public string EndpointAddress { get; } + [Required(ErrorMessage = "Endpoint address is required")] + public string EndpointAddress { get; set; } = string.Empty; /// - /// Gets the name of the task hub resource associated with the Durable Task Scheduler resource. + /// Gets or sets the name of the task hub resource associated with the Durable Task Scheduler resource. /// - public string TaskHubName { get; } + [Required(ErrorMessage = "Task hub name is required")] + public string TaskHubName { get; set; } = string.Empty; /// - /// Gets the credential used to authenticate with the Durable Task Scheduler task hub resource. + /// Gets or sets the credential used to authenticate with the Durable Task Scheduler task hub resource. /// - public TokenCredential? Credential { get; } + public TokenCredential? Credential { get; set; } /// /// Gets or sets the resource ID of the Durable Task Scheduler resource. /// The default value is https://durabletask.io. /// - public string? ResourceId { get; set; } + public string ResourceId { get; set; } = "https://durabletask.io"; /// /// Gets or sets the worker ID used to identify the worker instance. /// The default value is a string containing the machine name, process ID, and a unique identifier. /// - public string? WorkerId { get; set; } + public string WorkerId { get; set; } = $"{Environment.MachineName},{Environment.ProcessId},{Guid.NewGuid():N}"; + + /// + /// Gets or sets a value indicating whether to allow insecure channel credentials. + /// This should only be set to true in development/testing scenarios. + /// + public bool AllowInsecureCredentials { get; set; } /// /// Creates a new instance of from a connection string. /// - /// The connection string containing the configuration settings. - /// A new instance of configured with the connection string settings. - /// Thrown when the connection string contains an unsupported authentication type. public static DurableTaskSchedulerOptions FromConnectionString(string connectionString) { return FromConnectionString(new DurableTaskSchedulerConnectionString(connectionString)); @@ -81,16 +64,15 @@ internal GrpcChannel GetGrpcChannel() Check.NotNullOrEmpty(this.TaskHubName, nameof(this.TaskHubName)); string taskHubName = this.TaskHubName; - string endpoint = this.EndpointAddress; - - string resourceId = this.ResourceId ?? "https://durabletask.io"; - string workerId = this.WorkerId ?? this.defaultWorkerId; + string endpoint = !this.EndpointAddress.Contains("://") + ? $"https://{this.EndpointAddress}" + : this.EndpointAddress; AccessTokenCache? cache = this.Credential is not null ? new AccessTokenCache( this.Credential, - new TokenRequestContext(new[] { $"{resourceId}/.default" }), + new TokenRequestContext(new[] { $"{this.ResourceId}/.default" }), TimeSpan.FromMinutes(5)) : null; @@ -98,90 +80,68 @@ this.Credential is not null async (context, metadata) => { metadata.Add("taskhub", taskHubName); - metadata.Add("workerid", workerId); + metadata.Add("workerid", this.WorkerId); if (cache == null) { return; } - + AccessToken token = await cache.GetTokenAsync(context.CancellationToken); metadata.Add("Authorization", $"Bearer {token.Token}"); }); // Production will use HTTPS, but local testing will use HTTP - ChannelCredentials channelCreds = endpoint.StartsWith("https://") ? + ChannelCredentials channelCreds = endpoint.StartsWith("https://", StringComparison.OrdinalIgnoreCase) ? ChannelCredentials.SecureSsl : ChannelCredentials.Insecure; - return GrpcChannel.ForAddress(endpoint, new GrpcChannelOptions - { - // The same credential is being used for all operations. - // https://learn.microsoft.com/aspnet/core/grpc/authn-and-authz#set-the-bearer-token-with-callcredentials - Credentials = ChannelCredentials.Create(channelCreds, managedBackendCreds), - // TODO: This is not appropriate for use in production settings. Setting this to true should - // only be done for local testing. We should hide this setting behind some kind of flag. - UnsafeUseInsecureChannelCallCredentials = true, - }); + return GrpcChannel.ForAddress(endpoint, new GrpcChannelOptions + { + Credentials = ChannelCredentials.Create(channelCreds, managedBackendCreds), + UnsafeUseInsecureChannelCallCredentials = this.AllowInsecureCredentials, + }); } /// /// Creates a new instance of from a parsed connection string. /// - /// The parsed connection string containing the configuration settings. - /// A new instance of configured with the connection string settings. - /// Thrown when the connection string contains an unsupported authentication type. public static DurableTaskSchedulerOptions FromConnectionString( DurableTaskSchedulerConnectionString connectionString) { - // Example connection strings: - // "Endpoint=https://myaccount.westus3.durabletask.io/;Authentication=ManagedIdentity;ClientID=00000000-0000-0000-0000-000000000000;TaskHubName=th01" - // "Endpoint=https://myaccount.westus3.durabletask.io/;Authentication=DefaultAzure;TaskHubName=th01" - // "Endpoint=https://myaccount.westus3.durabletask.io/;Authentication=None;TaskHubName=th01" (undocumented and only intended for local testing) - - string endpointAddress = connectionString.Endpoint; - - if (!endpointAddress.Contains("://")) + var options = new DurableTaskSchedulerOptions { - // If the protocol is missing, assume HTTPS. - endpointAddress = "https://" + endpointAddress; - } + EndpointAddress = connectionString.Endpoint, + TaskHubName = connectionString.TaskHubName, + Credential = GetCredentialFromConnectionString(connectionString) + }; - string authType = connectionString.Authentication; + return options; + } - TokenCredential? credential; + static TokenCredential? GetCredentialFromConnectionString(DurableTaskSchedulerConnectionString connectionString) + { + string authType = connectionString.Authentication; // Parse the supported auth types, in a case-insensitive way and without spaces switch (authType.ToLower(CultureInfo.InvariantCulture).Replace(" ", string.Empty)) { case "defaultazure": - // Default Azure credentials, suitable for a variety of scenarios - // In many cases, users will need to pass additional configuration options via env vars - credential = new DefaultAzureCredential(); - break; + return new DefaultAzureCredential(); case "managedidentity": - // Use Managed identity - // Suitable for Azure-hosted scenarios - // Note that ClientId could be null for system-assigned managed identities - credential = new ManagedIdentityCredential(connectionString.ClientId); - break; + return new ManagedIdentityCredential(connectionString.ClientId); case "workloadidentity": - // Use Workload Identity Federation. - // This is commonly-used in Kubernetes (hosted on Azure or anywhere), or in CI environments like - // Azure Pipelines or GitHub Actions. It can also be used with SPIFFE. - WorkloadIdentityCredentialOptions opts = new() { }; + var opts = new WorkloadIdentityCredentialOptions(); if (!string.IsNullOrEmpty(connectionString.ClientId)) { opts.ClientId = connectionString.ClientId; } - if (!string.IsNullOrEmpty(connectionString.TenantId)) { opts.TenantId = connectionString.TenantId; } - if (connectionString.AdditionallyAllowedTenants is not null) { foreach (string tenant in connectionString.AdditionallyAllowedTenants) @@ -189,38 +149,24 @@ public static DurableTaskSchedulerOptions FromConnectionString( opts.AdditionallyAllowedTenants.Add(tenant); } } - - credential = new WorkloadIdentityCredential(opts); - break; + return new WorkloadIdentityCredential(opts); case "environment": - // Use credentials from the environment - credential = new EnvironmentCredential(); - break; + return new EnvironmentCredential(); case "azurecli": - // Use credentials from the Azure CLI - credential = new AzureCliCredential(); - break; + return new AzureCliCredential(); case "azurepowershell": - // Use credentials from the Azure PowerShell modules - credential = new AzurePowerShellCredential(); - break; + return new AzurePowerShellCredential(); case "none": - // Do not use any authentication/authorization (for testing only) - // This is a no-op - credential = null; - break; + return null; default: throw new ArgumentException( $"The connection string contains an unsupported authentication type '{authType}'.", nameof(connectionString)); } - - DurableTaskSchedulerOptions options = new(endpointAddress, connectionString.TaskHubName, credential); - return options; } } \ No newline at end of file From ee517d2583cdbd7431b7e4d1a413e2320f8a4852 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Sat, 11 Jan 2025 08:45:52 -0800 Subject: [PATCH 28/33] save --- .../Azure/DurableTaskSchedulerExtensions.cs | 48 +++++++++---------- .../Azure/DurableTaskSchedulerOptions.cs | 2 +- 2 files changed, 24 insertions(+), 26 deletions(-) diff --git a/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs b/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs index 2e8f2e22..947caf77 100644 --- a/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs +++ b/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs @@ -36,11 +36,9 @@ public static void UseDurableTaskScheduler( .ValidateDataAnnotations() .ValidateOnStart(); - var options = builder.Services.BuildServiceProvider() - .GetRequiredService>() - .Get(builder.Name); - - builder.UseGrpc(options.GetGrpcChannel()); + builder.Services.TryAddEnumerable( + ServiceDescription.Singleton, ConfigureGrpcChannel>()); + builder.UseGrpc(_ => { }); } /// @@ -64,11 +62,9 @@ public static void UseDurableTaskScheduler( .ValidateDataAnnotations() .ValidateOnStart(); - var options = builder.Services.BuildServiceProvider() - .GetRequiredService>() - .Get(builder.Name); - - builder.UseGrpc(options.GetGrpcChannel()); + builder.Services.TryAddEnumerable( + ServiceDescription.Singleton, ConfigureGrpcChannel>()); + builder.UseGrpc(_ => { }); } /// @@ -92,11 +88,9 @@ public static void UseDurableTaskScheduler( .ValidateDataAnnotations() .ValidateOnStart(); - var options = builder.Services.BuildServiceProvider() - .GetRequiredService>() - .Get(Options.DefaultName); - - builder.UseGrpc(options.GetGrpcChannel()); + builder.Services.TryAddEnumerable( + ServiceDescription.Singleton, ConfigureGrpcChannel>()); + builder.UseGrpc(_ => { }); } /// @@ -110,20 +104,24 @@ public static void UseDurableTaskScheduler( var connectionOptions = DurableTaskSchedulerOptions.FromConnectionString(connectionString); builder.Services.AddOptions(Options.DefaultName) - .Configure(options => - { - options.EndpointAddress = connectionOptions.EndpointAddress; - options.TaskHubName = connectionOptions.TaskHubName; - options.Credential = connectionOptions.Credential; - }) .Configure(configure ?? (_ => { })) .ValidateDataAnnotations() .ValidateOnStart(); - var options = builder.Services.BuildServiceProvider() - .GetRequiredService>() - .Get(Options.DefaultName); + builder.Services.TryAddEnumerable( + ServiceDescription.Singleton, ConfigureGrpcChannel>()); + builder.UseGrpc(_ => { }); + } + + // helper internal class + class ConfigureGrpcChannel(IOptionsMonitor schedulerOptions) : IConfigureNamedOptions + { + public void Configure(GrpcDurableTaskWorkerOptions options) => this.Configure(Options.DefaultName, options); - builder.UseGrpc(options.GetGrpcChannel()); + public void Configure(string name, GrpcDurableTaskWorkerOptions options) + { + DurableTaskSchedulerOptions source = schedulerOptions.Get(name); + options.Channel = source.CreateChannel(); + } } } \ No newline at end of file diff --git a/src/Extensions/Azure/DurableTaskSchedulerOptions.cs b/src/Extensions/Azure/DurableTaskSchedulerOptions.cs index 06fdbf5d..25f994dd 100644 --- a/src/Extensions/Azure/DurableTaskSchedulerOptions.cs +++ b/src/Extensions/Azure/DurableTaskSchedulerOptions.cs @@ -58,7 +58,7 @@ public static DurableTaskSchedulerOptions FromConnectionString(string connection return FromConnectionString(new DurableTaskSchedulerConnectionString(connectionString)); } - internal GrpcChannel GetGrpcChannel() + internal GrpcChannel CreateChannel() { Check.NotNullOrEmpty(this.EndpointAddress, nameof(this.EndpointAddress)); Check.NotNullOrEmpty(this.TaskHubName, nameof(this.TaskHubName)); From 34a47addc1bc452b191d2bb7bce422c2dc974062 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Sat, 11 Jan 2025 09:05:56 -0800 Subject: [PATCH 29/33] save --- .../Azure/DurableTaskSchedulerExtensions.cs | 47 +++++++++++++++---- 1 file changed, 37 insertions(+), 10 deletions(-) diff --git a/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs b/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs index 947caf77..bc1e2374 100644 --- a/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs +++ b/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs @@ -3,11 +3,13 @@ using Azure.Core; using Microsoft.DurableTask.Client; +using Microsoft.DurableTask.Client.Grpc; using Microsoft.DurableTask.Worker; +using Microsoft.DurableTask.Worker.Grpc; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; - namespace Microsoft.DurableTask.Extensions.Azure; /// @@ -18,6 +20,11 @@ public static class DurableTaskSchedulerExtensions /// /// Configures Durable Task worker to use the Azure Durable Task Scheduler service. /// + /// The Durable Task worker builder to configure. + /// The endpoint address of the Durable Task Scheduler resource. Expected to be in the format "https://{scheduler-name}.{region}.durabletask.io". + /// The name of the task hub resource associated with the Durable Task Scheduler resource. + /// The credential used to authenticate with the Durable Task Scheduler task hub resource. + /// Optional callback to dynamically configure DurableTaskSchedulerOptions. public static void UseDurableTaskScheduler( this IDurableTaskWorkerBuilder builder, string endpointAddress, @@ -37,13 +44,16 @@ public static void UseDurableTaskScheduler( .ValidateOnStart(); builder.Services.TryAddEnumerable( - ServiceDescription.Singleton, ConfigureGrpcChannel>()); + ServiceDescriptor.Singleton, ConfigureGrpcChannel>()); builder.UseGrpc(_ => { }); } /// /// Configures Durable Task worker to use the Azure Durable Task Scheduler service using a connection string. /// + /// The Durable Task worker builder to configure. + /// The connection string used to connect to the Durable Task Scheduler service. + /// Optional callback to dynamically configure DurableTaskSchedulerOptions. public static void UseDurableTaskScheduler( this IDurableTaskWorkerBuilder builder, string connectionString, @@ -63,13 +73,18 @@ public static void UseDurableTaskScheduler( .ValidateOnStart(); builder.Services.TryAddEnumerable( - ServiceDescription.Singleton, ConfigureGrpcChannel>()); + ServiceDescriptor.Singleton, ConfigureGrpcChannel>()); builder.UseGrpc(_ => { }); } /// /// Configures Durable Task client to use the Azure Durable Task Scheduler service. /// + /// The Durable Task client builder to configure. + /// The endpoint address of the Durable Task Scheduler resource. Expected to be in the format "https://{scheduler-name}.{region}.durabletask.io". + /// The name of the task hub resource associated with the Durable Task Scheduler resource. + /// The credential used to authenticate with the Durable Task Scheduler task hub resource. + /// Optional callback to dynamically configure DurableTaskSchedulerOptions. public static void UseDurableTaskScheduler( this IDurableTaskClientBuilder builder, string endpointAddress, @@ -89,13 +104,16 @@ public static void UseDurableTaskScheduler( .ValidateOnStart(); builder.Services.TryAddEnumerable( - ServiceDescription.Singleton, ConfigureGrpcChannel>()); + ServiceDescriptor.Singleton, ConfigureGrpcChannel>()); builder.UseGrpc(_ => { }); } /// /// Configures Durable Task client to use the Azure Durable Task Scheduler service using a connection string. /// + /// The Durable Task client builder to configure. + /// The connection string used to connect to the Durable Task Scheduler service. + /// Optional callback to dynamically configure DurableTaskSchedulerOptions. public static void UseDurableTaskScheduler( this IDurableTaskClientBuilder builder, string connectionString, @@ -109,19 +127,28 @@ public static void UseDurableTaskScheduler( .ValidateOnStart(); builder.Services.TryAddEnumerable( - ServiceDescription.Singleton, ConfigureGrpcChannel>()); + ServiceDescriptor.Singleton, ConfigureGrpcChannel>()); builder.UseGrpc(_ => { }); } - // helper internal class - class ConfigureGrpcChannel(IOptionsMonitor schedulerOptions) : IConfigureNamedOptions + class ConfigureGrpcChannel(IOptionsMonitor schedulerOptions) : + IConfigureNamedOptions, + IConfigureNamedOptions { public void Configure(GrpcDurableTaskWorkerOptions options) => this.Configure(Options.DefaultName, options); - public void Configure(string name, GrpcDurableTaskWorkerOptions options) + public void Configure(GrpcDurableTaskClientOptions options) => this.Configure(Options.DefaultName, options); + + public void Configure(string? name, GrpcDurableTaskWorkerOptions options) + { + DurableTaskSchedulerOptions source = schedulerOptions.Get(name ?? Options.DefaultName); + options.Channel = source.CreateChannel(); + } + + public void Configure(string? name, GrpcDurableTaskClientOptions options) { - DurableTaskSchedulerOptions source = schedulerOptions.Get(name); + DurableTaskSchedulerOptions source = schedulerOptions.Get(name ?? Options.DefaultName); options.Channel = source.CreateChannel(); } } -} \ No newline at end of file +} From 2decb7940e4ad80164cf33f4d5c0d00fd615410a Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Sat, 11 Jan 2025 09:07:23 -0800 Subject: [PATCH 30/33] save --- src/Extensions/Azure/DurableTaskSchedulerExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs b/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs index bc1e2374..0be711d4 100644 --- a/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs +++ b/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs @@ -131,7 +131,7 @@ public static void UseDurableTaskScheduler( builder.UseGrpc(_ => { }); } - class ConfigureGrpcChannel(IOptionsMonitor schedulerOptions) : + internal class ConfigureGrpcChannel(IOptionsMonitor schedulerOptions) : IConfigureNamedOptions, IConfigureNamedOptions { From ee295d0a0119d5acfc63e15391e4e14d884e453e Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Sat, 11 Jan 2025 09:14:32 -0800 Subject: [PATCH 31/33] save --- .../Azure/DurableTaskSchedulerExtensions.cs | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs b/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs index 0be711d4..aabc23e1 100644 --- a/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs +++ b/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs @@ -131,20 +131,43 @@ public static void UseDurableTaskScheduler( builder.UseGrpc(_ => { }); } + /// + /// Internal configuration class that sets up gRPC channels for both worker and client options + /// using the provided Durable Task Scheduler options. + /// + /// Monitor for accessing the current scheduler options configuration. internal class ConfigureGrpcChannel(IOptionsMonitor schedulerOptions) : IConfigureNamedOptions, IConfigureNamedOptions { + /// + /// Configures worker options using the default options name. + /// + /// The worker options to configure. public void Configure(GrpcDurableTaskWorkerOptions options) => this.Configure(Options.DefaultName, options); + /// + /// Configures client options using the default options name. + /// + /// The client options to configure. public void Configure(GrpcDurableTaskClientOptions options) => this.Configure(Options.DefaultName, options); + /// + /// Configures named worker options by creating and assigning a gRPC channel. + /// + /// The name of the options instance being configured, or null for the default instance. + /// The worker options to configure. public void Configure(string? name, GrpcDurableTaskWorkerOptions options) { DurableTaskSchedulerOptions source = schedulerOptions.Get(name ?? Options.DefaultName); options.Channel = source.CreateChannel(); } + /// + /// Configures named client options by creating and assigning a gRPC channel. + /// + /// The name of the options instance being configured, or null for the default instance. + /// The client options to configure. public void Configure(string? name, GrpcDurableTaskClientOptions options) { DurableTaskSchedulerOptions source = schedulerOptions.Get(name ?? Options.DefaultName); From 522d4b01c45667ba6513ff4398d0a86b4f65bcd5 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Sat, 11 Jan 2025 09:38:19 -0800 Subject: [PATCH 32/33] fix --- .../Azure/DurableTaskSchedulerExtensions.cs | 22 ++----------------- 1 file changed, 2 insertions(+), 20 deletions(-) diff --git a/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs b/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs index aabc23e1..2be74649 100644 --- a/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs +++ b/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs @@ -92,7 +92,7 @@ public static void UseDurableTaskScheduler( TokenCredential credential, Action? configure = null) { - builder.Services.AddOptions(Options.DefaultName) + builder.Services.AddOptions(builder.Name) .Configure(options => { options.EndpointAddress = endpointAddress; @@ -121,7 +121,7 @@ public static void UseDurableTaskScheduler( { var connectionOptions = DurableTaskSchedulerOptions.FromConnectionString(connectionString); - builder.Services.AddOptions(Options.DefaultName) + builder.Services.AddOptions(builder.Name) .Configure(configure ?? (_ => { })) .ValidateDataAnnotations() .ValidateOnStart(); @@ -140,34 +140,16 @@ internal class ConfigureGrpcChannel(IOptionsMonitor IConfigureNamedOptions, IConfigureNamedOptions { - /// - /// Configures worker options using the default options name. - /// - /// The worker options to configure. public void Configure(GrpcDurableTaskWorkerOptions options) => this.Configure(Options.DefaultName, options); - /// - /// Configures client options using the default options name. - /// - /// The client options to configure. public void Configure(GrpcDurableTaskClientOptions options) => this.Configure(Options.DefaultName, options); - /// - /// Configures named worker options by creating and assigning a gRPC channel. - /// - /// The name of the options instance being configured, or null for the default instance. - /// The worker options to configure. public void Configure(string? name, GrpcDurableTaskWorkerOptions options) { DurableTaskSchedulerOptions source = schedulerOptions.Get(name ?? Options.DefaultName); options.Channel = source.CreateChannel(); } - /// - /// Configures named client options by creating and assigning a gRPC channel. - /// - /// The name of the options instance being configured, or null for the default instance. - /// The client options to configure. public void Configure(string? name, GrpcDurableTaskClientOptions options) { DurableTaskSchedulerOptions source = schedulerOptions.Get(name ?? Options.DefaultName); From d289c10a51176df1110312bf6ee058f87c710da7 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Sat, 11 Jan 2025 09:45:38 -0800 Subject: [PATCH 33/33] fix --- src/Extensions/Azure/DurableTaskSchedulerExtensions.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs b/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs index 2be74649..6692a6a3 100644 --- a/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs +++ b/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs @@ -122,6 +122,12 @@ public static void UseDurableTaskScheduler( var connectionOptions = DurableTaskSchedulerOptions.FromConnectionString(connectionString); builder.Services.AddOptions(builder.Name) + .Configure(options => + { + options.EndpointAddress = connectionOptions.EndpointAddress; + options.TaskHubName = connectionOptions.TaskHubName; + options.Credential = connectionOptions.Credential; + }) .Configure(configure ?? (_ => { })) .ValidateDataAnnotations() .ValidateOnStart();