-
Notifications
You must be signed in to change notification settings - Fork 245
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
base: master
Are you sure you want to change the base?
Шевырин Никита #225
Changes from 14 commits
441f072
bdadc67
0a315e1
9605d4a
54a9708
360534d
12332f2
9e2a950
c7b2f79
02918de
c87c057
92dbb2a
d6aeaa8
50334f7
743b6a8
69eca2a
90176f4
d79b36a
436cdd4
3c8a091
aea11ea
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -236,3 +236,6 @@ _Pvt_Extensions | |
.fake/ | ||
|
||
.idea/ | ||
|
||
# Verify.NUnit | ||
*.received.* |
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); | ||
} |
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); | ||
} |
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); | ||
} | ||
} |
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; | ||
internal int MaxRecursionDepth { get; set; } = 16; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Если глубина рекурсии справедлива для всего PrintingConfig, то давай ее передавать в конструкторе, а не через метод There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Поле можно сделать приватным There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. А не будет ли передача глубины рекурсии через конструктор противоречить идее Fluent api? У меня There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
|
||
|
@@ -23,19 +96,84 @@ private string PrintToString(object obj, int nestingLevel) | |
typeof(DateTime), typeof(TimeSpan) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Насколько эффективно каждый раз создавать эту коллекцию при условии, что она всегда одна и та же? |
||
}; | ||
if (finalTypes.Contains(obj.GetType())) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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)), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Можешь подсказать, почему именно There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Потому что |
||
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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Сейчас ты каждый раз добавляешь перевод строки, возможно получится как-то избежать этого повторения? И еще ты сейчас дважды создаешь строку при конкатенации, т.е. сначала приводишь объект к строке, а потом создаешь новую с символом переноса строки, что неэффективно. Подсказка: дальше по коду у тебя идет использование |
||
} | ||
|
||
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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
} | ||
} | ||
} |
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[]: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Сейчас ты ограничиваешь длину сразу для всех типов, но что, если мы захотим ограничить длину у одного конкретного поля? Сейчас твоя реализация не позволит такого сделать
Так же мне не кажется, что задание значения по умолчанию является удачным выбором. Число действительно большое, но это ограничение, которого без указания пользователем по идее быть не должно, в теории на вход может прийти строка, длина которой превосходит int.MaxValue, и тогда мы без предупреждения обрежем строку. Если пользователь явно не указал, то обрезать строку не надо
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Да, ты прав, у строки действительно не может быть длины, превышающей это значение. Но мне все равно не кажется хорошей идея задавать потолок в поле, без явной на то необходимости. Как мне кажется, лучше было бы сделать поле нулабельным, и если значение на ограничение не устанавлено, то не обрезать строку вовсе