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
22 changes: 22 additions & 0 deletions ObjectPrinting/MemberPrintingConfig.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;

namespace ObjectPrinting
{
public class MemberPrintingConfig<TOwner, TProp>(PrintingConfig<TOwner> parent, MemberInfo member)
{
protected readonly PrintingConfig<TOwner> Parent = parent;
protected readonly MemberInfo Member = member;

public PrintingConfig<TOwner> Using(Func<TProp, string> serializer)
{
Parent.SetMemberSerializer(Member, serializer);
return Parent;
}

}
}
19 changes: 19 additions & 0 deletions ObjectPrinting/MemberPrintingConfigForString.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;

namespace ObjectPrinting
{
public class MemberPrintingConfigForString<TOwner>(PrintingConfig<TOwner> parent, MemberInfo member)
: MemberPrintingConfig<TOwner, string>(parent, member)
{
public PrintingConfig<TOwner> TrimmedToLength(int maxLen)
{
Parent.SetMemberTrimLength(Member, maxLen);
return Parent;
}
}
}
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.8.0" />
<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
100 changes: 76 additions & 24 deletions ObjectPrinting/PrintingConfig.cs
Original file line number Diff line number Diff line change
@@ -1,41 +1,93 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;

using System.Reflection;
namespace ObjectPrinting
{
public class PrintingConfig<TOwner>
{
public string PrintToString(TOwner obj)
private readonly HashSet<Type> excludedTypes = new HashSet<Type>();
private readonly HashSet<MemberInfo> excludedMembers = new HashSet<MemberInfo>();

private readonly Dictionary<Type, Delegate> typeSerializers = new Dictionary<Type, Delegate>();
private readonly Dictionary<Type, CultureInfo> typeCultures = new Dictionary<Type, CultureInfo>();

private readonly Dictionary<MemberInfo, Delegate> memberSerializers = new Dictionary<MemberInfo, Delegate>();
private readonly Dictionary<MemberInfo, int> memberTrimLengths = new Dictionary<MemberInfo, int>();
private readonly HashSet<Type> finalTypes =
[
typeof(int), typeof(double), typeof(float), typeof(long), typeof(short), typeof(string),
typeof(byte), typeof(decimal), typeof(bool), typeof(DateTime), typeof(TimeSpan)
];
internal IReadOnlyCollection<Type> ExcludedTypes => excludedTypes;
internal IReadOnlyCollection<MemberInfo> ExcludedMembers => excludedMembers;
internal IReadOnlyDictionary<Type, Delegate> TypeSerializers => typeSerializers;
internal IReadOnlyDictionary<Type, CultureInfo> TypeCultures => typeCultures;
internal IReadOnlyDictionary<MemberInfo, Delegate> MemberSerializers => memberSerializers;
internal IReadOnlyDictionary<MemberInfo, int> MemberTrimLengths => memberTrimLengths;

internal IReadOnlyCollection<Type> FinalTypes => finalTypes;
public PrintingConfig<TOwner> Excluding<TProp>()
{
excludedTypes.Add(typeof(TProp));
return this;
}
public PrintingConfig<TOwner> Excluding<TProp>(Expression<Func<TOwner, TProp>> memberSelector)
{
return PrintToString(obj, 0);
var member = GetMemberInfo(memberSelector);
excludedMembers.Add(member);
return this;
}
internal void SetTypeSerializer<TProp>(Func<TProp, string> serializer)
{
typeSerializers[typeof(TProp)] = serializer;
}

private string PrintToString(object obj, int nestingLevel)
internal void SetTypeCulture<TProp>(CultureInfo culture)
{
//TODO apply configurations
if (obj == null)
return "null" + Environment.NewLine;
typeCultures[typeof(TProp)] = culture;
}

var finalTypes = new[]
{
typeof(int), typeof(double), typeof(float), typeof(string),
typeof(DateTime), typeof(TimeSpan)
};
if (finalTypes.Contains(obj.GetType()))
return obj + Environment.NewLine;

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())
internal void SetMemberSerializer<TProp>(MemberInfo member, Func<TProp, string> serializer)
{
memberSerializers[member] = serializer;
}

internal void SetMemberTrimLength(MemberInfo member, int length)
{
memberTrimLengths[member] = length;
}
public TypePrintingConfig<TOwner, TProp> Printing<TProp>()
{
return new TypePrintingConfig<TOwner, TProp>(this);
}

public MemberPrintingConfig<TOwner, TProp> Printing<TProp>(Expression<Func<TOwner, TProp>> memberSelector)
{
var member = GetMemberInfo(memberSelector);
return new MemberPrintingConfig<TOwner, TProp>(this, member);
}

public MemberPrintingConfigForString<TOwner> Printing(Expression<Func<TOwner, string>> memberSelector)
{
var member = GetMemberInfo(memberSelector);
return new MemberPrintingConfigForString<TOwner>(this, member);
}
public string PrintToString(TOwner obj)
{
return new Serializer<TOwner>(this).Serialize(obj);
}
private static MemberInfo GetMemberInfo<TPropType>(Expression<Func<TOwner, TPropType>> memberSelector)
{
if (memberSelector.Body is MemberExpression memberExpression)
{
sb.Append(identation + propertyInfo.Name + " = " +
PrintToString(propertyInfo.GetValue(obj),
nestingLevel + 1));
return memberExpression.Member;
}
return sb.ToString();
throw new ArgumentException("Expression is not a member access", nameof(memberSelector));
}
}
}
218 changes: 218 additions & 0 deletions ObjectPrinting/Serializer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;

namespace ObjectPrinting
{
public class Serializer<T>(PrintingConfig<T> config)
{
private readonly PrintingConfig<T> config = config;
private StringBuilder sb;
private HashSet<object> visited;

public string Serialize(T root)
{
sb = new StringBuilder();
visited = new HashSet<object>(new ReferenceEqualityComparer());

PrintObject(root, 0, null);

var result = sb.ToString();

return result;
}

private void PrintObject(object? obj, int nestingLevel, MemberInfo? currentMember)
{
if (obj == null) { sb.AppendLine("null"); return; }

var type = obj.GetType();

if (IsExcluded(type, currentMember))
{
sb.AppendLine(string.Empty);
return;
}

if (TryApplyMemberSerializer(obj, currentMember)) return;
if (TryApplyTypeSerializer(type, obj)) return;

if (HandleString(obj, currentMember)) return;
if (HandleFormattable(type, obj)) return;
if (HandleFinals(type, obj)) return;

if (HandleReferenceTracking(type, obj)) return;

if (HandleDictionary(obj, nestingLevel)) return;
if (HandleEnumerable(obj, nestingLevel)) return;

sb.AppendLine(type.Name);
PrintProperties(type, obj, nestingLevel);
PrintFields(type, obj, nestingLevel);
}

private static string Indent(int lvl) => new string('\t', lvl);

private static object? GetValueSafely(MemberInfo member, object obj)
{
try
{
switch (member)
{
case PropertyInfo p: return p.GetValue(obj);
case FieldInfo f: return f.GetValue(obj);
default: return null;
}
}
catch
{
return null;
}
}

private bool IsExcluded(Type type, MemberInfo? member) =>
config.ExcludedTypes.Contains(type) || (member != null && config.ExcludedMembers.Contains(member));

private bool TryApplyMemberSerializer(object obj, MemberInfo? member)
{
if (member == null || !config.MemberSerializers.TryGetValue(member, out var mser)) return false;
var s = mser.DynamicInvoke(obj);
sb.AppendLine(s?.ToString());
return true;
}

private bool TryApplyTypeSerializer(Type type, object obj)
{
if (!config.TypeSerializers.TryGetValue(type, out var tser)) return false;
var s = tser.DynamicInvoke(obj);
sb.AppendLine(s?.ToString());
return true;
}

private bool HandleString(object? obj, MemberInfo? member)
{
if (obj == null) return false;
if (obj.GetType() != typeof(string)) return false;

var s = obj as string;
if (member != null && config.MemberTrimLengths.TryGetValue(member, out var l) && s != null && s.Length > l)
s = s.Substring(0, l);

sb.AppendLine(s);
return true;
}

private bool HandleFormattable(Type type, object obj)
{
if (obj is not IFormattable formattable || !config.TypeCultures.TryGetValue(type, out var culture))
return false;
sb.AppendLine(formattable.ToString(null, culture));
return true;
}

private bool HandleFinals(Type type, object obj)
{
if (!config.FinalTypes.Contains(type)) return false;
sb.AppendLine(obj.ToString());
return true;
}

private bool HandleReferenceTracking(Type type, object obj)
{
if (type.IsValueType) return false;
if (visited.Contains(obj))
{
sb.AppendLine($"<Циклическая ссылка {type.Name}>");
return true;
}
visited.Add(obj);
return false;
}

private bool HandleDictionary(object obj, int nestingLevel)
{
if (obj is not IDictionary dict) return false;

var type = obj.GetType();
sb.AppendLine(type.Name);
foreach (DictionaryEntry e in dict)
{
sb.Append(Indent(nestingLevel + 1));
sb.Append("Key = ");
PrintObject(e.Key, nestingLevel + 1, null);

sb.Append(Indent(nestingLevel + 1));
sb.Append("Value = ");
PrintObject(e.Value, nestingLevel + 1, null);
}
return true;
}

private bool HandleEnumerable(object obj, int nestingLevel)
{
if (obj is not IEnumerable enumerable || obj is string) return false;

var type = obj.GetType();
sb.AppendLine(type.Name);
int i = 0;
foreach (var item in enumerable)
{
sb.Append(Indent(nestingLevel + 1));
sb.Append($"[{i}] = ");
PrintObject(item, nestingLevel + 1, null);
i++;
}
return true;
}

private void PrintProperties(Type type, object obj, int nestingLevel)
{
var props = type.GetProperties(BindingFlags.Public | BindingFlags.Instance);
foreach (var p in props)
{
if (p.GetIndexParameters().Length > 0) continue;
if (config.ExcludedTypes.Contains(p.PropertyType) || config.ExcludedMembers.Contains(p)) continue;

sb.Append(Indent(nestingLevel + 1));
sb.Append(p.Name);
sb.Append(" = ");

var value = GetValueSafely(p, obj);

if (value is string sVal && config.MemberTrimLengths.TryGetValue(p, out var trim) && sVal.Length > trim)
value = sVal.Substring(0, trim);

PrintObject(value, nestingLevel + 1, p);
}
}

private void PrintFields(Type type, object obj, int nestingLevel)
{
var fields = type.GetFields(BindingFlags.Public | BindingFlags.Instance);
foreach (var f in fields)
{
if (config.ExcludedTypes.Contains(f.FieldType) || config.ExcludedMembers.Contains(f)) continue;

sb.Append(Indent(nestingLevel + 1));
sb.Append(f.Name);
sb.Append(" = ");

var value = GetValueSafely(f, obj);

if (value is string sf && config.MemberTrimLengths.TryGetValue(f, out var trimf) && sf.Length > trimf)
value = sf.Substring(0, trimf);

PrintObject(value, nestingLevel + 1, f);
}
}

private class ReferenceEqualityComparer : IEqualityComparer<object>
{
public new bool Equals(object x, object y) => ReferenceEquals(x, y);
public int GetHashCode(object obj) => System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(obj);
}
}
}
Loading