diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Models/ResolverParameter.cs b/src/HotChocolate/Core/src/Types.Analyzers/Models/ResolverParameter.cs index 986094d290a..cf767f26dfa 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/Models/ResolverParameter.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/Models/ResolverParameter.cs @@ -75,7 +75,6 @@ public bool IsPure ResolverParameterKind.Parent or ResolverParameterKind.Service or ResolverParameterKind.GetGlobalState or - ResolverParameterKind.SetGlobalState or ResolverParameterKind.GetScopedState or ResolverParameterKind.HttpContext or ResolverParameterKind.HttpRequest or diff --git a/src/HotChocolate/Core/src/Types/Execution/DependencyInjection/SchemaRequestExecutorBuilderExtensions.Resolvers.cs b/src/HotChocolate/Core/src/Types/Execution/DependencyInjection/SchemaRequestExecutorBuilderExtensions.Resolvers.cs index 7168ddf7b0a..24bcdb5ee59 100644 --- a/src/HotChocolate/Core/src/Types/Execution/DependencyInjection/SchemaRequestExecutorBuilderExtensions.Resolvers.cs +++ b/src/HotChocolate/Core/src/Types/Execution/DependencyInjection/SchemaRequestExecutorBuilderExtensions.Resolvers.cs @@ -469,6 +469,9 @@ public static IRequestExecutorBuilder AddResolver( /// A predicate that can be used to specify to which parameter the /// expression shall be applied to. /// + /// + /// Defines if the parameter expression can be used for pure resolvers. + /// /// /// The parameter result type. /// @@ -479,7 +482,8 @@ public static IRequestExecutorBuilder AddResolver( public static IRequestExecutorBuilder AddParameterExpressionBuilder( this IRequestExecutorBuilder builder, Expression> expression, - Func? canHandle = null) + Func? canHandle = null, + bool isPure = true) { ArgumentNullException.ThrowIfNull(builder); ArgumentNullException.ThrowIfNull(expression); @@ -487,12 +491,12 @@ public static IRequestExecutorBuilder AddParameterExpressionBuilder( if (canHandle is null) { builder.Services.AddParameterExpressionBuilder( - _ => new CustomParameterExpressionBuilder(expression)); + _ => new CustomParameterExpressionBuilder(expression, isPure)); } else { builder.Services.AddParameterExpressionBuilder( - _ => new CustomParameterExpressionBuilder(expression, canHandle)); + _ => new CustomParameterExpressionBuilder(expression, canHandle, isPure)); } return builder; diff --git a/src/HotChocolate/Core/src/Types/Internal/CustomParameterExpressionBuilder.cs b/src/HotChocolate/Core/src/Types/Internal/CustomParameterExpressionBuilder.cs index 3faff20a796..d77759ea1e3 100644 --- a/src/HotChocolate/Core/src/Types/Internal/CustomParameterExpressionBuilder.cs +++ b/src/HotChocolate/Core/src/Types/Internal/CustomParameterExpressionBuilder.cs @@ -10,9 +10,29 @@ namespace HotChocolate.Internal; /// public abstract class CustomParameterExpressionBuilder : IParameterExpressionBuilder { + private readonly bool _isPure; + + /// + /// Initializes a new instance of + /// that is not considered pure. + /// + protected CustomParameterExpressionBuilder() { } + + /// + /// Initializes a new instance of + /// with an explicit purity setting. + /// + /// + /// Defines if the parameter expression can be used for pure resolvers. + /// + internal CustomParameterExpressionBuilder(bool isPure) + { + _isPure = isPure; + } + ArgumentKind IParameterExpressionBuilder.Kind => ArgumentKind.Custom; - bool IParameterExpressionBuilder.IsPure => false; + bool IParameterExpressionBuilder.IsPure => _isPure; bool IParameterExpressionBuilder.IsDefaultHandler => false; @@ -58,6 +78,25 @@ public class CustomParameterExpressionBuilder : CustomParameterExpressionB /// public CustomParameterExpressionBuilder( Expression> expression) + : base(isPure: false) + { + _canHandle = p => p.ParameterType == typeof(TArg); + _expression = expression; + } + + /// + /// Initializes a new instance of . + /// + /// + /// The expression that shall be used to resolve the parameter value. + /// + /// + /// Defines if the parameter expression can be used for pure resolvers. + /// + internal CustomParameterExpressionBuilder( + Expression> expression, + bool isPure) + : base(isPure) { _canHandle = p => p.ParameterType == typeof(TArg); _expression = expression; @@ -75,6 +114,29 @@ public CustomParameterExpressionBuilder( public CustomParameterExpressionBuilder( Expression> expression, Func canHandle) + : base(isPure: false) + { + _expression = expression; + _canHandle = canHandle; + } + + /// + /// Initializes a new instance of . + /// + /// + /// A func that defines if a parameter can be handled by this expression builder. + /// + /// + /// The expression that shall be used to resolve the parameter value. + /// + /// + /// Defines if the parameter expression can be used for pure resolvers. + /// + internal CustomParameterExpressionBuilder( + Expression> expression, + Func canHandle, + bool isPure) + : base(isPure) { _expression = expression; _canHandle = canHandle; diff --git a/src/HotChocolate/Core/src/Types/Resolvers/DefaultResolverCompiler.cs b/src/HotChocolate/Core/src/Types/Resolvers/DefaultResolverCompiler.cs index e4f509b19cd..ede16b5876b 100644 --- a/src/HotChocolate/Core/src/Types/Resolvers/DefaultResolverCompiler.cs +++ b/src/HotChocolate/Core/src/Types/Resolvers/DefaultResolverCompiler.cs @@ -437,6 +437,16 @@ private bool IsPureResolver( if (!builder.IsPure) { + // We allow scoped state getters to be considered pure because + // PureResolverContext can read ScopedContextData (it delegates + // to its parent). Setters and local state remain not pure. + if (builder is ScopedStateParameterExpressionBuilder + and not LocalStateParameterExpressionBuilder + && !ParameterExpressionBuilderHelpers.IsStateSetter(parameter.ParameterType)) + { + continue; + } + return false; } } diff --git a/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/ScopedStateParameterExpressionBuilder.cs b/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/ScopedStateParameterExpressionBuilder.cs index 2d86f6ea54b..00e3c34d9e9 100644 --- a/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/ScopedStateParameterExpressionBuilder.cs +++ b/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/ScopedStateParameterExpressionBuilder.cs @@ -155,7 +155,7 @@ public ParameterBinding( public ArgumentKind Kind => _parent.Kind; - public bool IsPure => _parent.IsPure; + public bool IsPure => true; public T Execute(IResolverContext context) => context.GetScopedStateOrDefault(_key, default!); diff --git a/src/HotChocolate/Core/test/Types.Tests/Resolvers/Issue7399Tests.cs b/src/HotChocolate/Core/test/Types.Tests/Resolvers/Issue7399Tests.cs new file mode 100644 index 00000000000..716f3dfee1a --- /dev/null +++ b/src/HotChocolate/Core/test/Types.Tests/Resolvers/Issue7399Tests.cs @@ -0,0 +1,30 @@ +using HotChocolate.Execution; +using HotChocolate.Execution.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace HotChocolate.Resolvers; + +public class Issue7399Tests +{ + [Fact] + public async Task AddParameterExpressionBuilder_For_GlobalState_Does_Not_AutoInject_Cost() + { + var executor = await new ServiceCollection() + .AddGraphQLServer() + .AddQueryType() + .AddParameterExpressionBuilder( + ctx => ctx.GetGlobalState("MyGlobalState")) + .BuildRequestExecutorAsync(); + + Assert.DoesNotContain("@cost(", executor.Schema.ToString(), StringComparison.Ordinal); + } + + public sealed record MyGlobalState; + + public sealed record MyThing(string Id); + + public sealed class Query + { + public MyThing Test(MyGlobalState state) => new("Foo"); + } +}