Skip to content

Commit

Permalink
add support for ignoring members
Browse files Browse the repository at this point in the history
  • Loading branch information
lofcz committed Jan 8, 2025
1 parent b617523 commit 396f016
Show file tree
Hide file tree
Showing 3 changed files with 209 additions and 30 deletions.
131 changes: 130 additions & 1 deletion FastCloner.Tests/SpecialCaseTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
using System.Net.Http.Headers;
using System.Runtime.InteropServices;
using System.Text;
using FastCloner.Code;
using FastCloner.Contrib;
using Microsoft.EntityFrameworkCore;

Expand Down Expand Up @@ -262,14 +263,142 @@ public void Test_DeepClone_ClassHierarchy()
Assert.That(cloned1.c2.c3.Id, Is.EqualTo(original.c2.c3.Id));
});
}

private class TestProps
{
public int A { get; set; } = 10;
public string B { get; set; } = "My string";
}

private class TestPropsWithIgnored
{
public int A { get; set; } = 10;

[DeepCloneIgnore]
public string B { get; set; } = "My string";
}

[Test]
public void Test_Clone_Props()
{
TestProps original = new TestProps { A = 42, B = "Test value" };
TestProps clone = original.DeepClone();

Assert.Multiple(() =>
{
Assert.That(clone.A, Is.EqualTo(42));
Assert.That(clone.B, Is.EqualTo("Test value"));
Assert.That(clone, Is.Not.SameAs(original));
});
}

[Test]
public void Test_Clone_Props_With_Ignored()
{
TestPropsWithIgnored original = new TestPropsWithIgnored { A = 42, B = "Test value" };
TestPropsWithIgnored clone = original.DeepClone();

Assert.Multiple(() =>
{
Assert.That(clone.A, Is.EqualTo(42));
Assert.That(clone.B, Is.EqualTo(null)); // default value
Assert.That(clone, Is.Not.SameAs(original));
});
}

private class TestAutoProps
{
public int A { get; set; } = 10;
public string B { get; private set; } = "My string";
public int C => A * 2;

private int _d;
public int D
{
get => _d;
set => _d = value;
}
}

[Test]
public void Test_Clone_Auto_Properties()
{
// Arrange
TestAutoProps original = new TestAutoProps
{
A = 42,
D = 100
};

// Set private setter property via reflection
original.GetType().GetProperty("B")!
.SetValue(original, "Test value", null);

// Act
TestAutoProps clone = original.DeepClone();

// Assert
Assert.Multiple(() =>
{
Assert.That(clone.A, Is.EqualTo(42));
Assert.That(clone.B, Is.EqualTo("Test value"));
Assert.That(clone.C, Is.EqualTo(84));
Assert.That(clone.D, Is.EqualTo(100));
Assert.That(clone, Is.Not.SameAs(original));
});
}

private class TestAutoPropsWithIgnored
{
public int A { get; set; } = 10;

[DeepCloneIgnore]
public string B { get; private set; } = "My string";

public int C => A * 2;

private int _d;
[DeepCloneIgnore]
public int D
{
get => _d;
set => _d = value;
}
}

[Test]
public void Test_Clone_Auto_Properties_With_Ignored()
{
// Arrange
TestAutoPropsWithIgnored original = new TestAutoPropsWithIgnored
{
A = 42,
D = 100
};
original.GetType().GetProperty("B")!
.SetValue(original, "Test value", null);

// Act
TestAutoPropsWithIgnored clone = original.DeepClone();

// Assert
Assert.Multiple(() =>
{
Assert.That(clone.A, Is.EqualTo(42));
Assert.That(clone.B, Is.EqualTo(null));
Assert.That(clone.C, Is.EqualTo(84));
Assert.That(clone.D, Is.EqualTo(0));
Assert.That(clone, Is.Not.SameAs(original));
});
}

[Test]
public void Test_ExpressionTree_OrderBy1()
{
IOrderedQueryable<int> q = Enumerable.Range(1, 5).Reverse().AsQueryable().OrderBy(x => x);
IOrderedQueryable<int> q2 = q.DeepClone();
Assert.That(q2.ToArray()[0], Is.EqualTo(1));
Assert.That(q.ToArray().Length, Is.EqualTo(5));
Assert.That(q.ToArray(), Has.Length.EqualTo(5));
}

[Test]
Expand Down
13 changes: 13 additions & 0 deletions FastCloner/Code/DeepCloneIgnoreAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
namespace FastCloner.Code;

/// <summary>
/// Marks given field / property as ignored, effectively assigning a default value when cloning such entity.
/// </summary>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
public class DeepCloneIgnoreAttribute(bool ignored = true) : Attribute
{
/// <summary>
/// Gets whether the member should be ignored during cloning.
/// </summary>
public bool Ignored { get; } = ignored;
}
95 changes: 66 additions & 29 deletions FastCloner/Code/FastClonerExprGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ internal static class FastClonerExprGenerator

