Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Шевырин Никита #225

Open
wants to merge 21 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -236,3 +236,6 @@ _Pvt_Extensions
.fake/

.idea/

# Verify.NUnit
*.received.*
8 changes: 8 additions & 0 deletions ObjectPrinting/IPropertySerializer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using System;

namespace ObjectPrinting;

public interface IPropertySerializer<TOwner, TProperty>
{
public PrintingConfig<TOwner> Use(Func<TProperty, string> converter);
}
8 changes: 8 additions & 0 deletions ObjectPrinting/ITypeSerializer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using System;

namespace ObjectPrinting;

public interface ITypeSerializer<TParam, TOwner>
{
public PrintingConfig<TOwner> Use(Func<TParam, string> converter);
}
17 changes: 17 additions & 0 deletions ObjectPrinting/ObjectExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using System;

namespace ObjectPrinting;

public static class ObjectExtensions
{
public static string PrintToString<T>(this T obj)
{
return ObjectPrinter.For<T>().PrintToString(obj);
}

public static string PrintToString<T>(this T obj, Func<PrintingConfig<T>, PrintingConfig<T>> configurer)
{
var config = configurer(ObjectPrinter.For<T>());
return config.PrintToString(obj);
}
}
1 change: 1 addition & 0 deletions ObjectPrinting/ObjectPrinting.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="NUnit" Version="4.2.2" />
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
<PackageReference Include="Verify.NUnit" Version="28.3.2" />
</ItemGroup>
</Project>
152 changes: 145 additions & 7 deletions ObjectPrinting/PrintingConfig.cs
Original file line number Diff line number Diff line change
@@ -1,19 +1,92 @@
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<TOwner>
{
private readonly List<Type> _excludedTypes = new();
private readonly List<MemberInfo> _excludedProperties = new();
private readonly Dictionary<Type, Delegate> _typeConverters = new();
private readonly Dictionary<MemberInfo, Delegate> _propertyConverters = new();
internal CultureInfo DoubleCultureInfo { get; set; } = CultureInfo.CurrentCulture;
internal CultureInfo FloatCultureInfo { get; set; } = CultureInfo.CurrentCulture;
internal CultureInfo DateTimeCultureInfo { get; set; } = CultureInfo.CurrentCulture;
internal int MaxStringLength { get; set; } = int.MaxValue;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Сейчас ты ограничиваешь длину сразу для всех типов, но что, если мы захотим ограничить длину у одного конкретного поля? Сейчас твоя реализация не позволит такого сделать

Так же мне не кажется, что задание значения по умолчанию является удачным выбором. Число действительно большое, но это ограничение, которого без указания пользователем по идее быть не должно, в теории на вход может прийти строка, длина которой превосходит int.MaxValue, и тогда мы без предупреждения обрежем строку. Если пользователь явно не указал, то обрезать строку не надо

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Вообще, если сослаться на исходник класса String в .NET, то там можно увидеть строку internal const int MaxLength = 0x3FFFFFDF;, в которой указано максимально возможная длина строки, и это значение сильно меньше чем int.MaxValue. Также если попытаться создать строку длины, большей, чем MaxLength, например так: var str = new string('a', 0x3FFFFFDF + 1);. То код падает с System.OutOfMemoryException

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Да, ты прав, у строки действительно не может быть длины, превышающей это значение. Но мне все равно не кажется хорошей идея задавать потолок в поле, без явной на то необходимости. Как мне кажется, лучше было бы сделать поле нулабельным, и если значение на ограничение не устанавлено, то не обрезать строку вовсе

internal int MaxRecursionDepth { get; set; } = 16;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Если глубина рекурсии справедлива для всего PrintingConfig, то давай ее передавать в конструкторе, а не через метод

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Поле можно сделать приватным

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

А не будет ли передача глубины рекурсии через конструктор противоречить идее Fluent api? У меня PrintingConfig вообще добывается через ObjectPrinter.For<T>()

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Если в ObjectPrinter.For добавить необязательный параметр, который потом передавать в конструктор PrintingConfig'а, то противоречий не будет


public string PrintToString(TOwner obj)
{
return PrintToString(obj, 0);
}

public PrintingConfig<TOwner> WithMaxRecursionDepth(int maxRecursionDepth)
{
if (maxRecursionDepth < 0)
throw new ArgumentOutOfRangeException($"{nameof(maxRecursionDepth)} must not be less than 0");
MaxRecursionDepth = maxRecursionDepth;
return this;
}

internal void AddTypeConverter<TParam>(Type type, Func<TParam, string?> converter)
{
_typeConverters.Add(type, converter);
}

internal void AddPropertyConverter<TParam>(Func<TParam, string> converter, MemberInfo propertyInfo)
{
_propertyConverters.Add(propertyInfo, converter);
}

public PrintingConfig<TOwner> ExceptType<T>()
{
_excludedTypes.Add(typeof(T));
return this;
}

public PrintingConfig<TOwner> ExceptProperty(Expression<Func<TOwner, object>> propertyExpression)
{
if (propertyExpression == null)
throw new ArgumentNullException($"{nameof(propertyExpression)} cannot be null");

_excludedProperties.Add(GetMemberInfo(propertyExpression));
return this;
}

public ITypeSerializer<TParam, TOwner> ForType<TParam>()
{
return new TypeSerializerImpl<TParam, TOwner>(this);
}

public IPropertySerializer<TOwner, TProperty> ForProperty<TProperty>(
Expression<Func<TOwner, TProperty>> propertyExpression)
{
if (propertyExpression == null)
throw new ArgumentNullException($"{nameof(propertyExpression)} cannot be null");

return new PropertySerializerImpl<TOwner, TProperty>(this, GetMemberInfo(propertyExpression));
}

private static MemberInfo GetMemberInfo<TProperty>(Expression<Func<TOwner, TProperty>> propertyExpression)
{
if (propertyExpression.Body is MemberExpression memberExpression)
return memberExpression.Member;

if (propertyExpression.Body is UnaryExpression unaryExpression
&& unaryExpression.Operand is MemberExpression unaryMemberExpression)
return unaryMemberExpression.Member;

throw new ArgumentException("Expression does not refer to a property or field.");
}

private string PrintToString(object obj, int nestingLevel)
{
//TODO apply configurations
if (obj == null)
return "null" + Environment.NewLine;

Expand All @@ -23,19 +96,84 @@ private string PrintToString(object obj, int nestingLevel)
typeof(DateTime), typeof(TimeSpan)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Насколько эффективно каждый раз создавать эту коллекцию при условии, что она всегда одна и та же?

};
if (finalTypes.Contains(obj.GetType()))

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

А если я захочу сериализовать, допустим, char или байт? Или любой другой примитив? Возможно для такого есть подходящий метод, покрывающий недостающие сценарии

return obj + Environment.NewLine;
return SerializeFinalType(obj);

if (nestingLevel > MaxRecursionDepth)
return "null" + Environment.NewLine;

var identation = new string('\t', nestingLevel + 1);
var indentation = new string('\t', nestingLevel + 1);
var sb = new StringBuilder();
var type = obj.GetType();
sb.AppendLine(type.Name);
sb.AppendLine($"{type.Name}:");

if (obj is IEnumerable enumerable)
return SerializeEnumerable(sb, enumerable, nestingLevel);

foreach (var propertyInfo in type.GetProperties())
{
sb.Append(identation + propertyInfo.Name + " = " +
PrintToString(propertyInfo.GetValue(obj),
nestingLevel + 1));
if (!_excludedProperties.Contains(propertyInfo) && !_excludedTypes.Contains(propertyInfo.PropertyType))
{
var valueString = GetValueString(propertyInfo, obj, nestingLevel);
sb.Append($"{indentation}{propertyInfo.Name} = {valueString}");
}
}
return sb.ToString();
}

