diff --git a/Homework/Homework.csproj b/Homework/Homework.csproj new file mode 100644 index 00000000..bc4bb846 --- /dev/null +++ b/Homework/Homework.csproj @@ -0,0 +1,11 @@ + + + + net8.0 + enable + enable + + Library + + + diff --git a/Homework/IPrintingConfig.cs b/Homework/IPrintingConfig.cs new file mode 100644 index 00000000..0a6009b4 --- /dev/null +++ b/Homework/IPrintingConfig.cs @@ -0,0 +1,14 @@ +using System.Globalization; +using System.Reflection; + +namespace Homework; + +public interface IPrintingConfig +{ + HashSet ExcludeTypes { get; } + HashSet ExcludeMembers { get; } + Dictionary> AlternativeTypesSerialization { get; } + Dictionary> AlternativeMembersSerialization { get; } + Dictionary TypesCultureInfo { get; } + Dictionary StringsTrim { get; } +} diff --git a/Homework/IPropertyPrintingConfig.cs b/Homework/IPropertyPrintingConfig.cs new file mode 100644 index 00000000..6d1c8fd4 --- /dev/null +++ b/Homework/IPropertyPrintingConfig.cs @@ -0,0 +1,9 @@ +using System.Reflection; + +namespace Homework; + +public interface IPropertyPrintingConfig +{ + PrintingConfig ParentConfig { get; } + MemberInfo? MemberInfo { get; } +} diff --git a/Homework/LastMemberVisitor.cs b/Homework/LastMemberVisitor.cs new file mode 100644 index 00000000..0daaada6 --- /dev/null +++ b/Homework/LastMemberVisitor.cs @@ -0,0 +1,15 @@ +using System.Linq.Expressions; + +namespace Homework; + +public class LastMemberVisitor : ExpressionVisitor +{ + public MemberExpression? LastMemberExpression { get; private set; } + + protected override Expression VisitMember(MemberExpression node) + { + LastMemberExpression = node; + + return base.VisitMember(node); + } +} diff --git a/Homework/ObjectExtensions.cs b/Homework/ObjectExtensions.cs new file mode 100644 index 00000000..020dfe39 --- /dev/null +++ b/Homework/ObjectExtensions.cs @@ -0,0 +1,9 @@ +namespace Homework; + +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/Homework/ObjectPrinter.cs b/Homework/ObjectPrinter.cs new file mode 100644 index 00000000..58e9519e --- /dev/null +++ b/Homework/ObjectPrinter.cs @@ -0,0 +1,9 @@ +namespace Homework; + +public class ObjectPrinter +{ + public static PrintingConfig For() + { + return new PrintingConfig(); + } +} \ No newline at end of file diff --git a/Homework/ObjectReferenceEqualityComparer.cs b/Homework/ObjectReferenceEqualityComparer.cs new file mode 100644 index 00000000..32726cd5 --- /dev/null +++ b/Homework/ObjectReferenceEqualityComparer.cs @@ -0,0 +1,16 @@ +using System.Diagnostics.CodeAnalysis; + +namespace Homework; + +public class ObjectReferenceEqualityComparer : IEqualityComparer +{ + public new bool Equals(object? x, object? y) + { + return ReferenceEquals(x, y); + } + + public int GetHashCode([DisallowNull] object obj) + { + return obj.GetHashCode(); + } +} diff --git a/Homework/PrintingConfig.cs b/Homework/PrintingConfig.cs new file mode 100644 index 00000000..ee234288 --- /dev/null +++ b/Homework/PrintingConfig.cs @@ -0,0 +1,172 @@ +using System.Collections; +using System.Globalization; +using System.Linq.Expressions; +using System.Reflection; +using System.Text; + +namespace Homework; + +public class PrintingConfig : IPrintingConfig +{ + public PropertyPrintingConfig Printing() + { + return new PropertyPrintingConfig(this, null); + } + + public PropertyPrintingConfig Printing(Expression> memberSelector) + { + var lambda = memberSelector as LambdaExpression; + var visitor = new LastMemberVisitor(); + visitor.Visit(lambda.Body); + var memberInfo = visitor.LastMemberExpression?.Member ?? throw new ArgumentException(); + + return new PropertyPrintingConfig(this, memberInfo); + } + + public PrintingConfig Excluding(Expression> memberSelector) + { + var lambda = memberSelector as LambdaExpression; + var visitor = new LastMemberVisitor(); + visitor.Visit(lambda.Body); + var memberInfo = visitor.LastMemberExpression?.Member ?? throw new ArgumentException(); + + ((IPrintingConfig)this).ExcludeMembers.Add(memberInfo); + + return this; + } + + public PrintingConfig Excluding() + { + ((IPrintingConfig)this).ExcludeTypes.Add(typeof(TPropType)); + + return this; + } + + public string PrintToString(TOwner obj) + { + return PrintToString(obj, null, 0, new(new ObjectReferenceEqualityComparer())); + } + + private string PrintToString(object? obj, MemberInfo? memberInfo, int nestingLevel, Dictionary printedObjects) + { + if (obj == null) + return "null" + Environment.NewLine; + + var type = obj.GetType(); + if (printedObjects.TryGetValue(obj, out var level)) + return $"(cycle with object {type.Name} at level {level}){Environment.NewLine}"; + + if (DoesTypeOverrideToString(type)) + return Serialize(obj, memberInfo) + Environment.NewLine; + + if (type.IsClass) + printedObjects.Add(obj, nestingLevel); + + var result = obj is ICollection collection + ? PrintCollectionToString(collection, nestingLevel, printedObjects) + : PrintComplexObjectToString(obj, nestingLevel, printedObjects); + + if (type.IsClass) + printedObjects.Remove(obj); + + return result; + } + + private string PrintCollectionToString(ICollection collection, int nestingLevel, Dictionary printedObjects) + { + var identation = new string('\t', nestingLevel); + var sb = new StringBuilder(); + sb.Append((nestingLevel > 0 ? Environment.NewLine + identation : "") + '[' + Environment.NewLine); + foreach (var item in collection) + sb.Append(identation + '\t' + + PrintToString( + item, + null, + nestingLevel + 1, + printedObjects)); + + sb.Append(identation + ']' + Environment.NewLine); + + return sb.ToString(); + } + + private string PrintComplexObjectToString(object obj, int nestingLevel, Dictionary printedObjects) + { + var type = obj.GetType(); + var identation = new string('\t', nestingLevel + 1); + var sb = new StringBuilder(); + sb.AppendLine(type.Name); + foreach (var nestedMemberInfo in + GetPublicPropertiesAndFields(type).OrderBy(t => t.Name)) + { + if (((IPrintingConfig)this).ExcludeMembers.Contains(nestedMemberInfo)) + continue; + + var newObj = GetValueFromPropertyOrField(obj, nestedMemberInfo); + + if (newObj is not null + && ((IPrintingConfig)this).ExcludeTypes.Contains(newObj.GetType())) + continue; + + sb.Append(identation + nestedMemberInfo.Name + " = " + + PrintToString( + newObj, + nestedMemberInfo, + nestingLevel + 1, + printedObjects)); + } + + return sb.ToString(); + } + + private IEnumerable GetPublicPropertiesAndFields(Type type) + => type + .GetProperties() + .Select(t => (MemberInfo)t) + .Concat(type.GetFields()); + + private object? GetValueFromPropertyOrField(object obj, MemberInfo memberInfo) + { + if (memberInfo is FieldInfo fieldInfo) + return fieldInfo.GetValue(obj); + + return ((PropertyInfo)memberInfo).GetValue(obj); + } + + private string Serialize(object obj, MemberInfo? memberInfo) + { + if (memberInfo is not null + && ((IPrintingConfig)this).AlternativeMembersSerialization.TryGetValue(memberInfo, out var serializeMember)) + return serializeMember(obj); + + if (((IPrintingConfig)this).AlternativeTypesSerialization.TryGetValue(obj.GetType(), out var serializeType)) + return serializeType(obj); + + string? str; + if (((IPrintingConfig)this).TypesCultureInfo.TryGetValue(obj.GetType(), out var culture)) + str = (string)obj.GetType().GetMethod("ToString", [typeof(IFormatProvider)]).Invoke(obj, [culture]); + else + str = obj.ToString(); + + if (memberInfo is not null + && ((IPrintingConfig)this).StringsTrim.TryGetValue(memberInfo, out var maxLength) + && str?.Length > maxLength) + str = str[..maxLength]; + + return str ?? "null"; + } + + private bool DoesTypeOverrideToString(Type type) + { + var toStringMethod = type.GetMethod("ToString", Type.EmptyTypes); + return toStringMethod?.DeclaringType != typeof(object) && + toStringMethod?.DeclaringType == type; + } + + HashSet IPrintingConfig.ExcludeTypes { get; } = []; + HashSet IPrintingConfig.ExcludeMembers { get; } = []; + Dictionary> IPrintingConfig.AlternativeTypesSerialization { get; } = []; + Dictionary> IPrintingConfig.AlternativeMembersSerialization { get; } = []; + Dictionary IPrintingConfig.TypesCultureInfo { get; } = []; + Dictionary IPrintingConfig.StringsTrim { get; } = []; +} diff --git a/Homework/PropertyPrintingConfig.cs b/Homework/PropertyPrintingConfig.cs new file mode 100644 index 00000000..ee3965bf --- /dev/null +++ b/Homework/PropertyPrintingConfig.cs @@ -0,0 +1,24 @@ +using System.Reflection; + +namespace Homework; + +public class PropertyPrintingConfig(PrintingConfig printingConfig, MemberInfo? memberInfo) + : IPropertyPrintingConfig +{ + public PrintingConfig Using(Func print) + { + if (memberInfo is not null) + { + ((IPrintingConfig)printingConfig).AlternativeMembersSerialization[memberInfo] = obj => print((TPropType)obj); + } + else + { + ((IPrintingConfig)printingConfig).AlternativeTypesSerialization[typeof(TPropType)] = obj => print((TPropType)obj); + } + + return printingConfig; + } + + PrintingConfig IPropertyPrintingConfig.ParentConfig => printingConfig; + MemberInfo? IPropertyPrintingConfig.MemberInfo => memberInfo; +} diff --git a/Homework/PropertyPrintingConfigExtensions.cs b/Homework/PropertyPrintingConfigExtensions.cs new file mode 100644 index 00000000..bff6ac84 --- /dev/null +++ b/Homework/PropertyPrintingConfigExtensions.cs @@ -0,0 +1,31 @@ +using System.Globalization; + +namespace Homework; + +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) + { + var configIface = (IPropertyPrintingConfig)propConfig; + if (configIface.MemberInfo is not null) + ((IPrintingConfig)configIface.ParentConfig).StringsTrim[configIface.MemberInfo] = maxLen; + + return configIface.ParentConfig; + } + + public static PrintingConfig Using( + this PropertyPrintingConfig propConfig, + CultureInfo culture) + where TPropType : IFormattable + { + var configIface = (IPropertyPrintingConfig)propConfig; + ((IPrintingConfig)configIface.ParentConfig).TypesCultureInfo[typeof(TPropType)] = culture; + + return configIface.ParentConfig; + } +} \ No newline at end of file diff --git a/HomeworkTests/HomeworkTests.csproj b/HomeworkTests/HomeworkTests.csproj new file mode 100644 index 00000000..fdfda772 --- /dev/null +++ b/HomeworkTests/HomeworkTests.csproj @@ -0,0 +1,29 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + + + + diff --git a/HomeworkTests/ObjectPrinterTests.cs b/HomeworkTests/ObjectPrinterTests.cs new file mode 100644 index 00000000..99734bf2 --- /dev/null +++ b/HomeworkTests/ObjectPrinterTests.cs @@ -0,0 +1,261 @@ +using FluentAssertions; +using Homework; +using System.Globalization; + +namespace HomeworkTests; + +[TestFixture] +public class ObjectPrinterTests +{ + [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. Настроить сериализацию конкретного свойства + .Printing(p => p.Weight).Using(t => $"{t} kg") + //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); + } + + [Test] + public void Should_Serialize() + { + var person = CreatePerson(); + var printer = ObjectPrinter.For(); + + var actual = printer.PrintToString(person); + var expected = + $""" + Person + {"\t"}Age = {person.Age} + {"\t"}Height = {person.Height} + {"\t"}Id = {person.Id} + {"\t"}Name = {person.Name} + {"\t"}Parent = {person.Parent?.ToString() ?? "null"} + {"\t"}Weight = {person.Weight} + + """; + + actual.Should().Be(expected); + } + + [Test] + public void Should_ExcludePropertyOrFieldWithParticularType() + { + var person = CreatePerson(); + var printer = ObjectPrinter.For() + .Excluding(); + + var actual = printer.PrintToString(person); + var expected = + $""" + Person + {"\t"}Height = {person.Height} + {"\t"}Id = {person.Id} + {"\t"}Name = {person.Name} + {"\t"}Parent = {person.Parent?.ToString() ?? "null"} + + """; + + actual.Should().Be(expected); + } + + [Test] + public void Should_UsingAlternativeSerializationForParticularType() + { + var person = CreatePerson(); + var printer = ObjectPrinter.For() + .Printing().Using(t => $"{t} y.o."); + + var actual = printer.PrintToString(person); + var expected = + $""" + Person + {"\t"}Age = {person.Age} y.o. + {"\t"}Height = {person.Height} + {"\t"}Id = {person.Id} + {"\t"}Name = {person.Name} + {"\t"}Parent = {person.Parent?.ToString() ?? "null"} + {"\t"}Weight = {person.Weight} y.o. + + """; + + actual.Should().Be(expected); + } + + [Test] + public void Should_UsingCultureInfo() + { + var person = CreatePerson(); + var culture = CultureInfo.GetCultureInfo("ru-RU"); + var printer = ObjectPrinter.For() + .Printing().Using(culture); + + var actual = printer.PrintToString(person); + var expected = + $""" + Person + {"\t"}Age = {person.Age} + {"\t"}Height = {person.Height.ToString(culture)} + {"\t"}Id = {person.Id} + {"\t"}Name = {person.Name} + {"\t"}Parent = {person.Parent?.ToString() ?? "null"} + {"\t"}Weight = {person.Weight} + + """; + + actual.Should().Be(expected); + } + + [Test] + public void Should_ConfigurePropertyOrField() + { + var person = CreatePerson(); + var printer = ObjectPrinter.For() + .Printing(t => t.Age).Using(t => $"{t} y.o.") + .Printing(t => t.Weight).Using(t => $"{t} kg"); + + var actual = printer.PrintToString(person); + var expected = + $""" + Person + {"\t"}Age = {person.Age} y.o. + {"\t"}Height = {person.Height} + {"\t"}Id = {person.Id} + {"\t"}Name = {person.Name} + {"\t"}Parent = {person.Parent?.ToString() ?? "null"} + {"\t"}Weight = {person.Weight} kg + + """; + + actual.Should().Be(expected); + } + + [Test] + public void Should_TrimString() + { + var person = CreatePerson(); + var printer = ObjectPrinter.For() + .Printing(t => t.Name).TrimmedToLength(3); + + var actual = printer.PrintToString(person); + var expected = + $""" + Person + {"\t"}Age = {person.Age} + {"\t"}Height = {person.Height} + {"\t"}Id = {person.Id} + {"\t"}Name = {person.Name[..3]} + {"\t"}Parent = {person.Parent?.ToString() ?? "null"} + {"\t"}Weight = {person.Weight} + + """; + + actual.Should().Be(expected); + } + + [Test] + public void Should_ExcludeParticularPropertyOrField() + { + var person = CreatePerson(); + var printer = ObjectPrinter.For() + .Excluding(t => t.Parent); + + var actual = printer.PrintToString(person); + var expected = + $""" + Person + {"\t"}Age = {person.Age} + {"\t"}Height = {person.Height} + {"\t"}Id = {person.Id} + {"\t"}Name = {person.Name} + {"\t"}Weight = {person.Weight} + + """; + + actual.Should().Be(expected); + } + + [Test] + public void Should_HandlingCircularReferences() + { + var person = CreatePerson(); + person.Parent = person; + var printer = ObjectPrinter.For(); + + var actual = printer.PrintToString(person); + var expected = + $""" + Person + {"\t"}Age = {person.Age} + {"\t"}Height = {person.Height} + {"\t"}Id = {person.Id} + {"\t"}Name = {person.Name} + {"\t"}Parent = (cycle with object Person at level 0) + {"\t"}Weight = {person.Weight} + + """; + + actual.Should().Be(expected); + } + + [Test] + public void Should_SerializeCollection() + { + var dictionary = new Dictionary + { + [1] = 2, + [3] = 4, + [5] = 6 + }; + var printer = ObjectPrinter.For>(); + + var actual = printer.PrintToString(dictionary); + var expected = + $""" + [ + {"\t"}[1, 2] + {"\t"}[3, 4] + {"\t"}[5, 6] + ] + + """; + + actual.Should().Be(expected); + } + + private Person CreatePerson() + { + var person = new Person + { + Id = Guid.NewGuid(), + Age = 19, + Height = 172.5, + Name = "Name", + Weight = 75 + }; + + return person; + } +} \ No newline at end of file diff --git a/HomeworkTests/Person.cs b/HomeworkTests/Person.cs new file mode 100644 index 00000000..84d0a95d --- /dev/null +++ b/HomeworkTests/Person.cs @@ -0,0 +1,12 @@ +namespace HomeworkTests; + +public class Person +{ + public Guid Id { get; set; } + public string? Name { get; set; } + public double Height { get; set; } + public int Age { get; set; } + public Person? Parent { get; set; } + + public int Weight; +} \ No newline at end of file diff --git a/fluent-api.sln b/fluent-api.sln index 69c8db9e..ff2fd98a 100644 --- a/fluent-api.sln +++ b/fluent-api.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 14 -VisualStudioVersion = 14.0.25123.0 +# Visual Studio Version 17 +VisualStudioVersion = 17.14.36705.20 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ObjectPrinting", "ObjectPrinting\ObjectPrinting.csproj", "{07B8C9B7-8289-46CB-9875-048A57758EEE}" EndProject @@ -13,6 +13,10 @@ 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}") = "Homework", "Homework\Homework.csproj", "{D3D88E59-B917-49B7-A7B6-78A16FCAD329}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HomeworkTests", "HomeworkTests\HomeworkTests.csproj", "{6EFE1C93-15A6-4405-A832-D61153373FBF}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -35,6 +39,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 + {D3D88E59-B917-49B7-A7B6-78A16FCAD329}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D3D88E59-B917-49B7-A7B6-78A16FCAD329}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D3D88E59-B917-49B7-A7B6-78A16FCAD329}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D3D88E59-B917-49B7-A7B6-78A16FCAD329}.Release|Any CPU.Build.0 = Release|Any CPU + {6EFE1C93-15A6-4405-A832-D61153373FBF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6EFE1C93-15A6-4405-A832-D61153373FBF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6EFE1C93-15A6-4405-A832-D61153373FBF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6EFE1C93-15A6-4405-A832-D61153373FBF}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -44,4 +56,7 @@ Global {8A7BB3EA-3E6A-4D04-A801-D5CD1620DA0D} = {6D308E4A-CEC7-4536-9B87-81CD337A87AD} {EFA9335C-411B-4597-B0B6-5438D1AE04C3} = {6D308E4A-CEC7-4536-9B87-81CD337A87AD} EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {EA57358B-B582-4077-B608-1C1F166CC04A} + EndGlobalSection EndGlobal