Parte de: Shell Libraries Paquetes NuGet:
BeyondNetCode.Shell.Bootstrapper·BeyondNetCode.Shell.DI·BeyondNetCode.Shell.ObservabilityDependencias:Microsoft.Extensions.DependencyInjection·Serilog.Sinks.OpenTelemetry·OpenTelemetryRepositorio:github.com/beyondnetcode/Shell.Bootstrapper
BeyondNetCode.Shell.Bootstrapper implementa el Patron Composite Bootstrapper — una forma estructurada y testeable de descomponer el inicio complejo de una aplicacion en pequenas unidades independientes que se pueden composicionar en un pipeline.
- Cuando Usar
- Estructura del Proyecto
- Interfaces Core
- CompositeBootstrapper
- Bootstrappers Incluidos
- Escribir un Bootstrapper Custom
- Bootstrappers Async
- Ejemplos de Uso Standalone
- Referencia API
- Patron de Integracion UMS
Usa BeyondNetCode.Shell.Bootstrapper cuando:
- El inicio tiene multiples fases independientes que deben ser testeables en aislamiento.
- Quires una forma fluente y composable de describir el orden de inicializacion.
- Necesitas un
Resulttipado desde una fase de inicializacion (ej., elIServiceCollectiondespues de configurar DI).
Prefiere IHostedService o IStartupFilter para:
- Trabajo que debe happen dentro del host en ejecucion (despues de
app.Run()). - Inicializacion de un solo paso que no necesita ser compuesta o testeada en aislamiento.
BeyondNetCode.Shell.Bootstrapper/
├── Interface/
│ ├── IBootstrapper.cs ← IBootstrapper + IBootstrapper<out T>
│ └── IBootstrapperAsync.cs ← IBootstrapperAsync + IBootstrapperAsync<out T>
└── Impl/
├── CompositeBootstrapper.cs ← runner sync secuencial
└── CompositeBootstrapperAsync.cs ← runner async secuencial
BeyondNetCode.Shell.DI/
└── DependencyInjectionBootstrapper.cs ← envuelve configuracion de IServiceCollection
BeyondNetCode.Shell.Observability/
├── ObservabilityBootstrapper.cs ← cableado de Serilog + OpenTelemetry
└── ObservabilityConfiguration.cs ← endpoint OTLP, nombre/servicio/version, atributos de recurso// Bootstrapper sync -- sin resultado
public interface IBootstrapper
{
void Run();
}
// Bootstrapper sync con resultado tipado
public interface IBootstrapper<out T> : IBootstrapper
{
T? Result { get; }
}
// Bootstrapper async -- sin resultado
public interface IBootstrapperAsync
{
Task RunAsync(CancellationToken cancellationToken = default);
}
// Bootstrapper async con resultado tipado
public interface IBootstrapperAsync<out T> : IBootstrapperAsync
{
T? Result { get; }
}Ejecuta una secuencia de bootstrappers uno tras otro. API fluida via .Add(bootstrapper).
new CompositeBootstrapper()
.Add(new PhaseABootstrapper())
.Add(new PhaseBBootstrapper())
.Add(new PhaseCBootstrapper())
.Run();Tambien puedes pasar la lista en el constructor:
var bootstrappers = new IBootstrapper[]
{
new PhaseABootstrapper(),
new PhaseBBootstrapper()
};
new CompositeBootstrapper(bootstrappers).Run();await new CompositeBootstrapperAsync()
.Add(new DatabaseMigrationBootstrapper())
.Add(new SeedDataBootstrapper())
.RunAsync(cancellationToken);Envuelve configuracion de IServiceCollection. Result es el IServiceCollection poblado.
var di = new DependencyInjectionBootstrapper(services =>
{
services.AddSingleton<IMyService, MyService>();
services.AddScoped<IOrderRepository, SqlOrderRepository>();
});
di.Run();
IServiceCollection configured = di.Result!;
// construye y usa
var sp = configured.BuildServiceProvider();Alternativamente, pasa un IServiceCollection existente:
var services = new ServiceCollection();
var di = new DependencyInjectionBootstrapper(services, s =>
{
s.AddSingleton<IMyService, MyService>();
});
di.Run();
// services ahora esta pobladoEnvuelve MapperConfigurationExpression de AutoMapper. Result mantiene el objeto de expresion que se paso al lambda de configuracion de AutoMapper.
Nota:
MapperConfigurationExpressionno exponeCreateMapper()directamente. Usa el bootstrapper para colectar tus declaraciones de mapeo, luego pasa la misma Action anew MapperConfiguration(...)para obtener unIMapper. El bootstrapper es mas util como unidad testeable para verificar que las declaraciones de mapeo fueron registradas.
// Declara la configuracion del mapper en el bootstrapper
Action<IMapperConfigurationExpression> mappings = cfg =>
{
cfg.CreateMap<OrderEntity, OrderDto>();
cfg.CreateMap<LineItemEntity, LineItemDto>();
};
var autoMapper = new AutoMapperBootstrapper(mappings);
autoMapper.Run();
// autoMapper.Result no es null -- las declaraciones fueron aplicadas
Debug.Assert(autoMapper.Result != null);
// Para obtener un IMapper funcional, envuelve la misma action en MapperConfiguration:
var mapperConfig = new MapperConfiguration(mappings);
mapperConfig.AssertConfigurationIsValid();
IMapper mapper = mapperConfig.CreateMapper();
OrderDto dto = mapper.Map<OrderDto>(entity);Patron DI (recomendado): Registra AutoMapper directamente con services.AddAutoMapper(typeof(MyProfile)) para uso en produccion. Usa AutoMapperBootstrapper cuando quieres testing aislado y composable de declaraciones de mapeo.
Configura Serilog (via sink OTLP) y OpenTelemetry tracing (via exportador OTLP) en un paso.
var config = new ObservabilityConfiguration
{
OTLPEndpoint = "http://otel-collector:4317",
ServiceName = "ums-api",
ServiceVersion = "2.1.0",
ResourceAttributes = new Dictionary<string, object>
{
{ "deployment.environment", "production" },
{ "cloud.region", "us-east-1" }
}
};
var obs = new ObservabilityBootstrapper(services, config);
obs.Run();
// Despues de Run():
// - Serilog.Log.Logger esta configurado con sink OTLP
// - OpenTelemetry tracing con exportador OTLP esta registrado en services| Propiedad | Default | Descripcion |
|---|---|---|
OTLPEndpoint |
http://localhost:4317 |
Endpoint del collector OTLP gRPC |
ServiceName |
"UnknownService" |
Aparece en traces y contexto Serilog |
ServiceVersion |
"1.0.0" |
Atributo de recurso service.version |
ResourceAttributes |
null |
Atributos de recurso OTLP adicionales (pares key-value) |
public class DatabaseSchemaBootstrapper(string connectionString)
: IBootstrapper<bool>
{
public bool? Result { get; private set; }
public void Run()
{
// Aplica migraciones, valida schema, etc.
using var conn = new SqlConnection(connectionString);
conn.Open();
// ... schema checks ...
Result = true;
}
}
// Uso
var schema = new DatabaseSchemaBootstrapper(connectionString);
schema.Run();
if (schema.Result != true) throw new InvalidOperationException("Schema validation failed.");public class SeedBootstrapper(IServiceProvider sp)
: IBootstrapperAsync<int> // int = numero de registros sembrados
{
public int? Result { get; private set; }
public async Task RunAsync(CancellationToken ct = default)
{
using var scope = sp.CreateScope();
var repo = scope.ServiceProvider.GetRequiredService<ITenantRepository>();
var count = await SeedTenantsAsync(repo, ct);
Result = count;
}
private static async Task<int> SeedTenantsAsync(ITenantRepository repo, CancellationToken ct) { ... }
}await new CompositeBootstrapperAsync()
.Add(new DatabaseSchemaBootstrapper(connectionString)) // bootstrapper envuelto? no -- ver nota
.Add(new SeedBootstrapper(serviceProvider))
.RunAsync(cancellationToken);Nota:
CompositeBootstrapperAsyncespera instancias deIBootstrapperAsync. Envuelve un bootstrapper sync si es necesario:public class SyncToAsyncWrapper(IBootstrapper inner) : IBootstrapperAsync { public Task RunAsync(CancellationToken ct = default) { inner.Run(); return Task.CompletedTask; } }
CompositeBootstrapperAsync ejecuta todas las instancias de IBootstrapperAsync registradas secuencialmente usando await.
await new CompositeBootstrapperAsync()
.Add(new CheckConnectivityBootstrapper())
.Add(new ApplyMigrationsBootstrapper())
.Add(new WarmupCacheBootstrapper())
.RunAsync(stoppingToken);Cada bootstrapper recibe el mismo CancellationToken -- maneja la cancelacion en tu implementacion de RunAsync.
using BeyondNetCode.Shell.Bootstrapper.Impl;
using BeyondNetCode.Shell.DI;
using BeyondNetCode.Shell.Observability;
var services = new ServiceCollection();
// Fase 1: configura DI
var di = new DependencyInjectionBootstrapper(services, s =>
{
s.AddSingleton<IDiscountService, DiscountService>();
s.AddSingleton<IOrderRepository, InMemoryOrderRepository>();
});
// Fase 2: configura AutoMapper
Action<IMapperConfigurationExpression> mappings = cfg =>
{
cfg.CreateMap<OrderEntity, OrderDto>();
cfg.CreateMap<Order, OrderEntity>().ReverseMap();
};
var autoMapper = new AutoMapperBootstrapper(mappings);
// Ejecuta ambas fases en secuencia
new CompositeBootstrapper()
.Add(di)
.Add(autoMapper)
.Run();
// Construye
var sp = services.BuildServiceProvider();
var mapper = new MapperConfiguration(mappings).CreateMapper();await new CompositeBootstrapperAsync()
.Add(new ConnectivityCheckBootstrapper(connectionString)) // custom
.Add(new DatabaseSchemaBootstrapper(connectionString)) // custom
.Add(new SeedBootstrapper(serviceProvider)) // custom
.RunAsync(CancellationToken.None);var services = new ServiceCollection();
new ObservabilityBootstrapper(services, new ObservabilityConfiguration
{
OTLPEndpoint = Environment.GetEnvironmentVariable("OTEL_EXPORTER_OTLP_ENDPOINT")
?? "http://localhost:4317",
ServiceName = "ums-worker",
ServiceVersion = "1.0.0"
}).Run();
var host = Host.CreateDefaultBuilder(args)
.ConfigureServices((_, s) => { /* copia registros */ })
.Build();
await host.RunAsync();| Metodo | Descripcion |
|---|---|
CompositeBootstrapper Add(IBootstrapper b) |
Agrega un bootstrapper; devuelve this para encadenar |
void Run() |
Ejecuta todos los bootstrappers en orden de adicion |
| Metodo | Descripcion |
|---|---|
CompositeBootstrapperAsync Add(IBootstrapperAsync b) |
Agrega; devuelve this |
Task RunAsync(CancellationToken ct = default) |
Ejecuta todos secuencialmente con await |
| Constructor | Descripcion |
|---|---|
(Action<IServiceCollection>?) |
Crea un nuevo ServiceCollection internamente |
(IServiceCollection, Action<IServiceCollection>?) |
Usa la coleccion proporcionada |
Result → el IServiceCollection configurado.
| Constructor | Descripcion |
|---|---|
(Action<IMapperConfigurationExpression>?) |
Registra perfil de mapeo |
Result → MapperConfigurationExpression. Llama .CreateMapper() para obtener IMapper.
| Constructor | Descripcion |
|---|---|
(IServiceCollection, ObservabilityConfiguration, Action<IServiceCollection>?) |
Configura Serilog + tracing OTLP |
Result → el IServiceCollection (misma referencia pasada).
UMS actualmente usa IHostedService, IStartupFilter, y cableado directo de Program.cs para inicializacion de inicio. El patron Bootstrapper puede superponerse para casos complejos de multiples pasos.
// En Ums.Infrastructure/Hosting/SchemaBootstrapperService.cs
public class SchemaBootstrapperService(
IServiceProvider sp,
ILogger<SchemaBootstrapperService> logger) : IHostedService
{
public async Task StartAsync(CancellationToken ct)
{
await new CompositeBootstrapperAsync()
.Add(new SqlServerSchemaBootstrapper(sp, logger))
.Add(new DevDataSeedBootstrapper(sp, logger))
.RunAsync(ct);
}
public Task StopAsync(CancellationToken ct) => Task.CompletedTask;
}
// Implementacion de cada fase:
public class SqlServerSchemaBootstrapper(IServiceProvider sp, ILogger logger)
: IBootstrapperAsync
{
public async Task RunAsync(CancellationToken ct)
{
using var scope = sp.CreateScope();
var bootstrapper = scope.ServiceProvider
.GetRequiredService<SqlServerSchemaBootstrapper>();
await bootstrapper.InitializeAsync(ct);
}
}// En Ums.Presentation/Program.cs (o donde se componga el host)
var obsConfig = new ObservabilityConfiguration
{
OTLPEndpoint = builder.Configuration["OpenTelemetry:Endpoint"] ?? "http://localhost:4317",
ServiceName = "ums-api",
ServiceVersion = "2.0.0"
};
new ObservabilityBootstrapper(builder.Services, obsConfig).Run();
// Serilog + OTel tracing ahora estan configurados- AOP -- concerns cross-cutting para servicios inicializados por bootstrappers
- Uso Combinado -- ejemplo completo de Bootstrapper + DDD + Factory + AOP