patterncsharpMinor
Accessing properties by name with compile-time typesafety
Viewed 0 times
compilepropertieswithtimetypesafetynameaccessing
Problem
I've recently answered a question here, where the goal is to mimic some functions from
Currently available features are :
The OP's code is really good but I've decided to add a compile time type-safety to avoid misspelling a variable names.
It works with a
It uses both a private method which is not compile-time typesafe and is used only internally and a public one which is compile-time typesafe and this is the method that you can call outside of the class, so the user won't be able to mess up his naming. But inside the class, we can't know what the type argument
```
public class TypeAccessor
{
private readonly Func m_applyDefaultValues;
private readonly Func m_constructType;
public ReadOnlyCollection CloneableProperties { get; }
public ReadOnlyDictionary> GetterCache { get; }
public ReadOnlyDictionary> SetterCache { get; }
public TypeAccessor(T defaultValue, bool includeNonPublic = false)
{
PropertyInfo[] properties = typeof(T).GetProperties(BindingFlags.Instance |
(includeNonPublic
? BindingFlags.NonPublic
: BindingFlags.Default) |
System.Reflection in order to avoid it's direct usage as much as possible, because it works slow.Currently available features are :
- Cloning properties from one instance to another.
- Setting properties of instances.
- Getting property values from instances.
- Set instance properties to default values, where the values are determined at the creation of the
TypeAccessorclass.
- Creating new instances of the type argument
T, using default constructor, again this is specified at the instantiation of theTypeAccessorclass
The OP's code is really good but I've decided to add a compile time type-safety to avoid misspelling a variable names.
It works with a
GetterCache and a SetterCache which are separated for faster lookup.It uses both a private method which is not compile-time typesafe and is used only internally and a public one which is compile-time typesafe and this is the method that you can call outside of the class, so the user won't be able to mess up his naming. But inside the class, we can't know what the type argument
T is, so we still need to use some methods which work with string as parameters instead of Expression.```
public class TypeAccessor
{
private readonly Func m_applyDefaultValues;
private readonly Func m_constructType;
public ReadOnlyCollection CloneableProperties { get; }
public ReadOnlyDictionary> GetterCache { get; }
public ReadOnlyDictionary> SetterCache { get; }
public TypeAccessor(T defaultValue, bool includeNonPublic = false)
{
PropertyInfo[] properties = typeof(T).GetProperties(BindingFlags.Instance |
(includeNonPublic
? BindingFlags.NonPublic
: BindingFlags.Default) |
Solution
Expressions & Factories
You call here the delegate via an expression (the setter does the same). This is like you've just called the delegate.
Use can use the
Less nesting is usually better so if there is no getter or it's an indexer you can return right away.
The other method for creating setteers can be made pretty in a similar manner:
We still need expressions for indexed properties. Here's an example for a property with a single index. I think more then two are not necessary.
The class that contains the above methods is
Constructors
This is tricky. The
In such cases the tester-doer pattern is recommendable. To implement it you'd need to add a property like
There is one more issue in this method. The usage of this is invalid:
FormatterServices.GetUninitializedObject Method
Because the new instance of the object is initialized to zero and no constructors are run, the object might not represent a state that is regarded as valid by that object. The current method should only be used for deserialization when the user intends to immediately populate all fields. It does not create an uninitialized string, since creating an empty instance of an immutable type serves no purpose.
Returning this from the
Creating TypeAccessor
```
PropertyInfo[] properties = typeof(T).GetProperties(BindingFlags.Instance |
(includeNonPublic
var getMethod = propertyInfo.GetGetMethod(includeNonPublic);
if (getMethod != null && propertyInfo.GetIndexParameters().Length == 0)
{
var instance = Expression.Parameter(typeof(TSource), "instance");
var value = Expression.Call(instance, getMethod);
return Expression.Lambda>(
propertyInfo.PropertyType.IsValueType
? Expression.Convert(value, typeof(TProperty))
: Expression.TypeAs(value, typeof(TProperty)),
instance
);
}You call here the delegate via an expression (the setter does the same). This is like you've just called the delegate.
Use can use the
Property expression to call the property directly:public static Expression> CreateGetterExpression(
this PropertyInfo propertyInfo,
bool nonPublic = false)
{
var hasGetter = propertyInfo.GetGetMethod(nonPublic) != null;
if (!hasGetter || propertyInfo.GetIndexParameters().Any())
{
return null;
}
var obj = Expression.Parameter(typeof(T), "obj");
var property = Expression.Property(obj, propertyInfo);
return Expression.Lambda>(property, obj);
}Less nesting is usually better so if there is no getter or it's an indexer you can return right away.
The other method for creating setteers can be made pretty in a similar manner:
public static Expression> CreateSetterExpression(
this PropertyInfo propertyInfo,
bool nonPublic = false)
{
var hasSetter = propertyInfo.GetSetMethod() != null;
if (!hasSetter || propertyInfo.GetIndexParameters().Any())
{
return null;
}
var obj = Expression.Parameter(typeof(T), "obj");
var value = Expression.Parameter(typeof(TProperty), "value");
var property = Expression.Property(obj, propertyInfo);
return Expression.Lambda>(
Expression.Assign(property, value), obj, value
);
}We still need expressions for indexed properties. Here's an example for a property with a single index. I think more then two are not necessary.
public static Expression> CreateGetterExpression(
this PropertyInfo propertyInfo,
bool nonPublic = false)
{
var hasGetter = propertyInfo.GetGetMethod(nonPublic) != null;
if (!hasGetter || propertyInfo.GetIndexParameters().Length != 1)
{
return null;
}
var obj = Expression.Parameter(typeof(T), "obj");
var index1 = Expression.Parameter(typeof(TIndex1), "i");
var property = Expression.Property(obj, propertyInfo, index1);
return Expression.Lambda>(property, obj, index1);
}The class that contains the above methods is
PropertyInfoExtensions. I think it's not a good name. Even though they are extensions you should try to find a name that better describes the functionality the class contains. Here I find the name ExpressionFactory good because this class is a factory. It creates expressions. Consequently need its methods also new names like CreateSomething. GetSomething implies you already have it somewhere and just retrieve it which is not the case here.Constructors
private static Func GetDefaultConstructor()
{
var type = typeof(T);
if (type == typeof(string))
{
return Expression.Lambda>(
Expression.TypeAs(Expression.Constant(null),
typeof(string))
).Compile();
}
if (type.HasDefaultConstructor())
{
return Expression.Lambda>(Expression.New(type)).Compile();
}
return () => (T)FormatterServices.GetUninitializedObject(type);
}This is tricky. The
string does not have a default constructor. This means that the New() method should throw an InvalidOperationException for types without a default constructor. An empty instance of a string is of no use. Strings are immutable so you cannot set anything useful and therefore it should be forbidden to create one.In such cases the tester-doer pattern is recommendable. To implement it you'd need to add a property like
HasDefaultConstructor or CanCreateNew. This would be the tester. New() is the doer.There is one more issue in this method. The usage of this is invalid:
FormatterServices.GetUninitializedObject(..)FormatterServices.GetUninitializedObject Method
Because the new instance of the object is initialized to zero and no constructors are run, the object might not represent a state that is regarded as valid by that object. The current method should only be used for deserialization when the user intends to immediately populate all fields. It does not create an uninitialized string, since creating an empty instance of an immutable type serves no purpose.
Returning this from the
New() is of no use. No default constructor -> exception. All other workarounds do only harm.Creating TypeAccessor
```
PropertyInfo[] properties = typeof(T).GetProperties(BindingFlags.Instance |
(includeNonPublic
Code Snippets
var getMethod = propertyInfo.GetGetMethod(includeNonPublic);
if (getMethod != null && propertyInfo.GetIndexParameters().Length == 0)
{
var instance = Expression.Parameter(typeof(TSource), "instance");
var value = Expression.Call(instance, getMethod);
return Expression.Lambda<Func<TSource, TProperty>>(
propertyInfo.PropertyType.IsValueType
? Expression.Convert(value, typeof(TProperty))
: Expression.TypeAs(value, typeof(TProperty)),
instance
);
}public static Expression<Func<T, TProperty>> CreateGetterExpression<T, TProperty>(
this PropertyInfo propertyInfo,
bool nonPublic = false)
{
var hasGetter = propertyInfo.GetGetMethod(nonPublic) != null;
if (!hasGetter || propertyInfo.GetIndexParameters().Any())
{
return null;
}
var obj = Expression.Parameter(typeof(T), "obj");
var property = Expression.Property(obj, propertyInfo);
return Expression.Lambda<Func<T, TProperty>>(property, obj);
}public static Expression<Action<T, TProperty>> CreateSetterExpression<T, TProperty>(
this PropertyInfo propertyInfo,
bool nonPublic = false)
{
var hasSetter = propertyInfo.GetSetMethod() != null;
if (!hasSetter || propertyInfo.GetIndexParameters().Any())
{
return null;
}
var obj = Expression.Parameter(typeof(T), "obj");
var value = Expression.Parameter(typeof(TProperty), "value");
var property = Expression.Property(obj, propertyInfo);
return Expression.Lambda<Action<T, TProperty>>(
Expression.Assign(property, value), obj, value
);
}public static Expression<Func<T, TIndex1, TProperty>> CreateGetterExpression<T, TIndex1, TProperty>(
this PropertyInfo propertyInfo,
bool nonPublic = false)
{
var hasGetter = propertyInfo.GetGetMethod(nonPublic) != null;
if (!hasGetter || propertyInfo.GetIndexParameters().Length != 1)
{
return null;
}
var obj = Expression.Parameter(typeof(T), "obj");
var index1 = Expression.Parameter(typeof(TIndex1), "i");
var property = Expression.Property(obj, propertyInfo, index1);
return Expression.Lambda<Func<T, TIndex1, TProperty>>(property, obj, index1);
}private static Func<T> GetDefaultConstructor()
{
var type = typeof(T);
if (type == typeof(string))
{
return Expression.Lambda<Func<T>>(
Expression.TypeAs(Expression.Constant(null),
typeof(string))
).Compile();
}
if (type.HasDefaultConstructor())
{
return Expression.Lambda<Func<T>>(Expression.New(type)).Compile();
}
return () => (T)FormatterServices.GetUninitializedObject(type);
}Context
StackExchange Code Review Q#151201, answer score: 4
Revisions (0)
No revisions yet.