Skip to content
Merged
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Diagnostics.CodeAnalysis;
using System.Linq.Expressions;
using System.Reflection;
using HotChocolate.Resolvers;
Expand Down Expand Up @@ -65,10 +66,15 @@ internal CustomParameterExpressionBuilder(bool isPure)
/// A custom parameter expression builder that allows to specify the expressions by
/// passing them into the constructor.
/// </summary>
public class CustomParameterExpressionBuilder<TArg> : CustomParameterExpressionBuilder
public class CustomParameterExpressionBuilder<TArg>
: CustomParameterExpressionBuilder
, IParameterBindingFactory
, IParameterBinding
{
private readonly Func<ParameterInfo, bool> _canHandle;
private readonly Expression<Func<IResolverContext, TArg>> _expression;
private readonly bool _matchesParameterType;
private Func<IResolverContext, TArg>? _compiledExpression;

/// <summary>
/// Initializes a new instance of <see cref="CustomParameterExpressionBuilder"/>.
Expand All @@ -82,6 +88,7 @@ public CustomParameterExpressionBuilder(
{
_canHandle = p => p.ParameterType == typeof(TArg);
_expression = expression;
_matchesParameterType = true;
}

/// <summary>
Expand All @@ -100,6 +107,7 @@ internal CustomParameterExpressionBuilder(
{
_canHandle = p => p.ParameterType == typeof(TArg);
_expression = expression;
_matchesParameterType = true;
}

/// <summary>
Expand Down Expand Up @@ -167,4 +175,33 @@ public override bool CanHandle(ParameterInfo parameter)
/// </returns>
public override Expression Build(ParameterExpressionBuilderContext context)
=> Expression.Invoke(_expression, context.ResolverContext);

ArgumentKind IParameterBindingFactory.Kind => ArgumentKind.Custom;

bool IParameterBindingFactory.IsPure => ((IParameterExpressionBuilder)this).IsPure;

bool IParameterBindingFactory.IsDefaultHandler => false;

bool IParameterBinding.IsPure => ((IParameterExpressionBuilder)this).IsPure;

bool IParameterBindingFactory.CanHandle(ParameterDescriptor parameter)
=> _matchesParameterType && parameter.Type == typeof(TArg);

IParameterBinding IParameterBindingFactory.Create(ParameterDescriptor parameter)
=> this;

[UnconditionalSuppressMessage(
"AOT",
"IL3050",
Justification =
"Custom parameter expressions are compiled at schema initialization time and are only "
+ "used in JIT-compatible environments.")]
T IParameterBinding.Execute<T>(IResolverContext context)
{
var compiled = _compiledExpression ??= _expression.Compile();

// The binding only handles parameters whose type equals TArg, so T is always TArg
// and the compiled delegate can be reinterpreted as Func<IResolverContext, T>.
return ((Func<IResolverContext, T>)(object)compiled)(context);
}
Comment thread
michaelstaib marked this conversation as resolved.
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
using HotChocolate.Execution;
using HotChocolate.Resolvers;
using Microsoft.Extensions.DependencyInjection;

namespace HotChocolate.Types;

public class ParameterExpressionBuilderTests
{
[Fact]
public async Task AddParameterExpressionBuilder_Should_NotExposeParameterAsArgument_When_ResolverIsSourceGenerated()
{
// arrange
var schema = await new ServiceCollection()
.AddGraphQLServer()
.AddIntegrationTestTypes()
.AddPagingArguments()
.AddParameterExpressionBuilder(
static (IResolverContext ctx) => ctx.GetGlobalStateOrDefault<CurrentUser>("currentUser")!)
.BuildSchemaAsync(cancellationToken: TestContext.Current.CancellationToken);

// act
var field = schema.Types.GetType<ObjectType>("Mutation").Fields["createExport"];

// assert
// The custom expression builder handles the currentUser parameter, so only the
// name parameter must remain as a GraphQL argument on the field.
field.ToString().MatchInlineSnapshot("createExport(name: String!): String!");
}

[Fact]
public async Task AddParameterExpressionBuilder_Should_InjectValueFromExpression_When_ResolverIsSourceGenerated()
{
// arrange
var executor = await new ServiceCollection()
.AddGraphQLServer()
.AddIntegrationTestTypes()
.AddPagingArguments()
.AddParameterExpressionBuilder(
static (IResolverContext ctx) => ctx.GetGlobalStateOrDefault<CurrentUser>("currentUser")!)
.BuildRequestExecutorAsync(cancellationToken: TestContext.Current.CancellationToken);

// act
// The resolver combines the injected currentUser with the name argument, so the
// result proves the source-generated binding executed the custom expression.
var result = await executor.ExecuteAsync(
OperationRequestBuilder.New()
.SetDocument("""mutation { createExport(name: "sales") }""")
.SetGlobalState("currentUser", new CurrentUser("alice"))
.Build(),
TestContext.Current.CancellationToken);

// assert
result.MatchInlineSnapshot(
"""
{
"data": {
"createExport": "alice:sales"
}
}
""");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -133,8 +133,17 @@ public static partial class Mutation
// 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";

// When the CurrentUser parameter is supplied through AddParameterExpressionBuilder,
// the source-generated binding must resolve it from that custom builder rather than
// exposing it as a GraphQL input argument. Without a custom builder registered the
// parameter legitimately becomes an implicit argument of type CurrentUserInput.
public static string CreateExport(CurrentUser currentUser, string name)
=> $"{currentUser.Name}:{name}";
}

public sealed record CurrentUser(string Name);

public class IsSelectedNode
{
public int Id { get; set; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ type Query {
type Mutation {
saveShapes(shapes: [ShapeInput!]!): Boolean!
setOptionalValue(value: String): String!
createExport(currentUser: CurrentUserInput!, name: String!): String!
}

type Subscription {
Expand Down Expand Up @@ -401,6 +402,10 @@ interface Product {
id: String!
}

input CurrentUserInput {
name: String!
}

input ShapeInput {
key: String!
name: String!
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ type Query {
type Mutation {
saveShapes(shapes: [ShapeInput!]!): Boolean!
setOptionalValue(value: String): String!
createExport(currentUser: CurrentUserInput!, name: String!): String!
}

type Subscription {
Expand Down Expand Up @@ -300,6 +301,10 @@ interface Product {
id: String!
}

input CurrentUserInput {
name: String!
}

input ShapeInput {
key: String!
name: String!
Expand Down
Loading