From 711c6505c61c55ff4f1e35296e314d2d67192c38 Mon Sep 17 00:00:00 2001 From: qreaqtor Date: Wed, 11 Dec 2024 03:05:25 +0500 Subject: [PATCH 1/5] object printer and tests --- ObjectPrinting/ObjectExtensions.cs | 21 ++ ObjectPrinting/ObjectPrinting.csproj | 1 + ObjectPrinting/PrintingConfig.cs | 121 ++++++++++-- ObjectPrinting/PropertyPrintingConfig.cs | 44 +++++ .../PropertyPrintingConfigExtensions.cs | 32 +++ ObjectPrinting/SerrializeConfig.cs | 58 ++++++ .../Tests/ObjectPrinterAcceptanceTests.cs | 42 ++-- ObjectPrinting/Tests/ObjectPrinterTests.cs | 187 ++++++++++++++++++ ObjectPrinting/Tests/Person.cs | 1 + 9 files changed, 480 insertions(+), 27 deletions(-) create mode 100644 ObjectPrinting/ObjectExtensions.cs create mode 100644 ObjectPrinting/PropertyPrintingConfig.cs create mode 100644 ObjectPrinting/PropertyPrintingConfigExtensions.cs create mode 100644 ObjectPrinting/SerrializeConfig.cs create mode 100644 ObjectPrinting/Tests/ObjectPrinterTests.cs diff --git a/ObjectPrinting/ObjectExtensions.cs b/ObjectPrinting/ObjectExtensions.cs new file mode 100644 index 00000000..c71769a8 --- /dev/null +++ b/ObjectPrinting/ObjectExtensions.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace ObjectPrinting +{ + public static class ObjectExtensions + { + 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); + } + } +} diff --git a/ObjectPrinting/ObjectPrinting.csproj b/ObjectPrinting/ObjectPrinting.csproj index c5db392f..a5c71017 100644 --- a/ObjectPrinting/ObjectPrinting.csproj +++ b/ObjectPrinting/ObjectPrinting.csproj @@ -5,6 +5,7 @@ + diff --git a/ObjectPrinting/PrintingConfig.cs b/ObjectPrinting/PrintingConfig.cs index a9e08211..b5ecfaff 100644 --- a/ObjectPrinting/PrintingConfig.cs +++ b/ObjectPrinting/PrintingConfig.cs @@ -1,41 +1,130 @@ using System; +using System.Collections; using System.Linq; +using System.Linq.Expressions; +using System.Reflection; using System.Text; namespace ObjectPrinting { public class PrintingConfig { - public string PrintToString(TOwner obj) + private readonly SerrializeConfig serrializeConfig; + + public PrintingConfig() + { + serrializeConfig = new SerrializeConfig(); + } + + public PrintingConfig(SerrializeConfig serrializeConfig) { - return PrintToString(obj, 0); + this.serrializeConfig = serrializeConfig; } private string PrintToString(object obj, int nestingLevel) { - //TODO apply configurations if (obj == null) return "null" + Environment.NewLine; - var finalTypes = new[] - { - typeof(int), typeof(double), typeof(float), typeof(string), - typeof(DateTime), typeof(TimeSpan) - }; - if (finalTypes.Contains(obj.GetType())) - return obj + Environment.NewLine; + var objType = obj.GetType(); + + if (serrializeConfig.TypeSerrializers.TryGetValue(objType, out var serrialize)) + return serrialize.DynamicInvoke(obj) + Environment.NewLine; + + if (obj is ICollection collection) + return SerializeCollection(collection, nestingLevel); var identation = new string('\t', nestingLevel + 1); + var sb = new StringBuilder(); - var type = obj.GetType(); - sb.AppendLine(type.Name); - foreach (var propertyInfo in type.GetProperties()) + + sb.AppendLine(objType.Name); + + foreach (var propertyInfo in objType.GetProperties()) { - sb.Append(identation + propertyInfo.Name + " = " + - PrintToString(propertyInfo.GetValue(obj), - nestingLevel + 1)); + var propType = propertyInfo.PropertyType; + var propValue = propertyInfo.GetValue(obj); + + if (serrializeConfig.ExcludedTypes.Contains(propType) || + serrializeConfig.ExcludedProperties.Contains(propertyInfo)) + continue; + + var contains = serrializeConfig.PropertySerrializers.TryGetValue(propertyInfo, out serrialize); + + var objStr = contains ? + serrialize.DynamicInvoke(propValue) + Environment.NewLine : + PrintToString(propertyInfo.GetValue(obj), nestingLevel + 1); + + sb.Append(identation + propertyInfo.Name + " = " + objStr); } + return sb.ToString(); } + + private string SerializeCollection(ICollection collection, int nestingLevel) + { + if (collection is IDictionary dictionary) + return SerializeDictionary(dictionary, nestingLevel); + + var sb = new StringBuilder(); + + foreach (var item in collection) + sb.Append(PrintToString(item, nestingLevel + 1).Trim() + " "); + + return $"[ {sb}]" + Environment.NewLine; + } + + private string SerializeDictionary(IDictionary dictionary, int nestingLevel) + { + var sb = new StringBuilder(); + var identation = new string('\t', nestingLevel); + sb.Append(identation + "{" + Environment.NewLine); + foreach (DictionaryEntry keyValuePair in dictionary) + { + identation = new string('\t', nestingLevel + 1); + sb.Append(identation + "[" + PrintToString(keyValuePair.Key, nestingLevel + 1).Trim() + " - "); + sb.Append(PrintToString(keyValuePair.Value, 0).Trim()); + sb.Append("],"); + sb.Append(Environment.NewLine); + } + + return sb + "}" + Environment.NewLine; + } + + public string PrintToString(TOwner obj) => PrintToString(obj, 0); + + public PrintingConfig Exclude() + { + var config = new PrintingConfig(serrializeConfig); + + config.serrializeConfig.ExcludedTypes.Add(typeof(T)); + + return config; + } + + public PrintingConfig Exclude(Expression> memberSelector) + { + var config = new PrintingConfig(serrializeConfig); + + var memberExp = memberSelector.Body as MemberExpression; + var propInfo = memberExp.Member as PropertyInfo; + + config.serrializeConfig.ExcludedProperties.Add(propInfo); + + return config; + } + + public PropertyPrintingConfig Print() + { + return new PropertyPrintingConfig(serrializeConfig); + } + + public PropertyPrintingConfig Print(Expression> memberSelector) + { + var memberExp = memberSelector.Body as MemberExpression; + var propInfo = memberExp.Member as PropertyInfo; + + return new PropertyPrintingConfig(serrializeConfig, propInfo); + } } } \ No newline at end of file diff --git a/ObjectPrinting/PropertyPrintingConfig.cs b/ObjectPrinting/PropertyPrintingConfig.cs new file mode 100644 index 00000000..2ea0fce8 --- /dev/null +++ b/ObjectPrinting/PropertyPrintingConfig.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; + +namespace ObjectPrinting +{ + public class PropertyPrintingConfig + { + private readonly SerrializeConfig serrializeConfig; + private readonly PropertyInfo propertyInfo; + + public PropertyPrintingConfig(SerrializeConfig serrializeConfig, PropertyInfo propertyInfo = null) + { + this.serrializeConfig = new SerrializeConfig(serrializeConfig); + this.propertyInfo = propertyInfo; + } + + public PrintingConfig Using(Func serrializer) + { + if (propertyInfo == null) + { + var type = typeof(TProp); + + if (serrializeConfig.TypeSerrializers.ContainsKey(type)) + serrializeConfig.TypeSerrializers[type] = serrializer; + else + serrializeConfig.TypeSerrializers.Add(type, serrializer); + } + else + { + if (serrializeConfig.PropertySerrializers.ContainsKey(propertyInfo)) + serrializeConfig.PropertySerrializers[propertyInfo] = serrializer; + else + serrializeConfig.PropertySerrializers.Add(propertyInfo, serrializer); + } + + + return new PrintingConfig(serrializeConfig); + } + } +} diff --git a/ObjectPrinting/PropertyPrintingConfigExtensions.cs b/ObjectPrinting/PropertyPrintingConfigExtensions.cs new file mode 100644 index 00000000..ea648a5b --- /dev/null +++ b/ObjectPrinting/PropertyPrintingConfigExtensions.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace ObjectPrinting +{ + public static class PropertyPrintingConfigExtensions + { + public static PrintingConfig TrimmedToLength(this PropertyPrintingConfig propConfig, int maxLen) + { + return propConfig.Using(property => property.Substring(0, Math.Min(maxLen, property.Length)).ToString()); + } + + public static PrintingConfig Using(this PropertyPrintingConfig propConfig, CultureInfo culture) + { + return propConfig.Using(x => x.ToString(culture)); + } + + public static PrintingConfig Using(this PropertyPrintingConfig propConfig, CultureInfo culture) + { + return propConfig.Using(x => x.ToString(culture)); + } + + public static PrintingConfig Using(this PropertyPrintingConfig propConfig, CultureInfo culture) + { + return propConfig.Using(x => x.ToString(culture)); + } + } +} diff --git a/ObjectPrinting/SerrializeConfig.cs b/ObjectPrinting/SerrializeConfig.cs new file mode 100644 index 00000000..7c87de38 --- /dev/null +++ b/ObjectPrinting/SerrializeConfig.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; + +namespace ObjectPrinting +{ + public class SerrializeConfig + { + /* + * У объекта для серриализации может быть вложенное поле, имеющшее тип, который мы сами определили, + * и для него тоже может быть возможность определить кастомный сериализатор. + * Также изначально не известно какие собственные типы есть у объекта, поэтому нужен отдельный хеш-сет, + * где хранятся все исключенные типы. + */ + + public readonly HashSet ExcludedTypes; + public readonly HashSet ExcludedProperties; + + public readonly Dictionary TypeSerrializers; + public readonly Dictionary PropertySerrializers; + + public SerrializeConfig() + { + ExcludedTypes = new HashSet(); + + ExcludedProperties = new HashSet(); + + TypeSerrializers = new Dictionary + { + { typeof(int), DefaultSerrialize }, + { typeof(double), DefaultSerrialize }, + { typeof(float), DefaultSerrialize }, + { typeof(string), DefaultSerrialize }, + { typeof(DateTime), DefaultSerrialize }, + { typeof(TimeSpan), DefaultSerrialize }, + { typeof(Guid), DefaultSerrialize }, + }; + + PropertySerrializers = new Dictionary(); + } + + public SerrializeConfig(SerrializeConfig old) + { + ExcludedTypes = new HashSet(old.ExcludedTypes); + + ExcludedProperties = new HashSet(old.ExcludedProperties); + + TypeSerrializers = new Dictionary(old.TypeSerrializers); + + PropertySerrializers = new Dictionary(old.PropertySerrializers); + } + + private string DefaultSerrialize(object obj) => obj.ToString(); + } +} diff --git a/ObjectPrinting/Tests/ObjectPrinterAcceptanceTests.cs b/ObjectPrinting/Tests/ObjectPrinterAcceptanceTests.cs index 4c8b2445..41e04632 100644 --- a/ObjectPrinting/Tests/ObjectPrinterAcceptanceTests.cs +++ b/ObjectPrinting/Tests/ObjectPrinterAcceptanceTests.cs @@ -1,4 +1,6 @@ using NUnit.Framework; +using System.Globalization; +using System; namespace ObjectPrinting.Tests { @@ -10,18 +12,36 @@ public void Demo() { var person = new Person { Name = "Alex", Age = 19 }; - var printer = ObjectPrinter.For(); - //1. Исключить из сериализации свойства определенного типа - //2. Указать альтернативный способ сериализации для определенного типа - //3. Для числовых типов указать культуру - //4. Настроить сериализацию конкретного свойства - //5. Настроить обрезание строковых свойств (метод должен быть виден только для строковых свойств) - //6. Исключить из сериализации конкретного свойства - - string s1 = printer.PrintToString(person); - - //7. Синтаксический сахар в виде метода расширения, сериализующего по-умолчанию + var printer = ObjectPrinter.For() + //1. Исключить из сериализации свойства определенного типа + .Exclude() + + //2. Указать альтернативный способ сериализации для определенного типа + .Print().Using(x => $"value - {x}") + + //3. Для числовых типов указать культуру + .Print().Using(CultureInfo.InvariantCulture) + + //4. Настроить сериализацию конкретного свойства + .Print(x => x.Name).Using(x => $"name - {x}") + + //5. Настроить обрезание строковых свойств (метод должен быть виден только для строковых свойств) + .Print().TrimmedToLength(10) + + //6. Исключить из сериализации конкретного свойства + .Exclude(x => x.Age); + + var s1 = printer.PrintToString(person); + + //7. Синтаксический сахар в виде метода расширения, сериализующего по-умолчанию + var s2 = person.PrintToString(); + //8. ...с конфигурированием + var s3 = person.PrintToString(x => x.Exclude(x => x.Name)); + + Console.WriteLine(s1); + Console.WriteLine(s2); + Console.WriteLine(s3); } } } \ No newline at end of file diff --git a/ObjectPrinting/Tests/ObjectPrinterTests.cs b/ObjectPrinting/Tests/ObjectPrinterTests.cs new file mode 100644 index 00000000..6c7c787c --- /dev/null +++ b/ObjectPrinting/Tests/ObjectPrinterTests.cs @@ -0,0 +1,187 @@ +using FluentAssertions; +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace ObjectPrinting.Tests +{ + [TestFixture] + public class ObjectPrinterTests + { + private Person person; + + [SetUp] + public void Setup() + { + var friend = new Person() + { + Name = "Ilya", + Age = 50, + Height = 111.2 + }; + + person = new Person + { + Id = new Guid(), + Name = "Ivan", + Age = 33, + Height = 200.1, + Friend = friend, + }; + } + + [Test] + public void PrintToString_ShouldReturnsString() + { + var result = ObjectPrinter.For().PrintToString(person); + + result.Should().BeOfType(); + } + + [Test] + public void PrintToString_WithExcludedTypes() + { + var result = ObjectPrinter.For() + .Exclude() + .PrintToString(person); + + result.Should().NotContain(nameof(person.Id)); + result.Should().NotContain($"{person.Id}"); + } + + [Test] + public void PrintToString_WithExcludedProperties() + { + var result = ObjectPrinter.For() + .Exclude(person => person.Name) + .PrintToString(person); + + result.Should().NotContainAll(nameof(person.Name)); + result.Should().NotContain(person.Name); + } + + [Test] + public void PrintToString_WithCustomTypeSerrializer() + { + var result = ObjectPrinter.For() + .Print().Using(i => i.ToString("X")) + .PrintToString(person); + + result.Should().ContainAll(new[] { nameof(person.Age), $"{person.Age.ToString("X")}" }); + } + + [Test] + public void PrintToString_WithCustomPropertySerrializer() + { + var result = ObjectPrinter.For() + .Print(p => p.Name) + .Using(name => $"{name} - is my name") + .PrintToString(person); + + result.Should().ContainAll(new[] { nameof(person.Name), $"{person.Name} - is my name" }); + } + + [TestCase("ru-RU")] + [TestCase("en-US")] + public void PrintToString_WithCultureType(string cultureType) + { + var culture = new CultureInfo(cultureType); + + var result = ObjectPrinter.For() + .Print() + .Using(culture) + .PrintToString(person); + + result.Should().ContainAll(new[] { nameof(person.Height), $"{person.Height.ToString(culture)}" }); + } + + [TestCase("Leonardo", 1, "L")] + [TestCase("Michelangelo", 2, "Mi")] + [TestCase("Raphael", 3, "Rap")] + [TestCase("Donatello", 4, "Dona")] + public void PrintToString_WithTrimLengthForString(string name, int maxLength, string expected) + { + person = new Person + { + Id = new Guid(), + Name = name, + Age = 20, + Height = 181.5, + }; + + var result = ObjectPrinter.For() + .Print(person => person.Name) + .TrimmedToLength(maxLength) + .PrintToString(person); + + result.Should().Contain(expected); + result.Should().NotContain(name); + } + + [Test] + public void PrintToString_SerializesArray() + { + var arr = new[] { 1, 2, 3, 4, 5 }; + + var result = ObjectPrinter.For().PrintToString(arr); + + result.Should().Contain("[ 1 2 3 4 5 ]"); + } + + [Test] + public void PrintToString_SerializesDictionary() + { + var dict = new Dictionary + { + { "первый", 1 }, + { "это второй", 2 } + }; + + var values = dict.Values.Select(value => value.ToString()).ToList(); + var keys = dict.Keys.ToList(); + + var str = ObjectPrinter.For>().PrintToString(dict); + + str.Should().ContainAll(values.Concat(keys)); + } + + [Test] + public void PrintToString_Immutable() + { + var printer1 = ObjectPrinter.For().Print(x => x.Name).Using(x => "Ilya"); + + var printer2 = printer1.Print(x => x.Name).Using(x => "Home"); + + var result1 = printer1.PrintToString(person); + var result2 = printer2.PrintToString(person); + + result1.Should().NotContain("Home"); + result1.Should().Contain("Ilya"); + + result2.Should().NotContain("Ilya"); + result2.Should().Contain("Home"); + } + + [Test] + public void PrintTostring_WhenUsingExtensionMethod() + { + var result = person.PrintToString(); + + var fields = new[] { + $"Name = {person.Name}", + $"Id = {person.Id}", + $"Age = {person.Age}", + $"Height = {person.Height}", + }; + + result.Should().BeOfType(); + + result.Should().ContainAll(fields); + result.Should().ContainAll(person.Friend.PrintToString().Split('\n')); + } + } +} diff --git a/ObjectPrinting/Tests/Person.cs b/ObjectPrinting/Tests/Person.cs index f9555955..34c43d31 100644 --- a/ObjectPrinting/Tests/Person.cs +++ b/ObjectPrinting/Tests/Person.cs @@ -8,5 +8,6 @@ public class Person public string Name { get; set; } public double Height { get; set; } public int Age { get; set; } + public Person Friend { get; set; } } } \ No newline at end of file From 0b86a4ba2dbd64ff94e0459a55e7d3de4c24fa38 Mon Sep 17 00:00:00 2001 From: qreaqtor Date: Fri, 13 Dec 2024 18:38:56 +0500 Subject: [PATCH 2/5] refactor --- ObjectPrinting/ObjectExtensions.cs | 4 -- ObjectPrinting/PrintingConfig.cs | 15 +++---- ObjectPrinting/PropertyPrintingConfig.cs | 4 -- .../PropertyPrintingConfigExtensions.cs | 4 -- ObjectPrinting/SerrializeConfig.cs | 3 -- ObjectPrinting/Tests/ObjectPrinterTests.cs | 40 ++++++++++++++----- 6 files changed, 39 insertions(+), 31 deletions(-) diff --git a/ObjectPrinting/ObjectExtensions.cs b/ObjectPrinting/ObjectExtensions.cs index c71769a8..c3821f23 100644 --- a/ObjectPrinting/ObjectExtensions.cs +++ b/ObjectPrinting/ObjectExtensions.cs @@ -1,8 +1,4 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace ObjectPrinting { diff --git a/ObjectPrinting/PrintingConfig.cs b/ObjectPrinting/PrintingConfig.cs index b5ecfaff..47fe2d45 100644 --- a/ObjectPrinting/PrintingConfig.cs +++ b/ObjectPrinting/PrintingConfig.cs @@ -1,6 +1,5 @@ using System; using System.Collections; -using System.Linq; using System.Linq.Expressions; using System.Reflection; using System.Text; @@ -78,14 +77,16 @@ private string SerializeDictionary(IDictionary dictionary, int nestingLevel) { var sb = new StringBuilder(); var identation = new string('\t', nestingLevel); - sb.Append(identation + "{" + Environment.NewLine); - foreach (DictionaryEntry keyValuePair in dictionary) + + sb.AppendLine(identation + "{"); + + foreach (var key in dictionary.Keys) { identation = new string('\t', nestingLevel + 1); - sb.Append(identation + "[" + PrintToString(keyValuePair.Key, nestingLevel + 1).Trim() + " - "); - sb.Append(PrintToString(keyValuePair.Value, 0).Trim()); - sb.Append("],"); - sb.Append(Environment.NewLine); + + sb.Append(identation + "[" + PrintToString(key, nestingLevel + 1).Trim() + " - ") + .Append(PrintToString(dictionary[key], 0).Trim()) + .AppendLine("],"); } return sb + "}" + Environment.NewLine; diff --git a/ObjectPrinting/PropertyPrintingConfig.cs b/ObjectPrinting/PropertyPrintingConfig.cs index 2ea0fce8..18fb9dce 100644 --- a/ObjectPrinting/PropertyPrintingConfig.cs +++ b/ObjectPrinting/PropertyPrintingConfig.cs @@ -1,9 +1,5 @@ using System; -using System.Collections.Generic; -using System.Linq; using System.Reflection; -using System.Text; -using System.Threading.Tasks; namespace ObjectPrinting { diff --git a/ObjectPrinting/PropertyPrintingConfigExtensions.cs b/ObjectPrinting/PropertyPrintingConfigExtensions.cs index ea648a5b..26be1ebe 100644 --- a/ObjectPrinting/PropertyPrintingConfigExtensions.cs +++ b/ObjectPrinting/PropertyPrintingConfigExtensions.cs @@ -1,9 +1,5 @@ using System; -using System.Collections.Generic; using System.Globalization; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace ObjectPrinting { diff --git a/ObjectPrinting/SerrializeConfig.cs b/ObjectPrinting/SerrializeConfig.cs index 7c87de38..63d75641 100644 --- a/ObjectPrinting/SerrializeConfig.cs +++ b/ObjectPrinting/SerrializeConfig.cs @@ -1,9 +1,6 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Reflection; -using System.Text; -using System.Threading.Tasks; namespace ObjectPrinting { diff --git a/ObjectPrinting/Tests/ObjectPrinterTests.cs b/ObjectPrinting/Tests/ObjectPrinterTests.cs index 6c7c787c..b173ac4e 100644 --- a/ObjectPrinting/Tests/ObjectPrinterTests.cs +++ b/ObjectPrinting/Tests/ObjectPrinterTests.cs @@ -4,8 +4,6 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace ObjectPrinting.Tests { @@ -19,6 +17,7 @@ public void Setup() { var friend = new Person() { + Id = Guid.NewGuid(), Name = "Ilya", Age = 50, Height = 111.2 @@ -26,7 +25,7 @@ public void Setup() person = new Person { - Id = new Guid(), + Id = Guid.NewGuid(), Name = "Ivan", Age = 33, Height = 200.1, @@ -51,6 +50,7 @@ public void PrintToString_WithExcludedTypes() result.Should().NotContain(nameof(person.Id)); result.Should().NotContain($"{person.Id}"); + result.Should().NotContain($"{person.Friend.Id}"); } [Test] @@ -60,8 +60,9 @@ public void PrintToString_WithExcludedProperties() .Exclude(person => person.Name) .PrintToString(person); - result.Should().NotContainAll(nameof(person.Name)); + result.Should().NotContain(nameof(person.Name)); result.Should().NotContain(person.Name); + result.Should().NotContain($"{person.Friend.Name}"); } [Test] @@ -71,7 +72,14 @@ public void PrintToString_WithCustomTypeSerrializer() .Print().Using(i => i.ToString("X")) .PrintToString(person); - result.Should().ContainAll(new[] { nameof(person.Age), $"{person.Age.ToString("X")}" }); + var values = new[] + { + nameof(person.Age), + $"{person.Age.ToString("X")}", + $"{person.Friend.Age.ToString("X")}" + }; + + result.Should().ContainAll(values); } [Test] @@ -82,7 +90,14 @@ public void PrintToString_WithCustomPropertySerrializer() .Using(name => $"{name} - is my name") .PrintToString(person); - result.Should().ContainAll(new[] { nameof(person.Name), $"{person.Name} - is my name" }); + var values = new[] + { + nameof(person.Name), + $"{person.Name} - is my name", + $"{person.Friend.Name} - is my name" + }; + + result.Should().ContainAll(values); } [TestCase("ru-RU")] @@ -96,7 +111,14 @@ public void PrintToString_WithCultureType(string cultureType) .Using(culture) .PrintToString(person); - result.Should().ContainAll(new[] { nameof(person.Height), $"{person.Height.ToString(culture)}" }); + var values = new[] + { + nameof(person.Height), + $"{person.Height.ToString(culture)}", + $"{person.Friend.Height.ToString(culture)}" + }; + + result.Should().ContainAll(values); } [TestCase("Leonardo", 1, "L")] @@ -105,7 +127,7 @@ public void PrintToString_WithCultureType(string cultureType) [TestCase("Donatello", 4, "Dona")] public void PrintToString_WithTrimLengthForString(string name, int maxLength, string expected) { - person = new Person + var person = new Person { Id = new Guid(), Name = name, @@ -167,7 +189,7 @@ public void PrintToString_Immutable() } [Test] - public void PrintTostring_WhenUsingExtensionMethod() + public void PrintToString_WhenUsingExtensionMethod() { var result = person.PrintToString(); From cb0c98a82a5356d05458744097fda471a848421f Mon Sep 17 00:00:00 2001 From: qreaqtor Date: Fri, 13 Dec 2024 19:28:53 +0500 Subject: [PATCH 3/5] cyclic references --- ObjectPrinting/PrintingConfig.cs | 19 +++++++++++++++++-- ObjectPrinting/Tests/ObjectPrinterTests.cs | 13 +++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/ObjectPrinting/PrintingConfig.cs b/ObjectPrinting/PrintingConfig.cs index 47fe2d45..ddccf6a8 100644 --- a/ObjectPrinting/PrintingConfig.cs +++ b/ObjectPrinting/PrintingConfig.cs @@ -1,5 +1,6 @@ using System; using System.Collections; +using System.Collections.Generic; using System.Linq.Expressions; using System.Reflection; using System.Text; @@ -9,15 +10,18 @@ namespace ObjectPrinting public class PrintingConfig { private readonly SerrializeConfig serrializeConfig; + private HashSet serializedObjects; public PrintingConfig() { serrializeConfig = new SerrializeConfig(); + serializedObjects = new HashSet(); } public PrintingConfig(SerrializeConfig serrializeConfig) { this.serrializeConfig = serrializeConfig; + serializedObjects = new HashSet(); } private string PrintToString(object obj, int nestingLevel) @@ -25,6 +29,8 @@ private string PrintToString(object obj, int nestingLevel) if (obj == null) return "null" + Environment.NewLine; + serializedObjects.Add(obj); + var objType = obj.GetType(); if (serrializeConfig.TypeSerrializers.TryGetValue(objType, out var serrialize)) @@ -45,7 +51,9 @@ private string PrintToString(object obj, int nestingLevel) var propValue = propertyInfo.GetValue(obj); if (serrializeConfig.ExcludedTypes.Contains(propType) || - serrializeConfig.ExcludedProperties.Contains(propertyInfo)) + serrializeConfig.ExcludedProperties.Contains(propertyInfo) || + serializedObjects.Contains(propValue) + ) continue; var contains = serrializeConfig.PropertySerrializers.TryGetValue(propertyInfo, out serrialize); @@ -92,7 +100,14 @@ private string SerializeDictionary(IDictionary dictionary, int nestingLevel) return sb + "}" + Environment.NewLine; } - public string PrintToString(TOwner obj) => PrintToString(obj, 0); + public string PrintToString(TOwner obj) + { + var result = PrintToString(obj, 0); + + serializedObjects.Clear(); + + return result; + } public PrintingConfig Exclude() { diff --git a/ObjectPrinting/Tests/ObjectPrinterTests.cs b/ObjectPrinting/Tests/ObjectPrinterTests.cs index b173ac4e..1ab124d6 100644 --- a/ObjectPrinting/Tests/ObjectPrinterTests.cs +++ b/ObjectPrinting/Tests/ObjectPrinterTests.cs @@ -205,5 +205,18 @@ public void PrintToString_WhenUsingExtensionMethod() result.Should().ContainAll(fields); result.Should().ContainAll(person.Friend.PrintToString().Split('\n')); } + + [Test] + public void PrintToString_NotThrowStackOverflow_WithCyclicReferences() + { + var person1 = new Person(); + var person2 = new Person { Friend = person1 }; + + person1.Friend = person2; + + var printAction = new Action(() => person1.PrintToString()); + + printAction.Should().NotThrow(); + } } } From b8c734fe57bd9c287b9460974c76aee9446420fd Mon Sep 17 00:00:00 2001 From: qreaqtor Date: Fri, 13 Dec 2024 20:35:38 +0500 Subject: [PATCH 4/5] fields config for printing --- ObjectPrinting/PrintingConfig.cs | 37 +++++++--- ObjectPrinting/PropertyPrintingConfig.cs | 25 ++----- ObjectPrinting/SerrializeConfig.cs | 83 ++++++++++++++++++++-- ObjectPrinting/Tests/ObjectPrinterTests.cs | 34 ++++++++- ObjectPrinting/Tests/Person.cs | 2 + 5 files changed, 148 insertions(+), 33 deletions(-) diff --git a/ObjectPrinting/PrintingConfig.cs b/ObjectPrinting/PrintingConfig.cs index ddccf6a8..94a81c86 100644 --- a/ObjectPrinting/PrintingConfig.cs +++ b/ObjectPrinting/PrintingConfig.cs @@ -1,6 +1,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Data; using System.Linq.Expressions; using System.Reflection; using System.Text; @@ -33,7 +34,7 @@ private string PrintToString(object obj, int nestingLevel) var objType = obj.GetType(); - if (serrializeConfig.TypeSerrializers.TryGetValue(objType, out var serrialize)) + if (serrializeConfig.TryGetSerializer(objType, out var serrialize)) return serrialize.DynamicInvoke(obj) + Environment.NewLine; if (obj is ICollection collection) @@ -50,13 +51,33 @@ private string PrintToString(object obj, int nestingLevel) var propType = propertyInfo.PropertyType; var propValue = propertyInfo.GetValue(obj); - if (serrializeConfig.ExcludedTypes.Contains(propType) || - serrializeConfig.ExcludedProperties.Contains(propertyInfo) || + if (serrializeConfig.IsExcludedType(propType) || + serrializeConfig.IsExcludedMember(propertyInfo) || serializedObjects.Contains(propValue) ) continue; - var contains = serrializeConfig.PropertySerrializers.TryGetValue(propertyInfo, out serrialize); + var contains = serrializeConfig.TryGetSerializer(propertyInfo, out serrialize); + + var objStr = contains ? + serrialize.DynamicInvoke(propValue) + Environment.NewLine : + PrintToString(propertyInfo.GetValue(obj), nestingLevel + 1); + + sb.Append(identation + propertyInfo.Name + " = " + objStr); + } + + foreach (var propertyInfo in objType.GetFields()) + { + var propType = propertyInfo.DeclaringType; + var propValue = propertyInfo.GetValue(obj); + + if (serrializeConfig.IsExcludedType(propType) || + serrializeConfig.IsExcludedMember(propertyInfo) || + serializedObjects.Contains(propValue) + ) + continue; + + var contains = serrializeConfig.TryGetSerializer(propertyInfo, out serrialize); var objStr = contains ? serrialize.DynamicInvoke(propValue) + Environment.NewLine : @@ -113,7 +134,7 @@ public PrintingConfig Exclude() { var config = new PrintingConfig(serrializeConfig); - config.serrializeConfig.ExcludedTypes.Add(typeof(T)); + config.serrializeConfig.ExcludeType(typeof(T)); return config; } @@ -123,9 +144,8 @@ public PrintingConfig Exclude(Expression> mem var config = new PrintingConfig(serrializeConfig); var memberExp = memberSelector.Body as MemberExpression; - var propInfo = memberExp.Member as PropertyInfo; - config.serrializeConfig.ExcludedProperties.Add(propInfo); + config.serrializeConfig.ExcludeMember(memberExp.Member); return config; } @@ -138,9 +158,8 @@ public PropertyPrintingConfig Print() public PropertyPrintingConfig Print(Expression> memberSelector) { var memberExp = memberSelector.Body as MemberExpression; - var propInfo = memberExp.Member as PropertyInfo; - return new PropertyPrintingConfig(serrializeConfig, propInfo); + return new PropertyPrintingConfig(serrializeConfig, memberExp.Member); } } } \ No newline at end of file diff --git a/ObjectPrinting/PropertyPrintingConfig.cs b/ObjectPrinting/PropertyPrintingConfig.cs index 18fb9dce..567c3d18 100644 --- a/ObjectPrinting/PropertyPrintingConfig.cs +++ b/ObjectPrinting/PropertyPrintingConfig.cs @@ -6,33 +6,20 @@ namespace ObjectPrinting public class PropertyPrintingConfig { private readonly SerrializeConfig serrializeConfig; - private readonly PropertyInfo propertyInfo; + private readonly MemberInfo memberInfo; - public PropertyPrintingConfig(SerrializeConfig serrializeConfig, PropertyInfo propertyInfo = null) + public PropertyPrintingConfig(SerrializeConfig serrializeConfig, MemberInfo memberInfo = null) { this.serrializeConfig = new SerrializeConfig(serrializeConfig); - this.propertyInfo = propertyInfo; + this.memberInfo = memberInfo; } public PrintingConfig Using(Func serrializer) { - if (propertyInfo == null) - { - var type = typeof(TProp); - - if (serrializeConfig.TypeSerrializers.ContainsKey(type)) - serrializeConfig.TypeSerrializers[type] = serrializer; - else - serrializeConfig.TypeSerrializers.Add(type, serrializer); - } + if (memberInfo == null) + serrializeConfig.AddTypeSerializer(typeof(TProp), serrializer); else - { - if (serrializeConfig.PropertySerrializers.ContainsKey(propertyInfo)) - serrializeConfig.PropertySerrializers[propertyInfo] = serrializer; - else - serrializeConfig.PropertySerrializers.Add(propertyInfo, serrializer); - } - + serrializeConfig.AddMemberSerializer(memberInfo, serrializer); return new PrintingConfig(serrializeConfig); } diff --git a/ObjectPrinting/SerrializeConfig.cs b/ObjectPrinting/SerrializeConfig.cs index 63d75641..6bd0996f 100644 --- a/ObjectPrinting/SerrializeConfig.cs +++ b/ObjectPrinting/SerrializeConfig.cs @@ -13,11 +13,14 @@ public class SerrializeConfig * где хранятся все исключенные типы. */ - public readonly HashSet ExcludedTypes; - public readonly HashSet ExcludedProperties; + private readonly HashSet ExcludedTypes; + private readonly HashSet ExcludedProperties; + private readonly HashSet ExcludedFields; + + private readonly Dictionary TypeSerrializers; + private readonly Dictionary PropertySerrializers; + private readonly Dictionary FieldSerrializers; - public readonly Dictionary TypeSerrializers; - public readonly Dictionary PropertySerrializers; public SerrializeConfig() { @@ -25,6 +28,8 @@ public SerrializeConfig() ExcludedProperties = new HashSet(); + ExcludedFields = new HashSet(); + TypeSerrializers = new Dictionary { { typeof(int), DefaultSerrialize }, @@ -37,6 +42,8 @@ public SerrializeConfig() }; PropertySerrializers = new Dictionary(); + + FieldSerrializers = new Dictionary(); } public SerrializeConfig(SerrializeConfig old) @@ -45,11 +52,79 @@ public SerrializeConfig(SerrializeConfig old) ExcludedProperties = new HashSet(old.ExcludedProperties); + ExcludedFields = new HashSet(old.ExcludedFields); + TypeSerrializers = new Dictionary(old.TypeSerrializers); PropertySerrializers = new Dictionary(old.PropertySerrializers); + + FieldSerrializers = new Dictionary(old.FieldSerrializers); } private string DefaultSerrialize(object obj) => obj.ToString(); + + public bool IsExcludedMember(MemberInfo member) + { + switch (member.MemberType) + { + case MemberTypes.Field: + return ExcludedFields.Contains(member as FieldInfo); + case MemberTypes.Property: + return ExcludedProperties.Contains(member as PropertyInfo); + default: + return false; + } + } + + public bool IsExcludedType(Type type) => ExcludedTypes.Contains(type); + + public bool TryGetSerializer(MemberInfo member, out Delegate serializer) + { + switch (member.MemberType) + { + case MemberTypes.Field: + return FieldSerrializers.TryGetValue(member as FieldInfo, out serializer); + case MemberTypes.Property: + return PropertySerrializers.TryGetValue(member as PropertyInfo, out serializer); + default: + serializer = null; + return false; + } + } + + public void AddMemberSerializer(MemberInfo member, Delegate serializer) + { + switch (member.MemberType) + { + case MemberTypes.Field: + FieldSerrializers[member as FieldInfo] = serializer; + break; + case MemberTypes.Property: + PropertySerrializers[member as PropertyInfo] = serializer; + break; + default: + TypeSerrializers[member.DeclaringType] = serializer; + break; + } + } + + public bool ExcludeMember(MemberInfo member) + { + switch (member.MemberType) + { + case MemberTypes.Field: + return ExcludedFields.Add(member as FieldInfo); + case MemberTypes.Property: + return ExcludedProperties.Add(member as PropertyInfo); + default: + return false; + } + } + + public bool ExcludeType(Type type) => ExcludedTypes.Add(type); + + public void AddTypeSerializer(Type type, Delegate serializer) => TypeSerrializers[type] = serializer; + + public bool TryGetSerializer(Type type, out Delegate serializer) => TypeSerrializers.TryGetValue(type, out serializer); } } diff --git a/ObjectPrinting/Tests/ObjectPrinterTests.cs b/ObjectPrinting/Tests/ObjectPrinterTests.cs index 1ab124d6..bc309c30 100644 --- a/ObjectPrinting/Tests/ObjectPrinterTests.cs +++ b/ObjectPrinting/Tests/ObjectPrinterTests.cs @@ -20,7 +20,8 @@ public void Setup() Id = Guid.NewGuid(), Name = "Ilya", Age = 50, - Height = 111.2 + Height = 111.2, + Haircut = "Fade" }; person = new Person @@ -30,6 +31,7 @@ public void Setup() Age = 33, Height = 200.1, Friend = friend, + Haircut = "Undercut" }; } @@ -65,6 +67,18 @@ public void PrintToString_WithExcludedProperties() result.Should().NotContain($"{person.Friend.Name}"); } + [Test] + public void PrintToString_WithExcludedFields() + { + var result = ObjectPrinter.For() + .Exclude(person => person.Haircut) + .PrintToString(person); + + result.Should().NotContain(nameof(person.Haircut)); + result.Should().NotContain(person.Haircut); + result.Should().NotContain($"{person.Friend.Haircut}"); + } + [Test] public void PrintToString_WithCustomTypeSerrializer() { @@ -100,6 +114,24 @@ public void PrintToString_WithCustomPropertySerrializer() result.Should().ContainAll(values); } + [Test] + public void PrintToString_WithCustomFieldSerrializer() + { + var result = ObjectPrinter.For() + .Print(p => p.Haircut) + .Using(name => $"{name} - is my favourite haircut") + .PrintToString(person); + + var values = new[] + { + nameof(person.Haircut), + $"{person.Haircut} - is my favourite haircut", + $"{person.Friend.Haircut} - is my favourite haircut" + }; + + result.Should().ContainAll(values); + } + [TestCase("ru-RU")] [TestCase("en-US")] public void PrintToString_WithCultureType(string cultureType) diff --git a/ObjectPrinting/Tests/Person.cs b/ObjectPrinting/Tests/Person.cs index 34c43d31..3c48b4ea 100644 --- a/ObjectPrinting/Tests/Person.cs +++ b/ObjectPrinting/Tests/Person.cs @@ -9,5 +9,7 @@ public class Person public double Height { get; set; } public int Age { get; set; } public Person Friend { get; set; } + + public string Haircut; } } \ No newline at end of file From 107f1f6e33836f7386e7173cfa487745d57949ee Mon Sep 17 00:00:00 2001 From: qreaqtor Date: Fri, 13 Dec 2024 21:12:15 +0500 Subject: [PATCH 5/5] PrintToString refactor --- ObjectPrinting/PrintingConfig.cs | 91 ++++++++++++++++++++------------ 1 file changed, 56 insertions(+), 35 deletions(-) diff --git a/ObjectPrinting/PrintingConfig.cs b/ObjectPrinting/PrintingConfig.cs index 94a81c86..eff2f0ad 100644 --- a/ObjectPrinting/PrintingConfig.cs +++ b/ObjectPrinting/PrintingConfig.cs @@ -32,61 +32,82 @@ private string PrintToString(object obj, int nestingLevel) serializedObjects.Add(obj); - var objType = obj.GetType(); - - if (serrializeConfig.TryGetSerializer(objType, out var serrialize)) + if (serrializeConfig.TryGetSerializer(obj.GetType(), out var serrialize)) return serrialize.DynamicInvoke(obj) + Environment.NewLine; if (obj is ICollection collection) return SerializeCollection(collection, nestingLevel); - var identation = new string('\t', nestingLevel + 1); + return PrintToStringObject(obj, nestingLevel); + } + private string PrintToStringObject(object obj, int nestingLevel) + { var sb = new StringBuilder(); - sb.AppendLine(objType.Name); - - foreach (var propertyInfo in objType.GetProperties()) - { - var propType = propertyInfo.PropertyType; - var propValue = propertyInfo.GetValue(obj); + sb.AppendLine(obj.GetType().Name); - if (serrializeConfig.IsExcludedType(propType) || - serrializeConfig.IsExcludedMember(propertyInfo) || - serializedObjects.Contains(propValue) - ) - continue; + PrintToStringProperties(sb, obj, nestingLevel); + PrintToStringFields(sb, obj, nestingLevel); - var contains = serrializeConfig.TryGetSerializer(propertyInfo, out serrialize); + return sb.ToString(); + } - var objStr = contains ? - serrialize.DynamicInvoke(propValue) + Environment.NewLine : - PrintToString(propertyInfo.GetValue(obj), nestingLevel + 1); + private void PrintToStringProperties(StringBuilder sb, object obj, int nestingLevel) + { + foreach (var property in obj.GetType().GetProperties()) + { + var (objStr, ok) = PrintToStringMember( + property, + property.PropertyType, + property.GetValue(obj), + nestingLevel + ); + + if (!ok) + continue; - sb.Append(identation + propertyInfo.Name + " = " + objStr); + sb.Append(objStr); } + } - foreach (var propertyInfo in objType.GetFields()) + private void PrintToStringFields(StringBuilder sb, object obj, int nestingLevel) + { + foreach (var field in obj.GetType().GetFields()) { - var propType = propertyInfo.DeclaringType; - var propValue = propertyInfo.GetValue(obj); - - if (serrializeConfig.IsExcludedType(propType) || - serrializeConfig.IsExcludedMember(propertyInfo) || - serializedObjects.Contains(propValue) - ) + var (objStr, ok) = PrintToStringMember( + field, + field.DeclaringType, + field.GetValue(obj), + nestingLevel + ); + + if (!ok) continue; - var contains = serrializeConfig.TryGetSerializer(propertyInfo, out serrialize); + sb.Append(objStr); + } + } - var objStr = contains ? - serrialize.DynamicInvoke(propValue) + Environment.NewLine : - PrintToString(propertyInfo.GetValue(obj), nestingLevel + 1); + private (string?, bool) PrintToStringMember(MemberInfo member, Type? type, object? value, int nestingLevel) + { + if (type is null || + value is null || + serrializeConfig.IsExcludedType(type) || + serrializeConfig.IsExcludedMember(member) || + serializedObjects.Contains(value) + ) + return (null, false); - sb.Append(identation + propertyInfo.Name + " = " + objStr); - } + var contains = serrializeConfig.TryGetSerializer(member, out var serrialize); - return sb.ToString(); + var objStr = contains ? + serrialize.DynamicInvoke(value) + Environment.NewLine : + PrintToString(value, nestingLevel + 1); + + var identation = new string('\t', nestingLevel + 1); + + return (identation + member.Name + " = " + objStr, true); } private string SerializeCollection(ICollection collection, int nestingLevel)