Skip to main content
Version: Next

Lifecycle Hooks

Sharkable provides several hooks and services that run during application startup and shutdown, giving you fine-grained control over the application lifecycle.

Startup Banner

When UseShark() completes, Sharkable prints a formatted banner to the console with version, environment, and UTC timestamp:

╔══════════════════════════════════════════════╗
║ Sharkable v0.6.0 ║
║ Environment: Development ║
║ Started at: 2026-07-15 12:34:56 ║
╚══════════════════════════════════════════════╝

The banner is printed at the end of UseShark(), after all wiring, warmup, and validation complete. Set ShowStartupBanner = false to disable:

builder.Services.AddShark(opt =>
{
opt.ShowStartupBanner = false;
});

Application Lifecycle Hooks

Register callbacks that fire on IHostApplicationLifetime events:

builder.Services.AddShark(opt =>
{
opt.ConfigureOnStarted(sp =>
{
// Runs after app.Run() begins accepting requests
var logger = sp.GetRequiredService<ILogger<Program>>();
logger.LogInformation("Application started, ready to serve");
});

opt.ConfigureOnStopped(sp =>
{
// Runs on SIGTERM / graceful shutdown
var logger = sp.GetRequiredService<ILogger<Program>>();
logger.LogInformation("Application stopping");
});
});

The OnStarted callback fires when IHostApplicationLifetime.ApplicationStarted is raised (after the server starts). The OnStopped callback fires on ApplicationStopping (during graceful shutdown). Both receive the root IServiceProvider.

Warmup Service

Implement IWarmupService to run initialization logic synchronously during UseShark(), before the readiness gate opens:

public class DatabaseWarmup : IWarmupService
{
private readonly MyDbContext _db;

public DatabaseWarmup(MyDbContext db)
{
_db = db;
}

public async Task WarmupAsync(CancellationToken cancellationToken)
{
// Run schema migrations, seed data, pre-compile queries
await _db.Database.EnsureCreatedAsync(cancellationToken);
}
}

Register the warmup service:

builder.Services.AddShark(opt =>
{
opt.ConfigureWarmup<DatabaseWarmup>();
});

Sharkable resolves the warmup service from DI, calls WarmupAsync() with a 30-second timeout, and opens the readiness gate only on success. Throw to fail startup.

Eager Singleton

Mark a singleton service as Eager = true to have it resolved from DI during UseShark() rather than lazily on first access:

[SingletonService(Eager = true)]
public interface ICacheWarmup
{
void Preload();
}

public class CacheWarmup : ICacheWarmup
{
public void Preload()
{
// Initialize cache, connect to remote services, etc.
}
}

The service is resolved during startup, before the readiness gate opens and before the server accepts requests. This ensures eagerly-loaded singletons are fully initialized before traffic arrives.

Group & Endpoint Conventions

Use GroupConvention and EndpointConvention to apply shared middleware, filters, or metadata across groups or endpoints declaratively:

builder.Services.AddShark(opt =>
{
opt.GroupConvention = (group, name) =>
{
group.RequireAuthorization();
group.WithOpenApi();
};

opt.EndpointConvention = (group, type) =>
{
group.WithMetadata(new ProducesResponseTypeMetadata
{
StatusCode = 200,
ContentTypes = ["application/json"]
});
};
});
  • GroupConvention is called once per endpoint group (e.g., api/users). Use it for shared authorization, OpenAPI, or middleware applied to the entire group.
  • EndpointConvention is called once per endpoint class. Use it for ProducesResponseType metadata, common filters, or transformer registration at the class level.

Parallel Warmup

Multiple IWarmupService implementations registered via separate ConfigureWarmup<T>() calls run in parallel during startup:

builder.Services.AddShark(opt =>
{
opt.ConfigureWarmup<DatabaseWarmup>();
opt.ConfigureWarmup<CacheWarmup>();
});

Each warmup service:

  • Runs concurrently with other warmup services
  • Has its own 30-second timeout (adjustable per service)
  • Throwing fails startup — aggregate exceptions are propagated

Pipeline Injection Points

Inject custom middleware at specific positions in Sharkable's pipeline using UseSharkOptions:

app.UseShark(opt =>
{
// Runs before authentication/authorization
opt.AddBeforeAuth(app =>
{
app.Use(async (context, next) =>
{
// Custom pre-auth logic (request logging, header validation)
await next(context);
});
});

// Runs after authentication/authorization but before endpoints
opt.AddAfterAuth(app =>
{
app.UseMiddleware<CustomTenantMiddleware>();
});

// Runs after all Sharkable endpoints are mapped
opt.AddAfterEndpoints(app =>
{
app.MapFallback(() => Results.NotFound());
});
});

Available injection points (in pipeline order):

PointLocationUse Case
AddBeforeAuthAfter CORS, before AuthenticationRequest logging, header validation
AddAfterAuthAfter Authorization, before exception handlerTenant resolution, culture setting
AddAfterEndpointsAfter all endpoints mappedFallback routes, custom error pages

DI Validation

Validate critical service registrations at startup. If a service cannot be resolved, UseShark() throws immediately, preventing the server from starting with broken dependencies:

builder.Services.AddShark(opt =>
{
opt.ValidateOnStart<IMyService>();
opt.ValidateOnStart<IOtherService>();
});

Each registered type is resolved via GetRequiredService<T>() during UseShark(), before the readiness gate opens. This provides fast feedback during development and prevents silent failures in production.