Una librería ligera y extensible para orquestar la secuencia de arranque de cualquier aplicación o librería .NET. Basada en el patrón Composite, permite encapsular cada paso de inicialización como una unidad independiente y testeable.
Construida en .NET 10 con soporte completo para async/await, Nullable Reference Types y un stack de observabilidad Cloud Native.
- ¿Por qué BeyondNet.Bootstrapper?
- Instalación
- Conceptos Fundamentales
- Inicio Rápido — Síncrono
- Inicio Rápido — Asíncrono
- Adaptadores
- Ejemplo Real — Combinando Todos los Adaptadores
- Glosario
Sin un estándar, el código de arranque tiende a convertirse en un bloque monolítico en Program.cs difícil de probar y mantener. Esta librería resuelve ese problema aplicando una sola regla:
Cada responsabilidad de inicialización vive en su propia clase. El Composite las ejecuta en orden.
Beneficios:
- Cada bootstrapper es unitariamente testeable de forma independiente.
- La secuencia de arranque es explícita y legible.
- Agregar o eliminar un paso no modifica el código circundante.
- El I/O asíncrono en el arranque no puede generar deadlocks.
Instala únicamente los paquetes que necesitas:
# Core (siempre requerido)
dotnet add package BeyondNet.Bootstrapper
# Adaptadores oficiales (agregar según necesidad)
dotnet add package BeyondNet.Bootstrapper.DependencyInjection
dotnet add package BeyondNet.Bootstrapper.AutoMapper
dotnet add package BeyondNet.Bootstrapper.ObservabilityIBootstrapper ← contrato síncrono
IBootstrapper<T> ← contrato síncrono con resultado tipado
IBootstrapperAsync ← contrato asíncrono con CancellationToken
IBootstrapperAsync<T> ← contrato asíncrono con resultado tipado
CompositeBootstrapper ← ejecuta la lista de IBootstrapper en orden
CompositeBootstrapperAsync ← ejecuta la lista de IBootstrapperAsync en orden, respeta cancelaciónFlujo:
Inicio de la app
└─ CompositeBootstrapper / CompositeBootstrapperAsync
├─ Paso 1: DatabaseBootstrapper.Run()
├─ Paso 2: AutoMapperBootstrapper.Run()
└─ Paso 3: ObservabilityBootstrapper.Run()using BeyondNet.Bootstrapper.Interface;
// Encapsula cualquier lógica de inicialización y expone el resultado en Result
public class FeatureFlagBootstrapper : IBootstrapper<bool>
{
public bool? Result { get; private set; }
public void Run()
{
// Cualquier configuración síncrona: leer config, validar variables de entorno, etc.
Result = true;
}
}using BeyondNet.Bootstrapper.Impl;
var featureFlags = new FeatureFlagBootstrapper();
new CompositeBootstrapper()
.Add(featureFlags)
.Run();
if (featureFlags.Result == true)
Console.WriteLine("Feature flags listos.");var paso1 = new DatabaseBootstrapper(connectionString);
var paso2 = new CacheBootstrapper(redisUrl);
var paso3 = new FeatureFlagBootstrapper();
new CompositeBootstrapper()
.Add(paso1)
.Add(paso2)
.Add(paso3)
.Run();Usa el motor asíncrono cuando un paso requiera I/O (ping a base de datos, llamada HTTP, lectura de archivo).
using BeyondNet.Bootstrapper.Interface;
public class DatabaseConnectionBootstrapper : IBootstrapperAsync<bool>
{
private readonly string _connectionString;
public DatabaseConnectionBootstrapper(string connectionString)
=> _connectionString = connectionString;
public bool? Result { get; private set; }
public async Task RunAsync(CancellationToken cancellationToken = default)
{
// Reemplaza con un ping real a la base de datos
await Task.Delay(50, cancellationToken);
Result = true;
}
}using BeyondNet.Bootstrapper.Impl;
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var dbStep = new DatabaseConnectionBootstrapper("Server=localhost;...");
await new CompositeBootstrapperAsync()
.Add(dbStep)
.RunAsync(cts.Token);
if (dbStep.Result == true)
Console.WriteLine("Conexión a base de datos establecida.");Regla: Si algún paso es asíncrono, usa
CompositeBootstrapperAsyncpara todos los pasos. No mezcles bootstrappers síncronos y asíncronos.
Paquete: BeyondNet.Bootstrapper.DependencyInjection
Registra servicios en IServiceCollection y expone la colección poblada como resultado.
Ejemplo mínimo:
using BeyondNet.Bootstrapper.DependencyInjection;
using Microsoft.Extensions.DependencyInjection;
var diBootstrapper = new DependencyInjectionBootstrapper(services =>
{
services.AddSingleton<IGreeter, ConsoleGreeter>();
});
diBootstrapper.Run();Pipeline completo — registrar, construir provider, resolver:
using BeyondNet.Bootstrapper.DependencyInjection;
using BeyondNet.Bootstrapper.Impl;
using Microsoft.Extensions.DependencyInjection;
// 1. Crear el bootstrapper y registrar dependencias
var diBootstrapper = new DependencyInjectionBootstrapper(services =>
{
services.AddSingleton<IOrderRepository, SqlOrderRepository>();
services.AddScoped<IOrderService, OrderService>();
services.AddLogging();
});
// 2. Ejecutar el registro
new CompositeBootstrapper()
.Add(diBootstrapper)
.Run();
// 3. Construir IServiceProvider desde la colección poblada
var provider = diBootstrapper.Result!.BuildServiceProvider();
// 4. Resolver y usar tus servicios
var orderService = provider.GetRequiredService<IOrderService>();Pasar una IServiceCollection existente (ej. ASP.NET Core):
// En Program.cs — inyectar en la colección existente de ASP.NET Core
var diBootstrapper = new DependencyInjectionBootstrapper(builder.Services, services =>
{
services.AddSingleton<IPaymentGateway, StripeGateway>();
});
diBootstrapper.Run();Paquete: BeyondNet.Bootstrapper.AutoMapper
Construye un MapperConfiguration y lo expone como resultado para que puedas crear instancias de IMapper en cualquier parte de la aplicación.
Ejemplo mínimo:
using BeyondNet.Bootstrapper.AutoMapper;
var mapperBootstrapper = new AutoMapperBootstrapper(cfg =>
{
cfg.CreateMap<UserEntity, UserDto>();
});
mapperBootstrapper.Run();Pipeline completo — configurar, construir, mapear:
using BeyondNet.Bootstrapper.AutoMapper;
using BeyondNet.Bootstrapper.Impl;
using AutoMapper;
var mapperBootstrapper = new AutoMapperBootstrapper(cfg =>
{
cfg.CreateMap<ProductEntity, ProductDto>()
.ForMember(dest => dest.FullName,
opt => opt.MapFrom(src => $"{src.Brand} {src.Model}"));
cfg.CreateMap<OrderEntity, OrderSummaryDto>();
});
new CompositeBootstrapper()
.Add(mapperBootstrapper)
.Run();
// Construir el mapper — hacerlo una sola vez y almacenarlo (singleton)
IMapper mapper = mapperBootstrapper.Result!.CreateMapper();
// Usarlo en cualquier parte
var dto = mapper.Map<ProductDto>(productEntity);Paquete: BeyondNet.Bootstrapper.Observability
Configura el V1 Observability Stack: logs estructurados vía Serilog (sink OTLP) y trazado distribuido vía OpenTelemetry, ambos apuntando a un colector OTLP único.
App → ObservabilityBootstrapper → Colector OTLP → Tempo (trazas)
→ Loki (logs)
→ Grafana (dashboards)Ejemplo mínimo:
using BeyondNet.Bootstrapper.Observability;
using Microsoft.Extensions.DependencyInjection;
var services = new ServiceCollection();
new ObservabilityBootstrapper(services, new ObservabilityConfiguration
{
ServiceName = "OrderService",
ServiceVersion = "2.0.0",
OTLPEndpoint = "http://localhost:4317"
}).Run();Configuración completa con todos los atributos:
using BeyondNet.Bootstrapper.Observability;
using Microsoft.Extensions.DependencyInjection;
var config = new ObservabilityConfiguration
{
ServiceName = "PaymentService",
ServiceVersion = "1.4.2",
OTLPEndpoint = "http://otel-collector:4317",
// Atributos de recurso adicionales enviados en cada traza y log
ResourceAttributes = new Dictionary<string, object>
{
{ "deployment.environment", "production" },
{ "cloud.region", "us-east-1" }
}
};
var obsBootstrapper = new ObservabilityBootstrapper(services, config);
obsBootstrapper.Run();
// Result expone IServiceCollection con OpenTelemetry registrado
var provider = obsBootstrapper.Result!.BuildServiceProvider();Entorno local: la carpeta
examples/observability/contiene undocker-compose.ymlque levanta Colector OTel, Tempo, Loki y Grafana con un solo comando:cd examples/observability docker-compose up -d
El siguiente muestra un Program.cs completo que conecta todo usando un solo CompositeBootstrapper.
using BeyondNet.Bootstrapper.Impl;
using BeyondNet.Bootstrapper.DependencyInjection;
using BeyondNet.Bootstrapper.AutoMapper;
using BeyondNet.Bootstrapper.Observability;
using Microsoft.Extensions.DependencyInjection;
// ── 1. Declarar cada bootstrapper ─────────────────────────────────────
var services = new ServiceCollection();
var di = new DependencyInjectionBootstrapper(services, svc =>
{
svc.AddSingleton<IOrderRepository, SqlOrderRepository>();
svc.AddScoped<IOrderService, OrderService>();
});
var mapper = new AutoMapperBootstrapper(cfg =>
{
cfg.CreateMap<OrderEntity, OrderDto>();
});
var observability = new ObservabilityBootstrapper(services, new ObservabilityConfiguration
{
ServiceName = "OrderService",
ServiceVersion = "2.0.0",
OTLPEndpoint = "http://localhost:4317"
});
// ── 2. Ejecutar todos los pasos en secuencia ──────────────────────────
new CompositeBootstrapper()
.Add(di)
.Add(mapper)
.Add(observability)
.Run();
// ── 3. Usar los resultados ────────────────────────────────────────────
var provider = di.Result!.BuildServiceProvider();
var iMapper = mapper.Result!.CreateMapper();
var orderSvc = provider.GetRequiredService<IOrderService>();Variante asíncrona (ej. arranque con health-check a base de datos):
using BeyondNet.Bootstrapper.Impl;
using BeyondNet.Bootstrapper.Interface;
public class DatabasePingBootstrapper : IBootstrapperAsync<bool>
{
public bool? Result { get; private set; }
public async Task RunAsync(CancellationToken ct = default)
{
// Reemplaza con una verificación real de conexión
await Task.Delay(20, ct);
Result = true;
}
}
// En Program.cs
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(15));
var dbPing = new DatabasePingBootstrapper();
await new CompositeBootstrapperAsync()
.Add(dbPing)
.RunAsync(cts.Token);
if (dbPing.Result != true)
throw new InvalidOperationException("Base de datos inaccesible al iniciar.");| Término | Definición |
|---|---|
| Bootstrapper | Clase que encapsula una responsabilidad de inicio (conexión a DB, registro de DI, perfiles de mapeo, etc.) y expone su resultado en Result. |
| Composite | Patrón de diseño estructural que agrupa múltiples bootstrappers y los ejecuta secuencialmente como una unidad. |
| IBootstrapperAsync | Contrato asíncrono. Úsalo cuando un paso realice trabajo I/O-bound (red, disco, base de datos) para no bloquear el thread pool. |
| CancellationToken | Pasado a través de RunAsync para permitir que el host cancele toda la secuencia de arranque si se excede un timeout. |
| OTLP | OpenTelemetry Protocol — formato agnóstico de proveedor para enviar métricas, trazas y logs a un colector unificado. |
| Colector OTel | Proxy que recibe señales OTLP y las distribuye a backends de almacenamiento (Tempo para trazas, Loki para logs). |