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 10 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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using System;
using Microsoft.AspNetCore.Http;
using Microsoft.Azure.Functions.Worker.Http;

namespace HotChocolate.AzureFunctions.IsolatedProcess;

public static class GraphQLRequestExecutorExtensions
{
public static async 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));

//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.
await using HttpContextShim httpContextShim = await HttpContextShim.CreateHttpContextAsync(httpRequestData).ConfigureAwait(false);

//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(httpContextShim.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 httpContextShim.CreateHttpResponseDataAsync().ConfigureAwait(false);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
using HotChocolate.AzureFunctions;
using HotChocolate.Execution.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace Microsoft.Azure.Functions.Extensions.DependencyInjection;

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="graphqlConfigureFunc">
/// 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> graphqlConfigureFunc,
int maxAllowedRequestSize = GraphQLAzureFunctionsConstants.DefaultMaxRequests,
string apiRoute = GraphQLAzureFunctionsConstants.DefaultGraphQLRoute
)
{
if (hostBuilder is null)
throw new ArgumentNullException(nameof(hostBuilder));

if (graphqlConfigureFunc is null)
throw new ArgumentNullException(nameof(graphqlConfigureFunc));

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

return hostBuilder;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using HotChocolate.AzureFunctions.IsolatedProcess.Extensions;
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 IEnumerable<string>? contentTypeHeaders)
? contentTypeHeaders.FirstOrDefault()
: defaultValue;

return contentType ?? defaultValue;
}

public static async Task<string?> ReadResponseContentAsync(this HttpResponseData httpResponseData)
{
return await httpResponseData.Body.ReadStreamAsStringAsync().ConfigureAwait(false);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<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>
<Compile Remove="Extensions\HotChocolateAzFuncIsolatedProcessServiceCollectionExtensions.cs" />
cajuncoding marked this conversation as resolved.
Show resolved Hide resolved
<Compile Remove="GraphQLIsolatedProcessExtensions.cs" />
</ItemGroup>

<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="..\..\..\AspNetCore\src\AspNetCore\HotChocolate.AspNetCore.csproj" />
<ProjectReference Include="..\HotChocolate.AzureFunctions\HotChocolate.AzureFunctions.csproj" />
</ItemGroup>

<ItemGroup>
<PackageReference Update="Microsoft.SourceLink.GitHub" Version="1.1.1" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
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 class HttpContextShim : IDisposable, IAsyncDisposable
{
protected HttpRequestData? IsolatedProcessHttpRequestData { get; set; }

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

protected virtual bool IsDisposed { get; set; } = false;

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));
}

