From 396f016481b90ca4c0bdcf44e8a8dc8db2f895a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C4=9Bj=20=C5=A0t=C3=A1gl?= Date: Wed, 8 Jan 2025 05:48:39 +0100 Subject: [PATCH] add support for ignoring members --- FastCloner.Tests/SpecialCaseTests.cs | 131 +++++++++++++++++++- FastCloner/Code/DeepCloneIgnoreAttribute.cs | 13 ++ FastCloner/Code/FastClonerExprGenerator.cs | 95 +++++++++----- 3 files changed, 209 insertions(+), 30 deletions(-) create mode 100644 FastCloner/Code/DeepCloneIgnoreAttribute.cs diff --git a/FastCloner.Tests/SpecialCaseTests.cs b/FastCloner.Tests/SpecialCaseTests.cs index 491a637..cbea3f5 100644 --- a/FastCloner.Tests/SpecialCaseTests.cs +++ b/FastCloner.Tests/SpecialCaseTests.cs @@ -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; @@ -262,6 +263,134 @@ 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() @@ -269,7 +398,7 @@ public void Test_ExpressionTree_OrderBy1() IOrderedQueryable q = Enumerable.Range(1, 5).Reverse().AsQueryable().OrderBy(x => x); IOrderedQueryable 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] diff --git a/FastCloner/Code/DeepCloneIgnoreAttribute.cs b/FastCloner/Code/DeepCloneIgnoreAttribute.cs new file mode 100644 index 0000000..9b3bdaa --- /dev/null +++ b/FastCloner/Code/DeepCloneIgnoreAttribute.cs @@ -0,0 +1,13 @@ +namespace FastCloner.Code; + +/// +/// Marks given field / property as ignored, effectively assigning a default value when cloning such entity. +/// +[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] +public class DeepCloneIgnoreAttribute(bool ignored = true) : Attribute +{ + /// + /// Gets whether the member should be ignored during cloning. + /// + public bool Ignored { get; } = ignored; +} \ No newline at end of file diff --git a/FastCloner/Code/FastClonerExprGenerator.cs b/FastCloner/Code/FastClonerExprGenerator.cs index 55503e2..562955a 100644 --- a/FastCloner/Code/FastClonerExprGenerator.cs +++ b/FastCloner/Code/FastClonerExprGenerator.cs @@ -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(); + 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 @@ -85,6 +91,21 @@ private static bool IsCloneable(Type type) return !BadTypes.ContainsAnyPattern(type.FullName); } + private static List GetAllMembers(Type type) + { + List 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)) @@ -172,50 +193,66 @@ private static bool IsCloneable(Type type) } } - List fi = []; - Type? tp = type; - do + ExpressionPosition currentPosition = position; + IEnumerable 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(); + + 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();