lunes, 2 de diciembre de 2019

Jerarquía de contenedores de Inyección de dependencias en ASP.NET MVC

¿Has tenido alguna vez un sistema de información; cuya interfaz para la parte interactiva es ASP.NET MVC; que necesita mantener varias versiones de comportamiento en la capa de aplicación y/o negocio a la vez y la decisión de qué versión utilizar tenía que establecerse en tiempo de ejecución según el input del usuario? Si es así, quizás esto te interese.

La mejor solución al problema de versionado de reglas de dominio es la inyección de dependencias (ID). No hay nada mejor para mantener una coherencia y segregación del comportamiento a la vez que reduces la complejidad ciclomática.

La mayoría de los contenedores de ID permiten una organización jerárquica en la que un contenedor hijo puede agregar y/o sobrescribir la política de inyecciones del contenedor padre. Gracias a esto podemos tener un contenedor hijo por cada versión de la capa de aplicación/dominio que necesitemos y usaremos ese contenedor hijo para resolver las dependencias.

Pero con ASP.NET MVC hemos topado... Si intentas utilizar DependencyResolver.SetResolver por defecto te darás cuenta que solo permite usar un contenedor. Y no puedes cambiar este valor; para usar el contenedor hijo que toque; en cada request porque puede estar siendo utilizado por otro usuario de forma concurrente. A esto se suma la inherente falta de estado del protocolo HTTP. U obligamos al usuario a introducir el valor que nos permite inferir la versión de las reglas que vamos a usar en cada input de cada acción realizada o creamos un estado mutable en sesión asignado a ese usuario.  Ambas soluciones son horrorosas y tienen consecuencias desastrosas para la experiencia del usuario y la navegabilidad de la aplicación.

Para solucionar el problema del input se me ha ocurrido que podemos abstraerlo a un valor incluido en las rutas virtuales de MVC. Gracias al input especificado en la ruta; podemos inferir que versión de reglas debemos resolver. También permitiría crear cualquier hiperenlace con ese valor por lo que el usuario está mandando en cada request (ya sea post o get) el input discriminatorio de reglas casi sin que se de cuenta.

Para hacer que ASP.NET MVC utilice varios contenedores de dependencias tendríamos que crear nuestro IDependencyResolver personalizado. Allí podemos recuperar el contenedor hijo que le corresponda según el valor de la información de la ruta virtual y usarlo para resolver las dependencias.

Public Class MyDependencyResolver
  Implements IDependencyResolver
 
  Private childContainers As New Dictionary(Of StringIUnityContainer)
 
  Public Sub New(container As IUnityContainer)
 
    ServicesConfig.RegisterTypes(container)
    childContainers.Add(String.Empty, container)
 
    Dim child = container.CreateChildContainer()
    ServicesConfig.RegisterCampania20018Types(child)
    childContainers.Add("2018", child)
 
    child = container.CreateChildContainer()
    ServicesConfig.RegisterCampania20019Types(child)
    childContainers.Add("2019", child)
 
  End Sub
 
  Private Function IDependencyResolver_GetService(serviceType As TypeAs Object Implements IDependencyResolver.GetService
    Dim campania = HttpContext.Current.Request.RequestContext.RouteData?.Values("myDiscriminatorRouteValue")
    If String.IsNullOrEmpty(campania) Then Throw Exception
    Return childContainers(campania).Resolve(serviceType)
  End Function
 
  
End Class

Pero nos encontramos con una movida muy chunga y es que HttpContext.Current.Request.RequestContext.RouteData.Values("myDiscriminatorRouteValue"está vacío! Da igual que definamos la rutas por código o por atributo en los métodos del controlador; da igual que usemos prefijos de ruta en el controlador o no; siempre siempre te encontrarás que en este momento del pipeline de ASP.NET MVC RouteData no tiene mapeado los valores de las rutas... o sí?

Resulta que están escondidas por vete tú a saber que deuda técnica bizarra. Para acceder a los valores de las rutas dentro de un DependencyResolver hay que excavar más a fondo y nos las encontramos aquí:

HttpContext.Current.Request.RequestContext.RouteData?
.Values("MS_DirectRouteMatches")?(0)?.Values?("myDiscriminatorRouteValue")

Ale. Ahora no quiero ver un solo IF que intente discriminar comportamientos en la capa de aplicación o dominio. Cada versión tiene su clase que inyectamos gracias al contenedor de ID y ya solo queda llamarlo a piñón.

No hay comentarios:

Publicar un comentario