private string SerializeEnumerable(StringBuilder sb, IEnumerable enumerable, int nestingLevel)
{
var bracketIndentation = new string('\t', nestingLevel);
sb.AppendLine($"{bracketIndentation}[");
foreach (var element in enumerable)
{
sb.Append($"{bracketIndentation}-\t");
var valueString = String.Empty;
if (_typeConverters.TryGetValue(element.GetType(), out var typeConverter))
valueString =
$"{typeConverter.DynamicInvoke(element) as string ?? "null"}{Environment.NewLine}";
else
valueString = PrintToString(element, nestingLevel + 1);
sb.Append($"{valueString}");
}
sb.AppendLine($"{bracketIndentation}]");
return sb.ToString();
}

private string SerializeFinalType(object obj)
{
if (obj is string stringValue)
return string.Concat(
stringValue.AsSpan(0, Math.Min(MaxStringLength, stringValue.Length)),

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Можешь подсказать, почему именно AsSpan, а не Substring?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Потому что SubString создаёт промежуточную строку, к которой мне потом надо приписать Environment.NewLine. За счёт AsSpan можно создавать на одну строку меньше.

Environment.NewLine);

if (obj is double doubleValue)
return doubleValue.ToString(DoubleCultureInfo) + Environment.NewLine;

if (obj is float floatValue)
return floatValue.ToString(FloatCultureInfo) + Environment.NewLine;

if (obj is DateTime dateTimeValue)
return dateTimeValue.ToString(DateTimeCultureInfo) + Environment.NewLine;

return obj + Environment.NewLine;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Сейчас ты каждый раз добавляешь перевод строки, возможно получится как-то избежать этого повторения?

И еще ты сейчас дважды создаешь строку при конкатенации, т.е. сначала приводишь объект к строке, а потом создаешь новую с символом переноса строки, что неэффективно.

Подсказка: дальше по коду у тебя идет использование StringBuilder

}

