Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,14 @@ public async Task WriteSchemaAsync(
Version,
context.RequestAborted);

public async Task WriteSemanticNonNullSchemaAsync(
HttpContext context)
=> await _responseFormatter.FormatSemanticNonNullSchemaAsync(
context.Response,
Schema,
Version,
context.RequestAborted);
Comment thread
tobias-tengler marked this conversation as resolved.

public RequestFlags CreateRequestFlags(AcceptMediaType[] acceptMediaTypes)
=> _responseFormatter.CreateRequestFlags(acceptMediaTypes);
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ public static class EndpointRouteBuilderExtensions
private const string GraphQLHttpPath = "/graphql";
private const string GraphQLWebSocketPath = "/graphql/ws";
private const string GraphQLSchemaPath = "/graphql/sdl";
private const string GraphQLSemanticNonNullSchemaPath = "/graphql/semantic-non-null-schema.graphql";
private const string GraphQLToolPath = "/graphql/ui";
private const string GraphQLPersistedOperationPath = "/graphql/persisted";
private const string GraphQLToolRelativeRequestPath = "..";
Expand Down Expand Up @@ -389,6 +390,88 @@ public static IEndpointConventionBuilder MapGraphQLSchema(
.WithDisplayName("Hot Chocolate GraphQL Schema Pipeline"));
}

/// <summary>
/// Adds a GraphQL semantic non-null schema SDL endpoint to the endpoint configurations.
/// The endpoint serves a schema document where non-null wrappers have been replaced with
/// the @semanticNonNull directive.
/// </summary>
/// <param name="endpointRouteBuilder">
/// The <see cref="IEndpointConventionBuilder"/>.
/// </param>
/// <param name="pattern">
/// The path to which the GraphQL semantic non-null schema endpoint shall be mapped.
/// </param>
/// <param name="schemaName">
/// The name of the schema that shall be used by this endpoint.
/// </param>
/// <returns>
/// Returns the <see cref="IEndpointConventionBuilder"/> so that
/// configuration can be chained.
/// </returns>
/// <exception cref="ArgumentNullException">
/// The <paramref name="endpointRouteBuilder" /> is <c>null</c>.
/// </exception>
public static IEndpointConventionBuilder MapGraphQLSemanticNonNullSchema(
this IEndpointRouteBuilder endpointRouteBuilder,
[StringSyntax("Route")] string pattern = GraphQLSemanticNonNullSchemaPath,
string? schemaName = null)
=> MapGraphQLSemanticNonNullSchema(endpointRouteBuilder, Parse(pattern), schemaName);

/// <summary>
/// Adds a GraphQL semantic non-null schema SDL endpoint to the endpoint configurations.
/// The endpoint serves a schema document where non-null wrappers have been replaced with
/// the @semanticNonNull directive.
/// </summary>
/// <param name="endpointRouteBuilder">
/// The <see cref="IEndpointConventionBuilder"/>.
/// </param>
/// <param name="pattern">
/// The path to which the GraphQL semantic non-null schema endpoint shall be mapped.
/// </param>
/// <param name="schemaName">
/// The name of the schema that shall be used by this endpoint.
/// </param>
/// <returns>
/// Returns the <see cref="IEndpointConventionBuilder"/> so that
/// configuration can be chained.
/// </returns>
/// <exception cref="ArgumentNullException">
/// The <paramref name="endpointRouteBuilder" /> is <c>null</c>.
/// </exception>
public static IEndpointConventionBuilder MapGraphQLSemanticNonNullSchema(
this IEndpointRouteBuilder endpointRouteBuilder,
RoutePattern pattern,
string? schemaName = null)
{
ArgumentNullException.ThrowIfNull(endpointRouteBuilder);
ArgumentNullException.ThrowIfNull(pattern);

TryResolveSchemaName(endpointRouteBuilder.ServiceProvider, ref schemaName);

var requestPipeline = endpointRouteBuilder.CreateApplicationBuilder();
var schemaNameOrDefault = schemaName ?? ISchemaDefinition.DefaultName;

var services = endpointRouteBuilder.ServiceProvider;
var executorProvider = services.GetRequiredService<IRequestExecutorProvider>();
var executorEvents = services.GetRequiredService<IRequestExecutorEvents>();
var executor = new HttpRequestExecutorProxy(executorProvider, executorEvents, schemaNameOrDefault);
var serverOptions = services.GetServerOptions(schemaNameOrDefault);

requestPipeline
.Use(MiddlewareFactory.CreateCancellationMiddleware())
.Use(MiddlewareFactory.CreateHttpGetSemanticNonNullSchemaMiddleware(executor, serverOptions))
.Use(_ => context =>
{
context.Response.StatusCode = 404;
return Task.CompletedTask;
});

return new GraphQLEndpointConventionBuilder(
endpointRouteBuilder
.Map(pattern, requestPipeline.Build())
.WithDisplayName("Hot Chocolate GraphQL Semantic Non-Null Schema Pipeline"));
}

/// <summary>
/// Adds a Nitro endpoint to the endpoint configurations.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ namespace HotChocolate.AspNetCore.Formatters;
public class DefaultHttpResponseFormatter : IHttpResponseFormatter
{
private readonly ConcurrentDictionary<string, CachedSchemaOutput> _schemaCache = new(StringComparer.Ordinal);
private readonly ConcurrentDictionary<string, CachedSemanticNonNullSchemaOutput> _semanticNonNullSchemaCache = new(StringComparer.Ordinal);
private readonly ITimeProvider _timeProvider;
private readonly FormatInfo _defaultFormat;
private readonly FormatInfo _graphqlResponseFormat;
Expand Down Expand Up @@ -344,6 +345,40 @@ CachedSchemaOutput Update(string _)
=> new(schema, version, _timeProvider.UtcNow);
}

public async ValueTask FormatSemanticNonNullSchemaAsync(
HttpResponse response,
ISchemaDefinition schema,
ulong version,
CancellationToken cancellationToken)
{
var output = _semanticNonNullSchemaCache.GetOrAdd(schema.Name, Update);

if (output.Version < version)
{
lock (_semanticNonNullSchemaCache)
{
if (!_semanticNonNullSchemaCache.TryGetValue(schema.Name, out output)
|| output.Version < version)
{
_semanticNonNullSchemaCache[schema.Name] = output = Update(schema.Name);
}
}
}

var memory = output.AsMemory();
response.ContentType = ContentType.GraphQL;
response.Headers.SetContentDisposition(output.FileName);
response.Headers.ETag = output.ETag;
response.Headers.LastModified = output.LastModified;
response.Headers.CacheControl = "public, max-age=3600, must-revalidate";
response.Headers.ContentLength = memory.Length;
await response.Body.WriteAsync(memory, cancellationToken);
return;

CachedSemanticNonNullSchemaOutput Update(string _)
=> new(schema, version, _timeProvider.UtcNow);
}

/// <summary>
/// Determines which status code shall be returned for this result.
/// </summary>
Expand Down Expand Up @@ -865,4 +900,43 @@ private static string GetSchemaFileName(ISchemaDefinition schema)
? "schema.graphql"
: schema.Name + ".schema.graphql";
}

private sealed class CachedSemanticNonNullSchemaOutput
{
private readonly byte[] _schema;

public CachedSemanticNonNullSchemaOutput(ISchemaDefinition schema, ulong version, DateTimeOffset lastModifiedTime)
{
var document = (HotChocolate.Language.DocumentNode)schema.ToSyntaxNode();
var rewritten = SemanticNonNullSchemaRewriter.Rewrite(document);
_schema = Encoding.UTF8.GetBytes(rewritten.ToString(indented: true));
FileName = GetSchemaFileName(schema);
ETag = CreateETag(_schema, version);
LastModified = lastModifiedTime.ToString("R");
Version = version;
}

public string FileName { get; }

public string ETag { get; }

public ulong Version { get; }

public string LastModified { get; }

public ReadOnlyMemory<byte> AsMemory() => _schema;

private static string CreateETag(byte[] schema, ulong version)
{
Span<byte> hashBytes = stackalloc byte[32];
SHA256.HashData(schema, hashBytes);
var hash = Convert.ToBase64String(hashBytes);
return $"\"{version}-{hash}\"";
}

private static string GetSchemaFileName(ISchemaDefinition schema)
=> schema.Name.Equals(ISchemaDefinition.DefaultName, StringComparison.OrdinalIgnoreCase)
? "schema.graphql"
: schema.Name + ".schema.graphql";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,29 @@ ValueTask FormatAsync(
ISchemaDefinition schema,
ulong version,
CancellationToken cancellationToken);

/// <summary>
/// Formats the given <paramref name="schema"/> into a GraphQL schema SDL response that has
/// non-null wrappers replaced with the @semanticNonNull directive.
/// </summary>
/// <param name="response">
/// The HTTP response.
/// </param>
/// <param name="schema">
/// The GraphQL schema.
/// </param>
/// <param name="version">
/// The schema version.
/// </param>
/// <param name="cancellationToken">
/// The request cancellation token.
/// </param>
/// <returns>
/// A task that represents the asynchronous operation.
/// </returns>
ValueTask FormatSemanticNonNullSchemaAsync(
HttpResponse response,
ISchemaDefinition schema,
ulong version,
CancellationToken cancellationToken);
Comment thread
tobias-tengler marked this conversation as resolved.
}
Loading
Loading