Utilities

FluentDocker provides several utility classes and extension methods to simplify common operations.

Step by Step

TemplateString

Dynamic path interpolation with support for environment variables, temporary paths, and random strings.

Basic Usage

using FluentDocker.Model.Common;

// Temporary directory
var tempPath = new TemplateString("${TEMP}/myapp");
// Expands to: /tmp/myapp (Linux) or C:\Users\...\AppData\Local\Temp\myapp (Windows)

// With random suffix
var uniquePath = new TemplateString("${TEMP}/test-${RND}");
// Expands to: /tmp/test-tmpk4xz0f.tmp

// Current directory
var workPath = new TemplateString("${PWD}/config");
// Expands to: /current/working/directory/config

Environment Variables

// Access any environment variable with E_ prefix
var homePath = new TemplateString("${E_HOME}/myapp");
// Expands to: /home/user/myapp

var userPath = new TemplateString("${E_USER}/.config");
// Expands to: username/.config

// Custom environment variables
Environment.SetEnvironmentVariable("MY_VAR", "custom-value");
var customPath = new TemplateString("${E_MY_VAR}/data");
// Expands to: custom-value/data

Custom Environment Variables

// If the environment variable is not set, the token remains unexpanded
var path = new TemplateString("${E_CUSTOM_PATH}");
// Expands to: value of CUSTOM_PATH env var, or literal "${E_CUSTOM_PATH}" if unset

// Combine with other variables
var config = new TemplateString("${E_CONFIG_DIR}/data");
// Expands to: <CONFIG_DIR value>/data

Supported Variables

Variable Description Example
${TEMP} System temp directory /tmp
${TMP} Same as TEMP /tmp
${RND} Random filename via Path.GetRandomFileName() tmpk4xz0f.tmp
${PWD} Current working directory /home/user/project
${E_*} Environment variable ${E_HOME} -> /home/user

With Containers

// Create unique temp directory for test
var testDir = new TemplateString("${TEMP}/integration-test-${RND}");

using var results = new Builder()
    .WithinDriver("docker", kernel)
    .UseContainer(c => c
        .UseImage("myapp:latest")
        .WithVolume(testDir, "/app/data"))
    .Build();

Combined Variables

var path = new TemplateString("${TEMP}/${E_USER}/session-${RND}");
// Might expand to: /tmp/john/session-tmpk4xz0f.tmp

HTTP Extensions (Wget)

Simple HTTP operations for health checks and API testing.

Basic GET Request

using FluentDocker.Extensions;

// Simple GET
var response = await "http://localhost:8080/health".Wget();
Console.WriteLine(response);  // Response body

Full Request with Status Code

using FluentDocker.Extensions;

// DoRequest returns a RequestResponse struct with Code, Body, Headers, Err
var result = await "http://localhost:8080/api/users".DoRequest();

if (result.Code == HttpStatusCode.OK)
{
    Console.WriteLine($"Users: {result.Body}");
}

// POST with JSON body
var postResult = await "http://localhost:8080/api/users".DoRequest(
    method: HttpMethod.Post,
    contentType: "application/json",
    body: "{\"name\":\"test\"}");

Health Check Pattern

using var results = new Builder()
    .WithinDriver("docker", kernel)
    .UseContainer(c => c
        .UseImage("myapi:latest")
        .ExposePort("8080"))
    .Build();

var container = results.Containers.First();
var endpoint = container.ToHostExposedEndpoint("8080/tcp");
var healthUrl = $"http://localhost:{endpoint.Port}/health";

// Wait for healthy
for (int i = 0; i < 30; i++)
{
    try
    {
        var response = await healthUrl.Wget();
        if (response.Contains("healthy"))
        {
            Console.WriteLine("Service is healthy!");
            break;
        }
    }
    catch
    {
        // Not ready yet
    }
    await Task.Delay(1000);
}

Download File

var url = new Uri("https://example.com/file.zip");
await url.Download("/local/path/file.zip");

Resource Extensions

Extract embedded resources from assemblies.

Extract Single File

using FluentDocker.Extensions;

// Extract embedded resource to temp directory (returns void)
typeof(MyTests).ResourceExtract(
    new TemplateString("${TEMP}/test-config"),
    "config.json"
);

// File is now at: ${TEMP}/test-config/config.json

Extract Multiple Files

// Extract multiple resources
typeof(MyTests).ResourceExtract(
    new TemplateString("${TEMP}/test-resources"),
    "config.json",
    "schema.sql",
    "test-data.csv"
);

Query Resources

// List all embedded resources
var resources = typeof(MyTests).ResourceQuery();
foreach (var resource in resources)
{
    Console.WriteLine($"Resource: {resource.Name}");
}

Extract to File

// Extract matching resources to a directory (returns void)
typeof(MyTests)
    .ResourceQuery()
    .Where(r => r.Name.EndsWith("config.json"))
    .ToFile(new TemplateString("${TEMP}/extracted"));

