Skip to main content

Infrastructure recipes

The concept is in Infrastructure: one target, an ordered chain, one winner. The recipes below start from that page's container chain.

The sample's two flows​

The sample suite registers its broker and database targets as above (Environments shows the three shapes they select). The browser journey's address comes from the application chain: see Hosting a browser journey for the loopback recipe.

An application image is the container counterpart: ApplicationContainer (ProtoTest.Testcontainers) starts the image as run infrastructure and fills ProtoTest:Applications:{application}:BaseUrl from the mapped address, so the application's clients, browser sessions and readiness probe resolve it:

var api = ApplicationContainer.Container("Api", "my-registry.example.test/orders-api:1.4", port: 8080);
builder.AddInfrastructure(
"OrdersApi",
chain => chain
.UseConfigured()
.UseContainer(api),
api.BaseUrlKey);

Register it instead of AddAspNetCoreServer for that application: a containerized application has no in-process server. Hosting a browser journey shows the full composition next to the loopback recipe.

Wait until it is ready​

A running container is not necessarily serving, and a published application may still be coming up. Readiness probes replace the sleep at the top of setup:

builder
.AddInfrastructure(
"NorthstarDatabase",
chain => chain.UseContainer(PostgresDatabase.Container()),
"ConnectionStrings:Northstar")
.AddReadinessProbe("Northstar API", ProtoReadiness.Http(new Uri("http://localhost:5080/health")))
.ConfigureReadiness(readiness =>
{
readiness.Timeout = TimeSpan.FromSeconds(60);
readiness.Interval = TimeSpan.FromMilliseconds(200);
});
  • A probe is infrastructure: the host awaits it at its registration position, and it is recorded as a readiness run entity carrying the attempts and the wait it spent.
  • ProtoReadiness.Tcp(host, port) is ready when a connection succeeds. ProtoReadiness.Http(url) is ready when the address answers at all; pass an acceptance check to demand a status or a health payload. Any delegate returning ValueTask<bool> works too.
  • An exception is "not ready yet": a connection refusal while a container boots is normal, and the last error appears in the timeout failure. The default timeout is 30 seconds.
  • One policy governs every wait: ConfigureReadiness sets the timeout and interval for host probes and for the containers the run starts, and the section ProtoTest:Readiness binds over the code values when the host is built. A slow image is tuned in one place.
  • A probe that never becomes ready fails the run before the first test, naming the probe, its attempts and the last error.

The shipped containers declare their own checks: a PostgreSQL container waits for its standard port to accept connections, RabbitMQ for the AMQP port. When a custom image listens elsewhere, override the port:

builder.AddInfrastructure(
"NorthstarDatabase",
chain => chain.UseContainer(PostgresDatabase.Container().ReadyOn(5433)),
"ConnectionStrings:Northstar");

A published application is waited for where its address is declared, in ProtoTest:Applications:{application}:BaseUrl or the address a settings piece published:

builder.AddHttpReadiness("Northstar API");

Register the probe after the piece that publishes the address. Probes are awaited at their registration position, so a probe registered first resolves nothing and records readiness.skipped naming its position and the later publisher instead of waiting. It never claims the application runs in-process. An in-process application has no address to wait for, so the probe is skipped and records why.

Run-scoped setup​

Some run-owned state is an action rather than a piece to own: create the schema of a container database, seed a catalogue, warm a cache. AddRunSetup(name, delegate) runs it once at the run's start, at its registration position in the infrastructure order. A step registered after a container reads the connection string that container published:

builder
.AddInfrastructure(
"NorthstarDatabase",
chain => chain
.UseConfigured()
.UseContainer(PostgresDatabase.Container()),
"ConnectionStrings:Northstar")
.AddSql(
provider => new NpgsqlConnection(ResolveDatabase(provider, "ConnectionStrings:Northstar")),
sql => sql.AddressKeys.Add("ConnectionStrings:Northstar"))
.AddEntityFrameworkCore<OrdersDbContext>((services, options) =>
options.UseNpgsql(services.GetRequiredService<DbConnection>()))
.AddRunSetup("database schema", async setup =>
{
var connectionString = setup.Settings.Values.TryGetValue("ConnectionStrings:Northstar", out var published)
? published
: setup.Configuration["ConnectionStrings:Northstar"]
?? throw new InvalidOperationException(
"ConnectionStrings:Northstar is not configured and no container published it.");
var options = new DbContextOptionsBuilder<OrdersDbContext>().UseNpgsql(connectionString).Options;
await using var context = new OrdersDbContext(options);
await context.Database.EnsureCreatedAsync(setup.CancellationToken);
});

The step receives a ProtoRunSetupContext:

  • Settings: the values the pieces registered before it published, so it reads a container's connection string without a second lookup.
  • Configuration: the suite's configuration, for an environment that provides the address and makes the container skip.
  • CancellationToken: the run's start token.

A step owns nothing to release. Stop and dispose release the run's resources and do not call the step again, and the run records it as an entity like any other piece. A step that throws fails the run's start with its own exception. What had started is released and the host stays retryable, so a retry runs the step again.

Use a step instead of a run hook or a test setup when the state belongs to the whole run: a run hook runs before infrastructure starts and cannot see a container's address, and a test hook or test body runs inside the per-test transaction, where its DDL is rolled back with the test. The SQL page shows the run-owned schema recipe.

AddResource versus AddInfrastructure​

IProtoHostBuilder.AddResource(IProtoResource) adds a run-scoped resource and nothing else: the host does not call StartAsync (a plain resource has none) and does not fill any settings. The resource is still recorded as owned and released with the run.

AddInfrastructureAddResource
Starts with the runyes, the winning provider's pieceno
Fills ProtoInfrastructureSettingsconnection string per key, plus settingsno
Registered as run entity and releasedyesyes

Use AddInfrastructure when the piece must start with the run or publish values. Use AddResource for an already-started handle that only needs the lifecycle.