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

Сазонов Александр #220

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

namespace ObjectPrinting.Extensions;

public static class DictionaryExtensions
{
public static void AddRange<TKey, TValue>(this Dictionary<TKey, TValue> current, Dictionary<TKey, TValue> toAdd)
where TKey : notnull
{
foreach (var item in toAdd)
current.Add(item.Key, item.Value);
}

public static void AddMethod<TKey, TValue>(this Dictionary<TKey, TValue> current, TKey key, TValue value)

Choose a reason for hiding this comment

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

Можно убрать этот экстеншн, словарь кинет примерно такую же ошибку, если попытаешься добавить в него ключ, который уже есть

where TKey : notnull
{
if (!current.TryAdd(key, value))
throw new InvalidOperationException($"Type {typeof(TKey).Name} is already registered");
}
}
12 changes: 12 additions & 0 deletions ObjectPrinting/Extensions/HashsetExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using System.Collections.Generic;

namespace ObjectPrinting.Extensions;

public static class HashsetExtensions
{
public static void AddRange<T>(this HashSet<T> current, HashSet<T> toAdd)
{
foreach (var item in toAdd)
current.Add(item);
}
}
30 changes: 30 additions & 0 deletions ObjectPrinting/Extensions/MemberInfoExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using System.Reflection;

namespace ObjectPrinting.Extensions;

public static class MemberInfoExtensions
{
public static object? GetValue(this MemberInfo? memberInfo, object obj) =>
memberInfo switch
{
FieldInfo fieldInfo => fieldInfo.GetValue(obj),
PropertyInfo propertyInfo => propertyInfo.GetValue(obj),
_ => null
};
Comment on lines +7 to +13

Choose a reason for hiding this comment

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

Круто, что сразу обработал и поля, и свойства 🔥


public static Type? GetMemberType(this MemberInfo memberInfo) =>
memberInfo.MemberType switch
{
MemberTypes.Constructor => memberInfo.DeclaringType,
MemberTypes.Event => ((EventInfo)memberInfo).EventHandlerType,
MemberTypes.Field => ((FieldInfo)memberInfo).FieldType,
MemberTypes.Method => ((MethodInfo)memberInfo).ReturnType,
MemberTypes.Property => ((PropertyInfo)memberInfo).PropertyType,
MemberTypes.TypeInfo => ((TypeInfo)memberInfo).BaseType,
MemberTypes.Custom => memberInfo.DeclaringType,
MemberTypes.NestedType => memberInfo.DeclaringType,
MemberTypes.All => memberInfo.DeclaringType,

Choose a reason for hiding this comment

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

А разве нас интересует из этого списка что-то кроме Field и Property? Зачем описывать логику для типов, которые не будут использоваться в сериализации?

_ => throw new ArgumentOutOfRangeException(nameof(memberInfo), memberInfo,
"Unsupported member type.")
};
}
9 changes: 9 additions & 0 deletions ObjectPrinting/Extensions/ObjectExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace ObjectPrinting.Extensions;

public static class ObjectExtensions
{
public static string PrintToString<T>(this T obj)
{
return ObjectPrinter.For<T>().PrintToString(obj);
}
}
42 changes: 42 additions & 0 deletions ObjectPrinting/Extensions/PropertyPrintingConfigExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using ObjectPrinting.PropertyPrintingConfig;

namespace ObjectPrinting.Extensions;

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

public static PrintingConfig<TOwner> Using<TOwner, TPropType>(
this PropertyPrintingConfig<TOwner, TPropType> propConfig, CultureInfo culture, string? format = null)
where TPropType : IFormattable
{
var parentConfig = ((IPropertyPrintingConfig<TOwner, TPropType>)propConfig).ParentConfig;

parentConfig.TypeSerializationMethod.AddMethod(typeof(TPropType), (Func<TPropType, string>)FormatFunc);

return parentConfig;

string FormatFunc(TPropType obj) => obj.ToString(format, culture);
}

public static PrintingConfig<TOwner> TrimmedToLength<TOwner>(this PropertyPrintingConfig<TOwner, string> propConfig,
int maxLength)
{
if (maxLength < 1)
throw new ArgumentException($"{nameof(maxLength)} should be greater than 1");

var parentConfig = ((IPropertyPrintingConfig<TOwner, string>)propConfig).ParentConfig;

parentConfig.MemberSerializationMethod.AddMethod(propConfig.PropertyMemberInfo, (Func<string, string>)Function);

return parentConfig;

string Function(string str) =>
string.IsNullOrEmpty(str) || str.Length <= maxLength
? str
: str[..maxLength];
}
}
21 changes: 21 additions & 0 deletions ObjectPrinting/Extensions/TypeExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using System.Collections.Generic;

namespace ObjectPrinting.Extensions;

