diff --git a/src/Kurrent.Client/Core/ResolvedEvent.cs b/src/Kurrent.Client/Core/ResolvedEvent.cs
index 27cc72154..085eb0c1c 100644
--- a/src/Kurrent.Client/Core/ResolvedEvent.cs
+++ b/src/Kurrent.Client/Core/ResolvedEvent.cs
@@ -1,3 +1,6 @@
+using System.Diagnostics.CodeAnalysis;
+using EventStore.Client.Serialization;
+
namespace EventStore.Client {
///
/// A structure representing a single event or a resolved link event.
@@ -43,23 +46,31 @@ public readonly struct ResolvedEvent {
///
public bool IsResolved => Link != null && Event != null;
+ readonly ISchemaSerializer _serializer;
+
///
/// Constructs a new .
///
///
///
///
- public ResolvedEvent(EventRecord @event, EventRecord? link, ulong? commitPosition) {
- Event = @event;
- Link = link;
+ ///
+ public ResolvedEvent(EventRecord @event, EventRecord? link, ulong? commitPosition, ISchemaSerializer serializer) {
+ Event = @event;
+ Link = link;
+ _serializer = serializer;
OriginalPosition = commitPosition.HasValue
? new Position(commitPosition.Value, (link ?? @event).Position.PreparePosition)
: new Position?();
}
- public bool TryDeserialize(out object o) {
- o = true;
- return true;
+#if NET48
+ public bool TryDeserialize(out object? deserialized) {
+#else
+ public bool TryDeserialize([NotNullWhen(true)] out object? deserialized) {
+#endif
+ deserialized = _serializer.Deserialize(OriginalEvent.Data, OriginalEvent.EventType);
+ return deserialized != null;
}
}
}
diff --git a/src/Kurrent.Client/Core/Serialization/EventTypeMapper.cs b/src/Kurrent.Client/Core/Serialization/EventTypeMapper.cs
new file mode 100644
index 000000000..14310b31c
--- /dev/null
+++ b/src/Kurrent.Client/Core/Serialization/EventTypeMapper.cs
@@ -0,0 +1,52 @@
+using System.Collections.Concurrent;
+
+namespace Kurrent.Client.Tests.Streams.Serialization;
+
+// TODO: Discuss how to proceed with that and whether to move the Schema Registry code here
+// The scanning part and registration seems to be more robust there
+// I used this for simplicity
+public interface IEventTypeMapper {
+ void AddCustomMap(string eventTypeName);
+ void AddCustomMap(Type eventType, string eventTypeName);
+ string ToName();
+ string ToName(Type eventType);
+ Type? ToType(string eventTypeName);
+}
+
+public class EventTypeMapper : IEventTypeMapper {
+ public static readonly EventTypeMapper Instance = new();
+
+ private readonly ConcurrentDictionary typeMap = new();
+ private readonly ConcurrentDictionary typeNameMap = new();
+
+ public void AddCustomMap(string eventTypeName) => AddCustomMap(typeof(T), eventTypeName);
+
+ public void AddCustomMap(Type eventType, string eventTypeName)
+ {
+ typeNameMap.AddOrUpdate(eventType, eventTypeName, (_, typeName) => typeName);
+ typeMap.AddOrUpdate(eventTypeName, eventType, (_, type) => type);
+ }
+
+ public string ToName() => ToName(typeof(TEventType));
+
+ public string ToName(Type eventType) => typeNameMap.GetOrAdd(eventType, _ =>
+ {
+ var eventTypeName = eventType.FullName!;
+
+ typeMap.TryAdd(eventTypeName, eventType);
+
+ return eventTypeName;
+ });
+
+ public Type? ToType(string eventTypeName) => typeMap.GetOrAdd(eventTypeName, _ =>
+ {
+ var type = TypeProvider.GetFirstMatchingTypeFromCurrentDomainAssembly(eventTypeName);
+
+ if (type == null)
+ return null;
+
+ typeNameMap.TryAdd(type, eventTypeName);
+
+ return type;
+ });
+}
diff --git a/src/Kurrent.Client/Core/Serialization/ISchemaSerializer.cs b/src/Kurrent.Client/Core/Serialization/ISchemaSerializer.cs
new file mode 100644
index 000000000..d89091e14
--- /dev/null
+++ b/src/Kurrent.Client/Core/Serialization/ISchemaSerializer.cs
@@ -0,0 +1,7 @@
+namespace EventStore.Client.Serialization;
+
+public interface ISchemaSerializer {
+ public (ReadOnlyMemory Bytes, string typeName) Serialize(object value);
+
+ public object? Deserialize(ReadOnlyMemory data, string typeName);
+}
diff --git a/src/Kurrent.Client/Core/Serialization/ISerializer.cs b/src/Kurrent.Client/Core/Serialization/ISerializer.cs
new file mode 100644
index 000000000..224a4b0bd
--- /dev/null
+++ b/src/Kurrent.Client/Core/Serialization/ISerializer.cs
@@ -0,0 +1,7 @@
+namespace EventStore.Client.Serialization;
+
+public interface ISerializer {
+ public ReadOnlyMemory Serialize(object value);
+
+ public object? Deserialize(ReadOnlyMemory data, Type type);
+}
diff --git a/src/Kurrent.Client/Core/Serialization/SchemaSerializer.cs b/src/Kurrent.Client/Core/Serialization/SchemaSerializer.cs
new file mode 100644
index 000000000..0bbcf683e
--- /dev/null
+++ b/src/Kurrent.Client/Core/Serialization/SchemaSerializer.cs
@@ -0,0 +1,17 @@
+using Kurrent.Client.Tests.Streams.Serialization;
+
+namespace EventStore.Client.Serialization;
+
+public class SchemaSerializer(ISerializer serializer, IEventTypeMapper eventTypeMapper) : ISchemaSerializer {
+ public (ReadOnlyMemory Bytes, string typeName) Serialize(object value) {
+ var eventType = eventTypeMapper.ToName(value.GetType());
+ var bytes = serializer.Serialize(value);
+
+ return (bytes, eventType);
+ }
+
+ public object? Deserialize(ReadOnlyMemory data, string typeName) {
+ var clrType = eventTypeMapper.ToType(typeName);
+ return clrType != null ? serializer.Deserialize(data, clrType) : null;
+ }
+}
diff --git a/src/Kurrent.Client/Core/Serialization/SystemTextJsonSerializer.cs b/src/Kurrent.Client/Core/Serialization/SystemTextJsonSerializer.cs
new file mode 100644
index 000000000..39f4c85ec
--- /dev/null
+++ b/src/Kurrent.Client/Core/Serialization/SystemTextJsonSerializer.cs
@@ -0,0 +1,14 @@
+using System.Text;
+using System.Text.Json;
+using EventStore.Client.Serialization;
+
+namespace Kurrent.Client.Core.Serialization;
+
+public class SystemTextJsonSerializer: ISerializer {
+ public ReadOnlyMemory Serialize(object value) {
+ return Encoding.UTF8.GetBytes(JsonSerializer.Serialize(value));
+ }
+ public object? Deserialize(ReadOnlyMemory data, Type type) {
+ return JsonSerializer.Deserialize(data.Span, type);
+ }
+}
diff --git a/src/Kurrent.Client/Core/Serialization/TypeProvider.cs b/src/Kurrent.Client/Core/Serialization/TypeProvider.cs
new file mode 100644
index 000000000..a4d7ec20e
--- /dev/null
+++ b/src/Kurrent.Client/Core/Serialization/TypeProvider.cs
@@ -0,0 +1,26 @@
+using System.Reflection;
+
+namespace Kurrent.Client.Tests.Streams.Serialization;
+
+public static class TypeProvider
+{
+ public static Type? GetTypeFromAnyReferencingAssembly(string typeName)
+ {
+ var referencedAssemblies = Assembly.GetEntryAssembly()?
+ .GetReferencedAssemblies()
+ .Select(a => a.FullName);
+
+ if (referencedAssemblies == null)
+ return null;
+
+ return AppDomain.CurrentDomain.GetAssemblies()
+ .Where(a => referencedAssemblies.Contains(a.FullName))
+ .SelectMany(a => a.GetTypes().Where(x => x.FullName == typeName || x.Name == typeName))
+ .FirstOrDefault();
+ }
+
+ public static Type? GetFirstMatchingTypeFromCurrentDomainAssembly(string typeName) =>
+ AppDomain.CurrentDomain.GetAssemblies()
+ .SelectMany(a => a.GetTypes().Where(x => x.FullName == typeName || x.Name == typeName))
+ .FirstOrDefault();
+}
diff --git a/src/Kurrent.Client/PersistentSubscriptions/KurrentPersistentSubscriptionsClient.Read.cs b/src/Kurrent.Client/PersistentSubscriptions/KurrentPersistentSubscriptionsClient.Read.cs
index 779413111..3e727c08c 100644
--- a/src/Kurrent.Client/PersistentSubscriptions/KurrentPersistentSubscriptionsClient.Read.cs
+++ b/src/Kurrent.Client/PersistentSubscriptions/KurrentPersistentSubscriptionsClient.Read.cs
@@ -1,6 +1,7 @@
using System.Threading.Channels;
using EventStore.Client.PersistentSubscriptions;
using EventStore.Client.Diagnostics;
+using EventStore.Client.Serialization;
using Grpc.Core;
using static EventStore.Client.PersistentSubscriptions.PersistentSubscriptions;
@@ -128,6 +129,7 @@ public PersistentSubscriptionResult SubscribeToStream(
new() { Options = readOptions },
Settings,
userCredentials,
+ _schemaSerializer,
cancellationToken
);
}
@@ -221,9 +223,13 @@ async IAsyncEnumerable GetMessages() {
}
internal PersistentSubscriptionResult(
- string streamName, string groupName,
+ string streamName,
+ string groupName,
Func> selectChannelInfo,
- ReadReq request, KurrentClientSettings settings, UserCredentials? userCredentials,
+ ReadReq request,
+ KurrentClientSettings settings,
+ UserCredentials? userCredentials,
+ ISchemaSerializer schemaSerializer,
CancellationToken cancellationToken
) {
StreamName = streamName;
@@ -260,7 +266,7 @@ async Task PumpMessages() {
response.SubscriptionConfirmation.SubscriptionId
),
Event => new PersistentSubscriptionMessage.Event(
- ConvertToResolvedEvent(response),
+ ConvertToResolvedEvent(response, schemaSerializer),
response.Event.CountCase switch {
ReadResp.Types.ReadEvent.CountOneofCase.RetryCount => response.Event.RetryCount,
_ => null
@@ -363,13 +369,14 @@ public Task Nack(PersistentSubscriptionNakEventAction action, string reason, par
public Task Nack(PersistentSubscriptionNakEventAction action, string reason, params ResolvedEvent[] resolvedEvents) =>
Nack(action, reason, Array.ConvertAll(resolvedEvents, re => re.OriginalEvent.EventId));
- static ResolvedEvent ConvertToResolvedEvent(ReadResp response) => new(
+ static ResolvedEvent ConvertToResolvedEvent(ReadResp response, ISchemaSerializer schemaSerializer) => new(
ConvertToEventRecord(response.Event.Event)!,
ConvertToEventRecord(response.Event.Link),
response.Event.PositionCase switch {
ReadResp.Types.ReadEvent.PositionOneofCase.CommitPosition => response.Event.CommitPosition,
_ => null
- }
+ },
+ schemaSerializer
);
Task AckInternal(params Uuid[] eventIds) {
diff --git a/src/Kurrent.Client/PersistentSubscriptions/KurrentPersistentSubscriptionsClient.Serialization.cs b/src/Kurrent.Client/PersistentSubscriptions/KurrentPersistentSubscriptionsClient.Serialization.cs
new file mode 100644
index 000000000..d31abd6b3
--- /dev/null
+++ b/src/Kurrent.Client/PersistentSubscriptions/KurrentPersistentSubscriptionsClient.Serialization.cs
@@ -0,0 +1,13 @@
+using EventStore.Client.Serialization;
+using Kurrent.Client.Core.Serialization;
+using Kurrent.Client.Tests.Streams.Serialization;
+
+namespace EventStore.Client {
+ public partial class KurrentPersistentSubscriptionsClient {
+ // TODO: Resolve based on options
+ ISchemaSerializer _schemaSerializer = new SchemaSerializer(
+ new SystemTextJsonSerializer(),
+ EventTypeMapper.Instance
+ );
+ }
+}
diff --git a/src/Kurrent.Client/Streams/KurrentClient.Append.cs b/src/Kurrent.Client/Streams/KurrentClient.Append.cs
index b0b80a8cc..533c43522 100644
--- a/src/Kurrent.Client/Streams/KurrentClient.Append.cs
+++ b/src/Kurrent.Client/Streams/KurrentClient.Append.cs
@@ -30,6 +30,7 @@ public Task AppendToStreamAsync(
string streamName,
StreamState expectedState,
IEnumerable