As the correct answer didn't work for me (collections in the expression) but pushed me to the right direction, I needed to investigate this a lot and I think I came up with a method which can generate setter for literally any member expression.
For properties and fields it behaves the same as the marked answer (I believe it is much more transparent though).
It has additional support for Lists and Dictionaries - please see in the comments.
public static Action GetSetter(Expression> getterExpression)
{
/*** SIMPLE PROPERTIES AND FIELDS ***/
// check if the getter expression refers directly to a PROPERTY or FIELD
var memberAcessExpression = getterExpression.Body as MemberExpression;
if (memberAcessExpression != null)
{
//to here we assign the SetValue method of a property or field
Action propertyOrFieldSetValue = null;
// property
var propertyInfo = memberAcessExpression.Member as PropertyInfo;
if (propertyInfo != null)
{
propertyOrFieldSetValue = (declaringObjectInstance, propertyOrFieldValue) => propertyInfo.SetValue(declaringObjectInstance, propertyOrFieldValue);
};
// field
var fieldInfo = memberAcessExpression.Member as FieldInfo;
if (fieldInfo != null)
{
propertyOrFieldSetValue = (declaringObjectInstance, propertyOrFieldValue) => fieldInfo.SetValue(declaringObjectInstance, propertyOrFieldValue);
}
// This is the expression to get declaring object instance.
// Example: for expression "o=>o.Property1.Property2.CollectionProperty[3].TargetProperty" it gives us the "o.Property1.Property2.CollectionProperty[3]" part
var memberAcessExpressionCompiledLambda = Expression.Lambda(memberAcessExpression.Expression, getterExpression.Parameters.Single()).Compile();
Action setter = (expressionParameter, value) =>
{
// get the object instance on which is the property we want to set
var declaringObjectInstance = memberAcessExpressionCompiledLambda.DynamicInvoke(expressionParameter);
Debug.Assert(propertyOrFieldSetValue != null, "propertyOrFieldSetValue != null");
// set the value of the property
propertyOrFieldSetValue(declaringObjectInstance, value);
};
return setter;
}
/*** COLLECTIONS ( IDictionary<,> and IList<,>) ***/
/*
* DICTIONARY:
* Sample expression:
* "myObj => myObj.Property1.ListProperty[5].AdditionalInfo["KEY"]"
* Setter behaviour:
* The same as adding to a dictionary.
* It does Add("KEY", ) to the dictionary. It fails if the jey already exists.
*
*
* LIST
* Sample expression:
* "myObj => myObj.Property1.ListProperty[INDEX]"
* Setter behaviour:
* If INDEX >= 0 and the index exists in the collection it behaves the same like inserting to a collection.
* IF INDEX < 0 (is negative) it adds the value at the end of the collection.
*/
var methodCallExpression = getterExpression.Body as MethodCallExpression;
if (
methodCallExpression != null && methodCallExpression.Object != null &&
methodCallExpression.Object.Type.IsGenericType)
{
var collectionGetterExpression = methodCallExpression.Object as MemberExpression;
Debug.Assert(collectionGetterExpression != null, "collectionGetterExpression != null");
// This gives us the collection instance when it is invoked on the object instance whic the expression is for
var collectionGetterCompiledLambda =Expression.Lambda(collectionGetterExpression, getterExpression.Parameters.Single()).Compile();
// this returns the "KEY" which is the key (object) in case of Dictionaries and Index (integer) in case of other collections
var collectionKey = ((ConstantExpression) methodCallExpression.Arguments[0]).Value;
var collectionType = collectionGetterExpression.Type;
// IDICTIONARY
if (collectionType.GetGenericTypeDefinition() == typeof(IDictionary<,>))
{
// Create an action which accepts the instance of the object which the "dictionary getter" expression is for and a value
// to be added to the dictionary.
Action dictionaryAdder = (expressionParameter, value) =>
{
try
{
var dictionaryInstance = (IDictionary)collectionGetterCompiledLambda.DynamicInvoke(expressionParameter);
dictionaryInstance.Add(collectionKey, value);
}
catch (Exception exception)
{
throw new Exception(
string.Format(
"Addition to dictionary failed [Key='{0}', Value='{1}']. The \"adder\" was generated from getter expression: '{2}'.",
collectionKey,
value,
getterExpression.ToString()), exception);
}
};
return dictionaryAdder;
}
// ILIST
if (typeof (IList<>).MakeGenericType(typeof (bool)).IsAssignableFrom(collectionType.GetGenericTypeDefinition().MakeGenericType(typeof(bool))))
{
// Create an action which accepts the instance of the object which the "collection getter" expression is for and a value
// to be inserted
Action collectionInserter = (expressionParameter, value) =>
{
try
{
var collectionInstance = (IList)collectionGetterCompiledLambda.DynamicInvoke(expressionParameter);
var collectionIndexFromExpression = int.Parse(collectionKey.ToString());
// The semantics of a collection setter is to add value if the index in expression is <0 and set the item at the index
// if the index >=0.
if (collectionIndexFromExpression < 0)
{
collectionInstance.Add(value);
}
else
{
collectionInstance[collectionIndexFromExpression] = value;
}
}
catch (Exception invocationException)
{
throw new Exception(
string.Format(
"Insertion to collection failed [Index='{0}', Value='{1}']. The \"inserter\" was generated from getter expression: '{2}'.",
collectionKey,
value,
getterExpression.ToString()), invocationException);
}
};
return collectionInserter;
}
throw new NotSupportedException(
string.Format(
"Cannot generate setter from the given expression: '{0}'. Collection type: '{1}' not supported.",
getterExpression, collectionType));
}
throw new NotSupportedException("Cannot generate setter from the given expression: "+getterExpression);
}