From ddd8f3c2aa641645e23ada0edd722c019d74f071 Mon Sep 17 00:00:00 2001 From: PavelMartinelli Date: Sat, 15 Nov 2025 19:04:51 +0500 Subject: [PATCH 1/3] Made ObjectPrinting --- ObjectPrinting/ObjectExtensions.cs | 16 + ObjectPrinting/ObjectPrinting.csproj | 1 + ObjectPrinting/PrintingConfig.cs | 293 ++++++++++++++++-- ObjectPrinting/PropertySerializingConfig.cs | 21 ++ .../StringPropertySerializingConfig.cs | 13 + .../Tests/ObjectPrinterAcceptanceTests.cs | 23 +- ...jectPrinterCollectionSerializationTests.cs | 98 ++++++ ...tPrinterConfigurationSerializationTests.cs | 152 +++++++++ .../Tests/ObjectPrinterEdgeCasesTests.cs | 80 +++++ ...ObjectPrinterStandartSerializationTests.cs | 118 +++++++ .../Tests/ObjectPrinterTestBaseClass.cs | 60 ++++ .../Tests/ObjectPrinterTestDataClasses.cs | 28 ++ ObjectPrinting/Tests/Person.cs | 12 - ObjectPrinting/TypeSerializingConfig.cs | 26 ++ 14 files changed, 899 insertions(+), 42 deletions(-) create mode 100644 ObjectPrinting/ObjectExtensions.cs create mode 100644 ObjectPrinting/PropertySerializingConfig.cs create mode 100644 ObjectPrinting/StringPropertySerializingConfig.cs create mode 100644 ObjectPrinting/Tests/ObjectPrinterCollectionSerializationTests.cs create mode 100644 ObjectPrinting/Tests/ObjectPrinterConfigurationSerializationTests.cs create mode 100644 ObjectPrinting/Tests/ObjectPrinterEdgeCasesTests.cs create mode 100644 ObjectPrinting/Tests/ObjectPrinterStandartSerializationTests.cs create mode 100644 ObjectPrinting/Tests/ObjectPrinterTestBaseClass.cs create mode 100644 ObjectPrinting/Tests/ObjectPrinterTestDataClasses.cs delete mode 100644 ObjectPrinting/Tests/Person.cs create mode 100644 ObjectPrinting/TypeSerializingConfig.cs diff --git a/ObjectPrinting/ObjectExtensions.cs b/ObjectPrinting/ObjectExtensions.cs new file mode 100644 index 000000000..e050404b4 --- /dev/null +++ b/ObjectPrinting/ObjectExtensions.cs @@ -0,0 +1,16 @@ +using System; + +namespace ObjectPrinting; + +public static class ObjectPrinterExtensions +{ + public static string PrintToString(this T obj) + { + return ObjectPrinter.For().PrintToString(obj); + } + + public static string PrintToString(this T obj, Func, PrintingConfig> config) + { + return config(ObjectPrinter.For()).PrintToString(obj); + } +} \ No newline at end of file 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/ObjectPrinting/PrintingConfig.cs b/ObjectPrinting/PrintingConfig.cs index a9e082117..e129016eb 100644 --- a/ObjectPrinting/PrintingConfig.cs +++ b/ObjectPrinting/PrintingConfig.cs @@ -1,41 +1,286 @@ using System; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; using System.Linq; +using System.Linq.Expressions; +using System.Reflection; using System.Text; -namespace ObjectPrinting +namespace ObjectPrinting; + +public class PrintingConfig { - public class PrintingConfig + private readonly Dictionary typeSerializers = new(); + private readonly Dictionary typeCultures = new(); + private readonly Dictionary propertySerializers = new(); + private readonly Dictionary propertyTrimLengths = new(); + private readonly HashSet excludedProperties = []; + private readonly HashSet excludedTypes = []; + private readonly HashSet visitedObjects = []; + + public PrintingConfig ExcludeType() + { + excludedTypes.Add(typeof(T)); + return this; + } + + public TypeSerializingConfig SerializeType() + { + return new TypeSerializingConfig(this); + } + + public PropertySerializingConfig SerializeProperty( + Expression> propertySelector) + { + var propertyName = GetPropertyName(propertySelector); + return new PropertySerializingConfig(this, + propertyName); + } + + public StringPropertySerializingConfig SerializeProperty( + Expression> propertySelector) + { + var propertyName = GetPropertyName(propertySelector); + return new StringPropertySerializingConfig(this, propertyName); + } + + public PrintingConfig ExcludeProperty( + Expression> propertySelector) + { + var propertyName = GetPropertyName(propertySelector); + excludedProperties.Add(propertyName); + return this; + } + + public string PrintToString(TOwner obj) + { + visitedObjects.Clear(); + return PrintToString(obj, 0); + } + + private string PrintToString(object obj, int nestingLevel) + { + if (obj == null) + return "null" + Environment.NewLine; + + var type = obj.GetType(); + + if (visitedObjects.Contains(obj)) + return $"Cyclic reference detected ({type.Name})" + Environment.NewLine; + + visitedObjects.Add(obj); + + try + { + if (IsFinalType(type)) + return SerializeValue(obj, type, null) + Environment.NewLine; + + if (obj is IEnumerable enumerable && !(obj is string)) + return SerializeCollection(enumerable, nestingLevel); + + return SerializeObject(obj, nestingLevel, type); + } + finally + { + visitedObjects.Remove(obj); + } + } + + private string SerializeCollection(IEnumerable collection, + int nestingLevel) + { + var indentation = new string('\t', nestingLevel + 1); + var sb = new StringBuilder(); + var type = collection.GetType(); + + var collectionTypeName = type.IsGenericType ? GetGenericTypeName(type) : type.Name; + sb.AppendLine(collectionTypeName); + + var index = 0; + foreach (var item in collection) + { + var serializedItem = SerializeValue(item, + item?.GetType() ?? typeof(object), null, nestingLevel + 1); + + sb.Append($"{indentation}[{index}] = {serializedItem}"); + index++; + } + + if (collection is Array || collection is not ICollection coll) + return sb.ToString(); + + sb.Append($"{indentation}Count = {coll.Count}" + Environment.NewLine); + + return sb.ToString(); + } + + private string GetGenericTypeName(Type type) + { + var genericArgs = type.GetGenericArguments(); + var genericTypeName = type.GetGenericTypeDefinition().Name; + var cleanName = genericTypeName[..genericTypeName.IndexOf('`')]; + var args = string.Join(", ", genericArgs.Select(t => t.Name)); + return $"{cleanName}`{genericArgs.Length}<{args}>"; + } + + + private string SerializeObject(object obj, int nestingLevel, Type type) + { + var indentation = new string('\t', nestingLevel + 1); + var sb = new StringBuilder(); + sb.AppendLine(type.Name); + + foreach (var fieldInfo in type.GetFields(BindingFlags.Public | BindingFlags.Instance)) + { + if (ShouldExcludeMember(fieldInfo.FieldType, fieldInfo.Name)) + continue; + + var value = fieldInfo.GetValue(obj); + var serializedValue = SerializeValue(value, fieldInfo.FieldType, + fieldInfo.Name, nestingLevel); + + sb.Append(indentation + fieldInfo.Name + " = " + serializedValue); + } + + foreach (var propertyInfo in type.GetProperties(BindingFlags.Public | BindingFlags.Instance)) + { + if (propertyInfo.GetIndexParameters().Length > 0) + continue; + + if (ShouldExcludeMember(propertyInfo.PropertyType, + propertyInfo.Name)) + continue; + + var value = propertyInfo.GetValue(obj); + var serializedValue = SerializeValue(value, propertyInfo.PropertyType, propertyInfo.Name, nestingLevel); + sb.Append(indentation + propertyInfo.Name + " = " + serializedValue); + } + + return sb.ToString(); + } + + private string SerializeValue(object value, Type valueType, + string memberName, int nestingLevel = 0) { - public string PrintToString(TOwner obj) + if (value == null) + return "null" + Environment.NewLine; + + if (memberName != null && propertySerializers.TryGetValue(memberName, out var propertySerializer)) { - return PrintToString(obj, 0); + try + { + var result = propertySerializer.DynamicInvoke(value); + return ProcessStringResult(result, memberName); + } + catch + { + // Fall back to default serialization + } } - private string PrintToString(object obj, int nestingLevel) + if (memberName != null && propertyTrimLengths.TryGetValue(memberName, out var trimLength) && value is string str) { - //TODO apply configurations - if (obj == null) - return "null" + Environment.NewLine; + return (str.Length <= trimLength + ? str + : str.Substring(0, trimLength)) + Environment.NewLine; + } - var finalTypes = new[] + if (typeSerializers.TryGetValue(valueType, out var typeSerializer)) + { + try { - 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()) + var result = typeSerializer.DynamicInvoke(value); + return ProcessStringResult(result, memberName); + } + catch { - sb.Append(identation + propertyInfo.Name + " = " + - PrintToString(propertyInfo.GetValue(obj), - nestingLevel + 1)); + // Fall back to default serialization } - return sb.ToString(); } + + if (typeCultures.TryGetValue(valueType, out var culture) && value is IFormattable formattable) + { + return formattable.ToString(null, culture) + Environment.NewLine; + } + + if (!IsFinalType(valueType)) + return PrintToString(value, nestingLevel + 1); + + if (value is IFormattable formattableDefault) + return formattableDefault.ToString(null, CultureInfo.InvariantCulture) + Environment.NewLine; + + return value.ToString() + Environment.NewLine; + } + + private string ProcessStringResult(object result, string memberName) + { + if (result == null) + return "null" + Environment.NewLine; + + var resultString = result.ToString(); + + if (memberName != null && propertyTrimLengths.TryGetValue(memberName, out var trimLength)) + { + resultString = resultString.Length <= trimLength + ? resultString + : resultString.Substring(0, trimLength); + } + + return resultString + Environment.NewLine; + } + + private bool ShouldExcludeMember(Type memberType, string memberName) + { + return excludedTypes.Contains(memberType) || excludedProperties.Contains(memberName); + } + + private bool IsFinalType(Type type) + { + var finalTypes = new[] + { + typeof(int), typeof(double), typeof(float), typeof(string), + typeof(DateTime), typeof(TimeSpan), typeof(Guid), typeof(decimal), + typeof(long), typeof(short), typeof(byte), typeof(bool), + typeof(char), typeof(sbyte), typeof(ushort), typeof(uint), + typeof(ulong) + }; + + return finalTypes.Contains(type) || type.IsEnum; + } + + private string GetPropertyName( + Expression> propertySelector) + { + switch (propertySelector.Body) + { + case MemberExpression memberExpression: + return memberExpression.Member.Name; + case UnaryExpression { Operand: MemberExpression unaryMemberExpression }: + return unaryMemberExpression.Member.Name; + default: + throw new ArgumentException("Expression must be a property or field selector"); + } + } + + internal void AddTypeSerializer(Func serializeFunc) + { + typeSerializers[typeof(TType)] = serializeFunc; + } + + internal void AddTypeCulture(CultureInfo cultureInfo) + { + typeCultures[typeof(TType)] = cultureInfo; + } + + internal void AddPropertySerializer(string propertyName, + Func serializer) + { + propertySerializers[propertyName] = serializer; + } + + internal void AddPropertyTrim(string propertyName, int maxLength) + { + propertyTrimLengths[propertyName] = maxLength; } } \ No newline at end of file diff --git a/ObjectPrinting/PropertySerializingConfig.cs b/ObjectPrinting/PropertySerializingConfig.cs new file mode 100644 index 000000000..aac8f6863 --- /dev/null +++ b/ObjectPrinting/PropertySerializingConfig.cs @@ -0,0 +1,21 @@ +using System; + +namespace ObjectPrinting; + +public class PropertySerializingConfig +{ + protected readonly PrintingConfig printingConfig; + protected readonly string propertyName; + + public PropertySerializingConfig(PrintingConfig printingConfig, string propertyName) + { + this.printingConfig = printingConfig; + this.propertyName = propertyName; + } + + public PrintingConfig Use(Func serializer) + { + printingConfig.AddPropertySerializer(propertyName, serializer); + return printingConfig; + } +} \ No newline at end of file diff --git a/ObjectPrinting/StringPropertySerializingConfig.cs b/ObjectPrinting/StringPropertySerializingConfig.cs new file mode 100644 index 000000000..3959cdca8 --- /dev/null +++ b/ObjectPrinting/StringPropertySerializingConfig.cs @@ -0,0 +1,13 @@ +namespace ObjectPrinting; + +public class StringPropertySerializingConfig : PropertySerializingConfig +{ + public StringPropertySerializingConfig(PrintingConfig printingConfig, string propertyName) + : base(printingConfig, propertyName) { } + + public PrintingConfig TrimTo(int maxLength) + { + printingConfig.AddPropertyTrim(propertyName, maxLength); + return printingConfig; + } +} \ No newline at end of file diff --git a/ObjectPrinting/Tests/ObjectPrinterAcceptanceTests.cs b/ObjectPrinting/Tests/ObjectPrinterAcceptanceTests.cs index 4c8b2445c..1c3ec79d4 100644 --- a/ObjectPrinting/Tests/ObjectPrinterAcceptanceTests.cs +++ b/ObjectPrinting/Tests/ObjectPrinterAcceptanceTests.cs @@ -1,4 +1,6 @@ -using NUnit.Framework; +using System; +using System.Globalization; +using NUnit.Framework; namespace ObjectPrinting.Tests { @@ -8,20 +10,29 @@ public class ObjectPrinterAcceptanceTests [Test] public void Demo() { - var person = new Person { Name = "Alex", Age = 19 }; + var person = new Person {Id = Guid.NewGuid(), Name = "Alex", Age = 19, Height = 180}; - var printer = ObjectPrinter.For(); + var printer = ObjectPrinter.For() //1. Исключить из сериализации свойства определенного типа + .ExcludeType() //2. Указать альтернативный способ сериализации для определенного типа + .SerializeType().Use(prop => "Int Number: " + prop.ToString()) //3. Для числовых типов указать культуру + .SerializeType().Use(CultureInfo.InvariantCulture) //4. Настроить сериализацию конкретного свойства + .SerializeProperty(obj => obj.Height).Use(prop => "Person Height: " + prop.ToString()) //5. Настроить обрезание строковых свойств (метод должен быть виден только для строковых свойств) + .SerializeProperty(obj => obj.Name).TrimTo(1) //6. Исключить из сериализации конкретного свойства + .ExcludeProperty(obj => obj.Age); - string s1 = printer.PrintToString(person); - - //7. Синтаксический сахар в виде метода расширения, сериализующего по-умолчанию + var s1 = printer.PrintToString(person); + //7. Синтаксический сахар в виде метода расширения, сериализующего по-умолчанию + var s2 = person.PrintToString(); //8. ...с конфигурированием + var s3 = person.PrintToString(config => + config.ExcludeType() + .SerializeProperty(p => p.Name).TrimTo(2)); } } } \ No newline at end of file diff --git a/ObjectPrinting/Tests/ObjectPrinterCollectionSerializationTests.cs b/ObjectPrinting/Tests/ObjectPrinterCollectionSerializationTests.cs new file mode 100644 index 000000000..1f9c119bd --- /dev/null +++ b/ObjectPrinting/Tests/ObjectPrinterCollectionSerializationTests.cs @@ -0,0 +1,98 @@ +using System.Collections.Generic; +using FluentAssertions; +using NUnit.Framework; + +namespace ObjectPrinting.Tests; + +[TestFixture] +public class ObjectPrinterCollectionSerializationTests : TestBase +{ + [Test] + public void PrintToString_WithList_SerializesListElements() + { + var list = CreateTestPeopleList(); + var result = list.PrintToString(); + + result.Should() + .Contain("List`1") + .And.Contain("[0] = Person") + .And.Contain("[1] = Person") + .And.Contain("Name = First") + .And.Contain("Name = Second") + .And.Contain("Count = 2"); + } + + [Test] + public void PrintToString_WithArray_SerializesArrayElements() + { + var array = CreateTestStringArray(); + var result = array.PrintToString(); + + result.Should() + .Contain("String[]") + .And.Contain("[0] = apple") + .And.Contain("[1] = banana") + .And.Contain("[2] = cherry"); + } + + [Test] + public void PrintToString_WithDictionary_SerializesKeyValuePairs() + { + var dict = CreateTestDictionary(); + var result = dict.PrintToString(); + + result.Should() + .Contain("Dictionary`2") + .And.Contain("Key = first") + .And.Contain("Value = 1") + .And.Contain("Key = second") + .And.Contain("Value = 2") + .And.Contain("Key = third") + .And.Contain("Value = 3") + .And.Contain("Count = 3"); + } + + + [Test] + public void PrintToString_WithEmptyCollection_ShowsEmptyCollection() + { + var result = new List().PrintToString(); + + result.Should() + .Contain("List`1") + .And.Contain("Count = 0"); + } + + [Test] + public void PrintToString_WithNestedCollections_SerializesRecursively() + { + var matrix = new List> + { + new List { 1, 2, 3 }, + new List { 4, 5, 6 } + }; + + var result = matrix.PrintToString(); + + result.Should() + .Contain("List`1") + .And.Contain("[0] = List`1") + .And.Contain("[1] = List`1") + .And.Contain("[0] = 1") + .And.Contain("[1] = 2") + .And.Contain("[2] = 3"); + } + + [Test] + public void + PrintToString_WithCollectionContainingNull_HandlesNullElements() + { + var listWithNull = new List { "first", null, "third" }; + var result = listWithNull.PrintToString(); + + result.Should() + .Contain("[0] = first") + .And.Contain("[1] = null") + .And.Contain("[2] = third"); + } +} \ No newline at end of file diff --git a/ObjectPrinting/Tests/ObjectPrinterConfigurationSerializationTests.cs b/ObjectPrinting/Tests/ObjectPrinterConfigurationSerializationTests.cs new file mode 100644 index 000000000..6d6b9025f --- /dev/null +++ b/ObjectPrinting/Tests/ObjectPrinterConfigurationSerializationTests.cs @@ -0,0 +1,152 @@ +using System; +using System.Globalization; +using FluentAssertions; +using NUnit.Framework; + +namespace ObjectPrinting.Tests; + +[TestFixture] +public class ObjectPrinterConfigurationSerializationTests : TestBase +{ + [Test] + public void PrintToString_WithExcludeType_ExcludesPropertiesOfType() + { + var printer = ObjectPrinter.For().ExcludeType(); + var result = printer.PrintToString(TestPerson); + + result.Should().NotContain("Id = ").And.Contain("Name = John Doe"); + } + + [Test] + public void PrintToString_WithExcludeProperty_ExcludesSpecificProperty() + { + var printer = ObjectPrinter.For().ExcludeProperty(p => p.Age); + var result = printer.PrintToString(TestPerson); + + result.Should().NotContain("Age = 30").And.Contain("Name = John Doe"); + } + + [Test] + public void PrintToString_WithTypeSerializer_UsesCustomSerialization() + { + var printer = ObjectPrinter.For().SerializeType() + .Use(i => $"Integer: {i}"); + var result = printer.PrintToString(TestPerson); + + result.Should().Contain("Age = Integer: 30"); + } + + [Test] + public void PrintToString_WithCulture_UsesSpecifiedCulture() + { + var person = new Person { Height = 180.5 }; + var japaneseCulture = CultureInfo.GetCultureInfo("ja-JP"); + var printer = ObjectPrinter.For().SerializeType().Use(japaneseCulture); + var result = printer.PrintToString(person); + + result.Should().Contain("180.5"); + } + + [Test] + public void + PrintToString_WithPropertySerializer_UsesCustomSerializationForProperty() + { + var printer = ObjectPrinter.For() + .SerializeProperty(p => p.Name) + .Use(name => $"Name: {name.ToUpper()}"); + var result = printer.PrintToString(TestPerson); + + result.Should().Contain("Name = Name: JOHN DOE"); + } + + [Test] + public void PrintToString_WithStringPropertyTrim_TrimsStringProperty() + { + var printer = ObjectPrinter.For() + .SerializeProperty(p => p.Name).TrimTo(4); + var result = printer.PrintToString(TestPerson); + + result.Should().Contain("Name = John"); + } + + [Test] + public void + PrintToString_WithMultipleConfigurations_AppliesAllConfigurations() + { + var printer = ObjectPrinter.For() + .ExcludeType() + .ExcludeProperty(p => p.Email) + .SerializeType().Use(i => $"{i} years") + .SerializeProperty(p => p.Name).TrimTo(4); + + var result = printer.PrintToString(TestPerson); + + result.Should() + .NotContain("Id = ") + .And.NotContain("Email = ") + .And.Contain("Age = 30 years") + .And.Contain("Name = John"); + } + + [Test] + public void PrintToString_WithCustomTypeSerializer_PriorityOverCulture() + { + var printer = ObjectPrinter.For().SerializeType() + .Use(d => $"Height: {d} cm"); + var result = printer.PrintToString(TestPerson); + + result.Should().Contain("Height: 180,5 cm"); + } + + [Test] + public void + PrintToString_WithPropertySerializer_PriorityOverTypeSerializer() + { + var printer = ObjectPrinter.For() + .SerializeType().Use(s => s.ToUpper()) + .SerializeProperty(p => p.Name).Use(name => $"Mr. {name}"); + + var result = printer.PrintToString(TestPerson); + result.Should().Contain("Name = Mr. John Doe"); + } + + [Test] + public void PrintToString_WithInvariantCulture_FormatsNumbersConsistently() + { + var printer = ObjectPrinter.For().SerializeType() + .Use(CultureInfo.InvariantCulture); + var result = printer.PrintToString(TestPerson); + + result.Should().Contain("180.5"); + } + + [Test] + public void PrintingConfig_ExcludeMultipleTypes_WorksCorrectly() + { + var config = ObjectPrinter.For() + .ExcludeType() + .ExcludeType(); + + var person = new Person + { Id = Guid.NewGuid(), Name = "Test", Age = 25 }; + var result = config.PrintToString(person); + + result.Should() + .NotContain("Id = ") + .And.NotContain("Name = ") + .And.Contain("Age = 25"); + } + + [Test] + public void PrintingConfig_ChainedConfiguration_ReturnsCorrectType() + { + var config = ObjectPrinter.For() + .ExcludeType() + .SerializeType().Use(i => i.ToString()) + .SerializeProperty(p => p.Name).Use(n => n) + .ExcludeProperty(p => p.Age); + + config.Should().BeOfType>(); + } + +} \ No newline at end of file diff --git a/ObjectPrinting/Tests/ObjectPrinterEdgeCasesTests.cs b/ObjectPrinting/Tests/ObjectPrinterEdgeCasesTests.cs new file mode 100644 index 000000000..9590cf416 --- /dev/null +++ b/ObjectPrinting/Tests/ObjectPrinterEdgeCasesTests.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections.Generic; +using FluentAssertions; +using NUnit.Framework; + +namespace ObjectPrinting.Tests; + +[TestFixture] +public class ObjectPrinterEdgeCasesTests : TestBase +{ + [Test] + public void PrintToString_WithCyclicReference_HandlesCyclicReference() + { + var result = PersonWithCyclicReference.PrintToString(); + + result.Should().Contain("Cyclic reference detected"); + Action action = () => PersonWithCyclicReference.PrintToString(); + action.Should().NotThrow(); + } + + [Test] + public void PrintToString_WithCyclicReferencesInCollections_DetectsCycles() + { + var list1 = new List(); + var list2 = new List { list1 }; + list1.Add(list2); + + var result = list1.PrintToString(); + result.Should().Contain("Cyclic reference detected"); + } + + + [Test] + public void PrintToString_WithZeroTrimLength_HandlesCorrectly() + { + var testClass = TestPerson; + var printer = ObjectPrinter.For() + .SerializeProperty(x => x.Name).TrimTo(0); + + var result = printer.PrintToString(testClass); + result.Should().Contain("Name = "); + } + + [Test] + public void PrintToString_WithComplexObjectGraph_DoesNotThrow() + { + var complexObject = new + { + Id = 1, + Name = "Test", + Items = new[] { "A", "B", "C" }, + Nested = new { Value = 42, Date = DateTime.Now } + }; + + Action action = () => complexObject.PrintToString(); + action.Should().NotThrow(); + } + + [Test] + public void PrintToString_WithFailingCustomSerializer_FallsBackToDefault() + { + var printer = ObjectPrinter.For() + .SerializeType() + .Use(s => throw new Exception("Test exception")); + + var person = new Person { Name = "Test" }; + Action action = () => printer.PrintToString(person); + action.Should().NotThrow(); + } + + [Test] + public void PrintToString_WithSelfReference_DetectsCycle() + { + var selfReferencing = new Person(); + selfReferencing.Children.Add(selfReferencing); + + var result = selfReferencing.PrintToString(); + result.Should().Contain("Cyclic reference detected"); + } +} \ No newline at end of file diff --git a/ObjectPrinting/Tests/ObjectPrinterStandartSerializationTests.cs b/ObjectPrinting/Tests/ObjectPrinterStandartSerializationTests.cs new file mode 100644 index 000000000..04e72053b --- /dev/null +++ b/ObjectPrinting/Tests/ObjectPrinterStandartSerializationTests.cs @@ -0,0 +1,118 @@ +using System; +using FluentAssertions; +using NUnit.Framework; + +namespace ObjectPrinting.Tests; + +[TestFixture] +public class ObjectPrinterStandartSerializationTests : TestBase +{ + [Test] + public void PrintToString_SimpleObject_ReturnsCorrectFormat() + { + var result = TestPerson.PrintToString(); + + result.Should().NotBeNullOrEmpty(); + result.Should().Contain("Person"); + result.Should().Contain("Name = John Doe"); + result.Should().Contain("Age = 30"); + result.Should().Contain("Height = 180.5"); + } + + [Test] + public void PrintToString_WithNullProperty_HandlesNullCorrectly() + { + var person = new Person { Name = null }; + var result = person.PrintToString(); + + result.Should().Contain("Name = null"); + } + + [Test] + public void PrintToString_WithInheritance_IncludesAllProperties() + { + var result = TestEmployee.PrintToString(); + + result.Should() + .Contain("Employee") + .And.Contain("Position = Developer") + .And.Contain("Salary = 50000.50") + .And.Contain("Name = Jane") + .And.Contain("Age = 25"); + } + + [Test] + public void PrintToString_ExtensionMethodWithoutConfig_Works() + { + var result = TestPerson.PrintToString(); + + result.Should().NotBeNullOrEmpty().And.Contain("Person"); + } + + [Test] + public void PrintToString_ExtensionMethodWithConfig_AppliesConfiguration() + { + var result = TestPerson.PrintToString(config => + config.ExcludeProperty(p => p.Age) + .SerializeProperty(p => p.Name).TrimTo(2)); + + result.Should().NotContain("Age = 30").And.Contain("Name = Jo"); + } + + [Test] + public void PrintToString_WithDateTime_SerializesCorrectly() + { + var testClass = new { Date = new DateTime(2023, 12, 31) }; + var result = testClass.PrintToString(); + + result.Should().Contain("Date = "); + } + + [Test] + public void PrintToString_WithEnum_SerializesCorrectly() + { + var testClass = new { Status = System.DateTimeKind.Utc }; + var result = testClass.PrintToString(); + + result.Should().Contain("Status = Utc"); + } + + [Test] + public void PrintToString_WithNestedObjects_SerializesRecursively() + { + var result = TestCompany.PrintToString(); + + result.Should() + .Contain("Company") + .And.Contain("CEO = Employee") + .And.Contain("Name = CEO") + .And.Contain("Age = 45"); + } + + [Test] + public void PrintToString_WithDifferentNumericTypes_UsesCorrectFormatting() + { + var numericObject = new + { + IntValue = 42, + DoubleValue = 3.14159, + DecimalValue = 123.456m, + FloatValue = 2.718f + }; + + var result = numericObject.PrintToString(); + + result.Should() + .Contain("IntValue = 42") + .And.Contain("DoubleValue = 3.14159") + .And.Contain("DecimalValue = 123.456") + .And.Contain("FloatValue = 2.718"); + } + + [Test] + public void PrintToString_EmptyObject_ReturnsTypeName() + { + var result = new object().PrintToString(); + result.Should().Contain("Object"); + } +} \ No newline at end of file diff --git a/ObjectPrinting/Tests/ObjectPrinterTestBaseClass.cs b/ObjectPrinting/Tests/ObjectPrinterTestBaseClass.cs new file mode 100644 index 000000000..f7e43a929 --- /dev/null +++ b/ObjectPrinting/Tests/ObjectPrinterTestBaseClass.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using NUnit.Framework; + +namespace ObjectPrinting.Tests; + +public class TestBase +{ + protected Person TestPerson { get; private set; } + protected Person PersonWithCyclicReference { get; private set; } + protected Employee TestEmployee { get; private set; } + protected Company TestCompany { get; private set; } + + [OneTimeSetUp] + public virtual void SetUp() + { + TestPerson = new Person + { + Id = Guid.NewGuid(), + Name = "John Doe", + Age = 30, + Height = 180.5, + Email = "john.doe@example.com" + }; + + var parent = new Person { Name = "Parent" }; + var child = new Person { Name = "Child", Parent = parent }; + parent.Children.Add(child); + PersonWithCyclicReference = parent; + + TestEmployee = new Employee + { + Name = "Jane", + Age = 25, + Position = "Developer", + Salary = 50000.50m + }; + + TestCompany = new Company + { + Name = "Test Corp", + CEO = new Employee { Name = "CEO", Age = 45 } + }; + } + + protected List CreateTestPeopleList() => new() + { + new Person { Name = "First", Age = 20 }, + new Person { Name = "Second", Age = 25 } + }; + + protected string[] CreateTestStringArray() => ["apple", "banana", "cherry"]; + + protected Dictionary CreateTestDictionary() => new() + { + ["first"] = 1, + ["second"] = 2, + ["third"] = 3 + }; +} \ No newline at end of file diff --git a/ObjectPrinting/Tests/ObjectPrinterTestDataClasses.cs b/ObjectPrinting/Tests/ObjectPrinterTestDataClasses.cs new file mode 100644 index 000000000..4f4dada95 --- /dev/null +++ b/ObjectPrinting/Tests/ObjectPrinterTestDataClasses.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; + + +namespace ObjectPrinting.Tests; + +public class Person +{ + public Guid Id { get; set; } + public string Name { get; set; } + public int Age { get; set; } + public double Height { get; set; } + public string Email { get; set; } + public Person Parent { get; set; } + public List Children { get; set; } = []; +} + +public class Employee : Person +{ + public string Position { get; set; } + public decimal Salary { get; set; } +} + +public class Company +{ + public string Name { get; set; } + public Employee CEO { get; set; } +} \ 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 diff --git a/ObjectPrinting/TypeSerializingConfig.cs b/ObjectPrinting/TypeSerializingConfig.cs new file mode 100644 index 000000000..644ab743b --- /dev/null +++ b/ObjectPrinting/TypeSerializingConfig.cs @@ -0,0 +1,26 @@ +using System; +using System.Globalization; + +namespace ObjectPrinting; + +public class TypeSerializingConfig +{ + private readonly PrintingConfig printingConfig; + + public TypeSerializingConfig(PrintingConfig printingConfig) + { + this.printingConfig = printingConfig; + } + + public PrintingConfig Use(Func serializeFunc) + { + printingConfig.AddTypeSerializer(serializeFunc); + return printingConfig; + } + + public PrintingConfig Use(CultureInfo cultureInfo) + { + printingConfig.AddTypeCulture(cultureInfo); + return printingConfig; + } +} \ No newline at end of file From a5900bbe733c83f2a07eb14a485d16533feeb6d9 Mon Sep 17 00:00:00 2001 From: PavelMartinelli Date: Tue, 18 Nov 2025 21:40:31 +0500 Subject: [PATCH 2/3] fix ObjectPrinter --- .../{ => Configs}/PrintingConfig.cs | 158 +++++++++++++----- .../PropertySerializingConfig.cs | 3 +- .../StringPropertySerializingConfig.cs | 3 +- .../{ => Configs}/TypeSerializingConfig.cs | 6 +- ObjectPrinting/ObjectExtensions.cs | 8 +- ...rAcceptanceTests.cs => AcceptanceTests.cs} | 2 +- ...sts.cs => CollectionSerializationTests.cs} | 2 +- ....cs => ConfigurationSerializationTests.cs} | 34 +++- ...terEdgeCasesTests.cs => EdgeCasesTests.cs} | 13 +- ...Tests.cs => StandartSerializationTests.cs} | 45 ++++- ...interTestBaseClass.cs => TestBaseClass.cs} | 16 -- .../Tests/TestDataClasses/Company.cs | 7 + .../Tests/TestDataClasses/Employee.cs | 7 + .../Tests/TestDataClasses/NestedContainer.cs | 7 + .../Person.cs} | 12 -- 15 files changed, 231 insertions(+), 92 deletions(-) rename ObjectPrinting/{ => Configs}/PrintingConfig.cs (58%) rename ObjectPrinting/{ => Configs}/PropertySerializingConfig.cs (82%) rename ObjectPrinting/{ => Configs}/StringPropertySerializingConfig.cs (78%) rename ObjectPrinting/{ => Configs}/TypeSerializingConfig.cs (73%) rename ObjectPrinting/Tests/{ObjectPrinterAcceptanceTests.cs => AcceptanceTests.cs} (97%) rename ObjectPrinting/Tests/{ObjectPrinterCollectionSerializationTests.cs => CollectionSerializationTests.cs} (97%) rename ObjectPrinting/Tests/{ObjectPrinterConfigurationSerializationTests.cs => ConfigurationSerializationTests.cs} (81%) rename ObjectPrinting/Tests/{ObjectPrinterEdgeCasesTests.cs => EdgeCasesTests.cs} (85%) rename ObjectPrinting/Tests/{ObjectPrinterStandartSerializationTests.cs => StandartSerializationTests.cs} (71%) rename ObjectPrinting/Tests/{ObjectPrinterTestBaseClass.cs => TestBaseClass.cs} (72%) create mode 100644 ObjectPrinting/Tests/TestDataClasses/Company.cs create mode 100644 ObjectPrinting/Tests/TestDataClasses/Employee.cs create mode 100644 ObjectPrinting/Tests/TestDataClasses/NestedContainer.cs rename ObjectPrinting/Tests/{ObjectPrinterTestDataClasses.cs => TestDataClasses/Person.cs} (63%) diff --git a/ObjectPrinting/PrintingConfig.cs b/ObjectPrinting/Configs/PrintingConfig.cs similarity index 58% rename from ObjectPrinting/PrintingConfig.cs rename to ObjectPrinting/Configs/PrintingConfig.cs index e129016eb..5aa9458d4 100644 --- a/ObjectPrinting/PrintingConfig.cs +++ b/ObjectPrinting/Configs/PrintingConfig.cs @@ -11,18 +11,77 @@ namespace ObjectPrinting; public class PrintingConfig { - private readonly Dictionary typeSerializers = new(); - private readonly Dictionary typeCultures = new(); - private readonly Dictionary propertySerializers = new(); - private readonly Dictionary propertyTrimLengths = new(); - private readonly HashSet excludedProperties = []; - private readonly HashSet excludedTypes = []; + private readonly Dictionary typeSerializers; + private readonly Dictionary typeCultures; + private readonly Dictionary propertySerializers; + private readonly Dictionary propertyTrimLengths; + private readonly HashSet excludedProperties; + private readonly HashSet excludedTypes; private readonly HashSet visitedObjects = []; + + private readonly Type[] finalTypes = + [ + typeof(int), typeof(double), typeof(float), typeof(string), + typeof(DateTime), typeof(TimeSpan), typeof(Guid), typeof(decimal), + typeof(long), typeof(short), typeof(byte), typeof(bool), + typeof(char), typeof(sbyte), typeof(ushort), typeof(uint), + typeof(ulong) + ]; + + private int nestingLevel; + + public PrintingConfig() + { + typeSerializers = new Dictionary(); + typeCultures = new Dictionary(); + propertySerializers = new Dictionary(); + propertyTrimLengths = new Dictionary(); + excludedProperties = []; + excludedTypes = []; + nestingLevel = 50; + } + + private PrintingConfig( + Dictionary typeSerializers, + Dictionary typeCultures, + Dictionary propertySerializers, + Dictionary propertyTrimLengths, + HashSet excludedProperties, + HashSet excludedTypes, + int maxNestingLevel) + { + this.typeSerializers = new Dictionary(typeSerializers); + this.typeCultures = new Dictionary(typeCultures); + this.propertySerializers = new Dictionary(propertySerializers); + this.propertyTrimLengths = new Dictionary(propertyTrimLengths); + this.excludedProperties = new HashSet(excludedProperties); + this.excludedTypes = new HashSet(excludedTypes); + this.nestingLevel = maxNestingLevel; + } + + private PrintingConfig PrintingConfigWith( + Dictionary typeSerializers = null, + Dictionary typeCultures = null, + Dictionary propertySerializers = null, + Dictionary propertyTrimLengths = null, + HashSet excludedProperties = null, + HashSet excludedTypes = null, + int? maxNestingLevel = null) + { + return new PrintingConfig( + typeSerializers ?? this.typeSerializers, + typeCultures ?? this.typeCultures, + propertySerializers ?? this.propertySerializers, + propertyTrimLengths ?? this.propertyTrimLengths, + excludedProperties ?? this.excludedProperties, + excludedTypes ?? this.excludedTypes, + maxNestingLevel ?? this.nestingLevel); + } public PrintingConfig ExcludeType() { - excludedTypes.Add(typeof(T)); - return this; + var newExcludedTypes = new HashSet(excludedTypes) { typeof(T) }; + return PrintingConfigWith(excludedTypes: newExcludedTypes); } public TypeSerializingConfig SerializeType() @@ -34,8 +93,7 @@ public PropertySerializingConfig SerializeProperty( Expression> propertySelector) { var propertyName = GetPropertyName(propertySelector); - return new PropertySerializingConfig(this, - propertyName); + return new PropertySerializingConfig(this, propertyName); } public StringPropertySerializingConfig SerializeProperty( @@ -49,20 +107,27 @@ public PrintingConfig ExcludeProperty( Expression> propertySelector) { var propertyName = GetPropertyName(propertySelector); - excludedProperties.Add(propertyName); - return this; + var newExcludedProperties = new HashSet(excludedProperties) { propertyName }; + return PrintingConfigWith(excludedProperties: newExcludedProperties); } - public string PrintToString(TOwner obj) + public string PrintToString(TOwner obj, int? nestingLevel = null) { + if (nestingLevel < 0) + throw new ArgumentException("Nesting level cannot be negative"); + this.nestingLevel = nestingLevel ?? this.nestingLevel; + visitedObjects.Clear(); - return PrintToString(obj, 0); + return PrintToStringInternal(obj, 0); } - private string PrintToString(object obj, int nestingLevel) + private string PrintToStringInternal(object obj, int currentnestingLevel) { if (obj == null) return "null" + Environment.NewLine; + + if (currentnestingLevel >= this.nestingLevel) + return $"Max nesting level ({this.nestingLevel}) reached" + Environment.NewLine; var type = obj.GetType(); @@ -77,9 +142,9 @@ private string PrintToString(object obj, int nestingLevel) return SerializeValue(obj, type, null) + Environment.NewLine; if (obj is IEnumerable enumerable && !(obj is string)) - return SerializeCollection(enumerable, nestingLevel); + return SerializeCollection(enumerable, currentnestingLevel); - return SerializeObject(obj, nestingLevel, type); + return SerializeObject(obj, currentnestingLevel, type); } finally { @@ -87,8 +152,7 @@ private string PrintToString(object obj, int nestingLevel) } } - private string SerializeCollection(IEnumerable collection, - int nestingLevel) + private string SerializeCollection(IEnumerable collection, int nestingLevel) { var indentation = new string('\t', nestingLevel + 1); var sb = new StringBuilder(); @@ -148,12 +212,12 @@ private string SerializeObject(object obj, int nestingLevel, Type type) if (propertyInfo.GetIndexParameters().Length > 0) continue; - if (ShouldExcludeMember(propertyInfo.PropertyType, - propertyInfo.Name)) + if (ShouldExcludeMember(propertyInfo.PropertyType, propertyInfo.Name)) continue; var value = propertyInfo.GetValue(obj); - var serializedValue = SerializeValue(value, propertyInfo.PropertyType, propertyInfo.Name, nestingLevel); + var serializedValue = SerializeValue(value, propertyInfo.PropertyType, + propertyInfo.Name, nestingLevel); sb.Append(indentation + propertyInfo.Name + " = " + serializedValue); } @@ -205,7 +269,7 @@ private string SerializeValue(object value, Type valueType, } if (!IsFinalType(valueType)) - return PrintToString(value, nestingLevel + 1); + return PrintToStringInternal(value, nestingLevel + 1); if (value is IFormattable formattableDefault) return formattableDefault.ToString(null, CultureInfo.InvariantCulture) + Environment.NewLine; @@ -235,19 +299,7 @@ private bool ShouldExcludeMember(Type memberType, string memberName) return excludedTypes.Contains(memberType) || excludedProperties.Contains(memberName); } - private bool IsFinalType(Type type) - { - var finalTypes = new[] - { - typeof(int), typeof(double), typeof(float), typeof(string), - typeof(DateTime), typeof(TimeSpan), typeof(Guid), typeof(decimal), - typeof(long), typeof(short), typeof(byte), typeof(bool), - typeof(char), typeof(sbyte), typeof(ushort), typeof(uint), - typeof(ulong) - }; - - return finalTypes.Contains(type) || type.IsEnum; - } + private bool IsFinalType(Type type) => finalTypes.Contains(type) || type.IsEnum; private string GetPropertyName( Expression> propertySelector) @@ -262,25 +314,41 @@ private string GetPropertyName( throw new ArgumentException("Expression must be a property or field selector"); } } - - internal void AddTypeSerializer(Func serializeFunc) + + internal PrintingConfig AddTypeSerializer(Func serializeFunc) { - typeSerializers[typeof(TType)] = serializeFunc; + var newTypeSerializers = new Dictionary(typeSerializers) + { + [typeof(TType)] = serializeFunc + }; + return PrintingConfigWith(typeSerializers: newTypeSerializers); } - internal void AddTypeCulture(CultureInfo cultureInfo) + internal PrintingConfig AddTypeCulture(CultureInfo cultureInfo) { - typeCultures[typeof(TType)] = cultureInfo; + var newTypeCultures = new Dictionary(typeCultures) + { + [typeof(TType)] = cultureInfo + }; + return PrintingConfigWith(typeCultures: newTypeCultures); } - internal void AddPropertySerializer(string propertyName, + internal PrintingConfig AddPropertySerializer(string propertyName, Func serializer) { - propertySerializers[propertyName] = serializer; + var newPropertySerializers = new Dictionary(propertySerializers) + { + [propertyName] = serializer + }; + return PrintingConfigWith(propertySerializers: newPropertySerializers); } - internal void AddPropertyTrim(string propertyName, int maxLength) + internal PrintingConfig AddPropertyTrim(string propertyName, int maxLength) { - propertyTrimLengths[propertyName] = maxLength; + var newPropertyTrimLengths = new Dictionary(propertyTrimLengths) + { + [propertyName] = maxLength + }; + return PrintingConfigWith(propertyTrimLengths: newPropertyTrimLengths); } } \ No newline at end of file diff --git a/ObjectPrinting/PropertySerializingConfig.cs b/ObjectPrinting/Configs/PropertySerializingConfig.cs similarity index 82% rename from ObjectPrinting/PropertySerializingConfig.cs rename to ObjectPrinting/Configs/PropertySerializingConfig.cs index aac8f6863..7568719ba 100644 --- a/ObjectPrinting/PropertySerializingConfig.cs +++ b/ObjectPrinting/Configs/PropertySerializingConfig.cs @@ -15,7 +15,6 @@ public PropertySerializingConfig(PrintingConfig printingConfig, string p public PrintingConfig Use(Func serializer) { - printingConfig.AddPropertySerializer(propertyName, serializer); - return printingConfig; + return printingConfig.AddPropertySerializer(propertyName, serializer); } } \ No newline at end of file diff --git a/ObjectPrinting/StringPropertySerializingConfig.cs b/ObjectPrinting/Configs/StringPropertySerializingConfig.cs similarity index 78% rename from ObjectPrinting/StringPropertySerializingConfig.cs rename to ObjectPrinting/Configs/StringPropertySerializingConfig.cs index 3959cdca8..c420c4250 100644 --- a/ObjectPrinting/StringPropertySerializingConfig.cs +++ b/ObjectPrinting/Configs/StringPropertySerializingConfig.cs @@ -7,7 +7,6 @@ public StringPropertySerializingConfig(PrintingConfig printingConfig, st public PrintingConfig TrimTo(int maxLength) { - printingConfig.AddPropertyTrim(propertyName, maxLength); - return printingConfig; + return printingConfig.AddPropertyTrim(propertyName, maxLength); } } \ No newline at end of file diff --git a/ObjectPrinting/TypeSerializingConfig.cs b/ObjectPrinting/Configs/TypeSerializingConfig.cs similarity index 73% rename from ObjectPrinting/TypeSerializingConfig.cs rename to ObjectPrinting/Configs/TypeSerializingConfig.cs index 644ab743b..deebd5d74 100644 --- a/ObjectPrinting/TypeSerializingConfig.cs +++ b/ObjectPrinting/Configs/TypeSerializingConfig.cs @@ -14,13 +14,11 @@ public TypeSerializingConfig(PrintingConfig printingConfig) public PrintingConfig Use(Func serializeFunc) { - printingConfig.AddTypeSerializer(serializeFunc); - return printingConfig; + return printingConfig.AddTypeSerializer(serializeFunc); } public PrintingConfig Use(CultureInfo cultureInfo) { - printingConfig.AddTypeCulture(cultureInfo); - return printingConfig; + return printingConfig.AddTypeCulture(cultureInfo); } } \ No newline at end of file diff --git a/ObjectPrinting/ObjectExtensions.cs b/ObjectPrinting/ObjectExtensions.cs index e050404b4..627048339 100644 --- a/ObjectPrinting/ObjectExtensions.cs +++ b/ObjectPrinting/ObjectExtensions.cs @@ -4,13 +4,13 @@ namespace ObjectPrinting; public static class ObjectPrinterExtensions { - public static string PrintToString(this T obj) + public static string PrintToString(this T obj, int? nestingLevel = null) { - return ObjectPrinter.For().PrintToString(obj); + return ObjectPrinter.For().PrintToString(obj, nestingLevel); } - public static string PrintToString(this T obj, Func, PrintingConfig> config) + public static string PrintToString(this T obj, Func, PrintingConfig> configurator, int? nestingLevel = null) { - return config(ObjectPrinter.For()).PrintToString(obj); + return configurator(ObjectPrinter.For()).PrintToString(obj, nestingLevel); } } \ No newline at end of file diff --git a/ObjectPrinting/Tests/ObjectPrinterAcceptanceTests.cs b/ObjectPrinting/Tests/AcceptanceTests.cs similarity index 97% rename from ObjectPrinting/Tests/ObjectPrinterAcceptanceTests.cs rename to ObjectPrinting/Tests/AcceptanceTests.cs index 1c3ec79d4..0557c6f3a 100644 --- a/ObjectPrinting/Tests/ObjectPrinterAcceptanceTests.cs +++ b/ObjectPrinting/Tests/AcceptanceTests.cs @@ -5,7 +5,7 @@ namespace ObjectPrinting.Tests { [TestFixture] - public class ObjectPrinterAcceptanceTests + public class AcceptanceTests { [Test] public void Demo() diff --git a/ObjectPrinting/Tests/ObjectPrinterCollectionSerializationTests.cs b/ObjectPrinting/Tests/CollectionSerializationTests.cs similarity index 97% rename from ObjectPrinting/Tests/ObjectPrinterCollectionSerializationTests.cs rename to ObjectPrinting/Tests/CollectionSerializationTests.cs index 1f9c119bd..7e6b2624a 100644 --- a/ObjectPrinting/Tests/ObjectPrinterCollectionSerializationTests.cs +++ b/ObjectPrinting/Tests/CollectionSerializationTests.cs @@ -5,7 +5,7 @@ namespace ObjectPrinting.Tests; [TestFixture] -public class ObjectPrinterCollectionSerializationTests : TestBase +public class CollectionSerializationTests : TestBase { [Test] public void PrintToString_WithList_SerializesListElements() diff --git a/ObjectPrinting/Tests/ObjectPrinterConfigurationSerializationTests.cs b/ObjectPrinting/Tests/ConfigurationSerializationTests.cs similarity index 81% rename from ObjectPrinting/Tests/ObjectPrinterConfigurationSerializationTests.cs rename to ObjectPrinting/Tests/ConfigurationSerializationTests.cs index 6d6b9025f..b03444d97 100644 --- a/ObjectPrinting/Tests/ObjectPrinterConfigurationSerializationTests.cs +++ b/ObjectPrinting/Tests/ConfigurationSerializationTests.cs @@ -6,7 +6,7 @@ namespace ObjectPrinting.Tests; [TestFixture] -public class ObjectPrinterConfigurationSerializationTests : TestBase +public class ConfigurationSerializationTests : TestBase { [Test] public void PrintToString_WithExcludeType_ExcludesPropertiesOfType() @@ -149,4 +149,36 @@ public void PrintingConfig_ChainedConfiguration_ReturnsCorrectType() config.Should().BeOfType>(); } + [Test] + public void PrintToString_WithConfigAndNestingLevel_AppliesBoth() + { + var nestedObject = new NestedContainer + { + Value = "Level 1", + Child = new NestedContainer + { + Value = "Very Long Value That Should Be Trimmed", + Child = new NestedContainer { Value = "Level 3" } + } + }; + + var result = nestedObject.PrintToString( + configurator => configurator + .SerializeProperty(x => x.Value).TrimTo(10), + nestingLevel: 2 + ); + + result.Should().Contain("Value = Level 1"); + result.Should().Contain("Value = Very Long"); + result.Should().NotContain("Level 3"); + } + + [Test] + public void PrintingConfig_ShouldReturnNewInstance_AfterConfiguration() + { + var printer = ObjectPrinter.For().ExcludeType(); + var printer2 = printer.ExcludeType(); + + printer.Should().NotBeSameAs(printer2); + } } \ No newline at end of file diff --git a/ObjectPrinting/Tests/ObjectPrinterEdgeCasesTests.cs b/ObjectPrinting/Tests/EdgeCasesTests.cs similarity index 85% rename from ObjectPrinting/Tests/ObjectPrinterEdgeCasesTests.cs rename to ObjectPrinting/Tests/EdgeCasesTests.cs index 9590cf416..5d9c2c8a2 100644 --- a/ObjectPrinting/Tests/ObjectPrinterEdgeCasesTests.cs +++ b/ObjectPrinting/Tests/EdgeCasesTests.cs @@ -6,7 +6,7 @@ namespace ObjectPrinting.Tests; [TestFixture] -public class ObjectPrinterEdgeCasesTests : TestBase +public class EdgeCasesTests : TestBase { [Test] public void PrintToString_WithCyclicReference_HandlesCyclicReference() @@ -77,4 +77,15 @@ public void PrintToString_WithSelfReference_DetectsCycle() var result = selfReferencing.PrintToString(); result.Should().Contain("Cyclic reference detected"); } + + [Test] + public void PrintToString_WithNegativeNestingLevel_ThrowsArgumentException() + { + var person = new Person { Name = "Test" }; + + Action action = () => person.PrintToString(nestingLevel: -1); + + action.Should().Throw() + .WithMessage("Nesting level cannot be negative*"); + } } \ No newline at end of file diff --git a/ObjectPrinting/Tests/ObjectPrinterStandartSerializationTests.cs b/ObjectPrinting/Tests/StandartSerializationTests.cs similarity index 71% rename from ObjectPrinting/Tests/ObjectPrinterStandartSerializationTests.cs rename to ObjectPrinting/Tests/StandartSerializationTests.cs index 04e72053b..4f23b511a 100644 --- a/ObjectPrinting/Tests/ObjectPrinterStandartSerializationTests.cs +++ b/ObjectPrinting/Tests/StandartSerializationTests.cs @@ -5,7 +5,7 @@ namespace ObjectPrinting.Tests; [TestFixture] -public class ObjectPrinterStandartSerializationTests : TestBase +public class StandartSerializationTests : TestBase { [Test] public void PrintToString_SimpleObject_ReturnsCorrectFormat() @@ -31,7 +31,15 @@ public void PrintToString_WithNullProperty_HandlesNullCorrectly() [Test] public void PrintToString_WithInheritance_IncludesAllProperties() { - var result = TestEmployee.PrintToString(); + var testEmployee = new Employee + { + Name = "Jane", + Age = 25, + Position = "Developer", + Salary = 50000.50m + }; + + var result = testEmployee.PrintToString(); result.Should() .Contain("Employee") @@ -80,7 +88,13 @@ public void PrintToString_WithEnum_SerializesCorrectly() [Test] public void PrintToString_WithNestedObjects_SerializesRecursively() { - var result = TestCompany.PrintToString(); + var testCompany = new Company + { + Name = "Test Corp", + CEO = new Employee { Name = "CEO", Age = 45 } + }; + + var result = testCompany.PrintToString(); result.Should() .Contain("Company") @@ -115,4 +129,29 @@ public void PrintToString_EmptyObject_ReturnsTypeName() var result = new object().PrintToString(); result.Should().Contain("Object"); } + + [Test] + public void PrintToString_WithNestingLevelLimit_StopsAtSpecifiedDepth() + { + var nestedObject = new NestedContainer + { + Value = "Level 1", + Child = new NestedContainer + { + Value = "Level 2", + Child = new NestedContainer + { + Value = "Level 3", + Child = new NestedContainer { Value = "Level 4" } + } + } + }; + + var result = nestedObject.PrintToString(2); + + result.Should().Contain("Level 1"); + result.Should().Contain("Level 2"); + result.Should().NotContain("Level 3"); + result.Should().NotContain("Level 4"); + } } \ No newline at end of file diff --git a/ObjectPrinting/Tests/ObjectPrinterTestBaseClass.cs b/ObjectPrinting/Tests/TestBaseClass.cs similarity index 72% rename from ObjectPrinting/Tests/ObjectPrinterTestBaseClass.cs rename to ObjectPrinting/Tests/TestBaseClass.cs index f7e43a929..7d1ad3689 100644 --- a/ObjectPrinting/Tests/ObjectPrinterTestBaseClass.cs +++ b/ObjectPrinting/Tests/TestBaseClass.cs @@ -8,8 +8,6 @@ public class TestBase { protected Person TestPerson { get; private set; } protected Person PersonWithCyclicReference { get; private set; } - protected Employee TestEmployee { get; private set; } - protected Company TestCompany { get; private set; } [OneTimeSetUp] public virtual void SetUp() @@ -27,20 +25,6 @@ public virtual void SetUp() var child = new Person { Name = "Child", Parent = parent }; parent.Children.Add(child); PersonWithCyclicReference = parent; - - TestEmployee = new Employee - { - Name = "Jane", - Age = 25, - Position = "Developer", - Salary = 50000.50m - }; - - TestCompany = new Company - { - Name = "Test Corp", - CEO = new Employee { Name = "CEO", Age = 45 } - }; } protected List CreateTestPeopleList() => new() diff --git a/ObjectPrinting/Tests/TestDataClasses/Company.cs b/ObjectPrinting/Tests/TestDataClasses/Company.cs new file mode 100644 index 000000000..f6583ad4a --- /dev/null +++ b/ObjectPrinting/Tests/TestDataClasses/Company.cs @@ -0,0 +1,7 @@ +namespace ObjectPrinting.Tests; + +public class Company +{ + public string Name { get; set; } + public Employee CEO { get; set; } +} \ No newline at end of file diff --git a/ObjectPrinting/Tests/TestDataClasses/Employee.cs b/ObjectPrinting/Tests/TestDataClasses/Employee.cs new file mode 100644 index 000000000..b73b23099 --- /dev/null +++ b/ObjectPrinting/Tests/TestDataClasses/Employee.cs @@ -0,0 +1,7 @@ +namespace ObjectPrinting.Tests; + +public class Employee : Person +{ + public string Position { get; set; } + public decimal Salary { get; set; } +} \ No newline at end of file diff --git a/ObjectPrinting/Tests/TestDataClasses/NestedContainer.cs b/ObjectPrinting/Tests/TestDataClasses/NestedContainer.cs new file mode 100644 index 000000000..b263d7059 --- /dev/null +++ b/ObjectPrinting/Tests/TestDataClasses/NestedContainer.cs @@ -0,0 +1,7 @@ +namespace ObjectPrinting.Tests; + +public class NestedContainer +{ + public string Value { get; set; } + public NestedContainer Child { get; set; } +} \ No newline at end of file diff --git a/ObjectPrinting/Tests/ObjectPrinterTestDataClasses.cs b/ObjectPrinting/Tests/TestDataClasses/Person.cs similarity index 63% rename from ObjectPrinting/Tests/ObjectPrinterTestDataClasses.cs rename to ObjectPrinting/Tests/TestDataClasses/Person.cs index 4f4dada95..022af032c 100644 --- a/ObjectPrinting/Tests/ObjectPrinterTestDataClasses.cs +++ b/ObjectPrinting/Tests/TestDataClasses/Person.cs @@ -13,16 +13,4 @@ public class Person public string Email { get; set; } public Person Parent { get; set; } public List Children { get; set; } = []; -} - -public class Employee : Person -{ - public string Position { get; set; } - public decimal Salary { get; set; } -} - -public class Company -{ - public string Name { get; set; } - public Employee CEO { get; set; } } \ No newline at end of file From e27a9548bf8d9f8dbc02b78a57fcf484ba5dd724 Mon Sep 17 00:00:00 2001 From: PavelMartinelli Date: Wed, 19 Nov 2025 22:37:25 +0500 Subject: [PATCH 3/3] fix TrimTo --- ObjectPrinting/Configs/PrintingConfig.cs | 43 +------------------ .../StringPropertySerializingConfig.cs | 2 +- 2 files changed, 3 insertions(+), 42 deletions(-) diff --git a/ObjectPrinting/Configs/PrintingConfig.cs b/ObjectPrinting/Configs/PrintingConfig.cs index 5aa9458d4..e8b8b185f 100644 --- a/ObjectPrinting/Configs/PrintingConfig.cs +++ b/ObjectPrinting/Configs/PrintingConfig.cs @@ -14,7 +14,6 @@ public class PrintingConfig private readonly Dictionary typeSerializers; private readonly Dictionary typeCultures; private readonly Dictionary propertySerializers; - private readonly Dictionary propertyTrimLengths; private readonly HashSet excludedProperties; private readonly HashSet excludedTypes; private readonly HashSet visitedObjects = []; @@ -35,7 +34,6 @@ public PrintingConfig() typeSerializers = new Dictionary(); typeCultures = new Dictionary(); propertySerializers = new Dictionary(); - propertyTrimLengths = new Dictionary(); excludedProperties = []; excludedTypes = []; nestingLevel = 50; @@ -45,7 +43,6 @@ private PrintingConfig( Dictionary typeSerializers, Dictionary typeCultures, Dictionary propertySerializers, - Dictionary propertyTrimLengths, HashSet excludedProperties, HashSet excludedTypes, int maxNestingLevel) @@ -53,7 +50,6 @@ private PrintingConfig( this.typeSerializers = new Dictionary(typeSerializers); this.typeCultures = new Dictionary(typeCultures); this.propertySerializers = new Dictionary(propertySerializers); - this.propertyTrimLengths = new Dictionary(propertyTrimLengths); this.excludedProperties = new HashSet(excludedProperties); this.excludedTypes = new HashSet(excludedTypes); this.nestingLevel = maxNestingLevel; @@ -63,7 +59,6 @@ private PrintingConfig PrintingConfigWith( Dictionary typeSerializers = null, Dictionary typeCultures = null, Dictionary propertySerializers = null, - Dictionary propertyTrimLengths = null, HashSet excludedProperties = null, HashSet excludedTypes = null, int? maxNestingLevel = null) @@ -72,7 +67,6 @@ private PrintingConfig PrintingConfigWith( typeSerializers ?? this.typeSerializers, typeCultures ?? this.typeCultures, propertySerializers ?? this.propertySerializers, - propertyTrimLengths ?? this.propertyTrimLengths, excludedProperties ?? this.excludedProperties, excludedTypes ?? this.excludedTypes, maxNestingLevel ?? this.nestingLevel); @@ -235,7 +229,7 @@ private string SerializeValue(object value, Type valueType, try { var result = propertySerializer.DynamicInvoke(value); - return ProcessStringResult(result, memberName); + return result?.ToString() + Environment.NewLine; } catch { @@ -243,19 +237,12 @@ private string SerializeValue(object value, Type valueType, } } - if (memberName != null && propertyTrimLengths.TryGetValue(memberName, out var trimLength) && value is string str) - { - return (str.Length <= trimLength - ? str - : str.Substring(0, trimLength)) + Environment.NewLine; - } - if (typeSerializers.TryGetValue(valueType, out var typeSerializer)) { try { var result = typeSerializer.DynamicInvoke(value); - return ProcessStringResult(result, memberName); + return result?.ToString() + Environment.NewLine; } catch { @@ -277,23 +264,6 @@ private string SerializeValue(object value, Type valueType, return value.ToString() + Environment.NewLine; } - private string ProcessStringResult(object result, string memberName) - { - if (result == null) - return "null" + Environment.NewLine; - - var resultString = result.ToString(); - - if (memberName != null && propertyTrimLengths.TryGetValue(memberName, out var trimLength)) - { - resultString = resultString.Length <= trimLength - ? resultString - : resultString.Substring(0, trimLength); - } - - return resultString + Environment.NewLine; - } - private bool ShouldExcludeMember(Type memberType, string memberName) { return excludedTypes.Contains(memberType) || excludedProperties.Contains(memberName); @@ -342,13 +312,4 @@ internal PrintingConfig AddPropertySerializer(string propertyName }; return PrintingConfigWith(propertySerializers: newPropertySerializers); } - - internal PrintingConfig AddPropertyTrim(string propertyName, int maxLength) - { - var newPropertyTrimLengths = new Dictionary(propertyTrimLengths) - { - [propertyName] = maxLength - }; - return PrintingConfigWith(propertyTrimLengths: newPropertyTrimLengths); - } } \ No newline at end of file diff --git a/ObjectPrinting/Configs/StringPropertySerializingConfig.cs b/ObjectPrinting/Configs/StringPropertySerializingConfig.cs index c420c4250..88b524360 100644 --- a/ObjectPrinting/Configs/StringPropertySerializingConfig.cs +++ b/ObjectPrinting/Configs/StringPropertySerializingConfig.cs @@ -7,6 +7,6 @@ public StringPropertySerializingConfig(PrintingConfig printingConfig, st public PrintingConfig TrimTo(int maxLength) { - return printingConfig.AddPropertyTrim(propertyName, maxLength); + return Use(s => s?.Length <= maxLength ? s : s.Substring(0, maxLength)); } } \ No newline at end of file