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");
+ }
+}