diff --git a/Kurrent.Client.sln b/Kurrent.Client.sln index 1ea14ce6f..00544398a 100644 --- a/Kurrent.Client.sln +++ b/Kurrent.Client.sln @@ -13,10 +13,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kurrent.Client", "src\Kurre EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kurrent.Client.Tests.Common", "test\Kurrent.Client.Tests.Common\Kurrent.Client.Tests.Common.csproj", "{47BF715B-A0BF-4044-B335-717E56422550}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kurrent.Client.Tests.NeverLoadedAssembly", "test\Kurrent.Client.Tests.NeverLoadedAssembly\Kurrent.Client.Tests.NeverLoadedAssembly.csproj", "{0AC8A7E9-6839-4B4C-B299-950C376DF71F}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kurrent.Client.Tests.ExternalAssembly", "test\Kurrent.Client.Tests.ExternalAssembly\Kurrent.Client.Tests.ExternalAssembly.csproj", "{829AF806-1144-408A-85FE-763835775086}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|x64 = Debug|x64 @@ -38,20 +34,10 @@ Global {47BF715B-A0BF-4044-B335-717E56422550}.Debug|x64.Build.0 = Debug|Any CPU {47BF715B-A0BF-4044-B335-717E56422550}.Release|x64.ActiveCfg = Release|Any CPU {47BF715B-A0BF-4044-B335-717E56422550}.Release|x64.Build.0 = Release|Any CPU - {0AC8A7E9-6839-4B4C-B299-950C376DF71F}.Debug|x64.ActiveCfg = Debug|Any CPU - {0AC8A7E9-6839-4B4C-B299-950C376DF71F}.Debug|x64.Build.0 = Debug|Any CPU - {0AC8A7E9-6839-4B4C-B299-950C376DF71F}.Release|x64.ActiveCfg = Release|Any CPU - {0AC8A7E9-6839-4B4C-B299-950C376DF71F}.Release|x64.Build.0 = Release|Any CPU - {829AF806-1144-408A-85FE-763835775086}.Debug|x64.ActiveCfg = Debug|Any CPU - {829AF806-1144-408A-85FE-763835775086}.Debug|x64.Build.0 = Debug|Any CPU - {829AF806-1144-408A-85FE-763835775086}.Release|x64.ActiveCfg = Release|Any CPU - {829AF806-1144-408A-85FE-763835775086}.Release|x64.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {FC829F1B-43AD-4C96-9002-23D04BBA3AF3} = {C51F2C69-45A9-4D0D-A708-4FC319D5D340} {762EECAA-122E-4B0C-BC50-5AA4F72CA4E0} = {EA59C1CB-16DA-4F68-AF8A-642A969B4CF8} {47BF715B-A0BF-4044-B335-717E56422550} = {C51F2C69-45A9-4D0D-A708-4FC319D5D340} - {0AC8A7E9-6839-4B4C-B299-950C376DF71F} = {C51F2C69-45A9-4D0D-A708-4FC319D5D340} - {829AF806-1144-408A-85FE-763835775086} = {C51F2C69-45A9-4D0D-A708-4FC319D5D340} EndGlobalSection EndGlobal diff --git a/src/Kurrent.Client/Core/KurrentClientSerializationSettings.cs b/src/Kurrent.Client/Core/KurrentClientSerializationSettings.cs deleted file mode 100644 index 294cdc169..000000000 --- a/src/Kurrent.Client/Core/KurrentClientSerializationSettings.cs +++ /dev/null @@ -1,360 +0,0 @@ -using System.Text.Json; -using Kurrent.Client.Core.Serialization; - -namespace EventStore.Client; - -/// -/// Provides configuration options for messages serialization and deserialization in the KurrentDB client. -/// -public class KurrentClientSerializationSettings { - /// - /// The serializer responsible for handling JSON-formatted data. This serializer is used both for - /// serializing outgoing JSON messages and deserializing incoming JSON messages. If not specified, - /// a default System.Text.Json serializer will be used with standard settings. - ///
- /// That also allows you to bring your custom JSON serializer implementation (e.g. JSON.NET) - ///
- public ISerializer? JsonSerializer { get; set; } - - /// - /// The serializer responsible for handling binary data formats. This is used when working with - /// binary-encoded messages rather than text-based formats (e.g. Protobuf or Avro). Required when storing - /// or retrieving content with "application/octet-stream" content type - /// - public ISerializer? BytesSerializer { get; set; } - - /// - /// Determines which serialization format (JSON or binary) is used by default when writing messages - /// where the content type isn't explicitly specified. The default content type is "application/json" - /// - public ContentType DefaultContentType { get; set; } = ContentType.Json; - - /// - /// Defines the custom strategy used to map between the type name stored in messages and .NET type names. - /// If not provided the default will be used. - /// It resolves the CLR type name to the format: "{stream category name}-{CLR Message Type}". - /// You can provide your own implementation of - /// and register it here to override the default behaviour - /// - public IMessageTypeNamingStrategy? MessageTypeNamingStrategy { get; set; } - - /// - /// Allows to register mapping of CLR message types to their corresponding message type names used in serialized messages. - /// - public IDictionary MessageTypeMap { get; set; } = new Dictionary(); - - /// - /// Registers CLR message types that can be appended to the specific stream category. - /// Types will have message type names resolved based on the used - /// - public IDictionary CategoryMessageTypesMap { get; set; } = new Dictionary(); - - /// - /// Specifies the CLR type that should be used when deserializing metadata for all events. - /// When set, the client will attempt to deserialize event metadata into this type. - /// If not provided, will be used. - /// - public Type? DefaultMetadataType { get; set; } - - /// - /// Creates a new instance of serialization settings with either default values or custom configuration. - /// This factory method is the recommended way to create serialization settings for the KurrentDB client. - /// - /// Optional callback to customize the settings. If null, default settings are used. - /// A fully configured instance ready to be used with the KurrentDB client. - /// - /// - /// var settings = KurrentClientSerializationSettings.Default(options => { - /// options.RegisterMessageType<UserCreated>("user-created"); - /// options.RegisterMessageType<UserUpdated>("user-updated"); - /// options.RegisterMessageTypeForCategory<UserCreated>("user"); - /// }); - /// - /// - public static KurrentClientSerializationSettings Default( - Action? configure = null - ) { - var settings = new KurrentClientSerializationSettings(); - - configure?.Invoke(settings); - - return settings; - } - - /// - /// Configures the JSON serializer using custom options while inheriting from the default System.Text.Json settings. - /// This allows fine-tuning serialization behavior such as case sensitivity, property naming, etc. - /// - /// A function that receives the default options and returns modified options. - /// The current instance for method chaining. - /// - /// - /// settings.UseJsonSettings(options => { - /// options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; - /// options.WriteIndented = true; - /// options.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; - /// return options; - /// }); - /// - /// - public KurrentClientSerializationSettings UseJsonSettings( - Func configure - ) { - JsonSerializer = new SystemTextJsonSerializer( - new SystemTextJsonSerializationSettings - { Options = configure(SystemTextJsonSerializationSettings.DefaultJsonSerializerOptions) } - ); - - return this; - } - - /// - /// Configures JSON serialization using provided System.Text.Json serializer options. - /// - /// The JSON serializer options to use. - /// The current instance for method chaining. - public KurrentClientSerializationSettings UseJsonSettings(JsonSerializerOptions systemTextJsonSerializerOptions) { - JsonSerializer = new SystemTextJsonSerializer( - new SystemTextJsonSerializationSettings { Options = systemTextJsonSerializerOptions } - ); - - return this; - } - - /// - /// Configures JSON serialization using provided - /// - /// The SystemTextJson serialization settings to use. - /// The current instance for method chaining. - public KurrentClientSerializationSettings UseJsonSettings( - SystemTextJsonSerializationSettings jsonSerializationSettings - ) { - JsonSerializer = new SystemTextJsonSerializer(jsonSerializationSettings); - - return this; - } - - /// - /// Sets a custom JSON serializer implementation. - /// That also allows you to bring your custom JSON serializer implementation (e.g. JSON.NET) - /// - /// The serializer to use for JSON content. - /// The current instance for method chaining. - public KurrentClientSerializationSettings UseJsonSerializer(ISerializer serializer) { - JsonSerializer = serializer; - - return this; - } - - /// - /// Sets a custom binary serializer implementation. - /// That also allows you to bring your custom binary serializer implementation (e.g. Protobuf or Avro) - /// - /// The serializer to use for binary content. - /// The current instance for method chaining. - public KurrentClientSerializationSettings UseBytesSerializer(ISerializer serializer) { - BytesSerializer = serializer; - - return this; - } - - /// - /// Configures a custom message type naming strategy. - /// - /// The type of naming strategy to use. - /// The current instance for method chaining. - public KurrentClientSerializationSettings UseMessageTypeNamingStrategy() - where TCustomMessageTypeResolutionStrategy : IMessageTypeNamingStrategy, new() => - UseMessageTypeNamingStrategy(new TCustomMessageTypeResolutionStrategy()); - - /// - /// Configures a custom message type naming strategy. - /// - /// The naming strategy instance to use. - /// The current instance for method chaining. - public KurrentClientSerializationSettings UseMessageTypeNamingStrategy( - IMessageTypeNamingStrategy messageTypeNamingStrategy - ) { - MessageTypeNamingStrategy = messageTypeNamingStrategy; - - return this; - } - - /// - /// Associates a message type with a specific stream category to enable automatic deserialization. - /// In event sourcing, streams are often prefixed with a category (e.g., "user-123", "order-456"). - /// This method tells the client which message types can appear in streams of a given category. - /// - /// The event or message type that can appear in the category's streams. - /// The category prefix (e.g., "user", "order", "account"). - /// The current instance for method chaining. - /// - /// - /// // Register event types that can appear in user streams - /// settings.RegisterMessageTypeForCategory<UserCreated>("user") - /// .RegisterMessageTypeForCategory<UserUpdated>("user") - /// .RegisterMessageTypeForCategory<UserDeleted>("user"); - /// - /// - public KurrentClientSerializationSettings RegisterMessageTypeForCategory(string categoryName) => - RegisterMessageTypeForCategory(categoryName, typeof(T)); - - /// - /// Registers multiple message types for a specific stream category. - /// - /// The category name to register the types with. - /// The message types to register. - /// The current instance for method chaining. - public KurrentClientSerializationSettings RegisterMessageTypeForCategory(string categoryName, params Type[] types) { - CategoryMessageTypesMap[categoryName] = CategoryMessageTypesMap.TryGetValue(categoryName, out var current) - ? [..current, ..types] - : types; - - return this; - } - - /// - /// Maps a .NET type to a specific message type name that will be stored in the message metadata. - /// This mapping is used during automatic deserialization, as it tells the client which CLR type - /// to instantiate when encountering a message with a particular type name in the database. - /// - /// The .NET type to register (typically a message class). - /// The string identifier to use for this type in the database. - /// The current instance for method chaining. - /// - /// The type name is often different from the .NET type name to support versioning and evolution - /// of your domain model without breaking existing stored messages. - /// - /// - /// - /// // Register me types with their corresponding type identifiers - /// settings.RegisterMessageType<UserCreated>("user-created-v1") - /// .RegisterMessageType<OrderPlaced>("order-placed-v2"); - /// - /// - public KurrentClientSerializationSettings RegisterMessageType(string typeName) => - RegisterMessageType(typeof(T), typeName); - - /// - /// Registers a message type with a specific type name. - /// - /// The message type to register. - /// The type name to register for the message type. - /// The current instance for method chaining. - public KurrentClientSerializationSettings RegisterMessageType(Type type, string typeName) { - MessageTypeMap[type] = typeName; - - return this; - } - - /// - /// Registers multiple message types with their corresponding type names. - /// - /// Dictionary mapping types to their type names. - /// The current instance for method chaining. - public KurrentClientSerializationSettings RegisterMessageTypes(IDictionary typeMap) { - foreach (var map in typeMap) { - MessageTypeMap[map.Key] = map.Value; - } - - return this; - } - - /// - /// Configures a strongly-typed metadata class for all mes in the system. - /// This enables accessing metadata properties in a type-safe manner rather than using dynamic objects. - /// - /// The metadata class type containing properties matching the expected metadata fields. - /// The current instance for method chaining. - public KurrentClientSerializationSettings UseMetadataType() => - UseMetadataType(typeof(T)); - - /// - /// Configures a strongly-typed metadata class for all mes in the system. - /// This enables accessing metadata properties in a type-safe manner rather than using dynamic objects. - /// - /// The metadata class type containing properties matching the expected metadata fields. - /// The current instance for method chaining. - public KurrentClientSerializationSettings UseMetadataType(Type type) { - DefaultMetadataType = type; - - return this; - } - - /// - /// Creates a deep copy of the current serialization settings. - /// - /// A new instance with copied settings. - internal KurrentClientSerializationSettings Clone() { - return new KurrentClientSerializationSettings { - BytesSerializer = BytesSerializer, - JsonSerializer = JsonSerializer, - DefaultContentType = DefaultContentType, - MessageTypeMap = new Dictionary(MessageTypeMap), - CategoryMessageTypesMap = new Dictionary(CategoryMessageTypesMap), - MessageTypeNamingStrategy = MessageTypeNamingStrategy - }; - } -} - -/// -/// Provides operation-specific serialization settings that override the global client configuration -/// for individual operations like reading from or appending to streams. This allows fine-tuning -/// serialization behavior on a per-operation basis without changing the client-wide settings. -/// -public class OperationSerializationSettings { - /// - /// Controls whether mes should be automatically deserialized for this specific operation. - /// When enabled (the default), messages will be converted to their appropriate CLR types. - /// When disabled, messages will be returned in their raw serialized form. - /// - public AutomaticDeserialization AutomaticDeserialization { get; private set; } = AutomaticDeserialization.Enabled; - - /// - /// A callback that allows customizing serialization settings for this specific operation. - /// This can be used to override type mappings, serializers, or other settings just for - /// the scope of a single operation without affecting other operations. - /// - public Action? ConfigureSettings { get; private set; } - - /// - /// A pre-configured settings instance that disables automatic deserialization. - /// Use this when you need to access raw message data in its serialized form. - /// - public static readonly OperationSerializationSettings Disabled = new OperationSerializationSettings { - AutomaticDeserialization = AutomaticDeserialization.Disabled - }; - - /// - /// Creates operation-specific serialization settings with custom configuration while keeping - /// automatic deserialization enabled. This allows operation-specific type mappings or - /// serializer settings without changing the global client configuration. - /// - /// A callback to customize serialization settings for this operation. - /// A configured instance of with enabled deserialization. - public static OperationSerializationSettings Configure(Action configure) => - new OperationSerializationSettings { - AutomaticDeserialization = AutomaticDeserialization.Enabled, - ConfigureSettings = configure - }; -} - -/// -/// Controls whether the KurrentDB client should automatically deserialize message payloads -/// into their corresponding CLR types based on the configured type mappings. -/// -public enum AutomaticDeserialization { - /// - /// Disables automatic deserialization. Messages will be returned in their raw serialized form, - /// requiring manual deserialization by the application. Use this when you need direct access to the raw data - /// or when working with messages that don't have registered type mappings. - /// - Disabled = 0, - - /// - /// Enables automatic deserialization. The client will attempt to convert messages into their appropriate - /// CLR types using the configured serializers and type mappings. This simplifies working with strongly-typed - /// domain messages but requires proper type registration. - /// - Enabled = 1 -} diff --git a/src/Kurrent.Client/Core/KurrentClientSettings.ConnectionString.cs b/src/Kurrent.Client/Core/KurrentClientSettings.ConnectionString.cs index 57be0c950..c730b7b5a 100644 --- a/src/Kurrent.Client/Core/KurrentClientSettings.ConnectionString.cs +++ b/src/Kurrent.Client/Core/KurrentClientSettings.ConnectionString.cs @@ -14,20 +14,6 @@ public partial class KurrentClientSettings { public static KurrentClientSettings Create(string connectionString) => ConnectionStringParser.Parse(connectionString); - /// - /// Creates client settings from a connection string with additional configuration - /// - /// - /// allows you to make additional customization of client settings - /// - public static KurrentClientSettings Create(string connectionString, Action configure) { - var settings = ConnectionStringParser.Parse(connectionString); - - configure(settings); - - return settings; - } - private static class ConnectionStringParser { private const string SchemeSeparator = "://"; private const string UserInfoSeparator = "@"; diff --git a/src/Kurrent.Client/Core/KurrentClientSettings.cs b/src/Kurrent.Client/Core/KurrentClientSettings.cs index 71bfa446e..aed914074 100644 --- a/src/Kurrent.Client/Core/KurrentClientSettings.cs +++ b/src/Kurrent.Client/Core/KurrentClientSettings.cs @@ -57,11 +57,5 @@ public partial class KurrentClientSettings { /// The default deadline for calls. Will not be applied to reads or subscriptions. /// public TimeSpan? DefaultDeadline { get; set; } = TimeSpan.FromSeconds(10); - - /// - /// Provides configuration options for messages serialization and deserialization in the KurrentDB client. - /// If null, default settings are used. - /// - public KurrentClientSerializationSettings Serialization { get; set; } = KurrentClientSerializationSettings.Default(); } } diff --git a/src/Kurrent.Client/Core/ResolvedEvent.cs b/src/Kurrent.Client/Core/ResolvedEvent.cs index 3b4209082..25ca13a78 100644 --- a/src/Kurrent.Client/Core/ResolvedEvent.cs +++ b/src/Kurrent.Client/Core/ResolvedEvent.cs @@ -1,5 +1,3 @@ -using Kurrent.Client.Core.Serialization; - namespace EventStore.Client { /// /// A structure representing a single event or a resolved link event. @@ -24,23 +22,6 @@ public readonly struct ResolvedEvent { /// public EventRecord OriginalEvent => Link ?? Event; - /// - /// Returns the deserialized message - /// It will be provided or equal to null, depending on the automatic deserialization settings you choose. - /// If it's null, you can use to deserialize it manually. - /// - public readonly Message? Message; - - /// - /// Returns the deserialized message data. - /// - public object? DeserializedData => Message?.Data; - - /// - /// Returns the deserialized message metadata. - /// - public object? DeserializedMetadata => Message?.Metadata; - /// /// Position of the if available. /// @@ -68,44 +49,12 @@ public readonly struct ResolvedEvent { /// /// /// - public ResolvedEvent(EventRecord @event, EventRecord? link, ulong? commitPosition) : this( - @event, - link, - null, - commitPosition - ) { } - - /// - /// Constructs a new . - /// - /// - /// - /// - /// - ResolvedEvent( - EventRecord @event, - EventRecord? link, - Message? message, - ulong? commitPosition - ) { - Event = @event; - Link = link; - Message = message; + public ResolvedEvent(EventRecord @event, EventRecord? link, ulong? commitPosition) { + Event = @event; + Link = link; OriginalPosition = commitPosition.HasValue ? new Position(commitPosition.Value, (link ?? @event).Position.PreparePosition) : new Position?(); } - - internal static ResolvedEvent From( - EventRecord @event, - EventRecord? link, - ulong? commitPosition, - IMessageSerializer messageSerializer - ) { - var originalEvent = link ?? @event; - return messageSerializer.TryDeserialize(originalEvent, out var message) - ? new ResolvedEvent(@event, link, message, commitPosition) - : new ResolvedEvent(@event, link, commitPosition); - } } } diff --git a/src/Kurrent.Client/Core/Serialization/ISerializer.cs b/src/Kurrent.Client/Core/Serialization/ISerializer.cs deleted file mode 100644 index 93382428d..000000000 --- a/src/Kurrent.Client/Core/Serialization/ISerializer.cs +++ /dev/null @@ -1,30 +0,0 @@ -namespace Kurrent.Client.Core.Serialization; - -/// -/// Defines the core serialization capabilities required by the KurrentDB client. -/// Implementations of this interface handle the conversion between .NET objects and their -/// binary representation for storage in and retrieval from the event store. -///
-/// The client ships default System.Text.Json implementation, but custom implementations can be provided or other formats. -///
-public interface ISerializer { - /// - /// Converts a .NET object to its binary representation for storage in the event store. - /// - /// The object to serialize. This could be an event, command, or metadata object. - /// - /// A binary representation of the object that can be stored in KurrentDB. - /// - public ReadOnlyMemory Serialize(object value); - - /// - /// Reconstructs a .NET object from its binary representation retrieved from the event store. - /// - /// The binary data to deserialize, typically retrieved from a KurrentDB event. - /// The target .NET type to deserialize the data into, determined from message type mappings. - /// - /// The deserialized object cast to the specified type, or null if the data cannot be deserialized. - /// The returned object will be an instance of the specified type or a compatible subtype. - /// - public object? Deserialize(ReadOnlyMemory data, Type type); -} diff --git a/src/Kurrent.Client/Core/Serialization/Message.cs b/src/Kurrent.Client/Core/Serialization/Message.cs deleted file mode 100644 index 6666831dd..000000000 --- a/src/Kurrent.Client/Core/Serialization/Message.cs +++ /dev/null @@ -1,62 +0,0 @@ -using EventStore.Client; - -namespace Kurrent.Client.Core.Serialization; - -/// -/// Represents a message wrapper in the KurrentDB system, containing both domain data and optional metadata. -/// Messages can represent events, commands, or other domain objects along with their associated metadata. -/// -/// The message domain data. -/// Optional metadata providing additional context about the message, such as correlation IDs, timestamps, or user information. -/// Unique identifier for this specific message instance. When null, the system will auto-generate an ID. -public record Message(object Data, object? Metadata, Uuid? MessageId = null) { - /// - /// Creates a new Message with the specified domain data and message ID, but without metadata. - /// This factory method is a convenient shorthand when working with systems that don't require metadata. - /// - /// The message domain data. - /// Unique identifier for this message instance. Must not be Uuid.Empty. - /// A new immutable Message instance containing the provided data and ID with null metadata. - /// - /// - /// // Create a message with a specific ID - /// var userCreated = new UserCreated { Id = "123", Name = "Alice" }; - /// var messageId = Uuid.NewUuid(); - /// var message = Message.From(userCreated, messageId); - /// - /// - public static Message From(object data, Uuid messageId) => - From(data, null, messageId); - - /// - /// Creates a new Message with the specified domain data and message ID and metadata. - /// - /// The message domain data. - /// Optional metadata providing additional context about the message, such as correlation IDs, timestamps, or user information. - /// Unique identifier for this specific message instance. - /// A new immutable Message instance with the specified properties. - /// Thrown when messageId is explicitly set to Uuid.Empty, which is an invalid identifier. - /// - /// - /// // Create a message with data and metadata - /// var orderPlaced = new OrderPlaced { OrderId = "ORD-123", Amount = 99.99m }; - /// var metadata = new EventMetadata { - /// UserId = "user-456", - /// Timestamp = DateTimeOffset.UtcNow, - /// CorrelationId = correlationId - /// }; - /// - /// // Let the system assign an ID automatically - /// var message = Message.From(orderPlaced, metadata); - /// - /// // Or specify a custom ID - /// var messageWithId = Message.From(orderPlaced, metadata, Uuid.NewUuid()); - /// - /// - public static Message From(object data, object? metadata = null, Uuid? messageId = null) { - if (messageId == Uuid.Empty) - throw new ArgumentOutOfRangeException(nameof(messageId), "Message ID cannot be an empty UUID."); - - return new Message(data, metadata, messageId); - } -} diff --git a/src/Kurrent.Client/Core/Serialization/MessageSerializer.cs b/src/Kurrent.Client/Core/Serialization/MessageSerializer.cs deleted file mode 100644 index de66372b2..000000000 --- a/src/Kurrent.Client/Core/Serialization/MessageSerializer.cs +++ /dev/null @@ -1,148 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using EventStore.Client; - -namespace Kurrent.Client.Core.Serialization; - -using static ContentTypeExtensions; - -interface IMessageSerializer { - public EventData Serialize(Message value, MessageSerializationContext context); - -#if NET48 - public bool TryDeserialize(EventRecord record, out Message? deserialized); -#else - public bool TryDeserialize(EventRecord record, [NotNullWhen(true)] out Message? deserialized); -#endif -} - -record MessageSerializationContext( - string StreamName, - ContentType ContentType -) { - public string CategoryName => - StreamName.Split('-').FirstOrDefault() ?? "no_stream_category"; -} - -static class MessageSerializerExtensions { - public static EventData[] Serialize( - this IMessageSerializer serializer, - IEnumerable messages, - MessageSerializationContext context - ) { - return messages.Select(m => serializer.Serialize(m, context)).ToArray(); - } - - public static IMessageSerializer With( - this IMessageSerializer defaultMessageSerializer, - KurrentClientSerializationSettings defaultSettings, - OperationSerializationSettings? operationSettings - ) { - if (operationSettings == null) - return defaultMessageSerializer; - - if (operationSettings.AutomaticDeserialization == AutomaticDeserialization.Disabled) - return NullMessageSerializer.Instance; - - if (operationSettings.ConfigureSettings == null) - return defaultMessageSerializer; - - var settings = defaultSettings.Clone(); - operationSettings.ConfigureSettings.Invoke(settings); - - return new MessageSerializer(SchemaRegistry.From(settings)); - } -} - -class MessageSerializer(SchemaRegistry schemaRegistry) : IMessageSerializer { - readonly ISerializer _jsonSerializer = - schemaRegistry.GetSerializer(ContentType.Json); - - readonly IMessageTypeNamingStrategy _messageTypeNamingStrategy = - schemaRegistry.MessageTypeNamingStrategy; - - public EventData Serialize(Message message, MessageSerializationContext serializationContext) { - var (data, metadata, eventId) = message; - - var eventType = _messageTypeNamingStrategy - .ResolveTypeName( - message.Data.GetType(), - new MessageTypeNamingResolutionContext(serializationContext.CategoryName) - ); - - var serializedData = schemaRegistry - .GetSerializer(serializationContext.ContentType) - .Serialize(data); - - var serializedMetadata = metadata != null - ? _jsonSerializer.Serialize(metadata) - : ReadOnlyMemory.Empty; - - return new EventData( - eventId ?? Uuid.NewUuid(), - eventType, - serializedData, - serializedMetadata, - serializationContext.ContentType.ToMessageContentType() - ); - } - -#if NET48 - public bool TryDeserialize(EventRecord record, out Message? deserialized) { -#else - public bool TryDeserialize(EventRecord record, [NotNullWhen(true)] out Message? deserialized) { -#endif - if (!TryResolveClrType(record, out var clrType)) { - deserialized = null; - return false; - } - - var data = schemaRegistry - .GetSerializer(FromMessageContentType(record.ContentType)) - .Deserialize(record.Data, clrType!); - - if (data == null) { - deserialized = null; - return false; - } - - object? metadata = record.Metadata.Length > 0 && TryResolveClrMetadataType(record, out var clrMetadataType) - ? _jsonSerializer.Deserialize(record.Metadata, clrMetadataType!) - : null; - - deserialized = Message.From(data, metadata, record.EventId); - return true; - } - - public static MessageSerializer From(KurrentClientSerializationSettings? settings = null) { - settings ??= KurrentClientSerializationSettings.Default(); - - return new MessageSerializer(SchemaRegistry.From(settings)); - } - - bool TryResolveClrType(EventRecord record, out Type? clrType) => - schemaRegistry - .MessageTypeNamingStrategy - .TryResolveClrType(record.EventType, out clrType); - - bool TryResolveClrMetadataType(EventRecord record, out Type? clrMetadataType) => - schemaRegistry - .MessageTypeNamingStrategy - .TryResolveClrMetadataType(record.EventType, out clrMetadataType); -} - -class NullMessageSerializer : IMessageSerializer { - public static readonly NullMessageSerializer Instance = new NullMessageSerializer(); - - public EventData Serialize(Message value, MessageSerializationContext context) { - throw new InvalidOperationException("Cannot serialize, automatic deserialization is disabled"); - } - -#if NET48 - public bool TryDeserialize(EventRecord record, out Message? deserialized) { -#else - public bool TryDeserialize(EventRecord eventRecord, [NotNullWhen(true)] out Message? deserialized) { -#endif - deserialized = null; - return false; - } -} diff --git a/src/Kurrent.Client/Core/Serialization/MessageTypeRegistry.cs b/src/Kurrent.Client/Core/Serialization/MessageTypeRegistry.cs deleted file mode 100644 index 68752884a..000000000 --- a/src/Kurrent.Client/Core/Serialization/MessageTypeRegistry.cs +++ /dev/null @@ -1,76 +0,0 @@ -using System.Collections.Concurrent; - -namespace Kurrent.Client.Core.Serialization; - -interface IMessageTypeRegistry { - void Register(Type messageType, string messageTypeName); - string? GetTypeName(Type messageType); - string GetOrAddTypeName(Type clrType, Func getTypeName); - Type? GetClrType(string messageTypeName); - Type? GetOrAddClrType(string messageTypeName, Func getClrType); -} - -class MessageTypeRegistry : IMessageTypeRegistry { - readonly ConcurrentDictionary _typeMap = new(); - readonly ConcurrentDictionary _typeNameMap = new(); - - public void Register(Type messageType, string messageTypeName) { - _typeNameMap.AddOrUpdate(messageType, messageTypeName, (_, _) => messageTypeName); - _typeMap.AddOrUpdate(messageTypeName, messageType, (_, type) => type); - } - - public string? GetTypeName(Type messageType) => -#if NET48 - _typeNameMap.TryGetValue(messageType, out var typeName) ? typeName : null; -#else - _typeNameMap.GetValueOrDefault(messageType); -#endif - - public string GetOrAddTypeName(Type clrType, Func getTypeName) => - _typeNameMap.GetOrAdd( - clrType, - _ => { - var typeName = getTypeName(clrType); - - _typeMap.TryAdd(typeName, clrType); - - return typeName; - } - ); - - public Type? GetClrType(string messageTypeName) => -#if NET48 - _typeMap.TryGetValue(messageTypeName, out var clrType) ? clrType : null; -#else - _typeMap.GetValueOrDefault(messageTypeName); -#endif - - public Type? GetOrAddClrType(string messageTypeName, Func getClrType) => - _typeMap.GetOrAdd( - messageTypeName, - _ => { - var clrType = getClrType(messageTypeName); - - if (clrType == null) - return null; - - _typeNameMap.TryAdd(clrType, messageTypeName); - - return clrType; - } - ); -} - -static class MessageTypeRegistryExtensions { - public static void Register(this IMessageTypeRegistry messageTypeRegistry, string messageTypeName) => - messageTypeRegistry.Register(typeof(T), messageTypeName); - - public static void Register(this IMessageTypeRegistry messageTypeRegistry, IDictionary typeMap) { - foreach (var map in typeMap) { - messageTypeRegistry.Register(map.Key, map.Value); - } - } - - public static string? GetTypeName(this IMessageTypeRegistry messageTypeRegistry) => - messageTypeRegistry.GetTypeName(typeof(TMessageType)); -} diff --git a/src/Kurrent.Client/Core/Serialization/MessageTypeResolutionStrategy.cs b/src/Kurrent.Client/Core/Serialization/MessageTypeResolutionStrategy.cs deleted file mode 100644 index 12ff65f6b..000000000 --- a/src/Kurrent.Client/Core/Serialization/MessageTypeResolutionStrategy.cs +++ /dev/null @@ -1,100 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using Kurrent.Diagnostics.Tracing; - -namespace Kurrent.Client.Core.Serialization; - -public interface IMessageTypeNamingStrategy { - string ResolveTypeName(Type messageType, MessageTypeNamingResolutionContext resolutionContext); - -#if NET48 - bool TryResolveClrType(string messageTypeName, out Type? type); -#else - bool TryResolveClrType(string messageTypeName, [NotNullWhen(true)] out Type? type); -#endif - - -#if NET48 - bool TryResolveClrMetadataType(string messageTypeName, out Type? type); -#else - bool TryResolveClrMetadataType(string messageTypeName, [NotNullWhen(true)] out Type? type); -#endif -} - -public record MessageTypeNamingResolutionContext(string CategoryName); - -class MessageTypeNamingStrategyWrapper( - IMessageTypeRegistry messageTypeRegistry, - IMessageTypeNamingStrategy messageTypeNamingStrategy -) : IMessageTypeNamingStrategy { - public string ResolveTypeName(Type messageType, MessageTypeNamingResolutionContext resolutionContext) { - return messageTypeRegistry.GetOrAddTypeName( - messageType, - _ => messageTypeNamingStrategy.ResolveTypeName(messageType, resolutionContext) - ); - } - -#if NET48 - public bool TryResolveClrType(string messageTypeName, out Type? type) { -#else - public bool TryResolveClrType(string messageTypeName, [NotNullWhen(true)] out Type? type) { -#endif - type = messageTypeRegistry.GetOrAddClrType( - messageTypeName, - _ => messageTypeNamingStrategy.TryResolveClrType(messageTypeName, out var resolvedType) - ? resolvedType - : null - ); - - return type != null; - } - -#if NET48 - public bool TryResolveClrMetadataType(string messageTypeName, out Type? type) { -#else - public bool TryResolveClrMetadataType(string messageTypeName, [NotNullWhen(true)] out Type? type) { -#endif - type = messageTypeRegistry.GetOrAddClrType( - $"{messageTypeName}-metadata", - _ => messageTypeNamingStrategy.TryResolveClrMetadataType(messageTypeName, out var resolvedType) - ? resolvedType - : null - ); - - return type != null; - } -} - -public class DefaultMessageTypeNamingStrategy(Type? defaultMetadataType) : IMessageTypeNamingStrategy { - readonly Type _defaultMetadataType = defaultMetadataType ?? typeof(TracingMetadata); - - public string ResolveTypeName(Type messageType, MessageTypeNamingResolutionContext resolutionContext) => - $"{resolutionContext.CategoryName}-{messageType.FullName}"; - -#if NET48 - public bool TryResolveClrType(string messageTypeName, out Type? type) { -#else - public bool TryResolveClrType(string messageTypeName, [NotNullWhen(true)] out Type? type) { -#endif - var categorySeparatorIndex = messageTypeName.IndexOf('-'); - - if (categorySeparatorIndex == -1 || categorySeparatorIndex == messageTypeName.Length - 1) { - type = null; - return false; - } - - var clrTypeName = messageTypeName[(categorySeparatorIndex + 1)..]; - - type = TypeProvider.GetTypeByFullName(clrTypeName); - - return type != null; - } - -#if NET48 - public bool TryResolveClrMetadataType(string messageTypeName, out Type? type) { -#else - public bool TryResolveClrMetadataType(string messageTypeName, [NotNullWhen(true)] out Type? type) { -#endif - type = _defaultMetadataType; - return true; - } -} diff --git a/src/Kurrent.Client/Core/Serialization/SchemaRegistry.cs b/src/Kurrent.Client/Core/Serialization/SchemaRegistry.cs deleted file mode 100644 index 9a5ad7515..000000000 --- a/src/Kurrent.Client/Core/Serialization/SchemaRegistry.cs +++ /dev/null @@ -1,91 +0,0 @@ -using EventStore.Client; - -namespace Kurrent.Client.Core.Serialization; - -using static Constants.Metadata.ContentTypes; - -public enum ContentType { - Json = 1, - - // Protobuf = 2, - // Avro = 3, - Bytes = 4 -} - -static class ContentTypeExtensions { - public static ContentType FromMessageContentType(string contentType) => - contentType == ApplicationJson - ? ContentType.Json - : ContentType.Bytes; - - public static string ToMessageContentType(this ContentType contentType) => - contentType switch { - ContentType.Json => ApplicationJson, - ContentType.Bytes => ApplicationOctetStream, - _ => throw new ArgumentOutOfRangeException(nameof(contentType), contentType, null) - }; -} - -class SchemaRegistry( - IDictionary serializers, - IMessageTypeNamingStrategy messageTypeNamingStrategy -) { - public IMessageTypeNamingStrategy MessageTypeNamingStrategy { get; } = messageTypeNamingStrategy; - - public ISerializer GetSerializer(ContentType schemaType) => - serializers[schemaType]; - - public static SchemaRegistry From(KurrentClientSerializationSettings settings) { - var messageTypeNamingStrategy = - settings.MessageTypeNamingStrategy ?? new DefaultMessageTypeNamingStrategy(settings.DefaultMetadataType); - - var categoriesTypeMap = ResolveMessageTypeUsingNamingStrategy( - settings.CategoryMessageTypesMap, - messageTypeNamingStrategy - ); - - var messageTypeRegistry = new MessageTypeRegistry(); - messageTypeRegistry.Register(settings.MessageTypeMap); - messageTypeRegistry.Register(categoriesTypeMap); - - var serializers = new Dictionary { - { - ContentType.Json, - settings.JsonSerializer ?? new SystemTextJsonSerializer() - }, { - ContentType.Bytes, - settings.BytesSerializer ?? new SystemTextJsonSerializer() - } - }; - - return new SchemaRegistry( - serializers, - new MessageTypeNamingStrategyWrapper( - messageTypeRegistry, - settings.MessageTypeNamingStrategy ?? new DefaultMessageTypeNamingStrategy(settings.DefaultMetadataType) - ) - ); - } - - static Dictionary ResolveMessageTypeUsingNamingStrategy( - IDictionary categoryMessageTypesMap, - IMessageTypeNamingStrategy messageTypeNamingStrategy - ) => - categoryMessageTypesMap - .SelectMany( - categoryTypes => categoryTypes.Value.Select( - type => - ( - Type: type, - TypeName: messageTypeNamingStrategy.ResolveTypeName( - type, - new MessageTypeNamingResolutionContext(categoryTypes.Key) - ) - ) - ) - ) - .ToDictionary( - ks => ks.Type, - vs => vs.TypeName - ); -} diff --git a/src/Kurrent.Client/Core/Serialization/SystemTextJsonSerializer.cs b/src/Kurrent.Client/Core/Serialization/SystemTextJsonSerializer.cs deleted file mode 100644 index 4efa7be96..000000000 --- a/src/Kurrent.Client/Core/Serialization/SystemTextJsonSerializer.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace Kurrent.Client.Core.Serialization; - -public class SystemTextJsonSerializationSettings { - public static readonly JsonSerializerOptions DefaultJsonSerializerOptions = - new JsonSerializerOptions(JsonSerializerOptions.Default) { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - DictionaryKeyPolicy = JsonNamingPolicy.CamelCase, - PropertyNameCaseInsensitive = false, - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, - UnknownTypeHandling = JsonUnknownTypeHandling.JsonNode, - UnmappedMemberHandling = JsonUnmappedMemberHandling.Skip, - NumberHandling = JsonNumberHandling.AllowReadingFromString, - Converters = { - new JsonStringEnumConverter(JsonNamingPolicy.CamelCase), - } - }; - - public JsonSerializerOptions Options { get; set; } = DefaultJsonSerializerOptions; -} - -public class SystemTextJsonSerializer(SystemTextJsonSerializationSettings? options = null) : ISerializer { - readonly JsonSerializerOptions _options = options?.Options ?? SystemTextJsonSerializationSettings.DefaultJsonSerializerOptions; - - public ReadOnlyMemory Serialize(object value) => - JsonSerializer.SerializeToUtf8Bytes(value, _options); - - public object? Deserialize(ReadOnlyMemory data, Type type) => - !data.IsEmpty ? JsonSerializer.Deserialize(data.Span, type, _options) : null; -} diff --git a/src/Kurrent.Client/Core/Serialization/TypeProvider.cs b/src/Kurrent.Client/Core/Serialization/TypeProvider.cs deleted file mode 100644 index f05f23f87..000000000 --- a/src/Kurrent.Client/Core/Serialization/TypeProvider.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace Kurrent.Client.Core.Serialization; - -static class TypeProvider { - public static Type? GetTypeByFullName(string fullName) => - Type.GetType(fullName) ?? GetFirstMatchingTypeFromCurrentDomainAssembly(fullName); - - static Type? GetFirstMatchingTypeFromCurrentDomainAssembly(string fullName) { - var firstNamespacePart = fullName.Split('.')[0]; - - return AppDomain.CurrentDomain.GetAssemblies() - .OrderByDescending(assembly => assembly.FullName?.StartsWith(firstNamespacePart) == true) - .Select(assembly => assembly.GetType(fullName)) - .FirstOrDefault(type => type != null); - } -} diff --git a/src/Kurrent.Client/Kurrent.Client.csproj b/src/Kurrent.Client/Kurrent.Client.csproj index 59ebc0861..e6652b773 100644 --- a/src/Kurrent.Client/Kurrent.Client.csproj +++ b/src/Kurrent.Client/Kurrent.Client.csproj @@ -101,7 +101,7 @@ - + diff --git a/src/Kurrent.Client/PersistentSubscriptions/KurrentPersistentSubscriptionsClient.Read.cs b/src/Kurrent.Client/PersistentSubscriptions/KurrentPersistentSubscriptionsClient.Read.cs index 1ed1b40a0..779413111 100644 --- a/src/Kurrent.Client/PersistentSubscriptions/KurrentPersistentSubscriptionsClient.Read.cs +++ b/src/Kurrent.Client/PersistentSubscriptions/KurrentPersistentSubscriptionsClient.Read.cs @@ -2,65 +2,11 @@ using EventStore.Client.PersistentSubscriptions; using EventStore.Client.Diagnostics; using Grpc.Core; -using Kurrent.Client.Core.Serialization; + using static EventStore.Client.PersistentSubscriptions.PersistentSubscriptions; using static EventStore.Client.PersistentSubscriptions.ReadResp.ContentOneofCase; namespace EventStore.Client { - public class SubscribeToPersistentSubscriptionOptions { - /// - /// The size of the buffer. - /// - public int BufferSize { get; set; } = 10; - - /// - /// The optional user credentials to perform operation with. - /// - public UserCredentials? UserCredentials { get; set; } = null; - - /// - /// Allows to customize or disable the automatic deserialization - /// - public OperationSerializationSettings? SerializationSettings { get; set; } - } - - public class PersistentSubscriptionListener { - /// - /// A handler called when a new event is received over the subscription. - /// -#if NET48 - public Func EventAppeared { get; set; } = - null!; -#else - public required Func EventAppeared { - get; - set; - } -#endif - /// - /// A handler called if the subscription is dropped. - /// - public Action? SubscriptionDropped { get; set; } - - /// - /// Returns the subscription listener with configured handlers - /// - /// Handler invoked when a new event is received over the subscription. - /// A handler invoked if the subscription is dropped. - /// A handler called when a checkpoint is reached. - /// Set the checkpointInterval in subscription filter options to define how often this method is called. - /// - /// - public static PersistentSubscriptionListener Handle( - Func eventAppeared, - Action? subscriptionDropped = null - ) => - new PersistentSubscriptionListener { - EventAppeared = eventAppeared, - SubscriptionDropped = subscriptionDropped - }; - } - partial class KurrentPersistentSubscriptionsClient { /// /// Subscribes to a persistent subscription. @@ -70,13 +16,10 @@ partial class KurrentPersistentSubscriptionsClient { /// [Obsolete("SubscribeAsync is no longer supported. Use SubscribeToStream with manual acks instead.", false)] public async Task SubscribeAsync( - string streamName, - string groupName, + string streamName, string groupName, Func eventAppeared, Action? subscriptionDropped = null, - UserCredentials? userCredentials = null, - int bufferSize = 10, - bool autoAck = true, + UserCredentials? userCredentials = null, int bufferSize = 10, bool autoAck = true, CancellationToken cancellationToken = default ) { if (autoAck) { @@ -85,17 +28,16 @@ public async Task SubscribeAsync( ); } - return await SubscribeToStreamAsync( - streamName, - groupName, - PersistentSubscriptionListener.Handle(eventAppeared, subscriptionDropped), - new SubscribeToPersistentSubscriptionOptions { - UserCredentials = userCredentials, - BufferSize = bufferSize, - SerializationSettings = OperationSerializationSettings.Disabled - }, - cancellationToken - ); + return await PersistentSubscription + .Confirm( + SubscribeToStream(streamName, groupName, bufferSize, userCredentials, cancellationToken), + eventAppeared, + subscriptionDropped ?? delegate { }, + _log, + userCredentials, + cancellationToken + ) + .ConfigureAwait(false); } /// @@ -104,45 +46,20 @@ public async Task SubscribeAsync( /// /// /// - public Task SubscribeToStreamAsync( - string streamName, - string groupName, + public async Task SubscribeToStreamAsync( + string streamName, string groupName, Func eventAppeared, Action? subscriptionDropped = null, - UserCredentials? userCredentials = null, - int bufferSize = 10, - CancellationToken cancellationToken = default - ) => - SubscribeToStreamAsync( - streamName, - groupName, - PersistentSubscriptionListener.Handle(eventAppeared, subscriptionDropped), - new SubscribeToPersistentSubscriptionOptions { - UserCredentials = userCredentials, - BufferSize = bufferSize, - SerializationSettings = OperationSerializationSettings.Disabled - }, - cancellationToken - ); - - /// - /// Subscribes to a persistent subscription. Messages must be manually acknowledged - /// - /// - /// - /// - public async Task SubscribeToStreamAsync( - string streamName, - string groupName, - PersistentSubscriptionListener listener, - SubscribeToPersistentSubscriptionOptions options, + UserCredentials? userCredentials = null, int bufferSize = 10, CancellationToken cancellationToken = default ) { return await PersistentSubscription .Confirm( - SubscribeToStream(streamName, groupName, options, cancellationToken), - listener, + SubscribeToStream(streamName, groupName, bufferSize, userCredentials, cancellationToken), + eventAppeared, + subscriptionDropped ?? delegate { }, _log, + userCredentials, cancellationToken ) .ConfigureAwait(false); @@ -158,65 +75,8 @@ public async Task SubscribeToStreamAsync( /// The optional . /// public PersistentSubscriptionResult SubscribeToStream( - string streamName, - string groupName, - int bufferSize, - UserCredentials? userCredentials = null, - CancellationToken cancellationToken = default - ) { - return SubscribeToStream( - streamName, - groupName, - new SubscribeToPersistentSubscriptionOptions { - BufferSize = bufferSize, - UserCredentials = userCredentials, - SerializationSettings = OperationSerializationSettings.Disabled - }, - cancellationToken - ); - } - - - - /// - /// Subscribes to a persistent subscription. Messages must be manually acknowledged. - /// - /// The name of the stream to read events from. - /// The name of the persistent subscription group. - /// The size of the buffer. - /// The optional user credentials to perform operation with. - /// The optional . - /// - public PersistentSubscriptionResult SubscribeToStream( - string streamName, - string groupName, - UserCredentials? userCredentials, - CancellationToken cancellationToken = default - ) { - return SubscribeToStream( - streamName, - groupName, - new SubscribeToPersistentSubscriptionOptions { - UserCredentials = userCredentials, - SerializationSettings = OperationSerializationSettings.Disabled - }, - cancellationToken - ); - } - - /// - /// Subscribes to a persistent subscription. Messages must be manually acknowledged. - /// - /// The name of the stream to read events from. - /// The name of the persistent subscription group. - /// Optional settings to configure subscription - /// The optional . - /// - public PersistentSubscriptionResult SubscribeToStream( - string streamName, - string groupName, - SubscribeToPersistentSubscriptionOptions options, - CancellationToken cancellationToken = default + string streamName, string groupName, int bufferSize = 10, + UserCredentials? userCredentials = null, CancellationToken cancellationToken = default ) { if (streamName == null) { throw new ArgumentNullException(nameof(streamName)); @@ -234,12 +94,12 @@ public PersistentSubscriptionResult SubscribeToStream( throw new ArgumentException($"{nameof(groupName)} may not be empty.", nameof(groupName)); } - if (options.BufferSize <= 0) { - throw new ArgumentOutOfRangeException(nameof(options.BufferSize)); + if (bufferSize <= 0) { + throw new ArgumentOutOfRangeException(nameof(bufferSize)); } var readOptions = new ReadReq.Types.Options { - BufferSize = options.BufferSize, + BufferSize = bufferSize, GroupName = groupName, UuidOption = new ReadReq.Types.Options.Types.UUIDOption { Structured = new Empty() } }; @@ -267,8 +127,7 @@ public PersistentSubscriptionResult SubscribeToStream( }, new() { Options = readOptions }, Settings, - options.UserCredentials, - _messageSerializer.With(Settings.Serialization, options.SerializationSettings), + userCredentials, cancellationToken ); } @@ -276,41 +135,23 @@ public PersistentSubscriptionResult SubscribeToStream( /// /// Subscribes to a persistent subscription to $all. Messages must be manually acknowledged /// - public Task SubscribeToAllAsync( + public async Task SubscribeToAllAsync( string groupName, Func eventAppeared, Action? subscriptionDropped = null, - UserCredentials? userCredentials = null, - int bufferSize = 10, + UserCredentials? userCredentials = null, int bufferSize = 10, CancellationToken cancellationToken = default ) => - SubscribeToAllAsync( - groupName, - PersistentSubscriptionListener.Handle(eventAppeared, subscriptionDropped), - new SubscribeToPersistentSubscriptionOptions { - BufferSize = bufferSize, - UserCredentials = userCredentials, - SerializationSettings = OperationSerializationSettings.Disabled - }, - cancellationToken - ); - - /// - /// Subscribes to a persistent subscription to $all. Messages must be manually acknowledged - /// - public Task SubscribeToAllAsync( - string groupName, - PersistentSubscriptionListener listener, - SubscribeToPersistentSubscriptionOptions options, - CancellationToken cancellationToken = default - ) => - SubscribeToStreamAsync( - SystemStreams.AllStream, - groupName, - listener, - options, - cancellationToken - ); + await SubscribeToStreamAsync( + SystemStreams.AllStream, + groupName, + eventAppeared, + subscriptionDropped, + userCredentials, + bufferSize, + cancellationToken + ) + .ConfigureAwait(false); /// /// Subscribes to a persistent subscription to $all. Messages must be manually acknowledged. @@ -321,76 +162,22 @@ public Task SubscribeToAllAsync( /// The optional . /// public PersistentSubscriptionResult SubscribeToAll( - string groupName, - int bufferSize, - UserCredentials? userCredentials = null, - CancellationToken cancellationToken = default + string groupName, int bufferSize = 10, + UserCredentials? userCredentials = null, CancellationToken cancellationToken = default ) => - SubscribeToStream( - SystemStreams.AllStream, - groupName, - new SubscribeToPersistentSubscriptionOptions { - BufferSize = bufferSize, - UserCredentials = userCredentials, - SerializationSettings = OperationSerializationSettings.Disabled - }, - cancellationToken - ); - - - /// - /// Subscribes to a persistent subscription to $all. Messages must be manually acknowledged. - /// - /// The name of the persistent subscription group. - /// The size of the buffer. - /// The optional user credentials to perform operation with. - /// The optional . - /// - public PersistentSubscriptionResult SubscribeToAll( - string groupName, - UserCredentials? userCredentials, - CancellationToken cancellationToken = default - ) => - SubscribeToStream( - SystemStreams.AllStream, - groupName, - new SubscribeToPersistentSubscriptionOptions { - UserCredentials = userCredentials, - SerializationSettings = OperationSerializationSettings.Disabled - }, - cancellationToken - ); - - /// - /// Subscribes to a persistent subscription to $all. Messages must be manually acknowledged. - /// - /// The name of the persistent subscription group. - /// Optional settings to configure subscription - /// The optional . - /// - public PersistentSubscriptionResult SubscribeToAll( - string groupName, - SubscribeToPersistentSubscriptionOptions options, - CancellationToken cancellationToken = default - ) => - SubscribeToStream( - SystemStreams.AllStream, - groupName, - options, - cancellationToken - ); + SubscribeToStream(SystemStreams.AllStream, groupName, bufferSize, userCredentials, cancellationToken); /// public class PersistentSubscriptionResult : IAsyncEnumerable, IAsyncDisposable, IDisposable { - const int MaxEventIdLength = 2000; + const int MaxEventIdLength = 2000; + + readonly ReadReq _request; + readonly Channel _channel; + readonly CancellationTokenSource _cts; + readonly CallOptions _callOptions; - readonly ReadReq _request; - readonly Channel _channel; - readonly CancellationTokenSource _cts; - readonly CallOptions _callOptions; - - AsyncDuplexStreamingCall? _call; - int _messagesEnumerated; + AsyncDuplexStreamingCall? _call; + int _messagesEnumerated; /// /// The server-generated unique identifier for the subscription. @@ -413,33 +200,30 @@ public class PersistentSubscriptionResult : IAsyncEnumerable, IAs public IAsyncEnumerable Messages { get { if (Interlocked.Exchange(ref _messagesEnumerated, 1) == 1) - throw new InvalidOperationException("Messages may only be enumerated once."); + throw new InvalidOperationException("Messages may only be enumerated once."); return GetMessages(); async IAsyncEnumerable GetMessages() { - try { - await foreach (var message in _channel.Reader.ReadAllAsync(_cts.Token)) { - if (message is PersistentSubscriptionMessage.SubscriptionConfirmation(var subscriptionId)) - SubscriptionId = subscriptionId; - - yield return message; - } - } finally { - _cts.Cancel(); - } + try { + await foreach (var message in _channel.Reader.ReadAllAsync(_cts.Token)) { + if (message is PersistentSubscriptionMessage.SubscriptionConfirmation(var subscriptionId)) + SubscriptionId = subscriptionId; + + yield return message; + } + } + finally { + _cts.Cancel(); + } } } } internal PersistentSubscriptionResult( - string streamName, - string groupName, + string streamName, string groupName, Func> selectChannelInfo, - ReadReq request, - KurrentClientSettings settings, - UserCredentials? userCredentials, - IMessageSerializer messageSerializer, + ReadReq request, KurrentClientSettings settings, UserCredentials? userCredentials, CancellationToken cancellationToken ) { StreamName = streamName; @@ -463,21 +247,20 @@ CancellationToken cancellationToken async Task PumpMessages() { try { - var channelInfo = await selectChannelInfo(_cts.Token).ConfigureAwait(false); - var client = new PersistentSubscriptionsClient(channelInfo.CallInvoker); + var channelInfo = await selectChannelInfo(_cts.Token).ConfigureAwait(false); + var client = new PersistentSubscriptionsClient(channelInfo.CallInvoker); _call = client.Read(_callOptions); await _call.RequestStream.WriteAsync(_request).ConfigureAwait(false); - await foreach (var response in _call.ResponseStream.ReadAllAsync(_cts.Token) - .ConfigureAwait(false)) { + await foreach (var response in _call.ResponseStream.ReadAllAsync(_cts.Token).ConfigureAwait(false)) { PersistentSubscriptionMessage subscriptionMessage = response.ContentCase switch { SubscriptionConfirmation => new PersistentSubscriptionMessage.SubscriptionConfirmation( response.SubscriptionConfirmation.SubscriptionId ), Event => new PersistentSubscriptionMessage.Event( - ConvertToResolvedEvent(response, messageSerializer), + ConvertToResolvedEvent(response), response.Event.CountCase switch { ReadResp.Types.ReadEvent.CountOneofCase.RetryCount => response.Event.RetryCount, _ => null @@ -509,18 +292,17 @@ async Task PumpMessages() { // The absence of this header leads to an RpcException with the status code 'Cancelled' and the message "No grpc-status found on response". // The switch statement below handles these specific exceptions and translates them into the appropriate // PersistentSubscriptionDroppedByServerException exception. - case RpcException { StatusCode: StatusCode.Unavailable } rex1 - when rex1.Status.Detail.Contains("WinHttpException: Error 12030"): + case RpcException { StatusCode: StatusCode.Unavailable } rex1 when rex1.Status.Detail.Contains("WinHttpException: Error 12030"): case RpcException { StatusCode: StatusCode.Cancelled } rex2 - when rex2.Status.Detail.Contains("No grpc-status found on response"): + when rex2.Status.Detail.Contains("No grpc-status found on response"): ex = new PersistentSubscriptionDroppedByServerException(StreamName, GroupName, ex); break; } #endif if (ex is PersistentSubscriptionNotFoundException) { await _channel.Writer - .WriteAsync(PersistentSubscriptionMessage.NotFound.Instance, cancellationToken) - .ConfigureAwait(false); + .WriteAsync(PersistentSubscriptionMessage.NotFound.Instance, cancellationToken) + .ConfigureAwait(false); _channel.Writer.TryComplete(); return; @@ -578,26 +360,19 @@ public Task Nack(PersistentSubscriptionNakEventAction action, string reason, par /// A reason given. /// The s to nak. There should not be more than 2000 to nak at a time. /// The number of resolvedEvents exceeded the limit of 2000. - 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, - IMessageSerializer messageSerializer - ) => - ResolvedEvent.From( - ConvertToEventRecord(response.Event.Event)!, - ConvertToEventRecord(response.Event.Link), - response.Event.PositionCase switch { - ReadResp.Types.ReadEvent.PositionOneofCase.CommitPosition => response.Event.CommitPosition, - _ => null - }, - messageSerializer - ); + 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( + ConvertToEventRecord(response.Event.Event)!, + ConvertToEventRecord(response.Event.Link), + response.Event.PositionCase switch { + ReadResp.Types.ReadEvent.PositionOneofCase.CommitPosition => response.Event.CommitPosition, + _ => null + } + ); - Task AckInternal(params Uuid[] eventIds) { + Task AckInternal(params Uuid[] eventIds) { if (eventIds.Length > MaxEventIdLength) { throw new ArgumentException( $"The number of eventIds exceeds the maximum length of {MaxEventIdLength}.", @@ -618,7 +393,7 @@ Task AckInternal(params Uuid[] eventIds) { ); } - Task NackInternal(Uuid[] eventIds, PersistentSubscriptionNakEventAction action, string reason) { + Task NackInternal(Uuid[] eventIds, PersistentSubscriptionNakEventAction action, string reason) { if (eventIds.Length > MaxEventIdLength) { throw new ArgumentException( $"The number of eventIds exceeds the maximum length of {MaxEventIdLength}.", @@ -647,7 +422,7 @@ Task NackInternal(Uuid[] eventIds, PersistentSubscriptionNakEventAction action, ); } - static EventRecord? ConvertToEventRecord(ReadResp.Types.ReadEvent.Types.RecordedEvent? e) => + static EventRecord? ConvertToEventRecord(ReadResp.Types.ReadEvent.Types.RecordedEvent? e) => e is null ? null : new EventRecord( @@ -690,94 +465,14 @@ public void Dispose() { } /// - public async IAsyncEnumerator GetAsyncEnumerator( - CancellationToken cancellationToken = default - ) { + public async IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default) { await foreach (var message in Messages.WithCancellation(cancellationToken)) { if (message is not PersistentSubscriptionMessage.Event(var resolvedEvent, _)) - continue; + continue; yield return resolvedEvent; } } } } - - public static class KurrentClientPersistentSubscriptionsExtensions { - /// - /// Subscribes to a persistent subscription. Messages must be manually acknowledged - /// - /// - /// - /// - public static Task SubscribeToStreamAsync( - this KurrentPersistentSubscriptionsClient kurrentClient, - string streamName, - string groupName, - PersistentSubscriptionListener listener, - CancellationToken cancellationToken = default - ) => - kurrentClient.SubscribeToStreamAsync( - streamName, - groupName, - listener, - new SubscribeToPersistentSubscriptionOptions(), - cancellationToken - ); - - /// - /// Subscribes to a persistent subscription. Messages must be manually acknowledged. - /// - /// - /// The name of the stream to read events from. - /// The name of the persistent subscription group. - /// The optional . - /// - public static KurrentPersistentSubscriptionsClient.PersistentSubscriptionResult SubscribeToStream( - this KurrentPersistentSubscriptionsClient kurrentClient, - string streamName, - string groupName, - CancellationToken cancellationToken = default - ) => - kurrentClient.SubscribeToStream( - streamName, - groupName, - new SubscribeToPersistentSubscriptionOptions(), - cancellationToken - ); - - /// - /// Subscribes to a persistent subscription to $all. Messages must be manually acknowledged - /// - public static Task SubscribeToAllAsync( - this KurrentPersistentSubscriptionsClient kurrentClient, - string groupName, - PersistentSubscriptionListener listener, - CancellationToken cancellationToken = default - ) => - kurrentClient.SubscribeToAllAsync( - groupName, - listener, - new SubscribeToPersistentSubscriptionOptions(), - cancellationToken - ); - - /// - /// Subscribes to a persistent subscription to $all. Messages must be manually acknowledged. - /// - /// - /// The name of the persistent subscription group. - /// The optional . - /// - public static KurrentPersistentSubscriptionsClient.PersistentSubscriptionResult SubscribeToAll( - this KurrentPersistentSubscriptionsClient kurrentClient, - string groupName, - CancellationToken cancellationToken = default - ) => - kurrentClient.SubscribeToAll( - groupName, - new SubscribeToPersistentSubscriptionOptions(), - cancellationToken - ); - } } diff --git a/src/Kurrent.Client/PersistentSubscriptions/KurrentPersistentSubscriptionsClient.cs b/src/Kurrent.Client/PersistentSubscriptions/KurrentPersistentSubscriptionsClient.cs index 0d251f59c..070f32698 100644 --- a/src/Kurrent.Client/PersistentSubscriptions/KurrentPersistentSubscriptionsClient.cs +++ b/src/Kurrent.Client/PersistentSubscriptions/KurrentPersistentSubscriptionsClient.cs @@ -1,7 +1,6 @@ using System.Text.Encodings.Web; using System.Threading.Channels; using Grpc.Core; -using Kurrent.Client.Core.Serialization; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; @@ -10,14 +9,13 @@ namespace EventStore.Client { /// The client used to manage persistent subscriptions in the KurrentDB. /// public sealed partial class KurrentPersistentSubscriptionsClient : KurrentClientBase { - static BoundedChannelOptions ReadBoundedChannelOptions = new (1) { + private static BoundedChannelOptions ReadBoundedChannelOptions = new (1) { SingleReader = true, SingleWriter = true, AllowSynchronousContinuations = true }; - readonly ILogger _log; - readonly IMessageSerializer _messageSerializer; + private readonly ILogger _log; /// /// Constructs a new . @@ -39,8 +37,6 @@ public KurrentPersistentSubscriptionsClient(KurrentClientSettings? settings) : b }) { _log = Settings.LoggerFactory?.CreateLogger() ?? new NullLogger(); - - _messageSerializer = MessageSerializer.From(settings?.Serialization); } private static string UrlEncode(string s) { diff --git a/src/Kurrent.Client/PersistentSubscriptions/PersistentSubscription.cs b/src/Kurrent.Client/PersistentSubscriptions/PersistentSubscription.cs index 0674cb9ac..637f54e30 100644 --- a/src/Kurrent.Client/PersistentSubscriptions/PersistentSubscription.cs +++ b/src/Kurrent.Client/PersistentSubscriptions/PersistentSubscription.cs @@ -7,9 +7,7 @@ namespace EventStore.Client { /// Represents a persistent subscription connection. /// public class PersistentSubscription : IDisposable { - private readonly KurrentPersistentSubscriptionsClient.PersistentSubscriptionResult - _persistentSubscriptionResult; - + private readonly KurrentPersistentSubscriptionsClient.PersistentSubscriptionResult _persistentSubscriptionResult; private readonly IAsyncEnumerator _enumerator; private readonly Func _eventAppeared; private readonly Action _subscriptionDropped; @@ -25,10 +23,9 @@ private readonly KurrentPersistentSubscriptionsClient.PersistentSubscriptionResu internal static async Task Confirm( KurrentPersistentSubscriptionsClient.PersistentSubscriptionResult persistentSubscriptionResult, - PersistentSubscriptionListener listener, - ILogger log, - CancellationToken cancellationToken = default - ) { + Func eventAppeared, + Action subscriptionDropped, + ILogger log, UserCredentials? userCredentials, CancellationToken cancellationToken = default) { var enumerator = persistentSubscriptionResult .Messages .GetAsyncEnumerator(cancellationToken); @@ -37,19 +34,11 @@ internal static async Task Confirm( return (result, enumerator.Current) switch { (true, PersistentSubscriptionMessage.SubscriptionConfirmation (var subscriptionId)) => - new PersistentSubscription( - persistentSubscriptionResult, - enumerator, - subscriptionId, - listener, - log, - cancellationToken - ), + new PersistentSubscription(persistentSubscriptionResult, enumerator, subscriptionId, eventAppeared, + subscriptionDropped, log, cancellationToken), (true, PersistentSubscriptionMessage.NotFound) => - throw new PersistentSubscriptionNotFoundException( - persistentSubscriptionResult.StreamName, - persistentSubscriptionResult.GroupName - ), + throw new PersistentSubscriptionNotFoundException(persistentSubscriptionResult.StreamName, + persistentSubscriptionResult.GroupName), _ => throw new InvalidOperationException("Subscription could not be confirmed.") }; } @@ -57,19 +46,17 @@ internal static async Task Confirm( // PersistentSubscription takes responsibility for disposing the call and the disposable private PersistentSubscription( KurrentPersistentSubscriptionsClient.PersistentSubscriptionResult persistentSubscriptionResult, - IAsyncEnumerator enumerator, - string subscriptionId, - PersistentSubscriptionListener listener, - ILogger log, - CancellationToken cancellationToken - ) { + IAsyncEnumerator enumerator, string subscriptionId, + Func eventAppeared, + Action subscriptionDropped, ILogger log, + CancellationToken cancellationToken) { _persistentSubscriptionResult = persistentSubscriptionResult; - _enumerator = enumerator; - SubscriptionId = subscriptionId; - _eventAppeared = listener.EventAppeared; - _subscriptionDropped = listener.SubscriptionDropped ?? delegate { }; - _log = log; - _cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + _enumerator = enumerator; + SubscriptionId = subscriptionId; + _eventAppeared = eventAppeared; + _subscriptionDropped = subscriptionDropped; + _log = log; + _cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); Task.Run(Subscribe, _cts.Token); } @@ -104,6 +91,7 @@ public Task Ack(params ResolvedEvent[] resolvedEvents) => public Task Ack(IEnumerable resolvedEvents) => Ack(resolvedEvents.Select(resolvedEvent => resolvedEvent.OriginalEvent.EventId)); + /// /// Acknowledge that a message has failed processing (this will tell the server it has not been processed). /// @@ -111,8 +99,7 @@ public Task Ack(IEnumerable resolvedEvents) => /// A reason given. /// The of the s to nak. There should not be more than 2000 to nak at a time. /// The number of eventIds exceeded the limit of 2000. - public Task Nack(PersistentSubscriptionNakEventAction action, string reason, params Uuid[] eventIds) => - NackInternal(eventIds, action, reason); + public Task Nack(PersistentSubscriptionNakEventAction action, string reason, params Uuid[] eventIds) => NackInternal(eventIds, action, reason); /// /// Acknowledge that a message has failed processing (this will tell the server it has not been processed). @@ -121,15 +108,10 @@ public Task Nack(PersistentSubscriptionNakEventAction action, string reason, par /// A reason given. /// The s to nak. There should not be more than 2000 to nak at a time. /// The number of resolvedEvents exceeded the limit of 2000. - public Task Nack( - PersistentSubscriptionNakEventAction action, string reason, - params ResolvedEvent[] resolvedEvents - ) => - Nack( - action, - reason, - Array.ConvertAll(resolvedEvents, resolvedEvent => resolvedEvent.OriginalEvent.EventId) - ); + public Task Nack(PersistentSubscriptionNakEventAction action, string reason, + params ResolvedEvent[] resolvedEvents) => + Nack(action, reason, + Array.ConvertAll(resolvedEvents, resolvedEvent => resolvedEvent.OriginalEvent.EventId)); /// public void Dispose() => SubscriptionDropped(SubscriptionDroppedReason.Disposed); @@ -139,8 +121,7 @@ private async Task Subscribe() { try { while (await _enumerator.MoveNextAsync(_cts.Token).ConfigureAwait(false)) { - if (_enumerator.Current is not - PersistentSubscriptionMessage.Event(var resolvedEvent, var retryCount)) { + if (_enumerator.Current is not PersistentSubscriptionMessage.Event(var resolvedEvent, var retryCount)) { continue; } @@ -148,54 +129,39 @@ private async Task Subscribe() { if (_subscriptionDroppedInvoked != 0) { return; } - - SubscriptionDropped( - SubscriptionDroppedReason.ServerError, + SubscriptionDropped(SubscriptionDroppedReason.ServerError, new PersistentSubscriptionNotFoundException( - _persistentSubscriptionResult.StreamName, - _persistentSubscriptionResult.GroupName - ) - ); - + _persistentSubscriptionResult.StreamName, _persistentSubscriptionResult.GroupName)); return; } - + _log.LogTrace( "Persistent Subscription {subscriptionId} received event {streamName}@{streamRevision} {position}", - SubscriptionId, - resolvedEvent.OriginalEvent.EventStreamId, - resolvedEvent.OriginalEvent.EventNumber, - resolvedEvent.OriginalEvent.Position - ); + SubscriptionId, resolvedEvent.OriginalEvent.EventStreamId, + resolvedEvent.OriginalEvent.EventNumber, resolvedEvent.OriginalEvent.Position); try { await _eventAppeared( this, resolvedEvent, retryCount, - _cts.Token - ).ConfigureAwait(false); + _cts.Token).ConfigureAwait(false); } catch (Exception ex) when (ex is ObjectDisposedException or OperationCanceledException) { if (_subscriptionDroppedInvoked != 0) { return; } - _log.LogWarning( - ex, + _log.LogWarning(ex, "Persistent Subscription {subscriptionId} was dropped because cancellation was requested by another caller.", - SubscriptionId - ); + SubscriptionId); SubscriptionDropped(SubscriptionDroppedReason.Disposed); return; } catch (Exception ex) { - _log.LogError( - ex, + _log.LogError(ex, "Persistent Subscription {subscriptionId} was dropped because the subscriber made an error.", - SubscriptionId - ); - + SubscriptionId); SubscriptionDropped(SubscriptionDroppedReason.SubscriberError, ex); return; @@ -203,21 +169,16 @@ await _eventAppeared( } } catch (Exception ex) { if (_subscriptionDroppedInvoked == 0) { - _log.LogError( - ex, + _log.LogError(ex, "Persistent Subscription {subscriptionId} was dropped because an error occurred on the server.", - SubscriptionId - ); - + SubscriptionId); SubscriptionDropped(SubscriptionDroppedReason.ServerError, ex); } } finally { if (_subscriptionDroppedInvoked == 0) { _log.LogError( "Persistent Subscription {subscriptionId} was unexpectedly terminated.", - SubscriptionId - ); - + SubscriptionId); SubscriptionDropped(SubscriptionDroppedReason.ServerError); } } diff --git a/src/Kurrent.Client/Streams/KurrentClient.Append.cs b/src/Kurrent.Client/Streams/KurrentClient.Append.cs index 593d2ab40..39d6f3066 100644 --- a/src/Kurrent.Client/Streams/KurrentClient.Append.cs +++ b/src/Kurrent.Client/Streams/KurrentClient.Append.cs @@ -6,7 +6,6 @@ using Grpc.Core; using Microsoft.Extensions.Logging; using EventStore.Client.Diagnostics; -using Kurrent.Client.Core.Serialization; using Kurrent.Diagnostics; using Kurrent.Diagnostics.Telemetry; using Kurrent.Diagnostics.Tracing; @@ -15,48 +14,6 @@ namespace EventStore.Client { public partial class KurrentClient { - /// - /// Appends events asynchronously to a stream. Messages are serialized using default or custom serialization configured through - /// - /// The name of the stream to append events to. - /// Messages to append to the stream. - /// Optional settings for the append operation, e.g. expected stream position for optimistic concurrency check - /// The optional . - /// - public Task AppendToStreamAsync( - string streamName, - IEnumerable messages, - AppendToStreamOptions options, - CancellationToken cancellationToken = default - ) { - var serializationContext = new MessageSerializationContext( - streamName, - Settings.Serialization.DefaultContentType - ); - - var eventsData = _messageSerializer.Serialize(messages, serializationContext); - - return options.ExpectedStreamRevision.HasValue - ? AppendToStreamAsync( - streamName, - options.ExpectedStreamRevision.Value, - eventsData, - options.ConfigureOperationOptions, - options.Deadline, - options.UserCredentials, - cancellationToken - ) - : AppendToStreamAsync( - streamName, - options.ExpectedStreamState ?? StreamState.Any, - eventsData, - options.ConfigureOperationOptions, - options.Deadline, - options.UserCredentials, - cancellationToken - ); - } - /// /// Appends events asynchronously to a stream. /// @@ -157,28 +114,16 @@ ValueTask AppendToStreamInternal( CancellationToken cancellationToken ) { var tags = new ActivityTagsCollection() - .WithRequiredTag( - TelemetryTags.Kurrent.Stream, - header.Options.StreamIdentifier.StreamName.ToStringUtf8() - ) + .WithRequiredTag(TelemetryTags.Kurrent.Stream, header.Options.StreamIdentifier.StreamName.ToStringUtf8()) .WithGrpcChannelServerTags(channelInfo) .WithClientSettingsServerTags(Settings) - .WithOptionalTag( - TelemetryTags.Database.User, - userCredentials?.Username ?? Settings.DefaultCredentials?.Username - ); + .WithOptionalTag(TelemetryTags.Database.User, userCredentials?.Username ?? Settings.DefaultCredentials?.Username); - return KurrentClientDiagnostics.ActivitySource.TraceClientOperation( - Operation, - TracingConstants.Operations.Append, - tags - ); + return KurrentClientDiagnostics.ActivitySource.TraceClientOperation(Operation, TracingConstants.Operations.Append, tags); async ValueTask Operation() { using var call = new StreamsClient(channelInfo.CallInvoker) - .Append( - KurrentCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken) - ); + .Append(KurrentCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); await call.RequestStream .WriteAsync(header) @@ -215,13 +160,11 @@ await call.RequestStream } IWriteResult HandleSuccessAppend(AppendResp response, AppendReq header) { - var currentRevision = response.Success.CurrentRevisionOptionCase - == AppendResp.Types.Success.CurrentRevisionOptionOneofCase.NoStream + var currentRevision = response.Success.CurrentRevisionOptionCase == AppendResp.Types.Success.CurrentRevisionOptionOneofCase.NoStream ? StreamRevision.None : new StreamRevision(response.Success.CurrentRevision); - var position = response.Success.PositionOptionCase - == AppendResp.Types.Success.PositionOptionOneofCase.Position + var position = response.Success.PositionOptionCase == AppendResp.Types.Success.PositionOptionOneofCase.Position ? new Position(response.Success.Position.CommitPosition, response.Success.Position.PreparePosition) : default; @@ -238,8 +181,7 @@ IWriteResult HandleSuccessAppend(AppendResp response, AppendReq header) { IWriteResult HandleWrongExpectedRevision( AppendResp response, AppendReq header, KurrentClientOperationOptions operationOptions ) { - var actualStreamRevision = response.WrongExpectedVersion.CurrentRevisionOptionCase - == CurrentRevisionOptionOneofCase.CurrentRevision + var actualStreamRevision = response.WrongExpectedVersion.CurrentRevisionOptionCase == CurrentRevisionOptionOneofCase.CurrentRevision ? new StreamRevision(response.WrongExpectedVersion.CurrentRevision) : StreamRevision.None; @@ -251,8 +193,7 @@ IWriteResult HandleWrongExpectedRevision( ); if (operationOptions.ThrowOnAppendFailure) { - if (response.WrongExpectedVersion.ExpectedRevisionOptionCase - == ExpectedRevisionOptionOneofCase.ExpectedRevision) { + if (response.WrongExpectedVersion.ExpectedRevisionOptionCase == ExpectedRevisionOptionOneofCase.ExpectedRevision) { throw new WrongExpectedVersionException( header.Options.StreamIdentifier!, new StreamRevision(response.WrongExpectedVersion.ExpectedRevision), @@ -274,8 +215,7 @@ IWriteResult HandleWrongExpectedRevision( ); } - var expectedRevision = response.WrongExpectedVersion.ExpectedRevisionOptionCase - == ExpectedRevisionOptionOneofCase.ExpectedRevision + var expectedRevision = response.WrongExpectedVersion.ExpectedRevisionOptionCase == ExpectedRevisionOptionOneofCase.ExpectedRevision ? new StreamRevision(response.WrongExpectedVersion.ExpectedRevision) : StreamRevision.None; @@ -287,7 +227,7 @@ IWriteResult HandleWrongExpectedRevision( } class StreamAppender : IDisposable { - readonly KurrentClientSettings _settings; + readonly KurrentClientSettings _settings; readonly CancellationToken _cancellationToken; readonly Action _onException; readonly Channel _channel; @@ -362,7 +302,8 @@ async ValueTask Operation() { try { foreach (var appendRequest in GetRequests(events, options, correlationId)) await _channel.Writer.WriteAsync(appendRequest, cancellationToken).ConfigureAwait(false); - } catch (ChannelClosedException ex) { + } + catch (ChannelClosedException ex) { // channel is closed, our tcs won't necessarily get completed, don't wait for it. throw ex.InnerException ?? ex; } @@ -392,7 +333,8 @@ async Task Duplex(ValueTask channelInfoTask) { _ = Task.Run(Receive, _cancellationToken); _isUsable.TrySetResult(true); - } catch (Exception ex) { + } + catch (Exception ex) { _isUsable.TrySetException(ex); _onException(ex); } @@ -402,8 +344,7 @@ async Task Duplex(ValueTask channelInfoTask) { async Task Send() { if (_call is null) return; - await foreach (var appendRequest in _channel.Reader.ReadAllAsync(_cancellationToken) - .ConfigureAwait(false)) + await foreach (var appendRequest in _channel.Reader.ReadAllAsync(_cancellationToken).ConfigureAwait(false)) await _call.RequestStream.WriteAsync(appendRequest).ConfigureAwait(false); await _call.RequestStream.CompleteAsync().ConfigureAwait(false); @@ -413,22 +354,20 @@ async Task Receive() { if (_call is null) return; try { - await foreach (var response in _call.ResponseStream.ReadAllAsync(_cancellationToken) - .ConfigureAwait(false)) { - if (!_pendingRequests.TryRemove( - Uuid.FromDto(response.CorrelationId), - out var writeResult - )) { + await foreach (var response in _call.ResponseStream.ReadAllAsync(_cancellationToken).ConfigureAwait(false)) { + if (!_pendingRequests.TryRemove(Uuid.FromDto(response.CorrelationId), out var writeResult)) { continue; // TODO: Log? } try { writeResult.TrySetResult(response.ToWriteResult()); - } catch (Exception ex) { + } + catch (Exception ex) { writeResult.TrySetException(ex); } } - } catch (Exception ex) { + } + catch (Exception ex) { // signal that no tcs added to _pendingRequests after this point will necessarily complete _channel.Writer.TryComplete(ex); @@ -441,9 +380,7 @@ out var writeResult } } - IEnumerable GetRequests( - IEnumerable events, BatchAppendReq.Types.Options options, Uuid correlationId - ) { + IEnumerable GetRequests(IEnumerable events, BatchAppendReq.Types.Options options, Uuid correlationId) { var batchSize = 0; var first = true; var correlationIdDto = correlationId.ToDto(); @@ -490,153 +427,4 @@ public void Dispose() { } } } - - public static class KurrentClientAppendToStreamExtensions { - /// - /// Appends events asynchronously to a stream. Messages are serialized using default or custom serialization configured through - /// - /// - /// The name of the stream to append events to. - /// Messages to append to the stream. - /// The optional . - /// - public static Task AppendToStreamAsync( - this KurrentClient client, - string streamName, - IEnumerable messages, - CancellationToken cancellationToken = default - ) - => client.AppendToStreamAsync( - streamName, - messages, - new AppendToStreamOptions(), - cancellationToken - ); - - /// - /// Appends events asynchronously to a stream. Messages are serialized using default or custom serialization configured through - /// - /// - /// The name of the stream to append events to. - /// Messages to append to the stream. - /// The optional . - /// - public static Task AppendToStreamAsync( - this KurrentClient client, - string streamName, - IEnumerable messages, - CancellationToken cancellationToken = default - ) - => client.AppendToStreamAsync( - streamName, - messages.Select(m => Message.From(m)), - new AppendToStreamOptions(), - cancellationToken - ); - - - /// - /// Appends events asynchronously to a stream. Messages are serialized using default or custom serialization configured through - /// - /// - /// The name of the stream to append events to. - /// The expected of the stream to append to. - /// Messages to append to the stream. - /// The optional . - /// - public static Task AppendToStreamAsync( - this KurrentClient client, - string streamName, - StreamRevision expectedRevision, - IEnumerable messages, - CancellationToken cancellationToken = default - ) - => client.AppendToStreamAsync( - streamName, - messages, - new AppendToStreamOptions { - ExpectedStreamRevision = expectedRevision - }, - cancellationToken - ); - - /// - /// Appends events asynchronously to a stream. Messages are serialized using default or custom serialization configured through - /// - /// - /// The name of the stream to append events to. - /// The expected of the stream to append to. - /// Messages to append to the stream. - /// The optional . - /// - public static Task AppendToStreamAsync( - this KurrentClient client, - string streamName, - StreamRevision expectedRevision, - IEnumerable messages, - CancellationToken cancellationToken = default - ) - => client.AppendToStreamAsync( - streamName, - messages.Select(m => Message.From(m)), - new AppendToStreamOptions{ ExpectedStreamRevision = expectedRevision}, - cancellationToken - ); - - /// - /// Appends events asynchronously to a stream. Messages are serialized using default or custom serialization configured through - /// - /// - /// The name of the stream to append events to. - /// Messages to append to the stream. - /// Optional settings for the append operation, e.g. expected stream position for optimistic concurrency check - /// The optional . - /// - public static Task AppendToStreamAsync( - this KurrentClient client, - string streamName, - IEnumerable messages, - AppendToStreamOptions options, - CancellationToken cancellationToken = default - ) - => client.AppendToStreamAsync( - streamName, - messages.Select(m => Message.From(m)), - options, - cancellationToken - ); - } - - // TODO: In the follow up PR merge StreamState and StreamRevision into a one thing - public class AppendToStreamOptions { - /// - /// The expected of the stream to append to. - /// - public StreamState? ExpectedStreamState { get; set; } - - /// - /// The expected of the stream to append to. - /// - public StreamRevision? ExpectedStreamRevision { get; set; } - - /// - /// An to configure the operation's options. - /// - public Action? ConfigureOperationOptions { get; set; } - - /// - /// Maximum time that the operation will be run - /// - public TimeSpan? Deadline { get; set; } - - /// - /// The for the operation. - /// - public UserCredentials? UserCredentials { get; set; } - - /// - /// Allows to customize or disable the automatic deserialization - /// - public OperationSerializationSettings? SerializationSettings { get; set; } - } } diff --git a/src/Kurrent.Client/Streams/KurrentClient.Read.cs b/src/Kurrent.Client/Streams/KurrentClient.Read.cs index 0aff8be49..0523d9516 100644 --- a/src/Kurrent.Client/Streams/KurrentClient.Read.cs +++ b/src/Kurrent.Client/Streams/KurrentClient.Read.cs @@ -1,59 +1,11 @@ using System.Threading.Channels; using EventStore.Client.Streams; using Grpc.Core; -using Kurrent.Client.Core.Serialization; using static EventStore.Client.Streams.ReadResp; using static EventStore.Client.Streams.ReadResp.ContentOneofCase; namespace EventStore.Client { public partial class KurrentClient { - /// - /// Asynchronously reads all events. By default, it reads all of them from the start. The options parameter allows you to fine-tune it to your needs. - /// - /// Optional settings like: max count, in which to read, the to start reading from, etc. - /// The optional . - /// - public ReadAllStreamResult ReadAllAsync( - ReadAllOptions options, - CancellationToken cancellationToken = default - ) { - if (options.MaxCount <= 0) - throw new ArgumentOutOfRangeException(nameof(options.MaxCount)); - - var readReq = new ReadReq { - Options = new() { - ReadDirection = options.Direction switch { - Direction.Backwards => ReadReq.Types.Options.Types.ReadDirection.Backwards, - Direction.Forwards => ReadReq.Types.Options.Types.ReadDirection.Forwards, - _ => throw InvalidOption(options.Direction) - }, - ResolveLinks = options.ResolveLinkTos, - All = new() { - Position = new() { - CommitPosition = options.Position.CommitPosition, - PreparePosition = options.Position.PreparePosition - } - }, - Count = (ulong)options.MaxCount, - UuidOption = new() { Structured = new() }, - ControlOption = new() { Compatibility = 1 }, - Filter = GetFilterOptions(options.Filter) - } - }; - - return new ReadAllStreamResult( - async _ => { - var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); - return channelInfo.CallInvoker; - }, - readReq, - Settings, - options, - _messageSerializer.With(Settings.Serialization, options.SerializationSettings), - cancellationToken - ); - } - /// /// Asynchronously reads all events. /// @@ -73,20 +25,16 @@ public ReadAllStreamResult ReadAllAsync( TimeSpan? deadline = null, UserCredentials? userCredentials = null, CancellationToken cancellationToken = default - ) => - ReadAllAsync( - new ReadAllOptions { - Direction = direction, - Position = position, - Filter = null, - MaxCount = maxCount, - ResolveLinkTos = resolveLinkTos, - Deadline = deadline, - UserCredentials = userCredentials, - SerializationSettings = OperationSerializationSettings.Disabled - }, - cancellationToken - ); + ) => ReadAllAsync( + direction, + position, + eventFilter: null, + maxCount, + resolveLinkTos, + deadline, + userCredentials, + cancellationToken + ); /// /// Asynchronously reads all events with filtering. @@ -113,17 +61,36 @@ public ReadAllStreamResult ReadAllAsync( if (maxCount <= 0) throw new ArgumentOutOfRangeException(nameof(maxCount)); - return ReadAllAsync( - new ReadAllOptions { - Direction = direction, - Position = position, - Filter = eventFilter, - MaxCount = maxCount, - ResolveLinkTos = resolveLinkTos, - Deadline = deadline, - UserCredentials = userCredentials, - SerializationSettings = OperationSerializationSettings.Disabled + var readReq = new ReadReq { + Options = new() { + ReadDirection = direction switch { + Direction.Backwards => ReadReq.Types.Options.Types.ReadDirection.Backwards, + Direction.Forwards => ReadReq.Types.Options.Types.ReadDirection.Forwards, + _ => throw InvalidOption(direction) + }, + ResolveLinks = resolveLinkTos, + All = new() { + Position = new() { + CommitPosition = position.CommitPosition, + PreparePosition = position.PreparePosition + } + }, + Count = (ulong)maxCount, + UuidOption = new() { Structured = new() }, + ControlOption = new() { Compatibility = 1 }, + Filter = GetFilterOptions(eventFilter) + } + }; + + return new ReadAllStreamResult( + async _ => { + var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); + return channelInfo.CallInvoker; }, + readReq, + Settings, + deadline, + userCredentials, cancellationToken ); } @@ -163,7 +130,8 @@ async IAsyncEnumerable GetMessages() { yield return message; } - } finally { + } + finally { _cts.Cancel(); } } @@ -171,17 +139,14 @@ async IAsyncEnumerable GetMessages() { } internal ReadAllStreamResult( - Func> selectCallInvoker, - ReadReq request, - KurrentClientSettings settings, - ReadAllOptions options, - IMessageSerializer messageSerializer, + Func> selectCallInvoker, ReadReq request, + KurrentClientSettings settings, TimeSpan? deadline, UserCredentials? userCredentials, CancellationToken cancellationToken ) { var callOptions = KurrentCallOptions.CreateStreaming( settings, - options.Deadline, - options.UserCredentials, + deadline, + userCredentials, cancellationToken ); @@ -192,7 +157,7 @@ CancellationToken cancellationToken if (request.Options.FilterOptionCase == ReadReq.Types.Options.FilterOptionOneofCase.None) request.Options.NoFilter = new(); - + _ = PumpMessages(); return; @@ -202,21 +167,14 @@ async Task PumpMessages() { var callInvoker = await selectCallInvoker(linkedCancellationToken).ConfigureAwait(false); var client = new Streams.Streams.StreamsClient(callInvoker); using var call = client.Read(request, callOptions); - await foreach (var response in call.ResponseStream.ReadAllAsync(linkedCancellationToken) .ConfigureAwait(false)) { await _channel.Writer.WriteAsync( response.ContentCase switch { - StreamNotFound => StreamMessage.NotFound.Instance, - Event => new StreamMessage.Event( - ConvertToResolvedEvent(response.Event, messageSerializer) - ), - FirstStreamPosition => new StreamMessage.FirstStreamPosition( - new StreamPosition(response.FirstStreamPosition) - ), - LastStreamPosition => new StreamMessage.LastStreamPosition( - new StreamPosition(response.LastStreamPosition) - ), + StreamNotFound => StreamMessage.NotFound.Instance, + Event => new StreamMessage.Event(ConvertToResolvedEvent(response.Event)), + FirstStreamPosition => new StreamMessage.FirstStreamPosition(new StreamPosition(response.FirstStreamPosition)), + LastStreamPosition => new StreamMessage.LastStreamPosition(new StreamPosition(response.LastStreamPosition)), LastAllStreamPosition => new StreamMessage.LastAllStreamPosition( new Position( response.LastAllStreamPosition.CommitPosition, @@ -230,7 +188,8 @@ await _channel.Writer.WriteAsync( } _channel.Writer.Complete(); - } catch (Exception ex) { + } + catch (Exception ex) { _channel.Writer.TryComplete(ex); } } @@ -241,15 +200,15 @@ public async IAsyncEnumerator GetAsyncEnumerator( CancellationToken cancellationToken = default ) { try { - await foreach (var message in - _channel.Reader.ReadAllAsync(cancellationToken).ConfigureAwait(false)) { + await foreach (var message in _channel.Reader.ReadAllAsync(cancellationToken).ConfigureAwait(false)) { if (message is not StreamMessage.Event e) { continue; } yield return e.ResolvedEvent; } - } finally { + } + finally { _cts.Cancel(); } } @@ -260,17 +219,27 @@ public async IAsyncEnumerator GetAsyncEnumerator( /// /// The result could also be inspected as a means to avoid handling exceptions as the would indicate whether or not the stream is readable./> /// + /// The in which to read. /// The name of the stream to read. - /// Optional settings like: max count, in which to read, the to start reading from, etc. + /// The to start reading from. + /// The number of events to read from the stream. + /// Whether to resolve LinkTo events automatically. + /// + /// The optional to perform operation with. /// The optional . /// public ReadStreamResult ReadStreamAsync( + Direction direction, string streamName, - ReadStreamOptions options, + StreamPosition revision, + long maxCount = long.MaxValue, + bool resolveLinkTos = false, + TimeSpan? deadline = null, + UserCredentials? userCredentials = null, CancellationToken cancellationToken = default ) { - if (options.MaxCount <= 0) - throw new ArgumentOutOfRangeException(nameof(options.MaxCount)); + if (maxCount <= 0) + throw new ArgumentOutOfRangeException(nameof(maxCount)); return new ReadStreamResult( async _ => { @@ -279,68 +248,25 @@ public ReadStreamResult ReadStreamAsync( }, new ReadReq { Options = new() { - ReadDirection = options.Direction switch { + ReadDirection = direction switch { Direction.Backwards => ReadReq.Types.Options.Types.ReadDirection.Backwards, Direction.Forwards => ReadReq.Types.Options.Types.ReadDirection.Forwards, - _ => throw InvalidOption(options.Direction) + _ => throw InvalidOption(direction) }, - ResolveLinks = options.ResolveLinkTos, + ResolveLinks = resolveLinkTos, Stream = ReadReq.Types.Options.Types.StreamOptions.FromStreamNameAndRevision( streamName, - options.StreamPosition + revision ), - Count = (ulong)options.MaxCount, + Count = (ulong)maxCount, UuidOption = new() { Structured = new() }, NoFilter = new(), ControlOption = new() { Compatibility = 1 } } }, Settings, - options.Deadline, - options.UserCredentials, - _messageSerializer.With(Settings.Serialization, options.SerializationSettings), - cancellationToken - ); - } - - /// - /// Asynchronously reads all the events from a stream. - /// - /// The result could also be inspected as a means to avoid handling exceptions as the would indicate whether or not the stream is readable./> - /// - /// The in which to read. - /// The name of the stream to read. - /// The to start reading from. - /// The number of events to read from the stream. - /// Whether to resolve LinkTo events automatically. - /// - /// The optional to perform operation with. - /// The optional . - /// - public ReadStreamResult ReadStreamAsync( - Direction direction, - string streamName, - StreamPosition revision, - long maxCount = long.MaxValue, - bool resolveLinkTos = false, - TimeSpan? deadline = null, - UserCredentials? userCredentials = null, - CancellationToken cancellationToken = default - ) { - if (maxCount <= 0) - throw new ArgumentOutOfRangeException(nameof(maxCount)); - - return ReadStreamAsync( - streamName, - new ReadStreamOptions { - Direction = direction, - StreamPosition = revision, - MaxCount = maxCount, - ResolveLinkTos = resolveLinkTos, - Deadline = deadline, - UserCredentials = userCredentials, - SerializationSettings = OperationSerializationSettings.Disabled - }, + deadline, + userCredentials, cancellationToken ); } @@ -382,8 +308,7 @@ async IAsyncEnumerable GetMessages() { } try { - await foreach (var message in _channel.Reader.ReadAllAsync(_cts.Token) - .ConfigureAwait(false)) { + await foreach (var message in _channel.Reader.ReadAllAsync(_cts.Token).ConfigureAwait(false)) { switch (message) { case StreamMessage.FirstStreamPosition(var streamPosition): FirstStreamPosition = streamPosition; @@ -399,7 +324,8 @@ async IAsyncEnumerable GetMessages() { yield return message; } - } finally { + } + finally { _cts.Cancel(); } } @@ -412,12 +338,8 @@ async IAsyncEnumerable GetMessages() { public Task ReadState { get; } internal ReadStreamResult( - Func> selectCallInvoker, - ReadReq request, - KurrentClientSettings settings, - TimeSpan? deadline, - UserCredentials? userCredentials, - IMessageSerializer messageSerializer, + Func> selectCallInvoker, ReadReq request, + KurrentClientSettings settings, TimeSpan? deadline, UserCredentials? userCredentials, CancellationToken cancellationToken ) { var callOptions = KurrentCallOptions.CreateStreaming( @@ -460,7 +382,8 @@ await _channel.Writer.WriteAsync(StreamMessage.Ok.Instance, linkedCancellationTo .ConfigureAwait(false); tcs.SetResult(Client.ReadState.Ok); - } else { + } + else { tcs.SetResult(Client.ReadState.StreamNotFound); } } @@ -468,9 +391,7 @@ await _channel.Writer.WriteAsync(StreamMessage.Ok.Instance, linkedCancellationTo await _channel.Writer.WriteAsync( response.ContentCase switch { StreamNotFound => StreamMessage.NotFound.Instance, - Event => new StreamMessage.Event( - ConvertToResolvedEvent(response.Event, messageSerializer) - ), + Event => new StreamMessage.Event(ConvertToResolvedEvent(response.Event)), ContentOneofCase.FirstStreamPosition => new StreamMessage.FirstStreamPosition( new StreamPosition(response.FirstStreamPosition) ), @@ -490,7 +411,8 @@ await _channel.Writer.WriteAsync( } _channel.Writer.Complete(); - } catch (Exception ex) { + } + catch (Exception ex) { tcs.TrySetException(ex); _channel.Writer.TryComplete(ex); } @@ -502,8 +424,7 @@ public async IAsyncEnumerator GetAsyncEnumerator( CancellationToken cancellationToken = default ) { try { - await foreach (var message in - _channel.Reader.ReadAllAsync(cancellationToken).ConfigureAwait(false)) { + await foreach (var message in _channel.Reader.ReadAllAsync(cancellationToken).ConfigureAwait(false)) { if (message is StreamMessage.NotFound) { throw new StreamNotFoundException(StreamName); } @@ -514,24 +435,21 @@ public async IAsyncEnumerator GetAsyncEnumerator( yield return e.ResolvedEvent; } - } finally { + } + finally { _cts.Cancel(); } } } - static ResolvedEvent ConvertToResolvedEvent( - Types.ReadEvent readEvent, - IMessageSerializer messageSerializer - ) => - ResolvedEvent.From( + static ResolvedEvent ConvertToResolvedEvent(ReadResp.Types.ReadEvent readEvent) => + new ResolvedEvent( ConvertToEventRecord(readEvent.Event)!, ConvertToEventRecord(readEvent.Link), readEvent.PositionCase switch { - Types.ReadEvent.PositionOneofCase.CommitPosition => readEvent.CommitPosition, - _ => null - }, - messageSerializer + ReadResp.Types.ReadEvent.PositionOneofCase.CommitPosition => readEvent.CommitPosition, + _ => null + } ); static EventRecord? ConvertToEventRecord(ReadResp.Types.ReadEvent.Types.RecordedEvent? e) => @@ -547,126 +465,4 @@ IMessageSerializer messageSerializer e.CustomMetadata.ToByteArray() ); } - - /// - /// Optional settings to customize reading all messages, for instance: max count, - /// in which to read, the to start reading from, etc. - /// - public class ReadAllOptions { - /// - /// The in which to read. - /// - public Direction Direction { get; set; } = Direction.Forwards; - - /// - /// The to start reading from. - /// - public Position Position { get; set; } = Position.Start; - - /// - /// The to apply. - /// - public IEventFilter? Filter { get; set; } - - /// - /// The number of events to read from the stream. - /// - public long MaxCount { get; set; } = long.MaxValue; - - /// - /// Whether to resolve LinkTo events automatically. - /// - public bool ResolveLinkTos { get; set; } - - /// - /// Maximum time that the operation will be run - /// - public TimeSpan? Deadline { get; set; } - - /// - /// The optional to perform operation with. - /// - public UserCredentials? UserCredentials { get; set; } - - /// - /// Allows to customize or disable the automatic deserialization - /// - public OperationSerializationSettings? SerializationSettings { get; set; } - } - - /// - /// Optional settings to customize reading stream messages, for instance: max count, - /// in which to read, the to start reading from, etc. - /// - public class ReadStreamOptions { - /// - /// The in which to read. - /// - public Direction Direction { get; set; } = Direction.Forwards; - - /// - /// The to start reading from. - /// - public StreamPosition StreamPosition { get; set; } = StreamPosition.Start; - - /// - /// The number of events to read from the stream. - /// - public long MaxCount { get; set; } = long.MaxValue; - - /// - /// Whether to resolve LinkTo events automatically. - /// - public bool ResolveLinkTos { get; set; } - - /// - /// Maximum time that the operation will be run - /// - public TimeSpan? Deadline { get; set; } - - /// - /// The optional to perform operation with. - /// - public UserCredentials? UserCredentials { get; set; } - - /// - /// Allows to customize or disable the automatic deserialization - /// - public OperationSerializationSettings? SerializationSettings { get; set; } - } - - public static class KurrentClientReadExtensions { - /// - /// Asynchronously reads all events. By default, it reads all of them from the start. The options parameter allows you to fine-tune it to your needs. - /// - /// - /// The optional . - /// - public static KurrentClient.ReadAllStreamResult ReadAllAsync( - this KurrentClient client, - CancellationToken cancellationToken = default - ) => - client.ReadAllAsync(new ReadAllOptions(), cancellationToken); - - /// - /// Asynchronously reads all the events from a stream. - /// - /// The result could also be inspected as a means to avoid handling exceptions as the would indicate whether or not the stream is readable./> - /// - /// - /// The name of the stream to read. - /// Optional settings like: max count, in which to read, the to start reading from, etc. - /// The optional . - /// - public static KurrentClient.ReadStreamResult ReadStreamAsync( - this KurrentClient client, - string streamName, - CancellationToken cancellationToken = default - ) => - client.ReadStreamAsync( - streamName, - new ReadStreamOptions(), - cancellationToken - ); - } } diff --git a/src/Kurrent.Client/Streams/KurrentClient.Subscriptions.cs b/src/Kurrent.Client/Streams/KurrentClient.Subscriptions.cs index fca4e5275..92adb172b 100644 --- a/src/Kurrent.Client/Streams/KurrentClient.Subscriptions.cs +++ b/src/Kurrent.Client/Streams/KurrentClient.Subscriptions.cs @@ -2,111 +2,10 @@ using EventStore.Client.Diagnostics; using EventStore.Client.Streams; using Grpc.Core; -using Kurrent.Client.Core.Serialization; + using static EventStore.Client.Streams.ReadResp.ContentOneofCase; namespace EventStore.Client { - /// - /// Subscribes to all events options. - /// - public class SubscribeToAllOptions { - /// - /// A (exclusive of) to start the subscription from. - /// - public FromAll Start { get; set; } = FromAll.Start; - - /// - /// Whether to resolve LinkTo events automatically. - /// - public bool ResolveLinkTos { get; set; } - - /// - /// The optional to apply. - /// - public SubscriptionFilterOptions? FilterOptions { get; set; } - - /// - /// The optional to apply. - /// - public IEventFilter Filter { set => FilterOptions = new SubscriptionFilterOptions(value); } - - /// - /// The optional user credentials to perform operation with. - /// - public UserCredentials? UserCredentials { get; set; } - - /// - /// Allows to customize or disable the automatic deserialization - /// - public OperationSerializationSettings? SerializationSettings { get; set; } - } - - /// - /// Subscribes to all events options. - /// - public class SubscribeToStreamOptions { - /// - /// A (exclusive of) to start the subscription from. - /// - public FromStream Start { get; set; } = FromStream.Start; - - /// - /// Whether to resolve LinkTo events automatically. - /// - public bool ResolveLinkTos { get; set; } - - /// - /// The optional user credentials to perform operation with. - /// - public UserCredentials? UserCredentials { get; set; } - - /// - /// Allows to customize or disable the automatic deserialization - /// - public OperationSerializationSettings? SerializationSettings { get; set; } - } - - public class SubscriptionListener { -#if NET48 - /// - /// A handler called when a new event is received over the subscription. - /// - public Func EventAppeared { get; set; } = null!; -#else - public required Func EventAppeared { get; set; } -#endif - /// - /// A handler called if the subscription is dropped. - /// - public Action? SubscriptionDropped { get; set; } - - /// - /// A handler called when a checkpoint is reached. - /// Set the checkpointInterval in subscription filter options to define how often this method is called. - /// - public Func? CheckpointReached { get; set; } - - /// - /// Returns the subscription listener with configured handlers - /// - /// Handler invoked when a new event is received over the subscription. - /// A handler invoked if the subscription is dropped. - /// A handler called when a checkpoint is reached. - /// Set the checkpointInterval in subscription filter options to define how often this method is called. - /// - /// - public static SubscriptionListener Handle( - Func eventAppeared, - Action? subscriptionDropped = null, - Func? checkpointReached = null - ) => - new SubscriptionListener { - EventAppeared = eventAppeared, - SubscriptionDropped = subscriptionDropped, - CheckpointReached = checkpointReached - }; - } - public partial class KurrentClient { /// /// Subscribes to all events. @@ -127,105 +26,52 @@ public Task SubscribeToAllAsync( SubscriptionFilterOptions? filterOptions = null, UserCredentials? userCredentials = null, CancellationToken cancellationToken = default - ) { - var listener = SubscriptionListener.Handle( - eventAppeared, - subscriptionDropped, - filterOptions?.CheckpointReached - ); - - var options = new SubscribeToAllOptions { - Start = start, - FilterOptions = filterOptions, - ResolveLinkTos = resolveLinkTos, - UserCredentials = userCredentials, - SerializationSettings = OperationSerializationSettings.Disabled, - }; - - return SubscribeToAllAsync(listener, options, cancellationToken); - } - - /// - /// Subscribes to all events. - /// - /// Listener configured to receive notifications about new events and subscription state change. - /// Optional settings like: Position from which to read, to apply, etc. - /// The optional . - /// - public Task SubscribeToAllAsync( - SubscriptionListener listener, - SubscribeToAllOptions options, - CancellationToken cancellationToken = default - ) { - listener.CheckpointReached ??= options.FilterOptions?.CheckpointReached; - - return StreamSubscription.Confirm( - SubscribeToAll(options, cancellationToken), - listener, - _log, - cancellationToken - ); - } + ) => StreamSubscription.Confirm( + SubscribeToAll(start, resolveLinkTos, filterOptions, userCredentials, cancellationToken), + eventAppeared, + subscriptionDropped, + _log, + filterOptions?.CheckpointReached, + cancellationToken: cancellationToken + ); /// /// Subscribes to all events. /// - /// Optional settings like: Position from which to read, to apply, etc. + /// A (exclusive of) to start the subscription from. + /// Whether to resolve LinkTo events automatically. + /// The optional to apply. + /// The optional user credentials to perform operation with. /// The optional . /// public StreamSubscriptionResult SubscribeToAll( - SubscribeToAllOptions options, + FromAll start, + bool resolveLinkTos = false, + SubscriptionFilterOptions? filterOptions = null, + UserCredentials? userCredentials = null, CancellationToken cancellationToken = default ) => new( async _ => await GetChannelInfo(cancellationToken).ConfigureAwait(false), new ReadReq { Options = new ReadReq.Types.Options { ReadDirection = ReadReq.Types.Options.Types.ReadDirection.Forwards, - ResolveLinks = options.ResolveLinkTos, - All = ReadReq.Types.Options.Types.AllOptions.FromSubscriptionPosition(options.Start), + ResolveLinks = resolveLinkTos, + All = ReadReq.Types.Options.Types.AllOptions.FromSubscriptionPosition(start), Subscription = new ReadReq.Types.Options.Types.SubscriptionOptions(), - Filter = GetFilterOptions(options.FilterOptions)!, + Filter = GetFilterOptions(filterOptions)!, UuidOption = new() { Structured = new() } } }, Settings, - options.UserCredentials, - _messageSerializer.With(Settings.Serialization, options.SerializationSettings), + userCredentials, cancellationToken ); - /// - /// Subscribes to all events. - /// - /// A (exclusive of) to start the subscription from. - /// Whether to resolve LinkTo events automatically. - /// The optional to apply. - /// The optional user credentials to perform operation with. - /// The optional . - /// - public StreamSubscriptionResult SubscribeToAll( - FromAll start, - bool resolveLinkTos = false, - SubscriptionFilterOptions? filterOptions = null, - UserCredentials? userCredentials = null, - CancellationToken cancellationToken = default - ) => - SubscribeToAll( - new SubscribeToAllOptions { - Start = start, - ResolveLinkTos = resolveLinkTos, - FilterOptions = filterOptions, - UserCredentials = userCredentials, - SerializationSettings = OperationSerializationSettings.Disabled - }, - cancellationToken - ); - /// /// Subscribes to a stream from a checkpoint. /// /// A (exclusive of) to start the subscription from. - /// The name of the stream to subscribe for notifications about new events. + /// The name of the stream to read events from. /// A Task invoked and awaited when a new event is received over the subscription. /// Whether to resolve LinkTo events automatically. /// An action invoked if the subscription is dropped. @@ -240,46 +86,19 @@ public Task SubscribeToStreamAsync( Action? subscriptionDropped = default, UserCredentials? userCredentials = null, CancellationToken cancellationToken = default - ) => - SubscribeToStreamAsync( - streamName, - SubscriptionListener.Handle(eventAppeared, subscriptionDropped), - new SubscribeToStreamOptions { - Start = start, - ResolveLinkTos = resolveLinkTos, - UserCredentials = userCredentials, - SerializationSettings = OperationSerializationSettings.Disabled - }, - cancellationToken - ); - - /// - /// Subscribes to a stream from a checkpoint. - /// - /// The name of the stream to subscribe for notifications about new events. - /// Listener configured to receive notifications about new events and subscription state change. - /// Optional settings like: Position from which to read, etc. - /// The optional . - /// - public Task SubscribeToStreamAsync( - string streamName, - SubscriptionListener listener, - SubscribeToStreamOptions options, - CancellationToken cancellationToken = default - ) { - return StreamSubscription.Confirm( - SubscribeToStream(streamName, options, cancellationToken), - listener, - _log, - cancellationToken - ); - } + ) => StreamSubscription.Confirm( + SubscribeToStream(streamName, start, resolveLinkTos, userCredentials, cancellationToken), + eventAppeared, + subscriptionDropped, + _log, + cancellationToken: cancellationToken + ); /// /// Subscribes to a stream from a checkpoint. /// /// A (exclusive of) to start the subscription from. - /// The name of the stream to subscribe for notifications about new events. + /// The name of the stream to read events from. /// Whether to resolve LinkTo events automatically. /// The optional user credentials to perform operation with. /// The optional . @@ -290,46 +109,19 @@ public StreamSubscriptionResult SubscribeToStream( bool resolveLinkTos = false, UserCredentials? userCredentials = null, CancellationToken cancellationToken = default - ) => - SubscribeToStream( - streamName, - new SubscribeToStreamOptions { - Start = start, - ResolveLinkTos = resolveLinkTos, - UserCredentials = userCredentials, - SerializationSettings = OperationSerializationSettings.Disabled - }, - cancellationToken - ); - - /// - /// Subscribes to a stream from a checkpoint. - /// - /// The name of the stream to subscribe for notifications about new events. - /// Optional settings like: Position from which to read, etc. - /// The optional . - /// - public StreamSubscriptionResult SubscribeToStream( - string streamName, - SubscribeToStreamOptions options, - CancellationToken cancellationToken = default ) => new( async _ => await GetChannelInfo(cancellationToken).ConfigureAwait(false), new ReadReq { Options = new ReadReq.Types.Options { ReadDirection = ReadReq.Types.Options.Types.ReadDirection.Forwards, - ResolveLinks = options.ResolveLinkTos, - Stream = ReadReq.Types.Options.Types.StreamOptions.FromSubscriptionPosition( - streamName, - options.Start - ), + ResolveLinks = resolveLinkTos, + Stream = ReadReq.Types.Options.Types.StreamOptions.FromSubscriptionPosition(streamName, start), Subscription = new ReadReq.Types.Options.Types.SubscriptionOptions(), - UuidOption = new() { Structured = new() } + UuidOption = new() { Structured = new() } } }, Settings, - options.UserCredentials, - _messageSerializer.With(Settings.Serialization, options.SerializationSettings), + userCredentials, cancellationToken ); @@ -341,7 +133,7 @@ public class StreamSubscriptionResult : IAsyncEnumerable, IAsyncD private readonly Channel _channel; private readonly CancellationTokenSource _cts; private readonly CallOptions _callOptions; - private readonly KurrentClientSettings _settings; + private readonly KurrentClientSettings _settings; private AsyncServerStreamingCall? _call; private int _messagesEnumerated; @@ -357,40 +149,38 @@ public class StreamSubscriptionResult : IAsyncEnumerable, IAsyncD public IAsyncEnumerable Messages { get { if (Interlocked.Exchange(ref _messagesEnumerated, 1) == 1) - throw new InvalidOperationException("Messages may only be enumerated once."); + throw new InvalidOperationException("Messages may only be enumerated once."); return GetMessages(); async IAsyncEnumerable GetMessages() { try { await foreach (var message in _channel.Reader.ReadAllAsync(_cts.Token)) { - if (message is StreamMessage.SubscriptionConfirmation(var subscriptionId)) - SubscriptionId = subscriptionId; + if (message is StreamMessage.SubscriptionConfirmation(var subscriptionId)) + SubscriptionId = subscriptionId; yield return message; } - } finally { + } + finally { #if NET8_0_OR_GREATER - await _cts.CancelAsync().ConfigureAwait(false); + await _cts.CancelAsync().ConfigureAwait(false); #else - _cts.Cancel(); + _cts.Cancel(); #endif - } - } + } + } } } internal StreamSubscriptionResult( Func> selectChannelInfo, - ReadReq request, - KurrentClientSettings settings, - UserCredentials? userCredentials, - IMessageSerializer messageSerializer, + ReadReq request, KurrentClientSettings settings, UserCredentials? userCredentials, CancellationToken cancellationToken ) { _request = request; _settings = settings; - + _callOptions = KurrentCallOptions.CreateStreaming( settings, userCredentials: userCredentials, @@ -414,52 +204,43 @@ async Task PumpMessages() { var channelInfo = await selectChannelInfo(_cts.Token).ConfigureAwait(false); var client = new Streams.Streams.StreamsClient(channelInfo.CallInvoker); _call = client.Read(_request, _callOptions); - await foreach (var response in _call.ResponseStream.ReadAllAsync(_cts.Token) - .ConfigureAwait(false)) { - StreamMessage subscriptionMessage = - response.ContentCase switch { - Confirmation => new StreamMessage.SubscriptionConfirmation( - response.Confirmation.SubscriptionId - ), - Event => new StreamMessage.Event( - ConvertToResolvedEvent(response.Event, messageSerializer) - ), - FirstStreamPosition => new StreamMessage.FirstStreamPosition( - new StreamPosition(response.FirstStreamPosition) - ), - LastStreamPosition => new StreamMessage.LastStreamPosition( - new StreamPosition(response.LastStreamPosition) - ), - LastAllStreamPosition => new StreamMessage.LastAllStreamPosition( - new Position( - response.LastAllStreamPosition.CommitPosition, - response.LastAllStreamPosition.PreparePosition - ) - ), - Checkpoint => new StreamMessage.AllStreamCheckpointReached( - new Position( - response.Checkpoint.CommitPosition, - response.Checkpoint.PreparePosition - ) - ), - CaughtUp => StreamMessage.CaughtUp.Instance, - FellBehind => StreamMessage.FellBehind.Instance, - _ => StreamMessage.Unknown.Instance - }; - - if (subscriptionMessage is StreamMessage.Event evt) - KurrentClientDiagnostics.ActivitySource.TraceSubscriptionEvent( - SubscriptionId, - evt.ResolvedEvent, - channelInfo, - _settings, - userCredentials - ); - - await _channel.Writer - .WriteAsync(subscriptionMessage, _cts.Token) - .ConfigureAwait(false); - } + await foreach (var response in _call.ResponseStream.ReadAllAsync(_cts.Token).ConfigureAwait(false)) { + StreamMessage subscriptionMessage = + response.ContentCase switch { + Confirmation => new StreamMessage.SubscriptionConfirmation(response.Confirmation.SubscriptionId), + Event => new StreamMessage.Event(ConvertToResolvedEvent(response.Event)), + FirstStreamPosition => new StreamMessage.FirstStreamPosition(new StreamPosition(response.FirstStreamPosition)), + LastStreamPosition => new StreamMessage.LastStreamPosition(new StreamPosition(response.LastStreamPosition)), + LastAllStreamPosition => new StreamMessage.LastAllStreamPosition( + new Position( + response.LastAllStreamPosition.CommitPosition, + response.LastAllStreamPosition.PreparePosition + ) + ), + Checkpoint => new StreamMessage.AllStreamCheckpointReached( + new Position( + response.Checkpoint.CommitPosition, + response.Checkpoint.PreparePosition + ) + ), + CaughtUp => StreamMessage.CaughtUp.Instance, + FellBehind => StreamMessage.FellBehind.Instance, + _ => StreamMessage.Unknown.Instance + }; + + if (subscriptionMessage is StreamMessage.Event evt) + KurrentClientDiagnostics.ActivitySource.TraceSubscriptionEvent( + SubscriptionId, + evt.ResolvedEvent, + channelInfo, + _settings, + userCredentials + ); + + await _channel.Writer + .WriteAsync(subscriptionMessage, _cts.Token) + .ConfigureAwait(false); + } _channel.Writer.Complete(); } catch (Exception ex) { @@ -470,7 +251,7 @@ await _channel.Writer /// public async ValueTask DisposeAsync() { - //TODO SS: Check if `CastAndDispose` is still relevant + //TODO SS: Check if `CastAndDispose` is still relevant await CastAndDispose(_cts).ConfigureAwait(false); await CastAndDispose(_call).ConfigureAwait(false); @@ -499,172 +280,23 @@ public void Dispose() { } /// - public async IAsyncEnumerator GetAsyncEnumerator( - CancellationToken cancellationToken = default - ) { + public async IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default) { try { - await foreach (var message in - _channel.Reader.ReadAllAsync(cancellationToken).ConfigureAwait(false)) { + await foreach (var message in _channel.Reader.ReadAllAsync(cancellationToken).ConfigureAwait(false)) { if (message is not StreamMessage.Event e) - continue; + continue; yield return e.ResolvedEvent; } - } finally { + } + finally { #if NET8_0_OR_GREATER - await _cts.CancelAsync().ConfigureAwait(false); + await _cts.CancelAsync().ConfigureAwait(false); #else - _cts.Cancel(); + _cts.Cancel(); #endif } } } } - - public static class KurrentClientSubscribeToAllExtensions { - /// - /// Subscribes to all events. - /// - /// - /// Listener configured to receive notifications about new events and subscription state change. - /// The optional . - /// - public static Task SubscribeToAllAsync( - this KurrentClient kurrentClient, - SubscriptionListener listener, - CancellationToken cancellationToken = default - ) => - kurrentClient.SubscribeToAllAsync(listener, new SubscribeToAllOptions(), cancellationToken); - - /// - /// Subscribes to all events. - /// - /// - /// - /// The optional . - /// - public static Task SubscribeToAllAsync( - this KurrentClient kurrentClient, - Func eventAppeared, - CancellationToken cancellationToken = default - ) => - kurrentClient.SubscribeToAllAsync( - eventAppeared, - new SubscribeToAllOptions(), - cancellationToken - ); - - /// - /// Subscribes to all events. - /// - /// - /// Handler invoked when a new event is received over the subscription. - /// Optional settings like: Position from which to read, to apply, etc. - /// The optional . - /// - public static Task SubscribeToAllAsync( - this KurrentClient kurrentClient, - Func eventAppeared, - SubscribeToAllOptions options, - CancellationToken cancellationToken = default - ) => - kurrentClient.SubscribeToAllAsync( - SubscriptionListener.Handle(eventAppeared), - options, - cancellationToken - ); - - /// - /// Subscribes to all events. - /// - /// - /// The optional . - /// - public static KurrentClient.StreamSubscriptionResult SubscribeToAll( - this KurrentClient kurrentClient, - CancellationToken cancellationToken = default - ) => - kurrentClient.SubscribeToAll(new SubscribeToAllOptions(), cancellationToken); - } - - public static class KurrentClientSubscribeToStreamExtensions { - /// - /// Subscribes to messages from a specific stream - /// - /// - /// The name of the stream to subscribe for notifications about new events. - /// Listener configured to receive notifications about new events and subscription state change. - /// The optional . - /// - public static Task SubscribeToStreamAsync( - this KurrentClient kurrentClient, - string streamName, - SubscriptionListener listener, - CancellationToken cancellationToken = default - ) => - kurrentClient.SubscribeToStreamAsync( - streamName, - listener, - new SubscribeToStreamOptions(), - cancellationToken - ); - - /// - /// Subscribes to messages from a specific stream - /// - /// - /// The name of the stream to subscribe for notifications about new events. - /// - /// The optional . - /// - public static Task SubscribeToStreamAsync( - this KurrentClient kurrentClient, - string streamName, - Func eventAppeared, - CancellationToken cancellationToken = default - ) => - kurrentClient.SubscribeToStreamAsync( - streamName, - eventAppeared, - new SubscribeToStreamOptions(), - cancellationToken - ); - - /// - /// Subscribes to messages from a specific stream - /// - /// - /// The name of the stream to subscribe for notifications about new events. - /// Handler invoked when a new event is received over the subscription. - /// Optional settings like: Position from which to read, to apply, etc. - /// The optional . - /// - public static Task SubscribeToStreamAsync( - this KurrentClient kurrentClient, - string streamName, - Func eventAppeared, - SubscribeToStreamOptions options, - CancellationToken cancellationToken = default - ) => - kurrentClient.SubscribeToStreamAsync( - streamName, - SubscriptionListener.Handle(eventAppeared), - options, - cancellationToken - ); - - /// - /// Subscribes to messages from a specific stream - /// - /// - /// The name of the stream to subscribe for notifications about new events. - /// The optional . - /// - public static KurrentClient.StreamSubscriptionResult SubscribeToStream( - this KurrentClient kurrentClient, - string streamName, - CancellationToken cancellationToken = default - ) => - kurrentClient.SubscribeToStream(streamName, new SubscribeToStreamOptions(), cancellationToken); - } } diff --git a/src/Kurrent.Client/Streams/KurrentClient.cs b/src/Kurrent.Client/Streams/KurrentClient.cs index 524500c06..3dccf53ee 100644 --- a/src/Kurrent.Client/Streams/KurrentClient.cs +++ b/src/Kurrent.Client/Streams/KurrentClient.cs @@ -1,7 +1,6 @@ using System.Text.Json; using System.Threading.Channels; using Grpc.Core; -using Kurrent.Client.Core.Serialization; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; @@ -24,11 +23,10 @@ public sealed partial class KurrentClient : KurrentClientBase { AllowSynchronousContinuations = true }; - readonly ILogger _log; - Lazy _batchAppenderLazy; - StreamAppender BatchAppender => _batchAppenderLazy.Value; - readonly CancellationTokenSource _disposedTokenSource; - readonly IMessageSerializer _messageSerializer; + readonly ILogger _log; + Lazy _batchAppenderLazy; + StreamAppender BatchAppender => _batchAppenderLazy.Value; + readonly CancellationTokenSource _disposedTokenSource; static readonly Dictionary> ExceptionMap = new() { [Constants.Exceptions.InvalidTransaction] = ex => new InvalidTransactionException(ex.Message, ex), @@ -68,11 +66,9 @@ public KurrentClient(IOptions options) : this(options.Val /// /// public KurrentClient(KurrentClientSettings? settings = null) : base(settings, ExceptionMap) { - _log = Settings.LoggerFactory?.CreateLogger() ?? new NullLogger(); + _log = Settings.LoggerFactory?.CreateLogger() ?? new NullLogger(); _disposedTokenSource = new CancellationTokenSource(); - _batchAppenderLazy = new Lazy(CreateStreamAppender); - - _messageSerializer = MessageSerializer.From(settings?.Serialization); + _batchAppenderLazy = new Lazy(CreateStreamAppender); } void SwapStreamAppender(Exception ex) => diff --git a/src/Kurrent.Client/Streams/StreamSubscription.cs b/src/Kurrent.Client/Streams/StreamSubscription.cs index 0b8f2bc9d..e7271b364 100644 --- a/src/Kurrent.Client/Streams/StreamSubscription.cs +++ b/src/Kurrent.Client/Streams/StreamSubscription.cs @@ -22,8 +22,10 @@ public class StreamSubscription : IDisposable { internal static async Task Confirm( KurrentClient.StreamSubscriptionResult subscription, - SubscriptionListener subscriptionListener, + Func eventAppeared, + Action? subscriptionDropped, ILogger log, + Func? checkpointReached = null, CancellationToken cancellationToken = default ) { var messages = subscription.Messages; @@ -38,26 +40,29 @@ enumerator.Current is not StreamMessage.SubscriptionConfirmation(var subscriptio subscription, enumerator, subscriptionId, - subscriptionListener, + eventAppeared, + subscriptionDropped, log, + checkpointReached, cancellationToken ); } private StreamSubscription( KurrentClient.StreamSubscriptionResult subscription, - IAsyncEnumerator messages, - string subscriptionId, - SubscriptionListener subscriptionListener, + IAsyncEnumerator messages, string subscriptionId, + Func eventAppeared, + Action? subscriptionDropped, ILogger log, + Func? checkpointReached, CancellationToken cancellationToken = default ) { _cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); _subscription = subscription; _messages = messages; - _eventAppeared = subscriptionListener.EventAppeared; - _checkpointReached = subscriptionListener.CheckpointReached ?? ((_, _, _) => Task.CompletedTask); - _subscriptionDropped = subscriptionListener.SubscriptionDropped; + _eventAppeared = eventAppeared; + _checkpointReached = checkpointReached ?? ((_, _, _) => Task.CompletedTask); + _subscriptionDropped = subscriptionDropped; _log = log; _subscriptionDroppedInvoked = 0; SubscriptionId = subscriptionId; diff --git a/test/Kurrent.Client.Tests.Common/Fixtures/KurrentPermanentFixture.Helpers.cs b/test/Kurrent.Client.Tests.Common/Fixtures/KurrentPermanentFixture.Helpers.cs index 50c50a599..7eb5d9749 100644 --- a/test/Kurrent.Client.Tests.Common/Fixtures/KurrentPermanentFixture.Helpers.cs +++ b/test/Kurrent.Client.Tests.Common/Fixtures/KurrentPermanentFixture.Helpers.cs @@ -1,7 +1,6 @@ using System.Runtime.CompilerServices; using System.Text; using EventStore.Client; -using Kurrent.Client.Core.Serialization; namespace Kurrent.Client.Tests; @@ -49,11 +48,6 @@ public IEnumerable CreateTestEvents( Enumerable.Range(0, count) .Select(index => CreateTestEvent(index, type ?? TestEventType, metadata, contentType)); - - public IEnumerable CreateTestMessages(int count = 1, object? metadata = null) => - Enumerable.Range(0, count) - .Select(index => CreateTestMessage(index, metadata)); - public EventData CreateTestEvent( string? type = null, ReadOnlyMemory? metadata = null, string? contentType = null ) => @@ -69,13 +63,6 @@ public IEnumerable CreateTestEventsThatThrowsException() { protected static EventData CreateTestEvent(int index) => CreateTestEvent(index, TestEventType); - protected static Message CreateTestMessage(int index, object? metadata = null) => - Message.From( - new DummyEvent(index), - metadata, - Uuid.NewUuid() - ); - protected static EventData CreateTestEvent( int index, string type, ReadOnlyMemory? metadata = null, string? contentType = null ) => @@ -117,6 +104,4 @@ public async Task RestartService(TimeSpan delay) { await Streams.WarmUp(); Log.Information("Service restarted."); } - - public record DummyEvent(int X); } diff --git a/test/Kurrent.Client.Tests.Common/Fixtures/KurrentPermanentFixture.cs b/test/Kurrent.Client.Tests.Common/Fixtures/KurrentPermanentFixture.cs index 9aa9294ef..0b657b61f 100644 --- a/test/Kurrent.Client.Tests.Common/Fixtures/KurrentPermanentFixture.cs +++ b/test/Kurrent.Client.Tests.Common/Fixtures/KurrentPermanentFixture.cs @@ -70,8 +70,7 @@ protected KurrentPermanentFixture(ConfigureFixture configure) { OperationOptions = Options.ClientSettings.OperationOptions, ConnectivitySettings = Options.ClientSettings.ConnectivitySettings, DefaultCredentials = Options.ClientSettings.DefaultCredentials, - DefaultDeadline = Options.ClientSettings.DefaultDeadline, - Serialization = Options.ClientSettings.Serialization + DefaultDeadline = Options.ClientSettings.DefaultDeadline }; InterlockedBoolean WarmUpCompleted { get; } = new InterlockedBoolean(); diff --git a/test/Kurrent.Client.Tests.ExternalAssembly/ExternalEvents.cs b/test/Kurrent.Client.Tests.ExternalAssembly/ExternalEvents.cs deleted file mode 100644 index eb0d95943..000000000 --- a/test/Kurrent.Client.Tests.ExternalAssembly/ExternalEvents.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Kurrent.Client.Tests.ExternalAssembly; - -/// -/// External event class used for testing loaded assembly resolution -/// This assembly will be explicitly loaded during tests -/// -public class ExternalEvent { - public string Id { get; set; } = null!; - public string Name { get; set; } = null!; -} diff --git a/test/Kurrent.Client.Tests.ExternalAssembly/Kurrent.Client.Tests.ExternalAssembly.csproj b/test/Kurrent.Client.Tests.ExternalAssembly/Kurrent.Client.Tests.ExternalAssembly.csproj deleted file mode 100644 index 6078c004b..000000000 --- a/test/Kurrent.Client.Tests.ExternalAssembly/Kurrent.Client.Tests.ExternalAssembly.csproj +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/test/Kurrent.Client.Tests.NeverLoadedAssembly/Kurrent.Client.Tests.NeverLoadedAssembly.csproj b/test/Kurrent.Client.Tests.NeverLoadedAssembly/Kurrent.Client.Tests.NeverLoadedAssembly.csproj deleted file mode 100644 index 6078c004b..000000000 --- a/test/Kurrent.Client.Tests.NeverLoadedAssembly/Kurrent.Client.Tests.NeverLoadedAssembly.csproj +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/test/Kurrent.Client.Tests.NeverLoadedAssembly/NotLoadedExternalEvent.cs b/test/Kurrent.Client.Tests.NeverLoadedAssembly/NotLoadedExternalEvent.cs deleted file mode 100644 index 94bb5f4a2..000000000 --- a/test/Kurrent.Client.Tests.NeverLoadedAssembly/NotLoadedExternalEvent.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Kurrent.Client.Tests.NeverLoadedAssembly; - -/// -/// External event class used for testing unloaded assembly resolution -/// This event should never be referenced directly by the test project -/// -public record NotLoadedExternalEvent { - public string Id { get; set; } = null!; - public string Name { get; set; } = null!; -} diff --git a/test/Kurrent.Client.Tests/Core/Serialization/ContentTypeExtensionsTests.cs b/test/Kurrent.Client.Tests/Core/Serialization/ContentTypeExtensionsTests.cs deleted file mode 100644 index d2dd02889..000000000 --- a/test/Kurrent.Client.Tests/Core/Serialization/ContentTypeExtensionsTests.cs +++ /dev/null @@ -1,69 +0,0 @@ -using EventStore.Client; -using Kurrent.Client.Core.Serialization; - -namespace Kurrent.Client.Tests.Core.Serialization; - -using static Constants.Metadata.ContentTypes; - -public class ContentTypeExtensionsTests { - [Fact] - public void FromMessageContentType_WithApplicationJson_ReturnsJsonContentType() { - // Given - // When - var result = ContentTypeExtensions.FromMessageContentType(ApplicationJson); - - // Then - Assert.Equal(ContentType.Json, result); - } - - [Fact] - public void FromMessageContentType_WithAnyOtherContentType_ReturnsBytesContentType() { - // Given - // When - var result = ContentTypeExtensions.FromMessageContentType(ApplicationOctetStream); - - // Then - Assert.Equal(ContentType.Bytes, result); - } - - [Fact] - public void FromMessageContentType_WithRandomString_ReturnsBytesContentType() { - // Given - const string contentType = "some-random-content-type"; - - // When - var result = ContentTypeExtensions.FromMessageContentType(contentType); - - // Then - Assert.Equal(ContentType.Bytes, result); - } - - [Fact] - public void ToMessageContentType_WithJsonContentType_ReturnsApplicationJson() { - // Given - // When - var result = ContentType.Json.ToMessageContentType(); - - // Then - Assert.Equal(ApplicationJson, result); - } - - [Fact] - public void ToMessageContentType_WithBytesContentType_ReturnsApplicationOctetStream() { - // Given - // When - var result = ContentType.Bytes.ToMessageContentType(); - - // Then - Assert.Equal(ApplicationOctetStream, result); - } - - [Fact] - public void ToMessageContentType_WithInvalidContentType_ThrowsArgumentOutOfRangeException() { - // Given - var contentType = (ContentType)999; // Invalid content type - - // When/Then - Assert.Throws(() => contentType.ToMessageContentType()); - } -} diff --git a/test/Kurrent.Client.Tests/Core/Serialization/MessageSerializationContextTests.cs b/test/Kurrent.Client.Tests/Core/Serialization/MessageSerializationContextTests.cs deleted file mode 100644 index 295dd1e3e..000000000 --- a/test/Kurrent.Client.Tests/Core/Serialization/MessageSerializationContextTests.cs +++ /dev/null @@ -1,45 +0,0 @@ -using Kurrent.Client.Core.Serialization; - -namespace Kurrent.Client.Tests.Core.Serialization; - -public class MessageSerializationContextTests -{ - [Fact] - public void CategoryName_ExtractsFromStreamName() - { - // Arrange - var context = new MessageSerializationContext("user-123", ContentType.Json); - - // Act - var categoryName = context.CategoryName; - - // Assert - Assert.Equal("user", categoryName); - } - - [Fact] - public void CategoryName_ExtractsFromStreamNameWithMoreThanOneDash() - { - // Arrange - var context = new MessageSerializationContext("user-some-123", ContentType.Json); - - // Act - var categoryName = context.CategoryName; - - // Assert - Assert.Equal("user", categoryName); - } - - [Fact] - public void CategoryName_ReturnsTheWholeStreamName() - { - // Arrange - var context = new MessageSerializationContext("user123", ContentType.Json); - - // Act - var categoryName = context.CategoryName; - - // Assert - Assert.Equal("user123", categoryName); - } -} diff --git a/test/Kurrent.Client.Tests/Core/Serialization/MessageSerializerExtensionsTests.cs b/test/Kurrent.Client.Tests/Core/Serialization/MessageSerializerExtensionsTests.cs deleted file mode 100644 index 6262af66a..000000000 --- a/test/Kurrent.Client.Tests/Core/Serialization/MessageSerializerExtensionsTests.cs +++ /dev/null @@ -1,110 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using EventStore.Client; -using Kurrent.Client.Core.Serialization; - -namespace Kurrent.Client.Tests.Core.Serialization; - -public class MessageSerializerExtensionsTests { - [Fact] - public void With_NullOperationSettings_ReturnsDefaultSerializer() { - // Given - var defaultSerializer = new DummyMessageSerializer(); - var defaultSettings = new KurrentClientSerializationSettings(); - - // When - var result = defaultSerializer.With(defaultSettings, null); - - // Then - Assert.Same(defaultSerializer, result); - } - - [Fact] - public void With_DisabledAutomaticDeserialization_ReturnsNullSerializer() { - // Given - var defaultSerializer = new DummyMessageSerializer(); - var defaultSettings = new KurrentClientSerializationSettings(); - var operationSettings = OperationSerializationSettings.Disabled; - - // When - var result = defaultSerializer.With(defaultSettings, operationSettings); - - // Then - Assert.Same(NullMessageSerializer.Instance, result); - } - - [Fact] - public void With_NoConfigureSettings_ReturnsDefaultSerializer() { - // Given - var defaultSerializer = new DummyMessageSerializer(); - var defaultSettings = new KurrentClientSerializationSettings(); - var operationSettings = new OperationSerializationSettings(); // Default-enabled with no config - - // When - var result = defaultSerializer.With(defaultSettings, operationSettings); - - // Then - Assert.Same(defaultSerializer, result); - } - - [Fact] - public void With_ConfigureSettings_CreatesNewMessageSerializer() { - // Given - var defaultSerializer = new DummyMessageSerializer(); - var defaultSettings = KurrentClientSerializationSettings.Default(); - - var operationSettings = OperationSerializationSettings.Configure( - s => - s.RegisterMessageType("CustomMessageName") - ); - - // When - var result = defaultSerializer.With(defaultSettings, operationSettings); - - // Then - Assert.NotSame(defaultSerializer, result); - Assert.IsType(result); - } - - [Fact] - public void Serialize_WithMultipleMessages_ReturnsArrayOfEventData() { - // Given - var serializer = new DummyMessageSerializer(); - var messages = new List { - Message.From(new object()), - Message.From(new object()), - Message.From(new object()) - }; - - var context = new MessageSerializationContext("test-stream", ContentType.Json); - - // When - var result = serializer.Serialize(messages, context); - - // Then - Assert.Equal(3, result.Length); - Assert.All(result, eventData => Assert.Equal("TestEvent", eventData.Type)); - } - - class DummyMessageSerializer : IMessageSerializer { - public EventData Serialize(Message value, MessageSerializationContext context) { - return new EventData( - Uuid.NewUuid(), - "TestEvent", - ReadOnlyMemory.Empty, - ReadOnlyMemory.Empty, - "application/json" - ); - } - -#if NET48 - public bool TryDeserialize(EventRecord record, out Message? deserialized) { -#else - public bool TryDeserialize(EventRecord record, [NotNullWhen(true)] out Message? deserialized) { -#endif - deserialized = null; - return false; - } - } - - public record UserRegistered(string UserId, string Email); -} diff --git a/test/Kurrent.Client.Tests/Core/Serialization/MessageSerializerTests.cs b/test/Kurrent.Client.Tests/Core/Serialization/MessageSerializerTests.cs deleted file mode 100644 index fe57dab8b..000000000 --- a/test/Kurrent.Client.Tests/Core/Serialization/MessageSerializerTests.cs +++ /dev/null @@ -1,264 +0,0 @@ -using EventStore.Client; -using Kurrent.Client.Core.Serialization; - -namespace Kurrent.Client.Tests.Core.Serialization; - -public class MessageSerializerTests { - [Fact] - public void Serialize_WithValidMessage_ReturnsEventData() { - // Given - var settings = CreateTestSettings(); - var schemaRegistry = SchemaRegistry.From(settings); - var serializer = new MessageSerializer(schemaRegistry); - - var data = new UserRegistered("user-123", "user@random-email.com"); - var metadata = new TestMetadata(); - var messageId = Uuid.NewUuid(); - var message = Message.From(data, metadata, messageId); - - var context = new MessageSerializationContext("user-123", ContentType.Json); - - // When - var eventData = serializer.Serialize(message, context); - - // Then - Assert.Equal(messageId, eventData.EventId); - Assert.Equal("UserRegistered", eventData.Type); - Assert.NotEmpty(eventData.Data.Span.ToArray()); - Assert.NotEmpty(eventData.Metadata.Span.ToArray()); - Assert.Equal(ContentType.Json.ToMessageContentType(), eventData.ContentType); - } - - [Fact] - public void Serialize_WithAutoGeneratedId_GeneratesNewId() { - // Given - var settings = CreateTestSettings(); - var schemaRegistry = SchemaRegistry.From(settings); - var serializer = new MessageSerializer(schemaRegistry); - - var data = new UserRegistered("user-123", "user@random-email.com"); - var message = Message.From(data); // No ID provided - - var context = new MessageSerializationContext("user-123", ContentType.Json); - - // When - var eventData = serializer.Serialize(message, context); - - // Then - Assert.NotEqual(Uuid.Empty, eventData.EventId); - } - - [Fact] - public void Serialize_WithoutMetadata_GeneratesEmptyMetadata() { - // Given - var settings = CreateTestSettings(); - var schemaRegistry = SchemaRegistry.From(settings); - var serializer = new MessageSerializer(schemaRegistry); - - var data = new UserRegistered("user-123", "user@random-email.com"); - var message = Message.From(data); - - var context = new MessageSerializationContext("user-123", ContentType.Json); - - // When - var eventData = serializer.Serialize(message, context); - - // Then - Assert.Empty(eventData.Metadata.Span.ToArray()); - } - - [Fact] - public void Serialize_WithBinaryContentType_UsesBinarySerializer() { - // Given - var settings = CreateTestSettings(); - var schemaRegistry = SchemaRegistry.From(settings); - var serializer = new MessageSerializer(schemaRegistry); - - var data = new UserRegistered("user-123", "user@random-email.com"); - var message = Message.From(data); - - var context = new MessageSerializationContext("user-123", ContentType.Bytes); - - // When - var eventData = serializer.Serialize(message, context); - - // Then - Assert.Equal(ContentType.Bytes.ToMessageContentType(), eventData.ContentType); - } - - [Fact] - public void SerializeMultiple_WithValidMessages_ReturnsEventDataArray() { - // Given - var settings = CreateTestSettings(); - var schemaRegistry = SchemaRegistry.From(settings); - var serializer = new MessageSerializer(schemaRegistry); - - var messages = new[] { - Message.From(new UserRegistered("1", "u1@random-email.com")), - Message.From(new UserRegistered("2", "u2@random-email.com")), - Message.From(new UserRegistered("3", "u3@random-email.com")) - }; - - var context = new MessageSerializationContext("user-123", ContentType.Json); - - // When - var eventDataArray = serializer.Serialize(messages, context); - - // Then - Assert.Equal(3, eventDataArray.Length); - Assert.All(eventDataArray, data => Assert.Equal("UserRegistered", data.Type)); - } - - [Fact] - public void TryDeserialize_WithValidEventRecord_DeserializesSuccessfully() { - // Given - var settings = CreateTestSettings(); - var schemaRegistry = SchemaRegistry.From(settings); - var serializer = new MessageSerializer(schemaRegistry); - - var testEvent = new UserRegistered("user-123", "user@random-email.com"); - var testMetadata = new TestMetadata { CorrelationId = "corr-123", UserId = "user-456" }; - var eventId = Uuid.NewUuid(); - - var eventRecord = CreateTestEventRecord("UserRegistered", testEvent, testMetadata, eventId); - - // When - var success = serializer.TryDeserialize(eventRecord, out var message); - - // Then - Assert.True(success); - Assert.NotNull(message); - Assert.Equal(eventId, message.MessageId); - - var deserializedEvent = Assert.IsType(message.Data); - Assert.Equal("user-123", deserializedEvent.UserId); - Assert.Equal("user@random-email.com", deserializedEvent.Email); - - var deserializedMetadata = Assert.IsType(message.Metadata); - Assert.Equal("corr-123", deserializedMetadata.CorrelationId); - Assert.Equal("user-456", deserializedMetadata.UserId); - } - - [Fact] - public void TryDeserialize_WithUnknownEventType_ReturnsFalse() { - // Given - var settings = CreateTestSettings(); - var schemaRegistry = SchemaRegistry.From(settings); - var serializer = new MessageSerializer(schemaRegistry); - - var testEvent = new UserRegistered("user-123", "user@random-email.com"); - var eventRecord = CreateTestEventRecord("UnknownEventType", testEvent); - - // When - var success = serializer.TryDeserialize(eventRecord, out var message); - - // Then - Assert.False(success); - Assert.Null(message); - } - - [Fact] - public void TryDeserialize_WithNullData_ReturnsFalse() { - // Given - var settings = CreateTestSettings(); - var schemaRegistry = SchemaRegistry.From(settings); - var serializer = new MessageSerializer(schemaRegistry); - var eventRecord = CreateTestEventRecord("UserRegistered", null); - - // When - var success = serializer.TryDeserialize(eventRecord, out var message); - - // Then - Assert.False(success); - Assert.Null(message); - } - - [Fact] - public void TryDeserialize_WithEmptyMetadata_DeserializesWithNullMetadata() { - // Given - var settings = CreateTestSettings(); - var schemaRegistry = SchemaRegistry.From(settings); - var serializer = new MessageSerializer(schemaRegistry); - - var testEvent = new UserRegistered("user-123", "user@random-email.com"); - var eventId = Uuid.NewUuid(); - - var eventRecord = CreateTestEventRecord("UserRegistered", testEvent, null, eventId); - - // When - var success = serializer.TryDeserialize(eventRecord, out var message); - - // Then - Assert.True(success); - Assert.NotNull(message); - Assert.Equal(eventId, message.MessageId); - Assert.Null(message.Metadata); - - var deserializedEvent = Assert.IsType(message.Data); - Assert.Equal("user-123", deserializedEvent.UserId); - Assert.Equal("user@random-email.com", deserializedEvent.Email); - } - - [Fact] - public void MessageSerializer_From_CreatesInstanceWithDefaultSettings() { - // When - var serializer = MessageSerializer.From(); - - // Then - if this doesn't throw, it worked - Assert.NotNull(serializer); - } - - [Fact] - public void MessageSerializer_From_CreatesInstanceWithProvidedSettings() { - // Given - var settings = CreateTestSettings(); - - // When - var serializer = MessageSerializer.From(settings); - - // Then - test it works with our settings - var data = new UserRegistered("user-123", "user@random-email.com"); - var message = Message.From(data); - var context = new MessageSerializationContext("user-123", ContentType.Json); - - var eventData = serializer.Serialize(message, context); - Assert.Equal("UserRegistered", eventData.Type); - } - - static KurrentClientSerializationSettings CreateTestSettings() { - var settings = new KurrentClientSerializationSettings(); - settings.RegisterMessageType("UserRegistered"); - settings.RegisterMessageType("UserAssignedToRole"); - settings.UseMetadataType(); - - return settings; - } - - static EventRecord CreateTestEventRecord( - string eventType, object? data = null, object? metadata = null, Uuid? eventId = null - ) => - new( - Uuid.NewUuid().ToString(), - eventId ?? Uuid.NewUuid(), - StreamPosition.FromInt64(0), - new Position(1, 1), - new Dictionary { - { Constants.Metadata.Type, eventType }, - { Constants.Metadata.Created, DateTime.UtcNow.ToTicksSinceEpoch().ToString() }, - { Constants.Metadata.ContentType, Constants.Metadata.ContentTypes.ApplicationJson } - }, - data != null ? _serializer.Serialize(data) : ReadOnlyMemory.Empty, - metadata != null ? _serializer.Serialize(metadata) : ReadOnlyMemory.Empty - ); - - static readonly SystemTextJsonSerializer _serializer = new SystemTextJsonSerializer(); - - public record UserRegistered(string UserId, string Email); - - public record UserAssignedToRole(string UserId, string Role); - - public class TestMetadata { - public string CorrelationId { get; set; } = "correlation-id"; - public string UserId { get; set; } = "user-id"; - } -} diff --git a/test/Kurrent.Client.Tests/Core/Serialization/MessageTypeRegistryTests.cs b/test/Kurrent.Client.Tests/Core/Serialization/MessageTypeRegistryTests.cs deleted file mode 100644 index a74026ed0..000000000 --- a/test/Kurrent.Client.Tests/Core/Serialization/MessageTypeRegistryTests.cs +++ /dev/null @@ -1,237 +0,0 @@ -using Kurrent.Client.Core.Serialization; - -namespace Kurrent.Client.Tests.Core.Serialization; - -public class MessageTypeRegistryTests { - [Fact] - public void Register_StoresTypeAndTypeName() { - // Given - var registry = new MessageTypeRegistry(); - var type = typeof(TestEvent1); - const string typeName = "test-event-1"; - - // When - registry.Register(type, typeName); - - // Then - Assert.Equal(typeName, registry.GetTypeName(type)); - Assert.Equal(type, registry.GetClrType(typeName)); - } - - [Fact] - public void Register_CalledTwiceForTheSameTypeOverridesExistingRegistration() { - // Given - var registry = new MessageTypeRegistry(); - var type = typeof(TestEvent1); - const string originalTypeName = "original-name"; - const string updatedTypeName = "updated-name"; - - // When - registry.Register(type, originalTypeName); - registry.Register(type, updatedTypeName); - - // Then - Assert.Equal(updatedTypeName, registry.GetTypeName(type)); - Assert.Equal(type, registry.GetClrType(updatedTypeName)); - Assert.Equal(type, registry.GetClrType(originalTypeName)); - } - - [Fact] - public void GetTypeName_ReturnsNullForNotRegisteredType() { - // Given - var registry = new MessageTypeRegistry(); - var unregisteredType = typeof(TestEvent2); - - // When - var result = registry.GetTypeName(unregisteredType); - - // Then - Assert.Null(result); - } - - [Fact] - public void GetClrType_ReturnsNullForNotRegisteredTypeName() { - // Given - var registry = new MessageTypeRegistry(); - const string unregisteredTypeName = "unregistered-type"; - - // When - var result = registry.GetClrType(unregisteredTypeName); - - // Then - Assert.Null(result); - } - - [Fact] - public void GetOrAddTypeName_ReturnsExistingTypeName() { - // Given - var registry = new MessageTypeRegistry(); - var type = typeof(TestEvent1); - const string existingTypeName = "existing-type-name"; - - registry.Register(type, existingTypeName); - var typeResolutionCount = 0; - - // When - var result = registry.GetOrAddTypeName( - type, - _ => { - typeResolutionCount++; - return "factory-type-name"; - } - ); - - // Then - Assert.Equal(existingTypeName, result); - Assert.Equal(0, typeResolutionCount); - } - - [Fact] - public void GetOrAddTypeName_ForNotRegisteredTypeNameAddsNewTypeName() { - // Given - var registry = new MessageTypeRegistry(); - var type = typeof(TestEvent1); - const string newTypeName = "new-type-name"; - var typeResolutionCount = 0; - - // When - var result = registry.GetOrAddTypeName( - type, - _ => { - typeResolutionCount++; - return newTypeName; - } - ); - - // Then - Assert.Equal(newTypeName, result); - Assert.Equal(1, typeResolutionCount); - Assert.Equal(newTypeName, registry.GetTypeName(type)); - Assert.Equal(type, registry.GetClrType(newTypeName)); - } - - [Fact] - public void GetOrAddClrType_ReturnsExistingClrType() { - // Given - var registry = new MessageTypeRegistry(); - var type = typeof(TestEvent1); - const string typeName = "test-event-name"; - registry.Register(type, typeName); - var typeResolutionCount = 0; - - // When - var result = registry.GetOrAddClrType( - typeName, - _ => { - typeResolutionCount++; - return typeof(TestEvent2); - } - ); - - // Then - Assert.Equal(type, result); - Assert.Equal(0, typeResolutionCount); - } - - [Fact] - public void GetOrAddClrType_ForNotExistingTypeAddsNewClrType() { - // Given - var registry = new MessageTypeRegistry(); - const string typeName = "test-event-name"; - var type = typeof(TestEvent1); - var typeResolutionCount = 0; - - // When - var result = registry.GetOrAddClrType( - typeName, - _ => { - typeResolutionCount++; - return type; - } - ); - - // Then - Assert.Equal(type, result); - Assert.Equal(1, typeResolutionCount); - Assert.Equal(typeName, registry.GetTypeName(type)); - Assert.Equal(type, registry.GetClrType(typeName)); - } - - [Fact] - public void GetOrAddClrType_HandlesNullReturnFromTypeResolution() { - // Given - var registry = new MessageTypeRegistry(); - const string typeName = "unknown-type-name"; - - // When - var result = registry.GetOrAddClrType(typeName, _ => null); - - // Then - Assert.Null(result); - Assert.Null(registry.GetClrType(typeName)); - } - - - [Fact] - public void RegisterGeneric_RegistersTypeWithTypeName() { - // Given - var registry = new MessageTypeRegistry(); - const string typeName = "test-event-1"; - - // When - registry.Register(typeName); - - // Then - Assert.Equal(typeName, registry.GetTypeName(typeof(TestEvent1))); - Assert.Equal(typeof(TestEvent1), registry.GetClrType(typeName)); - } - - [Fact] - public void RegisterDictionary_RegistersMultipleTypes() { - // Given - var registry = new MessageTypeRegistry(); - var typeMap = new Dictionary { - { typeof(TestEvent1), "test-event-1" }, - { typeof(TestEvent2), "test-event-2" } - }; - - // When - registry.Register(typeMap); - - // Then - Assert.Equal("test-event-1", registry.GetTypeName(typeof(TestEvent1))); - Assert.Equal("test-event-2", registry.GetTypeName(typeof(TestEvent2))); - Assert.Equal(typeof(TestEvent1), registry.GetClrType("test-event-1")); - Assert.Equal(typeof(TestEvent2), registry.GetClrType("test-event-2")); - } - - [Fact] - public void GetTypeNameGeneric_ReturnsTypeName() { - // Given - var registry = new MessageTypeRegistry(); - const string typeName = "test-event-1"; - registry.Register(typeName); - - // When - var result = registry.GetTypeName(); - - // Then - Assert.Equal(typeName, result); - } - - [Fact] - public void GetTypeNameGeneric_ReturnsNullForUnregisteredType() { - // Given - var registry = new MessageTypeRegistry(); - - // When - var result = registry.GetTypeName(); - - // Then - Assert.Null(result); - } - - record TestEvent1; - - record TestEvent2; -} diff --git a/test/Kurrent.Client.Tests/Core/Serialization/NullMessageSerializerTests.cs b/test/Kurrent.Client.Tests/Core/Serialization/NullMessageSerializerTests.cs deleted file mode 100644 index 814440f38..000000000 --- a/test/Kurrent.Client.Tests/Core/Serialization/NullMessageSerializerTests.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System.Text; -using EventStore.Client; -using Kurrent.Client.Core.Serialization; - -namespace Kurrent.Client.Tests.Core.Serialization; - -public class NullMessageSerializerTests { - [Fact] - public void Serialize_ThrowsException() { - // Given - var serializer = NullMessageSerializer.Instance; - var message = Message.From(new object()); - var context = new MessageSerializationContext("test-stream", ContentType.Json); - - // When & Assert - Assert.Throws(() => serializer.Serialize(message, context)); - } - - [Fact] - public void TryDeserialize_ReturnsFalse() { - // Given - var serializer = NullMessageSerializer.Instance; - var eventRecord = CreateTestEventRecord(); - - // When - var result = serializer.TryDeserialize(eventRecord, out var message); - - // Then - Assert.False(result); - Assert.Null(message); - } - - static EventRecord CreateTestEventRecord() => - new( - Uuid.NewUuid().ToString(), - Uuid.NewUuid(), - StreamPosition.FromInt64(0), - new Position(1, 1), - new Dictionary { - { Constants.Metadata.Type, "test-event" }, - { Constants.Metadata.Created, DateTime.UtcNow.ToTicksSinceEpoch().ToString() }, - { Constants.Metadata.ContentType, Constants.Metadata.ContentTypes.ApplicationJson } - }, - """{"x":1}"""u8.ToArray(), - """{"x":2}"""u8.ToArray() - ); -} diff --git a/test/Kurrent.Client.Tests/Core/Serialization/SchemaRegistryTests.cs b/test/Kurrent.Client.Tests/Core/Serialization/SchemaRegistryTests.cs deleted file mode 100644 index 084f6ffd7..000000000 --- a/test/Kurrent.Client.Tests/Core/Serialization/SchemaRegistryTests.cs +++ /dev/null @@ -1,257 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using System.Text.Json; -using EventStore.Client; -using Kurrent.Client.Core.Serialization; - -namespace Kurrent.Client.Tests.Core.Serialization; - -public class SchemaRegistryTests { - // Test classes - record TestEvent1; - - record TestEvent2; - - record TestEvent3; - - record TestMetadata; - - [Fact] - public void Constructor_InitializesProperties() { - // Given - var serializers = new Dictionary { - { ContentType.Json, new SystemTextJsonSerializer() }, - { ContentType.Bytes, new SystemTextJsonSerializer() } - }; - - var namingStrategy = new DefaultMessageTypeNamingStrategy(typeof(TestMetadata)); - - // When - var registry = new SchemaRegistry(serializers, namingStrategy); - - // Then - Assert.Same(namingStrategy, registry.MessageTypeNamingStrategy); - } - - [Fact] - public void GetSerializer_ReturnsCorrectSerializer() { - // Given - var jsonSerializer = new SystemTextJsonSerializer(); - var bytesSerializer = new SystemTextJsonSerializer(); - - var serializers = new Dictionary { - { ContentType.Json, jsonSerializer }, - { ContentType.Bytes, bytesSerializer } - }; - - var registry = new SchemaRegistry( - serializers, - new DefaultMessageTypeNamingStrategy(typeof(TestMetadata)) - ); - - // When - var resultJsonSerializer = registry.GetSerializer(ContentType.Json); - var resultBytesSerializer = registry.GetSerializer(ContentType.Bytes); - - // Then - Assert.NotSame(resultJsonSerializer, resultBytesSerializer); - Assert.Same(jsonSerializer, resultJsonSerializer); - Assert.Same(bytesSerializer, resultBytesSerializer); - } - - [Fact] - public void From_WithDefaultSettings_CreatesRegistryWithDefaults() { - // Given - var settings = new KurrentClientSerializationSettings(); - - // When - var registry = SchemaRegistry.From(settings); - - // Then - Assert.NotNull(registry); - Assert.NotNull(registry.MessageTypeNamingStrategy); - Assert.NotNull(registry.GetSerializer(ContentType.Json)); - Assert.NotNull(registry.GetSerializer(ContentType.Bytes)); - - Assert.IsType(registry.MessageTypeNamingStrategy); - Assert.IsType(registry.GetSerializer(ContentType.Json)); - Assert.IsType(registry.GetSerializer(ContentType.Bytes)); - } - - [Fact] - public void From_WithCustomJsonSerializer_UsesProvidedSerializer() { - // Given - var customJsonSerializer = new SystemTextJsonSerializer( - new SystemTextJsonSerializationSettings { - Options = new JsonSerializerOptions { WriteIndented = true } - } - ); - - var settings = new KurrentClientSerializationSettings() - .UseJsonSerializer(customJsonSerializer); - - // When - var registry = SchemaRegistry.From(settings); - - // Then - Assert.Same(customJsonSerializer, registry.GetSerializer(ContentType.Json)); - Assert.NotSame(customJsonSerializer, registry.GetSerializer(ContentType.Bytes)); - } - - [Fact] - public void From_WithCustomBytesSerializer_UsesProvidedSerializer() { - // Given - var customBytesSerializer = new SystemTextJsonSerializer( - new SystemTextJsonSerializationSettings { - Options = new JsonSerializerOptions { WriteIndented = true } - } - ); - - var settings = new KurrentClientSerializationSettings() - .UseBytesSerializer(customBytesSerializer); - - // When - var registry = SchemaRegistry.From(settings); - - // Then - Assert.Same(customBytesSerializer, registry.GetSerializer(ContentType.Bytes)); - Assert.NotSame(customBytesSerializer, registry.GetSerializer(ContentType.Json)); - } - - [Fact] - public void From_WithMessageTypeMap_RegistersTypes() { - // Given - var settings = new KurrentClientSerializationSettings(); - settings.RegisterMessageType("test-event-1"); - settings.RegisterMessageType("test-event-2"); - - // When - var registry = SchemaRegistry.From(settings); - var namingStrategy = registry.MessageTypeNamingStrategy; - - // Then - // Verify types can be resolved - Assert.True(namingStrategy.TryResolveClrType("test-event-1", out var type1)); - Assert.Equal(typeof(TestEvent1), type1); - - Assert.True(namingStrategy.TryResolveClrType("test-event-2", out var type2)); - Assert.Equal(typeof(TestEvent2), type2); - } - - [Fact] - public void From_WithCategoryMessageTypesMap_RegistersTypesWithCategories() { - // Given - var settings = new KurrentClientSerializationSettings(); - settings.RegisterMessageTypeForCategory("category1"); - settings.RegisterMessageTypeForCategory("category1"); - settings.RegisterMessageTypeForCategory("category2"); - - // When - var registry = SchemaRegistry.From(settings); - var namingStrategy = registry.MessageTypeNamingStrategy; - - // Then - // For categories, the naming strategy should have resolved the type names - // using the ResolveTypeName method, which by default uses the type's name - string typeName1 = namingStrategy.ResolveTypeName( - typeof(TestEvent1), - new MessageTypeNamingResolutionContext("category1") - ); - - string typeName2 = namingStrategy.ResolveTypeName( - typeof(TestEvent2), - new MessageTypeNamingResolutionContext("category1") - ); - - string typeName3 = namingStrategy.ResolveTypeName( - typeof(TestEvent3), - new MessageTypeNamingResolutionContext("category2") - ); - - // Verify types can be resolved by the type names - Assert.True(namingStrategy.TryResolveClrType(typeName1, out var resolvedType1)); - Assert.Equal(typeof(TestEvent1), resolvedType1); - - Assert.True(namingStrategy.TryResolveClrType(typeName2, out var resolvedType2)); - Assert.Equal(typeof(TestEvent2), resolvedType2); - - Assert.True(namingStrategy.TryResolveClrType(typeName3, out var resolvedType3)); - Assert.Equal(typeof(TestEvent3), resolvedType3); - } - - [Fact] - public void From_WithCustomNamingStrategy_UsesProvidedStrategy() { - // Given - var customNamingStrategy = new TestNamingStrategy(); - var settings = new KurrentClientSerializationSettings() - .UseMessageTypeNamingStrategy(customNamingStrategy); - - // When - var registry = SchemaRegistry.From(settings); - - // Then - // The registry wraps the naming strategy, but should still use it - var wrappedStrategy = registry.MessageTypeNamingStrategy; - Assert.IsType(wrappedStrategy); - - // Test to make sure it behaves like our custom strategy - string typeName = wrappedStrategy.ResolveTypeName( - typeof(TestEvent1), - new MessageTypeNamingResolutionContext("test") - ); - - // Our test strategy adds "Custom-" prefix - Assert.StartsWith("Custom-", typeName); - } - - [Fact] - public void From_WithNoMessageTypeNamingStrategy_UsesDefaultStrategy() { - // Given - var settings = new KurrentClientSerializationSettings { - MessageTypeNamingStrategy = null, - DefaultMetadataType = typeof(TestMetadata) - }; - - // When - var registry = SchemaRegistry.From(settings); - - // Then - Assert.NotNull(registry.MessageTypeNamingStrategy); - - // The wrapped default strategy should use our metadata type - Assert.True( - registry.MessageTypeNamingStrategy.TryResolveClrMetadataType("some-type", out var defaultMetadataType) - ); - - Assert.Equal(typeof(TestMetadata), defaultMetadataType); - } - - // Custom naming strategy for testing - class TestNamingStrategy : IMessageTypeNamingStrategy { - public string ResolveTypeName(Type type, MessageTypeNamingResolutionContext context) { - return $"Custom-{type.Name}-{context.CategoryName}"; - } -#if NET48 - public bool TryResolveClrType(string messageTypeName, out Type? clrType) -#else - public bool TryResolveClrType(string messageTypeName, [NotNullWhen(true)] out Type? clrType) -#endif - { - // Simple implementation for testing - clrType = messageTypeName.StartsWith("Custom-TestEvent1") - ? typeof(TestEvent1) - : null; - - return clrType != null; - } - -#if NET48 - public bool TryResolveClrMetadataType(string messageTypeName, out Type? clrType) -#else - public bool TryResolveClrMetadataType(string messageTypeName, [NotNullWhen(true)] out Type? clrType) -#endif - { - clrType = typeof(TestMetadata); - return true; - } - } -} diff --git a/test/Kurrent.Client.Tests/Core/Serialization/TypeProviderTests.cs b/test/Kurrent.Client.Tests/Core/Serialization/TypeProviderTests.cs deleted file mode 100644 index 1c49ff102..000000000 --- a/test/Kurrent.Client.Tests/Core/Serialization/TypeProviderTests.cs +++ /dev/null @@ -1,184 +0,0 @@ -using System.Reflection; -using Kurrent.Client.Core.Serialization; - -namespace Kurrent.Client.Tests.Core.Serialization { - public record TestNestedNamespaceEvent; - - namespace Nested { - public record TestNestedNamespaceEvent; - } - - public class TypeProviderTests { - [Fact] - public void GetTypeByFullName_WorksWithLoadedAssemblyTypes() { - // Given - string enumFullName = typeof(TestEvent).FullName!; - - // When - var result = TypeProvider.GetTypeByFullName(enumFullName); - - // Then - Assert.NotNull(result); - Assert.Equal(typeof(TestEvent), result); - } - - [Fact] - public void GetTypeByFullName_ReturnsNull_WhenTypeDoesNotExist() { - // Given - const string fullName = "NonExistent.Type.That.Should.Not.Exist"; - - // When - var result = TypeProvider.GetTypeByFullName(fullName); - - // Then - Assert.Null(result); - } - - [Fact] - public void GetTypeByFullName_ForNestedNamespacesSortsAssembliesByNamespace_AndReturnsFirstMatch() { - // Given - string originalType = typeof(TestNestedNamespaceEvent).FullName!; - string nestedType = typeof(Kurrent.Client.Tests.Core.Serialization.Nested.TestNestedNamespaceEvent).FullName!; - - // When - var originalTypeResult = TypeProvider.GetTypeByFullName(originalType); - var nestedTypeResult = TypeProvider.GetTypeByFullName(nestedType); - - // Then - Assert.NotNull(originalTypeResult); - Assert.NotNull(nestedTypeResult); - Assert.NotEqual(originalTypeResult, nestedTypeResult); - } - - [Fact] - public void GetTypeByFullName_ForNestedClassesSortsAssembliesByNamespace_AndReturnsFirstMatch() { - // Given - string originalType = typeof(TestNestedInClassEvent).FullName!; - string nestedType = typeof(Nested.TestNestedInClassEvent).FullName!; - - // When - var originalTypeResult = TypeProvider.GetTypeByFullName(originalType); - var nestedTypeResult = TypeProvider.GetTypeByFullName(nestedType); - - // Then - Assert.NotNull(originalTypeResult); - Assert.NotNull(nestedTypeResult); - Assert.NotEqual(originalTypeResult, nestedTypeResult); - } - - [Fact] - public void GetTypeByFullName_HandlesGenericTypes() { - // Given - string fullName = typeof(GenericEvent).FullName!; - - // When - var result = TypeProvider.GetTypeByFullName(fullName); - - // Then - Assert.NotNull(result); - Assert.Equal(typeof(GenericEvent), result); - } - - [Fact] - public void GetTypeByFullName_ReturnsType_WhenTypeExistsInSystemLib() { - // Given - string fullName = typeof(string).FullName!; - - // When - var result = TypeProvider.GetTypeByFullName(fullName); - - // Then - Assert.NotNull(result); - Assert.Equal(typeof(string), result); - } - - public record TestEvent; - - public record TestNestedInClassEvent; - - public class Nested { - public record TestNestedInClassEvent; - } - - /// - /// Generic external event class to test generic type resolution - /// - /// The payload type - public class GenericEvent { - public string Id { get; set; } = null!; - public T Data { get; set; } = default!; - } - } - - /// - /// Tests for TypeProvider focusing on unloaded assemblies - /// Uses Kurrent.Client.Tests.NeverLoadedAssembly project which should never be loaded before this test runs - /// - public class UnloadedAssemblyTests { - const string NotLoadedTypeFullName = "Kurrent.Client.Tests.NeverLoadedAssembly.NotLoadedExternalEvent"; - - [Fact] - public void GetTypeByFullName_ReturnsNull_ForTypeInUnloadedAssembly() { - // When - var result = TypeProvider.GetTypeByFullName(NotLoadedTypeFullName); - - // Then - Assert.Null(result); - } - } - - /// - /// Tests for TypeProvider focusing on loaded assemblies - /// Uses Kurrent.Client.Tests.ExternalAssembly.dll which will be explicitly loaded during the tests - /// - public class LoadedAssemblyTests { - const string ExternalAssemblyName = "Kurrent.Client.Tests.ExternalAssembly"; - const string ExternalTypeFullName = "Kurrent.Client.Tests.ExternalAssembly.ExternalEvent"; - readonly Assembly _externalAssembly; - - public LoadedAssemblyTests() { - var path = Path.Combine( - AppDomain.CurrentDomain.BaseDirectory, - $"{ExternalAssemblyName}.dll" - ); - - _externalAssembly = Assembly.LoadFrom(path); - } - - [Fact] - public void GetTypeByFullName_FindsType_AfterAssemblyIsExplicitlyLoaded() { - // Given - - // When - var externalType = TypeProvider.GetTypeByFullName(ExternalTypeFullName); - - // Then - Assert.NotNull(externalType); - Assert.Equal(ExternalTypeFullName, externalType.FullName); - Assert.Equal(_externalAssembly, externalType.Assembly); - } - - [Fact] - public void GetTypeByFullName_PrioritizesAssembliesByNamespacePrefix() { - // This test verifies the namespace-based prioritization by: - // 1. Loading our test assembly which has a name matching its namespace prefix - // 2. Verifying that we can resolve a type from it - - // Given - // When - var result = TypeProvider.GetTypeByFullName(ExternalTypeFullName); - Assert.NotNull(result); - Assert.Equal(ExternalTypeFullName, result.FullName); - Assert.NotEqual(typeof(ExternalEvent), result); - } - - /// - /// External event class used for testing loaded assembly resolution, it should not be resolved, - /// because of prioritising exact namespaces resolutions first - /// - public class ExternalEvent { - public string Id { get; set; } = null!; - public string Name { get; set; } = null!; - } - } -} diff --git a/test/Kurrent.Client.Tests/Kurrent.Client.Tests.csproj b/test/Kurrent.Client.Tests/Kurrent.Client.Tests.csproj index 9bb11a881..ffca50910 100644 --- a/test/Kurrent.Client.Tests/Kurrent.Client.Tests.csproj +++ b/test/Kurrent.Client.Tests/Kurrent.Client.Tests.csproj @@ -2,8 +2,6 @@ - - diff --git a/test/Kurrent.Client.Tests/Streams/Read/ReadAllEventsFixture.cs b/test/Kurrent.Client.Tests/Streams/Read/ReadAllEventsFixture.cs index e98a6ae28..f3d2562e1 100644 --- a/test/Kurrent.Client.Tests/Streams/Read/ReadAllEventsFixture.cs +++ b/test/Kurrent.Client.Tests/Streams/Read/ReadAllEventsFixture.cs @@ -27,7 +27,7 @@ public ReadAllEventsFixture() { await Streams.AppendToStreamAsync(ExpectedStreamName, StreamState.NoStream, Events); ExpectedEvents = Events.ToBinaryData(); - ExpectedEventsReversed = Enumerable.Reverse(ExpectedEvents).ToArray(); + ExpectedEventsReversed = ExpectedEvents.Reverse().ToArray(); ExpectedFirstEvent = ExpectedEvents.First(); ExpectedLastEvent = ExpectedEvents.Last(); diff --git a/test/Kurrent.Client.Tests/Streams/Read/ReadStreamBackwardTests.cs b/test/Kurrent.Client.Tests/Streams/Read/ReadStreamBackwardTests.cs index 5ca640feb..04e8c441b 100644 --- a/test/Kurrent.Client.Tests/Streams/Read/ReadStreamBackwardTests.cs +++ b/test/Kurrent.Client.Tests/Streams/Read/ReadStreamBackwardTests.cs @@ -78,7 +78,7 @@ public async Task returns_events_in_reversed_order(string suffix, int count, int Assert.True( EventDataComparer.Equal( - Enumerable.Reverse(expected).ToArray(), + expected.Reverse().ToArray(), actual ) ); diff --git a/test/Kurrent.Client.Tests/Streams/Serialization/SerializationTests.PersistentSubscriptions.cs b/test/Kurrent.Client.Tests/Streams/Serialization/SerializationTests.PersistentSubscriptions.cs deleted file mode 100644 index 61dc3c18f..000000000 --- a/test/Kurrent.Client.Tests/Streams/Serialization/SerializationTests.PersistentSubscriptions.cs +++ /dev/null @@ -1,471 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using System.Text; -using System.Text.Json; -using EventStore.Client; -using Kurrent.Client.Core.Serialization; -using Kurrent.Diagnostics.Tracing; - -namespace Kurrent.Client.Tests.Streams.Serialization; - -[Trait("Category", "Target:Streams")] -[Trait("Category", "Operation:Append")] -public class PersistentSubscriptionsSerializationTests(ITestOutputHelper output, KurrentPermanentFixture fixture) - : KurrentPermanentTests(output, fixture) { - [RetryFact] - public async Task plain_clr_objects_are_serialized_and_deserialized_using_auto_serialization() { - // Given - var stream = Fixture.GetStreamName(); - List expected = GenerateMessages(); - - //When - await Fixture.Streams.AppendToStreamAsync(stream, expected); - - var group = await CreateToStreamSubscription(stream); - - var resolvedEvents = await Fixture.Subscriptions.SubscribeToStream(stream, group).Take(2).ToListAsync(); - - AssertThatMessages(AreDeserialized, expected, resolvedEvents); - } - - [RetryFact] - public async Task - message_data_and_metadata_are_serialized_and_deserialized_using_auto_serialization_with_registered_metadata() { - // Given - await using var client = NewClientWith(serialization => serialization.UseMetadataType()); - await using var subscriptionsClient = - NewSubscriptionsClientWith(serialization => serialization.UseMetadataType()); - - var stream = Fixture.GetStreamName(); - var metadata = new CustomMetadata(Guid.NewGuid()); - var expected = GenerateMessages(); - List messagesWithMetadata = - expected.Select(message => Message.From(message, metadata, Uuid.NewUuid())).ToList(); - - // When - await client.AppendToStreamAsync(stream, messagesWithMetadata); - - // Then - var group = await CreateToStreamSubscription(stream, subscriptionsClient); - var resolvedEvents = await subscriptionsClient.SubscribeToStream(stream, group).Take(2).ToListAsync(); - var messages = AssertThatMessages(AreDeserialized, expected, resolvedEvents); - - Assert.Equal(messagesWithMetadata, messages); - } - - [RetryFact] - public async Task - message_metadata_is_serialized_fully_byt_deserialized_to_tracing_metadata_using_auto_serialization_WITHOUT_registered_custom_metadata() { - var stream = Fixture.GetStreamName(); - var metadata = new CustomMetadata(Guid.NewGuid()); - var expected = GenerateMessages(); - List messagesWithMetadata = - expected.Select(message => Message.From(message, metadata, Uuid.NewUuid())).ToList(); - - // When - await Fixture.Streams.AppendToStreamAsync(stream, messagesWithMetadata); - - // Then - var group = await CreateToStreamSubscription(stream); - var resolvedEvents = await Fixture.Subscriptions.SubscribeToStream(stream, group).Take(2) - .ToListAsync(); - - var messages = AssertThatMessages(AreDeserialized, expected, resolvedEvents); - - Assert.Equal(messagesWithMetadata.Select(m => m with { Metadata = new TracingMetadata() }), messages); - } - - [RetryFact] - public async Task subscribe_to_stream_without_options_does_NOT_deserialize_resolved_message() { - // Given - var (stream, expected) = await AppendEventsUsingAutoSerialization(); - - // When - var group = await CreateToStreamSubscription(stream); - var resolvedEvents = await Fixture.Subscriptions - .SubscribeToStream(stream, group, int.MaxValue).Take(2) - .ToListAsync(); - - // Then - AssertThatMessages(AreNotDeserialized, expected, resolvedEvents); - } - - [RetryFact] - public async Task subscribe_to_all_without_options_does_NOT_deserialize_resolved_message() { - // Given - var (stream, expected) = await AppendEventsUsingAutoSerialization(); - - // When - var group = await CreateToAllSubscription(stream); - var resolvedEvents = await Fixture.Subscriptions - .SubscribeToAll(group, int.MaxValue) - .Take(2) - .ToListAsync(); - - // Then - AssertThatMessages(AreNotDeserialized, expected, resolvedEvents); - } - - public static TheoryData> CustomTypeMappings() { - return [ - (settings, typeName) => - settings.RegisterMessageType(typeName), - (settings, typeName) => - settings.RegisterMessageType(typeof(UserRegistered), typeName), - (settings, typeName) => - settings.RegisterMessageTypes(new Dictionary { { typeof(UserRegistered), typeName } }) - ]; - } - - [RetryTheory] - [MemberData(nameof(CustomTypeMappings))] - public async Task append_and_subscribe_to_stream_uses_custom_type_mappings( - Action customTypeMapping - ) { - // Given - await using var client = NewClientWith(serialization => customTypeMapping(serialization, "user_registered")); - await using var subscriptionsClient = - NewSubscriptionsClientWith(serialization => customTypeMapping(serialization, "user_registered")); - - // When - var (stream, expected) = await AppendEventsUsingAutoSerialization(client); - - // Then - var group = await CreateToStreamSubscription(stream, subscriptionsClient); - var resolvedEvents = await subscriptionsClient.SubscribeToStream(stream, group).Take(2) - .ToListAsync(); - - Assert.All(resolvedEvents, resolvedEvent => Assert.Equal("user_registered", resolvedEvent.Event.EventType)); - - AssertThatMessages(AreDeserialized, expected, resolvedEvents); - } - - [RetryTheory] - [MemberData(nameof(CustomTypeMappings))] - public async Task append_and_subscribe_to_all_uses_custom_type_mappings( - Action customTypeMapping - ) { - // Given - await using var client = NewClientWith(serialization => customTypeMapping(serialization, "user_registered")); - await using var subscriptionsClient = - NewSubscriptionsClientWith(serialization => customTypeMapping(serialization, "user_registered")); - - // When - var (stream, expected) = await AppendEventsUsingAutoSerialization(client); - - // Then - var group = await CreateToAllSubscription(stream, subscriptionsClient); - var resolvedEvents = await subscriptionsClient - .SubscribeToAll(group) - .Where(r => r.Event.EventStreamId == stream) - .Take(2) - .ToListAsync(); - - Assert.All(resolvedEvents, resolvedEvent => Assert.Equal("user_registered", resolvedEvent.Event.EventType)); - - AssertThatMessages(AreDeserialized, expected, resolvedEvents); - } - - [RetryFact] - public async Task automatic_serialization_custom_json_settings_are_applied() { - // Given - var systemTextJsonOptions = new JsonSerializerOptions { - PropertyNamingPolicy = JsonNamingPolicy.KebabCaseLower, - }; - - await using var client = NewClientWith(serialization => serialization.UseJsonSettings(systemTextJsonOptions)); - await using var subscriptionsClient = - NewSubscriptionsClientWith(serialization => serialization.UseJsonSettings(systemTextJsonOptions)); - - // When - var (stream, expected) = await AppendEventsUsingAutoSerialization(client); - - // Then - var group = await CreateToStreamSubscription(stream, subscriptionsClient); - var resolvedEvents = await subscriptionsClient.SubscribeToStream(stream, group).Take(2) - .ToListAsync(); - - var jsons = resolvedEvents.Select(e => JsonDocument.Parse(e.Event.Data).RootElement).ToList(); - - Assert.Equal(expected.Select(m => m.UserId), jsons.Select(j => j.GetProperty("user-id").GetGuid())); - - AssertThatMessages(AreDeserialized, expected, resolvedEvents); - } - - public class CustomMessageTypeNamingStrategy : IMessageTypeNamingStrategy { - public string ResolveTypeName(Type messageType, MessageTypeNamingResolutionContext resolutionContext) { - return $"custom-{messageType}"; - } - -#if NET48 - public bool TryResolveClrType(string messageTypeName, out Type? type) { -#else - public bool TryResolveClrType(string messageTypeName, [NotNullWhen(true)] out Type? type) { -#endif - var typeName = messageTypeName[(messageTypeName.IndexOf('-') + 1)..]; - type = Type.GetType(typeName); - - return type != null; - } - -#if NET48 - public bool TryResolveClrMetadataType(string messageTypeName, out Type? type) { -#else - public bool TryResolveClrMetadataType(string messageTypeName, [NotNullWhen(true)] out Type? type) { -#endif - type = null; - return false; - } - } - - [RetryFact] - public async Task append_and_subscribe_to_stream_uses_custom_message_type_naming_strategy() { - // Given - await using var client = NewClientWith( - serialization => serialization.UseMessageTypeNamingStrategy() - ); - - await using var subscriptionsClient = - NewSubscriptionsClientWith( - serialization => serialization.UseMessageTypeNamingStrategy() - ); - - //When - var (stream, expected) = await AppendEventsUsingAutoSerialization(client); - - //Then - var group = await CreateToStreamSubscription(stream, subscriptionsClient); - var resolvedEvents = await subscriptionsClient.SubscribeToStream(stream,group).Take(2) - .ToListAsync(); - - Assert.All( - resolvedEvents, - resolvedEvent => Assert.Equal($"custom-{typeof(UserRegistered).FullName}", resolvedEvent.Event.EventType) - ); - - AssertThatMessages(AreDeserialized, expected, resolvedEvents); - } - - [RetryFact] - public async Task append_and_subscribe_to_all_uses_custom_message_type_naming_strategy() { - // Given - await using var client = NewClientWith( - serialization => serialization.UseMessageTypeNamingStrategy() - ); - - await using var subscriptionsClient = - NewSubscriptionsClientWith( - serialization => serialization.UseMessageTypeNamingStrategy() - ); - - //When - var (stream, expected) = await AppendEventsUsingAutoSerialization(client); - - //Then - var group = await CreateToAllSubscription(stream, subscriptionsClient); - var resolvedEvents = await subscriptionsClient - .SubscribeToAll(group) - .Where(r => r.Event.EventStreamId == stream) - .Take(2) - .ToListAsync(); - - Assert.All( - resolvedEvents, - resolvedEvent => Assert.Equal($"custom-{typeof(UserRegistered).FullName}", resolvedEvent.Event.EventType) - ); - - AssertThatMessages(AreDeserialized, expected, resolvedEvents); - } - - [RetryFact] - public async Task - subscribe_to_stream_deserializes_resolved_message_appended_with_manual_compatible_serialization() { - // Given - var (stream, expected) = await AppendEventsUsingManualSerialization( - message => $"stream-{message.GetType().FullName!}" - ); - - // When - var group = await CreateToStreamSubscription(stream); - var resolvedEvents = await Fixture.Subscriptions.SubscribeToStream(stream, group).Take(2) - .ToListAsync(); - - // Then - AssertThatMessages(AreDeserialized, expected, resolvedEvents); - } - - [RetryFact] - public async Task subscribe_to_all_deserializes_resolved_message_appended_with_manual_compatible_serialization() { - // Given - var (stream, expected) = await AppendEventsUsingManualSerialization( - message => $"stream-{message.GetType().FullName!}" - ); - - // When - var group = await CreateToAllSubscription(stream); - var resolvedEvents = await Fixture.Subscriptions - .SubscribeToAll(group) - .Where(r => r.Event.EventStreamId == stream) - .Take(2) - .ToListAsync(); - - // Then - AssertThatMessages(AreDeserialized, expected, resolvedEvents); - } - - [RetryFact] - public async Task - subscribe_to_stream_does_NOT_deserialize_resolved_message_appended_with_manual_incompatible_serialization() { - // Given - var (stream, expected) = await AppendEventsUsingManualSerialization(_ => "user_registered"); - - // When - var group = await CreateToStreamSubscription(stream); - var resolvedEvents = await Fixture.Subscriptions.SubscribeToStream(stream, group).Take(2) - .ToListAsync(); - - // Then - AssertThatMessages(AreNotDeserialized, expected, resolvedEvents); - } - - [RetryFact] - public async Task - subscribe_to_all_does_NOT_deserialize_resolved_message_appended_with_manual_incompatible_serialization() { - // Given - var (stream, expected) = await AppendEventsUsingManualSerialization(_ => "user_registered"); - - // When - var group = await CreateToAllSubscription(stream); - var resolvedEvents = await Fixture.Subscriptions - .SubscribeToAll(group) - .Where(r => r.Event.EventStreamId == stream) - .Take(2) - .ToListAsync(); - - // Then - AssertThatMessages(AreNotDeserialized, expected, resolvedEvents); - } - - static List AssertThatMessages( - Action assertMatches, - List expected, - List resolvedEvents - ) { - Assert.Equal(expected.Count, resolvedEvents.Count); - Assert.NotEmpty(resolvedEvents); - - Assert.All(resolvedEvents, (resolvedEvent, idx) => assertMatches(expected[idx], resolvedEvent)); - - return resolvedEvents.Select(resolvedEvent => resolvedEvent.Message!).ToList(); - } - - static void AreDeserialized(UserRegistered expected, ResolvedEvent resolvedEvent) { - Assert.NotNull(resolvedEvent.Message); - Assert.Equal(expected, resolvedEvent.Message.Data); - Assert.Equal(expected, resolvedEvent.DeserializedData); - } - - static void AreNotDeserialized(UserRegistered expected, ResolvedEvent resolvedEvent) { - Assert.Null(resolvedEvent.Message); - Assert.Equal( - expected, - JsonSerializer.Deserialize( - resolvedEvent.Event.Data.Span, - SystemTextJsonSerializationSettings.DefaultJsonSerializerOptions - ) - ); - } - - async Task<(string, List)> AppendEventsUsingAutoSerialization(KurrentClient? kurrentClient = null) { - var stream = Fixture.GetStreamName(); - var messages = GenerateMessages(); - - var writeResult = await (kurrentClient ?? Fixture.Streams).AppendToStreamAsync(stream, messages); - Assert.Equal(new((ulong)messages.Count - 1), writeResult.NextExpectedStreamRevision); - - return (stream, messages); - } - - async Task<(string, List)> AppendEventsUsingManualSerialization( - Func getTypeName - ) { - var stream = Fixture.GetStreamName(); - var messages = GenerateMessages(); - var eventData = messages.Select( - message => - new EventData( - Uuid.NewUuid(), - getTypeName(message), - Encoding.UTF8.GetBytes( - JsonSerializer.Serialize( - message, - SystemTextJsonSerializationSettings.DefaultJsonSerializerOptions - ) - ) - ) - ); - - var writeResult = await Fixture.Streams.AppendToStreamAsync(stream, StreamRevision.None, eventData); - Assert.Equal(new((ulong)messages.Count - 1), writeResult.NextExpectedStreamRevision); - - return (stream, messages); - } - - static List GenerateMessages(int count = 2) => - Enumerable.Range(0, count) - .Select( - _ => new UserRegistered( - Guid.NewGuid(), - new Address(Guid.NewGuid().ToString(), Guid.NewGuid().GetHashCode()) - ) - ).ToList(); - - KurrentClient NewClientWith(Action customizeSerialization) { - var settings = Fixture.ClientSettings; - settings.Serialization = settings.Serialization.Clone(); - customizeSerialization(settings.Serialization); - - return new KurrentClient(settings); - } - - KurrentPersistentSubscriptionsClient NewSubscriptionsClientWith( - Action customizeSerialization - ) { - var settings = Fixture.ClientSettings; - settings.Serialization = settings.Serialization.Clone(); - customizeSerialization(settings.Serialization); - - return new KurrentPersistentSubscriptionsClient(settings); - } - - async Task CreateToStreamSubscription(string stream, KurrentPersistentSubscriptionsClient? client = null) { - string group = Fixture.GetGroupName(); - - await (client ?? Fixture.Subscriptions).CreateToStreamAsync( - stream, - group, - new(startFrom: StreamPosition.Start), - userCredentials: TestCredentials.Root - ); - - return group; - } - - async Task CreateToAllSubscription(string stream, KurrentPersistentSubscriptionsClient? client = null) { - string group = Fixture.GetGroupName(); - - await (client ?? Fixture.Subscriptions).CreateToAllAsync( - group, - StreamFilter.Prefix(stream), - new(startFrom: Position.Start), - userCredentials: TestCredentials.Root - ); - - return group; - } - - public record Address(string Street, int Number); - - public record UserRegistered(Guid UserId, Address Address); - - public record CustomMetadata(Guid UserId); -} diff --git a/test/Kurrent.Client.Tests/Streams/Serialization/SerializationTests.Subscriptions.cs b/test/Kurrent.Client.Tests/Streams/Serialization/SerializationTests.Subscriptions.cs deleted file mode 100644 index 12529d6c3..000000000 --- a/test/Kurrent.Client.Tests/Streams/Serialization/SerializationTests.Subscriptions.cs +++ /dev/null @@ -1,383 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using System.Text; -using System.Text.Json; -using EventStore.Client; -using Kurrent.Client.Core.Serialization; -using Kurrent.Diagnostics.Tracing; - -namespace Kurrent.Client.Tests.Streams.Serialization; - -[Trait("Category", "Target:Streams")] -[Trait("Category", "Operation:Append")] -public class SubscriptionsSerializationTests(ITestOutputHelper output, KurrentPermanentFixture fixture) - : KurrentPermanentTests(output, fixture) { - [RetryFact] - public async Task plain_clr_objects_are_serialized_and_deserialized_using_auto_serialization() { - // Given - var stream = Fixture.GetStreamName(); - List expected = GenerateMessages(); - - //When - await Fixture.Streams.AppendToStreamAsync(stream, expected); - - //Then - var resolvedEvents = await Fixture.Streams.SubscribeToStream(stream).Take(2).ToListAsync(); - AssertThatMessages(AreDeserialized, expected, resolvedEvents); - } - - [RetryFact] - public async Task - message_data_and_metadata_are_serialized_and_deserialized_using_auto_serialization_with_registered_metadata() { - // Given - await using var client = NewClientWith(serialization => serialization.UseMetadataType()); - - var stream = Fixture.GetStreamName(); - var metadata = new CustomMetadata(Guid.NewGuid()); - var expected = GenerateMessages(); - List messagesWithMetadata = - expected.Select(message => Message.From(message, metadata, Uuid.NewUuid())).ToList(); - - // When - await client.AppendToStreamAsync(stream, messagesWithMetadata); - - // Then - var resolvedEvents = await client.SubscribeToStream(stream).Take(2).ToListAsync(); - var messages = AssertThatMessages(AreDeserialized, expected, resolvedEvents); - - Assert.Equal(messagesWithMetadata, messages); - } - - [RetryFact] - public async Task - message_metadata_is_serialized_fully_byt_deserialized_to_tracing_metadata_using_auto_serialization_WITHOUT_registered_custom_metadata() { - var stream = Fixture.GetStreamName(); - var metadata = new CustomMetadata(Guid.NewGuid()); - var expected = GenerateMessages(); - List messagesWithMetadata = - expected.Select(message => Message.From(message, metadata, Uuid.NewUuid())).ToList(); - - // When - await Fixture.Streams.AppendToStreamAsync(stream, messagesWithMetadata); - - // Then - var resolvedEvents = await Fixture.Streams.SubscribeToStream(stream).Take(2).ToListAsync(); - var messages = AssertThatMessages(AreDeserialized, expected, resolvedEvents); - - Assert.Equal(messagesWithMetadata.Select(m => m with { Metadata = new TracingMetadata() }), messages); - } - - [RetryFact] - public async Task subscribe_to_stream_without_options_does_NOT_deserialize_resolved_message() { - // Given - var (stream, expected) = await AppendEventsUsingAutoSerialization(); - - // When - var resolvedEvents = await Fixture.Streams - .SubscribeToStream(stream, FromStream.Start).Take(2) - .ToListAsync(); - - // Then - AssertThatMessages(AreNotDeserialized, expected, resolvedEvents); - } - - [RetryFact] - public async Task subscribe_to_all_without_options_does_NOT_deserialize_resolved_message() { - // Given - var (stream, expected) = await AppendEventsUsingAutoSerialization(); - - // When - var resolvedEvents = await Fixture.Streams - .SubscribeToAll(FromAll.Start, filterOptions: new SubscriptionFilterOptions(StreamFilter.Prefix(stream))) - .Take(2) - .ToListAsync(); - - // Then - AssertThatMessages(AreNotDeserialized, expected, resolvedEvents); - } - - public static TheoryData> CustomTypeMappings() { - return [ - (settings, typeName) => - settings.RegisterMessageType(typeName), - (settings, typeName) => - settings.RegisterMessageType(typeof(UserRegistered), typeName), - (settings, typeName) => - settings.RegisterMessageTypes(new Dictionary { { typeof(UserRegistered), typeName } }) - ]; - } - - [RetryTheory] - [MemberData(nameof(CustomTypeMappings))] - public async Task append_and_subscribe_to_stream_uses_custom_type_mappings( - Action customTypeMapping - ) { - // Given - await using var client = NewClientWith(serialization => customTypeMapping(serialization, "user_registered")); - - // When - var (stream, expected) = await AppendEventsUsingAutoSerialization(client); - - // Then - var resolvedEvents = await client.SubscribeToStream(stream).Take(2).ToListAsync(); - Assert.All(resolvedEvents, resolvedEvent => Assert.Equal("user_registered", resolvedEvent.Event.EventType)); - - AssertThatMessages(AreDeserialized, expected, resolvedEvents); - } - - [RetryTheory] - [MemberData(nameof(CustomTypeMappings))] - public async Task append_and_subscribe_to_all_uses_custom_type_mappings( - Action customTypeMapping - ) { - // Given - await using var client = NewClientWith(serialization => customTypeMapping(serialization, "user_registered")); - - // When - var (stream, expected) = await AppendEventsUsingAutoSerialization(client); - - // Then - var resolvedEvents = await client - .SubscribeToAll(new SubscribeToAllOptions { Filter = StreamFilter.Prefix(stream) }).Take(2) - .ToListAsync(); - - Assert.All(resolvedEvents, resolvedEvent => Assert.Equal("user_registered", resolvedEvent.Event.EventType)); - - AssertThatMessages(AreDeserialized, expected, resolvedEvents); - } - - [RetryFact] - public async Task automatic_serialization_custom_json_settings_are_applied() { - // Given - var systemTextJsonOptions = new JsonSerializerOptions { - PropertyNamingPolicy = JsonNamingPolicy.KebabCaseLower, - }; - - await using var client = NewClientWith(serialization => serialization.UseJsonSettings(systemTextJsonOptions)); - - // When - var (stream, expected) = await AppendEventsUsingAutoSerialization(client); - - // Then - var resolvedEvents = await client.SubscribeToStream(stream).Take(2).ToListAsync(); - var jsons = resolvedEvents.Select(e => JsonDocument.Parse(e.Event.Data).RootElement).ToList(); - - Assert.Equal(expected.Select(m => m.UserId), jsons.Select(j => j.GetProperty("user-id").GetGuid())); - - AssertThatMessages(AreDeserialized, expected, resolvedEvents); - } - - public class CustomMessageTypeNamingStrategy : IMessageTypeNamingStrategy { - public string ResolveTypeName(Type messageType, MessageTypeNamingResolutionContext resolutionContext) { - return $"custom-{messageType}"; - } - -#if NET48 - public bool TryResolveClrType(string messageTypeName, out Type? type) { -#else - public bool TryResolveClrType(string messageTypeName, [NotNullWhen(true)] out Type? type) { -#endif - var typeName = messageTypeName[(messageTypeName.IndexOf('-') + 1)..]; - type = Type.GetType(typeName); - - return type != null; - } - -#if NET48 - public bool TryResolveClrMetadataType(string messageTypeName, out Type? type) { -#else - public bool TryResolveClrMetadataType(string messageTypeName, [NotNullWhen(true)] out Type? type) { -#endif - type = null; - return false; - } - } - - [RetryFact] - public async Task append_and_subscribe_to_stream_uses_custom_message_type_naming_strategy() { - // Given - await using var client = NewClientWith( - serialization => serialization.UseMessageTypeNamingStrategy() - ); - - //When - var (stream, expected) = await AppendEventsUsingAutoSerialization(client); - - //Then - var resolvedEvents = await Fixture.Streams.SubscribeToStream(stream).Take(2).ToListAsync(); - Assert.All( - resolvedEvents, - resolvedEvent => Assert.Equal($"custom-{typeof(UserRegistered).FullName}", resolvedEvent.Event.EventType) - ); - - AssertThatMessages(AreDeserialized, expected, resolvedEvents); - } - - [RetryFact] - public async Task append_and_subscribe_to_all_uses_custom_message_type_naming_strategy() { - // Given - await using var client = NewClientWith( - serialization => serialization.UseMessageTypeNamingStrategy() - ); - - //When - var (stream, expected) = await AppendEventsUsingAutoSerialization(client); - - //Then - var resolvedEvents = await client - .SubscribeToAll(new SubscribeToAllOptions { Filter = StreamFilter.Prefix(stream) }).Take(2) - .ToListAsync(); - - Assert.All( - resolvedEvents, - resolvedEvent => Assert.Equal($"custom-{typeof(UserRegistered).FullName}", resolvedEvent.Event.EventType) - ); - - AssertThatMessages(AreDeserialized, expected, resolvedEvents); - } - - [RetryFact] - public async Task - subscribe_to_stream_deserializes_resolved_message_appended_with_manual_compatible_serialization() { - // Given - var (stream, expected) = await AppendEventsUsingManualSerialization( - message => $"stream-{message.GetType().FullName!}" - ); - - // When - var resolvedEvents = await Fixture.Streams.SubscribeToStream(stream).Take(2).ToListAsync(); - - // Then - AssertThatMessages(AreDeserialized, expected, resolvedEvents); - } - - [RetryFact] - public async Task subscribe_to_all_deserializes_resolved_message_appended_with_manual_compatible_serialization() { - // Given - var (stream, expected) = await AppendEventsUsingManualSerialization( - message => $"stream-{message.GetType().FullName!}" - ); - - // When - var resolvedEvents = await Fixture.Streams - .SubscribeToAll(new SubscribeToAllOptions { Filter = StreamFilter.Prefix(stream) }).Take(2) - .ToListAsync(); - - // Then - AssertThatMessages(AreDeserialized, expected, resolvedEvents); - } - - [RetryFact] - public async Task subscribe_to_stream_does_NOT_deserialize_resolved_message_appended_with_manual_incompatible_serialization() { - // Given - var (stream, expected) = await AppendEventsUsingManualSerialization(_ => "user_registered"); - - // When - var resolvedEvents = await Fixture.Streams.SubscribeToStream(stream).Take(2).ToListAsync(); - - // Then - AssertThatMessages(AreNotDeserialized, expected, resolvedEvents); - } - - [RetryFact] - public async Task - subscribe_to_all_does_NOT_deserialize_resolved_message_appended_with_manual_incompatible_serialization() { - // Given - var (stream, expected) = await AppendEventsUsingManualSerialization(_ => "user_registered"); - - // When - var resolvedEvents = await Fixture.Streams - .SubscribeToAll(new SubscribeToAllOptions { Filter = StreamFilter.Prefix(stream) }).Take(2) - .ToListAsync(); - - // Then - AssertThatMessages(AreNotDeserialized, expected, resolvedEvents); - } - - static List AssertThatMessages( - Action assertMatches, - List expected, - List resolvedEvents - ) { - Assert.Equal(expected.Count, resolvedEvents.Count); - Assert.NotEmpty(resolvedEvents); - - Assert.All(resolvedEvents, (resolvedEvent, idx) => assertMatches(expected[idx], resolvedEvent)); - - return resolvedEvents.Select(resolvedEvent => resolvedEvent.Message!).ToList(); - } - - static void AreDeserialized(UserRegistered expected, ResolvedEvent resolvedEvent) { - Assert.NotNull(resolvedEvent.Message); - Assert.Equal(expected, resolvedEvent.Message.Data); - Assert.Equal(expected, resolvedEvent.DeserializedData); - } - - static void AreNotDeserialized(UserRegistered expected, ResolvedEvent resolvedEvent) { - Assert.Null(resolvedEvent.Message); - Assert.Equal( - expected, - JsonSerializer.Deserialize( - resolvedEvent.Event.Data.Span, - SystemTextJsonSerializationSettings.DefaultJsonSerializerOptions - ) - ); - } - - async Task<(string, List)> AppendEventsUsingAutoSerialization(KurrentClient? kurrentClient = null) { - var stream = Fixture.GetStreamName(); - var messages = GenerateMessages(); - - var writeResult = await (kurrentClient ?? Fixture.Streams).AppendToStreamAsync(stream, messages); - Assert.Equal(new((ulong)messages.Count - 1), writeResult.NextExpectedStreamRevision); - - return (stream, messages); - } - - async Task<(string, List)> AppendEventsUsingManualSerialization( - Func getTypeName - ) { - var stream = Fixture.GetStreamName(); - var messages = GenerateMessages(); - var eventData = messages.Select( - message => - new EventData( - Uuid.NewUuid(), - getTypeName(message), - Encoding.UTF8.GetBytes( - JsonSerializer.Serialize( - message, - SystemTextJsonSerializationSettings.DefaultJsonSerializerOptions - ) - ) - ) - ); - - var writeResult = await Fixture.Streams.AppendToStreamAsync(stream, StreamRevision.None, eventData); - Assert.Equal(new((ulong)messages.Count - 1), writeResult.NextExpectedStreamRevision); - - return (stream, messages); - } - - static List GenerateMessages(int count = 2) => - Enumerable.Range(0, count) - .Select( - _ => new UserRegistered( - Guid.NewGuid(), - new Address(Guid.NewGuid().ToString(), Guid.NewGuid().GetHashCode()) - ) - ).ToList(); - - KurrentClient NewClientWith(Action customizeSerialization) { - var settings = Fixture.ClientSettings; - settings.Serialization = settings.Serialization.Clone(); - customizeSerialization(settings.Serialization); - - return new KurrentClient(settings); - } - - public record Address(string Street, int Number); - - public record UserRegistered(Guid UserId, Address Address); - - public record CustomMetadata(Guid UserId); -} diff --git a/test/Kurrent.Client.Tests/Streams/Serialization/SerializationTests.cs b/test/Kurrent.Client.Tests/Streams/Serialization/SerializationTests.cs deleted file mode 100644 index 06224c227..000000000 --- a/test/Kurrent.Client.Tests/Streams/Serialization/SerializationTests.cs +++ /dev/null @@ -1,376 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using System.Text; -using System.Text.Json; -using EventStore.Client; -using Kurrent.Client.Core.Serialization; -using Kurrent.Diagnostics.Tracing; - -namespace Kurrent.Client.Tests.Streams.Serialization; - -[Trait("Category", "Target:Streams")] -[Trait("Category", "Operation:Append")] -public class SerializationTests(ITestOutputHelper output, KurrentPermanentFixture fixture) - : KurrentPermanentTests(output, fixture) { - [RetryFact] - public async Task plain_clr_objects_are_serialized_and_deserialized_using_auto_serialization() { - // Given - var stream = Fixture.GetStreamName(); - List expected = GenerateMessages(); - - //When - await Fixture.Streams.AppendToStreamAsync(stream, expected); - - //Then - var resolvedEvents = await Fixture.Streams.ReadStreamAsync(stream).ToListAsync(); - AssertThatMessages(AreDeserialized, expected, resolvedEvents); - } - - [RetryFact] - public async Task - message_data_and_metadata_are_serialized_and_deserialized_using_auto_serialization_with_registered_metadata() { - // Given - await using var client = NewClientWith(serialization => serialization.UseMetadataType()); - - var stream = Fixture.GetStreamName(); - var metadata = new CustomMetadata(Guid.NewGuid()); - var expected = GenerateMessages(); - List messagesWithMetadata = - expected.Select(message => Message.From(message, metadata, Uuid.NewUuid())).ToList(); - - // When - await client.AppendToStreamAsync(stream, messagesWithMetadata); - - // Then - var resolvedEvents = await client.ReadStreamAsync(stream).ToListAsync(); - var messages = AssertThatMessages(AreDeserialized, expected, resolvedEvents); - - Assert.Equal(messagesWithMetadata, messages); - } - - [RetryFact] - public async Task - message_metadata_is_serialized_fully_byt_deserialized_to_tracing_metadata_using_auto_serialization_WITHOUT_registered_custom_metadata() { - var stream = Fixture.GetStreamName(); - var metadata = new CustomMetadata(Guid.NewGuid()); - var expected = GenerateMessages(); - List messagesWithMetadata = - expected.Select(message => Message.From(message, metadata, Uuid.NewUuid())).ToList(); - - // When - await Fixture.Streams.AppendToStreamAsync(stream, messagesWithMetadata); - - // Then - var resolvedEvents = await Fixture.Streams.ReadStreamAsync(stream).ToListAsync(); - var messages = AssertThatMessages(AreDeserialized, expected, resolvedEvents); - - Assert.Equal(messagesWithMetadata.Select(m => m with { Metadata = new TracingMetadata() }), messages); - } - - [RetryFact] - public async Task read_stream_without_options_does_NOT_deserialize_resolved_message() { - // Given - var (stream, expected) = await AppendEventsUsingAutoSerialization(); - - // When - var resolvedEvents = await Fixture.Streams - .ReadStreamAsync(Direction.Forwards, stream, StreamPosition.Start) - .ToListAsync(); - - // Then - AssertThatMessages(AreNotDeserialized, expected, resolvedEvents); - } - - [RetryFact] - public async Task read_all_without_options_does_NOT_deserialize_resolved_message() { - // Given - var (stream, expected) = await AppendEventsUsingAutoSerialization(); - - // When - var resolvedEvents = await Fixture.Streams - .ReadAllAsync(Direction.Forwards, Position.Start, StreamFilter.Prefix(stream)) - .ToListAsync(); - - // Then - AssertThatMessages(AreNotDeserialized, expected, resolvedEvents); - } - - public static TheoryData> CustomTypeMappings() { - return [ - (settings, typeName) => - settings.RegisterMessageType(typeName), - (settings, typeName) => - settings.RegisterMessageType(typeof(UserRegistered), typeName), - (settings, typeName) => - settings.RegisterMessageTypes(new Dictionary { { typeof(UserRegistered), typeName } }) - ]; - } - - [RetryTheory] - [MemberData(nameof(CustomTypeMappings))] - public async Task append_and_read_stream_uses_custom_type_mappings( - Action customTypeMapping - ) { - // Given - await using var client = NewClientWith(serialization => customTypeMapping(serialization, "user_registered")); - - // When - var (stream, expected) = await AppendEventsUsingAutoSerialization(client); - - // Then - var resolvedEvents = await client.ReadStreamAsync(stream).ToListAsync(); - Assert.All(resolvedEvents, resolvedEvent => Assert.Equal("user_registered", resolvedEvent.Event.EventType)); - - AssertThatMessages(AreDeserialized, expected, resolvedEvents); - } - - [RetryTheory] - [MemberData(nameof(CustomTypeMappings))] - public async Task append_and_read_all_uses_custom_type_mappings( - Action customTypeMapping - ) { - // Given - await using var client = NewClientWith(serialization => customTypeMapping(serialization, "user_registered")); - - // When - var (stream, expected) = await AppendEventsUsingAutoSerialization(client); - - // Then - var resolvedEvents = await client - .ReadAllAsync(new ReadAllOptions { Filter = StreamFilter.Prefix(stream) }) - .ToListAsync(); - - Assert.All(resolvedEvents, resolvedEvent => Assert.Equal("user_registered", resolvedEvent.Event.EventType)); - - AssertThatMessages(AreDeserialized, expected, resolvedEvents); - } - - [RetryFact] - public async Task automatic_serialization_custom_json_settings_are_applied() { - // Given - var systemTextJsonOptions = new JsonSerializerOptions { - PropertyNamingPolicy = JsonNamingPolicy.KebabCaseLower, - }; - - await using var client = NewClientWith(serialization => serialization.UseJsonSettings(systemTextJsonOptions)); - - // When - var (stream, expected) = await AppendEventsUsingAutoSerialization(client); - - // Then - var resolvedEvents = await client.ReadStreamAsync(stream).ToListAsync(); - var jsons = resolvedEvents.Select(e => JsonDocument.Parse(e.Event.Data).RootElement).ToList(); - - Assert.Equal(expected.Select(m => m.UserId), jsons.Select(j => j.GetProperty("user-id").GetGuid())); - - AssertThatMessages(AreDeserialized, expected, resolvedEvents); - } - - public class CustomMessageTypeNamingStrategy : IMessageTypeNamingStrategy { - public string ResolveTypeName(Type messageType, MessageTypeNamingResolutionContext resolutionContext) { - return $"custom-{messageType}"; - } - -#if NET48 - public bool TryResolveClrType(string messageTypeName, out Type? type) { -#else - public bool TryResolveClrType(string messageTypeName, [NotNullWhen(true)] out Type? type) { -#endif - var typeName = messageTypeName[(messageTypeName.IndexOf('-') + 1)..]; - type = Type.GetType(typeName); - - return type != null; - } - -#if NET48 - public bool TryResolveClrMetadataType(string messageTypeName, out Type? type) { -#else - public bool TryResolveClrMetadataType(string messageTypeName, [NotNullWhen(true)] out Type? type) { -#endif - type = null; - return false; - } - } - - [RetryFact] - public async Task append_and_read_stream_uses_custom_message_type_naming_strategy() { - // Given - await using var client = NewClientWith( - serialization => serialization.UseMessageTypeNamingStrategy() - ); - - //When - var (stream, expected) = await AppendEventsUsingAutoSerialization(client); - - //Then - var resolvedEvents = await Fixture.Streams.ReadStreamAsync(stream).ToListAsync(); - Assert.All(resolvedEvents, resolvedEvent => Assert.Equal($"custom-{typeof(UserRegistered).FullName}", resolvedEvent.Event.EventType)); - AssertThatMessages(AreDeserialized, expected, resolvedEvents); - } - - [RetryFact] - public async Task append_and_read_all_uses_custom_message_type_naming_strategy() { - // Given - await using var client = NewClientWith( - serialization => serialization.UseMessageTypeNamingStrategy() - ); - - //When - var (stream, expected) = await AppendEventsUsingAutoSerialization(client); - - //Then - var resolvedEvents = await client - .ReadAllAsync(new ReadAllOptions { Filter = StreamFilter.Prefix(stream) }) - .ToListAsync(); - - Assert.All(resolvedEvents, resolvedEvent => Assert.Equal($"custom-{typeof(UserRegistered).FullName}", resolvedEvent.Event.EventType)); - AssertThatMessages(AreDeserialized, expected, resolvedEvents); - } - - [RetryFact] - public async Task read_stream_deserializes_resolved_message_appended_with_manual_compatible_serialization() { - // Given - var (stream, expected) = await AppendEventsUsingManualSerialization( - message => $"stream-{message.GetType().FullName!}" - ); - - // When - var resolvedEvents = await Fixture.Streams - .ReadStreamAsync(stream) - .ToListAsync(); - - // Then - AssertThatMessages(AreDeserialized, expected, resolvedEvents); - } - - [RetryFact] - public async Task read_all_deserializes_resolved_message_appended_with_manual_compatible_serialization() { - // Given - var (stream, expected) = await AppendEventsUsingManualSerialization( - message => $"stream-{message.GetType().FullName!}" - ); - - // When - var resolvedEvents = await Fixture.Streams - .ReadAllAsync(new ReadAllOptions { Filter = StreamFilter.Prefix(stream) }) - .ToListAsync(); - - // Then - AssertThatMessages(AreDeserialized, expected, resolvedEvents); - } - - [RetryFact] - public async Task read_stream_does_NOT_deserialize_resolved_message_appended_with_manual_incompatible_serialization() { - // Given - var (stream, expected) = await AppendEventsUsingManualSerialization(_ => "user_registered"); - - // When - var resolvedEvents = await Fixture.Streams - .ReadStreamAsync(stream) - .ToListAsync(); - - // Then - AssertThatMessages(AreNotDeserialized, expected, resolvedEvents); - } - - [RetryFact] - public async Task read_all_does_NOT_deserialize_resolved_message_appended_with_manual_incompatible_serialization() { - // Given - var (stream, expected) = await AppendEventsUsingManualSerialization(_ => "user_registered"); - - // When - var resolvedEvents = await Fixture.Streams - .ReadAllAsync(new ReadAllOptions { Filter = StreamFilter.Prefix(stream) }) - .ToListAsync(); - - // Then - AssertThatMessages(AreNotDeserialized, expected, resolvedEvents); - } - - static List AssertThatMessages( - Action assertMatches, - List expected, - List resolvedEvents - ) { - Assert.Equal(expected.Count, resolvedEvents.Count); - Assert.NotEmpty(resolvedEvents); - - Assert.All(resolvedEvents, (resolvedEvent, idx) => assertMatches(expected[idx], resolvedEvent)); - - return resolvedEvents.Select(resolvedEvent => resolvedEvent.Message!).ToList(); - } - - static void AreDeserialized(UserRegistered expected, ResolvedEvent resolvedEvent) { - Assert.NotNull(resolvedEvent.Message); - Assert.Equal(expected, resolvedEvent.Message.Data); - Assert.Equal(expected, resolvedEvent.DeserializedData); - } - - static void AreNotDeserialized(UserRegistered expected, ResolvedEvent resolvedEvent) { - Assert.Null(resolvedEvent.Message); - Assert.Equal( - expected, - JsonSerializer.Deserialize( - resolvedEvent.Event.Data.Span, - SystemTextJsonSerializationSettings.DefaultJsonSerializerOptions - ) - ); - } - - async Task<(string, List)> AppendEventsUsingAutoSerialization(KurrentClient? kurrentClient = null) { - var stream = Fixture.GetStreamName(); - var messages = GenerateMessages(); - - var writeResult = await (kurrentClient ?? Fixture.Streams).AppendToStreamAsync(stream, messages); - Assert.Equal(new((ulong)messages.Count - 1), writeResult.NextExpectedStreamRevision); - - return (stream, messages); - } - - async Task<(string, List)> AppendEventsUsingManualSerialization( - Func getTypeName - ) { - var stream = Fixture.GetStreamName(); - var messages = GenerateMessages(); - var eventData = messages.Select( - message => - new EventData( - Uuid.NewUuid(), - getTypeName(message), - Encoding.UTF8.GetBytes( - JsonSerializer.Serialize( - message, - SystemTextJsonSerializationSettings.DefaultJsonSerializerOptions - ) - ) - ) - ); - - var writeResult = await Fixture.Streams.AppendToStreamAsync(stream, StreamRevision.None, eventData); - Assert.Equal(new((ulong)messages.Count - 1), writeResult.NextExpectedStreamRevision); - - return (stream, messages); - } - - static List GenerateMessages(int count = 2) => - Enumerable.Range(0, count) - .Select( - _ => new UserRegistered( - Guid.NewGuid(), - new Address(Guid.NewGuid().ToString(), Guid.NewGuid().GetHashCode()) - ) - ).ToList(); - - KurrentClient NewClientWith(Action customizeSerialization) { - var settings = Fixture.ClientSettings; - settings.Serialization = settings.Serialization.Clone(); - customizeSerialization(settings.Serialization); - - return new KurrentClient(settings); - } - - public record Address(string Street, int Number); - - public record UserRegistered(Guid UserId, Address Address); - - public record CustomMetadata(Guid UserId); -}