With Containers

// Extract test fixtures and mount in container
var fixturesPath = new TemplateString("${TEMP}/fixtures-${RND}");
typeof(MyTests).ResourceExtract(
    fixturesPath,
    "test-data.sql",
    "seed-data.json"
);

using var results = new Builder()
    .WithinDriver("docker", kernel)
    .UseContainer(c => c
        .UseImage("postgres:15-alpine")
        .WithEnvironment("POSTGRES_PASSWORD=test")
        .WithVolume(fixturesPath, "/docker-entrypoint-initdb.d")
        .ExposePort("5432")
        .WaitForPort("5432/tcp", 30000))
    .Build();

// Database initialized with test-data.sql

Logging

FluentDocker logs through Microsoft.Extensions.Logging.Abstractions. An ILoggerFactory is required when constructing the kernel — there is no library-side default. To suppress logs entirely, pass NullLoggerFactory.Instance explicitly at the call site.

Plug in any logging provider

using Microsoft.Extensions.Logging;
using FluentDocker.Kernel;

using var factory = LoggerFactory.Create(b => b
    .SetMinimumLevel(LogLevel.Debug)
    .AddSimpleConsole(o => o.SingleLine = true));

var kernel = await FluentDockerKernel.Create(factory)
    .WithDockerCli("docker", d => d.AsDefault())
    .BuildAsync();

Suppress all logging explicitly

using Microsoft.Extensions.Logging.Abstractions;

var kernel = await FluentDockerKernel.Create(NullLoggerFactory.Instance)
    .WithDockerCli("docker", d => d.AsDefault())
    .BuildAsync();

Categories

Each FluentDocker type uses its fully-qualified type name as its log category, so you can filter at any granularity:

using var factory = LoggerFactory.Create(b => b
    .AddFilter("FluentDocker.Drivers.Docker.Cli", LogLevel.Warning)
    .AddFilter("FluentDocker.Services.Impl.ContainerService", LogLevel.Debug)
    .AddConsole());

appsettings.json configuration works the same way via the standard MEL AddConfiguration(IConfiguration) glue in your host:

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "FluentDocker.Drivers": "Warning",
      "FluentDocker.Services.Impl.ContainerService": "Debug"
    }
  }
}

Log levels used by the library

  • Debug: best-effort parse skips inside streaming JSON readers (stats, events, compose service info) — these are routine when output formats vary.
  • Warning: cleanup-time failures during dispose/teardown — non-fatal but flagged for visibility.
  • Error: operation failures that the library catches and surfaces back to the caller as a failed CommandResponse.

SudoMechanism

Configure sudo behavior for Linux environments via the kernel builder.

No Sudo (Default)

using var kernel = await FluentDockerKernel.Create()
    .WithDockerCli("docker", d => d.AsDefault())
    .BuildAsync();
// Commands run without sudo

Passwordless Sudo

using FluentDocker.Model.Common;

using var kernel = await FluentDockerKernel.Create()
    .WithDockerCli("docker", d => d
        .WithSudo(SudoMechanism.NoPassword)
        .AsDefault())
    .BuildAsync();
// Commands prefixed with: sudo

Sudo with Password

using var kernel = await FluentDockerKernel.Create()
    .WithDockerCli("docker", d => d
        .WithSudo(SudoMechanism.Password, "your-password")
        .AsDefault())
    .BuildAsync();
// Commands prefixed with: echo 'password' | sudo -S

Model Extensions

Useful extension methods for FluentDocker models.

Get Exposed Endpoint

using FluentDocker.Services.Extensions;

var container = /* ... */;

// Get endpoint for exposed port
var endpoint = container.ToHostExposedEndpoint("8080/tcp");
Console.WriteLine($"Connect to: {endpoint.Address}:{endpoint.Port}");

Get All Endpoints

var ports = container.GetConfiguration().NetworkSettings.Ports;
foreach (var port in ports)
{
    Console.WriteLine($"Port: {port.Key} -> {port.Value?.FirstOrDefault()?.HostPort}");
}

Check Container State

if (container.State == ServiceRunningState.Running)
{
    // Container is running
}

// Or check configuration
var config = container.GetConfiguration(fresh: true);
if (config.State.Running)
{
    Console.WriteLine($"Container running since: {config.State.StartedAt}");
}

Endpoint Resolution

Custom endpoint resolvers for special network configurations.

Default Resolver

// Uses container's exposed port mapping
var endpoint = container.ToHostExposedEndpoint("8080/tcp");

Custom Resolver

The custom resolver is set via UseCustomResolver() on the container builder, not passed to ToHostExposedEndpoint(). The resolver signature is: Func<Dictionary<string, HostIpEndpoint[]>, string, Uri, IPEndPoint>

using FluentDocker.Model.Containers;

