Adding and updating old propertyhelper code for dictionaries

This commit is contained in:
Ryan Nowak 2014-04-03 15:27:18 -07:00
parent ec4b3a29c0
commit df16982697
7 changed files with 687 additions and 0 deletions

View File

@ -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<Type, PropertyHelper[]> ReflectionCache =
new ConcurrentDictionary<Type, PropertyHelper[]>();
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('_', '-');
}
}
}
}

View File

@ -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")]

View File

@ -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<TDeclaringType, TValue>(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<Type, PropertyHelper[]> ReflectionCache =
new ConcurrentDictionary<Type, PropertyHelper[]>();
private readonly Func<object, object> _valueGetter;
/// <summary>
/// Initializes a fast property helper.
///
/// This constructor does not cache the helper. For caching, use GetProperties.
/// </summary>
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);
}
/// <summary>
/// Creates and caches fast property helpers that expose getters for every public get property on the
/// underlying type.
/// </summary>
/// <param name="instance">the instance to extract property accessors for.</param>
/// <returns>a cached array of all public property getters from the underlying type of target instance.</returns>
public static PropertyHelper[] GetProperties(object instance)
{
return GetProperties(instance, CreateInstance, ReflectionCache);
}
/// <summary>
/// Creates a single fast property getter. The result is not cached.
/// </summary>
/// <param name="propertyInfo">propertyInfo to extract the getter for.</param>
/// <returns>a fast getter.</returns>
/// <remarks>
/// This method is more memory efficient than a dynamically compiled lambda, and about the
/// same speed.
/// </remarks>
public static Func<object, object> 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<object, object>), 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<object, object>), propertyGetterAsFunc);
}
return (Func<object, object>)callPropertyGetterDelegate;
}
private static PropertyHelper CreateInstance(PropertyInfo property)
{
return new PropertyHelper(property);
}
// Called via reflection
private static object CallPropertyGetter<TDeclaringType, TValue>(Func<TDeclaringType, TValue> getter, object target)
{
return getter((TDeclaringType)target);
}
// Called via reflection
private static object CallPropertyGetterByReference<TDeclaringType, TValue>(
ByRefFunc<TDeclaringType, TValue> getter,
object target)
{
var unboxed = (TDeclaringType)target;
return getter(ref unboxed);
}
protected static PropertyHelper[] GetProperties(
object instance,
Func<PropertyInfo, PropertyHelper> createPropertyHelper,
ConcurrentDictionary<Type, PropertyHelper[]> 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;
}
}
}

View File

@ -0,0 +1,31 @@
using System;
using System.Collections.Generic;
namespace Microsoft.AspNet.Mvc.Rendering
{
internal static class TypeHelper
{
/// <summary>
/// Given an object, adds each instance property with a public get method as a key and its
/// associated value to a dictionary.
/// </summary>
//
// 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<string, object> ObjectToDictionary(object value)
{
var dictionary = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
if (value != null)
{
foreach (var helper in PropertyHelper.GetProperties(value))
{
dictionary.Add(helper.Name, helper.GetValue(value));
}
}
return dictionary;
}
}
}

View File

@ -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<PropertyHelper[]>(helpers1, helpers2);
Assert.NotEqual<PropertyHelper>(helpers1[0], helpers2[0]);
Assert.IsType<HtmlAttributePropertyHelper>(helpers1[0]);
Assert.IsNotType<HtmlAttributePropertyHelper>(helpers2[0]);
Assert.Equal("bar-baz1", helpers1[0].Name);
Assert.Equal("bar_baz1", helpers2[0].Name);
}
}
}

View File

@ -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"; } }
}
}
}

View File

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