Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

HTTP APIs for .NET Isolated Parity #2653

Merged
merged 10 commits into from
Nov 13, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions src/WebJobs.Extensions.DurableTask/OutOfProcMiddleware.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using DurableTask.Core;
using DurableTask.Core.Entities;
Expand All @@ -14,6 +15,7 @@
using DurableTask.Core.History;
using DurableTask.Core.Middleware;
using Microsoft.Azure.WebJobs.Host.Executors;
using Microsoft.Extensions.Primitives;
using Newtonsoft.Json;

namespace Microsoft.Azure.WebJobs.Extensions.DurableTask
Expand Down Expand Up @@ -416,6 +418,45 @@ public async Task CallActivityAsync(DispatchMiddlewareContext dispatchContext, F
throw new InvalidOperationException($"An activity was scheduled but no {nameof(TaskScheduledEvent)} was found!");
}

if (!string.IsNullOrEmpty(scheduledEvent.Name) && scheduledEvent.Name.StartsWith("BuiltIn::HttpActivity"))
bachuv marked this conversation as resolved.
Show resolved Hide resolved
{
try
{
if (dispatchContext.GetProperty<TaskActivity>() is TaskHttpActivityShim shim)
{
OrchestrationInstance orchestrationInstance = dispatchContext.GetProperty<OrchestrationInstance>();
TaskContext context = new TaskContext(orchestrationInstance);

// convert the DurableHttpRequest
DurableHttpRequest? req = ConvertDurableHttpRequest(scheduledEvent.Input);
IList<DurableHttpRequest> list = new List<DurableHttpRequest>() { req };
string serializedRequest = JsonConvert.SerializeObject(list);

string? output = await shim.RunAsync(context, serializedRequest);
dispatchContext.SetProperty(new ActivityExecutionResult
{
ResponseEvent = new TaskCompletedEvent(
eventId: -1,
taskScheduledId: scheduledEvent.EventId,
result: JsonConvert.SerializeObject(output)),
});
return;
}
}
catch (Exception e)
{
dispatchContext.SetProperty(new ActivityExecutionResult
{
ResponseEvent = new TaskFailedEvent(
eventId: -1,
taskScheduledId: scheduledEvent.EventId,
reason: $"Function failed",
details: e.Message),
});
return;
}
}

FunctionName functionName = new FunctionName(scheduledEvent.Name);

OrchestrationInstance? instance = dispatchContext.GetProperty<OrchestrationInstance>();
Expand Down Expand Up @@ -540,6 +581,23 @@ public async Task CallActivityAsync(DispatchMiddlewareContext dispatchContext, F
dispatchContext.SetProperty(activityResult);
}

private static DurableHttpRequest? ConvertDurableHttpRequest(string? inputString)
{
IList<dynamic>? input = JsonConvert.DeserializeObject<IList<dynamic>>(inputString);
bachuv marked this conversation as resolved.
Show resolved Hide resolved
dynamic? dynamicRequest = input[0];

HttpMethod httpMethod = dynamicRequest.method.ToObject<HttpMethod>();
Uri uri = dynamicRequest.uri.ToObject<Uri>();
string content = dynamicRequest.content.ToString();

JsonSerializerSettings settings = new JsonSerializerSettings { Converters = new List<JsonConverter> { new HttpHeadersConverter() } };
Dictionary<string, StringValues> headers = JsonConvert.DeserializeObject<Dictionary<string, StringValues>>(dynamicRequest.headers.ToString(), settings);

DurableHttpRequest request = new DurableHttpRequest(httpMethod, uri, headers, content);

return request;
}