public static class TypeExtensions
{
private static readonly HashSet<Type> FinalTypes =
[
typeof(int),
typeof(double),
typeof(float),
typeof(string),
typeof(DateTime),
typeof(TimeSpan),
typeof(Guid),
typeof(DateTimeOffset)

Choose a reason for hiding this comment

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

А тут точно перечислены все примитивные типы? Ещё забыл всякие uint, ulong, short и тд.

Нет ли более простого способа узнать, является ли тип простым, не перечисляя все вариации простых типов?
Небольшая подсказка: посмотри в сторону класса Type и нет ли у него метода, очень похожего на твой экстеншн снизу))

];

public static bool IsFinal(this Type currentType) =>
FinalTypes.Contains(currentType);
}
2 changes: 2 additions & 0 deletions ObjectPrinting/GlobalUsings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
global using System;
global using System.Globalization;
11 changes: 5 additions & 6 deletions ObjectPrinting/ObjectPrinter.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
namespace ObjectPrinting
namespace ObjectPrinting;

public class ObjectPrinter
{
public class ObjectPrinter
public static PrintingConfig<T> For<T>()
{
public static PrintingConfig<T> For<T>()
{
return new PrintingConfig<T>();
}
return new PrintingConfig<T>();
}
}
7 changes: 1 addition & 6 deletions ObjectPrinting/ObjectPrinting.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,6 @@
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="NUnit" Version="4.2.2" />
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
</ItemGroup>
</Project>
156 changes: 127 additions & 29 deletions ObjectPrinting/PrintingConfig.cs
Original file line number Diff line number Diff line change
@@ -1,41 +1,139 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
using ObjectPrinting.Extensions;
using ObjectPrinting.PropertyPrintingConfig;

namespace ObjectPrinting
namespace ObjectPrinting;

public class PrintingConfig<TOwner>
{
public class PrintingConfig<TOwner>
private readonly HashSet<object> seenObjects = [];
private readonly HashSet<Type> excludedProperties = [];
private readonly HashSet<MemberInfo> excludedMembers = [];
internal readonly Dictionary<Type, Delegate> TypeSerializationMethod = new();
internal readonly Dictionary<MemberInfo, Delegate> MemberSerializationMethod = new();

Choose a reason for hiding this comment

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

Кажется, что можно взять более точные типы вместо Delegate. Что нам нужно по факту от этой функции? Чтобы она некоторый тип приводила к строке. Поэтому можно раскрутить что-то типа Func<..., string>, тогда не надо будет писать DynamicInvoke при вызове этих функций

Choose a reason for hiding this comment

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

Круто, что сделал сразу internal и позаботился о защите доступа извне 🔥


public PrintingConfig(PrintingConfig<TOwner>? parent = null)
{
if (parent == null)
return;

excludedProperties.AddRange(parent.excludedProperties);
excludedMembers.AddRange(parent.excludedMembers);
TypeSerializationMethod.AddRange(parent.TypeSerializationMethod);
MemberSerializationMethod.AddRange(parent.MemberSerializationMethod);
}

public PropertyPrintingConfig<TOwner, TPropType> Printing<TPropType>()
{
var configCopy = new PrintingConfig<TOwner>(this);
return new PropertyPrintingConfig<TOwner, TPropType>(configCopy);

Choose a reason for hiding this comment

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

А почему просто не прокидываешь this в PropertyPrintingConfig? Зачем создавать копию текущего класса? Без копий и конструктор сверху схлопнется и кучка методов расширения с ними

}

public PropertyPrintingConfig<TOwner, TPropType> Printing<TPropType>(
Expression<Func<TOwner, TPropType>> memberSelector)
{
var configCopy = new PrintingConfig<TOwner>(this);
var memberInfo = GetMemberInfo(memberSelector);
return new PropertyPrintingConfig<TOwner, TPropType>(configCopy, memberInfo);
}

private static MemberInfo? GetMemberInfo<TPropType>(Expression<Func<TOwner, TPropType>> memberSelector) =>
(memberSelector.Body as MemberExpression)?.Member;

public PrintingConfig<TOwner> Excluding<TPropType>(Expression<Func<TOwner, TPropType>> memberSelector)
{
var configClone = new PrintingConfig<TOwner>(this);
var memberInfo = GetMemberInfo(memberSelector);
if (memberInfo is null)
throw new ArgumentException("Invalid member selector.");
configClone.excludedMembers.Add(memberInfo);
return configClone;
}

public PrintingConfig<TOwner> Excluding<TPropType>()
{
var configCopy = new PrintingConfig<TOwner>(this);
configCopy.excludedProperties.Add(typeof(TPropType));
return configCopy;
}

public string PrintToString(TOwner owner) =>
PrintToString(owner, 0);

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

if (seenObjects.Contains(obj))
return "recursive reference" + Environment.NewLine;
Comment on lines +55 to +56

Choose a reason for hiding this comment

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

Смотри, у тебя сейчас в свойствах класса образовалось два типа свойств:

  1. Настраиваемые свойства конфига - по сути наша конфигурация сериализации - excludedProperties, excludedMembers и ещё два
  2. Свойства конкретного объекта сериализации - seenObjects. Из-за того, что он находится в свойствах конфига мы не можем вызвать конфиг для двух и более разных объектов, ведь после первого прогона сериализации seenObjects уже заполнится данными первого объекта, и для второго прогона уже не будет пустым


var type = obj.GetType();

if (type.IsFinal())
return obj + Environment.NewLine;

var numberOfTabs = new string('\t', nestingLevel + 1);
var sb = new StringBuilder();
sb.AppendLine(type.Name);
if (obj is IEnumerable enumerable)
sb.Append(GetCollectionString(enumerable, numberOfTabs, nestingLevel));
else
sb.Append(GetSingleElementString(obj, numberOfTabs, nestingLevel));
Comment on lines +66 to +69

Choose a reason for hiding this comment

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

К слову о надежности тестов, попробовал засериализовать словарь
image

Получил такое значение (ещё с decimal и byte что-то, но это скорее всего от того, что ты их не добавил в FinalTypes)

Person
	Height = 182
	BirthDate = 12/14/2024 00:00:00
	Some = Dictionary`2
[
		KeyValuePair`2
			Key = firstValue
			Value = one
		KeyValuePair`2
			Key = secondValue
			Value = two
		KeyValuePair`2
			Key = thirdValue
			Value = three
]
	Parent = null
	Name = Alex
	BestField = Decimal
		Scale = Byte


return sb.ToString();
}

private string GetCollectionString(IEnumerable enumerable, string numberOfTabs, int nestingLevel)
{
public string PrintToString(TOwner obj)
var sb = new StringBuilder();
sb.AppendLine("[");
foreach (var el in enumerable)
{
return PrintToString(obj, 0);
var value = PrintToString(el, nestingLevel + 1);
sb.Append($"{numberOfTabs}{value}");
}

private string PrintToString(object obj, int nestingLevel)
seenObjects.Add(enumerable);
sb.AppendLine("]");
return sb.ToString();
}

private string GetSingleElementString(object obj, string numberOfTabs, int nestingLevel)
{
var propertiesAndFields = obj
.GetType()
.GetMembers(BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Public)
.Where(member => member is PropertyInfo or FieldInfo && !IsMemberExcluded(member));
var sb = new StringBuilder();
foreach (var memberInfo in propertiesAndFields)
{
//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();
sb.Append(numberOfTabs + memberInfo.Name + " = " +
PrintToString(GetValue(obj, memberInfo),
nestingLevel + 1));
}

seenObjects.Add(obj);
return sb.ToString();
}

private bool IsMemberExcluded(MemberInfo member) =>
excludedProperties.Contains(member.GetMemberType()!) ||
excludedMembers.Contains(member);

private object? GetValue(object obj, MemberInfo memberInfo)
{
var val = memberInfo.GetValue(obj);
var memberType = memberInfo.GetMemberType();
return MemberSerializationMethod.TryGetValue(memberInfo, out var memberFunc)
? memberFunc.DynamicInvoke(val)
: TypeSerializationMethod.TryGetValue(memberType!, out var typeFunc)
? typeFunc.DynamicInvoke(val)
: val;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace ObjectPrinting.PropertyPrintingConfig;


public interface IPropertyPrintingConfig<TOwner, TPropType>
{
PrintingConfig<TOwner> ParentConfig { get; }
}
25 changes: 25 additions & 0 deletions ObjectPrinting/PropertyPrintingConfig/PropertyPrintingConfig.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using System.Reflection;
using ObjectPrinting.Extensions;

namespace ObjectPrinting.PropertyPrintingConfig;

public class PropertyPrintingConfig<TOwner, TPropType>(
PrintingConfig<TOwner> printingConfig,
MemberInfo? memberInfo = null)

Choose a reason for hiding this comment

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

Тут что-то странное вышло: говорим в конструкторе, что можем принимать null, но при этом позже при обращении к PropertyMemberInfo, если он null, кинем исключение

: IPropertyPrintingConfig<TOwner, TPropType>
{
public MemberInfo PropertyMemberInfo =>
memberInfo != null ? memberInfo : throw new InvalidOperationException("MemberInfo is null.");

public PrintingConfig<TOwner> Using(Func<TPropType, string> print)
{
if (memberInfo is null)
printingConfig.TypeSerializationMethod.AddMethod(typeof(TPropType), print);
else
printingConfig.MemberSerializationMethod.AddMethod(memberInfo, print);

return printingConfig;
}

PrintingConfig<TOwner> IPropertyPrintingConfig<TOwner, TPropType>.ParentConfig => printingConfig;
}
10 changes: 0 additions & 10 deletions ObjectPrinting/Solved/ObjectExtensions.cs

This file was deleted.

Loading