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
57 changes: 57 additions & 0 deletions ObjectPrinting/EnumerablePrinter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using ObjectPrinting.Interface;

namespace ObjectPrinting;

internal class EnumerablePrinter
{
public string PrintEnumerable(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Я наоборот против того, чтобы выносить отдельно. У тебя есть конкретный контекст у этого сервиса, который тянется из родительского класса - config, visited. serializer.

Также нам теперь приходится передавать и тип, и сам объект. Это уже нарушение инкапсуляции

IEnumerable enumerable,
Type type,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Передаем только чтобы в StringBuilder записать?

int level,
IPrintingConfigInternal config,
HashSet<object> visited,
ObjectSerializer serializer)
{
var indent = new string('\t', level);
var sb = new StringBuilder();

sb.AppendLine(type.Name);

if (enumerable is IDictionary dict)
{
foreach (DictionaryEntry entry in dict)
{
sb.Append(indent + "\t");
sb.Append($"[{entry.Key}] = {serializer.PrintInternal(entry.Value, level + 1, config, visited)}");
}
}
else
{
var i = 0;
foreach (var item in enumerable)
{
sb.Append(indent + "\t");

if (item != null &&
item.GetType().IsGenericType &&
item.GetType().GetGenericTypeDefinition() == typeof(KeyValuePair<,>))
{
dynamic kv = item;
sb.Append($"[{kv.Key}] = {serializer.PrintInternal(kv.Value, level + 1, config, visited)}");
}
else
{
sb.Append($"[{i}] = {serializer.PrintInternal(item, level + 1, config, visited)}");
}

i++;
}
}

return sb.ToString();
}
}
26 changes: 26 additions & 0 deletions ObjectPrinting/FormattableMemberConfig.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using System;
using System.Globalization;
using ObjectPrinting.Interface;

namespace ObjectPrinting;

public class FormattableMemberConfig<TOwner, TProp> :
IFormattableMemberConfig<TOwner, TProp>
where TProp : IFormattable
{
private readonly PrintingConfig<TOwner> config;
private readonly string memberName;

public FormattableMemberConfig(PrintingConfig<TOwner> config, string memberName)
{
this.config = config;
this.memberName = memberName;
}

public PrintingConfig<TOwner> Using(CultureInfo culture)
{
config.PropertySerializers[memberName] = obj =>
((IFormattable)obj).ToString(null, culture);
return config;
}
}
10 changes: 10 additions & 0 deletions ObjectPrinting/Interface/IFormattableMemberConfig.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using System;
using System.Globalization;

namespace ObjectPrinting.Interface;

public interface IFormattableMemberConfig<TOwner, TProp>
where TProp : IFormattable
{
PrintingConfig<TOwner> Using(CultureInfo culture);
}
8 changes: 8 additions & 0 deletions ObjectPrinting/Interface/IMemberConfig.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using System;

namespace ObjectPrinting.Interface;

public interface IMemberConfig<TOwner, TProp>
{
PrintingConfig<TOwner> Using(Func<TProp, string> serializer);
}
15 changes: 15 additions & 0 deletions ObjectPrinting/Interface/IPrintingConfigInternal.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;

namespace ObjectPrinting.Interface;

internal interface IPrintingConfigInternal
{
HashSet<Type> ExcludedTypes { get; }
HashSet<string> ExcludedProperties { get; }

Dictionary<Type, Func<object, string>> TypeSerializers { get; }
Dictionary<string, Func<object, string>> PropertySerializers { get; }
Dictionary<string, int> TrimLengths { get; }
public int? GlobalStringTrimLength { get; }
Comment on lines +8 to +14

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Я продолжаю мочь добавлять элементы напрямую в коллекции.

Зачем мне мучиться с контекстами, валидациями и т.д, если я могу просто написать TrimLength["Name"]=0

}
6 changes: 6 additions & 0 deletions ObjectPrinting/Interface/IStringMemberConfig.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace ObjectPrinting.Interface;

public interface IStringMemberConfig<TOwner>
{
PrintingConfig<TOwner> Trim(int maxLength);
}
47 changes: 47 additions & 0 deletions ObjectPrinting/MemberPrinter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using System;
using System.Collections.Generic;
using ObjectPrinting.Interface;

namespace ObjectPrinting;

