Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
nytian committed Oct 7, 2024
1 parent 8a5db71 commit 0255475
Show file tree
Hide file tree
Showing 5 changed files with 87 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ public string DurableOrchestrationClientToString(IDurableOrchestrationClient cli
ConnectionName = attr.ConnectionName,
RpcBaseUrl = localRpcAddress,
RequiredQueryStringParameters = this.config.HttpApiHandler.GetUniversalQueryStrings(),
BaseUrl = this.config.HttpApiHandler.GetBaseUrl(),
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ public ValueTask<ConversionResult> 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>(ConversionResult.Success(client));
}
catch (Exception innerException)
Expand All @@ -62,5 +62,5 @@ public ValueTask<ConversionResult> 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);
}
57 changes: 52 additions & 5 deletions src/Worker.Extensions.DurableTask/DurableTaskClientExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,37 @@ public static HttpResponseData CreateCheckStatusResponse(
return response;
}

/// <summary>
/// Creates an HTTP management payload for the specified orchestration instance.
/// </summary>
/// <param name="client">The <see cref="DurableTaskClient"/>.</param>
/// <param name="instanceId">The ID of the orchestration instance.</param>
/// <param name="request">Optional HTTP request data to use for creating the base URL.</param>
/// <returns>An object containing instance control URLs.</returns>
/// <exception cref="ArgumentException">Thrown when instanceId is null or empty.</exception>
/// <exception cref="InvalidOperationException">Thrown when a valid base URL cannot be determined.</exception>
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)
{
Expand All @@ -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
{
Expand All @@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using Microsoft.Azure.Functions.Worker.Http;
using Microsoft.DurableTask.Client;
using Microsoft.DurableTask.Client.Grpc;
using Moq;
Expand All @@ -9,7 +10,7 @@ namespace Microsoft.Azure.Functions.Worker.Tests
/// </summary>
public class FunctionsDurableTaskClientTests
{
private FunctionsDurableTaskClient GetTestFunctionsDurableTaskClient()
private FunctionsDurableTaskClient GetTestFunctionsDurableTaskClient(string baseUrl = null)

Check warning on line 13 in test/Worker.Extensions.DurableTask.Tests/FunctionsDurableTaskClientTests.cs

View workflow job for this annotation

GitHub Actions / build

Cannot convert null literal to non-nullable reference type.

Check warning on line 13 in test/Worker.Extensions.DurableTask.Tests/FunctionsDurableTaskClientTests.cs

View workflow job for this annotation

GitHub Actions / build

Cannot convert null literal to non-nullable reference type.

Check warning on line 13 in test/Worker.Extensions.DurableTask.Tests/FunctionsDurableTaskClientTests.cs

View workflow job for this annotation

GitHub Actions / build

Cannot convert null literal to non-nullable reference type.

Check warning on line 13 in test/Worker.Extensions.DurableTask.Tests/FunctionsDurableTaskClientTests.cs

View workflow job for this annotation

GitHub Actions / build

Cannot convert null literal to non-nullable reference type.

Check warning on line 13 in test/Worker.Extensions.DurableTask.Tests/FunctionsDurableTaskClientTests.cs

View workflow job for this annotation

GitHub Actions / Analyze (csharp)

Cannot convert null literal to non-nullable reference type.

Check warning on line 13 in test/Worker.Extensions.DurableTask.Tests/FunctionsDurableTaskClientTests.cs

View workflow job for this annotation

GitHub Actions / build

Cannot convert null literal to non-nullable reference type.
{
// construct mock client

Expand All @@ -22,7 +23,7 @@ private FunctionsDurableTaskClient GetTestFunctionsDurableTaskClient()
It.IsAny<string>(), It.IsAny<TerminateInstanceOptions>(), It.IsAny<CancellationToken>())).Returns(completedTask);

DurableTaskClient durableClient = durableClientMock.Object;
FunctionsDurableTaskClient client = new FunctionsDurableTaskClient(durableClient, queryString: null);
FunctionsDurableTaskClient client = new FunctionsDurableTaskClient(durableClient, queryString: null, baseUrl: baseUrl);
return client;
}

Expand Down Expand Up @@ -53,5 +54,31 @@ public async void TerminateDoesNotThrow()
await client.TerminateInstanceAsync(instanceId, options);
await client.TerminateInstanceAsync(instanceId, options, token);
}

/// <summary>
/// Test that the `CreateHttpManagementPayload` method returns the expected payload structure without HttpRequestData.
/// </summary>
[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);
}
}
}

0 comments on commit 0255475

Please sign in to comment.