Creando proxys con Castle DynamicProxy

¡Hola!
En esta entrada vamos a ver algunas cosas básicas sobre proxys y cómo crearlos utilizando un estupenda librería llamada DynamicProxy. Esta librería forma parte del proyecto Castle que incluye, además de DynamicProxy, otras potentes librerías como el contenedor de IoC Windsor. Volviendo a DynamicProxy, seguro que consciente o inconscientemente la has utilizado muchas veces en alguna librería o framework que hayas incluido en algunos de tus proyectos.. en este post lo veremos.

¿Qué es un proxy?

Seguro que en mayor o menor medida estás familiarizado con esta palabra, y entiendes que un proxy es un sustituto o intermediario de algo, y que lo utilizaremos de forma más o menos transparente a cómo utilizaríamos ese algo al que sustituye. En el ámbito que vamos a ver, un proxy lo que hará será sustituir al objeto de real y realizar acciones cuando se realicen llamadas a alguno de sus miembros como métodos o propiedades. Por ejemplo, creamos un proxy de una clase y podemos capturar las llamadas que se hagan a sus métodos para realizar ciertas acciones. Realmente lo que se hará al crear el proxy es extender la clase sobre la que creamos el proxy y sobrescribir sus miembros, de ahí que tenga ciertas limitaciones que veremos más adelante.

¿Para qué se utilizan?

Los proxys se utilizan en muchas situaciones, por ejemplo en AOP, donde vamos a poder separar ciertos aspectos cross-cutting del resto del sistema, por ejemplo, para implementar un sistema de logging que registre las llamadas a ciertos métodos sin interferir en la lógica de negocio de nuestra clase, ya que realmente es algo que, aunque sea necesario, quizá no está directamente relacionada y nos permite separar bien las diferentes responsabilidades. Otro uso puede ser implementar propiedades Lazy y hacer que sólo se carguen cuando se accede a ellas, algunos ORMs las implementan aunque generalmente se desaconseja su uso. También se pueden utilizar para crear mixins y poder añadir funcionalidad de otra clase sobre la clase que haremos el proxy. En resumen, de lo que se trata es de extender y añadir funcionalidad adicional sin tocar la clase de origen.

DynamicProxy

Una vez que conocemos un poco qué son los proxys y en qué escenarios se pueden utilizar, vamos ver la librería DynamicProxy (que podemos descargar como paquete NuGet). Esta librería, como decía al principio de la entrada, seguramente la hayas utilizado alguna vez. Por ejemplo, si utilizas habitualmente Moq como framework para crear mocks, entonces has utilizado DynamicProxy.
Como podemos ver cuando creamos un mock de una clase, realmente está creando un proxy, y es ese proxy el que se configura para que devuelva los valores que nosotros queramos o compruebe el número de llamadas realizadas, entre otros.
Otro proyecto en el que se utiliza es el contenedor de IoC Castle Windsor para permitir añadir interceptores. En Ninject se pueden utilizar también para añadir interceptores, hace un tiempo vimos cómo hacerlo.
Otros proyectos donde se utiliza (según la documentación de Castle) es en NSubstitute (framework para mocks), NHibernate y Entity Framework Core para carga perezosa (lazy loading).
Ahora sí, después de todo este coñazo que he soltado, vamos a ver código. Asumiendo que hemos añadido la librería a nuestro proyecto vamos a crear una clase de prueba sobre la que crearemos un proxy:

1
2
3
4
5
6
7
8
9
    public class User
    {
        public virtual string Name { get; set; }
 
        public virtual void GreetsTo(string user)
        {
            Console.WriteLine($"Hi, {user}!");
        }
    }

Ahora vamos a crear un test que se encargue de crear un proxy a partir de la clase User:

1
2
3
4
5
6
7
8
    [Fact]
    public void CheckThat_IsProxy()
    {
        ProxyGenerator proxyGenerator = new ProxyGenerator();
        var userProxy = proxyGenerator.CreateClassProxy<User>();
 
        Assert.True(ProxyUtil.IsProxy(userProxy));
    }

