Interceptors - .NET SDK
Interceptors are SDK hooks that let you intercept inbound and outbound Temporal calls. You use them to apply shared behavior across many calls, such as tracing and authorization, before calls reach the application code and after they return. This is similar to middleware in other frameworks.
There are two main types of interceptors: inbound and outbound.
- Outbound interceptors wrap network calls, running before they reach the network and after they return.
- Inbound interceptors run after the network hop, wrapping application code and running before it starts and after it returns.
Those further break down into different interceptor types.
Workflow inbound and outbound interceptor methods also execute during replay. Use replay-safe APIs for logging, randomness, and time in these interceptors. See Develop Workflow logic for details.
Activity and Client interceptors are not affected by replay.
Register an Interceptor
Registering an interceptor means supplying an interceptor instance to the SDK so Temporal can invoke it when matching Client or Worker calls occur. Once registered, the interceptor runs as part of the call path and can observe or modify request and response data.
Register on the Client
Pass interceptors in the Interceptors argument of TemporalClient.ConnectAsync. Client interceptors modify outbound calls such
as starting and signaling Workflows.
var interceptor = new MyCounterInterceptor();
var client = await TemporalClient.ConnectAsync(new()
{
TargetHost = "localhost:7233",
Interceptors = [interceptor],
});
The Interceptors list can contain multiple interceptors.
The default behavior for interceptors is to form a chain. A method implemented on an interceptor instance in the list can perform side effects, and modify the data, before passing it on to the corresponding method on the next interceptor in the list.
Register via a Plugin
If you're building a reusable library or want to bundle interceptors with other primitives, you can register them through a Plugin.
Register on the Worker only
If your interceptor doesn't affect the Client, you can pass interceptors in the Interceptors argument of TemporalWorker().
Worker interceptors modify inbound and outbound Workflow and Activity calls.
using var worker = new TemporalWorker(
client,
new TemporalWorkerOptions("my-task-queue")
{
Interceptors = new IWorkerInterceptor[]
{
interceptor
}
}
.AddActivity(activities.SayHello)
.AddWorkflow<SayHelloWorkflow>()
);
How to implement Interceptors
Interceptors run as a chain. Each interceptor wraps the entire inner call: your code runs before the call, invokes next to execute the rest of the chain, and then runs after the call completes. This means you can inspect or modify both the input and the result, handle errors, and perform side effects at either stage.
Implementing Client call Interceptors
To modify outbound Client calls, define a class implementing IClientInterceptor. Implement InterceptClient() to return a ClientOutboundInterceptor, overriding the outbound Client calls you want to modify. IClientInterceptor.InterceptClient receives the next ClientOutboundInterceptor in the chain and returns the created interceptor.
This example implements an Interceptor on outbound Client calls that sets a certain key in the outbound headers field.
A User ID is context-propagated by being sent in a header field with outbound requests:
using Google.Protobuf;
using Temporalio.Api.Common.V1;
using Temporalio.Client;
using Temporalio.Client.Interceptors;
public class ContextPropagationInterceptor : IClientInterceptor
{
public ClientOutboundInterceptor InterceptClient(
ClientOutboundInterceptor nextInterceptor) =>
new ContextPropagationClientOutboundInterceptor(nextInterceptor);
}
public class ContextPropagationClientOutboundInterceptor(
ClientOutboundInterceptor next)
: ClientOutboundInterceptor(next)
{
public override Task<WorkflowHandle<TWorkflow, TResult>>
StartWorkflowAsync<TWorkflow, TResult>(StartWorkflowInput input)
{
input.Headers["user-id"] = new Payload
{
Metadata = { ["encoding"] = ByteString.CopyFromUtf8("plain/text") },
Data = ByteString.CopyFromUtf8(UserContext.UserId),
};
return base.StartWorkflowAsync<TWorkflow, TResult>(input);
}
}
You can then register this interceptor in your client/starter code.
Your interceptor classes don't need to implement every method. The default implementation is always to pass the data on to the next method in the interceptor chain. During execution, when the SDK encounters an Inbound Activity call, it will look to the first Interceptor instance, get hold of the appropriate intercepted method, and call it. The intercepted method will perform its function then call the same method on the next Interceptor in the chain. At the end of the chain the SDK will call the "real" SDK method.
Implementing Worker call Interceptors
To modify inbound Workflow and Activity calls, define a class implementing IWorkerInterceptor. It provides InterceptActivity(), InterceptWorkflow(), and InterceptNexusOperation() methods for Activity, Workflow, and Nexus interception.
This example demonstrates using an interceptor to measure Schedule-To-Start and Schedule-To-Close latency.
Notice how the interceptor wraps the call. It records Schedule-To-Start before ExecuteActivityAsync, then records Schedule-To-Close after it completes:
using Temporalio.Activities;
using Temporalio.Worker;
using Temporalio.Worker.Interceptors;
public class SimpleWorkerInterceptor : IWorkerInterceptor
{
public ActivityInboundInterceptor InterceptActivity(
ActivityInboundInterceptor nextInterceptor) =>
new ActivityMetricsInterceptor(nextInterceptor);
public WorkflowInboundInterceptor InterceptWorkflow(
WorkflowInboundInterceptor nextInterceptor) =>
nextInterceptor;
}
public class ActivityMetricsInterceptor(ActivityInboundInterceptor next)
: ActivityInboundInterceptor(next)
{
public override async Task<object?> ExecuteActivityAsync(
ExecuteActivityInput input)
{
var info = ActivityExecutionContext.Current.Info;
var started = DateTimeOffset.UtcNow;
// Before the activity executes
var scheduleToStart =
started - info.CurrentAttemptScheduledTime;
Console.WriteLine(
$"Schedule-To-Start latency: {scheduleToStart}");
// Execute the activity
var result = await base.ExecuteActivityAsync(input);
// After the activity completes
var scheduleToClose =
DateTimeOffset.UtcNow - info.CurrentAttemptScheduledTime;
Console.WriteLine(
$"Schedule-To-Close latency: {scheduleToClose}");
return result;
}
}
Register it on the Worker:
using var worker = new TemporalWorker(
client,
new TemporalWorkerOptions("my-task-queue")
{
Interceptors = new IWorkerInterceptor[]
{
new SimpleWorkerInterceptor(),
},
}
.AddActivity(activities.SayHello)
.AddWorkflow<SayHelloWorkflow>());
await worker.ExecuteAsync();