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 @@ -1406,6 +1406,20 @@ private void WriteResolverArguments(Resolver resolver, IMethodSymbol resolverMet
continue;
}

// Optional<T> arguments bind through ArgumentOptional<T>, which coerces the inner T
// and preserves whether the argument was supplied. This mirrors the reflection-based
// binding in ArgumentParameterExpressionBuilder.
if (parameter.Kind is ResolverParameterKind.Argument or ResolverParameterKind.Unknown
&& parameter.Type.IsOptional(out var optionalInnerType))
{
Writer.WriteIndentedLine(
"var args{0} = context.ArgumentOptional<{1}>(\"{2}\");",
i,
ToFullyQualifiedString(optionalInnerType, resolverMethod, typeLookup),
parameter.Key ?? parameter.Name);
continue;
}

switch (parameter.Kind)
{
case ResolverParameterKind.Parent:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -783,6 +783,19 @@ public static bool IsPagingArguments(this IParameterSymbol parameter)
public static bool IsSelection(this IParameterSymbol parameter)
=> parameter.Type.ToDisplayString() == WellKnownTypes.ISelection;

public static bool IsOptional(this ITypeSymbol type, [NotNullWhen(true)] out ITypeSymbol? innerType)
{
if (type is INamedTypeSymbol { IsGenericType: true, TypeArguments.Length: 1 } namedType
&& namedType.ToDisplayString().StartsWith(WellKnownTypes.OptionalGeneric, StringComparison.Ordinal))
{
innerType = namedType.TypeArguments[0];
return true;
}

innerType = null;
return false;
}

public static bool IsIsSelected(this IParameterSymbol parameter)
{
foreach (var attribute in parameter.GetAttributes())
Expand Down
2 changes: 2 additions & 0 deletions src/HotChocolate/Core/src/Types.Analyzers/WellKnownTypes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ public static class WellKnownTypes
public const string FieldResolverDelegate = "HotChocolate.Resolvers.FieldResolverDelegate";
public const string ResolverContext = "HotChocolate.Resolvers.IResolverContext";
public const string ParameterBinding = "HotChocolate.Internal.IParameterBinding";
public const string Optional = "HotChocolate.Optional";
public const string OptionalGeneric = Optional + "<";
public const string MemoryMarshal = "System.Runtime.InteropServices.MemoryMarshal";
public const string Unsafe = "System.Runtime.CompilerServices.Unsafe";
public const string Object = "System.Object";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -197,4 +197,56 @@ public async Task Resolves_Instance_Method_On_NonStatic_QueryType()
Assert.DoesNotContain("\"errors\"", json);
Assert.Matches("\"title\": \"[0-9a-f]{32}\"", json);
}

[Fact]
public async Task Resolves_Optional_Argument_That_Has_A_Value()
{
// arrange
var executor = await new ServiceCollection()
.AddGraphQLServer()
.AddIntegrationTestTypes()
.AddPagingArguments()
.BuildRequestExecutorAsync(cancellationToken: TestContext.Current.CancellationToken);

// act
var result = await executor.ExecuteAsync(
"""mutation { setOptionalValue(value: "abc") }""",
TestContext.Current.CancellationToken);

// assert
result.MatchInlineSnapshot(
"""
{
"data": {
"setOptionalValue": "abc"
}
}
""");
}

[Fact]
public async Task Resolves_Optional_Argument_That_Has_No_Value()
{
// arrange
var executor = await new ServiceCollection()
.AddGraphQLServer()
.AddIntegrationTestTypes()
.AddPagingArguments()
.BuildRequestExecutorAsync(cancellationToken: TestContext.Current.CancellationToken);

// act
var result = await executor.ExecuteAsync(
"mutation { setOptionalValue }",
TestContext.Current.CancellationToken);

// assert
result.MatchInlineSnapshot(
"""
{
"data": {
"setOptionalValue": "unset"
}
}
""");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,11 @@ public class Shape
public static partial class Mutation
{
public static bool SaveShapes(IReadOnlyList<Shape> shapes) => true;

// An Optional<T> parameter is bound through the runtime parameter binding,
// which must coerce the inner type and honor whether the argument was provided.
public static string SetOptionalValue(Optional<string?> value)
=> value.HasValue ? value.Value ?? "null" : "unset";
}

public class IsSelectedNode
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ type Query {

type Mutation {
saveShapes(shapes: [ShapeInput!]!): Boolean!
setOptionalValue(value: String): String!
}

type Subscription {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ type Query {

type Mutation {
saveShapes(shapes: [ShapeInput!]!): Boolean!
setOptionalValue(value: String): String!
}

type Subscription {
Expand Down
Loading