Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
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
179 changes: 153 additions & 26 deletions ObjectPrinting/PrintingConfig.cs
Original file line number Diff line number Diff line change
@@ -1,41 +1,168 @@
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)
{
if (obj is null)
{
return "null";
}

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

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

parsedObjects[obj] = nestingLevel;

return obj switch
{
IDictionary dictionary => SerializeDictionary(dictionary, nestingLevel, parsedObjects),
IEnumerable collection => SerializeCollection(collection, nestingLevel, parsedObjects),
_ => SerializeProperties(obj, nestingLevel, parsedObjects)
};
}

private string SerializeDictionary(IDictionary dictionary, int nestingLevel, Dictionary<object?, int> parsedObjects)
{
public string PrintToString(TOwner obj)
var indentation = new string('\t', nestingLevel + 1);
var sb = new StringBuilder("Dictionary {\n");

foreach (DictionaryEntry kvp in dictionary)
{
return PrintToString(obj, 0);
var key = Serialize(kvp.Key, nestingLevel + 1, parsedObjects).Trim();
var value = Serialize(kvp.Value, nestingLevel + 1, parsedObjects).Trim();
sb.AppendLine($"{indentation}{key} : {value}");
}

private string PrintToString(object obj, int nestingLevel)
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)
{
//TODO apply configurations
if (obj == null)
return "null" + Environment.NewLine;
var value = Serialize(element, nestingLevel + 1, parsedObjects).Trim();
sb.AppendLine($"{indentation}{value}");
}

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

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 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 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();
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) ?? "null"
: Serialize(propertyValue, nestingLevel, parsedObjects);
}

private static PropertyInfo GetProperty<TProperty>(Expression<Func<TOwner, TProperty>> memberSelector)
{
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.");
}
}
20 changes: 20 additions & 0 deletions ObjectPrinting/PropertyPrintingConfig.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using System;
using System.Reflection;

namespace ObjectPrinting
{
public class PropertyPrintingConfig<TOwner, TProperty>(PrintingConfig<TOwner> 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