Iniciador
Visión general
El proceso de inicio y inicialización de la aplicación se conoce como bootstrapping. Esto puede llevar algún tiempo (de 2 a 10 segundos), dependiendo de la calidad del hardware y el número de módulos cargados. Cuando la aplicación se apaga, por lo general, la primera solicitud activa el inicio de la aplicación.
Durante el inicio de la aplicación, se llevan a cabo las siguientes acciones:
- Se cargan todos los ensamblados principales en el dominio de la aplicación.
- Se detectan y cargan todas las bibliotecas de módulos instalados en el dominio de la aplicación.
- Se registran todos los servicios en el contenedor de servicios DI.
- Se configura el pipeline(canalización) de solicitudes HTTP.
- Se mapean los endpoints(puntosfinales) de ruta.
En una aplicación ASP.NET Core tradicional, estas acciones se realizan en Program.cs (o Startup.cs en versiones anteriores de ASP.NET), pero esta no es una opción para Crono porque los módulos externos necesitan engancharse/acoplarse en el proceso de inicio. Aquí es donde entran en juego los Iniciadores modulares.
Iniciadores modulares
El núcleo de la aplicación solo contiene un iniciador(bootstrapper) muy liviano (similar a un kernel). Después de que todas los ensamblados de módulos se cargan en el dominio de la aplicación, el escáner de tipos busca subclases concretas de la interfaz IIniciador en todos los ensamblados. Los iniciadores se ordenan y se ejecutan uno tras otro.
Cada proyecto puede tener cualquier número de clases de iniciadores o ninguna. No hay restricciones en absoluto.
Interfaz IIniciador
Aquí está la definición de la interfaz IIniciador:
public interface IIniciador : IOrdenacionTopologica<string>
{
int Orden { get; }
// Permitir o suprimir la ejecución del iniciador basándose en algunas
// condiciones como el estado de instalación de la aplicación, por ejemplo
bool Coincidir(IAplicacionContexto appContexto);
// Agregar servicios al contenedor
void ConfigurarServicios(IServiceCollection servicios, IAplicacionContexto appContexto);
// Configurar servicios MVC
void ConfigurarMvc(IMvcBuilder mvcConstructor, IServiceCollection servicios, IAplicacionContexto appContexto);
// Configurar el pipeline de solicitudes de la aplicación con un orden preciso de middleware.
void ConstruirCanalizacion(SolicitudTuberiaConstructor constructor);
// Registrar rutas de endpoints
void MapearRutas(EnrutamientoPuntosFinalesConstructor constructor);
}
Clase abstracta IniciadorBase
En Crono, se utiliza la clase abstracta IniciadorBase para mayor comodidad. Implementa la interfaz IIniciador con métodos virtuales anulables, por lo que tu iniciador debe derivarse de esta clase en lugar de la interfaz IIniciador.
Además del método ConfigurarServicios, la clase IniciadorBase también proporciona el método ConfigurarContenedor anulable. Hace lo mismo, pero al estilo Autofac, usando ContainerBuilder en lugar de IServiceCollection. No importa si anulas ninguno, uno o ambos.
Siguiendo la convención:
- Colocamos el archivo
Startup.csen la raíz del proyecto del módulo. - Nombramos la clase
Startup. - La derivamos de la clase abstracta
IniciadorBase. - La hacemos interna.
Ejecución condicional
Para la ejecución condicional de los iniciadores, anula el método IniciadorBase.Coincidir() y devuelve un valor que indique si se debe ejecutar o omitir el iniciador. Esto es útil cuando deseas permitir/suprimir la ejecución del iniciador según el estado de instalación de la aplicación.
internal class Startup : IniciadorBase
{
// NO se debe ejecutar cuando la aplicación aún no está completamente instalada
public override bool Coincidir(IApplicacionContexto appContexto)
=> appContexto.EstaInstalado;
}
Orden de ejecución
Por defecto, los iniciadores se ejecutan en el orden en que fueron detectados por el escáner de tipos (primero los ensamblados principales, luego los ensamblados de módulos, y así sucesivamente). Esto se debe a que la clase IniciadorBase asigna el valor predeterminado de la clase estática IniciadorOrdenacion a la propiedad IIniciador.Orden de forma predeterminada. Sin embargo, este valor se puede anular en la implementación de tu iniciador.
Si hay dos iniciadores con el mismo valor de Orden y es necesario especificar explícitamente el orden de ejecución, se utiliza el método IniciadorBase.EjecutarDespues(). Aquí tienes un ejemplo de implementación:
internal class Startup : IniciadorBase
{
public override int Orden => IniciadorOrdenacion.AntesStaticFilesMiddleware;
public Startup()
{
EjecutarDespues<MiPrimerModulo>();
}
}
Orden de middleware y Endpoint
A veces, incluso el orden preciso de los iniciadores no es suficiente. Los middleware y los endpoints requieren un poco más de control, por ejemplo, si necesitas poder definir con precisión el orden de un componente de middleware dentro del pipeline(canalización) de solicitudes. Imagina que has desarrollado dos componentes de middleware en un solo módulo. Uno debe ser AntesStaticFilesMiddleware y el otro DespuesRoutingMiddleware. En Crono, puedes lograr esto de la siguiente manera:
internal class Startup : IniciadorBase
{
public override void ConstruirCanalizacion(SolicitudTuberiaConstructor constructor)
{
constructor.Configurar(IniciadorOrdenacion.AntesStaticFilesMiddleware, app =>
{
app.UseMiddleware<MiPrimerMiddleware>();
});
constructor.Configurar(IniciadorOrdenacion.DespuesRoutingMiddleware, app =>
{
app.UseMiddleware<MiSegundoMiddleware>();
});
}
}
La clase estática IniciadorOrdenacion es muy útil aquí. Define numerosas constantes que representan el orden de los componentes de middleware conocidos (como StaticFiles, Routing, Authentication, ExceptionHandlers, ...). Solo necesitas enganchar antes o después de un componente.
Ejemplo completo de implementación de la clase Startup
internal class Startup : IniciadorBase
{
public Startup()
{
EjecutarDespues<MvcIniciador>();
}
// NO se debe ejecutar cuando la aplicación aún no está completamente instalada
public override bool Coincidir(IApplicacionContexto appContexto)
=> appContexto.EstaInstalado;
public override void ConfigurarServicios(
IServiceCollection servicios,
IApplicacionContexto appContexto)
{
// Anula "ConfigurarServicios" para cosas que solo
// pueda hacer el DI de ASP.NET, como la configuración de opciones
servicios.Configure<AlgunaOpciones>(o =>
{
// ... configura las opciones
});
}
public override void ConfigurarMvc(
IMvcBuilder mvcConstructor,
IServiceCollection servicios,
IAplicacionContexto appContexto)
{
// La configuración de MVC podría hacerse en "ConfigurarServicios",
// pero ¿no está mejor organizado así? :-)
// ... configura algunas cosas de MVC
}
public override void ConfigurarContenedor(
ContainerBuilder constructor,
IAplicacionContexto appContexto)
{
// No puedes hacer esto en "ConfigurarServicios", porque
// ASP.NET DI no admite fuentes de registro,
// decoradores, adaptadores, metadatos, Lazy<>, contenedor, módulos, etc.
constructor.RegisterSource(new MiAutofacRegistrationSource());
}
public override void ConstruirCanalizacion(SolicitudTuberiaConstructor constructor)
{
constructor.Configurar(IniciadorOrdenacion.AntesStaticFilesMiddleware, app =>
{
app.UseMiddleware<MiPrimerMiddleware>();
});
constructor.Configurar(IniciadorOrdenacion.DespuesRoutingMiddleware, app =>
{
app.UseMiddleware<MiSegundoMiddleware>();
});
}
public override void MapearRutas(EnrutamientoPuntosFinalesConstructor constructor)
{
if (constructor.AplicacionContexto.EstaInstalado)
{
constructor.MapearRutas(IniciadorOrdenacion.TardeRuta, routes =>
{
routes.MapBlazorHub();
});
};
}
}
Inicializadores
Los inicializadores que implementan IInicializadorAplicacion se utilizan para ejecutar código de inicialización de la aplicación durante la primera solicitud HTTP, y lo hacen muy temprano en el ciclo de vida de la solicitud. Esto los diferencia de los "iniciadores", que se ejecutan aún antes, cuando HttpContext aún no está disponible.
Sin embargo, parte de la lógica de inicialización, como el acceso a HttpContext, requiere un scope válido para resolver los servicios. No puedes acceder a dependencias con scope o transitorias dentro de un iniciador, a menos que crees manualmente un scope de dependencias, lo cual es una muy mala práctica y casi demoníaco 😄.
No entraremos en ese tema aquí, ya que hay abundante material disponible en línea.
Por defecto, un inicializador solo se ejecuta una vez, a menos que especifiques un valor más alto en la propiedad IntentosMaximos. Sin embargo, esta configuración no tiene efecto si la propiedad LanzarEnError está configurada en true. LanzarEnError indica si se debe lanzar una excepción y detener la ejecución de los inicializadores subsiguientes en caso de error. Si su valor es false, el inicializador se seguirá ejecutando y el método AlFallarAsyncserá invocado para darte la oportunidad de registrar el error y corregirlo.
No es necesario registrar un inicializador en el contenedor de inyección de dependencias (DI), ya que todos los tipos que implementan IInicializadorAplicacion son detectados automáticamente y resueltos durante la inicialización de la aplicación. Por lo tanto, los inicializadores pueden tomar cualquier dependencia que necesiten.
Inicializadores integrados de Crono
| Inicializador | Descripción |
|---|---|
| BaseDatosAplicacionInicializador | Inicializa la(s) base(s) de datos de la aplicación. Es el primer inicializador que se ejecuta. |
| TareaProgramadorInicializador | Activa el programador web después de verificar nombres de host válidos. Retorna una advertencia si no hay un programador u organizaicón registrada. |
| InstalarPermisosInicializador | Verifica nuevos registros de permisos y los inserta en la base de datos si es necesario. |
| ModulosInicializador | Entre otras cosas, detecta y actualiza los recursos locales de los módulos que hayan cambiado. |