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 } }