Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
5ad69b9
chore(deps): bump HotChocolate to 16.0.0-rc.1.43
aaronburtle Apr 29, 2026
3f557c2
refactor(graphql): migrate scalar APIs to Hot Chocolate v16
aaronburtle Apr 29, 2026
8a7c33d
refactor(core): adopt Selection / SyntaxNodes / DurationType / Contex…
aaronburtle Apr 29, 2026
b49d904
chore(graphql): drop EnableOneOf option (default in HC v16)
aaronburtle Apr 29, 2026
6ec7a4f
refactor(startup): adopt DateTimeOptions and per-request WithOptions …
aaronburtle Apr 29, 2026
13f7ade
test: adopt v16 ResultDocument API and exception contract changes
aaronburtle Apr 29, 2026
01b82c9
fix(test): mark files using nullable annotations explicitly
aaronburtle Apr 29, 2026
50ebc29
addressing comments
aaronburtle Apr 29, 2026
234b24f
addressing comments
aaronburtle Apr 29, 2026
b8b5aa0
fix tests
aaronburtle Apr 29, 2026
71855e7
fix more tests
aaronburtle Apr 29, 2026
40c2a8e
lay GQL schema build in test
aaronburtle Apr 29, 2026
bb20141
fix more test failures
aaronburtle Apr 29, 2026
c253578
Merge branch 'main' into dev/aaronburtle/HC-16-upgrade
aaronburtle Apr 29, 2026
ff088bd
fix more tests
aaronburtle Apr 29, 2026
eb37d90
more test fixes
aaronburtle Apr 29, 2026
d1f8950
fixing tests
aaronburtle Apr 30, 2026
f21c7a8
fixing more tests
aaronburtle Apr 30, 2026
45ef613
fixing more tests
aaronburtle Apr 30, 2026
2a7bca9
Merge branch 'main' into dev/aaronburtle/HC-16-upgrade
aaronburtle Apr 30, 2026
854391d
move to 16.0.0
aaronburtle May 5, 2026
66792fd
Merge branch 'main' into dev/aaronburtle/HC-16-upgrade
aaronburtle May 6, 2026
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
23 changes: 14 additions & 9 deletions src/Core/Parsers/IntrospectionInterceptor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using HotChocolate.AspNetCore;
using HotChocolate.Execution;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;

