Skip to content

Commit 897dfd3

Browse files
authored
feat: Add ability to override enumerable builder values (#1506)
1 parent c88a73e commit 897dfd3

File tree

14 files changed

+774
-41
lines changed

14 files changed

+774
-41
lines changed

docs/api/create_docker_container.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,37 @@ _ = new ContainerBuilder()
100100

101101
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`.
102102

103+
## Composing command arguments
104+
105+
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.
106+
107+
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.
108+
109+
`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.
110+
111+
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:
112+
113+
```csharp title="Resetting command arguments"
114+
// Default PostgreSQL builder configuration:
115+
//
116+
// base.Init()
117+
// ...
118+
// .WithCommand("-c", "fsync=off")
119+
// .WithCommand("-c", "full_page_writes=off")
120+
// .WithCommand("-c", "synchronous_commit=off")
121+
// ...
122+
123+
var postgreSqlContainer = new PostgreSqlBuilder()
124+
.WithCommand(new OverwriteEnumerable<string>(Array.Empty<string>()))
125+
.Build();
126+
```
127+
128+
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.
129+
130+
!!!tip
131+
132+
You can create your own `ComposableEnumerable<T>` implementation to control exactly how configuration values are composed or modified.
133+
103134
## Examples
104135

105136
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.

src/Testcontainers/Builders/BuildConfiguration.cs

Lines changed: 77 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,29 +4,38 @@ namespace DotNet.Testcontainers.Builders
44
using System.Collections.Generic;
55
using System.Collections.ObjectModel;
66
using System.Linq;
7+
using DotNet.Testcontainers.Configurations;
78

9+
/// <summary>
10+
/// Provides static utility methods for combining old and new configuration values
11+
/// across various collection and value types.
12+
/// </summary>
813
public static class BuildConfiguration
914
{
1015
/// <summary>
11-
/// Returns the changed configuration object. If there is no change, the previous configuration object is returned.
16+
/// Returns the updated configuration value. If the new value is <c>null</c> or
17+
/// <c>default</c>, the old value is returned.
1218
/// </summary>
13-
/// <param name="oldValue">The old configuration object.</param>
14-
/// <param name="newValue">The new configuration object.</param>
19+
/// <param name="oldValue">The old configuration value.</param>
20+
/// <param name="newValue">The new configuration value.</param>
1521
/// <typeparam name="T">Any class.</typeparam>
16-
/// <returns>Changed configuration object. If there is no change, the previous configuration object.</returns>
22+
/// <returns>The updated value, or the old value if unchanged.</returns>
1723
public static T Combine<T>(T oldValue, T newValue)
1824
{
1925
return Equals(default(T), newValue) ? oldValue : newValue;
2026
}
2127

2228
/// <summary>
23-
/// Combines all existing and new configuration changes. If there are no changes, the previous configurations are returned.
29+
/// Combines all existing and new configuration changes. If there are no changes,
30+
/// the previous configurations are returned.
2431
/// </summary>
2532
/// <param name="oldValue">The old configuration.</param>
2633
/// <param name="newValue">The new configuration.</param>
27-
/// <typeparam name="T">Type of <see cref="IEnumerable{T}" />.</typeparam>
34+
/// <typeparam name="T">The type of elements in the collection.</typeparam>
2835
/// <returns>An updated configuration.</returns>
29-
public static IEnumerable<T> Combine<T>(IEnumerable<T> oldValue, IEnumerable<T> newValue)
36+
public static IEnumerable<T> Combine<T>(
37+
IEnumerable<T> oldValue,
38+
IEnumerable<T> newValue)
3039
{
3140
if (newValue == null && oldValue == null)
3241
{
@@ -42,14 +51,17 @@ public static IEnumerable<T> Combine<T>(IEnumerable<T> oldValue, IEnumerable<T>
4251
}
4352

4453
/// <summary>
45-
/// Combines all existing and new configuration changes while preserving the order of insertion.
46-
/// If there are no changes, the previous configurations are returned.
54+
/// Combines all existing and new configuration changes while preserving the
55+
/// order of insertion. If there are no changes, the previous configurations
56+
/// are returned.
4757
/// </summary>
4858
/// <param name="oldValue">The old configuration.</param>
4959
/// <param name="newValue">The new configuration.</param>
50-
/// <typeparam name="T">Type of <see cref="IReadOnlyList{T}" />.</typeparam>
60+
/// <typeparam name="T">The type of elements in the collection.</typeparam>
5161
/// <returns>An updated configuration.</returns>
52-
public static IReadOnlyList<T> Combine<T>(IReadOnlyList<T> oldValue, IReadOnlyList<T> newValue)
62+
public static IReadOnlyList<T> Combine<T>(
63+
IReadOnlyList<T> oldValue,
64+
IReadOnlyList<T> newValue)
5365
{
5466
if (newValue == null && oldValue == null)
5567
{
@@ -65,14 +77,51 @@ public static IReadOnlyList<T> Combine<T>(IReadOnlyList<T> oldValue, IReadOnlyLi
6577
}
6678

6779
/// <summary>
68-
/// Combines all existing and new configuration changes. If there are no changes, the previous configurations are returned.
80+
/// Combines all existing and new configuration changes. If there are no changes,
81+
/// the previous configuration is returned.
6982
/// </summary>
83+
/// <remarks>
84+
/// Uses <see cref="ComposableEnumerable{T}.Compose" /> on <paramref name="newValue" />
85+
/// to combine configurations. The existing <paramref name="oldValue" /> is passed as
86+
/// an argument to that method.
87+
/// </remarks>
7088
/// <param name="oldValue">The old configuration.</param>
7189
/// <param name="newValue">The new configuration.</param>
72-
/// <typeparam name="TKey">The type of keys in the read-only dictionary.</typeparam>
73-
/// <typeparam name="TValue">The type of values in the read-only dictionary.</typeparam>
90+
/// <typeparam name="T">The type of elements in the collection.</typeparam>
7491
/// <returns>An updated configuration.</returns>
75-
public static IReadOnlyDictionary<TKey, TValue> Combine<TKey, TValue>(IReadOnlyDictionary<TKey, TValue> oldValue, IReadOnlyDictionary<TKey, TValue> newValue)
92+
public static ComposableEnumerable<T> Combine<T>(
93+
ComposableEnumerable<T> oldValue,
94+
ComposableEnumerable<T> newValue)
95+
{
96+
// Creating a new container configuration before merging will follow this branch
97+
// and return the default value. If we use the overwrite implementation,
98+
// merging will reset the collection, we should either return null or use
99+
// the append implementation.
100+
if (newValue == null && oldValue == null)
101+
{
102+
return new AppendEnumerable<T>(Array.Empty<T>());
103+
}
104+
105+
if (newValue == null || oldValue == null)
106+
{
107+
return newValue ?? oldValue;
108+
}
109+
110+
return newValue.Compose(oldValue);
111+
}
112+
113+
/// <summary>
114+
/// Combines all existing and new configuration changes. If there are no changes,
115+
/// the previous configurations are returned.
116+
/// </summary>
117+
/// <param name="oldValue">The old configuration.</param>
118+
/// <param name="newValue">The new configuration.</param>
119+
/// <typeparam name="TKey">The type of keys in the dictionary.</typeparam>
120+
/// <typeparam name="TValue">The type of values in the dictionary.</typeparam>
121+
/// <returns>An updated configuration.</returns>
122+
public static IReadOnlyDictionary<TKey, TValue> Combine<TKey, TValue>(
123+
IReadOnlyDictionary<TKey, TValue> oldValue,
124+
IReadOnlyDictionary<TKey, TValue> newValue)
76125
{
77126
if (newValue == null && oldValue == null)
78127
{
@@ -84,7 +133,19 @@ public static IReadOnlyDictionary<TKey, TValue> Combine<TKey, TValue>(IReadOnlyD
84133
return newValue ?? oldValue;
85134
}
86135

87-
return newValue.Concat(oldValue.Where(item => !newValue.Keys.Contains(item.Key))).ToDictionary(item => item.Key, item => item.Value);
136+
var result = new Dictionary<TKey, TValue>(oldValue.Count + newValue.Count);
137+
138+
foreach (var kvp in oldValue)
139+
{
140+
result[kvp.Key] = kvp.Value;
141+
}
142+
143+
foreach (var kvp in newValue)
144+
{
145+
result[kvp.Key] = kvp.Value;
146+
}
147+
148+
return new ReadOnlyDictionary<TKey, TValue>(result);
88149
}
89150
}
90151
}

src/Testcontainers/Builders/ContainerBuilder`3.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,13 @@ public TBuilderEntity WithEntrypoint(params string[] entrypoint)
133133

134134
/// <inheritdoc />
135135
public TBuilderEntity WithCommand(params string[] command)
136+
{
137+
var composable = new AppendEnumerable<string>(command);
138+
return WithCommand(composable);
139+
}
140+
141+
/// <inheritdoc />
142+
public TBuilderEntity WithCommand(ComposableEnumerable<string> command)
136143
{
137144
return Clone(new ContainerConfiguration(command: command));
138145
}

src/Testcontainers/Builders/IContainerBuilder`2.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,17 @@ public interface IContainerBuilder<out TBuilderEntity, out TContainerEntity> : I
140140
[PublicAPI]
141141
TBuilderEntity WithCommand(params string[] command);
142142

143+
/// <summary>
144+
/// Overrides the container's command arguments.
145+
/// </summary>
146+
/// <remarks>
147+
/// The <see cref="ComposableEnumerable{T}" /> allows to choose how existing builder configurations are composed.
148+
/// </remarks>
149+
/// <param name="command">A list of commands, "executable", "param1", "param2" or "param1", "param2".</param>
150+
/// <returns>A configured instance of <typeparamref name="TBuilderEntity" />.</returns>
151+
[PublicAPI]
152+
TBuilderEntity WithCommand(ComposableEnumerable<string> command);
153+
143154
/// <summary>
144155
/// Sets the environment variable.
145156
/// </summary>
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
namespace DotNet.Testcontainers.Configurations
2+
{
3+
using System.Collections.Generic;
4+
using JetBrains.Annotations;
5+
using DotNet.Testcontainers.Builders;
6+
7+
/// <summary>
8+
/// Represents a composable dictionary that combines its elements by appending
9+
/// the elements of another dictionary with overwriting existing keys.
10+
/// </summary>
11+
/// <typeparam name="TKey">The type of keys in the dictionary.</typeparam>
12+
/// <typeparam name="TValue">The type of values in the dictionary.</typeparam>
13+
[PublicAPI]
14+
public sealed class AppendDictionary<TKey, TValue> : ComposableDictionary<TKey, TValue>
15+
{
16+
/// <summary>
17+
/// Initializes a new instance of the <see cref="AppendDictionary{TKey,TValue}" /> class.
18+
/// </summary>
19+
/// <param name="dictionary">The dictionary whose elements are copied to the new dictionary.</param>
20+
public AppendDictionary(IReadOnlyDictionary<TKey, TValue> dictionary)
21+
: base(dictionary)
22+
{
23+
}
24+
25+
/// <inheritdoc />
26+
public override ComposableDictionary<TKey, TValue> Compose(IReadOnlyDictionary<TKey, TValue> other)
27+
{
28+
return new AppendDictionary<TKey, TValue>(BuildConfiguration.Combine(other, this));
29+
}
30+
}
31+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
namespace DotNet.Testcontainers.Configurations
2+
{
3+
using System.Collections.Generic;
4+
using JetBrains.Annotations;
5+
using DotNet.Testcontainers.Builders;
6+
7+
/// <summary>
8+
/// Represents a composable collection that combines its elements by appending
9+
/// the elements of another collection.
10+
/// </summary>
11+
/// <typeparam name="T">The type of elements in the collection.</typeparam>
12+
[PublicAPI]
13+
public sealed class AppendEnumerable<T> : ComposableEnumerable<T>
14+
{
15+
/// <summary>
16+
/// Initializes a new instance of the <see cref="AppendEnumerable{T}" /> class.
17+
/// </summary>
18+
/// <param name="collection">The collection of items. If <c>null</c>, an empty collection is used.</param>
19+
public AppendEnumerable(IEnumerable<T> collection)
20+
: base(collection)
21+
{
22+
}
23+
24+
/// <inheritdoc />
25+
public override ComposableEnumerable<T> Compose(IEnumerable<T> other)
26+
{
27+
return new AppendEnumerable<T>(BuildConfiguration.Combine(other, this));
28+
}
29+
}
30+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
namespace DotNet.Testcontainers.Configurations
2+
{
3+
using System.Collections;
4+
using System.Collections.Generic;
5+
using System.Collections.ObjectModel;
6+
using JetBrains.Annotations;
7+
8+
/// <summary>
9+
/// Represents an immutable dictionary that defines a custom strategy for
10+
/// composing its elements with those of another dictionary. This class is
11+
/// intended to be inherited by implementations that specify how two dictionaries
12+
/// should be combined.
13+
/// </summary>
14+
/// <typeparam name="TKey">The type of keys in the dictionary.</typeparam>
15+
/// <typeparam name="TValue">The type of values in the dictionary.</typeparam>
16+
[PublicAPI]
17+
public abstract class ComposableDictionary<TKey, TValue> : IReadOnlyDictionary<TKey, TValue>
18+
{
19+
private readonly IReadOnlyDictionary<TKey, TValue> _dictionary;
20+
21+
/// <summary>
22+
/// Initializes a new instance of the <see cref="ComposableDictionary{TKey, TValue}" /> class.
23+
/// </summary>
24+
/// <param name="dictionary">The dictionary of items. If <c>null</c>, an empty dictionary is used.</param>
25+
protected ComposableDictionary(IReadOnlyDictionary<TKey, TValue> dictionary)
26+
{
27+
_dictionary = dictionary ?? new ReadOnlyDictionary<TKey, TValue>(new Dictionary<TKey, TValue>());
28+
}
29+
30+
/// <summary>
31+
/// Combines the current dictionary with the specified dictionary according to
32+
/// the composition strategy defined by the class.
33+
/// </summary>
34+
/// <remarks>
35+
/// The <paramref name="other" /> parameter corresponds to the previous builder
36+
/// configuration.
37+
/// </remarks>
38+
/// <param name="other">The incoming dictionary to compose with this dictionary.</param>
39+
/// <returns>A new <see cref="IReadOnlyDictionary{TKey, TValue}" /> that contains the result of the composition.</returns>
40+
public abstract ComposableDictionary<TKey, TValue> Compose([NotNull] IReadOnlyDictionary<TKey, TValue> other);
41+
42+
/// <inheritdoc />
43+
public IEnumerable<TKey> Keys => _dictionary.Keys;
44+
45+
/// <inheritdoc />
46+
public IEnumerable<TValue> Values => _dictionary.Values;
47+
48+
/// <inheritdoc />
49+
public int Count => _dictionary.Count;
50+
51+
/// <inheritdoc />
52+
public TValue this[TKey key] => _dictionary[key];
53+
54+
/// <inheritdoc />
55+
public bool ContainsKey(TKey key) => _dictionary.ContainsKey(key);
56+
57+
/// <inheritdoc />
58+
public bool TryGetValue(TKey key, out TValue value) => _dictionary.TryGetValue(key, out value);
59+
60+
/// <inheritdoc />
61+
public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator() => _dictionary.GetEnumerator();
62+
63+
/// <inheritdoc />
64+
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
65+
}
66+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
namespace DotNet.Testcontainers.Configurations
2+
{
3+
using System;
4+
using System.Collections;
5+
using System.Collections.Generic;
6+
using JetBrains.Annotations;
7+
8+
/// <summary>
9+
/// Represents an immutable collection that defines a custom strategy for
10+
/// composing its elements with those of another collection. This class is
11+
/// intended to be inherited by implementations that specify how two collections
12+
/// should be combined.
13+
/// </summary>
14+
/// <typeparam name="T">The type of the elements in the collection.</typeparam>
15+
[PublicAPI]
16+
public abstract class ComposableEnumerable<T> : IEnumerable<T>
17+
{
18+
private readonly IEnumerable<T> _collection;
19+
20+
/// <summary>
21+
/// Initializes a new instance of the <see cref="ComposableEnumerable{T}" /> class.
22+
/// </summary>
23+
/// <param name="collection">The collection of items. If <c>null</c>, an empty collection is used.</param>
24+
protected ComposableEnumerable(IEnumerable<T> collection)
25+
{
26+
_collection = collection ?? Array.Empty<T>();
27+
}
28+
29+
/// <summary>
30+
/// Combines the current collection with the specified collection according to
31+
/// the composition strategy defined by the class.
32+
/// </summary>
33+
/// <remarks>
34+
/// The <paramref name="other" /> parameter corresponds to the previous builder
35+
/// configuration.
36+
/// </remarks>
37+
/// <param name="other">The incoming collection to compose with this collection.</param>
38+
/// <returns>A new <see cref="IEnumerable{T}" /> that contains the result of the composition.</returns>
39+
public abstract ComposableEnumerable<T> Compose([NotNull] IEnumerable<T> other);
40+
41+
/// <summary>
42+
/// Returns an enumerator that iterates through the collection.
43+
/// </summary>
44+
/// <returns>An enumerator for the current collection.</returns>
45+
public IEnumerator<T> GetEnumerator() => _collection.GetEnumerator();
46+
47+
/// <summary>
48+
/// Returns an enumerator that iterates through the collection.
49+
/// </summary>
50+
/// <returns>An enumerator for the current collection.</returns>
51+
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
52+
}
53+
}

0 commit comments

Comments
 (0)