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
11 changes: 11 additions & 0 deletions Homework/Homework.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>

<OutputType>Library</OutputType>
</PropertyGroup>

</Project>
14 changes: 14 additions & 0 deletions Homework/IPrintingConfig.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using System.Globalization;
using System.Reflection;

namespace Homework;

public interface IPrintingConfig
{
HashSet<Type> ExcludeTypes { get; }
HashSet<MemberInfo> ExcludeMembers { get; }
Dictionary<Type, Func<object, string>> AlternativeTypesSerialization { get; }
Dictionary<MemberInfo, Func<object, string>> AlternativeMembersSerialization { get; }
Dictionary<Type, CultureInfo> TypesCultureInfo { get; }
Dictionary<MemberInfo, int> StringsTrim { get; }
}
9 changes: 9 additions & 0 deletions Homework/IPropertyPrintingConfig.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using System.Reflection;

namespace Homework;

public interface IPropertyPrintingConfig<TOwner, TPropType>
{
PrintingConfig<TOwner> ParentConfig { get; }
MemberInfo? MemberInfo { get; }
}
15 changes: 15 additions & 0 deletions Homework/LastMemberVisitor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using System.Linq.Expressions;

namespace Homework;

public class LastMemberVisitor : ExpressionVisitor
{
public MemberExpression? LastMemberExpression { get; private set; }

protected override Expression VisitMember(MemberExpression node)
{
LastMemberExpression = node;

return base.VisitMember(node);
}
}
9 changes: 9 additions & 0 deletions Homework/ObjectExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace Homework;

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

public class ObjectPrinter
{
public static PrintingConfig<T> For<T>()
{
return new PrintingConfig<T>();
}
}
16 changes: 16 additions & 0 deletions Homework/ObjectReferenceEqualityComparer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using System.Diagnostics.CodeAnalysis;

namespace Homework;

public class ObjectReferenceEqualityComparer : IEqualityComparer<object>
{
public new bool Equals(object? x, object? y)
{
return ReferenceEquals(x, y);
}

public int GetHashCode([DisallowNull] object obj)
{
return obj.GetHashCode();
}
}
172 changes: 172 additions & 0 deletions Homework/PrintingConfig.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
using System.Collections;
using System.Globalization;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;

namespace Homework;

public class PrintingConfig<TOwner> : IPrintingConfig
{
public PropertyPrintingConfig<TOwner, TPropType> Printing<TPropType>()
{
return new PropertyPrintingConfig<TOwner, TPropType>(this, null);
}

public PropertyPrintingConfig<TOwner, TPropType> Printing<TPropType>(Expression<Func<TOwner, TPropType>> memberSelector)
{
var lambda = memberSelector as LambdaExpression;
var visitor = new LastMemberVisitor();
visitor.Visit(lambda.Body);
var memberInfo = visitor.LastMemberExpression?.Member ?? throw new ArgumentException();

return new PropertyPrintingConfig<TOwner, TPropType>(this, memberInfo);
}

public PrintingConfig<TOwner> Excluding<TPropType>(Expression<Func<TOwner, TPropType>> memberSelector)
{
var lambda = memberSelector as LambdaExpression;
var visitor = new LastMemberVisitor();
visitor.Visit(lambda.Body);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Интересно, зачем поход в Visitor?

var memberInfo = visitor.LastMemberExpression?.Member ?? throw new ArgumentException();

((IPrintingConfig)this).ExcludeMembers.Add(memberInfo);

return this;
}

public PrintingConfig<TOwner> Excluding<TPropType>()
{
((IPrintingConfig)this).ExcludeTypes.Add(typeof(TPropType));

return this;
}

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

private string PrintToString(object? obj, MemberInfo? memberInfo, int nestingLevel, Dictionary<object, int> printedObjects)
{
if (obj == null)
return "null" + Environment.NewLine;

var type = obj.GetType();
if (printedObjects.TryGetValue(obj, out var level))
return $"(cycle with object {type.Name} at level {level}){Environment.NewLine}";

if (DoesTypeOverrideToString(type))
return Serialize(obj, memberInfo) + Environment.NewLine;

if (type.IsClass)
printedObjects.Add(obj, nestingLevel);

var result = obj is ICollection collection
? PrintCollectionToString(collection, nestingLevel, printedObjects)
: PrintComplexObjectToString(obj, nestingLevel, printedObjects);

if (type.IsClass)
printedObjects.Remove(obj);

return result;
}

private string PrintCollectionToString(ICollection collection, int nestingLevel, Dictionary<object, int> printedObjects)
{
var identation = new string('\t', nestingLevel);
var sb = new StringBuilder();
sb.Append((nestingLevel > 0 ? Environment.NewLine + identation : "") + '[' + Environment.NewLine);
foreach (var item in collection)
sb.Append(identation + '\t' +
PrintToString(
item,
null,
nestingLevel + 1,
printedObjects));

sb.Append(identation + ']' + Environment.NewLine);

return sb.ToString();
}

private string PrintComplexObjectToString(object obj, int nestingLevel, Dictionary<object, int> printedObjects)
{
var type = obj.GetType();
var identation = new string('\t', nestingLevel + 1);
var sb = new StringBuilder();
sb.AppendLine(type.Name);
foreach (var nestedMemberInfo in
GetPublicPropertiesAndFields(type).OrderBy(t => t.Name))
{
if (((IPrintingConfig)this).ExcludeMembers.Contains(nestedMemberInfo))
continue;

var newObj = GetValueFromPropertyOrField(obj, nestedMemberInfo);

if (newObj is not null
&& ((IPrintingConfig)this).ExcludeTypes.Contains(newObj.GetType()))
continue;

sb.Append(identation + nestedMemberInfo.Name + " = " +
PrintToString(
newObj,
nestedMemberInfo,
nestingLevel + 1,
printedObjects));
}

return sb.ToString();
}

private IEnumerable<MemberInfo> GetPublicPropertiesAndFields(Type type)
=> type
.GetProperties()
.Select(t => (MemberInfo)t)
.Concat(type.GetFields());

private object? GetValueFromPropertyOrField(object obj, MemberInfo memberInfo)
{
if (memberInfo is FieldInfo fieldInfo)
return fieldInfo.GetValue(obj);

return ((PropertyInfo)memberInfo).GetValue(obj);
}

private string Serialize(object obj, MemberInfo? memberInfo)
{
if (memberInfo is not null
&& ((IPrintingConfig)this).AlternativeMembersSerialization.TryGetValue(memberInfo, out var serializeMember))
return serializeMember(obj);

if (((IPrintingConfig)this).AlternativeTypesSerialization.TryGetValue(obj.GetType(), out var serializeType))
return serializeType(obj);

string? str;
if (((IPrintingConfig)this).TypesCultureInfo.TryGetValue(obj.GetType(), out var culture))
str = (string)obj.GetType().GetMethod("ToString", [typeof(IFormatProvider)]).Invoke(obj, [culture]);
else
str = obj.ToString();

if (memberInfo is not null
&& ((IPrintingConfig)this).StringsTrim.TryGetValue(memberInfo, out var maxLength)
&& str?.Length > maxLength)
str = str[..maxLength];

return str ?? "null";
}

private bool DoesTypeOverrideToString(Type type)
{
var toStringMethod = type.GetMethod("ToString", Type.EmptyTypes);
return toStringMethod?.DeclaringType != typeof(object) &&
toStringMethod?.DeclaringType == type;
}

HashSet<Type> IPrintingConfig.ExcludeTypes { get; } = [];
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Можешь рассказать почему выбрал такое решение (с интерфейсом)?
Какие плюсы/минусы по сравнению с альтернативами?

HashSet<MemberInfo> IPrintingConfig.ExcludeMembers { get; } = [];
Dictionary<Type, Func<object, string>> IPrintingConfig.AlternativeTypesSerialization { get; } = [];
Dictionary<MemberInfo, Func<object, string>> IPrintingConfig.AlternativeMembersSerialization { get; } = [];
Dictionary<Type, CultureInfo> IPrintingConfig.TypesCultureInfo { get; } = [];
Dictionary<MemberInfo, int> IPrintingConfig.StringsTrim { get; } = [];
}
24 changes: 24 additions & 0 deletions Homework/PropertyPrintingConfig.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using System.Reflection;

