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
15 changes: 1 addition & 14 deletions .github/workflows/benchmarks.yml
Original file line number Diff line number Diff line change
@@ -1,20 +1,7 @@
name: Benchmarks

on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review, closed]
branches:
- main
- main-version-*
paths:
- 'src/HotChocolate/Fusion/**'
- '.github/workflows/benchmarks.yml'
push:
branches:
- main
paths:
- 'src/HotChocolate/Fusion/**'
- '.github/workflows/benchmarks.yml'
workflow_dispatch: {}

concurrency:
group: benchmarks-${{ github.event.pull_request.number || github.ref }}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,14 @@ private async Task<GraphQLHttpResponse> ExecuteInternalAsync(
using var arrayWriter = new PooledArrayWriter();
using var requestMessage = CreateRequestMessage(arrayWriter, request, requestUri);

#if FUSION
if (request.State is { } state)
{
request.OnMessageCreated?.Invoke(request, requestMessage, state);
}
#else
request.OnMessageCreated?.Invoke(request, requestMessage, request.State);
#endif

requestMessage.Version = _http.DefaultRequestVersion;
requestMessage.VersionPolicy = _http.DefaultVersionPolicy;
Expand All @@ -120,7 +127,14 @@ private async Task<GraphQLHttpResponse> ExecuteInternalAsync(
.SendAsync(requestMessage, ResponseHeadersRead, ct)
.ConfigureAwait(false);

#if FUSION
if (request.State is { } receivedState)
{
request.OnMessageReceived?.Invoke(request, responseMessage, receivedState);
}
#else
request.OnMessageReceived?.Invoke(request, responseMessage, request.State);
#endif

return new GraphQLHttpResponse(responseMessage);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.Collections.Immutable;
using System.Net.Http.Headers;
#if FUSION
using HotChocolate.Fusion.Execution.Clients;
using HotChocolate.Transport;
using HotChocolate.Transport.Http;
#endif
Expand Down Expand Up @@ -188,7 +189,11 @@ public GraphQLHttpRequest(OperationBatchRequest body, Uri? requestUri = null)
/// <summary>
/// Allows to specify some custom request state, that will be passed into the request hooks.
/// </summary>
#if FUSION
public RequestCallbackState? State { get; set; }
#else
public object? State { get; set; }
#endif

public static implicit operator GraphQLHttpRequest(OperationRequest body) => new(body);

Expand Down
135 changes: 133 additions & 2 deletions src/HotChocolate/AspNetCore/src/Transport.Http/GraphQLHttpResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,74 @@ private static ReadOnlySpan<char> TrimWhiteSpace(ReadOnlySpan<char> value)

return value[start..(end + 1)];
}

/// <summary>
/// Extracts the media type and charset from the raw Content-Type header
/// without allocating a <see cref="MediaTypeHeaderValue"/>.
/// </summary>
private bool TryGetRawMediaTypeAndCharSet(
out ReadOnlySpan<char> mediaType,
out string? charSet)
{
if (!_message.Content.Headers.NonValidated.TryGetValues(ContentTypeHeaderName, out var values))
{
mediaType = default;
charSet = null;
return false;
}

var enumerator = values.GetEnumerator();
if (!enumerator.MoveNext())
{
mediaType = default;
charSet = null;
return false;
}

var rawValue = enumerator.Current.AsSpan();

// Some handlers may emit media type and charset as separate values.
if (enumerator.MoveNext())
{
mediaType = NormalizeMediaType(rawValue);
var charsetValue = enumerator.Current.AsSpan();
charSet = IsUtf8(charsetValue) ? Utf8 : charsetValue.Trim().ToString();
return true;
}

// Single header value — split on ';' to separate media type from parameters.
var semicolonIndex = rawValue.IndexOf(';');
if (semicolonIndex < 0)
{
mediaType = TrimWhiteSpace(rawValue);
charSet = null;
return true;
}

mediaType = TrimWhiteSpace(rawValue[..semicolonIndex]);
var parameters = rawValue[(semicolonIndex + 1)..];

// Extract charset from parameters (e.g., " charset=utf-8").
var charsetIndex = parameters.IndexOf(CharsetPrefix, StringComparison.OrdinalIgnoreCase);
if (charsetIndex >= 0)
{
var charsetSpan = TrimWhiteSpace(parameters[(charsetIndex + CharsetPrefix.Length)..]);

// Strip quotes if present.
if (charsetSpan.Length > 1 && charsetSpan[0] == '"' && charsetSpan[^1] == '"')
{
charsetSpan = charsetSpan[1..^1];
}

charSet = charsetSpan.Equals(Utf8, StringComparison.OrdinalIgnoreCase) ? Utf8 : charsetSpan.ToString();
}
else
{
charSet = null;
}

return true;
}
#endif

/// <summary>
Expand All @@ -258,6 +326,32 @@ private static ReadOnlySpan<char> TrimWhiteSpace(ReadOnlySpan<char> value)
/// to read the <see cref="SourceResultDocument"/> from the underlying <see cref="HttpResponseMessage"/>.
/// </returns>
public ValueTask<SourceResultDocument> ReadAsResultAsync(CancellationToken cancellationToken = default)
{
if (!TryGetRawMediaTypeAndCharSet(out var mediaType, out var charSet))
{
_message.EnsureSuccessStatusCode();
throw new InvalidOperationException("Received a successful response with an unexpected content type.");
}

// The server supports the newer graphql-response+json media type, and users are free
// to use status codes.
if (mediaType.Equals(ContentType.GraphQL, StringComparison.OrdinalIgnoreCase))
{
return ReadAsResultInternalAsync(charSet, cancellationToken);
}

// The server supports the older application/json media type, and the status code
// is expected to be a 2xx for a valid GraphQL response.
if (mediaType.Equals(ContentType.Json, StringComparison.OrdinalIgnoreCase))
{
_message.EnsureSuccessStatusCode();
return ReadAsResultInternalAsync(charSet, cancellationToken);
}

_message.EnsureSuccessStatusCode();

throw new InvalidOperationException("Received a successful response with an unexpected content type.");
}
#else
/// <summary>
/// Reads the GraphQL response as a <see cref="OperationResult"/>.
Expand All @@ -270,7 +364,6 @@ public ValueTask<SourceResultDocument> ReadAsResultAsync(CancellationToken cance
/// to read the <see cref="OperationResult"/> from the underlying <see cref="HttpResponseMessage"/>.
/// </returns>
public ValueTask<OperationResult> ReadAsResultAsync(CancellationToken cancellationToken = default)
#endif
{
var contentType = _message.Content.Headers.ContentType;

Expand All @@ -293,6 +386,7 @@ public ValueTask<OperationResult> ReadAsResultAsync(CancellationToken cancellati

throw new InvalidOperationException("Received a successful response with an unexpected content type.");
}
#endif

#if FUSION
private async ValueTask<SourceResultDocument> ReadAsResultInternalAsync(string? charSet, CancellationToken ct)
Expand Down Expand Up @@ -462,6 +556,43 @@ private async ValueTask<OperationResult> ReadAsResultInternalAsync(string? charS
/// <see cref="HttpResponseMessage"/>.
/// </returns>
public IAsyncEnumerable<SourceResultDocument> ReadAsResultStreamAsync()
{
if (!TryGetRawMediaTypeAndCharSet(out var mediaType, out var charSet))
{
_message.EnsureSuccessStatusCode();
throw new InvalidOperationException("Received a successful response with an unexpected content type.");
}

if (mediaType.Equals(ContentType.EventStream, StringComparison.OrdinalIgnoreCase))
{
return new SseReader(_message);
}

if (mediaType.Equals(ContentType.GraphQLJsonLine, StringComparison.OrdinalIgnoreCase)
|| mediaType.Equals(ContentType.JsonLine, StringComparison.OrdinalIgnoreCase))
{
return new JsonLinesReader(_message);
}

// The server supports the newer graphql-response+json media type, and users are free
// to use status codes.
if (mediaType.Equals(ContentType.GraphQL, StringComparison.OrdinalIgnoreCase))
{
return new GraphQLHttpSingleResultEnumerable(
ct => ReadAsResultInternalAsync(charSet, ct));
}

_message.EnsureSuccessStatusCode();

// The server supports the older application/json media type, and the status code
// is expected to be a 2xx for a valid GraphQL response.
if (mediaType.Equals(ContentType.Json, StringComparison.OrdinalIgnoreCase))
{
return new JsonResultEnumerable(_message, charSet);
}

throw new InvalidOperationException("Received a successful response with an unexpected content type.");
}
#else
/// <summary>
/// Reads the GraphQL response as a <see cref="IAsyncEnumerable{T}"/> of <see cref="OperationResult"/>.
Expand All @@ -472,7 +603,6 @@ public IAsyncEnumerable<SourceResultDocument> ReadAsResultStreamAsync()
/// <see cref="HttpResponseMessage"/>.
/// </returns>
public IAsyncEnumerable<OperationResult> ReadAsResultStreamAsync()
#endif
{
var contentType = _message.Content.Headers.ContentType;

Expand Down Expand Up @@ -508,6 +638,7 @@ public IAsyncEnumerable<OperationResult> ReadAsResultStreamAsync()

throw new InvalidOperationException("Received a successful response with an unexpected content type.");
}
#endif

/// <summary>
/// Disposes the underlying <see cref="HttpResponseMessage"/>.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
#if FUSION
using HotChocolate.Fusion.Execution.Clients;

namespace HotChocolate.Fusion.Transport.Http;
#else
namespace HotChocolate.Transport.Http;
Expand All @@ -10,4 +12,8 @@ namespace HotChocolate.Transport.Http;
public delegate void OnHttpRequestMessageCreated(
GraphQLHttpRequest request,
HttpRequestMessage requestMessage,
#if FUSION
RequestCallbackState state);
#else
object? state);
#endif
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
#if FUSION
using HotChocolate.Fusion.Execution.Clients;

namespace HotChocolate.Fusion.Transport.Http;
#else
namespace HotChocolate.Transport.Http;
Expand All @@ -10,4 +12,8 @@ namespace HotChocolate.Transport.Http;
public delegate void OnHttpResponseMessageReceived(
GraphQLHttpRequest request,
HttpResponseMessage responseMessage,
#if FUSION
RequestCallbackState state);
#else
object? state);
#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using HotChocolate.Fusion.Execution.Nodes;

namespace HotChocolate.Fusion.Execution.Clients;

/// <summary>
/// Carries the context needed by the transport-level request hooks
/// (<see cref="SourceSchemaHttpClientConfiguration.OnBeforeSend"/> and
/// <see cref="SourceSchemaHttpClientConfiguration.OnAfterReceive"/>).
/// Stored on <see cref="Transport.Http.GraphQLHttpRequest.State"/>
/// so the static hook delegates can access it without capturing.
/// </summary>
public readonly struct RequestCallbackState
{
public RequestCallbackState(
OperationPlanContext context,
ExecutionNode node,
SourceSchemaHttpClientConfiguration configuration)
{
Context = context;
Node = node;
Configuration = configuration;
}

public OperationPlanContext Context { get; }

public ExecutionNode Node { get; }

public SourceSchemaHttpClientConfiguration Configuration { get; }
}
Loading
Loading