Créer dynamiquement une classe par interface

.net c# code-generation expression-trees reflection

Question

J'ai quelques expériences avec les .Net Expressions , quand je suis capable de générer dynamiquement des méthodes. C'est bon, c'est bon.

Mais maintenant, je dois générer toute une classe, et il semble que le seul moyen de le faire est d’émettre tout IL, ce qui est totalement inacceptable (c’est impossible à supporter).

Supposons que nous ayons l'interface suivante:

public interface IFoo
{
    [Description("5")]
    int Bar();
    [Description("true")]
    bool Baz();
}

qui devrait être converti en:

public class Foo : IFoo
{
    public int Bar() => 5;
    public bool Baz() => true;
}

Comment puis-je y arriver? Est-ce même possible sans outils et librairies tiers? Je sais qu'il y a beaucoup d'utilitaires utiles sur GitHub, mais je ne souhaite vraiment pas importer un framework MVVM complet pour créer un peu de code.

Si je pouvais simplement utiliser Expressions et créer une classe avec les méthodes que j'ai déjà générées avec elle. Mais pour l'instant je ne sais pas comment le faire.

Réponse acceptée

Premièrement, étant donné que vous travaillez avec la communication à distance, je dois mentionner que c’est quelque chose que .NET a été initialement conçu pour prendre en charge (à partir de ses racines COM. 2.0). Votre solution la plus simple consiste à implémenter un proxy distant transparent - créez simplement votre propre classe (probablement générique) à partir de System.Runtime.Remoting.Proxies.RealProxy , et vous pouvez fournir toute la logique nécessaire à la mise en oeuvre de la fonction la méthode Invoke . GetTransparentProxy , vous obtenez le proxy qui implémente votre interface et vous êtes GetTransparentProxy à partir.

Évidemment, cela a un coût à l'exécution, lors de chaque invocation. Cependant, cela n'a généralement aucune importance, à part le fait que vous effectuez des E / S, en particulier si vous travaillez avec le réseau. En fait, à moins que vous ne soyez dans une boucle serrée, cela n’a aucune importance, même lorsque vous ne faites pas d’E / S - seuls les tests de performance peuvent vraiment dire si le coût vous convient ou non.

Si vous voulez vraiment prégénérer tous les corps de méthodes, plutôt que de garder la logique dynamique au moment de l'exécution, vous pouvez exploiter le fait que LambdaExpression vous donne CompileToMethod . Contrairement à Compile , vous ne pouvez pas appeler directement un joli petit délégué, mais cela vous donne la possibilité d'utiliser des expressions lambda pour la construction explicite de corps de méthodes - ce qui vous permet de créer des classes entières sans recourir à des invocations de délégués.

Un exemple complet (mais simple):

void Main()
{
  var ab = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName("TestAssembly"), AssemblyBuilderAccess.Run);
  var mb = ab.DefineDynamicModule("Test");

  var tb = mb.DefineType("Foo");
  tb.AddInterfaceImplementation(typeof(IFoo));

  foreach (var imethod in typeof(IFoo).GetMethods())
  {
    var valueString = ((DescriptionAttribute)imethod.GetCustomAttribute(typeof(DescriptionAttribute))).Description;

    var method = 
      tb.DefineMethod
      (
        "@@" + imethod.Name, 
        MethodAttributes.Private | MethodAttributes.Static, 
        imethod.ReturnType,
        new [] { tb }
      );

    // Needless to say, I'm making a lot of assumptions here :)
    var thisParameter = Expression.Parameter(typeof(IFoo), "this");

    var bodyExpression =
      Expression.Lambda
      (
        Expression.Constant
        (
          Convert.ChangeType(valueString, imethod.ReturnType)
        ),
        thisParameter
      );

    bodyExpression.CompileToMethod(method);

    var stub =
      tb.DefineMethod(imethod.Name, MethodAttributes.Public | MethodAttributes.Virtual, imethod.ReturnType, new Type[0]);

    var il = stub.GetILGenerator();
    il.Emit(OpCodes.Ldarg_0);
    il.EmitCall(OpCodes.Call, method, null);
    il.Emit(OpCodes.Ret);

    tb.DefineMethodOverride(stub, imethod);
  }

  var fooType = tb.CreateType();
  var ifoo = (IFoo)Activator.CreateInstance(fooType);

  Console.WriteLine(ifoo.Bar()); // 5
  Console.WriteLine(ifoo.Baz()); // True
}

public interface IFoo
{
    [Description("5")]
    int Bar();
    [Description("true")]
    bool Baz();
}

Si vous avez déjà travaillé avec des émissions .NET, cela devrait être assez simple. Nous définissons un assemblage dynamique, un module, un type (idéalement, vous souhaitez définir tous vos types à la fois, dans un seul assemblage dynamique). La Lambda.CompileToMethod dans le fait que Lambda.CompileToMethod ne prend en charge que les méthodes statiques, nous devons donc tricher un peu. Tout d'abord, nous créons une méthode statique qui prend this comme argument et compile l'expression lamdba à cet endroit. Ensuite, nous créons un stub de méthode - un simple morceau d’IL qui garantit que notre méthode statique est appelée correctement. Enfin, nous lions la méthode d’interface au stub.

Dans mon exemple, je suppose une méthode sans paramètre, mais tant que vous vous assurez que LambdaExpression utilise exactement les mêmes types que la méthode d’interface, le stub est aussi simple que de faire tous les Ldarg dans une séquence, un seul Call et un seul Ret . Et si votre code réel (dans la méthode statique) est suffisamment court, il sera souvent en ligne. Et comme this s'agit d'un argument comme un autre, si vous vous sentez aventureux, vous pouvez simplement prendre le corps de la méthode générée et le placer directement dans la méthode virtuelle - notez que vous devez le faire en deux passes. , bien que.


Réponse populaire

Vous pouvez utiliser CodeDOM et Emit. Cependant, je ne pense pas que cela en vaille la peine.

Pour une chose similaire, j'utilise la bibliothèque suivante: http://www.castleproject.org/projects/dynamicproxy/

Il est capable de créer des classes proxy même lorsque la classe cible n'est pas disponible (vous devez alors intercepter toutes les méthodes).




Sous licence: CC-BY-SA with attribution
Non affilié à Stack Overflow
Est-ce KB légal? Oui, apprenez pourquoi
Sous licence: CC-BY-SA with attribution
Non affilié à Stack Overflow
Est-ce KB légal? Oui, apprenez pourquoi