private string GetValueString(PropertyInfo propertyInfo, object obj, int nestingLevel)
{
var propertyValue = propertyInfo.GetValue(obj);
if (propertyValue == null || !TryConvert(propertyInfo, propertyValue, out var valueString))
valueString = PrintToString(propertyValue, nestingLevel + 1);
return valueString;
}

private bool TryConvert(PropertyInfo propertyInfo, object? propertyValue, out string value)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Кажется, что если заинлайнить этот метод, то код станет проще и чище

{
value = String.Empty;
if (_propertyConverters.TryGetValue(propertyInfo, out var converter))
value = $"{converter.DynamicInvoke(propertyValue) as string ?? "null"}{Environment.NewLine}";
else if (_typeConverters.TryGetValue(propertyInfo.PropertyType, out var typeConverter))
value = $"{typeConverter.DynamicInvoke(propertyValue) as string ?? "null"}{Environment.NewLine}";
return value != String.Empty;
}
}
}
22 changes: 22 additions & 0 deletions ObjectPrinting/PropertySerializerImpl.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using System;
using System.Reflection;

namespace ObjectPrinting;

internal class PropertySerializerImpl<TOwner, TProperty> : IPropertySerializer<TOwner, TProperty>
{
public PrintingConfig<TOwner> Config { get; }
private readonly MemberInfo _memberInfo;

internal PropertySerializerImpl(PrintingConfig<TOwner> config, MemberInfo memberInfo)
{
Config = config;
_memberInfo = memberInfo;
}

public PrintingConfig<TOwner> Use(Func<TProperty, string> converter)
{
Config.AddPropertyConverter(converter, _memberInfo);
return Config;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
Double[]:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Давай сериализовать коллекции (в особенности словари) в более приятную для чтения структуру? Например, сделать сериализацию приближенную к Json

[
- 1
- 2
- 3.14
- 4
- 5
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
Dictionary`2:
[
- KeyValuePair`2:
Key = a
Value = 1.1
- KeyValuePair`2:
Key = b
Value = 2.2
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
List`1:
[
- Alex
- John
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Person:
Id = Guid:
Name = Alex
Surname = Smith
Height = 177.4
Age = 19
OtherPerson = null
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
Person:
Id = Guid:
Name = Alex
Surname = Smith
Height = 177,4
Age = 19
OtherPerson = Person:
Id = Guid:
Name = Alex
Surname = Smith
Height = 177,4
Age = 19
OtherPerson = Person:
Id = Guid:
Name = Alex
Surname = Smith
Height = 177,4
Age = 19
OtherPerson = Person:
Id = null
Name = Alex
Surname = Smith
Height = 177,4
Age = 19
OtherPerson = null
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Person:
Id = Guid:
Name = Alex
Surname = Smith
Height = 177,4
Age = 19
OtherPerson = null
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Person:
Name = Alex
Surname = Smith
Height = 177,4
Age = 19
OtherPerson = null
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Person:
Name = Alex
Surname = Smith
Height = 177,4
Age = 19
OtherPerson = null
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Person:
Id = Guid:
Name = Alex
Surname = Smith
Height = 177,4
Age = 19
OtherPerson = John
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Person:
Id = Guid:
Name = Al
Surname = Sm
Height = 177,4
Age = 19
OtherPerson = null
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Person:
Id = Guid:
Height = 177,4
Age = 19
OtherPerson = null
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Person:
Id = 00000000-0000-0000-0000-000000000000
Name = Alex
Surname = Smith
Height = 177,4
Age = 19
OtherPerson = null
Loading