From 025547553b49b7167f7d9548184157c1e305ec75 Mon Sep 17 00:00:00 2001 From: "naiyuantian@microsoft.com" Date: Mon, 7 Oct 2024 15:07:57 -0700 Subject: [PATCH 01/12] initial commit --- .../Bindings/BindingHelper.cs | 1 + .../DurableTaskClientConverter.cs | 4 +- .../DurableTaskClientExtensions.cs | 57 +++++++++++++++++-- .../FunctionsDurableTaskClient.cs | 5 +- .../FunctionsDurableTaskClientTests.cs | 31 +++++++++- 5 files changed, 87 insertions(+), 11 deletions(-) diff --git a/src/WebJobs.Extensions.DurableTask/Bindings/BindingHelper.cs b/src/WebJobs.Extensions.DurableTask/Bindings/BindingHelper.cs index a0c2fddde..2fde62e5a 100644 --- a/src/WebJobs.Extensions.DurableTask/Bindings/BindingHelper.cs +++ b/src/WebJobs.Extensions.DurableTask/Bindings/BindingHelper.cs @@ -43,6 +43,7 @@ public string DurableOrchestrationClientToString(IDurableOrchestrationClient cli ConnectionName = attr.ConnectionName, RpcBaseUrl = localRpcAddress, RequiredQueryStringParameters = this.config.HttpApiHandler.GetUniversalQueryStrings(), + BaseUrl = this.config.HttpApiHandler.GetBaseUrl(), }); } diff --git a/src/Worker.Extensions.DurableTask/DurableTaskClientConverter.cs b/src/Worker.Extensions.DurableTask/DurableTaskClientConverter.cs index 1d3da9003..c21b7456d 100644 --- a/src/Worker.Extensions.DurableTask/DurableTaskClientConverter.cs +++ b/src/Worker.Extensions.DurableTask/DurableTaskClientConverter.cs @@ -49,7 +49,7 @@ public ValueTask ConvertAsync(ConverterContext context) } DurableTaskClient client = this.clientProvider.GetClient(endpoint, inputData?.taskHubName, inputData?.connectionName); - client = new FunctionsDurableTaskClient(client, inputData!.requiredQueryStringParameters); + client = new FunctionsDurableTaskClient(client, inputData!.requiredQueryStringParameters, inputData!.baseUrl); return new ValueTask(ConversionResult.Success(client)); } catch (Exception innerException) @@ -62,5 +62,5 @@ public ValueTask ConvertAsync(ConverterContext context) } // Serializer is case-sensitive and incoming JSON properties are camel-cased. - private record DurableClientInputData(string rpcBaseUrl, string taskHubName, string connectionName, string requiredQueryStringParameters); + private record DurableClientInputData(string rpcBaseUrl, string taskHubName, string connectionName, string requiredQueryStringParameters, string baseUrl); } diff --git a/src/Worker.Extensions.DurableTask/DurableTaskClientExtensions.cs b/src/Worker.Extensions.DurableTask/DurableTaskClientExtensions.cs index 286c206fe..609bc1af2 100644 --- a/src/Worker.Extensions.DurableTask/DurableTaskClientExtensions.cs +++ b/src/Worker.Extensions.DurableTask/DurableTaskClientExtensions.cs @@ -120,8 +120,37 @@ public static HttpResponseData CreateCheckStatusResponse( return response; } + /// + /// Creates an HTTP management payload for the specified orchestration instance. + /// + /// The . + /// The ID of the orchestration instance. + /// Optional HTTP request data to use for creating the base URL. + /// An object containing instance control URLs. + /// Thrown when instanceId is null or empty. + /// Thrown when a valid base URL cannot be determined. + public static object CreateHttpManagementPayload( + this DurableTaskClient client, + string instanceId, + HttpRequestData? request = null) + { + if (string.IsNullOrEmpty(instanceId)) + { + throw new ArgumentException("InstanceId cannot be null or empty.", nameof(instanceId)); + } + + try + { + return SetHeadersAndGetPayload(client, request, null, instanceId); + } + catch (InvalidOperationException ex) + { + throw new InvalidOperationException("Failed to create HTTP management payload. " + ex.Message, ex); + } + } + private static object SetHeadersAndGetPayload( - DurableTaskClient client, HttpRequestData request, HttpResponseData response, string instanceId) + DurableTaskClient client, HttpRequestData? request, HttpResponseData? response, string instanceId) { static string BuildUrl(string url, params string?[] queryValues) { @@ -143,12 +172,25 @@ static string BuildUrl(string url, params string?[] queryValues) // request headers into consideration and generate the base URL accordingly. // More info: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Forwarded. // One potential workaround is to set ASPNETCORE_FORWARDEDHEADERS_ENABLED to true. - string baseUrl = request.Url.GetLeftPart(UriPartial.Authority); + string? baseUrl = (request != null) ? request.Url.GetLeftPart(UriPartial.Authority) : GetBaseUrl(client); + bool isFromRequest = request != null; + + if (baseUrl == null) + { + throw new InvalidOperationException("Base URL is null. Either use Functions bindings or provide an HTTP request to create the HttpPayload."); + } + string formattedInstanceId = Uri.EscapeDataString(instanceId); - string instanceUrl = $"{baseUrl}/runtime/webhooks/durabletask/instances/{formattedInstanceId}"; + string instanceUrl = isFromRequest + ? $"{baseUrl}/runtime/webhooks/durabletask/instances/{formattedInstanceId}" + : $"{baseUrl}/instances/{formattedInstanceId}"; string? commonQueryParameters = GetQueryParams(client); - response.Headers.Add("Location", BuildUrl(instanceUrl, commonQueryParameters)); - response.Headers.Add("Content-Type", "application/json"); + + if (response != null) + { + response.Headers.Add("Location", BuildUrl(instanceUrl, commonQueryParameters)); + response.Headers.Add("Content-Type", "application/json"); + } return new { @@ -172,4 +214,9 @@ private static ObjectSerializer GetObjectSerializer(HttpResponseData response) { return client is FunctionsDurableTaskClient functions ? functions.QueryString : null; } + + private static string? GetBaseUrl(DurableTaskClient client) + { + return client is FunctionsDurableTaskClient functions ? functions.BaseUrl : null; + } } diff --git a/src/Worker.Extensions.DurableTask/FunctionsDurableTaskClient.cs b/src/Worker.Extensions.DurableTask/FunctionsDurableTaskClient.cs index 0f9231375..8cc2d6801 100644 --- a/src/Worker.Extensions.DurableTask/FunctionsDurableTaskClient.cs +++ b/src/Worker.Extensions.DurableTask/FunctionsDurableTaskClient.cs @@ -17,15 +17,16 @@ internal sealed class FunctionsDurableTaskClient : DurableTaskClient { private readonly DurableTaskClient inner; - public FunctionsDurableTaskClient(DurableTaskClient inner, string? queryString) + public FunctionsDurableTaskClient(DurableTaskClient inner, string? queryString, string? baseUrl) : base(inner.Name) { this.inner = inner; this.QueryString = queryString; + this.BaseUrl = baseUrl; } public string? QueryString { get; } - + public string? BaseUrl { get; } public override DurableEntityClient Entities => this.inner.Entities; public override ValueTask DisposeAsync() diff --git a/test/Worker.Extensions.DurableTask.Tests/FunctionsDurableTaskClientTests.cs b/test/Worker.Extensions.DurableTask.Tests/FunctionsDurableTaskClientTests.cs index 5a335aefa..34548a384 100644 --- a/test/Worker.Extensions.DurableTask.Tests/FunctionsDurableTaskClientTests.cs +++ b/test/Worker.Extensions.DurableTask.Tests/FunctionsDurableTaskClientTests.cs @@ -1,3 +1,4 @@ +using Microsoft.Azure.Functions.Worker.Http; using Microsoft.DurableTask.Client; using Microsoft.DurableTask.Client.Grpc; using Moq; @@ -9,7 +10,7 @@ namespace Microsoft.Azure.Functions.Worker.Tests /// public class FunctionsDurableTaskClientTests { - private FunctionsDurableTaskClient GetTestFunctionsDurableTaskClient() + private FunctionsDurableTaskClient GetTestFunctionsDurableTaskClient(string baseUrl = null) { // construct mock client @@ -22,7 +23,7 @@ private FunctionsDurableTaskClient GetTestFunctionsDurableTaskClient() It.IsAny(), It.IsAny(), It.IsAny())).Returns(completedTask); DurableTaskClient durableClient = durableClientMock.Object; - FunctionsDurableTaskClient client = new FunctionsDurableTaskClient(durableClient, queryString: null); + FunctionsDurableTaskClient client = new FunctionsDurableTaskClient(durableClient, queryString: null, baseUrl: baseUrl); return client; } @@ -53,5 +54,31 @@ public async void TerminateDoesNotThrow() await client.TerminateInstanceAsync(instanceId, options); await client.TerminateInstanceAsync(instanceId, options, token); } + + /// + /// Test that the `CreateHttpManagementPayload` method returns the expected payload structure without HttpRequestData. + /// + [Fact] + public void CreateHttpManagementPayload_WithBaseUrl_ReturnsExpectedStructure() + { + string BaseUrl = "http://localhost:7071/runtime/webhooks/durabletask"; + FunctionsDurableTaskClient client = this.GetTestFunctionsDurableTaskClient(BaseUrl); + string instanceId = "testInstanceId"; + + dynamic payload = client.CreateHttpManagementPayload(instanceId); + + AssertHttpManagementPayload(payload, BaseUrl, instanceId); + } + + private static void AssertHttpManagementPayload(dynamic payload, string BaseUrl, string instanceId) + { + Assert.Equal(instanceId, payload.id); + Assert.Equal($"{BaseUrl}/instances/{instanceId}", payload.purgeHistoryDeleteUri); + Assert.Equal($"{BaseUrl}/instances/{instanceId}/raiseEvent/{{eventName}}", payload.sendEventPostUri); + Assert.Equal($"{BaseUrl}/instances/{instanceId}", payload.statusQueryGetUri); + Assert.Equal($"{BaseUrl}/instances/{instanceId}/terminate?reason={{{{text}}}}", payload.terminatePostUri); + Assert.Equal($"{BaseUrl}/instances/{instanceId}/suspend?reason={{{{text}}}}", payload.suspendPostUri); + Assert.Equal($"{BaseUrl}/instances/{instanceId}/resume?reason={{{{text}}}}", payload.resumePostUri); + } } } \ No newline at end of file From 48df767206daab5b69c668f66c8174d166336a65 Mon Sep 17 00:00:00 2001 From: "naiyuantian@microsoft.com" Date: Tue, 8 Oct 2024 09:53:00 -0700 Subject: [PATCH 02/12] add comment --- .../DurableTaskClientExtensions.cs | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/Worker.Extensions.DurableTask/DurableTaskClientExtensions.cs b/src/Worker.Extensions.DurableTask/DurableTaskClientExtensions.cs index 609bc1af2..4ac3fc041 100644 --- a/src/Worker.Extensions.DurableTask/DurableTaskClientExtensions.cs +++ b/src/Worker.Extensions.DurableTask/DurableTaskClientExtensions.cs @@ -172,15 +172,21 @@ static string BuildUrl(string url, params string?[] queryValues) // request headers into consideration and generate the base URL accordingly. // More info: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Forwarded. // One potential workaround is to set ASPNETCORE_FORWARDEDHEADERS_ENABLED to true. - string? baseUrl = (request != null) ? request.Url.GetLeftPart(UriPartial.Authority) : GetBaseUrl(client); + + // If HttpRequestData is provided, use its URL; otherwise, get the baseUrl from the DurableTaskClient. + // The base URL could be null if: + // 1. The DurableTaskClient isn't a FunctionsDurableTaskClient (which would have the baseUrl from bindings) + // 2. There's no valid HttpRequestData provided + string? baseUrl = ((request != null) ? request.Url.GetLeftPart(UriPartial.Authority) : GetBaseUrl(client)) + ?? throw new InvalidOperationException("Base URL is null. Either use Functions bindings or provide an HTTP request to create the HttpPayload."); bool isFromRequest = request != null; - - if (baseUrl == null) - { - throw new InvalidOperationException("Base URL is null. Either use Functions bindings or provide an HTTP request to create the HttpPayload."); - } string formattedInstanceId = Uri.EscapeDataString(instanceId); + + // The baseUrl differs depending on the source. Eg: + // - From request: http://localhost:7071/ + // - From durable client: http://localhost:7071/runtime/webhooks/durabletask + // We adjust the instanceUrl construction accordingly. string instanceUrl = isFromRequest ? $"{baseUrl}/runtime/webhooks/durabletask/instances/{formattedInstanceId}" : $"{baseUrl}/instances/{formattedInstanceId}"; From 376fb695f8683a2ff1ce08568707d8b4fd168ec1 Mon Sep 17 00:00:00 2001 From: "naiyuantian@microsoft.com" Date: Tue, 8 Oct 2024 13:25:18 -0700 Subject: [PATCH 03/12] add test --- .../FunctionsDurableTaskClientTests.cs | 69 ++++++++++++++++++- 1 file changed, 66 insertions(+), 3 deletions(-) diff --git a/test/Worker.Extensions.DurableTask.Tests/FunctionsDurableTaskClientTests.cs b/test/Worker.Extensions.DurableTask.Tests/FunctionsDurableTaskClientTests.cs index 34548a384..296cfc28c 100644 --- a/test/Worker.Extensions.DurableTask.Tests/FunctionsDurableTaskClientTests.cs +++ b/test/Worker.Extensions.DurableTask.Tests/FunctionsDurableTaskClientTests.cs @@ -1,3 +1,4 @@ +using System.Security.Claims; using Microsoft.Azure.Functions.Worker.Http; using Microsoft.DurableTask.Client; using Microsoft.DurableTask.Client.Grpc; @@ -10,7 +11,7 @@ namespace Microsoft.Azure.Functions.Worker.Tests /// public class FunctionsDurableTaskClientTests { - private FunctionsDurableTaskClient GetTestFunctionsDurableTaskClient(string baseUrl = null) + private FunctionsDurableTaskClient GetTestFunctionsDurableTaskClient(string? baseUrl = null) { // construct mock client @@ -59,17 +60,34 @@ public async void TerminateDoesNotThrow() /// Test that the `CreateHttpManagementPayload` method returns the expected payload structure without HttpRequestData. /// [Fact] - public void CreateHttpManagementPayload_WithBaseUrl_ReturnsExpectedStructure() + public void CreateHttpManagementPayload_WithBaseUrl() { string BaseUrl = "http://localhost:7071/runtime/webhooks/durabletask"; FunctionsDurableTaskClient client = this.GetTestFunctionsDurableTaskClient(BaseUrl); - string instanceId = "testInstanceId"; + string instanceId = "testInstanceIdWithHostBaseUrl"; dynamic payload = client.CreateHttpManagementPayload(instanceId); AssertHttpManagementPayload(payload, BaseUrl, instanceId); } + /// + /// Test that the `CreateHttpManagementPayload` method returns the expected payload structure with HttpRequestData. + /// + [Fact] + public void CreateHttpManagementPayload_WithHttpRequestData() + { + string requestUrl = "http://localhost:7075/orchestrators/E1_HelloSequence"; + FunctionsDurableTaskClient client = this.GetTestFunctionsDurableTaskClient(); + string instanceId = "testInstanceIdWithRequest"; + var context = new TestFunctionContext(); + var request = new TestHttpRequestData(context, new Uri(requestUrl)); + + dynamic payload = client.CreateHttpManagementPayload(instanceId, request); + + AssertHttpManagementPayload(payload, "http://localhost:7075/runtime/webhooks/durabletask", instanceId); + } + private static void AssertHttpManagementPayload(dynamic payload, string BaseUrl, string instanceId) { Assert.Equal(instanceId, payload.id); @@ -81,4 +99,49 @@ private static void AssertHttpManagementPayload(dynamic payload, string BaseUrl, Assert.Equal($"{BaseUrl}/instances/{instanceId}/resume?reason={{{{text}}}}", payload.resumePostUri); } } + + /// + /// A minimal implementation of FunctionContext for testing purposes. + /// It's used to create a TestHttpRequestData instance, which requires a FunctionContext. + /// + public class TestFunctionContext : FunctionContext + { + public override string InvocationId => throw new NotImplementedException(); + public override string FunctionId => throw new NotImplementedException(); + public override TraceContext TraceContext => throw new NotImplementedException(); + public override BindingContext BindingContext => throw new NotImplementedException(); + public override RetryContext RetryContext => throw new NotImplementedException(); + public override IServiceProvider InstanceServices { get; set; } = new EmptyServiceProvider(); + public override FunctionDefinition FunctionDefinition => throw new NotImplementedException(); + public override IDictionary Items { get; set; } = new Dictionary(); + public override IInvocationFeatures Features => throw new NotImplementedException(); + } + + /// + /// A minimal implementation of IServiceProvider for testing purposes. + /// + public class EmptyServiceProvider : IServiceProvider + { + public object GetService(Type serviceType) => null; + } + + // + /// A test implementation of HttpRequestData used for unit testing. + /// This class allows us to create instances of HttpRequestData, which is normally abstract. + /// + public class TestHttpRequestData : HttpRequestData + { + public TestHttpRequestData(FunctionContext functionContext, Uri url) : base(functionContext) + { + Url = url; + } + + public override Stream Body => throw new NotImplementedException(); + public override HttpHeadersCollection Headers => throw new NotImplementedException(); + public override IReadOnlyCollection Cookies => throw new NotImplementedException(); + public override Uri Url { get; } + public override IEnumerable Identities => throw new NotImplementedException(); + public override string Method => throw new NotImplementedException(); + public override HttpResponseData CreateResponse() => throw new NotImplementedException(); + } } \ No newline at end of file From 221138f0368324597adf8b7260a51bdb56c306a4 Mon Sep 17 00:00:00 2001 From: "naiyuantian@microsoft.com" Date: Tue, 8 Oct 2024 15:59:59 -0700 Subject: [PATCH 04/12] update by comment --- .../Bindings/BindingHelper.cs | 10 ++- ...t.Azure.WebJobs.Extensions.DurableTask.xml | 7 +++ .../DurableTaskClientConverter.cs | 4 +- .../DurableTaskClientExtensions.cs | 2 +- .../FunctionsDurableTaskClient.cs | 6 +- .../FunctionsDurableTaskClientTests.cs | 62 +++---------------- 6 files changed, 31 insertions(+), 60 deletions(-) diff --git a/src/WebJobs.Extensions.DurableTask/Bindings/BindingHelper.cs b/src/WebJobs.Extensions.DurableTask/Bindings/BindingHelper.cs index 2fde62e5a..b0b7bcdc1 100644 --- a/src/WebJobs.Extensions.DurableTask/Bindings/BindingHelper.cs +++ b/src/WebJobs.Extensions.DurableTask/Bindings/BindingHelper.cs @@ -43,7 +43,7 @@ public string DurableOrchestrationClientToString(IDurableOrchestrationClient cli ConnectionName = attr.ConnectionName, RpcBaseUrl = localRpcAddress, RequiredQueryStringParameters = this.config.HttpApiHandler.GetUniversalQueryStrings(), - BaseUrl = this.config.HttpApiHandler.GetBaseUrl(), + HttpBaseUrl = this.config.HttpApiHandler.GetBaseUrl(), }); } @@ -131,6 +131,14 @@ private class OrchestrationClientInputData /// [JsonProperty("rpcBaseUrl")] public string? RpcBaseUrl { get; set; } + + /// + /// The base URL of the Azure Functions host, used in the out-of-proc model. + /// This URL is sent by the client binding object to the Durable Worker extension, + /// allowing the extension to know the host's base URL for constructing complete URLs. + /// + [JsonProperty("HttpBaseUrl")] + public string? HttpBaseUrl { get; set; } } } } diff --git a/src/WebJobs.Extensions.DurableTask/Microsoft.Azure.WebJobs.Extensions.DurableTask.xml b/src/WebJobs.Extensions.DurableTask/Microsoft.Azure.WebJobs.Extensions.DurableTask.xml index 8faf3dfff..088be7969 100644 --- a/src/WebJobs.Extensions.DurableTask/Microsoft.Azure.WebJobs.Extensions.DurableTask.xml +++ b/src/WebJobs.Extensions.DurableTask/Microsoft.Azure.WebJobs.Extensions.DurableTask.xml @@ -93,6 +93,13 @@ HTTP endpoint. For out-of-proc "v2" (middelware passthrough), this is a gRPC endpoint. + + + The base URL of the Azure Functions host, used in the out-of-proc model. + This URL is sent by the client binding object to the Durable Worker extension, + allowing the extension to know the host's base URL for constructing complete URLs. + + The result of a clean entity storage operation. diff --git a/src/Worker.Extensions.DurableTask/DurableTaskClientConverter.cs b/src/Worker.Extensions.DurableTask/DurableTaskClientConverter.cs index c21b7456d..2cfc2706e 100644 --- a/src/Worker.Extensions.DurableTask/DurableTaskClientConverter.cs +++ b/src/Worker.Extensions.DurableTask/DurableTaskClientConverter.cs @@ -49,7 +49,7 @@ public ValueTask ConvertAsync(ConverterContext context) } DurableTaskClient client = this.clientProvider.GetClient(endpoint, inputData?.taskHubName, inputData?.connectionName); - client = new FunctionsDurableTaskClient(client, inputData!.requiredQueryStringParameters, inputData!.baseUrl); + client = new FunctionsDurableTaskClient(client, inputData!.requiredQueryStringParameters, inputData!.httpBaseUrl); return new ValueTask(ConversionResult.Success(client)); } catch (Exception innerException) @@ -62,5 +62,5 @@ public ValueTask ConvertAsync(ConverterContext context) } // Serializer is case-sensitive and incoming JSON properties are camel-cased. - private record DurableClientInputData(string rpcBaseUrl, string taskHubName, string connectionName, string requiredQueryStringParameters, string baseUrl); + private record DurableClientInputData(string rpcBaseUrl, string taskHubName, string connectionName, string requiredQueryStringParameters, string httpBaseUrl); } diff --git a/src/Worker.Extensions.DurableTask/DurableTaskClientExtensions.cs b/src/Worker.Extensions.DurableTask/DurableTaskClientExtensions.cs index 4ac3fc041..baf1088a3 100644 --- a/src/Worker.Extensions.DurableTask/DurableTaskClientExtensions.cs +++ b/src/Worker.Extensions.DurableTask/DurableTaskClientExtensions.cs @@ -223,6 +223,6 @@ private static ObjectSerializer GetObjectSerializer(HttpResponseData response) private static string? GetBaseUrl(DurableTaskClient client) { - return client is FunctionsDurableTaskClient functions ? functions.BaseUrl : null; + return client is FunctionsDurableTaskClient functions ? functions.HttpBaseUrl : null; } } diff --git a/src/Worker.Extensions.DurableTask/FunctionsDurableTaskClient.cs b/src/Worker.Extensions.DurableTask/FunctionsDurableTaskClient.cs index 8cc2d6801..3c919362d 100644 --- a/src/Worker.Extensions.DurableTask/FunctionsDurableTaskClient.cs +++ b/src/Worker.Extensions.DurableTask/FunctionsDurableTaskClient.cs @@ -17,16 +17,16 @@ internal sealed class FunctionsDurableTaskClient : DurableTaskClient { private readonly DurableTaskClient inner; - public FunctionsDurableTaskClient(DurableTaskClient inner, string? queryString, string? baseUrl) + public FunctionsDurableTaskClient(DurableTaskClient inner, string? queryString, string? httpBaseUrl) : base(inner.Name) { this.inner = inner; this.QueryString = queryString; - this.BaseUrl = baseUrl; + this.HttpBaseUrl = httpBaseUrl; } public string? QueryString { get; } - public string? BaseUrl { get; } + public string? HttpBaseUrl { get; } public override DurableEntityClient Entities => this.inner.Entities; public override ValueTask DisposeAsync() diff --git a/test/Worker.Extensions.DurableTask.Tests/FunctionsDurableTaskClientTests.cs b/test/Worker.Extensions.DurableTask.Tests/FunctionsDurableTaskClientTests.cs index 296cfc28c..a0f90239b 100644 --- a/test/Worker.Extensions.DurableTask.Tests/FunctionsDurableTaskClientTests.cs +++ b/test/Worker.Extensions.DurableTask.Tests/FunctionsDurableTaskClientTests.cs @@ -1,7 +1,5 @@ -using System.Security.Claims; using Microsoft.Azure.Functions.Worker.Http; using Microsoft.DurableTask.Client; -using Microsoft.DurableTask.Client.Grpc; using Moq; namespace Microsoft.Azure.Functions.Worker.Tests @@ -24,7 +22,7 @@ private FunctionsDurableTaskClient GetTestFunctionsDurableTaskClient(string? bas It.IsAny(), It.IsAny(), It.IsAny())).Returns(completedTask); DurableTaskClient durableClient = durableClientMock.Object; - FunctionsDurableTaskClient client = new FunctionsDurableTaskClient(durableClient, queryString: null, baseUrl: baseUrl); + FunctionsDurableTaskClient client = new FunctionsDurableTaskClient(durableClient, queryString: null, httpBaseUrl: baseUrl); return client; } @@ -62,7 +60,7 @@ public async void TerminateDoesNotThrow() [Fact] public void CreateHttpManagementPayload_WithBaseUrl() { - string BaseUrl = "http://localhost:7071/runtime/webhooks/durabletask"; + const string BaseUrl = "http://localhost:7071/runtime/webhooks/durabletask"; FunctionsDurableTaskClient client = this.GetTestFunctionsDurableTaskClient(BaseUrl); string instanceId = "testInstanceIdWithHostBaseUrl"; @@ -77,13 +75,16 @@ public void CreateHttpManagementPayload_WithBaseUrl() [Fact] public void CreateHttpManagementPayload_WithHttpRequestData() { - string requestUrl = "http://localhost:7075/orchestrators/E1_HelloSequence"; + const string requestUrl = "http://localhost:7075/orchestrators/E1_HelloSequence"; FunctionsDurableTaskClient client = this.GetTestFunctionsDurableTaskClient(); string instanceId = "testInstanceIdWithRequest"; - var context = new TestFunctionContext(); - var request = new TestHttpRequestData(context, new Uri(requestUrl)); - dynamic payload = client.CreateHttpManagementPayload(instanceId, request); + // Create mock HttpRequestData object. + var mockFunctionContext = new Mock(); + var mockHttpRequestData = new Mock(mockFunctionContext.Object); + mockHttpRequestData.SetupGet(r => r.Url).Returns(new Uri(requestUrl)); + + dynamic payload = client.CreateHttpManagementPayload(instanceId, mockHttpRequestData.Object); AssertHttpManagementPayload(payload, "http://localhost:7075/runtime/webhooks/durabletask", instanceId); } @@ -99,49 +100,4 @@ private static void AssertHttpManagementPayload(dynamic payload, string BaseUrl, Assert.Equal($"{BaseUrl}/instances/{instanceId}/resume?reason={{{{text}}}}", payload.resumePostUri); } } - - /// - /// A minimal implementation of FunctionContext for testing purposes. - /// It's used to create a TestHttpRequestData instance, which requires a FunctionContext. - /// - public class TestFunctionContext : FunctionContext - { - public override string InvocationId => throw new NotImplementedException(); - public override string FunctionId => throw new NotImplementedException(); - public override TraceContext TraceContext => throw new NotImplementedException(); - public override BindingContext BindingContext => throw new NotImplementedException(); - public override RetryContext RetryContext => throw new NotImplementedException(); - public override IServiceProvider InstanceServices { get; set; } = new EmptyServiceProvider(); - public override FunctionDefinition FunctionDefinition => throw new NotImplementedException(); - public override IDictionary Items { get; set; } = new Dictionary(); - public override IInvocationFeatures Features => throw new NotImplementedException(); - } - - /// - /// A minimal implementation of IServiceProvider for testing purposes. - /// - public class EmptyServiceProvider : IServiceProvider - { - public object GetService(Type serviceType) => null; - } - - // - /// A test implementation of HttpRequestData used for unit testing. - /// This class allows us to create instances of HttpRequestData, which is normally abstract. - /// - public class TestHttpRequestData : HttpRequestData - { - public TestHttpRequestData(FunctionContext functionContext, Uri url) : base(functionContext) - { - Url = url; - } - - public override Stream Body => throw new NotImplementedException(); - public override HttpHeadersCollection Headers => throw new NotImplementedException(); - public override IReadOnlyCollection Cookies => throw new NotImplementedException(); - public override Uri Url { get; } - public override IEnumerable Identities => throw new NotImplementedException(); - public override string Method => throw new NotImplementedException(); - public override HttpResponseData CreateResponse() => throw new NotImplementedException(); - } } \ No newline at end of file From 33dbd20fb76e85eef4643964c259f24dd24d10ca Mon Sep 17 00:00:00 2001 From: "naiyuantian@microsoft.com" Date: Mon, 21 Oct 2024 12:13:24 -0700 Subject: [PATCH 05/12] add httpmanagementpayload class --- .../Bindings/BindingHelper.cs | 2 +- ...t.Azure.WebJobs.Extensions.DurableTask.xml | 2 +- .../DurableTaskClientExtensions.cs | 20 ++-- .../HttpManagementPayload.cs | 96 +++++++++++++++++++ .../FunctionsDurableTaskClientTests.cs | 14 +-- 5 files changed, 115 insertions(+), 19 deletions(-) create mode 100644 src/Worker.Extensions.DurableTask/HttpManagementPayload.cs diff --git a/src/WebJobs.Extensions.DurableTask/Bindings/BindingHelper.cs b/src/WebJobs.Extensions.DurableTask/Bindings/BindingHelper.cs index b0b7bcdc1..b26d90bb1 100644 --- a/src/WebJobs.Extensions.DurableTask/Bindings/BindingHelper.cs +++ b/src/WebJobs.Extensions.DurableTask/Bindings/BindingHelper.cs @@ -135,7 +135,7 @@ private class OrchestrationClientInputData /// /// The base URL of the Azure Functions host, used in the out-of-proc model. /// This URL is sent by the client binding object to the Durable Worker extension, - /// allowing the extension to know the host's base URL for constructing complete URLs. + /// allowing the extension to know the host's base URL for constructing management URLs. /// [JsonProperty("HttpBaseUrl")] public string? HttpBaseUrl { get; set; } diff --git a/src/WebJobs.Extensions.DurableTask/Microsoft.Azure.WebJobs.Extensions.DurableTask.xml b/src/WebJobs.Extensions.DurableTask/Microsoft.Azure.WebJobs.Extensions.DurableTask.xml index 088be7969..cdf5d8700 100644 --- a/src/WebJobs.Extensions.DurableTask/Microsoft.Azure.WebJobs.Extensions.DurableTask.xml +++ b/src/WebJobs.Extensions.DurableTask/Microsoft.Azure.WebJobs.Extensions.DurableTask.xml @@ -97,7 +97,7 @@ The base URL of the Azure Functions host, used in the out-of-proc model. This URL is sent by the client binding object to the Durable Worker extension, - allowing the extension to know the host's base URL for constructing complete URLs. + allowing the extension to know the host's base URL for constructing management URLs. diff --git a/src/Worker.Extensions.DurableTask/DurableTaskClientExtensions.cs b/src/Worker.Extensions.DurableTask/DurableTaskClientExtensions.cs index baf1088a3..418d91a20 100644 --- a/src/Worker.Extensions.DurableTask/DurableTaskClientExtensions.cs +++ b/src/Worker.Extensions.DurableTask/DurableTaskClientExtensions.cs @@ -129,7 +129,7 @@ public static HttpResponseData CreateCheckStatusResponse( /// An object containing instance control URLs. /// Thrown when instanceId is null or empty. /// Thrown when a valid base URL cannot be determined. - public static object CreateHttpManagementPayload( + public static HttpManagementPayload CreateHttpManagementPayload( this DurableTaskClient client, string instanceId, HttpRequestData? request = null) @@ -149,7 +149,7 @@ public static object CreateHttpManagementPayload( } } - private static object SetHeadersAndGetPayload( + private static HttpManagementPayload SetHeadersAndGetPayload( DurableTaskClient client, HttpRequestData? request, HttpResponseData? response, string instanceId) { static string BuildUrl(string url, params string?[] queryValues) @@ -198,15 +198,15 @@ static string BuildUrl(string url, params string?[] queryValues) response.Headers.Add("Content-Type", "application/json"); } - return new + return new HttpManagementPayload { - id = instanceId, - purgeHistoryDeleteUri = BuildUrl(instanceUrl, commonQueryParameters), - sendEventPostUri = BuildUrl($"{instanceUrl}/raiseEvent/{{eventName}}", commonQueryParameters), - statusQueryGetUri = BuildUrl(instanceUrl, commonQueryParameters), - terminatePostUri = BuildUrl($"{instanceUrl}/terminate", "reason={{text}}", commonQueryParameters), - suspendPostUri = BuildUrl($"{instanceUrl}/suspend", "reason={{text}}", commonQueryParameters), - resumePostUri = BuildUrl($"{instanceUrl}/resume", "reason={{text}}", commonQueryParameters) + Id = instanceId, + PurgeHistoryDeleteUri = BuildUrl(instanceUrl, commonQueryParameters), + SendEventPostUri = BuildUrl($"{instanceUrl}/raiseEvent/{{eventName}}", commonQueryParameters), + StatusQueryGetUri = BuildUrl(instanceUrl, commonQueryParameters), + TerminatePostUri = BuildUrl($"{instanceUrl}/terminate", "reason={{text}}", commonQueryParameters), + SuspendPostUri = BuildUrl($"{instanceUrl}/suspend", "reason={{text}}", commonQueryParameters), + ResumePostUri = BuildUrl($"{instanceUrl}/resume", "reason={{text}}", commonQueryParameters) }; } diff --git a/src/Worker.Extensions.DurableTask/HttpManagementPayload.cs b/src/Worker.Extensions.DurableTask/HttpManagementPayload.cs new file mode 100644 index 000000000..62f2301d3 --- /dev/null +++ b/src/Worker.Extensions.DurableTask/HttpManagementPayload.cs @@ -0,0 +1,96 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Text; +using Newtonsoft.Json; + +namespace Microsoft.Azure.Functions.Worker; + +/// +/// Data structure containing status, terminate and send external event HTTP endpoints. +/// +public class HttpManagementPayload +{ + /// + /// Gets the ID of the orchestration instance. + /// + /// + /// The ID of the orchestration instance. + /// + [JsonProperty("id")] + public string Id { get; internal set; } + + /// + /// Gets the HTTP GET status query endpoint URL. + /// + /// + /// The HTTP URL for fetching the instance status. + /// + [JsonProperty("statusQueryGetUri")] + public string StatusQueryGetUri { get; internal set; } + + /// + /// Gets the HTTP POST external event sending endpoint URL. + /// + /// + /// The HTTP URL for posting external event notifications. + /// + [JsonProperty("sendEventPostUri")] + public string SendEventPostUri { get; internal set; } + + /// + /// Gets the HTTP POST instance termination endpoint. + /// + /// + /// The HTTP URL for posting instance termination commands. + /// + [JsonProperty("terminatePostUri")] + public string TerminatePostUri { get; internal set; } + + /// + /// Gets the HTTP POST instance rewind endpoint. + /// + /// + /// The HTTP URL for rewinding orchestration instances. + /// + [JsonProperty("rewindPostUri")] + public string RewindPostUri { get; internal set; } + + /// + /// Gets the HTTP DELETE purge instance history by instance ID endpoint. + /// + /// + /// The HTTP URL for purging instance history by instance ID. + /// + [JsonProperty("purgeHistoryDeleteUri")] + public string PurgeHistoryDeleteUri { get; internal set; } + + /// + /// Gets the HTTP POST instance restart endpoint. + /// + /// + /// The HTTP URL for restarting an orchestration instance. + /// + [JsonProperty("restartPostUri")] + public string RestartPostUri { get; internal set; } + + /// + /// Gets the HTTP POST instance suspend endpoint. + /// + /// + /// The HTTP URL for suspending an orchestration instance. + /// + [JsonProperty("suspendPostUri")] + public string SuspendPostUri { get; internal set; } + + /// + /// Gets the HTTP POST instance resume endpoint. + /// + /// + /// The HTTP URL for resuming an orchestration instance. + /// + [JsonProperty("resumePostUri")] + public string ResumePostUri { get; internal set; } +} diff --git a/test/Worker.Extensions.DurableTask.Tests/FunctionsDurableTaskClientTests.cs b/test/Worker.Extensions.DurableTask.Tests/FunctionsDurableTaskClientTests.cs index a0f90239b..ae7a6d950 100644 --- a/test/Worker.Extensions.DurableTask.Tests/FunctionsDurableTaskClientTests.cs +++ b/test/Worker.Extensions.DurableTask.Tests/FunctionsDurableTaskClientTests.cs @@ -91,13 +91,13 @@ public void CreateHttpManagementPayload_WithHttpRequestData() private static void AssertHttpManagementPayload(dynamic payload, string BaseUrl, string instanceId) { - Assert.Equal(instanceId, payload.id); - Assert.Equal($"{BaseUrl}/instances/{instanceId}", payload.purgeHistoryDeleteUri); - Assert.Equal($"{BaseUrl}/instances/{instanceId}/raiseEvent/{{eventName}}", payload.sendEventPostUri); - Assert.Equal($"{BaseUrl}/instances/{instanceId}", payload.statusQueryGetUri); - Assert.Equal($"{BaseUrl}/instances/{instanceId}/terminate?reason={{{{text}}}}", payload.terminatePostUri); - Assert.Equal($"{BaseUrl}/instances/{instanceId}/suspend?reason={{{{text}}}}", payload.suspendPostUri); - Assert.Equal($"{BaseUrl}/instances/{instanceId}/resume?reason={{{{text}}}}", payload.resumePostUri); + Assert.Equal(instanceId, payload.Id); + Assert.Equal($"{BaseUrl}/instances/{instanceId}", payload.PurgeHistoryDeleteUri); + Assert.Equal($"{BaseUrl}/instances/{instanceId}/raiseEvent/{{eventName}}", payload.SendEventPostUri); + Assert.Equal($"{BaseUrl}/instances/{instanceId}", payload.StatusQueryGetUri); + Assert.Equal($"{BaseUrl}/instances/{instanceId}/terminate?reason={{{{text}}}}", payload.TerminatePostUri); + Assert.Equal($"{BaseUrl}/instances/{instanceId}/suspend?reason={{{{text}}}}", payload.SuspendPostUri); + Assert.Equal($"{BaseUrl}/instances/{instanceId}/resume?reason={{{{text}}}}", payload.ResumePostUri); } } } \ No newline at end of file From 7a98d3d6dc2a92e8889f495ab716e3ee122632af Mon Sep 17 00:00:00 2001 From: "naiyuantian@microsoft.com" Date: Mon, 21 Oct 2024 12:44:49 -0700 Subject: [PATCH 06/12] re-arrange if section to make code more readable --- .../DurableTaskClientExtensions.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Worker.Extensions.DurableTask/DurableTaskClientExtensions.cs b/src/Worker.Extensions.DurableTask/DurableTaskClientExtensions.cs index 418d91a20..9e7f16762 100644 --- a/src/Worker.Extensions.DurableTask/DurableTaskClientExtensions.cs +++ b/src/Worker.Extensions.DurableTask/DurableTaskClientExtensions.cs @@ -177,8 +177,13 @@ static string BuildUrl(string url, params string?[] queryValues) // The base URL could be null if: // 1. The DurableTaskClient isn't a FunctionsDurableTaskClient (which would have the baseUrl from bindings) // 2. There's no valid HttpRequestData provided - string? baseUrl = ((request != null) ? request.Url.GetLeftPart(UriPartial.Authority) : GetBaseUrl(client)) - ?? throw new InvalidOperationException("Base URL is null. Either use Functions bindings or provide an HTTP request to create the HttpPayload."); + string? baseUrl = ((request != null) ? request.Url.GetLeftPart(UriPartial.Authority) : GetBaseUrl(client)); + + if (baseUrl == null) + { + throw new InvalidOperationException("Base URL is null. Either use Functions bindings or provide an HTTP request to create the HttpPayload."); + } + bool isFromRequest = request != null; string formattedInstanceId = Uri.EscapeDataString(instanceId); From aa49b311bcfe6a56a40253ed5a1bec0d253830fe Mon Sep 17 00:00:00 2001 From: "naiyuantian@microsoft.com" Date: Mon, 21 Oct 2024 13:36:57 -0700 Subject: [PATCH 07/12] remove unnecessary exception catch --- .../DurableTaskClientExtensions.cs | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/Worker.Extensions.DurableTask/DurableTaskClientExtensions.cs b/src/Worker.Extensions.DurableTask/DurableTaskClientExtensions.cs index 9e7f16762..bbd6222a8 100644 --- a/src/Worker.Extensions.DurableTask/DurableTaskClientExtensions.cs +++ b/src/Worker.Extensions.DurableTask/DurableTaskClientExtensions.cs @@ -139,14 +139,7 @@ public static HttpManagementPayload CreateHttpManagementPayload( throw new ArgumentException("InstanceId cannot be null or empty.", nameof(instanceId)); } - try - { - return SetHeadersAndGetPayload(client, request, null, instanceId); - } - catch (InvalidOperationException ex) - { - throw new InvalidOperationException("Failed to create HTTP management payload. " + ex.Message, ex); - } + return SetHeadersAndGetPayload(client, request, null, instanceId); } private static HttpManagementPayload SetHeadersAndGetPayload( @@ -181,7 +174,7 @@ static string BuildUrl(string url, params string?[] queryValues) if (baseUrl == null) { - throw new InvalidOperationException("Base URL is null. Either use Functions bindings or provide an HTTP request to create the HttpPayload."); + throw new InvalidOperationException("Failed to create HTTP management payload as base URL is null. Either use Functions bindings or provide an HTTP request to create the HttpPayload."); } bool isFromRequest = request != null; From d7ba38468cdd6d4aa8abb9124a466d45a68147c0 Mon Sep 17 00:00:00 2001 From: "naiyuantian@microsoft.com" Date: Mon, 21 Oct 2024 14:54:27 -0700 Subject: [PATCH 08/12] add nullable check at HttpManagementPayload --- .../HttpManagementPayload.cs | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/WebJobs.Extensions.DurableTask/HttpManagementPayload.cs b/src/WebJobs.Extensions.DurableTask/HttpManagementPayload.cs index db91fdb0b..1cc7cecc2 100644 --- a/src/WebJobs.Extensions.DurableTask/HttpManagementPayload.cs +++ b/src/WebJobs.Extensions.DurableTask/HttpManagementPayload.cs @@ -3,6 +3,7 @@ using Newtonsoft.Json; +#nullable enable namespace Microsoft.Azure.WebJobs.Extensions.DurableTask { /// @@ -17,7 +18,7 @@ public class HttpManagementPayload /// The ID of the orchestration instance. /// [JsonProperty("id")] - public string Id { get; internal set; } + public string? Id { get; internal set; } /// /// Gets the HTTP GET status query endpoint URL. @@ -26,7 +27,7 @@ public class HttpManagementPayload /// The HTTP URL for fetching the instance status. /// [JsonProperty("statusQueryGetUri")] - public string StatusQueryGetUri { get; internal set; } + public string? StatusQueryGetUri { get; internal set; } /// /// Gets the HTTP POST external event sending endpoint URL. @@ -35,7 +36,7 @@ public class HttpManagementPayload /// The HTTP URL for posting external event notifications. /// [JsonProperty("sendEventPostUri")] - public string SendEventPostUri { get; internal set; } + public string? SendEventPostUri { get; internal set; } /// /// Gets the HTTP POST instance termination endpoint. @@ -44,7 +45,7 @@ public class HttpManagementPayload /// The HTTP URL for posting instance termination commands. /// [JsonProperty("terminatePostUri")] - public string TerminatePostUri { get; internal set; } + public string? TerminatePostUri { get; internal set; } /// /// Gets the HTTP POST instance rewind endpoint. @@ -53,7 +54,7 @@ public class HttpManagementPayload /// The HTTP URL for rewinding orchestration instances. /// [JsonProperty("rewindPostUri")] - public string RewindPostUri { get; internal set; } + public string? RewindPostUri { get; internal set; } /// /// Gets the HTTP DELETE purge instance history by instance ID endpoint. @@ -62,7 +63,7 @@ public class HttpManagementPayload /// The HTTP URL for purging instance history by instance ID. /// [JsonProperty("purgeHistoryDeleteUri")] - public string PurgeHistoryDeleteUri { get; internal set; } + public string? PurgeHistoryDeleteUri { get; internal set; } /// /// Gets the HTTP POST instance restart endpoint. @@ -71,7 +72,7 @@ public class HttpManagementPayload /// The HTTP URL for restarting an orchestration instance. /// [JsonProperty("restartPostUri")] - public string RestartPostUri { get; internal set; } + public string? RestartPostUri { get; internal set; } /// /// Gets the HTTP POST instance suspend endpoint. @@ -80,7 +81,7 @@ public class HttpManagementPayload /// The HTTP URL for suspending an orchestration instance. /// [JsonProperty("suspendPostUri")] - public string SuspendPostUri { get; internal set; } + public string? SuspendPostUri { get; internal set; } /// /// Gets the HTTP POST instance resume endpoint. @@ -89,6 +90,6 @@ public class HttpManagementPayload /// The HTTP URL for resuming an orchestration instance. /// [JsonProperty("resumePostUri")] - public string ResumePostUri { get; internal set; } + public string? ResumePostUri { get; internal set; } } } From 8bb16015e4aa9c39e447f07ee52de1b4abcfd868 Mon Sep 17 00:00:00 2001 From: "naiyuantian@microsoft.com" Date: Mon, 21 Oct 2024 14:58:34 -0700 Subject: [PATCH 09/12] add nullable check --- .../HttpManagementPayload.cs | 19 +++++++++---------- .../HttpManagementPayload.cs | 18 +++++++++--------- 2 files changed, 18 insertions(+), 19 deletions(-) diff --git a/src/WebJobs.Extensions.DurableTask/HttpManagementPayload.cs b/src/WebJobs.Extensions.DurableTask/HttpManagementPayload.cs index 1cc7cecc2..db91fdb0b 100644 --- a/src/WebJobs.Extensions.DurableTask/HttpManagementPayload.cs +++ b/src/WebJobs.Extensions.DurableTask/HttpManagementPayload.cs @@ -3,7 +3,6 @@ using Newtonsoft.Json; -#nullable enable namespace Microsoft.Azure.WebJobs.Extensions.DurableTask { /// @@ -18,7 +17,7 @@ public class HttpManagementPayload /// The ID of the orchestration instance. /// [JsonProperty("id")] - public string? Id { get; internal set; } + public string Id { get; internal set; } /// /// Gets the HTTP GET status query endpoint URL. @@ -27,7 +26,7 @@ public class HttpManagementPayload /// The HTTP URL for fetching the instance status. /// [JsonProperty("statusQueryGetUri")] - public string? StatusQueryGetUri { get; internal set; } + public string StatusQueryGetUri { get; internal set; } /// /// Gets the HTTP POST external event sending endpoint URL. @@ -36,7 +35,7 @@ public class HttpManagementPayload /// The HTTP URL for posting external event notifications. /// [JsonProperty("sendEventPostUri")] - public string? SendEventPostUri { get; internal set; } + public string SendEventPostUri { get; internal set; } /// /// Gets the HTTP POST instance termination endpoint. @@ -45,7 +44,7 @@ public class HttpManagementPayload /// The HTTP URL for posting instance termination commands. /// [JsonProperty("terminatePostUri")] - public string? TerminatePostUri { get; internal set; } + public string TerminatePostUri { get; internal set; } /// /// Gets the HTTP POST instance rewind endpoint. @@ -54,7 +53,7 @@ public class HttpManagementPayload /// The HTTP URL for rewinding orchestration instances. /// [JsonProperty("rewindPostUri")] - public string? RewindPostUri { get; internal set; } + public string RewindPostUri { get; internal set; } /// /// Gets the HTTP DELETE purge instance history by instance ID endpoint. @@ -63,7 +62,7 @@ public class HttpManagementPayload /// The HTTP URL for purging instance history by instance ID. /// [JsonProperty("purgeHistoryDeleteUri")] - public string? PurgeHistoryDeleteUri { get; internal set; } + public string PurgeHistoryDeleteUri { get; internal set; } /// /// Gets the HTTP POST instance restart endpoint. @@ -72,7 +71,7 @@ public class HttpManagementPayload /// The HTTP URL for restarting an orchestration instance. /// [JsonProperty("restartPostUri")] - public string? RestartPostUri { get; internal set; } + public string RestartPostUri { get; internal set; } /// /// Gets the HTTP POST instance suspend endpoint. @@ -81,7 +80,7 @@ public class HttpManagementPayload /// The HTTP URL for suspending an orchestration instance. /// [JsonProperty("suspendPostUri")] - public string? SuspendPostUri { get; internal set; } + public string SuspendPostUri { get; internal set; } /// /// Gets the HTTP POST instance resume endpoint. @@ -90,6 +89,6 @@ public class HttpManagementPayload /// The HTTP URL for resuming an orchestration instance. /// [JsonProperty("resumePostUri")] - public string? ResumePostUri { get; internal set; } + public string ResumePostUri { get; internal set; } } } diff --git a/src/Worker.Extensions.DurableTask/HttpManagementPayload.cs b/src/Worker.Extensions.DurableTask/HttpManagementPayload.cs index 62f2301d3..df2afe92c 100644 --- a/src/Worker.Extensions.DurableTask/HttpManagementPayload.cs +++ b/src/Worker.Extensions.DurableTask/HttpManagementPayload.cs @@ -20,7 +20,7 @@ public class HttpManagementPayload /// The ID of the orchestration instance. /// [JsonProperty("id")] - public string Id { get; internal set; } + public string? Id { get; internal set; } /// /// Gets the HTTP GET status query endpoint URL. @@ -29,7 +29,7 @@ public class HttpManagementPayload /// The HTTP URL for fetching the instance status. /// [JsonProperty("statusQueryGetUri")] - public string StatusQueryGetUri { get; internal set; } + public string? StatusQueryGetUri { get; internal set; } /// /// Gets the HTTP POST external event sending endpoint URL. @@ -38,7 +38,7 @@ public class HttpManagementPayload /// The HTTP URL for posting external event notifications. /// [JsonProperty("sendEventPostUri")] - public string SendEventPostUri { get; internal set; } + public string? SendEventPostUri { get; internal set; } /// /// Gets the HTTP POST instance termination endpoint. @@ -47,7 +47,7 @@ public class HttpManagementPayload /// The HTTP URL for posting instance termination commands. /// [JsonProperty("terminatePostUri")] - public string TerminatePostUri { get; internal set; } + public string? TerminatePostUri { get; internal set; } /// /// Gets the HTTP POST instance rewind endpoint. @@ -56,7 +56,7 @@ public class HttpManagementPayload /// The HTTP URL for rewinding orchestration instances. /// [JsonProperty("rewindPostUri")] - public string RewindPostUri { get; internal set; } + public string? RewindPostUri { get; internal set; } /// /// Gets the HTTP DELETE purge instance history by instance ID endpoint. @@ -65,7 +65,7 @@ public class HttpManagementPayload /// The HTTP URL for purging instance history by instance ID. /// [JsonProperty("purgeHistoryDeleteUri")] - public string PurgeHistoryDeleteUri { get; internal set; } + public string? PurgeHistoryDeleteUri { get; internal set; } /// /// Gets the HTTP POST instance restart endpoint. @@ -74,7 +74,7 @@ public class HttpManagementPayload /// The HTTP URL for restarting an orchestration instance. /// [JsonProperty("restartPostUri")] - public string RestartPostUri { get; internal set; } + public string? RestartPostUri { get; internal set; } /// /// Gets the HTTP POST instance suspend endpoint. @@ -83,7 +83,7 @@ public class HttpManagementPayload /// The HTTP URL for suspending an orchestration instance. /// [JsonProperty("suspendPostUri")] - public string SuspendPostUri { get; internal set; } + public string? SuspendPostUri { get; internal set; } /// /// Gets the HTTP POST instance resume endpoint. @@ -92,5 +92,5 @@ public class HttpManagementPayload /// The HTTP URL for resuming an orchestration instance. /// [JsonProperty("resumePostUri")] - public string ResumePostUri { get; internal set; } + public string? ResumePostUri { get; internal set; } } From 412ff589612988aeddec29c2220d32b1232d50ea Mon Sep 17 00:00:00 2001 From: Naiyuan Tian <110135109+nytian@users.noreply.github.com> Date: Mon, 21 Oct 2024 16:20:16 -0700 Subject: [PATCH 10/12] Add comment as suggested --- src/Worker.Extensions.DurableTask/HttpManagementPayload.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Worker.Extensions.DurableTask/HttpManagementPayload.cs b/src/Worker.Extensions.DurableTask/HttpManagementPayload.cs index df2afe92c..4e7228f87 100644 --- a/src/Worker.Extensions.DurableTask/HttpManagementPayload.cs +++ b/src/Worker.Extensions.DurableTask/HttpManagementPayload.cs @@ -1,5 +1,6 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT License. See License.txt in the project root for license information. +// This is a copy of: https://github.com/Azure/azure-functions-durable-extension/blob/dev/src/WebJobs.Extensions.DurableTask/HttpManagementPayload.cs using System; using System.Collections.Generic; From d3cdebe99ace8a80eb5c98cc3ead49f9c1865a8c Mon Sep 17 00:00:00 2001 From: Naiyuan Tian <110135109+nytian@users.noreply.github.com> Date: Tue, 22 Oct 2024 16:07:15 -0700 Subject: [PATCH 11/12] Update FunctionsDurableTaskClientTests.cs --- .../FunctionsDurableTaskClientTests.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/Worker.Extensions.DurableTask.Tests/FunctionsDurableTaskClientTests.cs b/test/Worker.Extensions.DurableTask.Tests/FunctionsDurableTaskClientTests.cs index ae7a6d950..6f975d2c5 100644 --- a/test/Worker.Extensions.DurableTask.Tests/FunctionsDurableTaskClientTests.cs +++ b/test/Worker.Extensions.DurableTask.Tests/FunctionsDurableTaskClientTests.cs @@ -64,7 +64,7 @@ public void CreateHttpManagementPayload_WithBaseUrl() FunctionsDurableTaskClient client = this.GetTestFunctionsDurableTaskClient(BaseUrl); string instanceId = "testInstanceIdWithHostBaseUrl"; - dynamic payload = client.CreateHttpManagementPayload(instanceId); + HttpManagementPayload payload = client.CreateHttpManagementPayload(instanceId); AssertHttpManagementPayload(payload, BaseUrl, instanceId); } @@ -84,12 +84,12 @@ public void CreateHttpManagementPayload_WithHttpRequestData() var mockHttpRequestData = new Mock(mockFunctionContext.Object); mockHttpRequestData.SetupGet(r => r.Url).Returns(new Uri(requestUrl)); - dynamic payload = client.CreateHttpManagementPayload(instanceId, mockHttpRequestData.Object); + HttpManagementPayload payload = client.CreateHttpManagementPayload(instanceId, mockHttpRequestData.Object); AssertHttpManagementPayload(payload, "http://localhost:7075/runtime/webhooks/durabletask", instanceId); } - private static void AssertHttpManagementPayload(dynamic payload, string BaseUrl, string instanceId) + private static void AssertHttpManagementPayload(HttpManagementPayload payload, string BaseUrl, string instanceId) { Assert.Equal(instanceId, payload.Id); Assert.Equal($"{BaseUrl}/instances/{instanceId}", payload.PurgeHistoryDeleteUri); @@ -100,4 +100,4 @@ private static void AssertHttpManagementPayload(dynamic payload, string BaseUrl, Assert.Equal($"{BaseUrl}/instances/{instanceId}/resume?reason={{{{text}}}}", payload.ResumePostUri); } } -} \ No newline at end of file +} From 34a8979b29284b191b33c4eb0c599193dc2463b7 Mon Sep 17 00:00:00 2001 From: "naiyuantian@microsoft.com" Date: Tue, 22 Oct 2024 20:24:30 -0700 Subject: [PATCH 12/12] update a typo as I found this at my e2e test --- src/WebJobs.Extensions.DurableTask/Bindings/BindingHelper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/WebJobs.Extensions.DurableTask/Bindings/BindingHelper.cs b/src/WebJobs.Extensions.DurableTask/Bindings/BindingHelper.cs index b26d90bb1..499628929 100644 --- a/src/WebJobs.Extensions.DurableTask/Bindings/BindingHelper.cs +++ b/src/WebJobs.Extensions.DurableTask/Bindings/BindingHelper.cs @@ -137,7 +137,7 @@ private class OrchestrationClientInputData /// This URL is sent by the client binding object to the Durable Worker extension, /// allowing the extension to know the host's base URL for constructing management URLs. /// - [JsonProperty("HttpBaseUrl")] + [JsonProperty("httpBaseUrl")] public string? HttpBaseUrl { get; set; } } }