2017-07-20 3 views
1

J'essaie actuellement de créer un "Mock" pour une interface utilisant Reflection.Emit. Par conséquent j'ai créé une classe de base que j'utilise pour tous les simulacres générés dynamiquement. Pour les propriétés de l'interface, je souhaite appeler une méthode "Get" dans la classe de base qui renvoie la valeur de la propriété.Reflection.Emit InvalidProgramException

public class Mock 
{ 
    public static TIf Wrap<TIf>() where TIf : class 
    { 
    if (!typeof(TIf).IsInterface) 
     throw new Exception(typeof(TIf) + " is no interface"); 

    var asmBuilder = Thread.GetDomain().DefineDynamicAssembly(new AssemblyName("Test"), AssemblyBuilderAccess.Run); 
    var modBuilder = asmBuilder.DefineDynamicModule("Mock", true); 
    var typename = "ImplOf" + typeof(TIf).Name.Replace(" ", "") + ".Mock"; 
    var typeBuilder = modBuilder.DefineType(typename, TypeAttributes.Public, typeof(WrapperBase)); 

    typeBuilder.AddInterfaceImplementation(typeof(TIf)); 

    // methods 
    foreach (var meth in typeof(TIf).GetMethods()) 
    { 
     var del = typeof(WrapperBase).GetMethod(meth.ReturnType != typeof(void) ? "TryCallMethod" : "TryCallMethodOneWay"); 

     var mb = typeBuilder.DefineMethod(meth.Name, meth.Attributes^MethodAttributes.Abstract); 
     mb.SetParameters(meth.GetParameters().Select(p => p.ParameterType)?.ToArray()); 
     mb.SetReturnType(meth.ReturnType); 
     var mbil = mb.GetILGenerator(); 
     mbil.Emit(OpCodes.Ldarg_0); 
     mbil.Emit(OpCodes.Ldstr, meth.Name); 
     for (var i = 0; i < meth.GetParameters().Length; i++) 
     { 
     mbil.Emit(OpCodes.Ldarg, i + 1); 
     } 

     mbil.Emit(OpCodes.Call, del); 
     mbil.Emit(OpCodes.Ret); 
    } 

    // properties 
    foreach (var prop in typeof(TIf).GetProperties()) 
    { 
     var propertyBuilder = typeBuilder.DefineProperty(prop.Name, prop.Attributes, prop.PropertyType, null); 

     if (prop.CanRead) 
     { 
     var getterDelegate = typeof(WrapperBase).GetMethod("TryGetProperty"); 
     var getter = typeBuilder.DefineMethod("get_" + prop.Name, MethodAttributes.Public, prop.PropertyType, Type.EmptyTypes); 

     var gil = getter.GetILGenerator(); 
     gil.Emit(OpCodes.Ldarg_0); 
     gil.Emit(OpCodes.Ldstr, prop.Name); 
     gil.Emit(OpCodes.Callvirt, getterDelegate); 
     gil.Emit(OpCodes.Castclass, prop.PropertyType); 
     gil.Emit(OpCodes.Ret); 
     propertyBuilder.SetGetMethod(getter); 
     } 

     if (prop.CanWrite) 
     { 
     var setterDelegate = typeof(WrapperBase).GetMethod("TrySetProperty"); 
     var setter = typeBuilder.DefineMethod("set_" + prop.Name, MethodAttributes.Public, typeof(void), Type.EmptyTypes); 

     var sil = setter.GetILGenerator(); 
     sil.Emit(OpCodes.Ldarg_0); 
     sil.Emit(OpCodes.Ldstr, prop.Name); 
     sil.Emit(OpCodes.Ldarg_1); 
     sil.Emit(OpCodes.Call, setterDelegate); 
     sil.Emit(OpCodes.Ret); 
     propertyBuilder.SetSetMethod(setter); 
     } 
    } 

    var retType = typeBuilder.CreateType(); 
    return retType.GetConstructor(new Type[0]).Invoke(new object[0]) as TIf; 
    } 

