Hook, Selector, Mixin y más con DynamicProxy

¡Hola!
Después de ver qué son y cómo podemos crear proxys con DynamicProxy, hoy vamos a ver algunas características que nos ofrece para poder mejorar estos proxys, separar ciertas responsabilidades y alguna cosa que se quedó pendiente en el anterior post. Vamos a ello.
Si revisamos los parámetros y sobrecargas que tienen los diferentes métodos de creación de proxys (CreateClassProxy, CreateInterfaceProxyWithTargetInterface…) todas ellas comparten, entre otros, un parámetro options de tipo ProxyGenerationOptions en el que podemos configurar varias propiedades. En concreto nos vamos a centrar en tres de ellas, a saber, Hook, Selector y Mixin. Hay alguna otra como BaseTypeForInterfaceProxy que, como su nombre indica, se utiliza para especificar el tipo base que queremos que tengan nuestros proxys cuando los creamos a partir de una interfaz (por ejemplo con el método CreateInterfaceProxyWithoutTarget).

IProxyGenerationHook

Vamos a imaginar que en nuestro LoggingInterceptor (que vimos en la anterior entrada) queremos dejar traza de todo excepto de las llamadas a propiedades. En primer lugar vamos a crear el test que compruebe que no se deja traza de los getter y setters de las propiedades:

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

Recuerdo por si acaso la clase User:

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.Write($"Hi, {user}!");
        }
    }

Si ejecutamos el test con la implementación que teníamos de LoggingInterceptor va a fallar. El siguiente paso es modificar LoggingInterceptor para que nuestro test pase, es decir, que si la llamada es un getter o setter de una propiedad, no se deje traza:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
    public class LoggingInterceptor : IInterceptor
    {
        private readonly IWriterService writer;
 
        public LoggingInterceptor(IWriterService writer)
        {
            this.writer = writer;
        }
        public void Intercept(IInvocation invocation)
        {
            if (IsGetterOrSetter(invocation) == false)
                writer.Write($"Invoke method: {invocation.Method.Name} with {invocation.Arguments.Length} arguments");
            invocation.Proceed();
        }
 
        private bool IsGetterOrSetter(IInvocation invocation)
        {
            return invocation.Method.IsSpecialName &&
                   (invocation.Method.Name.StartsWith("set_") || invocation.Method.Name.StartsWith("get_"));
        }
    }

Antes de que te eches las manos a la cabeza por la línea if (IsGetterOrSetter(invocation) == false), tengo que decir que me parece que queda más claro que if (!IsGetterOrSetter(invocation)), aunque también se podría crear un método IsNotGetterOrSetter… bueno, esto es otra batalla 🙂 Si ahora ejecutamos el test se pone el verde. ¿Cuál es el problema aquí? Que ahora si en otro punto de la aplicación sí que nos interesa dejar traza de las propiedades, o sólo de las propiedades y no de los métodos o cualquier otra combinación tenemos un problema. Podemos parametrizar LoggingInterceptor para las diferentes combinaciones o crear diferentes Interceptors, pero no parece la mejor solución si este tipo de cosas también se van a tener que aplicar a otros Interceptors diferentes (podríamos crear un interceptor base y que herede de él quien lo requiera). Tiene pinta de que no debería ser responsabilidad de LoggingInterceptor decidir si la invocación debe o no dejar traza, más bien debería únicamente recibir la invocación y sin más comprobaciones dejar la traza. Para solucionar esto podemos añadir utilizar la interfaz IProxyGenerationHook, que tiene el siguiente aspecto:

1
2
3
4
5
6
    public interface IProxyGenerationHook
    {
        void MethodsInspected();
        void NonProxyableMemberNotification(Type type, MemberInfo memberInfo);
        bool ShouldInterceptMethod(Type type, MethodInfo methodInfo);
  }

El método MethodsInspected se va a ejecutar cuando se hayan inspeccionado todos los miembros de la clase de la que se va a solicitar un proxy. El método NonProxyableMemberNotification se va a lanzar para cada miembro de la clase que no pueda ser capturado por un proxy, en resumen, para los miembros que no sean virtual. El último método, ShouldInterceptMethod, es seguramente el que más utilicemos; se va a invocar para cada método (que sea virtual) de la clase y si devolvemos true el miembro será proxiable y en caso de false no. Es decir, que si devolvemos false para un método, cuando éste se invoque no va a pasar a través de los interceptores.
Vamos a crear un Hook que excluya los getter y setter de las propiedades:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
    public class ExcludePropertiesHook : IProxyGenerationHook
    {
        public void MethodsInspected()
        {
 
        }
 
        public void NonProxyableMemberNotification(Type type, MemberInfo memberInfo)
        {
        }
 
        public bool ShouldInterceptMethod(Type type, MethodInfo methodInfo)
        {
            return IsGetterOrSetter(methodInfo) == false;
        }
 
        private bool IsGetterOrSetter(MethodInfo methodInfo)
        {
            return methodInfo.IsSpecialName && 
                   (methodInfo.Name.StartsWith("set_") || methodInfo.Name.StartsWith("get_"));
        }
    }

Ahora vamos a añadir un test que compruebe que si a la implementación original (la que vimos en el anterior post) de LoggingInterceptor le añadimos ExcludePropertiesHook no deja traza en los setters/getters de las propiedades.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
    [Fact]
    public void HookThat_NotInterceptProperties_Test()
    {
        Mock<IWriterService> writerServiceMock = new Mock<IWriterService>();
        ProxyGenerator proxyGenerator = new ProxyGenerator();
        ProxyGenerationOptions options = new ProxyGenerationOptions()
        {
            Hook = new ExcludePropertiesHook()
        };
 
        var user = proxyGenerator.CreateClassProxy<User>(options, new LoggingInterceptor(writerServiceMock.Object));
 
        user.Name = "Pepe";
        var userName = user.Name;
        writerServiceMock.Verify(w=>w.Write(It.IsAny<string>()),Times.Never); 
    }

Efectivamente el test se pone en verde. Lo que hemos hecho es añadir nuestro Hook a ProxyGenerationOptions y, cuando solicitamos la creación de un proxy, lo incluimos. Hemos visto que añadir un Hook a la creación de nuestros proxys ha hecho que nuestro interceptor quede más limpio y que las responsabilidades se hayan separado.

IInterceptorSelector

Esta funcionalidad nos va a permitir algo bastante interesante, seleccionar los interceptores que queremos tener para cada método. La interfaz sólo define un método:

1
2
3
4
    public interface IInterceptorSelector
    {
        IInterceptor[] SelectInterceptors(Type type, MethodInfo method, IInterceptor[] interceptors);
    }

El método SelectInterceptors será llamado una vez por cada método y por instancia de proxy, es decir, si creamos un proxy de User y accedemos al getter de la propiedad Name, pasará por el método SelectInterceptors la primera vez para la instancia que hayamos creado, la segunda vez que accedamos al getter de la propiedad Name no va a volver a pasar por SelectInterceptors. En caso de que creemos una nuevo proxy de User sí que se va a volver a llamar a SelectInterceptors (la primera vez).
¿Para qué podemos utilizar IInterceptorSelector? Por ejemplo si queremos eliminar algunos interceptores para ciertos métodos, o leemos desde algún archivo o propiedad de configuración un valor para indicar si ciertos interceptores están o no activados. Puedes pensar que IInterceptorSelector es bastante similar a los Hooks que vimos en el apartado anterior y en cierta parte es así, pero tienen diferencias bastante claras. Un Hook se utilizan para hacer que los métodos pasen o no por (todos) los interceptores, en cambio con los Selectors decimos qué interceptores queremos que tenga un método, es decir, un Hook aplica al total de interceptores y un Selector define qué interceptores queremos utilizar. Otra diferencia es en qué momento actúan, los Hooks actúan cuando se crea el proxy y una única vez por tipo de proxy creado, en cambio un Selector actúa en el momento de acceder a un método del proxy y una vez por instancia de proxy.
Vamos a ver un ejemplo. Nos imaginamos que estamos creando proxys y les estamos añadiendo el interceptor para dejar trazas de las llamadas a los métodos (LoggingInterceptor) y también queremos que los métodos decorados con el atributo [Retry] tengan una política de 3 reintentos en caso de fallo. En este caso no sería posible utilizar Hooks ya que sólo nos permite que se utilizasen todos los interceptores o ninguno, pero creando un IInterceptorSelector sí podemos hacer esto. En primer lugar vamos a ver nuestro RetryInterceptor:

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 RetryInterceptor : IInterceptor
    {
        public void Intercept(IInvocation invocation)
        {
            var tryNumber = 0;
            do
            {
                try
                {
                    invocation.Proceed();
                    return;
                }
                catch (Exception)
                {
                    tryNumber++;
                    if (tryNumber == 3)
                    {
                        throw;
                    }
                }
            } while (true);
        }
    }

Además de que el código no es especialmente bonito (tampoco me parece un problema en este caso), aplicar una política de reintentos tan agresiva sin esperar cierto tiempo entre llamadas puede no ser la mejor opción, aunque supongo que dependerá del contexto. Dejando esto de lado, vamos a ver nuestro servicio que contiene el atributo Retry:

1
2
3
4
5
    public interface IGreetingsService
    {
        [Retry]
        void Greets(User origin, User destination);
    }

Es hora de crear el test que compruebe que si nuestro método contiene el atributo Retry el interceptor RetryInterceptor no se elimina… aunque quizá ese test no sea demasiado correcto porque nos va a ser un poco complicado pasar por el rojo antes que por el verde. Vamos a comprobar que un método que no esté decorado con [Retry] no utiliza RetryInterceptor. Como nos va a ser un poco complicado testear RetryInterceptor, vamos a sacar código al exterior para poder mockearlo.

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
    public interface IRetry
    {
        void Do(Action action, int attempts = 3);
    }
    public class Retry: IRetry
    {
        public void Do(Action action, int attempts = 3)
        {
            int tryNumber = 0;
            do
            {
                try
                {
                    action.Invoke();
                    return;
                }
                catch (Exception)
                {
                    tryNumber++;
                    if (tryNumber == attempts)
                    {
                        throw;
                    }
                }
            } while (true);
        }
    }
 
    public class RetryInterceptor : IInterceptor
    {
        private readonly IRetry retry;
 
        public RetryInterceptor(IRetry retry)
        {
            this.retry = retry;
        }
 
        public void Intercept(IInvocation invocation)
        {
            retry.Do(invocation.Proceed);
        }
    }

Ahora vamos a crear nuestro selector. En primer lugar lo vamos a dejar sin funcionalidad:

1
2
3
4
5
6
7
    public class RetryInterceptorSelector : IInterceptorSelector
    {
        public IInterceptor[] SelectInterceptors(Type type, MethodInfo method, IInterceptor[] interceptors)
        {
            return interceptors;
        }
    }

