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

Савенко Дмитрий #226

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
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
16 changes: 16 additions & 0 deletions ObjectPrinting/ObjectPrinterExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using System;

namespace ObjectPrinting;

public static class ObjectPrinterExtensions
{
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>> printConfig)
{
return printConfig(ObjectPrinter.For<T>()).PrintToString(obj);
}
}
1 change: 1 addition & 0 deletions ObjectPrinting/ObjectPrinting.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="FluentAssertions" Version="8.0.0-alpha.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="NUnit" Version="4.2.2" />
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
Expand Down
182 changes: 156 additions & 26 deletions ObjectPrinting/PrintingConfig.cs
Original file line number Diff line number Diff line change
@@ -1,41 +1,171 @@
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
namespace ObjectPrinting;

public class PrintingConfig<TOwner>
{
public class PrintingConfig<TOwner>
private readonly Type[] primitiveTypes =
{
typeof(int),
typeof(double),
typeof(float),
typeof(string),
typeof(DateTime),
typeof(TimeSpan),
typeof(Guid)
};

private readonly HashSet<Type> excludingTypes = new();
private readonly HashSet<PropertyInfo> excludingProperties = new();

public Dictionary<Type, Func<object, string>> TypeSerializers { get; } = new();
public Dictionary<Type, CultureInfo> Cultures { get; } = new();
public Dictionary<PropertyInfo, Func<object, string>> PropertySerializers { get; } = new();
public Dictionary<PropertyInfo, int> PropertiesMaxLength { get; } = new();

public PrintingConfig<TOwner> Excluding<TPropertyType>()
{
excludingTypes.Add(typeof(TPropertyType));
return this;
}

public PrintingConfig<TOwner> Excluding<TProperty>(Expression<Func<TOwner, TProperty>> memberSelector)
{
excludingProperties.Add(GetProperty(memberSelector));
return this;
}

public TypePrintingConfig<TOwner, TPropertyType> For<TPropertyType>() => new(this);

public PropertyPrintingConfig<TOwner, TProperty> For<TProperty>(Expression<Func<TOwner, TProperty>> memberSelector)
{
return new PropertyPrintingConfig<TOwner, TProperty>(this, GetProperty(memberSelector));
}

public string PrintToString(TOwner obj)
{
return Serialize(obj, 0, new Dictionary<object, int>());
}

private string Serialize(object obj, int nestingLevel, Dictionary<object, int> parsedObjects)
Copy link

@SlavikGh0st SlavikGh0st Dec 13, 2024

Choose a reason for hiding this comment

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

Тут надо пометить, что obj может быть null:

private string Serialize(object? obj, ...)

Отсюда же вытекает, что коллекции TypeSerializers и PropertySerializers могут содержать nullable типы:

public Dictionary<Type, Func<object?, string>> TypeSerializers { get; } = new();
public Dictionary<PropertyInfo, Func<object?, string>> PropertySerializers { get; } = new();

Если это всё проделать, то останется одно неочевидное место поведения в методе SerializeProperty:
image

Convert.ToString может вернуть null, а не строку - непонятно, как это в итоге отразиться на всей логике.

Copy link
Author

Choose a reason for hiding this comment

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

Добавил
Convert.ToString(propertyValue, culture) ?? "null"

{
public string PrintToString(TOwner obj)
if (obj is null)
{
return "null";
}

if (primitiveTypes.Contains(obj.GetType()))
{
return PrintToString(obj, 0);
return obj + Environment.NewLine;
}

private string PrintToString(object obj, int nestingLevel)
if (parsedObjects.TryGetValue(obj, out var level))
{
return $"<circular reference to {obj.GetType().Name} at level {level}>";
}

parsedObjects[obj] = nestingLevel;

switch (obj)

Choose a reason for hiding this comment

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

Можно на более красивый switch переделать:
image

{
//TODO apply configurations
if (obj == null)
return "null" + Environment.NewLine;
case IDictionary dictionary:
return SerializeDictionary(dictionary, nestingLevel, parsedObjects);
case IEnumerable collection:
return SerializeCollection(collection, nestingLevel, parsedObjects);
default:
return SerializeProperties(obj, nestingLevel, parsedObjects);
}
}

var finalTypes = new[]
{
typeof(int), typeof(double), typeof(float), typeof(string),
typeof(DateTime), typeof(TimeSpan)
};
if (finalTypes.Contains(obj.GetType()))
return obj + Environment.NewLine;
private string SerializeDictionary(IDictionary dictionary, int nestingLevel, Dictionary<object, int> parsedObjects)
{
var indentation = new string('\t', nestingLevel + 1);
var sb = new StringBuilder("Dictionary {\n");

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();
foreach (DictionaryEntry kvp in dictionary)
{
var key = Serialize(kvp.Key, nestingLevel + 1, parsedObjects).Trim();
var value = Serialize(kvp.Value, nestingLevel + 1, parsedObjects).Trim();
sb.AppendLine($"{indentation}{key} : {value}");
}

sb.AppendLine(new string('\t', nestingLevel) + "}");
return sb.ToString();
}

private string SerializeCollection(IEnumerable collection, int nestingLevel, Dictionary<object, int> parsedObjects)
{
var indentation = new string('\t', nestingLevel + 1);
var sb = new StringBuilder("Collection [\n");

foreach (var element in collection)
{
var value = Serialize(element, nestingLevel + 1, parsedObjects).Trim();
sb.AppendLine($"{indentation}{value}");
}

sb.AppendLine(new string('\t', nestingLevel) + "]");
return sb.ToString();
}

private string SerializeProperties(object obj, int nestingLevel, Dictionary<object, int> parsedObjects)
{
var type = obj.GetType();
var sb = new StringBuilder($"{type.Name} {{\n");
var indentation = new string('\t', nestingLevel + 1);

foreach (var propertyInfo in type.GetProperties(BindingFlags.Public | BindingFlags.Instance))
{
if (excludingProperties.Contains(propertyInfo)
|| excludingTypes.Contains(propertyInfo.PropertyType))
continue;

var serializedValue = SerializeProperty(propertyInfo, obj, nestingLevel + 1, parsedObjects);
sb.AppendLine($"{indentation}{propertyInfo.Name} = {serializedValue}");
}

sb.AppendLine(new string('\t', nestingLevel) + "}");
return sb.ToString();
}

private string SerializeProperty(PropertyInfo propertyInfo, object obj, int nestingLevel, Dictionary<object, int> parsedObjects)
{
var propertyValue = propertyInfo.GetValue(obj);

if (PropertySerializers.TryGetValue(propertyInfo, out var propertySerializer))
{
return propertySerializer(propertyValue);
}

if (TypeSerializers.TryGetValue(propertyInfo.PropertyType, out var typeSerializer))
{
return typeSerializer(propertyValue);
}

if (PropertiesMaxLength.TryGetValue(propertyInfo, out var maxLength) && propertyValue is string str)
{
return str[..Math.Min(str.Length, maxLength)];
}

return Cultures.TryGetValue(propertyInfo.PropertyType, out var culture)
? Convert.ToString(propertyValue, culture)
: Serialize(propertyValue, nestingLevel, parsedObjects);
}

private PropertyInfo GetProperty<TProperty>(Expression<Func<TOwner, TProperty>> memberSelector)

Choose a reason for hiding this comment

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

статический метод

{
if (memberSelector.Body is not MemberExpression memberExpression)
{
throw new ArgumentException("Expression must refer to a property.");
}

return memberExpression.Member as PropertyInfo ?? throw new ArgumentException("Expression must refer to a property.");
}
}
29 changes: 29 additions & 0 deletions ObjectPrinting/PropertyPrintingConfig.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using System;
using System.Reflection;