namespace Homework;

public class PropertyPrintingConfig<TOwner, TPropType>(PrintingConfig<TOwner> printingConfig, MemberInfo? memberInfo)
: IPropertyPrintingConfig<TOwner, TPropType>
{
public PrintingConfig<TOwner> Using(Func<TPropType, string> print)
{
if (memberInfo is not null)
{
((IPrintingConfig)printingConfig).AlternativeMembersSerialization[memberInfo] = obj => print((TPropType)obj);
}
else
{
((IPrintingConfig)printingConfig).AlternativeTypesSerialization[typeof(TPropType)] = obj => print((TPropType)obj);
}

return printingConfig;
}

PrintingConfig<TOwner> IPropertyPrintingConfig<TOwner, TPropType>.ParentConfig => printingConfig;
MemberInfo? IPropertyPrintingConfig<TOwner, TPropType>.MemberInfo => memberInfo;
}
31 changes: 31 additions & 0 deletions Homework/PropertyPrintingConfigExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using System.Globalization;

namespace Homework;

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> TrimmedToLength<TOwner>(this PropertyPrintingConfig<TOwner, string?> propConfig, int maxLen)
{
var configIface = (IPropertyPrintingConfig<TOwner, string>)propConfig;
if (configIface.MemberInfo is not null)
((IPrintingConfig)configIface.ParentConfig).StringsTrim[configIface.MemberInfo] = maxLen;

return configIface.ParentConfig;
}

public static PrintingConfig<TOwner> Using<TOwner, TPropType>(
this PropertyPrintingConfig<TOwner, TPropType> propConfig,
CultureInfo culture)
where TPropType : IFormattable
{
var configIface = (IPropertyPrintingConfig<TOwner, TPropType>)propConfig;
((IPrintingConfig)configIface.ParentConfig).TypesCultureInfo[typeof(TPropType)] = culture;

return configIface.ParentConfig;
}
}
29 changes: 29 additions & 0 deletions HomeworkTests/HomeworkTests.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>

<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.0" />
<PackageReference Include="FluentAssertions" Version="8.8.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="NUnit" Version="3.14.0" />
<PackageReference Include="NUnit.Analyzers" Version="3.9.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Homework\Homework.csproj" />
</ItemGroup>

<ItemGroup>
<Using Include="NUnit.Framework" />
</ItemGroup>

</Project>
Loading