Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions docs/api/connection_string_provider.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# Connection String Provider

The Connection String Provider API provides a standardized way to access and manage connection information for Testcontainers (modules). It allows developers to customize module-provided connection strings or add their own, and to access module-specific connection strings or endpoints (e.g., database connection strings, HTTP API base addresses) in a uniform way.

!!!note

Testcontainers modules do not yet implement this feature. Developers can use the provider to define and manage their own connection strings or endpoints. Providers will be integrated by modules in future releases.

## Example

Register a custom connection string provider via the container builder:

```csharp
IContainer container = new ContainerBuilder()
.WithConnectionStringProvider(new MyProvider1())
.Build();

// Implicit host connection string (default)
var hostConnectionStringImplicit = container.GetConnectionString();

// Explicit host connection string
var hostConnectionStringExplicit = container.GetConnectionString(ConnectionMode.Host);

// Container-to-container connection string
var containerConnectionString = container.GetConnectionString(ConnectionMode.Container);
```

## Implementing a custom provider

To create a custom provider, implement the generic interface: `IConnectionStringProvider<TContainer, TConfiguration>`. The `Configure(TContainer, TConfiguration)` method is invoked after the container has successfully started, ensuring that all runtime-assigned values are available.

=== "Generic builder"
```csharp
public sealed class MyProvider1 : IConnectionStringProvider<IContainer, IContainerConfiguration>
{
public void Configure(IContainer container, IContainerConfiguration configuration)
{
// Initialize provider with container information.
}

public string GetConnectionString(ConnectionMode connectionMode = ConnectionMode.Host)
{
// This method returns a default connection string. The connection mode argument
// lets you choose between a host connection or a container-to-container connection.
return "...";
}

public string GetConnectionString(string name, ConnectionMode connectionMode = ConnectionMode.Host)
{
// This method returns a connection string for the given name. Useful for modules
// with multiple endpoints (e.g., Azurite blob, queue, or table).
return "...";
}
}
```

