(a barrier shared with a sibling process),
+// and checks every 500 ms that the store is still THIS process's: its own container (by the id seen right after start) is
+// running, and its own marker event is still readable (a restart of an in-memory store wipes it).
+// Prints one line: "PORT= CONTAINER= RESULT=SURVIVED" or "... RESULT=LOST:"; exit 0 only when it survived.
+using Docker.DotNet;
+using Docker.DotNet.Models;
+using KurrentDB.Client;
+using MicroPlumberd.Testing;
+
+var startAt = new DateTime(long.Parse(args[0]), DateTimeKind.Utc);
+var hold = TimeSpan.FromSeconds(int.Parse(args[1]));
+var delay = startAt - DateTime.UtcNow;
+if (delay > TimeSpan.Zero) await Task.Delay(delay);
+
+using var docker = new DockerClientConfiguration().CreateClient();
+string? id = null;
+string result;
+var server = new EventStoreServer();
+try
+{
+ await server.StartInDocker();
+ id = (await docker.Containers.ListContainersAsync(new ContainersListParameters { All = true }))
+ .FirstOrDefault(c => c.Names.Contains("/" + server.ContainerName))?.ID;
+ var client = new KurrentDBClient(server.GetEventStoreSettings());
+ var mine = $"holder-{Environment.ProcessId}";
+ await client.AppendToStreamAsync(mine, StreamState.Any,
+ [new EventData(Uuid.NewUuid(), "Marker", "{}"u8.ToArray())]);
+
+ result = "SURVIVED";
+ var until = DateTime.UtcNow + hold;
+ while (DateTime.UtcNow < until)
+ {
+ await Task.Delay(500);
+ var running = id != null && (await docker.Containers.ListContainersAsync(new ContainersListParameters { All = true }))
+ .Any(c => c.ID == id && c.State == "running");
+ if (!running) { result = "LOST:my-container-is-gone-or-stopped"; break; }
+ try
+ {
+ var read = client.ReadStreamAsync(Direction.Forwards, mine, StreamPosition.Start);
+ if (await read.ReadState == ReadState.StreamNotFound) { result = "LOST:my-marker-is-gone(store-restarted)"; break; }
+ }
+ catch (Exception e) { result = $"LOST:store-unreachable({e.GetType().Name})"; break; }
+ }
+}
+catch (Exception e)
+{
+ result = $"LOST:start-failed({e.GetType().Name}: {e.Message.Split('\n')[0]})";
+}
+finally
+{
+ await server.DisposeAsync();
+}
+
+Console.WriteLine($"PORT={server.HttpPort} CONTAINER={id?[..12] ?? "none"} RESULT={result}");
+return result == "SURVIVED" ? 0 : 1;
diff --git a/src/MicroPlumberd.Testing/EventStoreServer.cs b/src/MicroPlumberd.Testing/EventStoreServer.cs
index fa9122d..762dd4d 100644
--- a/src/MicroPlumberd.Testing/EventStoreServer.cs
+++ b/src/MicroPlumberd.Testing/EventStoreServer.cs
@@ -37,8 +37,15 @@ public class EventStoreServer : IDisposable, IAsyncDisposable
public static TimeSpan ReadyBudget(TimeSpan perStore, int startingAtOnce) => perStore * Math.Max(1, startingAtOnce);
/// The readiness wait itself — a seam so a test can see the budget each start actually waited with.
- internal static Func WaitReady { get; set; } =
- (settings, budget) => settings.WaitUntilReady(budget);
+ ///
+ /// The removal Cleanup makes — a seam so a test can make it fail (as a concurrent removal's 409 did). It is given the
+ /// server, so a test touches only its own store while the suite runs classes in parallel.
+ ///
+ internal static Func RemoveContainer { get; set; } =
+ (_, docker, id) => docker.Containers.RemoveContainerAsync(id, new ContainerRemoveParameters { Force = true, RemoveVolumes = true });
+
+ internal static Func WaitReady { get; set; } =
+ (_, settings, budget) => settings.WaitUntilReady(budget);
private static readonly ConcurrentDictionary Undisposed = new();
private static readonly string CurrentOwner = OwnerOf(Environment.ProcessId, Process.GetCurrentProcess().StartTime);
@@ -56,7 +63,7 @@ internal EventStoreServer(string? containerName = null)
{
if (containerName != null)
_containerName = containerName;
- httpPort = _searcher.FindNextAvailablePort();
+ (httpPort, _portLock) = PortLock.Acquire(_searcher);
const string eventStoreHostName = "127.0.0.1";
HttpUrl = new Uri($"esdb://admin:changeit@{eventStoreHostName}:{httpPort}?tls=false&tlsVerifyCert=false");
client = new DockerClientConfiguration()
@@ -64,6 +71,12 @@ internal EventStoreServer(string? containerName = null)
}
private string? _containerName;
+
+ // card microplumberd-testing-store-port-race: this store's port, held machine-wide for its lifetime (see PortLock), and
+ // the id of the container IT created. Removal and restart go by that id — never by name: the name is derived from the
+ // port, so another process's store can carry it.
+ private PortLock? _portLock;
+ private string? _containerId;
public string ContainerName => _containerName ?? $"eventstore-mem-{httpPort}";
public int HttpPort => httpPort;
@@ -86,10 +99,10 @@ internal EventStoreServer(string? containerName = null)
public async Task Stop()
{
- var container = await GetEventStoreContainer();
- if (container != null)
+ // This server's own container only (by id) — a name can be another process's store.
+ if (_containerId != null)
{
- var data = await client.Containers.InspectContainerAsync(container.ID);
+ var data = await client.Containers.InspectContainerAsync(_containerId);
if (data.State.Running)
{
await client.Containers.StopContainerAsync(data.ID, new ContainerStopParameters());
@@ -102,10 +115,9 @@ public async Task Stop()
public async Task Restart(TimeSpan delay)
{
- var container = await GetEventStoreContainer();
- if (container != null)
+ if (_containerId != null)
{
- var data = await client.Containers.InspectContainerAsync(container.ID);
+ var data = await client.Containers.InspectContainerAsync(_containerId);
if (data.State.Running)
{
await client.Containers.StopContainerAsync(data.ID, new ContainerStopParameters());
@@ -139,10 +151,20 @@ private async Task StartAndWait(bool wait, bool inMemory, Time
await ReapOrphans(client);
var container = await GetEventStoreContainer();
+ if (container != null && container.ID != _containerId)
+ {
+ // A container with this name that THIS server did not create: another live process's store (refused, named —
+ // restarting it would wipe it, removing it would pull it from under that process), or a dead owner's (reaped).
+ if (!container.Labels.TryGetValue(OwnerLabel, out var owner) || !IsDeadOwnerOnThisMachine(owner))
+ throw new InvalidOperationException(
+ $"Container '{ContainerName}' ({container.ID[..12]}) belongs to another process ({owner ?? "no owner label"}); " +
+ "it was neither restarted nor removed. Two processes chose the same store port.");
+ await client.Containers.RemoveContainerAsync(container.ID, new ContainerRemoveParameters { Force = true, RemoveVolumes = true });
+ container = null;
+ }
if (!inMemory && container != null)
{
- var data = await client.Containers.InspectContainerAsync(container.ID);
- await client.Containers.RemoveContainerAsync(data.ID, new ContainerRemoveParameters() { RemoveVolumes = true});
+ await client.Containers.RemoveContainerAsync(container.ID, new ContainerRemoveParameters() { RemoveVolumes = true});
container = null;
}
@@ -178,6 +200,7 @@ private async Task StartAndWait(bool wait, bool inMemory, Time
{ "2113", default},
}
});
+ _containerId = response.ID;
await client.Containers.StartContainerAsync(response.ID, new ContainerStartParameters());
}
else
@@ -191,12 +214,16 @@ private async Task StartAndWait(bool wait, bool inMemory, Time
// Health THEN a gRPC round trip (WaitUntilReady), on a budget that grows with the stores starting
// alongside this one — read now, when they are all competing for the host.
- await WaitReady(this.GetEventStoreSettings(), ReadyBudget(readyTimeout, Volatile.Read(ref _starting)));
+ await WaitReady(this, this.GetEventStoreSettings(), ReadyBudget(readyTimeout, Volatile.Read(ref _starting)));
}
- catch
+ catch (Exception started)
{
- await Cleanup();
- throw;
+ // The START failure is the one to see (dev-clean, 2026-10-18): a cleanup that throws too (Docker 409 "removal
+ // already in progress") replaced it, and the first failure was lost. Cleanup's failure rides along, never instead.
+ try { await Cleanup(); }
+ catch (Exception cleanup) { started.Data["MicroPlumberd.Testing.CleanupFailure"] = cleanup.ToString(); }
+ System.Runtime.ExceptionServices.ExceptionDispatchInfo.Capture(started).Throw();
+ throw; // unreachable
}
return this;
@@ -285,19 +312,21 @@ private static void RemoveUndisposed()
async ValueTask Cleanup()
{
- var container = await GetEventStoreContainer();
- if (container != null)
+ // ONLY the container this server created, by its id: a name lookup here removed another process's live store.
+ if (_containerId != null)
{
try
{
- await client.Containers.RemoveContainerAsync(container.ID, new ContainerRemoveParameters() { Force = true, RemoveVolumes = true });
+ await RemoveContainer(this, client, _containerId);
}
catch (DockerContainerNotFoundException)
{
}
+ _containerId = null;
}
Undisposed.TryRemove(this, out _);
+ Interlocked.Exchange(ref _portLock, null)?.Dispose();
}
public void Dispose()
diff --git a/src/MicroPlumberd.Testing/PortLock.cs b/src/MicroPlumberd.Testing/PortLock.cs
new file mode 100644
index 0000000..8fa978a
--- /dev/null
+++ b/src/MicroPlumberd.Testing/PortLock.cs
@@ -0,0 +1,48 @@
+using System.Net;
+using System.Net.Sockets;
+
+namespace MicroPlumberd.Testing;
+
+///
+/// A store port held MACHINE-WIDE for the store's lifetime (card microplumberd-testing-store-port-race). Every process's
+/// PortSearcher starts at the same port, and a free-port probe only sees a port as taken once its container has STARTED —
+/// seconds later — so two processes chose the same port. A lock file per port, opened exclusively (on Unix .NET takes an
+/// advisory flock for FileShare.None), is held until the store is disposed and released by the OS if the process dies.
+///
+internal sealed class PortLock : IDisposable
+{
+ /// One directory for every process on the machine (not the per-user/per-process TMPDIR).
+ internal static string Directory { get; set; } = OperatingSystem.IsWindows()
+ ? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "microplumberd-testing-ports")
+ : "/tmp/microplumberd-testing-ports";
+
+ private readonly FileStream _held;
+ private PortLock(FileStream held) => _held = held;
+
+ /// The next port that no other process holds AND that is free to bind, with its lock held.
+ internal static (int Port, PortLock Lock) Acquire(PortSearcher searcher)
+ {
+ System.IO.Directory.CreateDirectory(Directory);
+ while (true)
+ {
+ var port = searcher.FindNextAvailablePort();
+ var held = TryHold(port);
+ if (held != null) return (port, held);
+ }
+ }
+
+ internal static PortLock? TryHold(int port)
+ {
+ try
+ {
+ var stream = new FileStream(Path.Combine(Directory, $"{port}.lock"), FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None);
+ return new PortLock(stream);
+ }
+ catch (IOException)
+ {
+ return null; // another process holds this port
+ }
+ }
+
+ public void Dispose() => _held.Dispose();
+}
diff --git a/src/MicroPlumberd.Tests/Integration/EventStoreServerForeignContainerTests.cs b/src/MicroPlumberd.Tests/Integration/EventStoreServerForeignContainerTests.cs
new file mode 100644
index 0000000..023d7fe
--- /dev/null
+++ b/src/MicroPlumberd.Tests/Integration/EventStoreServerForeignContainerTests.cs
@@ -0,0 +1,71 @@
+using System.Diagnostics;
+using Docker.DotNet;
+using Docker.DotNet.Models;
+using FluentAssertions;
+using MicroPlumberd.Testing;
+using MicroPlumberd.Tests.Utils;
+
+namespace MicroPlumberd.Tests.Integration;
+
+///
+/// card microplumberd-testing-store-port-race: a container that carries this server's name but belongs to ANOTHER LIVE
+/// process is never restarted (that wiped its in-memory store) and never removed (that pulled it from under that process) —
+/// not at start, not at dispose. The port lock makes such a name clash rare; these rows hold the second line of defence
+/// without a race. The planted container's owner is PID 1 with its real start time: alive for as long as the machine is.
+///
+[TestCategory("Integration")]
+public class EventStoreServerForeignContainerTests : IAsyncLifetime
+{
+ private readonly DockerClient _docker = new DockerClientConfiguration().CreateClient();
+ private readonly string _name = $"mp-foreign-{Guid.NewGuid():N}"[..24];
+ private string _planted = "";
+
+ public async Task InitializeAsync()
+ {
+ var liveForeignOwner = EventStoreServer.OwnerOf(1, Process.GetProcessById(1).StartTime);
+ _planted = (await _docker.Containers.CreateContainerAsync(new CreateContainerParameters
+ {
+ Image = "docker.kurrent.io/kurrent-latest/kurrentdb:latest",
+ Name = _name,
+ Labels = new Dictionary { [EventStoreServer.Label] = "eventstore", [EventStoreServer.OwnerLabel] = liveForeignOwner },
+ })).ID;
+ }
+
+ public async Task DisposeAsync()
+ {
+ try { await _docker.Containers.RemoveContainerAsync(_planted, new ContainerRemoveParameters { Force = true }); }
+ catch (DockerContainerNotFoundException) { }
+ _docker.Dispose();
+ }
+
+ private async Task Planted()
+ {
+ try { return await _docker.Containers.InspectContainerAsync(_planted); }
+ catch (DockerContainerNotFoundException) { return null; }
+ }
+
+ [Fact]
+ public async Task AStart_OverAnotherLiveProcesssContainerOfTheSameName_IsRefused_AndLeavesItAlone()
+ {
+ await using var server = new EventStoreServer(_name);
+
+ var start = await Record.ExceptionAsync(() => server.StartInDocker());
+
+ start.Should().BeOfType().Which.Message.Should()
+ .Contain(_name).And.Contain("another process").And.Contain("neither restarted nor removed");
+ var planted = await Planted();
+ planted.Should().NotBeNull("it was not removed");
+ planted!.State.Running.Should().BeFalse("nor (re)started");
+ }
+
+ [Fact]
+ public async Task ADispose_NeverRemovesAContainerThisServerDidNotCreate()
+ {
+ var server = new EventStoreServer(_name);
+ await Record.ExceptionAsync(() => server.StartInDocker()); // refused: it created nothing
+
+ await server.DisposeAsync();
+
+ (await Planted()).Should().NotBeNull("dispose removes only the container THIS server created, by its id");
+ }
+}
diff --git a/src/MicroPlumberd.Tests/Integration/EventStoreServerReadinessTests.cs b/src/MicroPlumberd.Tests/Integration/EventStoreServerReadinessTests.cs
index cebfeb9..a220508 100644
--- a/src/MicroPlumberd.Tests/Integration/EventStoreServerReadinessTests.cs
+++ b/src/MicroPlumberd.Tests/Integration/EventStoreServerReadinessTests.cs
@@ -17,11 +17,16 @@ public async Task StoresStartedTogether_WaitWithABudgetThatCountsEachOther()
{
var budgets = new ConcurrentBag();
var real = EventStoreServer.WaitReady;
- EventStoreServer.WaitReady = async (settings, budget) => { budgets.Add(budget); await real(settings, budget); };
+ await using var a = EventStoreServer.Create();
+ await using var b = EventStoreServer.Create();
+ // Only these two stores' budgets: other classes start stores through the same static seam in parallel.
+ EventStoreServer.WaitReady = async (server, settings, budget) =>
+ {
+ if (ReferenceEquals(server, a) || ReferenceEquals(server, b)) budgets.Add(budget);
+ await real(server, settings, budget);
+ };
try
{
- await using var a = EventStoreServer.Create();
- await using var b = EventStoreServer.Create();
await Task.WhenAll(a.StartInDocker(), b.StartInDocker());
}
finally
diff --git a/src/MicroPlumberd.Tests/Integration/EventStoreServerStartFailureTests.cs b/src/MicroPlumberd.Tests/Integration/EventStoreServerStartFailureTests.cs
new file mode 100644
index 0000000..3dc7355
--- /dev/null
+++ b/src/MicroPlumberd.Tests/Integration/EventStoreServerStartFailureTests.cs
@@ -0,0 +1,45 @@
+using FluentAssertions;
+using MicroPlumberd.Testing;
+using MicroPlumberd.Tests.Utils;
+
+namespace MicroPlumberd.Tests.Integration;
+
+///
+/// A failed start removes its container — and if that removal ALSO fails (two processes removing one container gave Docker
+/// 409 "removal already in progress", 2026-10-18), the START failure is still the one thrown; the cleanup failure rides
+/// along in Data. It used to replace it, and the first failure was lost (dev-clean's observation).
+///
+[TestCategory("Integration")]
+public class EventStoreServerStartFailureTests
+{
+ private sealed class NotReady() : Exception("the store did not become ready (planted)");
+
+ [Fact]
+ public async Task AStartThatFails_AndWhoseCleanupAlsoFails_ThrowsTheStartFailure()
+ {
+ await using var server = EventStoreServer.Create();
+ var ready = EventStoreServer.WaitReady;
+ var remove = EventStoreServer.RemoveContainer;
+ // Only THIS server's store: the suite runs other classes' stores in parallel through the same static seams.
+ EventStoreServer.WaitReady = (s, settings, budget) =>
+ ReferenceEquals(s, server) ? Task.FromException(new NotReady()) : ready(s, settings, budget);
+ EventStoreServer.RemoveContainer = async (s, docker, id) =>
+ {
+ await remove(s, docker, id); // the container really goes
+ if (ReferenceEquals(s, server)) throw new InvalidOperationException("removal of container is already in progress (planted)");
+ };
+ try
+ {
+ var thrown = await Record.ExceptionAsync(() => server.StartInDocker());
+
+ thrown.Should().BeOfType("the start failure is the first failure, and it is the one thrown");
+ thrown!.Data["MicroPlumberd.Testing.CleanupFailure"].Should().BeOfType()
+ .Which.Should().Contain("already in progress", "the cleanup failure rides along, not instead");
+ }
+ finally
+ {
+ EventStoreServer.WaitReady = ready;
+ EventStoreServer.RemoveContainer = remove;
+ }
+ }
+}
diff --git a/src/MicroPlumberd.Tests/Integration/StorePortRaceTests.cs b/src/MicroPlumberd.Tests/Integration/StorePortRaceTests.cs
new file mode 100644
index 0000000..2041b27
--- /dev/null
+++ b/src/MicroPlumberd.Tests/Integration/StorePortRaceTests.cs
@@ -0,0 +1,54 @@
+using System.Diagnostics;
+using System.Reflection;
+using FluentAssertions;
+using MicroPlumberd.Tests.Utils;
+using Xunit.Abstractions;
+
+namespace MicroPlumberd.Tests.Integration;
+
+///
+/// card microplumberd-testing-store-port-race: two test PROCESSES on one host (two engineers' suites) each start
+/// EventStoreServers. Every process's PortSearcher began at 2701 and a port only counted as taken once its container
+/// STARTED; containers are named eventstore-mem-<port> and found BY NAME — so the second process restarted the first's
+/// in-memory store (wiping it) or removed it on cleanup ("connection refused"; Docker 409 "removal already in progress" was
+/// observed 2026-10-18 13:52Z when two suites overlapped). Two StoreHolder processes start their stores at ONE barrier
+/// instant; each must keep its own store, on its own port, in its own container, for the whole hold.
+///
+[TestCategory("Integration")]
+public class StorePortRaceTests(ITestOutputHelper output)
+{
+ private static readonly string Holder = typeof(StorePortRaceTests).Assembly
+ .GetCustomAttributes().Single(a => a.Key == "StoreHolderDll").Value!;
+
+ [Fact]
+ public async Task TwoProcessesStartingStoresAtOnce_EachKeepTheirOwnStore()
+ {
+ File.Exists(Holder).Should().BeTrue($"the StoreHolder child is built beside the tests ({Holder})");
+ var barrier = DateTime.UtcNow.AddSeconds(5).Ticks;
+
+ var children = Enumerable.Range(0, 2).Select(_ => Run(barrier, holdSeconds: 20)).ToArray();
+ var results = await Task.WhenAll(children);
+ foreach (var r in results) output.WriteLine($"exit={r.Exit} {r.Line}");
+
+ results.Should().OnlyContain(r => r.Exit == 0 && r.Line.EndsWith("RESULT=SURVIVED"), "each process keeps its own store");
+ results.Select(r => Field(r.Line, "PORT")).Should().OnlyHaveUniqueItems("two processes never share a port");
+ results.Select(r => Field(r.Line, "CONTAINER")).Should().OnlyHaveUniqueItems("nor a container");
+ }
+
+ private static string Field(string line, string key) =>
+ line.Split(' ').Single(f => f.StartsWith(key + "=", StringComparison.Ordinal))[(key.Length + 1)..];
+
+ private static async Task<(int Exit, string Line)> Run(long barrierTicks, int holdSeconds)
+ {
+ var psi = new ProcessStartInfo("dotnet", [Holder, barrierTicks.ToString(), holdSeconds.ToString()])
+ { RedirectStandardOutput = true, RedirectStandardError = true };
+ using var process = Process.Start(psi)!;
+ var stdout = process.StandardOutput.ReadToEndAsync();
+ var stderr = process.StandardError.ReadToEndAsync();
+ using var bound = new CancellationTokenSource(TimeSpan.FromMinutes(3));
+ try { await process.WaitForExitAsync(bound.Token); }
+ catch (OperationCanceledException) { process.Kill(entireProcessTree: true); return (-1, $"TIMEOUT stderr={await stderr}"); }
+ var line = (await stdout).Split('\n', StringSplitOptions.RemoveEmptyEntries).LastOrDefault()?.Trim() ?? $"no output; stderr={await stderr}";
+ return (process.ExitCode, line);
+ }
+}
diff --git a/src/MicroPlumberd.Tests/MicroPlumberd.Tests.csproj b/src/MicroPlumberd.Tests/MicroPlumberd.Tests.csproj
index 9ba282c..2845448 100644
--- a/src/MicroPlumberd.Tests/MicroPlumberd.Tests.csproj
+++ b/src/MicroPlumberd.Tests/MicroPlumberd.Tests.csproj
@@ -25,6 +25,9 @@
+
+
+
all
@@ -55,6 +58,8 @@
+
+
diff --git a/src/MicroPlumberd.sln b/src/MicroPlumberd.sln
index fb184da..c2d893e 100644
--- a/src/MicroPlumberd.sln
+++ b/src/MicroPlumberd.sln
@@ -85,6 +85,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MicroPlumberd.Rewrite.Tests
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MicroPlumberd.Rewrite", "MicroPlumberd.Rewrite\MicroPlumberd.Rewrite.csproj", "{11B13FE4-AF80-4801-9A4E-58752001A8D2}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MicroPlumberd.Testing.StoreHolder", "MicroPlumberd.Testing.StoreHolder\MicroPlumberd.Testing.StoreHolder.csproj", "{B31528B5-CD92-4D98-A2C5-AA8371CEAF45}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -527,6 +529,18 @@ Global
{11B13FE4-AF80-4801-9A4E-58752001A8D2}.Release|x64.Build.0 = Release|Any CPU
{11B13FE4-AF80-4801-9A4E-58752001A8D2}.Release|x86.ActiveCfg = Release|Any CPU
{11B13FE4-AF80-4801-9A4E-58752001A8D2}.Release|x86.Build.0 = Release|Any CPU
+ {B31528B5-CD92-4D98-A2C5-AA8371CEAF45}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B31528B5-CD92-4D98-A2C5-AA8371CEAF45}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B31528B5-CD92-4D98-A2C5-AA8371CEAF45}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {B31528B5-CD92-4D98-A2C5-AA8371CEAF45}.Debug|x64.Build.0 = Debug|Any CPU
+ {B31528B5-CD92-4D98-A2C5-AA8371CEAF45}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {B31528B5-CD92-4D98-A2C5-AA8371CEAF45}.Debug|x86.Build.0 = Debug|Any CPU
+ {B31528B5-CD92-4D98-A2C5-AA8371CEAF45}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {B31528B5-CD92-4D98-A2C5-AA8371CEAF45}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B31528B5-CD92-4D98-A2C5-AA8371CEAF45}.Release|x64.ActiveCfg = Release|Any CPU
+ {B31528B5-CD92-4D98-A2C5-AA8371CEAF45}.Release|x64.Build.0 = Release|Any CPU
+ {B31528B5-CD92-4D98-A2C5-AA8371CEAF45}.Release|x86.ActiveCfg = Release|Any CPU
+ {B31528B5-CD92-4D98-A2C5-AA8371CEAF45}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE