Skip to content
Merged
Show file tree
Hide file tree
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 @@ -305,6 +305,71 @@ builder

Register intermediate interfaces (like `DatedMessage`) explicitly if they are not returned directly from a resolver field.

# Default Resolvers

Interface fields can define default resolvers, similar to default interface methods in C#. When an object type implements the interface, it automatically inherits the resolver for any field where it does not define its own. If the object type does not even declare the field, the field is created automatically with the interface's resolver.

This is useful when multiple types share the same resolution logic for a field and you want to define it once on the interface rather than repeating it in every implementing type.

<ExampleTabs>
<Implementation>

```csharp
[InterfaceType("Message")]
public interface IMessage
{
User Author { get; }
DateTime CreatedAt { get; }
}

[InterfaceType<IMessage>]
public static partial class MessageNode
{
public static string DisplayName([Parent] IMessage message)
=> $"{message.Author.Name} - {message.CreatedAt:d}";
}
```

All object types implementing `IMessage` inherit the `displayName` field and its resolver. No additional configuration is needed on the implementing types.

</Implementation>
<Code>

```csharp
public class MessageType : InterfaceType
{
protected override void Configure(IInterfaceTypeDescriptor descriptor)
{
descriptor.Name("Message");

descriptor
.Field("displayName")
.Type<StringType>()
.Resolve(context =>
{
var message = context.Parent<IMessage>();
return $"{message.Author.Name} - {message.CreatedAt:d}";
});
}
}

public class TextMessageType : ObjectType<TextMessage>
{
protected override void Configure(
IObjectTypeDescriptor<TextMessage> descriptor)
{
descriptor.Implements<MessageType>();
}
}
```

`TextMessageType` inherits the `displayName` field and its resolver from `MessageType`. No additional configuration is needed.

</Code>
</ExampleTabs>

Object types can override an inherited resolver by defining their own resolver for the same field.

# Next Steps

- **Need types without shared fields?** See [Unions](./unions.md).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -213,9 +213,7 @@ builder
.AddHttpRequestInterceptor<NoCacheHeaderInterceptor>();
```

:::note
You can only register a single HttpRequestInterceptor per schema.
:::
> **Note:** You can only register a single HttpRequestInterceptor per schema.

# Putting It Together

Expand Down
4 changes: 1 addition & 3 deletions website-next/content/docs/mocha/exception-policies.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,7 @@ Exception matching respects inheritance. A policy on `NpgsqlException` also matc

`AddResilience` is the single entry point for all exception handling configuration. There is no separate `AddRetry` or `AddRedelivery` call - retry and redelivery settings are configured per-exception within the policy.

:::note Replacement semantics
Calling `On<T>()` for the same exception type replaces the previous rule for that type - last write wins. If you call `On<HttpRequestException>()` twice without a predicate, the second call overwrites the first. The same applies to `Default()`: calling it again replaces the previous default rule. For example, the parameterless `AddResilience()` registers `Default().Retry().ThenRedeliver()`. If you later call `AddResilience(p => p.Default().Retry(5))`, the new default replaces the one registered by the parameterless overload.
:::
> **Note:** Calling `On<T>()` for the same exception type replaces the previous rule for that type - last write wins. If you call `On<HttpRequestException>()` twice without a predicate, the second call overwrites the first. The same applies to `Default()`: calling it again replaces the previous default rule. For example, the parameterless `AddResilience()` registers `Default().Retry().ThenRedeliver()`. If you later call `AddResilience(p => p.Default().Retry(5))`, the new default replaces the one registered by the parameterless overload.

## Parameterless defaults

Expand Down
8 changes: 2 additions & 6 deletions website-next/content/docs/mocha/reliability.md
Original file line number Diff line number Diff line change
Expand Up @@ -480,9 +480,7 @@ builder.Services

With this configuration: `(5 + 1) x (3 + 1) = 24` total handler invocations before dead-letter.

:::note Request/reply messages skip redelivery
Redelivery does not apply to request/reply messages. The caller is waiting synchronously for a response - scheduling the message for delivery minutes later would cause a timeout. When a request/reply handler fails after retry exhaustion, the exception propagates directly to the fault middleware, which sends a `NotAcknowledgedEvent` back to the caller. Immediate retry still applies to request/reply handlers.
:::
> **Note:** Request/reply messages skip redelivery. Redelivery does not apply to request/reply messages. The caller is waiting synchronously for a response - scheduling the message for delivery minutes later would cause a timeout. When a request/reply handler fails after retry exhaustion, the exception propagates directly to the fault middleware, which sends a `NotAcknowledgedEvent` back to the caller. Immediate retry still applies to request/reply handlers.

# Inspect retry state in handlers

Expand Down Expand Up @@ -756,9 +754,7 @@ public class OrderPlacedHandler(AppDbContext db, IMessageBus bus)

After the transaction commits, the outbox processor detects the new message (via EF Core interceptors that signal on save and transaction commit) and dispatches it to the transport.

:::note Idempotency requirement
The outbox guarantees at-least-once delivery. Your handlers may be invoked more than once for the same message if the outbox dispatches successfully but the transport acknowledgment is lost before the message is deleted from the outbox table. You can handle this in two ways: design handlers to be idempotent manually (see the [Idempotent Consumer](https://microservices.io/patterns/communication-style/idempotent-consumer.html) pattern), or enable the [inbox](#deduplicate-messages-with-the-transactional-inbox) on the receiving side to let Mocha deduplicate automatically.
:::
> **Note:** The outbox guarantees at-least-once delivery. Your handlers may be invoked more than once for the same message if the outbox dispatches successfully but the transport acknowledgment is lost before the message is deleted from the outbox table. You can handle this in two ways: design handlers to be idempotent manually (see the [Idempotent Consumer](https://microservices.io/patterns/communication-style/idempotent-consumer.html) pattern), or enable the [inbox](#deduplicate-messages-with-the-transactional-inbox) on the receiving side to let Mocha deduplicate automatically.

## Use execution strategy resilience

Expand Down
4 changes: 1 addition & 3 deletions website-next/content/docs/mocha/transports/in-memory.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,7 @@ This model is why swapping `.AddInMemory()` for `.AddRabbitMQ()` requires no app

By default, the InMemory transport processes messages sequentially in the order they are published. Each message is delivered to its handler before the next one is dispatched.

:::note
InMemory tests exercise handler logic and message routing, but not RabbitMQ-specific behavior such as topology conflicts, acknowledgement semantics, connection recovery, or quorum queue characteristics. For testing broker-specific behavior, use a real broker in a test container.
:::
> **Note:** InMemory tests exercise handler logic and message routing, but not RabbitMQ-specific behavior such as topology conflicts, acknowledgement semantics, connection recovery, or quorum queue characteristics. For testing broker-specific behavior, use a real broker in a test container.

The following shows the default topology Mocha creates when you register an event handler with the InMemory transport:

Expand Down
4 changes: 1 addition & 3 deletions website-next/content/docs/mocha/transports/postgres.md
Original file line number Diff line number Diff line change
Expand Up @@ -249,9 +249,7 @@ The transport runs migrations automatically on first use. Migrations are protect

Each migration is tracked in the migrations table and is idempotent - running the same migration twice has no effect. The migration creates the schema if it does not exist, then applies each pending migration in order within a single transaction.

:::warning
**Advisory lock scope.** The advisory lock ID is fixed. If you run multiple independent Mocha transports in the same PostgreSQL cluster with different table prefixes, they share the same advisory lock. This is safe - it serializes migrations but does not block normal message operations.
:::
> **Warning:** The advisory lock ID is fixed. If you run multiple independent Mocha transports in the same PostgreSQL cluster with different table prefixes, they share the same advisory lock. This is safe - it serializes migrations but does not block normal message operations.

# Declare custom topology

Expand Down
8 changes: 3 additions & 5 deletions website-next/content/docs/mocha/transports/rabbitmq.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,11 +151,9 @@ graph LR

**Request/reply:** The transport creates a temporary reply queue per service instance. The reply address is embedded in the request message so the responder knows where to send the reply.

:::warning
**Message loss warning.** Messages published before the transport completes its Start phase may be lost if no queue is bound to the exchange yet. During deployment, ensure consuming services start before publishing services, or use [publisher confirms](https://www.rabbitmq.com/docs/reliability#publisher-confirms) to detect lost messages.

If a message is published to an exchange with no bound queue - for example, when no consumer has started - that message is dropped. Mocha auto-provisions topology, but the window between exchange creation and queue binding is a real operational risk.
:::
> **Warning:** Messages published before the transport completes its Start phase may be lost if no queue is bound to the exchange yet. During deployment, ensure consuming services start before publishing services, or use [publisher confirms](https://www.rabbitmq.com/docs/reliability#publisher-confirms) to detect lost messages.
>
> If a message is published to an exchange with no bound queue - for example, when no consumer has started - that message is dropped. Mocha auto-provisions topology, but the window between exchange creation and queue binding is a real operational risk.

## Publisher confirms

Expand Down
110 changes: 46 additions & 64 deletions website/src/docs/docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -267,12 +267,11 @@
"title": "Fusion",
"description": "Federated GraphQL Gateway",
"metaDescription": "Fusion is a powerful, open-source GraphQL gateway that helps developers to build a unified API for their services.",
"latestStableVersion": "v15",
"latestStableVersion": "v16",
"versions": [
{
"path": "v16",
"title": "v16 (preview)",
"preview": true,
"title": "v16",
"items": [
{
"path": "index",
Expand Down Expand Up @@ -469,12 +468,11 @@
"title": "Hot Chocolate",
"description": "GraphQL Server / Gateway",
"metaDescription": "Hot Chocolate is the most efficient, feature-rich, open-source GraphQL server in the .NET ecosystem, that helps developers to build powerful APIs.",
"latestStableVersion": "v15",
"latestStableVersion": "v16",
"versions": [
{
"path": "v16",
"title": "v16 (preview)",
"preview": true,
"title": "v16",
"items": [
{
"path": "index",
Expand All @@ -485,7 +483,7 @@
"title": "Getting Started"
},
{
"path": "building-a-schema",
"path": "defining-a-schema",
"title": "Building a Schema",
"items": [
{
Expand Down Expand Up @@ -540,10 +538,6 @@
"path": "unions",
"title": "Unions"
},
{
"path": "extending-types",
"title": "Extending Types"
},
{
"path": "directives",
"title": "Directives"
Expand All @@ -567,7 +561,7 @@
]
},
{
"path": "resolvers-and-data",
"path": "fetching-data",
"title": "Resolvers and Data",
"items": [
{
Expand Down Expand Up @@ -609,6 +603,34 @@
{
"path": "fetching-from-rest",
"title": "Fetching from REST"
},
{
"path": "entity-framework",
"title": "Entity Framework"
},
{
"path": "mongodb",
"title": "MongoDB"
},
{
"path": "spatial-data",
"title": "Spatial Data"
},
{
"path": "marten",
"title": "Marten"
},
{
"path": "errors",
"title": "Error Codes"
},
{
"path": "extending-filtering",
"title": "Extending Filtering"
},
{
"path": "executable",
"title": "Executable"
}
]
},
Expand Down Expand Up @@ -651,6 +673,10 @@
{
"path": "openapi-adapter",
"title": "OpenAPI Adapter"
},
{
"path": "federation",
"title": "Federation"
}
]
},
Expand All @@ -668,32 +694,6 @@
}
]
},
{
"path": "integrations",
"title": "Integrations",
"items": [
{
"path": "index",
"title": "Overview"
},
{
"path": "entity-framework",
"title": "Entity Framework"
},
{
"path": "mongodb",
"title": "MongoDB"
},
{
"path": "spatial-data",
"title": "Spatial Data"
},
{
"path": "marten",
"title": "Marten"
}
]
},
{
"path": "server",
"title": "Server",
Expand Down Expand Up @@ -741,6 +741,10 @@
{
"path": "command-line",
"title": "CLI Commands"
},
{
"path": "options",
"title": "Configuration Options"
}
]
},
Expand All @@ -763,7 +767,7 @@
]
},
{
"path": "securing-your-api",
"path": "security",
"title": "Securing Your API",
"items": [
{
Expand Down Expand Up @@ -800,33 +804,13 @@
"path": "custom-attributes",
"title": "Attributes"
},
{
"path": "options",
"title": "Configuration Options"
},
{
"path": "errors",
"title": "Error Codes"
},
{
"path": "extending-filtering",
"title": "Extending Filtering"
},
{
"path": "apollo-federation",
"title": "Apollo Federation"
},
{
"path": "language",
"title": "Language"
},
{
"path": "visitors",
"title": "Visitors"
},
{
"path": "executable",
"title": "Executable"
}
]
},
Expand Down Expand Up @@ -2685,12 +2669,11 @@
"title": "Strawberry Shake",
"description": "GraphQL Client for .NET",
"metaDescription": "Strawberry Shake is an incredible GraphQL client for the .NET ecosystem, that helps developers to build awesome UIs in Blazor, Maui, and more.",
"latestStableVersion": "v15",
"latestStableVersion": "v16",
"versions": [
{
"path": "v16",
"title": "v16 (preview)",
"preview": true,
"title": "v16",
"items": [
{
"path": "index",
Expand Down Expand Up @@ -3205,8 +3188,7 @@
"versions": [
{
"path": "v16",
"title": "v16 (preview)",
"preview": true,
"title": "v16",
"items": [
{
"path": "index",
Expand Down
Loading
Loading