=== "Module builder"
```csharp
public sealed class MyProvider2 : IConnectionStringProvider<PostgreSqlContainer, PostgreSqlConfiguration>
{
private string _host;

private ushort _port;

public void Configure(PostgreSqlContainer container, PostgreSqlConfiguration configuration)
{
// Initialize provider with container information.
_host = container.Hostname;
_port = container.GetMappedPublicPort(PostgreSqlBuilder.PostgreSqlPort);
}

public string GetConnectionString(ConnectionMode connectionMode = ConnectionMode.Host)
{
return $"Host={_host};Port={_port};...";
}

public string GetConnectionString(string name, ConnectionMode connectionMode = ConnectionMode.Host)
{
return $"Host={_host};Port={_port};...;SSL Mode=Require";
}
}
```
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ nav:
- api/resource_reaper.md
- api/resource_reuse.md
- api/wait_strategies.md
- api/connection_string_provider.md
- api/best_practices.md
- dind/index.md
- full_framework/index.md
Expand Down
2 changes: 1 addition & 1 deletion src/Testcontainers.Couchbase/CouchbaseBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ private CouchbaseBuilder WithBucket(params CouchbaseBucket[] bucket)
/// <summary>
/// Configures the Couchbase node.
/// </summary>
/// <param name="container">The container.</param>
/// <param name="container">The Couchbase container.</param>
/// <param name="ct">Cancellation token.</param>
private async Task ConfigureCouchbaseAsync(IContainer container, CancellationToken ct = default)
{
Expand Down
6 changes: 6 additions & 0 deletions src/Testcontainers/Builders/ContainerBuilder`3.cs
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,12 @@ public TBuilderEntity WithStartupCallback(Func<TContainerEntity, TConfigurationE
return Clone(new ContainerConfiguration(startupCallback: (container, configuration, ct) => startupCallback((TContainerEntity)container, (TConfigurationEntity)configuration, ct)));
}

/// <inheritdoc />
public TBuilderEntity WithConnectionStringProvider(IConnectionStringProvider<TContainerEntity, TConfigurationEntity> connectionStringProvider)
{
return Clone(new ContainerConfiguration(connectionStringProvider: new ConnectionStringProvider<TContainerEntity, TConfigurationEntity>(connectionStringProvider)));
}

/// <inheritdoc />
protected override TBuilderEntity Init()
{
Expand Down
10 changes: 10 additions & 0 deletions src/Testcontainers/Builders/IContainerBuilder`2.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ namespace DotNet.Testcontainers.Builders
/// <typeparam name="TConfigurationEntity">The configuration entity.</typeparam>
[PublicAPI]
public interface IContainerBuilder<out TBuilderEntity, out TContainerEntity, out TConfigurationEntity> : IAbstractBuilder<TBuilderEntity, TContainerEntity, CreateContainerParameters>
where TContainerEntity : IContainer
where TConfigurationEntity : IContainerConfiguration
{
/// <summary>
/// Accepts the license agreement.
Expand Down Expand Up @@ -500,5 +502,13 @@ public interface IContainerBuilder<out TBuilderEntity, out TContainerEntity, out
/// <returns>A configured instance of <typeparamref name="TBuilderEntity" />.</returns>
[PublicAPI]
TBuilderEntity WithStartupCallback(Func<TContainerEntity, TConfigurationEntity, CancellationToken, Task> startupCallback);

/// <summary>
/// Sets the connection string provider.
/// </summary>
/// <param name="connectionStringProvider">The connection string provider.</param>
/// <returns>A configured instance of <typeparamref name="TBuilderEntity" />.</returns>
[PublicAPI]
TBuilderEntity WithConnectionStringProvider(IConnectionStringProvider<TContainerEntity, TConfigurationEntity> connectionStringProvider);
}
}
21 changes: 21 additions & 0 deletions src/Testcontainers/Configurations/Containers/ConnectionMode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
namespace DotNet.Testcontainers.Configurations
{
using JetBrains.Annotations;

/// <summary>
/// Represents the connection mode.
/// </summary>
[PublicAPI]
public enum ConnectionMode
{
/// <summary>
/// The connection string for the container.
/// </summary>
Container,

/// <summary>
/// The connection string for the host.
/// </summary>
Host,
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
namespace DotNet.Testcontainers.Configurations
{
using DotNet.Testcontainers.Containers;

/// <inheritdoc cref="IConnectionStringProvider{TContainerEntity, TConfigurationEntity}" />
internal sealed class ConnectionStringProvider<TContainerEntity, TConfigurationEntity> : IConnectionStringProvider<IContainer, IContainerConfiguration>
where TContainerEntity : IContainer
where TConfigurationEntity : IContainerConfiguration
{
private readonly IConnectionStringProvider<TContainerEntity, TConfigurationEntity> _connectionStringProvider;

/// <summary>
/// Initializes a new instance of the <see cref="ConnectionStringProvider{TContainerEntity, TConfigurationEntity}" /> class.
/// </summary>
/// <param name="connectionStringProvider">The connection string provider.</param>
public ConnectionStringProvider(IConnectionStringProvider<TContainerEntity, TConfigurationEntity> connectionStringProvider)
{
_connectionStringProvider = connectionStringProvider;
}

/// <inheritdoc />
public void Configure(IContainer container, IContainerConfiguration configuration)
{
_connectionStringProvider.Configure((TContainerEntity)container, (TConfigurationEntity)configuration);
}

/// <inheritdoc />
public string GetConnectionString(ConnectionMode connectionMode = ConnectionMode.Host)
{
return _connectionStringProvider.GetConnectionString(connectionMode);
}

/// <inheritdoc />
public string GetConnectionString(string name, ConnectionMode connectionMode = ConnectionMode.Host)
{
return _connectionStringProvider.GetConnectionString(name, connectionMode);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ public ContainerConfiguration(
IOutputConsumer outputConsumer = null,
IEnumerable<WaitStrategy> waitStrategies = null,
Func<IContainer, IContainerConfiguration, CancellationToken, Task> startupCallback = null,
IConnectionStringProvider<IContainer, IContainerConfiguration> connectionStringProvider = null,
bool? autoRemove = null,
bool? privileged = null)
{
Expand All @@ -87,6 +88,7 @@ public ContainerConfiguration(
OutputConsumer = outputConsumer;
WaitStrategies = waitStrategies;
StartupCallback = startupCallback;
ConnectionStringProvider = connectionStringProvider;
}

/// <summary>
Expand Down Expand Up @@ -135,6 +137,7 @@ public ContainerConfiguration(IContainerConfiguration oldValue, IContainerConfig
OutputConsumer = BuildConfiguration.Combine(oldValue.OutputConsumer, newValue.OutputConsumer);
WaitStrategies = BuildConfiguration.Combine<IEnumerable<WaitStrategy>>(oldValue.WaitStrategies, newValue.WaitStrategies);
StartupCallback = BuildConfiguration.Combine(oldValue.StartupCallback, newValue.StartupCallback);
ConnectionStringProvider = BuildConfiguration.Combine(oldValue.ConnectionStringProvider, newValue.ConnectionStringProvider);
AutoRemove = (oldValue.AutoRemove.HasValue && oldValue.AutoRemove.Value) || (newValue.AutoRemove.HasValue && newValue.AutoRemove.Value);
Privileged = (oldValue.Privileged.HasValue && oldValue.Privileged.Value) || (newValue.Privileged.HasValue && newValue.Privileged.Value);
}
Expand Down Expand Up @@ -217,5 +220,9 @@ public ContainerConfiguration(IContainerConfiguration oldValue, IContainerConfig
/// <inheritdoc />
[JsonIgnore]
public Func<IContainer, IContainerConfiguration, CancellationToken, Task> StartupCallback { get; }

/// <inheritdoc />
[JsonIgnore]
public IConnectionStringProvider<IContainer, IContainerConfiguration> ConnectionStringProvider { get; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
namespace DotNet.Testcontainers.Configurations
{
using DotNet.Testcontainers.Containers;
using JetBrains.Annotations;

/// <summary>
/// A connection string provider.
/// </summary>
[PublicAPI]
public interface IConnectionStringProvider
{
/// <summary>
/// Gets the connection string.
/// </summary>
/// <param name="connectionMode">The connection mode.</param>
/// <returns>The connection string.</returns>
/// <exception cref="ConnectionStringProviderNotConfiguredException">Thrown when the connection string provider is not configured.</exception>
[NotNull]
string GetConnectionString(ConnectionMode connectionMode = ConnectionMode.Host);

/// <summary>
/// Gets the connection string.
/// </summary>
/// <param name="name">The connection string name.</param>
/// <param name="connectionMode">The connection mode.</param>
/// <returns>The connection string.</returns>
/// <exception cref="ConnectionStringProviderNotConfiguredException">Thrown when the connection string provider is not configured.</exception>
[NotNull]
string GetConnectionString(string name, ConnectionMode connectionMode = ConnectionMode.Host);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
namespace DotNet.Testcontainers.Configurations
{
using DotNet.Testcontainers.Containers;
using JetBrains.Annotations;

/// <inheritdoc cref="IConnectionStringProvider" />
[PublicAPI]
public interface IConnectionStringProvider<in TContainerEntity, in TConfigurationEntity> : IConnectionStringProvider
where TContainerEntity : IContainer
where TConfigurationEntity : IContainerConfiguration
{
/// <summary>
/// Configures the connection string provider.
/// </summary>
/// <param name="container">The container instance.</param>
/// <param name="configuration">The container configuration.</param>
void Configure(TContainerEntity container, TConfigurationEntity configuration);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -125,5 +125,10 @@ public interface IContainerConfiguration : IResourceConfiguration<CreateContaine
/// Gets the startup callback.
/// </summary>
Func<IContainer, IContainerConfiguration, CancellationToken, Task> StartupCallback { get; }

/// <summary>
/// Gets the connection string provider.
/// </summary>
IConnectionStringProvider<IContainer, IContainerConfiguration> ConnectionStringProvider { get; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public interface IWaitUntil
/// <summary>
/// Evaluates the condition asynchronously against the specified container.
/// </summary>
/// <param name="container">The container instance to check readiness against.</param>
/// <param name="container">The container to check the condition for.</param>
/// <returns>A task that returns <c>true</c> when the condition is satisfied; otherwise, <c>false</c>.</returns>
Task<bool> UntilAsync(IContainer container);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public interface IWaitWhile
/// <summary>
/// Evaluates the condition asynchronously against the specified container.
/// </summary>
/// <param name="container">The container instance to check readiness against.</param>
/// <param name="container">The container to check the condition for.</param>
/// <returns>A task that returns <c>true</c> while the condition holds; otherwise, <c>false</c>.</returns>
Task<bool> WhileAsync(IContainer container);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
namespace DotNet.Testcontainers.Containers
{
using System;
using JetBrains.Annotations;

/// <summary>
/// Represents an exception that is thrown when the connection string provider is not configured.
/// </summary>
[PublicAPI]
public sealed class ConnectionStringProviderNotConfiguredException : Exception
{
/// <summary>
/// Initializes a new instance of the <see cref="ConnectionStringProviderNotConfiguredException" /> class.
/// </summary>
public ConnectionStringProviderNotConfiguredException()
: base("No connection string provider is configured for this container.")
{
}
}
}
31 changes: 31 additions & 0 deletions src/Testcontainers/Containers/DockerContainer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ public class DockerContainer : Resource, IContainer

private ContainerInspectResponse _container = new ContainerInspectResponse();

private IConnectionStringProvider<IContainer, IContainerConfiguration> _connectionStringProvider;

/// <summary>
/// Initializes a new instance of the <see cref="DockerContainer" /> class.
/// </summary>
Expand Down Expand Up @@ -416,6 +418,28 @@ public Task<ExecResult> ExecAsync(IList<string> command, CancellationToken ct =
return _client.ExecAsync(Id, command, ct);
}

/// <inheritdoc />
public string GetConnectionString(ConnectionMode connectionMode = ConnectionMode.Host)
{
if (_connectionStringProvider == null)
{
throw new ConnectionStringProviderNotConfiguredException();
}

return _connectionStringProvider.GetConnectionString(connectionMode);
}

/// <inheritdoc />
public string GetConnectionString(string name, ConnectionMode connectionMode = ConnectionMode.Host)
{
if (_connectionStringProvider == null)
{
throw new ConnectionStringProviderNotConfiguredException();
}

return _connectionStringProvider.GetConnectionString(name, connectionMode);
}

/// <inheritdoc cref="IAsyncDisposable.DisposeAsync" />
protected override async ValueTask DisposeAsyncCore()
{
Expand Down Expand Up @@ -560,6 +584,13 @@ await _configuration.StartupCallback(this, _configuration, ct)
Logger.CompleteReadinessCheck(_container.ID);

StartedTime = DateTime.TryParse(_container.State!.StartedAt, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal, out var startedTime) ? startedTime : DateTime.UtcNow;

if (_configuration.ConnectionStringProvider != null)
{
_connectionStringProvider = _configuration.ConnectionStringProvider;
_connectionStringProvider.Configure(this, _configuration);
}

Started?.Invoke(this, EventArgs.Empty);
}

Expand Down
2 changes: 1 addition & 1 deletion src/Testcontainers/Containers/IContainer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ namespace DotNet.Testcontainers.Containers
/// A container instance.
/// </summary>
[PublicAPI]
public interface IContainer : IAsyncDisposable
public interface IContainer : IConnectionStringProvider, IAsyncDisposable
{
/// <summary>
/// Subscribes to the creating event.
Expand Down
Loading