A lightweight, extensible library for orchestrating the startup sequence of any .NET application or library. Based on the Composite pattern, it lets you encapsulate each initialization step as an independent, testable unit.
Built on .NET 10 with full support for async/await, Nullable Reference Types, and a Cloud Native observability stack.
- Why BeyondNet.Bootstrapper?
- Installation
- Core Concepts
- Quick Start — Synchronous
- Quick Start — Asynchronous
- Adapters
- Real-World Example — Combining All Adapters
- Glossary
Without a standard, startup code tends to become a monolithic block in Program.cs that is hard to test and maintain. This library solves that by enforcing a single rule:
Each initialization concern lives in its own class. The Composite runs them in order.
Benefits:
- Each bootstrapper is independently unit-testable.
- The startup sequence is explicit and readable.
- Adding or removing a step never changes surrounding code.
- Async I/O at startup cannot deadlock the application.
Install only the packages you need:
# Core (always required)
dotnet add package BeyondNet.Bootstrapper
# Official adapters (add as needed)
dotnet add package BeyondNet.Bootstrapper.DependencyInjection
dotnet add package BeyondNet.Bootstrapper.AutoMapper
dotnet add package BeyondNet.Bootstrapper.ObservabilityIBootstrapper ← synchronous contract
IBootstrapper<T> ← synchronous contract with typed result
IBootstrapperAsync ← async contract with CancellationToken
IBootstrapperAsync<T> ← async contract with typed result
CompositeBootstrapper ← runs IBootstrapper list in order
CompositeBootstrapperAsync ← runs IBootstrapperAsync list in order, respects cancellation
Flow:
App startup
└─ CompositeBootstrapper / CompositeBootstrapperAsync
├─ Step 1: DatabaseBootstrapper.Run()
├─ Step 2: AutoMapperBootstrapper.Run()
└─ Step 3: ObservabilityBootstrapper.Run()
usingBeyondNet.Bootstrapper.Interface;// Encapsulates any initialization logic and exposes the result via ResultpublicclassFeatureFlagBootstrapper:IBootstrapper<bool>{publicbool?Result{get;privateset;}publicvoidRun(){// Any synchronous setup: read config, validate env vars, etc.Result=true;}}usingBeyondNet.Bootstrapper.Impl;varfeatureFlags=newFeatureFlagBootstrapper();newCompositeBootstrapper().Add(featureFlags).Run();if(featureFlags.Result==true)Console.WriteLine("Feature flags ready.");varstep1=newDatabaseBootstrapper(connectionString);varstep2=newCacheBootstrapper(redisUrl);varstep3=newFeatureFlagBootstrapper();newCompositeBootstrapper().Add(step1).Add(step2).Add(step3).Run();Use the async engine whenever a step requires I/O (database ping, HTTP call, file read).
usingBeyondNet.Bootstrapper.Interface;publicclassDatabaseConnectionBootstrapper:IBootstrapperAsync<bool>{privatereadonlystring_connectionString;publicDatabaseConnectionBootstrapper(stringconnectionString)=>_connectionString=connectionString;publicbool?Result{get;privateset;}publicasyncTaskRunAsync(CancellationTokencancellationToken=default){// Simulate or replace with real DB pingawaitTask.Delay(50,cancellationToken);Result=true;}}usingBeyondNet.Bootstrapper.Impl;usingvarcts=newCancellationTokenSource(TimeSpan.FromSeconds(10));vardbStep=newDatabaseConnectionBootstrapper("Server=localhost;...");awaitnewCompositeBootstrapperAsync().Add(dbStep).RunAsync(cts.Token);if(dbStep.Result==true)Console.WriteLine("Database connection established.");Rule: If any step requires async, use
CompositeBootstrapperAsyncfor all steps. Do not mix sync and async bootstrappers.
Package:BeyondNet.Bootstrapper.DependencyInjection
Registers services into IServiceCollection and exposes the populated collection as the result.
Minimal example:
usingBeyondNet.Bootstrapper.DependencyInjection;usingMicrosoft.Extensions.DependencyInjection;vardiBootstrapper=newDependencyInjectionBootstrapper(services =>{services.AddSingleton<IGreeter,ConsoleGreeter>();});diBootstrapper.Run();Full pipeline — register, build provider, resolve:
usingBeyondNet.Bootstrapper.DependencyInjection;usingBeyondNet.Bootstrapper.Impl;usingMicrosoft.Extensions.DependencyInjection;// 1. Create bootstrapper and register dependenciesvardiBootstrapper=newDependencyInjectionBootstrapper(services =>{services.AddSingleton<IOrderRepository,SqlOrderRepository>();services.AddScoped<IOrderService,OrderService>();services.AddLogging();});// 2. Run registrationnewCompositeBootstrapper().Add(diBootstrapper).Run();// 3. Build IServiceProvider from the populated collectionvarprovider=diBootstrapper.Result!.BuildServiceProvider();// 4. Resolve and use your servicesvarorderService=provider.GetRequiredService<IOrderService>();Pass an existing IServiceCollection (e.g., ASP.NET Core):
// In Program.cs — inject into the existing ASP.NET Core collectionvardiBootstrapper=newDependencyInjectionBootstrapper(builder.Services, services =>{services.AddSingleton<IPaymentGateway,StripeGateway>();});diBootstrapper.Run();Package:BeyondNet.Bootstrapper.AutoMapper
Builds a MapperConfiguration and exposes it as the result so you can create IMapper instances anywhere in your application.
Minimal example:
usingBeyondNet.Bootstrapper.AutoMapper;varmapperBootstrapper=newAutoMapperBootstrapper(cfg =>{cfg.CreateMap<UserEntity,UserDto>();});mapperBootstrapper.Run();Full pipeline — configure, build, map:
usingBeyondNet.Bootstrapper.AutoMapper;usingBeyondNet.Bootstrapper.Impl;usingAutoMapper;varmapperBootstrapper=newAutoMapperBootstrapper(cfg =>{cfg.CreateMap<ProductEntity,ProductDto>().ForMember(dest =>dest.FullName,
opt =>opt.MapFrom(src =>$"{src.Brand}{src.Model}"));cfg.CreateMap<OrderEntity,OrderSummaryDto>();});newCompositeBootstrapper().Add(mapperBootstrapper).Run();// Build the mapper — do this once and store it (singleton)IMappermapper=mapperBootstrapper.Result!.CreateMapper();// Use it anywherevardto=mapper.Map<ProductDto>(productEntity);Package:BeyondNet.Bootstrapper.Observability
Configures the V1 Observability Stack: structured logs via Serilog (OTLP sink) and distributed tracing via OpenTelemetry, both pointing to a single OTLP collector.
App → ObservabilityBootstrapper → OTLP Collector → Tempo (traces)
→ Loki (logs)
→ Grafana (dashboards)
Minimal example:
usingBeyondNet.Bootstrapper.Observability;usingMicrosoft.Extensions.DependencyInjection;varservices=newServiceCollection();newObservabilityBootstrapper(services,newObservabilityConfiguration{ServiceName="OrderService",ServiceVersion="2.0.0",OTLPEndpoint="http://localhost:4317"}).Run();Full configuration with all options:
usingBeyondNet.Bootstrapper.Observability;usingMicrosoft.Extensions.DependencyInjection;varconfig=newObservabilityConfiguration{ServiceName="PaymentService",ServiceVersion="1.4.2",OTLPEndpoint="http://otel-collector:4317",// Additional resource attributes forwarded to every trace and logResourceAttributes=newDictionary<string,object>{{"deployment.environment","production"},{"cloud.region","us-east-1"}}};varobsBootstrapper=newObservabilityBootstrapper(services,config);obsBootstrapper.Run();// Result exposes the IServiceCollection with OTel tracing registeredvarprovider=obsBootstrapper.Result!.BuildServiceProvider();Local environment: the
examples/observability/folder contains adocker-compose.ymlthat spins up an OTel Collector, Tempo, Loki, and Grafana in one command:cd examples/observability docker-compose up -d
The following shows a complete Program.cs that wires everything together using a single CompositeBootstrapper.
usingBeyondNet.Bootstrapper.Impl;usingBeyondNet.Bootstrapper.DependencyInjection;usingBeyondNet.Bootstrapper.AutoMapper;usingBeyondNet.Bootstrapper.Observability;usingMicrosoft.Extensions.DependencyInjection;// ── 1. Declare each bootstrapper ──────────────────────────────────────varservices=newServiceCollection();vardi=newDependencyInjectionBootstrapper(services, svc =>{svc.AddSingleton<IOrderRepository,SqlOrderRepository>();svc.AddScoped<IOrderService,OrderService>();});varmapper=newAutoMapperBootstrapper(cfg =>{cfg.CreateMap<OrderEntity,OrderDto>();});varobservability=newObservabilityBootstrapper(services,newObservabilityConfiguration{ServiceName="OrderService",ServiceVersion="2.0.0",OTLPEndpoint="http://localhost:4317"});// ── 2. Run all steps in sequence ─────────────────────────────────────newCompositeBootstrapper().Add(di).Add(mapper).Add(observability).Run();// ── 3. Use the results ────────────────────────────────────────────────varprovider=di.Result!.BuildServiceProvider();variMapper=mapper.Result!.CreateMapper();varorderSvc=provider.GetRequiredService<IOrderService>();Async variant (e.g., startup with database health-check):
usingBeyondNet.Bootstrapper.Impl;usingBeyondNet.Bootstrapper.Interface;publicclassDatabasePingBootstrapper:IBootstrapperAsync<bool>{publicbool?Result{get;privateset;}publicasyncTaskRunAsync(CancellationTokenct=default){// Replace with actual connection checkawaitTask.Delay(20,ct);Result=true;}}// In Program.csusingvarcts=newCancellationTokenSource(TimeSpan.FromSeconds(15));vardbPing=newDatabasePingBootstrapper();awaitnewCompositeBootstrapperAsync().Add(dbPing).RunAsync(cts.Token);if(dbPing.Result!=true)thrownewInvalidOperationException("Database unreachable at startup.");| Term | Definition |
|---|---|
| Bootstrapper | A class that encapsulates one startup concern (DB connection, DI registration, mapping profiles, etc.) and exposes its result via Result. |
| Composite | Structural design pattern that groups multiple bootstrappers and runs them sequentially as a single unit. |
| IBootstrapperAsync | The async contract. Use it whenever a step performs I/O-bound work (network, disk, database) to avoid blocking the thread pool. |
| CancellationToken | Passed through RunAsync to allow the host to cancel the entire startup sequence if a timeout is exceeded. |
| OTLP | OpenTelemetry Protocol — the vendor-neutral format for shipping metrics, traces, and logs to a unified collector. |
| OTel Collector | A proxy that receives OTLP signals and fans them out to storage backends (Tempo for traces, Loki for logs). |