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
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
246 changes: 212 additions & 34 deletions ObjectPrinting/Solved/PrintingConfig.cs
Original file line number Diff line number Diff line change
@@ -1,62 +1,240 @@
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.Solved
namespace ObjectPrinting.Solved;

public class PrintingConfig<TOwner>
{
public class PrintingConfig<TOwner>
private readonly HashSet<Type> excludedTypes = [];
private readonly HashSet<string> excludedMembers = [];
private readonly Dictionary<Type, Func<object, string>> typeSerializers = new();
private readonly Dictionary<string, Func<object, string>> memberSerializers = new();
private readonly Dictionary<Type, CultureInfo> cultures = new();
private readonly Dictionary<string, int> stringTrimLengths = new();
private readonly HashSet<object> visitedObjects = [];

public PropertyPrintingConfig<TOwner, TPropType> Printing<TPropType>()
{
return new PropertyPrintingConfig<TOwner, TPropType>(this);
}

public PropertyPrintingConfig<TOwner, TPropType> Printing<TPropType>(
Expression<Func<TOwner, TPropType>> memberSelector)
{
var member = GetMemberInfo(memberSelector);
return new PropertyPrintingConfig<TOwner, TPropType>(this, member.Name);
}

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

public PrintingConfig<TOwner> Excluding<TPropType>(Expression<Func<TOwner, TPropType>> memberSelector)
{
var member = GetMemberInfo(memberSelector);
excludedMembers.Add(member.Name);
return this;
}

internal void AddTypeSerializer<TPropType>(Func<TPropType, string> serializer)
{
typeSerializers[typeof(TPropType)] = obj => serializer((TPropType)obj);
}

internal void AddMemberSerializer<TPropType>(string memberName, Func<TPropType, string> serializer)
{
memberSerializers[memberName] = obj => serializer((TPropType)obj);
}

internal void AddCulture<TPropType>(CultureInfo culture)
{
cultures[typeof(TPropType)] = culture;
}

internal void SetStringTrimLength(string memberName, int maxLength)
{
stringTrimLengths[memberName] = maxLength;
}


public string PrintToString(TOwner obj)
{
public PropertyPrintingConfig<TOwner, TPropType> Printing<TPropType>()
visitedObjects.Clear();
return PrintToString(obj, 0);
}

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

if (!visitedObjects.Add(obj))
return $"Cyclic reference detected ({obj.GetType().Name})" + Environment.NewLine;

try
{
return new PropertyPrintingConfig<TOwner, TPropType>(this);
}
var type = obj.GetType();

if (type == typeof(string))
{
var str = (string)obj;
return "\"" + str + "\"" + Environment.NewLine;
}

if (obj is IEnumerable enumerable && !IsFinalType(type))
{
return PrintCollection(enumerable, nestingLevel);
}

public PropertyPrintingConfig<TOwner, TPropType> Printing<TPropType>(Expression<Func<TOwner, TPropType>> memberSelector)
if (IsFinalType(type) || typeSerializers.ContainsKey(type))
{
return SerializeValue(obj, type) + Environment.NewLine;
}

return PrintObject(obj, nestingLevel, type);
}
finally

This comment was marked as resolved.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

type.GetFields и type.GetProperties в PrintObject () могут нам кинуть ошибки с рефлексией, это системные ошибки и их лучше пробросить дальше или "развалиться" :)
Также могут прилететь ошибки из сериализатора SerializeValue(), на этом уровне мне кажется лучше всего предупредить пользователя о том, что какой-то объект не сериализовался => я ловлю ошибку в методе SerializeValue()

{
return new PropertyPrintingConfig<TOwner, TPropType>(this);
visitedObjects.Remove(obj);
}
}

private static bool IsFinalType(Type type)
{
var finalTypes = new[]
{
typeof(int), typeof(double), typeof(float), typeof(string),
typeof(DateTime), typeof(TimeSpan), typeof(Guid), typeof(bool),
typeof(char), typeof(byte), typeof(short), typeof(long),
typeof(decimal)
};
return finalTypes.Contains(type) || type.IsEnum || type.IsPrimitive;
}

public PrintingConfig<TOwner> Excluding<TPropType>(Expression<Func<TOwner, TPropType>> memberSelector)
private string? SerializeValue(object value, Type valueType)
{
try
{
return this;
if (typeSerializers.TryGetValue(valueType, out var typeSerializer))
{
return typeSerializer(value);
}

if (cultures.TryGetValue(valueType, out var culture)
&& value is IFormattable formattable)
return formattable.ToString(null, culture);
return value.ToString();
}
catch (Exception ex) when (IsSerializationException(ex))
{
return $"[Serialization error: {ex.Message}]";
}
}

private static bool IsSerializationException(Exception ex)
{
return ex is FormatException or
InvalidCastException or
NullReferenceException or
ArgumentOutOfRangeException or
DivideByZeroException or
OverflowException or
NotSupportedException or
InvalidOperationException;
}