Y el test se pone en verde. ¿Qué estamos haciendo? En primer lugar creamos una instancia de ProxyGenerator, que será la clase a partir de la cual podemos crear proxys (más adelante veremos que a lo mejor nos conviene tener un ProxyGenerator estático), y después, simplemente con el método CreateClassProxy creamos un proxy de la clase User. Finalmente comprobamos que realmente hemos creado un proxy, utilizando para ello ProxyUtil. Si has estado atento te habrás dado cuenta de que tanto el método GreetsTo como la propiedad Name son virtuales, y es que deben de ser así; al final lo que está haciendo por debajo es extender la clase User y sobrescribir sus miembros, por lo que sigue las mismas reglas que cualquier otra clase (para sobrescribir un miembro debe ser virtual, la clase no puede ser sealed..).
Por si no te acabas de creer que hemos creado un proxy, vamos a ver una captura:
Como se ve en la imagen, el tipo de userProxy no es User, sino Castle.Proxies.UserProxy. Aquí lo interesante es que nosotros podemos seguir trabajando con la instancia que hemos creado de forma transparente, es decir, como si fuese una instancia de User:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
    [Fact]
    public void UserProxy_CanBeUsedAs_User()
    {
        StringBuilder consoleOutput = new StringBuilder();
        Console.SetOut(new StringWriter(consoleOutput));
 
        ProxyGenerator proxyGenerator = new ProxyGenerator();
        User user = proxyGenerator.CreateClassProxy<User>();
 
        user.Name = "Pepe";
        user.GreetsTo("Juanito");
 
        Assert.Equal("Pepe",user.Name);
        Assert.Equal("Hi, Juanito!", consoleOutput.ToString());
    }

Cuando antes he dicho que podemos trabajar como si fuese una instancia de User, es que realmente (al menos en compilación) es una instancia de User:
Podemos ver que nos devuelve un User… Aunque en una imagen más arriba vimos que era un Castle.Proxies.UserProxy… De ahí el nombre de Dynamic Proxy, porque se generan en tiempo de ejecución no en tiempo de compilación, dicho con otras palabras, cuando (en ejecución) solicitemos la creación de un proxy se va a crear la clase en ese momento. Esto lo hace a través de System.Reflection.Emit.
Además del método que hemos visto (CreateClassProxy) hay varios métodos y sobrecargas para crear proxys.
*CreateClassProxy: Como hemos visto, crea un proxy a partir de la clase que le indiquemos.
*CreateClassProxyWithTarget: La opción anterior se nos puede quedar algo corta, porque sólo funciona cuando existe un constructor sin parámetros (si no lo hay lanzará una excepción), o si no nos interesa crear la instancia directamente como un proxy sino que tenemos una instancia normal y queremos convertirla en proxy, esta es la opción adecuada. Vemos un ejemplo:

1
2
3
4
5
6
7
8
9
10
11
12
13
    [Fact]
    public void CreateProxyWithTarget_Test()
    {
        User user = new User {Name = "Pepe"};
        ProxyGenerator proxyGenerator = new ProxyGenerator();
        User userProxy = proxyGenerator.CreateClassProxyWithTarget<User>(user);
        //No es necesario especificar el tipo del target (en nuestro caso user), lo he añadido por claridad del ejemplo
        //se podría sustituir por:
        //User userProxy = proxyGenerator.CreateClassProxyWithTarget(user);
 
        Assert.True(ProxyUtil.IsProxy(userProxy));
        Assert.Equal("Pepe", user.Name);
    }