Ahora creamos el test que compruebe que el interceptor RetryInterceptor se excluya si hemos añadido a las ProxyGenerationOptions el selector que acabamos de ver:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
    [Fact]
    public void SelectorThat_OnlyExcludesRetryInterceptor_ForMethodsWithoutRetryAttribute_Test()
    {
        Mock<IRetry> retryMock = new Mock<IRetry>();
        ProxyGenerator proxyGenerator = new ProxyGenerator();
        ProxyGenerationOptions options = new ProxyGenerationOptions()
        {
            Selector = new RetryInterceptorSelector(),
        };
        var user = proxyGenerator.CreateClassProxy<User>(options, 
                new LoggingInterceptor((new WriterService()), new RetryInterceptor(retryMock.Object));
 
        user.GreetsTo("Juanito");
 
        retryMock.Verify(r=>r.Do(It.IsAny<Action>(),It.IsAny<int>()),Times.Never);
    }

Ahora mismo si ejecutamos el test falla porque no hemos añadido la funcionalidad a RetryInterceptorSelector y el método Do se ejecuta una vez. El siguiente paso es añadir la funcionalidad:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
    public class RetryInterceptorSelector : IInterceptorSelector
    {
        public IInterceptor[] SelectInterceptors(Type type, MethodInfo method, IInterceptor[] interceptors)
        {
            if (ContainsRetryAttribute(method))
                return interceptors;
 
            return interceptors.Where(i => !(i.GetType() == typeof(RetryInterceptor))).ToArray();
        }
 
        private bool ContainsRetryAttribute(MethodInfo method)
        {
            return method.GetCustomAttribute<RetryAttribute>() != null;
        }
    }

Ahora si ejecutamos el test sí se pone en verde, es decir, RetryInterceptor se elimina cuando llamamos al método GreetsTo de user. Realmente este test no nos asegura el caso contrario, cuando sí que ejecutamos un método con el atributo Retry. Vamos a crear un test que lo compruebe:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
    [Fact]
    public void RetryInterceptorIsUsed_When_MethodContainsRetryAttribute_Test()
    {
        Mock<IRetry> retryMock = new Mock<IRetry>();
        ProxyGenerator proxyGenerator = new ProxyGenerator();
        ProxyGenerationOptions options = new ProxyGenerationOptions()
        {
            Selector = new RetryInterceptorSelector(),
        };
        var greetingsService = proxyGenerator.CreateInterfaceProxyWithoutTarget<IGreetingsService>(options,
            new LoggingInterceptor(new WriterService()), new RetryInterceptor(retryMock.Object));
 
        greetingsService.Greets(new User(),new User());
 
        retryMock.Verify(r => r.Do(It.IsAny<Action>(), It.IsAny<int>()), Times.Once());
    }

Efectivamente el test pasa, la funcionalidad es correcta. Como comentaba antes, podríamos crear un Selector para poder activar o desactivar ciertos interceptores en base a alguna configuración. Por ejemplo podemos tener en un archivo de configuración una propiedad para indicar si queremos utilizar o no el interceptor LoggingInterceptor y con un Selector comprobar ese valor y eliminar el interceptor en caso de que la propiedad sea false. Aquí hay que tener en cuenta que, como dijimos antes, si el cambio lo hacemos en caliente (sin reiniciar la aplicación) los métodos de los proxys que ya hayan sido invocados no van a volver a pasar por el Selector, por lo que no serán evaluados de nuevo.

Mixin

Creo que nunca he utilizado mixins, así que es posible que los ejemplos que ponga quizá no se acerquen demasiado a los usos reales. Un mixin es una clase o bloque de código que contiene cierta funcionalidad que será utilizada por un tercero (otra clase) pero no de forma autónoma, es decir, tenemos una clase que por ejemplo expone un método para realizar cierta funcionalidad y nos interesa añadir esa funcionalidad a otra clase. Aquí tenemos la opción de heredar de esa clase, además de los problemas propios de la herencia, en C# no hay opción de herencia múltiple, o de composición. Lo malo de estas dos opciones es que estamos añadiendo una dependencia directa entre las clases y seguramente no queramos esto. Los mixin nos van a proporcionar la posibilidad de añadir la funcionalidad sin ensuciar nuestra clase original con la clase que proporciona la funcionalidad (por lo menos en tiempo de compilación).
Como no se me ocurre ningún ejemplo bueno, vamos a suponer que tenemos la clase User que hemos estado utilizando, y le queremos añadir un identificador único. Tenemos una interfaz IEntity y una implementación Entity:

1
2
3
4
5
6
7
8
9
    public interface IEntity
    {
        string Id { get; }
    }
 
    public class Entity : IEntity
    {
        public string Id { get; } = Guid.NewGuid().ToString();
    }

Gracias a los mixins, vamos a poder añadir la capacidad de tener un identificador a nuestra clase User sin que User sepa que existe un IEntity y sin que IEntity sepa que existe un User:

1
2
3
4
5
6
7
8
9
10
11
12
13
    [Fact]
    public void SimpleMixin_Test()
    {
        ProxyGenerator proxyGenerator = new ProxyGenerator();
        ProxyGenerationOptions options = new ProxyGenerationOptions();
        options.AddMixinInstance(new Entity());
 
        var user = proxyGenerator.CreateClassProxy<User>(options);
 
        Assert.True(user is IEntity);
        IEntity entity = (IEntity)user;
        Assert.NotNull(entity.Id);
    }

Lo único que tenemos que hacer es añadir a las ProxyGenerationOptions una instancia de la clase que queremos añadir como funcionalidad y automáticamente nuestro proxy tiene esa funcionalidad. Realmente lo que va a hacer al crear el mixin, es que nuestra clase user implemente la interfaz IEntity con la implementación de Entity; por eso es necesario que la instancia que añadamos como mixin sea una implementación de una interfaz.
Es posible añadir varios mixin a la vez. Por ejemplo, además de la capacidad de tener un identificador, queremos que User sea capaz de almacenar más propiedades de las que tenemos definidas en User actualmente (Name):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
    public interface IPropertiesContainer
    {
        object this[string key] { get; }
        void AddProperty(string name, object value);
    }
    public class PropertiesContainer : IPropertiesContainer
    {
        private readonly IDictionary<string,object> propertiesDictionary = new Dictionary<string, object>();
        public object this[string name]
        {
            get { return propertiesDictionary[name]; }
        }
 
        public void AddProperty(string name, object value)
        {
            propertiesDictionary[name] = value;
        }
    }

Esta funcionalidad realmente la podríamos obtener añadiendo como mixin directamente el Dictionary (que es implementación de IDictionary), pero creo que va a quedar más claro así. Vamos a crear un test que compruebe que podemos añadir a nuestra clase User la funcionalidad de Entity y de PropertiesContainer:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
    [Fact]
    public void TwoMixin_Test()
    {
        ProxyGenerator proxyGenerator = new ProxyGenerator();
        ProxyGenerationOptions options = new ProxyGenerationOptions();
        options.AddMixinInstance(new Entity());
        options.AddMixinInstance(new PropertiesContainer());
 
        var user = proxyGenerator.CreateClassProxy<User>(options);
 
        Assert.True(user is IEntity);
        IEntity entity = (IEntity)user;
        Assert.NotNull(entity.Id);
 
        Assert.True(user is IPropertiesContainer);
        IPropertiesContainer container = (IPropertiesContainer)user;
        container.AddProperty("Telephone", 666666666);
        Assert.Equal(666666666, container["Telephone"]);
    }

El test se pone en verde. También puede que tengamos una interfaz que implemente varias interfaces a su vez:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
    public interface IEntityWithPropertiesContainer : IEntity, IPropertiesContainer
    {
 
    }
 
    public class EntityWithPropertiesContainer : IEntityWithPropertiesContainer
    {
        public string Id { get; } = Guid.NewGuid().ToString();
 
        private readonly IDictionary<string, object> propertiesDictionary = new Dictionary<string, object>();
        public object this[string name]
        {
            get { return propertiesDictionary[name]; }
        }
 
        public void AddProperty(string name, object value)
        {
            propertiesDictionary[name] = value;
        }
    }

A la hora de crear el mixin tampoco tendríamos mayor problema. La instancia que creemos va a implementar IEntityWithPropertiesContainer, por lo tanto también implementará IEntity y IPropertiesContainer:

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
    [Fact]
    public void Mixin_With_MultipleInterfaces_Test()
    {
        ProxyGenerator proxyGenerator = new ProxyGenerator();
        ProxyGenerationOptions options = new ProxyGenerationOptions();
        options.AddMixinInstance(new EntityWithPropertiesContainer());
 
        var user = proxyGenerator.CreateClassProxy<User>(options);
 
        Assert.True(user is IEntity);
        Assert.True(user is IPropertiesContainer);
        Assert.True(user is IEntityWithPropertiesContainer);
 
        IEntity entity = (IEntity)user;
        Assert.NotNull(entity.Id);
 
        IPropertiesContainer container = (IPropertiesContainer)user;
        container.AddProperty("Telephone", 666666666);
        Assert.Equal(666666666, container["Telephone"]);
 
        IEntityWithPropertiesContainer entityWithProperties = (IEntityWithPropertiesContainer)user;
        Assert.NotNull(entityWithProperties.Id);
        entityWithProperties.AddProperty("Surname", "Pérez");
        Assert.Equal("Pérez", entityWithProperties["Surname"]);
    }

Podemos ver que es bastante fácil, flexible y potente crear mixin. No obstante, si lo pensamos un poco, hay ciertos casos en los que no sería necesario crear un mixin y un método de extensión (a lo mejor junto con una interfaz marcadora) haría las veces de mixin. Por ejemplo, queremos añadir a User la posibilidad de clonarse (crear una copia). Creamos una interfaz y una implementación:

1
2
3
4
5
6
7
8
9
10
11
    public interface ICloneable<T>
    {
        T Clone(T obj);
    }
    public class Cloneable<T> : ICloneable<T>
    {
        public T Clone(T obj)
        {
            return JsonConvert.DeserializeObject<T>(JsonConvert.SerializeObject(obj));
        }
    }

Ahora vamos a crear un test que compruebe que si añadimos un mixin a nuestro User se puede clonar:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
    [Fact]
    public void Add_Cloneable_As_Mixin_Test()
    {
        ProxyGenerator proxyGenerator = new ProxyGenerator();
        ProxyGenerationOptions options = new ProxyGenerationOptions();
        options.AddMixinInstance(new Cloneable<User>());
 
        var user = proxyGenerator.CreateClassProxy<User>(options);
 
        Assert.True(user is ICloneable<User>);
 
        user.Name = "Jaimito";
        ICloneable<User> cloneable = (ICloneable<User>)user;
        var clonedUser = cloneable.Clone(user);
 
        Assert.NotEqual(user,clonedUser);
        Assert.Equal("Jaimito", clonedUser.Name);
    }

Seguramente deberíamos elaborarlo un poco más para que no fuese necesario pasar la referencia de user al método clone, quizá escondiendo detrás de una factoría la creación del proxy y añadiendo una referencia de user a la instancia Cloneable. En cualquier caso, como ejemplo nos sirve. La cosa es que todo esto que hemos montando lo podríamos cambiar por un método de extensión fácilmente:

1
2
3
4
5
6
7
    public static class Extensions
    {
        public static T Clone<T>(this T obj)
        {
            return JsonConvert.DeserializeObject<T>(JsonConvert.SerializeObject(obj));
        }
    }

Creamos un test por si acaso:

1
2
3
4
5
6
7
8
9
10
11
    [Fact]
    public void CloneUser_With_ExtensionMethod_Test()
    {
        var user = new User();
 
        user.Name = "Jaimito";
        var clonedUser = user.Clone();
 
        Assert.NotEqual(user, clonedUser);
        Assert.Equal("Jaimito", clonedUser.Name);
    }

Vemos que en este caso es bastante más sencillo utilizar un método de extensión y realmente User no sabe que existe el método de extensión y viceversa.

CreateInterfaceProxyWithTargetInterface

Realmente esto no tiene mucha relación con esta entrada, pero estaba pendiente por explicar. Si recordamos, en la entrada vimos otra opción de crear un proxy con un nombre muy similar, CreateInterfaceProxyWithTarget. La principal diferencia es que nos va a permitir intercambiar el objetivo de la invocación que hacemos en el interceptor. Por ejemplo, nos imaginamos que tenemos una clase que nos proporciona noticias a través de un servicio web y queremos que, cuando no haya conexión a Internet, en vez de llamar al servicio web devolvamos las últimas noticias que el usuario tenga almacenado en el dispositivo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
    public interface INewsService
    {
        IEnumerable<News> GetLastestNews();
    }
    public interface InMemoryService { }
 
    public class NewsService: INewsService
    {
        public IEnumerable<News> GetLastestNews()
        {
            //Obtendríamos las noticias de algún servicio web por ejemplo
            return new[] { new News() { Title = "2017", Content = "Otra vez campeón de Europa" }};
        }
    }
    public class StoredNewsService: INewsService, InMemoryService
    {
        public readonly News[] storedNews = new News[]
            {new News(){Title = "1960", Content = "Otra vez campeón de Europa"}, };
        public IEnumerable<News> GetLastestNews()
        {
            //Obtendríamos por ejemplo las últimas noticias almacenadas en el dispositivo
            return storedNews;
        }
    }

(La interfaz InMemoryService la podríamos obviar). Lo que nos va a permitir CreateInterfaceProxyWithTargetInterface es la capacidad de utilizar NewsService o InMemoryNewsService de forma transparente y sin que ninguna de ambas clases sepan nada de la otra o que dependen de la conexión a internet. Esto lo vamos a conseguir gracias a que el invocation que recibamos en el interceptor implementa IChangeProxyTarget y dispone de un método para intercambiar el objetivo de la invocación, en nuestro caso cuando no haya conexión cambie NewsService por InMemoryNewsService. Vemos el interceptor:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
    public class ConnectionServiceInterceptor:IInterceptor
    {
        private readonly InMemoryService inMemoryService;
        private readonly IConnectionService connectionService;
 
        public ConnectionServiceInterceptor(InMemoryService inMemoryService,
            IConnectionService connectionService)
        {
            this.inMemoryService = inMemoryService;
            this.connectionService = connectionService;
        }
        public void Intercept(IInvocation invocation)
        {
            if (connectionService.IsAvailable() == false)
            {
                ((IChangeProxyTarget)invocation).ChangeInvocationTarget(inMemoryService);
            }
            invocation.Proceed();
        }
    }

Ahora creamos un test para comprobar que cuando la conexión no está disponible se utiliza inMemoryService:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
    [Fact]
    public void ChangeProxy_Test()
    {
        Mock<IConnectionService> connectionServiceMock = new Mock<IConnectionService>();
        ProxyGenerator proxyGenerator = new ProxyGenerator();
        INewsService newsService = proxyGenerator.CreateInterfaceProxyWithTargetInterface<INewsService>
            (new NewsService(),new ConnectionServiceInterceptor(new InMemoryNewsService(), connectionServiceMock.Object));
 
        connectionServiceMock.Setup(c => c.IsAvailable()).Returns(true);
        var lastestNews = newsService.GetLastestNews();
 
        Assert.Collection(lastestNews,news => news.Title="2017");
 
        connectionServiceMock.Setup(c => c.IsAvailable()).Returns(false);
        lastestNews = newsService.GetLastestNews();
 
        Assert.Collection(lastestNews, news => news.Title = "1960");
    }

Hay que tener en cuenta un detalle importante y es que se cambia el objetivo sobre la invocación actual, no definitivamente.

Conclusiones

Hemos dado un repaso por las características principales de DynamicProxy y, como todo, tiene sus pros y contras. Las ventajas principales es que nos permite añadir funcionalidades adicionales sin tocar las clases originales y podemos aplicar técnicas de AOP, decorators, mixins y más cosas chulas 🙂 Ya vimos que proyectos potentes como Moq la utilizan. Como contras, las principales creo que son la magia que se añade que puede hacer que depurar sea más complicado y que todo lo que queramos que sea perceptible de ser interceptado tiene que ser virtual (y las clases no sealed). También, aunque se encarga de cachear los tipos creados, hemos visto que no es una operación trivial y los tiempos no son despreciables. En cualquier caso, es una excelente librería a tener en cuenta y que puede ayudarnos mucho si la utilizamos de forma adecuada.
¡Un saludo!

One thought on “Hook, Selector, Mixin y más con DynamicProxy”

Deja un comentario

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