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
63 changes: 62 additions & 1 deletion docs/serialization.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ Please read the [Introduction](intro.md) before reading this provider documentat

- [Configuration](#configuration)
- [Json (System.Text.Json)](#json-systemtextjson)
- [Polymorphic Type Handling](#polymorphic-type-handling)
- [Object to Inferred Types Converter](#object-to-inferred-types-converter)
- [Json (Newtonsoft.Json)](#json-newtonsoftjson)
- [Avro](#avro)
- [GoogleProtobuf](#googleprotobuf)
Expand Down Expand Up @@ -83,7 +85,64 @@ services.AddSlimMessageBus(mbb =>
});
```

By default the plugin adds a custom converter (see [`ObjectToInferredTypesConverter`](../src/SlimMessageBus.Host.Serialization.SystemTextJson/ObjectToInferredTypesConverter.cs)) that infers primitive types whenever the type to deseriaize is object (unknown). This helps with header value serialization for transport providers that transmit the headers as binary (Kafka). See the source code for better explanation.
### Polymorphic Type Handling

By default, the serializer uses the `useActualTypeOnSerialize: true` parameter, which ensures that the actual runtime type of the message is used during serialization. This is important for polymorphic message types where you might publish a derived type but declare the producer with a base type.

```cs
// Default behavior - uses actual type during serialization
mbb.AddJsonSerializer(useActualTypeOnSerialize: true);

// Alternative - uses declared type during serialization
mbb.AddJsonSerializer(useActualTypeOnSerialize: false);
```

**When `useActualTypeOnSerialize: true` (default):**

- The serializer uses `message.GetType()` to determine which properties to serialize
- All properties of the derived type are included in the JSON
- Useful when consumers know the actual message type and need all properties
- Works without requiring `[JsonDerivedType]` attributes on base message types

**When `useActualTypeOnSerialize: false`:**

- The serializer uses the declared `messageType` parameter passed to the serialize method
- Only properties of the declared base type are serialized
- Useful for strict contract enforcement where consumers should only see base type properties
- Requires `[JsonDerivedType]` attributes on base types if you need polymorphic deserialization

**Example:**

```cs
public class BaseMessage
{
public string Id { get; set; }
}

public class DerivedMessage : BaseMessage
{
public string ExtraData { get; set; }
}

// Producer configuration
mbb.Produce<BaseMessage>(x => x.DefaultTopic("my-topic"));

// When publishing
var message = new DerivedMessage { Id = "123", ExtraData = "test" };
await bus.Publish(message);

// With useActualTypeOnSerialize: true (default)
// Serialized JSON: {"id":"123","extraData":"test"}

// With useActualTypeOnSerialize: false
// Serialized JSON: {"id":"123"}
```

> **Note:** System.Text.Json requires `[JsonDerivedType]` attributes on the base type for proper polymorphic deserialization. The `useActualTypeOnSerialize: true` setting helps with serialization but doesn't affect deserialization requirements.

### Object to Inferred Types Converter

By default the plugin adds a custom converter (see [`ObjectToInferredTypesConverter`](../src/SlimMessageBus.Host.Serialization.SystemTextJson/ObjectToInferredTypesConverter.cs)) that infers primitive types whenever the type to deserialize is object (unknown). This helps with header value serialization for transport providers that transmit the headers as binary (Kafka). See the source code for better explanation.

## Json (Newtonsoft.Json)

Expand All @@ -110,6 +169,8 @@ var jsonSerializerSettings = new Newtonsoft.Json.JsonSerializerSettings
mbb.AddJsonSerializer(jsonSerializerSettings, Encoding.UTF8);
```

> **Note:** Newtonsoft.Json always serializes using the declared type (`messageType` parameter). For polymorphic type handling with Newtonsoft.Json, use the `TypeNameHandling` setting in `JsonSerializerSettings`.

## Avro

Nuget package: [SlimMessageBus.Host.Serialization.Avro](https://www.nuget.org/packages/SlimMessageBus.Host.Serialization.Avro)
Expand Down
2 changes: 1 addition & 1 deletion src/Host.Plugin.Properties.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<Import Project="Common.NuGet.Properties.xml" />

<PropertyGroup>
<Version>3.3.7-rc100</Version>
<Version>3.4.0-rc100</Version>
</PropertyGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -1,22 +1,25 @@
namespace SlimMessageBus.Host.Serialization.SystemTextJson;

using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;

/// <summary>
/// Implementation of <see cref="IMessageSerializer"/> using <see cref="JsonSerializer"/>.
/// </summary>
public class JsonMessageSerializer : IMessageSerializer, IMessageSerializer<string>, IMessageSerializerProvider
{
public class JsonMessageSerializer : IMessageSerializer, IMessageSerializer<string>, IMessageSerializerProvider
{
private readonly bool _useActualTypeOnSerialize;

/// <summary>
/// <see cref="JsonSerializerOptions"/> options for the JSON serializer. By default adds <see cref="ObjectToInferredTypesConverter"/> converter.
/// </summary>
public JsonSerializerOptions Options { get; set; }

public JsonMessageSerializer(JsonSerializerOptions options = null)
public JsonMessageSerializer(JsonSerializerOptions options = null, bool useActualTypeOnSerialize = true)
{
Options = options ?? CreateDefaultOptions();
_useActualTypeOnSerialize = useActualTypeOnSerialize;
}

public virtual JsonSerializerOptions CreateDefaultOptions()
Expand All @@ -34,7 +37,7 @@ public virtual JsonSerializerOptions CreateDefaultOptions()
#region Implementation of IMessageSerializer

public byte[] Serialize(Type messageType, IDictionary<string, object> headers, object message, object transportMessage)
=> JsonSerializer.SerializeToUtf8Bytes(message, messageType, Options);
=> JsonSerializer.SerializeToUtf8Bytes(message, _useActualTypeOnSerialize && message != null ? message.GetType() : messageType, Options);

public object Deserialize(Type messageType, IReadOnlyDictionary<string, object> headers, byte[] payload, object transportMessage)
=> JsonSerializer.Deserialize(payload, messageType, Options)!;
Expand All @@ -44,7 +47,7 @@ public object Deserialize(Type messageType, IReadOnlyDictionary<string, object>
#region Implementation of IMessageSerializer<string>

string IMessageSerializer<string>.Serialize(Type messageType, IDictionary<string, object> headers, object message, object transportMessage)
=> JsonSerializer.Serialize(message, messageType, Options);
=> JsonSerializer.Serialize(message, _useActualTypeOnSerialize && message != null ? message.GetType() : messageType, Options);

public object Deserialize(Type messageType, IReadOnlyDictionary<string, object> headers, string payload, object transportMessage)
=> JsonSerializer.Deserialize(payload, messageType, Options)!;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,16 @@ public static class SerializationBuilderExtensions
/// Registers the <see cref="IMessageSerializer"/> with implementation as <see cref="JsonMessageSerializer"/>.
/// </summary>
/// <param name="builder"></param>
/// <param name="options"></param>
/// <param name="useActualTypeOnSerialize">Should the actual message type be used during serialization? If false, the producer declared type will be passed (e.g. base type of the actual message). It will make a difference for polymorphic types.</param>
/// <returns></returns>
public static TBuilder AddJsonSerializer<TBuilder>(this TBuilder builder, JsonSerializerOptions options = null)
public static TBuilder AddJsonSerializer<TBuilder>(this TBuilder builder, JsonSerializerOptions options = null, bool useActualTypeOnSerialize = true)
where TBuilder : ISerializationBuilder
{
builder.RegisterSerializer<JsonMessageSerializer>(services =>
{
// Add the implementation
services.TryAddSingleton(svp => new JsonMessageSerializer(options ?? svp.GetService<JsonSerializerOptions>()));
services.TryAddSingleton(svp => new JsonMessageSerializer(options ?? svp.GetService<JsonSerializerOptions>(), useActualTypeOnSerialize));
// Add the serializer as IMessageSerializer<string>
services.TryAddSingleton(svp => svp.GetRequiredService<JsonMessageSerializer>() as IMessageSerializer<string>);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public void When_SerializeAndDeserialize_Given_TypeObjectAndBytesPayload_Then_Tr
// arrange
var subject = new JsonMessageSerializer();

// actx
// act
var bytes = subject.Serialize(typeof(object), null, value, null);
var deserializedValue = subject.Deserialize(typeof(object), null, bytes, null);

Expand Down
Loading