Muy sencillo, tenemos nuestra instancia de user y la convertimos en un proxy. Como se indica en el test, el tipo de user realmente no es necesario (lo puede inferir), en los siguientes ejemplos lo seguiré añadiendo para que queden más claros.
*CreateInterfaceProxyWithoutTarget: Como puedes imaginar es muy similar al primer método que vimos (CreateClassProxy), pero nos permite crear una implementación al vuelo de la interfaz que le indiquemos:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
    public interface IWriterService
    {
        void Write(string text);
    }
 
    [Fact]
    public void CreateInterfaceProxyWithoutTarget_Test()
    {
        ProxyGenerator proxyGenerator = new ProxyGenerator();
        IWriterService writerServiceProxy = proxyGenerator.CreateInterfaceProxyWithoutTarget<IWriterService>();
 
        Assert.True(ProxyUtil.IsProxy(writerServiceProxy));
        Assert.True(typeof(IWriterService).IsAssignableFrom(writerServiceProxy.GetType()));
    }

El test nos dice que la instancia creada es un proxy y que implementa IWriterService.
*CreateInterfaceProxyWithTarget: Igual que su homólogo CreateClassProxyWithTarget nos permite generar un proxy de una interfaz a partir de un objeto que la implementa:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
    public class WriterService : IWriterService
    {
        public void Write(string text)
        {
            Console.Write(text);
        }
    }
 
    [Fact]
    public void CreateInterfaceProxyWithTarget_Test()
    {
        StringBuilder consoleOutput = new StringBuilder();
        Console.SetOut(new StringWriter(consoleOutput));
 
        IWriterService writerService = new WriterService();
        ProxyGenerator proxyGenerator = new ProxyGenerator();
        IWriterService writerServiceProxy = proxyGenerator.CreateInterfaceProxyWithTarget<IWriterService>(writerService);
 
        writerService.Write("Escribimos algo...");
 
        Assert.True(ProxyUtil.IsProxy(writerServiceProxy));
        Assert.Equal("Escribimos algo...", consoleOutput.ToString());
    }

El test nos indica que la instancia creada es un proxy y que el método Write sigue funcionando.
*CreateInterfaceProxyWithTargetInterface: Aunque el nombre sea muy similar al anterior, tiene algunas diferencias. Profundizaremos más adelante en ello así que lo dejamos por el momento.

Vale, ya sabemos cómo crear un proxy pero realmente no hemos visto su utilidad o qué podemos hacer con ellos. Esto es porque no les hemos añadido interceptores. ¿Qué son los interceptores? Es lo que nos va a permitir modificar el comportamiento cuando se haga una llamada a algún miembro de la clase como métodos o propiedades. Es decir, nos va a permitir hacer cosas antes y después de la llamada a un método (por ejemplo), o cambiar los parámetros que se envían al método, vamos, casi cualquier cosa. Para definir un interceptor debemos implementar la interfaz IInterceptor, que tiene la siguiente firma:

1
2
3
4
    public interface IInterceptor
    {
        void Intercept(IInvocation invocation);
    }

Cuando añadamos un interceptor a nuestro proxy, todas las llamadas pasarán por el método Intercept y en el parámetro invocation tendremos toda la información referente a qué método se está llamando, sus parámetros, tipo, etc. Vamos a ver un ejemplo de interceptor:

1
2
3
4
5
6
7
8
    public class LoggingInterceptor : IInterceptor
    {
        public void Intercept(IInvocation invocation)
        {
            Console.Write($"Invoke method: {invocation.Method.Name} with {invocation.Arguments.Length} arguments");
            invocation.Proceed();
        }
    }

Lo que hace el interceptor es imprimir el nombre del método al que se ha llamado y el número de argumentos. La siguiente línea (invocation.Proceed()) devolvería la ejecución a la llamada original que hayamos hecho. Esto último no es del todo cierto, en caso de que hubiese varios interceptores se van pasando la llamada de uno a otro y, finalmente, el último invocation.Proceed() pasaría al método original. Para que los siguientes tests no sean más sencillos vamos a dejar de utilizar Console.Write y le vamos a pasar la dependencia:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
    public class LoggingInterceptor : IInterceptor
    {
        private readonly IWriterService writer;
 
        public LoggingInterceptor(IWriterService writer)
        {
            this.writer = writer;
        }
 
        public void Intercept(IInvocation invocation)
        {
            writer.Write($"Invoke method: {invocation.Method.Name} with {invocation.Arguments.Length} arguments");
            invocation.Proceed();
        }
    }