// Configure custom resolver on the builder
using var results = new Builder()
    .WithinDriver("docker", kernel)
    .UseContainer(c => c
        .UseImage("myapp:latest")
        .ExposePort("8080")
        .UseCustomResolver((portBindings, portAndProto, requestUrl) =>
        {
            if (portBindings.TryGetValue(portAndProto, out var endpoints)
                && endpoints.Length > 0)
            {
                // Force localhost for Docker Desktop on Mac/Windows
                return new IPEndPoint(IPAddress.Loopback, endpoints[0].Port);
            }

            return null;
        }))
    .Build();

// ToHostExposedEndpoint uses the custom resolver automatically
var endpoint = results.Containers.First().ToHostExposedEndpoint("8080/tcp");

Command Response Handling

Work with command results from Docker operations.

Check Success

var result = await containerDriver.CreateAsync(context, config);

if (result.Success)
{
    Console.WriteLine($"Container ID: {result.Data}");
}
else
{
    Console.WriteLine($"Error: {result.Error}");
    Console.WriteLine($"Exit code: {result.ExitCode}");
}

Access Output

var result = await containerDriver.ExecAsync(context, containerId, "ls", "-la");

if (result.Success)
{
    Console.WriteLine(result.Data);          // Typed payload
    Console.WriteLine(result.Output);        // Combined stdout (string)
}
else
{
    Console.WriteLine($"{result.ErrorCode}: {result.Error}");
    var ctx = result.ErrorContext;
    Console.WriteLine($"stdout:\n{ctx?.StdOut}\nstderr:\n{ctx?.StdErr}");
}

Container Stats Parsing

Parse Docker stats output.

var stats = await container.GetStatsAsync();

// CPU usage
Console.WriteLine($"CPU: {stats.Cpu.UsagePercent:F2}%");

// Memory
Console.WriteLine($"Memory: {FormatBytes(stats.Memory.Usage)} / {FormatBytes(stats.Memory.Limit)}");
Console.WriteLine($"Memory %: {stats.Memory.UsagePercent:F2}%");

// Network
Console.WriteLine($"Network RX: {FormatBytes(stats.Network.RxBytes)}");
Console.WriteLine($"Network TX: {FormatBytes(stats.Network.TxBytes)}");

// Block I/O
Console.WriteLine($"Block Read: {FormatBytes(stats.Disk.ReadBytes)}");
Console.WriteLine($"Block Write: {FormatBytes(stats.Disk.WriteBytes)}");

string FormatBytes(long bytes)
{
    string[] sizes = { "B", "KB", "MB", "GB" };
    int order = 0;
    double len = bytes;
    while (len >= 1024 && order < sizes.Length - 1)
    {
        order++;
        len /= 1024;
    }
    return $"{len:0.##} {sizes[order]}";
}

Utility Examples

Test Data Generation

public static class TestDataGenerator
{
    public static string UniqueId() => Guid.NewGuid().ToString("N")[..8];

    public static string TempPath(string prefix = "test") =>
        new TemplateString($"$/{prefix}-$").ToString();

    public static async Task<string> WaitForHealthy(string url, int timeoutSeconds = 30)
    {
        for (int i = 0; i < timeoutSeconds; i++)
        {
            try
            {
                var response = await url.Wget();
                if (!string.IsNullOrEmpty(response))
                    return response;
            }
            catch { }
            await Task.Delay(1000);
        }
        throw new TimeoutException($"Service at {url} did not become healthy");
    }
}

// Usage
var testDir = TestDataGenerator.TempPath("integration");
var response = await TestDataGenerator.WaitForHealthy($"http://localhost:{port}/health");

Container Factory

public static class ContainerFactory
{
    public static BuildResults CreatePostgres(
        FluentDockerKernel kernel, string password = "test")
    {
        return new Builder()
            .WithinDriver("docker", kernel)
            .UseContainer(c => c
                .UseImage("postgres:15-alpine")
                .WithEnvironment($"POSTGRES_PASSWORD={password}")
                .ExposePort("5432")
                .WaitForPort("5432/tcp", 30000))
            .Build();
    }

    public static BuildResults CreateRedis(FluentDockerKernel kernel)
    {
        return new Builder()
            .WithinDriver("docker", kernel)
            .UseContainer(c => c
                .UseImage("redis:alpine")
                .ExposePort("6379")
                .WaitForPort("6379/tcp", 30000))
            .Build();
    }

    public static BuildResults CreateRabbitMQ(FluentDockerKernel kernel)
    {
        return new Builder()
            .WithinDriver("docker", kernel)
            .UseContainer(c => c
                .UseImage("rabbitmq:3-management-alpine")
                .ExposePort("5672")
                .ExposePort("15672")
                .WaitForPort("5672/tcp", 60000))
            .Build();
    }
}

// Usage
using var kernel = FluentDockerKernel.Create()
    .WithDockerCli("docker", d => d.AsDefault())
    .Build();

using var db = ContainerFactory.CreatePostgres(kernel);
using var cache = ContainerFactory.CreateRedis(kernel);

Next Steps