diff --git a/src/HotChocolate/Core/src/Types/Configuration/AggregateTypeInterceptor.cs b/src/HotChocolate/Core/src/Types/Configuration/AggregateTypeInterceptor.cs
index e7a6c55921c..adbb3a39111 100644
--- a/src/HotChocolate/Core/src/Types/Configuration/AggregateTypeInterceptor.cs
+++ b/src/HotChocolate/Core/src/Types/Configuration/AggregateTypeInterceptor.cs
@@ -110,6 +110,22 @@ internal override void InitializeContext(
}
}
+ internal override bool SkipDirectiveDefinition(DirectiveDefinitionNode node)
+ {
+ ref var first = ref GetReference();
+ var length = _typeInterceptors.Length;
+
+ for (var i = 0; i < length; i++)
+ {
+ if (Unsafe.Add(ref first, i).SkipDirectiveDefinition(node))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
public override void OnBeforeDiscoverTypes()
{
ref var first = ref GetReference();
diff --git a/src/HotChocolate/Core/src/Types/Configuration/TypeInterceptor.cs b/src/HotChocolate/Core/src/Types/Configuration/TypeInterceptor.cs
index 5b2d812414a..658292eaf48 100644
--- a/src/HotChocolate/Core/src/Types/Configuration/TypeInterceptor.cs
+++ b/src/HotChocolate/Core/src/Types/Configuration/TypeInterceptor.cs
@@ -74,6 +74,9 @@ public virtual void OnBeforeDiscoverTypes() { }
///
public virtual void OnAfterDiscoverTypes() { }
+ internal virtual bool SkipDirectiveDefinition(DirectiveDefinitionNode node)
+ => false;
+
///
/// This event is triggered after the type instance was created but before
/// any type definition was initialized.
diff --git a/src/HotChocolate/Core/src/Types/Types/Factories/SchemaSyntaxVisitor.cs b/src/HotChocolate/Core/src/Types/Types/Factories/SchemaSyntaxVisitor.cs
index 0d2b01c50cd..4f1aec7cab4 100644
--- a/src/HotChocolate/Core/src/Types/Types/Factories/SchemaSyntaxVisitor.cs
+++ b/src/HotChocolate/Core/src/Types/Types/Factories/SchemaSyntaxVisitor.cs
@@ -171,6 +171,11 @@ protected override ISyntaxVisitorAction VisitChildren(
goto EXIT;
}
+ if(context.DescriptorContext.TypeInterceptor.SkipDirectiveDefinition(node))
+ {
+ goto EXIT;
+ }
+
context.Types.Add(
TypeReference.Create(
_directiveTypeFactory.Create(
diff --git a/src/HotChocolate/CostAnalysis/src/CostAnalysis/CostAnalyzerMiddleware.cs b/src/HotChocolate/CostAnalysis/src/CostAnalysis/CostAnalyzerMiddleware.cs
index a13cc8c04c2..66290d93cd0 100644
--- a/src/HotChocolate/CostAnalysis/src/CostAnalysis/CostAnalyzerMiddleware.cs
+++ b/src/HotChocolate/CostAnalysis/src/CostAnalysis/CostAnalyzerMiddleware.cs
@@ -36,7 +36,7 @@ public async ValueTask InvokeAsync(IRequestContext context)
}
var requestOptions = context.TryGetCostOptions() ?? options;
- var mode = context.GetCostAnalyzerMode(requestOptions.EnforceCostLimits);
+ var mode = context.GetCostAnalyzerMode(requestOptions);
if (mode == CostAnalyzerMode.Skip)
{
diff --git a/src/HotChocolate/CostAnalysis/src/CostAnalysis/CostTypeInterceptor.cs b/src/HotChocolate/CostAnalysis/src/CostAnalysis/CostTypeInterceptor.cs
index ae769173431..514c9c95aa6 100644
--- a/src/HotChocolate/CostAnalysis/src/CostAnalysis/CostTypeInterceptor.cs
+++ b/src/HotChocolate/CostAnalysis/src/CostAnalysis/CostTypeInterceptor.cs
@@ -171,6 +171,13 @@ public override void OnBeforeCompleteType(ITypeCompletionContext completionConte
}
}
+internal sealed class CostDirectiveTypeInterceptor : TypeInterceptor
+{
+ internal override bool SkipDirectiveDefinition(DirectiveDefinitionNode node)
+ => node.Name.Value.Equals("cost", StringComparison.Ordinal)
+ || node.Name.Value.Equals("listSize", StringComparison.Ordinal);
+}
+
file static class Extensions
{
public static bool HasCostDirective(this IHasDirectiveDefinition directiveProvider)
diff --git a/src/HotChocolate/CostAnalysis/src/CostAnalysis/DependencyInjection/CostAnalyzerRequestExecutorBuilderExtensions.cs b/src/HotChocolate/CostAnalysis/src/CostAnalysis/DependencyInjection/CostAnalyzerRequestExecutorBuilderExtensions.cs
index 64560d2c558..7777311ac9e 100644
--- a/src/HotChocolate/CostAnalysis/src/CostAnalysis/DependencyInjection/CostAnalyzerRequestExecutorBuilderExtensions.cs
+++ b/src/HotChocolate/CostAnalysis/src/CostAnalysis/DependencyInjection/CostAnalyzerRequestExecutorBuilderExtensions.cs
@@ -56,12 +56,14 @@ public static IRequestExecutorBuilder AddCostAnalyzer(this IRequestExecutorBuild
requestOptions.MaxFieldCost,
requestOptions.MaxTypeCost,
requestOptions.EnforceCostLimits,
+ requestOptions.SkipAnalyzer,
requestOptions.Filtering.VariableMultiplier);
});
})
.AddDirectiveType()
.AddDirectiveType()
.TryAddTypeInterceptor()
+ .TryAddTypeInterceptor()
// we are replacing the default pipeline if the cost analyzer is added.
.Configure(c => c.DefaultPipelineFactory = AddDefaultPipeline);
diff --git a/src/HotChocolate/CostAnalysis/src/CostAnalysis/Extensions/CostAnalyzerRequestContextExtensions.cs b/src/HotChocolate/CostAnalysis/src/CostAnalysis/Extensions/CostAnalyzerRequestContextExtensions.cs
index e3c42d18810..e768a648434 100644
--- a/src/HotChocolate/CostAnalysis/src/CostAnalysis/Extensions/CostAnalyzerRequestContextExtensions.cs
+++ b/src/HotChocolate/CostAnalysis/src/CostAnalysis/Extensions/CostAnalyzerRequestContextExtensions.cs
@@ -58,13 +58,18 @@ public static CostMetrics GetCostMetrics(
internal static CostAnalyzerMode GetCostAnalyzerMode(
this IRequestContext context,
- bool enforceCostLimits)
+ RequestCostOptions options)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
+ if (options.SkipAnalyzer)
+ {
+ return CostAnalyzerMode.Skip;
+ }
+
if (context.ContextData.ContainsKey(WellKnownContextData.ValidateCost))
{
return CostAnalyzerMode.Analyze | CostAnalyzerMode.Report;
@@ -72,7 +77,7 @@ internal static CostAnalyzerMode GetCostAnalyzerMode(
var flags = CostAnalyzerMode.Analyze;
- if (enforceCostLimits)
+ if (options.EnforceCostLimits)
{
flags |= CostAnalyzerMode.Enforce;
}
diff --git a/src/HotChocolate/CostAnalysis/src/CostAnalysis/HotChocolate.CostAnalysis.csproj b/src/HotChocolate/CostAnalysis/src/CostAnalysis/HotChocolate.CostAnalysis.csproj
index e3fdbd68bfd..c3bd3025c3f 100644
--- a/src/HotChocolate/CostAnalysis/src/CostAnalysis/HotChocolate.CostAnalysis.csproj
+++ b/src/HotChocolate/CostAnalysis/src/CostAnalysis/HotChocolate.CostAnalysis.csproj
@@ -2,6 +2,7 @@
+
diff --git a/src/HotChocolate/CostAnalysis/src/CostAnalysis/Options/CostOptions.cs b/src/HotChocolate/CostAnalysis/src/CostAnalysis/Options/CostOptions.cs
index b036e72187b..3fd1ec5f593 100644
--- a/src/HotChocolate/CostAnalysis/src/CostAnalysis/Options/CostOptions.cs
+++ b/src/HotChocolate/CostAnalysis/src/CostAnalysis/Options/CostOptions.cs
@@ -5,6 +5,9 @@ namespace HotChocolate.CostAnalysis;
///
public sealed class CostOptions
{
+ private bool _skipAnalyzer = false;
+ private bool _enforceCostLimits = true;
+
///
/// Gets or sets the maximum allowed field cost.
///
@@ -18,7 +21,35 @@ public sealed class CostOptions
///
/// Defines if the analyzer shall enforce cost limits.
///
- public bool EnforceCostLimits { get; set; } = true;
+ public bool EnforceCostLimits
+ {
+ get => _enforceCostLimits;
+ set
+ {
+ if(value)
+ {
+ SkipAnalyzer = false;
+ }
+
+ _enforceCostLimits = value;
+ }
+ }
+
+ ///
+ /// Skips the cost analyzer.
+ ///
+ public bool SkipAnalyzer
+ {
+ get => _skipAnalyzer;
+ set
+ {
+ if(value)
+ {
+ EnforceCostLimits = false;
+ }
+ _skipAnalyzer = value;
+ }
+ }
///
/// Defines if cost defaults shall be applied to the schema.
diff --git a/src/HotChocolate/CostAnalysis/src/CostAnalysis/Options/RequestCostOptions.cs b/src/HotChocolate/CostAnalysis/src/CostAnalysis/Options/RequestCostOptions.cs
index 0b111285f7d..6d64d41630f 100644
--- a/src/HotChocolate/CostAnalysis/src/CostAnalysis/Options/RequestCostOptions.cs
+++ b/src/HotChocolate/CostAnalysis/src/CostAnalysis/Options/RequestCostOptions.cs
@@ -3,20 +3,145 @@ namespace HotChocolate.CostAnalysis;
///
/// Request options for cost analysis.
///
-///
-/// The maximum allowed field cost.
-///
-///
-/// The maximum allowed type cost.
-///
-///
-/// Defines if the analyzer shall enforce cost limits.
-///
-///
-/// The filter variable multiplier.
-///
-public record RequestCostOptions(
- double MaxFieldCost,
- double MaxTypeCost,
- bool EnforceCostLimits,
- int? FilterVariableMultiplier);
+public record RequestCostOptions
+{
+ ///
+ /// Request options for cost analysis.
+ ///
+ ///
+ /// The maximum allowed field cost.
+ ///
+ ///
+ /// The maximum allowed type cost.
+ ///
+ ///
+ /// Defines if the analyzer shall enforce cost limits.
+ ///
+ ///
+ /// The filter variable multiplier.
+ ///
+ public RequestCostOptions(
+ double maxFieldCost,
+ double maxTypeCost,
+ bool enforceCostLimits,
+ int? filterVariableMultiplier)
+ {
+ MaxFieldCost = maxFieldCost;
+ MaxTypeCost = maxTypeCost;
+ EnforceCostLimits = enforceCostLimits;
+ FilterVariableMultiplier = filterVariableMultiplier;
+ }
+
+ ///
+ /// Gets the maximum allowed field cost.
+ ///
+ ///
+ /// The maximum allowed field cost.
+ ///
+ ///
+ /// The maximum allowed type cost.
+ ///
+ ///
+ /// Defines if the analyzer shall enforce cost limits.
+ ///
+ ///
+ /// Defines if the cost analyzer shall be skipped.
+ ///
+ ///
+ /// The filter variable multiplier.
+ ///
+ public RequestCostOptions(
+ double maxFieldCost,
+ double maxTypeCost,
+ bool enforceCostLimits,
+ bool skipAnalyzer,
+ int? filterVariableMultiplier)
+ {
+ MaxFieldCost = maxFieldCost;
+ MaxTypeCost = maxTypeCost;
+ EnforceCostLimits = enforceCostLimits;
+ SkipAnalyzer = skipAnalyzer;
+ FilterVariableMultiplier = filterVariableMultiplier;
+ }
+
+ ///
+ /// Gets the maximum allowed field cost.
+ ///
+ public double MaxFieldCost { get; init; }
+
+ ///
+ /// Gets the maximum allowed type cost.
+ ///
+ public double MaxTypeCost { get; init; }
+
+ ///
+ /// Defines if the analyzer shall enforce cost limits.
+ ///
+ public bool EnforceCostLimits
+ {
+ get;
+ init
+ {
+ if (value)
+ {
+ SkipAnalyzer = false;
+ }
+
+ field = value;
+ }
+ }
+
+ ///
+ /// Defines if the cost analyzer shall be skipped.
+ ///
+ public bool SkipAnalyzer
+ {
+ get;
+ init
+ {
+ if (value)
+ {
+ EnforceCostLimits = false;
+ }
+
+ field = value;
+ }
+ }
+
+ ///
+ /// Gets the filter variable multiplier.
+ ///
+ public int? FilterVariableMultiplier { get; init; }
+
+ ///
+ /// Deconstructs the request options.
+ ///
+ ///
+ /// The maximum allowed field cost.
+ ///
+ ///
+ /// The maximum allowed type cost.
+ ///
+ ///
+ /// Defines if the analyzer shall enforce cost limits.
+ ///
+ ///
+ /// Defines if the cost analyzer shall be skipped.
+ ///
+ ///
+ /// The filter variable multiplier.
+ ///
+ public void Deconstruct(
+ out double maxFieldCost,
+ out double maxTypeCost,
+ out bool enforceCostLimits,
+ out bool skipAnalyzer,
+ out int? filterVariableMultiplier)
+ {
+ maxFieldCost = MaxFieldCost;
+ maxTypeCost = MaxTypeCost;
+ enforceCostLimits = EnforceCostLimits;
+ skipAnalyzer = SkipAnalyzer;
+ filterVariableMultiplier = FilterVariableMultiplier;
+ }
+}
diff --git a/src/HotChocolate/Fusion/src/Composition/Extensions/MergeExtensions.cs b/src/HotChocolate/Fusion/src/Composition/Extensions/MergeExtensions.cs
index 49503d22ec0..4e504bd2907 100644
--- a/src/HotChocolate/Fusion/src/Composition/Extensions/MergeExtensions.cs
+++ b/src/HotChocolate/Fusion/src/Composition/Extensions/MergeExtensions.cs
@@ -1,5 +1,7 @@
+using HotChocolate.Language;
using HotChocolate.Skimmed;
using HotChocolate.Types;
+using HotChocolate.Utilities;
namespace HotChocolate.Fusion.Composition;
@@ -189,6 +191,25 @@ internal static void MergeDirectivesWith(
}
else
{
+ if (directive.Name.EqualsOrdinal("cost"))
+ {
+ var currentCost = target.Directives.FirstOrDefault("cost")!;
+ if (currentCost.Arguments.TryGetValue("weight", out var value)
+ && value is StringValueNode stringValueNode
+ && double.TryParse(stringValueNode.Value, out var currentWeight)
+ && directive.Arguments.TryGetValue("weight", out value)
+ && value is StringValueNode newStringValueNode
+ && double.TryParse(newStringValueNode.Value, out var newWeight)
+ && newWeight > currentWeight)
+ {
+ target.Directives.Remove(currentCost);
+ target.Directives.Add(directive);
+ }
+
+ continue;
+ }
+
+
if (directiveDefinition is not null && directiveDefinition.IsRepeatable)
{
target.Directives.Add(directive);
diff --git a/src/HotChocolate/Fusion/src/Core/DependencyInjection/FusionRequestExecutorBuilderExtensions.cs b/src/HotChocolate/Fusion/src/Core/DependencyInjection/FusionRequestExecutorBuilderExtensions.cs
index 12ba1e5490a..7deb82e82c6 100644
--- a/src/HotChocolate/Fusion/src/Core/DependencyInjection/FusionRequestExecutorBuilderExtensions.cs
+++ b/src/HotChocolate/Fusion/src/Core/DependencyInjection/FusionRequestExecutorBuilderExtensions.cs
@@ -1,4 +1,5 @@
using HotChocolate;
+using HotChocolate.CostAnalysis;
using HotChocolate.Execution;
using HotChocolate.Execution.Configuration;
using HotChocolate.Execution.Errors;
@@ -30,6 +31,9 @@ public static class FusionRequestExecutorBuilderExtensions
///
/// The name of the fusion graph.
///
+ ///
+ /// If set to true the default security policy is disabled.
+ ///
///
/// Returns the that can be used to configure the Gateway.
///
@@ -38,7 +42,8 @@ public static class FusionRequestExecutorBuilderExtensions
///
public static FusionGatewayBuilder AddFusionGatewayServer(
this IServiceCollection services,
- string? graphName = null)
+ string? graphName = null,
+ bool disableDefaultSecurity = false)
{
ArgumentNullException.ThrowIfNull(services);
@@ -53,11 +58,12 @@ public static FusionGatewayBuilder AddFusionGatewayServer(
sp.GetRequiredService()));
var builder = services
- .AddGraphQLServer(graphName, disableDefaultSecurity: true)
+ .AddGraphQLServer(graphName, disableDefaultSecurity: disableDefaultSecurity)
.UseField(next => next)
.AddOperationCompilerOptimizer()
.AddOperationCompilerOptimizer()
.AddConvention(_ => new DefaultNamingConventions())
+ .ModifyCostOptions(o => o.ApplyCostDefaults = false)
.Configure(
c =>
{
@@ -562,6 +568,7 @@ private static IRequestExecutorBuilder UseFusionDefaultPipeline(
.UseDocumentCache()
.UseDocumentParser()
.UseDocumentValidation()
+ .UseCostAnalyzer()
.UseOperationCache()
.UseOperationResolver()
.UseSkipWarmupExecution()
@@ -638,6 +645,7 @@ internal static void AddDefaultPipeline(this IList pipeli
pipeline.Add(DocumentCacheMiddleware.Create());
pipeline.Add(DocumentParserMiddleware.Create());
pipeline.Add(DocumentValidationMiddleware.Create());
+ pipeline.Add(CostAnalyzerMiddleware.Create());
pipeline.Add(OperationCacheMiddleware.Create());
pipeline.Add(OperationResolverMiddleware.Create());
pipeline.Add(OperationVariableCoercionMiddleware.Create());
diff --git a/src/HotChocolate/Fusion/test/Composition.Tests/DemoIntegrationTests.cs b/src/HotChocolate/Fusion/test/Composition.Tests/DemoIntegrationTests.cs
index dd15057c9d6..9192af30607 100644
--- a/src/HotChocolate/Fusion/test/Composition.Tests/DemoIntegrationTests.cs
+++ b/src/HotChocolate/Fusion/test/Composition.Tests/DemoIntegrationTests.cs
@@ -26,6 +26,24 @@ public async Task Accounts_And_Reviews()
fusionConfig.MatchSnapshot(extension: ".graphql");
}
+ [Fact]
+ public async Task Accounts_And_Reviews_With_Cost()
+ {
+ // arrange
+ using var demoProject = await DemoProject.CreateAsync(enableCost: true);
+
+ var composer = new FusionGraphComposer(logFactory: _logFactory);
+
+ var fusionConfig = await composer.ComposeAsync(
+ [
+ demoProject.Accounts.ToConfiguration(AccountsExtensionWithCostSdl),
+ demoProject.Reviews.ToConfiguration(ReviewsExtensionWithCostSdl)
+ ]);
+
+ fusionConfig.MatchSnapshot(extension: ".graphql");
+ }
+
+
[Fact]
public async Task Accounts_And_Reviews_Infer_Patterns()
{
diff --git a/src/HotChocolate/Fusion/test/Composition.Tests/__snapshots__/DemoIntegrationTests.Accounts_And_Reviews_With_Cost.graphql b/src/HotChocolate/Fusion/test/Composition.Tests/__snapshots__/DemoIntegrationTests.Accounts_And_Reviews_With_Cost.graphql
new file mode 100644
index 00000000000..06f25f2f81a
--- /dev/null
+++ b/src/HotChocolate/Fusion/test/Composition.Tests/__snapshots__/DemoIntegrationTests.Accounts_And_Reviews_With_Cost.graphql
@@ -0,0 +1,148 @@
+schema
+ @fusion(version: 1)
+ @transport(subgraph: "Accounts", location: "http:\/\/localhost:5000\/graphql", kind: "HTTP")
+ @transport(subgraph: "Accounts", location: "ws:\/\/localhost:5000\/graphql", kind: "WebSocket")
+ @transport(subgraph: "Reviews", location: "http:\/\/localhost:5000\/graphql", kind: "HTTP")
+ @transport(subgraph: "Reviews", location: "ws:\/\/localhost:5000\/graphql", kind: "WebSocket") {
+ query: Query
+ mutation: Mutation
+ subscription: Subscription
+}
+
+type Query {
+ errorField: String
+ @resolver(subgraph: "Accounts", select: "{ errorField }")
+ productById(id: ID!): Product
+ @variable(subgraph: "Reviews", name: "id", argument: "id")
+ @resolver(subgraph: "Reviews", select: "{ productById(id: $id) }", arguments: [ { name: "id", type: "ID!" } ])
+ reviewById(id: ID!): Review
+ @variable(subgraph: "Reviews", name: "id", argument: "id")
+ @resolver(subgraph: "Reviews", select: "{ reviewById(id: $id) }", arguments: [ { name: "id", type: "ID!" } ])
+ reviewOrAuthor: ReviewOrAuthor!
+ @resolver(subgraph: "Reviews", select: "{ reviewOrAuthor }")
+ reviews: [Review!]!
+ @resolver(subgraph: "Reviews", select: "{ reviews }")
+ userById(id: ID!): User
+ @variable(subgraph: "Accounts", name: "id", argument: "id")
+ @resolver(subgraph: "Accounts", select: "{ userById(id: $id) }", arguments: [ { name: "id", type: "ID!" } ])
+ @cost(weight: "2.0")
+ @variable(subgraph: "Reviews", name: "id", argument: "id")
+ @resolver(subgraph: "Reviews", select: "{ authorById(id: $id) }", arguments: [ { name: "id", type: "ID!" } ])
+ users: [User!]!
+ @resolver(subgraph: "Accounts", select: "{ users }")
+ usersById(ids: [ID!]!): [User!]!
+ @variable(subgraph: "Accounts", name: "ids", argument: "ids")
+ @resolver(subgraph: "Accounts", select: "{ usersById(ids: $ids) }", arguments: [ { name: "ids", type: "[ID!]!" } ])
+ viewer: Viewer!
+ @resolver(subgraph: "Accounts", select: "{ viewer }")
+}
+
+type Mutation {
+ addReview(input: AddReviewInput!): AddReviewPayload!
+ @variable(subgraph: "Reviews", name: "input", argument: "input")
+ @resolver(subgraph: "Reviews", select: "{ addReview(input: $input) }", arguments: [ { name: "input", type: "AddReviewInput!" } ])
+ addUser(input: AddUserInput!): AddUserPayload!
+ @variable(subgraph: "Accounts", name: "input", argument: "input")
+ @resolver(subgraph: "Accounts", select: "{ addUser(input: $input) }", arguments: [ { name: "input", type: "AddUserInput!" } ])
+}
+
+type Subscription {
+ onError: Review!
+ @resolver(subgraph: "Reviews", select: "{ onError }", kind: "SUBSCRIBE")
+ onNewReview: Review!
+ @resolver(subgraph: "Reviews", select: "{ onNewReview }", kind: "SUBSCRIBE")
+}
+
+type AddReviewPayload {
+ review: Review
+ @source(subgraph: "Reviews")
+}
+
+type AddUserPayload {
+ user: User
+ @source(subgraph: "Accounts")
+}
+
+type Product
+ @variable(subgraph: "Reviews", name: "Product_id", select: "id")
+ @resolver(subgraph: "Reviews", select: "{ productById(id: $Product_id) }", arguments: [ { name: "Product_id", type: "ID!" } ]) {
+ id: ID!
+ @source(subgraph: "Reviews")
+ reviews: [Review!]!
+ @source(subgraph: "Reviews")
+}
+
+type Review implements Node
+ @variable(subgraph: "Reviews", name: "Review_id", select: "id")
+ @resolver(subgraph: "Reviews", select: "{ reviewById(id: $Review_id) }", arguments: [ { name: "Review_id", type: "ID!" } ])
+ @resolver(subgraph: "Reviews", select: "{ nodes(ids: $Review_id) { ... on Review { ... Review } } }", arguments: [ { name: "Review_id", type: "[ID!]!" } ], kind: "BATCH") {
+ author: User!
+ @source(subgraph: "Reviews")
+ body: String!
+ @source(subgraph: "Reviews")
+ id: ID!
+ @source(subgraph: "Reviews")
+ product: Product!
+ @source(subgraph: "Reviews")
+}
+
+type SomeData {
+ accountValue: String!
+ @source(subgraph: "Accounts")
+}
+
+type User implements Node
+ @source(subgraph: "Reviews", name: "Author")
+ @variable(subgraph: "Accounts", name: "User_id", select: "id")
+ @variable(subgraph: "Reviews", name: "User_id", select: "id")
+ @resolver(subgraph: "Accounts", select: "{ userById(id: $User_id) }", arguments: [ { name: "User_id", type: "ID!" } ])
+ @resolver(subgraph: "Accounts", select: "{ usersById(ids: $User_id) }", arguments: [ { name: "User_id", type: "[ID!]!" } ], kind: "BATCH")
+ @resolver(subgraph: "Reviews", select: "{ authorById(id: $User_id) }", arguments: [ { name: "User_id", type: "ID!" } ])
+ @resolver(subgraph: "Reviews", select: "{ nodes(ids: $User_id) { ... on User { ... User } } }", arguments: [ { name: "User_id", type: "[ID!]!" } ], kind: "BATCH") {
+ birthdate: Date!
+ @source(subgraph: "Accounts")
+ errorField: String
+ @source(subgraph: "Accounts")
+ id: ID!
+ @source(subgraph: "Accounts")
+ @source(subgraph: "Reviews")
+ name: String!
+ @source(subgraph: "Accounts")
+ @source(subgraph: "Reviews")
+ reviews: [Review!]!
+ @source(subgraph: "Reviews")
+ username: String!
+ @source(subgraph: "Accounts")
+}
+
+type Viewer {
+ data: SomeData!
+ @source(subgraph: "Accounts")
+ user: User
+ @source(subgraph: "Accounts")
+}
+
+"The node interface is implemented by entities that have a global unique identifier."
+interface Node {
+ id: ID!
+}
+
+union ReviewOrAuthor = User | Review
+
+input AddReviewInput {
+ authorId: Int!
+ body: String!
+ upc: Int!
+}
+
+input AddUserInput {
+ birthdate: Date!
+ name: String!
+ username: String!
+}
+
+"The `Date` scalar represents an ISO-8601 compliant date type."
+scalar Date
+
+"The purpose of the `cost` directive is to define a `weight` for GraphQL types, fields, and arguments. Static analysis can use these weights when calculating the overall cost of a query or response."
+directive @cost("The `weight` argument defines what value to add to the overall cost for every appearance, or possible appearance, of a type, field, argument, etc." weight: String!) on SCALAR | OBJECT | FIELD_DEFINITION | ARGUMENT_DEFINITION | ENUM | INPUT_FIELD_DEFINITION
diff --git a/src/HotChocolate/Fusion/test/Core.Tests/DemoIntegrationTests.cs b/src/HotChocolate/Fusion/test/Core.Tests/DemoIntegrationTests.cs
index 645ccc51c96..bf1ad87ccce 100644
--- a/src/HotChocolate/Fusion/test/Core.Tests/DemoIntegrationTests.cs
+++ b/src/HotChocolate/Fusion/test/Core.Tests/DemoIntegrationTests.cs
@@ -103,6 +103,58 @@ query GetUser {
Assert.Null(result.ExpectOperationResult().Errors);
}
+ [Fact]
+ public async Task Authors_And_Reviews_Query_GetUserReviews_Report_Cost()
+ {
+ // arrange
+ using var demoProject = await DemoProject.CreateAsync(enableCost: true);
+
+ // act
+ var fusionGraph = await new FusionGraphComposer(logFactory: _logFactory)
+ .ComposeAsync(
+ [
+ demoProject.Reviews2.ToConfiguration(Reviews2ExtensionWithCostSdl),
+ demoProject.Accounts.ToConfiguration(AccountsExtensionWithCostSdl)
+ ]);
+
+ var executor = await new ServiceCollection()
+ .AddSingleton(demoProject.HttpClientFactory)
+ .AddSingleton(demoProject.WebSocketConnectionFactory)
+ .AddFusionGatewayServer()
+ .ConfigureFromDocument(SchemaFormatter.FormatAsDocument(fusionGraph))
+ .BuildRequestExecutorAsync();
+
+ var request = Parse(
+ """
+ query GetUser {
+ users {
+ name
+ reviews {
+ body
+ author {
+ name
+ }
+ }
+ }
+ }
+ """);
+
+ // act
+ await using var result = await executor.ExecuteAsync(
+ OperationRequestBuilder
+ .New()
+ .SetDocument(request)
+ .ReportCost()
+ .Build());
+
+ // assert
+ var snapshot = new Snapshot();
+ CollectSnapshotData(snapshot, request, result);
+ await snapshot.MatchMarkdownAsync();
+
+ Assert.Null(result.ExpectOperationResult().Errors);
+ }
+
[Fact]
public async Task Authors_And_Reviews_Query_GetUserReviews_Skip_Author()
{
diff --git a/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/DemoIntegrationTests.Authors_And_Reviews_And_Products_Introspection.md b/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/DemoIntegrationTests.Authors_And_Reviews_And_Products_Introspection.md
index c34af4e6935..ae0a87f24c3 100644
--- a/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/DemoIntegrationTests.Authors_And_Reviews_And_Products_Introspection.md
+++ b/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/DemoIntegrationTests.Authors_And_Reviews_And_Products_Introspection.md
@@ -943,12 +943,12 @@
"fields": null
},
{
- "name": "ID",
+ "name": "Int",
"kind": "SCALAR",
"fields": null
},
{
- "name": "Int",
+ "name": "ID",
"kind": "SCALAR",
"fields": null
},
diff --git a/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/DemoIntegrationTests.Authors_And_Reviews_Query_GetUserReviews_Report_Cost.md b/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/DemoIntegrationTests.Authors_And_Reviews_Query_GetUserReviews_Report_Cost.md
new file mode 100644
index 00000000000..3e3c08d5265
--- /dev/null
+++ b/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/DemoIntegrationTests.Authors_And_Reviews_Query_GetUserReviews_Report_Cost.md
@@ -0,0 +1,129 @@
+# Authors_And_Reviews_Query_GetUserReviews_Report_Cost
+
+## Result
+
+```json
+{
+ "data": {
+ "users": [
+ {
+ "name": "Ada Lovelace",
+ "reviews": [
+ {
+ "body": "Love it!",
+ "author": {
+ "name": "@ada"
+ }
+ },
+ {
+ "body": "Could be better.",
+ "author": {
+ "name": "@ada"
+ }
+ }
+ ]
+ },
+ {
+ "name": "Alan Turing",
+ "reviews": [
+ {
+ "body": "Too expensive.",
+ "author": {
+ "name": "@alan"
+ }
+ },
+ {
+ "body": "Prefer something else.",
+ "author": {
+ "name": "@alan"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ "extensions": {
+ "operationCost": {
+ "fieldCost": 12,
+ "typeCost": 22
+ }
+ }
+}
+```
+
+## Request
+
+```graphql
+query GetUser {
+ users {
+ name
+ reviews {
+ body
+ author {
+ name
+ }
+ }
+ }
+}
+```
+
+## QueryPlan Hash
+
+```text
+8F6E2CCB58DA60498BA9F134BF2F1B8D5C24FBA0
+```
+
+## QueryPlan
+
+```json
+{
+ "document": "query GetUser { users { name reviews { body author { name } } } }",
+ "operation": "GetUser",
+ "rootNode": {
+ "type": "Sequence",
+ "nodes": [
+ {
+ "type": "Resolve",
+ "subgraph": "Accounts",
+ "document": "query GetUser_1 { users { name __fusion_exports__1: id } }",
+ "selectionSetId": 0,
+ "provides": [
+ {
+ "variable": "__fusion_exports__1"
+ }
+ ]
+ },
+ {
+ "type": "Compose",
+ "selectionSetIds": [
+ 0
+ ]
+ },
+ {
+ "type": "ResolveByKeyBatch",
+ "subgraph": "Reviews2",
+ "document": "query GetUser_2($__fusion_exports__1: [ID!]!) { nodes(ids: $__fusion_exports__1) { ... on User { reviews { body author { name } } __fusion_exports__1: id } } }",
+ "selectionSetId": 1,
+ "path": [
+ "nodes"
+ ],
+ "requires": [
+ {
+ "variable": "__fusion_exports__1"
+ }
+ ]
+ },
+ {
+ "type": "Compose",
+ "selectionSetIds": [
+ 1
+ ]
+ }
+ ]
+ },
+ "state": {
+ "__fusion_exports__1": "User_id"
+ }
+}
+```
+
diff --git a/src/HotChocolate/Fusion/test/Shared/Accounts/AccountQuery.cs b/src/HotChocolate/Fusion/test/Shared/Accounts/AccountQuery.cs
index eac2c01bbab..640a496e00d 100644
--- a/src/HotChocolate/Fusion/test/Shared/Accounts/AccountQuery.cs
+++ b/src/HotChocolate/Fusion/test/Shared/Accounts/AccountQuery.cs
@@ -1,3 +1,4 @@
+using HotChocolate.CostAnalysis.Types;
using HotChocolate.Resolvers;
using HotChocolate.Types.Relay;
diff --git a/src/HotChocolate/Fusion/test/Shared/DemoProject.cs b/src/HotChocolate/Fusion/test/Shared/DemoProject.cs
index a5abf239d40..89b14e0dbb5 100644
--- a/src/HotChocolate/Fusion/test/Shared/DemoProject.cs
+++ b/src/HotChocolate/Fusion/test/Shared/DemoProject.cs
@@ -82,7 +82,14 @@ private DemoProject(
public DemoSubgraph Resale { get; }
- public static async Task CreateAsync(CancellationToken ct = default)
+ public static async Task CreateAsync(
+ CancellationToken ct = default)
+ => await CreateAsync(false, ct).ConfigureAwait(false);
+
+
+ public static async Task CreateAsync(
+ bool enableCost,
+ CancellationToken ct = default)
{
var disposables = new List();
TestServerFactory testServerFactory = new();
@@ -92,7 +99,8 @@ public static async Task CreateAsync(CancellationToken ct = default
s => s
.AddRouting()
.AddSingleton()
- .AddGraphQLServer(disableDefaultSecurity: true)
+ .AddGraphQLServer(disableDefaultSecurity: !enableCost)
+ .DisableIntrospection(false)
.AddQueryType()
.AddMutationType()
.AddSubscriptionType()
@@ -115,7 +123,8 @@ public static async Task CreateAsync(CancellationToken ct = default
s => s
.AddRouting()
.AddSingleton()
- .AddGraphQLServer(disableDefaultSecurity: true)
+ .AddGraphQLServer(disableDefaultSecurity: !enableCost)
+ .DisableIntrospection(false)
.AddQueryType()
.AddMutationType()
.AddSubscriptionType()
@@ -138,7 +147,8 @@ public static async Task CreateAsync(CancellationToken ct = default
s => s
.AddRouting()
.AddSingleton()
- .AddGraphQLServer(disableDefaultSecurity: true)
+ .AddGraphQLServer(disableDefaultSecurity: !enableCost)
+ .DisableIntrospection(false)
.AddQueryType()
.AddMutationType()
.AddMutationConventions()
@@ -159,7 +169,8 @@ public static async Task CreateAsync(CancellationToken ct = default
s => s
.AddRouting()
.AddSingleton()
- .AddGraphQLServer(disableDefaultSecurity: true)
+ .AddGraphQLServer(disableDefaultSecurity: !enableCost)
+ .DisableIntrospection(false)
.AddQueryType()
.AddMutationType()
.AddGlobalObjectIdentification()
@@ -180,7 +191,8 @@ public static async Task CreateAsync(CancellationToken ct = default
var shipping = testServerFactory.Create(
s => s
.AddRouting()
- .AddGraphQLServer(disableDefaultSecurity: true)
+ .AddGraphQLServer(disableDefaultSecurity: !enableCost)
+ .DisableIntrospection(false)
.AddQueryType()
.ConfigureSchema(b => b.SetContextData(GlobalIdSupportEnabled, 1))
.AddConvention(_ => new DefaultNamingConventions()),
@@ -198,7 +210,8 @@ public static async Task CreateAsync(CancellationToken ct = default
var shipping2 = testServerFactory.Create(
s => s
.AddRouting()
- .AddGraphQLServer(disableDefaultSecurity: true)
+ .AddGraphQLServer(disableDefaultSecurity: !enableCost)
+ .DisableIntrospection(false)
.AddQueryType()
.ConfigureSchema(b => b.SetContextData(GlobalIdSupportEnabled, 1))
.AddConvention(_ => new DefaultNamingConventions()),
@@ -216,7 +229,8 @@ public static async Task CreateAsync(CancellationToken ct = default
var appointment = testServerFactory.Create(
s => s
.AddRouting()
- .AddGraphQLServer(disableDefaultSecurity: true)
+ .AddGraphQLServer(disableDefaultSecurity: !enableCost)
+ .DisableIntrospection(false)
.AddQueryType()
.AddObjectType()
.AddObjectType()
@@ -236,7 +250,8 @@ public static async Task CreateAsync(CancellationToken ct = default
var patient1 = testServerFactory.Create(
s => s
.AddRouting()
- .AddGraphQLServer(disableDefaultSecurity: true)
+ .AddGraphQLServer(disableDefaultSecurity: !enableCost)
+ .DisableIntrospection(false)
.AddQueryType()
.AddGlobalObjectIdentification()
.AddConvention(_ => new DefaultNamingConventions()),
@@ -254,7 +269,8 @@ public static async Task CreateAsync(CancellationToken ct = default
var books = testServerFactory.Create(
s => s
.AddRouting()
- .AddGraphQLServer(disableDefaultSecurity: true)
+ .AddGraphQLServer(disableDefaultSecurity: !enableCost)
+ .DisableIntrospection(false)
.AddQueryType()
.AddConvention(_ => new DefaultNamingConventions()),
c => c
@@ -271,7 +287,8 @@ public static async Task CreateAsync(CancellationToken ct = default
var authors = testServerFactory.Create(
s => s
.AddRouting()
- .AddGraphQLServer(disableDefaultSecurity: true)
+ .AddGraphQLServer(disableDefaultSecurity: !enableCost)
+ .DisableIntrospection(false)
.AddQueryType()
.AddConvention(_ => new DefaultNamingConventions()),
c => c
@@ -288,7 +305,8 @@ public static async Task CreateAsync(CancellationToken ct = default
var resale = testServerFactory.Create(
s => s
.AddRouting()
- .AddGraphQLServer(disableDefaultSecurity: true)
+ .AddGraphQLServer(disableDefaultSecurity: !enableCost)
+ .DisableIntrospection(false)
.AddQueryType()
.AddGlobalObjectIdentification()
.AddMutationConventions()
diff --git a/src/HotChocolate/Fusion/test/Shared/DemoProjectSchemaExtensions.cs b/src/HotChocolate/Fusion/test/Shared/DemoProjectSchemaExtensions.cs
index 295509aa216..845d755f053 100644
--- a/src/HotChocolate/Fusion/test/Shared/DemoProjectSchemaExtensions.cs
+++ b/src/HotChocolate/Fusion/test/Shared/DemoProjectSchemaExtensions.cs
@@ -10,6 +10,14 @@ extend type Query {
}
""";
+ public const string AccountsExtensionWithCostSdl =
+ """
+ extend type Query {
+ userById(id: ID! @is(field: "id")): User! @cost(weight: "1.0")
+ usersById(ids: [ID!]! @is(field: "id")): [User!]!
+ }
+ """;
+
public const string AccountsExtensionWithTagSdl =
"""
extend type Query {
@@ -46,6 +54,19 @@ extend type Query {
}
""";
+ public const string ReviewsExtensionWithCostSdl =
+ """
+ extend type Query {
+ authorById(id: ID! @is(field: "id")): Author @cost(weight: "2.0")
+ productById(id: ID! @is(field: "id")): Product
+ }
+
+ schema
+ @rename(coordinate: "Query.authorById", newName: "userById")
+ @rename(coordinate: "Author", newName: "User") {
+ }
+ """;
+
public const string ReviewsExtensionWithTagSdl =
"""
extend type Query {
@@ -72,6 +93,29 @@ extend type Query {
}
""";
+ public const string Reviews2ExtensionWithCostSdl =
+ """
+ extend type Query {
+ authorById(id: ID! @is(field: "id")): User @cost(weight: "2.0")
+ productById(id: ID! @is(field: "id")): Product @cost(weight: "1.0")
+ }
+
+ extend type User {
+ reviews: [Review!]! @listSize(assumedSize: 10)
+ }
+
+ schema
+ @rename(coordinate: "Query.authorById", newName: "userById") {
+ }
+
+ directive @listSize(
+ assumedSize: Int
+ slicingArguments: [String!]
+ slicingArgumentDefaultValue: Int
+ sizedFields: [String!]
+ requireOneSlicingArgument: Boolean! = true) on FIELD_DEFINITION
+ """;
+
public const string ProductsExtensionSdl =
"""
extend type Query {
diff --git a/src/HotChocolate/Fusion/test/Shared/Reviews/ReviewsQuery.cs b/src/HotChocolate/Fusion/test/Shared/Reviews/ReviewsQuery.cs
index f7ebfc499ae..dcf083cca56 100644
--- a/src/HotChocolate/Fusion/test/Shared/Reviews/ReviewsQuery.cs
+++ b/src/HotChocolate/Fusion/test/Shared/Reviews/ReviewsQuery.cs
@@ -1,3 +1,4 @@
+using HotChocolate.CostAnalysis.Types;
using HotChocolate.Types.Relay;
namespace HotChocolate.Fusion.Shared.Reviews;
diff --git a/src/HotChocolate/Skimmed/src/Skimmed/DirectiveDefinition.cs b/src/HotChocolate/Skimmed/src/Skimmed/DirectiveDefinition.cs
index 604c28c0edb..c2416810d49 100644
--- a/src/HotChocolate/Skimmed/src/Skimmed/DirectiveDefinition.cs
+++ b/src/HotChocolate/Skimmed/src/Skimmed/DirectiveDefinition.cs
@@ -8,13 +8,13 @@ namespace HotChocolate.Skimmed;
///
/// Represents a GraphQL directive definition.
///
-public class DirectiveDefinition(string name)
+public class DirectiveDefinition
: INamedTypeSystemMemberDefinition
, IDescriptionProvider
, IFeatureProvider
, ISealable
{
- private string _name = name.EnsureGraphQLName();
+ private string _name;
private IInputFieldDefinitionCollection? _arguments;
private IFeatureCollection? _features;
private string? _description;
@@ -23,6 +23,14 @@ public class DirectiveDefinition(string name)
private DirectiveLocation _locations;
private bool _isReadOnly;
+ ///
+ /// Represents a GraphQL directive definition.
+ ///
+ public DirectiveDefinition(string name)
+ {
+ _name = name.EnsureGraphQLName();
+ }
+
///
/// Gets or sets the name of the directive.
///