For the following interface and struct:
internal interface IRecord<T> where T : struct
{
ref T Values { get; }
}
public struct Entity
{
public int Field1;
...
}
I would like get the following lambda expression via reflection:
Expression<Func<IRecord<Entity>, int>> getter = x => x.Values.Field1;
Expression<Action<IRecord<Entity>, int>> setter = (x, value) => x.Values.Field1 = value;
Unfortunately this doesn't compile: cs8153: an expression tree lambda may not contain a call to a method, property, or indexer that returns by reference
. It seems get any member of ref struct
is not supported via reflection.
So I have to go for System.Reflection.Emit
to generate the following accessor class:
public static class Accessor
{
public static int GetField1(IRecord<Entity> record) => record.Values.Field1;
public static void SetField1(IRecord<Entity> record, int value) => record.Values.Field1 = value;
...
}
and get the following lambda expression via reflection:
Expression<Func<IRecord<Entity>, int>> getter = x => Accessor.GetField1(x);
Expression<Action<IRecord<Entity>, int>> setter = (x, value) => Accessor.SetField1(x, value);
Here is my code to generate the Accessor
class using System.Reflection.Emit
:
private static readonly ModuleBuilder ModuleBuilder = GetModuleBuilder();
private static ModuleBuilder GetModuleBuilder()
{
AssemblyName assemblyName = new AssemblyName("AccessTypeBuilder");
AssemblyBuilder assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run);
ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule("Module");
return moduleBuilder;
}
public static Type BuildAccessorType(Type fieldValuesType)
{
TypeBuilder typeBuilder = ModuleBuilder.DefineType("Accessor", TypeAttributes.Public | TypeAttributes.Abstract | TypeAttributes.Sealed, typeof(object));
BuildAccessor(typeBuilder, typeof(Entity), "Field1", typeof(int));
return typeBuilder.CreateType();
}
private static void BuildAccessor(TypeBuilder typeBuilder, Type fieldValuesType, string fieldName, Type dataType)
{
typeBuilder.DefineGetter(fieldValuesType, $"Get{fieldName}", dataType, fieldName);
typeBuilder.DefineSetter(fieldValuesType, $"Set{fieldName}", dataType, fieldName);
}
private static void DefineField(this TypeBuilder typeBuilder, Type dataType, string fieldName)
{
typeBuilder.DefineField(fieldName, dataType, FieldAttributes.Public);
}
private static Type RecordType(this Type fieldValuesType)
{
return typeof(IRecord<>).MakeGenericType(fieldValuesType);
}
private static MethodInfo FieldValues(this Type fieldValuesType)
{
var recordType = fieldValuesType.RecordType();
var property = recordType.GetProperty(nameof(IRecord<int>.Values));
return property.GetMethod;
}
private static FieldInfo Field(this Type fieldValuesType, string fieldName) => fieldValuesType.GetField(fieldName, BindingFlags.Public | BindingFlags.Instance);
private static void DefineGetter(this TypeBuilder typeBuilder, Type fieldValuesType, string methodName, Type dataType, string fieldName)
{
var method = typeBuilder.DefineMethod(methodName, MethodAttributes.Public | MethodAttributes.Static, dataType, new Type[] { fieldValuesType.RecordType() });
var methodBody = method.GetILGenerator();
methodBody.EmitGetter(fieldValuesType, fieldName);
}
private static void EmitGetter(this ILGenerator methodBody, Type fieldValuesType, string fieldName)
{
methodBody.Emit(OpCodes.Ldarg_0);
methodBody.Emit(OpCodes.Callvirt, fieldValuesType.FieldValues());
methodBody.Emit(OpCodes.Ldfld, fieldValuesType.Field(fieldName));
methodBody.Emit(OpCodes.Stloc_0);
methodBody.Emit(OpCodes.Ldloc_0);
methodBody.Emit(OpCodes.Ret);
}
private static void DefineSetter(this TypeBuilder typeBuilder, Type fieldValuesType, string methodName, Type dataType, string fieldName)
{
var method = typeBuilder.DefineMethod(methodName, MethodAttributes.Public | MethodAttributes.Static, typeof(void), new Type[] { fieldValuesType.RecordType(), dataType });
var methodBody = method.GetILGenerator();
methodBody.EmitSetter(fieldValuesType, fieldName);
}
private static void EmitSetter(this ILGenerator methodBody, Type fieldValuesType, string fieldName)
{
methodBody.Emit(OpCodes.Nop);
methodBody.Emit(OpCodes.Ldarg_0);
methodBody.Emit(OpCodes.Callvirt, fieldValuesType.FieldValues());
methodBody.Emit(OpCodes.Ldarg_1);
methodBody.Emit(OpCodes.Stfld, fieldValuesType.Field(fieldName));
methodBody.Emit(OpCodes.Ret);
}
When consuming the generated Accessor
class, I get InvalidProgramException: Common Language Runtime detected an invalid program.
when invoking the generated getter; and get System.MethodAccessException: Attempt by method 'Accessor.SetField0(IRecord`1<Entity>, Int32)' to access IRecord`1<Entity>.get_FieldValues()' failed.
when invoking the generated setter.
What am I doing wrong? I've spent whole day for this and is pretty frustrated. Any help will be very much appreciated!
Fixed.
The
IRecord<T>
interface must bepublic
;Remove two lines of opcode emitting: