From d88247521fc2510a8d68fc7b60d13cfe1ce0c468 Mon Sep 17 00:00:00 2001 From: Matvey Mednikov Date: Sun, 16 Nov 2025 14:16:22 +0500 Subject: [PATCH 1/2] ObjectPrinting Solution with Tests --- .../IPropertyPrinterConfigContext.cs | 10 + .../PropertyContext/ISrtingPropertyContext.cs | 6 + .../PropertyContext/PropertyContext.cs | 33 ++ .../PropertyContext/StringPropertyContext.cs | 49 +++ .../Context/TypeContext/BaseContext.cs | 37 ++ .../TypeContext/IPrintingConfigContext.cs | 11 + ObjectPrinting/PrintingConfig.cs | 219 +++++++++-- .../Tests/ObjectPrinterAcceptanceTests.cs | 47 ++- ObjectPrinting/Tests/Person.cs | 17 +- ObjectPrinting/Tests/PersonGroup.cs | 13 + ObjectPrinting/Tests/PrintingConfigTests.cs | 367 ++++++++++++++++++ 11 files changed, 755 insertions(+), 54 deletions(-) create mode 100644 ObjectPrinting/Context/PropertyContext/IPropertyPrinterConfigContext.cs create mode 100644 ObjectPrinting/Context/PropertyContext/ISrtingPropertyContext.cs create mode 100644 ObjectPrinting/Context/PropertyContext/PropertyContext.cs create mode 100644 ObjectPrinting/Context/PropertyContext/StringPropertyContext.cs create mode 100644 ObjectPrinting/Context/TypeContext/BaseContext.cs create mode 100644 ObjectPrinting/Context/TypeContext/IPrintingConfigContext.cs create mode 100644 ObjectPrinting/Tests/PersonGroup.cs create mode 100644 ObjectPrinting/Tests/PrintingConfigTests.cs diff --git a/ObjectPrinting/Context/PropertyContext/IPropertyPrinterConfigContext.cs b/ObjectPrinting/Context/PropertyContext/IPropertyPrinterConfigContext.cs new file mode 100644 index 000000000..ea0c1c31a --- /dev/null +++ b/ObjectPrinting/Context/PropertyContext/IPropertyPrinterConfigContext.cs @@ -0,0 +1,10 @@ +using System; + +namespace ObjectPrinting.Context; + +public interface IPropertyPrinterConfigContext +{ + IPropertyPrinterConfigContext SetSerializer(Func serializer); + IPropertyPrinterConfigContext ExcludeProperty(); + PrintingConfig End(); +} \ No newline at end of file diff --git a/ObjectPrinting/Context/PropertyContext/ISrtingPropertyContext.cs b/ObjectPrinting/Context/PropertyContext/ISrtingPropertyContext.cs new file mode 100644 index 000000000..85ff2a4a9 --- /dev/null +++ b/ObjectPrinting/Context/PropertyContext/ISrtingPropertyContext.cs @@ -0,0 +1,6 @@ +namespace ObjectPrinting.Context; + +public interface IStringPropertyPrinterConfigContext : IPropertyPrinterConfigContext +{ + IStringPropertyPrinterConfigContext TrimToLength(int maxLength); +} \ No newline at end of file diff --git a/ObjectPrinting/Context/PropertyContext/PropertyContext.cs b/ObjectPrinting/Context/PropertyContext/PropertyContext.cs new file mode 100644 index 000000000..65a869408 --- /dev/null +++ b/ObjectPrinting/Context/PropertyContext/PropertyContext.cs @@ -0,0 +1,33 @@ +using System; +using ObjectPrinting.Context; + +namespace ObjectPrinting; + +public class PropertyContext : IPropertyPrinterConfigContext +{ + private readonly PrintingConfig parent; + private readonly string propertyName; + + internal PropertyContext(PrintingConfig parent, string propertyName) + { + this.parent = parent; + this.propertyName = propertyName; + } + + public IPropertyPrinterConfigContext SetSerializer(Func serializer) + { + parent.SetPropertySerializer(propertyName, serializer); + return this; + } + + public IPropertyPrinterConfigContext ExcludeProperty() + { + parent.ExcludeProperty(propertyName); + return this; + } + + public PrintingConfig End() + { + return parent; + } +} \ No newline at end of file diff --git a/ObjectPrinting/Context/PropertyContext/StringPropertyContext.cs b/ObjectPrinting/Context/PropertyContext/StringPropertyContext.cs new file mode 100644 index 000000000..63c7c23b8 --- /dev/null +++ b/ObjectPrinting/Context/PropertyContext/StringPropertyContext.cs @@ -0,0 +1,49 @@ +using System; +using ObjectPrinting.Context; + +namespace ObjectPrinting; + +public class StringPropertyContext : IStringPropertyPrinterConfigContext +{ + private readonly PrintingConfig parent; + private readonly string propertyName; + + internal StringPropertyContext(PrintingConfig parent, string propertyName) + { + this.parent = parent; + this.propertyName = propertyName; + } + + public IStringPropertyPrinterConfigContext Set(Func serializer) + { + parent.SetPropertySerializer(propertyName, serializer); + return this; + } + + public IStringPropertyPrinterConfigContext Exclude() + { + parent.ExcludeProperty(propertyName); + return this; + } + + public IStringPropertyPrinterConfigContext TrimToLength(int maxLength) + { + parent.TrimStringProperty(propertyName, maxLength); + return this; + } + + public PrintingConfig End() + { + return parent; + } + + IPropertyPrinterConfigContext IPropertyPrinterConfigContext.SetSerializer(Func serializer) + { + return Set(serializer); + } + + IPropertyPrinterConfigContext IPropertyPrinterConfigContext.ExcludeProperty() + { + return Exclude(); + } +} \ No newline at end of file diff --git a/ObjectPrinting/Context/TypeContext/BaseContext.cs b/ObjectPrinting/Context/TypeContext/BaseContext.cs new file mode 100644 index 000000000..67d19caf6 --- /dev/null +++ b/ObjectPrinting/Context/TypeContext/BaseContext.cs @@ -0,0 +1,37 @@ +using System; +using ObjectPrinting.Context; + +namespace ObjectPrinting; + +public class BaseContext : IPrintingConfigContext +{ + private readonly PrintingConfig parent; + + internal BaseContext(PrintingConfig parent) + { + this.parent = parent; + } + + public IPrintingConfigContext SetSerializer(Func serializer) + { + parent.SetTypeSerializer(serializer); + return this; + } + + public IPrintingConfigContext UsingCulture(IFormatProvider culture) + { + parent.SetNumericCulture(culture); + return this; + } + + public IPrintingConfigContext ExcludeType() + { + parent.ExcludeType(); + return this; + } + + public PrintingConfig End() + { + return parent; + } +} \ No newline at end of file diff --git a/ObjectPrinting/Context/TypeContext/IPrintingConfigContext.cs b/ObjectPrinting/Context/TypeContext/IPrintingConfigContext.cs new file mode 100644 index 000000000..ffc2e011d --- /dev/null +++ b/ObjectPrinting/Context/TypeContext/IPrintingConfigContext.cs @@ -0,0 +1,11 @@ +using System; + +namespace ObjectPrinting.Context; + +public interface IPrintingConfigContext +{ + IPrintingConfigContext SetSerializer(Func serializer); + IPrintingConfigContext UsingCulture(IFormatProvider culture); + IPrintingConfigContext ExcludeType(); + PrintingConfig End(); +} \ No newline at end of file diff --git a/ObjectPrinting/PrintingConfig.cs b/ObjectPrinting/PrintingConfig.cs index a9e082117..13c710124 100644 --- a/ObjectPrinting/PrintingConfig.cs +++ b/ObjectPrinting/PrintingConfig.cs @@ -1,41 +1,208 @@ using System; +using System.Collections; +using System.Collections.Generic; using System.Linq; +using System.Linq.Expressions; using System.Text; +using ObjectPrinting.Context; -namespace ObjectPrinting +namespace ObjectPrinting; + +public class PrintingConfig { - public class PrintingConfig + private static readonly Type[] FinalTypes = + [ + typeof(int), typeof(double), typeof(float), typeof(string), + typeof(DateTime), typeof(TimeSpan) + ]; + + private readonly Dictionary> typeSerializers = new(); + private readonly Dictionary numericCultures = new(); + private readonly HashSet excludedTypes = []; + + private readonly Dictionary> propertySerializers = new(); + private readonly Dictionary stringMaxLengths = new(); + private readonly HashSet excludedProperties = []; + + internal void SetTypeSerializer(Func serializer) + { + typeSerializers[typeof(T)] = o => serializer((T)o); + } + + internal void SetNumericCulture(IFormatProvider culture) + { + numericCultures[typeof(T)] = culture; + } + + internal void ExcludeType() + { + excludedTypes.Add(typeof(T)); + } + + internal void SetPropertySerializer(string propertyName, Func serializer) + { + propertySerializers[propertyName] = o => serializer((T)o); + } + + internal void TrimStringProperty(string propertyName, int maxLength) + { + stringMaxLengths[propertyName] = maxLength; + } + + internal void ExcludeProperty(string propertyName) { - public string PrintToString(TOwner obj) + excludedProperties.Add(propertyName); + } + + public string PrintToString(TOwner obj) + { + var visited = new HashSet(); + return PrintToString(obj, 0, visited); + } + + private string PrintToString(object obj, int nestingLevel, HashSet visited) + { + if (obj == null) + return "null" + Environment.NewLine; + + var type = obj.GetType(); + + if (excludedTypes.Contains(type)) + return string.Empty; + + if (!visited.Add(obj)) { - return PrintToString(obj, 0); + if (typeof(IEnumerable).IsAssignableFrom(type)) + { + return type.Name + " circular dependency" + Environment.NewLine; + } + return obj + Environment.NewLine; } + + if (TryPrintSimple(type, obj, out var simpleResult)) + return simpleResult; - private string PrintToString(object obj, int nestingLevel) + if (typeof(IEnumerable).IsAssignableFrom(type)) + return PrintEnumerable((IEnumerable)obj, type, nestingLevel, visited); + + return PrintComplexObject(obj, type, nestingLevel, visited); + } + + private bool TryPrintSimple(Type type, object obj, out string result) + { + if (typeSerializers.TryGetValue(type, out var typeSerializer)) { - //TODO apply configurations - if (obj == null) - return "null" + Environment.NewLine; + result = typeSerializer(obj) + Environment.NewLine; + return true; + } - 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()) + if (FinalTypes.Contains(type)) + { + if (obj is IFormattable f && numericCultures.TryGetValue(type, out var culture)) + result = f.ToString(null, culture) + Environment.NewLine; + else + result = obj + Environment.NewLine; + + return true; + } + + result = null; + return false; + } + + private string PrintComplexObject(object obj, Type type, int nestingLevel, HashSet visited) + { + var ident = new string('\t', nestingLevel + 1); + var sb = new StringBuilder(); + sb.AppendLine(type.Name); + + foreach (var propertyInfo in type.GetProperties()) + { + var propName = propertyInfo.Name; + var propType = propertyInfo.PropertyType; + + if (excludedProperties.Contains(propName) || excludedTypes.Contains(propType)) + continue; + + var value = propertyInfo.GetValue(obj); + + if (TryPrintPropertyWithCustomOrTrim(sb, ident, propName, propType, value)) + continue; + + sb.Append(ident + propName + " = " + + PrintToString(value, nestingLevel + 1, visited)); + } + + return sb.ToString(); + } + + private bool TryPrintPropertyWithCustomOrTrim( + StringBuilder sb, + string ident, + string propName, + Type propType, + object value) + { + if (propertySerializers.TryGetValue(propName, out var propSerializer)) + { + sb.Append(ident + propName + " = " + + propSerializer(value) + Environment.NewLine); + return true; + } + + if (propType != typeof(string) || !stringMaxLengths.TryGetValue(propName, out var maxLength)) return false; + var s = (string)value; + if (s != null && s.Length > maxLength) + s = s[..maxLength]; + + sb.Append(ident + propName + " = " + s + Environment.NewLine); + return true; + + } + + private string PrintEnumerable(IEnumerable enumerable, Type type, int nestingLevel, HashSet visited) + { + var sb = new StringBuilder(); + var ident = new string('\t', nestingLevel + 1); + + sb.AppendLine(type.Name); + + foreach (var item in enumerable) + { + if (item == null) { - sb.Append(identation + propertyInfo.Name + " = " + - PrintToString(propertyInfo.GetValue(obj), - nestingLevel + 1)); + sb.Append(ident + "null" + Environment.NewLine); + continue; } - return sb.ToString(); + + var itemType = item.GetType(); + + if (excludedTypes.Contains(itemType)) + continue; + + var printed = PrintToString(item, nestingLevel + 1, visited); + sb.Append(ident + printed); } + + return sb.ToString(); + } + + public IPrintingConfigContext For() + { + return new BaseContext(this); + } + + public IPropertyPrinterConfigContext For(Expression> propertySelector) + { + var member = (MemberExpression)propertySelector.Body; + var propName = member.Member.Name; + return new PropertyContext(this, propName); + } + + public IStringPropertyPrinterConfigContext For(Expression> propertySelector) + { + var member = (MemberExpression)propertySelector.Body; + var propName = member.Member.Name; + return new StringPropertyContext(this, propName); } -} \ No newline at end of file +} diff --git a/ObjectPrinting/Tests/ObjectPrinterAcceptanceTests.cs b/ObjectPrinting/Tests/ObjectPrinterAcceptanceTests.cs index 4c8b2445c..dbe8e6049 100644 --- a/ObjectPrinting/Tests/ObjectPrinterAcceptanceTests.cs +++ b/ObjectPrinting/Tests/ObjectPrinterAcceptanceTests.cs @@ -1,27 +1,34 @@ -using NUnit.Framework; +using System; +using System.Globalization; +using FluentAssertions; +using NUnit.Framework; -namespace ObjectPrinting.Tests +namespace ObjectPrinting.Tests; + +[TestFixture] +public class ObjectPrinterAcceptanceTests { - [TestFixture] - public class ObjectPrinterAcceptanceTests + [Test] + public void Demo() { - [Test] - public void Demo() - { - var person = new Person { Name = "Alex", Age = 19 }; + var person = new Person { Name = "Alex", Age = 19 }; - var printer = ObjectPrinter.For(); - //1. Исключить из сериализации свойства определенного типа - //2. Указать альтернативный способ сериализации для определенного типа - //3. Для числовых типов указать культуру - //4. Настроить сериализацию конкретного свойства - //5. Настроить обрезание строковых свойств (метод должен быть виден только для строковых свойств) - //6. Исключить из сериализации конкретного свойства + var printer = ObjectPrinter.For() + .For().ExcludeType().End() + .For().SetSerializer(x=>$"int{x}int").End() + .For().UsingCulture(new CultureInfo("en-US")).End() + .For(x=>x.Id).SetSerializer(x=>$"id{x}").End() + .For(x=>x.Name).TrimToLength(2).End() + .For(x=>x.Description).ExcludeProperty().End(); + //1. Исключить из сериализации свойства определенного типа + //2. Указать альтернативный способ сериализации для определенного типа + //3. Для числовых типов указать культуру + //4. Настроить сериализацию конкретного свойства + //5. Настроить обрезание строковых свойств (метод должен быть виден только для строковых свойств) + //6. Исключить из сериализации конкретного свойства - string s1 = printer.PrintToString(person); - - //7. Синтаксический сахар в виде метода расширения, сериализующего по-умолчанию - //8. ...с конфигурированием - } + 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 index f95559554..788b3c7f2 100644 --- a/ObjectPrinting/Tests/Person.cs +++ b/ObjectPrinting/Tests/Person.cs @@ -1,12 +1,13 @@ using System; -namespace ObjectPrinting.Tests +namespace ObjectPrinting.Tests; + +public class Person { - public class Person - { - public Guid Id { get; set; } - public string Name { get; set; } - public double Height { get; set; } - public int Age { get; set; } - } + public Guid Id { get; set; } + public string Name { get; set; } + public int Age { get; set; } + public double Salary { get; set; } + public DateTime BirthDate { get; set; } + public string Description { get; set; } } \ No newline at end of file diff --git a/ObjectPrinting/Tests/PersonGroup.cs b/ObjectPrinting/Tests/PersonGroup.cs new file mode 100644 index 000000000..1ab2972da --- /dev/null +++ b/ObjectPrinting/Tests/PersonGroup.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; + +namespace ObjectPrinting.Tests; + +public class PersonGroup +{ + public string GroupName { get; set; } + public Person Leader { get; set; } + public Person[] Members { get; set; } + public int[] Scores { get; set; } + public List ScoresList { get; set; } + public Dictionary> ScoresDictionary { get; set; } +} \ No newline at end of file diff --git a/ObjectPrinting/Tests/PrintingConfigTests.cs b/ObjectPrinting/Tests/PrintingConfigTests.cs new file mode 100644 index 000000000..d283fb435 --- /dev/null +++ b/ObjectPrinting/Tests/PrintingConfigTests.cs @@ -0,0 +1,367 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using FluentAssertions; +using NUnit.Framework; + +namespace ObjectPrinting.Tests; + +[TestFixture] +public class PrintingConfigTests +{ + private Person CreatePerson(string name = "Alexander", int age = 30) + { + return new Person + { + Id = Guid.Empty, + Name = name, + Salary = 1234.5, + Age = age, + BirthDate = new DateTime(2000, 1, 2), + Description = "Some long description" + }; + } + + private PersonGroup CreateGroup() + { + return new PersonGroup + { + GroupName = "DevTeam", + Leader = CreatePerson("Leader", 30), + Members = + [ + CreatePerson("Alice", 25), + CreatePerson("Bob", 28) + ], + Scores = [322, 67, 35], + ScoresList = [3.0, 4.2, 6.1], + ScoresDictionary = new Dictionary> + { + ["User1"] = new() + { + ["Score"] = 10, + ["Level"] = 1 + }, + ["User2"] = new() + { + ["Score"] = 5, + ["Level"] = 2 + } + } + }; + } + + [Test] + public void ExcludeType_Remove_PropertiesType() + { + var person = CreatePerson(); + var config = new PrintingConfig(); + + config.For() + .ExcludeType() + .End(); + + var result = config.PrintToString(person); + + result.Should().NotContain("Age ="); + result.Should().NotContain("30"); + result.Should().Contain("Name = Alexander"); + result.Should().Contain("Salary = 1234,5"); + } + + [Test] + public void SetSerializer_SetSerialization_ForType() + { + var person = CreatePerson(); + var config = new PrintingConfig(); + + config.For() + .SetSerializer(i => $"str:{i}") + .End(); + + var result = config.PrintToString(person); + + result.Should().Contain("Name = str:Alexander"); + result.Should().Contain("Description = str:Some long description"); + } + + [Test] + public void UsingCulture_ChangeCulture_ForPropertiesType() + { + var person = CreatePerson(); + var config = new PrintingConfig(); + + config.For() + .UsingCulture(new CultureInfo("en-US")) + .End(); + + var result = config.PrintToString(person); + + result.Should().Contain("Salary = 1234.5"); + } + + [Test] + public void PropertySetSerializer_SetSerialization_ForProperty() + { + var person = CreatePerson(); + var config = new PrintingConfig(); + + config.For(p => p.Age) + .SetSerializer(a => $"AgeIs:{a}") + .End(); + + var result = config.PrintToString(person); + + result.Should().Contain("Age = AgeIs:30"); + result.Should().Contain("Description = Some long description"); + } + + [Test] + public void PropertyExclude_Remove_Properties() + { + var person = CreatePerson(); + var config = new PrintingConfig(); + + config.For(p => p.Description) + .ExcludeProperty() + .End(); + + var result = config.PrintToString(person); + + result.Should().NotContain("Description ="); + result.Should().Contain("Name = Alexander"); + } + + [Test] + public void StringTrimToLength_Trim_StringProperty() + { + var person = CreatePerson(); + var config = new PrintingConfig(); + + config.For(p => p.Name) + .TrimToLength(5) + .End(); + + var result = config.PrintToString(person); + + result.Should().Contain("Name = Alexa"); + result.Should().NotContain("Alexander"); + } + + [Test] public void StringTrimToLengthAndSetSerializerShould_BothApplied() + { + var person = CreatePerson(); + var config = new PrintingConfig(); + + config.For(p => p.Name) + .TrimToLength(5) + .SetSerializer(s => s.ToUpperInvariant()) + .End(); + + var result = config.PrintToString(person); + + result.Should().Contain("Name = ALEXA"); + } + + [Test] + public void ExcludeType_HavePriority_OverPropertySetSerializer() + { + var person = CreatePerson(); + var config = new PrintingConfig(); + + config.For(p => p.Age) + .SetSerializer(a => $"AgeIs:{a}") + .End(); + + config.For() + .ExcludeType() + .End(); + + var result = config.PrintToString(person); + + result.Should().NotContain("Age ="); + result.Should().NotContain("AgeIs:30"); + } + + [Test] + public void Collections_SerializeCorrectly() + { + var group = CreateGroup(); + var config = new PrintingConfig(); + + var result = config.PrintToString(group); + + result.Should().Contain("PersonGroup"); + result.Should().Contain("GroupName = DevTeam"); + + result.Should().Contain("Members = Person[]"); + result.Should().Contain("Name = Alice"); + result.Should().Contain("Name = Bob"); + + result.Should().Contain("Scores = Int32[]"); + result.Should().Contain("322"); + result.Should().Contain("67"); + result.Should().Contain("35"); + + result.Should().Contain("ScoresList = List`1"); + result.Should().Contain("3"); + result.Should().Contain("4,2"); + result.Should().Contain("6,1"); + + result.Should().Contain("ScoresDictionary = Dictionary`2"); + result.Should().Contain("Key = User1"); + result.Should().Contain("Score"); + result.Should().Contain("10"); + result.Should().Contain("Key = User2"); + result.Should().Contain("5"); + } + + [Test] + public void ExcludeType_WorkInsideCollections() + { + var group = CreateGroup(); + var config = new PrintingConfig(); + + config.For() + .ExcludeType() + .End(); + + var result = config.PrintToString(group); + + result.Should().NotContain("Age ="); + result.Should().Contain("Scores = Int32[]"); + result.Should().NotContain("322"); + result.Should().NotContain("67"); + result.Should().NotContain("35"); + } + + [Test] + public void PropertySetSerializer_Work_ForNestedProperty() + { + var group = CreateGroup(); + var config = new PrintingConfig(); + + config.For(g => g.Leader.Name) + .SetSerializer(s => $"LEADER:{s}") + .End(); + + var result = config.PrintToString(group); + + result.Should().Contain("Name = LEADER:Leader"); + result.Should().Contain("Name = LEADER:Alice"); + result.Should().Contain("Name = LEADER:Bob"); + } + + [Test] + public void TypeSetSerializer_Work_InsideCollections() + { + var group = CreateGroup(); + var config = new PrintingConfig(); + + config.For() + .SetSerializer(i => $"INT:{i}") + .End(); + + var result = config.PrintToString(group); + + result.Should().Contain("Age = INT:30"); + result.Should().Contain("Age = INT:25"); + result.Should().Contain("Age = INT:28"); + result.Should().Contain("Scores = Int32[]"); + result.Should().Contain("INT:322"); + result.Should().Contain("INT:67"); + result.Should().Contain("INT:35"); + result.Should().Contain("ScoresDictionary = Dictionary`2"); + result.Should().Contain("INT:10"); + result.Should().Contain("INT:1"); + result.Should().Contain("INT:5"); + result.Should().Contain("INT:2"); + } + + [Test] + public void UsingCulture_Work_InsideCollections() + { + var group = CreateGroup(); + var config = new PrintingConfig(); + + config.For() + .UsingCulture(new CultureInfo("en-US")) + .End(); + + var result = config.PrintToString(group); + + result.Should().Contain("Salary = 1234.5"); + result.Should().Contain("ScoresList = List`1"); + result.Should().Contain("3"); + result.Should().Contain("4.2"); + result.Should().Contain("6.1"); + } + + [Test] + public void StringTrimToLength_Work_InsideCollections() + { + var group = CreateGroup(); + var config = new PrintingConfig(); + + config.For(p => p.Leader.Name) + .TrimToLength(1) + .End(); + + var result = config.PrintToString(group); + + result.Should().Contain("Name = L"); + result.Should().Contain("Name = A"); + result.Should().Contain("Name = B"); + } + + [Test] + public void ExcludeCollections_NotSerializeCollectionsWithElements() + { + var group = CreateGroup(); + var config = new PrintingConfig(); + + config.For() + .ExcludeType() + .End(); + + var result = config.PrintToString(group); + + result.Should().NotContain("Scores = Int32[]"); + result.Should().NotContain("322"); + result.Should().NotContain("67"); + result.Should().NotContain("35"); + } + + [Test] + public void ExcludeProperty_Work_ForNestedProperty() + { + var group = CreateGroup(); + var config = new PrintingConfig(); + + config.For(x=>x.Leader.Age) + .ExcludeProperty() + .End(); + + var result = config.PrintToString(group); + + result.Should().NotContain("Age = 30"); + result.Should().NotContain("Age = 25"); + result.Should().NotContain("Age = 28"); + } + [Test] + public void CircularDependency_NotCauseStackOverflow() + { + var list = new List(); + list.Add("first"); + list.Add(list); + + var config = new PrintingConfig>(); + + var result = config.PrintToString(list); + + result.Should().Contain("List`1"); + result.Should().Contain("first"); + result.Should().Contain("List`1 circular dependency"); + } + +} \ No newline at end of file From e5178a9f2a6ea92951e2e526d92a81efee1607c4 Mon Sep 17 00:00:00 2001 From: Matvey Mednikov Date: Thu, 20 Nov 2025 14:48:29 +0500 Subject: [PATCH 2/2] =?UTF-8?q?=D0=A1=D0=B4=D0=B5=D0=BB=D0=B0=D0=BB=20?= =?UTF-8?q?=D0=BD=D0=B0=D1=81=D1=82=D1=80=D0=BE=D0=B9=D0=BA=D1=83=20=D0=BA?= =?UTF-8?q?=D0=BE=D0=BD=D0=BA=D1=80=D0=B5=D1=82=D0=BD=D0=BE=D0=B3=D0=BE=20?= =?UTF-8?q?=D1=81=D0=B2=D0=BE=D0=B9=D1=81=D1=82=D0=B2=D0=B0,=20=D0=B4?= =?UTF-8?q?=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D0=BB=20=D1=81=D0=B5=D1=80=D0=B8?= =?UTF-8?q?=D0=B0=D0=BB=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D1=8E=20=D0=BF=D0=BE?= =?UTF-8?q?=D0=BB=D0=B5=D0=B9,=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20=D0=BA=D1=80=D0=B0=D1=81=D0=B8=D0=B2=D1=83=D1=8E=20?= =?UTF-8?q?=D1=81=D0=B5=D1=80=D0=B8=D0=B0=D0=BB=D0=B8=D0=B7=D0=B0=D1=86?= =?UTF-8?q?=D0=B8=D1=8E=20=D1=81=D0=BB=D0=BE=D0=B2=D0=B0=D1=80=D1=8F,=20?= =?UTF-8?q?=D1=81=D0=B4=D0=B5=D0=BB=D0=B0=D0=BB=20=D0=BF=D1=80=D0=BE=D0=B2?= =?UTF-8?q?=D0=B5=D1=80=D0=BA=D1=83=20=D0=BD=D0=B0=20=D1=86=D0=B8=D0=BA?= =?UTF-8?q?=D0=BB=D0=B8=D1=87=D0=BD=D0=BE=D1=81=D1=82=D1=8C=20=D0=B1=D0=B5?= =?UTF-8?q?=D0=B7=D0=BE=D0=BF=D0=B0=D1=81=D1=82=D0=BD=D0=BE=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ObjectPrinting/PrintingConfig.cs | 147 ++++++++++++++------ ObjectPrinting/Tests/PersonGroup.cs | 1 + ObjectPrinting/Tests/PrintingConfigTests.cs | 33 ++++- 3 files changed, 129 insertions(+), 52 deletions(-) diff --git a/ObjectPrinting/PrintingConfig.cs b/ObjectPrinting/PrintingConfig.cs index 13c710124..885e0a8ff 100644 --- a/ObjectPrinting/PrintingConfig.cs +++ b/ObjectPrinting/PrintingConfig.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; +using System.Reflection; using System.Text; using ObjectPrinting.Context; @@ -23,7 +24,7 @@ public class PrintingConfig private readonly Dictionary> propertySerializers = new(); private readonly Dictionary stringMaxLengths = new(); private readonly HashSet excludedProperties = []; - + internal void SetTypeSerializer(Func serializer) { typeSerializers[typeof(T)] = o => serializer((T)o); @@ -56,11 +57,11 @@ internal void ExcludeProperty(string propertyName) public string PrintToString(TOwner obj) { - var visited = new HashSet(); - return PrintToString(obj, 0, visited); + var visited = new HashSet(ReferenceEqualityComparer.Instance); + return PrintToString(obj, 0, visited, ""); } - private string PrintToString(object obj, int nestingLevel, HashSet visited) + private string PrintToString(object obj, int nestingLevel, HashSet visited, string currentPath) { if (obj == null) return "null" + Environment.NewLine; @@ -69,25 +70,43 @@ private string PrintToString(object obj, int nestingLevel, HashSet visit if (excludedTypes.Contains(type)) return string.Empty; + + if (TryPrintSimple(type, obj, out var simple)) + return simple; if (!visited.Add(obj)) - { - if (typeof(IEnumerable).IsAssignableFrom(type)) - { - return type.Name + " circular dependency" + Environment.NewLine; - } - return obj + Environment.NewLine; - } + return "circular dependency" + Environment.NewLine; + + if (obj is IDictionary dict) + return PrintDictionary(dict, type, nestingLevel, visited, currentPath); - if (TryPrintSimple(type, obj, out var simpleResult)) - return simpleResult; - if (typeof(IEnumerable).IsAssignableFrom(type)) - return PrintEnumerable((IEnumerable)obj, type, nestingLevel, visited); + return PrintEnumerable((IEnumerable)obj, type, nestingLevel, visited, currentPath); + + return PrintComplexObject(obj, type, nestingLevel, visited, currentPath); + } + + private string PrintDictionary(IDictionary dict, Type type, int nestingLevel, HashSet visited, string currentPath) + { + var sb = new StringBuilder(); + var ident = new string('\t', nestingLevel + 1); + sb.AppendLine(type.Name); + + foreach (DictionaryEntry entry in dict) + { + var keyPath = currentPath + "[key]"; + var valuePath = currentPath + "[value]"; + + var keyPrinted = PrintToString(entry.Key, nestingLevel + 1, visited, keyPath).TrimEnd('\r', '\n'); + var valuePrinted = PrintToString(entry.Value, nestingLevel + 1, visited, valuePath).TrimEnd('\r', '\n'); + + sb.AppendLine($"{ident}Key = {keyPrinted}, Value = {valuePrinted}"); + } - return PrintComplexObject(obj, type, nestingLevel, visited); + return sb.ToString(); } + private bool TryPrintSimple(Type type, object obj, out string result) { if (typeSerializers.TryGetValue(type, out var typeSerializer)) @@ -110,77 +129,102 @@ private bool TryPrintSimple(Type type, object obj, out string result) return false; } - private string PrintComplexObject(object obj, Type type, int nestingLevel, HashSet visited) + private string PrintComplexObject(object obj, Type type, int nestingLevel, HashSet visited, string currentPath) { var ident = new string('\t', nestingLevel + 1); var sb = new StringBuilder(); sb.AppendLine(type.Name); - foreach (var propertyInfo in type.GetProperties()) + var flags = BindingFlags.Public | BindingFlags.Instance; + + foreach (var propertyInfo in type.GetProperties(flags)) { var propName = propertyInfo.Name; var propType = propertyInfo.PropertyType; - if (excludedProperties.Contains(propName) || excludedTypes.Contains(propType)) + var fullPropPath = string.IsNullOrEmpty(currentPath) + ? propName + : currentPath + "." + propName; + + if (excludedProperties.Contains(fullPropPath) || excludedTypes.Contains(propType)) continue; var value = propertyInfo.GetValue(obj); - if (TryPrintPropertyWithCustomOrTrim(sb, ident, propName, propType, value)) + if (TryPrintPropertyWithCustomOrTrim(sb, ident, fullPropPath, propType, value)) continue; sb.Append(ident + propName + " = " + - PrintToString(value, nestingLevel + 1, visited)); + PrintToString(value, nestingLevel + 1, visited, fullPropPath)); + } + + foreach (var fieldInfo in type.GetFields(flags)) + { + var fieldName = fieldInfo.Name; + var fieldType = fieldInfo.FieldType; + + var fullFieldPath = string.IsNullOrEmpty(currentPath) + ? fieldName + : currentPath + "." + fieldName; + + if (excludedProperties.Contains(fullFieldPath) || excludedTypes.Contains(fieldType)) + continue; + + var value = fieldInfo.GetValue(obj); + + if (TryPrintPropertyWithCustomOrTrim(sb, ident, fullFieldPath, fieldType, value)) + continue; + + sb.Append(ident + fieldName + " = " + + PrintToString(value, nestingLevel + 1, visited, fullFieldPath)); } return sb.ToString(); } - private bool TryPrintPropertyWithCustomOrTrim( - StringBuilder sb, - string ident, - string propName, - Type propType, - object value) - { - if (propertySerializers.TryGetValue(propName, out var propSerializer)) + private bool TryPrintPropertyWithCustomOrTrim(StringBuilder sb, string ident, string fullPropPath, Type propType, object value) + { + if (propertySerializers.TryGetValue(fullPropPath, out var propSerializer)) { - sb.Append(ident + propName + " = " + + sb.Append(ident + fullPropPath + " = " + propSerializer(value) + Environment.NewLine); return true; } - if (propType != typeof(string) || !stringMaxLengths.TryGetValue(propName, out var maxLength)) return false; + if (propType != typeof(string) || !stringMaxLengths.TryGetValue(fullPropPath, out var maxLength)) + return false; + var s = (string)value; if (s != null && s.Length > maxLength) s = s[..maxLength]; - sb.Append(ident + propName + " = " + s + Environment.NewLine); + sb.Append(ident + fullPropPath + " = " + s + Environment.NewLine); return true; - } - - private string PrintEnumerable(IEnumerable enumerable, Type type, int nestingLevel, HashSet visited) + + private string PrintEnumerable(IEnumerable enumerable, Type type, int nestingLevel, HashSet visited, string currentPath) { var sb = new StringBuilder(); var ident = new string('\t', nestingLevel + 1); - sb.AppendLine(type.Name); + var index = 0; foreach (var item in enumerable) { + var itemPath = currentPath + "[" + index + "]"; + index++; + if (item == null) { sb.Append(ident + "null" + Environment.NewLine); continue; } - var itemType = item.GetType(); if (excludedTypes.Contains(itemType)) continue; - var printed = PrintToString(item, nestingLevel + 1, visited); + var printed = PrintToString(item, nestingLevel + 1, visited, itemPath); sb.Append(ident + printed); } @@ -191,18 +235,31 @@ public IPrintingConfigContext For() { return new BaseContext(this); } - + public IPropertyPrinterConfigContext For(Expression> propertySelector) { - var member = (MemberExpression)propertySelector.Body; - var propName = member.Member.Name; - return new PropertyContext(this, propName); + var propPath = GetPropertyPath(propertySelector); + return new PropertyContext(this, propPath); } public IStringPropertyPrinterConfigContext For(Expression> propertySelector) { - var member = (MemberExpression)propertySelector.Body; - var propName = member.Member.Name; - return new StringPropertyContext(this, propName); + var propPath = GetPropertyPath(propertySelector); + return new StringPropertyContext(this, propPath); + } + + private static string GetPropertyPath(LambdaExpression expression) + { + var parts = new List(); + var current = expression.Body; + + while (current is MemberExpression m) + { + parts.Add(m.Member.Name); + current = m.Expression; + } + + parts.Reverse(); + return string.Join(".", parts); } } diff --git a/ObjectPrinting/Tests/PersonGroup.cs b/ObjectPrinting/Tests/PersonGroup.cs index 1ab2972da..820ac72ae 100644 --- a/ObjectPrinting/Tests/PersonGroup.cs +++ b/ObjectPrinting/Tests/PersonGroup.cs @@ -9,5 +9,6 @@ public class PersonGroup public Person[] Members { get; set; } public int[] Scores { get; set; } public List ScoresList { get; set; } + public List> ScoresLists { get; set; } public Dictionary> ScoresDictionary { get; set; } } \ No newline at end of file diff --git a/ObjectPrinting/Tests/PrintingConfigTests.cs b/ObjectPrinting/Tests/PrintingConfigTests.cs index d283fb435..c4a0388e2 100644 --- a/ObjectPrinting/Tests/PrintingConfigTests.cs +++ b/ObjectPrinting/Tests/PrintingConfigTests.cs @@ -35,6 +35,7 @@ private PersonGroup CreateGroup() ], Scores = [322, 67, 35], ScoresList = [3.0, 4.2, 6.1], + ScoresLists = [[3.0, 4.2, 6.1], [3.0, 4.2, 6.1]], ScoresDictionary = new Dictionary> { ["User1"] = new() @@ -214,6 +215,8 @@ public void Collections_SerializeCorrectly() result.Should().Contain("10"); result.Should().Contain("Key = User2"); result.Should().Contain("5"); + result.Should().Contain("Key = User1, Value = Dictionary`2"); + result.Should().Contain("Key = Score, Value = 10"); } [Test] @@ -248,8 +251,8 @@ public void PropertySetSerializer_Work_ForNestedProperty() var result = config.PrintToString(group); result.Should().Contain("Name = LEADER:Leader"); - result.Should().Contain("Name = LEADER:Alice"); - result.Should().Contain("Name = LEADER:Bob"); + result.Should().Contain("Name = Alice"); + result.Should().Contain("Name = Bob"); } [Test] @@ -343,10 +346,10 @@ public void ExcludeProperty_Work_ForNestedProperty() .End(); var result = config.PrintToString(group); - + result.Should().NotContain("Age = 30"); - result.Should().NotContain("Age = 25"); - result.Should().NotContain("Age = 28"); + result.Should().Contain("Age = 25"); + result.Should().Contain("Age = 28"); } [Test] public void CircularDependency_NotCauseStackOverflow() @@ -356,12 +359,28 @@ public void CircularDependency_NotCauseStackOverflow() list.Add(list); var config = new PrintingConfig>(); - var result = config.PrintToString(list); result.Should().Contain("List`1"); result.Should().Contain("first"); - result.Should().Contain("List`1 circular dependency"); + result.Should().Contain("circular dependency"); } + + [Test] + public void CollectionTypeSetSerializer_Work_ForConcreteCollectionType() + { + var group = CreateGroup(); + var config = new PrintingConfig(); + + config.For>() + .SetSerializer(list => + $"List (Count = {list.Count}): [{string.Join(", ", list)}]") + .End(); + var result = config.PrintToString(group); + + result.Should().Contain("ScoresList = List (Count = 3): [3, 4,2, 6,1]"); + result.Should().Contain("ScoresLists = List`1"); + result.Should().Contain("List (Count = 3): [3, 4,2, 6,1]"); + } } \ No newline at end of file