-
-
Notifications
You must be signed in to change notification settings - Fork 748
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Azure Functions isolated process (#4988)
- Loading branch information
1 parent
deec15d
commit 0190045
Showing
29 changed files
with
1,428 additions
and
54 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
52 changes: 52 additions & 0 deletions
52
...otChocolate.AzureFunctions.IsolatedProcess/Extensions/GraphQLRequestExecutorExtensions.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
61 changes: 61 additions & 0 deletions
61
...ions.IsolatedProcess/Extensions/HotChocolateAzFuncIsolatedProcessHostBuilderExtensions.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
34 changes: 34 additions & 0 deletions
34
...s/src/HotChocolate.AzureFunctions.IsolatedProcess/Extensions/HttpRequestDataExtensions.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |
21 changes: 21 additions & 0 deletions
21
...ocolate.AzureFunctions.IsolatedProcess/HotChocolate.AzureFunctions.IsolatedProcess.csproj
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
135 changes: 135 additions & 0 deletions
135
...ocolate/AzureFunctions/src/HotChocolate.AzureFunctions.IsolatedProcess/HttpContextShim.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.