From ab1a23c84add8df44cd617a0e021ab6b5d79c256 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A2=D0=B8=D0=BC=D1=83=D1=80=20=D0=91=D0=B0=D0=B1=D0=B0?= =?UTF-8?q?=D0=B5=D0=B2?= Date: Thu, 20 Nov 2025 02:00:43 +0500 Subject: [PATCH 1/3] =?UTF-8?q?=D0=A0=D0=B5=D0=B0=D0=BB=D0=B8=D0=B7=D0=BE?= =?UTF-8?q?=D0=B2=D0=B0=D0=BD=20=D0=B2=D0=B5=D1=81=D1=8C=20=D1=84=D1=83?= =?UTF-8?q?=D0=BD=D0=BA=D1=86=D0=B8=D0=BE=D0=BD=D0=B0=D0=BB=20=D0=B8=D0=B7?= =?UTF-8?q?=20Homework.md,=20=D1=82=D0=B5=D1=81=D1=82=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ObjectPrinting/ObjectPrinter.cs | 10 - ObjectPrinting/PrintingConfig.cs | 41 -- .../Solved/IPropertyPrintingConfig.cs | 9 + ObjectPrinting/Solved/ObjectExtensions.cs | 11 +- ObjectPrinting/Solved/ObjectPrinter.cs | 11 +- ObjectPrinting/Solved/PrintingConfig.cs | 277 +++++++++++-- .../Solved/PropertyPrintingConfig.cs | 42 +- .../PropertyPrintingConfigExtensions.cs | 23 +- ObjectPrinting/Solved/Tests/Person.cs | 12 - .../Tests/ObjectPrinterAcceptanceTests.cs | 27 -- ObjectPrinting/Tests/Person.cs | 12 - ObjectPrintingTests/CyclicReference.cs | 7 + .../ObjectPrinterAcceptanceTests.cs | 7 +- ObjectPrintingTests/ObjectPrinterTests.cs | 386 ++++++++++++++++++ .../ObjectPrintingTests.csproj | 28 ++ ObjectPrintingTests/Person.cs | 18 + fluent-api.sln | 6 + fluent-api.sln.DotSettings | 3 + 18 files changed, 748 insertions(+), 182 deletions(-) delete mode 100644 ObjectPrinting/ObjectPrinter.cs delete mode 100644 ObjectPrinting/PrintingConfig.cs create mode 100644 ObjectPrinting/Solved/IPropertyPrintingConfig.cs delete mode 100644 ObjectPrinting/Solved/Tests/Person.cs delete mode 100644 ObjectPrinting/Tests/ObjectPrinterAcceptanceTests.cs delete mode 100644 ObjectPrinting/Tests/Person.cs create mode 100644 ObjectPrintingTests/CyclicReference.cs rename {ObjectPrinting/Solved/Tests => ObjectPrintingTests}/ObjectPrinterAcceptanceTests.cs (87%) create mode 100644 ObjectPrintingTests/ObjectPrinterTests.cs create mode 100644 ObjectPrintingTests/ObjectPrintingTests.csproj create mode 100644 ObjectPrintingTests/Person.cs diff --git a/ObjectPrinting/ObjectPrinter.cs b/ObjectPrinting/ObjectPrinter.cs deleted file mode 100644 index 3c7867c32..000000000 --- a/ObjectPrinting/ObjectPrinter.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace ObjectPrinting -{ - public class ObjectPrinter - { - public static PrintingConfig For() - { - return new PrintingConfig(); - } - } -} \ No newline at end of file diff --git a/ObjectPrinting/PrintingConfig.cs b/ObjectPrinting/PrintingConfig.cs deleted file mode 100644 index a9e082117..000000000 --- a/ObjectPrinting/PrintingConfig.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System; -using System.Linq; -using System.Text; - -namespace ObjectPrinting -{ - public class PrintingConfig - { - public string PrintToString(TOwner obj) - { - return PrintToString(obj, 0); - } - - private string PrintToString(object obj, int nestingLevel) - { - //TODO apply configurations - if (obj == null) - return "null" + Environment.NewLine; - - var finalTypes = new[] - { - typeof(int), typeof(double), typeof(float), typeof(string), - typeof(DateTime), typeof(TimeSpan) - }; - if (finalTypes.Contains(obj.GetType())) - return obj + Environment.NewLine; - - var identation = new string('\t', nestingLevel + 1); - var sb = new StringBuilder(); - var type = obj.GetType(); - sb.AppendLine(type.Name); - foreach (var propertyInfo in type.GetProperties()) - { - sb.Append(identation + propertyInfo.Name + " = " + - PrintToString(propertyInfo.GetValue(obj), - nestingLevel + 1)); - } - return sb.ToString(); - } - } -} \ No newline at end of file diff --git a/ObjectPrinting/Solved/IPropertyPrintingConfig.cs b/ObjectPrinting/Solved/IPropertyPrintingConfig.cs new file mode 100644 index 000000000..8753cb1cf --- /dev/null +++ b/ObjectPrinting/Solved/IPropertyPrintingConfig.cs @@ -0,0 +1,9 @@ +using System.Reflection; + +namespace ObjectPrinting.Solved; + +public interface IPropertyPrintingConfig +{ + public PrintingConfig ParentConfig { get; } + public MemberInfo? MemberInfo { get; } +} \ No newline at end of file diff --git a/ObjectPrinting/Solved/ObjectExtensions.cs b/ObjectPrinting/Solved/ObjectExtensions.cs index b0c94553c..3dbe326c4 100644 --- a/ObjectPrinting/Solved/ObjectExtensions.cs +++ b/ObjectPrinting/Solved/ObjectExtensions.cs @@ -1,10 +1,9 @@ -namespace ObjectPrinting.Solved +namespace ObjectPrinting.Solved; + +public static class ObjectExtensions { - public static class ObjectExtensions + public static string? PrintToString(this T obj) { - public static string PrintToString(this T obj) - { - return ObjectPrinter.For().PrintToString(obj); - } + return ObjectPrinter.For().PrintToString(obj); } } \ No newline at end of file diff --git a/ObjectPrinting/Solved/ObjectPrinter.cs b/ObjectPrinting/Solved/ObjectPrinter.cs index 540ee769c..d9e5aec43 100644 --- a/ObjectPrinting/Solved/ObjectPrinter.cs +++ b/ObjectPrinting/Solved/ObjectPrinter.cs @@ -1,10 +1,9 @@ -namespace ObjectPrinting.Solved +namespace ObjectPrinting.Solved; + +public class ObjectPrinter { - public class ObjectPrinter + public static PrintingConfig For() { - public static PrintingConfig For() - { - return new PrintingConfig(); - } + return new PrintingConfig(); } } \ No newline at end of file diff --git a/ObjectPrinting/Solved/PrintingConfig.cs b/ObjectPrinting/Solved/PrintingConfig.cs index 0ec5aeb2b..386ef74c8 100644 --- a/ObjectPrinting/Solved/PrintingConfig.cs +++ b/ObjectPrinting/Solved/PrintingConfig.cs @@ -1,62 +1,273 @@ 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.Solved +namespace ObjectPrinting.Solved; + +public class PrintingConfig { - public class PrintingConfig + private readonly HashSet excludedTypes = []; + private readonly HashSet excludedMembers = []; + private readonly Dictionary> typeSerializers = new(); + private readonly Dictionary> memberSerializers = new(); + private readonly Dictionary typeCultures = new(); + private readonly Dictionary memberCultures = new(); + + private readonly HashSet parsedObjects = []; + + private readonly Type[] primitiveTypes = + [ + typeof(int), typeof(double), typeof(float), typeof(string), + typeof(DateTime), typeof(TimeSpan), typeof(decimal), typeof(Guid) + ]; + + public PropertyPrintingConfig Printing() + { + return new PropertyPrintingConfig(this, null); + } + + public PropertyPrintingConfig Printing(Expression> memberSelector) + { + return new PropertyPrintingConfig(this, GetMemberInfo(memberSelector)); + } + + public PrintingConfig Excluding(Expression> memberSelector) + { + var member = GetMemberInfo(memberSelector); + excludedMembers.Add(member); + return this; + } + + public PrintingConfig Excluding() { - public PropertyPrintingConfig Printing() + excludedTypes.Add(typeof(TPropType)); + return this; + } + + public string? PrintToString(TOwner obj) + { + return PrintToString(obj, 0, null); + } + + private string? PrintToString(object? obj, int nestingLevel, MemberInfo? member) + { + if (TryPrintNullOrExcluded(obj, nestingLevel, out var nullOrExcludedResult)) + return nullOrExcludedResult; + + var type = obj!.GetType(); + + if (TryPrintSimpleType(obj, type, member, out var simpleResult)) + return simpleResult; + + if (!type.IsValueType && obj is not string) { - return new PropertyPrintingConfig(this); + if (!parsedObjects.Add(obj)) + return $"Cyclic reference at {type.Name}" + Environment.NewLine; } + + var indentation = new string('\t', nestingLevel + 1); + + if (TryProcessDictionary(obj, type, indentation, nestingLevel, out var dictResult)) + return dictResult; + + if (TryProcessEnumerable(obj, type, indentation, nestingLevel, out var collectionResult)) + return collectionResult; + + return PrintComplexType(obj, type, indentation, nestingLevel); + } + + private string PrintComplexType(object obj, Type type, string indentation, int nestingLevel) + { + var sb = new StringBuilder(); + sb.AppendLine(type.Name); - public PropertyPrintingConfig Printing(Expression> memberSelector) + var printableMembers = GetPrintableMembers(type); + foreach (var memberInfo in printableMembers) { - return new PropertyPrintingConfig(this); + var value = GetMemberValue(memberInfo, obj); + sb.Append($"{indentation}{memberInfo.Name} = {PrintToString(value, nestingLevel + 1, memberInfo)}"); } - public PrintingConfig Excluding(Expression> memberSelector) + return sb.ToString(); + } + + private bool TryPrintNullOrExcluded(object? obj, int nestingLevel, out string? result) + { + if (obj is null) { - return this; + result = "null" + Environment.NewLine; + return true; } - internal PrintingConfig Excluding() + var type = obj.GetType(); + + if (excludedTypes.Contains(type) && nestingLevel > 0) { - return this; + result = string.Empty; + return true; } - public string PrintToString(TOwner obj) + result = null; + return false; + } + + private bool TryProcessDictionary(object? obj, Type type, string indentation, + int nestingLevel, out string? result) + { + var sb = new StringBuilder(); + if (obj is IDictionary dict) { - return PrintToString(obj, 0); + sb.AppendLine(type.Name); + foreach (DictionaryEntry entry in dict) + { + sb.Append($"{indentation}Key = {PrintToString(entry.Key, nestingLevel + 1, null)}"); + sb.Append($"{indentation}Value = {PrintToString(entry.Value, nestingLevel + 1, null)}"); + } + + result = sb.ToString(); + return true; } - private string PrintToString(object obj, int nestingLevel) - { - //TODO apply configurations - if (obj == null) - return "null" + Environment.NewLine; + result = null; + return false; + } - var finalTypes = new[] - { - typeof(int), typeof(double), typeof(float), typeof(string), - typeof(DateTime), typeof(TimeSpan) - }; - if (finalTypes.Contains(obj.GetType())) - return obj + Environment.NewLine; - - var identation = new string('\t', nestingLevel + 1); - var sb = new StringBuilder(); - var type = obj.GetType(); + private bool TryProcessEnumerable(object? obj, Type type, string indentation, + int nestingLevel, out string? result) + { + var sb = new StringBuilder(); + if (obj is IEnumerable enumerable and not string) + { sb.AppendLine(type.Name); - foreach (var propertyInfo in type.GetProperties()) + var index = 0; + foreach (var item in enumerable) { - sb.Append(identation + propertyInfo.Name + " = " + - PrintToString(propertyInfo.GetValue(obj), - nestingLevel + 1)); + sb.Append($"{indentation}[{index}] = {PrintToString(item, nestingLevel + 1, null)}"); + index++; } - return sb.ToString(); + + result = sb.ToString(); + return true; + } + result = null; + return false; + } + + private bool TryPrintSimpleType(object obj, Type type, MemberInfo? memberInfo, out string? result) + { + if (memberInfo != null && memberSerializers.TryGetValue(memberInfo, out var memberSerializer)) + { + result = memberSerializer(obj) + Environment.NewLine; + return true; + } + + if (typeSerializers.TryGetValue(type, out var typeSerializer)) + { + result = typeSerializer(obj) + Environment.NewLine; + return true; } + + if (memberInfo != null && memberCultures.TryGetValue(memberInfo, out var memberCulture) && + obj is IFormattable formattable1) + { + result = formattable1.ToString(null, memberCulture) + Environment.NewLine; + return true; + } + + if (typeCultures.TryGetValue(type, out var typeCulture) && obj is IFormattable formattable2) + { + result = formattable2.ToString(null, typeCulture) + Environment.NewLine; + return true; + } + + if (primitiveTypes.Contains(type) || type.IsEnum) + { + result = obj + Environment.NewLine; + return true; + } + + result = null; + return false; + } + + private MemberInfo GetMemberInfo(Expression> memberSelector) + { + if (memberSelector.Body is MemberExpression memberExpression) + return memberExpression.Member; + throw new ArgumentException(); + } + + private List GetPrintableMembers(Type type) + { + var result = new List(); + var flags = BindingFlags.Instance | BindingFlags.Public; + foreach (var propertyInfo in type.GetProperties(flags)) + { + if (!propertyInfo.CanRead) + continue; + + if (IsMemberExcluded(propertyInfo, propertyInfo.PropertyType)) + continue; + + result.Add(propertyInfo); + } + + foreach (var fieldInfo in type.GetFields(flags)) + { + if (IsMemberExcluded(fieldInfo, fieldInfo.FieldType)) + continue; + + result.Add(fieldInfo); + } + return result; + } + + private bool IsMemberExcluded(MemberInfo member, Type memberType) + { + return excludedMembers.Contains(member) || excludedTypes.Contains(memberType); + } + + private object? GetMemberValue(MemberInfo member, object obj) + { + return member switch + { + PropertyInfo p => p.GetValue(obj), + FieldInfo f => f.GetValue(obj), + _ => null + }; + } + + internal void AddTypeSerializer(Func serialize) + { + typeSerializers[typeof(TPropType)] = o => serialize((TPropType)o); + } + + internal void AddMemberSerializer(MemberInfo? member, Func serialize) + { + ArgumentNullException.ThrowIfNull(member); + memberSerializers[member] = o => serialize((TPropType)o); + } + + internal void AddTypeCulture(CultureInfo culture) + { + typeCultures[typeof(TPropType)] = culture; + } + + internal void AddMemberCulture(MemberInfo member, CultureInfo culture) + { + if (member == null) + throw new ArgumentNullException(nameof(member)); + + memberCultures[member] = culture; + } + + internal void AddStringTrimming(MemberInfo? member, int maxLen) + { + AddMemberSerializer(member, s => s.Length <= maxLen ? s : s.Substring(0, maxLen)); } } \ No newline at end of file diff --git a/ObjectPrinting/Solved/PropertyPrintingConfig.cs b/ObjectPrinting/Solved/PropertyPrintingConfig.cs index a509697d1..95b9e0751 100644 --- a/ObjectPrinting/Solved/PropertyPrintingConfig.cs +++ b/ObjectPrinting/Solved/PropertyPrintingConfig.cs @@ -1,32 +1,30 @@ using System; using System.Globalization; +using System.Reflection; -namespace ObjectPrinting.Solved +namespace ObjectPrinting.Solved; + +public class PropertyPrintingConfig(PrintingConfig printingConfig, MemberInfo? memberInfo) + : IPropertyPrintingConfig { - public class PropertyPrintingConfig : IPropertyPrintingConfig + public PrintingConfig Using(Func print) { - private readonly PrintingConfig printingConfig; - - public PropertyPrintingConfig(PrintingConfig printingConfig) - { - this.printingConfig = printingConfig; - } - - public PrintingConfig Using(Func print) - { - return printingConfig; - } - - public PrintingConfig Using(CultureInfo culture) - { - return printingConfig; - } - - PrintingConfig IPropertyPrintingConfig.ParentConfig => printingConfig; + if (memberInfo == null) + printingConfig.AddTypeSerializer(print); + else + printingConfig.AddMemberSerializer(memberInfo, print); + return printingConfig; } - public interface IPropertyPrintingConfig + public PrintingConfig Using(CultureInfo culture) { - PrintingConfig ParentConfig { get; } + if (memberInfo == null) + printingConfig.AddTypeCulture(culture); + else + printingConfig.AddMemberCulture(memberInfo, culture); + return printingConfig; } + + PrintingConfig IPropertyPrintingConfig.ParentConfig => printingConfig; + MemberInfo? IPropertyPrintingConfig.MemberInfo => memberInfo; } \ No newline at end of file diff --git a/ObjectPrinting/Solved/PropertyPrintingConfigExtensions.cs b/ObjectPrinting/Solved/PropertyPrintingConfigExtensions.cs index dd3922394..d0d95672b 100644 --- a/ObjectPrinting/Solved/PropertyPrintingConfigExtensions.cs +++ b/ObjectPrinting/Solved/PropertyPrintingConfigExtensions.cs @@ -1,18 +1,21 @@ using System; -namespace ObjectPrinting.Solved +namespace ObjectPrinting.Solved; + +public static class PropertyPrintingConfigExtensions { - public static class PropertyPrintingConfigExtensions + public static string? PrintToString(this T obj, Func, PrintingConfig> config) { - public static string PrintToString(this T obj, Func, PrintingConfig> config) - { - return config(ObjectPrinter.For()).PrintToString(obj); - } + return config(ObjectPrinter.For()).PrintToString(obj); + } - public static PrintingConfig TrimmedToLength(this PropertyPrintingConfig propConfig, int maxLen) - { - return ((IPropertyPrintingConfig)propConfig).ParentConfig; - } + public static PrintingConfig TrimmedToLength(this PropertyPrintingConfig propConfig, int maxLen) + { + IPropertyPrintingConfig config = propConfig; + var parent = config.ParentConfig; + var memberInfo = config.MemberInfo; + parent.AddStringTrimming(memberInfo, maxLen); + return parent; } } \ No newline at end of file diff --git a/ObjectPrinting/Solved/Tests/Person.cs b/ObjectPrinting/Solved/Tests/Person.cs deleted file mode 100644 index 858ebbf8d..000000000 --- a/ObjectPrinting/Solved/Tests/Person.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; - -namespace ObjectPrinting.Solved.Tests -{ - public class Person - { - public Guid Id { get; set; } - public string Name { get; set; } - public double Height { get; set; } - public int Age { get; set; } - } -} \ No newline at end of file diff --git a/ObjectPrinting/Tests/ObjectPrinterAcceptanceTests.cs b/ObjectPrinting/Tests/ObjectPrinterAcceptanceTests.cs deleted file mode 100644 index 4c8b2445c..000000000 --- a/ObjectPrinting/Tests/ObjectPrinterAcceptanceTests.cs +++ /dev/null @@ -1,27 +0,0 @@ -using NUnit.Framework; - -namespace ObjectPrinting.Tests -{ - [TestFixture] - public class ObjectPrinterAcceptanceTests - { - [Test] - public void Demo() - { - var person = new Person { Name = "Alex", Age = 19 }; - - var printer = ObjectPrinter.For(); - //1. Исключить из сериализации свойства определенного типа - //2. Указать альтернативный способ сериализации для определенного типа - //3. Для числовых типов указать культуру - //4. Настроить сериализацию конкретного свойства - //5. Настроить обрезание строковых свойств (метод должен быть виден только для строковых свойств) - //6. Исключить из сериализации конкретного свойства - - string s1 = printer.PrintToString(person); - - //7. Синтаксический сахар в виде метода расширения, сериализующего по-умолчанию - //8. ...с конфигурированием - } - } -} \ No newline at end of file diff --git a/ObjectPrinting/Tests/Person.cs b/ObjectPrinting/Tests/Person.cs deleted file mode 100644 index f95559554..000000000 --- a/ObjectPrinting/Tests/Person.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; - -namespace ObjectPrinting.Tests -{ - public class Person - { - public Guid Id { get; set; } - public string Name { get; set; } - public double Height { get; set; } - public int Age { get; set; } - } -} \ No newline at end of file diff --git a/ObjectPrintingTests/CyclicReference.cs b/ObjectPrintingTests/CyclicReference.cs new file mode 100644 index 000000000..ccdc6c31b --- /dev/null +++ b/ObjectPrintingTests/CyclicReference.cs @@ -0,0 +1,7 @@ +namespace ObjectPrintingTests; + +public class CyclicReference +{ + public string Name { get; set; } + public CyclicReference Obj { get; set; } +} \ No newline at end of file diff --git a/ObjectPrinting/Solved/Tests/ObjectPrinterAcceptanceTests.cs b/ObjectPrintingTests/ObjectPrinterAcceptanceTests.cs similarity index 87% rename from ObjectPrinting/Solved/Tests/ObjectPrinterAcceptanceTests.cs rename to ObjectPrintingTests/ObjectPrinterAcceptanceTests.cs index ac52d5ee5..a270a0876 100644 --- a/ObjectPrinting/Solved/Tests/ObjectPrinterAcceptanceTests.cs +++ b/ObjectPrintingTests/ObjectPrinterAcceptanceTests.cs @@ -20,18 +20,19 @@ public void Demo() //3. Для числовых типов указать культуру .Printing().Using(CultureInfo.InvariantCulture) //4. Настроить сериализацию конкретного свойства + .Printing(p => p.Name).Using(i => i.Replace("A", "a")) //5. Настроить обрезание строковых свойств (метод должен быть виден только для строковых свойств) .Printing(p => p.Name).TrimmedToLength(10) //6. Исключить из сериализации конкретного свойства .Excluding(p => p.Age); - string s1 = printer.PrintToString(person); + string? s1 = printer.PrintToString(person); //7. Синтаксический сахар в виде метода расширения, сериализующего по-умолчанию - string s2 = person.PrintToString(); + string? s2 = person.PrintToString(); //8. ...с конфигурированием - string s3 = person.PrintToString(s => s.Excluding(p => p.Age)); + string? s3 = person.PrintToString(s => s.Excluding(p => p.Age)); Console.WriteLine(s1); Console.WriteLine(s2); Console.WriteLine(s3); diff --git a/ObjectPrintingTests/ObjectPrinterTests.cs b/ObjectPrintingTests/ObjectPrinterTests.cs new file mode 100644 index 000000000..7364cdec4 --- /dev/null +++ b/ObjectPrintingTests/ObjectPrinterTests.cs @@ -0,0 +1,386 @@ +using System.Globalization; +using FluentAssertions; +using ObjectPrinting.Solved; +using ObjectPrinting.Solved.Tests; + +namespace ObjectPrintingTests; + +public class ObjectPrinterTests +{ + private Person person = new(); + private Guid guid = Guid.NewGuid(); + [SetUp] + public void SetUp() + { + person = new Person + { + Id = guid, + Name = "Alex", + Age = 19, + Birthday = new DateTime(1982, 06, 01), + FriendsBirthdays = [new DateTime(1991, 03, 12), new DateTime(1985, 09, 19)], + FriendsNames = ["John", "Amy", "Martin"], + Height = 190.5, + Money = 1000.1m, + Pets = new() { { "Asya", "Cat" }, { "Garry", "Fish" } }, + Parent = new Person + { + Name = "Jack" + } + }; + } + + [Test] + public void PrintToString_ShouldPrintPrimitiveProperties_WhenAllPropertiesAreSet() + { + var result = ObjectPrinter.For() + .PrintToString(person); + + result.Should().Contain("Name = Alex") + .And.Contain("Age = 19") + .And.Contain($"Id = {guid}") + .And.Contain("Height = 190,5") + .And.Contain("Money = 1000,1") + .And.Contain("Birthday = 01.06.1982 0:00:00"); + } + + [Test] + public void PrintToString_ShouldPrintArray_WhenAllPropertiesAreSet() + { + var result = ObjectPrinter.For() + .PrintToString(person); + + result.Should().Contain("FriendsBirthdays = DateTime[]") + .And.Contain("[0] = 12.03.1991 0:00:00") + .And.Contain("[1] = 19.09.1985 0:00:00"); + } + + [Test] + public void PrintToString_ShouldPrintList_WhenAllPropertiesAreSet() + { + var result = ObjectPrinter.For() + .PrintToString(person); + + result.Should().Contain("FriendsNames = List") + .And.Contain("[0] = John") + .And.Contain("[1] = Amy") + .And.Contain("[2] = Martin"); + } + + [Test] + public void PrintToString_ShouldPrintDictionary_WhenAllPropertiesAreSet() + { + var result = ObjectPrinter.For() + .PrintToString(person); + + result.Should().Contain("Pets = Dictionary") + .And.Contain("Key = Asya") + .And.Contain("Value = Cat") + .And.Contain("Key = Garry") + .And.Contain("Value = Fish"); + } + + [Test] + public void PrintToString_ShouldPrintInstance_WhenAllPropertiesAreSet() + { + var result = ObjectPrinter.For() + .PrintToString(person); + + result.Should().Contain("Parent = Person") + .And.Contain("Name = Jack"); + } + + [Test] + public void PrintToString_ShouldExcludeInstanceType_WhenAllPropertiesAreSet() + { + var result = ObjectPrinter.For() + .Excluding() + .PrintToString(person); + + result.Should().NotContain("Parent = Person") + .And.NotContain("Name = Jack"); + } + + [Test] + public void PrintToString_ShouldExcludeInstanceMember_WhenAllPropertiesAreSet() + { + var result = ObjectPrinter.For() + .Excluding(m => m.Parent) + .PrintToString(person); + + result.Should().NotContain("Parent = Person") + .And.NotContain("Name = Jack"); + } + + + [Test] + public void PrintToString_ShouldExcludePrimitiveType_WhenAllPropertiesAreSet() + { + var result = ObjectPrinter.For() + .Excluding() + .PrintToString(person); + + result.Should().NotContain("Birthday = 01.06.1982 0:00:00"); + } + + [Test] + public void PrintToString_ShouldExcludePrimitiveMember_WhenAllPropertiesAreSet() + { + var result = ObjectPrinter.For() + .Excluding(m => m.Name) + .PrintToString(person); + + result.Should().NotContain("Name = Alex"); + } + + [Test] + public void PrintToString_ShouldExcludeArrayType_WhenAllPropertiesAreSet() + { + var result = ObjectPrinter.For() + .Excluding() + .PrintToString(person); + + result.Should().NotContain("FriendsBirthdays = DateTime[]") + .And.NotContain("[0] = 12.03.1991 0:00:00") + .And.NotContain("[1] = 19.09.1985 0:00:00"); + } + + [Test] + public void PrintToString_ShouldExcludeArrayMember_WhenAllPropertiesAreSet() + { + var result = ObjectPrinter.For() + .Excluding(m => m.FriendsBirthdays) + .PrintToString(person); + + result.Should().NotContain("FriendsBirthdays = DateTime[]") + .And.NotContain("[0] = 12.03.1991 0:00:00") + .And.NotContain("[1] = 19.09.1985 0:00:00"); + } + + [Test] + public void PrintToString_ShouldExcludeListType_WhenAllPropertiesAreSet() + { + var result = ObjectPrinter.For() + .Excluding>() + .PrintToString(person); + + result.Should().NotContain("FriendsNames = List") + .And.NotContain("[0] = John") + .And.NotContain("[1] = Amy") + .And.NotContain("[2] = Martin"); + } + + [Test] + public void PrintToString_ShouldExcludeListMember_WhenAllPropertiesAreSet() + { + var result = ObjectPrinter.For() + .Excluding(m => m.FriendsNames) + .PrintToString(person); + + result.Should().NotContain("FriendsNames = List") + .And.NotContain("[0] = John") + .And.NotContain("[1] = Amy") + .And.NotContain("[2] = Martin"); + } + + [Test] + public void PrintToString_ShouldExcludeDictionaryType_WhenAllPropertiesAreSet() + { + var result = ObjectPrinter.For() + .Excluding>() + .PrintToString(person); + + result.Should().NotContain("Pets = Dictionary") + .And.NotContain("Key = Asya") + .And.NotContain("Value = Cat") + .And.NotContain("Key = Garry") + .And.NotContain("Value = Fish"); + } + + [Test] + public void PrintToString_ShouldExcludeDictionaryMember_WhenAllPropertiesAreSet() + { + var result = ObjectPrinter.For() + .Excluding(m => m.Pets) + .PrintToString(person); + + result.Should().NotContain("Pets = Dictionary") + .And.NotContain("Key = Asya") + .And.NotContain("Value = Cat") + .And.NotContain("Key = Garry") + .And.NotContain("Value = Fish"); + } + + [Test] + public void PrintToString_ShouldSerializeStringTypeAlternatively_WhenAllPropertiesAreSet() + { + var result = ObjectPrinter.For() + .Printing().Using(s => s.Replace("A", "a")) + .PrintToString(person); + + result.Should().Contain("Name = alex"); + } + + [Test] + public void PrintToString_ShouldSerializeDateTimeTypeAlternatively_WhenAllPropertiesAreSet() + { + var result = ObjectPrinter.For() + .Printing().Using(s => s.ToString(CultureInfo.InvariantCulture).Replace("1982", "1337")) + .PrintToString(person); + + result.Should().Contain("Birthday = 06/01/1337 00:00:00"); + } + + [Test] + public void PrintToString_ShouldSerializeDecimalTypeAlternatively_WhenAllPropertiesAreSet() + { + var result = ObjectPrinter.For() + .Printing().Using(s => s.ToString(CultureInfo.InvariantCulture).Replace("0", "9")) + .PrintToString(person); + + result.Should().Contain("Money = 1999.1"); + } + + [Test] + public void PrintToString_ShouldSerializeStringMemberAlternatively_WhenAllPropertiesAreSet() + { + var result = ObjectPrinter.For() + .Printing(m => m.Name).Using(s => s.Replace("A", "a")) + .PrintToString(person); + + result.Should().Contain("Name = alex"); + } + + [Test] + public void PrintToString_ShouldSerializeDateTimeMemberAlternatively_WhenAllPropertiesAreSet() + { + var result = ObjectPrinter.For() + .Printing(m => m.Birthday).Using(s => s.ToString(CultureInfo.InvariantCulture).Replace("1982", "1337")) + .PrintToString(person); + + result.Should().Contain("Birthday = 06/01/1337 00:00:00"); + } + + [Test] + public void PrintToString_ShouldSerializeDecimalMemberAlternatively_WhenAllPropertiesAreSet() + { + var result = ObjectPrinter.For() + .Printing(m => m.Money).Using(s => s.ToString(CultureInfo.InvariantCulture).Replace("0", "9")) + .PrintToString(person); + + result.Should().Contain("Money = 1999.1"); + } + + [Test] + public void PrintToString_ShouldSerializeArrayTypeAlternatively_WhenAllPropertiesAreSet() + { + var result = ObjectPrinter.For() + .Printing().Using(s => string.Join(" ", s).Replace("1", "2")) + .PrintToString(person); + + result.Should().Contain("FriendsBirthdays = 22.03.2992 0:00:00 29.09.2985 0:00:00"); + } + + [Test] + public void PrintToString_ShouldSerializeListTypeAlternatively_WhenAllPropertiesAreSet() + { + var result = ObjectPrinter.For() + .Printing>().Using(s => string.Join(" ", s).Replace("A", "a")) + .PrintToString(person); + + result.Should().Contain("FriendsNames = John amy Martin"); + } + + [Test] + public void PrintToString_ShouldSerializeDictionaryTypeAlternatively_WhenAllPropertiesAreSet() + { + var result = ObjectPrinter.For() + .Printing>().Using(s => string.Join(" ", s).Replace("1", "2")) + .PrintToString(person); + + result.Should().Contain("Pets = [Asya, Cat] [Garry, Fish]"); + } + + [Test] + public void PrintToString_ShouldSerializeArrayMemberAlternatively_WhenAllPropertiesAreSet() + { + var result = ObjectPrinter.For() + .Printing(m => m.FriendsBirthdays).Using(s => string.Join(" ", s).Replace("1", "2")) + .PrintToString(person); + + result.Should().Contain("FriendsBirthdays = 22.03.2992 0:00:00 29.09.2985 0:00:00"); + } + + [Test] + public void PrintToString_ShouldSerializeListMemberAlternatively_WhenAllPropertiesAreSet() + { + var result = ObjectPrinter.For() + .Printing(m => m.FriendsNames).Using(s => string.Join(" ", s).Replace("A", "a")) + .PrintToString(person); + + result.Should().Contain("FriendsNames = John amy Martin"); + } + + [Test] + public void PrintToString_ShouldSerializeDictionaryMemberAlternatively_WhenAllPropertiesAreSet() + { + var result = ObjectPrinter.For() + .Printing(m => m.Pets).Using(s => string.Join(" ", s).Replace("1", "2")) + .PrintToString(person); + + result.Should().Contain("Pets = [Asya, Cat] [Garry, Fish]"); + } + + [Test] + public void PrintToString_ShouldSerializeDoubleWithCulture_WhenAllPropertiesAreSet() + { + var result = ObjectPrinter.For() + .Printing(m => m.Height).Using(CultureInfo.InvariantCulture) + .PrintToString(person); + + result.Should().Contain("Height = 190.5"); + } + + [Test] + public void PrintToString_ShouldSerializeDecimalWithCulture_WhenAllPropertiesAreSet() + { + var result = ObjectPrinter.For() + .Printing(m => m.Money).Using(CultureInfo.InvariantCulture) + .PrintToString(person); + + result.Should().Contain("Money = 1000.1"); + } + + [Test] + public void PrintToString_ShouldSerializeStringWithTrimming_WhenAllPropertiesAreSet() + { + var result = ObjectPrinter.For() + .Printing(m => m.Name).TrimmedToLength(2) + .PrintToString(person); + + result.Should().NotContain("Name = Alex") + .And.Contain("Name = Al"); + } + + [Test] + public void PrintToString_ShouldNotSerializeCyclicReference_WhenCyclicReferenceIsPresent() + { + var a = new CyclicReference + { + Name = "Test" + }; + + var b = new CyclicReference + { + Name = "Test2" + }; + + a.Obj = b; + b.Obj = a; + + var result = ObjectPrinter.For() + .PrintToString(a); + + result.Should().Contain("Obj = Cyclic reference at CyclicReference"); + } +} \ No newline at end of file diff --git a/ObjectPrintingTests/ObjectPrintingTests.csproj b/ObjectPrintingTests/ObjectPrintingTests.csproj new file mode 100644 index 000000000..d60bf920b --- /dev/null +++ b/ObjectPrintingTests/ObjectPrintingTests.csproj @@ -0,0 +1,28 @@ + + + + net9.0 + latest + enable + enable + false + + + + + + + + + + + + + + + + + + + + diff --git a/ObjectPrintingTests/Person.cs b/ObjectPrintingTests/Person.cs new file mode 100644 index 000000000..64e9253a1 --- /dev/null +++ b/ObjectPrintingTests/Person.cs @@ -0,0 +1,18 @@ +using System; + +namespace ObjectPrinting.Solved.Tests +{ + public class Person + { + public Guid Id { get; set; } + public string Name { get; set; } + public double Height { get; set; } + public int Age { get; set; } + public decimal Money { get; set; } + public Person Parent { get; set; } + public DateTime Birthday { get; set; } + public List FriendsNames { get; set; } + public DateTime[] FriendsBirthdays { get; set; } + public Dictionary Pets { get; set; } + } +} \ No newline at end of file diff --git a/fluent-api.sln b/fluent-api.sln index 69c8db9ed..ef4c89315 100644 --- a/fluent-api.sln +++ b/fluent-api.sln @@ -13,6 +13,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FluentMapping.Tests", "Samp EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Spectacle", "Samples\Spectacle\Spectacle.csproj", "{EFA9335C-411B-4597-B0B6-5438D1AE04C3}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ObjectPrintingTests", "ObjectPrintingTests\ObjectPrintingTests.csproj", "{AFC04253-34F3-4B03-B1AF-CF6B68757A8E}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -35,6 +37,10 @@ Global {EFA9335C-411B-4597-B0B6-5438D1AE04C3}.Debug|Any CPU.Build.0 = Debug|Any CPU {EFA9335C-411B-4597-B0B6-5438D1AE04C3}.Release|Any CPU.ActiveCfg = Release|Any CPU {EFA9335C-411B-4597-B0B6-5438D1AE04C3}.Release|Any CPU.Build.0 = Release|Any CPU + {AFC04253-34F3-4B03-B1AF-CF6B68757A8E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AFC04253-34F3-4B03-B1AF-CF6B68757A8E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AFC04253-34F3-4B03-B1AF-CF6B68757A8E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AFC04253-34F3-4B03-B1AF-CF6B68757A8E}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/fluent-api.sln.DotSettings b/fluent-api.sln.DotSettings index 135b83ecb..53fe49b2f 100644 --- a/fluent-api.sln.DotSettings +++ b/fluent-api.sln.DotSettings @@ -1,6 +1,9 @@  <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb_AaBb" /> + <Policy><Descriptor Staticness="Instance" AccessRightKinds="Private" Description="Instance fields (private)"><ElementKinds><Kind Name="FIELD" /><Kind Name="READONLY_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="" Suffix="" Style="aaBb" /></Policy> + <Policy><Descriptor Staticness="Any" AccessRightKinds="Any" Description="Types and namespaces"><ElementKinds><Kind Name="NAMESPACE" /><Kind Name="CLASS" /><Kind Name="STRUCT" /><Kind Name="ENUM" /><Kind Name="DELEGATE" /></ElementKinds></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="" Suffix="" Style="AaBb_AaBb" /></Policy> + True True True Imported 10.10.2016 From 1914b87a89a55b863b0ca53a6cd9b9b2a20da425 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A2=D0=B8=D0=BC=D1=83=D1=80=20=D0=91=D0=B0=D0=B1=D0=B0?= =?UTF-8?q?=D0=B5=D0=B2?= Date: Fri, 21 Nov 2025 01:40:32 +0500 Subject: [PATCH 2/3] =?UTF-8?q?=D0=9F=D1=80=D0=B0=D0=B2=D0=BA=D0=B8=20?= =?UTF-8?q?=D0=BF=D0=BE=20=D0=9F=D0=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ObjectPrinting/Solved/Printer.cs | 186 +++++++++++++++ ObjectPrinting/Solved/PrintingConfig.cs | 215 +----------------- ObjectPrinting/Solved/PrintingSettings.cs | 22 ++ .../Solved/PropertyPrintingConfig.cs | 9 - .../PropertyPrintingConfigExtensions.cs | 13 ++ ObjectPrintingTests/ObjectPrinterTests.cs | 23 ++ 6 files changed, 255 insertions(+), 213 deletions(-) create mode 100644 ObjectPrinting/Solved/Printer.cs create mode 100644 ObjectPrinting/Solved/PrintingSettings.cs diff --git a/ObjectPrinting/Solved/Printer.cs b/ObjectPrinting/Solved/Printer.cs new file mode 100644 index 000000000..226796eaf --- /dev/null +++ b/ObjectPrinting/Solved/Printer.cs @@ -0,0 +1,186 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; + +namespace ObjectPrinting.Solved; + +public class Printer(PrintingSettings settings) +{ + private readonly HashSet parsedObjects = []; + + public string PrintToString(object? obj, int nestingLevel, MemberInfo? member) + { + var sb = new StringBuilder(); + if (TryPrintNullOrExcluded(obj, nestingLevel, sb)) + return sb.ToString(); + + var type = obj!.GetType(); + + if (TryPrintSimpleType(obj, type, member, sb)) + return sb.ToString(); + + if (!type.IsValueType && obj is not string) + { + if (!parsedObjects.Add(obj)) + return $"Cyclic reference at {type.Name}" + Environment.NewLine; + } + + var indentation = new string('\t', nestingLevel + 1); + + if (TryProcessDictionary(obj, type, indentation, nestingLevel, sb)) + return sb.ToString(); + + if (TryProcessEnumerable(obj, type, indentation, nestingLevel, sb)) + return sb.ToString(); + + return PrintComplexType(obj, type, indentation, nestingLevel); + } + + private string PrintComplexType(object obj, Type type, string indentation, int nestingLevel) + { + var sb = new StringBuilder(); + sb.AppendLine(type.Name); + + var printableMembers = GetPrintableMembers(type); + foreach (var memberInfo in printableMembers) + { + var value = GetMemberValue(memberInfo, obj); + sb.Append($"{indentation}{memberInfo.Name} = {PrintToString(value, nestingLevel + 1, memberInfo)}"); + } + + return sb.ToString(); + } + + private bool TryPrintNullOrExcluded(object? obj, int nestingLevel, StringBuilder result) + { + if (obj is null) + { + result.Append("null" + Environment.NewLine); + return true; + } + + var type = obj.GetType(); + + if (settings.ExcludedTypes.Contains(type) && nestingLevel > 0) + return true; + + return false; + } + + private bool TryProcessDictionary(object? obj, Type type, string indentation, + int nestingLevel, StringBuilder result) + { + if (obj is IDictionary dict) + { + result.AppendLine(type.Name); + foreach (DictionaryEntry entry in dict) + { + result.Append($"{indentation}Key = {PrintToString(entry.Key, nestingLevel + 1, null)}"); + result.Append($"{indentation}Value = {PrintToString(entry.Value, nestingLevel + 1, null)}"); + } + + return true; + } + + return false; + } + + private bool TryProcessEnumerable(object? obj, Type type, string indentation, + int nestingLevel, StringBuilder result) + { + if (obj is IEnumerable enumerable and not string) + { + result.AppendLine(type.Name); + var index = 0; + foreach (var item in enumerable) + { + result.Append($"{indentation}[{index}] = {PrintToString(item, nestingLevel + 1, null)}"); + index++; + } + + return true; + } + + return false; + } + + private bool TryPrintSimpleType(object obj, Type type, MemberInfo? memberInfo, StringBuilder result) + { + if (memberInfo != null && settings.MemberSerializers.TryGetValue(memberInfo, out var memberSerializer)) + { + result.Append(memberSerializer(obj) + Environment.NewLine); + return true; + } + + if (settings.TypeSerializers.TryGetValue(type, out var typeSerializer)) + { + result.Append(typeSerializer(obj) + Environment.NewLine); + return true; + } + + if (memberInfo != null && settings.MemberCultures.TryGetValue(memberInfo, out var memberCulture) && + obj is IFormattable formattable1) + { + result.Append(formattable1.ToString(null, memberCulture) + Environment.NewLine); + return true; + } + + if (settings.TypeCultures.TryGetValue(type, out var typeCulture) && obj is IFormattable formattable2) + { + result.Append(formattable2.ToString(null, typeCulture) + Environment.NewLine); + return true; + } + + if (settings.PrimitiveTypes.Contains(type) || type.IsEnum) + { + result.Append(obj + Environment.NewLine); + return true; + } + + return false; + } + + private List GetPrintableMembers(Type type) + { + var result = new List(); + var flags = BindingFlags.Instance | BindingFlags.Public; + foreach (var propertyInfo in type.GetProperties(flags)) + { + if (!propertyInfo.CanRead) + continue; + + if (IsMemberExcluded(propertyInfo, propertyInfo.PropertyType)) + continue; + + result.Add(propertyInfo); + } + + foreach (var fieldInfo in type.GetFields(flags)) + { + if (IsMemberExcluded(fieldInfo, fieldInfo.FieldType)) + continue; + + result.Add(fieldInfo); + } + + return result; + } + + private bool IsMemberExcluded(MemberInfo member, Type memberType) + { + return settings.ExcludedMembers.Contains(member) || settings.ExcludedTypes.Contains(memberType); + } + + private object? GetMemberValue(MemberInfo member, object obj) + { + return member switch + { + PropertyInfo p => p.GetValue(obj), + FieldInfo f => f.GetValue(obj), + _ => null + }; + } +} \ No newline at end of file diff --git a/ObjectPrinting/Solved/PrintingConfig.cs b/ObjectPrinting/Solved/PrintingConfig.cs index 386ef74c8..57d0ac801 100644 --- a/ObjectPrinting/Solved/PrintingConfig.cs +++ b/ObjectPrinting/Solved/PrintingConfig.cs @@ -11,20 +11,7 @@ namespace ObjectPrinting.Solved; public class PrintingConfig { - private readonly HashSet excludedTypes = []; - private readonly HashSet excludedMembers = []; - private readonly Dictionary> typeSerializers = new(); - private readonly Dictionary> memberSerializers = new(); - private readonly Dictionary typeCultures = new(); - private readonly Dictionary memberCultures = new(); - - private readonly HashSet parsedObjects = []; - - private readonly Type[] primitiveTypes = - [ - typeof(int), typeof(double), typeof(float), typeof(string), - typeof(DateTime), typeof(TimeSpan), typeof(decimal), typeof(Guid) - ]; + internal PrintingSettings Settings = new(); public PropertyPrintingConfig Printing() { @@ -39,162 +26,22 @@ public PropertyPrintingConfig Printing(Expression< public PrintingConfig Excluding(Expression> memberSelector) { var member = GetMemberInfo(memberSelector); - excludedMembers.Add(member); + Settings.ExcludedMembers.Add(member); return this; } public PrintingConfig Excluding() { - excludedTypes.Add(typeof(TPropType)); + Settings.ExcludedTypes.Add(typeof(TPropType)); return this; } - public string? PrintToString(TOwner obj) - { - return PrintToString(obj, 0, null); - } - - private string? PrintToString(object? obj, int nestingLevel, MemberInfo? member) - { - if (TryPrintNullOrExcluded(obj, nestingLevel, out var nullOrExcludedResult)) - return nullOrExcludedResult; - - var type = obj!.GetType(); - - if (TryPrintSimpleType(obj, type, member, out var simpleResult)) - return simpleResult; - - if (!type.IsValueType && obj is not string) - { - if (!parsedObjects.Add(obj)) - return $"Cyclic reference at {type.Name}" + Environment.NewLine; - } - - var indentation = new string('\t', nestingLevel + 1); - - if (TryProcessDictionary(obj, type, indentation, nestingLevel, out var dictResult)) - return dictResult; - - if (TryProcessEnumerable(obj, type, indentation, nestingLevel, out var collectionResult)) - return collectionResult; - - return PrintComplexType(obj, type, indentation, nestingLevel); - } - - private string PrintComplexType(object obj, Type type, string indentation, int nestingLevel) - { - var sb = new StringBuilder(); - sb.AppendLine(type.Name); - - var printableMembers = GetPrintableMembers(type); - foreach (var memberInfo in printableMembers) - { - var value = GetMemberValue(memberInfo, obj); - sb.Append($"{indentation}{memberInfo.Name} = {PrintToString(value, nestingLevel + 1, memberInfo)}"); - } - - return sb.ToString(); - } - - private bool TryPrintNullOrExcluded(object? obj, int nestingLevel, out string? result) - { - if (obj is null) - { - result = "null" + Environment.NewLine; - return true; - } - - var type = obj.GetType(); - - if (excludedTypes.Contains(type) && nestingLevel > 0) - { - result = string.Empty; - return true; - } - - result = null; - return false; - } - - private bool TryProcessDictionary(object? obj, Type type, string indentation, - int nestingLevel, out string? result) - { - var sb = new StringBuilder(); - if (obj is IDictionary dict) - { - sb.AppendLine(type.Name); - foreach (DictionaryEntry entry in dict) - { - sb.Append($"{indentation}Key = {PrintToString(entry.Key, nestingLevel + 1, null)}"); - sb.Append($"{indentation}Value = {PrintToString(entry.Value, nestingLevel + 1, null)}"); - } - - result = sb.ToString(); - return true; - } - - result = null; - return false; - } - - private bool TryProcessEnumerable(object? obj, Type type, string indentation, - int nestingLevel, out string? result) - { - var sb = new StringBuilder(); - if (obj is IEnumerable enumerable and not string) - { - sb.AppendLine(type.Name); - var index = 0; - foreach (var item in enumerable) - { - sb.Append($"{indentation}[{index}] = {PrintToString(item, nestingLevel + 1, null)}"); - index++; - } - - result = sb.ToString(); - return true; - } - result = null; - return false; - } - - private bool TryPrintSimpleType(object obj, Type type, MemberInfo? memberInfo, out string? result) + public string PrintToString(TOwner obj) { - if (memberInfo != null && memberSerializers.TryGetValue(memberInfo, out var memberSerializer)) - { - result = memberSerializer(obj) + Environment.NewLine; - return true; - } - - if (typeSerializers.TryGetValue(type, out var typeSerializer)) - { - result = typeSerializer(obj) + Environment.NewLine; - return true; - } - - if (memberInfo != null && memberCultures.TryGetValue(memberInfo, out var memberCulture) && - obj is IFormattable formattable1) - { - result = formattable1.ToString(null, memberCulture) + Environment.NewLine; - return true; - } - - if (typeCultures.TryGetValue(type, out var typeCulture) && obj is IFormattable formattable2) - { - result = formattable2.ToString(null, typeCulture) + Environment.NewLine; - return true; - } - - if (primitiveTypes.Contains(type) || type.IsEnum) - { - result = obj + Environment.NewLine; - return true; - } - - result = null; - return false; + var printer = new Printer(Settings); + return printer.PrintToString(obj, 0, null); } - + private MemberInfo GetMemberInfo(Expression> memberSelector) { if (memberSelector.Body is MemberExpression memberExpression) @@ -202,60 +49,20 @@ private MemberInfo GetMemberInfo(Expression> throw new ArgumentException(); } - private List GetPrintableMembers(Type type) - { - var result = new List(); - var flags = BindingFlags.Instance | BindingFlags.Public; - foreach (var propertyInfo in type.GetProperties(flags)) - { - if (!propertyInfo.CanRead) - continue; - - if (IsMemberExcluded(propertyInfo, propertyInfo.PropertyType)) - continue; - - result.Add(propertyInfo); - } - - foreach (var fieldInfo in type.GetFields(flags)) - { - if (IsMemberExcluded(fieldInfo, fieldInfo.FieldType)) - continue; - - result.Add(fieldInfo); - } - return result; - } - - private bool IsMemberExcluded(MemberInfo member, Type memberType) - { - return excludedMembers.Contains(member) || excludedTypes.Contains(memberType); - } - - private object? GetMemberValue(MemberInfo member, object obj) - { - return member switch - { - PropertyInfo p => p.GetValue(obj), - FieldInfo f => f.GetValue(obj), - _ => null - }; - } - internal void AddTypeSerializer(Func serialize) { - typeSerializers[typeof(TPropType)] = o => serialize((TPropType)o); + Settings.TypeSerializers[typeof(TPropType)] = o => serialize((TPropType)o); } internal void AddMemberSerializer(MemberInfo? member, Func serialize) { ArgumentNullException.ThrowIfNull(member); - memberSerializers[member] = o => serialize((TPropType)o); + Settings.MemberSerializers[member] = o => serialize((TPropType)o); } internal void AddTypeCulture(CultureInfo culture) { - typeCultures[typeof(TPropType)] = culture; + Settings.TypeCultures[typeof(TPropType)] = culture; } internal void AddMemberCulture(MemberInfo member, CultureInfo culture) @@ -263,7 +70,7 @@ internal void AddMemberCulture(MemberInfo member, CultureInfo culture) if (member == null) throw new ArgumentNullException(nameof(member)); - memberCultures[member] = culture; + Settings.MemberCultures[member] = culture; } internal void AddStringTrimming(MemberInfo? member, int maxLen) diff --git a/ObjectPrinting/Solved/PrintingSettings.cs b/ObjectPrinting/Solved/PrintingSettings.cs new file mode 100644 index 000000000..9bb5b3d69 --- /dev/null +++ b/ObjectPrinting/Solved/PrintingSettings.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Reflection; + +namespace ObjectPrinting.Solved; + +public class PrintingSettings +{ + public HashSet ExcludedTypes = []; + public HashSet ExcludedMembers = []; + public Dictionary> TypeSerializers = new(); + public Dictionary> MemberSerializers = new(); + public Dictionary TypeCultures = new(); + public Dictionary MemberCultures = new(); + + public Type[] PrimitiveTypes = + [ + typeof(int), typeof(double), typeof(float), typeof(string), + typeof(DateTime), typeof(TimeSpan), typeof(decimal), typeof(Guid) + ]; +} \ No newline at end of file diff --git a/ObjectPrinting/Solved/PropertyPrintingConfig.cs b/ObjectPrinting/Solved/PropertyPrintingConfig.cs index 95b9e0751..e527760e4 100644 --- a/ObjectPrinting/Solved/PropertyPrintingConfig.cs +++ b/ObjectPrinting/Solved/PropertyPrintingConfig.cs @@ -16,15 +16,6 @@ public PrintingConfig Using(Func print) return printingConfig; } - public PrintingConfig Using(CultureInfo culture) - { - if (memberInfo == null) - printingConfig.AddTypeCulture(culture); - else - printingConfig.AddMemberCulture(memberInfo, culture); - return printingConfig; - } - PrintingConfig IPropertyPrintingConfig.ParentConfig => printingConfig; MemberInfo? IPropertyPrintingConfig.MemberInfo => memberInfo; } \ No newline at end of file diff --git a/ObjectPrinting/Solved/PropertyPrintingConfigExtensions.cs b/ObjectPrinting/Solved/PropertyPrintingConfigExtensions.cs index d0d95672b..f08ecc1b8 100644 --- a/ObjectPrinting/Solved/PropertyPrintingConfigExtensions.cs +++ b/ObjectPrinting/Solved/PropertyPrintingConfigExtensions.cs @@ -1,4 +1,5 @@ using System; +using System.Globalization; namespace ObjectPrinting.Solved; @@ -18,4 +19,16 @@ public static PrintingConfig TrimmedToLength(this PropertyPrinti parent.AddStringTrimming(memberInfo, maxLen); return parent; } + + public static PrintingConfig Using(this IPropertyPrintingConfig config, + CultureInfo culture) + where TPropType : IFormattable + { + if (config.MemberInfo == null) + config.ParentConfig.AddTypeCulture(culture); + else + config.ParentConfig.AddMemberCulture(config.MemberInfo, culture); + + return config.ParentConfig; + } } \ No newline at end of file diff --git a/ObjectPrintingTests/ObjectPrinterTests.cs b/ObjectPrintingTests/ObjectPrinterTests.cs index 7364cdec4..25e55035d 100644 --- a/ObjectPrintingTests/ObjectPrinterTests.cs +++ b/ObjectPrintingTests/ObjectPrinterTests.cs @@ -1,4 +1,5 @@ using System.Globalization; +using System.Text; using FluentAssertions; using ObjectPrinting.Solved; using ObjectPrinting.Solved.Tests; @@ -9,6 +10,7 @@ public class ObjectPrinterTests { private Person person = new(); private Guid guid = Guid.NewGuid(); + private string newLine = Environment.NewLine; [SetUp] public void SetUp() { @@ -29,6 +31,25 @@ public void SetUp() } }; } + + [Test] + public void PrintToString_ShouldPrintExactly_WhenAllPropertiesAreSet() + { + var result = ObjectPrinter.For() + .PrintToString(person); + + var expected = new StringBuilder(); + expected.AppendLine("Person") + .AppendLine($"\tId = {person.Id}") + .AppendLine($"\tName = {person.Name}") + .AppendLine($"\tHeight = {person.Height}") + .AppendLine($"\tAge = {person.Age}") + .AppendLine($"\tMoney = {person.Money}") + .AppendLine("\tParent = Person") + .AppendLine($"\t\tId = {person.Parent.Id}"); // возможно стоит добавить больше, либо разнести их на разные тесты + + result.Should().Contain(expected.ToString()); + } [Test] public void PrintToString_ShouldPrintPrimitiveProperties_WhenAllPropertiesAreSet() @@ -339,6 +360,7 @@ public void PrintToString_ShouldSerializeDoubleWithCulture_WhenAllPropertiesAreS .PrintToString(person); result.Should().Contain("Height = 190.5"); + result.Should().Contain("Money = 1000,1"); } [Test] @@ -349,6 +371,7 @@ public void PrintToString_ShouldSerializeDecimalWithCulture_WhenAllPropertiesAre .PrintToString(person); result.Should().Contain("Money = 1000.1"); + result.Should().Contain("Height = 190,5"); } [Test] From 398d06168e8d063dae97c10cf79c978bfd619b7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A2=D0=B8=D0=BC=D1=83=D1=80=20=D0=91=D0=B0=D0=B1=D0=B0?= =?UTF-8?q?=D0=B5=D0=B2?= Date: Fri, 21 Nov 2025 19:25:42 +0500 Subject: [PATCH 3/3] =?UTF-8?q?=D0=9F=D0=BE=D0=BF=D1=80=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20=D0=BB=D0=BE=D0=B3=D0=B8=D0=BA=D1=83=20=D0=BD=D0=B0?= =?UTF-8?q?=D1=85=D0=BE=D0=B6=D0=B4=D0=B5=D0=BD=D0=B8=D1=8F=20=D1=86=D0=B8?= =?UTF-8?q?=D0=BA=D0=BB=D0=B8=D1=87=D0=B5=D1=81=D0=BA=D0=BE=D0=B9=20=D1=81?= =?UTF-8?q?=D1=81=D1=8B=D0=BB=D0=BA=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ObjectPrinting/Solved/Printer.cs | 68 +++++++++++------------ ObjectPrintingTests/ObjectPrinterTests.cs | 12 ++++ ObjectPrintingTests/TestDto.cs | 6 ++ ObjectPrintingTests/TestModel.cs | 7 +++ 4 files changed, 58 insertions(+), 35 deletions(-) create mode 100644 ObjectPrintingTests/TestDto.cs create mode 100644 ObjectPrintingTests/TestModel.cs diff --git a/ObjectPrinting/Solved/Printer.cs b/ObjectPrinting/Solved/Printer.cs index 226796eaf..c37228a0f 100644 --- a/ObjectPrinting/Solved/Printer.cs +++ b/ObjectPrinting/Solved/Printer.cs @@ -22,21 +22,29 @@ public string PrintToString(object? obj, int nestingLevel, MemberInfo? member) if (TryPrintSimpleType(obj, type, member, sb)) return sb.ToString(); + var isRemoveNeeded = false; + if (!type.IsValueType && obj is not string) { if (!parsedObjects.Add(obj)) return $"Cyclic reference at {type.Name}" + Environment.NewLine; + isRemoveNeeded = true; } var indentation = new string('\t', nestingLevel + 1); - - if (TryProcessDictionary(obj, type, indentation, nestingLevel, sb)) - return sb.ToString(); - - if (TryProcessEnumerable(obj, type, indentation, nestingLevel, sb)) - return sb.ToString(); - - return PrintComplexType(obj, type, indentation, nestingLevel); + var result = string.Empty; + + if (obj is IDictionary dictionary) + result = ProcessDictionary(dictionary, type, indentation, nestingLevel); + else if (obj is IEnumerable enumerable and not string) + result = ProcessEnumerable(enumerable, type, indentation, nestingLevel); + else + result = PrintComplexType(obj, type, indentation, nestingLevel); + + if (isRemoveNeeded) + parsedObjects.Remove(obj); + + return result; } private string PrintComplexType(object obj, Type type, string indentation, int nestingLevel) @@ -70,41 +78,31 @@ private bool TryPrintNullOrExcluded(object? obj, int nestingLevel, StringBuilder return false; } - private bool TryProcessDictionary(object? obj, Type type, string indentation, - int nestingLevel, StringBuilder result) + private string ProcessDictionary(IDictionary dictionary, Type type, string indentation, int nestingLevel) { - if (obj is IDictionary dict) + var result = new StringBuilder(); + result.AppendLine(type.Name); + foreach (DictionaryEntry entry in dictionary) { - result.AppendLine(type.Name); - foreach (DictionaryEntry entry in dict) - { - result.Append($"{indentation}Key = {PrintToString(entry.Key, nestingLevel + 1, null)}"); - result.Append($"{indentation}Value = {PrintToString(entry.Value, nestingLevel + 1, null)}"); - } - - return true; + result.Append($"{indentation}Key = {PrintToString(entry.Key, nestingLevel + 1, null)}"); + result.Append($"{indentation}Value = {PrintToString(entry.Value, nestingLevel + 1, null)}"); } - - return false; + return result.ToString(); } - private bool TryProcessEnumerable(object? obj, Type type, string indentation, - int nestingLevel, StringBuilder result) + private string ProcessEnumerable(IEnumerable enumerable, Type type, string indentation, int nestingLevel) { - if (obj is IEnumerable enumerable and not string) + var result = new StringBuilder(); + + result.AppendLine(type.Name); + var index = 0; + foreach (var item in enumerable) { - result.AppendLine(type.Name); - var index = 0; - foreach (var item in enumerable) - { - result.Append($"{indentation}[{index}] = {PrintToString(item, nestingLevel + 1, null)}"); - index++; - } - - return true; + result.Append($"{indentation}[{index}] = {PrintToString(item, nestingLevel + 1, null)}"); + index++; } - - return false; + + return result.ToString(); } private bool TryPrintSimpleType(object obj, Type type, MemberInfo? memberInfo, StringBuilder result) diff --git a/ObjectPrintingTests/ObjectPrinterTests.cs b/ObjectPrintingTests/ObjectPrinterTests.cs index 25e55035d..9fa6700fa 100644 --- a/ObjectPrintingTests/ObjectPrinterTests.cs +++ b/ObjectPrintingTests/ObjectPrinterTests.cs @@ -406,4 +406,16 @@ public void PrintToString_ShouldNotSerializeCyclicReference_WhenCyclicReferenceI result.Should().Contain("Obj = Cyclic reference at CyclicReference"); } + + [Test] + public void PrintToString_ShouldNotPrintCyclicReference_WhenTwoReferencesOnClassPresent() + { + var a = new TestDto { Id = 1 }; + var b = new TestModel { A = a, B = a }; + + var result = ObjectPrinter.For() + .PrintToString(b); + + result.Should().NotContain("Cyclic reference at TestDto"); + } } \ No newline at end of file diff --git a/ObjectPrintingTests/TestDto.cs b/ObjectPrintingTests/TestDto.cs new file mode 100644 index 000000000..d4994b5a2 --- /dev/null +++ b/ObjectPrintingTests/TestDto.cs @@ -0,0 +1,6 @@ +namespace ObjectPrintingTests; + +public class TestDto +{ + public int Id { get; set; } +} \ No newline at end of file diff --git a/ObjectPrintingTests/TestModel.cs b/ObjectPrintingTests/TestModel.cs new file mode 100644 index 000000000..a29045eac --- /dev/null +++ b/ObjectPrintingTests/TestModel.cs @@ -0,0 +1,7 @@ +namespace ObjectPrintingTests; + +public class TestModel +{ + public TestDto A { get; set; } + public TestDto B { get; set; } +} \ No newline at end of file