Skip to content
Draft
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
125 changes: 125 additions & 0 deletions docs/orleans/host/configuration-guide/custom-activators.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
---
title: Custom activators with IActivator<T>
description: Learn how to control object construction during deserialization in .NET Orleans by implementing IActivator<T>.
ms.date: 02/24/2026
ai-usage: ai-assisted
---

# Custom activators with IActivator\<T>

During deserialization, Orleans needs to create instances of your types before populating their fields. By default, Orleans uses a parameterless constructor (or <xref:System.Runtime.CompilerServices.RuntimeHelpers.GetUninitializedObject%2A> if no parameterless constructor exists). The <xref:Orleans.Serialization.Activators.IActivator%601> interface lets you take control of this process.

Custom activators are useful when:

- Your type requires constructor arguments (such as dependency injection services).
- You want to return instances from an object pool instead of allocating new objects.
- You need to perform initialization logic before the serializer populates the object's fields.
- Your type doesn't have a parameterless constructor.

## The IActivator\<T> interface

The `IActivator<T>` interface is defined in the `Orleans.Serialization.Activators` namespace and contains a single method:

```csharp
namespace Orleans.Serialization.Activators;

public interface IActivator<T>
{
T Create();
}
```

The `Create` method is called by the serializer whenever it needs a new instance of `T` during deserialization. The serializer then populates the object's serialized fields after creation.

## Implement a custom activator

To implement a custom activator:

1. Create a class that implements `IActivator<T>` for your target type.
2. Decorate your activator class with the `[RegisterActivator]` attribute so Orleans discovers it automatically.
3. Decorate the target type with the `[UseActivator]` attribute to tell the code generator to use a registered activator instead of calling the constructor directly.

### Basic example

The following example demonstrates a simple custom activator. First, define the serializable type with the `[UseActivator]` attribute:

:::code language="csharp" source="snippets/custom-activators/BasicActivator.cs" id="BasicType":::

Then, implement the activator with the `[RegisterActivator]` attribute:

:::code language="csharp" source="snippets/custom-activators/BasicActivator.cs" id="BasicActivator":::

### Object pooling example

A common use case for custom activators is returning objects from a pool to reduce garbage collection pressure. Define the pooled type:

:::code language="csharp" source="snippets/custom-activators/PooledActivator.cs" id="PooledType":::

Then implement the activator that draws from an <xref:Microsoft.Extensions.ObjectPool.ObjectPool%601>:

:::code language="csharp" source="snippets/custom-activators/PooledActivator.cs" id="PooledActivator":::

### Dependency injection example

Custom activators participate in dependency injection, so you can inject services into the activator's constructor. This is useful when your serializable type needs access to services that can't be serialized. Define a type that requires an injected service:

:::code language="csharp" source="snippets/custom-activators/DiActivator.cs" id="DiType":::

Then implement the activator that resolves the service and passes it to the constructor:

:::code language="csharp" source="snippets/custom-activators/DiActivator.cs" id="DiActivator":::

## Generic activators

You can implement activators for generic types. The Orleans code generator recognizes open generic activator registrations:

:::code language="csharp" source="snippets/custom-activators/GenericActivator.cs" id="GenericType":::

:::code language="csharp" source="snippets/custom-activators/GenericActivator.cs" id="GenericActivator":::

## Important attributes

| Attribute | Applied to | Purpose |
|---|---|---|
| `[UseActivator]` | Serializable type (class or struct) | Tells the code generator that instances of this type should be created using a registered `IActivator<T>` instead of calling the constructor directly. |
| `[RegisterActivator]` | Activator implementation (class or struct) | Registers the activator with Orleans so it's automatically discovered and used during deserialization. |

Both attributes are defined in the `Orleans.Serialization` namespace.

## Simple dependency injection with `[GeneratedActivatorConstructor]`

If your type only needs injected dependencies during deserialization and you don't need custom creation logic such as object pooling, you can use the <xref:Orleans.GeneratedActivatorConstructorAttribute> attribute instead of implementing `IActivator<T>`. This attribute marks a specific constructor for the Orleans code generator to use when activating instances. Constructor parameters are resolved from the <xref:System.IServiceProvider>.

:::code language="csharp" source="snippets/custom-activators/ActivatorConstructor.cs" id="ActivatorConstructorType":::

In the preceding example:

- The `[GeneratedActivatorConstructor]` attribute tells the code generator to use that constructor when creating instances during deserialization.
- The `ILogger<NotificationMessage>` parameter is resolved from dependency injection automatically.
- No separate activator class, `[UseActivator]`, or `[RegisterActivator]` attribute is needed.

### When to use each approach

| Approach | Use when |
|---|---|
| `[GeneratedActivatorConstructor]` | You need constructor-injected services and standard `new` construction is sufficient. |
| `IActivator<T>` with `[UseActivator]` | You need custom creation logic (object pooling, complex initialization, factory patterns). |

