diff --git a/.markdownlint.yml b/.markdownlint.yml
index a42215e63e5..49e354cb724 100644
--- a/.markdownlint.yml
+++ b/.markdownlint.yml
@@ -18,9 +18,12 @@ MD033:
- ExampleTabs
- img
- InputChoiceTabs
+ - List
- PackageInstallation
+ - Panel
- Schema
- strong
+ - Tab
- u
- Video
MD036: false # Rationale: Possibly too strict, used frequently in documentation.
diff --git a/website/src/docs/docs.json b/website/src/docs/docs.json
index 8a6a51369ee..58f50d82d5c 100644
--- a/website/src/docs/docs.json
+++ b/website/src/docs/docs.json
@@ -392,8 +392,8 @@
"title": "Authorization"
},
{
- "path": "operation-complexity",
- "title": "Operation Complexity"
+ "path": "cost-analysis",
+ "title": "Cost Analysis"
}
]
},
diff --git a/website/src/docs/hotchocolate/v14/migrating/migrate-from-13-to-14.md b/website/src/docs/hotchocolate/v14/migrating/migrate-from-13-to-14.md
index c6a3728efbd..e507b493d51 100644
--- a/website/src/docs/hotchocolate/v14/migrating/migrate-from-13-to-14.md
+++ b/website/src/docs/hotchocolate/v14/migrating/migrate-from-13-to-14.md
@@ -76,6 +76,15 @@ The interface `IQueryResultBuilder` and its implementations were replaced with `
The interface `IQueryResultBuilder` and its implementations were replaced with `OperationResultBuilder` which produces an `OperationResult` on `Build()`.
+## Operation complexity analyzer replaced
+
+The Operation Complexity Analyzer in v13 has been replaced by Cost Analysis in v14, based on the draft [IBM Cost Analysis specification](https://ibm.github.io/graphql-specs/cost-spec.html).
+
+- The `Complexity` property on `RequestExecutorOptions` (accessed via `ModifyRequestOptions`) has been removed.
+- Cost analysis is enabled by default.
+
+Please see the [documentation](../../security/cost-analysis) for further information.
+
## DateTime scalar enforces a specific format
The `DateTime` scalar will now enforce a specific format. The time and offset are now required, and fractional seconds are limited to 7. This aligns it with the DateTime Scalar spec (), with the one difference being that fractions of a second are optional, and 0-7 digits may be specified.
diff --git a/website/src/docs/hotchocolate/v14/security/cost-analysis.md b/website/src/docs/hotchocolate/v14/security/cost-analysis.md
new file mode 100644
index 00000000000..56ee9704b8b
--- /dev/null
+++ b/website/src/docs/hotchocolate/v14/security/cost-analysis.md
@@ -0,0 +1,490 @@
+---
+title: Cost Analysis
+---
+
+import { List, Panel, Tab, Tabs } from "../../../../components/mdx/tabs";
+
+
+
+Cost analysis is a useful tool to make your API more secure. With Hot Chocolate, static cost analysis is built in, and is based on the draft [IBM Cost Analysis specification](https://ibm.github.io/graphql-specs/cost-spec.html).
+
+# Directives
+
+## Cost directive
+
+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.
+
+The `weight` argument defines what value to add to the overall cost for every
+appearance, or possible appearance, of a type, field, argument, etc.
+
+The `@cost` directive can be applied to argument definitions, enums, field definitions, input field definitions, object types, and scalars.
+
+## List Size directive
+
+The purpose of the `@listSize` directive is to either inform the static analysis about the size of returned lists (if that information is statically available), or to point the analysis to where to find that information.
+
+- The `assumedSize` argument can be used to statically define the maximum length of a list returned by a field.
+- The `slicingArguments` argument can be used to define which of the field's arguments with numeric type are slicing arguments, so that their value determines the size of the list returned by that field. It may specify a list of multiple slicing arguments.
+- The `sizedFields` argument can be used to define that the value of the `assumedSize` argument or of a slicing argument does not affect the size of a list returned by a field itself, but that of a list returned by one of its sub-fields.
+- The `requireOneSlicingArgument` argument can be used to inform the static analysis that it should expect that exactly one of the defined slicing arguments is present in a query. If that is not the case (i.e., if none or multiple slicing arguments are present), the static analysis will throw an error.
+
+The `@listSize` directive can only be applied to field definitions.
+
+# Defaults
+
+By default, Hot Chocolate will apply a cost weight of `10` to async resolvers, `1` to composite types, and `0` to scalar fields.
+
+Filtering and sorting field arguments and operations also have default cost weights, as shown in their respective [Options](#options) section below.
+
+Finally, resolvers using pagination will have list size settings applied automatically:
+
+
+
+ Connection
+ Offset
+
+
+
+
+ ```graphql
+ books(first: Int, after: String, last: Int, before: String): BooksConnection
+ @listSize(
+ assumedSize: 50
+ slicingArguments: ["first", "last"]
+ sizedFields: ["edges", "nodes"]
+ )
+ @cost(weight: "10")
+ ```
+
+
+
+
+ ```graphql
+ books(skip: Int, take: Int): BooksCollectionSegment
+ @listSize(
+ assumedSize: 50
+ slicingArguments: ["take"]
+ sizedFields: ["items"]
+ )
+ @cost(weight: "10")
+ ```
+
+
+
+
+# Applying a cost weight
+
+
+
+
+
+When using an annotation-based implementation, apply the `Cost` attribute to the query resolver.
+
+```csharp
+[QueryType]
+public static class Query
+{
+ [Cost(100)]
+ public static Book GetBook() => new("C# in depth.", new Author("Jon Skeet"));
+}
+```
+
+
+
+
+
+When using a code-first implementation, invoke the `Cost` method on the `IObjectFieldDescriptor`.
+
+```csharp
+public sealed class QueryType : ObjectType
+{
+ protected override void Configure(IObjectTypeDescriptor descriptor)
+ {
+ descriptor.Name(OperationTypeNames.Query);
+
+ descriptor
+ .Field("book")
+ .Resolve(_ => new Book("C# in depth.", new Author("Jon Skeet")))
+ .Cost(100);
+ }
+}
+```
+
+
+
+
+
+When using a schema-first implementation, apply the `@cost` directive to the field.
+
+```csharp
+builder.Services
+ .AddGraphQLServer()
+ .AddDocumentFromString(
+ """
+ type Query {
+ book: Book @cost(weight: "100")
+ }
+
+ type Book {
+ title: String!
+ author: Author!
+ }
+
+ type Author {
+ name: String!
+ }
+ """)
+ .BindRuntimeType();
+
+public sealed class Query
+{
+ public Book GetBook() => new("C# in depth.", new Author("Jon Skeet"));
+}
+```
+
+
+
+
+
+# Applying list size settings
+
+
+
+
+
+When using an annotation-based implementation, apply the `ListSize` attribute to a query resolver returning a list of items.
+
+```csharp
+[QueryType]
+public static class Query
+{
+ [ListSize(
+ AssumedSize = 100,
+ SlicingArguments = ["first", "last"],
+ SizedFields = ["edges", "nodes"],
+ RequireOneSlicingArgument = false)]
+ public static IEnumerable GetBooks()
+ => [new("C# in depth.", new Author("Jon Skeet"))];
+}
+```
+
+
+
+
+
+When using a code-first implementation, invoke the `ListSize` method on the `IObjectFieldDescriptor`.
+
+```csharp
+public sealed class QueryType : ObjectType
+{
+ protected override void Configure(IObjectTypeDescriptor descriptor)
+ {
+ descriptor.Name(OperationTypeNames.Query);
+
+ descriptor
+ .Field("books")
+ .Resolve>(
+ _ => [new Book("C# in depth.", new Author("Jon Skeet"))])
+ .ListSize(
+ assumedSize: 100,
+ slicingArguments: ["first", "last"],
+ sizedFields: ["edges", "nodes"],
+ requireOneSlicingArgument: false);
+ }
+}
+```
+
+
+
+
+
+When using a schema-first implementation, apply the `@listSize` directive to a field returning a list of items.
+
+```csharp
+builder.Services
+ .AddGraphQLServer()
+ .AddDocumentFromString(
+ """
+ type Query {
+ books: [Book!]!
+ @listSize(
+ assumedSize: 100,
+ slicingArguments: ["first", "last"],
+ sizedFields: ["edges", "nodes"],
+ requireOneSlicingArgument: false
+ )
+ }
+
+ type Book {
+ title: String!
+ author: Author!
+ }
+
+ type Author {
+ name: String!
+ }
+ """)
+ .BindRuntimeType();
+
+public sealed class Query
+{
+ public IEnumerable GetBooks()
+ => [new("C# in depth.", new Author("Jon Skeet"))];
+}
+```
+
+
+
+
+
+# Cost metrics
+
+Cost metrics include two properties, `FieldCost` and `TypeCost`:
+
+- `FieldCost` represents the execution impact on the server.
+- `TypeCost` represents the data impact on the server (instantiated objects).
+
+## Accessing cost metrics
+
+To access the cost metrics via the `IResolverContext` or `IMiddlewareContext`, use the context data key `WellKnownContextData.CostMetrics`:
+
+
+
+ Resolver
+ Middleware
+
+
+
+
+ ```csharp
+ public static Book GetBook(IResolverContext resolverContext)
+ {
+ const string key = WellKnownContextData.CostMetrics;
+ var costMetrics = (CostMetrics)resolverContext.ContextData[key]!;
+
+ double fieldCost = costMetrics.FieldCost;
+ double typeCost = costMetrics.TypeCost;
+
+ // ...
+ }
+ ```
+
+
+
+
+ ```csharp
+ public static class MyMiddlewareObjectFieldDescriptorExtension
+ {
+ public static IObjectFieldDescriptor UseMyMiddleware(
+ this IObjectFieldDescriptor descriptor)
+ {
+ return descriptor
+ .Use(next => async context =>
+ {
+ const string key = WellKnownContextData.CostMetrics;
+ var costMetrics = (CostMetrics)context.ContextData[key]!;
+
+ double fieldCost = costMetrics.FieldCost;
+ double typeCost = costMetrics.TypeCost;
+
+ await next(context);
+ });
+ }
+ }
+ ```
+
+
+
+
+## Reporting cost metrics
+
+To output the cost metrics, set an HTTP header named `GraphQL-Cost` with one of the following values:
+
+| Value | Description |
+|------------|------------------------------------------------------------------------|
+| `report` | The request is executed, and the costs are reported in the response. |
+| `validate` | The costs are reported in the response, without executing the request. |
+
+> Note: When using `validate`, Banana Cake Pop will currently _not_ display the response in the `Response` pane. Until this is fixed, you can inspect the response body in the request log.
+
+![Reporting costs in BCP](../../../../images/reporting-costs.webp)
+
+# Cost calculation examples
+
+## Field cost
+
+
+
+ Object
+ Connection
+
+
+
+
+ ```graphql
+ query {
+ book { # 10 (async resolver)
+ title # 0 (scalar)
+ author { # 1 (composite type)
+ name # 0 (scalar)
+ }
+ }
+ }
+
+ # Field cost: 11
+ ```
+
+
+
+
+ ```graphql
+ query {
+ books(first: 50) { # 10 (async resolver)
+ edges { # 1 (composite type)
+ node { # 50 (1 [composite type] x 50 items)
+ title # 0 (scalar)
+ author { # 50 (1 [composite type] x 50 items)
+ name # 0 (scalar)
+ }
+ }
+ }
+ }
+ }
+
+ # Field cost: 111
+ ```
+
+
+
+
+## Type cost
+
+
+
+ Object
+ Connection
+
+
+
+
+ ```graphql
+ query { # 1 Query
+ book { # 1 Book
+ title
+ author { # 1 Author
+ name
+ }
+ }
+ }
+
+ # Type cost: 3
+ ```
+
+
+
+
+ ```graphql
+ query { # 1 Query
+ books(first: 50) { # 50 BooksConnections
+ edges { # 1 BooksEdge
+ node { # 50 Books
+ title
+ author { # 50 Authors
+ name
+ }
+ }
+ }
+ }
+ }
+
+ # Type cost: 152
+ ```
+
+
+
+
+# Options
+
+## Cost options
+
+Options for cost analysis.
+
+| Option | Description | Default |
+|---------------------|---------------------------------------------------------------|---------|
+| MaxFieldCost | Gets or sets the maximum allowed field cost. | 1_000 |
+| MaxTypeCost | Gets or sets the maximum allowed type cost. | 1_000 |
+| EnforceCostLimits | Defines if the analyzer shall enforce cost limits. | true |
+| ApplyCostDefaults | Defines if cost defaults shall be applied to the schema. | true |
+| DefaultResolverCost | Gets or sets the default cost for an async resolver pipeline. | 10.0 |
+
+Modifying cost options:
+
+```csharp
+builder.Services
+ .AddGraphQLServer()
+ .ModifyCostOptions(options =>
+ {
+ options.MaxFieldCost = 1_000;
+ options.MaxTypeCost = 1_000;
+ options.EnforceCostLimits = true;
+ options.ApplyCostDefaults = true;
+ options.DefaultResolverCost = 10.0;
+ });
+```
+
+## Filtering cost options
+
+Represents the cost options for filtering.
+
+| Option | Description | Default |
+|-------------------------------------|----------------------------------------------------------------------------|---------|
+| DefaultFilterArgumentCost | Gets or sets the default cost for a filter argument. | 10.0 |
+| DefaultFilterOperationCost | Gets or sets the default cost for a filter operation. | 10.0 |
+| DefaultExpensiveFilterOperationCost | Gets or sets the default cost for an expensive filter argument. | 20.0 |
+| VariableMultiplier | Gets or sets a multiplier when a variable is used for the filter argument. | 5 |
+
+Modifying filtering cost options:
+
+```csharp
+builder.Services
+ .AddGraphQLServer()
+ .ModifyCostOptions(options =>
+ {
+ options.Filtering.DefaultFilterArgumentCost = 10.0;
+ options.Filtering.DefaultFilterOperationCost = 10.0;
+ options.Filtering.DefaultExpensiveFilterOperationCost = 20.0;
+ options.Filtering.VariableMultiplier = 5;
+ });
+```
+
+## Sorting cost options
+
+Represents the cost options for sorting.
+
+| Option | Description | Default |
+|--------------------------|------------------------------------------------------------------------|---------|
+| DefaultSortArgumentCost | Gets or sets the default cost for a sort argument. | 10.0 |
+| DefaultSortOperationCost | Gets or sets the default cost for a sort operation. | 10.0 |
+| VariableMultiplier | Gets or sets multiplier when a variable is used for the sort argument. | 5 |
+
+Modifying sorting cost options:
+
+```csharp
+builder.Services
+ .AddGraphQLServer()
+ .ModifyCostOptions(options =>
+ {
+ options.Sorting.DefaultSortArgumentCost = 10.0;
+ options.Sorting.DefaultSortOperationCost = 10.0;
+ options.Sorting.VariableMultiplier = 5;
+ });
+```
+
+# Disabling cost analysis
+
+While we generally don't recommended disabling cost analysis, you may wish to do so if you're using other methods to restrict operation complexity. If that's the case, simply set the `disableCostAnalyzer` option to `true`:
+
+```csharp
+builder.Services
+ .AddGraphQLServer(disableCostAnalyzer: true)
+```
diff --git a/website/src/docs/hotchocolate/v14/security/index.md b/website/src/docs/hotchocolate/v14/security/index.md
index 53b8be44468..f4d9397ef63 100644
--- a/website/src/docs/hotchocolate/v14/security/index.md
+++ b/website/src/docs/hotchocolate/v14/security/index.md
@@ -85,11 +85,11 @@ builder.Services.AddGraphQLServer()
.ModifyOptions(o => o.MaxAllowedNodeBatchSize = 1);
```
-# Operation complexity
+# Cost analysis
-With technologies like REST, it was easy to scale servers and measure the impact of a single request on our server infrastructure. With GraphQL, we need to do a bit more to enforce that requests have a consistent impact on our servers. Hot Chocolate can track the cost of fields and deny the execution of requests that exceed the allowed impact on our system.
+With technologies like [REST](https://en.wikipedia.org/wiki/REST), it was easy to scale servers and measure the impact of a single request on our server infrastructure. With GraphQL, we need to do a bit more to enforce that requests have a consistent impact on our servers. Hot Chocolate can track the cost of fields and deny the execution of requests that exceed the allowed impact on our system.
-[Learn more about the operation complexity analyzer](/docs/hotchocolate/v14/security/operation-complexity).
+[Learn more about cost analysis](/docs/hotchocolate/v14/security/cost-analysis)
# FIPS compliance
diff --git a/website/src/docs/hotchocolate/v14/security/operation-complexity.md b/website/src/docs/hotchocolate/v14/security/operation-complexity.md
deleted file mode 100644
index f8237d737ee..00000000000
--- a/website/src/docs/hotchocolate/v14/security/operation-complexity.md
+++ /dev/null
@@ -1,216 +0,0 @@
----
-title: Operation Complexity
----
-
-The operation complexity analyzer is a useful tool to make your API secure. The operation complexity analyzer assigns by default every field a complexity of `1`. The complexity of all fields in one of the operations of a GraphQL request is not allowed to be greater than the maximum permitted operation complexity.
-
-# Static Request Analysis
-
-This sounds fairly simple at first, but the more you think about this, the more you wonder if that is so. Does every field have the same complexity?
-
-In a data graph, not every field is the same. We have fields that fetch data that are more expensive than fields that just complete already resolved data.
-
-```graphql
-type Query {
- books(take: Int = 10): [Book]
-}
-
-type Book {
- title
- author: Author
-}
-
-type Author {
- name
-}
-```
-
-In the above example executing the `books` field on the `Query` type might go to the database and fetch the `Book`. This means that the cost of the `books` field is probably higher than the cost of the `title` field. The cost of the title field might be the impact on the memory and to the transport. For `title`, the default cost of `1` os OK. But for `books`, we might want to go with a higher cost of `10` since we are getting a list of books from our database.
-
-Moreover, we have the field `author` on the book, which might go to the database as well to fetch the `Author` object. Since we are only fetching a single item here, we might want to apply a cost of `5` to this field.
-
-```graphql
-type Query {
- books(take: Int = 10): [Book] @cost(complexity: 10)
-}
-
-type Book {
- title
- author: Author @cost(complexity: 5)
-}
-
-type Author {
- name
-}
-```
-
-If we run the following query against our data graph, we will come up with the cost of `11`.
-
-```graphql
-query {
- books {
- title
- }
-}
-```
-
-When drilling in further, a cost of `17` occurs.
-
-```graphql
-query {
- books {
- title
- author {
- name
- }
- }
-}
-```
-
-This kind of analysis is entirely static and could just be done by inspecting the query syntax tree. The impact on the overall execution performance is very low. But with this static approach, we do have a very rough idea of the performance. Is it correct to apply always a cost of `10` even though we might get one or one hundred books back?
-
-# Full Request Analysis
-
-The hot chocolate operation complexity analyzer can also take arguments into account when analyzing operation complexity.
-
-If we look at our data graph, we can see that the `books` field actually has an argument that defines how many books are returned. The `take` argument, in this case, specifies the maximum books that the field will return.
-
-When measuring the field\`s impact, we can take the argument `take` into account as a multiplier of our cost. This means we might want to lower the cost to `5` since now we get a more fine-grained cost calculation by multiplying the complexity of the field with the `take` argument.
-
-```graphql
-type Query {
- books(take: Int = 10): [Book] @cost(complexity: 5, multipliers:[take])
-}
-
-type Book {
- title
- author: Author @cost(complexity: 5)
-}
-
-type Author {
- name
-}
-```
-
-With the multiplier in place, we now get a cost of `60` for the request since the multiplier is applied to the books field and the child fields' cost.
-
-Cost calculation: `(5 * 10) + (1 * 10)`
-
-```graphql
-query {
- books {
- title
- }
-}
-```
-
-When drilling in further, the cost will go up to `110` since we are also now pulling in the author and by doing so causing a second database call.
-
-Cost calculation: `(5 * 10) + ((1 + 5) * 10)`
-
-```graphql
-query {
- books {
- title
- author {
- name
- }
- }
-}
-```
-
-```csharp
-services
- .AddGraphQL()
- .ModifyRequestOptions(o =>
- {
- o.Complexity.Enable = true;
- o.Complexity.MaximumAllowed = 1500;
- });
-```
-
-# Default Complexity Rules
-
-Hot Chocolate will automatically apply multipliers to fields that enable pagination. Moreover, explicit resolvers and resolvers compiled from async resolvers are by default weighted with `5` to mark them as having more impact than fields that do not fetch data.
-
-These defaults can be configured.
-
-```csharp
-services
- .AddGraphQL()
- .ModifyRequestOptions(o =>
- {
- o.Complexity.ApplyDefaults = true;
- o.Complexity.DefaultComplexity = 1;
- o.Complexity.DefaultResolverComplexity = 5;
- });
-```
-
-# Advanced
-
-Often we not only want to make sure that a consumer of our API does not do too complex queries, but we also want to make sure that the consumer does not issue too many complex queries in a given time window. For this reason, the complexity analysis will store the query complexity on the request context data.
-
-The context data key can be configured like the following:
-
-```csharp
-services
- .AddGraphQL()
- .ModifyRequestOptions(o =>
- {
- o.Complexity.ContextDataKey = "MyContextDataKey";
- });
-```
-
-With this, it is possible to add a request middleware and aggregate the complexity over time on something like _Redis_ and fail a request if the allowed complexity was used up.
-
-## Custom Complexity Calculation
-
-The default complexity calculation is fairly basic and can be customized to fit your needs.
-
-```csharp
-services
- .AddGraphQL()
- .ModifyRequestOptions(o =>
- {
- o.Complexity.Calculation = context =>
- {
- if (context.Multipliers.Count == 0)
- {
- return context.Complexity + context.ChildComplexity;
- }
-
- var cost = context.Complexity + context.ChildComplexity;
- bool needsDefaultMultiplier = true;
-
- foreach (MultiplierPathString multiplier in context.Multipliers)
- {
- if (context.TryGetArgumentValue(multiplier, out int value))
- {
- cost *= value;
- needsDefaultMultiplier = false;
- }
- }
-
- if(needsDefaultMultiplier && context.DefaultMultiplier.HasValue)
- {
- cost *= context.DefaultMultiplier.Value;
- }
-
- return cost;
- });
- });
-```
-
-**Complexity Context**
-
-| Member | Description |
-| ------------------- | --------------------------------------------------------------------- |
-| Field | The `IOutputField` for which the complexity is calculated. |
-| Selection | The field selection node in the query syntax tree. |
-| Complexity | The field`s base complexity. |
-| ChildComplexity | The calculated complexity of all child fields. |
-| Multipliers | The multiplier argument names. |
-| Multipliers | The default multiplier value when no multiplier argument has a value. |
-| FieldDepth | The field depth in the query. |
-| NodeDepth | The syntax node depth in the query syntax tree. |
-| TryGetArgumentValue | Helper to get the coerced argument value of a multiplier. |
diff --git a/website/src/images/reporting-costs.webp b/website/src/images/reporting-costs.webp
new file mode 100644
index 00000000000..35669f26701
Binary files /dev/null and b/website/src/images/reporting-costs.webp differ