    public abstract class WrapperBase 
    { 
    public event Func<string, object[], object> OnTryCallMethod; 
    public event Action<string, object[]> OnTryCallMethodOneWay; 
    public event Func<string, object> OnTryGetProperty; 
    public event Action<string, object> OnTrySetProperty; 

    /// <inheritdoc /> 
    public object TryCallMethod(string name, object[] pars) 
    { 
     return OnTryCallMethod?.Invoke(name, pars); 
    } 

    /// <inheritdoc /> 
    public void TryCallMethodOneWay(string name, object[] pars) 
    { 
     OnTryCallMethodOneWay?.Invoke(name, pars); 
    } 

    /// <inheritdoc /> 
    public object TryGetProperty(string name) 
    { 
     return OnTryGetProperty?.Invoke(name); 
    } 

    /// <inheritdoc /> 
    public void TrySetProperty(string name, object value) 
    { 
     OnTrySetProperty?.Invoke(name, value); 
    } 
    } 
} 

Malheureusement, je reçois toujours un InvalidProgramException en essayant de lire une propriété « moqué ». La définition de la propriété (qui déléguera également l'appel à une méthode de classe de base) fonctionne correctement, de même pour les appels de méthode.

Pour les tests, j'ai créé une interface assez simple:

public interface ITest 
{ 
    void Show(string text); 

    string Text { get; set; } 
} 

Maintenant, je suis d'appeler la maquette comme ceci:

var wrapped = Mock.Wrap<ITest>(); 

    // ***************** works - EventHandler is called with correct parameters! 
    ((Mock.WrapperBase)wrapped).OnTryCallMethodOneWay += (s, objects) => { }; 
    wrapped.Show("sss"); 

    // ***************** works - EventHandler is called with correct parameters! 
    wrapped.Text = ""; 
    ((Mock.WrapperBase)wrapped).OnTrySetProperty += (s, val) => { }; 

    // ***************** does NOT work - getting InvalidProgramException 
    ((Mock.WrapperBase)wrapped).OnTryGetProperty += s => ""; 
    var t = wrapped.Text; 
+0

'setter var = typeBuilder.DefineMethod (" set_ "+ prop.Name, MethodAttributes.Public, typeof (void), Type.EmptyTypes);" définit une méthode qui ne prend aucun argument (puisque vous n'avez fourni aucun type pour les arguments). Ceci est une erreur. Aussi, pouvez-vous montrer le code de travail complet? Y compris le code qui implémente votre interface, afin que nous puissions le répliquer? – Rob

+0

En outre, tester votre code semble fonctionner correctement, après avoir apporté la modification au setter – Rob

+0

Merci pour l'indice concernant le TypeParameter. Je les ai ajouté, mais l'exception est toujours levée ... – chrisih

Répondre

1

Après un peu de débogage, j'ai trouvé votre problème . J'ai remarqué que

wrapped.Text = "" entrait dans TryCallMethodOneWay quand il est clairement écrit comme appelant TrySetProperty.

C'est parce que foreach (var meth in typeof(TIf).GetMethods()) va vous renvoyer vos méthodes getter et setter. C'est; vous définissez les getters et les setters deux fois.

Ceci est résolu par une simple vérification:

var properties = typeof(TIf).GetProperties(); 
var propertyMethods = properties.SelectMany(p => new[] { p.GetGetMethod(), p.GetSetMethod() }).ToLookup(p => p); 

foreach (var meth in typeof(TIf).GetMethods()) 
{ 
    if (propertyMethods.Contains(meth)) 
     continue; 
    ... 
}    

Maintenant, vous avez également marquer vos méthodes de mise en œuvre comme Virtual si elles sont à mettre en œuvre une interface. Ainsi, vous devrez changer le code pour les suivantes:

var getter = typeBuilder.DefineMethod("get_" + prop.Name, MethodAttributes.Public | MethodAttributes.Virtual, prop.PropertyType, Type.EmptyTypes); 

Et

var setter = typeBuilder.DefineMethod("set_" + prop.Name, MethodAttributes.Public | MethodAttributes.Virtual, typeof(void), new[] { prop.PropertyType }); 

Et votre code devrait fonctionner sans problème

+1

Super! travailler maintenant comme un charme :-) merci beaucoup !! – chrisih