From cda7b93e680988f17a451c39400108c8e04af150 Mon Sep 17 00:00:00 2001 From: MihailK Date: Fri, 21 Nov 2025 18:20:14 +0500 Subject: [PATCH 1/8] =?UTF-8?q?=D0=A3=D0=B4=D0=B0=D0=BB=D0=B8=D0=BB=20?= =?UTF-8?q?=D0=BD=D0=B5=D0=BD=D1=83=D0=B6=D0=BD=D1=8B=D0=B5=20=D1=84=D0=B0?= =?UTF-8?q?=D0=B9=D0=BB=D1=8B=20=D0=B8=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D0=BB=20=D0=BD=D1=83=D0=B6=D0=BD=D1=8B=D0=B5=20=D1=84?= =?UTF-8?q?=D0=B0=D0=B9=D0=BB=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ObjectPrinter/MemberPrintingConfig.cs | 33 +++ .../{ => ObjectPrinter}/ObjectPrinter.cs | 0 .../ObjectPrinter/ObjectPrintingExtensions.cs | 12 + .../ObjectPrinter/PrintingConfig.cs | 146 +++++++++++ ObjectPrinting/ObjectPrinter/Serializer.cs | 232 ++++++++++++++++++ .../StringMemberPrintingConfig.cs | 27 ++ .../ObjectPrinterAcceptanceTests.cs | 55 +++++ .../ObjectPrinterTests/ObjectPrinterTests.cs | 6 + ObjectPrinting/ObjectPrinterTests/Person.cs | 21 ++ ObjectPrinting/PrintingConfig.cs | 41 ---- ObjectPrinting/Solved/ObjectExtensions.cs | 10 - ObjectPrinting/Solved/ObjectPrinter.cs | 10 - ObjectPrinting/Solved/PrintingConfig.cs | 62 ----- .../Solved/PropertyPrintingConfig.cs | 32 --- .../PropertyPrintingConfigExtensions.cs | 18 -- .../Tests/ObjectPrinterAcceptanceTests.cs | 40 --- ObjectPrinting/Solved/Tests/Person.cs | 12 - .../Tests/ObjectPrinterAcceptanceTests.cs | 27 -- ObjectPrinting/Tests/Person.cs | 12 - 19 files changed, 532 insertions(+), 264 deletions(-) create mode 100644 ObjectPrinting/ObjectPrinter/MemberPrintingConfig.cs rename ObjectPrinting/{ => ObjectPrinter}/ObjectPrinter.cs (100%) create mode 100644 ObjectPrinting/ObjectPrinter/ObjectPrintingExtensions.cs create mode 100644 ObjectPrinting/ObjectPrinter/PrintingConfig.cs create mode 100644 ObjectPrinting/ObjectPrinter/Serializer.cs create mode 100644 ObjectPrinting/ObjectPrinter/StringMemberPrintingConfig.cs create mode 100644 ObjectPrinting/ObjectPrinterTests/ObjectPrinterAcceptanceTests.cs create mode 100644 ObjectPrinting/ObjectPrinterTests/ObjectPrinterTests.cs create mode 100644 ObjectPrinting/ObjectPrinterTests/Person.cs delete mode 100644 ObjectPrinting/PrintingConfig.cs delete mode 100644 ObjectPrinting/Solved/ObjectExtensions.cs delete mode 100644 ObjectPrinting/Solved/ObjectPrinter.cs delete mode 100644 ObjectPrinting/Solved/PrintingConfig.cs delete mode 100644 ObjectPrinting/Solved/PropertyPrintingConfig.cs delete mode 100644 ObjectPrinting/Solved/PropertyPrintingConfigExtensions.cs delete mode 100644 ObjectPrinting/Solved/Tests/ObjectPrinterAcceptanceTests.cs delete mode 100644 ObjectPrinting/Solved/Tests/Person.cs delete mode 100644 ObjectPrinting/Tests/ObjectPrinterAcceptanceTests.cs delete mode 100644 ObjectPrinting/Tests/Person.cs diff --git a/ObjectPrinting/ObjectPrinter/MemberPrintingConfig.cs b/ObjectPrinting/ObjectPrinter/MemberPrintingConfig.cs new file mode 100644 index 000000000..e6b031f70 --- /dev/null +++ b/ObjectPrinting/ObjectPrinter/MemberPrintingConfig.cs @@ -0,0 +1,33 @@ +using System; +using System.Globalization; +using System.Reflection; + +namespace ObjectPrinting; + +public class MemberPrintingConfig( + PrintingConfig printingConfig, MemberInfo? memberInfo = null) +{ + readonly PrintingConfig PrintingConfig = printingConfig; + readonly MemberInfo? MemberInfo = memberInfo; + + public PrintingConfig Using(Func printingMethod) + { + if (MemberInfo is null) + PrintingConfig.CustomTypeSerializers[typeof(TMemberType)] = printingMethod; + else + PrintingConfig.CustomMemberSerializers[MemberInfo] = printingMethod; + return PrintingConfig; + } + + public PrintingConfig Using(CultureInfo culture) + { + // Ограничим область применения: только типовой канал (без конкретного члена) + if (MemberInfo is not null) + throw new InvalidOperationException( + "Using(CultureInfo) доступен только для SetPrintingFor(), а не для конкретного члена."); + + // Применяем культуру ко всему типу TMemberType + PrintingConfig.CulturesForTypes[typeof(TMemberType)] = culture; + return PrintingConfig; + } +} \ No newline at end of file diff --git a/ObjectPrinting/ObjectPrinter.cs b/ObjectPrinting/ObjectPrinter/ObjectPrinter.cs similarity index 100% rename from ObjectPrinting/ObjectPrinter.cs rename to ObjectPrinting/ObjectPrinter/ObjectPrinter.cs diff --git a/ObjectPrinting/ObjectPrinter/ObjectPrintingExtensions.cs b/ObjectPrinting/ObjectPrinter/ObjectPrintingExtensions.cs new file mode 100644 index 000000000..5104af96d --- /dev/null +++ b/ObjectPrinting/ObjectPrinter/ObjectPrintingExtensions.cs @@ -0,0 +1,12 @@ +using System; + +namespace ObjectPrinting; + +public static class ObjectPrintingExtensions +{ + public static string PrintToString(this T obj) + => ObjectPrinter.For().PrintToString(obj); + + public static string PrintToString(this T obj, Func, PrintingConfig> config) + => config(ObjectPrinter.For()).PrintToString(obj); +} \ No newline at end of file diff --git a/ObjectPrinting/ObjectPrinter/PrintingConfig.cs b/ObjectPrinting/ObjectPrinter/PrintingConfig.cs new file mode 100644 index 000000000..0064f1767 --- /dev/null +++ b/ObjectPrinting/ObjectPrinter/PrintingConfig.cs @@ -0,0 +1,146 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Text; +using ObjectPrinting.Solved; + +namespace ObjectPrinting +{ + public class PrintingConfig + { + public HashSet ExcludedTypes = []; + public HashSet ExcludedMembers = []; + public readonly Dictionary TrimmedMembers = new(); + public readonly Dictionary CulturesForTypes = new(); + internal readonly Dictionary CustomTypeSerializers = new(); + internal readonly Dictionary CustomMemberSerializers = new(); + internal int? TrimStringLength; + + private int maxNestingLevel = 10; + // public Dictionary typeSerializer = new (); + // public Dictionary cultureSerializer = new (); + // public List ExcludedMember = new(); + + public int MaxNestingLevel + { + get => maxNestingLevel; + private set + { + if (value <= 0) throw new ArgumentOutOfRangeException(nameof(value)); + maxNestingLevel = value; + } + } + + public PrintingConfig Exclude() + { + ExcludedTypes.Add(typeof(TMemberType)); + return this; + } + + public PrintingConfig Exclude(Expression> memberSelector) + { + var member = GetMember(memberSelector); + ExcludedMembers.Add(member); + return this; + } + + public MemberPrintingConfig SetPrintingFor() + => new(this); + + public MemberPrintingConfig SetPrintingFor( + Expression> memberSelector) + => new(this, GetMember(memberSelector)); + + public PrintingConfig SetCulture(CultureInfo culture) + { + CulturesForTypes[typeof(TNumeric)] = culture; + return this; + } + + public PrintingConfig TrimStringsTo(int maxLength) + { + if (maxLength < 0) throw new ArgumentOutOfRangeException(nameof(maxLength)); + TrimStringLength = maxLength; + return this; + } + + public PrintingConfig Trim(Expression> memberSelector, int maxLength) + { + if (maxLength < 0) throw new ArgumentOutOfRangeException(nameof(maxLength)); + var member = GetMember(memberSelector); + TrimmedMembers[member] = maxLength; + return this; + } + + public PrintingConfig SetSerializationDepth(int depth) + { + MaxNestingLevel = depth; + return this; + } + + public string PrintToString(TOwner? obj) + => new Serializer(this).SerializeObject(obj); + + private static MemberInfo GetMember(Expression> selector) + { + if (selector.Body is MemberExpression m) return m.Member; + throw new ArgumentException("Member selector must be a simple member access.", nameof(selector)); + } + + + + private string PrintToString(object obj, int nestingLevel) + { + //TODO apply configurations + if (obj == null) + return "null" + Environment.NewLine; + + var finalTypes = new[] + { + typeof(int), typeof(double), typeof(float), typeof(string), + typeof(DateTime), typeof(TimeSpan) + }; + if (finalTypes.Contains(obj.GetType())) + return obj + Environment.NewLine; + + var identation = new string('\t', nestingLevel + 1); + var sb = new StringBuilder(); + var type = obj.GetType(); + sb.AppendLine(type.Name); + foreach (var propertyInfo in type.GetProperties()) + { + sb.Append(identation + propertyInfo.Name + " = " + + PrintToString(propertyInfo.GetValue(obj), + nestingLevel + 1)); + } + + return sb.ToString(); + } + + // public PrintingConfig ExcludeType() + // { + // excludedTypes.Add(typeof(T)); + // return this; + // } + // + // public PrintingConfig ExcludeProp(Expression> memberSelector) + // { + // var expression = (MemberExpression)memberSelector.Body; + // ExcludedMember.Add(expression.Member); + // return this; + // } + // + // public PropertyPrintingConfig Printing() + // { + // return new PropertyPrintingConfig(this); + // } + // + // public PropertyPrintingConfig Printing(Expression> memberSelector) + // { + // return new PropertyPrintingConfig(this, memberSelector.Name); + // } + } +} \ No newline at end of file diff --git a/ObjectPrinting/ObjectPrinter/Serializer.cs b/ObjectPrinting/ObjectPrinter/Serializer.cs new file mode 100644 index 000000000..31909ba6f --- /dev/null +++ b/ObjectPrinting/ObjectPrinter/Serializer.cs @@ -0,0 +1,232 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; + +namespace ObjectPrinting; + +internal sealed class Serializer +{ + private readonly PrintingConfig config; + private readonly HashSet visited = new(ReferenceEqualityComparer.Instance); + + private static readonly Type[] FinalTypes = + { + typeof(string), typeof(DateTime), typeof(TimeSpan), + typeof(Guid), typeof(decimal), + typeof(byte), typeof(sbyte), typeof(short), typeof(ushort), + typeof(int), typeof(uint), typeof(long), typeof(ulong), + typeof(float), typeof(double), typeof(bool), typeof(char) + }; + + internal Serializer(PrintingConfig config) => this.config = config; + + internal string SerializeObject(object? obj) + { + var sb = new StringBuilder(); + Serialize(obj, sb, 0, parentMember: null); + return sb.ToString(); + } + + private void Serialize(object? obj, StringBuilder sb, int level, MemberInfo? parentMember) + { + if (obj is null) + { + sb.AppendLine("null"); + return; + } + + var type = obj.GetType(); + + // Исключённые типы + if (config.ExcludedTypes.Contains(type)) + { + sb.AppendLine("[Excluded Type]"); + return; + } + + // Ограничение глубины + if (level >= config.MaxNestingLevel) + { + sb.AppendLine($"[{type.Name} ...]"); + return; + } + + // Циклические ссылки + if (!type.IsValueType) + { + if (visited.Contains(obj)) + { + sb.AppendLine($"[CyclicRef {type.Name}]"); + return; + } + + visited.Add(obj); + } + + // 1) Приоритет: кастомный сериализатор ТИПА (включая массивы и любые коллекции) + if (config.CustomTypeSerializers.TryGetValue(type, out var typeSer)) + { + var text = InvokeSerializer(typeSer, obj); + sb.Append(ApplyTrimming(text, parentMember)); + return; + } + + // 2) Финальные типы и форматирование с культурой + if (IsFinal(type)) + { + sb.Append(FormatScalar(obj, type, parentMember)); + return; + } + + // 3) Коллекции: словари + if (obj is IDictionary dict) + { + sb.AppendLine(type.Name); + foreach (DictionaryEntry entry in dict) + { + Indent(sb, level + 1); + sb.Append("Key = "); + Serialize(entry.Key, sb, level + 1, null); + + Indent(sb, level + 1); + sb.Append("Value = "); + Serialize(entry.Value, sb, level + 1, null); + } + + return; + } + + // 4) Коллекции: перечислимые (массивы, списки, и пр.) — если нет кастомного типа-сериализатора + if (obj is IEnumerable enumerable && type != typeof(string)) + { + sb.AppendLine(type.Name); + int i = 0; + foreach (var item in enumerable) + { + Indent(sb, level + 1); + sb.Append($"[{i}] = "); + Serialize(item, sb, level + 1, null); + i++; + } + + return; + } + + // 5) Сложные объекты: публичные свойства и поля + sb.AppendLine(type.Name); + + foreach (var prop in type.GetProperties(BindingFlags.Instance | BindingFlags.Public)) + { + if (!prop.CanRead) continue; + if (config.ExcludedMembers.Contains(prop)) continue; + if (config.ExcludedTypes.Contains(prop.PropertyType)) continue; + + Indent(sb, level + 1); + sb.Append(prop.Name); + sb.Append(" = "); + + if (config.CustomMemberSerializers.TryGetValue(prop, out var memberSer)) + { + var value = SafeGet(() => prop.GetValue(obj)); + var text = value is null ? "null" : InvokeSerializer(memberSer, value!); + sb.AppendLine(ApplyTrimming(text, parentMember: prop).TrimEnd('\r', '\n')); + continue; + } + + var propValue = SafeGet(() => prop.GetValue(obj)); + Serialize(propValue, sb, level + 1, prop); + } + + foreach (var field in type.GetFields(BindingFlags.Instance | BindingFlags.Public)) + { + if (config.ExcludedMembers.Contains(field)) continue; + if (config.ExcludedTypes.Contains(field.FieldType)) continue; + + Indent(sb, level + 1); + sb.Append(field.Name); + sb.Append(" = "); + + if (config.CustomMemberSerializers.TryGetValue(field, out var memberSer)) + { + var value = SafeGet(() => field.GetValue(obj)); + var text = value is null ? "null" : InvokeSerializer(memberSer, value!); + sb.AppendLine(ApplyTrimming(text, parentMember: field).TrimEnd('\r', '\n')); + continue; + } + + var fieldValue = SafeGet(() => field.GetValue(obj)); + Serialize(fieldValue, sb, level + 1, field); + } + } + + + private static void Indent(StringBuilder sb, int level) + => sb.Append(new string('\t', level)); + + private static bool IsFinal(Type t) => FinalTypes.Contains(t) || t.IsEnum; + + private string FormatScalar(object value, Type type, MemberInfo? parentMember) + { + // Member-level serializer has higher priority already handled before call. + if (config.CustomTypeSerializers.TryGetValue(type, out var del)) + { + var s = InvokeSerializer(del, value); + return ApplyTrimming(s, parentMember); + } + + if (type == typeof(string)) + { + var s = (string)value; + return ApplyTrimming(s, parentMember); + } + + // Culture for numeric types and DateTime/decimal/float/double/etc. + if (config.CulturesForTypes.TryGetValue(type, out var culture)) + { + if (value is IFormattable fmt) + return fmt.ToString(null, culture) + Environment.NewLine; + } + + return value + Environment.NewLine; + } + + private string ApplyTrimming(string s, MemberInfo? parentMember) + { + int? length = null; + if (parentMember != null && config.TrimmedMembers.TryGetValue(parentMember, out var memberLen)) + length = memberLen; + else if (config.TrimStringLength.HasValue) + length = config.TrimStringLength; + + if (length.HasValue && s != null && s.Length > length.Value) + s = s.Substring(0, length.Value); + + return s.EndsWith(Environment.NewLine) ? s : s + Environment.NewLine; + } + + private static string InvokeSerializer(Delegate del, object value) + => (string)del.DynamicInvoke(value)!; + + private static object? SafeGet(Func getter) + { + try + { + return getter(); + } + catch + { + return null; + } + } + + // Reference equality comparer for visited set + private sealed class ReferenceEqualityComparer : IEqualityComparer + { + public static readonly ReferenceEqualityComparer Instance = new(); + public new bool Equals(object x, object y) => ReferenceEquals(x, y); + public int GetHashCode(object obj) => System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(obj); + } +} \ No newline at end of file diff --git a/ObjectPrinting/ObjectPrinter/StringMemberPrintingConfig.cs b/ObjectPrinting/ObjectPrinter/StringMemberPrintingConfig.cs new file mode 100644 index 000000000..bc0a3ab35 --- /dev/null +++ b/ObjectPrinting/ObjectPrinter/StringMemberPrintingConfig.cs @@ -0,0 +1,27 @@ +using System; +using System.Reflection; + +namespace ObjectPrinting.Solved; + +public class StringMemberPrintingConfig +{ + private readonly PrintingConfig _parent; + private readonly MemberInfo _member; + + internal StringMemberPrintingConfig(PrintingConfig parent, MemberInfo member) + { + _parent = parent; + _member = member; + } + + // Специфично для string + public PrintingConfig TrimmedToLength(int maxLength) + { + if (typeof(TMember) != typeof(string)) + throw new InvalidOperationException("TrimmedToLength доступен только для строковых свойств."); + + ArgumentOutOfRangeException.ThrowIfNegative(maxLength); + _parent.TrimmedMembers[_member] = maxLength; + return _parent; + } +} \ No newline at end of file diff --git a/ObjectPrinting/ObjectPrinterTests/ObjectPrinterAcceptanceTests.cs b/ObjectPrinting/ObjectPrinterTests/ObjectPrinterAcceptanceTests.cs new file mode 100644 index 000000000..4aad5e4da --- /dev/null +++ b/ObjectPrinting/ObjectPrinterTests/ObjectPrinterAcceptanceTests.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using NUnit.Framework; +using ObjectPrinting.Solved; + +namespace ObjectPrinting.Tests +{ + [TestFixture] + public class ObjectPrinterAcceptanceTests + { + [Test] + public void Demo() + { + var person = new Person + { + Id = Guid.NewGuid(), + Name = "Alex", + Age = 19, + Height = 161.14, + Scores = [100, 95, 88], + Tags = ["student", "intern", "dotnet"], + Ratings = new Dictionary + { + ["math"] = 5, + ["cs"] = 5, + ["english"] = 4 + } + }; + person.Friend = person; + + var printer = ObjectPrinter.For() + .Exclude() + .Exclude(p => p.Age) + .SetPrintingFor().Using(arr => $"int[{arr.Length}] {{ {string.Join(", ", arr)} }}") + .SetPrintingFor>().Using(list => $"List (Count = {list.Count}) [ {string.Join(", ", list)} ]") + .SetPrintingFor>().Using(dict => + { + var pairs = dict.Select(kv => $"{kv.Key}: {kv.Value}"); + return $"Dictionary (Count = {dict.Count}) {{ {string.Join(", ", pairs)} }}"; + }) + .SetPrintingFor().Using(n => $"Number: {n}") + .SetPrintingFor().Using(CultureInfo.InvariantCulture) + .Trim(p => p.Name, 10); + + var s1 = printer.PrintToString(person); + var s2 = person.PrintToString(); + var s3 = person.PrintToString(); + Console.WriteLine(s1); + Console.WriteLine(s2); + Console.WriteLine(s3); + } + } +} \ No newline at end of file diff --git a/ObjectPrinting/ObjectPrinterTests/ObjectPrinterTests.cs b/ObjectPrinting/ObjectPrinterTests/ObjectPrinterTests.cs new file mode 100644 index 000000000..5f7efb6ad --- /dev/null +++ b/ObjectPrinting/ObjectPrinterTests/ObjectPrinterTests.cs @@ -0,0 +1,6 @@ +namespace ObjectPrinting.Tests; + +public class ObjectPrinterTests +{ + +} \ No newline at end of file diff --git a/ObjectPrinting/ObjectPrinterTests/Person.cs b/ObjectPrinting/ObjectPrinterTests/Person.cs new file mode 100644 index 000000000..9de388e73 --- /dev/null +++ b/ObjectPrinting/ObjectPrinterTests/Person.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; + +namespace ObjectPrinting.Tests +{ + public class Person + { + public Guid Id { get; set; } + public string Name { get; set; } + public double Height { get; set; } + public int Age { get; set; } + + public int[] Scores { get; set; } + + public List Tags { get; set; } + + public Dictionary Ratings { get; set; } + + public Person? Friend { get; set; } + } +} \ No newline at end of file diff --git a/ObjectPrinting/PrintingConfig.cs b/ObjectPrinting/PrintingConfig.cs deleted file mode 100644 index a9e082117..000000000 --- a/ObjectPrinting/PrintingConfig.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System; -using System.Linq; -using System.Text; - -namespace ObjectPrinting -{ - public class PrintingConfig - { - public string PrintToString(TOwner obj) - { - return PrintToString(obj, 0); - } - - private string PrintToString(object obj, int nestingLevel) - { - //TODO apply configurations - if (obj == null) - return "null" + Environment.NewLine; - - var finalTypes = new[] - { - typeof(int), typeof(double), typeof(float), typeof(string), - typeof(DateTime), typeof(TimeSpan) - }; - if (finalTypes.Contains(obj.GetType())) - return obj + Environment.NewLine; - - var identation = new string('\t', nestingLevel + 1); - var sb = new StringBuilder(); - var type = obj.GetType(); - sb.AppendLine(type.Name); - foreach (var propertyInfo in type.GetProperties()) - { - sb.Append(identation + propertyInfo.Name + " = " + - PrintToString(propertyInfo.GetValue(obj), - nestingLevel + 1)); - } - return sb.ToString(); - } - } -} \ No newline at end of file diff --git a/ObjectPrinting/Solved/ObjectExtensions.cs b/ObjectPrinting/Solved/ObjectExtensions.cs deleted file mode 100644 index b0c94553c..000000000 --- a/ObjectPrinting/Solved/ObjectExtensions.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace ObjectPrinting.Solved -{ - public static class ObjectExtensions - { - public static string PrintToString(this T obj) - { - return ObjectPrinter.For().PrintToString(obj); - } - } -} \ No newline at end of file diff --git a/ObjectPrinting/Solved/ObjectPrinter.cs b/ObjectPrinting/Solved/ObjectPrinter.cs deleted file mode 100644 index 540ee769c..000000000 --- a/ObjectPrinting/Solved/ObjectPrinter.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace ObjectPrinting.Solved -{ - public class ObjectPrinter - { - public static PrintingConfig For() - { - return new PrintingConfig(); - } - } -} \ No newline at end of file diff --git a/ObjectPrinting/Solved/PrintingConfig.cs b/ObjectPrinting/Solved/PrintingConfig.cs deleted file mode 100644 index 0ec5aeb2b..000000000 --- a/ObjectPrinting/Solved/PrintingConfig.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System; -using System.Linq; -using System.Linq.Expressions; -using System.Text; - -namespace ObjectPrinting.Solved -{ - public class PrintingConfig - { - public PropertyPrintingConfig Printing() - { - return new PropertyPrintingConfig(this); - } - - public PropertyPrintingConfig Printing(Expression> memberSelector) - { - return new PropertyPrintingConfig(this); - } - - public PrintingConfig Excluding(Expression> memberSelector) - { - return this; - } - - internal PrintingConfig Excluding() - { - return this; - } - - public string PrintToString(TOwner obj) - { - return PrintToString(obj, 0); - } - - private string PrintToString(object obj, int nestingLevel) - { - //TODO apply configurations - if (obj == null) - return "null" + Environment.NewLine; - - var finalTypes = new[] - { - typeof(int), typeof(double), typeof(float), typeof(string), - typeof(DateTime), typeof(TimeSpan) - }; - if (finalTypes.Contains(obj.GetType())) - return obj + Environment.NewLine; - - var identation = new string('\t', nestingLevel + 1); - var sb = new StringBuilder(); - var type = obj.GetType(); - sb.AppendLine(type.Name); - foreach (var propertyInfo in type.GetProperties()) - { - sb.Append(identation + propertyInfo.Name + " = " + - PrintToString(propertyInfo.GetValue(obj), - nestingLevel + 1)); - } - return sb.ToString(); - } - } -} \ No newline at end of file diff --git a/ObjectPrinting/Solved/PropertyPrintingConfig.cs b/ObjectPrinting/Solved/PropertyPrintingConfig.cs deleted file mode 100644 index a509697d1..000000000 --- a/ObjectPrinting/Solved/PropertyPrintingConfig.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; -using System.Globalization; - -namespace ObjectPrinting.Solved -{ - public class PropertyPrintingConfig : IPropertyPrintingConfig - { - private readonly PrintingConfig printingConfig; - - public PropertyPrintingConfig(PrintingConfig printingConfig) - { - this.printingConfig = printingConfig; - } - - public PrintingConfig Using(Func print) - { - return printingConfig; - } - - public PrintingConfig Using(CultureInfo culture) - { - return printingConfig; - } - - PrintingConfig IPropertyPrintingConfig.ParentConfig => printingConfig; - } - - public interface IPropertyPrintingConfig - { - PrintingConfig ParentConfig { get; } - } -} \ No newline at end of file diff --git a/ObjectPrinting/Solved/PropertyPrintingConfigExtensions.cs b/ObjectPrinting/Solved/PropertyPrintingConfigExtensions.cs deleted file mode 100644 index dd3922394..000000000 --- a/ObjectPrinting/Solved/PropertyPrintingConfigExtensions.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; - -namespace ObjectPrinting.Solved -{ - public static class PropertyPrintingConfigExtensions - { - public static string PrintToString(this T obj, Func, PrintingConfig> config) - { - return config(ObjectPrinter.For()).PrintToString(obj); - } - - public static PrintingConfig TrimmedToLength(this PropertyPrintingConfig propConfig, int maxLen) - { - return ((IPropertyPrintingConfig)propConfig).ParentConfig; - } - - } -} \ No newline at end of file diff --git a/ObjectPrinting/Solved/Tests/ObjectPrinterAcceptanceTests.cs b/ObjectPrinting/Solved/Tests/ObjectPrinterAcceptanceTests.cs deleted file mode 100644 index ac52d5ee5..000000000 --- a/ObjectPrinting/Solved/Tests/ObjectPrinterAcceptanceTests.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System; -using System.Globalization; -using NUnit.Framework; - -namespace ObjectPrinting.Solved.Tests -{ - [TestFixture] - public class ObjectPrinterAcceptanceTests - { - [Test] - public void Demo() - { - var person = new Person { Name = "Alex", Age = 19 }; - - var printer = ObjectPrinter.For() - //1. Исключить из сериализации свойства определенного типа - .Excluding() - //2. Указать альтернативный способ сериализации для определенного типа - .Printing().Using(i => i.ToString("X")) - //3. Для числовых типов указать культуру - .Printing().Using(CultureInfo.InvariantCulture) - //4. Настроить сериализацию конкретного свойства - //5. Настроить обрезание строковых свойств (метод должен быть виден только для строковых свойств) - .Printing(p => p.Name).TrimmedToLength(10) - //6. Исключить из сериализации конкретного свойства - .Excluding(p => p.Age); - - string s1 = printer.PrintToString(person); - - //7. Синтаксический сахар в виде метода расширения, сериализующего по-умолчанию - string s2 = person.PrintToString(); - - //8. ...с конфигурированием - string s3 = person.PrintToString(s => s.Excluding(p => p.Age)); - Console.WriteLine(s1); - Console.WriteLine(s2); - Console.WriteLine(s3); - } - } -} \ No newline at end of file diff --git a/ObjectPrinting/Solved/Tests/Person.cs b/ObjectPrinting/Solved/Tests/Person.cs deleted file mode 100644 index 858ebbf8d..000000000 --- a/ObjectPrinting/Solved/Tests/Person.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; - -namespace ObjectPrinting.Solved.Tests -{ - public class Person - { - public Guid Id { get; set; } - public string Name { get; set; } - public double Height { get; set; } - public int Age { get; set; } - } -} \ No newline at end of file diff --git a/ObjectPrinting/Tests/ObjectPrinterAcceptanceTests.cs b/ObjectPrinting/Tests/ObjectPrinterAcceptanceTests.cs deleted file mode 100644 index 4c8b2445c..000000000 --- a/ObjectPrinting/Tests/ObjectPrinterAcceptanceTests.cs +++ /dev/null @@ -1,27 +0,0 @@ -using NUnit.Framework; - -namespace ObjectPrinting.Tests -{ - [TestFixture] - public class ObjectPrinterAcceptanceTests - { - [Test] - public void Demo() - { - var person = new Person { Name = "Alex", Age = 19 }; - - var printer = ObjectPrinter.For(); - //1. Исключить из сериализации свойства определенного типа - //2. Указать альтернативный способ сериализации для определенного типа - //3. Для числовых типов указать культуру - //4. Настроить сериализацию конкретного свойства - //5. Настроить обрезание строковых свойств (метод должен быть виден только для строковых свойств) - //6. Исключить из сериализации конкретного свойства - - string s1 = printer.PrintToString(person); - - //7. Синтаксический сахар в виде метода расширения, сериализующего по-умолчанию - //8. ...с конфигурированием - } - } -} \ No newline at end of file diff --git a/ObjectPrinting/Tests/Person.cs b/ObjectPrinting/Tests/Person.cs deleted file mode 100644 index f95559554..000000000 --- a/ObjectPrinting/Tests/Person.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; - -namespace ObjectPrinting.Tests -{ - public class Person - { - public Guid Id { get; set; } - public string Name { get; set; } - public double Height { get; set; } - public int Age { get; set; } - } -} \ No newline at end of file From bdaa68c9b397d28a70394e5df2f174697e59f5d1 Mon Sep 17 00:00:00 2001 From: MihailK Date: Fri, 21 Nov 2025 18:25:38 +0500 Subject: [PATCH 2/8] =?UTF-8?q?=D0=92=D1=8B=D0=B1=D1=80=D0=B0=D0=BB=20?= =?UTF-8?q?=D1=81=D0=B5=D0=BC=D0=B0=D0=BD=D1=82=D0=B8=D0=BA=D1=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ObjectPrinterAcceptanceTests.cs | 32 ++++++++++--------- ObjectPrinting/ObjectPrinting.csproj | 1 + fluent-api.sln.DotSettings | 3 ++ 3 files changed, 21 insertions(+), 15 deletions(-) diff --git a/ObjectPrinting/ObjectPrinterTests/ObjectPrinterAcceptanceTests.cs b/ObjectPrinting/ObjectPrinterTests/ObjectPrinterAcceptanceTests.cs index 4aad5e4da..8e74c0ca2 100644 --- a/ObjectPrinting/ObjectPrinterTests/ObjectPrinterAcceptanceTests.cs +++ b/ObjectPrinting/ObjectPrinterTests/ObjectPrinterAcceptanceTests.cs @@ -3,7 +3,7 @@ using System.Globalization; using System.Linq; using NUnit.Framework; -using ObjectPrinting.Solved; + namespace ObjectPrinting.Tests { @@ -29,24 +29,26 @@ public void Demo() } }; person.Friend = person; - + var printer = ObjectPrinter.For() - .Exclude() - .Exclude(p => p.Age) - .SetPrintingFor().Using(arr => $"int[{arr.Length}] {{ {string.Join(", ", arr)} }}") - .SetPrintingFor>().Using(list => $"List (Count = {list.Count}) [ {string.Join(", ", list)} ]") - .SetPrintingFor>().Using(dict => - { - var pairs = dict.Select(kv => $"{kv.Key}: {kv.Value}"); - return $"Dictionary (Count = {dict.Count}) {{ {string.Join(", ", pairs)} }}"; - }) - .SetPrintingFor().Using(n => $"Number: {n}") - .SetPrintingFor().Using(CultureInfo.InvariantCulture) - .Trim(p => p.Name, 10); - + .Excluding() + .Excluding(p => p.Age) + .Printing().Using(arr => $"int[{arr.Length}] {{ {string.Join(", ", arr)} }}") + .Printing>() + .Using(list => $"List (Count = {list.Count}) [ {string.Join(", ", list)} ]") + .Printing>().Using(dict => + { + var pairs = dict.Select(kv => $"{kv.Key}: {kv.Value}"); + return $"Dictionary (Count = {dict.Count}) {{ {string.Join(", ", pairs)} }}"; + }) + .Printing().Using(n => $"Number: {n}") + .Printing().Using(CultureInfo.InvariantCulture) + .Printing(p => p.Name).TrimmedToLength(3); + var s1 = printer.PrintToString(person); var s2 = person.PrintToString(); var s3 = person.PrintToString(); + Console.WriteLine(s1); Console.WriteLine(s2); Console.WriteLine(s3); diff --git a/ObjectPrinting/ObjectPrinting.csproj b/ObjectPrinting/ObjectPrinting.csproj index c5db392ff..ea98111e3 100644 --- a/ObjectPrinting/ObjectPrinting.csproj +++ b/ObjectPrinting/ObjectPrinting.csproj @@ -5,6 +5,7 @@ + diff --git a/fluent-api.sln.DotSettings b/fluent-api.sln.DotSettings index 135b83ecb..53fe49b2f 100644 --- a/fluent-api.sln.DotSettings +++ b/fluent-api.sln.DotSettings @@ -1,6 +1,9 @@  <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb_AaBb" /> + <Policy><Descriptor Staticness="Instance" AccessRightKinds="Private" Description="Instance fields (private)"><ElementKinds><Kind Name="FIELD" /><Kind Name="READONLY_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="" Suffix="" Style="aaBb" /></Policy> + <Policy><Descriptor Staticness="Any" AccessRightKinds="Any" Description="Types and namespaces"><ElementKinds><Kind Name="NAMESPACE" /><Kind Name="CLASS" /><Kind Name="STRUCT" /><Kind Name="ENUM" /><Kind Name="DELEGATE" /></ElementKinds></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="" Suffix="" Style="AaBb_AaBb" /></Policy> + True True True Imported 10.10.2016 From 33fac7ad71e901711a12c00b7c3a224ca2bb3e26 Mon Sep 17 00:00:00 2001 From: MihailK Date: Fri, 21 Nov 2025 18:28:15 +0500 Subject: [PATCH 3/8] =?UTF-8?q?=D0=A0=D0=B0=D0=B7=D1=80=D0=B0=D0=B1=D0=BE?= =?UTF-8?q?=D1=82=D0=B0=D0=BB=20PrintingConfig?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ObjectPrinter/MemberPrintingConfig.cs | 13 +- .../ObjectPrinter/PrintingConfig.cs | 128 +++--------------- .../StringMemberPrintingConfig.cs | 28 +--- 3 files changed, 29 insertions(+), 140 deletions(-) diff --git a/ObjectPrinting/ObjectPrinter/MemberPrintingConfig.cs b/ObjectPrinting/ObjectPrinter/MemberPrintingConfig.cs index e6b031f70..71cc94e09 100644 --- a/ObjectPrinting/ObjectPrinter/MemberPrintingConfig.cs +++ b/ObjectPrinting/ObjectPrinter/MemberPrintingConfig.cs @@ -5,10 +5,11 @@ namespace ObjectPrinting; public class MemberPrintingConfig( - PrintingConfig printingConfig, MemberInfo? memberInfo = null) + PrintingConfig printingConfig, + MemberInfo? memberInfo = null) { - readonly PrintingConfig PrintingConfig = printingConfig; - readonly MemberInfo? MemberInfo = memberInfo; + protected readonly PrintingConfig PrintingConfig = printingConfig; + protected readonly MemberInfo MemberInfo = memberInfo; public PrintingConfig Using(Func printingMethod) { @@ -18,15 +19,13 @@ public PrintingConfig Using(Func printingMethod) PrintingConfig.CustomMemberSerializers[MemberInfo] = printingMethod; return PrintingConfig; } - + public PrintingConfig Using(CultureInfo culture) { - // Ограничим область применения: только типовой канал (без конкретного члена) if (MemberInfo is not null) throw new InvalidOperationException( "Using(CultureInfo) доступен только для SetPrintingFor(), а не для конкретного члена."); - - // Применяем культуру ко всему типу TMemberType + PrintingConfig.CulturesForTypes[typeof(TMemberType)] = culture; return PrintingConfig; } diff --git a/ObjectPrinting/ObjectPrinter/PrintingConfig.cs b/ObjectPrinting/ObjectPrinter/PrintingConfig.cs index 0064f1767..46a3450c6 100644 --- a/ObjectPrinting/ObjectPrinter/PrintingConfig.cs +++ b/ObjectPrinting/ObjectPrinter/PrintingConfig.cs @@ -1,146 +1,50 @@ using System; using System.Collections.Generic; using System.Globalization; -using System.Linq; using System.Linq.Expressions; using System.Reflection; -using System.Text; -using ObjectPrinting.Solved; + namespace ObjectPrinting { public class PrintingConfig { - public HashSet ExcludedTypes = []; - public HashSet ExcludedMembers = []; + public readonly HashSet ExcludedTypes = []; + public readonly HashSet ExcludedMember = []; public readonly Dictionary TrimmedMembers = new(); public readonly Dictionary CulturesForTypes = new(); internal readonly Dictionary CustomTypeSerializers = new(); internal readonly Dictionary CustomMemberSerializers = new(); internal int? TrimStringLength; + public int MaxNestingLevel { get; set; } = 5; - private int maxNestingLevel = 10; - // public Dictionary typeSerializer = new (); - // public Dictionary cultureSerializer = new (); - // public List ExcludedMember = new(); - - public int MaxNestingLevel - { - get => maxNestingLevel; - private set - { - if (value <= 0) throw new ArgumentOutOfRangeException(nameof(value)); - maxNestingLevel = value; - } - } - - public PrintingConfig Exclude() + public PrintingConfig Excluding() { - ExcludedTypes.Add(typeof(TMemberType)); + ExcludedTypes.Add(typeof(TMember)); return this; } - - public PrintingConfig Exclude(Expression> memberSelector) + + public PrintingConfig Excluding(Expression> memberSelector) { - var member = GetMember(memberSelector); - ExcludedMembers.Add(member); + ExcludedMember.Add(GetMember(memberSelector)); return this; } - - public MemberPrintingConfig SetPrintingFor() + + public MemberPrintingConfig Printing() => new(this); - public MemberPrintingConfig SetPrintingFor( - Expression> memberSelector) - => new(this, GetMember(memberSelector)); - - public PrintingConfig SetCulture(CultureInfo culture) - { - CulturesForTypes[typeof(TNumeric)] = culture; - return this; - } - - public PrintingConfig TrimStringsTo(int maxLength) - { - if (maxLength < 0) throw new ArgumentOutOfRangeException(nameof(maxLength)); - TrimStringLength = maxLength; - return this; - } - - public PrintingConfig Trim(Expression> memberSelector, int maxLength) - { - if (maxLength < 0) throw new ArgumentOutOfRangeException(nameof(maxLength)); - var member = GetMember(memberSelector); - TrimmedMembers[member] = maxLength; - return this; - } - - public PrintingConfig SetSerializationDepth(int depth) - { - MaxNestingLevel = depth; - return this; - } - + public StintingPrintConfig Printing(Expression> stringMemberSelector) + => new(this, GetMember(stringMemberSelector)); + + public string PrintToString(TOwner? obj) => new Serializer(this).SerializeObject(obj); + private static MemberInfo GetMember(Expression> selector) { if (selector.Body is MemberExpression m) return m.Member; throw new ArgumentException("Member selector must be a simple member access.", nameof(selector)); } - - - - private string PrintToString(object obj, int nestingLevel) - { - //TODO apply configurations - if (obj == null) - return "null" + Environment.NewLine; - - var finalTypes = new[] - { - typeof(int), typeof(double), typeof(float), typeof(string), - typeof(DateTime), typeof(TimeSpan) - }; - if (finalTypes.Contains(obj.GetType())) - return obj + Environment.NewLine; - - var identation = new string('\t', nestingLevel + 1); - var sb = new StringBuilder(); - var type = obj.GetType(); - sb.AppendLine(type.Name); - foreach (var propertyInfo in type.GetProperties()) - { - sb.Append(identation + propertyInfo.Name + " = " + - PrintToString(propertyInfo.GetValue(obj), - nestingLevel + 1)); - } - - return sb.ToString(); - } - - // public PrintingConfig ExcludeType() - // { - // excludedTypes.Add(typeof(T)); - // return this; - // } - // - // public PrintingConfig ExcludeProp(Expression> memberSelector) - // { - // var expression = (MemberExpression)memberSelector.Body; - // ExcludedMember.Add(expression.Member); - // return this; - // } - // - // public PropertyPrintingConfig Printing() - // { - // return new PropertyPrintingConfig(this); - // } - // - // public PropertyPrintingConfig Printing(Expression> memberSelector) - // { - // return new PropertyPrintingConfig(this, memberSelector.Name); - // } } } \ No newline at end of file diff --git a/ObjectPrinting/ObjectPrinter/StringMemberPrintingConfig.cs b/ObjectPrinting/ObjectPrinter/StringMemberPrintingConfig.cs index bc0a3ab35..d17c59947 100644 --- a/ObjectPrinting/ObjectPrinter/StringMemberPrintingConfig.cs +++ b/ObjectPrinting/ObjectPrinter/StringMemberPrintingConfig.cs @@ -1,27 +1,13 @@ -using System; -using System.Reflection; +using System.Reflection; -namespace ObjectPrinting.Solved; +namespace ObjectPrinting; -public class StringMemberPrintingConfig +public class StintingPrintConfig(PrintingConfig printingConfig, MemberInfo memberInfo) + : MemberPrintingConfig(printingConfig, memberInfo) { - private readonly PrintingConfig _parent; - private readonly MemberInfo _member; - - internal StringMemberPrintingConfig(PrintingConfig parent, MemberInfo member) + public PrintingConfig TrimmedToLength(int length) { - _parent = parent; - _member = member; - } - - // Специфично для string - public PrintingConfig TrimmedToLength(int maxLength) - { - if (typeof(TMember) != typeof(string)) - throw new InvalidOperationException("TrimmedToLength доступен только для строковых свойств."); - - ArgumentOutOfRangeException.ThrowIfNegative(maxLength); - _parent.TrimmedMembers[_member] = maxLength; - return _parent; + printingConfig.TrimmedMembers[memberInfo] = length; + return PrintingConfig; } } \ No newline at end of file From 078336d6939eae9d312330b91f82869c8ae30332 Mon Sep 17 00:00:00 2001 From: MihailK Date: Fri, 21 Nov 2025 18:29:11 +0500 Subject: [PATCH 4/8] =?UTF-8?q?=D0=A1=D0=B4=D0=B5=D0=BB=D0=B0=D0=BB=20Seri?= =?UTF-8?q?alizer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ObjectPrinting/ObjectPrinter/Serializer.cs | 76 +++++++++++----------- 1 file changed, 39 insertions(+), 37 deletions(-) diff --git a/ObjectPrinting/ObjectPrinter/Serializer.cs b/ObjectPrinting/ObjectPrinter/Serializer.cs index 31909ba6f..b6b9bf94b 100644 --- a/ObjectPrinting/ObjectPrinter/Serializer.cs +++ b/ObjectPrinting/ObjectPrinter/Serializer.cs @@ -12,14 +12,14 @@ internal sealed class Serializer private readonly PrintingConfig config; private readonly HashSet visited = new(ReferenceEqualityComparer.Instance); - private static readonly Type[] FinalTypes = - { + private static readonly Type[] TerminalTypes = + [ typeof(string), typeof(DateTime), typeof(TimeSpan), typeof(Guid), typeof(decimal), typeof(byte), typeof(sbyte), typeof(short), typeof(ushort), typeof(int), typeof(uint), typeof(long), typeof(ulong), typeof(float), typeof(double), typeof(bool), typeof(char) - }; + ]; internal Serializer(PrintingConfig config) => this.config = config; @@ -39,34 +39,31 @@ private void Serialize(object? obj, StringBuilder sb, int level, MemberInfo? par } var type = obj.GetType(); - - // Исключённые типы + if (config.ExcludedTypes.Contains(type)) { sb.AppendLine("[Excluded Type]"); return; } - - // Ограничение глубины + if (level >= config.MaxNestingLevel) { - sb.AppendLine($"[{type.Name} ...]"); + sb.AppendLine($"[{FormatTypeName(type)} превышен уровень вложенности]"); return; } - - // Циклические ссылки + if (!type.IsValueType) { if (visited.Contains(obj)) { - sb.AppendLine($"[CyclicRef {type.Name}]"); + sb.AppendLine($"[CyclicRef {FormatTypeName(type)}]"); return; } visited.Add(obj); } - // 1) Приоритет: кастомный сериализатор ТИПА (включая массивы и любые коллекции) + if (config.CustomTypeSerializers.TryGetValue(type, out var typeSer)) { var text = InvokeSerializer(typeSer, obj); @@ -74,17 +71,16 @@ private void Serialize(object? obj, StringBuilder sb, int level, MemberInfo? par return; } - // 2) Финальные типы и форматирование с культурой - if (IsFinal(type)) + + if (TerminalTypes.Contains(type) || type.IsEnum) { sb.Append(FormatScalar(obj, type, parentMember)); return; } - - // 3) Коллекции: словари + if (obj is IDictionary dict) { - sb.AppendLine(type.Name); + sb.AppendLine(FormatTypeName(type)); foreach (DictionaryEntry entry in dict) { Indent(sb, level + 1); @@ -98,12 +94,11 @@ private void Serialize(object? obj, StringBuilder sb, int level, MemberInfo? par return; } - - // 4) Коллекции: перечислимые (массивы, списки, и пр.) — если нет кастомного типа-сериализатора + if (obj is IEnumerable enumerable && type != typeof(string)) { - sb.AppendLine(type.Name); - int i = 0; + sb.AppendLine(FormatTypeName(type)); + var i = 0; foreach (var item in enumerable) { Indent(sb, level + 1); @@ -114,14 +109,13 @@ private void Serialize(object? obj, StringBuilder sb, int level, MemberInfo? par return; } - - // 5) Сложные объекты: публичные свойства и поля - sb.AppendLine(type.Name); + + sb.AppendLine(FormatTypeName(type)); foreach (var prop in type.GetProperties(BindingFlags.Instance | BindingFlags.Public)) { if (!prop.CanRead) continue; - if (config.ExcludedMembers.Contains(prop)) continue; + if (config.ExcludedMember.Contains(prop)) continue; if (config.ExcludedTypes.Contains(prop.PropertyType)) continue; Indent(sb, level + 1); @@ -142,7 +136,7 @@ private void Serialize(object? obj, StringBuilder sb, int level, MemberInfo? par foreach (var field in type.GetFields(BindingFlags.Instance | BindingFlags.Public)) { - if (config.ExcludedMembers.Contains(field)) continue; + if (config.ExcludedMember.Contains(field)) continue; if (config.ExcludedTypes.Contains(field.FieldType)) continue; Indent(sb, level + 1); @@ -165,12 +159,10 @@ private void Serialize(object? obj, StringBuilder sb, int level, MemberInfo? par private static void Indent(StringBuilder sb, int level) => sb.Append(new string('\t', level)); - - private static bool IsFinal(Type t) => FinalTypes.Contains(t) || t.IsEnum; + private string FormatScalar(object value, Type type, MemberInfo? parentMember) { - // Member-level serializer has higher priority already handled before call. if (config.CustomTypeSerializers.TryGetValue(type, out var del)) { var s = InvokeSerializer(del, value); @@ -182,13 +174,11 @@ private string FormatScalar(object value, Type type, MemberInfo? parentMember) var s = (string)value; return ApplyTrimming(s, parentMember); } - - // Culture for numeric types and DateTime/decimal/float/double/etc. - if (config.CulturesForTypes.TryGetValue(type, out var culture)) - { - if (value is IFormattable fmt) - return fmt.ToString(null, culture) + Environment.NewLine; - } + + if (!config.CulturesForTypes.TryGetValue(type, out var culture)) + return value + Environment.NewLine; + if (value is IFormattable fmt) + return fmt.ToString(null, culture) + Environment.NewLine; return value + Environment.NewLine; } @@ -221,8 +211,20 @@ private static string InvokeSerializer(Delegate del, object value) return null; } } + + private static string FormatTypeName(Type t) + { + if (!t.IsGenericType) return t.Name; - // Reference equality comparer for visited set + var defName = t.Name; + var backtick = defName.IndexOf('`'); + if (backtick > 0) defName = defName.Substring(0, backtick); + + var args = t.GetGenericArguments(); + var argNames = string.Join(", ", args.Select(FormatTypeName)); + return $"{defName}<{argNames}>"; + } + private sealed class ReferenceEqualityComparer : IEqualityComparer { public static readonly ReferenceEqualityComparer Instance = new(); From f64f7af8b92d42107b93b07ccc7b6def416dc8ad Mon Sep 17 00:00:00 2001 From: MihailK Date: Fri, 21 Nov 2025 18:29:52 +0500 Subject: [PATCH 5/8] =?UTF-8?q?=D0=9D=D0=B0=D0=BF=D0=B8=D1=81=D0=B0=D0=BB?= =?UTF-8?q?=20=D1=82=D0=B5=D1=81=D1=82=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ObjectPrinterTests/ObjectPrinterTests.cs | 263 +++++++++++++++++- ObjectPrinting/ObjectPrinterTests/Person.cs | 8 +- 2 files changed, 264 insertions(+), 7 deletions(-) diff --git a/ObjectPrinting/ObjectPrinterTests/ObjectPrinterTests.cs b/ObjectPrinting/ObjectPrinterTests/ObjectPrinterTests.cs index 5f7efb6ad..8c4a1cbfc 100644 --- a/ObjectPrinting/ObjectPrinterTests/ObjectPrinterTests.cs +++ b/ObjectPrinting/ObjectPrinterTests/ObjectPrinterTests.cs @@ -1,6 +1,263 @@ -namespace ObjectPrinting.Tests; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using NUnit.Framework; +using FluentAssertions; -public class ObjectPrinterTests +namespace ObjectPrinting.Tests; + +[TestFixture] +public class ObjectPrinterStandardSerializationTests { - + private Person? setUpPerson; + + [OneTimeSetUp] + public void Setup() + { + setUpPerson = new Person + { + Id = Guid.Parse("f14dd761-3260-4463-a4ad-6ba14de2026c"), + Name = "Fai", + Height = 160.5, + Age = 20, + Scores = [95, 88, 76], + Tags = ["brooch", "Nevada"], + Ratings = new Dictionary + { + ["quality"] = 5, + ["speed"] = 4 + }, + Friend = new Person + { + Id = Guid.Parse("cd55ace6-ac55-4f85-9434-82672b2fd9ee"), + Name = "Sigma", + Height = 180.3, + Age = 81, + Scores = [93, 55, 99, 32], + Tags = ["robots", "moon", "zero"], + Ratings = new Dictionary(), + Friend = null + } + }; + } + + [Test] + public void PrintToString_ShouldIncludeBasicProperties_WhenPersonIsValid() + { + var actual = setUpPerson.PrintToString(); + + actual.Should().NotBeNullOrEmpty(); + actual.Should().Contain("Id = f14dd761-3260-4463-a4ad-6ba14de2026c"); + actual.Should().Contain("Person"); + actual.Should().Contain("Name = Fai"); + actual.Should().Contain("Age = 20"); + actual.Should().Contain("Height = 160,5"); + actual.Should().Contain("Tags = List"); + actual.Should().Contain("Ratings = Dictionary"); + actual.Should().Contain("Friend = Person"); + actual.Should().Contain("Friend = null"); + } + + + [Test] + public void PrintToString_ShouldApplyMemberExclusion_WhenNameExcludedInConfig() + { + var actual = setUpPerson.PrintToString(cfg => cfg + .Excluding(p => p.Name)); + + actual.Should().NotContain("Name = Fai"); + actual.Should().Contain("Id = f14dd761-3260-4463-a4ad-6ba14de2026c"); + } + + [Test] + public void PrintToString_ShouldApplyTypeExclusion_WhenIntTypeExcludedInConfig() + { + var actual = setUpPerson.PrintToString(cfg => cfg + .Excluding()); + + actual.Should().NotContain("Age = 20"); + actual.Should().Contain("Id = f14dd761-3260-4463-a4ad-6ba14de2026c"); + } + + [Test] + public void PrintToString_ShouldUseCustomFormatter_WhenIntTypeFormatterIsConfigured() + { + var actual = setUpPerson.PrintToString(cfg => cfg + .Printing().Using(n => $"Age: {n}")); + + actual.Should().Contain("Age: 20"); + } + + [Test] + public void PrintToString_ShouldUseInvariantCulture_WhenDoubleCultureIsSetToInvariant() + { + var actual = setUpPerson.PrintToString(cfg => cfg + .Printing().Using(CultureInfo.InvariantCulture)); + + actual.Should().Contain("Height = 160.5"); + } + + [Test] + public void PrintToString_ShouldApplyMemberSerializer_WhenNameCustomSerializerIsSet() + { + var actual = setUpPerson.PrintToString(cfg => cfg + .Printing(p => p.Name).Using(p => p.ToUpper())); + + actual.Should().Contain("Name = FAI"); + } + + [Test] + public void PrintToString_ShouldTrimStringProperty_WhenNameTrimmedToLengthSpecified() // pomenyat + { + var actual = setUpPerson.PrintToString(cfg => cfg + .Printing(p => p.Name).TrimmedToLength(2)); + + actual.Should().Contain("Name = Fa"); + } + + [Test] + public void PrintToString_ShouldApplyMultipleCustomizations_WhenCombinedExclusionsAndSerializersUsed() + { + var actual = setUpPerson.PrintToString(cfg => cfg + .Excluding() + .Excluding(p => p.Tags) + .Printing().Using(n => $"Number: {n}") + .Printing().Using(CultureInfo.InvariantCulture) + .Printing(p => p.Name).TrimmedToLength(2) + .Printing(p => p.Name).Using(p => p.ToUpper())); + + actual.Should().NotContain("Guid"); + actual.Should().NotContain("Tags"); + actual.Should().Contain("Number: 20"); + actual.Should().NotContain("Age = 20"); + actual.Should().Contain("Height = 160.5"); + actual.Should().NotContain("Heigh = 160,5"); + actual.Should().Contain("Name = FA"); + } + + [Test] + public void PrintToString_ShouldRenderIntArrayWithIndexedItems_WhenPersonHasArray() + { + var actual = setUpPerson.PrintToString(); + + actual.Should().Contain("Scores = Int32[]"); + actual.Should().Contain("[0] = 95"); + actual.Should().Contain("[1] = 88"); + actual.Should().Contain("[2] = 76"); + } + + [Test] + public void PrintToString_ShouldRenderGenericListWithIndexedItems_WhenPersonHasList() + { + var actual = setUpPerson.PrintToString(); + + actual.Should().Contain("Tags = List"); + actual.Should().Contain("[0] = brooch"); + actual.Should().Contain("[1] = Nevada"); + } + + [Test] + public void PrintToString_ShouldRenderDictionaryWithKeyValuePairs_WhenPersonHasDictionary() + { + var actual = setUpPerson.PrintToString(); + + actual.Should().Contain("Ratings = Dictionary"); + actual.Should().Contain("Key = quality"); + actual.Should().Contain("Value = 5"); + actual.Should().Contain("Key = speed"); + actual.Should().Contain("Value = 4"); + } + + [Test] + public void PrintToString_ShouldRenderMultidimensionalArrayRank_WhenArrayIsTwoDimensional() + { + var matrix = new[,] + { + { 1, 1, 1 }, + { 1, 1, 1 } + }; + + var actual = matrix.PrintToString(); + + actual.Should().Contain("Int32[,]"); + } + + [Test] + public void PrintToString_ShouldRenderJaggedArraySuffixes_WhenArrayIsArrayOfArrays() + { + var jagged = new int[][] + { + [1, 2, 3], + [4, 5, 6] + }; + + var actual = jagged.PrintToString(); + + actual.Should().Contain("Int32[][]"); + } + + [Test] + public void PrintToString_ShouldUseCustomSerializer_WhenIntArraySerializerIsConfigured() + { + var actual = setUpPerson.PrintToString(cfg => cfg + .Printing() + .Using(arr => $"int[{arr.Length}] {{ {string.Join(", ", arr)} }}")); + + actual.Should().Contain("Scores = int[3] { 95, 88, 76 }"); + } + + [Test] + public void PrintToString_ShouldUseCustomSerializer_WhenStringListSerializerIsConfigured() + { + var actual = setUpPerson.PrintToString(cfg => cfg + .Printing>() + .Using(list => $"List (Count = {list.Count}) [ {string.Join(", ", list)} ]")); + + actual.Should().Contain("Tags = List (Count = 2) [ brooch, Nevada ]"); + } + + [Test] + public void PrintToString_ShouldUseCustomSerializer_WhenDictionarySerializerIsConfigured() + { + var actual = setUpPerson.PrintToString(cfg => cfg + .Printing>() + .Using(dict => + { + var pairs = dict.Select(kv => $"{kv.Key}: {kv.Value}"); + return $"Dictionary (Count = {dict.Count}) {{ {string.Join(", ", pairs)} }}"; + })); + + actual.Should().Contain("Ratings = Dictionary (Count = 2) { quality: 5, speed: 4 }"); + } + + [Test] + public void PrintToString_ShouldEmitCycleMarker_WhenObjectGraphHasCyclicReference() + { + var person = new Person(); + person.Friend = person; + + var act = () => person.PrintToString(); + var actual = person.PrintToString(); + + act.Should().NotThrow(); + actual.Should().Contain("CyclicRef"); + } + + [Test] + public void PrintToString_ShouldReportNestingLevelExceeded_WhenFriendChainDepthBeyondMax() + { + var person = new Person(); + person.Friend = new Person(); + person.Friend.Friend = new Person(); + + var printer = new PrintingConfig + { + MaxNestingLevel = 2 + }; + + var actual = printer.PrintToString(person); + + actual.Should().Contain("превышен уровень вложенности"); + } } \ No newline at end of file diff --git a/ObjectPrinting/ObjectPrinterTests/Person.cs b/ObjectPrinting/ObjectPrinterTests/Person.cs index 9de388e73..06fe0967a 100644 --- a/ObjectPrinting/ObjectPrinterTests/Person.cs +++ b/ObjectPrinting/ObjectPrinterTests/Person.cs @@ -9,13 +9,13 @@ public class Person public string Name { get; set; } public double Height { get; set; } public int Age { get; set; } - + public int[] Scores { get; set; } - + public List Tags { get; set; } - + public Dictionary Ratings { get; set; } - + public Person? Friend { get; set; } } } \ No newline at end of file From 49436e355e7538008757817a3f6b3454820bbef4 Mon Sep 17 00:00:00 2001 From: MihailK Date: Sat, 22 Nov 2025 15:15:10 +0500 Subject: [PATCH 6/8] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20=D1=82=D0=B5=D1=81=D1=82=D1=8B=20=D0=B8=20=D0=B4=D0=BE?= =?UTF-8?q?=D0=B1=D0=B0=D0=B2=D0=B8=D0=BB=20=D0=BD=D0=BE=D0=B2=D1=8B=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ObjectPrinterAcceptanceTests.cs | 85 +++++----- .../ObjectPrinterTests/ObjectPrinterTests.cs | 160 +++++++++++++----- ObjectPrinting/ObjectPrinterTests/Person.cs | 7 +- .../ObjectPrinting.csproj.DotSettings | 2 + 4 files changed, 168 insertions(+), 86 deletions(-) create mode 100644 ObjectPrinting/ObjectPrinting.csproj.DotSettings diff --git a/ObjectPrinting/ObjectPrinterTests/ObjectPrinterAcceptanceTests.cs b/ObjectPrinting/ObjectPrinterTests/ObjectPrinterAcceptanceTests.cs index 8e74c0ca2..61f7a5a7d 100644 --- a/ObjectPrinting/ObjectPrinterTests/ObjectPrinterAcceptanceTests.cs +++ b/ObjectPrinting/ObjectPrinterTests/ObjectPrinterAcceptanceTests.cs @@ -3,55 +3,56 @@ using System.Globalization; using System.Linq; using NUnit.Framework; +using ObjectPrinting.ObjectPrinter; -namespace ObjectPrinting.Tests +namespace ObjectPrinting.ObjectPrinterTests; + +[TestFixture] +public class ObjectPrinterAcceptanceTests { - [TestFixture] - public class ObjectPrinterAcceptanceTests + [Test] + public void Demo() { - [Test] - public void Demo() + var person = new Person { - var person = new Person + Id = Guid.NewGuid(), + Name = "Alex", + Age = 19, + Height = 161.14, + Scores = [100, 95, 88], + Tags = ["student", "intern", "dotnet"], + Ratings = new Dictionary + { + ["math"] = 5, + ["cs"] = 5, + ["english"] = 4 + } + }; + person.Friend = person; + + var printer = ObjectPrinter.ObjectPrinter.For() + .Excluding() + .Excluding(p => p.Age) + .Printing().Using(arr => $"int[{arr.Length}] {{ {string.Join(", ", arr)} }}") + .Printing>() + .Using(list => $"List (Count = {list.Count}) [ {string.Join(", ", list)} ]") + .Printing>().Using(dict => { - Id = Guid.NewGuid(), - Name = "Alex", - Age = 19, - Height = 161.14, - Scores = [100, 95, 88], - Tags = ["student", "intern", "dotnet"], - Ratings = new Dictionary - { - ["math"] = 5, - ["cs"] = 5, - ["english"] = 4 - } - }; - person.Friend = person; + var pairs = dict.Select(kv => $"{kv.Key}: {kv.Value}"); + return $"Dictionary (Count = {dict.Count}) {{ {string.Join(", ", pairs)} }}"; + }) + .Printing().Using(n => $"Number: {n}") + .Printing().Using(CultureInfo.InvariantCulture) + .Printing(p => p.Name).TrimmedToLength(3) + .Printing(p => p.Age).TrimmedToLength(1); - var printer = ObjectPrinter.For() - .Excluding() - .Excluding(p => p.Age) - .Printing().Using(arr => $"int[{arr.Length}] {{ {string.Join(", ", arr)} }}") - .Printing>() - .Using(list => $"List (Count = {list.Count}) [ {string.Join(", ", list)} ]") - .Printing>().Using(dict => - { - var pairs = dict.Select(kv => $"{kv.Key}: {kv.Value}"); - return $"Dictionary (Count = {dict.Count}) {{ {string.Join(", ", pairs)} }}"; - }) - .Printing().Using(n => $"Number: {n}") - .Printing().Using(CultureInfo.InvariantCulture) - .Printing(p => p.Name).TrimmedToLength(3); + var s1 = printer.PrintToString(person); + var s2 = person.PrintToString(); + var s3 = person.PrintToString(); - var s1 = printer.PrintToString(person); - var s2 = person.PrintToString(); - var s3 = person.PrintToString(); - - Console.WriteLine(s1); - Console.WriteLine(s2); - Console.WriteLine(s3); - } + Console.WriteLine(s1); + Console.WriteLine(s2); + Console.WriteLine(s3); } } \ No newline at end of file diff --git a/ObjectPrinting/ObjectPrinterTests/ObjectPrinterTests.cs b/ObjectPrinting/ObjectPrinterTests/ObjectPrinterTests.cs index 8c4a1cbfc..13d21b50b 100644 --- a/ObjectPrinting/ObjectPrinterTests/ObjectPrinterTests.cs +++ b/ObjectPrinting/ObjectPrinterTests/ObjectPrinterTests.cs @@ -2,10 +2,11 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; -using NUnit.Framework; using FluentAssertions; +using NUnit.Framework; +using ObjectPrinting.ObjectPrinter; -namespace ObjectPrinting.Tests; +namespace ObjectPrinting.ObjectPrinterTests; [TestFixture] public class ObjectPrinterStandardSerializationTests @@ -21,6 +22,8 @@ public void Setup() Name = "Fai", Height = 160.5, Age = 20, + IQ = 120, + BirthDate = new DateTime(2029, 11, 16, 04, 05, 06, DateTimeKind.Utc), Scores = [95, 88, 76], Tags = ["brooch", "Nevada"], Ratings = new Dictionary @@ -46,47 +49,64 @@ public void Setup() public void PrintToString_ShouldIncludeBasicProperties_WhenPersonIsValid() { var actual = setUpPerson.PrintToString(); - actual.Should().NotBeNullOrEmpty(); - actual.Should().Contain("Id = f14dd761-3260-4463-a4ad-6ba14de2026c"); - actual.Should().Contain("Person"); - actual.Should().Contain("Name = Fai"); - actual.Should().Contain("Age = 20"); - actual.Should().Contain("Height = 160,5"); - actual.Should().Contain("Tags = List"); - actual.Should().Contain("Ratings = Dictionary"); - actual.Should().Contain("Friend = Person"); - actual.Should().Contain("Friend = null"); + actual.Should().Contain($"{nameof(setUpPerson.Id)} = {setUpPerson!.Id}"); + actual.Should().Contain($"{nameof(setUpPerson.Name)} = {setUpPerson.Name}"); + actual.Should().Contain($"{nameof(setUpPerson.Height)} = {setUpPerson.Height}"); + actual.Should().Contain($"{nameof(setUpPerson.Age)} = {setUpPerson.Age}"); + actual.Should().Contain($"{nameof(setUpPerson.IQ)} = {setUpPerson.IQ}"); + actual.Should().Contain($"{nameof(setUpPerson.BirthDate)} = {setUpPerson.BirthDate}"); + actual.Should().Contain($"{nameof(setUpPerson.Scores)} = {setUpPerson.Scores.GetType().Name}"); + actual.Should().Contain($"{nameof(setUpPerson.Tags)} = List"); + actual.Should().Contain($"{nameof(setUpPerson.Ratings)} = Dictionary"); + actual.Should().Contain($"{nameof(setUpPerson.Friend)} = {setUpPerson.Friend!.GetType().Name}"); + actual.Should().Contain($"{nameof(setUpPerson.Friend.Friend)} = {null}"); } [Test] - public void PrintToString_ShouldApplyMemberExclusion_WhenNameExcludedInConfig() + public void PrintToString_ShouldApplyMemberExclusion_WhenMemberExcludedInConfig() { var actual = setUpPerson.PrintToString(cfg => cfg .Excluding(p => p.Name)); - actual.Should().NotContain("Name = Fai"); - actual.Should().Contain("Id = f14dd761-3260-4463-a4ad-6ba14de2026c"); + actual.Should().NotContain($"{nameof(setUpPerson.Name)} = {setUpPerson!.Name}"); + actual.Should().Contain($"{nameof(setUpPerson.Id)} = {setUpPerson!.Id}"); } [Test] - public void PrintToString_ShouldApplyTypeExclusion_WhenIntTypeExcludedInConfig() + public void PrintToString_ShouldApplyTypeExclusion_WhenTypeExcludedInConfig() { var actual = setUpPerson.PrintToString(cfg => cfg .Excluding()); + actual.Should().NotContain($"{nameof(setUpPerson.Age)} = {setUpPerson!.Age}"); + actual.Should().NotContain($"{nameof(setUpPerson.IQ)} = {setUpPerson!.IQ}"); + actual.Should().Contain($"{nameof(setUpPerson.Id)} = {setUpPerson!.Id}"); + } + + [Test] + public void PrintToString_ShouldUseCustomFormatter_WhenTypeFormatterIsConfigured() + { + var actual = setUpPerson.PrintToString(cfg => cfg + .Printing().Using(n => $"|{n}|")); + + actual.Should().Contain("Age = |20|"); actual.Should().NotContain("Age = 20"); - actual.Should().Contain("Id = f14dd761-3260-4463-a4ad-6ba14de2026c"); + actual.Should().Contain("IQ = |120|"); + actual.Should().NotContain("IQ = 120"); } [Test] - public void PrintToString_ShouldUseCustomFormatter_WhenIntTypeFormatterIsConfigured() + public void PrintToString_ShouldUseCustomFormatter_WhenMemberFormatterIsConfigured() { var actual = setUpPerson.PrintToString(cfg => cfg - .Printing().Using(n => $"Age: {n}")); + .Printing(p => p.Age).Using(n => $"Age: {n}")); actual.Should().Contain("Age: 20"); + actual.Should().NotContain("Age = 20"); + actual.Should().Contain("IQ = 120"); + actual.Should().NotContain("IQ: 120"); } [Test] @@ -95,25 +115,60 @@ public void PrintToString_ShouldUseInvariantCulture_WhenDoubleCultureIsSetToInva var actual = setUpPerson.PrintToString(cfg => cfg .Printing().Using(CultureInfo.InvariantCulture)); - actual.Should().Contain("Height = 160.5"); + actual.Should().Contain($"{nameof(setUpPerson.Height)} = 160.5"); + actual.Should().NotContain($"{nameof(setUpPerson.BirthDate)} = 11/16/2029 04:05:06"); } + [Test] + public void PrintToString_ShouldUseInvariantCulture_WhenDateTimeCultureIsSetToInvariant() + { + var actual = setUpPerson.PrintToString(cfg => cfg + .Printing().Using(CultureInfo.InvariantCulture)); + + actual.Should().Contain($"{nameof(setUpPerson.BirthDate)} = 11/16/2029 04:05:06"); + actual.Should().NotContain($"{nameof(setUpPerson.Height)} = 160.5"); + } + + [Test] public void PrintToString_ShouldApplyMemberSerializer_WhenNameCustomSerializerIsSet() { var actual = setUpPerson.PrintToString(cfg => cfg - .Printing(p => p.Name).Using(p => p.ToUpper())); + .Printing(p => p!.Name).Using(p => p.ToUpper())); - actual.Should().Contain("Name = FAI"); + actual.Should().Contain($"{nameof(setUpPerson.Name)} = FAI"); } [Test] - public void PrintToString_ShouldTrimStringProperty_WhenNameTrimmedToLengthSpecified() // pomenyat + public void PrintToString_ShouldTrimStringProperty_WhenStringMemberTrimmedToLengthSpecified() { var actual = setUpPerson.PrintToString(cfg => cfg - .Printing(p => p.Name).TrimmedToLength(2)); + .Printing(p => p.Name).StringTrimmedToLength(2)); - actual.Should().Contain("Name = Fa"); + actual.Should().Contain($"{nameof(setUpPerson.Name)} = Fa"); + actual.Should().NotContain($"{nameof(setUpPerson.Name)} = Fai"); + } + + [Test] + public void PrintToString_ShouldTrimStringProperty_WhenIntMemberTrimmedToLengthSpecified() + { + var actual = setUpPerson.PrintToString(cfg => cfg + .Printing(p => p.IQ).TrimmedToLength(2)); + + actual.Should().Contain($"{nameof(setUpPerson.IQ)} = 12"); + actual.Should().NotContain($"{nameof(setUpPerson.IQ)} = 120"); + } + + [Test] + public void PrintToString_ShouldTrimStringProperty_WhenIntTypeTrimmedToLengthSpecified() + { + var actual = setUpPerson.PrintToString(cfg => cfg + .Printing().TrimmedToLength(1)); + + actual.Should().Contain($"{nameof(setUpPerson.IQ)} = 1"); + actual.Should().NotContain($"{nameof(setUpPerson.IQ)} = 120"); + actual.Should().Contain($"{nameof(setUpPerson.Age)} = 2"); + actual.Should().NotContain($"{nameof(setUpPerson.Age)} = 20"); } [Test] @@ -127,13 +182,13 @@ public void PrintToString_ShouldApplyMultipleCustomizations_WhenCombinedExclusio .Printing(p => p.Name).TrimmedToLength(2) .Printing(p => p.Name).Using(p => p.ToUpper())); - actual.Should().NotContain("Guid"); - actual.Should().NotContain("Tags"); + actual.Should().NotContain($"{nameof(setUpPerson.Id)}"); + actual.Should().NotContain($"{nameof(setUpPerson.Tags)}"); actual.Should().Contain("Number: 20"); actual.Should().NotContain("Age = 20"); - actual.Should().Contain("Height = 160.5"); - actual.Should().NotContain("Heigh = 160,5"); - actual.Should().Contain("Name = FA"); + actual.Should().Contain($"{nameof(setUpPerson.Height)} = 160.5"); + actual.Should().NotContain($"{nameof(setUpPerson.Height)} = 160,5"); + actual.Should().Contain($"{nameof(setUpPerson.Name)} = FA"); } [Test] @@ -141,10 +196,10 @@ public void PrintToString_ShouldRenderIntArrayWithIndexedItems_WhenPersonHasArra { var actual = setUpPerson.PrintToString(); - actual.Should().Contain("Scores = Int32[]"); - actual.Should().Contain("[0] = 95"); - actual.Should().Contain("[1] = 88"); - actual.Should().Contain("[2] = 76"); + actual.Should().Contain($"{nameof(setUpPerson.Scores)} = {setUpPerson!.Scores.GetType().Name}"); + actual.Should().Contain($"[0] = {setUpPerson.Scores[0]}"); + actual.Should().Contain($"[1] = {setUpPerson.Scores[1]}"); + actual.Should().Contain($"[2] = {setUpPerson.Scores[2]}"); } [Test] @@ -152,9 +207,9 @@ public void PrintToString_ShouldRenderGenericListWithIndexedItems_WhenPersonHasL { var actual = setUpPerson.PrintToString(); - actual.Should().Contain("Tags = List"); - actual.Should().Contain("[0] = brooch"); - actual.Should().Contain("[1] = Nevada"); + actual.Should().Contain($"{nameof(setUpPerson.Tags)} = List"); + actual.Should().Contain($"[0] = {setUpPerson.Tags[0]}"); + actual.Should().Contain($"[1] = {setUpPerson.Tags[1]}"); } [Test] @@ -162,11 +217,11 @@ public void PrintToString_ShouldRenderDictionaryWithKeyValuePairs_WhenPersonHasD { var actual = setUpPerson.PrintToString(); - actual.Should().Contain("Ratings = Dictionary"); - actual.Should().Contain("Key = quality"); - actual.Should().Contain("Value = 5"); - actual.Should().Contain("Key = speed"); - actual.Should().Contain("Value = 4"); + actual.Should().Contain($"{nameof(setUpPerson.Ratings)} = Dictionary"); + actual.Should().Contain($"Key = {setUpPerson!.Ratings.Keys.First()}"); + actual.Should().Contain($"Value = {setUpPerson.Ratings.Values.First()}"); + actual.Should().Contain($"Key = {setUpPerson.Ratings.Keys.Last()}"); + actual.Should().Contain($"Value = {setUpPerson.Ratings.Values.Last()}"); } [Test] @@ -244,6 +299,27 @@ public void PrintToString_ShouldEmitCycleMarker_WhenObjectGraphHasCyclicReferenc actual.Should().Contain("CyclicRef"); } + [Test] + public void PrintToString_ShouldNotShareVisitedState_WhenDifferentRootsShareSameFriend() + { + var sharedFriend = new Person { Name = "Shared", Age = 42 }; + + var person1 = new Person { Name = "P1", Friend = sharedFriend }; + + var person2 = new Person { Name = "P2", Friend = sharedFriend }; + + var actual1 = person1.PrintToString(); + var actual2 = person2.PrintToString(); + + + actual1.Should().Contain("Friend = Person"); + actual1.Should().NotContain("CyclicRef"); + + actual2.Should().Contain("Friend = Person"); + actual2.Should().NotContain("CyclicRef"); + } + + [Test] public void PrintToString_ShouldReportNestingLevelExceeded_WhenFriendChainDepthBeyondMax() { diff --git a/ObjectPrinting/ObjectPrinterTests/Person.cs b/ObjectPrinting/ObjectPrinterTests/Person.cs index 06fe0967a..4fe158a92 100644 --- a/ObjectPrinting/ObjectPrinterTests/Person.cs +++ b/ObjectPrinting/ObjectPrinterTests/Person.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; -namespace ObjectPrinting.Tests +namespace ObjectPrinting.ObjectPrinterTests { public class Person { @@ -9,7 +9,10 @@ public class Person public string Name { get; set; } public double Height { get; set; } public int Age { get; set; } - + + public int IQ { get; set; } + + public DateTime BirthDate {get; set;} public int[] Scores { get; set; } public List Tags { get; set; } diff --git a/ObjectPrinting/ObjectPrinting.csproj.DotSettings b/ObjectPrinting/ObjectPrinting.csproj.DotSettings new file mode 100644 index 000000000..28a74f772 --- /dev/null +++ b/ObjectPrinting/ObjectPrinting.csproj.DotSettings @@ -0,0 +1,2 @@ + + True \ No newline at end of file From dfc6ea8a99ff305ae09396212f344258a7520854 Mon Sep 17 00:00:00 2001 From: MihailK Date: Sat, 22 Nov 2025 15:18:50 +0500 Subject: [PATCH 7/8] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D0=BB?= =?UTF-8?q?=20Trimm=20=D0=B4=D0=BB=D1=8F=20=D0=B2=D1=81=D0=B5=D1=85=20?= =?UTF-8?q?=D0=BE=D0=B1=D1=8A=D0=B5=D0=BA=D1=82=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ObjectPrinter/MemberPrintingConfig.cs | 27 +++++-- ObjectPrinting/ObjectPrinter/ObjectPrinter.cs | 11 ++- .../ObjectPrinter/ObjectPrintingExtensions.cs | 2 +- .../ObjectPrinter/PrintingConfig.cs | 81 ++++++++++--------- .../StringMemberPrintingConfig.cs | 12 +-- 5 files changed, 74 insertions(+), 59 deletions(-) diff --git a/ObjectPrinting/ObjectPrinter/MemberPrintingConfig.cs b/ObjectPrinting/ObjectPrinter/MemberPrintingConfig.cs index 71cc94e09..adbf30413 100644 --- a/ObjectPrinting/ObjectPrinter/MemberPrintingConfig.cs +++ b/ObjectPrinting/ObjectPrinter/MemberPrintingConfig.cs @@ -2,31 +2,42 @@ using System.Globalization; using System.Reflection; -namespace ObjectPrinting; +namespace ObjectPrinting.ObjectPrinter; public class MemberPrintingConfig( PrintingConfig printingConfig, MemberInfo? memberInfo = null) { protected readonly PrintingConfig PrintingConfig = printingConfig; - protected readonly MemberInfo MemberInfo = memberInfo; + private readonly MemberInfo? MemberInfo = memberInfo; public PrintingConfig Using(Func printingMethod) { if (MemberInfo is null) - PrintingConfig.CustomTypeSerializers[typeof(TMemberType)] = printingMethod; + PrintingConfig.CustomTypeSerializers[typeof(TMemberType)] = Wrapper; else - PrintingConfig.CustomMemberSerializers[MemberInfo] = printingMethod; + PrintingConfig.CustomMemberSerializers[MemberInfo] = Wrapper; + return PrintingConfig; + + string Wrapper(object value) => printingMethod((TMemberType)value); } public PrintingConfig Using(CultureInfo culture) { - if (MemberInfo is not null) - throw new InvalidOperationException( - "Using(CultureInfo) доступен только для SetPrintingFor(), а не для конкретного члена."); - PrintingConfig.CulturesForTypes[typeof(TMemberType)] = culture; return PrintingConfig; } + + public PrintingConfig TrimmedToLength(int length) + { + ArgumentOutOfRangeException.ThrowIfNegative(length); + + if (MemberInfo is null) + PrintingConfig.TrimmedTypes[typeof(TMemberType)] = length; + else + PrintingConfig.TrimmedMembers[MemberInfo] = length; + + return PrintingConfig; + } } \ No newline at end of file diff --git a/ObjectPrinting/ObjectPrinter/ObjectPrinter.cs b/ObjectPrinting/ObjectPrinter/ObjectPrinter.cs index 3c7867c32..e990ab983 100644 --- a/ObjectPrinting/ObjectPrinter/ObjectPrinter.cs +++ b/ObjectPrinting/ObjectPrinter/ObjectPrinter.cs @@ -1,10 +1,9 @@ -namespace ObjectPrinting +namespace ObjectPrinting.ObjectPrinter; + +public static class ObjectPrinter { - public class ObjectPrinter + public static PrintingConfig For() { - public static PrintingConfig For() - { - return new PrintingConfig(); - } + return new PrintingConfig(); } } \ No newline at end of file diff --git a/ObjectPrinting/ObjectPrinter/ObjectPrintingExtensions.cs b/ObjectPrinting/ObjectPrinter/ObjectPrintingExtensions.cs index 5104af96d..04bbc424a 100644 --- a/ObjectPrinting/ObjectPrinter/ObjectPrintingExtensions.cs +++ b/ObjectPrinting/ObjectPrinter/ObjectPrintingExtensions.cs @@ -1,6 +1,6 @@ using System; -namespace ObjectPrinting; +namespace ObjectPrinting.ObjectPrinter; public static class ObjectPrintingExtensions { diff --git a/ObjectPrinting/ObjectPrinter/PrintingConfig.cs b/ObjectPrinting/ObjectPrinter/PrintingConfig.cs index 46a3450c6..408120edb 100644 --- a/ObjectPrinting/ObjectPrinter/PrintingConfig.cs +++ b/ObjectPrinting/ObjectPrinter/PrintingConfig.cs @@ -4,47 +4,50 @@ using System.Linq.Expressions; using System.Reflection; +namespace ObjectPrinting.ObjectPrinter; -namespace ObjectPrinting +public class PrintingConfig { - public class PrintingConfig + public readonly HashSet ExcludedTypes = []; + public readonly HashSet ExcludedMember = []; + public readonly Dictionary TrimmedMembers = new(); + public readonly Dictionary CulturesForTypes = new(); + internal readonly Dictionary> CustomTypeSerializers = new(); + internal readonly Dictionary> CustomMemberSerializers = new(); + public readonly Dictionary TrimmedTypes = new(); + + public int MaxNestingLevel { get; init; } = 5; + + public PrintingConfig Excluding() + { + ExcludedTypes.Add(typeof(TMember)); + return this; + } + + public PrintingConfig Excluding(Expression> memberSelector) + { + ExcludedMember.Add(GetMember(memberSelector)); + return this; + } + + public MemberPrintingConfig Printing() + => new(this); + + public StringPrintConfig Printing(Expression> stringMemberSelector) + => new(this, GetMember(stringMemberSelector)); + + public MemberPrintingConfig Printing(Expression> memberSelector) + => new(this, GetMember(memberSelector)); + + + public string PrintToString(TOwner? obj) + => Serializer.SerializeObject(obj, this); + + + private static MemberInfo GetMember(Expression> selector) { - public readonly HashSet ExcludedTypes = []; - public readonly HashSet ExcludedMember = []; - public readonly Dictionary TrimmedMembers = new(); - public readonly Dictionary CulturesForTypes = new(); - internal readonly Dictionary CustomTypeSerializers = new(); - internal readonly Dictionary CustomMemberSerializers = new(); - internal int? TrimStringLength; - public int MaxNestingLevel { get; set; } = 5; - - public PrintingConfig Excluding() - { - ExcludedTypes.Add(typeof(TMember)); - return this; - } - - public PrintingConfig Excluding(Expression> memberSelector) - { - ExcludedMember.Add(GetMember(memberSelector)); - return this; - } - - public MemberPrintingConfig Printing() - => new(this); - - public StintingPrintConfig Printing(Expression> stringMemberSelector) - => new(this, GetMember(stringMemberSelector)); - - - public string PrintToString(TOwner? obj) - => new Serializer(this).SerializeObject(obj); - - - private static MemberInfo GetMember(Expression> selector) - { - if (selector.Body is MemberExpression m) return m.Member; - throw new ArgumentException("Member selector must be a simple member access.", nameof(selector)); - } + if (selector.Body is MemberExpression m) + return m.Member; + throw new ArgumentException("Member selector must be a simple member access.", nameof(selector)); } } \ No newline at end of file diff --git a/ObjectPrinting/ObjectPrinter/StringMemberPrintingConfig.cs b/ObjectPrinting/ObjectPrinter/StringMemberPrintingConfig.cs index d17c59947..e9458ae49 100644 --- a/ObjectPrinting/ObjectPrinter/StringMemberPrintingConfig.cs +++ b/ObjectPrinting/ObjectPrinter/StringMemberPrintingConfig.cs @@ -1,12 +1,14 @@ -using System.Reflection; +using System; +using System.Reflection; -namespace ObjectPrinting; +namespace ObjectPrinting.ObjectPrinter; -public class StintingPrintConfig(PrintingConfig printingConfig, MemberInfo memberInfo) +public class StringPrintConfig(PrintingConfig printingConfig, MemberInfo memberInfo) : MemberPrintingConfig(printingConfig, memberInfo) { - public PrintingConfig TrimmedToLength(int length) - { + public PrintingConfig StringTrimmedToLength(int length) //по сути уже не нужен, так как дублирует поведение TrimmedToLength. + { //Просто хотелось соблюсти формальности, чтобы этот метод отображался только у string. + ArgumentOutOfRangeException.ThrowIfNegative(length); printingConfig.TrimmedMembers[memberInfo] = length; return PrintingConfig; } From a1d75a9861d98dc84b3f46a00ebe481b701df93e Mon Sep 17 00:00:00 2001 From: MihailK Date: Sat, 22 Nov 2025 15:19:17 +0500 Subject: [PATCH 8/8] =?UTF-8?q?=D0=9E=D1=82=D1=80=D0=B5=D1=84=D0=B0=D0=BA?= =?UTF-8?q?=D1=82=D0=BE=D1=80=D0=B8=D0=BB=20Serializer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ObjectPrinter/SerializationContext.cs | 15 + ObjectPrinting/ObjectPrinter/Serializer.cs | 352 +++++++++++------- 2 files changed, 230 insertions(+), 137 deletions(-) create mode 100644 ObjectPrinting/ObjectPrinter/SerializationContext.cs diff --git a/ObjectPrinting/ObjectPrinter/SerializationContext.cs b/ObjectPrinting/ObjectPrinter/SerializationContext.cs new file mode 100644 index 000000000..f95ad16cb --- /dev/null +++ b/ObjectPrinting/ObjectPrinter/SerializationContext.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using System.Text; + +namespace ObjectPrinting.ObjectPrinter; + +internal class SerializationContext(PrintingConfig config, StringBuilder builder) +{ + public readonly PrintingConfig Config = config; + public readonly HashSet Visited = new(ReferenceEqualityComparer.Instance); + public readonly StringBuilder Builder = builder; + + public void Indent(int level) => Builder.Append(new string('\t', level)); +} + + diff --git a/ObjectPrinting/ObjectPrinter/Serializer.cs b/ObjectPrinting/ObjectPrinter/Serializer.cs index b6b9bf94b..38dc983de 100644 --- a/ObjectPrinting/ObjectPrinter/Serializer.cs +++ b/ObjectPrinting/ObjectPrinter/Serializer.cs @@ -1,18 +1,16 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Reflection; using System.Text; -namespace ObjectPrinting; +namespace ObjectPrinting.ObjectPrinter; -internal sealed class Serializer +internal static class Serializer { - private readonly PrintingConfig config; - private readonly HashSet visited = new(ReferenceEqualityComparer.Instance); - - private static readonly Type[] TerminalTypes = + private static readonly HashSet TerminalTypes = [ typeof(string), typeof(DateTime), typeof(TimeSpan), typeof(Guid), typeof(decimal), @@ -21,185 +19,244 @@ internal sealed class Serializer typeof(float), typeof(double), typeof(bool), typeof(char) ]; - internal Serializer(PrintingConfig config) => this.config = config; - - internal string SerializeObject(object? obj) + internal static string SerializeObject(TOwner? obj, PrintingConfig config) { var sb = new StringBuilder(); - Serialize(obj, sb, 0, parentMember: null); + var ctx = new SerializationContext(config, sb); + Serialize(obj, 0, null, ctx); return sb.ToString(); } - private void Serialize(object? obj, StringBuilder sb, int level, MemberInfo? parentMember) + private static void Serialize( + object? obj, + int level, + MemberInfo? parentMember, + SerializationContext ctx) + { + if (TryHandleNullOrExcludedOrDepth(obj, level, ctx)) + return; + + var type = obj!.GetType(); + + if (TryHandleCycle(obj, type, ctx)) + return; + + if (TryHandleCustomTypeSerializer(obj, type, parentMember, ctx)) + return; + + if (TryHandleTerminal(obj, type, parentMember, ctx)) + return; + + if (TryHandleDictionary(obj, type, level, ctx)) + return; + + if (TryHandleEnumerable(obj, type, level, ctx)) + return; + + SerializeComplexObject(obj, type, level, ctx); + } + + + private static bool TryHandleNullOrExcludedOrDepth( + object? obj, + int level, + SerializationContext ctx) { if (obj is null) { - sb.AppendLine("null"); - return; + ctx.Builder.AppendLine("null"); + return true; } var type = obj.GetType(); - - if (config.ExcludedTypes.Contains(type)) + + if (ctx.Config.ExcludedTypes.Contains(type)) { - sb.AppendLine("[Excluded Type]"); - return; + ctx.Builder.AppendLine("[Excluded Type]"); + return true; } - - if (level >= config.MaxNestingLevel) + + if (level >= ctx.Config.MaxNestingLevel) { - sb.AppendLine($"[{FormatTypeName(type)} превышен уровень вложенности]"); - return; + ctx.Builder.AppendLine($"[{FormatTypeName(type)} превышен уровень вложенности]"); + return true; } - - if (!type.IsValueType) - { - if (visited.Contains(obj)) - { - sb.AppendLine($"[CyclicRef {FormatTypeName(type)}]"); - return; - } - visited.Add(obj); - } + return false; + } + + private static bool TryHandleCycle( + object obj, + Type type, + SerializationContext ctx) + { + if (type.IsValueType) + return false; + + if (ctx.Visited.Add(obj)) + return false; + ctx.Builder.AppendLine($"[CyclicRef {FormatTypeName(type)}]"); + return true; + + } + + private static bool TryHandleCustomTypeSerializer( + object obj, + Type type, + MemberInfo? parentMember, + SerializationContext ctx) + { + if (!ctx.Config.CustomTypeSerializers.TryGetValue(type, out var typeSer)) + return false; + + var text = typeSer(obj); + ctx.Builder.Append(ApplyTrimming(text, parentMember, ctx.Config)); + return true; + } + + private static bool TryHandleTerminal( + object obj, + Type type, + MemberInfo? parentMember, + SerializationContext ctx) + { + if (!TerminalTypes.Contains(type) && !type.IsEnum) + return false; + ctx.Builder.Append(FormatScalar(obj, type, parentMember, ctx.Config)); + return true; + } + + private static bool TryHandleDictionary( + object obj, + Type type, + int level, + SerializationContext ctx) + { + if (obj is not IDictionary dict) + return false; - if (config.CustomTypeSerializers.TryGetValue(type, out var typeSer)) + ctx.Builder.AppendLine(FormatTypeName(type)); + foreach (DictionaryEntry entry in dict) { - var text = InvokeSerializer(typeSer, obj); - sb.Append(ApplyTrimming(text, parentMember)); - return; + ctx.Indent(level + 1); + ctx.Builder.Append("Key = "); + Serialize(entry.Key, level + 1, null, ctx); + + ctx.Indent(level + 1); + ctx.Builder.Append("Value = "); + Serialize(entry.Value, level + 1, null, ctx); } + return true; + } + + private static bool TryHandleEnumerable( + object obj, + Type type, + int level, + SerializationContext ctx) + { + if (obj is not IEnumerable enumerable || type == typeof(string)) + return false; - if (TerminalTypes.Contains(type) || type.IsEnum) + ctx.Builder.AppendLine(FormatTypeName(type)); + var i = 0; + foreach (var item in enumerable) { - sb.Append(FormatScalar(obj, type, parentMember)); - return; + ctx.Indent(level + 1); + ctx.Builder.Append($"[{i}] = "); + Serialize(item, level + 1, null, ctx); + i++; } - - if (obj is IDictionary dict) - { - sb.AppendLine(FormatTypeName(type)); - foreach (DictionaryEntry entry in dict) - { - Indent(sb, level + 1); - sb.Append("Key = "); - Serialize(entry.Key, sb, level + 1, null); - - Indent(sb, level + 1); - sb.Append("Value = "); - Serialize(entry.Value, sb, level + 1, null); - } - return; - } - - if (obj is IEnumerable enumerable && type != typeof(string)) - { - sb.AppendLine(FormatTypeName(type)); - var i = 0; - foreach (var item in enumerable) - { - Indent(sb, level + 1); - sb.Append($"[{i}] = "); - Serialize(item, sb, level + 1, null); - i++; - } + return true; + } - return; - } - - sb.AppendLine(FormatTypeName(type)); + private static void SerializeComplexObject( + object obj, + Type type, + int level, + SerializationContext ctx) + { + ctx.Builder.AppendLine(FormatTypeName(type)); foreach (var prop in type.GetProperties(BindingFlags.Instance | BindingFlags.Public)) { if (!prop.CanRead) continue; - if (config.ExcludedMember.Contains(prop)) continue; - if (config.ExcludedTypes.Contains(prop.PropertyType)) continue; - - Indent(sb, level + 1); - sb.Append(prop.Name); - sb.Append(" = "); - - if (config.CustomMemberSerializers.TryGetValue(prop, out var memberSer)) - { - var value = SafeGet(() => prop.GetValue(obj)); - var text = value is null ? "null" : InvokeSerializer(memberSer, value!); - sb.AppendLine(ApplyTrimming(text, parentMember: prop).TrimEnd('\r', '\n')); - continue; - } - - var propValue = SafeGet(() => prop.GetValue(obj)); - Serialize(propValue, sb, level + 1, prop); + + SerializeMember(prop, level, + p => p.PropertyType, + p => p.GetValue(obj), + p => p.Name, + ctx); } foreach (var field in type.GetFields(BindingFlags.Instance | BindingFlags.Public)) { - if (config.ExcludedMember.Contains(field)) continue; - if (config.ExcludedTypes.Contains(field.FieldType)) continue; - - Indent(sb, level + 1); - sb.Append(field.Name); - sb.Append(" = "); - - if (config.CustomMemberSerializers.TryGetValue(field, out var memberSer)) - { - var value = SafeGet(() => field.GetValue(obj)); - var text = value is null ? "null" : InvokeSerializer(memberSer, value!); - sb.AppendLine(ApplyTrimming(text, parentMember: field).TrimEnd('\r', '\n')); - continue; - } - - var fieldValue = SafeGet(() => field.GetValue(obj)); - Serialize(fieldValue, sb, level + 1, field); + SerializeMember(field, level, + f => f.FieldType, + f => f.GetValue(obj), + f => f.Name, + ctx); } } - private static void Indent(StringBuilder sb, int level) - => sb.Append(new string('\t', level)); - - - private string FormatScalar(object value, Type type, MemberInfo? parentMember) + private static string FormatScalar( + object value, + Type type, + MemberInfo? parentMember, + PrintingConfig config) { if (config.CustomTypeSerializers.TryGetValue(type, out var del)) { - var s = InvokeSerializer(del, value); - return ApplyTrimming(s, parentMember); + var s = del(value); + return ApplyTrimming(s, parentMember, config); } + string raw; + if (type == typeof(string)) - { - var s = (string)value; - return ApplyTrimming(s, parentMember); - } - - if (!config.CulturesForTypes.TryGetValue(type, out var culture)) - return value + Environment.NewLine; - if (value is IFormattable fmt) - return fmt.ToString(null, culture) + Environment.NewLine; + raw = (string)value; + else if (config.CulturesForTypes.TryGetValue(type, out var culture) && value is IFormattable fmt) + raw = fmt.ToString(null, culture); + else + raw = value.ToString() ?? string.Empty; - return value + Environment.NewLine; + return ApplyTrimming(raw, parentMember, config); } - private string ApplyTrimming(string s, MemberInfo? parentMember) + private static string ApplyTrimming( + string? s, + MemberInfo? parentMember, + PrintingConfig config) { int? length = null; - if (parentMember != null && config.TrimmedMembers.TryGetValue(parentMember, out var memberLen)) + + if (parentMember != null && + config.TrimmedMembers.TryGetValue(parentMember, out var memberLen)) + { length = memberLen; - else if (config.TrimStringLength.HasValue) - length = config.TrimStringLength; + } + else if (parentMember is PropertyInfo pi && + config.TrimmedTypes.TryGetValue(pi.PropertyType, out var typeLen)) + { + length = typeLen; + } + else if (parentMember is FieldInfo fi && + config.TrimmedTypes.TryGetValue(fi.FieldType, out var fieldTypeLen)) + { + length = fieldTypeLen; + } if (length.HasValue && s != null && s.Length > length.Value) - s = s.Substring(0, length.Value); + s = s[..length.Value]; + Debug.Assert(s != null, nameof(s) + " != null"); return s.EndsWith(Environment.NewLine) ? s : s + Environment.NewLine; } - private static string InvokeSerializer(Delegate del, object value) - => (string)del.DynamicInvoke(value)!; - private static object? SafeGet(Func getter) { try @@ -211,24 +268,45 @@ private static string InvokeSerializer(Delegate del, object value) return null; } } - + private static string FormatTypeName(Type t) { - if (!t.IsGenericType) return t.Name; + if (!t.IsGenericType) + return t.Name; var defName = t.Name; var backtick = defName.IndexOf('`'); - if (backtick > 0) defName = defName.Substring(0, backtick); + defName = defName[..backtick]; var args = t.GetGenericArguments(); var argNames = string.Join(", ", args.Select(FormatTypeName)); return $"{defName}<{argNames}>"; } - - private sealed class ReferenceEqualityComparer : IEqualityComparer + + private static void SerializeMember( + TMember member, + int level, + Func getMemberType, + Func getValue, + Func getName, + SerializationContext ctx) + where TMember : MemberInfo { - public static readonly ReferenceEqualityComparer Instance = new(); - public new bool Equals(object x, object y) => ReferenceEquals(x, y); - public int GetHashCode(object obj) => System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(obj); + if (ctx.Config.ExcludedMember.Contains(member)) return; + if (ctx.Config.ExcludedTypes.Contains(getMemberType(member))) return; + + ctx.Indent(level + 1); + ctx.Builder.Append(getName(member)).Append(" = "); + + if (ctx.Config.CustomMemberSerializers.TryGetValue(member, out var memberSer)) + { + var value = SafeGet(() => getValue(member)); + var text = value is null ? "null" : memberSer(value); + ctx.Builder.AppendLine(ApplyTrimming(text, member, ctx.Config).TrimEnd('\r', '\n')); + return; + } + + var memberValue = SafeGet(() => getValue(member)); + Serialize(memberValue, level + 1, member, ctx); } } \ No newline at end of file