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
31 changes: 31 additions & 0 deletions docs/api/create_docker_container.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,37 @@ _ = new ContainerBuilder()

The static class `Consume` offers pre-configured implementations of the `IOutputConsumer` interface for common use cases. If you need additional functionalities beyond those provided by the default implementations, you can create your own implementations of `IOutputConsumer`.

## Composing command arguments

Testcontainers for .NET provides the `WithCommand(ComposableEnumerable<string>)` API to give you flexible control over container command arguments. While currently used for container commands, the `ComposableEnumerable<T>` abstraction is designed to support other builder APIs in the future, allowing similar composition and override functionality.

Because our builders are immutable, this feature allows you to extend or override pre-configured configurations, such as those in Testcontainers [modules](../modules/index.md), without modifying the original builder.

`ComposableEnumerable<T>` lets you decide how new API arguments should be combined with existing ones. You can choose to append, overwrite, or apply other strategies based on your needs.

If a module applies default commands and you need to override or remove them entirely, you can do this e.g. by explicitly resetting the command list:

```csharp title="Resetting command arguments"
// Default PostgreSQL builder configuration:
//
// base.Init()
// ...
// .WithCommand("-c", "fsync=off")
// .WithCommand("-c", "full_page_writes=off")
// .WithCommand("-c", "synchronous_commit=off")
// ...

var postgreSqlContainer = new PostgreSqlBuilder()
.WithCommand(new OverwriteEnumerable<string>(Array.Empty<string>()))
.Build();
```

Using `OverwriteEnumerable<string>(Array.Empty<string>())` removes all default command configurations. This is useful when you want full control over the PostgreSQL startup or when the default configurations do not match your requirements.

!!!tip

You can create your own `ComposableEnumerable<T>` implementation to control exactly how configuration values are composed or modified.

## Examples

An NGINX container that binds the HTTP port to a random host port and hosts static content. The example connects to the web server and checks the HTTP status code.
Expand Down
93 changes: 77 additions & 16 deletions src/Testcontainers/Builders/BuildConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,29 +4,38 @@ namespace DotNet.Testcontainers.Builders
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using DotNet.Testcontainers.Configurations;