internal class MemberPrinter
{
public string PrintMember(
string memberName,
object value,
Type memberType,
IPrintingConfigInternal config,
int level,
HashSet<object> visited,
ObjectSerializer serializer)
{
if (value == null)
return memberName + " = null" + Environment.NewLine;

if (config.PropertySerializers.TryGetValue(memberName, out var serializerFunc))
return memberName + " = " + serializerFunc(value) + Environment.NewLine;

if (config.TypeSerializers.TryGetValue(memberType, out var typeSerializer))
return memberName + " = " + typeSerializer(value) + Environment.NewLine;

if (value is string s)
{
int? maxLen = null;

if (config.TrimLengths.TryGetValue(memberName, out var propTrim))
maxLen = propTrim;
else if (config.GlobalStringTrimLength.HasValue)
maxLen = config.GlobalStringTrimLength.Value;

if (maxLen.HasValue && s.Length > maxLen.Value)
s = s.Substring(0, maxLen.Value);

return memberName + " = " + s + Environment.NewLine;
}

if (TypeHelper.IsSimpleType(memberType))
return memberName + " = " + value + Environment.NewLine;

return memberName + " = " + serializer.PrintInternal(value, level, config, visited);
}
}
30 changes: 30 additions & 0 deletions ObjectPrinting/MemberPrintingConfig.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using System;
using System.Globalization;
using ObjectPrinting.Interface;

namespace ObjectPrinting;

public class MemberPrintingConfig<TOwner, TProp>
{
private readonly PrintingConfig<TOwner> config;
private readonly string memberName;

internal MemberPrintingConfig(PrintingConfig<TOwner> config, string memberName)
{
this.config = config;
this.memberName = memberName;
}

public PrintingConfig<TOwner> Using(Func<TProp, string> serializer)
{
config.PropertySerializers[memberName] = obj => serializer((TProp)obj);
return config;
}
Comment on lines +18 to +22

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

unused


public IStringMemberConfig<TOwner> AsString()
{
return typeof(TProp) == typeof(string)
? new StringMemberConfig<TOwner>(config, memberName)
: throw new InvalidOperationException("Property is not string");
}
Comment on lines +24 to +29

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

С точки зрения интерфейса выглядит ультра странно. Я и так передал конкретное поле с конкретным типом. Но чтобы появилась пачка методов, я должен еще раз сказать тип.

Допустим, у нас появятся для других типов кастомные методы. Мы также будем дописывать AsGuid, AsEnum, AsInt?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Плюсом, у нас возможно исключение. Что вообще весь пользовательский опыт рушит

}
13 changes: 13 additions & 0 deletions ObjectPrinting/ObjectPrinterExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System;

namespace ObjectPrinting;

public static class ObjectPrinterExtensions
{
public static string PrintToString<T>(
this T obj, Func<PrintingConfig<T>, PrintingConfig<T>> config)
{
var cfg = config(new PrintingConfig<T>());
return cfg.PrintToString(obj);
}
}
69 changes: 69 additions & 0 deletions ObjectPrinting/ObjectSerializer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using System.Text;
using ObjectPrinting.Interface;

namespace ObjectPrinting;

public class ObjectSerializer
{
private readonly MemberPrinter memberPrinter = new MemberPrinter();
private readonly EnumerablePrinter enumerablePrinter = new EnumerablePrinter();

public string Print<TOwner>(TOwner obj, PrintingConfig<TOwner> config)
{
var visited = new HashSet<object>(new ReferenceEqualityComparer());
return PrintInternal(obj, 0, config, visited);
}

internal string PrintInternal(
object obj,
int level,
IPrintingConfigInternal config,
HashSet<object> visited)
{
if (obj == null)
return "null" + Environment.NewLine;
Comment on lines +27 to +28

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Если у тебя сценарно может передаваться null, так и укажи nullable-type через "?". типа object? obj


var type = obj.GetType();

if (TypeHelper.IsSimpleType(type))
return obj + Environment.NewLine;

if (!type.IsValueType && !visited.Add(obj))
return $"<cyclic reference to {type.Name}>" + Environment.NewLine;

if (obj is IEnumerable enumerable)
return enumerablePrinter.PrintEnumerable(enumerable, type, level, config, visited, this);

var indent = new string('\t', level);
var sb = new StringBuilder();

sb.AppendLine(type.Name);

foreach (var property in type.GetProperties(BindingFlags.Public | BindingFlags.Instance))
{
if (property.GetIndexParameters().Length > 0) continue;
if (config.ExcludedTypes.Contains(property.PropertyType)) continue;
if (config.ExcludedProperties.Contains(property.Name)) continue;

var value = property.GetValue(obj);
sb.Append(indent + "\t" +
memberPrinter.PrintMember(property.Name, value, property.PropertyType, config, level + 1, visited, this));
}

foreach (var field in type.GetFields(BindingFlags.Public | BindingFlags.Instance))
{
if (config.ExcludedTypes.Contains(field.FieldType)) continue;
if (config.ExcludedProperties.Contains(field.Name)) continue;

var value = field.GetValue(obj);
sb.Append(indent + "\t" +
memberPrinter.PrintMember(field.Name, value, field.FieldType, config, level + 1, visited, this));
}
Comment on lines +46 to +65

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

DRY так и осталось


return sb.ToString();
}
}
94 changes: 58 additions & 36 deletions ObjectPrinting/PrintingConfig.cs
Original file line number Diff line number Diff line change
@@ -1,41 +1,63 @@
using System;
using System.Linq;
using System.Text;
using System.Collections.Generic;
using System.Globalization;
using System.Linq.Expressions;
using ObjectPrinting.Interface;