/// <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 marshalled
/// 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>
/// <returns></returns>
public static async Task<HttpContextShim> CreateHttpContextAsync(HttpRequestData httpRequestData)
{
if (httpRequestData == null)
throw new ArgumentNullException(nameof(httpRequestData));

var requestBody = await httpRequestData.ReadAsStringAsync().ConfigureAwait(false);

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

//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; marshalled back from the HttpContext.
/// </summary>
/// <returns></returns>
public async Task<HttpResponseData> CreateHttpResponseDataAsync()
{
HttpContext httpContext = HttpContext
?? throw new NullReferenceException("The HttpContext has not been initialized correctly.");

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

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

//Initialize the Http Response...
HttpResponseData 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....
IHeaderDictionary responseHeaders = httpContext.Response.Headers;
if (responseHeaders.Any())
foreach ((var key, StringValues 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 virtual ValueTask DisposeAsync()
{
if(!IsDisposed) Dispose();
return ValueTask.CompletedTask;
}

public virtual void Dispose()
{
if (IsDisposed) return;

HttpContext.DisposeSafely();

GC.SuppressFinalize(this);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,28 +33,50 @@ 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));
}

IRequestExecutorBuilder 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>());
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>
/// <param name="services"></param>
/// <param name="apiRoute"></param>
/// <returns></returns>
public static IServiceCollection AddAzureFunctionsGraphQLRequestExecutorDependency(
this IServiceCollection services,
string apiRoute = GraphQLAzureFunctionsConstants.DefaultGraphQLRoute
)
{
services.AddSingleton<IGraphQLRequestExecutor>(sp =>
{
PathString path = apiRoute.TrimEnd('/');
PathString path = apiRoute?.TrimEnd('/');
IFileProvider fileProvider = CreateFileProvider();
var options = new GraphQLServerOptions();

foreach (Action<GraphQLServerOptions> configure in
sp.GetServices<Action<GraphQLServerOptions>>())
foreach (Action<GraphQLServerOptions> configure in sp.GetServices<Action<GraphQLServerOptions>>())
{
configure(options);
}
Expand All @@ -74,7 +96,7 @@ public static class HotChocolateAzureFunctionServiceCollectionExtensions
return new DefaultGraphQLRequestExecutor(pipeline, options);
});

return executorBuilder;
return services;
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
using HotChocolate.AzureFunctions;
using HotChocolate.Execution.Configuration;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Hosting;
using Microsoft.Extensions.DependencyInjection;

namespace Microsoft.Azure.Functions.Extensions.DependencyInjection;

public static class HotChocolateFunctionsHostBuilderExtensions
{
/// <summary>
/// Adds a GraphQL server and Azure Functions integration services.
/// Adds a GraphQL server and Azure Functions integration services. This specific configuration method is only supported by the Azure Functions In-process model;
/// the overload offers compatibility with the isolated process model for configuration code portability.
/// </summary>
/// <param name="builder">
/// <param name="hostBuilder">
/// The <see cref="IFunctionsHostBuilder"/>.
/// </param>
/// <param name="maxAllowedRequestSize">
Expand All @@ -27,15 +26,54 @@ public static class HotChocolateFunctionsHostBuilderExtensions
/// The <see cref="IServiceCollection"/> is <c>null</c>.
/// </exception>
public static IRequestExecutorBuilder AddGraphQLFunction(
this IFunctionsHostBuilder builder,
int maxAllowedRequestSize = 20 * 1000 * 1000,
string apiRoute = "/api/graphql")
this IFunctionsHostBuilder hostBuilder,
int maxAllowedRequestSize = GraphQLAzureFunctionsConstants.DefaultMaxRequests,
string apiRoute = GraphQLAzureFunctionsConstants.DefaultGraphQLRoute)
{
if (builder is null)
{
throw new ArgumentNullException(nameof(builder));
}
if (hostBuilder is null)
throw new ArgumentNullException(nameof(hostBuilder));

return builder.Services.AddGraphQLFunction(maxAllowedRequestSize, apiRoute);
return hostBuilder.Services.AddGraphQLFunction(maxAllowedRequestSize, apiRoute);
}

/// <summary>
/// Adds a GraphQL server and Azure Functions integration services in an identical way as the Azure Functions Isolated processing model; providing compatibility and portability of configuration code.
/// </summary>
/// <param name="hostBuilder">
/// The <see cref="IFunctionsHostBuilder"/>.
/// </param>
/// <param name="graphqlConfigureFunc">
/// 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 IFunctionsHostBuilder AddGraphQLFunction(
this IFunctionsHostBuilder hostBuilder,
Action<IRequestExecutorBuilder> graphqlConfigureFunc,
int maxAllowedRequestSize = GraphQLAzureFunctionsConstants.DefaultMaxRequests,
string apiRoute = GraphQLAzureFunctionsConstants.DefaultGraphQLRoute
)
{
//NOTE: HostBuilder null check will be done by AddGraphQLFunction() call below...
//if (hostBuilder is null)
// throw new ArgumentNullException(nameof(hostBuilder));

if (graphqlConfigureFunc is null)
throw new ArgumentNullException(nameof(graphqlConfigureFunc));

IRequestExecutorBuilder executorBuilder = hostBuilder.AddGraphQLFunction(maxAllowedRequestSize, apiRoute);
graphqlConfigureFunc.Invoke(executorBuilder);

return hostBuilder;
}
}