namespace Azure.DataApiBuilder.Core.Parsers
{
Expand All @@ -14,17 +15,18 @@ namespace Azure.DataApiBuilder.Core.Parsers
/// </summary>
public class IntrospectionInterceptor : DefaultHttpRequestInterceptor
{
private RuntimeConfigProvider _runtimeConfigProvider;

/// <summary>
/// Constructor injects RuntimeConfigProvider to allow
/// HotChocolate to attempt to retrieve the runtime config
/// when evaluating GraphQL requests.
/// Parameterless constructor.
/// </summary>
/// <param name="runtimeConfigProvider"></param>
public IntrospectionInterceptor(RuntimeConfigProvider runtimeConfigProvider)
/// <remarks>
/// Hot Chocolate v16 isolates schema services from the application's request services.
/// Resolving constructor-injected app singletons (e.g. <see cref="RuntimeConfigProvider"/>)
/// against the schema service provider therefore fails at executor session creation.
/// We instead resolve dependencies from <see cref="HttpContext.RequestServices"/> in
/// <see cref="OnCreateAsync"/>, where the application's request scope is in effect.
/// </remarks>
public IntrospectionInterceptor()
{
_runtimeConfigProvider = runtimeConfigProvider;
}

/// <summary>
Expand All @@ -51,7 +53,10 @@ public override ValueTask OnCreateAsync(
OperationRequestBuilder requestBuilder,
CancellationToken cancellationToken)
{
if (_runtimeConfigProvider.GetConfig().AllowIntrospection)
RuntimeConfigProvider runtimeConfigProvider =
context.RequestServices.GetRequiredService<RuntimeConfigProvider>();

if (runtimeConfigProvider.GetConfig().AllowIntrospection)
{
requestBuilder.AllowIntrospection();
}
Expand Down
7 changes: 4 additions & 3 deletions src/Core/Resolvers/CosmosQueryStructure.cs
Original file line number Diff line number Diff line change
Expand Up @@ -117,14 +117,15 @@ private static IEnumerable<LabelledColumn> GenerateQueryColumns(SelectionSetNode
[MemberNotNull(nameof(OrderByColumns))]
private void Init(IDictionary<string, object?> queryParams)
{
ISelection selection = _context.Selection;
Selection selection = _context.Selection;
ObjectType underlyingType = selection.Field.Type.NamedType<ObjectType>();

IsPaginated = QueryBuilder.IsPaginationType(underlyingType);
OrderByColumns = new();
FieldNode selectionFieldNode = selection.RequireFieldNode();
if (IsPaginated)
{
FieldNode? fieldNode = ExtractQueryField(selection.SyntaxNode);
FieldNode? fieldNode = ExtractQueryField(selectionFieldNode);

if (fieldNode is not null)
{
Expand All @@ -139,7 +140,7 @@ private void Init(IDictionary<string, object?> queryParams)
}
else
{
Columns.AddRange(GenerateQueryColumns(selection.SyntaxNode.SelectionSet!, _context.Operation.Document, SourceAlias));
Columns.AddRange(GenerateQueryColumns(selectionFieldNode.SelectionSet!, _context.Operation.Document, SourceAlias));
string typeName = GraphQLUtils.TryExtractGraphQLFieldModelName(underlyingType.Directives, out string? modelName) ?
Comment thread
aaronburtle marked this conversation as resolved.
modelName :
underlyingType.Name;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ public SqlQueryStructure(
sqlMetadataProvider,
authorizationResolver,
ctx.Selection.Field,
ctx.Selection.SyntaxNode,
ctx.Selection.RequireFieldNode(),
// The outermost query is where we start, so this can define
Comment thread
aaronburtle marked this conversation as resolved.
// create the IncrementingInteger that will be shared between
// all subqueries in this query.
Expand Down Expand Up @@ -173,7 +173,7 @@ public SqlQueryStructure(
IsMultipleCreateOperation = isMultipleCreateOperation;

ObjectField schemaField = _ctx.Selection.Field;
FieldNode? queryField = _ctx.Selection.SyntaxNode;
FieldNode? queryField = _ctx.Selection.RequireFieldNode();

IOutputType outputType = schemaField.Type;
_underlyingFieldType = outputType.NamedType<ObjectType>();
Expand Down
2 changes: 1 addition & 1 deletion src/Core/Services/DetermineStatusCodeMiddleware.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public async ValueTask InvokeAsync(RequestContext context)
}

contextData[ExecutionContextData.HttpStatusCode] = HttpStatusCode.BadRequest;
context.Result = singleResult.WithContextData(contextData.ToImmutable());
singleResult.ContextData = contextData.ToImmutable();
}
}
}
Expand Down
20 changes: 15 additions & 5 deletions src/Core/Services/ExecutionHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Net;
using System.Text;
using System.Text.Json;
using System.Xml;
using Azure.DataApiBuilder.Config.ObjectModel;
using Azure.DataApiBuilder.Core.Configurations;
using Azure.DataApiBuilder.Core.Models;
Expand All @@ -18,7 +19,6 @@
using Azure.DataApiBuilder.Service.GraphQLBuilder.GraphQLTypes;
using Azure.DataApiBuilder.Service.GraphQLBuilder.Queries;
using HotChocolate.Execution;
using HotChocolate.Execution.Processing;
using HotChocolate.Language;
using HotChocolate.Resolvers;
using NodaTime.Text;
Expand Down Expand Up @@ -201,7 +201,7 @@ fieldValue.ValueKind is not (JsonValueKind.Undefined or JsonValueKind.Null))
return namedType switch
{
StringType => fieldValue.GetString(), // spec
ByteType => fieldValue.GetByte(),
UnsignedByteType => fieldValue.GetByte(),
ShortType => fieldValue.GetInt16(),
IntType => fieldValue.GetInt32(), // spec
LongType => fieldValue.GetInt64(),
Expand All @@ -211,11 +211,21 @@ fieldValue.ValueKind is not (JsonValueKind.Undefined or JsonValueKind.Null))
DateTimeType => DateTimeOffset.TryParse(fieldValue.GetString()!, DateTimeFormatInfo.InvariantInfo, DateTimeStyles.AssumeUniversal, out DateTimeOffset date) ? date : null, // for DW when datetime is null it will be in "" (double quotes) due to stringagg parsing and hence we need to ensure parsing is correct.
DateType => DateTimeOffset.TryParse(fieldValue.GetString()!, out DateTimeOffset date) ? date : null,
HotChocolate.Types.NodaTime.LocalTimeType => fieldValue.GetString()!.Equals("null", StringComparison.OrdinalIgnoreCase) ? null : LocalTimePattern.ExtendedIso.Parse(fieldValue.GetString()!).Value,
ByteArrayType => fieldValue.GetBytesFromBase64(),
// HC v16 ships both ByteArrayType (legacy, GraphQL name "ByteArray", runtime byte[])
// and Base64StringType (new, GraphQL name "Base64String", runtime byte[]). DAB's
// generated schemas still use the GraphQL name "ByteArray", so HC binds entity
// fields to ByteArrayType. We accept either type here so DAB also tolerates
// schemas that bind to Base64StringType (e.g. via DefaultValueType).
// CS0618: ByteArrayType is [Obsolete] in HC v16 in favor of Base64StringType,
// but we still need to pattern-match it because it remains the type bound to
// the GraphQL name "ByteArray" that DAB-generated schemas continue to use.
#pragma warning disable CS0618
Base64StringType or ByteArrayType => fieldValue.GetBytesFromBase64(),
#pragma warning restore CS0618
BooleanType => fieldValue.GetBoolean(), // spec
UrlType => new Uri(fieldValue.GetString()!),
UuidType => fieldValue.GetGuid(),
TimeSpanType => TimeSpan.Parse(fieldValue.GetString()!),
DurationType => XmlConvert.ToTimeSpan(fieldValue.GetString()!),
AnyType => fieldValue.ToString(),
_ => fieldValue.GetString()
};
Expand Down Expand Up @@ -508,7 +518,7 @@ public static InputObjectType InputObjectTypeFromIInputField(IInputValueDefiniti
{
return GetParametersFromSchemaAndQueryFields(
context.Selection.Field,
context.Selection.SyntaxNode,
context.Selection.RequireFieldNode(),
context.Variables);
Comment thread
aaronburtle marked this conversation as resolved.
}

Expand Down
71 changes: 69 additions & 2 deletions src/Core/Services/GraphQLSchemaCreator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,15 @@ private ISchemaBuilder Parse(
// Generate the Query and the Mutation Node.
(DocumentNode queryNode, DocumentNode mutationNode) = GenerateQueryAndMutationNodes(root, inputTypes);

// Hot Chocolate v16 validates schemas eagerly during host startup
// (RequestExecutorWarmupService) and rejects an empty Query type with
// "The object type `Query` has to at least define one field in order to be valid."
// This can occur in valid runtime configurations: GraphQL globally disabled,
// every entity opting out of GraphQL via `graphql.enabled = false`, or no entities
// configured. Inject a hidden placeholder field with a no-op resolver so the schema
// is structurally valid; HC v16 also rejects fields without resolvers.
queryNode = EnsureQueryHasAtLeastOneField(queryNode, sb);

return sb
.AddDocument(root)
.AddAuthorizeDirectiveType()
Expand All @@ -124,12 +133,70 @@ private ISchemaBuilder Parse(
.AddDocument(queryNode)
// Generate the GraphQL mutations from the provided objects
.AddDocument(mutationNode)
// Enable the OneOf directive (https://github.com/graphql/graphql-spec/pull/825) to support the DefaultValue type
.ModifyOptions(o => o.EnableOneOf = true)
// Adds our type interceptor that will create the resolvers.
.TryAddTypeInterceptor(new ResolverTypeInterceptor(new ExecutionHelper(_queryEngineFactory, _mutationEngineFactory, _runtimeConfigProvider)));
}

/// <summary>
/// Name of the hidden placeholder field added to <c>Query</c> when no entity contributes
/// a query field, used to keep the schema valid for HC v16's eager validation.
/// </summary>
private const string EMPTY_SCHEMA_PLACEHOLDER_FIELD_NAME = "_dab";

/// <summary>
/// If the generated <c>Query</c> object type has no fields, append a hidden placeholder
/// field and register a null-returning resolver for it. The placeholder is shadowed in
/// any configuration that produces real query fields, so it is only visible in
/// otherwise-empty schemas (GraphQL globally disabled, all entities opting out,
/// no entities configured).
/// </summary>
private static DocumentNode EnsureQueryHasAtLeastOneField(DocumentNode queryNode, ISchemaBuilder sb)
{
ImmutableArray<IDefinitionNode>.Builder rewritten = ImmutableArray.CreateBuilder<IDefinitionNode>(queryNode.Definitions.Count);
bool placeholderInjected = false;

foreach (IDefinitionNode definition in queryNode.Definitions)
{
if (definition is ObjectTypeDefinitionNode objectType
&& objectType.Name.Value == "Query"
&& objectType.Fields.Count == 0)
{
FieldDefinitionNode placeholderField = new(
location: null,
new NameNode(EMPTY_SCHEMA_PLACEHOLDER_FIELD_NAME),
new StringValueNode(
"Internal placeholder; only present when no entity contributes a query field. "
+ "Always returns null and is never reachable in normal operation."),
arguments: new List<InputValueDefinitionNode>(),
type: new NamedTypeNode(new NameNode("String")),
directives: new List<DirectiveNode>());

rewritten.Add(new ObjectTypeDefinitionNode(
objectType.Location,
objectType.Name,
objectType.Description,
objectType.Directives,
objectType.Interfaces,
new List<FieldDefinitionNode> { placeholderField }));
placeholderInjected = true;
}
else
{
rewritten.Add(definition);
}
}

if (placeholderInjected)
{
// HC v16 requires every field to have a resolver; bind a no-op that always
// returns null. The field is unreachable in normal operation because callers
// for empty-Query configurations never issue GraphQL requests.
sb.AddResolver("Query", EMPTY_SCHEMA_PLACEHOLDER_FIELD_NAME, _ => null);
}

return new DocumentNode(rewritten.ToImmutable());
}

/// <summary>
/// Generate the GraphQL schema query and mutation nodes from the provided database.
/// </summary>
Expand Down
15 changes: 6 additions & 9 deletions src/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,12 @@
<PackageVersion Include="CommandLineParser" Version="2.9.1" />
<PackageVersion Include="coverlet.msbuild" Version="6.0.2" />
<PackageVersion Include="coverlet.collector" Version="6.0.2" />
<PackageVersion Include="HotChocolate" Version="16.0.0-p.7.68" />
<PackageVersion Include="HotChocolate.AspNetCore" Version="16.0.0-p.7.68" />
<PackageVersion Include="HotChocolate.AspNetCore.Authorization" Version="16.0.0-p.7.68" />
<PackageVersion Include="HotChocolate.ModelContextProtocol" Version="16.0.0-p.7.68" />
<PackageVersion Include="HotChocolate.Types.NodaTime" Version="16.0.0-p.7.68" />
<PackageVersion Include="HotChocolate.Utilities.Introspection" Version="16.0.0-p.7.68" />
<PackageVersion Include="HotChocolate.Transport.Http" Version="16.0.0-p.7.68" />
<PackageVersion Include="HotChocolate.Diagnostics" Version="16.0.0-p.7.68" />
<PackageVersion Include="CookieCrumble" Version="16.0.0-p.7.68" />
<PackageVersion Include="HotChocolate" Version="16.0.0" />
<PackageVersion Include="HotChocolate.AspNetCore" Version="16.0.0" />
<PackageVersion Include="HotChocolate.AspNetCore.Authorization" Version="16.0.0" />
<PackageVersion Include="HotChocolate.Types.NodaTime" Version="16.0.0" />
<PackageVersion Include="HotChocolate.Utilities.Introspection" Version="16.0.0" />
<PackageVersion Include="HotChocolate.Diagnostics" Version="16.0.0" />
<PackageVersion Include="Humanizer" Version="2.14.1" />
<PackageVersion Include="Humanizer.Core" Version="2.14.1" />
<PackageVersion Include="DotNetEnv" Version="3.0.0" />
Expand Down
16 changes: 12 additions & 4 deletions src/Service.GraphQLBuilder/CustomScalars/SingleType.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Text.Json;
using HotChocolate.Language;
using HotChocolate.Text.Json;
using HotChocolate.Types;

namespace Azure.DataApiBuilder.Service.GraphQLBuilder.CustomScalars
Expand Down Expand Up @@ -48,10 +50,16 @@ public SingleType(
Description = description;
}

protected override float ParseLiteral(IFloatValueLiteral valueSyntax) =>
valueSyntax.ToSingle();
protected override float OnCoerceInputLiteral(IFloatValueLiteral valueLiteral)
=> valueLiteral.ToSingle();

protected override FloatValueNode ParseValue(float runtimeValue) =>
new(runtimeValue);
protected override float OnCoerceInputValue(JsonElement inputValue)
=> inputValue.GetSingle();

protected override void OnCoerceOutputValue(float runtimeValue, ResultElement resultValue)
=> resultValue.SetNumberValue(runtimeValue);

protected override IValueNode OnValueToLiteral(float runtimeValue)
=> new FloatValueNode(runtimeValue);
}
}
24 changes: 12 additions & 12 deletions src/Service.GraphQLBuilder/GraphQLStoredProcedureBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -156,14 +156,14 @@ private static Tuple<string, IValueNode> ConvertValueToGraphQLType(string defaul
{
Tuple<string, IValueNode> valueNode = paramValueType switch
{
UUID_TYPE => new(UUID_TYPE, new UuidType().ParseValue(Guid.Parse(defaultValueFromConfig))),
BYTE_TYPE => new(BYTE_TYPE, new IntValueNode(byte.Parse(defaultValueFromConfig))),
SHORT_TYPE => new(SHORT_TYPE, new IntValueNode(short.Parse(defaultValueFromConfig))),
INT_TYPE => new(INT_TYPE, new IntValueNode(int.Parse(defaultValueFromConfig))),
LONG_TYPE => new(LONG_TYPE, new IntValueNode(long.Parse(defaultValueFromConfig))),
SINGLE_TYPE => new(SINGLE_TYPE, new SingleType().ParseValue(float.Parse(defaultValueFromConfig))),
FLOAT_TYPE => new(FLOAT_TYPE, new FloatValueNode(double.Parse(defaultValueFromConfig))),
DECIMAL_TYPE => new(DECIMAL_TYPE, new FloatValueNode(decimal.Parse(defaultValueFromConfig))),
UUID_TYPE => new(UUID_TYPE, new UuidType().ValueToLiteral(Guid.Parse(defaultValueFromConfig))),
BYTE_TYPE => new(BYTE_TYPE, new IntValueNode(byte.Parse(defaultValueFromConfig, CultureInfo.InvariantCulture))),
SHORT_TYPE => new(SHORT_TYPE, new IntValueNode(short.Parse(defaultValueFromConfig, CultureInfo.InvariantCulture))),
INT_TYPE => new(INT_TYPE, new IntValueNode(int.Parse(defaultValueFromConfig, CultureInfo.InvariantCulture))),
LONG_TYPE => new(LONG_TYPE, new IntValueNode(long.Parse(defaultValueFromConfig, CultureInfo.InvariantCulture))),
SINGLE_TYPE => new(SINGLE_TYPE, new SingleType().ValueToLiteral(float.Parse(defaultValueFromConfig, CultureInfo.InvariantCulture))),
FLOAT_TYPE => new(FLOAT_TYPE, new FloatValueNode(double.Parse(defaultValueFromConfig, CultureInfo.InvariantCulture))),
DECIMAL_TYPE => new(DECIMAL_TYPE, new FloatValueNode(decimal.Parse(defaultValueFromConfig, CultureInfo.InvariantCulture))),
STRING_TYPE => new(STRING_TYPE, new StringValueNode(defaultValueFromConfig)),
BOOLEAN_TYPE => new(BOOLEAN_TYPE, new BooleanValueNode(
defaultValueFromConfig switch
Expand All @@ -174,10 +174,10 @@ var s when s.Equals("true", StringComparison.OrdinalIgnoreCase) => true,
var s when s.Equals("false", StringComparison.OrdinalIgnoreCase) => false,
_ => throw new FormatException($"String '{defaultValueFromConfig}' was not recognized as a valid Boolean.")
})),
DATETIME_TYPE => new(DATETIME_TYPE, new DateTimeType().ParseResult(
DateTime.Parse(defaultValueFromConfig, DateTimeFormatInfo.InvariantInfo, DateTimeStyles.AssumeUniversal))),
BYTEARRAY_TYPE => new(BYTEARRAY_TYPE, new ByteArrayType().ParseValue(Convert.FromBase64String(defaultValueFromConfig))),
LOCALTIME_TYPE => new(LOCALTIME_TYPE, new HotChocolate.Types.NodaTime.LocalTimeType().ParseResult(LocalTimePattern.ExtendedIso.Parse(defaultValueFromConfig).Value)),
DATETIME_TYPE => new(DATETIME_TYPE, new DateTimeType().ValueToLiteral(
DateTimeOffset.Parse(defaultValueFromConfig, DateTimeFormatInfo.InvariantInfo, DateTimeStyles.AssumeUniversal))),
BYTEARRAY_TYPE => new(BYTEARRAY_TYPE, new Base64StringType().ValueToLiteral(Convert.FromBase64String(defaultValueFromConfig))),
LOCALTIME_TYPE => new(LOCALTIME_TYPE, new HotChocolate.Types.NodaTime.LocalTimeType().ValueToLiteral(LocalTimePattern.ExtendedIso.Parse(defaultValueFromConfig).Value)),
_ => throw new NotSupportedException(message: $"The {defaultValueFromConfig} parameter's value type [{paramValueType}] is not supported.")
};

Expand Down
Loading
Loading