diff --git a/src/CloneExtensions.UnitTests/ModelInfoTests.cs b/src/CloneExtensions.UnitTests/ModelInfoTests.cs new file mode 100644 index 0000000..38f4392 --- /dev/null +++ b/src/CloneExtensions.UnitTests/ModelInfoTests.cs @@ -0,0 +1,215 @@ +using Deipax.Core.Common; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.Linq; + +namespace CloneExtensions.UnitTests +{ + [TestClass] + public class ModelInfoTests + { + [TestMethod] + public void ModelInfoTests_FieldTest() + { + AssertFields(51, 30, 15, 12, 3, 51, 39); + AssertFields(34, 20, 10, 8, 2, 34, 26); + AssertFields(17, 10, 5, 4, 1, 17, 13); + AssertFields(0, 0, 0, 0, 0, 0, 0); + } + + [TestMethod] + public void ModelInfoTests_PropertyTest() + { + AssertProperties(30, 18, 30, 6, 30, 24); + AssertProperties(20, 12, 20, 4, 20, 16); + AssertProperties(10, 6, 10, 2, 10, 8); + AssertProperties(2, 2, 0, 0, 2, 1); + } + + [TestMethod] + public void ModelInfoTests_CollectionEquivalence() + { + var m = ModelInfo.Create(typeof(GrandChildClass)); + Assert.IsTrue(ModelInfo.Fields == m.Fields); + Assert.IsTrue(ModelInfo.Properties == m.Properties); + } + + #region Private Members + private static void AssertFields( + int fieldCount, + int backingFieldCount, + int publicFieldCount, + int staticFieldCount, + int literalFieldCount, + int canReadFieldCount, + int canWriteFieldCount) + { + var fields1 = ModelInfo.Fields; + var fields2 = ModelInfo.Fields; + + var backingFields = fields1 + .Where(x => x.IsBackingField) + .ToList(); + + var publicFields = fields1 + .Where(x => x.IsPublic) + .ToList(); + + var staticFields = fields1 + .Where(x => x.IsStatic) + .ToList(); + + var literalFields = fields1 + .Where(x => x.IsLiteral) + .ToList(); + + var canReadFields = fields1 + .Where(x => x.CanRead) + .ToList(); + + var canWriteFields = fields1 + .Where(x => x.CanWrite) + .ToList(); + + Assert.IsTrue(fields1 == fields2); + Assert.IsTrue(fields1.Count() == fieldCount); + Assert.IsTrue(backingFields.Count == backingFieldCount); + Assert.IsTrue(publicFields.Count == publicFieldCount); + Assert.IsTrue(staticFields.Count == staticFieldCount); + Assert.IsTrue(literalFields.Count == literalFieldCount); + Assert.IsTrue(canReadFields.Count == canReadFieldCount); + Assert.IsTrue(canWriteFields.Count == canWriteFieldCount); + } + + private static void AssertProperties( + int propCount, + int publicPropCount, + int backingFieldCount, + int staticPropCount, + int canReadPropCount, + int canWritePropCount) + { + var props1 = ModelInfo.Properties; + var props2 = ModelInfo.Properties; + + var publicProps = props1 + .Where(x => x.IsPublic) + .ToList(); + + var backingFields = props1 + .Where(x => x.HasBackingField) + .ToList(); + + var staticProps = props1 + .Where(x => x.IsStatic) + .ToList(); + + var canReadProps = props1 + .Where(x => x.CanRead) + .ToList(); + + var canWriteProps = props1 + .Where(x => x.CanWrite) + .ToList(); + + var hasParameters = props1 + .Where(x => x.HasParameters) + .ToList(); + + Assert.IsTrue(props1 == props2); + Assert.IsTrue(props1.Count() == propCount); + Assert.IsTrue(publicProps.Count == publicPropCount); + Assert.IsTrue(backingFields.Count == backingFieldCount); + Assert.IsTrue(staticProps.Count == staticPropCount); + Assert.IsTrue(canReadProps.Count == canReadPropCount); + Assert.IsTrue(canWriteProps.Count == canWritePropCount); + Assert.IsTrue(hasParameters.Count == 0); + } + #endregion + + #region Helpers + class GrandChildClass : ChildAbstractClass + { + public new string PublicFieldCommonName; + private string PrivateFieldCommonName; + + private string PrivatePropCommonName { get; set; } + public new string PublicPropCommonName { get; set; } + + public string GrandChild_PublicField; + private string GrandChild_PrivateField; + public readonly string GrandChild_ReadOnlyField; + public static string GrandChild_StaticField; + public const string GrandChild_ConstField = ""; + + public static string GrandChild_Public_Static_GetSet_AutoProp { get; set; } + private static string GrandChild_Private_Static_GetSet_AutoProp { get; set; } + + public string GrandChild_Public_GetSet_AutoProp { get; set; } + public string GrandChild_Public_GetPSet_AutoProp { get; private set; } + public string GrandChild_Public_PGetSet_AutoProp { private get; set; } + public string GrandChild_Public_Get_AutoProp { get; } + + private string GrandChild_Private_GetSet_AutoProp { get; set; } + private string GrandChild_Private_Get_AutoProp { get; } + } + + abstract class ChildAbstractClass : ParentAbstractClass + { + public new string PublicFieldCommonName; + private string PrivateFieldCommonName; + + private string PrivatePropCommonName { get; set; } + public new string PublicPropCommonName { get; set; } + + public string Child_PublicField; + private string Child_PrivateField; + public readonly string Child_ReadOnlyField; + public static string Child_StaticField; + public const string Child_ConstField = ""; + + public static string Child_Public_Static_GetSet_AutoProp { get; set; } + private static string Child_Private_Static_GetSet_AutoProp { get; set; } + + public string Child_Public_GetSet_AutoProp { get; set; } + public string Child_Public_GetPSet_AutoProp { get; private set; } + public string Child_Public_PGetSet_AutoProp { private get; set; } + public string Child_Public_Get_AutoProp { get; } + + private string Child_Private_GetSet_AutoProp { get; set; } + private string Child_Private_Get_AutoProp { get; } + } + + abstract class ParentAbstractClass : MyInterface + { + public string PublicFieldCommonName; + private string PrivateFieldCommonName; + + private string PrivatePropCommonName { get; set; } + public string PublicPropCommonName { get; set; } + + public string Parent_PublicField; + private string Parent_PrivateField; + public readonly string Parent_ReadOnlyField; + public static string Parent_StaticField; + public const string Parent_ConstField = ""; + + public static string Parent_Public_Static_GetSet_AutoProp { get; set; } + private static string Parent_Private_Static_GetSet_AutoProp { get; set; } + + public string Parent_Public_GetSet_AutoProp { get; set; } + public string Parent_Public_GetPSet_AutoProp { get; private set; } + public string Parent_Public_PGetSet_AutoProp { private get; set; } + public string Parent_Public_Get_AutoProp { get; } + + private string Parent_Private_GetSet_AutoProp { get; set; } + private string Parent_Private_Get_AutoProp { get; } + } + + interface MyInterface + { + string Parent_Public_GetSet_AutoProp { get; set; } + string Parent_Public_Get_AutoProp { get; } + } + #endregion + } +} diff --git a/src/CloneExtensions/Deipax/FieldInfoExtensions.cs b/src/CloneExtensions/Deipax/FieldInfoExtensions.cs new file mode 100644 index 0000000..2eeb8b7 --- /dev/null +++ b/src/CloneExtensions/Deipax/FieldInfoExtensions.cs @@ -0,0 +1,17 @@ +using System.Reflection; + +namespace Deipax.Core.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/Deipax/IModelInfo.cs b/src/CloneExtensions/Deipax/IModelInfo.cs new file mode 100644 index 0000000..1151fc2 --- /dev/null +++ b/src/CloneExtensions/Deipax/IModelInfo.cs @@ -0,0 +1,32 @@ +using System; +using System.Reflection; + +namespace Deipax.Core.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; } + } +} \ No newline at end of file diff --git a/src/CloneExtensions/Deipax/ModelInfo.cs b/src/CloneExtensions/Deipax/ModelInfo.cs new file mode 100644 index 0000000..4ccc622 --- /dev/null +++ b/src/CloneExtensions/Deipax/ModelInfo.cs @@ -0,0 +1,198 @@ +using CloneExtensions; +using Deipax.Core.Extensions; +using Deipax.Core.Interfaces; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Reflection; + +namespace Deipax.Core.Common +{ + public class ModelInfo + { + static ModelInfo() + { + _cache = new ConcurrentDictionary(); + } + + private ModelInfo(Type t) + { + this.Type = t; + this.Fields = GetAllFields(this.Type); + this.Properties = GetAllProperties(this, this.Type); + } + + private ModelInfo() + { + } + + #region Field Members + private static ConcurrentDictionary _cache; + #endregion + + #region Public Members + public static ModelInfo Create(Type t) + { + if (t != null) + { + return _cache.GetOrAdd(t, (x) => + { + return new ModelInfo(x); + }); + } + + return null; + } + + public Type Type { get; private set; } + public IReadOnlyList Fields { get; private set; } + public IReadOnlyList Properties { get; private set; } + #endregion + + #region Private Members + private IFieldModelInfo GetBackingField(PropertyInfo info, int depth) + { + string key = string.Format("<{0}>k__BackingField", info.Name); + + return this.Fields + .Where(x => + x.IsBackingField && + string.Equals(x.FieldInfo.Name, key) && + x.FieldInfo.DeclaringType == info.DeclaringType && + x.Depth == depth) + .FirstOrDefault(); + } + + private static IReadOnlyList GetAllFields(Type t, int depth = 0) + { + if (t == null) + { + return new List(); + } + + var fields = t + .GetTypeInfo() + .GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance) + .Where(x => x.DeclaringType == t) + .Select(x => FieldModelInfo.Create(x, depth)) + .ToList(); + + fields.AddRange(GetAllFields(t.BaseType(), ++depth)); + + return fields; + } + + private static IReadOnlyList GetAllProperties(ModelInfo m, Type t, int depth = 0) + { + if (t == null) + { + return new List(); + } + + var props = t + .GetTypeInfo() + .GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance) + .Where(x => x.DeclaringType == t) + .Select(x => PropertyModelInfo.Create(m, x, depth)) + .ToList(); + + props.AddRange(GetAllProperties(m, t.BaseType(), ++depth)); + + return props; + } + #endregion + + #region Helpers + [DebuggerDisplay("{Name} - {IsStatic} - {IsPublic} - {CanRead} - {CanWrite} - {IsBackingField} - {Depth}")] + 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("{Name} - {IsStatic} - {IsPublic} - {CanRead} - {CanWrite} - {HasBackingField} - {Depth}")] + class PropertyModelInfo : IPropertyModelInfo + { + public static IPropertyModelInfo Create(ModelInfo m, PropertyInfo info, int depth) + { + var backingField = m.GetBackingField(info, depth); + + 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 + }; + } + + 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; } + } + #endregion + } + + public static class ModelInfo + { + static ModelInfo() + { + var modelInfo = ModelInfo.Create(typeof(T)); + Fields = modelInfo.Fields; + Properties = modelInfo.Properties; + } + + #region Public Members + public static IReadOnlyList Fields { get; private set; } + public static IReadOnlyList Properties { get; private set; } + #endregion + } +} \ No newline at end of file diff --git a/src/CloneExtensions/Deipax/PropertyInfoExtensions.cs b/src/CloneExtensions/Deipax/PropertyInfoExtensions.cs new file mode 100644 index 0000000..f6f768d --- /dev/null +++ b/src/CloneExtensions/Deipax/PropertyInfoExtensions.cs @@ -0,0 +1,66 @@ +using System.Linq; +using System.Reflection; + +namespace Deipax.Core.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; + } + } +} \ No newline at end of file diff --git a/src/CloneExtensions/ExpressionFactories/ComplexTypeExpressionFactory.cs b/src/CloneExtensions/ExpressionFactories/ComplexTypeExpressionFactory.cs index 8c1686c..0204c81 100644 --- a/src/CloneExtensions/ExpressionFactories/ComplexTypeExpressionFactory.cs +++ b/src/CloneExtensions/ExpressionFactories/ComplexTypeExpressionFactory.cs @@ -1,4 +1,5 @@ -using System; +using Deipax.Core.Common; +using System; using System.Collections; using System.Collections.Generic; using System.Linq; @@ -81,23 +82,41 @@ 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); + var fields = ModelInfo + .Fields + .Where(x => + x.CanRead && + x.CanWrite && + x.IsPublic && + !x.IsStatic && + !x.IsBackingField && + x.MemberInfo.GetCustomAttributes(typeof(NonClonedAttribute), true).Count() == 0) + .Select(x => new Member(x.MemberInfo, x.Type)) + .ToList(); return GetMembersCloneExpression(fields.ToArray(), getItemCloneExpression); } private Expression GetPropertiesCloneExpression(Func getItemCloneExpression) { - // get all public properties with public setter and getter, which are not indexed properties - var properties = from p in _type.GetTypeInfo().GetProperties(BindingFlags.Public | BindingFlags.Instance) - 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 new Member(p, p.PropertyType); + var properties = ModelInfo + .Properties + .Where(x => + x.CanRead && + x.CanWrite && + x.IsPublic && + !x.IsStatic && + !x.HasParameters && + x.MemberInfo.GetCustomAttributes(typeof(NonClonedAttribute), true).Count() == 0) + .Select(x => new + { + Type = x.Type, + MemberInfo = x.HasBackingField ? + (MemberInfo)x.BackingField.FieldInfo : + (MemberInfo)x.PropertyInfo + }) + .Select(x => new Member(x.MemberInfo, x.Type)) + .ToList(); return GetMembersCloneExpression(properties.ToArray(), getItemCloneExpression); } diff --git a/src/CloneExtensions/TypeExtensions.cs b/src/CloneExtensions/TypeExtensions.cs index da60f26..8ef432f 100644 --- a/src/CloneExtensions/TypeExtensions.cs +++ b/src/CloneExtensions/TypeExtensions.cs @@ -16,6 +16,11 @@ public static bool UsePrimitive(this Type type) return type.IsPrimitiveOrKnownImmutable() || typeof(Delegate).IsAssignableFrom(type); } + public static Type BaseType(this Type type) + { + return type.GetTypeInfo().BaseType; + } + #if NET40 || NET45 || NET461 public static bool IsAbstract(this Type type) {