Cómo crear dinámicamente un contexto de Entity Framework Core en tiempo de ejecución

A medida que me introduzco más en arquitecturas de microservicios, trato de explotar sus diferentes ventajas. Una de ellas es poder usar una tecnología diferente para cada microservicio, por lo que es la oportunidad perfecta para explorar dotNet Core.

Ya he creado una entrada previa centrada en Core, para validar un token generado desde un servicio creado con .Net Framework. Ahora presento esta otra entrada donde he vuelto a aprovechar la arquitectura de Unit of Work, Repository Pattern y Entity Framework Code First de la que hablo en una de mis primeras publicaciones, en esta ocasión se trata de usarla con Entity Framework Core y además reforzarla con la creación de contextos de datos de forma dinámica en tiempo de ejecución y manteniéndola desacoplada de las demás capas, de forma que Entity Framework sea algo particular de la capa de datos.

Ya comenté que esta arquitectura presentaba ciertas ventajas y me gustaba para darle versatilidad a mis desarrollos, manteniendo toda la lógica de negocio aislada de la tecnología de persistencia de datos utilizada. Esto se puede extrapolar a core sin mayor dificultad, simplemente cambiando las librerías por las de dotNet Core de forma adecuada para utilizar Entity Framework Core.

En esa entrada además hablo sobre la importancia de aplicar DI para resolver las interfaces con sus implementaciones correspondientes, en .Net Framework uso Unity para ello, Autofac es otra opción, o Ninject. Pero en esta ocasión para no introducir librerías adicionales he resuelto la DI necesaria para esta arquitectura con el propio módulo de DI que ya incorpora dotNet Core.

En esta ocasión el giro de tuerca viene de la necesidad de establecer un contexto dinámicamente.

Ponte en la situación de que tienes una arquitectura de microservicios, un cliente que ya tiene un token disponible podría necesitar consumir datos de otro microservicio, este microservicio puede tener una base de datos de propia si somos puristas con la arquitectura basada en microservicios. El tema es el siguiente:

Si quieres un sistema escalable podrías pensar… tendré microservicios que estén mas saturados que otros por la cantidad de datos que manejan, ya sea en volumen de cada petición, o en la cantidad de peticiones, por lo que podría ser una buena idea tener diferentes contextos para un mismo microservicio, de forma que asignes a cada uno de estos contextos un grupo de usuarios cuando formalizan su registro.

Sería el paso intermedio entre:

  • Tener un contexto para todos tus usuarios
  • Tener un contexto por cada uno de tus usuarios

Podrías tener 1000 usuarios y 500 usar un contexto y 500 otro contexto, definido cuando cada uno de ellos fue completando su proceso de registro.

Podrías tener almacenado este contexto en un claim del token, también podrías realizar una llamada en algún punto del ciclo de la petición para que obtenga el token desde un microservicio central encargado de ello.

Ambas opciones necesitarán definir tu contexto en tiempo de ejecución, nada de contextos predefinidos en los settings del proyecto en función del entorno o manejar un switch en tu startup. Estas dos opciones ni son dinámicas ni son escalables cómodamente.

Usar los claim del token puede ser una primera buena aproximación, pero si tu número de microservicios va creciendo ante una gran aplicación, puede llegar a resultar molesto o tedioso.

Por lo tanto yo he optado por realizar una petición a un microservicio central, el cual en sus tablas tendrá almacenados los contextos de todos mis microservicios, y la relación de cada usuario con el contexto de cada uno de los microservicios.

Ahora que estás al tanto de la justificación de esta entrada, o la idea que le da forma, pasemos a la práctica.

Provider para la cadena de conexión

Una clase bastante simple, con una propiedad para establecer y recuperar la cadena de conexión entre diferentes capas, pues la recuperara un base controller de tu lyfeCicle de la petición, pero la utilizará tu DataLayer al crear un contexto a través de la resolución de Dependency Injection. En este caso he situado la interfaz en la capa de dominio, en el mismo lugar que se encuentra la interfaz de Unit Of Work. Por otro lado la implementación la he situado en la capa de datos, donde se encuentra también la implementación de Unit Of Work, pues será su contexto quien use esta cadena.

StartUp

Al usar Entity Framework Core en tu startUp tendrás un método “void ConfigureServices(IServiceCollection services)” donde se definirá la resolución de tus DI, en este caso, para tu contexto tendrás algo como:

Contexto, Unit of Work

El contexto estará definido por tu unidad de trabajo cómo ya presento en mi entrada sobre esta arquitectura, en este caso el DbContext de Entity Framework Core presenta un método “void OnConfiguring(DbContextOptionsBuilder optionsBuilder)” al que voy a hacer override para modificar su comportamiento.

