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

Azure Functions isolated process #4988

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
9e17949
WIP... initial approach pending test/validation with Demo project
cajuncoding Feb 4, 2022
e407a0a
Merge branch 'main-version-12' into bbernard/support_for_azure_functi…
cajuncoding Feb 8, 2022
73b9c8c
Initial fully working GraphQL exection in Azure Functions Isolated Pr…
cajuncoding Apr 19, 2022
98b0eb7
Code cleanup, simplification. Added new configuration overload to in-…
cajuncoding Apr 20, 2022
d21c7e3
Code cleanup and some simplification and improvements to be more test…
cajuncoding Apr 21, 2022
dcc86cd
Fixed/expanded unit tests covering true End-to-End execution of hello…
cajuncoding Apr 21, 2022
5bf21c0
Merge branch 'main' into bbernard/support_for_azure_functions_isolate…
michaelstaib Apr 22, 2022
bfa1f02
Merge branch 'main' into bbernard/support_for_azure_functions_isolate…
michaelstaib May 3, 2022
746ae20
Merge branch 'main' into bbernard/support_for_azure_functions_isolate…
michaelstaib May 3, 2022
d9f14e5
Merge branch 'main' into bbernard/support_for_azure_functions_isolate…
michaelstaib May 4, 2022
f31d56b
fixed package description
michaelstaib May 4, 2022
5011003
refinements
michaelstaib May 4, 2022
7754c76
Cleanup SonarrCloud concerns found.
cajuncoding May 5, 2022
fb20411
Merge branch 'bbernard/support_for_azure_functions_isolated_process' …
cajuncoding May 5, 2022
f6264f8
Merge branch 'main' into bbernard/support_for_azure_functions_isolate…
michaelstaib May 9, 2022
1a48837
Merge branch 'main' into bbernard/support_for_azure_functions_isolate…
michaelstaib May 11, 2022
5b15ff6
Updated to restore references and fix targets in IsolatedProcess proj…
cajuncoding Aug 12, 2022
fff6715
Merge commit from main
cajuncoding Aug 12, 2022
37d5f33
Merge commit from main
cajuncoding Aug 12, 2022
d8dd585
Merge branch 'main' into bbernard/support_for_azure_functions_isolate…
cajuncoding Aug 12, 2022
252c629
- Fixed project dependency issues after merge, due to dirty local str…
cajuncoding Aug 12, 2022
1539d8d
Merge branch 'bbernard/support_for_azure_functions_isolated_process' …
cajuncoding Aug 12, 2022
0a968a7
Merge branch 'main' into bbernard/support_for_azure_functions_isolate…
michaelstaib Aug 23, 2022
6a8d208
formatting
michaelstaib Aug 23, 2022
2983135
Merge branch 'bbernard/support_for_azure_functions_isolated_process' …
cajuncoding Aug 23, 2022
ec00068
Added full support for IHttpContextAccessor within Az Func Isolated p…
cajuncoding Aug 24, 2022
3da1566
Small code cleanup and added DefaultAzFuncHttpTriggerRoute constant.
cajuncoding Aug 24, 2022
7456d05
Add Unit tests for BCP files (currently failing due to missing BCP in…
cajuncoding Aug 24, 2022
cfe929f
Merge Main
michaelstaib Sep 12, 2022
639e6b9
cleanup
michaelstaib Sep 12, 2022
0facbb9
cleanup
michaelstaib Sep 12, 2022
b6146e9
cleanup
michaelstaib Sep 12, 2022
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
7 changes: 5 additions & 2 deletions src/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,11 @@
<TestTargetFrameworks Condition="'$(IsMacOsArm)' == 'true'">net7.0; net6.0</TestTargetFrameworks>
<AspNetTargetFrameworks Condition="'$(IsMacOsArm)' != 'true'">net7.0; net6.0; net5.0; netcoreapp3.1</AspNetTargetFrameworks>
<AspNetTargetFrameworks Condition="'$(IsMacOsArm)' == 'true'">net7.0; net6.0</AspNetTargetFrameworks>
<AzfTargetFrameworks Condition="'$(IsMacOsArm)' != 'true'">net6.0; net5.0; netcoreapp3.1</AzfTargetFrameworks>
<AzfTargetFrameworks Condition="'$(IsMacOsArm)' == 'true'">net6.0</AzfTargetFrameworks>
<AzfTargetFrameworks Condition="'$(IsMacOsArm)' != 'true'">net7.0; net6.0; net5.0; netcoreapp3.1</AzfTargetFrameworks>
<AzfTargetFrameworks Condition="'$(IsMacOsArm)' == 'true'">net7.0; net6.0</AzfTargetFrameworks>
<!-- Azure Functions Isolated Process does not support netcoreapp3.1 (on net5.0+) -->
<AzfIsoProcTargetFrameworks Condition="'$(IsMacOsArm)' != 'true'">net7.0; net6.0; net5.0;</AzfIsoProcTargetFrameworks>
<AzfIsoProcTargetFrameworks Condition="'$(IsMacOsArm)' == 'true'">net7.0; net6.0</AzfIsoProcTargetFrameworks>
<SourceGenTargetFrameworks>net7.0; net6.0; netstandard2.0</SourceGenTargetFrameworks>
</PropertyGroup>

Expand Down
1 change: 1 addition & 0 deletions src/HotChocolate/AzureFunctions/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

<PropertyGroup>
<TargetFrameworks>$(AzfTargetFrameworks)</TargetFrameworks>
<TargetFrameworks Condition="$(MSBuildProjectName.Contains('Isolated'))">$(AzfIsoProcTargetFrameworks)</TargetFrameworks>
<Nullable>enable</Nullable>
</PropertyGroup>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
using Microsoft.Azure.Functions.Worker.Http;

namespace HotChocolate.AzureFunctions.IsolatedProcess;

public static class GraphQLRequestExecutorExtensions
{
public static Task<HttpResponseData> ExecuteAsync(
this IGraphQLRequestExecutor graphqlRequestExecutor,
HttpRequestData httpRequestData)
{
if (graphqlRequestExecutor is null)
{
throw new ArgumentNullException(nameof(graphqlRequestExecutor));
}

if (httpRequestData is null)
{
throw new ArgumentNullException(nameof(httpRequestData));
}

// Factored out Async logic to Address SonarCloud concern for exceptions in Async flow ...
return ExecuteGraphQLRequestInternalAsync(graphqlRequestExecutor, httpRequestData);
}

private static async Task<HttpResponseData> ExecuteGraphQLRequestInternalAsync(
IGraphQLRequestExecutor graphqlRequestExecutor,
HttpRequestData httpRequestData)
{
// Adapt the Isolated Process HttpRequestData to the HttpContext needed by
// HotChocolate and execute the Pipeline...
// NOTE: This must be disposed of properly to ensure our request/response
// resources are managed efficiently.
using var shim =
await HttpContextShim.CreateHttpContextAsync(httpRequestData).ConfigureAwait(false);

// Isolated Process doesn't natively support HttpContext so we must manually enable
// support for HttpContext injection within HotChocolate (e.g. into Resolvers) for
// low-level access.
httpRequestData.SetCurrentHttpContext(shim.HttpContext);

// Now we can execute the request by marshalling the HttpContext into the
// DefaultGraphQLRequestExecutor which will handle pre & post processing as needed ...
// NOTE: We discard the result returned (likely an EmptyResult) as all content is already
// written to the HttpContext Response.
await graphqlRequestExecutor.ExecuteAsync(shim.HttpContext.Request).ConfigureAwait(false);

// Last, in the Isolated Process model we marshall all data back to the HttpResponseData
// model and return it to the AzureFunctions process ...
// Therefore we need to marshall the Response back to the Isolated Process model ...
return await shim.CreateHttpResponseDataAsync().ConfigureAwait(false);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
using HotChocolate.AzureFunctions;
using HotChocolate.Execution.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

// ReSharper disable once CheckNamespace
namespace Microsoft.Azure.Functions.Extensions.DependencyInjection;

/// <summary>
/// Provides DI extension methods to configure a GraphQL server.
/// </summary>
public static class HotChocolateAzFuncIsolatedProcessHostBuilderExtensions
{
/// <summary>
/// Adds a GraphQL server and Azure Functions integration services for
/// Azure Functions Isolated processing model.
/// </summary>
/// <param name="hostBuilder">
/// The <see cref="IFunctionsHostBuilder"/>.
/// </param>
/// <param name="configure">
/// The GraphQL configuration function that will be invoked, for chained
/// configuration, when the Host is built.
/// </param>
/// <param name="maxAllowedRequestSize">
/// The max allowed GraphQL request size.
/// </param>
/// <param name="apiRoute">
/// The API route that was used in the GraphQL Azure Function.
/// </param>
/// <returns>
/// Returns the <see cref="IHostBuilder"/> so that host configuration can be chained.
/// </returns>
/// <exception cref="ArgumentNullException">
/// The <see cref="IServiceCollection"/> is <c>null</c>.
/// </exception>
public static IHostBuilder AddGraphQLFunction(
this IHostBuilder hostBuilder,
Action<IRequestExecutorBuilder> configure,
int maxAllowedRequestSize = GraphQLAzureFunctionsConstants.DefaultMaxRequests,
string apiRoute = GraphQLAzureFunctionsConstants.DefaultGraphQLRoute)
{
if (hostBuilder is null)
{
throw new ArgumentNullException(nameof(hostBuilder));
}

if (configure is null)
{
throw new ArgumentNullException(nameof(configure));
}

hostBuilder.ConfigureServices(services =>
{
var executorBuilder = services.AddGraphQLFunction(maxAllowedRequestSize, apiRoute);
configure(executorBuilder);
});

return hostBuilder;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using HotChocolate.AzureFunctions.IsolatedProcess.Extensions;
using Microsoft.AspNetCore.Http;
using Microsoft.Azure.Functions.Worker.Http;
using Microsoft.Net.Http.Headers;

namespace HotChocolate.AzureFunctions.IsolatedProcess;

public static class HttpRequestDataExtensions
{
public static string GetContentType(
this HttpRequestData httpRequestData,
string defaultValue = GraphQLAzureFunctionsConstants.DefaultJsonContentType)
{
var contentType = httpRequestData.Headers.TryGetValues(
HeaderNames.ContentType,
out var contentTypeHeaders)
? contentTypeHeaders.FirstOrDefault()
: defaultValue;

return contentType ?? defaultValue;
}

public static HttpRequestData SetCurrentHttpContext(
this HttpRequestData httpRequestData,
HttpContext httpContext)
{
httpRequestData.FunctionContext.InstanceServices.SetCurrentHttpContext(httpContext);
return httpRequestData;
}

public static async Task<string?> ReadResponseContentAsync(
this HttpResponseData httpResponseData)
=> await httpResponseData.Body.ReadStreamAsStringAsync().ConfigureAwait(false);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<PackageId>HotChocolate.AzureFunctions.IsolatedProcess</PackageId>
<AssemblyName>HotChocolate.AzureFunctions.IsolatedProcess</AssemblyName>
<RootNamespace>HotChocolate.AzureFunctions.IsolatedProcess</RootNamespace>
<Description>This package contains the GraphQL AzureFunctions Isolated Process integration for Hot Chocolate.</Description>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Abstractions" Version="1.1.0" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Core" Version="1.4.0" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\HotChocolate.AzureFunctions\HotChocolate.AzureFunctions.csproj" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
using System.Net;
using HotChocolate.AzureFunctions.IsolatedProcess.Extensions;
using Microsoft.AspNetCore.Http;
using Microsoft.Azure.Functions.Worker.Http;
using Microsoft.Extensions.Primitives;

namespace HotChocolate.AzureFunctions.IsolatedProcess;

public sealed class HttpContextShim : IDisposable
{
private bool _disposed;

public HttpContextShim(HttpContext httpContext)
{
HttpContext = httpContext ??
throw new ArgumentNullException(nameof(httpContext));
IsolatedProcessHttpRequestData = null;
}

public HttpContextShim(HttpContext httpContext, HttpRequestData? httpRequestData)
{
HttpContext = httpContext ??
throw new ArgumentNullException(nameof(httpContext));
IsolatedProcessHttpRequestData = httpRequestData ??
throw new ArgumentNullException(nameof(httpRequestData));
}

private HttpRequestData? IsolatedProcessHttpRequestData { get; set; }

// Must keep the Reference so we can safely Dispose!
public HttpContext HttpContext { get; }

/// <summary>
/// Factory method to Create an HttpContext that is AspNetCore compatible.
/// All pertinent data from the HttpRequestData provided by the
/// Azure Functions Isolated Process will be marshaled
/// into the HttpContext for HotChocolate to consume.
/// NOTE: This is done as Factory method (and not in the Constructor)
/// to support optimized Async reading of incoming Request Content/Stream.
/// </summary>
public static Task<HttpContextShim> CreateHttpContextAsync(HttpRequestData httpRequestData)
{
if (httpRequestData == null)
{
throw new ArgumentNullException(nameof(httpRequestData));
}

// Factored out Async logic to Address SonarCloud concern for exceptions in Async flow...
return CreateHttpContextInternalAsync(httpRequestData);
}

private static async Task<HttpContextShim> CreateHttpContextInternalAsync(
HttpRequestData httpRequestData)
{
var requestBody = await httpRequestData.ReadAsStringAsync().ConfigureAwait(false);

var httpContext = new HttpContextBuilder().CreateHttpContext(
requestHttpMethod: httpRequestData.Method,
requestUri: httpRequestData.Url,
requestBody: requestBody,
requestBodyContentType: httpRequestData.GetContentType(),
requestHeaders: httpRequestData.Headers,
claimsIdentities: httpRequestData.Identities,
contextItems: httpRequestData.FunctionContext.Items
);

// Ensure we track the HttpContext internally for cleanup when disposed!
return new HttpContextShim(httpContext, httpRequestData);
}

/// <summary>
/// Create an HttpResponseData containing the proxied response content results;
/// marshaled back from the HttpContext.
/// </summary>
/// <returns></returns>
public async Task<HttpResponseData> CreateHttpResponseDataAsync()
{
var httpContext = HttpContext
?? throw new NullReferenceException(
"The HttpContext has not been initialized correctly.");

var httpRequestData = IsolatedProcessHttpRequestData
?? throw new NullReferenceException(
"The HttpRequestData has not been initialized correctly.");

var httpStatusCode = (HttpStatusCode)httpContext.Response.StatusCode;

// Initialize the Http Response...
var httpResponseData = httpRequestData.CreateResponse(httpStatusCode);

// Marshall over all Headers from the HttpContext...
// Note: This should also handle Cookies since Cookies are stored as a Header value ....
var responseHeaders = httpContext.Response.Headers;

if (responseHeaders.Count > 0)
{
foreach (var (key, value) in responseHeaders)
{
httpResponseData.Headers.TryAddWithoutValidation(
key,
value.Select(sv => sv?.ToString()));
}
}

// Marshall the original response Bytes from HotChocolate...
// Note: This enables full support for GraphQL Json results/errors,
// binary downloads, SDL, & BCP binary data.
var responseBytes = await httpContext.ReadResponseBytesAsync().ConfigureAwait(false);

if (responseBytes != null)
{
await httpResponseData.WriteBytesAsync(responseBytes).ConfigureAwait(false);
}

return httpResponseData;
}

public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}

private void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
HttpContext.DisposeSafely();
}
_disposed = true;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,23 +33,46 @@ public static class HotChocolateAzureFunctionServiceCollectionExtensions
/// </exception>
public static IRequestExecutorBuilder AddGraphQLFunction(
this IServiceCollection services,
int maxAllowedRequestSize = 20 * 1000 * 1000,
string apiRoute = "/api/graphql")
int maxAllowedRequestSize = GraphQLAzureFunctionsConstants.DefaultMaxRequests,
string apiRoute = GraphQLAzureFunctionsConstants.DefaultGraphQLRoute)
{
if (services is null)
{
throw new ArgumentNullException(nameof(services));
}

var executorBuilder =
services.AddGraphQLServer(maxAllowedRequestSize: maxAllowedRequestSize);

// Register AzFunc Custom Binding Extensions for In-Process Functions.
// NOTE: This does not work for Isolated Process due to (but is not harmful at all of
// isolated process; it just remains dormant):
// 1) Bindings always execute in-process and values must be marshaled between
// the Host Process & the Isolated Process Worker!
// 2) Currently only String values are supported (obviously due to above complexities).
// More Info. here (using Blob binding docs):
// https://docs.microsoft.com/en-us/azure/azure-functions/functions-bindings-storage-
// blob-input?tabs=isolated-process%2Cextensionv5&pivots=programming-language-csharp#usage
services.TryAddEnumerable(
ServiceDescriptor.Singleton<IExtensionConfigProvider, GraphQLExtensions>());

//Add the Request Executor Dependency...
services.AddAzureFunctionsGraphQLRequestExecutorDependency(apiRoute);

return executorBuilder;
}

/// <summary>
/// Internal method to adds the Request Executor dependency for Azure Functions both
/// in-process and isolate-process. Normal configuration should use AddGraphQLFunction()
/// extension instead which correctly call this internally.
/// </summary>
public static IServiceCollection AddAzureFunctionsGraphQLRequestExecutorDependency(
this IServiceCollection services,
string apiRoute = GraphQLAzureFunctionsConstants.DefaultGraphQLRoute
)
{
services.AddSingleton<IGraphQLRequestExecutor>(sp =>
{
PathString path = apiRoute.TrimEnd('/');
PathString path = apiRoute?.TrimEnd('/');
var fileProvider = CreateFileProvider();
var options = new GraphQLServerOptions();

Expand All @@ -74,7 +97,7 @@ public static IRequestExecutorBuilder AddGraphQLFunction(
return new DefaultGraphQLRequestExecutor(pipeline, options);
});

return executorBuilder;
return services;
}

/// <summary>
Expand Down
Loading