Hosting Integration
TinyBDD.Extensions.Hosting integrates TinyBDD with Microsoft.Extensions.Hosting, enabling BDD workflows to run as background services, startup tasks, or scheduled jobs within .NET Generic Host applications.
Installation
dotnet add package TinyBDD.Extensions.Hosting
This package includes TinyBDD.Extensions.DependencyInjection as a dependency.
Quick Start
// Program.cs
var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddTinyBddHosting();
builder.Services.AddWorkflowHostedService<StartupWorkflow>();
var host = builder.Build();
await host.RunAsync();
// StartupWorkflow.cs
public class StartupWorkflow : IWorkflowDefinition
{
public string FeatureName => "Application Startup";
public string ScenarioName => "Initialize all services";
public string? FeatureDescription => "Ensures all dependencies are ready";
public async ValueTask ExecuteAsync(ScenarioContext context, CancellationToken ct)
{
await Bdd.Given(context, "configuration loaded", () => LoadConfig())
.When("database migrated", cfg => MigrateDatabase(cfg, ct))
.And("cache initialized", cfg => InitializeCache(cfg, ct))
.Then("system ready", _ => true);
}
}
Service Registration
Basic Registration
// Using IHostBuilder
builder.UseTinyBdd();
// Using IServiceCollection
services.AddTinyBddHosting();
With Configuration
builder.UseTinyBdd(options =>
{
options.StopHostOnCompletion = true; // Stop host when workflow completes
options.StopHostOnFailure = true; // Stop host if workflow fails
options.StartupDelay = TimeSpan.Zero; // Delay before starting
options.ShutdownTimeout = TimeSpan.FromSeconds(30);
});
Adding Workflow Hosted Services
// Register workflow type (resolved from DI)
services.AddWorkflowHostedService<MyWorkflow>();
// Register workflow instance
services.AddWorkflowHostedService(new MyWorkflow());
API Reference
TinyBddHostingOptions
| Property | Type | Default | Description |
|---|---|---|---|
StopHostOnCompletion |
bool |
false |
Stop host when workflow completes successfully |
StopHostOnFailure |
bool |
true |
Stop host when workflow fails |
StartupDelay |
TimeSpan |
Zero |
Delay before workflow execution begins |
ShutdownTimeout |
TimeSpan |
30s |
Graceful shutdown timeout for running workflows |
IWorkflowDefinition
public interface IWorkflowDefinition
{
/// <summary>Feature name for this workflow.</summary>
string FeatureName { get; }
/// <summary>Scenario name for this workflow.</summary>
string ScenarioName { get; }
/// <summary>Optional feature description. Return null if not needed.</summary>
string? FeatureDescription { get; }
/// <summary>Execute the workflow.</summary>
ValueTask ExecuteAsync(ScenarioContext context, CancellationToken cancellationToken);
}
IWorkflowRunner
public interface IWorkflowRunner
{
/// <summary>Run a workflow definition.</summary>
Task<ScenarioContext> RunAsync(
IWorkflowDefinition workflow,
CancellationToken cancellationToken = default);
/// <summary>Run a workflow delegate.</summary>
Task<ScenarioContext> RunAsync(
string featureName,
string scenarioName,
Func<ScenarioContext, CancellationToken, ValueTask> workflow,
CancellationToken cancellationToken = default);
}
Workflow Patterns
One-Shot Startup Workflow
Execute once at application startup, then stop:
builder.Services.AddTinyBddHosting(options =>
{
options.StopHostOnCompletion = true;
});
builder.Services.AddWorkflowHostedService<DatabaseMigrationWorkflow>();
public class DatabaseMigrationWorkflow : IWorkflowDefinition
{
private readonly IDbContext _db;
public DatabaseMigrationWorkflow(IDbContext db) => _db = db;
public string FeatureName => "Database Migration";
public string ScenarioName => "Apply pending migrations";
public string? FeatureDescription => null;
public async ValueTask ExecuteAsync(ScenarioContext context, CancellationToken ct)
{
await Bdd.Given(context, "pending migrations", () => _db.GetPendingMigrations())
.When("applied", migrations => ApplyMigrations(migrations, ct))
.Then("database up to date", result => result.Applied == result.Total);
}
}
Continuous Background Workflow
Run indefinitely, repeating the workflow:
public class HealthMonitorWorkflow : BackgroundService
{
private readonly IWorkflowRunner _runner;
public HealthMonitorWorkflow(IWorkflowRunner runner) => _runner = runner;
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
var context = await _runner.RunAsync(
"Health Monitoring",
"Check all services",
async (ctx, ct) =>
{
await Bdd.Given(ctx, "services to check", () => GetServiceEndpoints())
.When("health checked", endpoints => CheckHealthAsync(endpoints, ct))
.Then("all healthy", results => results.All(r => r.IsHealthy));
},
stoppingToken);
LogResults(context);
await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
}
}
}
Workflow with Dependencies
Workflows can inject services:
public class OrderProcessingWorkflow : IWorkflowDefinition
{
private readonly IOrderRepository _orders;
private readonly IPaymentGateway _payments;
private readonly IInventoryService _inventory;
private readonly INotificationService _notifications;
public OrderProcessingWorkflow(
IOrderRepository orders,
IPaymentGateway payments,
IInventoryService inventory,
INotificationService notifications)
{
_orders = orders;
_payments = payments;
_inventory = inventory;
_notifications = notifications;
}
public string FeatureName => "Order Processing";
public string ScenarioName => "Process pending orders";
public string? FeatureDescription => null;
public async ValueTask ExecuteAsync(ScenarioContext context, CancellationToken ct)
{
await Bdd.Given(context, "pending orders", () => _orders.GetPendingAsync(ct))
.When("payments processed", orders => ProcessPaymentsAsync(orders, ct))
.And("inventory reserved", orders => ReserveInventoryAsync(orders, ct))
.When("shipped", orders => CreateShipmentsAsync(orders, ct))
.Then("customers notified", orders => NotifyCustomersAsync(orders, ct));
}
private async Task<IList<Order>> ProcessPaymentsAsync(IList<Order> orders, CancellationToken ct)
{
foreach (var order in orders)
{
await _payments.ChargeAsync(order.PaymentDetails, order.Total, ct);
order.PaymentStatus = PaymentStatus.Charged;
}
return orders;
}
// ... other methods
}
Programmatic Workflow Execution
Use IWorkflowRunner directly for on-demand execution:
[ApiController]
[Route("api/[controller]")]
public class JobsController : ControllerBase
{
private readonly IWorkflowRunner _runner;
public JobsController(IWorkflowRunner runner) => _runner = runner;
[HttpPost("sync")]
public async Task<IActionResult> TriggerSync(CancellationToken ct)
{
var context = await _runner.RunAsync(
"Data Sync",
"Manual sync triggered",
async (ctx, token) =>
{
await Bdd.Given(ctx, "external data fetched", () => FetchExternalData(token))
.When("transformed", data => TransformData(data))
.When("saved", data => SaveData(data, token))
.Then("sync complete", result => result.Success);
},
ct);
return Ok(new
{
Success = context.Steps.All(s => s.Error == null),
Steps = context.Steps.Select(s => new
{
s.Kind,
s.Title,
DurationMs = s.Elapsed.TotalMilliseconds,
Error = s.Error?.Message
})
});
}
}
Error Handling
Workflow Failure Behavior
builder.Services.AddTinyBddHosting(options =>
{
// Stop the entire application on workflow failure
options.StopHostOnFailure = true;
});
Step-Level Error Handling
Configure scenario options for fine-grained control:
builder.Services.AddTinyBdd(options =>
{
options.DefaultScenarioOptions = new ScenarioOptions
{
ContinueOnError = true, // Continue after failures
MarkRemainingAsSkippedOnFailure = true, // Mark skipped steps
StepTimeout = TimeSpan.FromSeconds(30) // Per-step timeout
};
});
Graceful Shutdown
builder.Services.AddTinyBddHosting(options =>
{
options.ShutdownTimeout = TimeSpan.FromSeconds(60); // Wait for workflow to complete
});
When the host receives a shutdown signal:
- The
CancellationTokenpassed toExecuteAsyncis cancelled - The workflow has
ShutdownTimeoutto complete gracefully - After timeout, the workflow is forcibly terminated
Observability
Logging
WorkflowRunner logs workflow execution:
info: TinyBDD.Extensions.Hosting.WorkflowRunner[0]
Starting workflow: Order Processing - Process pending orders
info: TinyBDD.Extensions.Hosting.WorkflowRunner[0]
Workflow completed successfully: Order Processing - Process pending orders, 5 steps
Custom Step Logging
Access step results for detailed logging:
public class ObservableWorkflow : IWorkflowDefinition
{
private readonly ILogger<ObservableWorkflow> _logger;
public ObservableWorkflow(ILogger<ObservableWorkflow> logger) => _logger = logger;
public string FeatureName => "Observable Workflow";
public string ScenarioName => "With detailed logging";
public string? FeatureDescription => null;
public async ValueTask ExecuteAsync(ScenarioContext context, CancellationToken ct)
{
await Bdd.Given(context, "setup", () => Setup())
.When("execute", data => Execute(data, ct))
.Then("verify", result => Verify(result));
// Log all steps after execution
foreach (var step in context.Steps)
{
_logger.LogInformation(
"Step {Kind} '{Title}': {Status} in {Duration}ms",
step.Kind,
step.Title,
step.Error == null ? "OK" : $"FAILED: {step.Error.Message}",
step.Elapsed.TotalMilliseconds);
}
// Log step I/O for debugging
foreach (var io in context.IO)
{
_logger.LogDebug(
"Step '{Title}': Input={InputType}, Output={OutputType}",
io.Title,
io.Input?.GetType().Name ?? "null",
io.Output?.GetType().Name ?? "null");
}
}
}
Metrics Integration
public class MetricsWorkflowRunner : IWorkflowRunner
{
private readonly IWorkflowRunner _inner;
private readonly IMetricsCollector _metrics;
public MetricsWorkflowRunner(WorkflowRunner inner, IMetricsCollector metrics)
{
_inner = inner;
_metrics = metrics;
}
public async Task<ScenarioContext> RunAsync(IWorkflowDefinition workflow, CancellationToken ct)
{
var sw = Stopwatch.StartNew();
try
{
var context = await _inner.RunAsync(workflow, ct);
_metrics.RecordWorkflowDuration(workflow.FeatureName, sw.Elapsed);
_metrics.RecordWorkflowResult(workflow.FeatureName, context.Steps.All(s => s.Error == null));
return context;
}
catch (Exception ex)
{
_metrics.RecordWorkflowError(workflow.FeatureName, ex);
throw;
}
}
}
Testing Hosted Workflows
public class WorkflowTests
{
[Fact]
public async Task OrderProcessingWorkflow_ProcessesPendingOrders()
{
// Arrange
var services = new ServiceCollection();
services.AddLogging();
services.AddTinyBddHosting();
services.AddSingleton<IOrderRepository>(new FakeOrderRepository(pendingOrders: 3));
services.AddSingleton<IPaymentGateway>(new FakePaymentGateway());
services.AddSingleton<IInventoryService>(new FakeInventoryService());
services.AddSingleton<INotificationService>(new FakeNotificationService());
services.AddScoped<OrderProcessingWorkflow>();
var provider = services.BuildServiceProvider();
var runner = provider.GetRequiredService<IWorkflowRunner>();
var workflow = provider.GetRequiredService<OrderProcessingWorkflow>();
// Act
var context = await runner.RunAsync(workflow);
// Assert
Assert.True(context.Steps.All(s => s.Error == null));
Assert.Equal(5, context.Steps.Count);
}
}
Best Practices
- Single responsibility: Each workflow should do one thing well
- Inject dependencies: Use constructor injection for testability
- Handle cancellation: Always respect the
CancellationToken - Log step results: Capture execution details for debugging
- Configure timeouts: Prevent runaway workflows with
StepTimeout - Test workflows: Use the same DI setup in tests
Next Steps
- Dependency Injection Guide - DI integration details
- Reporting Extension - JSON reporting and observer pattern
- Orchestrator Patterns - Advanced workflow patterns
- Enterprise Samples - Production-ready examples
Return to: Extensions Index | User Guide