/// <summary>
/// Provides static utility methods for combining old and new configuration values
/// across various collection and value types.
/// </summary>
public static class BuildConfiguration
{
/// <summary>
/// Returns the changed configuration object. If there is no change, the previous configuration object is returned.
/// Returns the updated configuration value. If the new value is <c>null</c> or
/// <c>default</c>, the old value is returned.
/// </summary>
/// <param name="oldValue">The old configuration object.</param>
/// <param name="newValue">The new configuration object.</param>
/// <param name="oldValue">The old configuration value.</param>
/// <param name="newValue">The new configuration value.</param>
/// <typeparam name="T">Any class.</typeparam>
/// <returns>Changed configuration object. If there is no change, the previous configuration object.</returns>
/// <returns>The updated value, or the old value if unchanged.</returns>
public static T Combine<T>(T oldValue, T newValue)
{
return Equals(default(T), newValue) ? oldValue : newValue;
}

/// <summary>
/// Combines all existing and new configuration changes. If there are no changes, the previous configurations are returned.
/// Combines all existing and new configuration changes. If there are no changes,
/// the previous configurations are returned.
/// </summary>
/// <param name="oldValue">The old configuration.</param>
/// <param name="newValue">The new configuration.</param>
/// <typeparam name="T">Type of <see cref="IEnumerable{T}" />.</typeparam>
/// <typeparam name="T">The type of elements in the collection.</typeparam>
/// <returns>An updated configuration.</returns>
public static IEnumerable<T> Combine<T>(IEnumerable<T> oldValue, IEnumerable<T> newValue)
public static IEnumerable<T> Combine<T>(
IEnumerable<T> oldValue,
IEnumerable<T> newValue)
{
if (newValue == null && oldValue == null)
{
Expand All @@ -42,14 +51,17 @@ public static IEnumerable<T> Combine<T>(IEnumerable<T> oldValue, IEnumerable<T>
}

/// <summary>
/// Combines all existing and new configuration changes while preserving the order of insertion.
/// If there are no changes, the previous configurations are returned.
/// Combines all existing and new configuration changes while preserving the
/// order of insertion. If there are no changes, the previous configurations
/// are returned.
/// </summary>
/// <param name="oldValue">The old configuration.</param>
/// <param name="newValue">The new configuration.</param>
/// <typeparam name="T">Type of <see cref="IReadOnlyList{T}" />.</typeparam>
/// <typeparam name="T">The type of elements in the collection.</typeparam>
/// <returns>An updated configuration.</returns>
public static IReadOnlyList<T> Combine<T>(IReadOnlyList<T> oldValue, IReadOnlyList<T> newValue)
public static IReadOnlyList<T> Combine<T>(
IReadOnlyList<T> oldValue,
IReadOnlyList<T> newValue)
{
if (newValue == null && oldValue == null)
{
Expand All @@ -65,14 +77,51 @@ public static IReadOnlyList<T> Combine<T>(IReadOnlyList<T> oldValue, IReadOnlyLi
}

/// <summary>
/// Combines all existing and new configuration changes. If there are no changes, the previous configurations are returned.
/// Combines all existing and new configuration changes. If there are no changes,
/// the previous configuration is returned.
/// </summary>
/// <remarks>
/// Uses <see cref="ComposableEnumerable{T}.Compose" /> on <paramref name="newValue" />
/// to combine configurations. The existing <paramref name="oldValue" /> is passed as
/// an argument to that method.
/// </remarks>
/// <param name="oldValue">The old configuration.</param>
/// <param name="newValue">The new configuration.</param>
/// <typeparam name="TKey">The type of keys in the read-only dictionary.</typeparam>
/// <typeparam name="TValue">The type of values in the read-only dictionary.</typeparam>
/// <typeparam name="T">The type of elements in the collection.</typeparam>
/// <returns>An updated configuration.</returns>
public static IReadOnlyDictionary<TKey, TValue> Combine<TKey, TValue>(IReadOnlyDictionary<TKey, TValue> oldValue, IReadOnlyDictionary<TKey, TValue> newValue)
public static ComposableEnumerable<T> Combine<T>(
ComposableEnumerable<T> oldValue,
ComposableEnumerable<T> newValue)
{
// Creating a new container configuration before merging will follow this branch
// and return the default value. If we use the overwrite implementation,
// merging will reset the collection, we should either return null or use
// the append implementation.
if (newValue == null && oldValue == null)
{
return new AppendEnumerable<T>(Array.Empty<T>());
}

if (newValue == null || oldValue == null)
{
return newValue ?? oldValue;
}

return newValue.Compose(oldValue);
}

/// <summary>
/// Combines all existing and new configuration changes. If there are no changes,
/// the previous configurations are returned.
/// </summary>
/// <param name="oldValue">The old configuration.</param>
/// <param name="newValue">The new configuration.</param>
/// <typeparam name="TKey">The type of keys in the dictionary.</typeparam>
/// <typeparam name="TValue">The type of values in the dictionary.</typeparam>
/// <returns>An updated configuration.</returns>
public static IReadOnlyDictionary<TKey, TValue> Combine<TKey, TValue>(
IReadOnlyDictionary<TKey, TValue> oldValue,
IReadOnlyDictionary<TKey, TValue> newValue)
{
if (newValue == null && oldValue == null)
{
Expand All @@ -84,7 +133,19 @@ public static IReadOnlyDictionary<TKey, TValue> Combine<TKey, TValue>(IReadOnlyD
return newValue ?? oldValue;
}

return newValue.Concat(oldValue.Where(item => !newValue.Keys.Contains(item.Key))).ToDictionary(item => item.Key, item => item.Value);
var result = new Dictionary<TKey, TValue>(oldValue.Count + newValue.Count);

foreach (var kvp in oldValue)
{
result[kvp.Key] = kvp.Value;
}

foreach (var kvp in newValue)
{
result[kvp.Key] = kvp.Value;
}

return new ReadOnlyDictionary<TKey, TValue>(result);
}
}
}
7 changes: 7 additions & 0 deletions src/Testcontainers/Builders/ContainerBuilder`3.cs
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,13 @@ public TBuilderEntity WithEntrypoint(params string[] entrypoint)

/// <inheritdoc />
public TBuilderEntity WithCommand(params string[] command)
{
var composable = new AppendEnumerable<string>(command);
return WithCommand(composable);
}

/// <inheritdoc />
public TBuilderEntity WithCommand(ComposableEnumerable<string> command)
{
return Clone(new ContainerConfiguration(command: command));
}
Expand Down
11 changes: 11 additions & 0 deletions src/Testcontainers/Builders/IContainerBuilder`2.cs
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,17 @@ public interface IContainerBuilder<out TBuilderEntity, out TContainerEntity> : I
[PublicAPI]
TBuilderEntity WithCommand(params string[] command);

/// <summary>
/// Overrides the container's command arguments.
/// </summary>
/// <remarks>
/// The <see cref="ComposableEnumerable{T}" /> allows to choose how existing builder configurations are composed.
/// </remarks>
/// <param name="command">A list of commands, "executable", "param1", "param2" or "param1", "param2".</param>
/// <returns>A configured instance of <typeparamref name="TBuilderEntity" />.</returns>
[PublicAPI]
TBuilderEntity WithCommand(ComposableEnumerable<string> command);

/// <summary>
/// Sets the environment variable.
/// </summary>
Expand Down
31 changes: 31 additions & 0 deletions src/Testcontainers/Configurations/Commons/AppendDictionary`2.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
namespace DotNet.Testcontainers.Configurations
{
using System.Collections.Generic;
using JetBrains.Annotations;
using DotNet.Testcontainers.Builders;

/// <summary>
/// Represents a composable dictionary that combines its elements by appending
/// the elements of another dictionary with overwriting existing keys.
/// </summary>
/// <typeparam name="TKey">The type of keys in the dictionary.</typeparam>
/// <typeparam name="TValue">The type of values in the dictionary.</typeparam>
[PublicAPI]
public sealed class AppendDictionary<TKey, TValue> : ComposableDictionary<TKey, TValue>
{
/// <summary>
/// Initializes a new instance of the <see cref="AppendDictionary{TKey,TValue}" /> class.
/// </summary>
/// <param name="dictionary">The dictionary whose elements are copied to the new dictionary.</param>
public AppendDictionary(IReadOnlyDictionary<TKey, TValue> dictionary)
: base(dictionary)
{
}

/// <inheritdoc />
public override ComposableDictionary<TKey, TValue> Compose(IReadOnlyDictionary<TKey, TValue> other)
{
return new AppendDictionary<TKey, TValue>(BuildConfiguration.Combine(other, this));
}
}
}
30 changes: 30 additions & 0 deletions src/Testcontainers/Configurations/Commons/AppendEnumerable`1.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
namespace DotNet.Testcontainers.Configurations
{
using System.Collections.Generic;
using JetBrains.Annotations;
using DotNet.Testcontainers.Builders;

/// <summary>
/// Represents a composable collection that combines its elements by appending
/// the elements of another collection.
/// </summary>
/// <typeparam name="T">The type of elements in the collection.</typeparam>
[PublicAPI]
public sealed class AppendEnumerable<T> : ComposableEnumerable<T>
{
/// <summary>
/// Initializes a new instance of the <see cref="AppendEnumerable{T}" /> class.
/// </summary>
/// <param name="collection">The collection of items. If <c>null</c>, an empty collection is used.</param>
public AppendEnumerable(IEnumerable<T> collection)
: base(collection)
{
}

/// <inheritdoc />
public override ComposableEnumerable<T> Compose(IEnumerable<T> other)
{
return new AppendEnumerable<T>(BuildConfiguration.Combine(other, this));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
namespace DotNet.Testcontainers.Configurations
{
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using JetBrains.Annotations;

/// <summary>
/// Represents an immutable dictionary that defines a custom strategy for
/// composing its elements with those of another dictionary. This class is
/// intended to be inherited by implementations that specify how two dictionaries
/// should be combined.
/// </summary>
/// <typeparam name="TKey">The type of keys in the dictionary.</typeparam>
/// <typeparam name="TValue">The type of values in the dictionary.</typeparam>
[PublicAPI]
public abstract class ComposableDictionary<TKey, TValue> : IReadOnlyDictionary<TKey, TValue>
{
private readonly IReadOnlyDictionary<TKey, TValue> _dictionary;

/// <summary>
/// Initializes a new instance of the <see cref="ComposableDictionary{TKey, TValue}" /> class.
/// </summary>
/// <param name="dictionary">The dictionary of items. If <c>null</c>, an empty dictionary is used.</param>
protected ComposableDictionary(IReadOnlyDictionary<TKey, TValue> dictionary)
{
_dictionary = dictionary ?? new ReadOnlyDictionary<TKey, TValue>(new Dictionary<TKey, TValue>());
}

/// <summary>
/// Combines the current dictionary with the specified dictionary according to
/// the composition strategy defined by the class.
/// </summary>
/// <remarks>
/// The <paramref name="other" /> parameter corresponds to the previous builder
/// configuration.
/// </remarks>
/// <param name="other">The incoming dictionary to compose with this dictionary.</param>
/// <returns>A new <see cref="IReadOnlyDictionary{TKey, TValue}" /> that contains the result of the composition.</returns>
public abstract ComposableDictionary<TKey, TValue> Compose([NotNull] IReadOnlyDictionary<TKey, TValue> other);

/// <inheritdoc />
public IEnumerable<TKey> Keys => _dictionary.Keys;

/// <inheritdoc />
public IEnumerable<TValue> Values => _dictionary.Values;

/// <inheritdoc />
public int Count => _dictionary.Count;

/// <inheritdoc />
public TValue this[TKey key] => _dictionary[key];

/// <inheritdoc />
public bool ContainsKey(TKey key) => _dictionary.ContainsKey(key);

/// <inheritdoc />
public bool TryGetValue(TKey key, out TValue value) => _dictionary.TryGetValue(key, out value);

/// <inheritdoc />
public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator() => _dictionary.GetEnumerator();

/// <inheritdoc />
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
namespace DotNet.Testcontainers.Configurations
{
using System;
using System.Collections;
using System.Collections.Generic;
using JetBrains.Annotations;

/// <summary>
/// Represents an immutable collection that defines a custom strategy for
/// composing its elements with those of another collection. This class is
/// intended to be inherited by implementations that specify how two collections
/// should be combined.
/// </summary>
/// <typeparam name="T">The type of the elements in the collection.</typeparam>
[PublicAPI]
public abstract class ComposableEnumerable<T> : IEnumerable<T>
{
private readonly IEnumerable<T> _collection;

/// <summary>
/// Initializes a new instance of the <see cref="ComposableEnumerable{T}" /> class.
/// </summary>
/// <param name="collection">The collection of items. If <c>null</c>, an empty collection is used.</param>
protected ComposableEnumerable(IEnumerable<T> collection)
{
_collection = collection ?? Array.Empty<T>();
}

/// <summary>
/// Combines the current collection with the specified collection according to
/// the composition strategy defined by the class.
/// </summary>
/// <remarks>
/// The <paramref name="other" /> parameter corresponds to the previous builder
/// configuration.
/// </remarks>
/// <param name="other">The incoming collection to compose with this collection.</param>
/// <returns>A new <see cref="IEnumerable{T}" /> that contains the result of the composition.</returns>
public abstract ComposableEnumerable<T> Compose([NotNull] IEnumerable<T> other);

/// <summary>
/// Returns an enumerator that iterates through the collection.
/// </summary>
/// <returns>An enumerator for the current collection.</returns>
public IEnumerator<T> GetEnumerator() => _collection.GetEnumerator();

/// <summary>
/// Returns an enumerator that iterates through the collection.
/// </summary>
/// <returns>An enumerator for the current collection.</returns>
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
}
Loading