> [!NOTE]
> `[GeneratedActivatorConstructor]` and `[UseActivator]` serve different purposes. The constructor attribute tells the code generator which constructor to call, while `[UseActivator]` delegates creation entirely to an `IActivator<T>` implementation. Don't combine them on the same type.

## How it works

When Orleans deserializes a type marked with `[UseActivator]`:

1. The generated serializer requests an `IActivator<T>` from the <xref:Orleans.Serialization.Serializers.IActivatorProvider>.
2. The `IActivatorProvider` looks up the registered activator for the type. Types decorated with `[RegisterActivator]` are automatically registered during startup.
3. The activator's `Create()` method is called to produce a new instance.
4. The serializer populates the instance's serialized fields from the data stream.

If no custom activator is registered and the type doesn't have `[UseActivator]`, Orleans falls back to using the default activator, which calls the parameterless constructor or <xref:System.Runtime.CompilerServices.RuntimeHelpers.GetUninitializedObject%2A>.

## See also

- [Serialization and custom serializers](serialization.md)
- [Serialization lifecycle hooks](serialization-lifecycle-hooks.md)
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
---
title: Serialization lifecycle hooks
description: Learn how to use serialization lifecycle hooks in .NET Orleans to run custom logic during serialization, deserialization, and copying.
ms.date: 02/24/2026
ai-usage: ai-assisted
---

# Serialization lifecycle hooks

Orleans lets you run custom logic at specific points during serialization, deserialization, and copying. The <xref:Orleans.SerializationCallbacksAttribute> attribute associates a _hook type_ with your serializable type. The Orleans code generator calls methods on the hook type at each stage of the process.

Lifecycle hooks are useful when you need to:

- **Rehydrate transient state**: Recompute cached values or restore non-serialized fields after deserialization.
- **Validate data**: Verify invariants after deserialization or before serialization to catch corruption early.
- **Log or trace**: Record serialization activity for diagnostics, using services from dependency injection.
- **Normalize data**: Ensure fields are in a consistent format before serialization (for example, trimming strings or sorting collections).

## The `SerializationCallbacksAttribute`

Apply `[SerializationCallbacks(typeof(THook))]` to a class or struct that uses `[GenerateSerializer]`. The `THook` type parameter specifies a class whose public methods are called at each lifecycle stage. The hook type is resolved from the <xref:System.IServiceProvider>, so it can use dependency injection.

The hook type can implement any combination of the following public methods:

| Method signature | Called when |
|---|---|
| `void OnSerializing(T value)` | Before the serializer writes the object's fields. |
| `void OnSerialized(T value)` | After the serializer finishes writing the object's fields. |
| `void OnDeserializing(T value)` | Before the serializer reads the object's fields (instance is created but not yet populated). |
| `void OnDeserialized(T value)` | After the serializer finishes reading the object's fields. |
| `void OnCopying(T original, T result)` | Before the copier copies the object's fields. |
| `void OnCopied(T original, T result)` | After the copier finishes copying the object's fields. |

Where `T` is the type that the attribute is applied to. All methods are optional—implement only the ones you need.

> [!NOTE]
> The copier callbacks (`OnCopying` and `OnCopied`) take two parameters: the original instance and the copy being constructed. The serialization and deserialization callbacks take a single parameter: the instance being processed.

## Basic example: rehydrating cached state

A common scenario is recomputing a value that isn't serialized. In this example, a `TemperatureReading` stores Celsius and caches the Fahrenheit conversion in a non-serialized field. The hook recomputes it after deserialization.

Define the serializable type with the `[SerializationCallbacks]` attribute:

:::code language="csharp" source="snippets/serialization-lifecycle-hooks/BasicHook.cs" id="BasicHookType":::

Then define the hook class with an `OnDeserialized` method:

:::code language="csharp" source="snippets/serialization-lifecycle-hooks/BasicHook.cs" id="BasicHookClass":::

When Orleans deserializes a `TemperatureReading`, it creates the instance, populates the `Celsius` and `Timestamp` fields, and then calls `TemperatureReadingHooks.OnDeserialized` to recompute the cached `Fahrenheit` value.

## Dependency injection in hooks

Because the hook type is resolved from the `IServiceProvider`, you can inject services into its constructor. This is useful for logging, metrics, or accessing application services during serialization.

Define a type with a hook that uses an injected logger:

:::code language="csharp" source="snippets/serialization-lifecycle-hooks/DiHook.cs" id="DiHookType":::

The hook class receives an `ILogger<T>` through constructor injection:

:::code language="csharp" source="snippets/serialization-lifecycle-hooks/DiHook.cs" id="DiHookClass":::

> [!TIP]
> Register the hook type in the dependency injection container if it has constructor parameters. Orleans resolves hook types from the `IServiceProvider`, so types with parameterless constructors are resolved automatically, while types with dependencies must be registered.

