Ejemplo de uso de DynamicProxy

¡Hola!
Hace unos días vimos un par de entradas (ésta y ésta) sobre las características y usos básicos de los proxys a través de la libería DynamicProxy. Hoy vamos a ver un pequeño proyecto en el que utilizaremos esta libería para medir tiempos.
El proyecto es muy sencillo (está subido en Github) y de lo que se trata es de poder medir el tiempo que tarda en ejecutarse un método. Simplemente hay una clase estática llamada TimingExtension que tiene 3 métodos de extensión para poder transformar nuestra instancia en un proxy y que se encargue de cronometrar el tiempo desde que se llama al método y hasta que éste finaliza. Para poder imprimir el tiempo transcurrido se puede configurar cómo se quiere imprimir indicándole nuestro IWriter (una sencilla interfaz que define un método void Write(string text)). Vamos a ver un test:

1
2
3
4
5
6
7
8
9
10
11
    [Fact]
    public void WriterIsUsed_WhenA_MethodIsInvoke_Test()
    {
        Mock<IWriter> writerMoq = new Mock<IWriter>();
        TimingExtension.Configure(writerMoq.Object);
        User user = new User().AddTiming();
 
        user.Greet();
 
        writerMoq.Verify(w => w.Write(It.IsAny<string>()), Times.AtLeastOnce);
    }

Como vemos es bastante sencillo. Configuramos nuestro writer y utilizamos el método de extensión AddTiming para que las llamadas a cualquier método de user sean cronometradas. Si quisiéramos que sólo se midieran tiempos en ciertos métodos tenemos dos opciones. La primera es especificando el nombre del método que nos interesa medir:

1
2
3
4
5
6
7
8
9
10
11
12
13
    [Fact]
    public void ConfigureSingleMethod_When_AddTiming_Test()
    {
        Mock<IWriter> writerMoq = new Mock<IWriter>();
        TimingExtension.Configure(writerMoq.Object);
        User user = new User().AddTiming(nameof(User.SayGoodbye));
 
        user.Greet();
        writerMoq.Verify(w => w.Write(It.IsAny<string>()), Times.Never);
 
        user.SayGoodbye();
        writerMoq.Verify(w => w.Write(It.IsAny<string>()), Times.AtLeastOnce);
    }

En el test anterior estamos indicando que sólo se contabilice el tiempo del método SayGoodbye. Tiene al menos un par de problemas. El primero es que no puedes diferenciar qué sobrecarga del método quieres capturar si hay más de una, y el segundo, que tiene más fácil solución (la pereza es el mayor impedimento :)), es que sólo admite configurar un método.
Hay otra forma de establecer el método que nos interesa capturar, y es con una expresión lambda:

1
2
3
4
5
6
7
8
9
10
11
12
13
    [Fact]
    public void ConfigureSingleMethod_When_AddTiming_With_LambdaExpression_Test()
    {
        Mock<IWriter> writerMoq = new Mock<IWriter>();
        TimingExtension.Configure(writerMoq.Object);
        User user = new User().AddTiming(u => u.SayGoodbye());
 
        user.Greet();
        writerMoq.Verify(w => w.Write(It.IsAny<string>()), Times.Never);
 
        user.SayGoodbye();
        writerMoq.Verify(w => w.Write(It.IsAny<string>()), Times.AtLeastOnce);
    }

Muy similar al anterior pero recibe una Expression con el método que nos interesa. En esencia hace lo mismo que el anterior, y es obtener el nombre del método al que estás haciendo referencia, por lo que no tendrá en cuenta las diferentes sobrecargas. Además, si el método tiene parámetros los vas a tener que indicar con algunos valores… y puede quedar muy feo. Se podría hacer algo similar a lo que se hace en la librería Moq, y sería únicamente capturar (cronometrar) el método cuando los valores de los parámetros coincidan con los que has indicado e incluso especificar que podría ser cualquier valor para ese parámetro (It.IsAny()), pero como sólo es un ejemplo, lo que hay es más que suficiente.. Ambas formas (con el nombre del método y con la Expression) utilizan un Hook (IProxyGenerationHook) para decidir qué métodos va capturar.
Podemos ver cómo usarlo en una aplicación de consola:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
    class Program
    {
        static void Main(string[] args)
        {
            TimingExtension.Configure(new ConsoleWriter());
            var sleepyhead = new Sleepyhead().AddTiming();
            sleepyhead.Sleep();
            System.Console.ReadLine();
        }
    }
 
    public class Sleepyhead
    {
        public virtual void Sleep()
        {
            Thread.Sleep(500);
        }
    }
 
    class ConsoleWriter : IWriter
    {
        public void Write(string text)
        {
            System.Console.WriteLine(text);
        }
    }

El resultado sería:
Y poco más hay que contar, simplemente era ver un pequeño ejemplo de cómo los proxys pueden ayudarnos. Además de todas las pegas que hemos visto, también está que no va a funcionar bien con métodos asíncronos (ésta librería se supone que permite crear interceptores para métodos asíncronos de forma sencilla, pero no la he probado), y por supuesto los métodos deben ser virtuales y la clase que queremos convertir en proxy tiene que ser visible (public) para la clase TimingExtension.

¡Un saludo!

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *