Crear método genérico en tiempo de ejecución

¡Hola!
El otro día se dio el caso de que, utilizando la librería Akavache, necesitaba invocar un método genérico decidiendo el tipo en tiempo de ejecución. Me explico, el método que necesitaba ejecutar tenía la siguiente firma:

1
    static IObservable<T> GetObject<T>(this IBlobCache This, string key)

Realmente lo que vemos es un método de extensión al que le pasamos una key y nos devuelve el objeto correspondiente a esa key. Lo usaríamos de la siguiente forma:

1
2
3
4
5
6
7
8
9
10
11
12
13
    class Person
    {
        public string Name { get; set; }
 
        public Person(string name)
        {
            this.Name = name;
        }
    }
    private static async Task DummyPerson()
    {
        var dummyPerson = await Akavache.BlobCache.LocalMachine.GetObject<Person>("dummy-key");
    }

Aquí simplemente buscamos una persona que tenga la clave dummy-key y Akavache se va a encargar de buscarla en su sistema de almacenamiento y deserializar el objeto cuya clave sea dummy-key al tipo Person. Pero, ¿y qué pasa si por ejemplo sé la clave pero no sé el tipo al que se tiene que deserializar en tiempo de compilación sino que se va a decidir en tiempo de ejecución?
Por ejemplo, sé en tiempo de ejecución que la clave dummy-key va a ser de tipo Person, pero no a la hora de compilar. Esto puede darse a lo mejor si utilizamos un patrón command, donde tendremos un CommandBase y Commands específicos. Lo normal sería que la librería, además del método con genéricos, nos proporcionase la opción de pasarle la clave y el tipo:

1
    var command = await Akavache.BlobCache.LocalMachine.GetObject("dummy-command", typeof(ConcreteCommand));

Como digo, en el caso de Akavache no proporciona un método así.
Vamos a ver cómo crearlo y aplicarlo con un patrón command muy simplificado en donde simplemente vamos a saber la clave y el tipo del command en tiempo de ejecución, y debemos obtener y crear el command concreto para ese tipo. Para ello vamos a utilizar reflection.
Vamos a imaginarnos que (simplificando mucho) tenemos algo así:

1
2
3
4
5
6
7
8
9
10
11
12
13
    class CommandBase
    {
        public virtual void Execute() { }
    }
 
    class ConcreteCommand:CommandBase
    {
        public int Data { get; set; }
        public override void Execute()
        {
            System.Diagnostics.Debug.WriteLine("ConcreteCommand with " + Data.ToString());
        }
    }

¿Cómo podemos obtener un command dada su key y su tipo? De la siguiente forma:

1
2
3
4
5
6
7
8
9
10
11
    private async Task<CommandBase> GetCommand(string key, Type type)
    {
        //Creamos el método genérico GetObject<IObservable<CommandBase>>(string key)
        MethodInfo getObjectMethod = Akavache.BlobCache.LocalMachine.GetType().
                             GetMethod("GetObject", new Type[] { typeof(string) }).//Decimos que obtenga un método GetObject que recibe un único parámetro que sea de tipo string
                             MakeGenericMethod(new Type[] { type }); //Hacemos el método genérico cuyo tipo de retorno es IObservable<CommandBase>
 
        //Invocamos nuestro método GetObject y lo casteamos a IObservable<Person> 
        var command = await (IObservable<CommandBase>)getObjectMethod.Invoke(Akavache.BlobCache.LocalMachine, new object[] { key });
        return command;
        }

En mi caso concreto lo tuve que hacer en un proyecto PCL (para Xamarin Forms), por lo tanto tendríamos que cambiar en la línea 5, donde pone GetMethod por GetRuntimeMethod. A la anterior función la llamaríamos así:

1
2
    //Obviamente los strings clave y tipo los tendríamos en tiempo de ejecución...
    var command = await GetCommand("command2-key", Type.GetType("Namespace.ConcreteCommand, GenericMethods, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"));

Es posible que el ejemplo no haya sido del todo acertado, en cualquier caso, hemos visto cómo poder crear métodos genéricos en ejecución. Siempre que hacemos este tipo de cosas hay que tener un ojo puesto al rendimiento si es algo crítico para nosotros, pero normalmente (al menos en mi caso) las diferencias pueden ser despreciables.
¡Un saludo!

Deja un comentario

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