diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/HotChocolate.AspNetCore.csproj b/src/HotChocolate/AspNetCore/src/AspNetCore/HotChocolate.AspNetCore.csproj index f70b8eba00e..dee9fa3d99d 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore/HotChocolate.AspNetCore.csproj +++ b/src/HotChocolate/AspNetCore/src/AspNetCore/HotChocolate.AspNetCore.csproj @@ -11,6 +11,7 @@ + diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/Resources/index.html b/src/HotChocolate/AspNetCore/src/AspNetCore/Resources/index.html new file mode 100644 index 00000000000..6c70bcfe4d4 --- /dev/null +++ b/src/HotChocolate/AspNetCore/src/AspNetCore/Resources/index.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/GraphQLOverHttpSpecTests.cs b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/GraphQLOverHttpSpecTests.cs index 8f08c4c5d8b..6a098cbcf27 100644 --- a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/GraphQLOverHttpSpecTests.cs +++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/GraphQLOverHttpSpecTests.cs @@ -770,20 +770,13 @@ public async Task New_Query_With_Streams_3() .Add(response) .MatchInline( @"Headers: - Content-Type: multipart/mixed; boundary=""-""; charset=utf-8 + Content-Type: text/event-stream; charset=utf-8 --------------------------> Status Code: OK --------------------------> - - --- - Content-Type: application/json; charset=utf-8 - - {""data"":{},""hasNext"":true} - --- - Content-Type: application/json; charset=utf-8 - - {""path"":[],""data"":{""__typename"":""Query""},""hasNext"":false} - ----- + {""event"":""next"",""data"":{""data"":{},""hasNext"":true}} + {""event"":""next"",""data"":{""path"":[],""data"":{""__typename"":""Query""},""hasNext"":false}} + {""event"":""complete""} "); } } diff --git a/src/HotChocolate/AzureFunctions/src/HotChocolate.AzureFunctions.IsolatedProcess/AzureHeaderDictionary.cs b/src/HotChocolate/AzureFunctions/src/HotChocolate.AzureFunctions.IsolatedProcess/AzureHeaderDictionary.cs new file mode 100644 index 00000000000..c37e5efa041 --- /dev/null +++ b/src/HotChocolate/AzureFunctions/src/HotChocolate.AzureFunctions.IsolatedProcess/AzureHeaderDictionary.cs @@ -0,0 +1,105 @@ +using System.Collections; +using Microsoft.AspNetCore.Http; +using Microsoft.Azure.Functions.Worker.Http; +using Microsoft.Extensions.Primitives; +using Microsoft.Net.Http.Headers; + +namespace HotChocolate.AzureFunctions.IsolatedProcess; + +internal sealed class AzureHeaderDictionary : IHeaderDictionary +{ + private readonly HttpResponse _response; + private readonly HttpResponseData _responseData; + + public AzureHeaderDictionary(HttpResponse response, HttpResponseData responseData) + { + _response = response; + _responseData = responseData; + } + + public void Add(KeyValuePair item) + { + _response.Headers.Add(item.Key, item.Value); + _responseData.Headers.Add(item.Key, (IEnumerable)item.Value); + } + + public void Clear() + { + _response.Headers.Clear(); + _responseData.Headers.Clear(); + } + + public bool Contains(KeyValuePair item) + { + throw new NotSupportedException(); + } + + public void CopyTo(KeyValuePair[] array, int arrayIndex) + { + throw new NotSupportedException(); + } + + public bool Remove(KeyValuePair item) + { + var success = _response.Headers.Remove(item); + _responseData.Headers.Remove(item.Key); + return success; + } + + public int Count => _response.Headers.Count; + + public bool IsReadOnly => _response.Headers.IsReadOnly; + + public void Add(string key, StringValues value) + { + _response.Headers.Add(key, value); + _responseData.Headers.Add(key, (IEnumerable)value); + } + + public bool ContainsKey(string key) + => _response.Headers.ContainsKey(key); + + public bool Remove(string key) + { + var success = _response.Headers.Remove(key); + _responseData.Headers.Remove(key); + return success; + } + + public bool TryGetValue(string key, out StringValues value) + => _response.Headers.TryGetValue(key, out value); + + public StringValues this[string key] + { + get => _response.Headers[key]; + set + { + _response.Headers[key] = value; + _responseData.Headers.Add(key, (IEnumerable)value); + } + } + + public long? ContentLength + { + get => _response.Headers.ContentLength; + set + { + _response.Headers.ContentLength = value; + _responseData.Headers.Add( + HeaderNames.ContentLength, + (string?)_response.Headers[HeaderNames.ContentLength]); + } + } + + public ICollection Keys => _response.Headers.Keys; + + public ICollection Values => _response.Headers.Values; + + public IEnumerator> GetEnumerator() + => _response.Headers.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } +} diff --git a/src/HotChocolate/AzureFunctions/src/HotChocolate.AzureFunctions.IsolatedProcess/AzureHttpContext.cs b/src/HotChocolate/AzureFunctions/src/HotChocolate.AzureFunctions.IsolatedProcess/AzureHttpContext.cs new file mode 100644 index 00000000000..5372c02b82f --- /dev/null +++ b/src/HotChocolate/AzureFunctions/src/HotChocolate.AzureFunctions.IsolatedProcess/AzureHttpContext.cs @@ -0,0 +1,99 @@ +using System.Security.Claims; +using HotChocolate.AspNetCore; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Azure.Functions.Worker.Http; +using Microsoft.Extensions.Primitives; +using Microsoft.Net.Http.Headers; + +namespace HotChocolate.AzureFunctions.IsolatedProcess; + +internal sealed class AzureHttpContext : HttpContext +{ + private readonly DefaultHttpContext _innerContext; + private readonly AzureHttpResponse _innerResponse; + + public AzureHttpContext(HttpRequestData requestData) + { + if (requestData is null) + { + throw new ArgumentNullException(nameof(requestData)); + } + + _innerContext = new DefaultHttpContext(); + _innerResponse = new AzureHttpResponse(_innerContext.Response, requestData); + + var contentType = + requestData.Headers.TryGetValues(HeaderNames.ContentType, out var headerValue) + ? headerValue.First() + : ContentType.Json; + + _innerContext.User = new ClaimsPrincipal(requestData.Identities); + + var request = _innerContext.Request; + request.Method = requestData.Method; + request.Scheme = requestData.Url.Scheme; + request.Host = new HostString(requestData.Url.Host, requestData.Url.Port); + request.Path = new PathString(requestData.Url.AbsolutePath); + request.QueryString = new QueryString(requestData.Url.Query); + request.Body = requestData.Body; + request.ContentType = contentType; + + foreach (var (key, value) in requestData.Headers) + { + request.Headers.TryAdd(key, new StringValues(value.ToArray())); + } + } + + public override IFeatureCollection Features => _innerContext.Features; + + public override HttpRequest Request => _innerContext.Request; + + public override HttpResponse Response => _innerResponse; + + public override ConnectionInfo Connection => _innerContext.Connection; + + public override WebSocketManager WebSockets => _innerContext.WebSockets; + + public override ClaimsPrincipal User + { + get => _innerContext.User; + set => _innerContext.User = value; + } + + public override IDictionary Items + { + get => _innerContext.Items; + set => _innerContext.Items = value; + } + + public override IServiceProvider RequestServices + { + get => _innerContext.RequestServices; + set => _innerContext.RequestServices = value; + } + + public override CancellationToken RequestAborted + { + get => _innerContext.RequestAborted; + set => _innerContext.RequestAborted = value; + } + + public override string TraceIdentifier + { + get => _innerContext.TraceIdentifier; + set => _innerContext.TraceIdentifier = value; + } + + public override ISession Session + { + get => _innerContext.Session; + set => _innerContext.Session = value; + } + + public override void Abort() + => _innerContext.Abort(); + + public HttpResponseData CreateResponseData() + => _innerResponse.ResponseData; +} diff --git a/src/HotChocolate/AzureFunctions/src/HotChocolate.AzureFunctions.IsolatedProcess/AzureHttpResponse.cs b/src/HotChocolate/AzureFunctions/src/HotChocolate.AzureFunctions.IsolatedProcess/AzureHttpResponse.cs new file mode 100644 index 00000000000..b8699057ef6 --- /dev/null +++ b/src/HotChocolate/AzureFunctions/src/HotChocolate.AzureFunctions.IsolatedProcess/AzureHttpResponse.cs @@ -0,0 +1,97 @@ +using System.Net; +using Microsoft.AspNetCore.Http; +using Microsoft.Azure.Functions.Worker.Http; +using Microsoft.Net.Http.Headers; + +namespace HotChocolate.AzureFunctions.IsolatedProcess; + +internal sealed class AzureHttpResponse : HttpResponse +{ + private readonly HttpResponse _response; + private readonly HttpRequestData _requestData; + private readonly object _sync = new(); + private HttpResponseData? _responseData; + private AzureHeaderDictionary? _headers; + + public AzureHttpResponse(HttpResponse response, HttpRequestData requestData) + { + _response = response; + _requestData = requestData; + } + + internal HttpResponseData ResponseData + { + get + { + if (_responseData is null) + { + lock (_sync) + { + if (_responseData is null) + { + _responseData = _requestData.CreateResponse(); + } + } + } + + return _responseData; + } + } + + public override HttpContext HttpContext => _response.HttpContext; + + public override int StatusCode + { + get => (int)ResponseData.StatusCode; + set => ResponseData.StatusCode = (HttpStatusCode)value; + } + + public override IHeaderDictionary Headers + { + get + { + if (_headers is null) + { + lock (_sync) + { + if (_headers is null) + { + _headers = new AzureHeaderDictionary(_response, ResponseData); + } + } + } + return _headers; + } + } + + public override Stream Body + { + get => ResponseData.Body; + set => ResponseData.Body = value; + } + + public override long? ContentLength + { + get => Headers.ContentLength; + set => Headers.ContentLength = value; + } + + public override string? ContentType + { + get => Headers[HeaderNames.ContentType]; + set => Headers[HeaderNames.ContentType] = value; + } + + public override IResponseCookies Cookies => _response.Cookies; + + public override bool HasStarted => _response.HasStarted; + + public override void OnStarting(Func callback, object state) + => throw new NotSupportedException(); + + public override void OnCompleted(Func callback, object state) + => throw new NotSupportedException(); + + public override void Redirect(string location, bool permanent) + => throw new NotSupportedException(); +} diff --git a/src/HotChocolate/AzureFunctions/src/HotChocolate.AzureFunctions.IsolatedProcess/Extensions/GraphQLRequestExecutorExtensions.cs b/src/HotChocolate/AzureFunctions/src/HotChocolate.AzureFunctions.IsolatedProcess/Extensions/GraphQLRequestExecutorExtensions.cs index 2437170199e..b740abcf1a4 100644 --- a/src/HotChocolate/AzureFunctions/src/HotChocolate.AzureFunctions.IsolatedProcess/Extensions/GraphQLRequestExecutorExtensions.cs +++ b/src/HotChocolate/AzureFunctions/src/HotChocolate.AzureFunctions.IsolatedProcess/Extensions/GraphQLRequestExecutorExtensions.cs @@ -1,52 +1,33 @@ +using HotChocolate.AzureFunctions.IsolatedProcess; using Microsoft.Azure.Functions.Worker.Http; -namespace HotChocolate.AzureFunctions.IsolatedProcess; +namespace HotChocolate.AzureFunctions; public static class GraphQLRequestExecutorExtensions { public static Task ExecuteAsync( - this IGraphQLRequestExecutor graphqlRequestExecutor, - HttpRequestData httpRequestData) + this IGraphQLRequestExecutor executor, + HttpRequestData requestData) { - if (graphqlRequestExecutor is null) + if (executor is null) { - throw new ArgumentNullException(nameof(graphqlRequestExecutor)); + throw new ArgumentNullException(nameof(executor)); } - if (httpRequestData is null) + if (requestData is null) { - throw new ArgumentNullException(nameof(httpRequestData)); + throw new ArgumentNullException(nameof(requestData)); } - // Factored out Async logic to Address SonarCloud concern for exceptions in Async flow ... - return ExecuteGraphQLRequestInternalAsync(graphqlRequestExecutor, httpRequestData); + return ExecuteGraphQLRequestInternalAsync(executor, requestData); } private static async Task ExecuteGraphQLRequestInternalAsync( - IGraphQLRequestExecutor graphqlRequestExecutor, - HttpRequestData httpRequestData) + IGraphQLRequestExecutor executor, + HttpRequestData requestData) { - // 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); + var context = new AzureHttpContext(requestData); + await executor.ExecuteAsync(context).ConfigureAwait(false); + return context.CreateResponseData(); } } diff --git a/src/HotChocolate/AzureFunctions/src/HotChocolate.AzureFunctions.IsolatedProcess/Extensions/HotChocolateAzFuncIsolatedProcessHostBuilderExtensions.cs b/src/HotChocolate/AzureFunctions/src/HotChocolate.AzureFunctions.IsolatedProcess/Extensions/HotChocolateAzFuncIsolatedProcessHostBuilderExtensions.cs index e9b2e11e018..f29c37eefce 100644 --- a/src/HotChocolate/AzureFunctions/src/HotChocolate.AzureFunctions.IsolatedProcess/Extensions/HotChocolateAzFuncIsolatedProcessHostBuilderExtensions.cs +++ b/src/HotChocolate/AzureFunctions/src/HotChocolate.AzureFunctions.IsolatedProcess/Extensions/HotChocolateAzFuncIsolatedProcessHostBuilderExtensions.cs @@ -50,11 +50,8 @@ public static class HotChocolateAzFuncIsolatedProcessHostBuilderExtensions throw new ArgumentNullException(nameof(configure)); } - hostBuilder.ConfigureServices(services => - { - var executorBuilder = services.AddGraphQLFunction(maxAllowedRequestSize, apiRoute); - configure(executorBuilder); - }); + hostBuilder.ConfigureServices( + s => configure(s.AddGraphQLFunction(maxAllowedRequestSize, apiRoute))); return hostBuilder; } diff --git a/src/HotChocolate/AzureFunctions/src/HotChocolate.AzureFunctions.IsolatedProcess/Extensions/HttpRequestDataExtensions.cs b/src/HotChocolate/AzureFunctions/src/HotChocolate.AzureFunctions.IsolatedProcess/Extensions/HttpRequestDataExtensions.cs deleted file mode 100644 index a2538bbed6a..00000000000 --- a/src/HotChocolate/AzureFunctions/src/HotChocolate.AzureFunctions.IsolatedProcess/Extensions/HttpRequestDataExtensions.cs +++ /dev/null @@ -1,34 +0,0 @@ -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 ReadResponseContentAsync( - this HttpResponseData httpResponseData) - => await httpResponseData.Body.ReadStreamAsStringAsync().ConfigureAwait(false); -} diff --git a/src/HotChocolate/AzureFunctions/src/HotChocolate.AzureFunctions.IsolatedProcess/HttpContextShim.cs b/src/HotChocolate/AzureFunctions/src/HotChocolate.AzureFunctions.IsolatedProcess/HttpContextShim.cs deleted file mode 100644 index eb5e2b23c89..00000000000 --- a/src/HotChocolate/AzureFunctions/src/HotChocolate.AzureFunctions.IsolatedProcess/HttpContextShim.cs +++ /dev/null @@ -1,135 +0,0 @@ -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; } - - /// - /// 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. - /// - public static Task 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 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); - } - - /// - /// Create an HttpResponseData containing the proxied response content results; - /// marshaled back from the HttpContext. - /// - /// - public async Task 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; - } - } -} diff --git a/src/HotChocolate/AzureFunctions/src/HotChocolate.AzureFunctions/DefaultGraphQLRequestExecutor.cs b/src/HotChocolate/AzureFunctions/src/HotChocolate.AzureFunctions/DefaultGraphQLRequestExecutor.cs index bef5c4bd906..339e744f104 100644 --- a/src/HotChocolate/AzureFunctions/src/HotChocolate.AzureFunctions/DefaultGraphQLRequestExecutor.cs +++ b/src/HotChocolate/AzureFunctions/src/HotChocolate.AzureFunctions/DefaultGraphQLRequestExecutor.cs @@ -16,18 +16,18 @@ public DefaultGraphQLRequestExecutor(RequestDelegate pipeline, GraphQLServerOpti _options = options ?? throw new ArgumentNullException(nameof(options)); } - public async Task ExecuteAsync(HttpRequest request) + public async Task ExecuteAsync(HttpContext context) { - if (request is null) + if (context is null) { - throw new ArgumentNullException(nameof(request)); + throw new ArgumentNullException(nameof(context)); } // First we need to populate the HttpContext with the current GraphQL server options ... - request.HttpContext.Items.Add(nameof(GraphQLServerOptions), _options); + context.Items.Add(nameof(GraphQLServerOptions), _options); // after that we can execute the pipeline ... - await _pipeline.Invoke(request.HttpContext).ConfigureAwait(false); + await _pipeline.Invoke(context).ConfigureAwait(false); // last we return out empty result that we have cached in this class. // the pipeline actually takes care of writing the result to the diff --git a/src/HotChocolate/AzureFunctions/src/HotChocolate.AzureFunctions/Extensions/HotChocolateAzureFunctionServiceCollectionExtensions.cs b/src/HotChocolate/AzureFunctions/src/HotChocolate.AzureFunctions/Extensions/HotChocolateAzureFunctionServiceCollectionExtensions.cs index 85ed3efec28..7b4ab13de99 100644 --- a/src/HotChocolate/AzureFunctions/src/HotChocolate.AzureFunctions/Extensions/HotChocolateAzureFunctionServiceCollectionExtensions.cs +++ b/src/HotChocolate/AzureFunctions/src/HotChocolate.AzureFunctions/Extensions/HotChocolateAzureFunctionServiceCollectionExtensions.cs @@ -37,7 +37,9 @@ public static class HotChocolateAzureFunctionServiceCollectionExtensions string apiRoute = GraphQLAzureFunctionsConstants.DefaultGraphQLRoute) { if (services is null) + { throw new ArgumentNullException(nameof(services)); + } var executorBuilder = services.AddGraphQLServer(maxAllowedRequestSize: maxAllowedRequestSize); @@ -54,7 +56,7 @@ public static class HotChocolateAzureFunctionServiceCollectionExtensions services.TryAddEnumerable( ServiceDescriptor.Singleton()); - //Add the Request Executor Dependency... + // Add the Request Executor Dependency... services.AddAzureFunctionsGraphQLRequestExecutorDependency(apiRoute); return executorBuilder; @@ -65,19 +67,18 @@ public static class HotChocolateAzureFunctionServiceCollectionExtensions /// in-process and isolate-process. Normal configuration should use AddGraphQLFunction() /// extension instead which correctly call this internally. /// - public static IServiceCollection AddAzureFunctionsGraphQLRequestExecutorDependency( + private static IServiceCollection AddAzureFunctionsGraphQLRequestExecutorDependency( this IServiceCollection services, string apiRoute = GraphQLAzureFunctionsConstants.DefaultGraphQLRoute ) { services.AddSingleton(sp => { - PathString path = apiRoute?.TrimEnd('/'); + PathString path = apiRoute.TrimEnd('/'); var fileProvider = CreateFileProvider(); var options = new GraphQLServerOptions(); - foreach (var configure in - sp.GetServices>()) + foreach (var configure in sp.GetServices>()) { configure(options); } diff --git a/src/HotChocolate/AzureFunctions/src/HotChocolate.AzureFunctions/Extensions/HotChocolateFunctionsHostBuilderExtensions.cs b/src/HotChocolate/AzureFunctions/src/HotChocolate.AzureFunctions/Extensions/HotChocolateFunctionsHostBuilderExtensions.cs index 17e5089ec10..d1241d698a5 100644 --- a/src/HotChocolate/AzureFunctions/src/HotChocolate.AzureFunctions/Extensions/HotChocolateFunctionsHostBuilderExtensions.cs +++ b/src/HotChocolate/AzureFunctions/src/HotChocolate.AzureFunctions/Extensions/HotChocolateFunctionsHostBuilderExtensions.cs @@ -1,6 +1,7 @@ using HotChocolate.AzureFunctions; using HotChocolate.Execution.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; namespace Microsoft.Azure.Functions.Extensions.DependencyInjection; @@ -34,7 +35,9 @@ public static class HotChocolateFunctionsHostBuilderExtensions string apiRoute = GraphQLAzureFunctionsConstants.DefaultGraphQLRoute) { if (hostBuilder is null) + { throw new ArgumentNullException(nameof(hostBuilder)); + } return hostBuilder.Services.AddGraphQLFunction(maxAllowedRequestSize, apiRoute); } @@ -47,7 +50,7 @@ public static class HotChocolateFunctionsHostBuilderExtensions /// /// The . /// - /// + /// /// The GraphQL Configuration function that will be invoked, for chained configuration, /// when the Host is built. /// @@ -65,17 +68,22 @@ public static class HotChocolateFunctionsHostBuilderExtensions /// public static IFunctionsHostBuilder AddGraphQLFunction( this IFunctionsHostBuilder hostBuilder, - Action graphqlConfigureFunc, + Action configure, int maxAllowedRequestSize = GraphQLAzureFunctionsConstants.DefaultMaxRequests, - string apiRoute = GraphQLAzureFunctionsConstants.DefaultGraphQLRoute - ) + string apiRoute = GraphQLAzureFunctionsConstants.DefaultGraphQLRoute) { - if (graphqlConfigureFunc is null) - throw new ArgumentNullException(nameof(graphqlConfigureFunc)); + if (hostBuilder is null) + { + throw new ArgumentNullException(nameof(hostBuilder)); + } + + if (configure is null) + { + throw new ArgumentNullException(nameof(configure)); + } - // NOTE: HostBuilder null check will be done by AddGraphQLFunction() ... var executorBuilder = hostBuilder.AddGraphQLFunction(maxAllowedRequestSize, apiRoute); - graphqlConfigureFunc.Invoke(executorBuilder); + configure.Invoke(executorBuilder); return hostBuilder; } diff --git a/src/HotChocolate/AzureFunctions/src/HotChocolate.AzureFunctions/Extensions/HttpContextExtensions.cs b/src/HotChocolate/AzureFunctions/src/HotChocolate.AzureFunctions/Extensions/HttpContextExtensions.cs deleted file mode 100644 index b7f98928a60..00000000000 --- a/src/HotChocolate/AzureFunctions/src/HotChocolate.AzureFunctions/Extensions/HttpContextExtensions.cs +++ /dev/null @@ -1,84 +0,0 @@ -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.DependencyInjection; - -namespace HotChocolate.AzureFunctions.IsolatedProcess.Extensions; - -public static class HttpContextExtensions -{ - public static IServiceProvider SetCurrentHttpContext( - this IServiceProvider serviceProvider, - HttpContext httpContext) - { - // Ensure that we enable support for HttpContext injection within HotChocolate - // (e.g. into Resolvers) for low-level access. - // NOTE: This is leveraged in Unit Tests as well as in Azure Functions - // Isolated process flow. - var httpContextAccessor = serviceProvider.GetService(); - if (httpContextAccessor != null) - httpContextAccessor.HttpContext = httpContext; - - return serviceProvider; - } - - public static async Task ReadResponseBytesAsync(this HttpContext httpContext) - { - Stream? responseStream = httpContext?.Response?.Body; - switch (responseStream) - { - case null: - return null; - case MemoryStream alreadyMemoryStream: - return alreadyMemoryStream.ToArray(); - default: - await using (var memoryStream = new MemoryStream()) - { - await responseStream.CopyToAsync(memoryStream).ConfigureAwait(false); - return memoryStream.ToArray(); - } - } - } - - public static async Task ReadStreamAsStringAsync(this Stream responseStream) - { - string? responseContent = null; - var originalPosition = responseStream.Position; - - if (responseStream.CanSeek) - responseStream.Seek(0, SeekOrigin.Begin); - - using (var responseReader = new StreamReader(responseStream)) - responseContent = await responseReader.ReadToEndAsync().ConfigureAwait(false); - - if (responseStream.CanSeek) - responseStream.Seek(originalPosition, SeekOrigin.Begin); - - return responseContent; - } - - public static async Task ReadResponseContentAsync(this HttpContext httpContext) - { - Stream? responseStream = httpContext?.Response?.Body; - return responseStream != null - ? await responseStream.ReadStreamAsStringAsync().ConfigureAwait(false) - : null; - } - - public static Uri GetAbsoluteUri(this HttpRequest httpRequest) - { - var urlBuilder = new UriBuilder(httpRequest.Scheme, httpRequest.Host.Host); - - if (httpRequest.Host.Port != null) - urlBuilder.Port = (int)httpRequest.Host.Port; - - urlBuilder.Path = httpRequest.Path.Value; - urlBuilder.Query = httpRequest.QueryString.Value; - - return urlBuilder.Uri; - } - - public static void DisposeSafely(this HttpContext httpContext) - { - httpContext?.Request?.Body?.Dispose(); - httpContext?.Response?.Body?.Dispose(); - } -} diff --git a/src/HotChocolate/AzureFunctions/src/HotChocolate.AzureFunctions/GraphQLAttribute.cs b/src/HotChocolate/AzureFunctions/src/HotChocolate.AzureFunctions/GraphQLAttribute.cs index 690140354ad..45bf08fb186 100644 --- a/src/HotChocolate/AzureFunctions/src/HotChocolate.AzureFunctions/GraphQLAttribute.cs +++ b/src/HotChocolate/AzureFunctions/src/HotChocolate.AzureFunctions/GraphQLAttribute.cs @@ -4,6 +4,6 @@ namespace HotChocolate.AzureFunctions; [Binding] [AttributeUsage(AttributeTargets.Parameter)] -public class GraphQLAttribute : Attribute +public sealed class GraphQLAttribute : Attribute { } diff --git a/src/HotChocolate/AzureFunctions/src/HotChocolate.AzureFunctions/GraphQLAzureFunctionsConstants.cs b/src/HotChocolate/AzureFunctions/src/HotChocolate.AzureFunctions/GraphQLAzureFunctionsConstants.cs index 18b6fc32d1d..6ff9d79e426 100644 --- a/src/HotChocolate/AzureFunctions/src/HotChocolate.AzureFunctions/GraphQLAzureFunctionsConstants.cs +++ b/src/HotChocolate/AzureFunctions/src/HotChocolate.AzureFunctions/GraphQLAzureFunctionsConstants.cs @@ -1,10 +1,7 @@ namespace HotChocolate.AzureFunctions; -public static class GraphQLAzureFunctionsConstants +internal static class GraphQLAzureFunctionsConstants { - public const string DefaultAzFuncHttpTriggerRoute = "graphql/{**slug}"; public const string DefaultGraphQLRoute = "/api/graphql"; - public const string DefaultJsonContentType = "application/json"; - public const string DefaultBcpContentType = "text/html"; public const int DefaultMaxRequests = 20 * 1000 * 1000; } diff --git a/src/HotChocolate/AzureFunctions/src/HotChocolate.AzureFunctions/GraphQLExtensions.cs b/src/HotChocolate/AzureFunctions/src/HotChocolate.AzureFunctions/GraphQLExtensions.cs index 1209cc0652d..8869c41acdd 100644 --- a/src/HotChocolate/AzureFunctions/src/HotChocolate.AzureFunctions/GraphQLExtensions.cs +++ b/src/HotChocolate/AzureFunctions/src/HotChocolate.AzureFunctions/GraphQLExtensions.cs @@ -6,7 +6,7 @@ namespace HotChocolate.AzureFunctions; [Extension("GraphQLExtensions")] -internal class GraphQLExtensions : IExtensionConfigProvider +internal sealed class GraphQLExtensions : IExtensionConfigProvider { private readonly IServiceProvider _services; diff --git a/src/HotChocolate/AzureFunctions/src/HotChocolate.AzureFunctions/HotChocolate.AzureFunctions.csproj b/src/HotChocolate/AzureFunctions/src/HotChocolate.AzureFunctions/HotChocolate.AzureFunctions.csproj index 490cee45df7..559552b4cd7 100644 --- a/src/HotChocolate/AzureFunctions/src/HotChocolate.AzureFunctions/HotChocolate.AzureFunctions.csproj +++ b/src/HotChocolate/AzureFunctions/src/HotChocolate.AzureFunctions/HotChocolate.AzureFunctions.csproj @@ -10,9 +10,9 @@ - - <_Parameter1>$(AssemblyName).Tests - + + + diff --git a/src/HotChocolate/AzureFunctions/src/HotChocolate.AzureFunctions/HttpContextBuilder.cs b/src/HotChocolate/AzureFunctions/src/HotChocolate.AzureFunctions/HttpContextBuilder.cs deleted file mode 100644 index 5a01d34060f..00000000000 --- a/src/HotChocolate/AzureFunctions/src/HotChocolate.AzureFunctions/HttpContextBuilder.cs +++ /dev/null @@ -1,75 +0,0 @@ -using System.Net.Http.Headers; -using System.Security.Claims; -using System.Text; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Primitives; - -namespace HotChocolate.AzureFunctions; - -public class HttpContextBuilder -{ - public virtual HttpContext CreateHttpContext( - string requestHttpMethod, - Uri requestUri, - string? requestBody = null, - string requestBodyContentType = GraphQLAzureFunctionsConstants.DefaultJsonContentType, - HttpHeaders? requestHeaders = null, - IEnumerable? claimsIdentities = null, - IDictionary? contextItems = null - ) - { - // Initialize the root Http Context (Container)... - var httpContext = new DefaultHttpContext(); - - // Initialize the Http Request... - var httpRequest = httpContext.Request; - httpRequest.Method = requestHttpMethod ?? - throw new ArgumentNullException(nameof(requestHttpMethod)); - httpRequest.Scheme = requestUri?.Scheme ?? - throw new ArgumentNullException(nameof(requestUri)); - httpRequest.Host = new HostString(requestUri.Host, requestUri.Port); - httpRequest.Path = new PathString(requestUri.AbsolutePath); - httpRequest.QueryString = new QueryString(requestUri.Query); - - // Ensure we marshall across all Headers from the Client Request... - // Note: This should also handle Cookies since Cookies are stored as a Header value.... - if (requestHeaders?.Any() == true) - foreach (var (key, value) in requestHeaders) - httpRequest.Headers.TryAdd(key, new StringValues(value.ToArray())); - - if (!string.IsNullOrEmpty(requestBody)) - { - // Initialize a valid Stream for the Request (must be tracked & Disposed of!) - var requestBodyBytes = Encoding.UTF8.GetBytes(requestBody); - httpRequest.Body = new MemoryStream(requestBodyBytes); - httpRequest.ContentType = requestBodyContentType; - httpRequest.ContentLength = requestBodyBytes.Length; - } - - // Initialize the Http Response... - var httpResponse = httpContext.Response; - - // Initialize a valid Stream for the Response (must be tracked & Disposed of!) - // NOTE: Default Body is a NullStream... which ignores all Reads/Writes. - httpResponse.Body = new MemoryStream(); - - // Proxy over any possible authentication claims if available - var identities = claimsIdentities as ClaimsIdentity[] ?? claimsIdentities?.ToArray(); - - if (identities?.Any() == true) - { - httpContext.User = new ClaimsPrincipal(identities); - } - - // Set the Custom Context Items if specified... - if(contextItems?.Any() == true) - { - foreach (var item in contextItems) - { - httpContext.Items.TryAdd(item.Key, item.Value); - } - } - - return httpContext; - } -} diff --git a/src/HotChocolate/AzureFunctions/src/HotChocolate.AzureFunctions/IGraphQLRequestExecutor.cs b/src/HotChocolate/AzureFunctions/src/HotChocolate.AzureFunctions/IGraphQLRequestExecutor.cs index 2a057df23a9..1606498c229 100644 --- a/src/HotChocolate/AzureFunctions/src/HotChocolate.AzureFunctions/IGraphQLRequestExecutor.cs +++ b/src/HotChocolate/AzureFunctions/src/HotChocolate.AzureFunctions/IGraphQLRequestExecutor.cs @@ -12,10 +12,22 @@ public interface IGraphQLRequestExecutor /// Executes a GraphQL over HTTP request. /// /// - /// The GraphQL request. + /// The HTTP request. /// /// /// returns the GraphQL HTTP response. /// - Task ExecuteAsync(HttpRequest request); + Task ExecuteAsync(HttpRequest request) => ExecuteAsync(request.HttpContext); + + /// + /// Executes a GraphQL over HTTP request. + /// + /// + /// The HTTP context. + /// + /// + /// returns the GraphQL HTTP response. + /// + Task ExecuteAsync(HttpContext context); } + diff --git a/src/HotChocolate/AzureFunctions/src/HotChocolate.AzureFunctions/PipelineBuilder.cs b/src/HotChocolate/AzureFunctions/src/HotChocolate.AzureFunctions/PipelineBuilder.cs index 26c3e4a4d6a..12e1489904a 100644 --- a/src/HotChocolate/AzureFunctions/src/HotChocolate.AzureFunctions/PipelineBuilder.cs +++ b/src/HotChocolate/AzureFunctions/src/HotChocolate.AzureFunctions/PipelineBuilder.cs @@ -6,7 +6,7 @@ namespace HotChocolate.AzureFunctions; -internal class PipelineBuilder +internal sealed class PipelineBuilder { private static readonly ParameterExpression _context = Expression.Parameter(typeof(HttpContext), "context"); diff --git a/src/HotChocolate/AzureFunctions/test/HotChocolate.AzureFunctions.IsolatedProcess.Tests/Helpers/MockHttpRequestData.cs b/src/HotChocolate/AzureFunctions/test/HotChocolate.AzureFunctions.IsolatedProcess.Tests/Helpers/MockHttpRequestData.cs index 056e496c68e..06d072db584 100644 --- a/src/HotChocolate/AzureFunctions/test/HotChocolate.AzureFunctions.IsolatedProcess.Tests/Helpers/MockHttpRequestData.cs +++ b/src/HotChocolate/AzureFunctions/test/HotChocolate.AzureFunctions.IsolatedProcess.Tests/Helpers/MockHttpRequestData.cs @@ -21,7 +21,7 @@ public sealed class MockHttpRequestData : HttpRequestData, IDisposable string requestHttpMethod, Uri requestUri, string? requestBody = null, - string requestBodyContentType = GraphQLAzureFunctionsConstants.DefaultJsonContentType, + string requestBodyContentType = TestConstants.DefaultJsonContentType, HttpHeaders? requestHeaders = null, IEnumerable? claimsIdentities = null) : base(functionContext) diff --git a/src/HotChocolate/AzureFunctions/test/HotChocolate.AzureFunctions.IsolatedProcess.Tests/Helpers/TestHttpRequestDataHelper.cs b/src/HotChocolate/AzureFunctions/test/HotChocolate.AzureFunctions.IsolatedProcess.Tests/Helpers/TestHttpRequestDataHelper.cs index 554a764d87c..94c04b4b650 100644 --- a/src/HotChocolate/AzureFunctions/test/HotChocolate.AzureFunctions.IsolatedProcess.Tests/Helpers/TestHttpRequestDataHelper.cs +++ b/src/HotChocolate/AzureFunctions/test/HotChocolate.AzureFunctions.IsolatedProcess.Tests/Helpers/TestHttpRequestDataHelper.cs @@ -1,7 +1,7 @@ using System; -using HotChocolate.AzureFunctions.Tests.Helpers; using Microsoft.AspNetCore.Http; using Microsoft.Azure.Functions.Worker.Http; +using static HotChocolate.AzureFunctions.Tests.Helpers.TestHttpContextHelper; using static Microsoft.Net.Http.Headers.HeaderNames; using IO = System.IO; @@ -16,11 +16,11 @@ public class TestHttpRequestDataHelper HttpRequestData httpRequestData = new MockHttpRequestData( new MockFunctionContext(serviceProvider), HttpMethods.Post, - TestHttpContextHelper.DefaultAzFuncGraphQLUri, - requestBody: TestHttpContextHelper.CreateGraphQLRequestBody(graphqlQuery)); + DefaultAzFuncGraphQLUri, + requestBody: CreateRequestBody(graphqlQuery)); //Ensure we accept Json for GraphQL requests... - httpRequestData.Headers.Add(Accept, GraphQLAzureFunctionsConstants.DefaultJsonContentType); + httpRequestData.Headers.Add(Accept, TestConstants.DefaultJsonContentType); return httpRequestData; } @@ -32,11 +32,10 @@ public class TestHttpRequestDataHelper HttpRequestData httpRequestData = new MockHttpRequestData( new MockFunctionContext(serviceProvider), HttpMethods.Get, - new Uri(IO.Path.Combine(TestHttpContextHelper.DefaultAzFuncGraphQLUri.ToString(), path)) - ); + new Uri(IO.Path.Combine(DefaultAzFuncGraphQLUri.ToString(), path))); //Ensure we accept Text/Html for BCP requests... - httpRequestData.Headers.Add(Accept, GraphQLAzureFunctionsConstants.DefaultBcpContentType); + httpRequestData.Headers.Add(Accept, TestConstants.DefaultBcpContentType); return httpRequestData; } diff --git a/src/HotChocolate/AzureFunctions/test/HotChocolate.AzureFunctions.IsolatedProcess.Tests/IsolatedProcessEndToEndTests.cs b/src/HotChocolate/AzureFunctions/test/HotChocolate.AzureFunctions.IsolatedProcess.Tests/IsolatedProcessEndToEndTests.cs index 8eeb462151e..c6d9653b528 100644 --- a/src/HotChocolate/AzureFunctions/test/HotChocolate.AzureFunctions.IsolatedProcess.Tests/IsolatedProcessEndToEndTests.cs +++ b/src/HotChocolate/AzureFunctions/test/HotChocolate.AzureFunctions.IsolatedProcess.Tests/IsolatedProcessEndToEndTests.cs @@ -1,10 +1,11 @@ +using System.IO; +using System.Text; using System.Threading.Tasks; using HotChocolate.AzureFunctions.IsolatedProcess.Tests.Helpers; using HotChocolate.Types; -using Microsoft.AspNetCore.Http; using Microsoft.Azure.Functions.Extensions.DependencyInjection; +using Microsoft.Azure.Functions.Worker.Http; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; using Newtonsoft.Json.Linq; namespace HotChocolate.AzureFunctions.IsolatedProcess.Tests; @@ -26,20 +27,19 @@ public async Task AzFuncIsolatedProcess_EndToEndTestAsync() var requestExecutor = host.Services.GetRequiredService(); // Build an HttpRequestData that is valid for the Isolated Process to execute with... - var httpRequestData = TestHttpRequestDataHelper.NewGraphQLHttpRequestData(host.Services, @" - query { + var request = TestHttpRequestDataHelper.NewGraphQLHttpRequestData( + host.Services, + @"query { person - } - "); + }"); // Execute Query Test for end-to-end validation... // NOTE: This uses the new Az Func Isolated Process extension // to execute via HttpRequestData... - var httpResponseData = - await requestExecutor.ExecuteAsync(httpRequestData).ConfigureAwait(false); + var response = await requestExecutor.ExecuteAsync(request).ConfigureAwait(false); // Read, Parse & Validate the response... - var resultContent = await httpResponseData.ReadResponseContentAsync().ConfigureAwait(false); + var resultContent = await ReadResponseAsStringAsync(response).ConfigureAwait(false); Assert.False(string.IsNullOrWhiteSpace(resultContent)); dynamic json = JObject.Parse(resultContent!); @@ -47,54 +47,6 @@ public async Task AzFuncIsolatedProcess_EndToEndTestAsync() Assert.Equal("Luke Skywalker",json.data.person.ToString()); } - [Fact] - public async Task AzFuncIsolatedProcess_HttpContextAccessorTestAsync() - { - var host = new MockIsolatedProcessHostBuilder() - .ConfigureServices(services => - { - services.AddHttpContextAccessor(); - }) - .AddGraphQLFunction(graphQL => - { - graphQL.AddQueryType( - d => d.Name("Query") - .Field("isHttpContextInjected") - .Resolve(context => - { - var httpContext = context.Services.GetService()? - .HttpContext; - return httpContext != null; - })); - }) - .Build(); - - // The executor should resolve without error as a Required service... - var requestExecutor = host.Services.GetRequiredService(); - - // Build an HttpRequestData that is valid for the Isolated Process to execute with... - var httpRequestData = TestHttpRequestDataHelper.NewGraphQLHttpRequestData( - host.Services, - @"query { - isHttpContextInjected - }"); - - // Execute Query Test for end-to-end validation... - // NOTE: This uses the new Az Func Isolated Process extension to execute - // via HttpRequestData... - var httpResponseData = - await requestExecutor.ExecuteAsync(httpRequestData).ConfigureAwait(false); - - // Read, Parse & Validate the response... - var resultContent = - await httpResponseData.ReadResponseContentAsync().ConfigureAwait(false); - Assert.False(string.IsNullOrWhiteSpace(resultContent)); - - dynamic json = JObject.Parse(resultContent!); - Assert.Null(json.errors); - Assert.True((bool)json.data.isHttpContextInjected); - } - [Fact] public async Task AzFuncIsolatedProcess_BananaCakePopTestAsync() { @@ -118,10 +70,17 @@ public async Task AzFuncIsolatedProcess_BananaCakePopTestAsync() await requestExecutor.ExecuteAsync(httpRequestData).ConfigureAwait(false); // Read, Parse & Validate the response... - var resultContent = await httpResponseData.ReadResponseContentAsync().ConfigureAwait(false); + var resultContent = await ReadResponseAsStringAsync(httpResponseData).ConfigureAwait(false); Assert.NotNull(resultContent); Assert.False(string.IsNullOrWhiteSpace(resultContent)); Assert.True(resultContent!.Contains("")); } -} + private static Task ReadResponseAsStringAsync(HttpResponseData responseData) + { + responseData.Body.Seek(0, SeekOrigin.Begin); + + using var reader = new StreamReader(responseData.Body, Encoding.UTF8); + return reader.ReadToEndAsync(); + } +} diff --git a/src/HotChocolate/AzureFunctions/test/HotChocolate.AzureFunctions.IsolatedProcess.Tests/IsolatedProcessHostBuilderTests.cs b/src/HotChocolate/AzureFunctions/test/HotChocolate.AzureFunctions.IsolatedProcess.Tests/IsolatedProcessHostBuilderTests.cs index 2b169fbade1..274265c9dc3 100644 --- a/src/HotChocolate/AzureFunctions/test/HotChocolate.AzureFunctions.IsolatedProcess.Tests/IsolatedProcessHostBuilderTests.cs +++ b/src/HotChocolate/AzureFunctions/test/HotChocolate.AzureFunctions.IsolatedProcess.Tests/IsolatedProcessHostBuilderTests.cs @@ -3,7 +3,6 @@ using HotChocolate.Types; using Microsoft.Azure.Functions.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; namespace HotChocolate.AzureFunctions.IsolatedProcess.Tests; diff --git a/src/HotChocolate/AzureFunctions/test/HotChocolate.AzureFunctions.Tests/Helpers/HttpContextExtensions.cs b/src/HotChocolate/AzureFunctions/test/HotChocolate.AzureFunctions.Tests/Helpers/HttpContextExtensions.cs new file mode 100644 index 00000000000..b1a6005e8b6 --- /dev/null +++ b/src/HotChocolate/AzureFunctions/test/HotChocolate.AzureFunctions.Tests/Helpers/HttpContextExtensions.cs @@ -0,0 +1,39 @@ +using System.IO; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; + +namespace HotChocolate.AzureFunctions.Tests.Helpers; + +public static class HttpContextExtensions +{ + private static async Task ReadStreamAsStringAsync(this Stream responseStream) + { + string? responseContent; + var originalPosition = responseStream.Position; + + if (responseStream.CanSeek) + { + responseStream.Seek(0, SeekOrigin.Begin); + } + + using (var responseReader = new StreamReader(responseStream)) + { + responseContent = await responseReader.ReadToEndAsync().ConfigureAwait(false); + } + + if (responseStream.CanSeek) + { + responseStream.Seek(originalPosition, SeekOrigin.Begin); + } + + return responseContent; + } + + public static async Task ReadResponseContentAsync(this HttpContext? httpContext) + { + var responseStream = httpContext?.Response?.Body; + return responseStream != null + ? await responseStream.ReadStreamAsStringAsync().ConfigureAwait(false) + : null; + } +} diff --git a/src/HotChocolate/AzureFunctions/test/HotChocolate.AzureFunctions.Tests/Helpers/TestHttpContextHelper.cs b/src/HotChocolate/AzureFunctions/test/HotChocolate.AzureFunctions.Tests/Helpers/TestHttpContextHelper.cs index 6eded8af069..053060db7cd 100644 --- a/src/HotChocolate/AzureFunctions/test/HotChocolate.AzureFunctions.Tests/Helpers/TestHttpContextHelper.cs +++ b/src/HotChocolate/AzureFunctions/test/HotChocolate.AzureFunctions.Tests/Helpers/TestHttpContextHelper.cs @@ -1,10 +1,11 @@ using System; -using HotChocolate.AzureFunctions.IsolatedProcess.Extensions; +using System.Text; using Microsoft.AspNetCore.Http; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Microsoft.Net.Http.Headers; using IO = System.IO; +using System.IO; namespace HotChocolate.AzureFunctions.Tests.Helpers; @@ -12,49 +13,49 @@ public class TestHttpContextHelper { public static Uri DefaultAzFuncGraphQLUri { get; } = new( new Uri("https://localhost/"), - GraphQLAzureFunctionsConstants.DefaultGraphQLRoute - ); + GraphQLAzureFunctionsConstants.DefaultGraphQLRoute); - public static HttpContext NewGraphQLHttpContext( - IServiceProvider serviceProvider, - string graphqlQuery) + public static HttpContext NewGraphQLHttpContext(string query) { - var httpContext = new HttpContextBuilder() - .CreateHttpContext( - HttpMethods.Post, - DefaultAzFuncGraphQLUri, - requestBody: CreateGraphQLRequestBody(graphqlQuery)); + var httpContext = new DefaultHttpContext(); - // Ensure we accept Json for GraphQL requests... - httpContext.Request.Headers.Add( - HeaderNames.Accept, - GraphQLAzureFunctionsConstants.DefaultJsonContentType); + var request = httpContext.Request; + request.Method = HttpMethods.Post; + request.Scheme = DefaultAzFuncGraphQLUri.Scheme; + request.Host = new HostString(DefaultAzFuncGraphQLUri.Host, DefaultAzFuncGraphQLUri.Port); + request.Path = new PathString(DefaultAzFuncGraphQLUri.AbsolutePath); + request.QueryString = new QueryString(DefaultAzFuncGraphQLUri.Query); + request.Body = new IO.MemoryStream(Encoding.UTF8.GetBytes(CreateRequestBody(query))); + request.ContentType = TestConstants.DefaultJsonContentType; - // Ensure that we enable support for HttpContext injection for Unit Tests - serviceProvider.SetCurrentHttpContext(httpContext); + httpContext.Response.Body = new MemoryStream(); return httpContext; } - public static HttpContext NewBcpHttpContext(IServiceProvider serviceProvider, string path) + public static HttpContext NewBcpHttpContext() { - var httpContext = new HttpContextBuilder() - .CreateHttpContext( - HttpMethods.Get, - new Uri(IO.Path.Combine(DefaultAzFuncGraphQLUri.ToString(), path)), - requestBodyContentType: GraphQLAzureFunctionsConstants.DefaultBcpContentType); + var uri = new Uri(IO.Path.Combine(DefaultAzFuncGraphQLUri.ToString(), "index.html")); + + var httpContext = new DefaultHttpContext(); + + var request = httpContext.Request; + request.Method = HttpMethods.Get; + request.Scheme = uri.Scheme; + request.Host = new HostString(uri.Host, uri.Port); + request.Path = new PathString(uri.AbsolutePath); + request.QueryString = new QueryString(uri.Query); // Ensure we accept Text/Html for BCP requests... httpContext.Request.Headers.Add( HeaderNames.Accept, - GraphQLAzureFunctionsConstants.DefaultBcpContentType); + TestConstants.DefaultBcpContentType); - // Ensure that we enable support for HttpContext injection for Unit Tests - serviceProvider.SetCurrentHttpContext(httpContext); + httpContext.Response.Body = new MemoryStream(); return httpContext; } - public static string CreateGraphQLRequestBody(string graphQLQuery) - => JObject.FromObject(new { query = graphQLQuery }).ToString(Formatting.Indented); + public static string CreateRequestBody(string query) + => JObject.FromObject(new { query = query }).ToString(Formatting.Indented); } diff --git a/src/HotChocolate/AzureFunctions/test/HotChocolate.AzureFunctions.Tests/InProcessEndToEndTests.cs b/src/HotChocolate/AzureFunctions/test/HotChocolate.AzureFunctions.Tests/InProcessEndToEndTests.cs index 9a3bdb70e70..1e3b4b76170 100644 --- a/src/HotChocolate/AzureFunctions/test/HotChocolate.AzureFunctions.Tests/InProcessEndToEndTests.cs +++ b/src/HotChocolate/AzureFunctions/test/HotChocolate.AzureFunctions.Tests/InProcessEndToEndTests.cs @@ -1,8 +1,6 @@ using System.Threading.Tasks; -using HotChocolate.AzureFunctions.IsolatedProcess.Extensions; using HotChocolate.AzureFunctions.Tests.Helpers; using HotChocolate.Types; -using Microsoft.AspNetCore.Http; using Microsoft.Azure.Functions.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection; using Newtonsoft.Json.Linq; @@ -26,7 +24,6 @@ public async Task AzFuncInProcess_EndToEndTestAsync() var requestExecutor = serviceProvider.GetRequiredService(); var httpContext = TestHttpContextHelper.NewGraphQLHttpContext( - serviceProvider, @"query { person }"); @@ -38,47 +35,9 @@ public async Task AzFuncInProcess_EndToEndTestAsync() var resultContent = await httpContext.ReadResponseContentAsync(); Assert.False(string.IsNullOrWhiteSpace(resultContent)); - dynamic json = JObject.Parse(resultContent!); - Assert.NotNull(json.errors); - Assert.Equal("Luke Skywalker",json.data.person.ToString()); - } - - [Fact] - public async Task AzFuncInProcess_HttpContextAccessorTestAsync() - { - var hostBuilder = new MockInProcessFunctionsHostBuilder(); - - hostBuilder.Services.AddHttpContextAccessor(); - - hostBuilder - .AddGraphQLFunction() - .AddQueryType(d => d.Name("Query").Field("isHttpContextInjected").Resolve(context => - { - var httpContext = context.Services.GetService()?.HttpContext; - return httpContext != null; - })); - - var serviceProvider = hostBuilder.BuildServiceProvider(); - - // The executor should resolve without error as a Required service... - var requestExecutor = serviceProvider.GetRequiredService(); - - var httpContext = TestHttpContextHelper.NewGraphQLHttpContext(serviceProvider, @" - query { - isHttpContextInjected - } - "); - - // Execute Query Test for end-to-end validation... - await requestExecutor.ExecuteAsync(httpContext.Request); - - // Read, Parse & Validate the response... - var resultContent = await httpContext.ReadResponseContentAsync(); - Assert.False(string.IsNullOrWhiteSpace(resultContent)); - dynamic json = JObject.Parse(resultContent!); Assert.Null(json.errors); - Assert.True((bool)json.data.isHttpContextInjected); + Assert.Equal("Luke Skywalker",json.data.person.ToString()); } [Fact] @@ -100,7 +59,7 @@ public async Task AzFuncInProcess_BananaCakePopTestAsync() // The executor should resolve without error as a Required service... var requestExecutor = serviceProvider.GetRequiredService(); - var httpContext = TestHttpContextHelper.NewBcpHttpContext(serviceProvider, "index.html"); + var httpContext = TestHttpContextHelper.NewBcpHttpContext(); // Execute Query Test for end-to-end validation... await requestExecutor.ExecuteAsync(httpContext.Request); diff --git a/src/HotChocolate/AzureFunctions/test/HotChocolate.AzureFunctions.Tests/TestConstants.cs b/src/HotChocolate/AzureFunctions/test/HotChocolate.AzureFunctions.Tests/TestConstants.cs new file mode 100644 index 00000000000..2229d033bd1 --- /dev/null +++ b/src/HotChocolate/AzureFunctions/test/HotChocolate.AzureFunctions.Tests/TestConstants.cs @@ -0,0 +1,7 @@ +namespace HotChocolate.AzureFunctions; + +public static class TestConstants +{ + public const string DefaultJsonContentType = "application/json"; + public const string DefaultBcpContentType = "text/html"; +} diff --git a/src/HotChocolate/Core/src/Execution/Serialization/EventStreamFormatter.cs b/src/HotChocolate/Core/src/Execution/Serialization/EventStreamFormatter.cs index db3235ff74a..752c9fb21c1 100644 --- a/src/HotChocolate/Core/src/Execution/Serialization/EventStreamFormatter.cs +++ b/src/HotChocolate/Core/src/Execution/Serialization/EventStreamFormatter.cs @@ -26,7 +26,7 @@ private static ReadOnlySpan CompleteEvent (byte)'c', (byte)'o', (byte)'m', (byte)'p', (byte)'l', (byte)'e', (byte)'t', (byte)'e' }; - private static readonly byte[] _newLine = new byte[] { (byte)'\n' }; + private static readonly byte[] _newLine = { (byte)'\n' }; private readonly JsonQueryResultFormatter _payloadFormatter; private readonly JsonWriterOptions _options; @@ -79,7 +79,9 @@ private static ReadOnlySpan CompleteEvent if (result.Kind is SingleResult) { await WriteNextMessageAsync((IQueryResult)result, outputStream).ConfigureAwait(false); + await WriteNewLineAndFlushAsync(outputStream, ct).ConfigureAwait(false); await WriteCompleteMessage(outputStream).ConfigureAwait(false); + await WriteNewLineAndFlushAsync(outputStream, ct).ConfigureAwait(false); } else if (result.Kind is DeferredResult or BatchResult or SubscriptionResult) { @@ -98,11 +100,11 @@ await WriteNextMessageAsync(queryResult, outputStream) await queryResult.DisposeAsync().ConfigureAwait(false); } - await WriteNewLineAndFlushAsync(outputStream, ct); + await WriteNewLineAndFlushAsync(outputStream, ct).ConfigureAwait(false); } await WriteCompleteMessage(outputStream).ConfigureAwait(false); - await WriteNewLineAndFlushAsync(outputStream, ct); + await WriteNewLineAndFlushAsync(outputStream, ct).ConfigureAwait(false); } else { diff --git a/src/StrawberryShake/Tooling/src/dotnet-graphql/UpdateCommand.cs b/src/StrawberryShake/Tooling/src/dotnet-graphql/UpdateCommand.cs index a976b047f83..1e9428b1644 100644 --- a/src/StrawberryShake/Tooling/src/dotnet-graphql/UpdateCommand.cs +++ b/src/StrawberryShake/Tooling/src/dotnet-graphql/UpdateCommand.cs @@ -27,7 +27,7 @@ public static void Build(CommandLineApplication update) var headersArg = update.Option( "-x|--headers", "Custom headers used in request to Graph QL server. " + - "Can be used mulitple times. Example: --headers key1=value1 --headers key2=value2", + "Can be used multiple times. Example: --headers key1=value1 --headers key2=value2", CommandOptionType.MultipleValue); var authArguments = update.AddAuthArguments(); diff --git a/src/StrawberryShake/Tooling/src/dotnet-graphql/UpdateCommandHandler.cs b/src/StrawberryShake/Tooling/src/dotnet-graphql/UpdateCommandHandler.cs index c62121d4137..1251000891e 100644 --- a/src/StrawberryShake/Tooling/src/dotnet-graphql/UpdateCommandHandler.cs +++ b/src/StrawberryShake/Tooling/src/dotnet-graphql/UpdateCommandHandler.cs @@ -1,5 +1,6 @@ using System.Text; using StrawberryShake.Tools.Configuration; +using static System.IO.Path; namespace StrawberryShake.Tools { @@ -73,7 +74,7 @@ await arguments.AuthArguments string clientDirectory, CancellationToken cancellationToken) { - var configFilePath = Path.Combine(clientDirectory, WellKnownFiles.Config); + var configFilePath = Combine(clientDirectory, WellKnownFiles.Config); var buffer = await FileSystem.ReadAllBytesAsync(configFilePath).ConfigureAwait(false); var json = Encoding.UTF8.GetString(buffer); var configuration = GraphQLConfig.FromJson(json); @@ -99,10 +100,10 @@ await UpdateSchemaAsync(context, clientDirectory, configuration, cancellationTok if (configuration.Extensions.StrawberryShake.Url is not null) { var uri = new Uri(configuration.Extensions.StrawberryShake.Url); - var schemaFilePath = Path.Combine(clientDirectory, configuration.Schema); + var schemaFilePath = Combine(clientDirectory, configuration.Schema); var tempFile = CreateTempFileName(); - // we first attempt to download the new schema into a temp file. + // we first attempt to download the new schema into a temp file. // if that should fail we still have the original schema file and // the user can still work. if (!await DownloadSchemaAsync(context, uri, tempFile, cancellationToken) @@ -133,15 +134,14 @@ await UpdateSchemaAsync(context, clientDirectory, configuration, cancellationTok private static string CreateTempFileName() { var pathSegment = Random.Shared.Next(9999).ToString(); - string tempFile; for (var i = 0; i < 100; i++) { - tempFile = Path.Combine(Path.GetTempPath(), pathSegment, Path.GetRandomFileName()); + var tempFile = Combine(GetTempPath(), pathSegment, GetRandomFileName()); if (!File.Exists(tempFile)) { - var tempDir = Path.GetDirectoryName(tempFile)!; + var tempDir = GetDirectoryName(tempFile)!; if (!Directory.Exists(tempDir)) { diff --git a/templates/v12/function-isolated/.gitignore b/templates/v12/function-isolated/.gitignore new file mode 100644 index 00000000000..3c3f4e6a78d --- /dev/null +++ b/templates/v12/function-isolated/.gitignore @@ -0,0 +1,264 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# Azure Functions localsettings file +local.settings.json + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +project.fragment.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +#*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc \ No newline at end of file diff --git a/templates/v12/function-isolated/.template.config/template.json b/templates/v12/function-isolated/.template.config/template.json new file mode 100644 index 00000000000..b5cb905cb9e --- /dev/null +++ b/templates/v12/function-isolated/.template.config/template.json @@ -0,0 +1,44 @@ +{ + "$schema": "http://json.schemastore.org/template", + "author": "Michael Staib", + "classifications": ["Web", "GraphQL", "Azure"], + "identity": "HotChocolate.Template.AzureFunctions.Isolated", + "sourceName": "HotChocolate.Template.AzureFunctions.Isolated", + "name": "HotChocolate GraphQL Function Isolated", + "shortName": "graphql-azf-ip", + "defaultName": "GraphQL AzureFunction Isolated", + "description": "", + "preferNameDirectory": true, + "tags": { + "language": "C#", + "type": "project" + }, + "symbols": { + "Framework": { + "type": "parameter", + "description": "The target framework for the project.", + "datatype": "choice", + "choices": [ + { + "choice": "net6.0", + "description": "Target .NET 6" + } + ], + "replaces": "net6.0", + "defaultValue": "net6.0" + } + }, + "postActions": [ + { + "condition": "(!skipRestore)", + "description": "Restore NuGet packages required by this project.", + "manualInstructions": [ + { + "text": "Run 'dotnet restore'" + } + ], + "actionId": "210D431B-A78B-4D2F-B762-4ED3E3EA9025", + "continueOnError": true + } + ] +} diff --git a/templates/v12/function-isolated/GraphQLFunction.cs b/templates/v12/function-isolated/GraphQLFunction.cs new file mode 100644 index 00000000000..95c882527e2 --- /dev/null +++ b/templates/v12/function-isolated/GraphQLFunction.cs @@ -0,0 +1,21 @@ +using HotChocolate.AzureFunctions; +using Microsoft.Azure.Functions.Worker; +using Microsoft.Azure.Functions.Worker.Http; + +namespace HotChocolate.Template.AzureFunctions.Isolated; + +public class GraphQLFunction +{ + private readonly IGraphQLRequestExecutor _executor; + + public GraphQLFunction(IGraphQLRequestExecutor executor) + { + _executor = executor; + } + + [Function("GraphQLHttpFunction")] + public Task Run( + [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = "graphql/{**slug}")] + HttpRequestData request) + => _executor.ExecuteAsync(request); +} diff --git a/templates/v12/function-isolated/HotChocolate.Template.AzureFunctions.csproj b/templates/v12/function-isolated/HotChocolate.Template.AzureFunctions.csproj new file mode 100644 index 00000000000..3ad4b0962a3 --- /dev/null +++ b/templates/v12/function-isolated/HotChocolate.Template.AzureFunctions.csproj @@ -0,0 +1,38 @@ + + + net6.0 + v4 + Exe + enable + enable + + + + + + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + PreserveNewest + + + PreserveNewest + Never + + + + \ No newline at end of file diff --git a/templates/v12/function-isolated/Program.cs b/templates/v12/function-isolated/Program.cs new file mode 100644 index 00000000000..a3bf936f654 --- /dev/null +++ b/templates/v12/function-isolated/Program.cs @@ -0,0 +1,10 @@ +using Microsoft.Azure.Functions.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +var host = new HostBuilder() + .ConfigureFunctionsWorkerDefaults() + .AddGraphQLFunction(b => b.AddQueryType()) + .Build(); + +host.Run(); diff --git a/templates/v12/function-isolated/Query.cs b/templates/v12/function-isolated/Query.cs new file mode 100644 index 00000000000..91557c14075 --- /dev/null +++ b/templates/v12/function-isolated/Query.cs @@ -0,0 +1,16 @@ +namespace HotChocolate.Template.AzureFunctions.Isolated; + +public class Query +{ + public Person GetPerson() => new Person("Luke Skywalker"); +} + +public class Person +{ + public Person(string name) + { + Name = name; + } + + public string Name { get; } +} diff --git a/templates/v12/function-isolated/host.json b/templates/v12/function-isolated/host.json new file mode 100644 index 00000000000..809a3f20b92 --- /dev/null +++ b/templates/v12/function-isolated/host.json @@ -0,0 +1,11 @@ +{ + "version": "2.0", + "logging": { + "applicationInsights": { + "samplingSettings": { + "isEnabled": true, + "excludedTypes": "Request" + } + } + } +} \ No newline at end of file diff --git a/templates/v12/function/HotChocolate.Template.AzureFunctions.csproj b/templates/v12/function/HotChocolate.Template.AzureFunctions.csproj index fcc489077c8..e0749ce3ed5 100644 --- a/templates/v12/function/HotChocolate.Template.AzureFunctions.csproj +++ b/templates/v12/function/HotChocolate.Template.AzureFunctions.csproj @@ -2,9 +2,9 @@ net6.0 - v4 - enable enable + enable + v4 @@ -16,8 +16,8 @@ - - + + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/templates/v12/server/HotChocolate.Template.Server.csproj b/templates/v12/server/HotChocolate.Template.Server.csproj index a9c96ca7993..34d7e0cbde8 100644 --- a/templates/v12/server/HotChocolate.Template.Server.csproj +++ b/templates/v12/server/HotChocolate.Template.Server.csproj @@ -11,8 +11,8 @@ - - + + runtime; build; native; contentfiles; analyzers; buildtransitive all