internal static object GenerateClonerInternal(Type realType, bool asObject) => GenerateProcessMethod(realType, asObject && realType.IsValueType());

private static bool MemberIsIgnored(MemberInfo memberInfo)
{
DeepCloneIgnoreAttribute? attribute = memberInfo.GetCustomAttribute<DeepCloneIgnoreAttribute>();
return attribute?.Ignored ?? false;
}

// today, I found that it is not required to do such complex things. Just SetValue is enough
// is it new runtime changes, or I made incorrect assumptions earlier
// slow, but hardcore method to set readonly field
Expand Down Expand Up @@ -85,6 +91,21 @@ private static bool IsCloneable(Type type)
return !BadTypes.ContainsAnyPattern(type.FullName);
}

private static List<MemberInfo> GetAllMembers(Type type)
{
List<MemberInfo> members = [];
Type? currentType = type;

while (currentType != null && currentType != typeof(ContextBoundObject))
{
members.AddRange(currentType.GetDeclaredFields());
members.AddRange(currentType.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance).Where(p => p is { CanRead: true, CanWrite: true } && p.GetIndexParameters().Length == 0)); // Exclude indexers
currentType = currentType.BaseType();
}

return members;
}

private static object? GenerateProcessMethod(Type type, bool unboxStruct, ExpressionPosition position)
{
if (!IsCloneable(type))
Expand Down Expand Up @@ -172,50 +193,66 @@ private static bool IsCloneable(Type type)
}
}

List<FieldInfo> fi = [];
Type? tp = type;
do
ExpressionPosition currentPosition = position;
IEnumerable<MemberInfo> members = GetAllMembers(type);

foreach (MemberInfo member in members)
{
// don't do anything with this dark magic!
if (tp == typeof(ContextBoundObject)) break;
Type memberType = member switch
{
FieldInfo fi => fi.FieldType,
PropertyInfo pi => pi.PropertyType,
_ => throw new ArgumentException($"Unsupported member type: {member.GetType()}")
};

fi.AddRange(tp.GetDeclaredFields());
tp = tp.BaseType();
}
while (tp != null);
if (member is PropertyInfo piLocal)
{
DeepCloneIgnoreAttribute? attribute = piLocal.GetCustomAttribute<DeepCloneIgnoreAttribute>();

if (attribute?.Ignored ?? false)
{
expressionList.Add(Expression.Assign(
Expression.Property(toLocal, piLocal),
Expression.Default(piLocal.PropertyType)
));
}

ExpressionPosition currentPosition = position;
continue;
}

foreach (FieldInfo fieldInfo in fi)
{
if (!FastClonerSafeTypes.CanReturnSameObject(fieldInfo.FieldType))
if (!FastClonerSafeTypes.CanReturnSameObject(memberType))
{
MethodInfo methodInfo = fieldInfo.FieldType.IsValueType()
if (MemberIsIgnored(member))
{
expressionList.Add(Expression.Assign(
Expression.MakeMemberAccess(toLocal, member),
Expression.Default(memberType)
));
continue;
}

MethodInfo methodInfo = memberType.IsValueType()
? typeof(FastClonerGenerator).GetPrivateStaticMethod(nameof(FastClonerGenerator.CloneStructInternal))!
.MakeGenericMethod(fieldInfo.FieldType)
.MakeGenericMethod(memberType)
: typeof(FastClonerGenerator).GetPrivateStaticMethod(nameof(FastClonerGenerator.CloneClassInternal))!;

MemberExpression get = Expression.Field(fromLocal, fieldInfo);

// toLocal.Field = Clone...Internal(fromLocal.Field)
MemberExpression get = Expression.MakeMemberAccess(fromLocal, member);
Expression call = Expression.Call(methodInfo, get, state);
if (!fieldInfo.FieldType.IsValueType())
call = Expression.Convert(call, fieldInfo.FieldType);

if (!memberType.IsValueType())
call = Expression.Convert(call, memberType);

// should handle specially
// todo: think about optimization, but it rare case
bool isReadonly = _readonlyFields.GetOrAdd(fieldInfo, f => f.IsInitOnly);
if (isReadonly)
if (member is FieldInfo fieldInfo && _readonlyFields.GetOrAdd(fieldInfo, f => f.IsInitOnly))
{
expressionList.Add(Expression.Call(
Expression.Constant(fieldInfo),
_fieldSetMethod,
Expression.Convert(toLocal, typeof(object)),
Expression.Convert(call, typeof(object))));
Expression.Constant(fieldInfo),
_fieldSetMethod,
Expression.Convert(toLocal, typeof(object)),
Expression.Convert(call, typeof(object))));
}
else
{
expressionList.Add(Expression.Assign(Expression.Field(toLocal, fieldInfo), call));
expressionList.Add(Expression.Assign(Expression.MakeMemberAccess(toLocal, member), call));
}

currentPosition = currentPosition.Next();
Expand Down

0 comments on commit 396f016

Please sign in to comment.