From df16982697a18e29a34488b54e276240f3d1afa0 Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Thu, 3 Apr 2014 15:27:18 -0700 Subject: [PATCH] Adding and updating old propertyhelper code for dictionaries --- .../HtmlAttributePropertyHelper.cs | 40 +++ .../Properties/AssemblyInfo.cs | 3 + .../PropertyHelper.cs | 152 +++++++++++ .../TypeHelper.cs | 31 +++ .../HtmlAttributePropertyHelperTest.cs | 108 ++++++++ .../PropertyHelperTest.cs | 243 ++++++++++++++++++ .../TypeHelperTest.cs | 110 ++++++++ 7 files changed, 687 insertions(+) create mode 100644 src/Microsoft.AspNet.Mvc.Rendering/HtmlAttributePropertyHelper.cs create mode 100644 src/Microsoft.AspNet.Mvc.Rendering/PropertyHelper.cs create mode 100644 src/Microsoft.AspNet.Mvc.Rendering/TypeHelper.cs create mode 100644 test/Microsoft.AspNet.Mvc.Rendering.Test/HtmlAttributePropertyHelperTest.cs create mode 100644 test/Microsoft.AspNet.Mvc.Rendering.Test/PropertyHelperTest.cs create mode 100644 test/Microsoft.AspNet.Mvc.Rendering.Test/TypeHelperTest.cs diff --git a/src/Microsoft.AspNet.Mvc.Rendering/HtmlAttributePropertyHelper.cs b/src/Microsoft.AspNet.Mvc.Rendering/HtmlAttributePropertyHelper.cs new file mode 100644 index 0000000000..5a5350e6c2 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Rendering/HtmlAttributePropertyHelper.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Concurrent; +using System.Reflection; + +namespace Microsoft.AspNet.Mvc.Rendering +{ + internal class HtmlAttributePropertyHelper : PropertyHelper + { + private static readonly ConcurrentDictionary ReflectionCache = + new ConcurrentDictionary(); + + public static new PropertyHelper[] GetProperties(object instance) + { + return GetProperties(instance, CreateInstance, ReflectionCache); + } + + private static PropertyHelper CreateInstance(PropertyInfo property) + { + return new HtmlAttributePropertyHelper(property); + } + + public HtmlAttributePropertyHelper(PropertyInfo property) + : base(property) + { + } + + public override string Name + { + get + { + return base.Name; + } + + protected set + { + base.Name = value == null ? null : value.Replace('_', '-'); + } + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.Rendering/Properties/AssemblyInfo.cs b/src/Microsoft.AspNet.Mvc.Rendering/Properties/AssemblyInfo.cs index f3ed52df40..1ba3c72153 100644 --- a/src/Microsoft.AspNet.Mvc.Rendering/Properties/AssemblyInfo.cs +++ b/src/Microsoft.AspNet.Mvc.Rendering/Properties/AssemblyInfo.cs @@ -1,4 +1,5 @@ using System.Reflection; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; // General Information about an assembly is controlled through the following @@ -33,3 +34,5 @@ using System.Runtime.InteropServices; // [assembly: AssemblyVersion("1.0.*")] [assembly: AssemblyVersion("1.0.0.0")] [assembly: AssemblyFileVersion("1.0.0.0")] + +[assembly: InternalsVisibleTo("Microsoft.AspNet.Mvc.Rendering.Test")] diff --git a/src/Microsoft.AspNet.Mvc.Rendering/PropertyHelper.cs b/src/Microsoft.AspNet.Mvc.Rendering/PropertyHelper.cs new file mode 100644 index 0000000000..cfb6036154 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Rendering/PropertyHelper.cs @@ -0,0 +1,152 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics.Contracts; +using System.Linq; +using System.Reflection; + +namespace Microsoft.AspNet.Mvc.Rendering +{ + internal class PropertyHelper + { + // Delegate type for a by-ref property getter + private delegate TValue ByRefFunc(ref TDeclaringType arg); + + private static readonly MethodInfo CallPropertyGetterOpenGenericMethod = + typeof(PropertyHelper).GetTypeInfo().GetDeclaredMethod("CallPropertyGetter"); + + private static readonly MethodInfo CallPropertyGetterByReferenceOpenGenericMethod = + typeof(PropertyHelper).GetTypeInfo().GetDeclaredMethod("CallPropertyGetterByReference"); + + private static readonly ConcurrentDictionary ReflectionCache = + new ConcurrentDictionary(); + + private readonly Func _valueGetter; + + /// + /// Initializes a fast property helper. + /// + /// This constructor does not cache the helper. For caching, use GetProperties. + /// + public PropertyHelper(PropertyInfo property) + { + Contract.Assert(property != null); + + Name = property.Name; + _valueGetter = MakeFastPropertyGetter(property); + } + + public virtual string Name { get; protected set; } + + public object GetValue(object instance) + { + return _valueGetter(instance); + } + + /// + /// Creates and caches fast property helpers that expose getters for every public get property on the + /// underlying type. + /// + /// the instance to extract property accessors for. + /// a cached array of all public property getters from the underlying type of target instance. + public static PropertyHelper[] GetProperties(object instance) + { + return GetProperties(instance, CreateInstance, ReflectionCache); + } + + /// + /// Creates a single fast property getter. The result is not cached. + /// + /// propertyInfo to extract the getter for. + /// a fast getter. + /// + /// This method is more memory efficient than a dynamically compiled lambda, and about the + /// same speed. + /// + public static Func MakeFastPropertyGetter(PropertyInfo propertyInfo) + { + Contract.Assert(propertyInfo != null); + + var getMethod = propertyInfo.GetMethod; + Contract.Assert(getMethod != null); + Contract.Assert(!getMethod.IsStatic); + Contract.Assert(getMethod.GetParameters().Length == 0); + + // Instance methods in the CLR can be turned into static methods where the first parameter + // is open over "target". This parameter is always passed by reference, so we have a code + // path for value types and a code path for reference types. + var typeInput = getMethod.DeclaringType; + var typeOutput = getMethod.ReturnType; + + Delegate callPropertyGetterDelegate; + if (typeInput.IsValueType()) + { + // Create a delegate (ref TDeclaringType) -> TValue + var delegateType = typeof(ByRefFunc<,>).MakeGenericType(typeInput, typeOutput); + var propertyGetterAsFunc = getMethod.CreateDelegate(delegateType); + var callPropertyGetterClosedGenericMethod = + CallPropertyGetterByReferenceOpenGenericMethod.MakeGenericMethod(typeInput, typeOutput); + callPropertyGetterDelegate = + callPropertyGetterClosedGenericMethod.CreateDelegate(typeof(Func), propertyGetterAsFunc); + } + else + { + // Create a delegate TDeclaringType -> TValue + var propertyGetterAsFunc = getMethod.CreateDelegate(typeof(Func<,>).MakeGenericType(typeInput, typeOutput)); + var callPropertyGetterClosedGenericMethod = + CallPropertyGetterOpenGenericMethod.MakeGenericMethod(typeInput, typeOutput); + callPropertyGetterDelegate = + callPropertyGetterClosedGenericMethod.CreateDelegate(typeof(Func), propertyGetterAsFunc); + } + + return (Func)callPropertyGetterDelegate; + } + + private static PropertyHelper CreateInstance(PropertyInfo property) + { + return new PropertyHelper(property); + } + + // Called via reflection + private static object CallPropertyGetter(Func getter, object target) + { + return getter((TDeclaringType)target); + } + + // Called via reflection + private static object CallPropertyGetterByReference( + ByRefFunc getter, + object target) + { + var unboxed = (TDeclaringType)target; + return getter(ref unboxed); + } + + protected static PropertyHelper[] GetProperties( + object instance, + Func createPropertyHelper, + ConcurrentDictionary cache) + { + // Using an array rather than IEnumerable, as target will be called on the hot path numerous times. + PropertyHelper[] helpers; + + var type = instance.GetType(); + + if (!cache.TryGetValue(type, out helpers)) + { + // We avoid loading indexed properties using the where statement. + // Indexed properties are not useful (or valid) for grabbing properties off an anonymous object. + var properties = type.GetRuntimeProperties().Where( + prop => prop.GetIndexParameters().Length == 0 && + prop.GetMethod != null && + prop.GetMethod.IsPublic && + !prop.GetMethod.IsStatic); + + helpers = properties.Select(p => createPropertyHelper(p)).ToArray(); + cache.TryAdd(type, helpers); + } + + return helpers; + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.Rendering/TypeHelper.cs b/src/Microsoft.AspNet.Mvc.Rendering/TypeHelper.cs new file mode 100644 index 0000000000..31aaaeeb0e --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Rendering/TypeHelper.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; + +namespace Microsoft.AspNet.Mvc.Rendering +{ + internal static class TypeHelper + { + /// + /// Given an object, adds each instance property with a public get method as a key and its + /// associated value to a dictionary. + /// + // + // The implementation of PropertyHelper will cache the property accessors per-type. This is + // faster when the the same type is used multiple times with ObjectToDictionary. + public static IDictionary ObjectToDictionary(object value) + { + var dictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); + + if (value != null) + { + foreach (var helper in PropertyHelper.GetProperties(value)) + { + dictionary.Add(helper.Name, helper.GetValue(value)); + } + } + + return dictionary; + } + + } +} diff --git a/test/Microsoft.AspNet.Mvc.Rendering.Test/HtmlAttributePropertyHelperTest.cs b/test/Microsoft.AspNet.Mvc.Rendering.Test/HtmlAttributePropertyHelperTest.cs new file mode 100644 index 0000000000..4ab92175ec --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Rendering.Test/HtmlAttributePropertyHelperTest.cs @@ -0,0 +1,108 @@ +using System.Linq; +using Xunit; + +namespace Microsoft.AspNet.Mvc.Rendering +{ + public class HtmlAttributePropertyHelperTest + { + [Fact] + public void HtmlAttributePropertyHelper_RenamesPropertyNames() + { + // Arrange + var anonymous = new { bar_baz = "foo" }; + var property = anonymous.GetType().GetProperties().First(); + + // Act + var helper = new HtmlAttributePropertyHelper(property); + + // Assert + Assert.Equal("bar_baz", property.Name); + Assert.Equal("bar-baz", helper.Name); + } + + [Fact] + public void HtmlAttributePropertyHelper_ReturnsNameCorrectly() + { + // Arrange + var anonymous = new { foo = "bar" }; + var property = anonymous.GetType().GetProperties().First(); + + // Act + var helper = new HtmlAttributePropertyHelper(property); + + // Assert + Assert.Equal("foo", property.Name); + Assert.Equal("foo", helper.Name); + } + + [Fact] + public void HtmlAttributePropertyHelper_ReturnsValueCorrectly() + { + // Arrange + var anonymous = new { bar = "baz" }; + var property = anonymous.GetType().GetProperties().First(); + + // Act + var helper = new HtmlAttributePropertyHelper(property); + + // Assert + Assert.Equal("bar", helper.Name); + Assert.Equal("baz", helper.GetValue(anonymous)); + } + + [Fact] + public void HtmlAttributePropertyHelper_ReturnsValueCorrectly_ForValueTypes() + { + // Arrange + var anonymous = new { foo = 32 }; + var property = anonymous.GetType().GetProperties().First(); + + // Act + var helper = new HtmlAttributePropertyHelper(property); + + // Assert + Assert.Equal("foo", helper.Name); + Assert.Equal(32, helper.GetValue(anonymous)); + } + + [Fact] + public void HtmlAttributePropertyHelper_ReturnsCachedPropertyHelper() + { + // Arrange + var anonymous = new { foo = "bar" }; + + // Act + var helpers1 = HtmlAttributePropertyHelper.GetProperties(anonymous); + var helpers2 = HtmlAttributePropertyHelper.GetProperties(anonymous); + + // Assert + Assert.Equal(1, helpers1.Length); + Assert.Same(helpers1, helpers2); + Assert.Same(helpers1[0], helpers2[0]); + } + + [Fact] + public void HtmlAttributePropertyHelper_DoesNotShareCacheWithPropertyHelper() + { + // Arrange + var anonymous = new { bar_baz1 = "foo" }; + + // Act + var helpers1 = HtmlAttributePropertyHelper.GetProperties(anonymous); + var helpers2 = PropertyHelper.GetProperties(anonymous); + + // Assert + Assert.Equal(1, helpers1.Length); + Assert.Equal(1, helpers2.Length); + + Assert.NotEqual(helpers1, helpers2); + Assert.NotEqual(helpers1[0], helpers2[0]); + + Assert.IsType(helpers1[0]); + Assert.IsNotType(helpers2[0]); + + Assert.Equal("bar-baz1", helpers1[0].Name); + Assert.Equal("bar_baz1", helpers2[0].Name); + } + } +} diff --git a/test/Microsoft.AspNet.Mvc.Rendering.Test/PropertyHelperTest.cs b/test/Microsoft.AspNet.Mvc.Rendering.Test/PropertyHelperTest.cs new file mode 100644 index 0000000000..2d561c276c --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Rendering.Test/PropertyHelperTest.cs @@ -0,0 +1,243 @@ +using System.Linq; +using System.Reflection; +using Xunit; + +namespace Microsoft.AspNet.Mvc.Rendering +{ + public class PropertyHelperTest + { + [Fact] + public void PropertyHelper_ReturnsNameCorrectly() + { + // Arrange + var anonymous = new { foo = "bar" }; + PropertyInfo property = anonymous.GetType().GetProperties().First(); + + // Act + PropertyHelper helper = new PropertyHelper(property); + + // Assert + Assert.Equal("foo", property.Name); + Assert.Equal("foo", helper.Name); + } + + [Fact] + public void PropertyHelper_ReturnsValueCorrectly() + { + // Arrange + var anonymous = new { bar = "baz" }; + PropertyInfo property = anonymous.GetType().GetProperties().First(); + + // Act + PropertyHelper helper = new PropertyHelper(property); + + // Assert + Assert.Equal("bar", helper.Name); + Assert.Equal("baz", helper.GetValue(anonymous)); + } + + [Fact] + public void PropertyHelper_ReturnsValueCorrectly_ForValueTypes() + { + // Arrange + var anonymous = new { foo = 32 }; + var property = anonymous.GetType().GetProperties().First(); + + // Act + var helper = new PropertyHelper(property); + + // Assert + Assert.Equal("foo", helper.Name); + Assert.Equal(32, helper.GetValue(anonymous)); + } + + [Fact] + public void PropertyHelper_ReturnsCachedPropertyHelper() + { + // Arrange + var anonymous = new { foo = "bar" }; + + // Act + var helpers1 = PropertyHelper.GetProperties(anonymous); + var helpers2 = PropertyHelper.GetProperties(anonymous); + + // Assert + Assert.Equal(1, helpers1.Length); + Assert.Same(helpers1, helpers2); + Assert.Same(helpers1[0], helpers2[0]); + } + + [Fact] + public void PropertyHelper_DoesNotChangeUnderscores() + { + // Arrange + var anonymous = new { bar_baz2 = "foo" }; + + // Act + Assert + var helper = Assert.Single(PropertyHelper.GetProperties(anonymous)); + Assert.Equal("bar_baz2", helper.Name); + } + + [Fact] + public void PropertyHelper_DoesNotFindPrivateProperties() + { + // Arrange + var anonymous = new PrivateProperties(); + + // Act + Assert + var helper = Assert.Single(PropertyHelper.GetProperties(anonymous)); + Assert.Equal("Prop1", helper.Name); + } + + [Fact] + public void PropertyHelper_DoesNotFindStaticProperties() + { + // Arrange + var anonymous = new Static(); + + // Act + Assert + var helper = Assert.Single(PropertyHelper.GetProperties(anonymous)); + Assert.Equal("Prop5", helper.Name); + } + + [Fact] + public void PropertyHelper_DoesNotFindSetOnlyProperties() + { + // Arrange + var anonymous = new SetOnly(); + + // Act + Assert + var helper = Assert.Single(PropertyHelper.GetProperties(anonymous)); + Assert.Equal("Prop6", helper.Name); + } + + [Fact] + public void PropertyHelper_WorksForStruct() + { + // Arrange + var anonymous = new MyProperties(); + + anonymous.IntProp = 3; + anonymous.StringProp = "Five"; + + // Act + Assert + var helper1 = Assert.Single(PropertyHelper.GetProperties(anonymous).Where(prop => prop.Name == "IntProp")); + var helper2 = Assert.Single(PropertyHelper.GetProperties(anonymous).Where(prop => prop.Name == "StringProp")); + Assert.Equal(3, helper1.GetValue(anonymous)); + Assert.Equal("Five", helper2.GetValue(anonymous)); + } + + [Fact] + public void PropertyHelper_ForDerivedClass() + { + // Arrange + var derived = new DerivedClass { PropA = "propAValue", PropB = "propBValue" }; + + // Act + var helpers = PropertyHelper.GetProperties(derived).ToArray(); + + // Assert + Assert.NotNull(helpers); + Assert.Equal(2, helpers.Length); + + var propAHelper = Assert.Single(helpers.Where(h => h.Name == "PropA")); + var propBHelper = Assert.Single(helpers.Where(h => h.Name == "PropB")); + + Assert.Equal("propAValue", propAHelper.GetValue(derived)); + Assert.Equal("propBValue", propBHelper.GetValue(derived)); + } + + [Fact] + public void PropertyHelper_ForDerivedClass_WithNew() + { + // Arrange + var derived = new DerivedClassWithNew { PropA = "propAValue" }; + + // Act + var helpers = PropertyHelper.GetProperties(derived).ToArray(); + + // Assert + Assert.NotNull(helpers); + Assert.Equal(2, helpers.Length); + + var propAHelper = Assert.Single(helpers.Where(h => h.Name == "PropA")); + var propBHelper = Assert.Single(helpers.Where(h => h.Name == "PropB")); + + Assert.Equal("propAValue", propAHelper.GetValue(derived)); + Assert.Equal("Newed", propBHelper.GetValue(derived)); + } + + [Fact] + public void PropertyHelper_ForDerived_WithVirtual() + { + // Arrange + var derived = new DerivedClassWithOverride { PropA = "propAValue", PropB = "propBValue" }; + + // Act + var helpers = PropertyHelper.GetProperties(derived).ToArray(); + + // Assert + Assert.NotNull(helpers); + Assert.Equal(2, helpers.Length); + + var propAHelper = Assert.Single(helpers.Where(h => h.Name == "PropA")); + var propBHelper = Assert.Single(helpers.Where(h => h.Name == "PropB")); + + Assert.Equal("Overriden", propAHelper.GetValue(derived)); + Assert.Equal("propBValue", propBHelper.GetValue(derived)); + } + + private class Static + { + public static int Prop2 { get; set; } + public int Prop5 { get; set; } + } + + private struct MyProperties + { + public int IntProp { get; set; } + public string StringProp { get; set; } + } + + private class SetOnly + { + public int Prop2 { set { } } + public int Prop6 { get; set; } + } + + private class PrivateProperties + { + public int Prop1 { get; set; } + protected int Prop2 { get; set; } + private int Prop3 { get; set; } + } + + public class BaseClass + { + public string PropA { get; set; } + + protected string PropProtected { get; set; } + } + + public class DerivedClass : BaseClass + { + public string PropB { get; set; } + } + + public class BaseClassWithVirtual + { + public virtual string PropA { get; set; } + public string PropB { get; set; } + } + + public class DerivedClassWithNew : BaseClassWithVirtual + { + public new string PropB { get { return "Newed"; } } + } + + public class DerivedClassWithOverride : BaseClassWithVirtual + { + public override string PropA { get { return "Overriden"; } } + } + } +} diff --git a/test/Microsoft.AspNet.Mvc.Rendering.Test/TypeHelperTest.cs b/test/Microsoft.AspNet.Mvc.Rendering.Test/TypeHelperTest.cs new file mode 100644 index 0000000000..fbdecdc410 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Rendering.Test/TypeHelperTest.cs @@ -0,0 +1,110 @@ +using Xunit; + +namespace Microsoft.AspNet.Mvc.Rendering +{ + public class TypeHelperTest + { + [Fact] + public void ObjectToDictionary_WithNullObject_ReturnsEmptyDictionary() + { + // Arrange + object value = null; + + // Act + var dictValues = TypeHelper.ObjectToDictionary(value); + + // Assert + Assert.NotNull(dictValues); + Assert.Equal(0, dictValues.Count); + } + + [Fact] + public void ObjectToDictionary_WithPlainObjectType_ReturnsEmptyDictionary() + { + // Arrange + var value = new object(); + + // Act + var dictValues = TypeHelper.ObjectToDictionary(value); + + // Assert + Assert.NotNull(dictValues); + Assert.Equal(0, dictValues.Count); + } + + [Fact] + public void ObjectToDictionary_WithPrimitiveType_LooksUpPublicProperties() + { + // Arrange + var value = "test"; + + // Act + var dictValues = TypeHelper.ObjectToDictionary(value); + + // Assert + Assert.NotNull(dictValues); + Assert.Equal(1, dictValues.Count); + Assert.Equal(4, dictValues["Length"]); + } + + [Fact] + public void ObjectToDictionary_WithAnonymousType_LooksUpProperties() + { + // Arrange + var value = new { test = "value", other = 1 }; + + // Act + var dictValues = TypeHelper.ObjectToDictionary(value); + + // Assert + Assert.NotNull(dictValues); + Assert.Equal(2, dictValues.Count); + Assert.Equal("value", dictValues["test"]); + Assert.Equal(1, dictValues["other"]); + } + + [Fact] + public void ObjectToDictionary_ReturnsCaseInsensitiveDictionary() + { + // Arrange + var value = new { TEST = "value", oThEr = 1 }; + + // Act + var dictValues = TypeHelper.ObjectToDictionary(value); + + // Assert + Assert.NotNull(dictValues); + Assert.Equal(2, dictValues.Count); + Assert.Equal("value", dictValues["test"]); + Assert.Equal(1, dictValues["other"]); + } + + [Fact] + public void ObjectToDictionary_ReturnsInheritedProperties() + { + // Arrange + var value = new ThreeDPoint() {X = 5, Y = 10, Z = 17}; + + // Act + var dictValues = TypeHelper.ObjectToDictionary(value); + + // Assert + Assert.NotNull(dictValues); + Assert.Equal(3, dictValues.Count); + Assert.Equal(5, dictValues["X"]); + Assert.Equal(10, dictValues["Y"]); + Assert.Equal(17, dictValues["Z"]); + } + + private class Point + { + public int X { get; set; } + public int Y { get; set; } + } + + private class ThreeDPoint : Point + { + public int Z { get; set; } + } + } +}