From 9d15ba23834daee4833979779c0ae73358dcd334 Mon Sep 17 00:00:00 2001 From: Enes Hoxha Date: Tue, 21 Oct 2025 01:10:23 +0200 Subject: [PATCH 1/6] v2/feature/ [146]: Introduction of Cortex.Serialization.Yaml Add YAML serialization/deserialization library Introduced `Cortex.Serialization.Yaml`, a new library for YAML serialization and deserialization. Added `YamlDeserializer` and `YamlSerializer` classes with support for custom naming conventions, type converters, and flexible property handling. Implemented attributes (`YamlIgnoreAttribute`, `YamlPropertyAttribute`) for customization and `YamlException` for error handling. Added settings classes (`YamlDeserializerSettings`, `YamlSerializerSettings`) to configure behavior. Introduced `INamingConvention` and implementations (e.g., `CamelCaseConvention`, `SnakeCaseConvention`) for property name transformations. Implemented a YAML parsing system with `Scanner`, `Parser`, and `Emitter` components, along with utility classes for reflection (`CachedTypeInfo`, `PropertyMap`, `TypeInspector`) and string transformations (`StringUtils`). Added `YamlConverterAttribute` for associating custom type converters. --- Cortex.sln | 45 +- .../Attributes/YamlIgnoreAttribute.cs | 45 ++ .../Attributes/YamlPropertyAttribute.cs | 47 ++ .../Common/StringUtils.cs | 25 ++ .../Common/YamlException.cs | 87 ++++ .../Converters/INamingConvention.cs | 7 + .../Converters/IYamlTypeConverter.cs | 9 + .../Converters/PrimitiveConverter.cs | 35 ++ .../Cortex.Serialization.Yaml.csproj | 9 + .../Emitter/Emitter.cs | 78 ++++ .../Parser/Parser.cs | 159 +++++++ .../Parser/Scanner.cs | 85 ++++ src/Cortex.Serialization.Yaml/Parser/Token.cs | 8 + .../Parser/TokenType.cs | 22 + .../Reflection/CachedTypeInfo.cs | 8 + .../Reflection/PropertyMap.cs | 44 ++ .../Reflection/TypeInspector.cs | 23 + .../Converters/CamelCaseConvention.cs | 10 + .../Converters/KebabCaseConvention.cs | 7 + .../Converters/OriginalCaseConvention.cs | 7 + .../Converters/PascalCaseConvention.cs | 13 + .../Converters/SnakeCaseConvention.cs | 7 + .../Converters/YamlConverterAttribute.cs | 9 + .../Serialization/YamlDeserializerSettings.cs | 139 ++++++ .../Serialization/YamlSerializerSettings.cs | 263 +++++++++++ .../YamlDeserializer.cs | 410 ++++++++++++++++++ .../YamlSerializer.cs | 247 +++++++++++ 27 files changed, 1846 insertions(+), 2 deletions(-) create mode 100644 src/Cortex.Serialization.Yaml/Attributes/YamlIgnoreAttribute.cs create mode 100644 src/Cortex.Serialization.Yaml/Attributes/YamlPropertyAttribute.cs create mode 100644 src/Cortex.Serialization.Yaml/Common/StringUtils.cs create mode 100644 src/Cortex.Serialization.Yaml/Common/YamlException.cs create mode 100644 src/Cortex.Serialization.Yaml/Converters/INamingConvention.cs create mode 100644 src/Cortex.Serialization.Yaml/Converters/IYamlTypeConverter.cs create mode 100644 src/Cortex.Serialization.Yaml/Converters/PrimitiveConverter.cs create mode 100644 src/Cortex.Serialization.Yaml/Cortex.Serialization.Yaml.csproj create mode 100644 src/Cortex.Serialization.Yaml/Emitter/Emitter.cs create mode 100644 src/Cortex.Serialization.Yaml/Parser/Parser.cs create mode 100644 src/Cortex.Serialization.Yaml/Parser/Scanner.cs create mode 100644 src/Cortex.Serialization.Yaml/Parser/Token.cs create mode 100644 src/Cortex.Serialization.Yaml/Parser/TokenType.cs create mode 100644 src/Cortex.Serialization.Yaml/Reflection/CachedTypeInfo.cs create mode 100644 src/Cortex.Serialization.Yaml/Reflection/PropertyMap.cs create mode 100644 src/Cortex.Serialization.Yaml/Reflection/TypeInspector.cs create mode 100644 src/Cortex.Serialization.Yaml/Serialization/Converters/CamelCaseConvention.cs create mode 100644 src/Cortex.Serialization.Yaml/Serialization/Converters/KebabCaseConvention.cs create mode 100644 src/Cortex.Serialization.Yaml/Serialization/Converters/OriginalCaseConvention.cs create mode 100644 src/Cortex.Serialization.Yaml/Serialization/Converters/PascalCaseConvention.cs create mode 100644 src/Cortex.Serialization.Yaml/Serialization/Converters/SnakeCaseConvention.cs create mode 100644 src/Cortex.Serialization.Yaml/Serialization/Converters/YamlConverterAttribute.cs create mode 100644 src/Cortex.Serialization.Yaml/Serialization/YamlDeserializerSettings.cs create mode 100644 src/Cortex.Serialization.Yaml/Serialization/YamlSerializerSettings.cs create mode 100644 src/Cortex.Serialization.Yaml/YamlDeserializer.cs create mode 100644 src/Cortex.Serialization.Yaml/YamlSerializer.cs diff --git a/Cortex.sln b/Cortex.sln index de5bc21..4c8bdf3 100644 --- a/Cortex.sln +++ b/Cortex.sln @@ -1,6 +1,6 @@ Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.10.34607.79 +# Visual Studio Version 18 +VisualStudioVersion = 18.0.11111.16 d18.0 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Cortex.Mediator", "src\Cortex.Mediator\Cortex.Mediator.csproj", "{F1CC775A-95DA-4A5A-879F-66BFCB0FDCC9}" EndProject @@ -60,6 +60,16 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Cortex.Vectors", "src\Corte EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Cortex.Mediator.Behaviors.FluentValidation", "src\Cortex.Mediator.Behaviors.FluentValidation\Cortex.Mediator.Behaviors.FluentValidation.csproj", "{44A166BD-01E9-4A4B-9BC5-7DE01B472E73}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Cortex.Serialization.Yaml", "src\Cortex.Serialization.Yaml\Cortex.Serialization.Yaml.csproj", "{472BC645-9E2F-4205-A571-4D9184747EC5}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Mediator", "Mediator", "{1C5D462D-168D-4D3F-B96E-CCE5517DB197}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Streams", "Streams", "{4C68702C-1661-4AD9-83FD-E0B52B791969}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "States", "States", "{C31F8C0F-8BCF-4959-9BA1-8645D058EAA0}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Serialization", "Serialization", "{7F9E0AEA-721E-46F8-90ED-8EA8423647FB}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -181,10 +191,41 @@ Global {44A166BD-01E9-4A4B-9BC5-7DE01B472E73}.Debug|Any CPU.Build.0 = Debug|Any CPU {44A166BD-01E9-4A4B-9BC5-7DE01B472E73}.Release|Any CPU.ActiveCfg = Release|Any CPU {44A166BD-01E9-4A4B-9BC5-7DE01B472E73}.Release|Any CPU.Build.0 = Release|Any CPU + {472BC645-9E2F-4205-A571-4D9184747EC5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {472BC645-9E2F-4205-A571-4D9184747EC5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {472BC645-9E2F-4205-A571-4D9184747EC5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {472BC645-9E2F-4205-A571-4D9184747EC5}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {F1CC775A-95DA-4A5A-879F-66BFCB0FDCC9} = {1C5D462D-168D-4D3F-B96E-CCE5517DB197} + {1C8605F4-91CB-49A4-A080-0A6DFE1FB010} = {4C68702C-1661-4AD9-83FD-E0B52B791969} + {96701658-663E-41C5-9EF9-843C79CE727A} = {4C68702C-1661-4AD9-83FD-E0B52B791969} + {EE799E10-7469-428E-AF8C-F2807F6CE7E5} = {4C68702C-1661-4AD9-83FD-E0B52B791969} + {34CA231A-1E3A-4CA4-820C-946DC8E2737F} = {C31F8C0F-8BCF-4959-9BA1-8645D058EAA0} + {16FA00B1-973B-443C-BF84-9B76DCAF341B} = {C31F8C0F-8BCF-4959-9BA1-8645D058EAA0} + {1F60F66A-5DC0-4C1D-A7B7-66F568A26911} = {4C68702C-1661-4AD9-83FD-E0B52B791969} + {2B949637-FC31-4F51-A391-19F76B57CC28} = {4C68702C-1661-4AD9-83FD-E0B52B791969} + {6E5AC0AC-A364-4DB3-9E9A-C2FB54BD6D1E} = {4C68702C-1661-4AD9-83FD-E0B52B791969} + {6019F9E6-C377-416D-9E3B-3D7104FAEB63} = {4C68702C-1661-4AD9-83FD-E0B52B791969} + {20FAE1F1-D677-4E0E-B3A7-E2B1485C874C} = {4C68702C-1661-4AD9-83FD-E0B52B791969} + {D376D6CA-3192-4EDC-B840-31F58B6457DD} = {4C68702C-1661-4AD9-83FD-E0B52B791969} + {447970B9-C5AA-41D9-A07F-330A251597D0} = {C31F8C0F-8BCF-4959-9BA1-8645D058EAA0} + {00358701-D117-4953-A673-D60625D38466} = {C31F8C0F-8BCF-4959-9BA1-8645D058EAA0} + {77AD462F-A248-43AF-9212-43031F22F23D} = {C31F8C0F-8BCF-4959-9BA1-8645D058EAA0} + {980EDBFE-40C2-4EFD-96C2-FED1032FB5E6} = {C31F8C0F-8BCF-4959-9BA1-8645D058EAA0} + {0F9FCB99-D00F-4396-8E2B-6E627076ADA0} = {C31F8C0F-8BCF-4959-9BA1-8645D058EAA0} + {20BD7107-8199-4CA8-815B-4D156B522B82} = {4C68702C-1661-4AD9-83FD-E0B52B791969} + {19167D25-6383-46B4-9449-B9E364F809FF} = {C31F8C0F-8BCF-4959-9BA1-8645D058EAA0} + {81A01446-A8AA-4F9D-BB9B-B66E21B2C348} = {4C68702C-1661-4AD9-83FD-E0B52B791969} + {0E60F75D-C44B-428A-9252-A11C365E2C56} = {4C68702C-1661-4AD9-83FD-E0B52B791969} + {FC86D3AB-778D-45D7-AF36-1F89FC16DE55} = {4C68702C-1661-4AD9-83FD-E0B52B791969} + {4D1F117D-48D7-47AD-9DAC-3B2DB45E628A} = {4C68702C-1661-4AD9-83FD-E0B52B791969} + {44A166BD-01E9-4A4B-9BC5-7DE01B472E73} = {1C5D462D-168D-4D3F-B96E-CCE5517DB197} + {472BC645-9E2F-4205-A571-4D9184747EC5} = {7F9E0AEA-721E-46F8-90ED-8EA8423647FB} + EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {E20303B6-8AC9-4FFF-B645-4608309ADA94} EndGlobalSection diff --git a/src/Cortex.Serialization.Yaml/Attributes/YamlIgnoreAttribute.cs b/src/Cortex.Serialization.Yaml/Attributes/YamlIgnoreAttribute.cs new file mode 100644 index 0000000..0aff3e4 --- /dev/null +++ b/src/Cortex.Serialization.Yaml/Attributes/YamlIgnoreAttribute.cs @@ -0,0 +1,45 @@ +namespace Cortex.Serialization.Yaml.Attributes +{ + /// + /// Indicates that a field or property should be ignored during YAML serialization and deserialization. + /// + /// + /// When applied to a field or property, this attribute instructs the YAML serializer to completely + /// exclude the member from both serialization (writing to YAML) and deserialization (reading from YAML). + /// This is useful for: + /// + /// Excluding computed or derived properties that shouldn't be persisted + /// Ignoring internal state or temporary fields that are irrelevant to serialization + /// Preventing sensitive data (like passwords or tokens) from being serialized + /// Omitting properties that would cause circular references during serialization + /// + /// + /// + /// The following example shows how to use YamlIgnoreAttribute to exclude properties from YAML serialization: + /// + /// public class UserSettings + /// { + /// public string UserName { get; set; } + /// public string Email { get; set; } + /// + /// [YamlIgnore] + /// public string PasswordHash { get; set; } + /// + /// [YamlIgnore] + /// public DateTime LastAccess { get; set; } // Computed property + /// + /// // This property will be serialized normally + /// public bool IsActive { get; set; } + /// } + /// + /// When serialized to YAML, the output will not include the PasswordHash and LastAccess properties: + /// + /// UserName: john_doe + /// Email: john@example.com + /// IsActive: true + /// + /// + /// + [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] + public sealed class YamlIgnoreAttribute : Attribute { } +} \ No newline at end of file diff --git a/src/Cortex.Serialization.Yaml/Attributes/YamlPropertyAttribute.cs b/src/Cortex.Serialization.Yaml/Attributes/YamlPropertyAttribute.cs new file mode 100644 index 0000000..22fe72e --- /dev/null +++ b/src/Cortex.Serialization.Yaml/Attributes/YamlPropertyAttribute.cs @@ -0,0 +1,47 @@ +namespace Cortex.Serialization.Yaml.Attributes +{ + /// + /// Specifies a custom YAML property name for a field or property during serialization and deserialization. + /// + /// + /// When applied to a field or property, this attribute allows you to specify an alternative name + /// that should be used in the YAML representation, different from the actual member name in code. + /// This is particularly useful for: + /// + /// Mapping between different naming conventions (e.g., camelCase in YAML and PascalCase in C#) + /// Using YAML property names that are not valid C# identifiers + /// Maintaining compatibility with existing YAML schemas + /// + /// + /// + /// The following example shows how to use YamlPropertyAttribute to specify custom YAML property names: + /// + /// public class SerializableObject + /// { + /// [YamlProperty(Name = "output-format")] + /// public string OutputFormat { get; set; } + /// + /// [YamlProperty(Name = "maxItems")] + /// public int MaximumItems { get; set; } + /// } + /// + /// This will serialize to YAML as: + /// + /// output-format: SomeValue + /// maxItems: 42 + /// + /// + [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] + public sealed class YamlPropertyAttribute : Attribute + { + /// + /// Gets the custom name to use for the YAML property. + /// If null or empty, the default name (the member name) will be used. + /// + /// + /// A string representing the custom property name to use in YAML serialization, + /// or null to use the default behavior. + /// + public string? Name { get; init; } + } +} \ No newline at end of file diff --git a/src/Cortex.Serialization.Yaml/Common/StringUtils.cs b/src/Cortex.Serialization.Yaml/Common/StringUtils.cs new file mode 100644 index 0000000..79a7df0 --- /dev/null +++ b/src/Cortex.Serialization.Yaml/Common/StringUtils.cs @@ -0,0 +1,25 @@ +namespace Cortex.Serialization.Yaml.Common +{ + internal static class StringUtils + { + public static string ToSnakeCase(string s) + { + if (string.IsNullOrEmpty(s)) return s; + var sb = new System.Text.StringBuilder(); + for (int i = 0; i < s.Length; i++) + { + var c = s[i]; + if (char.IsUpper(c)) + { + if (i > 0) sb.Append('_'); + sb.Append(char.ToLowerInvariant(c)); + } + else + sb.Append(c); + } + return sb.ToString(); + } + public static string ToKebabCase(string s) => ToSnakeCase(s).Replace('_', '-'); + public static string ToCamelCase(string s) => string.IsNullOrEmpty(s) || !char.IsUpper(s[0]) ? s : char.ToLowerInvariant(s[0]) + s[1..]; + } +} diff --git a/src/Cortex.Serialization.Yaml/Common/YamlException.cs b/src/Cortex.Serialization.Yaml/Common/YamlException.cs new file mode 100644 index 0000000..0f96f6f --- /dev/null +++ b/src/Cortex.Serialization.Yaml/Common/YamlException.cs @@ -0,0 +1,87 @@ +namespace Cortex.Serialization.Yaml.Common +{ + /// + /// The exception that is thrown when an error occurs during YAML serialization or deserialization. + /// + /// + /// + /// This exception provides detailed information about YAML processing errors, including the specific + /// location in the YAML document where the error occurred. The and + /// properties indicate the position in the YAML source, making it easier to diagnose and fix issues + /// in YAML files. + /// + /// + /// Common scenarios that may throw a include: + /// + /// Invalid YAML syntax or formatting errors + /// Type conversion failures during deserialization + /// Missing required properties or invalid property values + /// Circular references that cannot be serialized + /// I/O errors when reading from or writing to streams + /// + /// + /// + /// + /// The following example shows how to catch and handle a YamlException: + /// + /// try + /// { + /// var obj = YamlSerializer.Deserialize<MyClass>(yamlString); + /// } + /// catch (YamlException ex) + /// { + /// Console.WriteLine($"YAML Error at line {ex.Line}, column {ex.Column}:"); + /// Console.WriteLine(ex.Message); + /// + /// if (ex.InnerException != null) + /// { + /// Console.WriteLine($"Inner exception: {ex.InnerException.Message}"); + /// } + /// } + /// + /// Example error message format: + /// + /// "Expected scalar value but found sequence (line 5, col 12)" + /// + /// + /// + public class YamlException : Exception + { + /// + /// Gets the line number in the YAML document where the error occurred. + /// + /// + /// The one-based line number where the error was detected. Returns 0 if the line number + /// is not available or applicable to the specific error. + /// + public int Line { get; } + + /// + /// Gets the column position in the YAML document where the error occurred. + /// + /// + /// The one-based column number where the error was detected. Returns 0 if the column position + /// is not available or applicable to the specific error. + /// + public int Column { get; } + + /// + /// Initializes a new instance of the class with a specified error message, + /// line and column numbers, and an optional inner exception. + /// + /// The error message that explains the reason for the exception. + /// The line number in the YAML document where the error occurred. + /// The column position in the YAML document where the error occurred. + /// The exception that is the cause of the current exception, or null if no inner exception is specified. + /// + /// The exception message is automatically formatted to include the line and column information + /// in the format: "{message} (line {line}, col {column})". + /// + public YamlException(string message, int line = 0, int column = 0, Exception? inner = null) + : base($"{message} (line {line}, col {column})", inner) + { + Line = line; + Column = column; + } + } +} \ No newline at end of file diff --git a/src/Cortex.Serialization.Yaml/Converters/INamingConvention.cs b/src/Cortex.Serialization.Yaml/Converters/INamingConvention.cs new file mode 100644 index 0000000..ed9dc8b --- /dev/null +++ b/src/Cortex.Serialization.Yaml/Converters/INamingConvention.cs @@ -0,0 +1,7 @@ +namespace Cortex.Serialization.Yaml.Converters +{ + public interface INamingConvention + { + string Convert(string name); + } +} diff --git a/src/Cortex.Serialization.Yaml/Converters/IYamlTypeConverter.cs b/src/Cortex.Serialization.Yaml/Converters/IYamlTypeConverter.cs new file mode 100644 index 0000000..24cc891 --- /dev/null +++ b/src/Cortex.Serialization.Yaml/Converters/IYamlTypeConverter.cs @@ -0,0 +1,9 @@ +namespace Cortex.Serialization.Yaml.Converters +{ + public interface IYamlTypeConverter + { + bool CanConvert(Type t); + object? Read(object? yamlNode, Type targetType); + object? Write(object? clrObject, Type declaredType); + } +} \ No newline at end of file diff --git a/src/Cortex.Serialization.Yaml/Converters/PrimitiveConverter.cs b/src/Cortex.Serialization.Yaml/Converters/PrimitiveConverter.cs new file mode 100644 index 0000000..b2490ae --- /dev/null +++ b/src/Cortex.Serialization.Yaml/Converters/PrimitiveConverter.cs @@ -0,0 +1,35 @@ +namespace Cortex.Serialization.Yaml.Converters +{ + public sealed class PrimitiveConverter : IYamlTypeConverter + { + public bool CanConvert(Type t) => t.IsPrimitive || t == typeof(string) || t == typeof(decimal) || t == typeof(DateTime) || t == typeof(Guid) || t == typeof(DateOnly) || t == typeof(TimeOnly); + public object? Read(object? yamlNode, Type targetType) + { + if (yamlNode is null) + return null; + if (targetType == typeof(string)) + return yamlNode.ToString(); + if (targetType == typeof(bool)) + return yamlNode is bool b ? b : bool.Parse(yamlNode.ToString()!); + if (targetType == typeof(int)) + return Convert.ToInt32(yamlNode, System.Globalization.CultureInfo.InvariantCulture); + if (targetType == typeof(long)) + return Convert.ToInt64(yamlNode, System.Globalization.CultureInfo.InvariantCulture); + if (targetType == typeof(double)) + return Convert.ToDouble(yamlNode, System.Globalization.CultureInfo.InvariantCulture); + if (targetType == typeof(decimal)) + return Convert.ToDecimal(yamlNode, System.Globalization.CultureInfo.InvariantCulture); + if (targetType == typeof(Guid)) + return Guid.Parse(yamlNode.ToString()!); + if (targetType == typeof(DateTime)) + return DateTime.Parse(yamlNode.ToString()!, System.Globalization.CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.RoundtripKind); + if (targetType == typeof(DateOnly)) + return DateOnly.Parse(yamlNode.ToString()!, System.Globalization.CultureInfo.InvariantCulture); + if (targetType == typeof(TimeOnly)) + return TimeOnly.Parse(yamlNode.ToString()!, System.Globalization.CultureInfo.InvariantCulture); + + return yamlNode; + } + public object? Write(object? clrObject, Type declaredType) => clrObject; + } +} diff --git a/src/Cortex.Serialization.Yaml/Cortex.Serialization.Yaml.csproj b/src/Cortex.Serialization.Yaml/Cortex.Serialization.Yaml.csproj new file mode 100644 index 0000000..125f4c9 --- /dev/null +++ b/src/Cortex.Serialization.Yaml/Cortex.Serialization.Yaml.csproj @@ -0,0 +1,9 @@ + + + + net9.0 + enable + enable + + + diff --git a/src/Cortex.Serialization.Yaml/Emitter/Emitter.cs b/src/Cortex.Serialization.Yaml/Emitter/Emitter.cs new file mode 100644 index 0000000..324a767 --- /dev/null +++ b/src/Cortex.Serialization.Yaml/Emitter/Emitter.cs @@ -0,0 +1,78 @@ +using Cortex.Serialization.Yaml.Parser; +using System.Text; + +namespace Cortex.Serialization.Yaml.Emitter +{ + internal sealed class Emitter + { + private readonly StringBuilder _sb = new(); + private readonly int _indentSize; + + public Emitter(int indentSize = 2) => _indentSize = indentSize; + public string Emit(YamlNode node) + { + WriteNode(node, 0); + return _sb.ToString(); + } + + private void Indent(int level) => _sb.Append(' ', _indentSize * level); + + private void WriteNode(YamlNode node, int level) + { + switch (node) + { + case YamlScalar s: WriteScalar(s.Value); _sb.Append('\n'); break; + case YamlSequence seq: + foreach (var item in seq.Items) + { + Indent(level); _sb.Append("- "); + if (item is YamlScalar sc) + { + WriteScalar(sc.Value); + _sb.Append('\n'); + } + else + { + _sb.Append('\n'); + WriteNode(item, level + 1); + } + } + break; + case YamlMapping map: + foreach (var kvp in map.Entries) + { + Indent(level); _sb.Append(kvp.Key).Append(": "); + if (kvp.Value is YamlScalar sv) { WriteScalar(sv.Value); _sb.Append('\n'); } + else { _sb.Append('\n'); WriteNode(kvp.Value, level + 1); } + } + break; + } + } + private void WriteScalar(object? value) + { + if (value is null) + { + _sb.Append("null"); + return; + } + if (value is bool b) + { + _sb.Append(b ? "true" : "false"); + return; + } + if (value is string s) + { + if (s.Contains(':') || s.StartsWith(' ') || s.EndsWith(' ') || s.Contains('#') || s.Contains('\n')) + { + _sb.Append('"') + .Append(s.Replace("\"", "\\\"")) + .Append('"'); + } + else _sb.Append(s); return; + } + + _sb + .Append(Convert.ToString(value, System.Globalization.CultureInfo.InvariantCulture)); + } + } +} diff --git a/src/Cortex.Serialization.Yaml/Parser/Parser.cs b/src/Cortex.Serialization.Yaml/Parser/Parser.cs new file mode 100644 index 0000000..44eb5b5 --- /dev/null +++ b/src/Cortex.Serialization.Yaml/Parser/Parser.cs @@ -0,0 +1,159 @@ +using Cortex.Serialization.Yaml.Common; + +namespace Cortex.Serialization.Yaml.Parser +{ + internal sealed class Parser + { + private readonly List _tokens; + private int _idx; + public Parser(IEnumerable tokens) => _tokens = tokens.ToList(); + private Token Peek() => _tokens[_idx]; + private Token Next() => _tokens[_idx++]; + private bool Match(TokenType t) + { + if (Peek().Type == t) + { + _idx++; + return true; + } + return false; + } + public YamlNode ParseDocument() => ParseNode(); + private YamlNode ParseNode() + { + if (Peek().Type == TokenType.Key) + return ParseMapping(); + + if (Peek().Type == TokenType.Dash) + return ParseSequence(); + + return ParseScalar(); + } + private YamlNode ParseMapping() + { + var dict = new Dictionary(); + while (Peek().Type == TokenType.Key) + { + var keyTok = Next(); + var key = keyTok.Value!; + var after = Next(); + + if (after.Type == TokenType.BlockLiteral) + { + var text = ReadBlock(true); + dict[key] = new YamlScalar(text); + } + else if (after.Type == TokenType.BlockFolded) + { + var text = ReadBlock(false); + dict[key] = new YamlScalar(text); + } + else if (after.Type == TokenType.Scalar) + { + dict[key] = ParseScalarValue(after.Value!); + } + if (Match(TokenType.NewLine)) { } + + while (Match(TokenType.Dedent)) { } + + while (Match(TokenType.Indent)) + { + dict[key] = ParseNode(); + } + } + return new YamlMapping(dict); + } + private YamlNode ParseSequence() + { + var list = new List(); + while (Peek().Type == TokenType.Dash) + { + Next(); + + if (Peek().Type == TokenType.NewLine) + { + Next(); + Match(TokenType.Indent); + list.Add(ParseNode()); + while (Match(TokenType.Dedent)) { } + } + else + { + if (Peek().Type == TokenType.Key) + list.Add(ParseMapping()); + else if (Peek().Type == TokenType.Scalar) + list.Add(ParseScalar()); + } + + Match(TokenType.NewLine); + } + return new YamlSequence(list); + } + + private YamlNode ParseScalar() + { + var tok = Next(); + if (tok.Type != TokenType.Scalar) + throw new YamlException($"Expected scalar but got {tok.Type}", tok.Line, tok.Column); + return ParseScalarValue(tok.Value!); + } + + private YamlNode ParseScalarValue(string raw) + { + if (raw == "null" || raw == "~") + return new YamlScalar(null); + + if (raw == "true" || raw == "false") + return new YamlScalar(bool.Parse(raw)); + + if (int.TryParse(raw, out var i)) + return new YamlScalar(i); + + if (double.TryParse(raw, System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, out var d)) + return new YamlScalar(d); + + if ((raw.StartsWith('"') && raw.EndsWith('"')) || (raw.StartsWith('\'') && raw.EndsWith('\''))) + return new YamlScalar(raw[1..^1]); + + return new YamlScalar(raw); + } + private string ReadBlock(bool literal) + { + if (!Match(TokenType.NewLine)) { } + if (!Match(TokenType.Indent)) + throw new YamlException("Expected indentation for block scalar", Peek().Line, Peek().Column); + + var sb = new System.Text.StringBuilder(); + while (Peek().Type is TokenType.Scalar or TokenType.Key or TokenType.Dash) + { + var tok = Next(); + if (tok.Type == TokenType.Scalar) + { + if (!literal && sb.Length > 0) + sb.Append(' '); + sb.Append(tok.Value); + } + else if (tok.Type == TokenType.Key) + { + sb.Append(tok.Value).Append(':'); + if (Peek().Type == TokenType.Scalar) + sb.Append(' ').Append(Next().Value); + } + else if (tok.Type == TokenType.Dash) + { + sb.Append("- "); + if (Peek().Type == TokenType.Scalar) + sb.Append(Next().Value); + } + + Match(TokenType.NewLine); + + if (Peek().Type == TokenType.Dedent) + break; + } + Match(TokenType.Dedent); + + return sb.ToString(); + } + } +} \ No newline at end of file diff --git a/src/Cortex.Serialization.Yaml/Parser/Scanner.cs b/src/Cortex.Serialization.Yaml/Parser/Scanner.cs new file mode 100644 index 0000000..6c063f0 --- /dev/null +++ b/src/Cortex.Serialization.Yaml/Parser/Scanner.cs @@ -0,0 +1,85 @@ +namespace Cortex.Serialization.Yaml.Parser +{ + internal sealed class Scanner + { + private readonly string[] _lines; + private int _lineIdx; + + public Scanner(string input) + { + _lines = input.Replace("\r\n", "\n") + .Replace('\r', '\n') + .Split('\n'); + } + public IEnumerable Scan() + { + var indentStack = new Stack(); + indentStack.Push(0); + + for (_lineIdx = 0; _lineIdx < _lines.Length; _lineIdx++) + { + var raw = _lines[_lineIdx]; + if (string.IsNullOrWhiteSpace(raw)) + continue; + + int i = 0; + int spaces = 0; + while (i < raw.Length && raw[i] == ' ') + { + spaces++; + i++; + } + + while (spaces > indentStack.Peek()) + { + indentStack.Push(indentStack.Peek() + 2); + yield return new Token(TokenType.Indent, null, _lineIdx + 1, 1); + } + + while (spaces < indentStack.Peek()) + { + indentStack.Pop(); + yield return new Token(TokenType.Dedent, null, _lineIdx + 1, 1); + } + + if (i < raw.Length && raw[i] == '-') + { + yield return new Token(TokenType.Dash, null, _lineIdx + 1, i + 1); + i++; + if (i < raw.Length && raw[i] == ' ') + i++; + } + + var rest = raw[i..]; + if (rest.Contains(": ")) + { + var idx = rest.IndexOf(": "); + var key = rest[..idx]; + var val = rest[(idx + 2)..]; + yield return new Token(TokenType.Key, key, _lineIdx + 1, i + 1); + if (val == "|") + yield return new Token(TokenType.BlockLiteral, null, _lineIdx + 1, i + 1); + + else if (val == ">") + yield return new Token(TokenType.BlockFolded, null, _lineIdx + 1, i + 1); + + else yield return new Token(TokenType.Scalar, val, _lineIdx + 1, i + 1); + } + else + { + yield return new Token(TokenType.Scalar, rest, _lineIdx + 1, i + 1); + } + + yield return new Token(TokenType.NewLine, null, _lineIdx + 1, raw.Length + 1); + } + while (indentStack.Count > 1) + { + indentStack.Pop(); + yield return new Token(TokenType.Dedent, null, _lineIdx + 1, 1); + } + + yield return new Token(TokenType.EOF, null, _lineIdx + 1, 1); + } + } + +} diff --git a/src/Cortex.Serialization.Yaml/Parser/Token.cs b/src/Cortex.Serialization.Yaml/Parser/Token.cs new file mode 100644 index 0000000..6db3781 --- /dev/null +++ b/src/Cortex.Serialization.Yaml/Parser/Token.cs @@ -0,0 +1,8 @@ +namespace Cortex.Serialization.Yaml.Parser +{ + internal sealed record Token(TokenType Type, string? Value, int Line, int Column); + internal abstract record YamlNode; + internal sealed record YamlScalar(object? Value) : YamlNode; + internal sealed record YamlSequence(List Items) : YamlNode; + internal sealed record YamlMapping(Dictionary Entries) : YamlNode; +} diff --git a/src/Cortex.Serialization.Yaml/Parser/TokenType.cs b/src/Cortex.Serialization.Yaml/Parser/TokenType.cs new file mode 100644 index 0000000..d786d81 --- /dev/null +++ b/src/Cortex.Serialization.Yaml/Parser/TokenType.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Cortex.Serialization.Yaml.Parser +{ + internal enum TokenType + { + Scalar, + Key, + Dash, + NewLine, + Indent, + Dedent, + BlockLiteral, + BlockFolded, + EOF + } + +} diff --git a/src/Cortex.Serialization.Yaml/Reflection/CachedTypeInfo.cs b/src/Cortex.Serialization.Yaml/Reflection/CachedTypeInfo.cs new file mode 100644 index 0000000..2db6579 --- /dev/null +++ b/src/Cortex.Serialization.Yaml/Reflection/CachedTypeInfo.cs @@ -0,0 +1,8 @@ +namespace Cortex.Serialization.Yaml.Reflection +{ + internal sealed class CachedTypeInfo + { + private static readonly System.Collections.Concurrent.ConcurrentDictionary Cache = new(); + public static PropertyMap[] GetProps(Type t) => Cache.GetOrAdd(t, x => PropertyMap.FromType(x).ToArray()); + } +} diff --git a/src/Cortex.Serialization.Yaml/Reflection/PropertyMap.cs b/src/Cortex.Serialization.Yaml/Reflection/PropertyMap.cs new file mode 100644 index 0000000..99a40bd --- /dev/null +++ b/src/Cortex.Serialization.Yaml/Reflection/PropertyMap.cs @@ -0,0 +1,44 @@ +using Cortex.Serialization.Yaml.Attributes; +using System.Reflection; + +namespace Cortex.Serialization.Yaml.Reflection +{ + internal sealed record PropertyMap(string Name, MemberInfo Member, Type MemberType, bool canRead, bool canWrite, bool Ignored) + { + public object? GetValue(object instance) => Member switch { PropertyInfo p when canRead => p.GetValue(instance), FieldInfo f => f.GetValue(instance), _ => null }; + public void SetValue(object instance, object? value) + { + switch (Member) + { + case PropertyInfo p when canWrite: + p.SetValue(instance, value); + break; + case FieldInfo f: + f.SetValue(instance, value); + break; + } + } + public static IEnumerable FromType(Type t) + { + const BindingFlags Flags = BindingFlags.Instance | BindingFlags.Public; + foreach (var p in t.GetProperties(Flags)) + { + var ignore = p.GetCustomAttribute() != null; + var attr = p.GetCustomAttribute(); + var logical = attr?.Name ?? p.Name; + + yield return new PropertyMap(logical, p, p.PropertyType, p.CanRead, p.CanWrite, ignore); + } + + foreach (var f in t.GetFields(Flags)) + { + var ignore = f.GetCustomAttribute() != null; + var attr = f.GetCustomAttribute(); + var logical = attr?.Name ?? f.Name; + + yield return new PropertyMap(logical, f, f.FieldType, canRead: true, canWrite: !f.IsInitOnly, Ignored: ignore); + } + } + } + +} diff --git a/src/Cortex.Serialization.Yaml/Reflection/TypeInspector.cs b/src/Cortex.Serialization.Yaml/Reflection/TypeInspector.cs new file mode 100644 index 0000000..8dff33b --- /dev/null +++ b/src/Cortex.Serialization.Yaml/Reflection/TypeInspector.cs @@ -0,0 +1,23 @@ +using Cortex.Serialization.Yaml.Converters; + +namespace Cortex.Serialization.Yaml.Reflection +{ + internal sealed class TypeInspector + { + private readonly INamingConvention _conv; + public TypeInspector(INamingConvention c) => _conv = c; + public IEnumerable GetSerializableMembers(Type t, bool includeReadOnly) + { + foreach (var m in CachedTypeInfo.GetProps(t)) + { + if (m.Ignored) continue; + if (!includeReadOnly && !m.canWrite) continue; + yield return m with { Name = _conv.Convert(m.Name) }; + } + } + + public IEnumerable GetDeserializableMembers(Type t) => CachedTypeInfo + .GetProps(t) + .Where(m => !m.Ignored && m.canWrite); + } +} diff --git a/src/Cortex.Serialization.Yaml/Serialization/Converters/CamelCaseConvention.cs b/src/Cortex.Serialization.Yaml/Serialization/Converters/CamelCaseConvention.cs new file mode 100644 index 0000000..92d8508 --- /dev/null +++ b/src/Cortex.Serialization.Yaml/Serialization/Converters/CamelCaseConvention.cs @@ -0,0 +1,10 @@ +namespace Cortex.Serialization.Yaml.Converters +{ + public sealed class CamelCaseConvention : INamingConvention + { + public string Convert(string name) + { + return Common.StringUtils.ToCamelCase(name); + } + } +} diff --git a/src/Cortex.Serialization.Yaml/Serialization/Converters/KebabCaseConvention.cs b/src/Cortex.Serialization.Yaml/Serialization/Converters/KebabCaseConvention.cs new file mode 100644 index 0000000..3d846d7 --- /dev/null +++ b/src/Cortex.Serialization.Yaml/Serialization/Converters/KebabCaseConvention.cs @@ -0,0 +1,7 @@ +namespace Cortex.Serialization.Yaml.Converters +{ + public sealed class KebabCaseConvention : INamingConvention + { + public string Convert(string n) => Common.StringUtils.ToKebabCase(n); + } +} diff --git a/src/Cortex.Serialization.Yaml/Serialization/Converters/OriginalCaseConvention.cs b/src/Cortex.Serialization.Yaml/Serialization/Converters/OriginalCaseConvention.cs new file mode 100644 index 0000000..e61715b --- /dev/null +++ b/src/Cortex.Serialization.Yaml/Serialization/Converters/OriginalCaseConvention.cs @@ -0,0 +1,7 @@ +namespace Cortex.Serialization.Yaml.Converters +{ + public sealed class OriginalCaseConvention : INamingConvention + { + public string Convert(string n) => n; + } +} diff --git a/src/Cortex.Serialization.Yaml/Serialization/Converters/PascalCaseConvention.cs b/src/Cortex.Serialization.Yaml/Serialization/Converters/PascalCaseConvention.cs new file mode 100644 index 0000000..3d2a70b --- /dev/null +++ b/src/Cortex.Serialization.Yaml/Serialization/Converters/PascalCaseConvention.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Cortex.Serialization.Yaml.Converters +{ + public sealed class PascalCaseConvention : INamingConvention + { + public string Convert(string n) => char.ToUpperInvariant(n[0]) + n[1..]; + } +} diff --git a/src/Cortex.Serialization.Yaml/Serialization/Converters/SnakeCaseConvention.cs b/src/Cortex.Serialization.Yaml/Serialization/Converters/SnakeCaseConvention.cs new file mode 100644 index 0000000..f346cc4 --- /dev/null +++ b/src/Cortex.Serialization.Yaml/Serialization/Converters/SnakeCaseConvention.cs @@ -0,0 +1,7 @@ +namespace Cortex.Serialization.Yaml.Converters +{ + public sealed class SnakeCaseConvention : INamingConvention + { + public string Convert(string n) => Common.StringUtils.ToSnakeCase(n); + } +} diff --git a/src/Cortex.Serialization.Yaml/Serialization/Converters/YamlConverterAttribute.cs b/src/Cortex.Serialization.Yaml/Serialization/Converters/YamlConverterAttribute.cs new file mode 100644 index 0000000..366d929 --- /dev/null +++ b/src/Cortex.Serialization.Yaml/Serialization/Converters/YamlConverterAttribute.cs @@ -0,0 +1,9 @@ +namespace Cortex.Serialization.Yaml.Converters +{ + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum)] + public sealed class YamlConverterAttribute : Attribute + { + public Type ConverterType { get; } + public YamlConverterAttribute(Type t) => ConverterType = t; + } +} diff --git a/src/Cortex.Serialization.Yaml/Serialization/YamlDeserializerSettings.cs b/src/Cortex.Serialization.Yaml/Serialization/YamlDeserializerSettings.cs new file mode 100644 index 0000000..16cf4dd --- /dev/null +++ b/src/Cortex.Serialization.Yaml/Serialization/YamlDeserializerSettings.cs @@ -0,0 +1,139 @@ +using Cortex.Serialization.Yaml.Converters; + +namespace Cortex.Serialization.Yaml +{ + /// + /// Provides configuration options for customizing the behavior of YAML deserialization. + /// + /// + /// + /// This class allows you to control how YAML content is parsed and converted into .NET objects. + /// You can customize naming conventions, case sensitivity, and how unknown properties are handled + /// to match your specific requirements and data format. + /// + /// + /// The default settings are optimized for flexibility and compatibility with common YAML conventions: + /// + /// : camelCase (common in YAML/JSON ecosystems) + /// : true (for robust property matching) + /// : true (for forward/backward compatibility) + /// + /// + /// + /// + /// The following example shows how to configure custom deserializer settings: + /// + /// var settings = new YamlDeserializerSettings + /// { + /// NamingConvention = new SnakeCaseNamingConvention(), + /// CaseInsensitive = false, // Require exact case matching + /// IgnoreUnmatchedProperties = false // Throw on unknown properties + /// }; + /// + /// var obj = YamlDeserializer.Deserialize<MyClass>(yamlString, settings); + /// + /// + /// Example with custom naming convention: + /// + /// public class KebabCaseNamingConvention : INamingConvention + /// { + /// public string Convert(string name) + /// { + /// // Convert "FirstName" to "first-name" + /// return Regex.Replace(name, @"([a-z])([A-Z])", "$1-$2").ToLowerInvariant(); + /// } + /// } + /// + /// var settings = new YamlDeserializerSettings + /// { + /// NamingConvention = new KebabCaseNamingConvention() + /// }; + /// + /// + /// + public sealed class YamlDeserializerSettings + { + /// + /// Gets or sets the naming convention used to map between YAML property names and .NET member names. + /// + /// + /// An implementation of that defines how property names are transformed. + /// Default is . + /// + /// + /// + /// This convention is applied bidirectionally during deserialization to match YAML property names + /// with the corresponding .NET class members. For example, with camelCase convention, a YAML property + /// named "firstName" would map to a C# property named "FirstName". + /// + /// + /// Set to null to use exact name matching without any transformation. + /// + /// + /// + /// With camelCase convention: + /// - YAML: "userName" → C#: "UserName" + /// - YAML: "emailAddress" → C#: "EmailAddress" + /// + public INamingConvention NamingConvention { get; init; } = new CamelCaseConvention(); + + /// + /// Gets or sets a value indicating whether property name matching should be case-insensitive. + /// + /// + /// true to ignore case when matching YAML properties to .NET members; false to require exact case matching. + /// Default is true. + /// + /// + /// + /// When set to true, the deserializer will match properties regardless of case differences. + /// This provides flexibility when working with YAML files that may have inconsistent casing. + /// + /// + /// When set to false, property names must match exactly, including case. + /// + /// + /// + /// With CaseInsensitive = true: + /// - YAML: "username", "UserName", "USERNAME" all map to C# property "UserName" + /// + /// With CaseInsensitive = false: + /// - Only "UserName" in YAML maps to C# property "UserName" + /// - "username" and "USERNAME" would be treated as unmatched properties + /// + public bool CaseInsensitive { get; init; } = true; + + /// + /// Gets or sets a value indicating whether unmatched YAML properties should be ignored during deserialization. + /// + /// + /// true to silently ignore properties in the YAML that don't have matching members in the target type; + /// false to throw a when unmatched properties are encountered. + /// Default is true. + /// + /// + /// + /// Setting this to true provides better forward and backward compatibility, allowing your application + /// to work with YAML files that contain additional properties not defined in your .NET types. + /// + /// + /// Setting this to false is useful for strict validation, ensuring that the YAML structure exactly + /// matches the expected schema and helping to catch typos or structural errors. + /// + /// + /// + /// Given a class: + /// + /// public class User { public string Name { get; set; } } + /// + /// And YAML: + /// + /// name: John + /// age: 30 + /// + /// With IgnoreUnmatchedProperties = true: Deserialization succeeds, "age" is ignored + /// With IgnoreUnmatchedProperties = false: YamlException is thrown for unmatched property "age" + /// + public bool IgnoreUnmatchedProperties { get; init; } = true; + } +} \ No newline at end of file diff --git a/src/Cortex.Serialization.Yaml/Serialization/YamlSerializerSettings.cs b/src/Cortex.Serialization.Yaml/Serialization/YamlSerializerSettings.cs new file mode 100644 index 0000000..7da5989 --- /dev/null +++ b/src/Cortex.Serialization.Yaml/Serialization/YamlSerializerSettings.cs @@ -0,0 +1,263 @@ +using Cortex.Serialization.Yaml.Converters; + +namespace Cortex.Serialization.Yaml +{ + /// + /// Provides configuration options for customizing the behavior of YAML serialization. + /// + /// + /// + /// This class allows you to control how .NET objects are converted to YAML format. + /// You can customize naming conventions, formatting, null handling, and type conversion + /// to produce YAML output that matches your specific requirements and style preferences. + /// + /// + /// The default settings are optimized for readability and compatibility: + /// + /// : camelCase (common in YAML/JSON ecosystems) + /// : true (include null values in output) + /// : true (include default values in output) + /// : false (maintain property declaration order) + /// : 2 (standard YAML indentation) + /// : Includes for basic types + /// + /// + /// + /// + /// The following example shows how to configure custom serializer settings: + /// + /// var settings = new YamlSerializerSettings + /// { + /// NamingConvention = new PascalCaseNamingConvention(), + /// EmitNulls = false, // Skip null values + /// SortProperties = true, // Alphabetical order + /// Indentation = 4 // Use 4 spaces for indentation + /// }; + /// + /// string yaml = YamlSerializer.Serialize(obj, settings); + /// + /// + /// Example for compact YAML output: + /// + /// var compactSettings = new YamlSerializerSettings + /// { + /// EmitNulls = false, + /// EmitDefaults = false, + /// Indentation = 2 + /// }; + /// + /// + /// + /// + public sealed class YamlSerializerSettings + { + /// + /// Gets or sets the naming convention used to convert .NET property names to YAML property names. + /// + /// + /// An implementation of that defines how property names are transformed. + /// Default is . + /// + /// + /// + /// This convention is applied during serialization to convert .NET property names (typically PascalCase) + /// to the desired naming convention in the output YAML. + /// + /// + /// Set to null to use exact .NET property names without transformation. + /// + /// + /// + /// With camelCase convention: + /// - C#: "FirstName" → YAML: "firstName" + /// - C#: "EmailAddress" → YAML: "emailAddress" + /// + public INamingConvention NamingConvention { get; init; } = new CamelCaseConvention(); + + /// + /// Gets or sets a value indicating whether null values should be included in the YAML output. + /// + /// + /// true to emit null values as explicit nulls in the YAML; false to omit null properties entirely. + /// Default is true. + /// + /// + /// + /// When set to true, null properties are written as property: null in the YAML output. + /// When set to false, properties with null values are completely omitted from the output. + /// + /// + /// Setting this to false can produce more compact YAML but may lose information about + /// which properties were explicitly set to null versus omitted. + /// + /// + /// + /// Given an object: new { Name = "John", Address = null } + /// + /// With EmitNulls = true: + /// + /// name: John + /// address: null + /// + /// + /// With EmitNulls = false: + /// + /// name: John + /// + /// + public bool EmitNulls { get; init; } = true; + + /// + /// Gets or sets a value indicating whether default values should be included in the YAML output. + /// + /// + /// true to emit default values (0, false, empty strings, etc.) in the YAML; + /// false to omit properties with default values. + /// Default is true. + /// + /// + /// + /// When set to false, properties with default values (0 for numbers, false for booleans, + /// default for structs, empty collections, etc.) are omitted from the YAML output. + /// + /// + /// This setting works in conjunction with - null values are handled + /// by that setting regardless of this one. + /// + /// + /// + /// Given an object: new { Count = 0, Enabled = false, Name = "" } + /// + /// With EmitDefaults = true: + /// + /// count: 0 + /// enabled: false + /// name: "" + /// + /// + /// With EmitDefaults = false: + /// + /// # All properties omitted since they have default values + /// + /// + public bool EmitDefaults { get; init; } = true; + + /// + /// Gets or sets a value indicating whether properties should be sorted alphabetically in the YAML output. + /// + /// + /// true to sort properties alphabetically; false to maintain the order in which + /// properties are declared in the class. Default is false. + /// + /// + /// + /// When set to false (default), properties are emitted in the order they are declared + /// in the class, which can provide more logical grouping of related properties. + /// + /// + /// When set to true, properties are sorted alphabetically by their YAML names (after + /// applying the naming convention), which can provide consistent ordering across different + /// .NET runtime implementations. + /// + /// + /// + /// Given a class: + /// + /// public class Example + /// { + /// public string LastName { get; set; } + /// public string FirstName { get; set; } + /// } + /// + /// + /// With SortProperties = false (declaration order): + /// + /// lastName: Smith + /// firstName: John + /// + /// + /// With SortProperties = true (alphabetical order): + /// + /// firstName: John + /// lastName: Smith + /// + /// + public bool SortProperties { get; init; } = false; + + /// + /// Gets or sets the number of spaces used for each level of indentation in the YAML output. + /// + /// + /// The number of spaces for each indentation level. Default is 2. + /// + /// + /// + /// Common values are 2 (standard YAML convention) or 4 (common in some codebases). + /// The value must be positive. + /// + /// + /// This setting affects the readability and compactness of the generated YAML. + /// Smaller values produce more compact output, while larger values can improve + /// readability for deeply nested structures. + /// + /// + /// + /// With Indentation = 2: + /// + /// parent: + /// child: + /// value: example + /// + /// + /// With Indentation = 4: + /// + /// parent: + /// child: + /// value: example + /// + /// + public int Indentation { get; init; } = 2; + + /// + /// Gets the list of custom type converters used during serialization. + /// + /// + /// A list of instances that handle serialization of specific types. + /// Default includes for basic types. + /// + /// + /// + /// Custom converters allow you to control how specific .NET types are serialized to YAML. + /// You can add converters for custom types or override the default serialization behavior + /// for built-in types. + /// + /// + /// Converters are evaluated in order, and the first converter that can handle a type is used. + /// + /// + /// + /// Adding a custom converter for a DateTime format: + /// + /// var settings = new YamlSerializerSettings(); + /// settings.Converters.Add(new CustomDateTimeConverter("yyyy-MM-dd")); + /// + /// string yaml = YamlSerializer.Serialize(obj, settings); + /// + /// + /// Adding a converter for a custom type: + /// + /// public class ColorConverter : IYamlTypeConverter + /// { + /// public bool CanConvert(Type type) => type == typeof(Color); + /// + /// public void WriteYaml(YamlWriter writer, object value) + /// { + /// var color = (Color)value; + /// writer.WriteValue($"#{color.R:X2}{color.G:X2}{color.B:X2}"); + /// } + /// } + /// + /// + public List Converters { get; } = new() { new PrimitiveConverter() }; + } +} \ No newline at end of file diff --git a/src/Cortex.Serialization.Yaml/YamlDeserializer.cs b/src/Cortex.Serialization.Yaml/YamlDeserializer.cs new file mode 100644 index 0000000..51655ef --- /dev/null +++ b/src/Cortex.Serialization.Yaml/YamlDeserializer.cs @@ -0,0 +1,410 @@ +using Cortex.Serialization.Yaml.Converters; + +namespace Cortex.Serialization.Yaml +{ + /// + /// Provides methods for deserializing YAML content into .NET objects. + /// + /// + /// + /// The class converts YAML documents into .NET objects, supporting + /// a wide range of types including primitives, collections, dictionaries, and custom classes. + /// It offers both instance-based and static convenience methods for deserialization. + /// + /// + /// Key features include: + /// + /// Bidirectional naming convention support + /// Custom type converters for specialized serialization + /// Flexible property matching (case-sensitive or insensitive) + /// Configurable handling of unmatched properties + /// Support for arrays, lists, dictionaries, and complex object graphs + /// + /// + /// + /// + /// + /// Basic usage with static methods: + /// + /// + /// // Deserialize from string + /// var person = YamlDeserializer.Deserialize<Person>(yamlString); + /// + /// // Deserialize with custom settings + /// var settings = new YamlDeserializerSettings + /// { + /// CaseInsensitive = false, + /// IgnoreUnmatchedProperties = false + /// }; + /// var config = YamlDeserializer.Deserialize<Config>(yamlString, settings); + /// + /// // Deserialize from file + /// using var reader = new StreamReader("config.yaml"); + /// var data = YamlDeserializer.Deserialize<DataModel>(reader); + /// + /// + /// + /// Advanced usage with instance methods and custom converters: + /// + /// + /// // Create deserializer with custom converters + /// var converters = new List<IYamlTypeConverter> { new CustomDateConverter() }; + /// var deserializer = new YamlDeserializer(settings, converters); + /// + /// // Reuse the same instance for multiple deserializations + /// var obj1 = deserializer.Deserialize<Type1>(yaml1); + /// var obj2 = deserializer.Deserialize<Type2>(yaml2); + /// + /// + /// + /// + /// + public sealed class YamlDeserializer + { + private readonly YamlDeserializerSettings _settings; + private readonly List _converters = new() + { + new PrimitiveConverter() + }; + + + // --- Static convenience API --- + + /// + /// Deserializes YAML content from a string into an instance of the specified type. + /// + /// The type of object to deserialize. + /// The YAML string to deserialize. + /// Optional settings to control deserialization behavior. + /// Optional additional type converters to use during deserialization. + /// An instance of populated with data from the YAML string. + /// + /// Thrown when the YAML is invalid, cannot be parsed, or cannot be converted to the target type. + /// + /// + /// + /// This is a convenience method that creates a temporary instance + /// for a single deserialization operation. For multiple deserializations, consider creating + /// a instance and reusing it. + /// + /// + /// + /// + /// string yaml = @"name: John Smith + /// age: 30 + /// email: john@example.com"; + /// + /// var person = YamlDeserializer.Deserialize<Person>(yaml); + /// Console.WriteLine(person.Name); // "John Smith" + /// + /// + public static T Deserialize(string input, + YamlDeserializerSettings? settings = null, + IEnumerable? extra = null) + => new YamlDeserializer(settings, extra).Deserialize(input); + + /// + /// Deserializes YAML content from a string into an instance of the specified type. + /// + /// The YAML string to deserialize. + /// The type of object to deserialize. + /// Optional settings to control deserialization behavior. + /// Optional additional type converters to use during deserialization. + /// An instance of the specified type populated with data from the YAML string. + /// + /// Thrown when the YAML is invalid, cannot be parsed, or cannot be converted to the target type. + /// + /// + /// + /// Use this overload when the target type is not known at compile time. + /// + /// + /// + /// + /// string yaml = @"name: John Smith + /// age: 30"; + /// + /// Type targetType = typeof(Person); + /// var person = YamlDeserializer.Deserialize(yaml, targetType); + /// + /// + public static object? Deserialize(string input, Type t, + YamlDeserializerSettings? settings = null, + IEnumerable? extra = null) + => new YamlDeserializer(settings, extra).Deserialize(input, t); + + /// + /// Deserializes YAML content from a into an instance of the specified type. + /// + /// The type of object to deserialize. + /// The containing YAML content. + /// Optional settings to control deserialization behavior. + /// Optional additional type converters to use during deserialization. + /// An instance of populated with data from the YAML content. + /// + /// Thrown when the YAML is invalid, cannot be parsed, or cannot be converted to the target type. + /// + /// + /// + /// This method is suitable for reading YAML from files, network streams, or any other + /// source that can be wrapped in a . + /// + /// + /// + /// + /// using var reader = new StreamReader("data.yaml"); + /// var data = YamlDeserializer.Deserialize<DataModel>(reader); + /// + /// + public static T Deserialize(TextReader reader, + YamlDeserializerSettings? settings = null, + IEnumerable? extra = null) + => new YamlDeserializer(settings, extra).Deserialize(reader); + + /// + /// Deserializes YAML content from a into an instance of the specified type. + /// + /// The containing YAML content. + /// The type of object to deserialize. + /// Optional settings to control deserialization behavior. + /// Optional additional type converters to use during deserialization. + /// An instance of the specified type populated with data from the YAML content. + /// + /// Thrown when the YAML is invalid, cannot be parsed, or cannot be converted to the target type. + /// + /// + /// + /// using var reader = new StringReader(yamlString); + /// Type targetType = typeof(Person); + /// var person = YamlDeserializer.Deserialize(reader, targetType); + /// + /// + public static object? Deserialize(TextReader reader, Type t, + YamlDeserializerSettings? settings = null, + IEnumerable? extra = null) + => new YamlDeserializer(settings, extra).Deserialize(reader, t); + // ------------------------------------ + + /// + /// Initializes a new instance of the class with the specified settings and converters. + /// + /// The settings to use for deserialization. If null, default settings are used. + /// Additional type converters to use during deserialization. + /// + /// + /// Creating an instance of is recommended when you need to perform + /// multiple deserialization operations with the same configuration, as it avoids the overhead + /// of recreating internal structures for each operation. + /// + /// + /// The deserializer is initialized with a by default, which handles + /// basic types like strings, numbers, and booleans. Additional converters can be provided to handle + /// custom types or override default conversion behavior. + /// + /// + /// + /// + /// // With default settings + /// var deserializer1 = new YamlDeserializer(); + /// + /// // With custom settings + /// var settings = new YamlDeserializerSettings { CaseInsensitive = false }; + /// var deserializer2 = new YamlDeserializer(settings); + /// + /// // With custom settings and converters + /// var converters = new List<IYamlTypeConverter> { new CustomConverter() }; + /// var deserializer3 = new YamlDeserializer(settings, converters); + /// + /// + public YamlDeserializer(YamlDeserializerSettings? s = null, IEnumerable? extra = null) + { + _settings = s ?? new(); + if (extra is not null) + _converters.AddRange(extra); + } + + /// + /// Deserializes YAML content from a into an instance of the specified type. + /// + /// The type of object to deserialize. + /// The containing YAML content. + /// An instance of populated with data from the YAML content. + /// Thrown when is null. + /// + /// Thrown when the YAML is invalid, cannot be parsed, or cannot be converted to the target type. + /// + /// + /// + /// This method reads the entire content from the and parses it as YAML. + /// The reader is not closed or disposed by this method - the caller remains responsible for resource management. + /// + /// + public T Deserialize(TextReader reader) + { + var doc = Parse(reader.ReadToEnd()); + return (T)ConvertNode(doc, typeof(T))!; + } + + /// + /// Deserializes YAML content from a into an instance of the specified type. + /// + /// The containing YAML content. + /// The type of object to deserialize. + /// An instance of the specified type populated with data from the YAML content. + /// + /// Thrown when or is null. + /// + /// + /// Thrown when the YAML is invalid, cannot be parsed, or cannot be converted to the target type. + /// + public object? Deserialize(TextReader reader, Type t) + { + var doc = Parse(reader.ReadToEnd()); + return ConvertNode(doc, t); + } + + /// + /// Deserializes a YAML string into an instance of the specified type. + /// + /// The type of object to deserialize. + /// The YAML string to deserialize. + /// An instance of populated with data from the YAML string. + /// Thrown when is null. + /// + /// Thrown when the YAML is invalid, cannot be parsed, or cannot be converted to the target type. + /// + /// + /// + /// This is the primary method for deserializing YAML strings. It parses the YAML content + /// and converts it to the specified .NET type using the configured settings and converters. + /// + /// + public T Deserialize(string input) + { + var doc = Parse(input); + return (T)ConvertNode(doc, typeof(T))!; + } + + /// + /// Deserializes a YAML string into an instance of the specified type. + /// + /// The YAML string to deserialize. + /// The type of object to deserialize. + /// An instance of the specified type populated with data from the YAML string. + /// + /// Thrown when or is null. + /// + /// + /// Thrown when the YAML is invalid, cannot be parsed, or cannot be converted to the target type. + /// + /// + /// + /// Use this overload when the target type is determined at runtime rather than compile time. + /// + /// + public object? Deserialize(string input, Type t) + { + var doc = Parse(input); + return ConvertNode(doc, t); + } + + private Parser.YamlNode Parse(string input) + { + var scanner = new Parser.Scanner(input); + var tokens = scanner.Scan(); + var parser = new Parser.Parser(tokens); + return parser.ParseDocument(); + } + + private object? ConvertNode(Parser.YamlNode node, Type target) + { + foreach (var c in _converters) + if (c.CanConvert(target)) + return c.Read((node as Parser.YamlScalar)?.Value, target); + + if (node is Parser.YamlScalar s) + { + if (target == typeof(object)) + return s.Value; + foreach (var c in _converters) + if (c.CanConvert(target)) + return c.Read(s.Value, target); + return s.Value; + } + + if (typeof(System.Collections.IDictionary).IsAssignableFrom(target)) + { + var (kt, vt) = GetDictTypes(target); + var dict = (System.Collections.IDictionary)Activator.CreateInstance(target)!; + if (node is not Parser.YamlMapping map) + throw new Common.YamlException("Expected mapping for dictionary"); + foreach (var (k, v) in map.Entries) + { + var keyObj = Convert.ChangeType(k, kt); + var valObj = ConvertNode(v, vt); + dict.Add(keyObj!, valObj); + } + return dict; + } + + if (typeof(System.Collections.IEnumerable).IsAssignableFrom(target) && target != typeof(string)) + { + var et = target.IsArray ? target.GetElementType()! : target.GenericTypeArguments.FirstOrDefault() ?? typeof(object); + var list = (System.Collections.IList)Activator.CreateInstance(typeof(List<>).MakeGenericType(et))!; + if (node is not Parser.YamlSequence seq) + throw new Common.YamlException("Expected sequence for collection"); + + foreach (var it in seq.Items) + list.Add(ConvertNode(it, et)); + if (target.IsArray) + { + var arr = Array.CreateInstance(et, list.Count); + list.CopyTo(arr, 0); return arr; + } + if (target.IsAssignableFrom(list.GetType())) + return list; + var coll = Activator.CreateInstance(target); + var add = target.GetMethod("Add"); + foreach (var it in list) + add!.Invoke(coll, new[] { it }); + return coll; + } + + if (node is Parser.YamlMapping mapping) + { + var obj = Activator.CreateInstance(target)!; + var lookup = BuildMemberLookup(target); + foreach (var (key, val) in mapping.Entries) + { + if (!lookup.TryGetValue(key, out var pm)) + { + if (!_settings.IgnoreUnmatchedProperties) + throw new Common.YamlException($"Unknown property '{key}' for type {target.Name}"); + continue; + } + var converted = ConvertNode(val, pm.MemberType); + pm.SetValue(obj, converted); + } + return obj; + } + throw new Common.YamlException($"Cannot convert node {node.GetType().Name} to {target.Name}"); + } + private Dictionary BuildMemberLookup(Type t) + { + var insp = new Reflection.TypeInspector(_settings.NamingConvention); + var dict = new Dictionary(_settings.CaseInsensitive ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal); + foreach (var m in insp.GetDeserializableMembers(t)) + dict[_settings.NamingConvention.Convert(m.Name)] = m; + return dict; + } + private static (Type key, Type val) GetDictTypes(Type dict) + { + if (dict.IsGenericType) + { + var a = dict.GetGenericArguments(); + return (a[0], a[1]); + } + return (typeof(string), typeof(object)); + } + } +} \ No newline at end of file diff --git a/src/Cortex.Serialization.Yaml/YamlSerializer.cs b/src/Cortex.Serialization.Yaml/YamlSerializer.cs new file mode 100644 index 0000000..9774d2e --- /dev/null +++ b/src/Cortex.Serialization.Yaml/YamlSerializer.cs @@ -0,0 +1,247 @@ +using Cortex.Serialization.Yaml.Reflection; + +namespace Cortex.Serialization.Yaml +{ + /// + /// Provides methods for serializing .NET objects into YAML format. + /// + /// + /// + /// The class converts .NET objects into YAML documents, supporting + /// a wide range of types including primitives, collections, dictionaries, and custom classes. + /// It offers both instance-based and static convenience methods for serialization. + /// + /// + /// Key features include: + /// + /// Configurable naming conventions for property names + /// Control over null and default value emission + /// Property ordering (declaration order or alphabetical) + /// Customizable indentation + /// Automatic handling of collections and dictionaries + /// + /// + /// + /// The serializer automatically handles common .NET types including: + /// + /// Primitives: string, bool, int, long, double, decimal, Guid, DateTime, DateOnly, TimeOnly + /// Collections: arrays, lists, and any type implementing + /// Dictionaries: any type implementing + /// Custom objects: public properties are serialized as YAML mappings + /// + /// + /// + /// + /// + /// Basic usage with static method: + /// + /// + /// var person = new Person { Name = "John", Age = 30 }; + /// string yaml = YamlSerializer.Serialize(person); + /// + /// // Output: + /// // name: John + /// // age: 30 + /// + /// + /// + /// Serializing collections and complex objects: + /// + /// + /// var users = new List<Person> + /// { + /// new Person { Name = "John", Age = 30 }, + /// new Person { Name = "Jane", Age = 25 } + /// }; + /// + /// string yaml = YamlSerializer.Serialize(users); + /// + /// // Output: + /// // - name: John + /// // age: 30 + /// // - name: Jane + /// // age: 25 + /// + /// + /// + /// Using instance methods for multiple serializations: + /// + /// + /// var settings = new YamlSerializerSettings + /// { + /// EmitNulls = false, + /// SortProperties = true + /// }; + /// + /// var serializer = new YamlSerializer(settings); + /// string yaml1 = serializer.Serialize(obj1); + /// string yaml2 = serializer.Serialize(obj2); + /// + /// + /// + /// + public sealed class YamlSerializer + { + private readonly YamlSerializerSettings _settings; + + // --- Static convenience API --- + + /// + /// Serializes the specified object to a YAML string using the provided settings. + /// + /// The object to serialize. + /// The settings to use for serialization, or null to use default settings. + /// A YAML string representing the serialized object. + /// + /// + /// This is a convenience method that creates a temporary instance + /// for a single serialization operation. For multiple serializations, consider creating + /// a instance and reusing it. + /// + /// + /// If is null, the method returns a YAML null value. + /// + /// + /// + /// + /// var person = new Person { Name = "John", Age = 30 }; + /// + /// // With default settings + /// string yaml1 = YamlSerializer.Serialize(person); + /// + /// // With custom settings + /// var settings = new YamlSerializerSettings { EmitNulls = false }; + /// string yaml2 = YamlSerializer.Serialize(person, settings); + /// + /// + public static string Serialize(object? obj, YamlSerializerSettings? settings = null) + => new YamlSerializer(settings).Serialize(obj); + + // --- Instance API --- + + /// + /// Initializes a new instance of the class with the specified settings. + /// + /// The settings to use for serialization. If null, default settings are used. + /// + /// + /// Creating an instance of is recommended when you need to perform + /// multiple serialization operations with the same configuration, as it avoids the overhead + /// of recreating internal structures for each operation. + /// + /// + /// + /// + /// // With default settings + /// var serializer1 = new YamlSerializer(); + /// + /// // With custom settings + /// var settings = new YamlSerializerSettings + /// { + /// EmitNulls = false, + /// SortProperties = true + /// }; + /// var serializer2 = new YamlSerializer(settings); + /// + /// + public YamlSerializer(YamlSerializerSettings? s = null) => _settings = s ?? new(); + + /// + /// Serializes the specified object to a YAML string. + /// + /// The object to serialize. + /// A YAML string representing the serialized object. + /// + /// + /// The serialization process follows these steps: + /// + /// Convert the object to a YAML node tree using + /// Emit the YAML node tree to a string using the configured indentation + /// + /// + /// + /// The method handles null values by returning a YAML null scalar. For objects, + /// only public readable properties are serialized (read-only properties are excluded). + /// + /// + /// + /// + /// var serializer = new YamlSerializer(); + /// + /// // Serialize a simple object + /// var person = new Person { Name = "John", Age = 30 }; + /// string yaml = serializer.Serialize(person); + /// + /// // Serialize a collection + /// var numbers = new List<int> { 1, 2, 3 }; + /// string yamlNumbers = serializer.Serialize(numbers); + /// + /// // Serialize a dictionary + /// var dict = new Dictionary<string, object> + /// { + /// ["key1"] = "value1", + /// ["key2"] = 42 + /// }; + /// string yamlDict = serializer.Serialize(dict); + /// + /// + public string Serialize(object? obj) + { + var node = ToNode(obj); + var emitter = new Emitter.Emitter(_settings.Indentation); + + return emitter.Emit(node); + } + + private Parser.YamlNode ToNode(object? obj) + { + if (obj is null) + return new Parser.YamlScalar(null); + + // Handle primitive types as scalars + if (obj is string or bool or int or long or double or decimal or Guid or DateTime or DateOnly or TimeOnly) + return new Parser.YamlScalar(obj); + + // Handle dictionaries as mappings + if (obj is System.Collections.IDictionary dict) + { + var m = new Dictionary(); + foreach (System.Collections.DictionaryEntry de in dict) + m[de.Key!.ToString()!] = ToNode(de.Value); + return new Parser.YamlMapping(m); + } + + // Handle collections as sequences (but not strings, which are handled above) + if (obj is System.Collections.IEnumerable en && obj is not string) + { + var items = new List(); + foreach (var it in en) items.Add(ToNode(it)); + return new Parser.YamlSequence(items); + } + + // Handle complex objects as mappings with properties + var insp = new TypeInspector(_settings.NamingConvention); + var map = new Dictionary(); + var props = insp.GetSerializableMembers(obj.GetType(), includeReadOnly: false); + var seq = _settings.SortProperties ? props.OrderBy(p => p.Name) : props; + + foreach (var p in seq) + { + var val = p.GetValue(obj); + if (val is null && !_settings.EmitNulls) + continue; + if (val is not null && IsDefault(val) && !_settings.EmitDefaults) + continue; + map[p.Name] = ToNode(val); + } + + return new Parser.YamlMapping(map); + } + + private static bool IsDefault(object value) + { + var t = value.GetType(); + return Equals(value, t.IsValueType ? Activator.CreateInstance(t) : null); + } + } +} \ No newline at end of file From 9fd024dc8b4d5fbb319be0a2b3b362f1a6d90c8c Mon Sep 17 00:00:00 2001 From: Enes Hoxha Date: Tue, 21 Oct 2025 01:34:50 +0200 Subject: [PATCH 2/6] v2/feature / #146: Add Readme and support for multiple .NET Versions --- .../Assets/cortex.png | Bin 0 -> 63537 bytes .../Assets/license.md | 20 ++ .../Attributes/YamlIgnoreAttribute.cs | 4 +- .../Attributes/YamlPropertyAttribute.cs | 4 +- .../Common/YamlException.cs | 4 +- .../Converters/IYamlTypeConverter.cs | 4 +- .../Converters/PrimitiveConverter.cs | 4 +- .../Cortex.Serialization.Yaml.csproj | 58 ++++- .../Emitter/Emitter.cs | 1 + .../Parser/Parser.cs | 2 + .../Parser/Scanner.cs | 4 +- src/Cortex.Serialization.Yaml/Parser/Token.cs | 4 +- src/Cortex.Serialization.Yaml/README.md | 215 ++++++++++++++++++ .../Reflection/CachedTypeInfo.cs | 5 +- .../Reflection/PropertyMap.cs | 2 + .../Reflection/TypeInspector.cs | 3 + .../Converters/YamlConverterAttribute.cs | 4 +- .../Serialization/YamlSerializerSettings.cs | 1 + .../YamlDeserializer.cs | 4 + .../YamlSerializer.cs | 3 + 20 files changed, 332 insertions(+), 14 deletions(-) create mode 100644 src/Cortex.Serialization.Yaml/Assets/cortex.png create mode 100644 src/Cortex.Serialization.Yaml/Assets/license.md create mode 100644 src/Cortex.Serialization.Yaml/README.md diff --git a/src/Cortex.Serialization.Yaml/Assets/cortex.png b/src/Cortex.Serialization.Yaml/Assets/cortex.png new file mode 100644 index 0000000000000000000000000000000000000000..101a1fb10887915ba6cd81f7493120090cfab590 GIT binary patch literal 63537 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 V*Ppl8Hi;(v?^jgV{@+>5{|{p6e{lc+ literal 0 HcmV?d00001 diff --git a/src/Cortex.Serialization.Yaml/Assets/license.md b/src/Cortex.Serialization.Yaml/Assets/license.md new file mode 100644 index 0000000..3c845d4 --- /dev/null +++ b/src/Cortex.Serialization.Yaml/Assets/license.md @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2025 Buildersoft + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/src/Cortex.Serialization.Yaml/Attributes/YamlIgnoreAttribute.cs b/src/Cortex.Serialization.Yaml/Attributes/YamlIgnoreAttribute.cs index 0aff3e4..5176221 100644 --- a/src/Cortex.Serialization.Yaml/Attributes/YamlIgnoreAttribute.cs +++ b/src/Cortex.Serialization.Yaml/Attributes/YamlIgnoreAttribute.cs @@ -1,4 +1,6 @@ -namespace Cortex.Serialization.Yaml.Attributes +using System; + +namespace Cortex.Serialization.Yaml.Attributes { /// /// Indicates that a field or property should be ignored during YAML serialization and deserialization. diff --git a/src/Cortex.Serialization.Yaml/Attributes/YamlPropertyAttribute.cs b/src/Cortex.Serialization.Yaml/Attributes/YamlPropertyAttribute.cs index 22fe72e..59d035e 100644 --- a/src/Cortex.Serialization.Yaml/Attributes/YamlPropertyAttribute.cs +++ b/src/Cortex.Serialization.Yaml/Attributes/YamlPropertyAttribute.cs @@ -1,4 +1,6 @@ -namespace Cortex.Serialization.Yaml.Attributes +using System; + +namespace Cortex.Serialization.Yaml.Attributes { /// /// Specifies a custom YAML property name for a field or property during serialization and deserialization. diff --git a/src/Cortex.Serialization.Yaml/Common/YamlException.cs b/src/Cortex.Serialization.Yaml/Common/YamlException.cs index 0f96f6f..5b3fff1 100644 --- a/src/Cortex.Serialization.Yaml/Common/YamlException.cs +++ b/src/Cortex.Serialization.Yaml/Common/YamlException.cs @@ -1,4 +1,6 @@ -namespace Cortex.Serialization.Yaml.Common +using System; + +namespace Cortex.Serialization.Yaml.Common { /// /// The exception that is thrown when an error occurs during YAML serialization or deserialization. diff --git a/src/Cortex.Serialization.Yaml/Converters/IYamlTypeConverter.cs b/src/Cortex.Serialization.Yaml/Converters/IYamlTypeConverter.cs index 24cc891..bf06292 100644 --- a/src/Cortex.Serialization.Yaml/Converters/IYamlTypeConverter.cs +++ b/src/Cortex.Serialization.Yaml/Converters/IYamlTypeConverter.cs @@ -1,4 +1,6 @@ -namespace Cortex.Serialization.Yaml.Converters +using System; + +namespace Cortex.Serialization.Yaml.Converters { public interface IYamlTypeConverter { diff --git a/src/Cortex.Serialization.Yaml/Converters/PrimitiveConverter.cs b/src/Cortex.Serialization.Yaml/Converters/PrimitiveConverter.cs index b2490ae..3ca86a3 100644 --- a/src/Cortex.Serialization.Yaml/Converters/PrimitiveConverter.cs +++ b/src/Cortex.Serialization.Yaml/Converters/PrimitiveConverter.cs @@ -1,4 +1,6 @@ -namespace Cortex.Serialization.Yaml.Converters +using System; + +namespace Cortex.Serialization.Yaml.Converters { public sealed class PrimitiveConverter : IYamlTypeConverter { diff --git a/src/Cortex.Serialization.Yaml/Cortex.Serialization.Yaml.csproj b/src/Cortex.Serialization.Yaml/Cortex.Serialization.Yaml.csproj index 125f4c9..c495991 100644 --- a/src/Cortex.Serialization.Yaml/Cortex.Serialization.Yaml.csproj +++ b/src/Cortex.Serialization.Yaml/Cortex.Serialization.Yaml.csproj @@ -1,9 +1,57 @@  - - net9.0 - enable - enable - + + net9.0;net8.0 + + 1.0.1 + 1.0.1 + Buildersoft Cortex Framework + Buildersoft + Buildersoft,EnesHoxha + Copyright © Buildersoft 2025 + + Cortex Data Framework is a robust, extensible platform designed to facilitate real-time data streaming, processing, and state management. It provides developers with a comprehensive suite of tools and libraries to build scalable, high-performance data pipelines tailored to diverse use cases. By abstracting underlying streaming technologies and state management solutions, Cortex Data Framework enables seamless integration, simplified development workflows, and enhanced maintainability for complex data-driven applications. + + + https://github.com/buildersoftio/cortex + cortex vortex mediator eda streaming distributed streams states kafka pulsar rocksdb + + 1.0.1 + license.md + cortex.png + Cortex.Serialization.Yaml + True + True + True + git + Just as the Cortex in our brains handles complex processing efficiently, Cortex Data Framework brings brainpower to your data management! + https://buildersoft.io/ + README.md + + + + + 9.0 + + + + + True + \ + Always + + + + + + True + + + + True + + + + diff --git a/src/Cortex.Serialization.Yaml/Emitter/Emitter.cs b/src/Cortex.Serialization.Yaml/Emitter/Emitter.cs index 324a767..f670538 100644 --- a/src/Cortex.Serialization.Yaml/Emitter/Emitter.cs +++ b/src/Cortex.Serialization.Yaml/Emitter/Emitter.cs @@ -1,4 +1,5 @@ using Cortex.Serialization.Yaml.Parser; +using System; using System.Text; namespace Cortex.Serialization.Yaml.Emitter diff --git a/src/Cortex.Serialization.Yaml/Parser/Parser.cs b/src/Cortex.Serialization.Yaml/Parser/Parser.cs index 44eb5b5..3b59445 100644 --- a/src/Cortex.Serialization.Yaml/Parser/Parser.cs +++ b/src/Cortex.Serialization.Yaml/Parser/Parser.cs @@ -1,4 +1,6 @@ using Cortex.Serialization.Yaml.Common; +using System.Collections.Generic; +using System.Linq; namespace Cortex.Serialization.Yaml.Parser { diff --git a/src/Cortex.Serialization.Yaml/Parser/Scanner.cs b/src/Cortex.Serialization.Yaml/Parser/Scanner.cs index 6c063f0..3f4305b 100644 --- a/src/Cortex.Serialization.Yaml/Parser/Scanner.cs +++ b/src/Cortex.Serialization.Yaml/Parser/Scanner.cs @@ -1,4 +1,6 @@ -namespace Cortex.Serialization.Yaml.Parser +using System.Collections.Generic; + +namespace Cortex.Serialization.Yaml.Parser { internal sealed class Scanner { diff --git a/src/Cortex.Serialization.Yaml/Parser/Token.cs b/src/Cortex.Serialization.Yaml/Parser/Token.cs index 6db3781..b71748a 100644 --- a/src/Cortex.Serialization.Yaml/Parser/Token.cs +++ b/src/Cortex.Serialization.Yaml/Parser/Token.cs @@ -1,4 +1,6 @@ -namespace Cortex.Serialization.Yaml.Parser +using System.Collections.Generic; + +namespace Cortex.Serialization.Yaml.Parser { internal sealed record Token(TokenType Type, string? Value, int Line, int Column); internal abstract record YamlNode; diff --git a/src/Cortex.Serialization.Yaml/README.md b/src/Cortex.Serialization.Yaml/README.md new file mode 100644 index 0000000..df1dec6 --- /dev/null +++ b/src/Cortex.Serialization.Yaml/README.md @@ -0,0 +1,215 @@ +# Cortex.Serialization.Yaml 🧠 + +**Cortex.Serialization.Yaml** A lightweight, dependency‑free YAML serializer/deserializer for .NET 8+. + +Built as part of the [Cortex Data Framework](https://github.com/buildersoftio/cortex), this library simplifies serializer/deserializer for YAML: + + +- ✅ **Serialize & Deserialize** POCOs, collections, and dictionaries +- ✅ **Naming conventions**: CamelCase, PascalCase, SnakeCase, KebabCase, Original +- ✅ **Attributes** `[YamlProperty(Name=…)]`, `[YamlIgnore]` +- ✅ **Custom type converters** via `IYamlTypeConverter` (primitive/date/guid built‑ins included) +- ✅ **Settings**: indentation, emit nulls/defaults, sort properties, case‑insensitive matching + +This version doesnot include: flow style ([], {}), comments preservation, anchors/aliases & merge keys, custom tags, streaming APIs + +--- + +[![GitHub License](https://img.shields.io/github/license/buildersoftio/cortex)](https://github.com/buildersoftio/cortex/blob/master/LICENSE) +[![NuGet Version](https://img.shields.io/nuget/v/Cortex.Serialization.Yaml?label=Cortex.Serialization.Yaml)](https://www.nuget.org/packages/Cortex.Serialization.Yaml) +[![GitHub contributors](https://img.shields.io/github/contributors/buildersoftio/cortex)](https://github.com/buildersoftio/cortex) +[![Discord Shield](https://discord.com/api/guilds/1310034212371566612/widget.png?style=shield)](https://discord.gg/JnMJV33QHu) + + +## 🚀 Getting Started + +### Install via NuGet + +```bash +dotnet add package Cortex.Serialization.Yaml +``` + +## 🛠️ Quick Start +```csharp +using Cortex.Serialization.Yaml.Serialization; +using Cortex.Serialization.Yaml.Serialization.Conventions; + +public sealed record Address(string Street, string City); +public sealed class Person +{ + public string FirstName { get; set; } = string.Empty; + public string LastName { get; set; } = string.Empty; + public int Age { get; set; } + public List Tags { get; set; } = new(); + public Address? Address { get; set; } +} + +var person = new Person +{ + FirstName = "Ada", + LastName = "Lovelace", + Age = 36, + Tags = ["math", "poet"], + Address = new("12 St James's Sq", "London") +}; + +var serializer = new YamlSerializer(new YamlSerializerSettings +{ + NamingConvention = new SnakeCaseConvention(), + EmitNulls = false +}); + +string yaml = serializer.Serialize(person); +Console.WriteLine(yaml); + +var deserializer = new YamlDeserializer(new YamlDeserializerSettings +{ + NamingConvention = new SnakeCaseConvention() +}); + +var model = deserializer.Deserialize(yaml); +Console.WriteLine($"Hello {model.FirstName} {model.LastName}, {model.Age}"); +``` + +## 🔧 Configuration & Options + +### Serializer settings +```csharp +var settings = new YamlSerializerSettings +{ + NamingConvention = new CamelCaseConvention(), // how CLR names map to YAML keys + EmitNulls = true, // include null properties + EmitDefaults = true, // include default(T) values + SortProperties = false, // keep reflection order + Indentation = 2 // spaces per indent level +}; +``` + +### Deserializer settings + +```csharp +var settings = new YamlDeserializerSettings +{ + NamingConvention = new SnakeCaseConvention(), + CaseInsensitive = true, + IgnoreUnmatchedProperties = true +}; +``` + +## 📚 Examples + +### 1) Lists and nested objects + +```csharp +var yaml = """ +first_name: Ada +last_name: Lovelace +age: 36 +tags: + - math + - poet +address: + street: 12 St James's Sq + city: London +"""; + +var des = new YamlDeserializer(new YamlDeserializerSettings { NamingConvention = new SnakeCaseConvention() }); +var p = des.Deserialize(yaml); + +var s = new YamlSerializer(new YamlSerializerSettings { NamingConvention = new SnakeCaseConvention(), EmitNulls = false }); +var outYaml = s.Serialize(p); +``` + +### 2) Block scalars (| and >) + +```yaml +description: | + First line kept + Second line kept +note: > + Lines are folded + into a single paragraph +``` +These map to string properties on your CLR model. + +### 3) Attributes and explicit names + +```csharp +public sealed class Product +{ + [YamlProperty(Name = "product_id")] // explicit YAML key + public Guid Id { get; set; } + + [YamlIgnore] + public string? InternalNotes { get; set; } +} +``` + +### 4) Custom converters + +```csharp +public sealed class YesNoBoolConverter : IYamlTypeConverter +{ + public bool CanConvert(Type t) => t == typeof(bool); + public object? Read(object? node, Type targetType) => string.Equals(node?.ToString(), "yes", StringComparison.OrdinalIgnoreCase); + public object? Write(object? value, Type declared) => (bool?)value == true ? "yes" : "no"; +} + +var s = new YamlSerializer(new YamlSerializerSettings()); +s.Converters.Add(new YesNoBoolConverter()); +``` + +## ⚠️ Limits (current version) +- No flow style (`[]`, `{}`) collections +- No **comments** preservation/round‑trip of `# …` +- No **anchors/aliases/merge keys** +- No **custom tags** +- **Pragmatic YAML subset**; quoting/escaping is intentionally simple + + +## 💬 Contributing +We welcome contributions from the community! Whether it's reporting bugs, suggesting features, or submitting pull requests, your involvement helps improve Cortex for everyone. + +### 💬 How to Contribute +1. **Fork the Repository** +2. **Create a Feature Branch** +```bash +git checkout -b feature/YourFeature +``` +3. **Commit Your Changes** +```bash +git commit -m "Add your feature" +``` +4. **Push to Your Fork** +```bash +git push origin feature/YourFeature +``` +5. **Open a Pull Request** + +Describe your changes and submit the pull request for review. + +## 📄 License +This project is licensed under the MIT License. + +## 📚 Sponsorship +Cortex is an open-source project maintained by BuilderSoft. Your support helps us continue developing and improving Cortex. Consider sponsoring us to contribute to the future of resilient streaming platforms. + +### How to Sponsor +* **Financial Contributions**: Support us through [GitHub Sponsors](https://github.com/sponsors/buildersoftio) or other preferred platforms. +* **Corporate Sponsorship**: If your organization is interested in sponsoring Cortex, please contact us directly. + +Contact Us: cortex@buildersoft.io + + +## Contact +We'd love to hear from you! Whether you have questions, feedback, or need support, feel free to reach out. + +- Email: cortex@buildersoft.io +- Website: https://buildersoft.io +- GitHub Issues: [Cortex Data Framework Issues](https://github.com/buildersoftio/cortex/issues) +- Join our Discord Community: [![Discord Shield](https://discord.com/api/guilds/1310034212371566612/widget.png?style=shield)](https://discord.gg/JnMJV33QHu) + + +Thank you for using Cortex Data Framework! We hope it empowers you to build scalable and efficient data processing pipelines effortlessly. + +Built with ❤️ by the Buildersoft team. diff --git a/src/Cortex.Serialization.Yaml/Reflection/CachedTypeInfo.cs b/src/Cortex.Serialization.Yaml/Reflection/CachedTypeInfo.cs index 2db6579..196e64e 100644 --- a/src/Cortex.Serialization.Yaml/Reflection/CachedTypeInfo.cs +++ b/src/Cortex.Serialization.Yaml/Reflection/CachedTypeInfo.cs @@ -1,4 +1,7 @@ -namespace Cortex.Serialization.Yaml.Reflection +using System; +using System.Linq; + +namespace Cortex.Serialization.Yaml.Reflection { internal sealed class CachedTypeInfo { diff --git a/src/Cortex.Serialization.Yaml/Reflection/PropertyMap.cs b/src/Cortex.Serialization.Yaml/Reflection/PropertyMap.cs index 99a40bd..d2dfd89 100644 --- a/src/Cortex.Serialization.Yaml/Reflection/PropertyMap.cs +++ b/src/Cortex.Serialization.Yaml/Reflection/PropertyMap.cs @@ -1,4 +1,6 @@ using Cortex.Serialization.Yaml.Attributes; +using System; +using System.Collections.Generic; using System.Reflection; namespace Cortex.Serialization.Yaml.Reflection diff --git a/src/Cortex.Serialization.Yaml/Reflection/TypeInspector.cs b/src/Cortex.Serialization.Yaml/Reflection/TypeInspector.cs index 8dff33b..1c3ed17 100644 --- a/src/Cortex.Serialization.Yaml/Reflection/TypeInspector.cs +++ b/src/Cortex.Serialization.Yaml/Reflection/TypeInspector.cs @@ -1,4 +1,7 @@ using Cortex.Serialization.Yaml.Converters; +using System; +using System.Collections.Generic; +using System.Linq; namespace Cortex.Serialization.Yaml.Reflection { diff --git a/src/Cortex.Serialization.Yaml/Serialization/Converters/YamlConverterAttribute.cs b/src/Cortex.Serialization.Yaml/Serialization/Converters/YamlConverterAttribute.cs index 366d929..e2cae96 100644 --- a/src/Cortex.Serialization.Yaml/Serialization/Converters/YamlConverterAttribute.cs +++ b/src/Cortex.Serialization.Yaml/Serialization/Converters/YamlConverterAttribute.cs @@ -1,4 +1,6 @@ -namespace Cortex.Serialization.Yaml.Converters +using System; + +namespace Cortex.Serialization.Yaml.Converters { [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum)] public sealed class YamlConverterAttribute : Attribute diff --git a/src/Cortex.Serialization.Yaml/Serialization/YamlSerializerSettings.cs b/src/Cortex.Serialization.Yaml/Serialization/YamlSerializerSettings.cs index 7da5989..7ee372f 100644 --- a/src/Cortex.Serialization.Yaml/Serialization/YamlSerializerSettings.cs +++ b/src/Cortex.Serialization.Yaml/Serialization/YamlSerializerSettings.cs @@ -1,4 +1,5 @@ using Cortex.Serialization.Yaml.Converters; +using System.Collections.Generic; namespace Cortex.Serialization.Yaml { diff --git a/src/Cortex.Serialization.Yaml/YamlDeserializer.cs b/src/Cortex.Serialization.Yaml/YamlDeserializer.cs index 51655ef..1bab558 100644 --- a/src/Cortex.Serialization.Yaml/YamlDeserializer.cs +++ b/src/Cortex.Serialization.Yaml/YamlDeserializer.cs @@ -1,4 +1,8 @@ using Cortex.Serialization.Yaml.Converters; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; namespace Cortex.Serialization.Yaml { diff --git a/src/Cortex.Serialization.Yaml/YamlSerializer.cs b/src/Cortex.Serialization.Yaml/YamlSerializer.cs index 9774d2e..b8a4957 100644 --- a/src/Cortex.Serialization.Yaml/YamlSerializer.cs +++ b/src/Cortex.Serialization.Yaml/YamlSerializer.cs @@ -1,4 +1,7 @@ using Cortex.Serialization.Yaml.Reflection; +using System; +using System.Collections.Generic; +using System.Linq; namespace Cortex.Serialization.Yaml { From ee94bbfe500ee1343a67c18dfbe02aebaecf9e42 Mon Sep 17 00:00:00 2001 From: Enes Hoxha Date: Wed, 22 Oct 2025 13:18:19 +0200 Subject: [PATCH 3/6] v2/feature/ #148: Implement ValidationCommandBehavior Update package icon and add validation behavior Replaced `andyX.png` with `cortex.png` as the package icon in the project files, reflecting a branding update. Updated the `` property and asset references in `Cortex.Mediator.Behaviors.FluentValidation.csproj`. Added a new `ValidationCommandBehavior` class in `VoidValidationCommandBehavior.cs`. This class implements `ICommandPipelineBehavior` and introduces command validation using FluentValidation, throwing a `ValidationException` for validation failures. --- .../Assets/{andyX.png => cortex.png} | Bin ...Mediator.Behaviors.FluentValidation.csproj | 4 +- .../VoidValidationCommandBehavior.cs | 42 ++++++++++++++++++ 3 files changed, 44 insertions(+), 2 deletions(-) rename src/Cortex.Mediator.Behaviors.FluentValidation/Assets/{andyX.png => cortex.png} (100%) create mode 100644 src/Cortex.Mediator.Behaviors.FluentValidation/VoidValidationCommandBehavior.cs diff --git a/src/Cortex.Mediator.Behaviors.FluentValidation/Assets/andyX.png b/src/Cortex.Mediator.Behaviors.FluentValidation/Assets/cortex.png similarity index 100% rename from src/Cortex.Mediator.Behaviors.FluentValidation/Assets/andyX.png rename to src/Cortex.Mediator.Behaviors.FluentValidation/Assets/cortex.png diff --git a/src/Cortex.Mediator.Behaviors.FluentValidation/Cortex.Mediator.Behaviors.FluentValidation.csproj b/src/Cortex.Mediator.Behaviors.FluentValidation/Cortex.Mediator.Behaviors.FluentValidation.csproj index f367543..e46489f 100644 --- a/src/Cortex.Mediator.Behaviors.FluentValidation/Cortex.Mediator.Behaviors.FluentValidation.csproj +++ b/src/Cortex.Mediator.Behaviors.FluentValidation/Cortex.Mediator.Behaviors.FluentValidation.csproj @@ -21,7 +21,7 @@ 1.0.0 license.md - andyX.png + cortex.png Cortex.Mediator.Behaviors.FluentValidation True True @@ -46,7 +46,7 @@ - + True diff --git a/src/Cortex.Mediator.Behaviors.FluentValidation/VoidValidationCommandBehavior.cs b/src/Cortex.Mediator.Behaviors.FluentValidation/VoidValidationCommandBehavior.cs new file mode 100644 index 0000000..8d28554 --- /dev/null +++ b/src/Cortex.Mediator.Behaviors.FluentValidation/VoidValidationCommandBehavior.cs @@ -0,0 +1,42 @@ +using Cortex.Mediator.Commands; +using FluentValidation; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace Cortex.Mediator.Behaviors +{ + /// + /// Pipeline behavior for validation command execution. + /// + public sealed class ValidationCommandBehavior : ICommandPipelineBehavior + where TCommand : ICommand + { + private readonly IEnumerable> _validators; + + + public async Task Handle(TCommand command, CommandHandlerDelegate next, CancellationToken cancellationToken) + { + var context = new ValidationContext(command); + var failures = _validators + .Select(v => v.Validate(context)) + .SelectMany(r => r.Errors) + .Where(f => f != null) + .ToList(); + + if (failures.Count() > 0) + { + var errors = failures + .GroupBy(f => f.PropertyName) + .ToDictionary( + g => g.Key, + g => g.Select(f => f.ErrorMessage).ToArray()); + + throw new Exceptions.ValidationException(errors); + } + + await next(); + } + } +} From c6202323a6c3907f85022aae975a2552bdc398df Mon Sep 17 00:00:00 2001 From: Enes Hoxha Date: Sat, 15 Nov 2025 20:02:46 +0100 Subject: [PATCH 4/6] v2/feature/ #158: Add Async/Await support Add async support to data store interfaces and refactor Refactor `IDataStore` into its own file for better modularity. Introduce `IAsyncDataStore` with async methods like `GetAsync`, `PutAsync`, and `RemoveAsync`, including `IAsyncEnumerable` support for .NET Standard 2.1+ and .NET Core 3.0+. Update `InMemoryStateStore` to implement `IAsyncDataStore`, adding async methods for key-value operations. Improve input validation in the constructor. Reorganize namespaces and add conditional compilation for framework-specific features. These changes enhance scalability, modernize the API, and improve maintainability. --- .../Abstractions/IAsyncDataStore.cs | 42 +++++++ .../{IStateStore.cs => IDataStore.cs} | 0 src/Cortex.States/InMemoryStateStore.cs | 104 +++++++++++++++++- 3 files changed, 143 insertions(+), 3 deletions(-) create mode 100644 src/Cortex.States/Abstractions/IAsyncDataStore.cs rename src/Cortex.States/Abstractions/{IStateStore.cs => IDataStore.cs} (100%) diff --git a/src/Cortex.States/Abstractions/IAsyncDataStore.cs b/src/Cortex.States/Abstractions/IAsyncDataStore.cs new file mode 100644 index 0000000..621f9aa --- /dev/null +++ b/src/Cortex.States/Abstractions/IAsyncDataStore.cs @@ -0,0 +1,42 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Cortex.States.Abstractions +{ + public interface IAsyncDataStore : IDataStore + { + Task GetAsync( + TKey key, + CancellationToken cancellationToken = default); + + Task PutAsync( + TKey key, + TValue value, + CancellationToken cancellationToken = default); + + Task ContainsKeyAsync( + TKey key, + CancellationToken cancellationToken = default); + + Task RemoveAsync( + TKey key, + CancellationToken cancellationToken = default); + +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_0_OR_GREATER + IAsyncEnumerable> GetAllAsync( + CancellationToken cancellationToken = default); + + IAsyncEnumerable GetKeysAsync( + CancellationToken cancellationToken = default); +#endif + + Task> GetManyAsync( + IEnumerable keys, + CancellationToken cancellationToken = default); + + Task PutManyAsync( + IEnumerable> items, + CancellationToken cancellationToken = default); + } +} diff --git a/src/Cortex.States/Abstractions/IStateStore.cs b/src/Cortex.States/Abstractions/IDataStore.cs similarity index 100% rename from src/Cortex.States/Abstractions/IStateStore.cs rename to src/Cortex.States/Abstractions/IDataStore.cs diff --git a/src/Cortex.States/InMemoryStateStore.cs b/src/Cortex.States/InMemoryStateStore.cs index e898aed..173114b 100644 --- a/src/Cortex.States/InMemoryStateStore.cs +++ b/src/Cortex.States/InMemoryStateStore.cs @@ -1,10 +1,14 @@ -using System; +using Cortex.States.Abstractions; +using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; namespace Cortex.States { - public class InMemoryStateStore : IDataStore + public class InMemoryStateStore : IAsyncDataStore { private readonly ConcurrentDictionary _store = new ConcurrentDictionary(); @@ -12,7 +16,7 @@ public class InMemoryStateStore : IDataStore public InMemoryStateStore(string name) { - Name = name; + Name = name ?? throw new ArgumentNullException(nameof(name)); } public TValue Get(TKey key) @@ -47,5 +51,99 @@ public IEnumerable GetKeys() { return _store.Keys; } + + public Task GetAsync(TKey key, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + + _store.TryGetValue(key, out var value); + return Task.FromResult(value); + } + + public Task PutAsync(TKey key, TValue value, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + + _store[key] = value; + return Task.CompletedTask; + } + + public Task ContainsKeyAsync(TKey key, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + + var contains = _store.ContainsKey(key); + return Task.FromResult(contains); + } + + public Task RemoveAsync(TKey key, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + + _store.TryRemove(key, out _); + return Task.CompletedTask; + } + +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_0_OR_GREATER + public async IAsyncEnumerable> GetAllAsync( + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + foreach (var item in _store) + { + cancellationToken.ThrowIfCancellationRequested(); + yield return item; + + // Optional: yield control for very large stores + await Task.Yield(); + } + } + + public async IAsyncEnumerable GetKeysAsync( + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + foreach (var key in _store.Keys) + { + cancellationToken.ThrowIfCancellationRequested(); + yield return key; + + // Optional: yield control for very large keysets + await Task.Yield(); + } + } +#endif + + public Task> GetManyAsync(IEnumerable keys, CancellationToken cancellationToken = default) + { + if (keys == null) throw new ArgumentNullException(nameof(keys)); + cancellationToken.ThrowIfCancellationRequested(); + + var result = new Dictionary(); + + foreach (var key in keys) + { + cancellationToken.ThrowIfCancellationRequested(); + + if (_store.TryGetValue(key, out var value)) + { + result[key] = value; + } + } + + return Task.FromResult((IDictionary)result); + } + + public Task PutManyAsync(IEnumerable> items, CancellationToken cancellationToken = default) + { + if (items == null) throw new ArgumentNullException(nameof(items)); + cancellationToken.ThrowIfCancellationRequested(); + + foreach (var item in items) + { + cancellationToken.ThrowIfCancellationRequested(); + _store[item.Key] = item.Value; + } + + return Task.CompletedTask; + } } } From a41ed861de3f3f7e5e98a0c7e901fcd6b24723a6 Mon Sep 17 00:00:00 2001 From: devilishgoat Date: Thu, 18 Dec 2025 16:00:42 +0000 Subject: [PATCH 5/6] hookup the FluentValidation validators with behaviours --- Cortex.sln | 3 +- .../MediatorOptionsExtensions.cs | 16 +++++ .../ServiceCollectionExtensions.cs | 16 +++++ .../ValidationCommandBehavior.cs | 8 ++- .../ValidationQueryBehavior.cs | 8 ++- .../VoidValidationCommandBehavior.cs | 8 ++- src/Cortex.Tests/Cortex.Tests.csproj | 9 +-- .../Tests/ValidationCommandBehaviorTests.cs | 70 +++++++++++++++++++ .../Tests/ValidationQueryBehaviorTests.cs | 69 ++++++++++++++++++ .../VoidValidationCommandBehaviorTests.cs | 66 +++++++++++++++++ 10 files changed, 261 insertions(+), 12 deletions(-) create mode 100644 src/Cortex.Mediator.Behaviors.FluentValidation/DependencyInjection/MediatorOptionsExtensions.cs create mode 100644 src/Cortex.Mediator.Behaviors.FluentValidation/DependencyInjection/ServiceCollectionExtensions.cs create mode 100644 src/Cortex.Tests/Mediator/FluentValidation/Tests/ValidationCommandBehaviorTests.cs create mode 100644 src/Cortex.Tests/Mediator/FluentValidation/Tests/ValidationQueryBehaviorTests.cs create mode 100644 src/Cortex.Tests/Mediator/FluentValidation/Tests/VoidValidationCommandBehaviorTests.cs diff --git a/Cortex.sln b/Cortex.sln index 4c8bdf3..02961c6 100644 --- a/Cortex.sln +++ b/Cortex.sln @@ -1,6 +1,6 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 18 -VisualStudioVersion = 18.0.11111.16 d18.0 +VisualStudioVersion = 18.0.11111.16 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Cortex.Mediator", "src\Cortex.Mediator\Cortex.Mediator.csproj", "{F1CC775A-95DA-4A5A-879F-66BFCB0FDCC9}" EndProject @@ -129,6 +129,7 @@ Global {D51C6B82-ABD9-4C43-820E-237EDBD706A1}.Release|Any CPU.ActiveCfg = Release|AnyCPU {D51C6B82-ABD9-4C43-820E-237EDBD706A1}.Release|Any CPU.Build.0 = Release|AnyCPU {C9A7699A-9BCF-4B51-B29F-ABF78DCEA553}.Debug|Any CPU.ActiveCfg = Debug|AnyCPU + {C9A7699A-9BCF-4B51-B29F-ABF78DCEA553}.Debug|Any CPU.Build.0 = Debug|AnyCPU {C9A7699A-9BCF-4B51-B29F-ABF78DCEA553}.Release|Any CPU.ActiveCfg = Release|AnyCPU {C9A7699A-9BCF-4B51-B29F-ABF78DCEA553}.Release|Any CPU.Build.0 = Release|AnyCPU {D376D6CA-3192-4EDC-B840-31F58B6457DD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU diff --git a/src/Cortex.Mediator.Behaviors.FluentValidation/DependencyInjection/MediatorOptionsExtensions.cs b/src/Cortex.Mediator.Behaviors.FluentValidation/DependencyInjection/MediatorOptionsExtensions.cs new file mode 100644 index 0000000..efc042f --- /dev/null +++ b/src/Cortex.Mediator.Behaviors.FluentValidation/DependencyInjection/MediatorOptionsExtensions.cs @@ -0,0 +1,16 @@ +using Cortex.Mediator.DependencyInjection; + +namespace Cortex.Mediator.Behaviors.FluentValidation.DependencyInjection +{ + public static class MediatorOptionsExtensions + { + public static MediatorOptions AddFluentValidationBehaviors(this MediatorOptions options) + { + options.AddOpenCommandPipelineBehavior(typeof(ValidationCommandBehavior<,>)) + .AddOpenQueryPipelineBehavior(typeof(ValidationQueryBehavior<,>)) + .AddOpenCommandPipelineBehavior(typeof(ValidationCommandBehavior<>)); + + return options; + } + } +} diff --git a/src/Cortex.Mediator.Behaviors.FluentValidation/DependencyInjection/ServiceCollectionExtensions.cs b/src/Cortex.Mediator.Behaviors.FluentValidation/DependencyInjection/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..9f2f5db --- /dev/null +++ b/src/Cortex.Mediator.Behaviors.FluentValidation/DependencyInjection/ServiceCollectionExtensions.cs @@ -0,0 +1,16 @@ +using FluentValidation; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Linq; + +namespace Cortex.Mediator.Behaviors.FluentValidation.DependencyInjection +{ + public static class ServiceCollectionExtensions + { + public static IServiceCollection AddFluentValidationValidators(this IServiceCollection services, Type[] validationAssemblyMarkerTypes) + { + services.AddValidatorsFromAssemblies(validationAssemblyMarkerTypes.Select(t => t.Assembly)); + return services; + } + } +} diff --git a/src/Cortex.Mediator.Behaviors.FluentValidation/ValidationCommandBehavior.cs b/src/Cortex.Mediator.Behaviors.FluentValidation/ValidationCommandBehavior.cs index b9eb2e3..4baaf02 100644 --- a/src/Cortex.Mediator.Behaviors.FluentValidation/ValidationCommandBehavior.cs +++ b/src/Cortex.Mediator.Behaviors.FluentValidation/ValidationCommandBehavior.cs @@ -1,6 +1,5 @@ using Cortex.Mediator.Commands; using FluentValidation; -using System; using System.Collections.Generic; using System.Linq; using System.Threading; @@ -16,12 +15,17 @@ public sealed class ValidationCommandBehavior : ICommandPipel { private readonly IEnumerable> _validators; + public ValidationCommandBehavior(IEnumerable> validators) + { + _validators = validators; + } public async Task Handle(TCommand command, CommandHandlerDelegate next, CancellationToken cancellationToken) { var context = new ValidationContext(command); var failures = _validators - .Select(v => v.Validate(context)) + .Select(async v => await v.ValidateAsync(context)) + .Select(r => r.Result) .SelectMany(r => r.Errors) .Where(f => f != null) .ToList(); diff --git a/src/Cortex.Mediator.Behaviors.FluentValidation/ValidationQueryBehavior.cs b/src/Cortex.Mediator.Behaviors.FluentValidation/ValidationQueryBehavior.cs index b48c633..b446524 100644 --- a/src/Cortex.Mediator.Behaviors.FluentValidation/ValidationQueryBehavior.cs +++ b/src/Cortex.Mediator.Behaviors.FluentValidation/ValidationQueryBehavior.cs @@ -13,12 +13,18 @@ public sealed class ValidationQueryBehavior : IQueryPipelineBeh private readonly IEnumerable> _validators; + public ValidationQueryBehavior(IEnumerable> validators) + { + _validators = validators; + } + public async Task Handle(TQuery query, QueryHandlerDelegate next, CancellationToken cancellationToken) { var context = new ValidationContext(query); var failures = _validators - .Select(v => v.Validate(context)) + .Select(async v => await v.ValidateAsync(context)) + .Select(r => r.Result) .SelectMany(r => r.Errors) .Where(f => f != null) .ToList(); diff --git a/src/Cortex.Mediator.Behaviors.FluentValidation/VoidValidationCommandBehavior.cs b/src/Cortex.Mediator.Behaviors.FluentValidation/VoidValidationCommandBehavior.cs index 8d28554..e04a43d 100644 --- a/src/Cortex.Mediator.Behaviors.FluentValidation/VoidValidationCommandBehavior.cs +++ b/src/Cortex.Mediator.Behaviors.FluentValidation/VoidValidationCommandBehavior.cs @@ -15,12 +15,18 @@ public sealed class ValidationCommandBehavior : ICommandPipelineBehavi { private readonly IEnumerable> _validators; + public ValidationCommandBehavior(IEnumerable> validators) + { + _validators = validators; + } + public async Task Handle(TCommand command, CommandHandlerDelegate next, CancellationToken cancellationToken) { var context = new ValidationContext(command); var failures = _validators - .Select(v => v.Validate(context)) + .Select(async v => await v.ValidateAsync(context)) + .Select(r => r.Result) .SelectMany(r => r.Errors) .Where(f => f != null) .ToList(); diff --git a/src/Cortex.Tests/Cortex.Tests.csproj b/src/Cortex.Tests/Cortex.Tests.csproj index 6208286..5de5105 100644 --- a/src/Cortex.Tests/Cortex.Tests.csproj +++ b/src/Cortex.Tests/Cortex.Tests.csproj @@ -1,4 +1,4 @@ - + net8.0 @@ -9,12 +9,6 @@ true - - - - - - all @@ -30,6 +24,7 @@ + diff --git a/src/Cortex.Tests/Mediator/FluentValidation/Tests/ValidationCommandBehaviorTests.cs b/src/Cortex.Tests/Mediator/FluentValidation/Tests/ValidationCommandBehaviorTests.cs new file mode 100644 index 0000000..b77ec8e --- /dev/null +++ b/src/Cortex.Tests/Mediator/FluentValidation/Tests/ValidationCommandBehaviorTests.cs @@ -0,0 +1,70 @@ +using Cortex.Mediator.Behaviors; +using Cortex.Mediator.Commands; +using FluentValidation; +using FluentValidation.Results; +using Moq; + +namespace Cortex.Tests.Mediator.FluentValidation.Tests +{ + public class ValidationCommandBehaviorTests + { + public class FakeCommand : ICommand + { + } + + [Fact] + public async Task Handle_ShouldNotThrowExceptionsWhenThereAreNoValidationFailures() + { + // Arrange + var expectedResult = "completed"; + var validator = new Mock>(); + validator.Setup(v => v.ValidateAsync(It.IsAny(), default)).Returns(() => Task.FromResult(new ValidationResult { Errors = new List()})); + var validators = new IValidator[] + { + validator.Object, + }; + + var next = new Mock>(); + next.Setup(n => n.Invoke()).Returns(Task.FromResult(expectedResult)); + var systemUnderTest = new ValidationCommandBehavior(validators); + + // Act + + var result = await systemUnderTest.Handle(new FakeCommand(), next.Object, CancellationToken.None); + + // Assert + Assert.NotNull(result); + Assert.Equal(expectedResult, result); + } + + [Fact] + public async Task Handle_ShouldThrowAnExceptionsWhenThereIsAValidationFailure() + { + // Arrange + var expectedResult = "completed"; + var validator = new Mock>(); + validator.Setup(v => v.ValidateAsync(It.IsAny(), default)) + .Returns(() => Task.FromResult(new ValidationResult + { + Errors = new List + { + new ValidationFailure("some-property", "was invalid") + } + } + )); + + var validators = new IValidator[] + { + validator.Object, + }; + + var next = new Mock>(); + next.Setup(n => n.Invoke()).Returns(Task.FromResult(expectedResult)); + var systemUnderTest = new ValidationCommandBehavior(validators); + + // Act + // Assert + await Assert.ThrowsAsync(async () => await systemUnderTest.Handle(new FakeCommand(), next.Object, CancellationToken.None)); + } + } +} diff --git a/src/Cortex.Tests/Mediator/FluentValidation/Tests/ValidationQueryBehaviorTests.cs b/src/Cortex.Tests/Mediator/FluentValidation/Tests/ValidationQueryBehaviorTests.cs new file mode 100644 index 0000000..882e70c --- /dev/null +++ b/src/Cortex.Tests/Mediator/FluentValidation/Tests/ValidationQueryBehaviorTests.cs @@ -0,0 +1,69 @@ +using Cortex.Mediator.Behaviors.FluentValidation; +using Cortex.Mediator.Queries; +using FluentValidation; +using FluentValidation.Results; +using Moq; + +namespace Cortex.Tests.Mediator.FluentValidation.Tests +{ + public class FakeQuery : IQuery + { + } + + public class ValidationQueryBehaviorTests + { + [Fact] + public async Task Handle_ShouldNotThrowExceptionsWhenThereAreNoValidationFailures() + { + // Arrange + var expectedResult = "completed"; + var validator = new Mock>(); + validator.Setup(v => v.ValidateAsync(It.IsAny(), default)).Returns(() => Task.FromResult(new ValidationResult { Errors = new List()})); + var validators = new IValidator[] + { + validator.Object, + }; + + var next = new Mock>(); + next.Setup(n => n.Invoke()).Returns(Task.FromResult(expectedResult)); + var systemUnderTest = new ValidationQueryBehavior(validators); + + // Act + var result = await systemUnderTest.Handle(new FakeQuery(), next.Object, CancellationToken.None); + + // Assert + Assert.NotNull(result); + Assert.Equal(expectedResult, result); + } + + [Fact] + public async Task Handle_ShouldThrowAnExceptionsWhenThereIsAValidationFailure() + { + // Arrange + var expectedResult = "completed"; + var validator = new Mock>(); + validator.Setup(v => v.ValidateAsync(It.IsAny(), default)) + .Returns(() => Task.FromResult(new ValidationResult + { + Errors = + [ + new("some-property", "was invalid") + ] + } + )); + + var validators = new IValidator[] + { + validator.Object, + }; + + var next = new Mock>(); + next.Setup(n => n.Invoke()).Returns(Task.FromResult(expectedResult)); + var systemUnderTest = new ValidationQueryBehavior(validators); + + // Act + // Assert + await Assert.ThrowsAsync(async () => await systemUnderTest.Handle(new FakeQuery(), next.Object, CancellationToken.None)); + } + } +} diff --git a/src/Cortex.Tests/Mediator/FluentValidation/Tests/VoidValidationCommandBehaviorTests.cs b/src/Cortex.Tests/Mediator/FluentValidation/Tests/VoidValidationCommandBehaviorTests.cs new file mode 100644 index 0000000..9632715 --- /dev/null +++ b/src/Cortex.Tests/Mediator/FluentValidation/Tests/VoidValidationCommandBehaviorTests.cs @@ -0,0 +1,66 @@ +using Cortex.Mediator.Behaviors; +using Cortex.Mediator.Commands; +using FluentValidation; +using FluentValidation.Results; +using Moq; + +namespace Cortex.Tests.Mediator.FluentValidation.Tests +{ + public class FakeVoidCommand : ICommand + { + } + + public class VoidValidationCommandBehaviorTests + { + [Fact] + public async Task Handle_ShouldNotThrowExceptionsWhenThereAreNoValidationFailures() + { + // Arrange + var expectedResult = "completed"; + var validator = new Mock>(); + validator.Setup(v => v.ValidateAsync(It.IsAny(), default)).Returns(() => Task.FromResult(new ValidationResult { Errors = new List()})); + var validators = new IValidator[] + { + validator.Object, + }; + + var next = new Mock(); + next.Setup(n => n.Invoke()).Returns(Task.FromResult(expectedResult)); + var systemUnderTest = new ValidationCommandBehavior(validators); + + // Act + // Assert + await systemUnderTest.Handle(new FakeVoidCommand(), next.Object, CancellationToken.None); + } + + [Fact] + public async Task Handle_ShouldThrowAnExceptionsWhenThereIsAValidationFailure() + { + // Arrange + var expectedResult = "completed"; + var validator = new Mock>(); + validator.Setup(v => v.ValidateAsync(It.IsAny(), default)) + .Returns(() => Task.FromResult(new ValidationResult + { + Errors = new List + { + new ValidationFailure("some-property", "was invalid") + } + } + )); + + var validators = new IValidator[] + { + validator.Object, + }; + + var next = new Mock(); + next.Setup(n => n.Invoke()).Returns(Task.FromResult(expectedResult)); + var systemUnderTest = new ValidationCommandBehavior(validators); + + // Act + // Assert + await Assert.ThrowsAsync(async () => await systemUnderTest.Handle(new FakeVoidCommand(), next.Object, CancellationToken.None)); + } + } +} From c78c80da459c86ceaeed12471323a41b43b56f61 Mon Sep 17 00:00:00 2001 From: Enes Hoxha Date: Sat, 24 Jan 2026 13:57:24 +0100 Subject: [PATCH 6/6] v2.2/bug/161: Refactor AddCortexMediator method signature Removed the IConfiguration parameter from AddCortexMediator and made the configure parameter non-nullable. This streamlines service registration and eliminates the need to provide a configuration object. --- .../DependencyInjection/ServiceCollectionExtensions.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Cortex.Mediator/DependencyInjection/ServiceCollectionExtensions.cs b/src/Cortex.Mediator/DependencyInjection/ServiceCollectionExtensions.cs index 837fe50..fbc50ec 100644 --- a/src/Cortex.Mediator/DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/Cortex.Mediator/DependencyInjection/ServiceCollectionExtensions.cs @@ -15,9 +15,8 @@ public static class ServiceCollectionExtensions { public static IServiceCollection AddCortexMediator( this IServiceCollection services, - IConfiguration configuration, Type[] handlerAssemblyMarkerTypes, - Action? configure = null) + Action configure = null) { var options = new MediatorOptions(); configure?.Invoke(options);