diff --git a/.gitignore b/.gitignore
index f83279f..b92d86f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -179,3 +179,4 @@ FakesAssemblies/
*.sln.ide/
BenchmarkDotNet.Artifacts/results/
.vscode/
+/.vs/CloneExtensions/v15/Server/sqlite3
diff --git a/src/CloneExtensions.Benchmarks/CloneExtensions.Benchmarks.csproj b/src/CloneExtensions.Benchmarks/CloneExtensions.Benchmarks.csproj
index 04f2bc3..321f3c6 100644
--- a/src/CloneExtensions.Benchmarks/CloneExtensions.Benchmarks.csproj
+++ b/src/CloneExtensions.Benchmarks/CloneExtensions.Benchmarks.csproj
@@ -12,4 +12,7 @@
+
+
+
\ No newline at end of file
diff --git a/src/CloneExtensions.UnitTests/CollectionTests.cs b/src/CloneExtensions.UnitTests/CollectionTests.cs
index 3e0c11e..39cceca 100644
--- a/src/CloneExtensions.UnitTests/CollectionTests.cs
+++ b/src/CloneExtensions.UnitTests/CollectionTests.cs
@@ -76,9 +76,7 @@ public void GetClone_DerivedTypeWithShadowedProperty_ClonnedProperly()
var target = CloneFactory.GetClone(source);
Assert.AreEqual(1, target.Property);
-
- // TODO: Make it work ...
- // Assert.AreEqual(2, ((BaseClass)target).Property);
+ Assert.AreEqual(2, ((BaseClass)target).Property);
}
class MyClass
diff --git a/src/CloneExtensions.UnitTests/ComplexTypeTests.cs b/src/CloneExtensions.UnitTests/ComplexTypeTests.cs
index c1e5061..cfc68d1 100644
--- a/src/CloneExtensions.UnitTests/ComplexTypeTests.cs
+++ b/src/CloneExtensions.UnitTests/ComplexTypeTests.cs
@@ -152,6 +152,31 @@ public void GetClone_DerivedTypeWithShadowedProperty_ClonnedProperly()
Assert.AreEqual("test2", ((BaseClassOne)target).VirtualProperty3);
}
+ [TestMethod]
+ public void GetClone_DerivedTypeWithShadowedProperty_ClonnedProperly2()
+ {
+ D source = new D()
+ {
+ Foo = "D"
+ };
+
+ ((C)source).Foo = "C";
+ ((B)source).Foo = "B";
+ ((A)source).Foo = "A";
+
+ Assert.AreEqual("C", source.Foo);
+ Assert.AreEqual("C", ((C)source).Foo);
+ Assert.AreEqual("A", ((B)source).Foo);
+ Assert.AreEqual("A", ((A)source).Foo);
+
+ var target = source.GetClone();
+
+ Assert.AreEqual("C", target.Foo);
+ Assert.AreEqual("C", ((C)target).Foo);
+ Assert.AreEqual("A", ((B)target).Foo);
+ Assert.AreEqual("A", ((A)target).Foo);
+ }
+
struct SimpleStruct
{
public int _field;
@@ -198,7 +223,7 @@ class CircularReference2
public CircularReference1 Other { get;set; }
}
- abstract class BaseClassOne
+ abstract class BaseClassOne : IInterface
{
public int MyField;
@@ -212,7 +237,9 @@ virtual public string VirtualProperty3
get { return _virtualProperty; }
set { _virtualProperty = string.Empty; }
}
-
+
+ public int InterfaceProperty { get; set; }
+
private string _virtualProperty;
}
@@ -226,5 +253,25 @@ class DerivedClassOne : BaseClassOne
// use the default implementation for VirtualProperty2
public override string VirtualProperty3 { get; set; }
}
+
+ public class A
+ {
+ public virtual string Foo { get; set; }
+ }
+
+ public class B : A
+ {
+ public override string Foo { get; set; }
+ }
+
+ public class C : B
+ {
+ public virtual new string Foo { get; set; }
+ }
+
+ public class D : C
+ {
+ public override string Foo { get; set; }
+ }
}
}
diff --git a/src/CloneExtensions/ExpressionFactories/ComplexTypeExpressionFactory.cs b/src/CloneExtensions/ExpressionFactories/ComplexTypeExpressionFactory.cs
index e226c86..37fb54d 100644
--- a/src/CloneExtensions/ExpressionFactories/ComplexTypeExpressionFactory.cs
+++ b/src/CloneExtensions/ExpressionFactories/ComplexTypeExpressionFactory.cs
@@ -1,4 +1,5 @@
-using System;
+using CloneExtensions.Extensions;
+using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
@@ -83,36 +84,38 @@ private Expression GetInitializationExpression()
private Expression GetFieldsCloneExpression(Func getItemCloneExpression)
{
- var fields = from f in _type.GetTypeInfo().GetFields(BindingFlags.Public | BindingFlags.Instance)
- where !f.GetCustomAttributes(typeof(NonClonedAttribute), true).Any()
- where !f.IsInitOnly
- select new Member(f, f.FieldType);
-
- return GetMembersCloneExpression(fields.ToArray(), getItemCloneExpression);
+ var fields = _type
+ .GetAllFields()
+ .Where(x =>
+ x.CanRead &&
+ x.CanWrite &&
+ !x.IsLiteral &&
+ !x.IsBackingField &&
+ x.IsPublic &&
+ x.GetCustomAttributes().Count() == 0)
+ .Select(x => new Member(x.FieldInfo, x.FieldInfo.FieldType))
+ .ToArray();
+
+ return GetMembersCloneExpression(fields, getItemCloneExpression);
}
private Expression GetPropertiesCloneExpression(Func getItemCloneExpression)
{
- // get all private fields with `>k_BackingField` in case we can use them instead of automatic properties
- var backingFields = GetBackingFields(_type).ToDictionary(f => new BackingFieldInfo(f.DeclaringType, f.Name));
-
- // use the backing fields if available, otherwise use property
- var members = new List();
- var properties = GetProperties(_type);
- foreach (var property in properties)
- {
- FieldInfo fieldInfo;
- if (backingFields.TryGetValue(new BackingFieldInfo(property.DeclaringType, "<" + property.Name + ">k__BackingField"), out fieldInfo))
- {
- members.Add(new Member(fieldInfo, fieldInfo.FieldType));
- }
- else
- {
- members.Add(new Member(property, property.PropertyType));
- }
- }
-
- return GetMembersCloneExpression(members.ToArray(), getItemCloneExpression);
+ var members = _type
+ .GetFilteredProperties()
+ .Where(x =>
+ x.CanRead &&
+ x.CanWrite &&
+ x.IsPublic &&
+ !x.HasParameters &&
+ !x.IsLiteral &&
+ x.GetCustomAttributes().Count() == 0)
+ .Select(x => x.HasBackingField ?
+ new Member(x.BackingField.FieldInfo, x.BackingField.FieldInfo.FieldType) :
+ new Member(x.PropertyInfo, x.PropertyInfo.PropertyType))
+ .ToArray();
+
+ return GetMembersCloneExpression(members, getItemCloneExpression);
}
private Expression GetMembersCloneExpression(Member[] members, Func getItemCloneExpression)
@@ -174,65 +177,5 @@ private Expression GetForeachAddExpression(Type collectionType)
)
);
}
-
- private static IEnumerable GetBackingFields(Type type)
- {
- TypeInfo typeInfo = type.GetTypeInfo();
-
- while(typeInfo != null && typeInfo.UnderlyingSystemType != _objectType)
- {
- foreach (var field in typeInfo.GetFields(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly))
- {
- if (field.Name.Contains(">k__BackingField") && field.DeclaringType == typeInfo.UnderlyingSystemType)
- yield return field;
- }
-
- typeInfo = typeInfo.BaseType?.GetTypeInfo();
- }
- }
-
- private static IEnumerable GetProperties(Type type)
- {
- TypeInfo typeInfo = type.GetTypeInfo();
-
- while (typeInfo != null && typeInfo.UnderlyingSystemType != _objectType)
- {
- var properties = from p in typeInfo.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly)
- let setMethod = p.GetSetMethod(false)
- let getMethod = p.GetGetMethod(false)
- where !p.GetCustomAttributes(typeof(NonClonedAttribute), true).Any()
- where setMethod != null && getMethod != null && !p.GetIndexParameters().Any()
- select p;
-
- foreach (var p in properties)
- {
- yield return p;
- }
-
- typeInfo = typeInfo.BaseType?.GetTypeInfo();
- }
- }
-
- private struct BackingFieldInfo : IEquatable
- {
- public Type DeclaredType { get; }
- public string Name { get; set; }
-
- public BackingFieldInfo(Type declaringType, string name) : this()
- {
- DeclaredType = declaringType;
- Name = name;
- }
-
- public bool Equals(BackingFieldInfo other)
- {
- return other.DeclaredType == this.DeclaredType && other.Name == this.Name;
- }
-
- public override int GetHashCode()
- {
- return (17 * 23 + DeclaredType.GetHashCode()) * 23 + Name.GetHashCode();
- }
- }
}
}
\ No newline at end of file
diff --git a/src/CloneExtensions/Extensions/FieldInfoExtensions.cs b/src/CloneExtensions/Extensions/FieldInfoExtensions.cs
new file mode 100644
index 0000000..4169835
--- /dev/null
+++ b/src/CloneExtensions/Extensions/FieldInfoExtensions.cs
@@ -0,0 +1,19 @@
+using System.Reflection;
+
+namespace CloneExtensions.Extensions
+{
+ public static class FieldInfoExtensions
+ {
+ public static bool IsBackingField(
+ this FieldInfo source,
+ bool defaultValue = false)
+ {
+ if (source != null)
+ {
+ return source.Name.IndexOf(">k__BackingField", 0) >= 0;
+ }
+
+ return defaultValue;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/CloneExtensions/Extensions/IModelInfoExtensions.cs b/src/CloneExtensions/Extensions/IModelInfoExtensions.cs
new file mode 100644
index 0000000..28bcd5e
--- /dev/null
+++ b/src/CloneExtensions/Extensions/IModelInfoExtensions.cs
@@ -0,0 +1,21 @@
+using CloneExtensions.Interfaces;
+using System;
+using System.Collections.Generic;
+using System.Reflection;
+
+namespace CloneExtensions.Extensions
+{
+ public static class IModelInfoExtensions
+ {
+ public static IEnumerable GetCustomAttributes(
+ this IModelInfo source) where T : Attribute
+ {
+ if (source != null)
+ {
+ return source.MemberInfo.GetCustomAttributes();
+ }
+
+ return new List();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/CloneExtensions/Extensions/MethodInfoExtensions.cs b/src/CloneExtensions/Extensions/MethodInfoExtensions.cs
new file mode 100644
index 0000000..dbee5c7
--- /dev/null
+++ b/src/CloneExtensions/Extensions/MethodInfoExtensions.cs
@@ -0,0 +1,20 @@
+using System.Reflection;
+
+namespace CloneExtensions.Extensions
+{
+ public static class MethodInfoExtensions
+ {
+ public static bool HasAttribute(
+ this MethodInfo source,
+ MethodAttributes attr,
+ bool defaultValue = false)
+ {
+ if (source != null)
+ {
+ return (source.Attributes & attr) == attr;
+ }
+
+ return defaultValue;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/CloneExtensions/Extensions/PropertyInfoExtensions.cs b/src/CloneExtensions/Extensions/PropertyInfoExtensions.cs
new file mode 100644
index 0000000..260c899
--- /dev/null
+++ b/src/CloneExtensions/Extensions/PropertyInfoExtensions.cs
@@ -0,0 +1,118 @@
+using System.Linq;
+using System.Reflection;
+
+namespace CloneExtensions.Extensions
+{
+ public static class PropertyInfoExtensions
+ {
+ public static bool IsStatic(
+ this PropertyInfo source,
+ bool defaultValue = false)
+ {
+ if (source != null)
+ {
+ return
+ ((source.CanRead && source.GetMethod.IsStatic) ||
+ (source.CanWrite && source.SetMethod.IsStatic));
+ }
+
+ return defaultValue;
+ }
+
+ public static bool IsPublic(
+ this PropertyInfo source,
+ bool defaultValue = false)
+ {
+ if (source != null)
+ {
+ return
+ ((source.CanRead && source.GetMethod.IsPublic) ||
+ (source.CanWrite && source.SetMethod.IsPublic));
+ }
+
+ return defaultValue;
+ }
+
+ public static bool CanRead(
+ this PropertyInfo source,
+ bool defaultValue = false)
+ {
+ if (source != null)
+ {
+ return
+ source.CanRead &&
+ source.GetMethod != null;
+ }
+
+ return defaultValue;
+ }
+
+ public static bool CanWrite(
+ this PropertyInfo source,
+ bool defaultValue = false)
+ {
+ if (source != null)
+ {
+ return
+ source.CanWrite &&
+ source.SetMethod != null;
+ }
+
+ return defaultValue;
+ }
+
+ public static bool HasParameters(
+ this PropertyInfo source,
+ bool defaultValue = false)
+ {
+ if (source != null)
+ {
+ return source.GetIndexParameters().Any();
+ }
+
+ return defaultValue;
+ }
+
+ public static bool IsVitrual(
+ this PropertyInfo source,
+ bool defaultValue)
+ {
+ if (source != null)
+ {
+ return
+ (source.SetMethod != null && source.SetMethod.IsVirtual) ||
+ (source.GetMethod != null && source.GetMethod.IsVirtual);
+ }
+
+ return defaultValue;
+ }
+
+ public static bool IsAbstract(
+ this PropertyInfo source,
+ bool defaultValue)
+ {
+ if (source != null)
+ {
+ return
+ (source.SetMethod != null && source.SetMethod.IsAbstract) ||
+ (source.GetMethod != null && source.GetMethod.IsAbstract);
+ }
+
+ return defaultValue;
+ }
+
+ public static bool IsNew(
+ this PropertyInfo source,
+ bool defaultValue)
+ {
+ if (source != null)
+ {
+ return
+ (source.SetMethod != null && (source.SetMethod.HasAttribute(MethodAttributes.NewSlot))) ||
+ (source.GetMethod != null && (source.GetMethod.HasAttribute(MethodAttributes.NewSlot)));
+ }
+
+ return defaultValue;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/CloneExtensions/Interfaces/IModelInfo.cs b/src/CloneExtensions/Interfaces/IModelInfo.cs
new file mode 100644
index 0000000..2878155
--- /dev/null
+++ b/src/CloneExtensions/Interfaces/IModelInfo.cs
@@ -0,0 +1,35 @@
+using System;
+using System.Reflection;
+
+namespace CloneExtensions.Interfaces
+{
+ public interface IModelInfo
+ {
+ MemberInfo MemberInfo { get; }
+ string Name { get; }
+ Type Type { get; }
+ bool IsStatic { get; }
+ bool IsPublic { get; }
+ bool CanRead { get; }
+ bool CanWrite { get; }
+ int Depth { get; }
+ bool IsLiteral { get; }
+ }
+
+ public interface IFieldModelInfo : IModelInfo
+ {
+ FieldInfo FieldInfo { get; }
+ bool IsBackingField { get; }
+ }
+
+ public interface IPropertyModelInfo : IModelInfo
+ {
+ IFieldModelInfo BackingField { get; }
+ PropertyInfo PropertyInfo { get; }
+ bool HasParameters { get; }
+ bool HasBackingField { get; }
+ bool IsAbstract { get; }
+ bool IsVirtual { get; }
+ bool IsNew { get; }
+ }
+}
\ No newline at end of file
diff --git a/src/CloneExtensions/TypeExtensions.cs b/src/CloneExtensions/TypeExtensions.cs
index da60f26..e771c53 100644
--- a/src/CloneExtensions/TypeExtensions.cs
+++ b/src/CloneExtensions/TypeExtensions.cs
@@ -1,4 +1,8 @@
-using System;
+using CloneExtensions.Extensions;
+using CloneExtensions.Interfaces;
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
using System.Linq;
using System.Reflection;
@@ -95,5 +99,217 @@ public static bool IsInterface(this Type type)
return type.GetTypeInfo().IsInterface;
}
#endif
+
+ public static IReadOnlyList GetAllFields(this Type type)
+ {
+ return GetAllFieldsHelper(type, 0);
+ }
+
+ public static IReadOnlyList GetAllProperties(
+ this Type type,
+ IEnumerable fields = null)
+ {
+ var allFields = fields != null ?
+ fields :
+ type.GetAllFields();
+
+ return GetAllPropertiesHelper(allFields, type, 0);
+ }
+
+ public static IReadOnlyList GetFilteredProperties(
+ this Type type,
+ IEnumerable fields = null)
+ {
+ List properties = new List();
+ var allProperties = type.GetAllProperties(fields);
+
+ // If properties that share a name are marked as abstract
+ // or virtual, then only one of them is needed in order to
+ // set/get the value of the property.
+ allProperties
+ .Where(x => x.IsAbstract || x.IsVirtual)
+ .Select(x => x)
+ .GroupBy(x => x.Name)
+ .ToList()
+ .ForEach(x =>
+ {
+ // All Abstract properties are "new"
+ // All initial implemention of virtual properties
+ // are "new". Foreach "new" property
+ // find the property at the lowest depth.
+ foreach (var item in x.Where(y => y.IsNew))
+ {
+ var itemToAdd = item;
+
+ foreach (var p in x.Where(y => y.Depth < item.Depth).OrderByDescending(y => y.Depth))
+ {
+ if (!p.IsNew)
+ {
+ itemToAdd = p;
+ }
+ else
+ {
+ break;
+ }
+ }
+
+ properties.Add(itemToAdd);
+ }
+ });
+
+ // Add all non-virtual/non-abstract properties
+ properties.AddRange(allProperties.Where(x => !(x.IsAbstract || x.IsVirtual)).Select(x => x));
+
+ return properties;
+ }
+
+ #region Private Members
+ private static IReadOnlyList GetAllFieldsHelper(
+ Type type,
+ int depth = 0)
+ {
+ if (type == null)
+ {
+ return new List();
+ }
+
+ var fields = type
+ .GetTypeInfo()
+ .GetFields(
+ BindingFlags.Public |
+ BindingFlags.NonPublic |
+ BindingFlags.Static |
+ BindingFlags.Instance |
+ BindingFlags.DeclaredOnly)
+ .Select(x => FieldModelInfo.Create(x, depth))
+ .ToList();
+
+ fields.AddRange(GetAllFieldsHelper(type.GetTypeInfo().BaseType, ++depth));
+
+ return fields;
+ }
+
+ private static IReadOnlyList GetAllPropertiesHelper(
+ IEnumerable fields,
+ Type type,
+ int depth = 0)
+ {
+ if (type == null)
+ {
+ return new List();
+ }
+
+ var props = type
+ .GetTypeInfo()
+ .GetProperties(
+ BindingFlags.Public |
+ BindingFlags.NonPublic |
+ BindingFlags.Static |
+ BindingFlags.Instance |
+ BindingFlags.DeclaredOnly)
+ .Select(x => PropertyModelInfo.Create(fields, x, depth))
+ .ToList();
+
+ props.AddRange(GetAllPropertiesHelper(fields, type.GetTypeInfo().BaseType, ++depth));
+
+ return props;
+ }
+ #endregion
+
+ #region Helpers
+ [DebuggerDisplay("{FieldInfo.FieldType.Name} {FieldInfo.Name} {FieldInfo.DeclaringType.Name}")]
+ class FieldModelInfo : IFieldModelInfo
+ {
+ public static IFieldModelInfo Create(
+ FieldInfo info,
+ int depth)
+ {
+ return new FieldModelInfo()
+ {
+ Name = info.Name,
+ Type = info.FieldType,
+ FieldInfo = info,
+ MemberInfo = info,
+ IsStatic = info.IsStatic,
+ IsPublic = info.IsPublic,
+ CanRead = true,
+ CanWrite = !info.IsInitOnly && !info.IsLiteral,
+ IsBackingField = info.IsBackingField(false),
+ Depth = depth,
+ IsLiteral = info.IsLiteral
+ };
+ }
+
+ public string Name { get; private set; }
+ public Type Type { get; private set; }
+ public MemberInfo MemberInfo { get; private set; }
+ public FieldInfo FieldInfo { get; private set; }
+ public bool IsStatic { get; private set; }
+ public bool IsPublic { get; private set; }
+ public bool CanRead { get; private set; }
+ public bool CanWrite { get; private set; }
+ public bool IsBackingField { get; private set; }
+ public int Depth { get; private set; }
+ public bool IsLiteral { get; private set; }
+ }
+
+ [DebuggerDisplay("{PropertyInfo.PropertyType.Name} {PropertyInfo.Name} {PropertyInfo.DeclaringType.Name}")]
+ class PropertyModelInfo : IPropertyModelInfo
+ {
+ public static IPropertyModelInfo Create(
+ IEnumerable fields,
+ PropertyInfo info,
+ int depth)
+ {
+ if (fields == null) throw new ArgumentNullException(nameof(fields));
+ string key = string.Format("<{0}>k__BackingField", info.Name);
+
+ var backingField = fields
+ .Where(x =>
+ x.IsBackingField &&
+ string.Equals(x.FieldInfo.Name, key) &&
+ x.FieldInfo.DeclaringType == info.DeclaringType &&
+ x.Depth == depth)
+ .FirstOrDefault();
+
+ return new PropertyModelInfo()
+ {
+ Name = info.Name,
+ Type = info.PropertyType,
+ MemberInfo = info,
+ PropertyInfo = info,
+ BackingField = backingField,
+ IsStatic = info.IsStatic(false),
+ IsPublic = info.IsPublic(false),
+ CanRead = info.CanRead(false),
+ CanWrite = info.CanWrite(false),
+ HasParameters = info.HasParameters(false),
+ HasBackingField = backingField != null,
+ Depth = depth,
+ IsLiteral = false,
+ IsAbstract = info.IsAbstract(false),
+ IsVirtual = info.IsVitrual(false),
+ IsNew = info.IsNew(false)
+ };
+ }
+
+ public string Name { get; private set; }
+ public Type Type { get; private set; }
+ public MemberInfo MemberInfo { get; private set; }
+ public PropertyInfo PropertyInfo { get; private set; }
+ public IFieldModelInfo BackingField { get; private set; }
+ public bool IsStatic { get; private set; }
+ public bool IsPublic { get; private set; }
+ public bool CanRead { get; private set; }
+ public bool CanWrite { get; private set; }
+ public bool HasParameters { get; private set; }
+ public bool HasBackingField { get; private set; }
+ public int Depth { get; private set; }
+ public bool IsLiteral { get; private set; }
+ public bool IsAbstract { get; private set; }
+ public bool IsVirtual { get; private set; }
+ public bool IsNew { get; private set; }
+ }
+ #endregion
}
}