private static FailureDetails GetFailureDetails(Exception e)
{
if (e.InnerException != null && e.InnerException.Message.StartsWith("Result:"))
Expand Down
70 changes: 70 additions & 0 deletions src/Worker.Extensions.DurableTask/DurableHttpRequest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// 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.Net.Http;
using System.Text.Json.Serialization;
using Microsoft.Extensions.Primitives;

namespace Microsoft.Azure.Functions.Worker;

/// <summary>
/// Request used to make an HTTP call through Durable Functions.
/// </summary>
public class DurableHttpRequest
bachuv marked this conversation as resolved.
Show resolved Hide resolved
{
/// <summary>
/// Initializes a new instance of the <see cref="DurableHttpRequest"/> class.
/// </summary>
public DurableHttpRequest(
HttpMethod method,
Uri uri,
IDictionary<string, StringValues>? headers = null,
string? content = null)
{
this.Method = method;
this.Uri = uri;
this.Headers = HttpHeadersConverter.CreateCopy(headers);
this.Content = content;
}

/// <summary>
/// HttpMethod used in the HTTP request made by the Durable Function.
/// </summary>
[JsonPropertyName("method")]
public HttpMethod Method { get; }

/// <summary>
/// Uri used in the HTTP request made by the Durable Function.
/// </summary>
[JsonPropertyName("uri")]
public Uri Uri { get; }

/// <summary>
/// Headers passed with the HTTP request made by the Durable Function.
/// </summary>
[JsonPropertyName("headers")]
[JsonConverter(typeof(HttpHeadersConverter))]
public IDictionary<string, StringValues>? Headers { get; }

/// <summary>
/// Content passed with the HTTP request made by the Durable Function.
/// </summary>
[JsonPropertyName("content")]
public string? Content { get; }

internal static IDictionary<string, StringValues> CreateCopy(IDictionary<string, StringValues> input)
bachuv marked this conversation as resolved.
Show resolved Hide resolved
{
var copy = new Dictionary<string, StringValues>(StringComparer.OrdinalIgnoreCase);
if (input != null)
{
foreach (var pair in input)
{
copy[pair.Key] = pair.Value;
}
}

return copy;
}
}
83 changes: 83 additions & 0 deletions src/Worker.Extensions.DurableTask/DurableHttpResponse.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// 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.Linq;
using System.Net;
using System.Net.Http;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using Microsoft.Extensions.Primitives;

namespace Microsoft.Azure.Functions.Worker;

/// <summary>
/// Response received from the HTTP request made by the Durable Function.
/// </summary>
public class DurableHttpResponse
{
/// <summary>
/// Initializes a new instance of the <see cref="DurableHttpResponse"/> class.
/// </summary>
/// <param name="statusCode">HTTP Status code returned from the HTTP call.</param>
/// <param name="headers">Headers returned from the HTTP call.</param>
/// <param name="content">Content returned from the HTTP call.</param>
public DurableHttpResponse(
HttpStatusCode statusCode,
IDictionary<string, StringValues> headers = null,
string content = null)
{
this.StatusCode = statusCode;
this.Headers = HttpHeadersConverter.CreateCopy(headers);
this.Content = content;
}

/// <summary>
/// Status code returned from an HTTP request.
/// </summary>
[JsonPropertyName("statusCode")]
public HttpStatusCode StatusCode { get; }

/// <summary>
/// Headers in the response from an HTTP request.
/// </summary>
[JsonPropertyName("headers")]
[JsonConverter(typeof(HttpHeadersConverter))]
public IDictionary<string, StringValues> Headers { get; }

/// <summary>
/// Content returned from an HTTP request.
/// </summary>
[JsonPropertyName("content")]
public string Content { get; }

/// <summary>
/// Creates a DurableHttpResponse from an HttpResponseMessage.
/// </summary>
/// <param name="httpResponseMessage">HttpResponseMessage returned from the HTTP call.</param>
/// <returns>A <see cref="Task{TResult}"/> representing the result of the asynchronous operation.</returns>
public static async Task<DurableHttpResponse> CreateDurableHttpResponseWithHttpResponseMessage(HttpResponseMessage httpResponseMessage)
bachuv marked this conversation as resolved.
Show resolved Hide resolved
{
DurableHttpResponse durableHttpResponse = new DurableHttpResponse(
statusCode: httpResponseMessage.StatusCode,
headers: CreateStringValuesHeaderDictionary(httpResponseMessage.Headers),
content: await httpResponseMessage.Content.ReadAsStringAsync());

return durableHttpResponse;
}

private static IDictionary<string, StringValues> CreateStringValuesHeaderDictionary(IEnumerable<KeyValuePair<string, IEnumerable<string>>> headers)
{
IDictionary<string, StringValues> newHeaders = new Dictionary<string, StringValues>(StringComparer.OrdinalIgnoreCase);
if (headers != null)
{
foreach (var header in headers)
{
newHeaders[header.Key] = new StringValues(header.Value.ToArray());
}
}

return newHeaders;
}
}
103 changes: 103 additions & 0 deletions src/Worker.Extensions.DurableTask/HttpHeadersConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See LICENSE in the project root for license information.

using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Extensions.Primitives;

namespace Microsoft.Azure.Functions.Worker;

// StringValues does not deserialize as you would expect, so we need a custom mechanism
// for serializing HTTP header collections
internal class HttpHeadersConverter : JsonConverter<object>
bachuv marked this conversation as resolved.
Show resolved Hide resolved
{
public override bool CanConvert(Type objectType)
{
return typeof(IDictionary<string, StringValues>).IsAssignableFrom(objectType);
}

public override object Read(
ref Utf8JsonReader reader,
Type objectType,
JsonSerializerOptions options)
{
var headers = new Dictionary<string, StringValues>(StringComparer.OrdinalIgnoreCase);

if (reader.TokenType != JsonTokenType.StartObject)
{
return headers;
}

var valueList = new List<string>();
while (reader.Read() && reader.TokenType != JsonTokenType.EndObject)
{
string? propertyName = reader.GetString();

reader.Read();

// Header values can be either individual strings or string arrays
StringValues values = default(StringValues);
if (reader.TokenType == JsonTokenType.String)
{
values = new StringValues(reader.GetString());
}
else if (reader.TokenType == JsonTokenType.StartArray)
{
while (reader.Read() && reader.TokenType != JsonTokenType.EndArray)
{
valueList.Add(reader.GetString());
}

values = new StringValues(valueList.ToArray());
valueList.Clear();
}

headers[propertyName] = values;
}

return headers;
}

public override void Write(
Utf8JsonWriter writer,
object value,
JsonSerializerOptions options)
{
writer.WriteStartObject();

var headers = (IDictionary<string, StringValues>)value;
foreach (var pair in headers)
{
if (pair.Value.Count == 1)
{
// serialize as a single string value
writer.WriteString(pair.Key, pair.Value[0]);
}
else
{
// serializes as an array
writer.WriteStartArray(pair.Key);
writer.WriteStringValue(pair.Value);
writer.WriteEndArray();
}
}

writer.WriteEndObject();
}

internal static IDictionary<string, StringValues> CreateCopy(IDictionary<string, StringValues> input)
{
var copy = new Dictionary<string, StringValues>(StringComparer.OrdinalIgnoreCase);
if (input != null)
{
foreach (var pair in input)
{
copy[pair.Key] = pair.Value;
}
}

return copy;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.

using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.DurableTask;

namespace Microsoft.Azure.Functions.Worker;

/// <summary>
/// Extensions for <see cref="TaskOrchestrationContext"/>.
/// </summary>
public static class TaskOrchestrationContextExtensionMethods
{
/// <summary>
/// Makes an HTTP call using the information in the DurableHttpRequest.
/// </summary>
/// <param name="context">The task orchestration context.</param>
/// <param name="req">The DurableHttpRequest used to make the HTTP call.</param>
/// <returns></returns>
public static async Task<DurableHttpResponse> CallHttpAsync(this TaskOrchestrationContext context, DurableHttpRequest req)
{
string responseString = await context.CallActivityAsync<string>("BuiltIn::HttpActivity", req);

DurableHttpResponse? response = JsonSerializer.Deserialize<DurableHttpResponse>(responseString);

return response;
}
}
Loading