From bda9d350957784c0b57be5f272e6f8c78dc2f08a Mon Sep 17 00:00:00 2001 From: stupidnessplusplus <58785209+stupidnessplusplus@users.noreply.github.com> Date: Tue, 10 Dec 2024 21:11:28 +0500 Subject: [PATCH 01/13] Delete existing ObjectPrinting project --- ObjectPrinting/ObjectPrinter.cs | 10 --- ObjectPrinting/ObjectPrinting.csproj | 12 ---- ObjectPrinting/PrintingConfig.cs | 41 ------------ ObjectPrinting/Solved/ObjectExtensions.cs | 10 --- ObjectPrinting/Solved/ObjectPrinter.cs | 10 --- ObjectPrinting/Solved/PrintingConfig.cs | 62 ------------------- .../Solved/PropertyPrintingConfig.cs | 32 ---------- .../PropertyPrintingConfigExtensions.cs | 18 ------ .../Tests/ObjectPrinterAcceptanceTests.cs | 40 ------------ ObjectPrinting/Solved/Tests/Person.cs | 12 ---- .../Tests/ObjectPrinterAcceptanceTests.cs | 27 -------- ObjectPrinting/Tests/Person.cs | 12 ---- fluent-api.sln | 16 ++--- 13 files changed, 5 insertions(+), 297 deletions(-) delete mode 100644 ObjectPrinting/ObjectPrinter.cs delete mode 100644 ObjectPrinting/ObjectPrinting.csproj delete mode 100644 ObjectPrinting/PrintingConfig.cs delete mode 100644 ObjectPrinting/Solved/ObjectExtensions.cs delete mode 100644 ObjectPrinting/Solved/ObjectPrinter.cs delete mode 100644 ObjectPrinting/Solved/PrintingConfig.cs delete mode 100644 ObjectPrinting/Solved/PropertyPrintingConfig.cs delete mode 100644 ObjectPrinting/Solved/PropertyPrintingConfigExtensions.cs delete mode 100644 ObjectPrinting/Solved/Tests/ObjectPrinterAcceptanceTests.cs delete mode 100644 ObjectPrinting/Solved/Tests/Person.cs delete mode 100644 ObjectPrinting/Tests/ObjectPrinterAcceptanceTests.cs delete mode 100644 ObjectPrinting/Tests/Person.cs diff --git a/ObjectPrinting/ObjectPrinter.cs b/ObjectPrinting/ObjectPrinter.cs deleted file mode 100644 index 3c7867c3..00000000 --- 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/ObjectPrinting.csproj b/ObjectPrinting/ObjectPrinting.csproj deleted file mode 100644 index c5db392f..00000000 --- a/ObjectPrinting/ObjectPrinting.csproj +++ /dev/null @@ -1,12 +0,0 @@ - - - net8.0 - enable - - - - - - - - diff --git a/ObjectPrinting/PrintingConfig.cs b/ObjectPrinting/PrintingConfig.cs deleted file mode 100644 index a9e08211..00000000 --- a/ObjectPrinting/PrintingConfig.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System; -using System.Linq; -using System.Text; - -namespace ObjectPrinting -{ - public class PrintingConfig - { - public string PrintToString(TOwner obj) - { - return PrintToString(obj, 0); - } - - private string PrintToString(object obj, int nestingLevel) - { - //TODO apply configurations - if (obj == null) - return "null" + Environment.NewLine; - - var finalTypes = new[] - { - typeof(int), typeof(double), typeof(float), typeof(string), - typeof(DateTime), typeof(TimeSpan) - }; - if (finalTypes.Contains(obj.GetType())) - return obj + Environment.NewLine; - - var identation = new string('\t', nestingLevel + 1); - var sb = new StringBuilder(); - var type = obj.GetType(); - sb.AppendLine(type.Name); - foreach (var propertyInfo in type.GetProperties()) - { - sb.Append(identation + propertyInfo.Name + " = " + - PrintToString(propertyInfo.GetValue(obj), - nestingLevel + 1)); - } - return sb.ToString(); - } - } -} \ No newline at end of file diff --git a/ObjectPrinting/Solved/ObjectExtensions.cs b/ObjectPrinting/Solved/ObjectExtensions.cs deleted file mode 100644 index b0c94553..00000000 --- a/ObjectPrinting/Solved/ObjectExtensions.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace ObjectPrinting.Solved -{ - public static class ObjectExtensions - { - public static string PrintToString(this T obj) - { - return ObjectPrinter.For().PrintToString(obj); - } - } -} \ No newline at end of file diff --git a/ObjectPrinting/Solved/ObjectPrinter.cs b/ObjectPrinting/Solved/ObjectPrinter.cs deleted file mode 100644 index 540ee769..00000000 --- a/ObjectPrinting/Solved/ObjectPrinter.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace ObjectPrinting.Solved -{ - public class ObjectPrinter - { - public static PrintingConfig For() - { - return new PrintingConfig(); - } - } -} \ No newline at end of file diff --git a/ObjectPrinting/Solved/PrintingConfig.cs b/ObjectPrinting/Solved/PrintingConfig.cs deleted file mode 100644 index 0ec5aeb2..00000000 --- a/ObjectPrinting/Solved/PrintingConfig.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System; -using System.Linq; -using System.Linq.Expressions; -using System.Text; - -namespace ObjectPrinting.Solved -{ - public class PrintingConfig - { - public PropertyPrintingConfig Printing() - { - return new PropertyPrintingConfig(this); - } - - public PropertyPrintingConfig Printing(Expression> memberSelector) - { - return new PropertyPrintingConfig(this); - } - - public PrintingConfig Excluding(Expression> memberSelector) - { - return this; - } - - internal PrintingConfig Excluding() - { - return this; - } - - public string PrintToString(TOwner obj) - { - return PrintToString(obj, 0); - } - - private string PrintToString(object obj, int nestingLevel) - { - //TODO apply configurations - if (obj == null) - return "null" + Environment.NewLine; - - var finalTypes = new[] - { - typeof(int), typeof(double), typeof(float), typeof(string), - typeof(DateTime), typeof(TimeSpan) - }; - if (finalTypes.Contains(obj.GetType())) - return obj + Environment.NewLine; - - var identation = new string('\t', nestingLevel + 1); - var sb = new StringBuilder(); - var type = obj.GetType(); - sb.AppendLine(type.Name); - foreach (var propertyInfo in type.GetProperties()) - { - sb.Append(identation + propertyInfo.Name + " = " + - PrintToString(propertyInfo.GetValue(obj), - nestingLevel + 1)); - } - return sb.ToString(); - } - } -} \ No newline at end of file diff --git a/ObjectPrinting/Solved/PropertyPrintingConfig.cs b/ObjectPrinting/Solved/PropertyPrintingConfig.cs deleted file mode 100644 index a509697d..00000000 --- a/ObjectPrinting/Solved/PropertyPrintingConfig.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; -using System.Globalization; - -namespace ObjectPrinting.Solved -{ - public class PropertyPrintingConfig : IPropertyPrintingConfig - { - private readonly PrintingConfig printingConfig; - - public PropertyPrintingConfig(PrintingConfig printingConfig) - { - this.printingConfig = printingConfig; - } - - public PrintingConfig Using(Func print) - { - return printingConfig; - } - - public PrintingConfig Using(CultureInfo culture) - { - return printingConfig; - } - - PrintingConfig IPropertyPrintingConfig.ParentConfig => printingConfig; - } - - public interface IPropertyPrintingConfig - { - PrintingConfig ParentConfig { get; } - } -} \ No newline at end of file diff --git a/ObjectPrinting/Solved/PropertyPrintingConfigExtensions.cs b/ObjectPrinting/Solved/PropertyPrintingConfigExtensions.cs deleted file mode 100644 index dd392239..00000000 --- a/ObjectPrinting/Solved/PropertyPrintingConfigExtensions.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; - -namespace ObjectPrinting.Solved -{ - public static class PropertyPrintingConfigExtensions - { - public static string PrintToString(this T obj, Func, PrintingConfig> config) - { - return config(ObjectPrinter.For()).PrintToString(obj); - } - - public static PrintingConfig TrimmedToLength(this PropertyPrintingConfig propConfig, int maxLen) - { - return ((IPropertyPrintingConfig)propConfig).ParentConfig; - } - - } -} \ No newline at end of file diff --git a/ObjectPrinting/Solved/Tests/ObjectPrinterAcceptanceTests.cs b/ObjectPrinting/Solved/Tests/ObjectPrinterAcceptanceTests.cs deleted file mode 100644 index ac52d5ee..00000000 --- a/ObjectPrinting/Solved/Tests/ObjectPrinterAcceptanceTests.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System; -using System.Globalization; -using NUnit.Framework; - -namespace ObjectPrinting.Solved.Tests -{ - [TestFixture] - public class ObjectPrinterAcceptanceTests - { - [Test] - public void Demo() - { - var person = new Person { Name = "Alex", Age = 19 }; - - var printer = ObjectPrinter.For() - //1. Исключить из сериализации свойства определенного типа - .Excluding() - //2. Указать альтернативный способ сериализации для определенного типа - .Printing().Using(i => i.ToString("X")) - //3. Для числовых типов указать культуру - .Printing().Using(CultureInfo.InvariantCulture) - //4. Настроить сериализацию конкретного свойства - //5. Настроить обрезание строковых свойств (метод должен быть виден только для строковых свойств) - .Printing(p => p.Name).TrimmedToLength(10) - //6. Исключить из сериализации конкретного свойства - .Excluding(p => p.Age); - - string s1 = printer.PrintToString(person); - - //7. Синтаксический сахар в виде метода расширения, сериализующего по-умолчанию - string s2 = person.PrintToString(); - - //8. ...с конфигурированием - string s3 = person.PrintToString(s => s.Excluding(p => p.Age)); - Console.WriteLine(s1); - Console.WriteLine(s2); - Console.WriteLine(s3); - } - } -} \ No newline at end of file diff --git a/ObjectPrinting/Solved/Tests/Person.cs b/ObjectPrinting/Solved/Tests/Person.cs deleted file mode 100644 index 858ebbf8..00000000 --- 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 4c8b2445..00000000 --- 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 f9555955..00000000 --- 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/fluent-api.sln b/fluent-api.sln index 69c8db9e..6eb1804e 100644 --- a/fluent-api.sln +++ b/fluent-api.sln @@ -1,17 +1,15 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 14 -VisualStudioVersion = 14.0.25123.0 +# Visual Studio Version 17 +VisualStudioVersion = 17.11.35327.3 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ObjectPrinting", "ObjectPrinting\ObjectPrinting.csproj", "{07B8C9B7-8289-46CB-9875-048A57758EEE}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "Samples", "{6D308E4A-CEC7-4536-9B87-81CD337A87AD}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FluentMapping", "Samples\FluentMapper\FluentMapping.csproj", "{FEEA5AFE-459A-4D13-81D0-252E1A2E6F4E}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FluentMapping", "Samples\FluentMapper\FluentMapping.csproj", "{FEEA5AFE-459A-4D13-81D0-252E1A2E6F4E}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FluentMapping.Tests", "Samples\FluentMapper.Tests\FluentMapping.Tests.csproj", "{8A7BB3EA-3E6A-4D04-A801-D5CD1620DA0D}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FluentMapping.Tests", "Samples\FluentMapper.Tests\FluentMapping.Tests.csproj", "{8A7BB3EA-3E6A-4D04-A801-D5CD1620DA0D}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Spectacle", "Samples\Spectacle\Spectacle.csproj", "{EFA9335C-411B-4597-B0B6-5438D1AE04C3}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Spectacle", "Samples\Spectacle\Spectacle.csproj", "{EFA9335C-411B-4597-B0B6-5438D1AE04C3}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -19,10 +17,6 @@ Global Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {07B8C9B7-8289-46CB-9875-048A57758EEE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {07B8C9B7-8289-46CB-9875-048A57758EEE}.Debug|Any CPU.Build.0 = Debug|Any CPU - {07B8C9B7-8289-46CB-9875-048A57758EEE}.Release|Any CPU.ActiveCfg = Release|Any CPU - {07B8C9B7-8289-46CB-9875-048A57758EEE}.Release|Any CPU.Build.0 = Release|Any CPU {FEEA5AFE-459A-4D13-81D0-252E1A2E6F4E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {FEEA5AFE-459A-4D13-81D0-252E1A2E6F4E}.Debug|Any CPU.Build.0 = Debug|Any CPU {FEEA5AFE-459A-4D13-81D0-252E1A2E6F4E}.Release|Any CPU.ActiveCfg = Release|Any CPU From e8c52d6e0482d12666bd1f1dbedb22038f201ab7 Mon Sep 17 00:00:00 2001 From: stupidnessplusplus <58785209+stupidnessplusplus@users.noreply.github.com> Date: Tue, 10 Dec 2024 21:24:28 +0500 Subject: [PATCH 02/13] Set up project and classes --- ObjectPrinting/ObjectExtensions.cs | 9 +++ ObjectPrinting/ObjectPrinter.cs | 9 +++ ObjectPrinting/ObjectPrinting.csproj | 9 +++ ObjectPrinting/PrintingConfig.cs | 56 +++++++++++++++++++ ObjectPrinting/PropertyPrintingConfig.cs | 30 ++++++++++ .../PropertyPrintingConfigExtensions.cs | 11 ++++ .../ObjectPrinting_Tests.csproj | 27 +++++++++ ObjectPrinting_Tests/PrintingConfigTests.cs | 8 +++ fluent-api.sln | 12 ++++ 9 files changed, 171 insertions(+) create mode 100644 ObjectPrinting/ObjectExtensions.cs create mode 100644 ObjectPrinting/ObjectPrinter.cs create mode 100644 ObjectPrinting/ObjectPrinting.csproj create mode 100644 ObjectPrinting/PrintingConfig.cs create mode 100644 ObjectPrinting/PropertyPrintingConfig.cs create mode 100644 ObjectPrinting/PropertyPrintingConfigExtensions.cs create mode 100644 ObjectPrinting_Tests/ObjectPrinting_Tests.csproj create mode 100644 ObjectPrinting_Tests/PrintingConfigTests.cs diff --git a/ObjectPrinting/ObjectExtensions.cs b/ObjectPrinting/ObjectExtensions.cs new file mode 100644 index 00000000..9fc86f1e --- /dev/null +++ b/ObjectPrinting/ObjectExtensions.cs @@ -0,0 +1,9 @@ +namespace ObjectPrinting; + +public static class ObjectExtensions +{ + public static string PrintToString(this T obj) + { + return ObjectPrinter.For().PrintToString(obj); + } +} diff --git a/ObjectPrinting/ObjectPrinter.cs b/ObjectPrinting/ObjectPrinter.cs new file mode 100644 index 00000000..c46fc08a --- /dev/null +++ b/ObjectPrinting/ObjectPrinter.cs @@ -0,0 +1,9 @@ +namespace ObjectPrinting; + +public class ObjectPrinter +{ + public static PrintingConfig For() + { + return new PrintingConfig(); + } +} diff --git a/ObjectPrinting/ObjectPrinting.csproj b/ObjectPrinting/ObjectPrinting.csproj new file mode 100644 index 00000000..fa71b7ae --- /dev/null +++ b/ObjectPrinting/ObjectPrinting.csproj @@ -0,0 +1,9 @@ + + + + net8.0 + enable + enable + + + diff --git a/ObjectPrinting/PrintingConfig.cs b/ObjectPrinting/PrintingConfig.cs new file mode 100644 index 00000000..71ab04fb --- /dev/null +++ b/ObjectPrinting/PrintingConfig.cs @@ -0,0 +1,56 @@ +using System.Globalization; +using System.Linq.Expressions; +using System.Reflection; + +namespace ObjectPrinting; + +public class PrintingConfig +{ + internal PrintingConfig() + { + } + + public PropertyPrintingConfig Printing() + { + return new PropertyPrintingConfig(this); + } + + public PropertyPrintingConfig Printing( + Expression> propertySelector) + { + return new PropertyPrintingConfig(this); + } + + public PropertyPrintingConfig Printing( + Func propertyFilter) + { + return new PropertyPrintingConfig(this); + } + + public PrintingConfig Excluding() + { + return this; + } + + public PrintingConfig Excluding( + Expression> propertySelector) + { + return this; + } + + public PrintingConfig Excluding( + Func propertyFilter) + { + return this; + } + + public PrintingConfig Using(CultureInfo culture) + { + return this; + } + + public string PrintToString(TOwner obj) + { + throw new NotImplementedException(); + } +} diff --git a/ObjectPrinting/PropertyPrintingConfig.cs b/ObjectPrinting/PropertyPrintingConfig.cs new file mode 100644 index 00000000..e59765f9 --- /dev/null +++ b/ObjectPrinting/PropertyPrintingConfig.cs @@ -0,0 +1,30 @@ +using System.Globalization; + +namespace ObjectPrinting; + +public class PropertyPrintingConfig +{ + private readonly PrintingConfig _printingConfig; + + internal PropertyPrintingConfig(PrintingConfig printingConfig) + { + _printingConfig = printingConfig; + } + + internal PrintingConfig ParentConfig => _printingConfig; + + public PrintingConfig Using(Func print) + { + return _printingConfig; + } + + public PrintingConfig Using(CultureInfo culture) + { + return _printingConfig; + } + + public PrintingConfig Using(PrintingConfig propertyPrintingConfig) + { + return _printingConfig; + } +} diff --git a/ObjectPrinting/PropertyPrintingConfigExtensions.cs b/ObjectPrinting/PropertyPrintingConfigExtensions.cs new file mode 100644 index 00000000..d5274eed --- /dev/null +++ b/ObjectPrinting/PropertyPrintingConfigExtensions.cs @@ -0,0 +1,11 @@ +namespace ObjectPrinting; + +public static class PropertyPrintingConfigExtensions +{ + public static PrintingConfig TrimmedToLength( + this PropertyPrintingConfig propertyPrintingConfig, + int maxLength) + { + return propertyPrintingConfig.ParentConfig; + } +} diff --git a/ObjectPrinting_Tests/ObjectPrinting_Tests.csproj b/ObjectPrinting_Tests/ObjectPrinting_Tests.csproj new file mode 100644 index 00000000..a44b5ccb --- /dev/null +++ b/ObjectPrinting_Tests/ObjectPrinting_Tests.csproj @@ -0,0 +1,27 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + diff --git a/ObjectPrinting_Tests/PrintingConfigTests.cs b/ObjectPrinting_Tests/PrintingConfigTests.cs new file mode 100644 index 00000000..9ad9285c --- /dev/null +++ b/ObjectPrinting_Tests/PrintingConfigTests.cs @@ -0,0 +1,8 @@ +using FluentAssertions; + +namespace ObjectPrinting_Tests; + +[TestFixture] +public class PrintingConfigTests +{ +} diff --git a/fluent-api.sln b/fluent-api.sln index 6eb1804e..e2a8d54e 100644 --- a/fluent-api.sln +++ b/fluent-api.sln @@ -11,6 +11,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FluentMapping.Tests", "Samp EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Spectacle", "Samples\Spectacle\Spectacle.csproj", "{EFA9335C-411B-4597-B0B6-5438D1AE04C3}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ObjectPrinting", "ObjectPrinting\ObjectPrinting.csproj", "{441B2B47-2F12-4013-9145-BA1A79947D73}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ObjectPrinting_Tests", "ObjectPrinting_Tests\ObjectPrinting_Tests.csproj", "{ACDF68BA-F212-42A2-A6C7-F2E664DFC56F}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -29,6 +33,14 @@ 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 + {441B2B47-2F12-4013-9145-BA1A79947D73}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {441B2B47-2F12-4013-9145-BA1A79947D73}.Debug|Any CPU.Build.0 = Debug|Any CPU + {441B2B47-2F12-4013-9145-BA1A79947D73}.Release|Any CPU.ActiveCfg = Release|Any CPU + {441B2B47-2F12-4013-9145-BA1A79947D73}.Release|Any CPU.Build.0 = Release|Any CPU + {ACDF68BA-F212-42A2-A6C7-F2E664DFC56F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {ACDF68BA-F212-42A2-A6C7-F2E664DFC56F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {ACDF68BA-F212-42A2-A6C7-F2E664DFC56F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {ACDF68BA-F212-42A2-A6C7-F2E664DFC56F}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From 0f03c70a8e4b1e5c922964f9c8514087f816c04e Mon Sep 17 00:00:00 2001 From: stupidnessplusplus <58785209+stupidnessplusplus@users.noreply.github.com> Date: Thu, 12 Dec 2024 23:31:04 +0500 Subject: [PATCH 03/13] Add classes for tests --- .../ClassWithFinalTypeProperties.cs | 22 +++++++++++++++++++ .../ClassWithMultipleProperties.cs | 12 ++++++++++ .../ExampleClasses/LinkedClass.cs | 8 +++++++ 3 files changed, 42 insertions(+) create mode 100644 ObjectPrinting_Tests/ExampleClasses/ClassWithFinalTypeProperties.cs create mode 100644 ObjectPrinting_Tests/ExampleClasses/ClassWithMultipleProperties.cs create mode 100644 ObjectPrinting_Tests/ExampleClasses/LinkedClass.cs diff --git a/ObjectPrinting_Tests/ExampleClasses/ClassWithFinalTypeProperties.cs b/ObjectPrinting_Tests/ExampleClasses/ClassWithFinalTypeProperties.cs new file mode 100644 index 00000000..08a5ddad --- /dev/null +++ b/ObjectPrinting_Tests/ExampleClasses/ClassWithFinalTypeProperties.cs @@ -0,0 +1,22 @@ +namespace ObjectPrinting_Tests.ExampleClasses; + +internal class ClassWithFinalTypeProperties +{ + public bool Boolean { get; init; } + + public int Int32 { get; init; } + + public long Int64 { get; init; } + + public float Single { get; init; } + + public double Double { get; init; } + + public Guid Guid { get; init; } + + public DateTime DateTime { get; init; } + + public TimeSpan TimeSpan { get; init; } + + public string? String { get; init; } +} diff --git a/ObjectPrinting_Tests/ExampleClasses/ClassWithMultipleProperties.cs b/ObjectPrinting_Tests/ExampleClasses/ClassWithMultipleProperties.cs new file mode 100644 index 00000000..54bf722c --- /dev/null +++ b/ObjectPrinting_Tests/ExampleClasses/ClassWithMultipleProperties.cs @@ -0,0 +1,12 @@ +namespace ObjectPrinting_Tests.ExampleClasses; + +internal class ClassWithMultipleProperties +{ + public int Number1 { get; set; } + + public int Number2 { get; set; } + + public string? String1 { get; set; } + + public string? String2 { get; set; } +} diff --git a/ObjectPrinting_Tests/ExampleClasses/LinkedClass.cs b/ObjectPrinting_Tests/ExampleClasses/LinkedClass.cs new file mode 100644 index 00000000..3fa7758c --- /dev/null +++ b/ObjectPrinting_Tests/ExampleClasses/LinkedClass.cs @@ -0,0 +1,8 @@ +namespace ObjectPrinting_Tests.ExampleClasses; + +internal class LinkedClass +{ + public int Number { get; set; } + + public LinkedClass? Other { get; set; } +} From 1161a5a93c6b1d5998fe2525e6b6ed7f41880a95 Mon Sep 17 00:00:00 2001 From: stupidnessplusplus <58785209+stupidnessplusplus@users.noreply.github.com> Date: Thu, 12 Dec 2024 23:31:28 +0500 Subject: [PATCH 04/13] Add tests --- .../ObjectPrinting_Tests.csproj | 5 + ObjectPrinting_Tests/PrintingConfigTests.cs | 276 ++++++++++++++++++ ...ToString_CanLimitStringLength.verified.txt | 5 + ...TypeValues_IfUsingCultureInfo.verified.txt | 10 + ...angesValue_IfPrintIsOverriden.verified.txt | 5 + ...Values_IfMaxNestingLevelIsSet.verified.txt | 5 + ..._DoesNotPrintValue_IfExcluded.verified.txt | 2 + ...tToString_PrintsArrayElements.verified.txt | 4 + ...String_PrintsFinalTypesValues.verified.txt | 10 + ...ing_PrintsIDictionaryElements.verified.txt | 10 + ...ntToString_PrintsListElements.verified.txt | 4 + ...tToString_PrintsNestedObjects.verified.txt | 7 + 12 files changed, 343 insertions(+) create mode 100644 ObjectPrinting_Tests/PrintingConfigTestsResults/PrintingConfigTests.PrintToString_CanLimitStringLength.verified.txt create mode 100644 ObjectPrinting_Tests/PrintingConfigTestsResults/PrintingConfigTests.PrintToString_ChangesFinalTypeValues_IfUsingCultureInfo.verified.txt create mode 100644 ObjectPrinting_Tests/PrintingConfigTestsResults/PrintingConfigTests.PrintToString_ChangesValue_IfPrintIsOverriden.verified.txt create mode 100644 ObjectPrinting_Tests/PrintingConfigTestsResults/PrintingConfigTests.PrintToString_DoesNotPrintDeepPropertiesValues_IfMaxNestingLevelIsSet.verified.txt create mode 100644 ObjectPrinting_Tests/PrintingConfigTestsResults/PrintingConfigTests.PrintToString_DoesNotPrintValue_IfExcluded.verified.txt create mode 100644 ObjectPrinting_Tests/PrintingConfigTestsResults/PrintingConfigTests.PrintToString_PrintsArrayElements.verified.txt create mode 100644 ObjectPrinting_Tests/PrintingConfigTestsResults/PrintingConfigTests.PrintToString_PrintsFinalTypesValues.verified.txt create mode 100644 ObjectPrinting_Tests/PrintingConfigTestsResults/PrintingConfigTests.PrintToString_PrintsIDictionaryElements.verified.txt create mode 100644 ObjectPrinting_Tests/PrintingConfigTestsResults/PrintingConfigTests.PrintToString_PrintsListElements.verified.txt create mode 100644 ObjectPrinting_Tests/PrintingConfigTestsResults/PrintingConfigTests.PrintToString_PrintsNestedObjects.verified.txt diff --git a/ObjectPrinting_Tests/ObjectPrinting_Tests.csproj b/ObjectPrinting_Tests/ObjectPrinting_Tests.csproj index a44b5ccb..ae29d3fc 100644 --- a/ObjectPrinting_Tests/ObjectPrinting_Tests.csproj +++ b/ObjectPrinting_Tests/ObjectPrinting_Tests.csproj @@ -18,6 +18,11 @@ runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + diff --git a/ObjectPrinting_Tests/PrintingConfigTests.cs b/ObjectPrinting_Tests/PrintingConfigTests.cs index 9ad9285c..a692df8b 100644 --- a/ObjectPrinting_Tests/PrintingConfigTests.cs +++ b/ObjectPrinting_Tests/PrintingConfigTests.cs @@ -1,8 +1,284 @@ using FluentAssertions; +using ObjectPrinting; +using ObjectPrinting_Tests.ExampleClasses; +using System.Globalization; +using System.Linq.Expressions; namespace ObjectPrinting_Tests; [TestFixture] +[SetCulture("")] public class PrintingConfigTests { + private static readonly ClassWithFinalTypeProperties _classWithFinalTypeProperties = new() + { + Boolean = false, + Int32 = 123456, + Int64 = 123456, + Single = 123.456f, + Double = 123.456, + Guid = new Guid("61c3ee87-2434-4464-ba40-0226fd5fcef9"), + DateTime = new DateTime(638695533372380454), + TimeSpan = new TimeSpan(638695533372380454), + String = "abc", + }; + + private static readonly ClassWithMultipleProperties _classWithMultipleProperties = new() + { + Number1 = 123456, + Number2 = 456789, + String1 = "abcdef", + String2 = "ghijkl", + }; + + private static readonly LinkedClass _linkedClass = new() + { + Number = 1, + Other = new LinkedClass + { + Number = 2, + Other = new LinkedClass + { + Number = 3, + }, + }, + }; + + private readonly VerifySettings _verifySettings; + + public PrintingConfigTests() + { + _verifySettings = new VerifySettings(); + _verifySettings.UseDirectory("PrintingConfigTestsResults"); + } + + [Test] + public void PrintToString_CanPrintNull() + { + var obj = default(string); + var expected = $"null{Environment.NewLine}"; + + var actual = obj.PrintToString(); + + actual.Should().Be(expected); + } + + [Test] + public void PrintToString_PrintsSameStringForOneObject_WhenCalledFromConfigsOfDifferentTypes() + { + var obj = _classWithFinalTypeProperties; + + var printedByObjectPrintingConfig = ObjectPrinter.For().PrintToString(obj); + var printedByClassPrintingConfig = ObjectPrinter.For().PrintToString(obj); + + printedByObjectPrintingConfig.Should().Be(printedByClassPrintingConfig); + } + + [Test] + public Task PrintToString_PrintsFinalTypesValues() + { + var actual = _classWithFinalTypeProperties.PrintToString(); + + return Verify(actual, _verifySettings); + } + + [Test] + public Task PrintToString_PrintsNestedObjects() + { + var actual = _linkedClass.PrintToString(); + + return Verify(actual, _verifySettings); + } + + [Test] + public void PrintToString_ThrowsException_IfObjectHasCyclicReference() + { + var obj = new LinkedClass(); + obj.Other = obj; + + var print = () => obj.PrintToString(); + + print.Should().Throw() + .WithMessage("Unable to print object with cyclic reference."); + } + + [Test] + public Task PrintToString_ChangesFinalTypeValues_IfUsingCultureInfo() + { + var cultureInfo1 = new CultureInfo("ru-RU"); + var cultureInfo2 = new CultureInfo("en-US"); + + var actual = ObjectPrinter + .For() + .UsingCulture(cultureInfo1) + .Printing().Using(cultureInfo2) + .Printing().Using(cultureInfo2) + .PrintToString(_classWithFinalTypeProperties); + + return Verify(actual, _verifySettings); + } + + [Test] + public Task PrintToString_ChangesValue_IfPrintIsOverriden() + { + var actual = ObjectPrinter + .For() + .Printing(o => o.Number1).Using(i => i.ToString()[..3]) + .Printing().Using(str => $"\"{str}\"") + .PrintToString(_classWithMultipleProperties); + + return Verify(actual, _verifySettings); + } + + [Test] + public Task PrintToString_DoesNotPrintValue_IfExcluded() + { + var actual = ObjectPrinter + .For() + .Excluding(o => o.Number2) + .Excluding() + .PrintToString(_classWithMultipleProperties); + + return Verify(actual, _verifySettings); + } + + [Test] + public void PrintToString_PrintsNothing_IfExcludedOwner() + { + var actual = ObjectPrinter + .For() + .Excluding() + .PrintToString(_classWithMultipleProperties); + + actual.Should().Be(""); + } + + [Test] + public Task PrintToString_DoesNotPrintDeepPropertiesValues_IfMaxNestingLevelIsSet() + { + var actual = ObjectPrinter + .For() + .UsingMaxNestingLevel(2) + .PrintToString(_linkedClass); + + return Verify(actual, _verifySettings); + } + + [Test] + public Task PrintToString_CanLimitStringLength() + { + var actual = ObjectPrinter + .For() + .Printing(o => o.String1)!.TrimmedToLength(3) + .PrintToString(_classWithMultipleProperties); + + return Verify(actual, _verifySettings); + } + + [Test] + public Task PrintToString_PrintsArrayElements() + { + var obj = new[] { "abc", "def", "ghi" }; + + var actual = obj.PrintToString(); + + return Verify(actual, _verifySettings); + } + + [Test] + public Task PrintToString_PrintsListElements() + { + var obj = new List { "abc", "def", "ghi" }; + + var actual = obj.PrintToString(); + + return Verify(actual, _verifySettings); + } + + [Test] + public Task PrintToString_PrintsIDictionaryElements() + { + var obj = new Dictionary { { "abc", 0 }, { "def", 1 }, { "ghi", 2 } }; + + var actual = obj.PrintToString(); + + return Verify(actual, _verifySettings); + } + + [Test] + public void Printing_ThrowsException_IfExpressionIsNotPropertyPath() + { + Expression> propertySelector = o => _linkedClass.Number; + + var printing = () => ObjectPrinter + .For() + .Printing(propertySelector); + + printing.Should().Throw() + .WithMessage($"Expression '{propertySelector}' is not a property path."); + } + + [Test] + public void Printing_ThrowsException_IfExpressionIsNull() + { + var printing = () => ObjectPrinter + .For() + .Printing(default(Expression>)!); + + printing.Should().Throw(); + } + + [Test] + public void Excluding_ThrowsException_IfExpressionIsNotPropertyPath() + { + Expression> propertySelector = o => _linkedClass.Number; + + var excluding = () => ObjectPrinter + .For() + .Excluding(propertySelector); + + excluding.Should().Throw() + .WithMessage($"Expression '{propertySelector}' is not a property path."); + } + + [Test] + public void Excluding_ThrowsException_IfExpressionIsNull() + { + var excluding = () => ObjectPrinter + .For() + .Excluding(default(Expression>)!); + + excluding.Should().Throw(); + } + + [Test] + public void UsingCulture_ThrowsException_IfCultureInfoIsNull() + { + var usingCulture = () => ObjectPrinter + .For() + .UsingCulture(default!); + + usingCulture.Should().Throw(); + } + + [TestCase(-1)] + [TestCase(0)] + public void UsingMaxNestingLevel_ThrowsException_IfValueIsNegativeOrZero(int maxNestingLevel) + { + var usingMaxNestingLevel = () => ObjectPrinter + .For() + .UsingMaxNestingLevel(maxNestingLevel); + + usingMaxNestingLevel.Should().Throw(); + } + + [Test] + public void Using_ThrowsException_IfFuncIsNull() + { + var usingPrint = () => ObjectPrinter + .For() + .Printing().Using(default(Func)!); + + usingPrint.Should().Throw(); + } } diff --git a/ObjectPrinting_Tests/PrintingConfigTestsResults/PrintingConfigTests.PrintToString_CanLimitStringLength.verified.txt b/ObjectPrinting_Tests/PrintingConfigTestsResults/PrintingConfigTests.PrintToString_CanLimitStringLength.verified.txt new file mode 100644 index 00000000..e62807c9 --- /dev/null +++ b/ObjectPrinting_Tests/PrintingConfigTestsResults/PrintingConfigTests.PrintToString_CanLimitStringLength.verified.txt @@ -0,0 +1,5 @@ +ClassWithMultipleProperties + Number1 = 123456 + Number2 = 456789 + String1 = abc + String2 = ghijkl diff --git a/ObjectPrinting_Tests/PrintingConfigTestsResults/PrintingConfigTests.PrintToString_ChangesFinalTypeValues_IfUsingCultureInfo.verified.txt b/ObjectPrinting_Tests/PrintingConfigTestsResults/PrintingConfigTests.PrintToString_ChangesFinalTypeValues_IfUsingCultureInfo.verified.txt new file mode 100644 index 00000000..22ccf176 --- /dev/null +++ b/ObjectPrinting_Tests/PrintingConfigTestsResults/PrintingConfigTests.PrintToString_ChangesFinalTypeValues_IfUsingCultureInfo.verified.txt @@ -0,0 +1,10 @@ +ClassWithFinalTypeProperties + Boolean = False + Int32 = 123456 + Int64 = 123456 + Single = 123,456 + Double = 123.456 + Guid = 61c3ee87-2434-4464-ba40-0226fd5fcef9 + DateTime = 12/11/2024 10:35:37 PM + TimeSpan = 739230.22:35:37.2380454 + String = abc diff --git a/ObjectPrinting_Tests/PrintingConfigTestsResults/PrintingConfigTests.PrintToString_ChangesValue_IfPrintIsOverriden.verified.txt b/ObjectPrinting_Tests/PrintingConfigTestsResults/PrintingConfigTests.PrintToString_ChangesValue_IfPrintIsOverriden.verified.txt new file mode 100644 index 00000000..65c8653f --- /dev/null +++ b/ObjectPrinting_Tests/PrintingConfigTestsResults/PrintingConfigTests.PrintToString_ChangesValue_IfPrintIsOverriden.verified.txt @@ -0,0 +1,5 @@ +ClassWithMultipleProperties + Number1 = 123 + Number2 = 456789 + String1 = "abcdef" + String2 = "ghijkl" diff --git a/ObjectPrinting_Tests/PrintingConfigTestsResults/PrintingConfigTests.PrintToString_DoesNotPrintDeepPropertiesValues_IfMaxNestingLevelIsSet.verified.txt b/ObjectPrinting_Tests/PrintingConfigTestsResults/PrintingConfigTests.PrintToString_DoesNotPrintDeepPropertiesValues_IfMaxNestingLevelIsSet.verified.txt new file mode 100644 index 00000000..bca817ad --- /dev/null +++ b/ObjectPrinting_Tests/PrintingConfigTestsResults/PrintingConfigTests.PrintToString_DoesNotPrintDeepPropertiesValues_IfMaxNestingLevelIsSet.verified.txt @@ -0,0 +1,5 @@ +LinkedClass + Number = 1 + Other = LinkedClass + Number = 2 + Other = LinkedClass diff --git a/ObjectPrinting_Tests/PrintingConfigTestsResults/PrintingConfigTests.PrintToString_DoesNotPrintValue_IfExcluded.verified.txt b/ObjectPrinting_Tests/PrintingConfigTestsResults/PrintingConfigTests.PrintToString_DoesNotPrintValue_IfExcluded.verified.txt new file mode 100644 index 00000000..14b347c2 --- /dev/null +++ b/ObjectPrinting_Tests/PrintingConfigTestsResults/PrintingConfigTests.PrintToString_DoesNotPrintValue_IfExcluded.verified.txt @@ -0,0 +1,2 @@ +ClassWithMultipleProperties + Number1 = 123456 diff --git a/ObjectPrinting_Tests/PrintingConfigTestsResults/PrintingConfigTests.PrintToString_PrintsArrayElements.verified.txt b/ObjectPrinting_Tests/PrintingConfigTestsResults/PrintingConfigTests.PrintToString_PrintsArrayElements.verified.txt new file mode 100644 index 00000000..04f7e27a --- /dev/null +++ b/ObjectPrinting_Tests/PrintingConfigTestsResults/PrintingConfigTests.PrintToString_PrintsArrayElements.verified.txt @@ -0,0 +1,4 @@ +String[] + {0} = abc + {1} = def + {2} = ghi diff --git a/ObjectPrinting_Tests/PrintingConfigTestsResults/PrintingConfigTests.PrintToString_PrintsFinalTypesValues.verified.txt b/ObjectPrinting_Tests/PrintingConfigTestsResults/PrintingConfigTests.PrintToString_PrintsFinalTypesValues.verified.txt new file mode 100644 index 00000000..99dd047c --- /dev/null +++ b/ObjectPrinting_Tests/PrintingConfigTestsResults/PrintingConfigTests.PrintToString_PrintsFinalTypesValues.verified.txt @@ -0,0 +1,10 @@ +ClassWithFinalTypeProperties + Boolean = False + Int32 = 123456 + Int64 = 123456 + Single = 123.456 + Double = 123.456 + Guid = 61c3ee87-2434-4464-ba40-0226fd5fcef9 + DateTime = 12/11/2024 22:35:37 + TimeSpan = 739230.22:35:37.2380454 + String = abc diff --git a/ObjectPrinting_Tests/PrintingConfigTestsResults/PrintingConfigTests.PrintToString_PrintsIDictionaryElements.verified.txt b/ObjectPrinting_Tests/PrintingConfigTestsResults/PrintingConfigTests.PrintToString_PrintsIDictionaryElements.verified.txt new file mode 100644 index 00000000..ea668402 --- /dev/null +++ b/ObjectPrinting_Tests/PrintingConfigTestsResults/PrintingConfigTests.PrintToString_PrintsIDictionaryElements.verified.txt @@ -0,0 +1,10 @@ +Dictionary + {0} = KeyValuePair + Key = abc + Value = 0 + {1} = KeyValuePair + Key = def + Value = 1 + {2} = KeyValuePair + Key = ghi + Value = 2 diff --git a/ObjectPrinting_Tests/PrintingConfigTestsResults/PrintingConfigTests.PrintToString_PrintsListElements.verified.txt b/ObjectPrinting_Tests/PrintingConfigTestsResults/PrintingConfigTests.PrintToString_PrintsListElements.verified.txt new file mode 100644 index 00000000..544d9a15 --- /dev/null +++ b/ObjectPrinting_Tests/PrintingConfigTestsResults/PrintingConfigTests.PrintToString_PrintsListElements.verified.txt @@ -0,0 +1,4 @@ +List + {0} = abc + {1} = def + {2} = ghi diff --git a/ObjectPrinting_Tests/PrintingConfigTestsResults/PrintingConfigTests.PrintToString_PrintsNestedObjects.verified.txt b/ObjectPrinting_Tests/PrintingConfigTestsResults/PrintingConfigTests.PrintToString_PrintsNestedObjects.verified.txt new file mode 100644 index 00000000..4a0b1b59 --- /dev/null +++ b/ObjectPrinting_Tests/PrintingConfigTestsResults/PrintingConfigTests.PrintToString_PrintsNestedObjects.verified.txt @@ -0,0 +1,7 @@ +LinkedClass + Number = 1 + Other = LinkedClass + Number = 2 + Other = LinkedClass + Number = 3 + Other = null From 0720f9073fd370f550f3184fd4ab2644f40dfe35 Mon Sep 17 00:00:00 2001 From: stupidnessplusplus <58785209+stupidnessplusplus@users.noreply.github.com> Date: Thu, 12 Dec 2024 23:43:01 +0500 Subject: [PATCH 05/13] Add PropertyPath and PropertyValue --- ObjectPrinting/PropertyPath.cs | 95 +++++++++++++++++++++++++++++++++ ObjectPrinting/PropertyValue.cs | 11 ++++ 2 files changed, 106 insertions(+) create mode 100644 ObjectPrinting/PropertyPath.cs create mode 100644 ObjectPrinting/PropertyValue.cs diff --git a/ObjectPrinting/PropertyPath.cs b/ObjectPrinting/PropertyPath.cs new file mode 100644 index 00000000..a6da2410 --- /dev/null +++ b/ObjectPrinting/PropertyPath.cs @@ -0,0 +1,95 @@ +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using System.Linq.Expressions; + +namespace ObjectPrinting; + +internal class PropertyPath +{ + private static readonly EqualityComparer _referenceComparer = EqualityComparer + .Create(ReferenceEquals, obj => obj.GetHashCode()); + + private readonly ImmutableHashSet _values; + private readonly int _hashCode; + + public PropertyPath(PropertyValue propertyValue, PropertyPath? previous = null) + { + PropertyValue = propertyValue; + Previous = previous; + _values = previous?._values ?? ImmutableHashSet.Create(_referenceComparer); + + if (propertyValue.Value != null && !propertyValue.Value.GetType().IsValueType) + { + _values = _values.Add(propertyValue.Value); + } + + _hashCode = HashCode.Combine(propertyValue.DeclaringType, propertyValue.Name, previous); + } + + public PropertyValue? PropertyValue { get; } + + public PropertyPath? Previous { get; } + + public override bool Equals(object? obj) + { + return obj is PropertyPath propertyPath + && AreEqualPropertyValues(PropertyValue, propertyPath.PropertyValue) + && (Previous == propertyPath.Previous + || Previous != null && Previous.Equals(propertyPath.Previous)); + } + + public override int GetHashCode() + { + return _hashCode; + } + + public bool Contains(object? obj) + { + return obj != null + && _values.TryGetValue(obj, out var key) + && ReferenceEquals(obj, key); + } + + public static bool TryGetPropertyPath( + Expression> propertySelector, + [MaybeNullWhen(false)] out PropertyPath path) + { + var propertyNames = propertySelector.Body.ToString().Split('.'); + var type = typeof(TOwner); + path = default; + + if (propertyNames.Length >= 1 && propertyNames[0] == propertySelector.Parameters.Single().Name) + { + path = new PropertyPath(new PropertyValue(null, null), null); + } + else + { + path = default; + return false; + } + + for (var i = 1; i < propertyNames.Length; i++) + { + var property = type.GetProperty(propertyNames[i]); + + if (property == null) + { + path = default; + return false; + } + + type = property.PropertyType; + path = new PropertyPath(new PropertyValue(property, null), path); + } + + return true; + } + + private static bool AreEqualPropertyValues(PropertyValue? property1, PropertyValue? property2) + { + return property1 == property2 + || property1 != null && property2 != null + && property1.DeclaringType == property2.DeclaringType + && property1.Name == property2.Name; + } +} diff --git a/ObjectPrinting/PropertyValue.cs b/ObjectPrinting/PropertyValue.cs new file mode 100644 index 00000000..d512b15a --- /dev/null +++ b/ObjectPrinting/PropertyValue.cs @@ -0,0 +1,11 @@ +using System.Reflection; + +namespace ObjectPrinting; + +internal record PropertyValue(string? Name, Type? DeclaringType, object? Value) +{ + public PropertyValue(PropertyInfo? propertyInfo, object? value) + : this(propertyInfo?.Name, propertyInfo?.DeclaringType, value) + { + } +} From 43dc28ac12d5114e00a71629ab6bbebcbc66215a Mon Sep 17 00:00:00 2001 From: stupidnessplusplus <58785209+stupidnessplusplus@users.noreply.github.com> Date: Thu, 12 Dec 2024 23:43:43 +0500 Subject: [PATCH 06/13] Add internal PrintingConfig interfaces --- ObjectPrinting/IPrintingConfig.cs | 16 ++++++++++++++++ ObjectPrinting/IPropertyPrintingConfig.cs | 14 ++++++++++++++ 2 files changed, 30 insertions(+) create mode 100644 ObjectPrinting/IPrintingConfig.cs create mode 100644 ObjectPrinting/IPropertyPrintingConfig.cs diff --git a/ObjectPrinting/IPrintingConfig.cs b/ObjectPrinting/IPrintingConfig.cs new file mode 100644 index 00000000..6efb7754 --- /dev/null +++ b/ObjectPrinting/IPrintingConfig.cs @@ -0,0 +1,16 @@ +using System.Globalization; + +namespace ObjectPrinting; + +internal interface IPrintingConfig +{ + Dictionary> PropertyConfigsByType { get; } + + Dictionary> PropertyConfigsByPath { get; } + + CultureInfo CultureInfo { get; } + + bool IsToLimitNestingLevel { get; } + + int MaxNestingLevel { get; } +} diff --git a/ObjectPrinting/IPropertyPrintingConfig.cs b/ObjectPrinting/IPropertyPrintingConfig.cs new file mode 100644 index 00000000..c6cdad07 --- /dev/null +++ b/ObjectPrinting/IPropertyPrintingConfig.cs @@ -0,0 +1,14 @@ +using System.Globalization; + +namespace ObjectPrinting; + +internal interface IPropertyPrintingConfig +{ + PrintingConfig ParentConfig { get; } + + bool IsExcluded { get; } + + CultureInfo? CultureInfo { get; } + + Func? PrintOverride { get; } +} From 98f8bcf6634442ec80996df49c7102998b348183 Mon Sep 17 00:00:00 2001 From: stupidnessplusplus <58785209+stupidnessplusplus@users.noreply.github.com> Date: Thu, 12 Dec 2024 23:46:33 +0500 Subject: [PATCH 07/13] Add main classes --- ObjectPrinting/ObjectPrinter.cs | 146 ++++++++++++++++++ ObjectPrinting/PrintingConfig.cs | 74 +++++++-- ObjectPrinting/PropertyPrintingConfig.cs | 22 ++- .../PropertyPrintingConfigExtensions.cs | 9 +- 4 files changed, 230 insertions(+), 21 deletions(-) diff --git a/ObjectPrinting/ObjectPrinter.cs b/ObjectPrinting/ObjectPrinter.cs index c46fc08a..ef502820 100644 --- a/ObjectPrinting/ObjectPrinter.cs +++ b/ObjectPrinting/ObjectPrinter.cs @@ -1,9 +1,155 @@ +using System.Collections; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Reflection; +using System.Text; + namespace ObjectPrinting; public class ObjectPrinter { + private static readonly HashSet _finalTypes = + [ + typeof(Guid), + typeof(DateTime), + typeof(TimeSpan), + typeof(string), + ]; + public static PrintingConfig For() { return new PrintingConfig(); } + + internal static string PrintToString(TOwner? obj, IPrintingConfig printingConfig) + { + var sb = new StringBuilder(); + var stack = new Stack>(); + var path = default(PropertyPath); + + if (TryGetStringValue(printingConfig, new PropertyValue(null, obj), stack, ref path, out var stringValue)) + { + sb.AppendLine(stringValue); + } + + while (stack.TryPeek(out var enumerator)) + { + if (!enumerator.MoveNext()) + { + stack.Pop(); + path = path!.Previous; + continue; + } + + var propertyValue = enumerator.Current; + var indentation = stack.Count; + + if (path != null && path.Contains(propertyValue.Value)) + { + throw new InvalidOperationException("Unable to print object with cyclic reference."); + } + + if (TryGetStringValue(printingConfig, propertyValue, stack, ref path, out stringValue)) + { + sb.Append('\t', indentation); + sb.AppendLine($"{propertyValue.Name} = {stringValue}"); + } + } + + return sb.ToString(); + } + + private static bool TryGetStringValue( + IPrintingConfig printingConfig, + PropertyValue propertyValue, + Stack> propertyTreeStack, + ref PropertyPath? path, + [MaybeNullWhen(false)] out string stringValue) + { + var obj = propertyValue.Value; + + if (obj == null) + { + propertyTreeStack.TryPop(out _); + stringValue = "null"; + return true; + } + + var type = obj.GetType(); + var nextPath = new PropertyPath(propertyValue, path); + + var hasConfig = printingConfig.PropertyConfigsByPath.TryGetValue(nextPath, out var propertyPrintingConfig) + || printingConfig.PropertyConfigsByType.TryGetValue(type, out propertyPrintingConfig); + + if (hasConfig && propertyPrintingConfig!.IsExcluded) + { + propertyTreeStack.TryPop(out _); + stringValue = default; + return false; + } + + if (hasConfig && propertyPrintingConfig!.PrintOverride != null) + { + stringValue = propertyPrintingConfig.PrintOverride(obj); + } + else if (IsFinalType(type)) + { + var cultureInfo = hasConfig && propertyPrintingConfig!.CultureInfo != null + ? propertyPrintingConfig.CultureInfo + : printingConfig.CultureInfo; + stringValue = PrintFinalTypeToString(obj, cultureInfo); + } + else if (obj is IEnumerable enumerable) + { + var values = enumerable + .Cast() + .Select((item, i) => new PropertyValue($"{{{i}}}", null, item)); + propertyTreeStack.Push(values.GetEnumerator()); + path = nextPath; + stringValue = GetNameWithoutGenericPart(type); + } + else + { + if (!printingConfig.IsToLimitNestingLevel + || propertyTreeStack.Count < printingConfig.MaxNestingLevel) + { + propertyTreeStack.Push(GetPropertyValues(obj).GetEnumerator()); + path = nextPath; + } + + stringValue = GetNameWithoutGenericPart(type); + } + + return true; + } + + private static bool IsFinalType(Type type) + { + return type.IsPrimitive + || _finalTypes.Contains(type); + } + + private static string PrintFinalTypeToString(object obj, CultureInfo cultureInfo) + { + return obj is IFormattable formattableObj + ? formattableObj.ToString(null, cultureInfo) + : obj.ToString()!; + } + + private static IEnumerable GetPropertyValues(object obj) + { + return obj + .GetType() + .GetProperties(BindingFlags.Public | BindingFlags.Instance) + .Where(property => property.GetIndexParameters().Length == 0) + .Select(property => new PropertyValue(property, property.GetValue(obj))); + } + + private static string GetNameWithoutGenericPart(Type type) + { + var end = type.Name.IndexOf('`'); + return end == -1 + ? type.Name + : type.Name[..end]; + } } diff --git a/ObjectPrinting/PrintingConfig.cs b/ObjectPrinting/PrintingConfig.cs index 71ab04fb..b6425469 100644 --- a/ObjectPrinting/PrintingConfig.cs +++ b/ObjectPrinting/PrintingConfig.cs @@ -1,56 +1,98 @@ using System.Globalization; using System.Linq.Expressions; -using System.Reflection; namespace ObjectPrinting; -public class PrintingConfig +public class PrintingConfig : IPrintingConfig { + private readonly Dictionary> _propertyConfigsByType = []; + private readonly Dictionary> _propertyConfigsByPath = []; + + private CultureInfo _cultureInfo = CultureInfo.InvariantCulture; + private bool _isToLimitNestingLevel = false; + private int _maxNestingLevel = 10; + internal PrintingConfig() { } + Dictionary> IPrintingConfig.PropertyConfigsByType + => _propertyConfigsByType; + + Dictionary> IPrintingConfig.PropertyConfigsByPath + => _propertyConfigsByPath; + + CultureInfo IPrintingConfig.CultureInfo => _cultureInfo; + + bool IPrintingConfig.IsToLimitNestingLevel => _isToLimitNestingLevel; + + int IPrintingConfig.MaxNestingLevel => _maxNestingLevel; + public PropertyPrintingConfig Printing() { - return new PropertyPrintingConfig(this); + var propertyType = typeof(TPropType); + + if (!_propertyConfigsByType.TryGetValue(propertyType, out var propertyPrintingConfig)) + { + propertyPrintingConfig = new PropertyPrintingConfig(this); + _propertyConfigsByType[propertyType] = propertyPrintingConfig; + } + + return (PropertyPrintingConfig)propertyPrintingConfig; } public PropertyPrintingConfig Printing( Expression> propertySelector) { - return new PropertyPrintingConfig(this); - } + ArgumentNullException.ThrowIfNull(propertySelector); - public PropertyPrintingConfig Printing( - Func propertyFilter) - { - return new PropertyPrintingConfig(this); + if (!PropertyPath.TryGetPropertyPath(propertySelector, out var propertyPath)) + { + var propertySelectorString = propertySelector.Body.ToString(); + throw new ArgumentException($"Expression '{propertySelector}' is not a property path."); + } + + if (!_propertyConfigsByPath.TryGetValue(propertyPath, out var propertyPrintingConfig)) + { + propertyPrintingConfig = new PropertyPrintingConfig(this); + _propertyConfigsByPath[propertyPath] = propertyPrintingConfig; + } + + return (PropertyPrintingConfig)propertyPrintingConfig; } public PrintingConfig Excluding() { - return this; + return Printing().Exclude(); } public PrintingConfig Excluding( Expression> propertySelector) { - return this; + ArgumentNullException.ThrowIfNull(propertySelector); + + return Printing(propertySelector).Exclude(); } - public PrintingConfig Excluding( - Func propertyFilter) + public PrintingConfig UsingCulture(CultureInfo cultureInfo) { + ArgumentNullException.ThrowIfNull(cultureInfo); + + _cultureInfo = cultureInfo; return this; } - public PrintingConfig Using(CultureInfo culture) + public PrintingConfig UsingMaxNestingLevel(int maxNestingLevel) { + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(maxNestingLevel); + + _isToLimitNestingLevel = true; + _maxNestingLevel = maxNestingLevel; return this; } - public string PrintToString(TOwner obj) + public string PrintToString(TOwner? obj) { - throw new NotImplementedException(); + return ObjectPrinter.PrintToString(obj, this); } } diff --git a/ObjectPrinting/PropertyPrintingConfig.cs b/ObjectPrinting/PropertyPrintingConfig.cs index e59765f9..97b56ae7 100644 --- a/ObjectPrinting/PropertyPrintingConfig.cs +++ b/ObjectPrinting/PropertyPrintingConfig.cs @@ -2,29 +2,43 @@ namespace ObjectPrinting; -public class PropertyPrintingConfig +public class PropertyPrintingConfig : IPropertyPrintingConfig { private readonly PrintingConfig _printingConfig; + private CultureInfo? _cultureInfo; + private Func? _printOverride; + private bool _isExcluded; internal PropertyPrintingConfig(PrintingConfig printingConfig) { _printingConfig = printingConfig; } - internal PrintingConfig ParentConfig => _printingConfig; + PrintingConfig IPropertyPrintingConfig.ParentConfig => _printingConfig; + + bool IPropertyPrintingConfig.IsExcluded => _isExcluded; + + CultureInfo? IPropertyPrintingConfig.CultureInfo => _cultureInfo; + + Func? IPropertyPrintingConfig.PrintOverride => _printOverride; public PrintingConfig Using(Func print) { + ArgumentNullException.ThrowIfNull(print); + + _printOverride = obj => print((TPropType)obj); return _printingConfig; } - public PrintingConfig Using(CultureInfo culture) + public PrintingConfig Using(CultureInfo cultureInfo) { + _cultureInfo = cultureInfo; return _printingConfig; } - public PrintingConfig Using(PrintingConfig propertyPrintingConfig) + internal PrintingConfig Exclude() { + _isExcluded = true; return _printingConfig; } } diff --git a/ObjectPrinting/PropertyPrintingConfigExtensions.cs b/ObjectPrinting/PropertyPrintingConfigExtensions.cs index d5274eed..04a55226 100644 --- a/ObjectPrinting/PropertyPrintingConfigExtensions.cs +++ b/ObjectPrinting/PropertyPrintingConfigExtensions.cs @@ -6,6 +6,13 @@ public static PrintingConfig TrimmedToLength( this PropertyPrintingConfig propertyPrintingConfig, int maxLength) { - return propertyPrintingConfig.ParentConfig; + var propertyPrintingConfigInterface = (IPropertyPrintingConfig)propertyPrintingConfig; + var printOverride = propertyPrintingConfigInterface.PrintOverride; + + Func newPrintOverride = printOverride == null + ? str => str[..maxLength] + : str => printOverride(str)[..maxLength]; + + return propertyPrintingConfig.Using(newPrintOverride); } } From c3ba1982f43822b0844cba8ca5e05fcae7024658 Mon Sep 17 00:00:00 2001 From: stupidnessplusplus <58785209+stupidnessplusplus@users.noreply.github.com> Date: Fri, 13 Dec 2024 00:24:34 +0500 Subject: [PATCH 08/13] Remove unnecessary check --- ObjectPrinting/PropertyPath.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/ObjectPrinting/PropertyPath.cs b/ObjectPrinting/PropertyPath.cs index a6da2410..77c644d8 100644 --- a/ObjectPrinting/PropertyPath.cs +++ b/ObjectPrinting/PropertyPath.cs @@ -45,9 +45,7 @@ public override int GetHashCode() public bool Contains(object? obj) { - return obj != null - && _values.TryGetValue(obj, out var key) - && ReferenceEquals(obj, key); + return obj != null && _values.Contains(obj); } public static bool TryGetPropertyPath( From 5c19883c5c839f131ced36c9dabb6350211b6277 Mon Sep 17 00:00:00 2001 From: stupidnessplusplus <58785209+stupidnessplusplus@users.noreply.github.com> Date: Fri, 13 Dec 2024 15:00:44 +0500 Subject: [PATCH 09/13] Add test --- ObjectPrinting_Tests/PrintingConfigTests.cs | 36 ++++++++++++------- ...ype_IfHasConfigByPropertyPath.verified.txt | 4 +++ 2 files changed, 28 insertions(+), 12 deletions(-) create mode 100644 ObjectPrinting_Tests/PrintingConfigTestsResults/PrintingConfigTests.PrintToString_IgnoresConfigByType_IfHasConfigByPropertyPath.verified.txt diff --git a/ObjectPrinting_Tests/PrintingConfigTests.cs b/ObjectPrinting_Tests/PrintingConfigTests.cs index a692df8b..250de8da 100644 --- a/ObjectPrinting_Tests/PrintingConfigTests.cs +++ b/ObjectPrinting_Tests/PrintingConfigTests.cs @@ -90,18 +90,6 @@ public Task PrintToString_PrintsNestedObjects() return Verify(actual, _verifySettings); } - [Test] - public void PrintToString_ThrowsException_IfObjectHasCyclicReference() - { - var obj = new LinkedClass(); - obj.Other = obj; - - var print = () => obj.PrintToString(); - - print.Should().Throw() - .WithMessage("Unable to print object with cyclic reference."); - } - [Test] public Task PrintToString_ChangesFinalTypeValues_IfUsingCultureInfo() { @@ -205,6 +193,30 @@ public Task PrintToString_PrintsIDictionaryElements() return Verify(actual, _verifySettings); } + [Test] + public Task PrintToString_IgnoresConfigByType_IfHasConfigByPropertyPath() + { + var actual = ObjectPrinter + .For() + .Excluding() + .Printing(o => o.String1).Using(str => $"\"{str}\"") + .PrintToString(_classWithMultipleProperties); + + return Verify(actual, _verifySettings); + } + + [Test] + public void PrintToString_ThrowsException_IfObjectHasCyclicReference() + { + var obj = new LinkedClass(); + obj.Other = obj; + + var print = () => obj.PrintToString(); + + print.Should().Throw() + .WithMessage("Unable to print object with cyclic reference."); + } + [Test] public void Printing_ThrowsException_IfExpressionIsNotPropertyPath() { diff --git a/ObjectPrinting_Tests/PrintingConfigTestsResults/PrintingConfigTests.PrintToString_IgnoresConfigByType_IfHasConfigByPropertyPath.verified.txt b/ObjectPrinting_Tests/PrintingConfigTestsResults/PrintingConfigTests.PrintToString_IgnoresConfigByType_IfHasConfigByPropertyPath.verified.txt new file mode 100644 index 00000000..23001642 --- /dev/null +++ b/ObjectPrinting_Tests/PrintingConfigTestsResults/PrintingConfigTests.PrintToString_IgnoresConfigByType_IfHasConfigByPropertyPath.verified.txt @@ -0,0 +1,4 @@ +ClassWithMultipleProperties + Number1 = 123456 + Number2 = 456789 + String1 = "abcdef" From 0ad59959d888c5b1b47d8c49703dec1fb115b0d0 Mon Sep 17 00:00:00 2001 From: stupidnessplusplus <58785209+stupidnessplusplus@users.noreply.github.com> Date: Sat, 14 Dec 2024 20:45:22 +0500 Subject: [PATCH 10/13] Fix property enumeration stops after null or excluded property --- ObjectPrinting/ObjectPrinter.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/ObjectPrinting/ObjectPrinter.cs b/ObjectPrinting/ObjectPrinter.cs index ef502820..4d727017 100644 --- a/ObjectPrinting/ObjectPrinter.cs +++ b/ObjectPrinting/ObjectPrinter.cs @@ -70,7 +70,6 @@ private static bool TryGetStringValue( if (obj == null) { - propertyTreeStack.TryPop(out _); stringValue = "null"; return true; } @@ -83,7 +82,6 @@ private static bool TryGetStringValue( if (hasConfig && propertyPrintingConfig!.IsExcluded) { - propertyTreeStack.TryPop(out _); stringValue = default; return false; } From fcbd4bccaad1ed811cce2ff8767ba3b30dee63c9 Mon Sep 17 00:00:00 2001 From: stupidnessplusplus <58785209+stupidnessplusplus@users.noreply.github.com> Date: Sat, 14 Dec 2024 20:46:43 +0500 Subject: [PATCH 11/13] Add tests for nested objects, rename test --- .../ClassWithNotFinalTypeProperties.cs | 8 +++++++ ObjectPrinting_Tests/PrintingConfigTests.cs | 22 ++++++++++++++++++- ...ing_PrintsDictionaryElements.verified.txt} | 0 ...String_PrintsNestedEnumerable.verified.txt | 5 +++++ ...sNestedObjectsOfDifferentType.verified.txt | 7 ++++++ 5 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 ObjectPrinting_Tests/ExampleClasses/ClassWithNotFinalTypeProperties.cs rename ObjectPrinting_Tests/PrintingConfigTestsResults/{PrintingConfigTests.PrintToString_PrintsIDictionaryElements.verified.txt => PrintingConfigTests.PrintToString_PrintsDictionaryElements.verified.txt} (100%) create mode 100644 ObjectPrinting_Tests/PrintingConfigTestsResults/PrintingConfigTests.PrintToString_PrintsNestedEnumerable.verified.txt create mode 100644 ObjectPrinting_Tests/PrintingConfigTestsResults/PrintingConfigTests.PrintToString_PrintsNestedObjectsOfDifferentType.verified.txt diff --git a/ObjectPrinting_Tests/ExampleClasses/ClassWithNotFinalTypeProperties.cs b/ObjectPrinting_Tests/ExampleClasses/ClassWithNotFinalTypeProperties.cs new file mode 100644 index 00000000..f89b567a --- /dev/null +++ b/ObjectPrinting_Tests/ExampleClasses/ClassWithNotFinalTypeProperties.cs @@ -0,0 +1,8 @@ +namespace ObjectPrinting_Tests.ExampleClasses; + +internal class ClassWithNotFinalTypeProperties +{ + public ClassWithMultipleProperties? Other { get; set; } + + public string[]? Strings { get; set; } +} diff --git a/ObjectPrinting_Tests/PrintingConfigTests.cs b/ObjectPrinting_Tests/PrintingConfigTests.cs index 250de8da..0efe28ff 100644 --- a/ObjectPrinting_Tests/PrintingConfigTests.cs +++ b/ObjectPrinting_Tests/PrintingConfigTests.cs @@ -90,6 +90,16 @@ public Task PrintToString_PrintsNestedObjects() return Verify(actual, _verifySettings); } + [Test] + public Task PrintToString_PrintsNestedObjectsOfDifferentType() + { + var obj = new ClassWithNotFinalTypeProperties { Other = _classWithMultipleProperties }; + + var actual = obj.PrintToString(); + + return Verify(actual, _verifySettings); + } + [Test] public Task PrintToString_ChangesFinalTypeValues_IfUsingCultureInfo() { @@ -184,7 +194,7 @@ public Task PrintToString_PrintsListElements() } [Test] - public Task PrintToString_PrintsIDictionaryElements() + public Task PrintToString_PrintsDictionaryElements() { var obj = new Dictionary { { "abc", 0 }, { "def", 1 }, { "ghi", 2 } }; @@ -193,6 +203,16 @@ public Task PrintToString_PrintsIDictionaryElements() return Verify(actual, _verifySettings); } + [Test] + public Task PrintToString_PrintsNestedEnumerable() + { + var obj = new ClassWithNotFinalTypeProperties { Strings = ["abc", "def"] }; + + var actual = obj.PrintToString(); + + return Verify(actual, _verifySettings); + } + [Test] public Task PrintToString_IgnoresConfigByType_IfHasConfigByPropertyPath() { diff --git a/ObjectPrinting_Tests/PrintingConfigTestsResults/PrintingConfigTests.PrintToString_PrintsIDictionaryElements.verified.txt b/ObjectPrinting_Tests/PrintingConfigTestsResults/PrintingConfigTests.PrintToString_PrintsDictionaryElements.verified.txt similarity index 100% rename from ObjectPrinting_Tests/PrintingConfigTestsResults/PrintingConfigTests.PrintToString_PrintsIDictionaryElements.verified.txt rename to ObjectPrinting_Tests/PrintingConfigTestsResults/PrintingConfigTests.PrintToString_PrintsDictionaryElements.verified.txt diff --git a/ObjectPrinting_Tests/PrintingConfigTestsResults/PrintingConfigTests.PrintToString_PrintsNestedEnumerable.verified.txt b/ObjectPrinting_Tests/PrintingConfigTestsResults/PrintingConfigTests.PrintToString_PrintsNestedEnumerable.verified.txt new file mode 100644 index 00000000..26b43186 --- /dev/null +++ b/ObjectPrinting_Tests/PrintingConfigTestsResults/PrintingConfigTests.PrintToString_PrintsNestedEnumerable.verified.txt @@ -0,0 +1,5 @@ +ClassWithNotFinalTypeProperties + Other = null + Strings = String[] + {0} = abc + {1} = def diff --git a/ObjectPrinting_Tests/PrintingConfigTestsResults/PrintingConfigTests.PrintToString_PrintsNestedObjectsOfDifferentType.verified.txt b/ObjectPrinting_Tests/PrintingConfigTestsResults/PrintingConfigTests.PrintToString_PrintsNestedObjectsOfDifferentType.verified.txt new file mode 100644 index 00000000..1ccca272 --- /dev/null +++ b/ObjectPrinting_Tests/PrintingConfigTestsResults/PrintingConfigTests.PrintToString_PrintsNestedObjectsOfDifferentType.verified.txt @@ -0,0 +1,7 @@ +ClassWithNotFinalTypeProperties + Other = ClassWithMultipleProperties + Number1 = 123456 + Number2 = 456789 + String1 = abcdef + String2 = ghijkl + Strings = null From 8046986fa1def3471d872650c6f1c2327e79fbdc Mon Sep 17 00:00:00 2001 From: stupidnessplusplus <58785209+stupidnessplusplus@users.noreply.github.com> Date: Sun, 15 Dec 2024 18:17:52 +0500 Subject: [PATCH 12/13] Change cyclic reference tests --- ObjectPrinting_Tests/PrintingConfigTests.cs | 34 ++++++++++++------- ...yclicReferenceToInitialObject.verified.txt | 5 +++ ...CyclicReferenceToNestedObject.verified.txt | 7 ++++ 3 files changed, 34 insertions(+), 12 deletions(-) create mode 100644 ObjectPrinting_Tests/PrintingConfigTestsResults/PrintingConfigTests.PrintToString_DoesNotPrintObject_WhenHasCyclicReferenceToInitialObject.verified.txt create mode 100644 ObjectPrinting_Tests/PrintingConfigTestsResults/PrintingConfigTests.PrintToString_DoesNotPrintObject_WhenHasCyclicReferenceToNestedObject.verified.txt diff --git a/ObjectPrinting_Tests/PrintingConfigTests.cs b/ObjectPrinting_Tests/PrintingConfigTests.cs index 0efe28ff..73b1e897 100644 --- a/ObjectPrinting_Tests/PrintingConfigTests.cs +++ b/ObjectPrinting_Tests/PrintingConfigTests.cs @@ -100,6 +100,28 @@ public Task PrintToString_PrintsNestedObjectsOfDifferentType() return Verify(actual, _verifySettings); } + [Test] + public Task PrintToString_DoesNotPrintObject_WhenHasCyclicReferenceToInitialObject() + { + var obj = new LinkedClass { Other = new LinkedClass() }; + obj.Other.Other = obj; + + var actual = obj.PrintToString(); + + return Verify(actual, _verifySettings); + } + + [Test] + public Task PrintToString_DoesNotPrintObject_WhenHasCyclicReferenceToNestedObject() + { + var obj = new LinkedClass { Other = new LinkedClass { Other = new LinkedClass() } }; + obj.Other.Other.Other = obj.Other; + + var actual = obj.PrintToString(); + + return Verify(actual, _verifySettings); + } + [Test] public Task PrintToString_ChangesFinalTypeValues_IfUsingCultureInfo() { @@ -225,18 +247,6 @@ public Task PrintToString_IgnoresConfigByType_IfHasConfigByPropertyPath() return Verify(actual, _verifySettings); } - [Test] - public void PrintToString_ThrowsException_IfObjectHasCyclicReference() - { - var obj = new LinkedClass(); - obj.Other = obj; - - var print = () => obj.PrintToString(); - - print.Should().Throw() - .WithMessage("Unable to print object with cyclic reference."); - } - [Test] public void Printing_ThrowsException_IfExpressionIsNotPropertyPath() { diff --git a/ObjectPrinting_Tests/PrintingConfigTestsResults/PrintingConfigTests.PrintToString_DoesNotPrintObject_WhenHasCyclicReferenceToInitialObject.verified.txt b/ObjectPrinting_Tests/PrintingConfigTestsResults/PrintingConfigTests.PrintToString_DoesNotPrintObject_WhenHasCyclicReferenceToInitialObject.verified.txt new file mode 100644 index 00000000..766a6f0a --- /dev/null +++ b/ObjectPrinting_Tests/PrintingConfigTestsResults/PrintingConfigTests.PrintToString_DoesNotPrintObject_WhenHasCyclicReferenceToInitialObject.verified.txt @@ -0,0 +1,5 @@ +LinkedClass + Number = 0 + Other = LinkedClass + Number = 0 + Other = [root] diff --git a/ObjectPrinting_Tests/PrintingConfigTestsResults/PrintingConfigTests.PrintToString_DoesNotPrintObject_WhenHasCyclicReferenceToNestedObject.verified.txt b/ObjectPrinting_Tests/PrintingConfigTestsResults/PrintingConfigTests.PrintToString_DoesNotPrintObject_WhenHasCyclicReferenceToNestedObject.verified.txt new file mode 100644 index 00000000..926c9098 --- /dev/null +++ b/ObjectPrinting_Tests/PrintingConfigTestsResults/PrintingConfigTests.PrintToString_DoesNotPrintObject_WhenHasCyclicReferenceToNestedObject.verified.txt @@ -0,0 +1,7 @@ +LinkedClass + Number = 0 + Other = LinkedClass + Number = 0 + Other = LinkedClass + Number = 0 + Other = [root.Other] From 432232c73a797ecc61362960fb707c879bce7a19 Mon Sep 17 00:00:00 2001 From: stupidnessplusplus <58785209+stupidnessplusplus@users.noreply.github.com> Date: Sun, 15 Dec 2024 20:25:59 +0500 Subject: [PATCH 13/13] Rewrite ObjectPrinter --- ObjectPrinting/IPrintingConfig.cs | 13 +- ObjectPrinting/ObjectPrinter.cs | 210 ++++++++++++++++++------------ ObjectPrinting/PropertyPath.cs | 31 ++++- 3 files changed, 166 insertions(+), 88 deletions(-) diff --git a/ObjectPrinting/IPrintingConfig.cs b/ObjectPrinting/IPrintingConfig.cs index 6efb7754..34c15fa8 100644 --- a/ObjectPrinting/IPrintingConfig.cs +++ b/ObjectPrinting/IPrintingConfig.cs @@ -1,4 +1,5 @@ -using System.Globalization; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; namespace ObjectPrinting; @@ -13,4 +14,14 @@ internal interface IPrintingConfig bool IsToLimitNestingLevel { get; } int MaxNestingLevel { get; } + + public bool TryGetConfig( + PropertyPath path, + [MaybeNullWhen(false)] out IPropertyPrintingConfig propertyPrintingConfig) + { + var obj = path.PropertyValue.Value; + return PropertyConfigsByPath.TryGetValue(path, out propertyPrintingConfig) + || obj != null + && PropertyConfigsByType.TryGetValue(obj.GetType(), out propertyPrintingConfig); + } } diff --git a/ObjectPrinting/ObjectPrinter.cs b/ObjectPrinting/ObjectPrinter.cs index 4d727017..2d06318c 100644 --- a/ObjectPrinting/ObjectPrinter.cs +++ b/ObjectPrinting/ObjectPrinter.cs @@ -1,6 +1,4 @@ using System.Collections; -using System.Diagnostics.CodeAnalysis; -using System.Globalization; using System.Reflection; using System.Text; @@ -23,127 +21,175 @@ public static PrintingConfig For() internal static string PrintToString(TOwner? obj, IPrintingConfig printingConfig) { - var sb = new StringBuilder(); - var stack = new Stack>(); - var path = default(PropertyPath); + var path = new PropertyPath(new PropertyValue(null, obj)); - if (TryGetStringValue(printingConfig, new PropertyValue(null, obj), stack, ref path, out var stringValue)) + if (!IsExcluded(path, printingConfig, 0)) { - sb.AppendLine(stringValue); + var sb = new StringBuilder(); + AppendPrintedObject(sb, path, printingConfig, 0); + return sb.ToString(); } - while (stack.TryPeek(out var enumerator)) - { - if (!enumerator.MoveNext()) - { - stack.Pop(); - path = path!.Previous; - continue; - } - - var propertyValue = enumerator.Current; - var indentation = stack.Count; - - if (path != null && path.Contains(propertyValue.Value)) - { - throw new InvalidOperationException("Unable to print object with cyclic reference."); - } - - if (TryGetStringValue(printingConfig, propertyValue, stack, ref path, out stringValue)) - { - sb.Append('\t', indentation); - sb.AppendLine($"{propertyValue.Name} = {stringValue}"); - } - } - - return sb.ToString(); + return string.Empty; } - private static bool TryGetStringValue( + private static void AppendPrintedObject( + StringBuilder sb, + PropertyPath path, IPrintingConfig printingConfig, - PropertyValue propertyValue, - Stack> propertyTreeStack, - ref PropertyPath? path, - [MaybeNullWhen(false)] out string stringValue) + int nestingLevel) { - var obj = propertyValue.Value; + var obj = path.PropertyValue.Value; if (obj == null) { - stringValue = "null"; - return true; + sb.AppendLine("null"); } - - var type = obj.GetType(); - var nextPath = new PropertyPath(propertyValue, path); - - var hasConfig = printingConfig.PropertyConfigsByPath.TryGetValue(nextPath, out var propertyPrintingConfig) - || printingConfig.PropertyConfigsByType.TryGetValue(type, out propertyPrintingConfig); - - if (hasConfig && propertyPrintingConfig!.IsExcluded) + else if (IsCyclicReference(path)) { - stringValue = default; - return false; + AppendCyclicReference(sb, path); } - - if (hasConfig && propertyPrintingConfig!.PrintOverride != null) + else if (IsAlternativelyPrintedObject(path, printingConfig)) { - stringValue = propertyPrintingConfig.PrintOverride(obj); + AppendAlternativelyPrintedObject(sb, path, printingConfig); } - else if (IsFinalType(type)) + else if (IsFinalTypeObject(obj)) { - var cultureInfo = hasConfig && propertyPrintingConfig!.CultureInfo != null - ? propertyPrintingConfig.CultureInfo - : printingConfig.CultureInfo; - stringValue = PrintFinalTypeToString(obj, cultureInfo); + AppendFinalTypeObject(sb, path, printingConfig); } - else if (obj is IEnumerable enumerable) + else if (obj is IEnumerable) { - var values = enumerable - .Cast() - .Select((item, i) => new PropertyValue($"{{{i}}}", null, item)); - propertyTreeStack.Push(values.GetEnumerator()); - path = nextPath; - stringValue = GetNameWithoutGenericPart(type); + AppendEnumerableObjectWithItems(sb, path, printingConfig, nestingLevel); } else { - if (!printingConfig.IsToLimitNestingLevel - || propertyTreeStack.Count < printingConfig.MaxNestingLevel) - { - propertyTreeStack.Push(GetPropertyValues(obj).GetEnumerator()); - path = nextPath; - } - - stringValue = GetNameWithoutGenericPart(type); + AppendObjectWithProperties(sb, path, printingConfig, nestingLevel); } + } - return true; + private static bool IsCyclicReference(PropertyPath path) + { + var obj = path.PropertyValue.Value!; + return path.Previous != null + && path.Previous.Contains(obj); + } + + private static void AppendCyclicReference(StringBuilder sb, PropertyPath path) + { + var obj = path.PropertyValue.Value!; + var pathToExistingValue = path.Previous!.FindPathTo(obj); + sb.AppendLine($"[root{pathToExistingValue}]"); + } + + private static bool IsAlternativelyPrintedObject(PropertyPath path, IPrintingConfig printingConfig) + { + return printingConfig.TryGetConfig(path, out var propertyPrintingConfig) + && propertyPrintingConfig.PrintOverride != null; } - private static bool IsFinalType(Type type) + private static void AppendAlternativelyPrintedObject( + StringBuilder sb, + PropertyPath path, + IPrintingConfig printingConfig) { + if (printingConfig.TryGetConfig(path, out var propertyPrintingConfig)) + { + var obj = path.PropertyValue.Value!; + var stringValue = propertyPrintingConfig.PrintOverride!(obj); + sb.AppendLine(stringValue); + } + } + + private static bool IsFinalTypeObject(object obj) + { + var type = obj.GetType(); return type.IsPrimitive || _finalTypes.Contains(type); } - private static string PrintFinalTypeToString(object obj, CultureInfo cultureInfo) + private static void AppendFinalTypeObject( + StringBuilder sb, + PropertyPath path, + IPrintingConfig printingConfig) { - return obj is IFormattable formattableObj + var cultureInfo = printingConfig.TryGetConfig(path, out var propertyPrintingConfig) + && propertyPrintingConfig.CultureInfo != null + ? propertyPrintingConfig.CultureInfo + : printingConfig.CultureInfo; + + var obj = path.PropertyValue.Value!; + var stringValue = obj is IFormattable formattableObj ? formattableObj.ToString(null, cultureInfo) - : obj.ToString()!; + : obj.ToString(); + + sb.AppendLine(stringValue); } - private static IEnumerable GetPropertyValues(object obj) + private static void AppendEnumerableObjectWithItems( + StringBuilder sb, + PropertyPath path, + IPrintingConfig printingConfig, + int nestingLevel) + { + var enumerable = (IEnumerable)path.PropertyValue.Value!; + var type = enumerable.GetType(); + var typeName = GetTypeNameWithoutGenericPart(type); + var values = enumerable + .Cast() + .Select((item, i) => new PropertyValue($"{{{i}}}", type, item)); + + sb.AppendLine(typeName); + AppendPropertyValues(sb, path, printingConfig, values, nestingLevel); + } + + private static void AppendObjectWithProperties( + StringBuilder sb, + PropertyPath path, + IPrintingConfig printingConfig, + int nestingLevel) { - return obj - .GetType() + var obj = path.PropertyValue.Value!; + var type = obj.GetType(); + var typeName = GetTypeNameWithoutGenericPart(type); + var values = type .GetProperties(BindingFlags.Public | BindingFlags.Instance) .Where(property => property.GetIndexParameters().Length == 0) .Select(property => new PropertyValue(property, property.GetValue(obj))); + + sb.AppendLine(typeName); + AppendPropertyValues(sb, path, printingConfig, values, nestingLevel); + } + + private static void AppendPropertyValues( + StringBuilder sb, + PropertyPath path, + IPrintingConfig printingConfig, + IEnumerable propertyValues, + int nestingLevel) + { + foreach (var propertyValue in propertyValues) + { + var nextPath = new PropertyPath(propertyValue, path); + + if (!IsExcluded(nextPath, printingConfig, nestingLevel + 1)) + { + sb.Append('\t', nestingLevel + 1); + sb.Append(propertyValue.Name); + sb.Append(" = "); + AppendPrintedObject(sb, nextPath, printingConfig, nestingLevel + 1); + } + } + } + + private static bool IsExcluded(PropertyPath path, IPrintingConfig printingConfig, int nestingLevel) + { + return printingConfig.IsToLimitNestingLevel + && nestingLevel > printingConfig.MaxNestingLevel + || printingConfig.TryGetConfig(path, out var propertyPrintingConfig) + && propertyPrintingConfig!.IsExcluded; } - private static string GetNameWithoutGenericPart(Type type) + private static string GetTypeNameWithoutGenericPart(Type type) { var end = type.Name.IndexOf('`'); return end == -1 diff --git a/ObjectPrinting/PropertyPath.cs b/ObjectPrinting/PropertyPath.cs index 77c644d8..e1ed9da1 100644 --- a/ObjectPrinting/PropertyPath.cs +++ b/ObjectPrinting/PropertyPath.cs @@ -26,10 +26,25 @@ public PropertyPath(PropertyValue propertyValue, PropertyPath? previous = null) _hashCode = HashCode.Combine(propertyValue.DeclaringType, propertyValue.Name, previous); } - public PropertyValue? PropertyValue { get; } + public PropertyValue PropertyValue { get; } public PropertyPath? Previous { get; } + public bool Contains([DisallowNull] object obj) + { + return _values.Contains(obj); + } + + public PropertyPath? FindPathTo(object obj) + { + if (ReferenceEquals(obj, PropertyValue.Value)) + { + return this; + } + + return Previous?.FindPathTo(obj); + } + public override bool Equals(object? obj) { return obj is PropertyPath propertyPath @@ -43,9 +58,16 @@ public override int GetHashCode() return _hashCode; } - public bool Contains(object? obj) + public override string ToString() { - return obj != null && _values.Contains(obj); + var stack = new Stack(); + + for (var node = this; node != null; node = node.Previous) + { + stack.Push(node.PropertyValue.Name); + } + + return string.Join('.', stack); } public static bool TryGetPropertyPath( @@ -54,8 +76,7 @@ public static bool TryGetPropertyPath( { var propertyNames = propertySelector.Body.ToString().Split('.'); var type = typeof(TOwner); - path = default; - + if (propertyNames.Length >= 1 && propertyNames[0] == propertySelector.Parameters.Single().Name) { path = new PropertyPath(new PropertyValue(null, null), null);