## Validation example

Hooks are a natural place to enforce data invariants. By validating in `OnDeserialized`, you catch invalid data at the point of entry rather than allowing it to propagate through the system.

Define the type:

:::code language="csharp" source="snippets/serialization-lifecycle-hooks/ValidationHook.cs" id="ValidationHookType":::

The hook validates on both serialization and deserialization:

:::code language="csharp" source="snippets/serialization-lifecycle-hooks/ValidationHook.cs" id="ValidationHookClass":::

## How it works

When the Orleans code generator encounters a type decorated with `[SerializationCallbacks(typeof(THook))]`:

1. The generated serializer stores a reference to the `THook` instance (resolved from DI).
2. During serialization, it calls `THook.OnSerializing(instance)` before writing fields and `THook.OnSerialized(instance)` after.
3. During deserialization, it calls `THook.OnDeserializing(instance)` before reading fields and `THook.OnDeserialized(instance)` after.
4. During copying, it calls `THook.OnCopying(original, copy)` before copying fields and `THook.OnCopied(original, copy)` after.

You can apply the attribute multiple times to register more than one hook type. Hooks are called in the order they are declared.

## See also

- [Custom activators with IActivator\<T>](custom-activators.md)
- [Serialization and custom serializers](serialization.md)
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using Microsoft.Extensions.Logging;
using Orleans;

namespace CustomActivators;

// <ActivatorConstructorType>
[GenerateSerializer]
public class NotificationMessage
{
[Id(0)]
public string Title { get; set; } = string.Empty;

[Id(1)]
public string Body { get; set; } = string.Empty;

[NonSerialized]
private readonly ILogger<NotificationMessage> _logger;

[GeneratedActivatorConstructor]
public NotificationMessage(ILogger<NotificationMessage> logger)
{
_logger = logger;
}

public void Send()
{
_logger.LogInformation("Sending notification: {Title}", Title);
}
}
// </ActivatorConstructorType>
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using Orleans;
using Orleans.Serialization.Activators;

namespace CustomActivators;

// <BasicType>
[GenerateSerializer]
[UseActivator]
public class MyMessage
{
[Id(0)]
public string? Text { get; set; }

[Id(1)]
public DateTime CreatedAt { get; set; }

public MyMessage()
{
CreatedAt = DateTime.UtcNow;
}
}
// </BasicType>

// <BasicActivator>
[RegisterActivator]
public class MyMessageActivator : IActivator<MyMessage>
{
public MyMessage Create() => new MyMessage();
}
// </BasicActivator>
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
using Orleans;
using Orleans.Serialization.Activators;
using Microsoft.Extensions.Logging;

namespace CustomActivators;

// <DiType>
[GenerateSerializer]
[UseActivator]
public class AuditEntry
{
[NonSerialized]
private readonly ILogger<AuditEntry> _logger;

[Id(0)]
public string? Action { get; set; }

[Id(1)]
public DateTime Timestamp { get; set; }

public AuditEntry(ILogger<AuditEntry> logger)
{
_logger = logger;
}

public void Log() =>
_logger.LogInformation(
"Audit: {Action} at {Timestamp}", Action, Timestamp);
}
// </DiType>

// <DiActivator>
[RegisterActivator]
public class AuditEntryActivator : IActivator<AuditEntry>
{
private readonly ILogger<AuditEntry> _logger;

public AuditEntryActivator(ILogger<AuditEntry> logger)
{
_logger = logger;
}

public AuditEntry Create() => new AuditEntry(_logger);
}
// </DiActivator>
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using Orleans;
using Orleans.Serialization.Activators;

namespace CustomActivators;

// <GenericType>
[GenerateSerializer]
[UseActivator]
public class Wrapper<T>
{
[Id(0)]
public T? Value { get; set; }

[Id(1)]
public string? Metadata { get; set; }

public Wrapper(string metadata)
{
Metadata = metadata;
}
}
// </GenericType>

// <GenericActivator>
[RegisterActivator]
public class WrapperActivator<T> : IActivator<Wrapper<T>>
{
public Wrapper<T> Create() => new Wrapper<T>("default");
}
// </GenericActivator>
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using Orleans;
using Orleans.Serialization.Activators;
using Microsoft.Extensions.ObjectPool;

namespace CustomActivators;

// <PooledType>
[GenerateSerializer]
[UseActivator]
public class FrequentMessage
{
[Id(0)]
public string? Payload { get; set; }

public void Reset()
{
Payload = null;
}
}
// </PooledType>

// <PooledActivator>
[RegisterActivator]
public class FrequentMessageActivator : IActivator<FrequentMessage>
{
private readonly ObjectPool<FrequentMessage> _pool;

public FrequentMessageActivator(ObjectPool<FrequentMessage> pool)
{
_pool = pool;
}

public FrequentMessage Create() => _pool.Get();
}
// </PooledActivator>
Loading