diff --git a/build/invokePrBuild.ps1 b/build/invoke-pr-build.ps1
similarity index 100%
rename from build/invokePrBuild.ps1
rename to build/invoke-pr-build.ps1
diff --git a/build/nuget-package-readme.md b/build/nuget-package-readme.md
index fb222da37..0824439e9 100644
--- a/build/nuget-package-readme.md
+++ b/build/nuget-package-readme.md
@@ -1,13 +1,8 @@
## GraphQL ASP.NET
-
-
### Documentation: [https://graphql-aspnet.github.io](https://graphql-aspnet.github.io)
-
-
-GraphQL ASP.NET is a fully featured graphql library that utilizes a controller/action programming model similar to ASP.NET.
-
+GraphQL ASP.NET is a fully featured graphql library that utilizes a controller/action programming model similar to ASP.NET.
**This Controller**
diff --git a/src/graphql-aspnet/Configuration/SchemaQueryHandlerConfiguration.cs b/src/graphql-aspnet/Configuration/SchemaQueryHandlerConfiguration.cs
index 457765307..e01d5f401 100644
--- a/src/graphql-aspnet/Configuration/SchemaQueryHandlerConfiguration.cs
+++ b/src/graphql-aspnet/Configuration/SchemaQueryHandlerConfiguration.cs
@@ -33,9 +33,9 @@ public class SchemaQueryHandlerConfiguration
///
/// Gets or sets a value indicating whether the default query processing controller
- /// should be registered to the application. If disabled, the application will not register
+ /// should be registered to the application. When disabled, the application will not register
/// its internal handler as a public end point; the application will need to handle
- /// HTTP request routing manually (Default: false).
+ /// HTTP request routing manually (Default: false, "do include the default route").
///
/// true if the route should be disabled; otherwise, false.
public bool DisableDefaultRoute { get; set; } = false;
@@ -43,7 +43,7 @@ public class SchemaQueryHandlerConfiguration
///
///
/// Gets or sets the route to which the internal controller registered for the schema will listen.
- /// The route is automatically registered as a POST request. (Default: '/graphql').
+ /// The route is automatically registered as a POST and GET request. (Default: '/graphql').
///
///
/// NOTE: If this application registers more than one this value must be unique
diff --git a/src/graphql-aspnet/Constants.cs b/src/graphql-aspnet/Constants.cs
index 60404e85e..ecb179195 100644
--- a/src/graphql-aspnet/Constants.cs
+++ b/src/graphql-aspnet/Constants.cs
@@ -568,6 +568,36 @@ public static class Pipelines
public const string DIRECTIVE_PIPELINE = "Directive Execution Pipeline";
}
+ ///
+ /// A collection of constants related to the web processing of a query.
+ ///
+ public static class Web
+ {
+ ///
+ /// The key in the query string representing the query text when
+ /// processing a GET request.
+ ///
+ public const string QUERYSTRING_QUERY_KEY = "query";
+
+ ///
+ /// The key in the query string representing the named operation when
+ /// processing a GET request.
+ ///
+ public const string QUERYSTRING_OPERATIONNAME_KEY = "operationName";
+
+ ///
+ /// The key in the query string representing the the json encoded variable collection
+ /// when processing a GET request.
+ ///
+ public const string QUERYSTRING_VARIABLES_KEY = "variables";
+
+ ///
+ /// A 'content-type' header value that indicates the body of a POST request
+ /// should be treated entirely as a graphql query string.
+ ///
+ public const string GRAPHQL_CONTENT_TYPE_HEADER_VALUE = "application/graphql";
+ }
+
///
/// Gets a URL pointing to the page of the graphql specification this library
/// targets. This value is used as a base url for most validation rules to generate
diff --git a/src/graphql-aspnet/Defaults/DefaultGraphQLHttpProcessor{TSchema}.cs b/src/graphql-aspnet/Defaults/DefaultGraphQLHttpProcessor{TSchema}.cs
index d8f736fb9..e6041cec9 100644
--- a/src/graphql-aspnet/Defaults/DefaultGraphQLHttpProcessor{TSchema}.cs
+++ b/src/graphql-aspnet/Defaults/DefaultGraphQLHttpProcessor{TSchema}.cs
@@ -11,9 +11,7 @@ namespace GraphQL.AspNet.Defaults
{
using System;
using System.Net;
- using System.Net.Http;
using System.Security.Claims;
- using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using GraphQL.AspNet.Common;
@@ -27,6 +25,7 @@ namespace GraphQL.AspNet.Defaults
using GraphQL.AspNet.Logging.Extensions;
using GraphQL.AspNet.Security.Web;
using GraphQL.AspNet.Web;
+ using GraphQL.AspNet.Web.Exceptions;
using Microsoft.AspNetCore.Http;
///
@@ -42,12 +41,6 @@ public class DefaultGraphQLHttpProcessor : IGraphQLHttpProcessor
protected const string ERROR_NO_QUERY_PROVIDED = "No query received on the request";
- ///
- /// An error message constant, in english, providing the text to return to the caller when they use any HTTP action verb
- /// other than post.
- ///
- protected const string ERROR_USE_POST = "GraphQL queries should be executed as a POST request";
-
///
/// An error message constant, in english, providing the text to return to the caller when a 500 error is generated.
///
@@ -67,10 +60,10 @@ public class DefaultGraphQLHttpProcessor : IGraphQLHttpProcessor
/// Initializes a new instance of the class.
///
- /// The singleton instance of representing this processor works against.
- /// The primary runtime instance in which GraphQL requests are processed for .
- /// The result writer capable of converting a into a serialized payload
- /// for the given .
+ /// The singleton instance of representing this processor works against.
+ /// The primary runtime instance in which GraphQL requests are processed for .
+ /// The result writer capable of converting a into a serialized payload
+ /// for the given .
/// A logger instance where this object can write and record log entries.
public DefaultGraphQLHttpProcessor(
TSchema schema,
@@ -81,6 +74,7 @@ public DefaultGraphQLHttpProcessor(
_schema = Validation.ThrowIfNullOrReturn(schema, nameof(schema));
_runtime = Validation.ThrowIfNullOrReturn(runtime, nameof(runtime));
_writer = Validation.ThrowIfNullOrReturn(writer, nameof(writer));
+
_logger = logger;
}
@@ -88,28 +82,35 @@ public DefaultGraphQLHttpProcessor(
public virtual async Task Invoke(HttpContext context)
{
this.HttpContext = Validation.ThrowIfNullOrReturn(context, nameof(context));
- if (!string.Equals(context.Request.Method, nameof(HttpMethod.Post), StringComparison.OrdinalIgnoreCase))
+
+ GraphQueryData queryData;
+ try
+ {
+ queryData = await this.ParseHttpContext();
+ }
+ catch (HttpContextParsingException ex)
{
- await this.WriteStatusCodeResponse(HttpStatusCode.BadRequest, ERROR_USE_POST, context.RequestAborted).ConfigureAwait(false);
+ await this.WriteStatusCodeResponse(ex.StatusCode, ex.Message, context.RequestAborted).ConfigureAwait(false);
return;
}
- // accepting a parsed object causes havoc with any variables collection
- // ------
- // By default:
- // netcoreapp2.2 and older would auto parse to JObject (Newtonsoft)
- // netcoreapp3.0 and later will parse to JsonElement (System.Text.Json).
- // ------
- // in lue of supporting deserialization from both generic json object types
- // we accept the raw data and parse the json document
- // using System.Text.Json on all clients (netstandard2.0 compatiable)
- var options = new JsonSerializerOptions();
- options.PropertyNameCaseInsensitive = true;
- options.AllowTrailingCommas = true;
- options.ReadCommentHandling = JsonCommentHandling.Skip;
-
- var data = await JsonSerializer.DeserializeAsync(context.Request.Body, options).ConfigureAwait(false);
- await this.SubmitGraphQLQuery(data, context.RequestAborted).ConfigureAwait(false);
+ await this.SubmitGraphQLQuery(queryData, context.RequestAborted).ConfigureAwait(false);
+ }
+
+ ///
+ /// When overriden in a child class, allows for the alteration of the method by which the various query
+ /// parameters are extracted from the for input to the graphql runtime.
+ ///
+ ///
+ /// Throw an to stop execution and quickly write
+ /// an error back to the requestor.
+ ///
+ /// A parsed query data object containing the input parameters for the
+ /// graphql runtime or null.
+ protected virtual async Task ParseHttpContext()
+ {
+ var dataGenerator = new HttpContextParser(this.HttpContext);
+ return await dataGenerator.Parse();
}
///
@@ -229,7 +230,7 @@ protected virtual IUserSecurityContext CreateUserSecurityContext()
}
///
- /// writes directly to the stream with the given status code
+ /// Writes directly to the stream with the given status code
/// and message.
///
/// The status code to deliver on the response.
@@ -238,6 +239,11 @@ protected virtual IUserSecurityContext CreateUserSecurityContext()
/// Task.
protected async Task WriteStatusCodeResponse(HttpStatusCode statusCode, string message, CancellationToken cancelToken = default)
{
+ if (_schema.Configuration.ResponseOptions.AppendServerHeader)
+ {
+ this.Response.Headers.Add(Constants.ServerInformation.SERVER_INFORMATION_HEADER, Constants.ServerInformation.ServerData);
+ }
+
this.Response.StatusCode = (int)statusCode;
await this.Response.WriteAsync(message, cancelToken).ConfigureAwait(false);
}
diff --git a/src/graphql-aspnet/GraphQueryData.cs b/src/graphql-aspnet/GraphQueryData.cs
index d475bb187..fbe6597a4 100644
--- a/src/graphql-aspnet/GraphQueryData.cs
+++ b/src/graphql-aspnet/GraphQueryData.cs
@@ -12,7 +12,7 @@ namespace GraphQL.AspNet
using GraphQL.AspNet.Variables;
///
- /// An binding model representing an incoming, structured GraphQL query on a HTTP request.
+ /// A raw data package representing the various inputs to the graphql runtime.
///
public class GraphQueryData
{
@@ -35,6 +35,13 @@ static GraphQueryData()
};
}
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public GraphQueryData()
+ {
+ }
+
///
/// Gets or sets the name of the operation in the query document to execute. Must be included
/// when the document defines more than one operation.
diff --git a/src/graphql-aspnet/Variables/InputVariableCollection.cs b/src/graphql-aspnet/Variables/InputVariableCollection.cs
index c6f5fa523..9de3c277c 100644
--- a/src/graphql-aspnet/Variables/InputVariableCollection.cs
+++ b/src/graphql-aspnet/Variables/InputVariableCollection.cs
@@ -32,6 +32,9 @@ public class InputVariableCollection : IInputVariableCollection
/// IInputVariableCollection.
public static InputVariableCollection FromJsonDocument(string jsonDocument)
{
+ if (string.IsNullOrWhiteSpace(jsonDocument))
+ return InputVariableCollection.Empty;
+
var options = new JsonSerializerOptions()
{
AllowTrailingCommas = true,
diff --git a/src/graphql-aspnet/Web/Exceptions/HttpContextParsingException.cs b/src/graphql-aspnet/Web/Exceptions/HttpContextParsingException.cs
new file mode 100644
index 000000000..165f998e6
--- /dev/null
+++ b/src/graphql-aspnet/Web/Exceptions/HttpContextParsingException.cs
@@ -0,0 +1,41 @@
+// *************************************************************
+// project: graphql-aspnet
+// --
+// repo: https://github.com/graphql-aspnet
+// docs: https://graphql-aspnet.github.io
+// --
+// License: MIT
+// *************************************************************
+
+namespace GraphQL.AspNet.Web.Exceptions
+{
+ using System;
+ using System.Net;
+ using Microsoft.AspNetCore.Http;
+
+ ///
+ /// An exception thrown when the runtime is uanble to successfully extract the
+ /// data from an to generate a data package
+ /// for the graphql runtime.
+ ///
+ public class HttpContextParsingException : Exception
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The status code taht should be set, indicating the reason for failure.
+ /// An end user friendly error message to include. This message will be
+ /// written directly to the http response.
+ public HttpContextParsingException(HttpStatusCode statusCode = HttpStatusCode.BadRequest, string errorMessage = "")
+ : base(errorMessage)
+ {
+ this.StatusCode = statusCode;
+ }
+
+ ///
+ /// Gets the status code that should be set on the http response, indicating the reason for the failure.
+ ///
+ /// The status code.
+ public HttpStatusCode StatusCode { get; }
+ }
+}
\ No newline at end of file
diff --git a/src/graphql-aspnet/Web/GraphQueryHandler{TSchema}.cs b/src/graphql-aspnet/Web/GraphQueryHandler{TSchema}.cs
index 7a61581df..6609bc574 100644
--- a/src/graphql-aspnet/Web/GraphQueryHandler{TSchema}.cs
+++ b/src/graphql-aspnet/Web/GraphQueryHandler{TSchema}.cs
@@ -44,7 +44,7 @@ protected Task Invoke(HttpContext context)
throw new InvalidOperationException(
$"No {nameof(IGraphQLHttpProcessor)} of type " +
$"{typeof(IGraphQLHttpProcessor).FriendlyName()} " +
- "is registered with the DI container. The GraphQL runtime cannot invoke the schema.");
+ "is registered with the DI container. The GraphQL runtime cannot invoke the target schema.");
}
return processor.Invoke(context);
diff --git a/src/graphql-aspnet/Web/HttpContextParser.cs b/src/graphql-aspnet/Web/HttpContextParser.cs
new file mode 100644
index 000000000..298a32432
--- /dev/null
+++ b/src/graphql-aspnet/Web/HttpContextParser.cs
@@ -0,0 +1,194 @@
+// *************************************************************
+// project: graphql-aspnet
+// --
+// repo: https://github.com/graphql-aspnet
+// docs: https://graphql-aspnet.github.io
+// --
+// License: MIT
+// *************************************************************
+
+namespace GraphQL.AspNet.Defaults
+{
+ using System;
+ using System.IO;
+ using System.Net.Http;
+ using System.Text;
+ using System.Text.Json;
+ using System.Threading.Tasks;
+ using GraphQL.AspNet.Common;
+ using GraphQL.AspNet.Variables;
+ using GraphQL.AspNet.Web.Exceptions;
+ using Microsoft.AspNetCore.Http;
+
+ ///
+ /// An implementation of the business rules that extract the data received on an
+ /// to create a
+ /// object used by the graphql runtime.
+ ///
+ public class HttpContextParser
+ {
+ ///
+ /// An error message constant, in english, providing the text to return to the caller when they use any HTTP action verb
+ /// other than post.
+ ///
+ protected const string ERROR_USE_POST = "GraphQL queries should be executed as a POST request";
+
+ ///
+ /// An error message format constant, in english, providing the text to return to teh caller
+ /// when an attempt to deserailize a json payload on a POST request fails.
+ ///
+ protected const string ERROR_POST_SERIALIZATION_ISSUE_FORMAT = "Unable to deserialize the POST body as a JSON object: {0}";
+
+ ///
+ /// An error message format constant, in english, providing the text to return to the caller
+ /// when an attempt to deserialize the json payload that represents the variables collection, on the
+ /// query string, fails.
+ ///
+ protected static readonly string ERROR_VARIABLE_PARAMETER_SERIALIZATION_ISSUE_FORMAT = $"Unable to deserialize the '{Constants.Web.QUERYSTRING_VARIABLES_KEY}' query string parameter: {{0}}";
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The context to generate from.
+ public HttpContextParser(HttpContext context)
+ {
+ this.HttpContext = Validation.ThrowIfNullOrReturn(context, nameof(context));
+ }
+
+ ///
+ /// Generates a query data object usable by the graphql runtime.
+ ///
+ /// GraphQueryData.
+ public virtual async Task Parse()
+ {
+ GraphQueryData queryData = new GraphQueryData();
+
+ // ------------------------------
+ // Step 0: GET or POST only
+ // ---------------------------
+ if (!this.IsPostRequest && !this.IsGetRequest)
+ throw new HttpContextParsingException(errorMessage: ERROR_USE_POST);
+
+ // ------------------------------
+ // Step 1: POST Body processing
+ // ------------------------------
+ // First attempt to decode the post body when applicable
+ if (this.IsPostRequest)
+ {
+ try
+ {
+ queryData = await this.DecodePostBody();
+ }
+ catch (JsonException ex)
+ {
+ var message = string.Format(ERROR_POST_SERIALIZATION_ISSUE_FORMAT, ex.Message);
+ throw new HttpContextParsingException(errorMessage: message);
+ }
+ }
+
+ // ------------------------------
+ // Step 2: Query String Updates
+ // ------------------------------
+ // Apply overrides with the query string parameters if they exist
+ this.ApplyQueryStringOverrides(queryData);
+
+ return queryData;
+ }
+
+ ///
+ /// Attempts to extract the necessary keys from the query string of the http context
+ /// and update the supplied query data object with the values found. Query string values not found
+ /// are not applied to the supplied object.
+ ///
+ /// The query data object to update.
+ protected virtual void ApplyQueryStringOverrides(GraphQueryData queryData)
+ {
+ var httpQueryString = this.HttpContext.Request.Query;
+ if (httpQueryString.ContainsKey(Constants.Web.QUERYSTRING_QUERY_KEY))
+ {
+ var query = httpQueryString[Constants.Web.QUERYSTRING_QUERY_KEY];
+ if (!string.IsNullOrWhiteSpace(query))
+ queryData.Query = query;
+ }
+
+ if (httpQueryString.ContainsKey(Constants.Web.QUERYSTRING_OPERATIONNAME_KEY))
+ {
+ var operationName = httpQueryString[Constants.Web.QUERYSTRING_OPERATIONNAME_KEY];
+ if (!string.IsNullOrWhiteSpace(operationName))
+ queryData.OperationName = operationName;
+ }
+
+ if (httpQueryString.ContainsKey(Constants.Web.QUERYSTRING_VARIABLES_KEY))
+ {
+ var variables = httpQueryString[Constants.Web.QUERYSTRING_VARIABLES_KEY];
+ if (!string.IsNullOrWhiteSpace(variables))
+ {
+ try
+ {
+ queryData.Variables = InputVariableCollection.FromJsonDocument(variables);
+ }
+ catch (JsonException ex)
+ {
+ var message = string.Format(ERROR_VARIABLE_PARAMETER_SERIALIZATION_ISSUE_FORMAT, ex.Message);
+ throw new HttpContextParsingException(errorMessage: message);
+ }
+ }
+ }
+ }
+
+ ///
+ /// Attempts to deserialize the POST body of the httpcontext
+ /// into a object that can be processed by the GraphQL runtime.
+ ///
+ /// GraphQueryData.
+ protected virtual async Task DecodePostBody()
+ {
+ // if the content-type is set to graphql, treat
+ // the whole body as the query string
+ // -----
+ // See: https://graphql.org/learn/serving-over-http/#http-methods-headers-and-body
+ // -----
+ if (this.IsGraphQLBody)
+ {
+ using var reader = new StreamReader(this.HttpContext.Request.Body, Encoding.UTF8, true, 1024, true);
+ var query = await reader.ReadToEndAsync();
+ return new GraphQueryData()
+ {
+ Query = query,
+ };
+ }
+
+ // attempt to decode the body as a json object
+ var options = new JsonSerializerOptions();
+ options.PropertyNameCaseInsensitive = true;
+ options.AllowTrailingCommas = true;
+ options.ReadCommentHandling = JsonCommentHandling.Skip;
+
+ return await JsonSerializer.DeserializeAsync(this.HttpContext.Request.Body, options, this.HttpContext.RequestAborted).ConfigureAwait(false);
+ }
+
+ ///
+ /// Gets the context being parsed by this instance.
+ ///
+ /// The context.
+ protected HttpContext HttpContext { get; }
+
+ ///
+ /// Gets a value indicating whether the watched http context is a GET request.
+ ///
+ /// A value indicating if the context is a GET request.
+ public bool IsGetRequest => string.Equals(this.HttpContext.Request.Method, nameof(HttpMethod.Get), StringComparison.OrdinalIgnoreCase);
+
+ ///
+ /// Gets a value indicating whether the watched http context is a POST request.
+ ///
+ /// A value indicating if the context is a post request.
+ public bool IsPostRequest => string.Equals(this.HttpContext.Request.Method, nameof(HttpMethod.Post), StringComparison.OrdinalIgnoreCase);
+
+ ///
+ /// Gets a value indicating whether the content-type of the http request is set to application/graphql.
+ ///
+ /// A value indicating if the http post body content type represents a graphql query.
+ public bool IsGraphQLBody => string.Equals(this.HttpContext.Request.ContentType, Constants.Web.GRAPHQL_CONTENT_TYPE_HEADER_VALUE, StringComparison.OrdinalIgnoreCase);
+ }
+}
\ No newline at end of file
diff --git a/src/graphql-aspnet/graphql-aspnet.csproj b/src/graphql-aspnet/graphql-aspnet.csproj
index 289af3aee..de45dfc3a 100644
--- a/src/graphql-aspnet/graphql-aspnet.csproj
+++ b/src/graphql-aspnet/graphql-aspnet.csproj
@@ -27,7 +27,7 @@
-
+
diff --git a/src/tests/graphql-aspnet-tests/Web/GetRequestTests.cs b/src/tests/graphql-aspnet-tests/Web/GetRequestTests.cs
new file mode 100644
index 000000000..eab5c163d
--- /dev/null
+++ b/src/tests/graphql-aspnet-tests/Web/GetRequestTests.cs
@@ -0,0 +1,276 @@
+// *************************************************************
+// project: graphql-aspnet
+// --
+// repo: https://github.com/graphql-aspnet
+// docs: https://graphql-aspnet.github.io
+// --
+// License: MIT
+// *************************************************************
+
+namespace GraphQL.AspNet.Tests.Web
+{
+ using System.Collections.Generic;
+ using System.IO;
+ using System.Text;
+ using System.Text.Json;
+ using System.Threading.Tasks;
+ using System.Web;
+ using GraphQL.AspNet.Defaults;
+ using GraphQL.AspNet.Schemas;
+ using GraphQL.AspNet.Tests.Framework;
+ using GraphQL.AspNet.Tests.Framework.CommonHelpers;
+ using GraphQL.AspNet.Tests.Web.CancelTokenTestData;
+ using Microsoft.AspNetCore.Http;
+ using Microsoft.AspNetCore.Http.Internal;
+ using Microsoft.Extensions.DependencyInjection;
+ using NUnit.Framework;
+
+ [TestFixture]
+ public class GetRequestTests
+ {
+ public static List