namespace ObjectPrinting
{
public class PropertyPrintingConfig<TOwner, TProperty>
{
private readonly PrintingConfig<TOwner> _config;
private readonly PropertyInfo _propertyInfo;

public PropertyPrintingConfig(PrintingConfig<TOwner> config, PropertyInfo propertyInfo)

Choose a reason for hiding this comment

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

Можно использовать PrimaryConstructor:

public class PropertyPrintingConfig<TOwner, TProperty>(PrintingConfig<TOwner> config, PropertyInfo propertyInfo)
{
   ...
}

{
_config = config;
_propertyInfo = propertyInfo;
}

public PrintingConfig<TOwner> UseSerializer(Func<TProperty, string> serializer)
{
_config.PropertySerializers[_propertyInfo] = value => serializer((TProperty)value);
return _config;
}

public PrintingConfig<TOwner> SetMaxLength(int maxLength)
{
_config.PropertiesMaxLength[_propertyInfo] = maxLength;
return _config;
}
}
}
18 changes: 14 additions & 4 deletions ObjectPrinting/Tests/ObjectPrinterAcceptanceTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using NUnit.Framework;
using System;
using System.Globalization;
using NUnit.Framework;

namespace ObjectPrinting.Tests
{
Expand All @@ -10,18 +12,26 @@ public void Demo()
{
var person = new Person { Name = "Alex", Age = 19 };

var printer = ObjectPrinter.For<Person>();
var printer = ObjectPrinter.For<Person>()
//1. Исключить из сериализации свойства определенного типа
.Excluding<Guid>()
//2. Указать альтернативный способ сериализации для определенного типа
.For<int>().UseSerializer(i => i.ToString("X"))
//3. Для числовых типов указать культуру
.For<double>().SetCulture(CultureInfo.InvariantCulture)
//4. Настроить сериализацию конкретного свойства
//5. Настроить обрезание строковых свойств (метод должен быть виден только для строковых свойств)
.For(p => p.Name).SetMaxLength(10)
//6. Исключить из сериализации конкретного свойства
.Excluding(p => p.Age);


string s1 = printer.PrintToString(person);
var s1 = printer.PrintToString(person);

//7. Синтаксический сахар в виде метода расширения, сериализующего по-умолчанию
//7. Синтаксический сахар в виде метода расширения, сериализующего по-умолчанию
var s2 = person.PrintToString();
//8. ...с конфигурированием
var s3 = person.PrintToString(config => config);
}
}
}
Loading