Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@ Things that have been removed or had a change in behavior that may cause your co
- Since the `RegisterService` method is no longer required, it has been removed, along with the `ServiceKind` enum.
- Scoped services injected into query resolvers are now resolver-scoped by default (not request scoped). For mutation resolvers, services are request-scoped by default.
- The default scope can be changed in two ways:

1. Globally, using `ModifyOptions`:

```csharp
Expand Down Expand Up @@ -124,6 +123,22 @@ builder.Services
.AddGlobalObjectIdentification();
```

## IIdSerializer replaced by INodeIdSerializer

Previously, you could grab the `IIdSerializer` from your dependency injection container to manually parse and serialize globally unique identifiers (GID).
As part of the changes to the GID format mentioned above, the `IIdSerializer` interface has been renamed to `INodeIdSerializer`.

The methods used for parsing and serialization have also been renamed:

| Before | After |
| ---------------------------------- | ---------------------------------------------------------------------------------------- |
| `.Deserialize("<gid-value>")` | `.Parse("<gid-value>", typeof(string))` where `string` is the underlying type of the GID |
| `.Serialize("MyType", "<raw-id>")` | `.Format("MyType", "<raw-id>")` |

The `Parse()` (previously `Deserialize()`) method has also changed its return type from `IdValue` to `NodeId`. The parsed Id value can now be accessed through the `NodeId.InternalId` instead of the `IdValue.Value` property.
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This sentence uses "Id" in "parsed Id value"; elsewhere in the doc the acronym is written as "ID" (e.g., "node(id: ID!)"). Consider changing this to "parsed ID value" for consistency.

Suggested change
The `Parse()` (previously `Deserialize()`) method has also changed its return type from `IdValue` to `NodeId`. The parsed Id value can now be accessed through the `NodeId.InternalId` instead of the `IdValue.Value` property.
The `Parse()` (previously `Deserialize()`) method has also changed its return type from `IdValue` to `NodeId`. The parsed ID value can now be accessed through the `NodeId.InternalId` instead of the `IdValue.Value` property.

Copilot uses AI. Check for mistakes.

The ability to encode the schema name in the GID via `.Serialize("SchemaName", "MyType", "<raw-id>")` has been dropped and is no longer supported.

## Node Resolver validation

We now enforce that each object type implementing the `Node` interface also defines a resolver, so that the object can be refetched through the `node(id: ID!)` field.
Expand All @@ -136,6 +151,11 @@ builder.Services
.ModifyOptions(o => o.EnsureAllNodesCanBeResolved = false)
```

## DataLoader.LoadAsync always returns nullable type

Previously, the `LoadAsync` method on a DataLoader was typed as non-nullable, even though `null` could be returned.
This release changes the return type of `LoadAsync` to always be nullable.

## Builder APIs

We have aligned all builder APIs to be more consistent and easier to use. Builders can now be created by using the static method `Builder.New()` and the `Build()` method to create the final object.
Expand All @@ -144,9 +164,14 @@ We have aligned all builder APIs to be more consistent and easier to use. Builde

The interface `IQueryRequestBuilder` and its implementations were replaced with `OperationRequestBuilder` which now supports building standard GraphQL operation requests as well as variable batch requests.

The `Build()` method returns now a `IOperationRequest` which is implemented by `OperationRequest` and `VariableBatchRequest`.
The `Build()` method now returns a `IOperationRequest` which is implemented by `OperationRequest` and `VariableBatchRequest`.
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this sentence the article should be "an" (vowel sound) rather than "a": "an IOperationRequest".

Suggested change
The `Build()` method now returns a `IOperationRequest` which is implemented by `OperationRequest` and `VariableBatchRequest`.
The `Build()` method now returns an `IOperationRequest` which is implemented by `OperationRequest` and `VariableBatchRequest`.

Copilot uses AI. Check for mistakes.

We have also simplified what the builder does and removed a lot of the convenience methods that allowed to add single variables to it. This has todo with the support of variable batching. Now, you have to provide the variable map directly.
We've also renamed and consolidated some methods on the `OperationRequestBuilder`:

| Before | After |
| ----------------------------------- | --------------------------------------------------------------------------- |
| `SetQuery("{ __typename }")` | `SetDocument("{ __typename }")` |
| `AddVariableValue("name", "value")` | `AddVariableValues(new Dictionary<string, object?> { ["name"] = "value" })` |
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The migration table lists AddVariableValues(...) as the replacement for AddVariableValue(...), but OperationRequestBuilder exposes SetVariableValues(...) overloads (including one that takes IReadOnlyDictionary<string, object?>). If AddVariableValues is not a real API, please update the table to the correct method name to avoid sending readers to a non-existent API.

Suggested change
| `AddVariableValue("name", "value")` | `AddVariableValues(new Dictionary<string, object?> { ["name"] = "value" })` |
| `AddVariableValue("name", "value")` | `SetVariableValues(new Dictionary<string, object?> { ["name"] = "value" })` |

Copilot uses AI. Check for mistakes.

### IQueryResultBuilder replaced by OperationResultBuilder

Expand All @@ -156,6 +181,11 @@ The interface `IQueryResultBuilder` and its implementations were replaced with `

The interface `IQueryResult` was replaced with `IOperationResult`.

### IExecutionResult.ExpectQueryResult replaced by .ExpectOperationResult
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The heading "IExecutionResult.ExpectQueryResult replaced by .ExpectOperationResult" is a bit inconsistent/ambiguous (leading dot without a receiver/type). Consider renaming it to either "IExecutionResult.ExpectQueryResult replaced by IExecutionResult.ExpectOperationResult" or just "ExpectQueryResult replaced by ExpectOperationResult" for clarity.

Suggested change
### IExecutionResult.ExpectQueryResult replaced by .ExpectOperationResult
### IExecutionResult.ExpectQueryResult replaced by IExecutionResult.ExpectOperationResult

Copilot uses AI. Check for mistakes.

In your unit tests you might have been using `result.ExpectQueryResult()` to assert that a result is not a streamed response and rather a completed result.
This assertion method has been renamed to `ExpectOperationResult()`.

## 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).
Expand Down Expand Up @@ -260,6 +290,38 @@ ModifyRequestOptions(o => o.OnlyAllowPersistedOperations = true);
ModifyRequestOptions(o => o.PersistedOperations.OnlyAllowPersistedDocuments = true);
```

## Connection getTotalCount constructor argument replaced with totalCount

Previously, you could supply an async method to the `getTotalCount` constructor argument when instantiating a `Connection<T>`. This method would only be evaluated to calculate the total count, if the `totalCount` field was selected on that Connection in a query.

```csharp
return new Connection<MyType>(
edges: [/* ... */],
info: new ConnectionPageInfo(/* ... */),
getTotalCount: async cancellationToken => 123)
```

In this release the constructor argument was renamed to `totalCount` and now only accepts an `int` for the total count, no longer a method to compute the total count.
If you want to re-create the old behavior, you can use the new `[IsSelected]` attribute to conditionally compute the total count.

```csharp
public Connection<MyType> GetMyTypes(
[IsSelected("totalCount")] bool hasSelectedTotalCount,
CancellationToken cancellationToken)
{
var totalCount = 0;
if (hasSelectedTotalCount)
{
totalCount = /* ... */;
}

return new Connection<MyType>(
edges: [/* ... */],
info: new ConnectionPageInfo(/* ... */),
totalCount: totalCount)
}
```

# Other changes

## Change to `SingleOrDefaultMiddleware`
Expand Down
Loading