Si instancias una clase que hereda de DbContext, este método OnConfiguring formará parte del LifeCicle, y esto será lo que aprovecharemos para recuperar la cadena de conexión de la propiedad de la clase provider anterior , quedando la implementación de mi anterior entrada de la siguiente manera.

En este caso, con Entity Framework Core, como puedes ver el constructor de DbContext ya no admite un string, si no que necesitarás utilizar un DbContextOptions. Pero lo importante aquí está en la línea 60, donde hago el override sobre ObConfiguring, donde ya se define un DbContextOptionsBuilder, que tiene una propiedad Options que representa un DbContextOptions.

Aquí estamos estableciendo un contexto que usará SqlServer que esta recuperando la cadena de conexión de la clase con el provider que presenté anteriormente (también hay metodos de optionsBuilder para usar contextos en memoria o SqLite entre otros).

El constructor público sin parámetros en el base (DbContext) es necesario para resolver la inyección de dependencias al comienzo del LifeCicle de la petición, no obstante cuando tu código vaya realmente a instanciar una clase de la implementación que resuelve la interfaz, ya usará este OnConfiguring para definir el contexto.

En cambio ese constructor recibe un IServiceProvider. Esta interfaz nos da los mecanismos necesarios con los que rescatar los servicios que están siendo resueltos por DI utilizando Entity Framework Core.

Como puedes observar en el metodo OnConfiguring, estoy utilizando una clase ServiceProviderServiceExtensions que tiene un método GetService, con el que podremos extraer la implementación que resuelve la interfaz indicada, recuperandola del serviceProvider. De está forma, toda tu aplicación estará usando en todas sus capas, una implementación de la interfaz IConnectionStringProvider, por lo que en tus controladores podrás definir la cadena conexión como resultado de una petición a un sistema externo, y en tu capa de datos podrás recuperar esta cadena sin crear ningún tipo de acoplamiento entre las capas.

Controladores

Todos tus controladores deberán heredar de un controlador base, donde manejarás la petición para obtener el contexto de tu microservicio. Con un método que será el encargado de inicializar tu contexto, usando este método al comienzo de cada endpoint, de forma que serás capaz de inicializar un contexto de forma dinámica en función de una petición a otro microservicio. Además podrás usar el mismo token de la petición contra el endpoint del que deseas obtener el contexto, de forma que este endpoint podrá estar autenticado por OAuth.

Para ello, para probar tus endpoints de otros microservicios, tendrás siempre que proporcionar una request autenticada, en esta entrada comento cómo configurar swagger para .Net Core de forma que puedas hacer peticiones autenticadas desde la UI de swagger.

A continuación te muestro el método que estoy utilizando en mi baseController para inicializar el contexto.

El “restService” es otro servicio que tengo en un paquete nuget para no repetir código en cuando a peticiones Http se refiere, y también lo resuelvo con una interfaz y una implementación por DI.

“serviceProvider” es de nuevo una implementación de IServiceProvider resuelta por DI. Por lo tanto el constructor de mi BaseController está utilizando IServiceProvider y IRestService en su constructor.

El endpoint para obtener el contexto recibe el mismo token que se usa en la petición además del id del microservicio que demanda el contexto. En base a esta información (usuario contenido en el token, y microservicio), el endpoint del microservicio central resolverá el contexto necesario para este microservicio y lo enviará en la respuesta para poder construir una cadena de conexión. Sería recomendable que tus microservicios se estén comunicando mediante Https para poder aportar seguridad a este tráfico.

Se construye una cadena de conexión con Server, user Id, Password y Database. Se asigna a la propiedad de la clase estática que presentaba al comienzo, y listo.

De esta forma, dependiendo del usuario conectado, tendrás un sistema que define un contexto dinámico, ya que en cada petición, tu instancia de Unit of Work estará usando el contexto apropiado para el usuario que realiza la petición, todo ello sin acoplar las capas, sin hacer que ni tu lógica de negocio, dominio, modelos o controladores tengan ninguna dependencia a Entity Framework.

Para limitar el número de peticiones, si un microservicio es muy consultado por el resto de tu sistema, podrías implementar un sistema de caché en un microservicio con estado, o incluso que en cada microservicio tengas un sistema de caché en memoria de forma que te ahorres peticiones de un contexto para un mismo usuario de un mismo microservicio N veces.

Aún quiero profundizar un poco sobre el testing en la capa de repositorio de forma que puedas usar un sistema de persistencia en memoria, sistema permitido por EF Core, pero esto lo reservaré para otra entrada. Aprovecharé ese constructor que admite un DbContextOptions, definiendo en lugar de UseSqlServer, el método UseInMemoryDatabase, o al menos esa es la idea inicial.

Un saludo.

 

Deja un comentario