diff --git a/ObjectPrinting/Extensions/ObjectExtensions.cs b/ObjectPrinting/Extensions/ObjectExtensions.cs new file mode 100644 index 00000000..495565f7 --- /dev/null +++ b/ObjectPrinting/Extensions/ObjectExtensions.cs @@ -0,0 +1,17 @@ +using System; + +namespace ObjectPrinting.Extensions +{ + public static class ObjectExtensions + { + public static string PrintToString(this T obj) + { + return ObjectPrinter.For().PrintToString(obj); + } + + public static string PrintToString(this T obj, Func,PrintingConfig> func) + { + return func(ObjectPrinter.For()).PrintToString(obj); + } + } +} \ No newline at end of file diff --git a/ObjectPrinting/Extensions/PropertyPrintingConfigExtension.cs b/ObjectPrinting/Extensions/PropertyPrintingConfigExtension.cs new file mode 100644 index 00000000..6128db34 --- /dev/null +++ b/ObjectPrinting/Extensions/PropertyPrintingConfigExtension.cs @@ -0,0 +1,11 @@ +namespace ObjectPrinting.Extensions +{ + public static class PropertyPrintingConfigExtension + { + public static PrintingConfig TrimmedStringProperty( + this PropertyPrintingConfig printingConfig, int length) + { + return printingConfig.SetSerializer(str => str.Length <= length ? str : str.Substring(0, length)); + } + } +} \ No newline at end of file diff --git a/ObjectPrinting/ObjectPrinting.csproj b/ObjectPrinting/ObjectPrinting.csproj index 1c5eaf1c..4f9c587e 100644 --- a/ObjectPrinting/ObjectPrinting.csproj +++ b/ObjectPrinting/ObjectPrinting.csproj @@ -7,6 +7,7 @@ + diff --git a/ObjectPrinting/PrintingConfig.cs b/ObjectPrinting/PrintingConfig.cs index a9e08211..dbb737d6 100644 --- a/ObjectPrinting/PrintingConfig.cs +++ b/ObjectPrinting/PrintingConfig.cs @@ -1,11 +1,37 @@ 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 { public class PrintingConfig { + private static readonly HashSet FinalTypes = new HashSet() + { + typeof(int), + typeof(double), + typeof(float), + typeof(string), + typeof(DateTime), + typeof(TimeSpan), + typeof(Guid) + }; + + private readonly HashSet excludedTypes = new HashSet(); + private readonly HashSet excludedProperty = new HashSet(); + + private readonly Dictionary> customPropertySerialize = + new Dictionary>(); + + private readonly Dictionary> customTypeSerializers = new Dictionary>(); + private readonly HashSet visitedObjects = new HashSet(); + + public string PrintToString(TOwner obj) { return PrintToString(obj, 0); @@ -13,29 +39,156 @@ public string PrintToString(TOwner obj) private string PrintToString(object obj, int nestingLevel) { + var builder = new StringBuilder(); //TODO apply configurations if (obj == null) - return "null" + Environment.NewLine; + return "null"; + + if (visitedObjects.Contains(obj)) + { + return $"Cycling references"; + } + + //visitedObjects.Add(obj); + + if (FinalTypes.Contains(obj.GetType())) + return obj.ToString(); + + if (obj is IDictionary dictionary) + { + return PrintToStringIDictionary(dictionary, nestingLevel + 1); + } + + if (obj is IEnumerable collection) + { + return PrintToStringIEnumerable(collection, nestingLevel + 1); + } - var finalTypes = new[] + return PrintToStringProperties(obj, nestingLevel + 1); + } + + private string PrintToStringProperties(object obj, int nestingLevel) + { + var identation = new string('\t', nestingLevel); + var builder = new StringBuilder(); + var objType = obj.GetType(); + builder.Append($"{objType.Name}"); + visitedObjects.Add(obj); + foreach (var propertyInfo in GetIncludedProperties(objType)) { - typeof(int), typeof(double), typeof(float), typeof(string), - typeof(DateTime), typeof(TimeSpan) - }; - if (finalTypes.Contains(obj.GetType())) - return obj + Environment.NewLine; + var value = propertyInfo.GetValue(obj); + var customSerializeStr = TryUseCustomSerializer(value, propertyInfo, out var str) + ? str + : PrintToString(value, nestingLevel + 1); + builder.Append('\n' + identation + propertyInfo.Name + " = " + customSerializeStr); + } + visitedObjects.Remove(obj); + return builder.ToString(); + } - 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()) + private string PrintToStringIEnumerable(IEnumerable collection, int nestingLevel) + { + var identation = new string('\t', nestingLevel); + var builder = new StringBuilder(); + visitedObjects.Add(collection); + foreach (var item in collection) { - sb.Append(identation + propertyInfo.Name + " = " + - PrintToString(propertyInfo.GetValue(obj), - nestingLevel + 1)); + builder.Append('\n' + identation + PrintToString(item, nestingLevel + 1)); } - return sb.ToString(); + + visitedObjects.Remove(collection); + return builder.ToString(); + } + + private string PrintToStringIDictionary(IDictionary dictionary, int nestingLevel) + { + var identation = new string('\t', nestingLevel); + var builder = new StringBuilder(); + + visitedObjects.Add(dictionary); + + foreach (var key in dictionary.Keys) + { + builder.Append('\n' + identation + PrintToString(key, nestingLevel + 1) + " = " + + PrintToString(dictionary[key], nestingLevel + 1)); + } + + visitedObjects.Remove(dictionary); + return builder.ToString(); + } + + private bool TryUseCustomSerializer(object value, PropertyInfo propertyInfo, out string str) + { + str = null; + if (customPropertySerialize.ContainsKey(propertyInfo)) + { + str = (string)customPropertySerialize[propertyInfo].DynamicInvoke(value); + return true; + } + + var type = propertyInfo.PropertyType; + if (customTypeSerializers.ContainsKey(propertyInfo.PropertyType)) + { + str = (string)customTypeSerializers[propertyInfo.PropertyType].DynamicInvoke(value); + return true; + } + + return false; + } + + private IEnumerable GetIncludedProperties(Type type) + { + return type.GetRuntimeProperties().Where(p => !IsExclude(p)); + } + + private bool IsExclude(PropertyInfo member) + { + return excludedTypes.Contains(member.PropertyType) || excludedProperty.Contains(member); + } + + public PrintingConfig Exclude() + { + excludedTypes.Add(typeof(TType)); + return this; + } + + public PrintingConfig Exclude(Expression> func) + { + var memberInfo = func.Body as MemberExpression; + if (memberInfo == null) + throw new ArgumentException(); + excludedProperty.Add(memberInfo.Member); + return this; + } + + + public PrintingConfig SetCustomTypeSerializer(Func serializer) + { + customTypeSerializers[typeof(TType)] = obj =>serializer((TType)obj); + return this; + } + + public PropertyPrintingConfig SerializeByProperty( + Expression> func) + { + var memberInfo = func.Body as MemberExpression; + if (memberInfo == null) + throw new ArgumentException(); + return new PropertyPrintingConfig(this, memberInfo.Member); + } + + public PrintingConfig SetCustomPropertySerializer(MemberInfo property, + Func serializer) + { + customPropertySerialize[property] = obj =>serializer((TProperty)obj); + return this; + } + + public PrintingConfig SetCulture(string format, CultureInfo culture) + where TType : IFormattable + + { + return SetCustomTypeSerializer((TType obj) => obj.ToString(format, culture)); } } } \ No newline at end of file diff --git a/ObjectPrinting/PropertyPrintingConfig.cs b/ObjectPrinting/PropertyPrintingConfig.cs new file mode 100644 index 00000000..84fca04e --- /dev/null +++ b/ObjectPrinting/PropertyPrintingConfig.cs @@ -0,0 +1,23 @@ +using System; +using System.Reflection; + +namespace ObjectPrinting +{ + public class PropertyPrintingConfig + { + private readonly PrintingConfig printingConfig; + private readonly MemberInfo property; + + public PropertyPrintingConfig(PrintingConfig printingConfig, MemberInfo property) + { + this.printingConfig = printingConfig; + this.property = property; + } + + public PrintingConfig SetSerializer(Func serializer) + { + printingConfig.SetCustomPropertySerializer(property, serializer); + return printingConfig; + } + } +} \ No newline at end of file diff --git a/ObjectPrinting/Tests/ObjectPrinterAcceptanceTests.cs b/ObjectPrinting/Tests/ObjectPrinterAcceptanceTests.cs index 4c8b2445..016e1a99 100644 --- a/ObjectPrinting/Tests/ObjectPrinterAcceptanceTests.cs +++ b/ObjectPrinting/Tests/ObjectPrinterAcceptanceTests.cs @@ -1,27 +1,168 @@ -using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Threading.Tasks; +using FluentAssertions; +using NUnit.Framework; +using ObjectPrinting.Extensions; namespace ObjectPrinting.Tests { [TestFixture] public class ObjectPrinterAcceptanceTests { + private Person simplePerson; + private Person personWithCyclingReference; + + [OneTimeSetUp] + public void OneTimeSetUp() + { + simplePerson = new Person + { + Id = new Guid("0f8fad5b-d9cb-469f-a165-70867728950e"), Name = "simplePerson", Age = 45, Height = 180, + }; + personWithCyclingReference = new Person + { Id = new Guid("b1754c14-d296-4b0f-a09a-030017f4461f"), Name = "Cycle", Age = 34, Height = 192 }; + personWithCyclingReference.Parents = new Person[] { personWithCyclingReference }; + } + + [Test] + public void ObjectPrinter_Should_Serialize_All_Properties_When_Setting_Are_Default() + { + var printer = ObjectPrinter.For(); + var actual = printer.PrintToString(simplePerson); + actual.Should() + .Be( + $"Person\n\tId = 0f8fad5b-d9cb-469f-a165-70867728950e\n\tName = simplePerson\n\tHeight = 180\n\tAge = 45\n\tParents = null"); + } + [Test] - public void Demo() + public void ObjectPrinter_Should_Excluding_PropertyType() { - var person = new Person { Name = "Alex", Age = 19 }; + var printer = ObjectPrinter.For(); + var actual = printer.Exclude().PrintToString(simplePerson); + actual.Should() + .Be( + "Person\n\tId = 0f8fad5b-d9cb-469f-a165-70867728950e\n\tName = simplePerson\n\tHeight = 180\n\tParents = null"); + } + [Test] + public void ObjectPrinter_Should_Excluding_Property() + { var printer = ObjectPrinter.For(); - //1. Исключить из сериализации свойства определенного типа - //2. Указать альтернативный способ сериализации для определенного типа - //3. Для числовых типов указать культуру - //4. Настроить сериализацию конкретного свойства - //5. Настроить обрезание строковых свойств (метод должен быть виден только для строковых свойств) - //6. Исключить из сериализации конкретного свойства - - string s1 = printer.PrintToString(person); - - //7. Синтаксический сахар в виде метода расширения, сериализующего по-умолчанию - //8. ...с конфигурированием + var actual = printer.Exclude(person => person.Name).PrintToString(simplePerson); + actual.Should() + .Be( + "Person\n\tId = 0f8fad5b-d9cb-469f-a165-70867728950e\n\tHeight = 180\n\tAge = 45\n\tParents = null"); + } + + [Test] + public void ObjectPrinter_Should_Set_Custom_Serialize_For_Type() + { + var printer = ObjectPrinter.For(); + var actual = printer.SetCustomTypeSerializer(i => 0.ToString()).PrintToString(simplePerson); + actual.Should() + .Be( + $"Person\n\tId = 0f8fad5b-d9cb-469f-a165-70867728950e\n\tName = simplePerson\n\tHeight = 180\n\tAge = 0\n\tParents = null"); + } + + [Test] + public void ObjectPrinter_Should_Set_Custom_Serializer_For_Property() + { + var printer = ObjectPrinter.For(); + var actual = printer.SerializeByProperty(person => person.Height).SetSerializer(height => height * 10 + "") + .PrintToString(simplePerson); + actual.Should() + .Be( + "Person\n\tId = 0f8fad5b-d9cb-469f-a165-70867728950e\n\tName = simplePerson\n\tHeight = 1800\n\tAge = 45\n\tParents = null"); + } + + [Test] + public void ObjectPrinter_Should_Trimm_String_Properties() + { + var printer = ObjectPrinter.For(); + var actual = printer.SerializeByProperty(person => person.Name).TrimmedStringProperty(2) + .PrintToString(simplePerson); + actual.Should() + .Be( + "Person\n\tId = 0f8fad5b-d9cb-469f-a165-70867728950e\n\tName = si\n\tHeight = 180\n\tAge = 45\n\tParents = null"); + } + + [Test] + public void ObjectPrinter_Should_Set_Culture_For_IFormatter_Types() + { + var d = 123456789.12345; + var printer = ObjectPrinter.For(); + var actual = printer.SetCulture("", CultureInfo.InvariantCulture).PrintToString(d); + actual.Should().Be("123456789,12345"); + } + + [Test] + public void ObjectPrinter_Should_Serialize_IEnumerable_Types() + { + var list = new List() { "hello", "world", "!" }; + var printer = ObjectPrinter.For>(); + var actual = printer.PrintToString(list); + actual.Should().Be("\n\thello\n\tworld\n\t!"); + } + + [Test] + public void ObjectPrinter_Should_Serialize_IDictionary_Types() + { + var dict = new Dictionary(); + dict[1] = "hello"; + dict[2] = "world"; + dict[3] = "!"; + + var printer = ObjectPrinter.For>(); + var actual = printer.PrintToString(dict); + actual.Should().Be("\n\t1 = hello\n\t2 = world\n\t3 = !"); + } + + [Test] + public void ObjectPrinter_Should_Ignore_Cycling_Reference() + { + var printer = ObjectPrinter.For(); + var actual = printer.PrintToString(personWithCyclingReference); + actual.Should() + .Be( + "Person\n\tId = b1754c14-d296-4b0f-a09a-030017f4461f\n\tName = Cycle\n\tHeight = 192\n\tAge = 34\n\tParents = \n\t\t\tCycling references"); + } + + + [Test] + public void PrintToString_Extension_Can_Call_From_Object() + { + var actual = simplePerson.PrintToString(); + actual.Should() + .Be( + $"Person\n\tId = 0f8fad5b-d9cb-469f-a165-70867728950e\n\tName = simplePerson\n\tHeight = 180\n\tAge = 45\n\tParents = null"); + } + + [Test] + public void PrintToString_Extension_Can_Call_From_Object_With_Config() + { + var actual = simplePerson.PrintToString(person => person.Exclude(p => p.Name)); + actual.Should() + .Be( + $"Person\n\tId = 0f8fad5b-d9cb-469f-a165-70867728950e\n\tHeight = 180\n\tAge = 45\n\tParents = null"); + } + + [Test] + public void ObjectPrinter_Parallel_Using_With_Same_Object() + { + var person = new Person() + { Id = new Guid("b1754c14-d296-4b0f-a09a-030027f4461f"), Name = "Parallel", Age = 94, Height = 200 }; + person.Parents = new[] + { personWithCyclingReference, personWithCyclingReference, simplePerson, simplePerson }; + var actual1 = ""; + var actual2 = ""; + Task task1 = Task.Run(() => { actual1 = person.PrintToString(); }); + Task task2 = Task.Run(() => { actual2 = person.PrintToString(); }); + + Task.WaitAll(task1, task2); + + actual1.Should().Be(actual2); } } } \ No newline at end of file diff --git a/ObjectPrinting/Tests/Person.cs b/ObjectPrinting/Tests/Person.cs index f9555955..fb14f232 100644 --- a/ObjectPrinting/Tests/Person.cs +++ b/ObjectPrinting/Tests/Person.cs @@ -8,5 +8,6 @@ public class Person public string Name { get; set; } public double Height { get; set; } public int Age { get; set; } + public Person[] Parents { get; set; } } } \ No newline at end of file