Ahora sí, vamos a crear un test para comprobar que nuestro interceptor funciona bien cuando lo añadimos a un proxy:

1
2
3
4
5
6
7
8
9
10
11
    [Fact]
    public void AddInterceptor_To_Proxy_Test()
    {
        Mock<IWriterService> writerServiceMock = new Mock<IWriterService>();
        ProxyGenerator proxyGenerator = new ProxyGenerator();
        var user = proxyGenerator.CreateClassProxy<User>(new LoggingInterceptor(writerServiceMock.Object));
 
        user.GreetsTo("Pepito");
 
        writerServiceMock.Verify(w=>w.Write(It.IsAny<string>()),Times.Once);
    }

El test nos dice que, efectivamente, el método write se ha llamado una vez. Hemos utilizado el método GreetsTo, pero también nos funcionaría con las propiedades de User:

1
2
3
4
5
6
7
8
9
10
11
    [Fact]
    public void Properties_Are_Intercepted_Test()
    {
        Mock<IWriterService> writerServiceMock = new Mock<IWriterService>();
        ProxyGenerator proxyGenerator = new ProxyGenerator();
        var user = proxyGenerator.CreateClassProxy<User>(new LoggingInterceptor(writerServiceMock.Object));
 
        user.Name = "Juan";
 
        writerServiceMock.Verify(w => w.Write(It.IsAny<string>()), Times.Once);
    }

Se pone en verde sin problema. ¿Y qué pasaría si en el interceptor quitásemos la llamada a invocation.Proceed()?

1
2
3
4
5
6
    public class CancelCallInterceptor : IInterceptor
    {
        public void Intercept(IInvocation invocation)
        {
        }
    }

Como vemos es un interceptor vacío (perdón por el nombre), es decir, no devuelve la llamada al método original (o siguiente interceptor).

1
2
3
4
5
6
7
8
9
10
    [Fact]
    public void CancelCallInterceptor_Stops_Call_Test()
    {
        ProxyGenerator proxyGenerator = new ProxyGenerator();
        var user = proxyGenerator.CreateClassProxy<User>(new CancelCallInterceptor());
 
        user.Name = "Juan";
 
        Assert.Null(user.Name);
    }

Podemos ver que, aunque hemos establecido la propiedad Name del usuario, como el interceptor no devuelve la llamada al método original (el set de la propiedad) nunca se llega a establecer.
En el test que vamos a ver a continuación, comprobaremos cómo podemos añadir más de un interceptor y cómo afecta el orden en que los añadimos:

1
2
3
4
5
6
7
8
9
10
11
12
13
    [Fact]
    public void MultipleInterceptors_Are_Added_In_Order_Test()
    {
        Mock<IWriterService> writerServiceMock = new Mock<IWriterService>();
        ProxyGenerator proxyGenerator = new ProxyGenerator();
        var user = proxyGenerator.CreateClassProxy<User>(new LoggingInterceptor(writerServiceMock.Object),
                                                    new CancelCallInterceptor());
 
        user.Name = "Juan";
 
        writerServiceMock.Verify(w => w.Write(It.IsAny<string>()), Times.Once);
        Assert.Null(user.Name);
    }

El test nos está diciendo que los dos interceptores se están aplicando puesto que el método Write de writerService se llama un vez y el set de la propiedad nunca se llega a invocar puesto que CancelCallInterceptor no hace invocation.Proceed(). Lo que hace el método invocation.Proceed() de LoggingInterceptor es pasar la ejecución al siguiente interceptor, en este caso CancelCallInterceptor y, como CancelCallInterceptor no llama a invocation.Proceed(), finaliza la ejecución. Al final Proceed lo que hace es pasar la ejecución al siguiente interceptor y si no quedan más interceptores, la pasa a la llamada original.
Un poco más arriba comenté que ProxyGenerator posiblemente nos interesaría que fuese estático; quizá en los tests puede que no nos importe demasiado, pero en el código de la aplicación mejora el rendimiento, ¿por qué?, vamos a ver un test:

1
2
3
4
5
6
7
8
9
10
11
    [Fact]
    public void DifferentsInstancesOf_ProxyGenerator_GeneratesDifferentTypes_Test()
    {
        ProxyGenerator proxyGenerator1 = new ProxyGenerator();
        ProxyGenerator proxyGenerator2 = new ProxyGenerator();
 
        var user1 = proxyGenerator1.CreateClassProxy<User>();
        var user2 = proxyGenerator2.CreateClassProxy<User>();
 
        Assert.NotEqual(user1.GetType(),user2.GetType());
    }

Diferentes instancias de ProxyGenerator generan diferentes tipos cuando solicitamos un proxy de la clase User. ¿Qué pasará si utilizamos el mismo ProxyGenerator?

1
2
3
4
5
6
7
8
9
10
    [Fact]
    public void SameInstanceOf_ProxyGenerator_GeneratesSameTypes_Test()
    {
        ProxyGenerator proxyGenerator = new ProxyGenerator();
 
        var user1 = proxyGenerator.CreateClassProxy<User>();
        var user2 = proxyGenerator.CreateClassProxy<User>();
 
        Assert.Equal(user1.GetType(), user2.GetType());
    }

En este caso si utilizamos la misma instancia de ProxyGenerator sí genera el mismo tipo al generar dos proxys de User. Además de que seguramente en la mayoría de los casos prefiramos que el comportamiento sea este (o por lo menos nos dé igual), en cuanto a rendimiento sí hay diferencias. Al final lo que está utilizando es Reflection (siempre se dice que es lento…) y cuando utilizamos la misma instancia se encarga de cachear el tipo generado y cada vez que solicitemos nuevos proxys de un tipo ya cacheado va a reutilizar el tipo que previamente había generado. Aunque normalmente los temas de rendimiento no suelen ser críticos o no nos suele importar demasiado unos milisegundos, en este caso sí que la diferencia es notable.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
    Stopwatch stopwatch = new Stopwatch();
    stopwatch.Start();
    for (int i = 0; i < 1000; i++)
    {
        ProxyGenerator proxyGenerator = new ProxyGenerator();
        proxyGenerator.CreateClassProxy<User>();
    }
    stopwatch.Stop();
    //ElapsedMilliseconds = 19752
 
    ProxyGenerator proxyGenerator2 = new ProxyGenerator();
    stopwatch.Restart();
    for (int i = 0; i < 1000; i++)
    {
        proxyGenerator2.CreateClassProxy<User>();
    }
    stopwatch.Stop();
    //ElapsedMilliseconds = 25

Reutilizando la instancia ha tardado 25 milisegundos y sin reutilizarla casi 20.000 milisegundos. Ya no es que la diferencia sea muy grande, sino que realmente nos damos cuenta que crear el tipo de tiempo de ejecución es muy lento (ha tardado casi 20 segundos en crear 1000). En resumen, normalmente nos va a convenir utilizar la misma instancia de ProxyGenerator en nuestra aplicación. Incluso es posible cachear los tipos y reutilizarlos entre diferentes ejecuciones de nuestra aplicación (más info aquí).

Hasta aquí el post de hoy. Hemos dado un repaso a cómo crear proxys y para qué nos pueden servir. En la siguiente entrada veremos otras características que nos van a servir para escoger los métodos que queremos interceptar, si nos interesa descartar algunos interceptores y algunas cosas más. Además del método CreateInterfaceProxyWithTargetInterface que nos ha quedado pendiente.

¡Un saludo!

3 thoughts on “Creando proxys con Castle DynamicProxy”

Deja un comentario

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