internal PrintingConfig<TOwner> Excluding<TPropType>()
private string PrintObject(object obj, int nestingLevel, Type type)
{
var indentationLine = new string('\t', nestingLevel + 1);
var stringBuilder = new StringBuilder();
stringBuilder.AppendLine(type.Name);

foreach (var field in type.GetFields(BindingFlags.Public | BindingFlags.Instance))
{
return this;
if (excludedTypes.Contains(field.FieldType) || excludedMembers.Contains(field.Name))
continue;
stringBuilder.Append(indentationLine + field.Name + " = " +
SerializeMember(field.Name, field.GetValue(obj), nestingLevel));
}

public string PrintToString(TOwner obj)
foreach (var property in type.GetProperties(BindingFlags.Public | BindingFlags.Instance))
{
if (excludedTypes.Contains(property.PropertyType) || excludedMembers.Contains(property.Name))
continue;

if (property.GetIndexParameters().Length > 0)
continue;

stringBuilder.Append(indentationLine + property.Name + " = " +
SerializeMember(property.Name, property.GetValue(obj), nestingLevel));
}
return stringBuilder.ToString();
}
private string SerializeMember(string memberName, object? value, int nestingLevel)
{
if (value == null)
return "null" + Environment.NewLine;

if (memberSerializers.TryGetValue(memberName, out var memberSerializer))
{
return memberSerializer(value) + Environment.NewLine;
}

if (value is string strValue && stringTrimLengths.TryGetValue(memberName, out var maxLength))
{
return PrintToString(obj, 0);
var trimmed = strValue.Length <= maxLength ? strValue : strValue[..maxLength];
return "\"" + trimmed + "\"" + Environment.NewLine;
}

private string PrintToString(object obj, int nestingLevel)
var valueType = value.GetType();
if (typeSerializers.TryGetValue(valueType, out var typeSerializer))
{
//TODO apply configurations
if (obj == null)
return "null" + Environment.NewLine;
return typeSerializer(value) + Environment.NewLine;
}

return PrintToString(value, nestingLevel + 1);
}

private string PrintCollection(IEnumerable collection, int nestingLevel)
{
var stringBuilder = new StringBuilder();
var indentationLine = new string('\t', nestingLevel + 1);
stringBuilder.AppendLine(collection.GetType().Name);

var finalTypes = new[]
if (collection is IDictionary dictionary)
{
foreach (var key in dictionary.Keys)
{
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())
stringBuilder.Append(indentationLine +
$"[{PrintToString(key, nestingLevel + 1).Trim()}] = " +
PrintToString(dictionary[key], nestingLevel + 1));
}
}
else
{
var index = 0;
foreach (var item in collection)
{
sb.Append(identation + propertyInfo.Name + " = " +
PrintToString(propertyInfo.GetValue(obj),
nestingLevel + 1));
stringBuilder.Append(indentationLine + $"[{index}] = " + PrintToString(item, nestingLevel + 1));
index++;
}
return sb.ToString();
}
return stringBuilder.ToString();
}

private static MemberInfo GetMemberInfo<TPropType>(Expression<Func<TOwner, TPropType>> memberSelector)
{
if (memberSelector.Body is MemberExpression memberExpression)
{
return memberExpression.Member;
}
throw new ArgumentException("Expression is not a member access", nameof(memberSelector));
}
}
}
41 changes: 24 additions & 17 deletions ObjectPrinting/Solved/PropertyPrintingConfig.cs
Original file line number Diff line number Diff line change
@@ -1,32 +1,39 @@
using System;
using System.Globalization;

namespace ObjectPrinting.Solved
{
public class PropertyPrintingConfig<TOwner, TPropType> : IPropertyPrintingConfig<TOwner, TPropType>
{
private readonly PrintingConfig<TOwner> printingConfig;
namespace ObjectPrinting.Solved;

public PropertyPrintingConfig(PrintingConfig<TOwner> printingConfig)
{
this.printingConfig = printingConfig;
}
public class PropertyPrintingConfig<TOwner, TPropType>(
PrintingConfig<TOwner> printingConfig,
string? memberName = null)
: IPropertyPrintingConfig<TOwner, TPropType>
{
public string? MemberName { get; } = memberName;

public PrintingConfig<TOwner> Using(Func<TPropType, string> print)
public PrintingConfig<TOwner> Using(Func<TPropType, string> print)
{
if (MemberName != null)
{
return printingConfig;
printingConfig.AddMemberSerializer(MemberName, print);
}

public PrintingConfig<TOwner> Using(CultureInfo culture)
else
{
return printingConfig;
printingConfig.AddTypeSerializer(print);
}

PrintingConfig<TOwner> IPropertyPrintingConfig<TOwner, TPropType>.ParentConfig => printingConfig;
return printingConfig;
}

public interface IPropertyPrintingConfig<TOwner, TPropType>
public PrintingConfig<TOwner> Using(CultureInfo culture)
{
PrintingConfig<TOwner> ParentConfig { get; }
printingConfig.AddCulture<TPropType>(culture);
return printingConfig;
}

public PrintingConfig<TOwner> ParentConfig => printingConfig;
}

public interface IPropertyPrintingConfig<TOwner, TPropType>
{
PrintingConfig<TOwner> ParentConfig { get; }
}
21 changes: 13 additions & 8 deletions ObjectPrinting/Solved/PropertyPrintingConfigExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
using System;

namespace ObjectPrinting.Solved
namespace ObjectPrinting.Solved;

public static class PropertyPrintingConfigExtensions
{
public static class PropertyPrintingConfigExtensions
public static string PrintToString<T>(this T obj, Func<PrintingConfig<T>, PrintingConfig<T>> config)
{
public static string PrintToString<T>(this T obj, Func<PrintingConfig<T>, PrintingConfig<T>> config)
{
return config(ObjectPrinter.For<T>()).PrintToString(obj);
}
return config(ObjectPrinter.For<T>()).PrintToString(obj);
}

public static PrintingConfig<TOwner> TrimmedToLength<TOwner>(this PropertyPrintingConfig<TOwner, string> propConfig, int maxLen)
public static PrintingConfig<TOwner> TrimmedToLength<TOwner>(this PropertyPrintingConfig<TOwner, string> propConfig,
int maxLen)
{
var memberName = propConfig.MemberName;
if (memberName != null)
{
return ((IPropertyPrintingConfig<TOwner, string>)propConfig).ParentConfig;
propConfig.ParentConfig.SetStringTrimLength(memberName, maxLen);
}

return propConfig.ParentConfig;
}
}
Loading