Skip to content

Commit 10f9ad1

Browse files
committed
Code cleanup, and flushed out details needed to fully support ?SDL, and BananaCakePop in the In Process Azure Functions...
1 parent 39b790c commit 10f9ad1

35 files changed

+660
-454
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net5.0</TargetFramework>
5+
<RootNamespace>HotChocolate.AzureFunctionsProxy</RootNamespace>
6+
</PropertyGroup>
7+
8+
<ItemGroup>
9+
<PackageReference Include="Microsoft.AspNetCore.Http" Version="2.2.0" />
10+
<PackageReference Include="Microsoft.Azure.Functions.Worker.Core" Version="1.3.1" />
11+
</ItemGroup>
12+
13+
<ItemGroup>
14+
<ProjectReference Include="..\GraphQL.AzureFunctionsProxy\GraphQL.AzureFunctionsProxy.csproj" />
15+
</ItemGroup>
16+
17+
</Project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
using System;
2+
using System.Threading;
3+
using System.Threading.Tasks;
4+
using Microsoft.Azure.Functions.Worker;
5+
using Microsoft.Azure.Functions.Worker.Http;
6+
using Microsoft.Extensions.Logging;
7+
8+
namespace HotChocolate.AzureFunctionsProxy.IsolatedProcess
9+
{
10+
public static class GraphQLAzureFunctionsExecutorProxyExtensions
11+
{
12+
public static async Task<HttpResponseData> ExecuteFunctionsQueryAsync(
13+
this IGraphQLAzureFunctionsExecutorProxy graphqlExecutorProxy,
14+
HttpRequestData httpRequestData,
15+
ILogger log = null,
16+
CancellationToken cancellationToken = default
17+
)
18+
{
19+
AssertParamIsNotNull(graphqlExecutorProxy, nameof(graphqlExecutorProxy));
20+
AssertParamIsNotNull(httpRequestData, nameof(httpRequestData));
21+
22+
//Create the GraphQL HttpContext Shim to help marshall data from the Isolated Process HttpRequestData into a
23+
// AspNetCore compatible HttpContext, and marshall results back into HttpResponseData from the HttpContext...
24+
await using var graphQLHttpContextShim = new GraphQLHttpContextShim(httpRequestData);
25+
26+
//Build the Http Context Shim for HotChocolate to consume via the AzureFunctionsProxy...
27+
var graphqlHttpContextShim = await graphQLHttpContextShim.CreateGraphQLHttpContextAsync();
28+
29+
30+
//Execute the full HotChocolate middleware pipeline via AzureFunctionsProxy...
31+
var logger = log ?? httpRequestData.FunctionContext.GetLogger<IGraphQLAzureFunctionsExecutorProxy>();
32+
33+
await graphqlExecutorProxy.ExecuteFunctionsQueryAsync(graphqlHttpContextShim, logger, cancellationToken).ConfigureAwait(false);
34+
35+
//Marshall the results back into the isolated process compatible HttpResponseData...
36+
var httpResponseData = await graphQLHttpContextShim.CreateHttpResponseDataAsync().ConfigureAwait(false);
37+
return httpResponseData;
38+
39+
}
40+
41+
private static void AssertParamIsNotNull(object value, string argName)
42+
{
43+
if (value == null)
44+
throw new ArgumentNullException(argName);
45+
}
46+
}
47+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
using System;
2+
using System.IO;
3+
using System.Linq;
4+
using System.Net;
5+
using System.Net.Http;
6+
using System.Text;
7+
using System.Threading.Tasks;
8+
using Microsoft.AspNetCore.Http;
9+
//using QueryCollection = Microsoft.AspNetCore.Http.QueryCollection;
10+
using Microsoft.AspNetCore.WebUtilities;
11+
using Microsoft.Azure.Functions.Worker.Http;
12+
using Microsoft.Extensions.Primitives;
13+
14+
namespace HotChocolate.AzureFunctionsProxy
15+
{
16+
public class GraphQLHttpContextShim : IDisposable, IAsyncDisposable
17+
{
18+
protected HttpContext HttpContextShim { get; set; }
19+
20+
public HttpRequestData IsolatedProcessHttpRequestData { get; protected set; }
21+
22+
public string ContentType { get; protected set; }
23+
24+
public GraphQLHttpContextShim(HttpRequestData httpRequestData)
25+
{
26+
this.IsolatedProcessHttpRequestData = httpRequestData ?? throw new ArgumentNullException(nameof(httpRequestData));
27+
this.ContentType = httpRequestData.GetContentType();
28+
}
29+
30+
/// <summary>
31+
/// Create an HttpContext (AspNetCore compatible) that can be provided to the AzureFunctionsProxy for GraphQL execution.
32+
/// All pertinent data from the HttpRequestData provided by the Azure Functions Isolated Process will be marshalled
33+
/// into the HttpContext for HotChocolate to consume.
34+
/// </summary>
35+
/// <returns></returns>
36+
public virtual async Task<HttpContext> CreateGraphQLHttpContextAsync()
37+
{
38+
var httpRequestData = this.IsolatedProcessHttpRequestData;
39+
40+
var httpContextShim = BuildGraphQLHttpContext(
41+
requestHttpMethod: httpRequestData.Method,
42+
requestUri: httpRequestData.Url,
43+
requestHeadersCollection: httpRequestData.Headers,
44+
requestBody: await httpRequestData.ReadAsStringAsync().ConfigureAwait(false),
45+
requestBodyContentType: httpRequestData.GetContentType()
46+
);
47+
48+
//Ensure we track the HttpContext internally for cleanup when disposed!
49+
this.HttpContextShim = httpContextShim;
50+
return httpContextShim;
51+
}
52+
53+
protected virtual HttpContext BuildGraphQLHttpContext(
54+
string requestHttpMethod,
55+
Uri requestUri,
56+
HttpHeadersCollection requestHeadersCollection,
57+
string requestBody = null,
58+
string requestBodyContentType = "application/json"
59+
)
60+
{
61+
//Initialize the root Http Context (Container)...
62+
var httpContext = new DefaultHttpContext();
63+
64+
//Initialize the Http Request...
65+
var httpRequest = httpContext.Request;
66+
httpRequest.Scheme = requestUri.Scheme;
67+
httpRequest.Path = new PathString(requestUri.AbsolutePath);
68+
httpRequest.Method = requestHttpMethod ?? HttpMethod.Post.Method;
69+
httpRequest.QueryString = new QueryString(requestUri.Query);
70+
71+
//Ensure we marshall across all Headers from teh Client Request...
72+
if (requestHeadersCollection?.Any() == true)
73+
foreach(var header in requestHeadersCollection)
74+
httpRequest.Headers.Add(header.Key, new StringValues(header.Value.ToArray()));
75+
76+
if (!string.IsNullOrEmpty(requestBody))
77+
{
78+
//Initialize a valid Stream for the Request (must be tracked & Disposed of!)
79+
var requestBodyBytes = Encoding.UTF8.GetBytes(requestBody);
80+
httpRequest.Body = new MemoryStream(requestBodyBytes);
81+
httpRequest.ContentType = requestBodyContentType;
82+
httpRequest.ContentLength = requestBodyBytes.Length;
83+
}
84+
85+
//Initialize the Http Response...
86+
var httpResponse = httpContext.Response;
87+
//Initialize a valid Stream for the Response (must be tracked & Disposed of!)
88+
//NOTE: Default Body is a NullStream...which ignores all Reads/Writes.
89+
httpResponse.Body = new MemoryStream();
90+
91+
return httpContext;
92+
}
93+
94+
/// <summary>
95+
/// Create an HttpResponseData containing the proxied GraphQL results; marshalled back from
96+
/// the HttpContext that HotChocolate populates.
97+
/// </summary>
98+
/// <returns></returns>
99+
public async Task<HttpResponseData> CreateHttpResponseDataAsync()
100+
{
101+
var graphqlResponseBytes = ReadResponseBytes();
102+
var httpContext = this.HttpContextShim;
103+
var httpStatusCode = (HttpStatusCode)httpContext.Response.StatusCode;
104+
105+
//Initialize the Http Response...
106+
var httpRequestData = this.IsolatedProcessHttpRequestData;
107+
var response = httpRequestData.CreateResponse(httpStatusCode);
108+
109+
//Marshall over all Headers from the HttpContext...
110+
//Note: This should also handle Cookies (not tested)....
111+
var responseHeaders = httpContext.Response?.Headers;
112+
if (responseHeaders?.Any() == true)
113+
foreach (var header in responseHeaders)
114+
response.Headers.Add(header.Key, header.Value.Select(sv => sv.ToString()));
115+
116+
//Marshall the original response Bytes from HotChocolate...
117+
//Note: This enables full support for GraphQL Json results/errors, binary downloads, SDL, & BCP binary data.
118+
await response.WriteBytesAsync(graphqlResponseBytes).ConfigureAwait(false);
119+
120+
return response;
121+
}
122+
123+
public byte[] ReadResponseBytes()
124+
{
125+
if (HttpContextShim?.Response?.Body is not MemoryStream responseMemoryStream)
126+
return null;
127+
128+
var bytes = responseMemoryStream.ToArray();
129+
return bytes;
130+
131+
}
132+
133+
public virtual string ReadResponseContentAsString()
134+
{
135+
var responseContent = Encoding.UTF8.GetString(ReadResponseBytes());
136+
return responseContent;
137+
}
138+
139+
public ValueTask DisposeAsync()
140+
{
141+
this.Dispose();
142+
return ValueTask.CompletedTask;
143+
}
144+
145+
public virtual void Dispose()
146+
{
147+
DisposeHttpContext();
148+
GC.SuppressFinalize(this);
149+
}
150+
151+
protected virtual void DisposeHttpContext()
152+
{
153+
var httpContext = this.HttpContextShim;
154+
httpContext?.Request?.Body?.Dispose();
155+
httpContext?.Response?.Body?.Dispose();
156+
157+
this.HttpContextShim = null;
158+
}
159+
}
160+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Net;
5+
using Microsoft.AspNetCore.WebUtilities;
6+
using Microsoft.Azure.Functions.Worker.Http;
7+
using Microsoft.Extensions.Primitives;
8+
using Microsoft.Net.Http.Headers;
9+
10+
namespace HotChocolate.AzureFunctionsProxy
11+
{
12+
public static class HttpRequestDataExtensions
13+
{
14+
public static string GetContentType(this HttpRequestData httpRequestData, string defaultValue = "application/json")
15+
{
16+
var contentType = httpRequestData.Headers.TryGetValues(HeaderNames.ContentType, out var contentTypeHeaders)
17+
? contentTypeHeaders.FirstOrDefault()
18+
: defaultValue;
19+
20+
return contentType;
21+
}
22+
23+
public static HttpResponseData CreateStringMessageResponse(this HttpRequestData httpRequestData, HttpStatusCode httpStatus, string message)
24+
{
25+
if (httpRequestData == null)
26+
throw new ArgumentNullException(nameof(httpRequestData));
27+
28+
var response = httpRequestData.CreateResponse();
29+
response.StatusCode = httpStatus;
30+
response.WriteString(message);
31+
return response;
32+
}
33+
34+
35+
public static HttpResponseData CreateBadRequestErrorMessageResponse(this HttpRequestData httpRequestData, string message)
36+
{
37+
return httpRequestData.CreateStringMessageResponse(HttpStatusCode.BadRequest, message);
38+
}
39+
40+
public static string GetQueryStringParam(this HttpRequestData httpRequestData, string queryParamName)
41+
{
42+
const string QueryStringItemsKey = "QueryStringParameters";
43+
var functionContextItems = httpRequestData.FunctionContext.Items;
44+
45+
Dictionary<string, StringValues> queryStringParams;
46+
if (functionContextItems.TryGetValue(QueryStringItemsKey, out var existingQueryParams))
47+
{
48+
queryStringParams = existingQueryParams as Dictionary<string, StringValues>;
49+
}
50+
else
51+
{
52+
queryStringParams = QueryHelpers.ParseQuery(httpRequestData.Url.Query);
53+
httpRequestData.FunctionContext.Items.Add(QueryStringItemsKey, queryStringParams);
54+
}
55+
56+
var queryValue = queryStringParams != null && queryStringParams.TryGetValue(queryParamName, out var stringValues)
57+
? stringValues.FirstOrDefault()
58+
: null;
59+
60+
return queryValue;
61+
}
62+
}
63+
}

GraphQL.AzureFunctionsProxy.Tests/GraphQL.AzureFunctionsProxy.Tests.csproj

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
<PackageReference Include="Microsoft.Azure.Functions.Extensions" Version="1.1.0" />
1212
<PackageReference Include="Microsoft.Azure.WebJobs.Extensions.Http" Version="3.0.12" />
1313
<PackageReference Include="Microsoft.Extensions.Logging" Version="3.1.20" />
14-
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
14+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
1515
<PackageReference Include="MSTest.TestAdapter" Version="2.2.7" />
1616
<PackageReference Include="MSTest.TestFramework" Version="2.2.7" />
1717
<PackageReference Include="coverlet.collector" Version="3.1.0">

GraphQL.AzureFunctionsProxy.Tests/_TestFrameworks/AzureFunctionsTestFramework/AzureFunctionTestContext.cs

-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
using System.Text;
77
using Functions.Tests;
88
using Microsoft.AspNetCore.Http;
9-
using Microsoft.Azure.WebJobs.Host.Executors;
109
using Microsoft.Extensions.DependencyInjection;
1110
using Microsoft.Extensions.Hosting;
1211
using Microsoft.Extensions.Logging;

GraphQL.AzureFunctionsProxy.sln

+13-7
Original file line numberDiff line numberDiff line change
@@ -3,26 +3,24 @@ Microsoft Visual Studio Solution File, Format Version 12.00
33
# Visual Studio Version 16
44
VisualStudioVersion = 16.0.30611.23
55
MinimumVisualStudioVersion = 10.0.40219.1
6-
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StarWars-AzureFunctions", "StarWars-AzureFunctions\StarWars-AzureFunctions.csproj", "{91E18C69-CD87-4305-B241-1DD04DB118F8}"
7-
EndProject
86
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GraphQL.AzureFunctionsProxy", "GraphQL.AzureFunctionsProxy\GraphQL.AzureFunctionsProxy.csproj", "{EADE1DA0-404D-4504-86FD-B32BF70AF88F}"
97
EndProject
108
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GraphQL.AzureFunctionsProxy.Tests", "GraphQL.AzureFunctionsProxy.Tests\GraphQL.AzureFunctionsProxy.Tests.csproj", "{602DB716-B801-412D-971D-735BFE29076E}"
119
EndProject
1210
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StarWars.GraphQL.Common", "StarWars.Common\StarWars.GraphQL.Common.csproj", "{3D884B8B-A117-41A0-8C15-A68D891E3021}"
1311
EndProject
14-
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StarWars-AzureFunctions-OutOfProcessProcess2", "StarWars-AzureFunctions-OutOfProcessProcess2\StarWars-AzureFunctions-OutOfProcessProcess2.csproj", "{07DF34DD-19D6-4CFB-80A7-3CAC6B102A97}"
12+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StarWars-AzureFunctions-OutOfProcessProcess", "StarWars-AzureFunctions-OutOfProcessProcess\StarWars-AzureFunctions-OutOfProcessProcess.csproj", "{07DF34DD-19D6-4CFB-80A7-3CAC6B102A97}"
13+
EndProject
14+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GraphQL.AzureFunctionsProxy.IsolatedProcess", "GraphQL.AzureFunctionsProxy.IsolatedProcess\GraphQL.AzureFunctionsProxy.IsolatedProcess.csproj", "{8A3E7335-60D8-4A1E-B9EC-FF3CCA581FDB}"
15+
EndProject
16+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StarWars-AzureFunctions-InProcess", "StarWars-AzureFunctions-InProcess\StarWars-AzureFunctions-InProcess.csproj", "{1B692FC6-4C6B-46B4-BBE7-BF405937380F}"
1517
EndProject
1618
Global
1719
GlobalSection(SolutionConfigurationPlatforms) = preSolution
1820
Debug|Any CPU = Debug|Any CPU
1921
Release|Any CPU = Release|Any CPU
2022
EndGlobalSection
2123
GlobalSection(ProjectConfigurationPlatforms) = postSolution
22-
{91E18C69-CD87-4305-B241-1DD04DB118F8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
23-
{91E18C69-CD87-4305-B241-1DD04DB118F8}.Debug|Any CPU.Build.0 = Debug|Any CPU
24-
{91E18C69-CD87-4305-B241-1DD04DB118F8}.Release|Any CPU.ActiveCfg = Release|Any CPU
25-
{91E18C69-CD87-4305-B241-1DD04DB118F8}.Release|Any CPU.Build.0 = Release|Any CPU
2624
{EADE1DA0-404D-4504-86FD-B32BF70AF88F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
2725
{EADE1DA0-404D-4504-86FD-B32BF70AF88F}.Debug|Any CPU.Build.0 = Debug|Any CPU
2826
{EADE1DA0-404D-4504-86FD-B32BF70AF88F}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -39,6 +37,14 @@ Global
3937
{07DF34DD-19D6-4CFB-80A7-3CAC6B102A97}.Debug|Any CPU.Build.0 = Debug|Any CPU
4038
{07DF34DD-19D6-4CFB-80A7-3CAC6B102A97}.Release|Any CPU.ActiveCfg = Release|Any CPU
4139
{07DF34DD-19D6-4CFB-80A7-3CAC6B102A97}.Release|Any CPU.Build.0 = Release|Any CPU
40+
{8A3E7335-60D8-4A1E-B9EC-FF3CCA581FDB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
41+
{8A3E7335-60D8-4A1E-B9EC-FF3CCA581FDB}.Debug|Any CPU.Build.0 = Debug|Any CPU
42+
{8A3E7335-60D8-4A1E-B9EC-FF3CCA581FDB}.Release|Any CPU.ActiveCfg = Release|Any CPU
43+
{8A3E7335-60D8-4A1E-B9EC-FF3CCA581FDB}.Release|Any CPU.Build.0 = Release|Any CPU
44+
{1B692FC6-4C6B-46B4-BBE7-BF405937380F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
45+
{1B692FC6-4C6B-46B4-BBE7-BF405937380F}.Debug|Any CPU.Build.0 = Debug|Any CPU
46+
{1B692FC6-4C6B-46B4-BBE7-BF405937380F}.Release|Any CPU.ActiveCfg = Release|Any CPU
47+
{1B692FC6-4C6B-46B4-BBE7-BF405937380F}.Release|Any CPU.Build.0 = Release|Any CPU
4248
EndGlobalSection
4349
GlobalSection(SolutionProperties) = preSolution
4450
HideSolutionNode = FALSE

GraphQL.AzureFunctionsProxy/GraphQL.AzureFunctionsProxy.csproj

+1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ Prior Releases Notes:
3535
<PackageId>GraphQL.AzureFunctionsProxy</PackageId>
3636
<AssemblyVersion>12.0.0.1</AssemblyVersion>
3737
<FileVersion>12.0.0.1</FileVersion>
38+
<RootNamespace>HotChocolate.AzureFunctionsProxy</RootNamespace>
3839
</PropertyGroup>
3940

4041
<ItemGroup>

GraphQL.AzureFunctionsProxy/GraphQLAzureFunctionsMiddlewareExtensions.cs

-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
using System;
2-
using HotChocolate.AspNetCore;
32
using HotChocolate.AspNetCore.Serialization;
4-
using HotChocolate.AzureFunctionsProxy;
53
using HotChocolate.Execution;
64
using Microsoft.Extensions.DependencyInjection;
75

0 commit comments

Comments
 (0)