namespace ObjectPrinting
namespace ObjectPrinting;

public class PrintingConfig<TOwner> : IPrintingConfigInternal
{
public class PrintingConfig<TOwner>
public HashSet<Type> ExcludedTypes { get; } = new();
public HashSet<string> ExcludedProperties { get; } = new();

public Dictionary<Type, Func<object, string>> TypeSerializers { get; } = new();
public Dictionary<string, Func<object, string>> PropertySerializers { get; } = new();
public Dictionary<string, int> TrimLengths { get; } = new();
Comment on lines +11 to +16

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Все это отображается в интелисенсе, что будет мешать изучению сервиса через fluent подход.
Плюсом, я могу в обход методов добавить в твои коллекции что угодно

@Balashov2004 Balashov2004 Nov 21, 2025

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.

не понял
они же должны вызываться то есть не могут быть private

public int? GlobalStringTrimLength { get; private set; }

private readonly ObjectSerializer objectSerializer = new ObjectSerializer();

public PrintingConfig<TOwner> TrimStringsGlobal(int maxLength)
{
GlobalStringTrimLength = maxLength;
return this;
}
Comment on lines +21 to +25

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

В целом так сделать можно. Но мы на этом уровне не работаем с конкретными типами.

Лучше, чтобы было типа такого: config.Printing().Trimmed(5)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Сделать это можно так:

public static PrintingConfig Trimmed(
this MemberPrintingConfig<TOwner, string> propConfig, int maxLen)


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

public PrintingConfig<TOwner> Excluding<TProp>(Expression<Func<TOwner, TProp>> selector)
{
var name = ((MemberExpression)selector.Body).Member.Name;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

По условиям задачи - "Исключение из сериализации конкретного свойства/поля".
А значит это исключение не должно влиять на другие типы с таким же названием поля. У тебя сейчас влияет

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Под эту ситуацию нужны также тесты

ExcludedProperties.Add(name);
return this;
}

public PrintingConfig<TOwner> Printing<TProp>(Func<TProp, string> selector)
{
TypeSerializers[typeof(TProp)] = o => selector((TProp)o);
return this;
}

public TypePrintingConfig<TOwner, TProp> Printing<TProp>()
{
return new TypePrintingConfig<TOwner, TProp>(this);
}

public MemberPrintingConfig<TOwner, TProp> SelectMember<TProp>(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

До этого и для исключения типа, и для исключения поля мы используем один и тот же метод. А здесь для поля - SelectMember, для типа - Printing. Давай к одному виду приведем, дабы избежать непонимания

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Ну и опять же. Если у меня будет поле AnotherPerson с свойством Name, то ты его тоже по-другому сериализовать будешь, а я его даже не выбирал.

Expression<Func<TOwner, TProp>> selector)
{
var name = ((MemberExpression)selector.Body).Member.Name;
return new MemberPrintingConfig<TOwner, TProp>(this, name);
}

public string PrintToString(TOwner obj)
{
public string PrintToString(TOwner obj)
{
return PrintToString(obj, 0);
}

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

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())
{
sb.Append(identation + propertyInfo.Name + " = " +
PrintToString(propertyInfo.GetValue(obj),
nestingLevel + 1));
}
return sb.ToString();
}
return objectSerializer.Print(obj, this);
}
}

}
Loading